From 478c44c17fbb05090a8b682ff5aed26110db4709 Mon Sep 17 00:00:00 2001 From: rish07 Date: Tue, 16 Feb 2021 09:47:17 +0530 Subject: [PATCH 001/306] Fix image size issue --- app/lib/main.dart | 1 + app/lib/pages/home_page.dart | 26 +++++++------- lib/widgets/FakeUi.dart | 4 +++ lib/widgets/RealUi.dart | 9 +++++ lib/widgets/editor.dart | 56 ++++++++++++++++++++---------- lib/widgets/responsive_widget.dart | 36 +++++++++++++++++++ pubspec.yaml | 1 + 7 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 lib/widgets/FakeUi.dart create mode 100644 lib/widgets/RealUi.dart create mode 100644 lib/widgets/responsive_widget.dart diff --git a/app/lib/main.dart b/app/lib/main.dart index 77622559..f3ec0666 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'pages/home_page.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); runApp(MyApp()); } diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 3663543f..422d0954 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -89,11 +89,11 @@ class _HomePageState extends State { } Widget _buildWelcomeEditor(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Container( + return SafeArea( + child: Stack( + children: [ + Container( + height: MediaQuery.of(context).size.height * 0.88, color: Colors.white, padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: QuillEditor( @@ -120,13 +120,15 @@ class _HomePageState extends State { sizeSmall: TextStyle(fontSize: 9.0)), ), ), - ), - Container( - child: QuillToolbar.basic( - controller: _controller, - uploadFileCallback: _fakeUploadImageCallBack), - ) - ], + Container( + padding: + EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.9), + child: QuillToolbar.basic( + controller: _controller, + uploadFileCallback: _fakeUploadImageCallBack), + ) + ], + ), ); } diff --git a/lib/widgets/FakeUi.dart b/lib/widgets/FakeUi.dart new file mode 100644 index 00000000..bc9799af --- /dev/null +++ b/lib/widgets/FakeUi.dart @@ -0,0 +1,4 @@ +// ignore: camel_case_types +class platformViewRegistry { + static registerViewFactory(String viewId, dynamic cb) {} +} diff --git a/lib/widgets/RealUi.dart b/lib/widgets/RealUi.dart new file mode 100644 index 00000000..c2b8ea23 --- /dev/null +++ b/lib/widgets/RealUi.dart @@ -0,0 +1,9 @@ +import 'dart:ui' as ui; + +// ignore: camel_case_types +class platformViewRegistry { + static registerViewFactory(String viewId, dynamic cb) { + // ignore:undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory(viewId, cb); + } +} diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 6a388ed8..57e51188 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1,7 +1,6 @@ -import 'dart:html' as html; import 'dart:io'; import 'dart:math' as math; -import 'dart:ui' as ui; +import 'dart:math' if (dart.library.html) 'dart:html' as html; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -19,9 +18,11 @@ 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/responsive_widget.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'FakeUi.dart' if (dart.library.html) 'RealUi.dart' as ui; import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; @@ -79,21 +80,6 @@ abstract class RenderAbstractEditor { Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { switch (node.value.type) { case 'image': - if (kIsWeb) { - String imageUrl = node.value.data; - - ui.platformViewRegistry.registerViewFactory( - imageUrl, - (int viewId) => html.ImageElement()..src = imageUrl, - ); - return Container( - constraints: BoxConstraints(maxWidth: 300), - height: MediaQuery.of(context).size.height, - child: HtmlElementView( - viewType: imageUrl, - ), - ); - } String imageUrl = node.value.data; return imageUrl.startsWith('http') ? Image.network(imageUrl) @@ -106,6 +92,39 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { } } +Widget _defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) { + switch (node.value.type) { + case 'image': + String imageUrl = node.value.data; + Size size = MediaQuery.of(context).size; + ui.platformViewRegistry.registerViewFactory( + imageUrl, + (int viewId) => html.ImageElement()..src = imageUrl, + ); + return Padding( + padding: EdgeInsets.only( + right: ResponsiveWidget.isMediumScreen(context) + ? size.width * 0.5 + : (ResponsiveWidget.isLargeScreen(context)) + ? size.width * 0.75 + : size.width * 0.2, + ), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.45, + child: HtmlElementView( + viewType: imageUrl, + ), + ), + ); + + default: + throw UnimplementedError( + 'Embeddable type "${node.value.type}" is not supported by default embed ' + 'builder of QuillEditor. You must pass your own builder function to ' + 'embedBuilder property of QuillEditor or QuillField widgets.'); + } +} + class QuillEditor extends StatefulWidget { final QuillController controller; final FocusNode focusNode; @@ -144,7 +163,8 @@ class QuillEditor extends StatefulWidget { this.keyboardAppearance = Brightness.light, this.scrollPhysics, this.onLaunchUrl, - this.embedBuilder = _defaultEmbedBuilder}) + this.embedBuilder = + kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder}) : assert(controller != null), assert(scrollController != null), assert(scrollable != null), diff --git a/lib/widgets/responsive_widget.dart b/lib/widgets/responsive_widget.dart new file mode 100644 index 00000000..dae14b73 --- /dev/null +++ b/lib/widgets/responsive_widget.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class ResponsiveWidget extends StatelessWidget { + final Widget largeScreen; + final Widget mediumScreen; + final Widget smallScreen; + + const ResponsiveWidget({Key key, @required this.largeScreen, this.mediumScreen, this.smallScreen}) : super(key: key); + + static bool isSmallScreen(BuildContext context) { + return MediaQuery.of(context).size.width < 800; + } + + static bool isLargeScreen(BuildContext context) { + return MediaQuery.of(context).size.width > 1200; + } + + static bool isMediumScreen(BuildContext context) { + return MediaQuery.of(context).size.width >= 800 && MediaQuery.of(context).size.width <= 1200; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 1200) { + return largeScreen; + } else if (constraints.maxWidth <= 1200 && constraints.maxWidth >= 800) { + return mediumScreen ?? largeScreen; + } else { + return smallScreen ?? largeScreen; + } + }, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index fb373ad1..a90908d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: flutter_colorpicker: ^0.3.4 image_picker: ^0.6.7+17 photo_view: ^0.10.3 + universal_html: ^1.2.4 dev_dependencies: flutter_test: From 3308e5d706a6e4f874b7a5a37225d77a89b81f2c Mon Sep 17 00:00:00 2001 From: rish07 Date: Tue, 16 Feb 2021 16:26:27 +0530 Subject: [PATCH 002/306] Fixed phone build break issue --- app/pubspec.lock | 49 +++++++++++++++ lib/widgets/editor.dart | 8 +-- lib/widgets/responsive_widget.dart | 13 +++- pubspec.lock | 96 +++++++++++++++++++++++------- pubspec.yaml | 2 +- 5 files changed, 140 insertions(+), 28 deletions(-) diff --git a/app/pubspec.lock b/app/pubspec.lock index 25012d30..f4e16a57 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -43,6 +43,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.2" cupertino_icons: dependency: "direct main" description: @@ -93,6 +114,13 @@ packages: description: flutter source: sdk version: "0.0.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+4" http: dependency: transitive description: @@ -252,6 +280,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + universal_html: + dependency: transitive + description: + name: universal_html + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" url_launcher: dependency: transitive description: @@ -301,6 +343,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + zone_local: + dependency: transitive + description: + name: zone_local + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" sdks: dart: ">=2.12.0-0.0 <3.0.0" flutter: ">=1.22.0" diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 57e51188..018ef0b2 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1,6 +1,5 @@ -import 'dart:io'; +import 'dart:io' as io; import 'dart:math' as math; -import 'dart:math' if (dart.library.html) 'dart:html' as html; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -20,6 +19,7 @@ import 'package:flutter_quill/widgets/image.dart'; import 'package:flutter_quill/widgets/raw_editor.dart'; import 'package:flutter_quill/widgets/responsive_widget.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; +import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; import 'FakeUi.dart' if (dart.library.html) 'RealUi.dart' as ui; @@ -83,7 +83,7 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { String imageUrl = node.value.data; return imageUrl.startsWith('http') ? Image.network(imageUrl) - : Image.file(File(imageUrl)); + : Image.file(io.File(imageUrl)); default: throw UnimplementedError( 'Embeddable type "${node.value.type}" is not supported by default embed ' @@ -403,7 +403,7 @@ class _QuillEditorSelectionGestureDetectorBuilder builder: (context) => ImageTapWrapper( imageProvider: imageUrl.startsWith('http') ? NetworkImage(imageUrl) - : FileImage(File(blockEmbed.data))), + : FileImage(io.File(blockEmbed.data))), ), ); } diff --git a/lib/widgets/responsive_widget.dart b/lib/widgets/responsive_widget.dart index dae14b73..a047deab 100644 --- a/lib/widgets/responsive_widget.dart +++ b/lib/widgets/responsive_widget.dart @@ -5,7 +5,12 @@ class ResponsiveWidget extends StatelessWidget { final Widget mediumScreen; final Widget smallScreen; - const ResponsiveWidget({Key key, @required this.largeScreen, this.mediumScreen, this.smallScreen}) : super(key: key); + const ResponsiveWidget( + {Key key, + @required this.largeScreen, + this.mediumScreen, + this.smallScreen}) + : super(key: key); static bool isSmallScreen(BuildContext context) { return MediaQuery.of(context).size.width < 800; @@ -16,7 +21,8 @@ class ResponsiveWidget extends StatelessWidget { } static bool isMediumScreen(BuildContext context) { - return MediaQuery.of(context).size.width >= 800 && MediaQuery.of(context).size.width <= 1200; + return MediaQuery.of(context).size.width >= 800 && + MediaQuery.of(context).size.width <= 1200; } @override @@ -25,7 +31,8 @@ class ResponsiveWidget extends StatelessWidget { builder: (context, constraints) { if (constraints.maxWidth > 1200) { return largeScreen; - } else if (constraints.maxWidth <= 1200 && constraints.maxWidth >= 800) { + } else if (constraints.maxWidth <= 1200 && + constraints.maxWidth >= 800) { return mediumScreen ?? largeScreen; } else { return smallScreen ?? largeScreen; diff --git a/pubspec.lock b/pubspec.lock index 09ae8cdc..4afa0337 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,49 +7,70 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.5.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" collection: dependency: "direct main" description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" + version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.2" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" flutter: dependency: "direct main" description: flutter @@ -79,6 +100,13 @@ packages: description: flutter source: sdk version: "0.0.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+4" http: dependency: transitive description: @@ -107,27 +135,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" + version: "1.8.0" pedantic: dependency: transitive description: @@ -181,42 +216,42 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.0" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" + version: "0.2.19" tuple: dependency: "direct main" description: @@ -230,7 +265,21 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" + universal_html: + dependency: "direct main" + description: + name: universal_html + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" url_launcher: dependency: "direct main" description: @@ -279,7 +328,14 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0" + zone_local: + dependency: transitive + description: + name: zone_local + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" sdks: - dart: ">=2.10.0-110 <2.11.0" - flutter: ">=1.22.0 <2.0.0" + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=1.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index a90908d0..d9cff81e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: flutter_colorpicker: ^0.3.4 image_picker: ^0.6.7+17 photo_view: ^0.10.3 - universal_html: ^1.2.4 + universal_html: ^1.2.1 dev_dependencies: flutter_test: From caf6db8416248e650648e77f656aac9453a23cdb Mon Sep 17 00:00:00 2001 From: singerdmx Date: Thu, 18 Feb 2021 21:50:03 -0800 Subject: [PATCH 003/306] Reuse textSpan --- lib/widgets/text_line.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 53bb07f8..6d7efa6b 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -48,7 +48,7 @@ class TextLine extends StatelessWidget { StrutStyle.fromTextStyle(textSpan.style, forceStrutHeight: true); final textAlign = _getTextAlign(); RichText child = RichText( - text: _buildTextSpan(context), + text: textSpan, textAlign: textAlign, textDirection: textDirection, strutStyle: strutStyle, From 0bae977188adc76decb6030a2defed72470c22b8 Mon Sep 17 00:00:00 2001 From: li3317 Date: Fri, 19 Feb 2021 17:05:52 -0500 Subject: [PATCH 004/306] enable autocorrect --- lib/widgets/raw_editor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 3ff24e55..6ae948ff 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -374,7 +374,7 @@ class RawEditorState extends EditorState inputType: TextInputType.multiline, readOnly: widget.readOnly, obscureText: false, - autocorrect: false, + autocorrect: true, inputAction: TextInputAction.newline, keyboardAppearance: widget.keyboardAppearance, textCapitalization: widget.textCapitalization, From ff3563ea6e7b021b6352573af05519b77ddb7aed Mon Sep 17 00:00:00 2001 From: singerdmx Date: Fri, 19 Feb 2021 22:04:42 -0800 Subject: [PATCH 005/306] Upgrade image_picker --- CHANGELOG.md | 5 ++++- app/pubspec.lock | 2 +- pubspec.lock | 2 +- pubspec.yaml | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a82501..5bfda437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,4 +78,7 @@ * Support display local image besides network image. ## [0.2.8] -* Support display local image besides network image in stable branch. \ No newline at end of file +* Support display local image besides network image in stable branch. + +## [0.2.9] +* Update TextInputConfiguration autocorrect to true. \ No newline at end of file diff --git a/app/pubspec.lock b/app/pubspec.lock index f4e16a57..66f4b1e0 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -103,7 +103,7 @@ packages: path: ".." relative: true source: path - version: "0.2.8" + version: "0.2.9" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.lock b/pubspec.lock index 4afa0337..25975c15 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -127,7 +127,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.7+17" + version: "0.6.7+22" image_picker_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d9cff81e..09d03a06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.2.8 +version: 0.2.9 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git @@ -18,7 +18,7 @@ dependencies: tuple: ^1.0.3 url_launcher: ^5.7.10 flutter_colorpicker: ^0.3.4 - image_picker: ^0.6.7+17 + image_picker: ^0.6.7+22 photo_view: ^0.10.3 universal_html: ^1.2.1 From 59410649db36eef01c17a9f2a757251d9bb1a94d Mon Sep 17 00:00:00 2001 From: Jon Mountjoy Date: Sat, 20 Feb 2021 09:26:18 +0000 Subject: [PATCH 006/306] More docs (#33) --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b94eaaf..4b03b0ac 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,72 @@ # FlutterQuill -Rich text editor and a [Quill] component for [Flutter]. +FlutterQuill is a rich text editor and a [Quill] component for [Flutter]. -This library is a WYSIWYG editor built for the modern mobile platform only and web is under development. You can join [Slack Group] for discussion. +This library is a WYSIWYG editor built for the modern mobile platform, with web compatibility under development. You can join our [Slack Group] for discussion. https://pub.dev/packages/flutter_quill +## Usage + +See the `example` directory for a minimal example of how to use FlutterQuill. You typically just need to instantiate a controller: + +``` +QuillController _controller = QuillController.basic(); +``` + +and then embed the toolbar and the editor, within your app. For example: + +```dart +Column( + children: [ + QuillToolbar.basic( + controller: _controller, uploadFileCallback: _uploadImageCallBack), + Expanded( + child: Container( + child: QuillEditor.basic( + controller: _controller, + readOnly: false, // true for view only mode + ), + ), + ) + ], +) +``` + +## Input / Output + This library uses [Quill] as an internal data format. -Use `_controller.document.toDelta()` to extract it or use `_controller.document.toPlainText()` for plain text. + +* Use `_controller.document.toDelta()` to extract the deltas. +* Use `_controller.document.toPlainText()` to extract plain text. + +FlutterQuill provides some JSON serialisation support, so that you can save and open documents. To save a document as JSON, do something like the following: + +``` +var json = jsonEncode(_controller.document.toDelta().toJson()); +``` + +You can then write this to storage. + +To open a FlutterQuill editor with an existing JSON representation that you've previously stored, you can do something like this: + +``` +var myJSON = jsonDecode(incomingJSONText); +_controller = QuillController( + document: Document.fromJson(myJSON), + selection: TextSelection.collapsed(offset: 0)); +``` + +## Configuration + +The `QuillToolbar` class lets you customise which formating options are available. To prevent the image uploading widget from appearing, set `uploadFileCallback` to null. + +## Web Default branch `master` is on channel `master`. To use channel `stable`, switch to branch `stable`. Branch `master` on channel `master` supports web. To run the app on web do the following: + 1) Change flutter channel to master using `flutter channel master`, followed by `flutter upgrade`. 2) Enable web using `flutter config --enable-web` and restart the IDE. 3) Upon successful execution of step 1 and 2 you should see `Chrome` as one of the devices which you run `flutter devices`. From a3ee421d1aab53a676d19fb31efb78a3eb5aa98e Mon Sep 17 00:00:00 2001 From: singerdmx Date: Sat, 20 Feb 2021 01:49:47 -0800 Subject: [PATCH 007/306] Add isEmpty method for Document class --- lib/models/documents/document.dart | 14 ++++++++ lib/widgets/raw_editor.dart | 55 +++++++++++++++++------------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index bcfd5152..93585897 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -237,6 +237,20 @@ class Document { _root.remove(node); } } + + bool isEmpty() { + if (root.children.length != 1) { + return false; + } + + final Node node = root.children.first; + if (!node.isLast) { + return false; + } + + Delta delta = node.toDelta(); + return delta.length == 1 && delta.first.data == '\n'; + } } enum ChangeSource { diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 6ae948ff..891bff86 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -503,13 +503,15 @@ class RawEditorState extends EditorState _focusAttachment.reparent(); super.build(context); + Document _doc = widget.controller.document; + Widget child = CompositedTransformTarget( link: _toolbarLayerLink, child: Semantics( child: _Editor( key: _editorKey, - children: _buildChildren(context), - document: widget.controller.document, + children: _buildChildren(_doc, context), + document: _doc, selection: widget.controller.selection, hasFocus: _hasFocus, textDirection: _textDirection, @@ -562,30 +564,13 @@ class RawEditorState extends EditorState requestKeyboard(); } - _buildChildren(BuildContext context) { + _buildChildren(Document doc, BuildContext context) { final result = []; Map indentLevelCounts = {}; - for (Node node in widget.controller.document.root.children) { + for (Node node in doc.root.children) { if (node is Line) { - TextLine textLine = TextLine( - line: node, - textDirection: _textDirection, - embedBuilder: widget.embedBuilder, - styles: _styles, - ); - EditableTextLine editableTextLine = EditableTextLine( - node, - null, - textLine, - 0, - _getVerticalSpacingForLine(node, _styles), - _textDirection, - widget.controller.selection, - widget.selectionColor, - widget.enableInteractiveSelection, - _hasFocus, - MediaQuery.of(context).devicePixelRatio, - _cursorCont); + EditableTextLine editableTextLine = + _getEditableTextLineFromNode(node, context); result.add(editableTextLine); } else if (node is Block) { Map attrs = node.style.attributes; @@ -612,6 +597,30 @@ class RawEditorState extends EditorState return result; } + EditableTextLine _getEditableTextLineFromNode( + Line node, BuildContext context) { + TextLine textLine = TextLine( + line: node, + textDirection: _textDirection, + embedBuilder: widget.embedBuilder, + styles: _styles, + ); + EditableTextLine editableTextLine = EditableTextLine( + node, + null, + textLine, + 0, + _getVerticalSpacingForLine(node, _styles), + _textDirection, + widget.controller.selection, + widget.selectionColor, + widget.enableInteractiveSelection, + _hasFocus, + MediaQuery.of(context).devicePixelRatio, + _cursorCont); + return editableTextLine; + } + Tuple2 _getVerticalSpacingForLine( Line line, DefaultStyles defaultStyles) { Map attrs = line.style.attributes; From f4f81d5b1df074f47e21fa8ffada6ccff8504ef2 Mon Sep 17 00:00:00 2001 From: pengw00 Date: Sat, 20 Feb 2021 16:47:06 -0600 Subject: [PATCH 008/306] Bump Version to 0.2.11 --- CHANGELOG.md | 8 +++++++- pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bfda437..b3a4b172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,4 +81,10 @@ * Support display local image besides network image in stable branch. ## [0.2.9] -* Update TextInputConfiguration autocorrect to true. \ No newline at end of file +* Update TextInputConfiguration autocorrect to true. + +## [0.2.10] +* Update TextInputConfiguration autocorrect to true in stable branch. + +## [0.2.11] +* Fix static analysis error. \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 09d03a06..b601cff1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.2.9 +version: 0.2.11 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From 7d4bfa67a8e33d9ac73386dbb0fe6e966d797468 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sat, 20 Feb 2021 22:26:37 -0500 Subject: [PATCH 009/306] support placeholder for empty content --- app/lib/pages/home_page.dart | 1 + lib/models/documents/attribute.dart | 11 ++++++++++- lib/models/documents/document.dart | 2 +- lib/widgets/default_styles.dart | 12 ++++++++++++ lib/widgets/editor.dart | 6 ++++++ lib/widgets/raw_editor.dart | 10 ++++++++++ lib/widgets/text_line.dart | 5 +++++ 7 files changed, 45 insertions(+), 2 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 3663543f..dafa4859 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -103,6 +103,7 @@ class _HomePageState extends State { focusNode: _focusNode, autoFocus: false, readOnly: false, + placeholder: 'Add content', enableInteractiveSelection: true, expands: false, padding: EdgeInsets.zero, diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index d7865e7d..7b363bed 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -24,6 +24,7 @@ class Attribute { Attribute.link.key: Attribute.link, Attribute.color.key: Attribute.color, Attribute.background.key: Attribute.background, + Attribute.placeholder.key: Attribute.placeholder, Attribute.header.key: Attribute.header, Attribute.indent.key: Attribute.indent, Attribute.align.key: Attribute.align, @@ -53,6 +54,8 @@ class Attribute { static final BackgroundAttribute background = BackgroundAttribute(null); + static final PlaceholderAttribute placeholder = PlaceholderAttribute(); + static final HeaderAttribute header = HeaderAttribute(); static final IndentAttribute indent = IndentAttribute(); @@ -78,7 +81,8 @@ class Attribute { Attribute.strikeThrough.key, Attribute.link.key, Attribute.color.key, - Attribute.background.key + Attribute.background.key, + Attribute.placeholder.key, }; static final Set blockKeys = { @@ -222,6 +226,11 @@ class BackgroundAttribute extends Attribute { : super('background', AttributeScope.INLINE, val); } +/// This is custom attribute for hint +class PlaceholderAttribute extends Attribute { + PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true); +} + class HeaderAttribute extends Attribute { HeaderAttribute({int level}) : super('header', AttributeScope.BLOCK, level); } diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 93585897..f41ee0bd 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -249,7 +249,7 @@ class Document { } Delta delta = node.toDelta(); - return delta.length == 1 && delta.first.data == '\n'; + return delta.length == 1 && delta.first.data == '\n' && delta.first.key == 'insert'; } } diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index cef0011b..4cad7bbb 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -54,6 +54,7 @@ class DefaultStyles { final TextStyle sizeLarge; // 'large' final TextStyle sizeHuge; // 'huge' final TextStyle link; + final DefaultTextBlockStyle placeHolder; final DefaultTextBlockStyle lists; final DefaultTextBlockStyle quote; final DefaultTextBlockStyle code; @@ -70,6 +71,7 @@ class DefaultStyles { this.underline, this.strikeThrough, this.link, + this.placeHolder, this.lists, this.quote, this.code, @@ -144,6 +146,15 @@ class DefaultStyles { color: themeData.accentColor, decoration: TextDecoration.underline, ), + placeHolder: DefaultTextBlockStyle( + defaultTextStyle.style.copyWith( + fontSize: 20.0, + height: 1.5, + color: Colors.grey.withOpacity(0.6), + ), + Tuple2(0.0, 0.0), + Tuple2(0.0, 0.0), + null), lists: DefaultTextBlockStyle( baseStyle, baseSpacing, Tuple2(0.0, 6.0), null), quote: DefaultTextBlockStyle( @@ -188,6 +199,7 @@ class DefaultStyles { underline: other.underline ?? this.underline, strikeThrough: other.strikeThrough ?? this.strikeThrough, link: other.link ?? this.link, + placeHolder: other.placeHolder ?? this.placeHolder, lists: other.lists ?? this.lists, quote: other.quote ?? this.quote, code: other.code ?? this.code, diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 018ef0b2..29472aae 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -134,6 +134,7 @@ class QuillEditor extends StatefulWidget { final bool autoFocus; final bool showCursor; final bool readOnly; + final String placeholder; final bool enableInteractiveSelection; final double minHeight; final double maxHeight; @@ -154,6 +155,7 @@ class QuillEditor extends StatefulWidget { @required this.autoFocus, this.showCursor, @required this.readOnly, + this.placeholder, this.enableInteractiveSelection, this.minHeight, this.maxHeight, @@ -256,6 +258,7 @@ class _QuillEditorState extends State widget.scrollable, widget.padding, widget.readOnly, + widget.placeholder, widget.onLaunchUrl, ToolbarOptions( copy: true, @@ -359,6 +362,9 @@ class _QuillEditorSelectionGestureDetectorBuilder } bool _onTapping(TapUpDetails details) { + if (_state.widget.controller.document.isEmpty()) { + return false; + } TextPosition pos = getRenderEditor().getPositionForOffset(details.globalPosition); containerNode.ChildQuery result = diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 891bff86..fbbb8a83 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -32,6 +34,7 @@ class RawEditor extends StatefulWidget { final bool scrollable; final EdgeInsetsGeometry padding; final bool readOnly; + final String placeholder; final ValueChanged onLaunchUrl; final ToolbarOptions toolbarOptions; final bool showSelectionHandles; @@ -58,6 +61,7 @@ class RawEditor extends StatefulWidget { this.scrollable, this.padding, this.readOnly, + this.placeholder, this.onLaunchUrl, this.toolbarOptions, this.showSelectionHandles, @@ -504,6 +508,12 @@ class RawEditorState extends EditorState super.build(context); Document _doc = widget.controller.document; + if (_doc.isEmpty() && + !widget.focusNode.hasFocus && + widget.placeholder != null) { + _doc = Document.fromJson(jsonDecode( + '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); + } Widget child = CompositedTransformTarget( link: _toolbarLayerLink, diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 6d7efa6b..986cf762 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -88,6 +88,11 @@ class TextLine extends StatelessWidget { TextStyle textStyle = 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 = { Attribute.h1: defaultStyles.h1.style, From 81fbca0dc83f272e30d17124b75e532aec43ca76 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sat, 20 Feb 2021 22:35:33 -0500 Subject: [PATCH 010/306] bump version to 0.2.12 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a4b172..0a6c79dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,4 +87,7 @@ * Update TextInputConfiguration autocorrect to true in stable branch. ## [0.2.11] -* Fix static analysis error. \ No newline at end of file +* Fix static analysis error. + +## [0.2.12] +* Support placeholder. \ No newline at end of file From 2d81b702261fc0af01261e0671d8e7dc11c0233c Mon Sep 17 00:00:00 2001 From: li3317 Date: Sat, 20 Feb 2021 22:39:57 -0500 Subject: [PATCH 011/306] update pubspec yaml version and format code --- lib/models/documents/document.dart | 4 +++- pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index f41ee0bd..1573f9af 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -249,7 +249,9 @@ class Document { } Delta delta = node.toDelta(); - return delta.length == 1 && delta.first.data == '\n' && delta.first.key == 'insert'; + return delta.length == 1 && + delta.first.data == '\n' && + delta.first.key == 'insert'; } } diff --git a/pubspec.yaml b/pubspec.yaml index b601cff1..2c925516 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.2.11 +version: 0.2.12 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From 4f044e2b0925634199442f98930ca8f3760ec306 Mon Sep 17 00:00:00 2001 From: singerdmx Date: Sun, 21 Feb 2021 06:33:50 -0800 Subject: [PATCH 012/306] Rename uploadFileCallback to onImagePickCallback --- README.md | 4 ++-- app/lib/pages/home_page.dart | 3 ++- app/pubspec.lock | 4 ++-- example/main.dart | 2 +- lib/widgets/text_line.dart | 2 +- lib/widgets/toolbar.dart | 20 ++++++++++---------- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4b03b0ac..b8f8b6be 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ and then embed the toolbar and the editor, within your app. For example: Column( children: [ QuillToolbar.basic( - controller: _controller, uploadFileCallback: _uploadImageCallBack), + controller: _controller, onImageTapCallBack: _uploadImageCallBack), Expanded( child: Container( child: QuillEditor.basic( @@ -65,7 +65,7 @@ _controller = QuillController( ## Configuration -The `QuillToolbar` class lets you customise which formating options are available. To prevent the image uploading widget from appearing, set `uploadFileCallback` to null. +The `QuillToolbar` class lets you customise which formatting options are available. To prevent the image uploading widget from appearing, set `onImageTapCallBack` to null. ## Web diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index dafa4859..fe8c101b 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -125,12 +125,13 @@ class _HomePageState extends State { Container( child: QuillToolbar.basic( controller: _controller, - uploadFileCallback: _fakeUploadImageCallBack), + onImageTapCallBack: _fakeUploadImageCallBack), ) ], ); } + /// Upload the image file to AWS s3 when image is picked Future _fakeUploadImageCallBack(File file) async { print(file); var completer = new Completer(); diff --git a/app/pubspec.lock b/app/pubspec.lock index 66f4b1e0..ec94357a 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -103,7 +103,7 @@ packages: path: ".." relative: true source: path - version: "0.2.9" + version: "0.2.12" flutter_test: dependency: "direct dev" description: flutter @@ -141,7 +141,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.7+17" + version: "0.6.7+22" image_picker_platform_interface: dependency: transitive description: diff --git a/example/main.dart b/example/main.dart index b79b35e5..c10e3a7b 100644 --- a/example/main.dart +++ b/example/main.dart @@ -20,7 +20,7 @@ class _HomePageState extends State { body: Column( children: [ QuillToolbar.basic( - controller: _controller, uploadFileCallback: _uploadImageCallBack), + controller: _controller, onImageTapCallBack: _uploadImageCallBack), Expanded( child: Container( child: QuillEditor.basic( diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 986cf762..8608e69b 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -48,7 +48,7 @@ class TextLine extends StatelessWidget { StrutStyle.fromTextStyle(textSpan.style, forceStrutHeight: true); final textAlign = _getTextAlign(); RichText child = RichText( - text: textSpan, + text: TextSpan(children: [textSpan]), textAlign: textAlign, textDirection: textDirection, strutStyle: strutStyle, diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 69de725a..a05fde04 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -14,7 +14,7 @@ import 'controller.dart'; double iconSize = 18.0; double kToolbarHeight = iconSize * 2; -typedef UploadFileCallback = Future Function(File file); +typedef OnImagePickCallback = Future Function(File file); class InsertEmbedButton extends StatelessWidget { final QuillController controller; @@ -490,7 +490,7 @@ class ImageButton extends StatefulWidget { final QuillController controller; - final UploadFileCallback uploadFileCallback; + final OnImagePickCallback onImagePickCallback; final ImageSource imageSource; @@ -499,7 +499,7 @@ class ImageButton extends StatefulWidget { @required this.icon, @required this.controller, @required this.imageSource, - this.uploadFileCallback}) + this.onImagePickCallback}) : assert(icon != null), assert(controller != null), super(key: key); @@ -515,10 +515,10 @@ class _ImageButtonState extends State { final PickedFile pickedFile = await _picker.getImage(source: source); final File file = File(pickedFile.path); - if (file == null || widget.uploadFileCallback == null) return null; + if (file == null || widget.onImagePickCallback == null) return null; // We simply return the absolute path to selected file. try { - String url = await widget.uploadFileCallback(file); + String url = await widget.onImagePickCallback(file); print('Image uploaded and its url is $url'); return url; } catch (error) { @@ -891,7 +891,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { bool showLink = true, bool showHistory = true, bool showHorizontalRule = false, - UploadFileCallback uploadFileCallback}) { + OnImagePickCallback onImageTapCallBack}) { iconSize = toolbarIconSize; return QuillToolbar(key: key, children: [ Visibility( @@ -974,22 +974,22 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { ), SizedBox(width: 0.6), Visibility( - visible: uploadFileCallback != null, + visible: onImageTapCallBack != null, child: ImageButton( icon: Icons.image, controller: controller, imageSource: ImageSource.gallery, - uploadFileCallback: uploadFileCallback, + onImagePickCallback: onImageTapCallBack, ), ), SizedBox(width: 0.6), Visibility( - visible: uploadFileCallback != null, + visible: onImageTapCallBack != null, child: ImageButton( icon: Icons.photo_camera, controller: controller, imageSource: ImageSource.camera, - uploadFileCallback: uploadFileCallback, + onImagePickCallback: onImageTapCallBack, ), ), Visibility( From 9bcee88705eef527a81bbffd61a6d328869e9389 Mon Sep 17 00:00:00 2001 From: singerdmx Date: Sun, 21 Feb 2021 06:39:47 -0800 Subject: [PATCH 013/306] Rename onImageTapCallBack to onImagePickCallback --- README.md | 4 ++-- app/lib/pages/home_page.dart | 2 +- example/main.dart | 2 +- lib/widgets/toolbar.dart | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b8f8b6be..0738a0f6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ and then embed the toolbar and the editor, within your app. For example: Column( children: [ QuillToolbar.basic( - controller: _controller, onImageTapCallBack: _uploadImageCallBack), + controller: _controller, onImagePickCallback: _uploadImageCallBack), Expanded( child: Container( child: QuillEditor.basic( @@ -65,7 +65,7 @@ _controller = QuillController( ## Configuration -The `QuillToolbar` class lets you customise which formatting options are available. To prevent the image uploading widget from appearing, set `onImageTapCallBack` to null. +The `QuillToolbar` class lets you customise which formatting options are available. To prevent the image uploading widget from appearing, set `onImagePickCallback` to null. ## Web diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index fe8c101b..9f8c11c2 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -125,7 +125,7 @@ class _HomePageState extends State { Container( child: QuillToolbar.basic( controller: _controller, - onImageTapCallBack: _fakeUploadImageCallBack), + onImagePickCallback: _fakeUploadImageCallBack), ) ], ); diff --git a/example/main.dart b/example/main.dart index c10e3a7b..e45f4298 100644 --- a/example/main.dart +++ b/example/main.dart @@ -20,7 +20,7 @@ class _HomePageState extends State { body: Column( children: [ QuillToolbar.basic( - controller: _controller, onImageTapCallBack: _uploadImageCallBack), + controller: _controller, onImagePickCallback: _uploadImageCallBack), Expanded( child: Container( child: QuillEditor.basic( diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index a05fde04..c7b4198c 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -891,7 +891,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { bool showLink = true, bool showHistory = true, bool showHorizontalRule = false, - OnImagePickCallback onImageTapCallBack}) { + OnImagePickCallback onImagePickCallback}) { iconSize = toolbarIconSize; return QuillToolbar(key: key, children: [ Visibility( @@ -974,22 +974,22 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { ), SizedBox(width: 0.6), Visibility( - visible: onImageTapCallBack != null, + visible: onImagePickCallback != null, child: ImageButton( icon: Icons.image, controller: controller, imageSource: ImageSource.gallery, - onImagePickCallback: onImageTapCallBack, + onImagePickCallback: onImagePickCallback, ), ), SizedBox(width: 0.6), Visibility( - visible: onImageTapCallBack != null, + visible: onImagePickCallback != null, child: ImageButton( icon: Icons.photo_camera, controller: controller, imageSource: ImageSource.camera, - onImagePickCallback: onImageTapCallBack, + onImagePickCallback: onImagePickCallback, ), ), Visibility( From 5dcbf78a98179583365d16c687907851a506c6f7 Mon Sep 17 00:00:00 2001 From: ArjanAswal Date: Sun, 21 Feb 2021 20:38:16 +0530 Subject: [PATCH 014/306] Change the image pick callback function --- app/lib/pages/home_page.dart | 20 ++++++++++++-------- app/pubspec.yaml | 1 + 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 9f8c11c2..4313a7e8 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:path/path.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,6 +11,7 @@ import 'package:flutter_quill/widgets/controller.dart'; import 'package:flutter_quill/widgets/default_styles.dart'; import 'package:flutter_quill/widgets/editor.dart'; import 'package:flutter_quill/widgets/toolbar.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:tuple/tuple.dart'; import 'read_only_page.dart'; @@ -125,19 +127,21 @@ class _HomePageState extends State { Container( child: QuillToolbar.basic( controller: _controller, - onImagePickCallback: _fakeUploadImageCallBack), + onImagePickCallback: _onImagePickCallback), ) ], ); } - /// Upload the image file to AWS s3 when image is picked - Future _fakeUploadImageCallBack(File file) async { - print(file); - var completer = new Completer(); - completer.complete( - 'https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png'); - return completer.future; + // Renders the image picked by imagePicker from local file storage + // 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 { + if (file == null) return null; + // Copies the picked file from temporary cache to applications directory + Directory appDocDir = await getApplicationDocumentsDirectory(); + File copiedFile = + await file.copy('${appDocDir.path}/${basename(file.path)}'); + return copiedFile.path.toString(); } Widget _buildMenuBar(BuildContext context) { diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 3a6814e0..4d471487 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.0 + path_provider: ^1.6.27 flutter_quill: path: ../ From b810dc00030fc4318a110bf41abbc6b9e2e20766 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 21 Feb 2021 07:21:21 -0800 Subject: [PATCH 015/306] Update home page --- app/lib/pages/home_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 4313a7e8..de343836 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -160,7 +160,7 @@ class _HomePageState extends State { void _readOnly() { Navigator.push( - context, + super.context, MaterialPageRoute( builder: (BuildContext context) => ReadOnlyPage(), ), From c561c852b021bb53b3c993085786551e2e6ce52a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 21 Feb 2021 07:32:40 -0800 Subject: [PATCH 016/306] Update README --- README.md | 8 +++++--- example/main.dart | 11 +---------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0738a0f6..76a347a9 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,7 @@ and then embed the toolbar and the editor, within your app. For example: ```dart Column( children: [ - QuillToolbar.basic( - controller: _controller, onImagePickCallback: _uploadImageCallBack), + QuillToolbar.basic(controller: _controller), Expanded( child: Container( child: QuillEditor.basic( @@ -38,6 +37,7 @@ Column( ], ) ``` +Check out [Sample Page] for advanced usage. ## Input / Output @@ -65,7 +65,8 @@ _controller = QuillController( ## Configuration -The `QuillToolbar` class lets you customise which formatting options are available. To prevent the image uploading widget from appearing, set `onImagePickCallback` to null. +The `QuillToolbar` class lets you customise which formatting options are available. +[Sample Page] provided sample code for advanced usage and configuration. ## Web @@ -93,3 +94,4 @@ One client and affiliated collaborator of **[FlutterQuill]** is Bullet Journal A [FlutterQuill]: https://pub.dev/packages/flutter_quill [ReactQuill]: https://github.com/zenoamaro/react-quill [Slack Group]: https://join.slack.com/t/bulletjournal1024/shared_invite/zt-fys7t9hi-ITVU5PGDen1rNRyCjdcQ2g +[Sample Page]: https://github.com/singerdmx/flutter-quill/blob/master/app/lib/pages/home_page.dart diff --git a/example/main.dart b/example/main.dart index e45f4298..a523c5a6 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,6 +1,3 @@ -import 'dart:async'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_quill/widgets/controller.dart'; import 'package:flutter_quill/widgets/editor.dart'; @@ -19,8 +16,7 @@ class _HomePageState extends State { return Scaffold( body: Column( children: [ - QuillToolbar.basic( - controller: _controller, onImagePickCallback: _uploadImageCallBack), + QuillToolbar.basic(controller: _controller), Expanded( child: Container( child: QuillEditor.basic( @@ -32,9 +28,4 @@ class _HomePageState extends State { ], )); } - - Future _uploadImageCallBack(File file) async { - // call upload file API and return file's absolute url - return new Completer().future; - } } From a07d2db906e88514885068337c098795ff3aae8f Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 21 Feb 2021 07:34:00 -0800 Subject: [PATCH 017/306] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76a347a9..73c5079e 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ _controller = QuillController( ## Configuration The `QuillToolbar` class lets you customise which formatting options are available. -[Sample Page] provided sample code for advanced usage and configuration. +[Sample Page] provides sample code for advanced usage and configuration. ## Web From 6cd6abf3cd9af14fa2ac4d5b1bd6d5761e55a71a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 21 Feb 2021 12:15:55 -0800 Subject: [PATCH 018/306] Upgrade version --- CHANGELOG.md | 5 ++++- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a6c79dc..7480006f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,4 +90,7 @@ * Fix static analysis error. ## [0.2.12] -* Support placeholder. \ No newline at end of file +* Support placeholder. + +## [0.3.0] +* Line Height calculated based on font size. \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 2c925516..abedfacf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.2.12 +version: 0.3.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From 471c1cb7ee542977b777b0fc10cbaa5cfb35ddea Mon Sep 17 00:00:00 2001 From: li3317 Date: Sun, 21 Feb 2021 22:53:04 -0500 Subject: [PATCH 019/306] scroll screen on keyboard popup --- lib/widgets/editor.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 29472aae..50b056dc 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -889,15 +889,10 @@ class RenderEditor extends RenderEditableContainerBox kMargin + offsetInViewport; final caretBottom = endpoints.single.point.dy + kMargin + offsetInViewport; - double dy; - if (caretTop < scrollOffset) { - dy = caretTop; - } else if (caretBottom > scrollOffset + viewportHeight) { + double dy = caretTop; + if (caretBottom > scrollOffset + viewportHeight) { dy = caretBottom - viewportHeight; } - if (dy == null) { - return null; - } return math.max(dy, 0.0); } } From c2c5b2a90f11a4e997c3787e28d5d0843eab0815 Mon Sep 17 00:00:00 2001 From: li3317 Date: Sun, 21 Feb 2021 22:54:53 -0500 Subject: [PATCH 020/306] bump version to 0.3.1 --- CHANGELOG.md | 5 ++++- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7480006f..cff31022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,4 +93,7 @@ * Support placeholder. ## [0.3.0] -* Line Height calculated based on font size. \ No newline at end of file +* Line Height calculated based on font size. + +## [0.3.1] +* cursor focus when keyboard is on. \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 93c91366..8abfafb7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.3.0 +version: 0.3.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From 16e247307989e00f6b8cbd9d825563d6f51779da Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 22 Feb 2021 17:45:22 -0500 Subject: [PATCH 021/306] Change type of SizeAttribute from String to double --- app/assets/sample_data.json | 24 ++++++++++++++++++++++++ lib/widgets/text_line.dart | 6 +++++- lib/widgets/toolbar.dart | 3 +++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/assets/sample_data.json b/app/assets/sample_data.json index 15cc2f2a..7fe41833 100644 --- a/app/assets/sample_data.json +++ b/app/assets/sample_data.json @@ -473,6 +473,30 @@ }, "insert": "Huge" }, + { + "attributes": { + "size": "15.0" + }, + "insert": "font size 15" + }, + { + "insert": " " + }, + { + "attributes": { + "size": "35" + }, + "insert": "font size 35" + }, + { + "insert": " " + }, + { + "attributes": { + "size": "20" + }, + "insert": "font size 20" + }, { "insert": "\n" } diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 8608e69b..ca8e52ce 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -153,7 +153,11 @@ class TextLine extends StatelessWidget { res = res.merge(defaultStyles.sizeHuge); break; default: - throw "Invalid size ${size.value}"; + double fontSize = double.tryParse(size.value); + if (fontSize != null) { + res = res.merge(TextStyle(fontSize: fontSize)); + } else + throw "Invalid size ${size.value}"; } } diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index ae0a6553..03c1bdf1 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -784,6 +784,9 @@ class _HistoryButtonState extends State { } void _setIconColor() { + + if(!mounted) return; + if (widget.undo) { setState(() { _iconColor = widget.controller.hasUndo From fd3aac999f4fb34d036d8d7c71b7152170fd21c0 Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 22 Feb 2021 17:49:29 -0500 Subject: [PATCH 022/306] small fix --- lib/widgets/text_line.dart | 3 ++- lib/widgets/toolbar.dart | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index ca8e52ce..ffe7ee93 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -156,8 +156,9 @@ class TextLine extends StatelessWidget { double fontSize = double.tryParse(size.value); if (fontSize != null) { res = res.merge(TextStyle(fontSize: fontSize)); - } else + } else { throw "Invalid size ${size.value}"; + } } } diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 03c1bdf1..921cb91c 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -784,8 +784,7 @@ class _HistoryButtonState extends State { } void _setIconColor() { - - if(!mounted) return; + if (!mounted) return; if (widget.undo) { setState(() { From 08ed4c3c85b2bf38cb9631339f81d5ab68edc114 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 24 Feb 2021 02:34:41 -0800 Subject: [PATCH 023/306] Fix cursor focus issue when keyboard is on --- CHANGELOG.md | 5 ++++- app/pubspec.lock | 23 ++++++++++++++++++++++- lib/widgets/editor.dart | 9 +++++++-- lib/widgets/raw_editor.dart | 21 ++++++++++++++++++++- pubspec.lock | 21 +++++++++++++++++++++ pubspec.yaml | 6 +++--- 6 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cff31022..2ae32204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,4 +96,7 @@ * Line Height calculated based on font size. ## [0.3.1] -* cursor focus when keyboard is on. \ No newline at end of file +* cursor focus when keyboard is on. + +## [0.3.2] +* Fix cursor focus issue when keyboard is on. \ No newline at end of file diff --git a/app/pubspec.lock b/app/pubspec.lock index 53c35ed4..92f7e30d 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -111,6 +111,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.4" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.4" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -124,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0" + version: "0.3.2" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 50b056dc..29472aae 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -889,10 +889,15 @@ class RenderEditor extends RenderEditableContainerBox kMargin + offsetInViewport; final caretBottom = endpoints.single.point.dy + kMargin + offsetInViewport; - double dy = caretTop; - if (caretBottom > scrollOffset + viewportHeight) { + double dy; + if (caretTop < scrollOffset) { + dy = caretTop; + } else if (caretBottom > scrollOffset + viewportHeight) { dy = caretBottom - viewportHeight; } + if (dy == null) { + return null; + } return math.max(dy, 0.0); } } diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index fbbb8a83..b99ddae1 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; 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'; @@ -123,8 +124,10 @@ class RawEditorState extends EditorState FocusAttachment _focusAttachment; CursorCont _cursorCont; ScrollController _scrollController; + KeyboardVisibilityController _keyboardVisibilityController; KeyboardListener _keyboardListener; bool _didAutoFocus = false; + bool _keyboardVisible = false; DefaultStyles _styles; final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); final LayerLink _toolbarLayerLink = LayerLink(); @@ -692,6 +695,16 @@ class RawEditorState extends EditorState handleDelete, ); + _keyboardVisibilityController = KeyboardVisibilityController(); + _keyboardVisibilityController.onChange.listen((bool visible) { + setState(() { + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(); + } + }); + }); + _focusAttachment = widget.focusNode.attach(context, onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); widget.focusNode.addListener(_handleFocusChanged); @@ -869,8 +882,14 @@ class RawEditorState extends EditorState } _didChangeTextEditingValue() { - requestKeyboard(); + if (_keyboardVisible) { + _onChangeTextEditingValue(); + } else { + requestKeyboard(); + } + } + _onChangeTextEditingValue() { _showCaretOnScreen(); updateRemoteValueIfNeeded(); _cursorCont.startOrStopCursorTimerIfNeeded( diff --git a/pubspec.lock b/pubspec.lock index 9842e845..664e3fc8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -90,6 +90,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.5" + flutter_keyboard_visibility: + dependency: "direct main" + description: + name: flutter_keyboard_visibility + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.4" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8abfafb7..90baae21 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.3.1 +version: 0.3.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git @@ -17,12 +17,12 @@ dependencies: collection: ^1.14.13 tuple: ^1.0.3 url_launcher: ^5.7.10 - flutter_colorpicker: ^0.3.4 + flutter_colorpicker: ^0.3.5 image_picker: ^0.6.7+22 photo_view: ^0.10.3 universal_html: ^1.2.1 file_picker: ^2.1.6 - + flutter_keyboard_visibility: ^4.0.4 dev_dependencies: flutter_test: From 33ce59ae4f39e04aea19600308621345b27e2cd6 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 24 Feb 2021 02:40:27 -0800 Subject: [PATCH 024/306] Upgrade flutter_colorpicker --- app/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pubspec.lock b/app/pubspec.lock index 92f7e30d..1b38ea1a 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -110,7 +110,7 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.3.4" + version: "0.3.5" flutter_keyboard_visibility: dependency: transitive description: From 06000734e68e16011eb403e4c99b10900c17fee1 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 24 Feb 2021 02:46:09 -0800 Subject: [PATCH 025/306] Update _keyboardVisibilityController.onChange.listen --- lib/widgets/raw_editor.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index b99ddae1..e9376e7f 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -699,10 +699,10 @@ class RawEditorState extends EditorState _keyboardVisibilityController.onChange.listen((bool visible) { setState(() { _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(); - } }); + if (visible) { + _onChangeTextEditingValue(); + } }); _focusAttachment = widget.focusNode.attach(context, From 2efb4b12c2ce6581b2c048ec33a1c3bbf210b998 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 24 Feb 2021 02:56:21 -0800 Subject: [PATCH 026/306] Update _keyboardVisibilityController.onChange.listen --- lib/widgets/raw_editor.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index e9376e7f..98a70955 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -697,9 +697,7 @@ class RawEditorState extends EditorState _keyboardVisibilityController = KeyboardVisibilityController(); _keyboardVisibilityController.onChange.listen((bool visible) { - setState(() { - _keyboardVisible = visible; - }); + _keyboardVisible = visible; if (visible) { _onChangeTextEditingValue(); } From 6c156d4a9a9b33a2d4a7529fe367e8d9dcaee718 Mon Sep 17 00:00:00 2001 From: li3317 Date: Wed, 24 Feb 2021 22:36:22 -0500 Subject: [PATCH 027/306] more fix on keyboard focus when cursor is on + bump version --- CHANGELOG.md | 5 ++++- lib/widgets/editor.dart | 8 +++----- pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae32204..3f23d572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,4 +99,7 @@ * cursor focus when keyboard is on. ## [0.3.2] -* Fix cursor focus issue when keyboard is on. \ No newline at end of file +* Fix cursor focus issue when keyboard is on. + +## [0.3.3] +* More fix on cursor focus issue when keyboard is on. \ No newline at end of file diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 29472aae..0c4668d0 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -876,19 +876,17 @@ class RenderEditor extends RenderEditableContainerBox double getOffsetToRevealCursor( double viewportHeight, double scrollOffset, double offsetInViewport) { List endpoints = getEndpointsForSelection(selection); - if (endpoints.length != 1) { - return null; - } + TextSelectionPoint endpoint = endpoints.first; RenderEditableBox child = childAtPosition(selection.extent); const kMargin = 8.0; - double caretTop = endpoints.single.point.dy - + double caretTop = endpoint.point.dy - child.preferredLineHeight(TextPosition( offset: selection.extentOffset - child.getContainer().getOffset())) - kMargin + offsetInViewport; - final caretBottom = endpoints.single.point.dy + kMargin + offsetInViewport; + final caretBottom = endpoint.point.dy + kMargin + offsetInViewport; double dy; if (caretTop < scrollOffset) { dy = caretTop; diff --git a/pubspec.yaml b/pubspec.yaml index 90baae21..9c6efdc4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.3.2 +version: 0.3.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From ae8f1a396b93078203848a2f7621df619d736d62 Mon Sep 17 00:00:00 2001 From: li3317 Date: Wed, 24 Feb 2021 22:38:57 -0500 Subject: [PATCH 028/306] small fix --- lib/widgets/raw_editor.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 98a70955..1b84e9b2 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -574,7 +574,9 @@ class RawEditorState extends EditorState _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); - requestKeyboard(); + if (!_keyboardVisible) { + requestKeyboard(); + } } _buildChildren(Document doc, BuildContext context) { From fe01f66b617d7e0c38a90b76aff81be8b6d57b01 Mon Sep 17 00:00:00 2001 From: Rishi Raj Singh <49035175+rish07@users.noreply.github.com> Date: Thu, 25 Feb 2021 15:45:57 +0530 Subject: [PATCH 029/306] Add base64 support in image import (#41) --- app/android/settings_aar.gradle | 1 + app/pubspec.lock | 11 +++++++++-- lib/widgets/editor.dart | 15 ++++++++++++--- pubspec.lock | 9 ++++++++- pubspec.yaml | 2 ++ 5 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 app/android/settings_aar.gradle diff --git a/app/android/settings_aar.gradle b/app/android/settings_aar.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/app/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/app/pubspec.lock b/app/pubspec.lock index 1b38ea1a..88e56154 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "0.3.2" + version: "0.3.3" flutter_test: dependency: "direct dev" description: flutter @@ -321,7 +321,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -343,6 +343,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + string_validator: + dependency: transitive + description: + name: string_validator + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" term_glyph: dependency: transitive description: diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 0c4668d0..fe16ca67 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io' as io; import 'dart:math' as math; @@ -19,6 +20,7 @@ import 'package:flutter_quill/widgets/image.dart'; import 'package:flutter_quill/widgets/raw_editor.dart'; import 'package:flutter_quill/widgets/responsive_widget.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; +import 'package:string_validator/string_validator.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; @@ -407,9 +409,16 @@ class _QuillEditorSelectionGestureDetectorBuilder getEditor().context, MaterialPageRoute( builder: (context) => ImageTapWrapper( - imageProvider: imageUrl.startsWith('http') - ? NetworkImage(imageUrl) - : FileImage(io.File(blockEmbed.data))), + imageProvider: imageUrl.startsWith('http') + ? NetworkImage(imageUrl) + : (isBase64(imageUrl)) + ? Image.memory( + base64.decode(imageUrl), + ) + : FileImage( + io.File(blockEmbed.data), + ), + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 664e3fc8..8cdc5c8f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -244,7 +244,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -266,6 +266,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + string_validator: + dependency: "direct main" + description: + name: string_validator + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9c6efdc4..e600b104 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,8 +22,10 @@ dependencies: photo_view: ^0.10.3 universal_html: ^1.2.1 file_picker: ^2.1.6 + string_validator: ^0.1.4 flutter_keyboard_visibility: ^4.0.4 + dev_dependencies: flutter_test: sdk: flutter From ec2b754a87bd9339fd096e3c3d1231d59985d70b Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 25 Feb 2021 10:26:31 -0800 Subject: [PATCH 030/306] Refactor code --- lib/widgets/editor.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index fe16ca67..14d81146 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -411,13 +411,9 @@ class _QuillEditorSelectionGestureDetectorBuilder builder: (context) => ImageTapWrapper( imageProvider: imageUrl.startsWith('http') ? NetworkImage(imageUrl) - : (isBase64(imageUrl)) - ? Image.memory( - base64.decode(imageUrl), - ) - : FileImage( - io.File(blockEmbed.data), - ), + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + : FileImage(io.File(imageUrl)), ), ), ); From 2122f9b7cdf75cb64e50c1c65accd1cf1fe209a6 Mon Sep 17 00:00:00 2001 From: pengw00 Date: Thu, 25 Feb 2021 21:50:13 -0600 Subject: [PATCH 031/306] Upgrade prerelease SDK & Bump master version --- CHANGELOG.md | 5 ++++- pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f23d572..bc086b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,4 +102,7 @@ * Fix cursor focus issue when keyboard is on. ## [0.3.3] -* More fix on cursor focus issue when keyboard is on. \ No newline at end of file +* More fix on cursor focus issue when keyboard is on. + +## [1.0.0-dev.1] +* Upgrade prerelease SDK & Bump for master \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index e600b104..133a3358 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.3.3 +version: 1.0.0-dev.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0-29.7.beta <3.0.0" flutter: ">=1.17.0 <2.0.0" dependencies: From 08734a8ec36309a8e4cfb7f046c5ed3654543dfd Mon Sep 17 00:00:00 2001 From: pengw00 Date: Thu, 25 Feb 2021 21:59:01 -0600 Subject: [PATCH 032/306] upgrade NullSafe dependencies --- pubspec.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 133a3358..87e8ab72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,16 +14,16 @@ dependencies: sdk: flutter quill_delta: ^2.0.0 quiver_hashcode: ^2.0.0 - collection: ^1.14.13 - tuple: ^1.0.3 - url_launcher: ^5.7.10 - flutter_colorpicker: ^0.3.5 - image_picker: ^0.6.7+22 + collection: ^1.15.0 + tuple: ^2.0.0-nullsafety.0 + url_launcher: ^6.0.0 + flutter_colorpicker: ^0.4.0-nullsafety.0 + image_picker: ^0.7.0 photo_view: ^0.10.3 - universal_html: ^1.2.1 - file_picker: ^2.1.6 - string_validator: ^0.1.4 - flutter_keyboard_visibility: ^4.0.4 + universal_html: ^1.2.4 + file_picker: ^3.0.0-nullsafety.2 + string_validator: ^0.2.0-nullsafety.0 + flutter_keyboard_visibility: ^5.0.0-nullsafety.2 dev_dependencies: From 78e480053851f69a6364b0e77edd5e2ee6e1583a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 25 Feb 2021 20:14:26 -0800 Subject: [PATCH 033/306] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 73c5079e..848f40c7 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,9 @@ The `QuillToolbar` class lets you customise which formatting options are availab Default branch `master` is on channel `master`. To use channel `stable`, switch to branch `stable`. Branch `master` on channel `master` supports web. To run the app on web do the following: +1 + +

1) Change flutter channel to master using `flutter channel master`, followed by `flutter upgrade`. 2) Enable web using `flutter config --enable-web` and restart the IDE. 3) Upon successful execution of step 1 and 2 you should see `Chrome` as one of the devices which you run `flutter devices`. From 9211afc47998a9c337ef953cb0c5f27ed8be0fa3 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Feb 2021 00:33:59 -0800 Subject: [PATCH 034/306] Remove quill_delta dependency --- lib/models/documents/document.dart | 2 +- lib/models/documents/history.dart | 2 +- lib/models/documents/nodes/block.dart | 2 +- lib/models/documents/nodes/leaf.dart | 2 +- lib/models/documents/nodes/line.dart | 2 +- lib/models/documents/nodes/node.dart | 2 +- lib/models/quill_delta.dart | 688 ++++++++++++++++++++++++++ lib/models/rules/delete.dart | 2 +- lib/models/rules/format.dart | 2 +- lib/models/rules/insert.dart | 2 +- lib/models/rules/rule.dart | 2 +- lib/utils/diff_delta.dart | 2 +- lib/widgets/controller.dart | 2 +- pubspec.lock | 7 - pubspec.yaml | 1 - 15 files changed, 700 insertions(+), 20 deletions(-) create mode 100644 lib/models/quill_delta.dart diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 1573f9af..73391d7b 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -4,7 +4,7 @@ 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:quill_delta/quill_delta.dart'; +import 'package:flutter_quill/models/quill_delta.dart'; import 'package:tuple/tuple.dart'; import '../rules/rule.dart'; diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index e0d26948..ea71afe9 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -1,4 +1,4 @@ -import 'package:quill_delta/quill_delta.dart'; +import 'package:flutter_quill/models/quill_delta.dart'; import 'package:tuple/tuple.dart'; import 'document.dart'; diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index 710de779..d48dc38d 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -1,4 +1,4 @@ -import 'package:quill_delta/quill_delta.dart'; +import 'package:flutter_quill/models/quill_delta.dart'; import 'container.dart'; import 'line.dart'; diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index aefb574d..13c7b6f0 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -1,6 +1,6 @@ import 'dart:math' as math; -import 'package:quill_delta/quill_delta.dart'; +import 'package:flutter_quill/models/quill_delta.dart'; import '../style.dart'; import 'embed.dart'; diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 2727e101..90b19f03 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter_quill/models/documents/attribute.dart'; import 'package:flutter_quill/models/documents/nodes/node.dart'; -import 'package:quill_delta/quill_delta.dart'; +import 'package:flutter_quill/models/quill_delta.dart'; import '../style.dart'; import 'block.dart'; diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index 6327a1bf..9c52f9c7 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'package:flutter_quill/models/documents/style.dart'; -import 'package:quill_delta/quill_delta.dart'; +import 'package:flutter_quill/models/quill_delta.dart'; import '../attribute.dart'; import 'container.dart'; diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart new file mode 100644 index 00000000..40a4998b --- /dev/null +++ b/lib/models/quill_delta.dart @@ -0,0 +1,688 @@ +// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +/// Implementation of Quill Delta format in Dart. +library quill_delta; + +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:quiver_hashcode/hashcode.dart'; + +const _attributeEquality = DeepCollectionEquality(); +const _valueEquality = DeepCollectionEquality(); + +/// Decoder function to convert raw `data` object into a user-defined data type. +/// +/// Useful with embedded content. +typedef DataDecoder = Object Function(Object data); + +/// Default data decoder which simply passes through the original value. +Object _passThroughDataDecoder(Object data) => data; + +/// Operation performed on a rich-text document. +class Operation { + /// Key of insert operations. + static const String insertKey = 'insert'; + + /// Key of delete operations. + static const String deleteKey = 'delete'; + + /// Key of retain operations. + static const String retainKey = 'retain'; + + /// Key of attributes collection. + static const String attributesKey = 'attributes'; + + static const List _validKeys = [insertKey, deleteKey, retainKey]; + + /// Key of this operation, can be "insert", "delete" or "retain". + final String key; + + /// Length of this operation. + final int length; + + /// Payload of "insert" operation, for other types is set to empty string. + final Object data; + + /// Rich-text attributes set by this operation, can be `null`. + Map get attributes => + _attributes == null ? null : Map.from(_attributes); + final Map _attributes; + + Operation._(this.key, this.length, this.data, Map attributes) + : assert(key != null && length != null && data != null), + 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 + /// decode the operation's data object. Only applied to insert operations. + static Operation fromJson(Map data, {DataDecoder dataDecoder}) { + dataDecoder ??= _passThroughDataDecoder; + final map = Map.from(data); + if (map.containsKey(Operation.insertKey)) { + final data = dataDecoder(map[Operation.insertKey]); + final dataLength = data is String ? data.length : 1; + return Operation._( + Operation.insertKey, dataLength, data, map[Operation.attributesKey]); + } else if (map.containsKey(Operation.deleteKey)) { + final int length = map[Operation.deleteKey]; + return Operation._(Operation.deleteKey, length, '', null); + } else if (map.containsKey(Operation.retainKey)) { + final int length = map[Operation.retainKey]; + return Operation._( + Operation.retainKey, length, '', map[Operation.attributesKey]); + } + throw ArgumentError.value(data, 'Invalid data for Delta operation.'); + } + + /// Returns JSON-serializable representation of this operation. + Map toJson() { + final json = {key: value}; + if (_attributes != null) json[Operation.attributesKey] = attributes; + 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. + dynamic get value => (key == Operation.insertKey) ? data : length; + + /// Returns `true` if this is a delete operation. + bool get isDelete => key == Operation.deleteKey; + + /// Returns `true` if this is an insert operation. + bool get isInsert => key == Operation.insertKey; + + /// Returns `true` if this is a retain operation. + bool get isRetain => key == Operation.retainKey; + + /// Returns `true` if this operation has no attributes, e.g. is plain text. + bool get isPlain => (_attributes == null || _attributes.isEmpty); + + /// Returns `true` if this operation sets at least one attribute. + bool get isNotPlain => !isPlain; + + /// Returns `true` is this operation is empty. + /// + /// An operation is considered empty if its [length] is equal to `0`. + bool get isEmpty => length == 0; + + /// Returns `true` is this operation is not empty. + bool get isNotEmpty => length > 0; + + @override + bool operator ==(other) { + if (identical(this, other)) return true; + if (other is! Operation) return false; + Operation typedOther = other; + return key == typedOther.key && + length == typedOther.length && + _valueEquality.equals(data, typedOther.data) && + hasSameAttributes(typedOther); + } + + /// Returns `true` if this operation has attribute specified by [name]. + bool hasAttribute(String name) => isNotPlain && _attributes.containsKey(name); + + /// Returns `true` if [other] operation has the same attributes as this one. + bool hasSameAttributes(Operation other) { + return _attributeEquality.equals(_attributes, other._attributes); + } + + @override + int get hashCode { + if (_attributes != null && _attributes.isNotEmpty) { + final attrsHash = + hashObjects(_attributes.entries.map((e) => hash2(e.key, e.value))); + return hash3(key, value, attrsHash); + } + return hash2(key, value); + } + + @override + String toString() { + final attr = attributes == null ? '' : ' + $attributes'; + final text = isInsert + ? (data is String + ? (data as String).replaceAll('\n', '⏎') + : data.toString()) + : '$length'; + return '$key⟨ $text ⟩$attr'; + } +} + +/// Delta represents a document or a modification of a document as a sequence of +/// insert, delete and retain operations. +/// +/// Delta consisting of only "insert" operations is usually referred to as +/// "document delta". When delta includes also "retain" or "delete" operations +/// it is a "change delta". +class Delta { + /// Transforms two attribute sets. + static Map transformAttributes( + Map a, Map b, bool priority) { + if (a == null) return b; + if (b == null) return null; + + if (!priority) return b; + + final result = b.keys.fold>({}, (attributes, key) { + if (!a.containsKey(key)) attributes[key] = b[key]; + return attributes; + }); + + return result.isEmpty ? null : result; + } + + /// Composes two attribute sets. + static Map composeAttributes( + Map a, Map b, + {bool keepNull = false}) { + a ??= const {}; + b ??= const {}; + + final result = Map.from(a)..addAll(b); + final keys = result.keys.toList(growable: false); + + if (!keepNull) { + for (final key in keys) { + if (result[key] == null) result.remove(key); + } + } + + return result.isEmpty ? null : result; + } + + ///get anti-attr result base on base + static Map invertAttributes( + Map attr, Map base) { + attr ??= const {}; + base ??= const {}; + + var baseInverted = base.keys.fold({}, (memo, key) { + if (base[key] != attr[key] && attr.containsKey(key)) { + memo[key] = base[key]; + } + return memo; + }); + + var inverted = + Map.from(attr.keys.fold(baseInverted, (memo, key) { + if (base[key] != attr[key] && !base.containsKey(key)) { + memo[key] = null; + } + return memo; + })); + return inverted; + } + + final List _operations; + + int _modificationCount = 0; + + Delta._(List operations) + : assert(operations != null), + _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 + /// decode the operation's data object. Only applied to insert operations. + static Delta fromJson(List data, {DataDecoder dataDecoder}) { + return Delta._(data + .map((op) => Operation.fromJson(op, dataDecoder: dataDecoder)) + .toList()); + } + + /// Returns list of operations in this delta. + List toList() => List.from(_operations); + + /// Returns JSON-serializable version of this delta. + List toJson() => toList(); + + /// Returns `true` if this delta is empty. + bool get isEmpty => _operations.isEmpty; + + /// Returns `true` if this delta is not empty. + bool get isNotEmpty => _operations.isNotEmpty; + + /// Returns number of operations in this delta. + int get length => _operations.length; + + /// Returns [Operation] at specified [index] in this delta. + Operation operator [](int index) => _operations[index]; + + /// Returns [Operation] at specified [index] in this delta. + Operation elementAt(int index) => _operations.elementAt(index); + + /// Returns the first [Operation] in this delta. + Operation get first => _operations.first; + + /// Returns the last [Operation] in this delta. + Operation get last => _operations.last; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! Delta) return false; + Delta typedOther = other; + final comparator = + ListEquality(const DefaultEquality()); + return comparator.equals(_operations, typedOther._operations); + } + + @override + int get hashCode => hashObjects(_operations); + + /// Retain [count] of characters from current position. + void retain(int count, [Map attributes]) { + assert(count >= 0); + if (count == 0) return; // no-op + push(Operation.retain(count, attributes)); + } + + /// Insert [data] at current position. + void insert(dynamic data, [Map attributes]) { + assert(data != null); + if (data is String && data.isEmpty) return; // no-op + push(Operation.insert(data, attributes)); + } + + /// Delete [count] characters from current position. + void delete(int count) { + assert(count >= 0); + if (count == 0) return; + push(Operation.delete(count)); + } + + void _mergeWithTail(Operation operation) { + assert(isNotEmpty); + assert(operation != null && last.key == operation.key); + assert(operation.data is String && last.data is String); + + final length = operation.length + last.length; + final lastText = last.data as String; + final opText = operation.data as String; + final resultText = lastText + opText; + final index = _operations.length; + _operations.replaceRange(index - 1, index, [ + Operation._(operation.key, length, resultText, operation.attributes), + ]); + } + + /// Pushes new operation into this delta. + /// + /// Performs compaction by composing [operation] with current tail operation + /// of this delta, when possible. For instance, if current tail is + /// `insert('abc')` and pushed operation is `insert('123')` then existing + /// tail is replaced with `insert('abc123')` - a compound result of the two + /// operations. + void push(Operation operation) { + if (operation.isEmpty) return; + + var index = _operations.length; + final lastOp = _operations.isNotEmpty ? _operations.last : null; + if (lastOp != null) { + if (lastOp.isDelete && operation.isDelete) { + _mergeWithTail(operation); + return; + } + + if (lastOp.isDelete && operation.isInsert) { + index -= 1; // Always insert before deleting + final nLastOp = (index > 0) ? _operations.elementAt(index - 1) : null; + if (nLastOp == null) { + _operations.insert(0, operation); + return; + } + } + + if (lastOp.isInsert && operation.isInsert) { + if (lastOp.hasSameAttributes(operation) && + operation.data is String && + lastOp.data is String) { + _mergeWithTail(operation); + return; + } + } + + if (lastOp.isRetain && operation.isRetain) { + if (lastOp.hasSameAttributes(operation)) { + _mergeWithTail(operation); + return; + } + } + } + if (index == _operations.length) { + _operations.add(operation); + } else { + final opAtIndex = _operations.elementAt(index); + _operations.replaceRange(index, index + 1, [operation, opAtIndex]); + } + _modificationCount++; + } + + /// Composes next operation from [thisIter] and [otherIter]. + /// + /// Returns new operation or `null` if operations from [thisIter] and + /// [otherIter] nullify each other. For instance, for the pair `insert('abc')` + /// and `delete(3)` composition result would be empty string. + Operation _composeOperation(DeltaIterator thisIter, DeltaIterator otherIter) { + if (otherIter.isNextInsert) return otherIter.next(); + if (thisIter.isNextDelete) return thisIter.next(); + + final length = math.min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter.next(length); + final otherOp = otherIter.next(length); + assert(thisOp.length == otherOp.length); + + if (otherOp.isRetain) { + final attributes = composeAttributes( + thisOp.attributes, + otherOp.attributes, + keepNull: thisOp.isRetain, + ); + if (thisOp.isRetain) { + return Operation.retain(thisOp.length, attributes); + } else if (thisOp.isInsert) { + return Operation.insert(thisOp.data, attributes); + } else { + throw StateError('Unreachable'); + } + } else { + // otherOp == delete && thisOp in [retain, insert] + assert(otherOp.isDelete); + if (thisOp.isRetain) return otherOp; + assert(thisOp.isInsert); + // otherOp(delete) + thisOp(insert) => null + } + return null; + } + + /// Composes this delta with [other] and returns new [Delta]. + /// + /// It is not required for this and [other] delta to represent a document + /// delta (consisting only of insert operations). + Delta compose(Delta other) { + final result = Delta(); + final thisIter = DeltaIterator(this); + final otherIter = DeltaIterator(other); + + while (thisIter.hasNext || otherIter.hasNext) { + final newOp = _composeOperation(thisIter, otherIter); + if (newOp != null) result.push(newOp); + } + return result..trim(); + } + + /// Transforms next operation from [otherIter] against next operation in + /// [thisIter]. + /// + /// Returns `null` if both operations nullify each other. + Operation _transformOperation( + DeltaIterator thisIter, DeltaIterator otherIter, bool priority) { + if (thisIter.isNextInsert && (priority || !otherIter.isNextInsert)) { + return Operation.retain(thisIter.next().length); + } else if (otherIter.isNextInsert) { + return otherIter.next(); + } + + final length = math.min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter.next(length); + final otherOp = otherIter.next(length); + assert(thisOp.length == otherOp.length); + + // At this point only delete and retain operations are possible. + if (thisOp.isDelete) { + // otherOp is either delete or retain, so they nullify each other. + return null; + } else if (otherOp.isDelete) { + return otherOp; + } else { + // Retain otherOp which is either retain or insert. + return Operation.retain( + length, + transformAttributes(thisOp.attributes, otherOp.attributes, priority), + ); + } + } + + /// Transforms [other] delta against operations in this delta. + Delta transform(Delta other, bool priority) { + final result = Delta(); + final thisIter = DeltaIterator(this); + final otherIter = DeltaIterator(other); + + while (thisIter.hasNext || otherIter.hasNext) { + final newOp = _transformOperation(thisIter, otherIter, priority); + if (newOp != null) result.push(newOp); + } + return result..trim(); + } + + /// Removes trailing retain operation with empty attributes, if present. + void trim() { + if (isNotEmpty) { + final last = _operations.last; + if (last.isRetain && last.isPlain) _operations.removeLast(); + } + } + + /// Concatenates [other] with this delta and returns the result. + Delta concat(Delta other) { + final result = Delta.from(this); + if (other.isNotEmpty) { + // In case first operation of other can be merged with last operation in + // our list. + result.push(other._operations.first); + result._operations.addAll(other._operations.sublist(1)); + } + return result; + } + + /// Inverts this delta against [base]. + /// + /// Returns new delta which negates effect of this delta when applied to + /// [base]. This is an equivalent of "undo" operation on deltas. + Delta invert(Delta base) { + final inverted = Delta(); + if (base.isEmpty) return inverted; + + var baseIndex = 0; + for (final op in _operations) { + if (op.isInsert) { + inverted.delete(op.length); + } else if (op.isRetain && op.isPlain) { + inverted.retain(op.length, null); + baseIndex += op.length; + } else if (op.isDelete || (op.isRetain && op.isNotPlain)) { + final length = op.length; + final sliceDelta = base.slice(baseIndex, baseIndex + length); + sliceDelta.toList().forEach((baseOp) { + if (op.isDelete) { + inverted.push(baseOp); + } else if (op.isRetain && op.isNotPlain) { + var invertAttr = invertAttributes(op.attributes, baseOp.attributes); + inverted.retain( + baseOp.length, invertAttr.isEmpty ? null : invertAttr); + } + }); + baseIndex += length; + } else { + throw StateError('Unreachable'); + } + } + inverted.trim(); + return inverted; + } + + /// Returns slice of this delta from [start] index (inclusive) to [end] + /// (exclusive). + Delta slice(int start, [int end]) { + final delta = Delta(); + var index = 0; + var opIterator = DeltaIterator(this); + + final actualEnd = end ?? double.infinity; + + while (index < actualEnd && opIterator.hasNext) { + Operation op; + if (index < start) { + op = opIterator.next(start - index); + } else { + op = opIterator.next(actualEnd - index); + delta.push(op); + } + index += op.length; + } + return delta; + } + + /// Transforms [index] against this delta. + /// + /// Any "delete" operation before specified [index] shifts it backward, as + /// well as any "insert" operation shifts it forward. + /// + /// The [force] argument is used to resolve scenarios when there is an + /// insert operation at the same position as [index]. If [force] is set to + /// `true` (default) then position is forced to shift forward, otherwise + /// position stays at the same index. In other words setting [force] to + /// `false` gives higher priority to the transformed position. + /// + /// Useful to adjust caret or selection positions. + int transformPosition(int index, {bool force = true}) { + final iter = DeltaIterator(this); + var offset = 0; + while (iter.hasNext && offset <= index) { + final op = iter.next(); + if (op.isDelete) { + index -= math.min(op.length, index - offset); + continue; + } else if (op.isInsert && (offset < index || force)) { + index += op.length; + } + offset += op.length; + } + return index; + } + + @override + String toString() => _operations.join('\n'); +} + +/// Specialized iterator for [Delta]s. +class DeltaIterator { + 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; + + bool get isNextRetain => nextOperationKey == Operation.retainKey; + + String get nextOperationKey { + if (_index < delta.length) { + return delta.elementAt(_index).key; + } else { + return null; + } + } + + bool get hasNext => peekLength() < double.infinity; + + /// Returns length of next operation without consuming it. + /// + /// Returns [double.infinity] if there is no more operations left to iterate. + num peekLength() { + if (_index < delta.length) { + final operation = delta._operations[_index]; + return operation.length - _offset; + } + return double.infinity; + } + + /// Consumes and returns next operation. + /// + /// Optional [length] specifies maximum length of operation to return. Note + /// that actual length of returned operation may be less than specified value. + Operation next([num length = double.infinity]) { + assert(length != null); + + if (_modificationCount != delta._modificationCount) { + throw ConcurrentModificationError(delta); + } + + if (_index < delta.length) { + final op = delta.elementAt(_index); + final opKey = op.key; + final opAttributes = op.attributes; + final _currentOffset = _offset; + final actualLength = math.min(op.length - _currentOffset, length); + if (actualLength == op.length - _currentOffset) { + _index++; + _offset = 0; + } else { + _offset += actualLength; + } + final opData = op.isInsert && op.data is String + ? (op.data as String) + .substring(_currentOffset, _currentOffset + actualLength) + : op.data; + 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; + return Operation._(opKey, opActualLength, opData, opAttributes); + } + return Operation.retain(length); + } + + /// Skips [length] characters in source delta. + /// + /// Returns last skipped operation, or `null` if there was nothing to skip. + Operation skip(int length) { + var skipped = 0; + Operation op; + while (skipped < length && hasNext) { + final opLength = peekLength(); + final skip = math.min(length - skipped, opLength); + op = next(skip); + skipped += op.length; + } + return op; + } +} diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index cac220d1..6cbb28ff 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 'package:quill_delta/quill_delta.dart'; abstract class DeleteRule extends Rule { const DeleteRule(); diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart index 13f7b518..755bc0bb 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 'package:quill_delta/quill_delta.dart'; abstract class FormatRule extends Rule { const FormatRule(); diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 4715ae87..1973d981 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -1,7 +1,7 @@ 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:quill_delta/quill_delta.dart'; import 'package:tuple/tuple.dart'; abstract class InsertRule extends Rule { diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index d0e3bc14..13c7d12e 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -1,6 +1,6 @@ import 'package:flutter_quill/models/documents/attribute.dart'; import 'package:flutter_quill/models/documents/document.dart'; -import 'package:quill_delta/quill_delta.dart'; +import 'package:flutter_quill/models/quill_delta.dart'; import 'delete.dart'; import 'format.dart'; diff --git a/lib/utils/diff_delta.dart b/lib/utils/diff_delta.dart index 49a06dab..673dacd2 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -1,6 +1,6 @@ import 'dart:math' as math; -import 'package:quill_delta/quill_delta.dart'; +import 'package:flutter_quill/models/quill_delta.dart'; const Set WHITE_SPACE = { 0x9, diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 5bd6b2d8..abf2a059 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -5,8 +5,8 @@ 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:quill_delta/quill_delta.dart'; import 'package:tuple/tuple.dart'; class QuillController extends ChangeNotifier { diff --git a/pubspec.lock b/pubspec.lock index 8cdc5c8f..75d48e83 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -212,13 +212,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" - quill_delta: - dependency: "direct main" - description: - name: quill_delta - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" quiver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 87e8ab72..4d1bdf7b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,6 @@ environment: dependencies: flutter: sdk: flutter - quill_delta: ^2.0.0 quiver_hashcode: ^2.0.0 collection: ^1.15.0 tuple: ^2.0.0-nullsafety.0 From a2de79ab49b8b9b3b68a88889ac971d01641d0ef Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Feb 2021 00:48:56 -0800 Subject: [PATCH 035/306] Fix quill_delta next op --- lib/models/quill_delta.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index 40a4998b..3bbda2eb 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -639,7 +639,7 @@ class DeltaIterator { /// /// Optional [length] specifies maximum length of operation to return. Note /// that actual length of returned operation may be less than specified value. - Operation next([num length = double.infinity]) { + Operation next([int length = 4294967296]) { assert(length != null); if (_modificationCount != delta._modificationCount) { From c7b0eac42d6b8ec63294a9c2b31737884e1f92b1 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Feb 2021 00:52:43 -0800 Subject: [PATCH 036/306] Fix for unable to remove the last character (#44) --- lib/widgets/raw_editor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 1b84e9b2..2ae65da4 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -389,7 +389,7 @@ class RawEditorState extends EditorState ); _textInputConnection.setEditingState(_lastKnownRemoteTextEditingValue); - _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); + // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); } _textInputConnection.show(); } From bef9c6e75485194d618c6605c8c4fac50ccc8305 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Feb 2021 09:19:00 -0800 Subject: [PATCH 037/306] Revert the sdk version back to sdk: ">=2.7.0 <3.0.0" --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4d1bdf7b..61460904 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git environment: - sdk: ">=2.12.0-29.7.beta <3.0.0" + sdk: ">=2.7.0 <3.0.0" flutter: ">=1.17.0 <2.0.0" dependencies: From 26652ef5724a2d0fbfc48435cc92911d77d05ea3 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 28 Feb 2021 02:31:17 -0800 Subject: [PATCH 038/306] Improve link handling for tel, mailto and etc --- lib/widgets/editor.dart | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 14d81146..5c87b4e6 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -31,8 +31,12 @@ import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; -const urlPattern = - r"^((https?|http)://)?([-A-Z0-9.]+)(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:‌​,.;]*)?$"; +const linkPrefixes = [ + 'mailto:', // email + 'tel:', // telephone + 'sms:', // SMS + 'http' +]; abstract class EditorState extends State { TextEditingValue getTextEditingValue(); @@ -317,8 +321,6 @@ class _QuillEditorState extends State class _QuillEditorSelectionGestureDetectorBuilder extends EditorTextSelectionGestureDetectorBuilder { - static final urlRegExp = new RegExp(urlPattern, caseSensitive: false); - final _QuillEditorState _state; _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); @@ -394,9 +396,12 @@ class _QuillEditorSelectionGestureDetectorBuilder launchUrl = _launchUrl; } String link = segment.style.attributes[Attribute.link.key].value; - if (getEditor().widget.readOnly && - link != null && - urlRegExp.firstMatch(link.trim()) != null) { + if (getEditor().widget.readOnly && link != null) { + link = link.trim(); + if (!linkPrefixes + .any((linkPrefix) => link.toLowerCase().startsWith(linkPrefix))) { + link = 'https://$link'; + } launchUrl(link); } return false; @@ -452,9 +457,6 @@ class _QuillEditorSelectionGestureDetectorBuilder } void _launchUrl(String url) async { - if (!url.startsWith('http')) { - url = 'https://$url'; - } await launch(url); } From 9a89953f24ad4b3d9415d1306a2206ff45109279 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 28 Feb 2021 02:39:08 -0800 Subject: [PATCH 039/306] Expand link prefixes https://beradrian.wordpress.com/2010/01/15/special-links/ --- lib/widgets/editor.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 5c87b4e6..b49598bc 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -35,6 +35,16 @@ const linkPrefixes = [ 'mailto:', // email 'tel:', // telephone 'sms:', // SMS + 'callto:', + 'wtai:', + 'market:', + 'geopoint:', + 'ymsgr:', + 'msnim:', + 'gtalk:', // Google Talk + 'skype:', + 'sip:', // Lync + 'whatsapp:', 'http' ]; From c816a765c3c6a61c347c6c83524edd425088953f Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 28 Feb 2021 03:08:23 -0800 Subject: [PATCH 040/306] Upgrade version for Improve link handling for tel, mailto and etc --- CHANGELOG.md | 5 ++++- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc086b88..8f996e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,4 +105,7 @@ * More fix on cursor focus issue when keyboard is on. ## [1.0.0-dev.1] -* Upgrade prerelease SDK & Bump for master \ No newline at end of file +* Upgrade prerelease SDK & Bump for master + +## [1.0.0-dev.2] +* Improve link handling for tel, mailto and etc. \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 61460904..48681f97 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.0-dev.1 +version: 1.0.0-dev.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From e8a3d4c222a677b64ad4046f821ab5a74cee685f Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 1 Mar 2021 11:39:44 -0500 Subject: [PATCH 041/306] close _keyboardVisibilityController.onChange in dispose --- lib/widgets/raw_editor.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 2ae65da4..294dd2d8 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/cupertino.dart'; @@ -125,6 +126,7 @@ class RawEditorState extends EditorState CursorCont _cursorCont; ScrollController _scrollController; KeyboardVisibilityController _keyboardVisibilityController; + StreamSubscription _keyboardVisibilitySubscription; KeyboardListener _keyboardListener; bool _didAutoFocus = false; bool _keyboardVisible = false; @@ -698,12 +700,13 @@ class RawEditorState extends EditorState ); _keyboardVisibilityController = KeyboardVisibilityController(); - _keyboardVisibilityController.onChange.listen((bool visible) { - _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(); - } - }); + _keyboardVisibilitySubscription = + _keyboardVisibilityController.onChange.listen((bool visible) { + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(); + } + }); _focusAttachment = widget.focusNode.attach(context, onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); @@ -865,6 +868,7 @@ class RawEditorState extends EditorState @override void dispose() { closeConnectionIfNeeded(); + _keyboardVisibilitySubscription.cancel(); assert(!hasConnection); _selectionOverlay?.dispose(); _selectionOverlay = null; From 4dfa6a476f42d5232eac5190c3274b32cac03199 Mon Sep 17 00:00:00 2001 From: li3317 Date: Mon, 1 Mar 2021 11:40:47 -0500 Subject: [PATCH 042/306] format --- lib/widgets/raw_editor.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 294dd2d8..7d95f4a0 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -702,11 +702,11 @@ class RawEditorState extends EditorState _keyboardVisibilityController = KeyboardVisibilityController(); _keyboardVisibilitySubscription = _keyboardVisibilityController.onChange.listen((bool visible) { - _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(); - } - }); + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(); + } + }); _focusAttachment = widget.focusNode.attach(context, onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); From a1d1a5cd2a0567755ea397ff586610ab63fe4c6d Mon Sep 17 00:00:00 2001 From: florianh01 <78513002+florianh01@users.noreply.github.com> Date: Fri, 5 Mar 2021 00:32:05 +0100 Subject: [PATCH 043/306] Adding an article to week 02 of articles of the week (#51) --- lib/widgets/raw_editor.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 7d95f4a0..6b095a27 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -905,6 +905,7 @@ class RawEditorState extends EditorState SchedulerBinding.instance.addPostFrameCallback( (Duration _) => _updateOrDisposeSelectionOverlayIfNeeded()); + if (!mounted) return; setState(() { // Use widget.controller.value in build() // Trigger build and updateChildren @@ -959,6 +960,7 @@ class RawEditorState extends EditorState } _onChangedClipboardStatus() { + if (!mounted) return; setState(() { // Inform the widget that the value of clipboardStatus has changed. // Trigger build and updateChildren From b1155e3ea8a3354d62e60bdc01a9f5e5ffb3039d Mon Sep 17 00:00:00 2001 From: li3317 Date: Fri, 5 Mar 2021 22:21:51 -0500 Subject: [PATCH 044/306] support flutter 2.0 --- CHANGELOG.md | 13 ++++++- README.md | 13 +------ lib/widgets/editor.dart | 2 +- lib/widgets/{FakeUi.dart => fake_ui.dart} | 0 lib/widgets/{RealUi.dart => real_ui.dart} | 0 lib/widgets/toolbar.dart | 4 +- pubspec.lock | 46 +++++++++++------------ pubspec.yaml | 2 +- 8 files changed, 38 insertions(+), 42 deletions(-) rename lib/widgets/{FakeUi.dart => fake_ui.dart} (100%) rename lib/widgets/{RealUi.dart => real_ui.dart} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f996e40..0e5cb7dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,8 +104,17 @@ ## [0.3.3] * More fix on cursor focus issue when keyboard is on. +## [0.3.4] +* Improve link handling for tel, mailto and etc. + +## [0.3.5] +* Fix for cursor focus issues when keyboard is on. + ## [1.0.0-dev.1] -* Upgrade prerelease SDK & Bump for master +* Upgrade prerelease SDK & Bump for master. ## [1.0.0-dev.2] -* Improve link handling for tel, mailto and etc. \ No newline at end of file +* Improve link handling for tel, mailto and etc. + +## [1.0.0] +* Support flutter 2.0. \ No newline at end of file diff --git a/README.md b/README.md index 848f40c7..3cb37a95 100644 --- a/README.md +++ b/README.md @@ -70,18 +70,7 @@ The `QuillToolbar` class lets you customise which formatting options are availab ## Web -Default branch `master` is on channel `master`. To use channel `stable`, switch to branch `stable`. -Branch `master` on channel `master` supports web. To run the app on web do the following: - -1 - -

-1) Change flutter channel to master using `flutter channel master`, followed by `flutter upgrade`. -2) Enable web using `flutter config --enable-web` and restart the IDE. -3) Upon successful execution of step 1 and 2 you should see `Chrome` as one of the devices which you run `flutter devices`. -4) Run the app. - -For web development, [ReactQuill] is recommended to use for compatibility. +For web development, use `flutter config --enable-web` for flutter and use [ReactQuill] for React. --- diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index b49598bc..d58fb7e4 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -24,7 +24,7 @@ import 'package:string_validator/string_validator.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; -import 'FakeUi.dart' if (dart.library.html) 'RealUi.dart' as ui; +import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui; import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; diff --git a/lib/widgets/FakeUi.dart b/lib/widgets/fake_ui.dart similarity index 100% rename from lib/widgets/FakeUi.dart rename to lib/widgets/fake_ui.dart diff --git a/lib/widgets/RealUi.dart b/lib/widgets/real_ui.dart similarity index 100% rename from lib/widgets/RealUi.dart rename to lib/widgets/real_ui.dart diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 921cb91c..96c87933 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -144,7 +144,7 @@ class _LinkDialogState extends State<_LinkDialog> { onChanged: _linkChanged, ), actions: [ - FlatButton( + TextButton( onPressed: _link.isNotEmpty ? _applyLink : null, child: Text('Apply'), ), @@ -512,7 +512,6 @@ class ImageButton extends StatefulWidget { class _ImageButtonState extends State { List _paths; - String _directoryPath; String _extension; final _picker = ImagePicker(); FileType _pickingType = FileType.any; @@ -535,7 +534,6 @@ class _ImageButtonState extends State { Future _pickImageWeb() async { try { - _directoryPath = null; _paths = (await FilePicker.platform.pickFiles( type: _pickingType, allowMultiple: false, diff --git a/pubspec.lock b/pubspec.lock index 75d48e83..59f3c93b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,7 +77,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "2.1.6" + version: "3.0.0" flutter: dependency: "direct main" description: flutter @@ -89,35 +89,35 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "0.4.0-nullsafety.0" flutter_keyboard_visibility: dependency: "direct main" description: name: flutter_keyboard_visibility url: "https://pub.dartlang.org" source: hosted - version: "4.0.4" + version: "5.0.0" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "1.0.11" + version: "2.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -141,28 +141,28 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "0.13.0" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.0" image_picker: dependency: "direct main" description: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.7+22" + version: "0.7.2" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "2.0.1" js: dependency: transitive description: @@ -211,14 +211,14 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.0.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.0" quiver_hashcode: dependency: "direct main" description: @@ -237,7 +237,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0" stack_trace: dependency: transitive description: @@ -265,7 +265,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.2.0-nullsafety.0" term_glyph: dependency: transitive description: @@ -286,7 +286,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.0.0-nullsafety.0" typed_data: dependency: transitive description: @@ -314,42 +314,42 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.7.10" + version: "6.0.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+4" + version: "2.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+9" + version: "2.0.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.9" + version: "2.0.2" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.5+3" + version: "2.0.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+3" + version: "2.0.0" vector_math: dependency: transitive description: @@ -365,5 +365,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.22.0" + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.24.0-10.2.pre" diff --git a/pubspec.yaml b/pubspec.yaml index 48681f97..68e79da2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.0-dev.2 +version: 1.0.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From e34998ab648f1cbcda595e7b58e62f6d8c18b87e Mon Sep 17 00:00:00 2001 From: li3317 Date: Fri, 5 Mar 2021 22:45:33 -0500 Subject: [PATCH 045/306] upgrade gradle version --- app/android/app/build.gradle | 2 +- app/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- app/pubspec.lock | 57 ++++++++----------- pubspec.yaml | 2 +- 5 files changed, 29 insertions(+), 36 deletions(-) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 4d2c8dd1..b53f38f0 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) { def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") + throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') diff --git a/app/android/build.gradle b/app/android/build.gradle index e0d7ae2c..11e3d090 100644 --- a/app/android/build.gradle +++ b/app/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.6.3' } } diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index 296b146b..c69d51db 100644 --- a/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip \ No newline at end of file diff --git a/app/pubspec.lock b/app/pubspec.lock index 88e56154..407518db 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -98,7 +98,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "2.1.6" + version: "3.0.0-nullsafety.2" flutter: dependency: "direct main" description: flutter @@ -110,42 +110,42 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "0.4.0-nullsafety.0" flutter_keyboard_visibility: dependency: transitive description: name: flutter_keyboard_visibility url: "https://pub.dartlang.org" source: hosted - version: "4.0.4" + version: "5.0.0-nullsafety.3" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0-nullsafety.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0-nullsafety.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "1.0.11" + version: "2.0.0" flutter_quill: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.3.3" + version: "1.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -169,28 +169,28 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "0.13.0" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" + version: "4.0.0" image_picker: dependency: transitive description: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.7+22" + version: "0.7.2" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "2.0.0" js: dependency: transitive description: @@ -260,7 +260,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.9.2" + version: "1.11.0" photo_view: dependency: transitive description: @@ -281,7 +281,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.1.0-nullsafety.2" process: dependency: transitive description: @@ -289,20 +289,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.1.0" - quill_delta: - dependency: transitive - description: - name: quill_delta - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.0" quiver_hashcode: dependency: transitive description: @@ -321,7 +314,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0" stack_trace: dependency: transitive description: @@ -349,7 +342,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.2.0-nullsafety.0" term_glyph: dependency: transitive description: @@ -370,7 +363,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.0.0-nullsafety.0" typed_data: dependency: transitive description: @@ -398,42 +391,42 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.7.10" + version: "6.0.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+4" + version: "2.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+9" + version: "2.0.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.9" + version: "2.0.1" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.5+1" + version: "2.0.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+3" + version: "2.0.0" vector_math: dependency: transitive description: @@ -463,5 +456,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.22.0" + dart: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.24.0-10.2.pre" diff --git a/pubspec.yaml b/pubspec.yaml index 68e79da2..3e87a468 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: image_picker: ^0.7.0 photo_view: ^0.10.3 universal_html: ^1.2.4 - file_picker: ^3.0.0-nullsafety.2 + file_picker: ^3.0.0-nullsafety.4 string_validator: ^0.2.0-nullsafety.0 flutter_keyboard_visibility: ^5.0.0-nullsafety.2 From d2e6c1314657c7a4e497fd581296d24686a2e0f8 Mon Sep 17 00:00:00 2001 From: Jochen Date: Sat, 6 Mar 2021 12:29:40 +0800 Subject: [PATCH 046/306] Update toolbar.dart (#55) fix --- lib/widgets/toolbar.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 96c87933..92dccc3a 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -518,6 +518,8 @@ class _ImageButtonState extends State { Future _pickImage(ImageSource source) async { final PickedFile pickedFile = await _picker.getImage(source: source); + if (pickedFile == null) return null; + final File file = File(pickedFile.path); if (file == null || widget.onImagePickCallback == null) return null; From ffbc2acf42e96895ae8a1d4b1cf7c4035b0b5b6f Mon Sep 17 00:00:00 2001 From: Rishi Raj Singh <49035175+rish07@users.noreply.github.com> Date: Sat, 6 Mar 2021 10:39:24 +0530 Subject: [PATCH 047/306] Bug Fix and version update in pubspec.yaml (#56) * Add base64 support in image import * fix bug Co-authored-by: Creator --- app/lib/pages/home_page.dart | 14 +++++++---- app/pubspec.lock | 49 ++++++++++++++++++++---------------- app/pubspec.yaml | 4 +-- lib/widgets/editor.dart | 45 +++++++++++++-------------------- pubspec.yaml | 13 +++++----- 5 files changed, 64 insertions(+), 61 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index cee58494..665ce1b9 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -92,7 +92,8 @@ class _HomePageState extends State { Widget _buildWelcomeEditor(BuildContext context) { return SafeArea( - child: Stack( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( height: MediaQuery.of(context).size.height * 0.88, @@ -124,10 +125,13 @@ class _HomePageState extends State { ), ), ), - Container( - child: QuillToolbar.basic( - controller: _controller, - onImagePickCallback: _onImagePickCallback), + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + child: QuillToolbar.basic( + controller: _controller, + onImagePickCallback: _onImagePickCallback), + ), ), ], ), diff --git a/app/pubspec.lock b/app/pubspec.lock index 407518db..424fec52 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -70,7 +70,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.2" fake_async: dependency: transitive description: @@ -84,7 +84,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.0.0" file: dependency: transitive description: @@ -98,7 +98,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.2" + version: "3.0.0" flutter: dependency: "direct main" description: flutter @@ -110,28 +110,28 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.4.0-nullsafety.0" + version: "0.3.5" flutter_keyboard_visibility: dependency: transitive description: name: flutter_keyboard_visibility url: "https://pub.dartlang.org" source: hosted - version: "5.0.0-nullsafety.3" + version: "5.0.0" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-nullsafety.0" + version: "2.0.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-nullsafety.0" + version: "2.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "1.0.0" + version: "0.3.3" flutter_test: dependency: "direct dev" description: flutter @@ -225,35 +225,35 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.27" + version: "2.0.1" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+2" + version: "2.0.0" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.4+8" + version: "2.0.0" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.4+3" + version: "2.0.0" pedantic: dependency: transitive description: @@ -281,7 +281,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.2" + version: "2.0.0" process: dependency: transitive description: @@ -289,13 +289,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.1.0" + quill_delta: + dependency: transitive + description: + name: quill_delta + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "2.1.5" quiver_hashcode: dependency: transitive description: @@ -342,7 +349,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.2.0-nullsafety.0" + version: "0.1.4" term_glyph: dependency: transitive description: @@ -363,7 +370,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-nullsafety.0" + version: "1.0.3" typed_data: dependency: transitive description: @@ -440,14 +447,14 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.4+1" + version: "2.0.0" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.2.0" zone_local: dependency: transitive description: @@ -456,5 +463,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.12.0-259.9.beta <3.0.0" - flutter: ">=1.24.0-10.2.pre" + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.22.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 4d471487..8858a8dc 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -27,8 +27,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.0 - path_provider: ^1.6.27 + cupertino_icons: ^1.0.2 + path_provider: ^2.0.1 flutter_quill: path: ../ diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index d58fb7e4..a2400bd5 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -20,33 +20,18 @@ import 'package:flutter_quill/widgets/image.dart'; import 'package:flutter_quill/widgets/raw_editor.dart'; import 'package:flutter_quill/widgets/responsive_widget.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; -import 'package:string_validator/string_validator.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; -import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui; +import 'FakeUi.dart' if (dart.library.html) 'RealUi.dart' as ui; import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; -const linkPrefixes = [ - 'mailto:', // email - 'tel:', // telephone - 'sms:', // SMS - 'callto:', - 'wtai:', - 'market:', - 'geopoint:', - 'ymsgr:', - 'msnim:', - 'gtalk:', // Google Talk - 'skype:', - 'sip:', // Lync - 'whatsapp:', - 'http' -]; +const urlPattern = + r"^((https?|http)://)?([-A-Z0-9.]+)(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:‌​,.;]*)?$"; abstract class EditorState extends State { TextEditingValue getTextEditingValue(); @@ -331,6 +316,8 @@ class _QuillEditorState extends State class _QuillEditorSelectionGestureDetectorBuilder extends EditorTextSelectionGestureDetectorBuilder { + static final urlRegExp = new RegExp(urlPattern, caseSensitive: false); + final _QuillEditorState _state; _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); @@ -406,12 +393,9 @@ class _QuillEditorSelectionGestureDetectorBuilder launchUrl = _launchUrl; } String link = segment.style.attributes[Attribute.link.key].value; - if (getEditor().widget.readOnly && link != null) { - link = link.trim(); - if (!linkPrefixes - .any((linkPrefix) => link.toLowerCase().startsWith(linkPrefix))) { - link = 'https://$link'; - } + if (getEditor().widget.readOnly && + link != null && + urlRegExp.firstMatch(link.trim()) != null) { launchUrl(link); } return false; @@ -426,9 +410,13 @@ class _QuillEditorSelectionGestureDetectorBuilder builder: (context) => ImageTapWrapper( imageProvider: imageUrl.startsWith('http') ? NetworkImage(imageUrl) - : isBase64(imageUrl) - ? Image.memory(base64.decode(imageUrl)) - : FileImage(io.File(imageUrl)), + : (isBase64(imageUrl)) + ? Image.memory( + base64.decode(imageUrl), + ) + : FileImage( + io.File(blockEmbed.data), + ), ), ), ); @@ -467,6 +455,9 @@ class _QuillEditorSelectionGestureDetectorBuilder } void _launchUrl(String url) async { + if (!url.startsWith('http')) { + url = 'https://$url'; + } await launch(url); } diff --git a/pubspec.yaml b/pubspec.yaml index 3e87a468..985e81cc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.0 +version: 0.3.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git @@ -12,12 +12,13 @@ environment: dependencies: flutter: sdk: flutter + quill_delta: ^2.0.0 quiver_hashcode: ^2.0.0 - collection: ^1.15.0 - tuple: ^2.0.0-nullsafety.0 - url_launcher: ^6.0.0 - flutter_colorpicker: ^0.4.0-nullsafety.0 - image_picker: ^0.7.0 + collection: ^1.14.13 + tuple: ^1.0.3 + url_launcher: ^5.7.10 + flutter_colorpicker: ^0.3.5 + image_picker: ^0.6.7+22 photo_view: ^0.10.3 universal_html: ^1.2.4 file_picker: ^3.0.0-nullsafety.4 From 41a055548271962fdaab110484103b49950d640f Mon Sep 17 00:00:00 2001 From: Rishi Raj Singh <49035175+rish07@users.noreply.github.com> Date: Sat, 6 Mar 2021 10:44:50 +0530 Subject: [PATCH 048/306] Revert "Bug Fix and version update in pubspec.yaml (#56)" (#57) This reverts commit ffbc2acf42e96895ae8a1d4b1cf7c4035b0b5b6f. --- app/lib/pages/home_page.dart | 14 ++++------- app/pubspec.lock | 49 ++++++++++++++++-------------------- app/pubspec.yaml | 4 +-- lib/widgets/editor.dart | 45 ++++++++++++++++++++------------- pubspec.yaml | 13 +++++----- 5 files changed, 61 insertions(+), 64 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 665ce1b9..cee58494 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -92,8 +92,7 @@ class _HomePageState extends State { Widget _buildWelcomeEditor(BuildContext context) { return SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Stack( children: [ Container( height: MediaQuery.of(context).size.height * 0.88, @@ -125,13 +124,10 @@ class _HomePageState extends State { ), ), ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - child: QuillToolbar.basic( - controller: _controller, - onImagePickCallback: _onImagePickCallback), - ), + Container( + child: QuillToolbar.basic( + controller: _controller, + onImagePickCallback: _onImagePickCallback), ), ], ), diff --git a/app/pubspec.lock b/app/pubspec.lock index 424fec52..407518db 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -70,7 +70,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.0" fake_async: dependency: transitive description: @@ -84,7 +84,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "0.1.3" file: dependency: transitive description: @@ -98,7 +98,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.0-nullsafety.2" flutter: dependency: "direct main" description: flutter @@ -110,28 +110,28 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "0.4.0-nullsafety.0" flutter_keyboard_visibility: dependency: transitive description: name: flutter_keyboard_visibility url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.0.0-nullsafety.3" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.0-nullsafety.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.0-nullsafety.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "0.3.3" + version: "1.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -225,35 +225,35 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "1.6.27" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "0.0.1+2" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "0.0.4+8" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "1.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "0.0.4+3" pedantic: dependency: transitive description: @@ -281,7 +281,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.1.0-nullsafety.2" process: dependency: transitive description: @@ -289,20 +289,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.1.0" - quill_delta: - dependency: transitive - description: - name: quill_delta - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.0" quiver_hashcode: dependency: transitive description: @@ -349,7 +342,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.2.0-nullsafety.0" term_glyph: dependency: transitive description: @@ -370,7 +363,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.0.0-nullsafety.0" typed_data: dependency: transitive description: @@ -447,14 +440,14 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.7.4+1" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.1.2" zone_local: dependency: transitive description: @@ -463,5 +456,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.22.0" + dart: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.24.0-10.2.pre" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8858a8dc..4d471487 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -27,8 +27,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - path_provider: ^2.0.1 + cupertino_icons: ^1.0.0 + path_provider: ^1.6.27 flutter_quill: path: ../ diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index a2400bd5..d58fb7e4 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -20,18 +20,33 @@ import 'package:flutter_quill/widgets/image.dart'; import 'package:flutter_quill/widgets/raw_editor.dart'; import 'package:flutter_quill/widgets/responsive_widget.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; +import 'package:string_validator/string_validator.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; -import 'FakeUi.dart' if (dart.library.html) 'RealUi.dart' as ui; +import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui; import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; -const urlPattern = - r"^((https?|http)://)?([-A-Z0-9.]+)(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:‌​,.;]*)?$"; +const linkPrefixes = [ + 'mailto:', // email + 'tel:', // telephone + 'sms:', // SMS + 'callto:', + 'wtai:', + 'market:', + 'geopoint:', + 'ymsgr:', + 'msnim:', + 'gtalk:', // Google Talk + 'skype:', + 'sip:', // Lync + 'whatsapp:', + 'http' +]; abstract class EditorState extends State { TextEditingValue getTextEditingValue(); @@ -316,8 +331,6 @@ class _QuillEditorState extends State class _QuillEditorSelectionGestureDetectorBuilder extends EditorTextSelectionGestureDetectorBuilder { - static final urlRegExp = new RegExp(urlPattern, caseSensitive: false); - final _QuillEditorState _state; _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); @@ -393,9 +406,12 @@ class _QuillEditorSelectionGestureDetectorBuilder launchUrl = _launchUrl; } String link = segment.style.attributes[Attribute.link.key].value; - if (getEditor().widget.readOnly && - link != null && - urlRegExp.firstMatch(link.trim()) != null) { + if (getEditor().widget.readOnly && link != null) { + link = link.trim(); + if (!linkPrefixes + .any((linkPrefix) => link.toLowerCase().startsWith(linkPrefix))) { + link = 'https://$link'; + } launchUrl(link); } return false; @@ -410,13 +426,9 @@ class _QuillEditorSelectionGestureDetectorBuilder builder: (context) => ImageTapWrapper( imageProvider: imageUrl.startsWith('http') ? NetworkImage(imageUrl) - : (isBase64(imageUrl)) - ? Image.memory( - base64.decode(imageUrl), - ) - : FileImage( - io.File(blockEmbed.data), - ), + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + : FileImage(io.File(imageUrl)), ), ), ); @@ -455,9 +467,6 @@ class _QuillEditorSelectionGestureDetectorBuilder } void _launchUrl(String url) async { - if (!url.startsWith('http')) { - url = 'https://$url'; - } await launch(url); } diff --git a/pubspec.yaml b/pubspec.yaml index 985e81cc..3e87a468 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 0.3.3 +version: 1.0.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git @@ -12,13 +12,12 @@ environment: dependencies: flutter: sdk: flutter - quill_delta: ^2.0.0 quiver_hashcode: ^2.0.0 - collection: ^1.14.13 - tuple: ^1.0.3 - url_launcher: ^5.7.10 - flutter_colorpicker: ^0.3.5 - image_picker: ^0.6.7+22 + collection: ^1.15.0 + tuple: ^2.0.0-nullsafety.0 + url_launcher: ^6.0.0 + flutter_colorpicker: ^0.4.0-nullsafety.0 + image_picker: ^0.7.0 photo_view: ^0.10.3 universal_html: ^1.2.4 file_picker: ^3.0.0-nullsafety.4 From d66c05899755294e064a44355225e7a9b8239c9e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 5 Mar 2021 21:25:15 -0800 Subject: [PATCH 049/306] Bug Fix and version update in pubspec.yaml --- app/lib/pages/home_page.dart | 14 +++++++++----- app/pubspec.lock | 4 ++-- app/pubspec.yaml | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index cee58494..665ce1b9 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -92,7 +92,8 @@ class _HomePageState extends State { Widget _buildWelcomeEditor(BuildContext context) { return SafeArea( - child: Stack( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( height: MediaQuery.of(context).size.height * 0.88, @@ -124,10 +125,13 @@ class _HomePageState extends State { ), ), ), - Container( - child: QuillToolbar.basic( - controller: _controller, - onImagePickCallback: _onImagePickCallback), + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + child: QuillToolbar.basic( + controller: _controller, + onImagePickCallback: _onImagePickCallback), + ), ), ], ), diff --git a/app/pubspec.lock b/app/pubspec.lock index 407518db..1db2fb1e 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -70,7 +70,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.2" fake_async: dependency: transitive description: @@ -225,7 +225,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.27" + version: "2.0.1" path_provider_linux: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 4d471487..8858a8dc 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -27,8 +27,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.0 - path_provider: ^1.6.27 + cupertino_icons: ^1.0.2 + path_provider: ^2.0.1 flutter_quill: path: ../ From 9810fae5d9de0393ba7b660f5e215bad488461e6 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 5 Mar 2021 23:50:59 -0800 Subject: [PATCH 050/306] Remove conditional import (#59) * Remove conditional import * fix pickWeb Co-authored-by: rish07 --- app/pubspec.lock | 22 +++++++++++----------- lib/widgets/editor.dart | 2 +- lib/widgets/fake_ui.dart | 4 ---- lib/widgets/real_ui.dart | 9 --------- lib/widgets/toolbar.dart | 3 ++- 5 files changed, 14 insertions(+), 26 deletions(-) delete mode 100644 lib/widgets/fake_ui.dart delete mode 100644 lib/widgets/real_ui.dart diff --git a/app/pubspec.lock b/app/pubspec.lock index 1db2fb1e..8b6e9a99 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -84,7 +84,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.0.0" file: dependency: transitive description: @@ -98,7 +98,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.2" + version: "3.0.0" flutter: dependency: "direct main" description: flutter @@ -124,7 +124,7 @@ packages: name: flutter_keyboard_visibility_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-nullsafety.0" + version: "2.0.0" flutter_keyboard_visibility_web: dependency: transitive description: @@ -232,28 +232,28 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+2" + version: "2.0.0" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.4+8" + version: "2.0.0" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.4+3" + version: "2.0.0" pedantic: dependency: transitive description: @@ -281,7 +281,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.2" + version: "2.0.0" process: dependency: transitive description: @@ -440,14 +440,14 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.4+1" + version: "2.0.0" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.2.0" zone_local: dependency: transitive description: @@ -456,5 +456,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.12.0-259.9.beta <3.0.0" + dart: ">=2.12.0 <3.0.0" flutter: ">=1.24.0-10.2.pre" diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index d58fb7e4..2e548474 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io' as io; import 'dart:math' as math; +import 'dart:ui' as ui; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -24,7 +25,6 @@ import 'package:string_validator/string_validator.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; -import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui; import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; diff --git a/lib/widgets/fake_ui.dart b/lib/widgets/fake_ui.dart deleted file mode 100644 index bc9799af..00000000 --- a/lib/widgets/fake_ui.dart +++ /dev/null @@ -1,4 +0,0 @@ -// ignore: camel_case_types -class platformViewRegistry { - static registerViewFactory(String viewId, dynamic cb) {} -} diff --git a/lib/widgets/real_ui.dart b/lib/widgets/real_ui.dart deleted file mode 100644 index c2b8ea23..00000000 --- a/lib/widgets/real_ui.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:ui' as ui; - -// ignore: camel_case_types -class platformViewRegistry { - static registerViewFactory(String viewId, dynamic cb) { - // ignore:undefined_prefixed_name - ui.platformViewRegistry.registerViewFactory(viewId, cb); - } -} diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 92dccc3a..9ca7ee80 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -519,7 +519,7 @@ class _ImageButtonState extends State { Future _pickImage(ImageSource source) async { final PickedFile pickedFile = await _picker.getImage(source: source); if (pickedFile == null) return null; - + final File file = File(pickedFile.path); if (file == null || widget.onImagePickCallback == null) return null; @@ -567,6 +567,7 @@ class _ImageButtonState extends State { } else { // User canceled the picker } + return null; } @override From 80c15fb2f4b1bbe8def01f7dcee15a34b9d94d65 Mon Sep 17 00:00:00 2001 From: Rishi Raj Singh <49035175+rish07@users.noreply.github.com> Date: Sat, 6 Mar 2021 13:34:56 +0530 Subject: [PATCH 051/306] Fix all issues (#60) * Remove conditional import * fix pickWeb * Fix all issues Co-authored-by: Xin Yao --- app/lib/pages/home_page.dart | 59 ++++++++++++++++++------------------ app/pubspec.lock | 23 +++++++++----- lib/widgets/editor.dart | 1 + pubspec.lock | 17 ++++++++--- pubspec.yaml | 14 +++++---- 5 files changed, 67 insertions(+), 47 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 665ce1b9..146c3088 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -95,38 +95,39 @@ class _HomePageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Container( - height: MediaQuery.of(context).size.height * 0.88, - color: Colors.white, - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: QuillEditor( - controller: _controller, - scrollController: ScrollController(), - scrollable: true, - focusNode: _focusNode, - autoFocus: false, - readOnly: false, - placeholder: 'Add content', - enableInteractiveSelection: true, - expands: false, - padding: EdgeInsets.zero, - customStyles: DefaultStyles( - h1: DefaultTextBlockStyle( - TextStyle( - fontSize: 32.0, - color: Colors.black, - height: 1.15, - fontWeight: FontWeight.w300, - ), - Tuple2(16.0, 0.0), - Tuple2(0.0, 0.0), - null), - sizeSmall: TextStyle(fontSize: 9.0), + Expanded( + flex: 20, + child: Container( + color: Colors.white, + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: QuillEditor( + controller: _controller, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: false, + readOnly: false, + placeholder: 'Add content', + enableInteractiveSelection: true, + expands: false, + padding: EdgeInsets.zero, + customStyles: DefaultStyles( + h1: DefaultTextBlockStyle( + TextStyle( + fontSize: 32.0, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w300, + ), + Tuple2(16.0, 0.0), + Tuple2(0.0, 0.0), + null), + sizeSmall: TextStyle(fontSize: 9.0), + ), ), ), ), - Padding( - padding: const EdgeInsets.all(8.0), + Expanded( child: Container( child: QuillToolbar.basic( controller: _controller, diff --git a/app/pubspec.lock b/app/pubspec.lock index 8b6e9a99..caad158c 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -110,14 +110,14 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.4.0-nullsafety.0" + version: "0.3.5" flutter_keyboard_visibility: dependency: transitive description: name: flutter_keyboard_visibility url: "https://pub.dartlang.org" source: hosted - version: "5.0.0-nullsafety.3" + version: "5.0.0" flutter_keyboard_visibility_platform_interface: dependency: transitive description: @@ -131,7 +131,7 @@ packages: name: flutter_keyboard_visibility_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-nullsafety.0" + version: "2.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -295,7 +295,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "2.1.5" quiver_hashcode: dependency: transitive description: @@ -342,7 +342,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.2.0-nullsafety.0" + version: "0.1.4" term_glyph: dependency: transitive description: @@ -363,7 +363,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-nullsafety.0" + version: "1.0.3" typed_data: dependency: transitive description: @@ -385,6 +385,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + universal_ui: + dependency: transitive + description: + name: universal_ui + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.8" url_launcher: dependency: transitive description: @@ -457,4 +464,6 @@ packages: version: "0.1.2" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.24.0-10.2.pre" + + flutter: ">=1.22.0" + diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 2e548474..e748373e 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -23,6 +23,7 @@ import 'package:flutter_quill/widgets/responsive_widget.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; import 'package:string_validator/string_validator.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; +import 'package:universal_ui/universal_ui.dart'; import 'package:url_launcher/url_launcher.dart'; import 'box.dart'; diff --git a/pubspec.lock b/pubspec.lock index 59f3c93b..a617457c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,7 +89,7 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.4.0-nullsafety.0" + version: "0.3.5" flutter_keyboard_visibility: dependency: "direct main" description: @@ -218,7 +218,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "2.1.5" quiver_hashcode: dependency: "direct main" description: @@ -265,7 +265,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.2.0-nullsafety.0" + version: "0.1.4" term_glyph: dependency: transitive description: @@ -286,7 +286,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-nullsafety.0" + version: "1.0.3" typed_data: dependency: transitive description: @@ -308,6 +308,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + universal_ui: + dependency: "direct main" + description: + name: universal_ui + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.8" url_launcher: dependency: "direct main" description: @@ -366,4 +373,4 @@ packages: version: "0.1.2" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.24.0-10.2.pre" + flutter: ">=1.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3e87a468..e185d838 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,15 +14,17 @@ dependencies: sdk: flutter quiver_hashcode: ^2.0.0 collection: ^1.15.0 - tuple: ^2.0.0-nullsafety.0 + tuple: ^1.0.3 url_launcher: ^6.0.0 - flutter_colorpicker: ^0.4.0-nullsafety.0 - image_picker: ^0.7.0 + flutter_colorpicker: ^0.3.5 + image_picker: ^0.7.2 photo_view: ^0.10.3 universal_html: ^1.2.4 - file_picker: ^3.0.0-nullsafety.4 - string_validator: ^0.2.0-nullsafety.0 - flutter_keyboard_visibility: ^5.0.0-nullsafety.2 + file_picker: ^3.0.0 + string_validator: ^0.1.4 + flutter_keyboard_visibility: ^5.0.0 + universal_ui: ^0.0.8 + dev_dependencies: From a6983d2ca6acecb6622db61ae22ed904be70c967 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 6 Mar 2021 00:06:37 -0800 Subject: [PATCH 052/306] Bump version: Fix static analysis errors --- CHANGELOG.md | 5 ++++- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5cb7dc..25a189a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,4 +117,7 @@ * Improve link handling for tel, mailto and etc. ## [1.0.0] -* Support flutter 2.0. \ No newline at end of file +* Support flutter 2.0. + +## [1.0.1] +* Fix static analysis errors. \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index e185d838..6945df09 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.0 +version: 1.0.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From c282f3c1c1c82c99038ba1c346e985ed0aca2c15 Mon Sep 17 00:00:00 2001 From: Rishi Raj Singh <49035175+rish07@users.noreply.github.com> Date: Sat, 6 Mar 2021 15:10:43 +0530 Subject: [PATCH 053/306] Fix Import issue (#61) * Remove conditional import * fix pickWeb * Fix all issues * fix import issue Co-authored-by: Xin Yao --- lib/widgets/editor.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index e748373e..a9f90d73 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -110,6 +110,7 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { } Widget _defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) { + var ui = UniversalUI(); switch (node.value.type) { case 'image': String imageUrl = node.value.data; From 94210c50de3779b9c2f344c1d47db723ef276db9 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 6 Mar 2021 01:50:01 -0800 Subject: [PATCH 054/306] Remove unused import --- app/pubspec.lock | 4 +--- lib/widgets/editor.dart | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/pubspec.lock b/app/pubspec.lock index caad158c..c45a35dd 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "1.0.0" + version: "1.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -464,6 +464,4 @@ packages: version: "0.1.2" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.22.0" - diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index a9f90d73..3ed4423f 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:io' as io; import 'dart:math' as math; -import 'dart:ui' as ui; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -110,15 +109,12 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { } Widget _defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) { - var ui = UniversalUI(); switch (node.value.type) { case 'image': String imageUrl = node.value.data; Size size = MediaQuery.of(context).size; - ui.platformViewRegistry.registerViewFactory( - imageUrl, - (int viewId) => html.ImageElement()..src = imageUrl, - ); + UniversalUI().platformViewRegistry.registerViewFactory( + imageUrl, (int viewId) => html.ImageElement()..src = imageUrl); return Padding( padding: EdgeInsets.only( right: ResponsiveWidget.isMediumScreen(context) From 9174208c1d92d5038db7e1fba7dff1ab685ba2a7 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 6 Mar 2021 02:10:17 -0800 Subject: [PATCH 055/306] Update toolbar in sample home page --- CHANGELOG.md | 5 ++++- app/lib/pages/home_page.dart | 10 ++++------ app/pubspec.lock | 2 +- pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25a189a1..fff75fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,4 +120,7 @@ * Support flutter 2.0. ## [1.0.1] -* Fix static analysis errors. \ No newline at end of file +* Fix static analysis errors. + +## [1.0.2] +* Update toolbar in sample home page. \ No newline at end of file diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 146c3088..c0b48222 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -127,12 +127,10 @@ class _HomePageState extends State { ), ), ), - Expanded( - child: Container( - child: QuillToolbar.basic( - controller: _controller, - onImagePickCallback: _onImagePickCallback), - ), + Container( + child: QuillToolbar.basic( + controller: _controller, + onImagePickCallback: _onImagePickCallback), ), ], ), diff --git a/app/pubspec.lock b/app/pubspec.lock index c45a35dd..87d40445 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "1.0.1" + version: "1.0.2" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 6945df09..b40f6f5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.1 +version: 1.0.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From 19f5b831f00eb6b43b2ff030c0c7b02d68c3cdff Mon Sep 17 00:00:00 2001 From: Rishi Raj Singh <49035175+rish07@users.noreply.github.com> Date: Sun, 7 Mar 2021 05:33:58 +0530 Subject: [PATCH 056/306] Fix toolbar height and change in drawer UI (#63) * fix import issues * Fix toolbar height and made UI of drawer better --- app/lib/pages/home_page.dart | 43 +++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index c0b48222..a6554e4b 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -64,7 +64,9 @@ class _HomePageState extends State { ), actions: [], ), - drawer: Material( + drawer: Container( + constraints: + BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7), color: Colors.grey.shade800, child: _buildMenuBar(context), ), @@ -96,7 +98,7 @@ class _HomePageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - flex: 20, + flex: 15, child: Container( color: Colors.white, padding: const EdgeInsets.only(left: 16.0, right: 16.0), @@ -127,10 +129,13 @@ class _HomePageState extends State { ), ), ), - Container( - child: QuillToolbar.basic( - controller: _controller, - onImagePickCallback: _onImagePickCallback), + Expanded( + child: Container( + padding: EdgeInsets.symmetric(vertical: 16, horizontal: 8), + child: QuillToolbar.basic( + controller: _controller, + onImagePickCallback: _onImagePickCallback), + ), ), ], ), @@ -149,15 +154,33 @@ class _HomePageState extends State { } Widget _buildMenuBar(BuildContext context) { - final itemStyle = TextStyle(color: Colors.white); - return ListView( + Size size = MediaQuery.of(context).size; + final itemStyle = TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ); + return Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ + Divider( + thickness: 2, + color: Colors.white, + indent: size.width * 0.1, + endIndent: size.width * 0.1, + ), ListTile( - title: Text('Read only demo', style: itemStyle), + title: Center(child: Text('Read only demo', style: itemStyle)), dense: true, visualDensity: VisualDensity.compact, onTap: _readOnly, - ) + ), + Divider( + thickness: 2, + color: Colors.white, + indent: size.width * 0.1, + endIndent: size.width * 0.1, + ), ], ); } From e8b2ef4b1c9510822def1c3dc8c870656ffb4dba Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 6 Mar 2021 23:49:21 -0800 Subject: [PATCH 057/306] Revert changes to home page --- app/lib/pages/home_page.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index a6554e4b..0ef5182d 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -129,13 +129,10 @@ class _HomePageState extends State { ), ), ), - Expanded( - child: Container( - padding: EdgeInsets.symmetric(vertical: 16, horizontal: 8), - child: QuillToolbar.basic( - controller: _controller, - onImagePickCallback: _onImagePickCallback), - ), + Container( + child: QuillToolbar.basic( + controller: _controller, + onImagePickCallback: _onImagePickCallback), ), ], ), From 766058b47332c68423de09b3a69c921351ef8dbb Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 7 Mar 2021 01:38:55 -0800 Subject: [PATCH 058/306] Upgrade version to 1.0.3 - Fix issue that text is not displayed while typing [WEB] --- CHANGELOG.md | 5 ++++- app/pubspec.lock | 2 +- lib/widgets/raw_editor.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fff75fbc..b71e9bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,4 +123,7 @@ * Fix static analysis errors. ## [1.0.2] -* Update toolbar in sample home page. \ No newline at end of file +* Update toolbar in sample home page. + +## [1.0.3] +* Fix issue that text is not displayed while typing [WEB]. \ No newline at end of file diff --git a/app/pubspec.lock b/app/pubspec.lock index 87d40445..4664ec7b 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "1.0.2" + version: "1.0.3" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 6b095a27..7389cab1 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -886,7 +886,7 @@ class RawEditorState extends EditorState } _didChangeTextEditingValue() { - if (_keyboardVisible) { + if (_keyboardVisible || kIsWeb) { _onChangeTextEditingValue(); } else { requestKeyboard(); diff --git a/pubspec.yaml b/pubspec.yaml index b40f6f5a..13b830a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.2 +version: 1.0.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From 5446a73b86056e925e9c81016a29556e4f03efb0 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 7 Mar 2021 01:47:24 -0800 Subject: [PATCH 059/306] Fix _didChangeTextEditingValue for web --- lib/widgets/raw_editor.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 7389cab1..2876c3fb 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -886,7 +886,13 @@ class RawEditorState extends EditorState } _didChangeTextEditingValue() { - if (_keyboardVisible || kIsWeb) { + if (kIsWeb) { + _onChangeTextEditingValue(); + requestKeyboard(); + return; + } + + if (_keyboardVisible) { _onChangeTextEditingValue(); } else { requestKeyboard(); From 4992367933a72a931c479a9e16813ae45944fb67 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 7 Mar 2021 10:54:52 -0800 Subject: [PATCH 060/306] Update CHANGELOG.md and upgrade photo_view to ^0.11.0 --- CHANGELOG.md | 170 ++++++++++++++++++++++++----------------------- app/pubspec.lock | 2 +- pubspec.yaml | 4 +- 3 files changed, 89 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b71e9bda..ddc59d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,129 +1,131 @@ -## [0.0.1] +## [1.0.4] +* Upgrade photo_view to ^0.11.0. -* Rich text editor based on Quill Delta. +## [1.0.3] +* Fix issue that text is not displayed while typing [WEB]. -## [0.0.2] -* Support image upload and launch url in read-only mode. +## [1.0.2] +* Update toolbar in sample home page. -## [0.0.3] -* Update home page meta data. +## [1.0.1] +* Fix static analysis errors. -## [0.0.4] -* Update example. +## [1.0.0] +* Support flutter 2.0. -## [0.0.5] -* Update example. +## [1.0.0-dev.2] +* Improve link handling for tel, mailto and etc. -## [0.0.6] -* More toolbar functionality. +## [1.0.0-dev.1] +* Upgrade prerelease SDK & Bump for master. -## [0.0.7] -* Handle multiple image inserts. +## [0.3.5] +* Fix for cursor focus issues when keyboard is on. -## [0.0.8] -* Fix launching url. +## [0.3.4] +* Improve link handling for tel, mailto and etc. -## [0.0.9] -* Handle rgba color. +## [0.3.3] +* More fix on cursor focus issue when keyboard is on. -## [0.1.0] -* Fix insert image. +## [0.3.2] +* Fix cursor focus issue when keyboard is on. -## [0.1.1] -* Fix cursor issue when undo. +## [0.3.1] +* cursor focus when keyboard is on. -## [0.1.2] -* Handle more text colors. +## [0.3.0] +* Line Height calculated based on font size. -## [0.1.3] -* Handle cursor position change when undo/redo. +## [0.2.12] +* Support placeholder. -## [0.1.4] -* Handle url with trailing spaces. +## [0.2.11] +* Fix static analysis error. -## [0.1.5] -* Support text alignment. +## [0.2.10] +* Update TextInputConfiguration autocorrect to true in stable branch. -## [0.1.6] -* Fix getExtentEndpointForSelection. +## [0.2.9] +* Update TextInputConfiguration autocorrect to true. -## [0.1.7] -* Support checked/unchecked list. +## [0.2.8] +* Support display local image besides network image in stable branch. -## [0.1.8] -* Support font and size attributes. +## [0.2.7] +* Support display local image besides network image. -## [0.2.0] -* Add checked/unchecked list button in toolbar. +## [0.2.6] +* Fix cursor after pasting. -## [0.2.1] -* Fix static analysis error. +## [0.2.5] +* Toggle text/background color button in toolbar. -## [0.2.2] -* Update git repo. +## [0.2.4] +* Support the use of custom icon size in toolbar. ## [0.2.3] * Support custom styles and image on local device storage without uploading. -## [0.2.4] -* Support the use of custom icon size in toolbar. +## [0.2.2] +* Update git repo. -## [0.2.5] -* Toggle text/background color button in toolbar. +## [0.2.1] +* Fix static analysis error. -## [0.2.6] -* Fix cursor after pasting. +## [0.2.0] +* Add checked/unchecked list button in toolbar. -## [0.2.7] -* Support display local image besides network image. +## [0.1.8] +* Support font and size attributes. -## [0.2.8] -* Support display local image besides network image in stable branch. +## [0.1.7] +* Support checked/unchecked list. -## [0.2.9] -* Update TextInputConfiguration autocorrect to true. +## [0.1.6] +* Fix getExtentEndpointForSelection. -## [0.2.10] -* Update TextInputConfiguration autocorrect to true in stable branch. +## [0.1.5] +* Support text alignment. -## [0.2.11] -* Fix static analysis error. +## [0.1.4] +* Handle url with trailing spaces. -## [0.2.12] -* Support placeholder. +## [0.1.3] +* Handle cursor position change when undo/redo. -## [0.3.0] -* Line Height calculated based on font size. +## [0.1.2] +* Handle more text colors. -## [0.3.1] -* cursor focus when keyboard is on. +## [0.1.1] +* Fix cursor issue when undo. -## [0.3.2] -* Fix cursor focus issue when keyboard is on. +## [0.1.0] +* Fix insert image. -## [0.3.3] -* More fix on cursor focus issue when keyboard is on. +## [0.0.9] +* Handle rgba color. -## [0.3.4] -* Improve link handling for tel, mailto and etc. +## [0.0.8] +* Fix launching url. -## [0.3.5] -* Fix for cursor focus issues when keyboard is on. +## [0.0.7] +* Handle multiple image inserts. -## [1.0.0-dev.1] -* Upgrade prerelease SDK & Bump for master. +## [0.0.6] +* More toolbar functionality. -## [1.0.0-dev.2] -* Improve link handling for tel, mailto and etc. +## [0.0.5] +* Update example. -## [1.0.0] -* Support flutter 2.0. +## [0.0.4] +* Update example. -## [1.0.1] -* Fix static analysis errors. +## [0.0.3] +* Update home page meta data. -## [1.0.2] -* Update toolbar in sample home page. +## [0.0.2] +* Support image upload and launch url in read-only mode. -## [1.0.3] -* Fix issue that text is not displayed while typing [WEB]. \ No newline at end of file +## [0.0.1] +* Rich text editor based on Quill Delta. \ No newline at end of file diff --git a/app/pubspec.lock b/app/pubspec.lock index 4664ec7b..d34813f5 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "1.0.3" + version: "1.0.4" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 13b830a9..105ec144 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.3 +version: 1.0.4 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git @@ -18,7 +18,7 @@ dependencies: url_launcher: ^6.0.0 flutter_colorpicker: ^0.3.5 image_picker: ^0.7.2 - photo_view: ^0.10.3 + photo_view: ^0.11.0 universal_html: ^1.2.4 file_picker: ^3.0.0 string_validator: ^0.1.4 From 6b7f7b4db634a4ef99fb2c0963334e80a32bf3bc Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 7 Mar 2021 10:58:04 -0800 Subject: [PATCH 061/306] Update home page --- app/lib/pages/home_page.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 0ef5182d..60a42a64 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/models/documents/attribute.dart'; @@ -129,11 +130,19 @@ class _HomePageState extends State { ), ), ), - Container( - child: QuillToolbar.basic( - controller: _controller, - onImagePickCallback: _onImagePickCallback), - ), + kIsWeb + ? Expanded( + child: Container( + padding: EdgeInsets.symmetric(vertical: 16, horizontal: 8), + child: QuillToolbar.basic( + controller: _controller, + onImagePickCallback: _onImagePickCallback), + )) + : Container( + child: QuillToolbar.basic( + controller: _controller, + onImagePickCallback: _onImagePickCallback), + ), ], ), ); From 59071021f5ec3535d3d19f5984d82901bc60563c Mon Sep 17 00:00:00 2001 From: FelixMittermeier <33794642+FelixMittermeier@users.noreply.github.com> Date: Tue, 9 Mar 2021 22:55:14 +0100 Subject: [PATCH 062/306] Bugfix: the "select, copy, paste" toolbar was still accessible even if the interactive selection has been disabled (#71) --- lib/widgets/editor.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 3ed4423f..6c355b22 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -275,10 +275,10 @@ class _QuillEditorState extends State widget.placeholder, widget.onLaunchUrl, ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, + copy: widget.enableInteractiveSelection ?? true, + cut: widget.enableInteractiveSelection ?? true, + paste: widget.enableInteractiveSelection ?? true, + selectAll: widget.enableInteractiveSelection ?? true, ), theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.android, From 9f4c1cf73f3c276486dcd8e65733494a53c076e1 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 9 Mar 2021 14:05:40 -0800 Subject: [PATCH 063/306] Update pbspec.lock --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index a617457c..7540db7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -204,7 +204,7 @@ packages: name: photo_view url: "https://pub.dartlang.org" source: hosted - version: "0.10.3" + version: "0.11.1" plugin_platform_interface: dependency: transitive description: From 17a38965c8753a8ce11656770d7da88161357ca8 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 10 Mar 2021 21:14:10 -0800 Subject: [PATCH 064/306] Update replaceText method --- lib/widgets/controller.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index abf2a059..2661b745 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -86,11 +86,12 @@ class QuillController extends ChangeNotifier { print('document.replace failed: $e'); throw e; } - if (delta != null && + final shouldRetainDelta = delta != null && toggledStyle.isNotEmpty && delta.isNotEmpty && delta.length <= 2 && - delta.last.isInsert) { + delta.last.isInsert; + if (shouldRetainDelta) { Delta retainDelta = Delta() ..retain(index) ..retain(data is String ? data.length : 1, toggledStyle.toJson()); From 14662c229d0d7bf39d0b3333419fb12b0e152bf8 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 10 Mar 2021 23:36:14 -0800 Subject: [PATCH 065/306] Bug fix: Can not insert newline when Bold is toggled ON (#75) * Bug fix: Can not insert newline when Bold is toggled ON * Fix --- lib/widgets/controller.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 2661b745..9c23d327 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -86,11 +86,22 @@ class QuillController extends ChangeNotifier { print('document.replace failed: $e'); throw e; } - final shouldRetainDelta = delta != null && + bool shouldRetainDelta = delta != null && toggledStyle.isNotEmpty && delta.isNotEmpty && delta.length <= 2 && delta.last.isInsert; + if (shouldRetainDelta && + toggledStyle.isNotEmpty && + delta.length == 2 && + delta.last.data == '\n') { + // if all attributes are inline, shouldRetainDelta should be false + final anyAttributeNotInline = + toggledStyle.values.any((attr) => !attr.isInline); + if (!anyAttributeNotInline) { + shouldRetainDelta = false; + } + } if (shouldRetainDelta) { Delta retainDelta = Delta() ..retain(index) From 2380c93b2b037f97e7bf4f661041b9079b36f387 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 10 Mar 2021 23:44:48 -0800 Subject: [PATCH 066/306] Upgrade version to 1.0.5 --- CHANGELOG.md | 3 +++ app/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddc59d68..b2d3b37b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.0.5] +* Bug fix: Can not insert newline when Bold is toggled ON. + ## [1.0.4] * Upgrade photo_view to ^0.11.0. diff --git a/app/pubspec.lock b/app/pubspec.lock index d34813f5..116d24bf 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -145,7 +145,7 @@ packages: path: ".." relative: true source: path - version: "1.0.4" + version: "1.0.5" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 105ec144..dc553796 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.4 +version: 1.0.5 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From cd47c76b6e44b5ef459ec528bd74262d28296bcb Mon Sep 17 00:00:00 2001 From: pengw00 Date: Thu, 11 Mar 2021 09:19:41 -0600 Subject: [PATCH 067/306] remove deprecated upper bound --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index dc553796..14f7123a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/singerdmx/flutter-quill.git environment: sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.17.0 <2.0.0" + flutter: ">=1.17.0" dependencies: flutter: From 4d0183936001363e84121d5317fbcf4d3fa09edd Mon Sep 17 00:00:00 2001 From: Jochen Date: Sat, 13 Mar 2021 16:15:20 +0800 Subject: [PATCH 068/306] desktop support (#79) Co-authored-by: jochen --- app/linux/.gitignore | 1 + app/linux/CMakeLists.txt | 106 ++++ app/linux/flutter/CMakeLists.txt | 91 +++ .../flutter/generated_plugin_registrant.cc | 13 + .../flutter/generated_plugin_registrant.h | 13 + app/linux/flutter/generated_plugins.cmake | 16 + app/linux/main.cc | 6 + app/linux/my_application.cc | 104 ++++ app/linux/my_application.h | 18 + app/macos/.gitignore | 6 + app/macos/Flutter/Flutter-Debug.xcconfig | 1 + app/macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 14 + app/macos/Runner.xcodeproj/project.pbxproj | 572 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 89 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + app/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 +++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 46993 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 3276 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 1429 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 5933 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1243 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 14800 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 1874 bytes app/macos/Runner/Base.lproj/MainMenu.xib | 339 +++++++++++ app/macos/Runner/Configs/AppInfo.xcconfig | 14 + app/macos/Runner/Configs/Debug.xcconfig | 2 + app/macos/Runner/Configs/Release.xcconfig | 2 + app/macos/Runner/Configs/Warnings.xcconfig | 13 + app/macos/Runner/DebugProfile.entitlements | 12 + app/macos/Runner/Info.plist | 32 + app/macos/Runner/MainFlutterWindow.swift | 15 + app/macos/Runner/Release.entitlements | 8 + app/windows/.gitignore | 17 + app/windows/CMakeLists.txt | 95 +++ app/windows/flutter/CMakeLists.txt | 103 ++++ .../flutter/generated_plugin_registrant.cc | 12 + .../flutter/generated_plugin_registrant.h | 13 + app/windows/flutter/generated_plugins.cmake | 16 + app/windows/runner/CMakeLists.txt | 18 + app/windows/runner/Runner.rc | 121 ++++ app/windows/runner/flutter_window.cpp | 64 ++ app/windows/runner/flutter_window.h | 39 ++ app/windows/runner/main.cpp | 42 ++ app/windows/runner/resource.h | 16 + app/windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes app/windows/runner/run_loop.cpp | 66 ++ app/windows/runner/run_loop.h | 40 ++ app/windows/runner/runner.exe.manifest | 20 + app/windows/runner/utils.cpp | 64 ++ app/windows/runner/utils.h | 19 + app/windows/runner/win32_window.cpp | 245 ++++++++ app/windows/runner/win32_window.h | 98 +++ lib/widgets/raw_editor.dart | 26 +- lib/widgets/toolbar.dart | 41 +- pubspec.lock | 99 ++- pubspec.yaml | 2 + 60 files changed, 2842 insertions(+), 22 deletions(-) create mode 100644 app/linux/.gitignore create mode 100644 app/linux/CMakeLists.txt create mode 100644 app/linux/flutter/CMakeLists.txt create mode 100644 app/linux/flutter/generated_plugin_registrant.cc create mode 100644 app/linux/flutter/generated_plugin_registrant.h create mode 100644 app/linux/flutter/generated_plugins.cmake create mode 100644 app/linux/main.cc create mode 100644 app/linux/my_application.cc create mode 100644 app/linux/my_application.h create mode 100644 app/macos/.gitignore create mode 100644 app/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 app/macos/Flutter/Flutter-Release.xcconfig create mode 100644 app/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 app/macos/Runner.xcodeproj/project.pbxproj create mode 100644 app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 app/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 app/macos/Runner/AppDelegate.swift create mode 100644 app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 app/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 app/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 app/macos/Runner/Configs/Debug.xcconfig create mode 100644 app/macos/Runner/Configs/Release.xcconfig create mode 100644 app/macos/Runner/Configs/Warnings.xcconfig create mode 100644 app/macos/Runner/DebugProfile.entitlements create mode 100644 app/macos/Runner/Info.plist create mode 100644 app/macos/Runner/MainFlutterWindow.swift create mode 100644 app/macos/Runner/Release.entitlements create mode 100644 app/windows/.gitignore create mode 100644 app/windows/CMakeLists.txt create mode 100644 app/windows/flutter/CMakeLists.txt create mode 100644 app/windows/flutter/generated_plugin_registrant.cc create mode 100644 app/windows/flutter/generated_plugin_registrant.h create mode 100644 app/windows/flutter/generated_plugins.cmake create mode 100644 app/windows/runner/CMakeLists.txt create mode 100644 app/windows/runner/Runner.rc create mode 100644 app/windows/runner/flutter_window.cpp create mode 100644 app/windows/runner/flutter_window.h create mode 100644 app/windows/runner/main.cpp create mode 100644 app/windows/runner/resource.h create mode 100644 app/windows/runner/resources/app_icon.ico create mode 100644 app/windows/runner/run_loop.cpp create mode 100644 app/windows/runner/run_loop.h create mode 100644 app/windows/runner/runner.exe.manifest create mode 100644 app/windows/runner/utils.cpp create mode 100644 app/windows/runner/utils.h create mode 100644 app/windows/runner/win32_window.cpp create mode 100644 app/windows/runner/win32_window.h diff --git a/app/linux/.gitignore b/app/linux/.gitignore new file mode 100644 index 00000000..c7ea17fc --- /dev/null +++ b/app/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/app/linux/CMakeLists.txt b/app/linux/CMakeLists.txt new file mode 100644 index 00000000..6ec85464 --- /dev/null +++ b/app/linux/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "app") +set(APPLICATION_ID "com.example.app") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/app/linux/flutter/CMakeLists.txt b/app/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..5cf73dd0 --- /dev/null +++ b/app/linux/flutter/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) +pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) +pkg_check_modules(LZMA REQUIRED IMPORTED_TARGET liblzma) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO + PkgConfig::BLKID + PkgConfig::LZMA +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + linux-x64 ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..026851fa --- /dev/null +++ b/app/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,13 @@ +// +// Generated file. Do not edit. +// + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/app/linux/flutter/generated_plugin_registrant.h b/app/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..9bf74789 --- /dev/null +++ b/app/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,13 @@ +// +// Generated file. Do not edit. +// + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/app/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..1fc8ed34 --- /dev/null +++ b/app/linux/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/app/linux/main.cc b/app/linux/main.cc new file mode 100644 index 00000000..4340ffc1 --- /dev/null +++ b/app/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/app/linux/my_application.cc b/app/linux/my_application.cc new file mode 100644 index 00000000..8ef02f26 --- /dev/null +++ b/app/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen *screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "app"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } + else { + gtk_window_set_title(window, "app"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar ***arguments, int *exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject *object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + nullptr)); +} diff --git a/app/linux/my_application.h b/app/linux/my_application.h new file mode 100644 index 00000000..8f20fb55 --- /dev/null +++ b/app/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/app/macos/.gitignore b/app/macos/.gitignore new file mode 100644 index 00000000..e146f77e --- /dev/null +++ b/app/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/app/macos/Flutter/Flutter-Debug.xcconfig b/app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..f022c34e --- /dev/null +++ b/app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/app/macos/Flutter/Flutter-Release.xcconfig b/app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..f022c34e --- /dev/null +++ b/app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..4618f386 --- /dev/null +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import path_provider_macos +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/app/macos/Runner.xcodeproj/project.pbxproj b/app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..9dfe210b --- /dev/null +++ b/app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* app.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..fc6bf807 --- /dev/null +++ b/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..a462e330 --- /dev/null +++ b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/macos/Runner.xcworkspace/contents.xcworkspacedata b/app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..59c6d394 --- /dev/null +++ b/app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..fc6bf807 --- /dev/null +++ b/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/macos/Runner/AppDelegate.swift b/app/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..553a135b --- /dev/null +++ b/app/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..8d4e7cb8 --- /dev/null +++ b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..3c4935a7ca84f0976aca34b7f2895d65fb94d1ea GIT binary patch literal 46993 zcmZ5|3p`X?`~OCwR3s6~xD(})N~M}fiXn6%NvKp3QYhuNN0*apqmfHdR7#ShNQ99j zQi+P9nwlXbmnktZ_WnO>bl&&<{m*;O=RK!cd#$zCdM@AR`#jH%+2~+BeX7b-48x|= zZLBt9*d+MZNtpCx_&asa{+CselLUV<<&ceQ5QfRjLjQDSL-t4eq}5znmIXDtfA|D+VRV$*2jxU)JopC)!37FtD<6L^&{ia zgVf1p(e;c3|HY;%uD5<-oSFkC2JRh- z&2RTL)HBG`)j5di8ys|$z_9LSm^22*uH-%MmUJs|nHKLHxy4xTmG+)JoA`BN7#6IN zK-ylvs+~KN#4NWaH~o5Wuwd@W?H@diExdcTl0!JJq9ZOA24b|-TkkeG=Q(pJw7O;i z`@q+n|@eeW7@ z&*NP+)wOyu^5oNJ=yi4~s_+N)#M|@8nfw=2#^BpML$~dJ6yu}2JNuq!)!;Uwxic(z zM@Wa-v|U{v|GX4;P+s#=_1PD7h<%8ey$kxVsS1xt&%8M}eOF98&Rx7W<)gY(fCdmo{y*FPC{My!t`i=PS1cdV7DD=3S1J?b2<5BevW7!rWJ%6Q?D9UljULd*7SxX05PP^5AklWu^y` z-m9&Oq-XNSRjd|)hZ44DK?3>G%kFHSJ8|ZXbAcRb`gH~jk}Iwkl$@lqg!vu)ihSl= zjhBh%%Hq|`Vm>T7+SYyf4bI-MgiBq4mZlZmsKv+S>p$uAOoNxPT)R6owU%t*#aV}B z5@)X8nhtaBhH=={w;Du=-S*xvcPz26EI!gt{(hf;TllHrvku`^8wMj7-9=By>n{b= zHzQ?Wn|y=;)XM#St@o%#8idxfc`!oVz@Lv_=y(t-kUC`W)c0H2TX}Lop4121;RHE(PPHKfe_e_@DoHiPbVP%JzNudGc$|EnIv`qww1F5HwF#@l(=V zyM!JQO>Rt_PTRF1hI|u^2Uo#w*rdF*LXJky0?|fhl4-M%zN_2RP#HFhSATE3&{sos zIE_?MdIn!sUH*vjs(teJ$7^7#|M_7m`T>r>qHw>TQh?yhhc8=TJk2B;KNXw3HhnQs za(Uaz2VwP;82rTy(T3FJNKA86Y7;L(K=~BW_Q=jjRh=-k_=wh-$`nY+#au+v^C4VV z)U?X(v-_#i=3bAylP1S*pM_y*DB z2fR!imng6Dk$>dl*K@AIj<~zw_f$T!-xLO8r{OkE(l?W#W<={460Y02*K#)O4xp?W zAN+isO}!*|mN7B#jUt&!KNyFOpUxv&ybM>jmkfn8z^llBslztv!!`TBEPwu;#eR3d z@_VDa)|ByvXx1V=^Up4{;M8ji3FC7gm(C7Ty-#1gs+U<{Ouc(iV67{< zam#KwvR&s=k4W<13`}DxzJ9{TUa97N-cgWkCDc+C339)EEnC@^HQK6OvKDSCvNz(S zOFAF_6omgG!+zaPC8fBO3kH8YVBx9_AoM?->pv~@$saf(Myo|e@onD`a=;kO*Utem ze=eUH&;JB2I4}?Pm@=VnE+yb$PD~sA5+)|iH3bi|s?ExIePeoAMd(Z4Z%$mCu{t;B9(sgdG~Q}0ShAwe!l8nw0tJn zJ+m?ogrgty$3=T&6+JJa!1oS3AtQQ1gJ z3gR1<=hXU>{SB-zq!okl4c+V9N;vo4{fyGeqtgBIt%TPC1P&k!pR-GZ7O8b}9=%>3 zQrV%FQdB+CcCRKK)0}v>U25rbQk(1^9Ax|WcAo5?L(H&H@%zAoT2RH$iN6boyXpsYqME}WJZI6T%OMlkWXK>R`^7AHG&31 z&MIU}igQ7$;)7AEm#dXA+!I&6ymb7n6D;F7c$tO3Ql(`ht z1sFrzIk_q5#=!#D(e~#SdWz5K;tPF*R883Yu>*@jTeOGUjQekw zM+7HlfP{y8p}jA9bLfyKC_Ti8k#;AVp@RML^9MQp-E+Ns-Y zKA!aAZV-sfm<23fy#@TZZlQVQxH%R7rD}00LxHPUF!Yg3%OX ziDe4m<4fp{7ivBS?*AlJz$~vw5m)Ei8`|+~xOSqJ$waA0+Yys$z$9iN9TIXu8 zaYacjd09uRAsU|)g|03w`F|b1Xg#K~*Mp2X^K^)r3P^juoc}-me&YhkW3#G|H<~jK zoKD?lE@jOw7>4cpKkh!8qU!bF(i~Oa8a!EGy-j46eZYbKUvF=^^nq`EtWFK}gwrsB zeu<6~?mk+;+$whP)8ud8vjqh+NofU+Nu`~|pb&CN1y_idxxf6cGbT=fBZR_hl&G)GgnW$*oDrN-zz;cKs18n+dAn95w z)Y>l6!5eYpebJGw7it~Q5m}8$7@%p&KS=VtydFj4HPJ{xqUVS_Ih}c(^4nUdwG|0% zw8Fnm{IT`8MqoL(1BNtu_#7alS@3WSUUOFT@U*`V!zrPIeCbbO=pE%|g92$EU|lw; z^;^AqMVWVf-R5^OI79TzIyYf}HX%0Y)=aYH;EKo}?=R~ZM&s&F;W>u%hFUfNafb;- z8OkmkK3k||J#3`xdLuMJAhj9oPI?Cjt}cDN7hw26n7irWS0hsy`fs&Y?Y&(QF*Nu! z!p`NggHXaBU6$P42LkqnKsPG@363DHYGXg{!|z6VMAQt??>FK1B4x4{j;iY8A+7o% z*!0qt&w+w#Ob@pQp;q)u0;v^9FlY=AK>2!qku)!%TO<^lNBr!6R8X)iXgXi^1p`T8 z6sU@Y_Fsp6E89E1*jz~Tm2kF=mjYz_q99r^v0h-l7SP6azzL%woM6!7>IFWyizrNwAqoia3nN0q343q zFztMPh0)?ugQg5Izbk{5$EGcMzt*|=S8ZFK%O&^YV@V;ZRL>f!iG?s5z{(*Xq20c^ z(hkk~PljBo%U`$q>mz!ir7chKlE-oHA2&0i@hn4O5scsI&nIWsM>sYg;Ph5IO~VpT z%c-3_{^N>4kECzk?2~Z@V|jWio&a&no;boiNxqXOpS;ph)gEDFJ6E=zPJ$>y5w`U0 z;h9_6ncIEY?#j1+IDUuixRg&(hw+QSSEmFi%_$ua$^K%(*jUynGU@FlvsyThxqMRw z7_ALpqTj~jOSu2_(@wc_Z?>X&(5jezB6w-@0X_34f&cZ=cA-t%#}>L7Q3QRx1$qyh zG>NF=Ts>)wA)fZIlk-kz%Xa;)SE(PLu(oEC8>9GUBgd$(^_(G6Y((Hi{fsV; zt*!IBWx_$5D4D&ezICAdtEU!WS3`YmC_?+o&1RDSfTbuOx<*v`G<2SP;5Q4TqFV&q zJL=90Lcm^TL7a9xck}XPMRnQ`l0%w-fi@bRI&c*VDj!W4nj=qaQd$2U?^9RTT{*qS_)Q9OL>s}2P3&da^Pf(*?> z#&2bt;Q7N2`P{{KH@>)Tf5&za?crRmQ%8xZi<9f=EV3={K zwMet=oA0-@`8F;u`8j-!8G~0TiH5yKemY+HU@Zw3``1nT>D ziK465-m?Nm^~@G@RW2xH&*C#PrvCWU)#M4jQ`I*>_^BZB_c!z5Wn9W&eCBE(oc1pw zmMr)iu74Xl5>pf&D7Ml>%uhpFGJGyj6Mx=t#`}Mt3tDZQDn~K`gp0d)P>>4{FGiP$sPK*ExVs!1)aGgAX z6eA;-9@@Muti3xYv$8U{?*NxlHxs?)(6%!Iw&&l79K86h+Z8;)m9+(zzX?cS zH*~)yk)X^H1?AfL!xctY-8T0G0Vh~kcP=8%Wg*zZxm*;eb)TEh&lGuNkqJib_}i;l z*35qQ@}I#v;EwCGM2phE1{=^T4gT63m`;UEf5x2Get-WSWmt6%T6NJM`|tk-~4<#HHwCXuduB4+vW!BywlH8murH@|32CNxx7} zAoF?Gu02vpSl|q1IFO0tNEvKwyH5V^3ZtEO(su1sIYOr{t@Tr-Ot@&N*enq;Je38} zOY+C1bZ?P~1=Qb%oStI-HcO#|WHrpgIDR0GY|t)QhhTg*pMA|%C~>;R4t_~H1J3!i zyvQeDi&|930wZlA$`Wa9)m(cB!lPKD>+Ag$5v-}9%87`|7mxoNbq7r^U!%%ctxiNS zM6pV6?m~jCQEKtF3vLnpag``|bx+eJ8h=(8b;R+8rzueQvXgFhAW*9y$!DgSJgJj% zWIm~}9(R6LdlXEg{Y3g_i7dP^98=-3qa z$*j&xC_$5btF!80{D&2*mp(`rNLAM$JhkB@3al3s=1k^Ud6HHontlcZw&y?`uPT#a za8$RD%e8!ph8Ow7kqI@_vd7lgRhkMvpzp@4XJ`9dA@+Xk1wYf`0Dk!hIrBxhnRR(_ z%jd(~x^oqA>r>`~!TEyhSyrwNA(i}={W+feUD^8XtX^7^Z#c7att{ot#q6B;;t~oq zct7WAa?UK0rj0yhRuY$7RPVoO29JV$o1Z|sJzG5<%;7pCu%L-deUon-X_wAtzY@_d z6S}&5xXBtsf8TZ13chR&vOMYs0F1?SJcvPn>SFe#+P3r=6=VIqcCU7<6-vxR*BZUm zO^DkE{(r8!e56)2U;+8jH4tuD2c(ptk0R{@wWK?%Wz?fJckr9vpIU27^UN*Q$}VyHWx)reWgmEls}t+2#Zm z_I5?+htcQl)}OTqF<`wht89>W*2f6e)-ewk^XU5!sW2A2VtaI=lggR&I z;Rw{xd)WMqw`VUPbhrx!!1Eg_*O0Si6t@ny)~X^Gu8wZZDockr)5)6tm+<=z+rYu? zCof+;!nq6r9MAfh zp4|^2w^-3vFK~{JFX|F5BIWecBJkkEuE%iP8AZ z^&e|C+VEH&i(4Y|oWPCa#C3T$129o5xaJa=y8f(!k&q+x=M|rq{?Zw_n?1X-bt&bP zD{*>Io`F4(i+5eE2oEo6iF}jNAZ52VN&Cp>LD{MyB=mCeiwP+v#gRvr%W)}?JBTMY z_hc2r8*SksC%(pp$KGmWSa|fx;r^9c;~Q(Jqw1%;$#azZf}#Fca9NZOh{*YxV9(1ivVA^2Wz>!A&Xvmm-~{y8n!^Jdl8c>`J#=2~!P{ zC1g_5Ye3={{fB`R%Q|%9<1p1;XmPo5lH5PHvX$bCIYzQhGqj7hZ?@P4M0^mkejD|H zVzARm7LRy|8`jSG^GpxRIs=aD>Y{Cb>^IwGEKCMd5LAoI;b{Q<-G}x*e>86R8dNAV z<@jb1q%@QQanW1S72kOQ$9_E#O?o}l{mHd=%Dl{WQcPio$baXZN!j{2m)TH1hfAp{ zM`EQ=4J`fMj4c&T+xKT!I0CfT^UpcgJK22vC962ulgV7FrUrII5!rx1;{@FMg(dIf zAC}stNqooiVol%%TegMuWnOkWKKA}hg6c)ssp~EnTUVUI98;a}_8UeTgT|<%G3J=n zKL;GzAhIQ_@$rDqqc1PljwpfUwiB)w!#cLAkgR_af;>}(BhnC9N zqL|q8-?jsO&Srv54TxVuJ=rfcX=C7{JNV zSmW@s0;$(#!hNuU0|YyXLs{9$_y2^fRmM&g#toh}!K8P}tlJvYyrs6yjTtHU>TB0} zNy9~t5F47ocE_+%V1(D!mKNBQc{bnrAbfPC2KO?qdnCv8DJzEBeDbW}gd!g2pyRyK`H6TVU^~K# z488@^*&{foHKthLu?AF6l-wEE&g1CTKV|hN7nP+KJnkd0sagHm&k{^SE-woW9^fYD z7y?g*jh+ELt;$OgP>Se3o#~w9qS}!%#vBvB?|I-;GM63oYrJ}HFRW6D+{54v@PN8K z2kG8`!VVc+DHl^8y#cevo4VCnTaPTzCB%*)sr&+=p{Hh#(MwaJbeuvvd!5fd67J_W za`oKxTR=mtM7P}i2qHG8=A(39l)_rHHKduDVA@^_Ueb7bq1A5#zHAi**|^H@fD`_W z#URdSG86hhQ#&S-Vf_8b`TIAmM55XhaHX7}Ci-^(ZDs*yb-WrWV&(oAQu3vMv%u$5 zc;!ADkeNBN_@47r!;%G3iFzo;?k)xTS-;1D-YeS5QXN7`p2PzGK~e6ib;8COBa5)p zfMn}dA--&A12~zr&GVk?qnBGfIEo`5yir;-Q;ZLn{Fimdrk;e!)q`sAkYh^~^>4Q@ zN5RT>s38+`V{|6@k&vZW!W0*BEqV&~34d+Ev8h)ObYL7Bd_hgbUzjdJaXP=S@Dp6X z)i013q3K4Gr5d%2YIp>218pYK!xwH;k)j?uUrT-yVKLg*L3y~=a+qd!RWGTL`z>29 z-Zb4Y{%pT%`R-iA#?T58c-i@?jf-Ckol9O>HAZPUxN%Z=<4ad9BL7n`_kH0i#E(m& zaNb039+z~ONUCLsf_a|x*&ptU?`=R*n}rm-tOdCDrS!@>>xBg)B3Sy8?x^e=U=i8< zy7H-^BPfM}$hf*d_`Qhk_V$dRYZw<)_mbC~gPPxf0$EeXhl-!(ZH3rkDnf`Nrf4$+ zh?jsRS+?Zc9Cx7Vzg?q53ffpp43po22^8i1Obih&$oBufMR;cT2bHlSZ#fDMZZr~u zXIfM5SRjBj4N1}#0Ez|lHjSPQoL&QiT4mZn=SxHJg~R`ZjP!+hJ?&~tf$N!spvKPi zfY;x~laI9X`&#i#Z}RJ`0+MO_j^3#3TQJu2r;A-maLD8xfI+2Y*iDf4LsQ$9xiu?~ z?^wHEf^qlgtjdj(u_(W5sbGx1;maVPDHvI-76u2uUywf;>()=e>0le;bO0LIvs)iy z*lJTO+7gyf^)2uS-PhS_O-+RToQmc6VT>ej^y^stNkwIxUg?E|YMAAwQ}U!dC&cXL ziXKU?zT~xbh6C};rICGbdX~;8Z%L~Jdg|`senVEJo-CiDsX47Kc`;EiXWO<9o)(`4 zGj(9@c+Me=F~y(HUehcAy!tkoM&e1y#(qqCkE(0lik_U>wg8vOhGR(=gBGFSbR`mh zn-%j3VTD4 zwA1Kqw!OSgi_v0;6?=Bk4Z{l-7Fl4`ZT535OC{73{rBwpNHMPH>((4G`sh zZhr!v{zM@4Q$5?8)Jm;v$A2v$Yp9qFG7y`9j7O-zhzC+7wr3Cb8sS$O{yOFOODdL) zV2pU{=nHne51{?^kh%a$WEro~o(rKQmM!p?#>5Pt`;!{0$2jkmVzsl|Nr^UF^IHxG z8?HmZEVMY~ec%Ow6hjfg6!9hCC4xY?V;5Ipo-myV=3TmfT^@XkKME`+=_inm4h7ki z->K~a+20?)zic^zc&7h=0)T{Aa24FU_}(O|9DMW3Bf>MW=O%~8{unFxp4}B+>>_KN zU%rKs3Va&&27&OX4-o&y2ie|sN2p-=S^V<2wa2NUQ4)?0e|hgna*1R7(#R_ys3xmG zE#(ry+q=O~&t|RX@ZMD`-)0QmE*x%SBc(Yvq60JtCQ4RL(gdA(@=}0rYo5yKz36bW zkvLOosP6I?7qH!rce(}q@cH-{oM2ThKV2RZe+{{25hkc?T>=Tky12xHr0jmfH@SZi zLHPJ@^Oo^Zo%`gZk_hrbCzS+t|=O!Bt zWi|>M8mz~sD|Z>C1ZPf_Cs&R!S5E2qK+@j*UpP>;5_|+h+y{gb=zub7#QKSUabet# zFH2H0ul;zO+uc+V=W_W@_Ig-791T7J9&=5)wrBE?JEHS_A6P~VQ)u6s1)Pu|VxP(aYJV*(e<)(42R zm3AK>dr1QLbC1RMoQ|M5k+TWBjY9q+_vY=K-tUte35m4RWl51A<4O0ptqV3)KzL7U z0gpp-I1)|zvtA8V7-e-o9H)lB_Rx6;Bu7A2yE)6)SuDqWDs}~Ojfk?DFwI% z3E1(>LbbB7I(&E@B7nlulhvY=Wa1mGXD@ijD7WF^y@L1e55h)-hzoq}eWe!fh9m3V{)x^6F8?ed1z>+4;qW6A4hYYj zZCYP=c#I8+$pAIVyiY*#%!j3ySAnH`tp|=^lh{)#JimWaP_rXK40A0WcsEUj`G1}O zG?XQ~qK4F!lqauv6-BL_Up3+-l1=kVfD;D*C)yr>o9>W=%mIyATtn_OBLK+h@p)j5jRAb;m&Ok?TZH-5Q)~#UwdYFp~rEE{judWa9E)z zE>135C-xMdHYY&AZGR)tb`K}s0CK9 z1!))p^ZaUC*e50t`sL+)@`)#kJ}?C_cCMH@k{f4wh~0`OFnGQ2nzUuuu;=r4BYRcI z){G#a6Y$S(mIc6B#YS;jFcU{0`c)Raa$nG+hV(K|2|^ZWOI566zlF0N;t~$jD<_AX zjnD?HN-G>xRmHwtL3BcJX7)Q^YGfc?cS4Nj=yYl5MB(uBD?r@VTB|mIYs=au$e)e{ zLHWd!+EN*v2*(=y%G1JzyQdY&%|?~R5NPb)`S2dw1AJW8O;L=p?yVxJs=X?U#-l1O zk6xh8yyY;OTR7aF{P=kQ>y`*EFivnw%rQioA-I67WS+~hVamG4_sI)(Jo4vHS|@F@ zqrBHbxHd_Y8+?8Gfq=Z1O^Fs5moGayCHVUHY^8)^j)Aj*RB!S2-FA?4#-`puwBW`` zJ_6OQj(FGo8DotHYRKq;;$4xDn9=4rgw}5xvxhi)?n?W5{*%4%h9Tg)zlQl&fN~Z1)gL(Dn7X!P428I zwA+U-x5!cQ57g1N=2bLqAWF z!&cbvsD)dvYoqP5vaQz%rL@kv*J>0AMzWAKn~Mxi5g2GlI7qvVZo)Z5oj=#O!M&*O z`3O3)uvrjNTeremC}nW@(m%#E-sITB>j-!yBM#(=FN`~c#@XjL3e)SjR9&%QO%tUg zzGv=SLH()`ZIt?Ayym;9VG1Muq+a+7Zo+59?SuRu_`k>@S4!yS3roMnq+SDO?`C7V#2 z8vHf4&0k;{kLT)fa==7EILSu3e|ZnxtFO;1 zGqP-;Xo(>_QKcYUhsi-X72BqH#7Zb-TsiNIF>G9xOHT3XoA*qX^10+#XCU0)UO4_%A_s_vO=uDd3_Q%D{OsvLMW9wGvuuRnF52{2vH06D~7N672!bIMt@it_D}& zwjZ7gV!RzZ86*wbEB5cnMJRbEqMM{G!K)bfJjyPH^9nGnrOI9S{~!dm4~P#&b*~)h zCMwM8mR+y5i~E5*JAopwZ>F`=ORfA&IF%O8(aS<}^H6wcY1g^=lYLPtFpyvW9F z3;FCS-TGFYPr#Y$ue>}?rTYrmWr^VbUu>!eL$cEdh1e>5_UDnZ@Mu$l*KVo_NDEu^ zBn*!qVnzYv>t|<(>nt8%CoNPhN!qGP|sANRN^#+2YSSYHa>R1mss->c0f=#g@U58@? zA4sUbrA7)&KrTddS0M6pTSRaz)wqUgsT3&8-0eG|d;ULOUztdaiD3~>!10H`rRHWY z1iNu6=UaA8LUBoaH9G*;m`Mzm6d1d+A#I8sdkl*zfvbmV0}+u` zDMv=HJJm?IOwbP;f~yn|AI_J7`~+5&bPq6Iv?ILo2kk$%vIlGsI0%nf1z9Mth8cy! zWumMn=RL1O9^~bVEFJ}QVvss?tHIwci#ldC`~&KFS~DU5K5zzneq_Q91T~%-SVU4S zJ6nVI5jeqfh~*2{AY#b(R*Ny95RQBGIp^fxDK{I9nG0uHCqc-Ib;pUUh$t0-4wX*< z=RzW~;iR3xfRnW<>5Jr5O1MP)brA3+ei@H8Hjkt7yuYIpd7c-4j%U=8vn8HD#TPJo zSe+7~Db}4U3Y^4dl1)4XuKZ67f(ZP;?TYg9te>hbAr4R_0K$oq3y5m-gb?fR$UtF9 zS~S^=aDyFSE}9W2;Okj%uoG-Um^&Qo^bB#!W?|%=6+P>``bumeA2E7ti7Aj%Fr~qm z2gbOY{WTyX$!s5_0jPGPQQ0#&zQ0Zj0=_74X8|(#FMzl`&9G_zX*j$NMf?i3M;FCU z6EUr4vnUOnZd`*)Uw#6yI!hSIXr%OF5H z5QlF8$-|yjc^Y89Qfl!Er_H$@khM6&N*VKjIZ15?&DB?);muI`r;7r0{mI03v9#31 z#4O*vNqb=1b}TjLY`&ww@u^SE{4ZiO=jOP3!|6cKUV2*@kI9Aw0ASwn-OAV~0843$1_FGl7}eF6C57dJb3grW)*jtoUd zpqXvfJSCIv4G*_@XZE?> z4Lt=jTSc*hG3`qVq!PVMR2~G-1P{%amYoIg!8Odf4~nv6wnEVrBt-R5Au=g~4=X|n zHRJGVd|$>4@y#w;g!wz>+z%x?XM^xY%iw%QoqY@`vSqg0c>n_}g^lrV))+9n$zGOP zs%d&JWT2Jjxaz`_V%XtANP$#kLLlW=OG2?!Q%#ThY#Sj}*XzMsYis2HiU2OlfeC>d z8n8j-{Npr1ri$Jv2E_QqKsbc$6vedBiugD~S`_0QjTTtX(mS}j6)6e;xdh*sp5U0aMpuN}qTP=^_Qn zh~0padPWs&aXmf6b~}{7Raglc)$~p?G89N4)&a}`izf|bA)IUmFLQ8UM$T!6siQxr z=%)pPsWYXWCNdGMS3fK6cxVuhp7>mug|>DVtxGd~O8v@NFz<+l`8^#e^KS3})bovWb^ zILp4a_9#%Y*b6m$VH8#)2NL@6a9|q!@#XOXyU-oAe)RR$Auj6?p2LEp*lD!KP{%(- z@5}`S$R)Kxf@m68b}Tr7eUTO=dh2wBjlx;PuO~gbbS2~9KK1szxbz$R|Frl8NqGn= z2RDp@$u5Obk&sxp!<;h=C=ZKPZB+jk zBxrCc_gxabNnh6Gl;RR6>Yt8c$vkv>_o@KDMFW1bM-3krWm|>RG>U`VedjCz2lAB1 zg(qb_C@Z~^cR=_BmGB@f;-Is3Z=*>wR2?r({x}qymVe?YnczkKG%k?McZ2v3OVpT* z(O$vnv}*Tle9WVK_@X@%tR^Z!3?FT_3s@jb3KBVf#)4!p~AFGgmn%1fBbZe3T53$_+UX_A!@Kz63qSLeH@8(augJDJ;RA>6rNxQYkd6t(sqK=*zv4j;O#N(%*2cdD z3FjN6`owjbF%UFbCO=haP<;Y1KozVgUy(nnnoV7{_l5OYK>DKEgy%~)Rjb0meL49X z7Fg;d!~;Wh63AcY--x{1XWn^J%DQMg*;dLKxs$;db`_0so$qO!>~yPDNd-CrdN!ea zMgHt24mD%(w>*7*z-@bNFaTJlz;N0SU4@J(zDH*@!0V00y{QfFTt>Vx7y5o2Mv9*( z1J#J27gHPEI3{!^cbKr^;T8 z{knt%bS@nrExJq1{mz2x~tc$Dm+yw=~vZD|A3q>d534za^{X9e7qF29H5yu};J)vlJkKq}< zXObu*@ioXGp!F=WVG3eUtfIA$GGgv0N?d&3C47`Zo)ms*qO}A9BAEke!nh#AfQ0d_ z&_N)E>5BsoR0rPqZb)YN}b~6Ppjyev;MMis-HkWF!az%G? z#&it84hv!%_Q>bnwch!nZKxB05M=jgiFaB^M=e-sj1xR?dPYUzZ#jua`ggyCAcWY> z-L$r#a{=;JP5X}9(ZPC&PdG~h5>_8SueX($_)Qu(;()N3*ZQH(VGnkWq^C}0r)~G3_?a10y*LsFz zokU5AKsW9DUr-ylK61shLS#4@vPcteK-Ga9xvRnPq=xSD_zC=Q_%6IuM?GpL(9aDx z|8d_;^6_D4{IQ1ndMAcFz5ZaT+Ww0wWN`xP(U#^=POs(BpKm;(H(lmYp+XCb7Kaw0 z;LT945Ev3IkhP6$lQBiMgr+vAL}{8xO&IObqJBEP4Y^x&V?iGC=1lVIbH^Z!eXxr@ zz)D7Fon`z~N|Pq>Bsue&_T9d;G+d8#@k^cq~F^I8ETsZ*cGOf*gZ4ghlAzW|aZ;WA13^B!Tlr0sWA zosgXD-%zvO-*GLU@hVV(bbQ`s@f~Ux=4}(@7O)%o5EH((gYflccBC@jbLF3IgPozv zglX2IL}kL1rtn4mu~`J(MMY83Rz6gc1}cX4RB+tZO2~;3FI# z@dU(xa5J_KvL0)oSkvwz9|!QcEA$jKR@a-4^SU3O449TrO+x$1fkBU<<=E_IHnF6> zPmZ7I2E+9A_>j6og$>Nih~b2F_^@6ef|Hm-K2(>`6ag{Vpd`g35n`yW|Jme78-cSy z2Jz7V#5=~u#0eLSh3U4uM3Smk31>xEh^-Os%&5tK6hSAX83jJi%5l!MmL4E?=FerNG#3lj^;-F1VISY!4E)__J~gY zP{o~Xo!8DW{5lsBFKL~OJiQoH>yBZ+b^};UL&UUs!Hbu7Gsf<9sLAsOPD4?-3CP{Q zIDu8jLk6(U3VQPyTP{Esf)1-trW5Mi#zfpgoc-!H>F$J#8uDRwDwOaohB(_I%SuHg zGP)11((V9rRAG>80NrW}d`=G(Kh>nzPa1M?sP;UNfGQaOMG1@_D0EMIWhIn#$u2_$ zlG-ED(PU+v<1Dd?q-O#bsA)LwrwL>q#_&75H)_X4sJK{n%SGvVsWH7@1QZqq|LM`l zDhX8m%Pe5`p1qR{^wuQ&>A+{{KWhXs<4RD< z=qU6)+btESL>kZWH8w}Q%=>NJTj=b%SKV3q%jSW>r*Qv1j$bX>}sQ%KO7Il zm?7>4%Q6Nk!2^z})Kchu%6lv-7i=rS26q7)-02q?2$yNt7Y={z<^<+wy6ja-_X6P4 zoqZ1PW#`qSqD4qH&UR57+z0-hm1lRO2-*(xN-42|%wl2i^h8I{d8lS+b=v9_>2C2> zz(-(%#s*fpe18pFi+EIHHeQvxJT*^HFj2QyP0cHJw?Kg+hC?21K&4>=jmwcu-dOqEs{%c+yaQ z2z6rB>nPdwuUR*j{BvM-)_XMd^S1U|6kOQ$rR`lHO3z~*QZ71(y(42g`csRZ1M@K7 zGeZ27hWA%v`&zQExDnc@cm9?ZO?$?0mWaO7E(Js|3_MAlXFB$^4#Zpo;x~xOEbay( zq=N;ZD9RVV7`dZNzz+p@YqH@dW*ij8g053Cbd=Mo!Ad8*L<5m1c4Kk ziuca5CyQ05z7gOMecqu!vU=y93p+$+;m=;s-(45taf_P(2%vER<8q3}actBuhfk)( zf7nccmO{8zL?N5oynmJM4T?8E))e;;+HfHZHr` zdK}~!JG}R#5Bk%M5FlTSPv}Eb9qs1r0ZH{tSk@I{KB|$|16@&`0h3m7S+)$k*3QbQ zasW2`9>hwc)dVNgx46{Io zZ}aJHHNf1?!K|P;>g7(>TefcLJk%!vM`gH8V3!b= z>YS+)1nw9U(G&;7;PV4eIl{=6DT^Vw<2Elnox;u@xF5ad*9Fo|yKgq<>*?C$jaG2j z|29>K)fI^U!v?55+kQ*d2#3}*libC4>Dl4 zIo3Jvsk?)edMnpH<|*l<*0Pf{2#KedIt>~-QiB{4+KEpSjUAYOhGDpn3H_N9$lxaP ztZwagSRY~x@81bqe^3fb;|_A7{FmMBvwHN*Xu006qKo{1i!RbN__2q!Q*A;U*g-Mz zg)-3FZ`VJdognZ~WrWW^2J$ArQAr1&jl~kWhn+osG5wAlE5W&V%GI{8iMQ!5lmV~# zeb3SKZ@?7p;?7{uviY6`Oz16t0=B70`im=`D@xJa16j2eHoCtElU*~7={YUzN41sE z#Th>DvJq-#UwEpJGKx;;wfDhShgO0cM|e!Ej){RX#~>a?)c2|7Hjhh2d=)VUVJL<^Aq|>_df4DX>b9W2$_DM zTjF#j(9?Co`yor?pK<16@{h#F&F8~1PG|qQNZPX^b!L*L&?PH#W8za0c~v6I2W($Jderl%4gufl z#s;C*7APQJP46xHqw;mUyKp3}W^hjJ-Dj>h%`^XS7WAab^C^aRu1?*vh-k2df&y9E z=0p*sn0<83UL4w30FqnZ0EvXCBIMVSY9Zf?H1%IrwQybOvn~4*NKYubcyVkBZ4F$z zkqcP*S>k6!_MiTKIdGlG+pfw>o{ni`;Z7pup#g z4tDx3Kl$)-msHd1r(YpVz7`VW=fx9{ zP}U8rJ-IP)m}~5t&0Y$~Quyjflm!-eXC?_LMGCkZtNDZf0?w<{f^zp&@U@sQxcPOZ zBbfQTFDWL_>HytC*QQG_=K7ZRbL!`q{m8IjE0cz(t`V0Ee}v!C74^!Fy~-~?@}rdn zABORRmgOLz8{r!anhFgghZc>0l7EpqWKU|tG$`VM=141@!EQ$=@Zmjc zTs`)!A&yNGY6WfKa?)h>zHn!)=Jd73@T^(m_j|Z;f?avJ{EOr~O~Q2gox6dkyY@%M zBU+#=T?P8tvGG|D5JTR}XXwjgbH(uwnW%W?9<-OQU9|6H{09v#+jmnxwaQ-V;q{v% zA8srmJX7Fn@7mr*ZQ@)haPjWVN@e3K z_`+@X$k*ocx*uF^_mTqJpwpuhBX~CSu=zPE(Sy%fYz&lzZmz3xo4~-xBBvU0Ao?;I-81*Z%8Do+*}pqg>bt^{w-`V6Sj>{Znj+ z70GS2evXinf|S#9=NNoXoS;$BTW*G0!xuTSZUY45yPE+~*&a-XC+3_YPqhd*&aQ>f z$oMUq^jjA;x#?iJKrpAqa<2<21h*_lx9a}VMib;a6c$~=PJOj6XJXJ|+rc7O7PEN5uE7!4n9nllo@BI4$VW2Nf_jqnkz%cvU4O4umV z#n6oXGWOt3tuIjmX*b!!$t~94@a@QgybLpQo3icAyU`iNbY~XNAArFAn$nFJ()d-U zFaO#nxxVF-%J{UB**uRo0*+?S>=^il)1m7v-u`PDy*ln%|3E-{3U~R=QcE&zhiG_c zDnGMgf1}3h1gWz8IV0Oc7FmEt>6W?Eva;J`(!;IIny}PvD?vztz`F6su_tUO`M%K5 z%C#=nXbX})#uE!zcq2mB;hPUVU1!`9^2K303XfOIVS{mlnMqJyt}FV=$&fgoquO+N zU6!gWoL%3N1kyrhd^3!u>?l6|cIl*t4$Z$=ihyzD7FFY~U~{RaZmfyO4+$kC7+m zo+-*f-VwpUjTi_Idyl~efx)!$GpE!h+in4G1WQkoUr<#2BtxLNn*2A>a-2BL#z%QO@w0v^{s=`*I6=ew2nUj1=mvi%^U@2#Wf& zs1@q6l8WqrqGm!)Yr|*``||#A+4#du6`mR^_#?CymIr}O!8Zm?(XY$u-RGH;?HFMGIEYVuA1& z`3RlG_y0%Mo5w@-_W$E&#>g6j5|y1)2$hg(6k<{&NsACgQQ0c8&8Tdth-{@srKE*I zAW64%AvJJ+Z-|I~8`+eWv&+k8vhdJk5%jolc%e`^%_vul0~U8t)>=bU&^ z6qXW&GDP%~1{L1-nKK>IsFgDJrh>!wr3?Vu-cmi#wn`;F`$GNc_>D|>RSuC8Vh21N z|G;J1%1YxwLZDD400Ggw+FirsoXVWYtOwg-srm}6woBb!8@OIc`P$!?kH>E55zbMB z8rdpODYfVmf>cF`1;>9N>Fl(Rov!pm=okW>I(GNJoNZ6jfIunKna-h6zXZPoZ9E2PythpyYk3HRN%xhq2c?gT$?4}Ybl42kip$QiA+ab zf-!EqBXkT1OLW>C4;|irG4sMfh;hYVSD_t6!MISn-IW)w#8kgY0cI>A`yl?j@x)hc z=wMU^=%71lcELG|Q-og8R{RC9cZ%6f7a#815zaPmyWPN*LS3co#vcvJ%G+>a3sYE`9Xc&ucfU0bB}c_3*W#V7btcG|iC>LctSZUfMOK zlIUt>NBmx6Ed}w_WQARG+9fLiRjS1;g49srN1Xi&DRd|r+zz*OPLWOu>M?V>@!i49 zPLZ3Q(99%(t|l%5=+9=t$slX0Pq(K@S`^n|MKTZL_Sj+DUZY?GU8sG=*6xu)k5V3v zd-flrufs*;j-rU9;qM zyJMlz(uBh0IkV<(HkUxJ747~|gDR6xFu?QvXn`Kr|IWY-Y!UsDCEqsE#Jp*RQpnc# z8y3RX%c2lY9D*aL!VS`xgQ^u0rvl#61yjg03CBER7-#t7Z++5h_4pw{ZZ~j0n_S_g zR=eVrlZDiH4y2}EZMq2(0#uU|XHnU!+}(H*l~J&)BUDN~&$ju@&a=s$tH5L`_wLeB z944k;)JIH^T9GEFlXiNJ6JRymqtLGZc?#Mqk2XIWMuGIt#z#*kJtnk+uS;Gp}zp$(O%LOC|U4ibw%ce-6>id$j5^y?wv zp1At~Sp7Fp_z24oIbOREU!Mji-M;a|15$#ZnBpa^h+HS&4TCU-ul0{^n1aPzkSi1i zuGcMSC@(3Ac6tdQ&TkMI|5n7(6P4(qUTCr)vt5F&iIj9_%tlb|fQ{DyVu!X(gn<3c zCN6?RwFjgCJ2EfV&6mjcfgKQ^rpUedLTsEu8z7=q;WsYb>)E}8qeLhxjhj9K**-Ti z9Z2A=gg+}6%r9HXF!Z~du|jPz&{zgWHpcE+j@p0WhyHpkA6`@q{wXl6g6rL5Z|j~G zbBS~X7QXr3Pq0$@mUH1Snk^1WJ0Fx2nTyCGkWKok$bJZV0*W?kjT|mkUpK<)_!_K^OoTjMc+CWc^~{ZP8vgm`f&=ppzKtw}cxwV^gppu}^df1|va7Q?@=(076-( z4KJVmu?l(aQwmQ*y_mke>YLW^^Rsj@diLY$uUBHL3yGMwNwb7OR3VD%%4tDW(nC984jBWCd90yY(GEdE8s(j>(uPfknLwh!i6*LX}@vvrRCG`c?EdB8uYU zqgsI4=akCeC+&iMNpVu56Fj2xZQHs6SdWssIF#Q@u@f9kab0&y*PlG+PynjHy`}GT zg%aTjRs2+7CknhTQKI%YZhFq1quSM{u24Oy2As@4g(bpbi%y1i0^TwI)%1Whpa~qE zX4MD(PgFEK@jZBPXkFd437aL6#COs$WrNT#U=er-X1FX{{v9!0AS$HR{!_u;zldwY zKko!`w2u@($c&k_3uLFE0Z*2vms?uw1A{AqZw^jwg$|D7jAY20j`s*l##=4Ne_K5) zOtu6_kziEF@vPsS7+@UwqOW6>OUwF$j{r4=nOSf-{UC(rEKidie7IUn>5`UoNJ9k) zxJXXEBQifng+Pte3mPQ76pVlZ<`jnI##F1*YFA*)ZCEncvgF-%)0dUXV*pXTT^L`n zL=?A5Vty#{R9W4K)m$`me~*_(&a88M?Eon$P-YdVG}#Gq4=hh#w=`>8f`9}}zhv;~ za?I=Gb3v$Ln?-SDTBow0J5Tt&xPlw|%`*VTyVee1Oh<-&;mA|;$ zoPl;^f7Q~}km#_#HT2|!;LEqORn%~KJaM)r#x_{PstSGOiZ!zX2c}^!ea3+HSWrwE z=6SJ!7sNDPdbVr#vnUf}hr&g@7_Yj&=sY=q(v^BwLKQm|oSB}172GpPlj?a3GqX#B zJko4zRRttIY>Fv#2b#A<_DLx=T@eUj+f}!u?p)hmN)u4(Jp(`9j58ze{&~rV?WVbP z%A=|J96mQjtD037%>=yk3lkF5EOIYwcE;uQ5J6wRfI^P3{9U$(b>BlcJF$2O;>-{+a1l4;FSlb z_LRpoy$L%S<&ATf#SE z;L?-lQlUDX_s&jz;Q1Lr@5>p_RPPReGnBNxgpD!5R#3)#thAI3ufgc^L)u%Rr+Hlb zT(pLDt%wP7<%z(utq=l%1M78jveI@T$dF#su(&>JkE(#=f4;D54l*%(-^(nfbCUQe)FV9non9F%K+KZ(4_`uOciy82CO)OolxisUd0m^cqueIRnY< z;BgA4S1&XC3uUP?U$}4o&r|0VCC7fkuMZBa|2n4asR>*5`zBaOJPWT$bNn(W_CK%L$c2AsfSlwq?A8Q6 zhK&USSV=^-4vZ^5<}pnAOb&IKseHNxv_!|B{g@d^&w%{?x;i3iSo)+vt^VnMmS!v) zM)W)05vXqzH5^hOWWw~$#&7HoIw}}DD3bCQgc=I8Rv|G5fM8O^58?--_-*>%Nwk)j zIfvfok0n05!w%tZ=-dpffezI7(+}yX5XhwYk#0@KW%PkR;%#t|P6Ze_K*N6ns%jOt zNeW(bRsv0BK7ah~9U~UBAVA_L34F+;14x6-;I|o=%>?sS3@dpRv|GKxilsa#7N#@! z!RX~>&JX&r{A^^>S~n_hPKkPR_(~~g>SuPj5Kx6VI%8BOa(Iit&xSMU8B#EY-Wr?9 zOaRPw0PEbVSW@Wk{8kkVn34;D1pV2mUXnXWp{V-M9+d}|qfb6F`!a9JQO_-wlH?zf z4Sn0F4-q-tzkaJ?1fV0+cJBF$f0g6*DL6U3y`Tr`1wzCiwY#muw7Q-Ki)uN}{MoCWP%tQ@~J4}tyr1^_bV9PScNKQHK=BZFV!`0gRe?mVxhcA4hW5?p0B<5oK+?vG^NM%B%NDOvu0FMq#)u&zt_-g&2 z7?z%~p&32OAUSQV{<=pc_j2^<;)`8$zxCEomh=rvMiliShS?ahdYI1grE-M&+qkK_ zD=5Hexi<&8qb4hgtgj81OD(tfX3EJSqy9KFcxpeBerG`apI4!#93xpEFT??vLt>kf zac28;86CpMu=BWIe$NOT~+Es!y#+$ zvm2s*c`J9Gy*ERvLSI<9<=j*O=0xUG>7rYh^R4bGsvz;j-SBO|P^OQ1>G9_akF}D; zlRmB@k3c5!s|Vz3OMZ8M*n0AMTiSt5ZpRy+R1|ckna&w`UQjklt9f&0Z~=->XImVA zLXizO2h=<|wM~w>%}3q1!E{oSq7LBPwQ~93p-peDq-W?wCm8NOKgTSz-P)|cm}S5&HBsx#C@Ba5;hzi#Yw@y-kC~)@u4}Rf?KV0$lPjv}} zcFpNy=YJfsS||9&!-JFjw=@NU96ESzU^gme0_oNy?})II`>Sy>bUCHs_(m&)vn^&isCl+`F~qu8elAO z)-ZP7`gYE2H(1)5tKalz&NJbcutAU&&JFV~$Jrai31^j>vZ|HV1f}#C1<5>F8 zS1RWIzM%b{@2dAF^$+i4p>TC8-weiLAPN+Aa#(bxXo9%Vz2NEkgF&s#_>V?YPye^_ z`` z-h3Cv^m6K%28I$e2i=cFdhZN?JTWhqJC{Q9mg0Vg|FiPEWDl&K)_;Bz_K`jH7W7QX^d$WQF*iF@#4_P*D36w9&iJr2E{w?LRFapwZIIVHGH ziTp*5>T{=;(E}z{1VL4;_H`BAXA~&zpeWX!gN9m|AfcJ{`!XVz48O^&+0Gd|w;udP zzU|DbGTS|7qZoEoDZEH9Kb0%DZvCaWDzuJ=8jZz}pqPn+I!c_+*~>m>BQqN2560*< z$6sx_y8WRqj$SugYGip+et$;iJ!SQAx=HgVSh_3e)MOFHuXD@sg>Yi_p8Sh`{lP=5 zo?AFv1h;KqR`Yj!8Pjji3lr+qae2|a1GmlxE*su%_V)K0Xu0(#2LcO!*k11w*V12$ z;f~i{kI#9PzvFLZ3pz@d558HeK2BTvk*JvS^J8L^_?q4q z);;4Z!DsV!P*M>F>FiF*{|p_nUgy;pDh?J8vwO;emgOAAcxrgDXiSDS5ag?0l*jj< z(khZ3-)>eiwPwpb6T9meeL)!2C-K@z9fF`0j|t@;^f5+dx86R3ZM{bnx9Hm1O$s)N zk$OvZR0u2`Z^QP8V%{8sEhW~_xbZMad2jtz&0+ekxmp;9`ae;_f%-ltk5E%)VT*a6 zRbMnpCLPnalu+1TafJ4M0xNV8g}U4Mjk{le6MA|0y0rk)is}M%Z9tUU22SvIAh7`w zTysd{Pztfkk=jD^*!lA+rBcqb)Fx`A5iaU2tl&XdL1D)U@pLEXdu%#YB*ol1N?4ti zHBQcU#_%UqiQ1)J^u-ovU@-7l?`YzYFvA2#tM0mEh3?CpyEh_NUuVajD16t zyg$C*5du9R=K~6mCJ`W+dFI$9WZZauO)p2H)*SKpHVsIu2CxfJvi2>; zcit#57RP7DpSwMF-VBm|4V5d=tRgX7RM9%KQ0JRo6d<)RmiIPWe2zh6tmswP`fs^) zwy};#jk|NXMqCSfwIR3QZ#W2`(%sJ>qvk=53CYoLmQt9q|2Gm$sB;rEuBqGJA1OUM zoyl4Wy-HYn0J6L=cad8o)R!Ea^;`rSMg9hYo3?Fw6B9dUq75a-MSb56n8~AAsS(JP zZ!1khPu}!GRpsj+jvl`N1tDD8m1myJCI3c-c<9U-1Vg`xJO~}5_wvPXYh^=Boo^|V z3Tp}|lH!9m4Ipa_$p;b8fjUd=zc4iO7vr)M&Xs0_m$fgY@+hB9%K~4*9$p0d)m2bO ze5JH`W0fnIKdcW!oO#^g1YceSQ4u->{>u@>tLi!fky)o&$h(=he?Fe_6?}O~iSf(F zV&(P~*5h>BW{3e1H%8*7#_%L1#>W97b0@jHtliES^w6w5oldI7QL+?I(Pl$DaN>~d5nXx z;CO1E+S?3E2PLq~)-?ygkHAO1m&hOYmj7?;2XM!$D^f0l9K4P{n}mgb{CoYH6RJ8o ztydc6dNqA)`CG?=Gd~EIbi`UM)eyzGF^+i?&TOdyW~mFH_^Gye(D}clDVFQ@V2Tvy z7rQIaq8Xx`kC;AO-_{k%VI2e6X@bIy^mupEX%{u0=KDUGu~r6lS*7GOeppy{&I&Ly zjOTz=9~jC|qWXznRbrfjg!1`cE!Hzyjzw6l{%>X)TK(UEGi9Uy3f9D6bbn0gT-s`< z8%$Msh!^8WidX7S;)n2jh_n1-QCtSyOAKcPQc(Xlf0*Q|5CSBjo(I-u!R0GJgzTkL z|6QdQRrUMbUO|q0dQ%+d^4)*Mjbm$R}RUcz(7|E0Bq-bAYY@)OsM<+2>}CV zzPBgeD~kBHE(Y+@l2orJrdtV7XXq_V8IETas%7OCYo`oi)+h&v#YN!Qpp7drXFS>6 z?r-q7px+(rIy+bo1uU#I2A5s@ASe01FgGMbouFkhbkm-9yZ8Q2@Q1vuhDQ3D3L+zA z(uz8^rc24VmE5r0Gbd;yOrXnQKAEBfa3@T7fcF$#QYv^00)VZPYehpSc@?^8we}o{ zlX0~o_I<`xSfI8xF(WXO-DX1>wJ`XN?4rw@}_RLD*${$}UaXL=oM(=SDMIxZj1Ji#jAcrH7nYG`r z#ewodj>F5Bf9j(j`a;>)=*2j_ZN}vf!~Hq`2Eyt;9UH1_(yjq1OUO(1M0lI3FZ2j-fU9)L59v&OiQ>5$;d!jg?Fo{Svf5t5FCZbb?)* zJN=Q!?2BztV$7)CWtG0MO~Lr4E5>aoHD5N4(+@~gQEbZTc4s3HrIl_G23PCng4Y3f zbLZK1A-x9x!)WwuI=UBkQ5QyE^&Nrw?@fsRKK41G9-xq=#VyO%CEo`{_eioDj%M!3x=>I zfOPFiFX{1t-|+3E@?UuK=0miGN04hW0=JnJrEyWw{Bg-jMvAA}cg<5LN1c5BQdrIZ z#+bxj9Jbu`11@IUjU|RKfL(UzRlVB4XT ze|(WaxL$KiRqkgCr3^Al(19!_Y7b=E(4Xm7LCO$y5+k;Fu6B#=OSzW`-7p{zRv-_) zPr!|km?8aF}+3hm)QG92YaI+jctX&5IrvTUGf{Y$)TK6)s9v!SMhU=HIpEC~2 z4>o14mG$El2sTA(Ct?xS!l*x7^)oo}|3+BF8QNe;bBHcqdHVmb?#cbS*NqZ%mYS~z z`KLoq7B#KULt%9a#DE%VTEo4TV03T2nr`FK5jUTA$FP0JH6F9oD*|0z1Yf2b5?H0_ zD|K|_5Zk`uu?ZN0U! z_mL>>F;mnHU=@to!Vv*s4;TQr9y)L@1BXXz^a85NSifPTL4h6I>+m_S3~FkXB{N?E zS<3ue_(wqaIS5;4e9{HB`Okl9Y}iFiju+oTqb)BY)QT?~3Oag7nGu-NB5VCOFsiRs zs@m%Ruwl^FuJ1b}g^=*_R?=SYJQ@7o>c9j>)1HgB zyN9LI9ifwu{Shlb6QO2#MWhxq~IG!U^I!6%5}(sbi>=bq8!8@s;4Iaun#kvh7NPwX34Rjbp2f!D)cF&sNIO%9~;C`cs&ZY2=d@c3PpN$YZjUT}X7rY`dlWX$yc znw(7=fzWapI=KzQnJ(6!o0K_aDk!^dZ#)pSTif+jQtQXga$bPApM z=);jZ5c*?*GoeGMnV0=RrZucRRYBjx>tx`A3OuY)#tp2w7mh}&kj)SKoAvbbf;uO! z?+RItUow0xc*6StuO4D--+qY!o}Isy}s;ts5aM5X~eJUZoLOq@dGv=a4hHJD<* z5q{dZSN{bv_(Vj#pFm7Q<$C;MwL|Qizm~QCFx~xQyJoCOZ$`sYD}}q>PwRZjb<=E< zAeMP?qVfM>xu2}Il2xT6={KBdDIstxY-`5IWXN zUiWV&Oiy5R_=2X9Y$ug9Ee=ZSCaza!>dWBMYWrq7uqp>25`btLn^@ydwz?+v?-?2V z?yVwD=rAO!JEABUU1hQ|cY+_OZ14Hb-Ef`qemxp+ZSK?Z;r!gDkJ}&ayJBx+7>#~^ zTm<>LzxR^t-P;1x3$h;-xzQgveY$^C28?jNM6@8$uJiY81sCwNi~+F=78qJZ@bIsz1CO! zgtPM~p6kaCR~-M>zpRCpQI}kUfaiZS`ez6%P6%*!$YCfF=sn}dg!593GFRw>OV2nQ ztTF6uB&}1J`r>gJuBP(z%KW{I^Uz%(^r5#$SK~%w1agl)Gg9Zy9fSK0kyLE24Z(34 zYtihZMQO^*=eY=<5R6LztHaB1AcuIrXoFuQ=7&C}L{c?Z$rto$%n=!whqoqG>#vvC z2%J5LVkU%Ta8hoM($p1WqN}wurA!d@#mQGU5Nb>~#XC84EYH)Zf&DZR!uY+-;VqS< z@q?$ggdX#auS#%%%oS^EN)?JhSR4JYpSgGRQZD<9!YvvF+zp0>C#$!x*x}l8U|Bb& zv?v*im5Bq_(5Wi40b1^nKun$XTST(a8yOAcqQZmKTgGLo)Ig6JuEh5J9NnqJXin@Gxzz-k6xXWYJ&@=JZw=$+ zFPGde%HsR`gI+y`rtiPaMYwbtyp!sVb!pX~;c3zLoPO0eaZSV+O_z z%9H@UhqNowzBTPcMfL6kC>LRaFF6KVaSv1R@%4}rtleX!EMnL`rethYrhTLj1x$tj z;)H!fKo08&T(;i|FT&rPgZ*D0d=B2dXuO_(Uaoi9+vEhs4%{AD{Fl@4^|`X=PvH(s zI7$6bWJiWndP$;&!kSCIR1l57F2?yzmZm~lA5%JKVb;1rQwj*O=^WW~`+n*+fQkK0 zydInOU1Be2`jhA!rnk1iRWR=1SOZpzFoU5{OPpc&A#j6Oc?D&>fAw=>x@H7?SN;d^ z-o&}WR;E|OR`QKItu(y4mT)%Pgqju-3uyH?Y@5>oSLO2Y(0(P!?_xOL=@5+R7rWw# z3J8%Hb@%Pzf^`=J6fEJ_aG6+e7>OUnhaO1(R1<6>f}L z?d@Wnqw9?^;2?q(b@?Wd=T6r_8a@Z4)*_@Q7A`+ zW3w?j!HW0KbhxF%D`9d2HpvIrBxM!36W3Yh5=8_0qYfnHm*yiLB?Ay|V10N%F9XYq zanaDtDk$rS+|_H_r|a${C}C7b{E)Ii20-a?Grff$E?&|gWF<#Ern2GqhCiS0~Y%knIi8zY^lE4qLaR-3M;_Rkz(s;wu z9207W1PXIe#4h4Zw}dvdV&FYcnUlD5_C4hzJ@bPSBVBLpl$&52mi+wwH;svyVIzAB zoA+NQ;Hpqh?A}^Et~xhl>YQNQwh20!muW{ zq}|Pg3jHZWnDBN?r1KhiVG$%Sm-4+=Q2MZzlNr3{#Abqb9j}KK%sHZj{Vr2y4~GIQ zA3Mz1DjQ3q(CC~OyCaZn0M2!){)S!!L~t>-wA&%01?-*H5?nzW?LJB`{r&)vLB4!K zrSm({8SeZ0w(bL9%ZZAZ*^jf=8mAjK^ZR0q9004|3%73z#`-Npqx*X^Ozbja!C1MW z-M~84#=rU1r>p{+h9JU<#K_x$eWqJ+aP%e?7KTSK&1>dlxwhQmkr69uG~0iD@y|L- zlY0vSR2|IhZoS6PpfUai_AhKo2HfdD&mhv#k51CX;T z*sU)XbDyfKjxYC$*_^(U)2-c0>GJ(zVm$CihHKlFSw&1A$mq$vsRt-!$jJe3GTaZ6 z3GcVvmwZ0D>`U+f3i*pQ>${p1UeyF~G9g~g-n{ThVOuC#9=ok`Zgz@qKCSN!1&P`N z=pdlGNwal%9;)ujwWH*#K6CQG*fJDAQiKlO2vKJHeA1lj&WQC+VU^@ea8$#~UOX$*Q!V^8L- zL0$W5(Y3=??%&j_WUq6*x>=?BfmI*d8fmDF*-!XVvxL8p7$r+}Igd_(&`|D*;Z#GE zqm{tHx&aHBpXw&~l6>7-FlyiSPJtTJblAjLU5Ho$FeN0mDguFAq?r+6^~o6|b+rfE zGVcZ&O-X~tE3liGcdI~hHSCT+&F&uH8rr&f{6pr^1y5061`fu~=^_|Idrgti5+*U7 zQOb9G?Rz$j-G0Y}x+i{HB0!4ZmKzykB<0;Rbmo2)T4|VdcwujI_otLG@@8OOKg3kw zP|0ST0D4@zT?O=(0Pikp)Rpwxw_VsmW4!^j^sFd6r5l zw}SG_HQPs>ae%Bq{sye_SaBX%|F-}&^)Wz@Xi<)YNbO?lPs7z@3c;$b^Aw@>E%mOj zW^c%IdtC(Kk@s*}9NbKxEf8SZtP+32ZTxjnrNWS7;W&D~ft{QY?oqOmxlV7JP!kW!Yj`Ur{QbbM1h=0KMaIAmWiISb7TKd4=gMeo+Tcz2>e#NihnOV%iNdx` zeiuoOK^{}D+M+p(Y7EC=&-`$B0F< zQ=zHaM;&QQR4jM$sG=N&sqOvD_Bx*drQ6c@u0()g05cwl`Xm{!S_Nuaa2KlL*rmmk z51yPE)q?Bl$sNM474Y!=zZ zc{EVGpdJ!Su{Qq%llR5O6#zK8l(ld*UVl87@|iaH@C3+*;XBxjEg&fsQrzpMo3EEG zv*Tpms7a;7!|iz8WY7={0a$0ItO-(ajXl;wX_$$yzEF5k9nc>L3wv!p{8h2)G0W?h z{v6vH=7+>$Ho^+)9hDtCd+S_yh8pzS9$)hYev-=eDu?lGIR;-fgz+dr+wcmM-^dZp z9}`&kAf$~z1ovF)>Hgxc!Xe3cju-jQRluCm;c_1=PYQygb?Oxe z!QG0L3sT_k=WpfOPL#|EPlD^t;ENCC39O?tHd<(kfx7SOcxl+E#;ff19_+{vbkZSvbS$I{#>31KZj^$n%ayX0jj}EvsgnHg16P z_A6Y)pdp>kLW<;PtR*Vs#mVb%)ao7AXw{O&hBDmD;?mc3iMH;Ac@rZZ_BQa8CQ~|0 z&d1L{in-z--lBO|pxqc%bqy^~LAGv=E*eaVU~OeuVV{d`Vv#-_W7EYdTDzVraG9H+LC_dWcgZMn~KcP)XvKWbcr5&d+=a>{*(Ha6Y1$==bR z{O-?$7H;`2dt0B%Vm?6`_?ZOjJkyu9ZJsh^WH*+es&^@KDcR%Zej%3PJ*XovgyhTbaH(!H1H_OF~=*f55Jr8A%uW zz5IoAB~1e2-tDGp9}`MnavAMy?jgPM5F%y`%$}dFLrz_* zIrO=afT8+AkK5B1s3{ZDVP$g6y$-*U*=?-fh!cNyn3q6YhNhfRxW&GLIJ2#>9bYMD7-F%{|Iw%@a=DoAAU;3k9p$`V zImKm{5HU~wq|nQFwab)_7lNckW#1z2$|oW5x7vDbBURVjw8674P?L1ogMKpHoV>;# zO%*1OwI|($UOr#hL(*M~qsn3PF%_|15uc%Hy9@D>_~N|?<%lig6yKX0a#1s$o(^Laj8bF#5fGPOFMGmMiUaxSwE}Qf#SG_f79d2Iv=TFBXzTpr$^avJ?=|arh2<+ce}&248Kw0} zhlva`wD6X~s7|37la4FnFOgIHhBiFo`lw~?lSbk{>)P(3jyVhM4O)a=GX3(sW1vIC zz0mJ>;J{!eN5#nf2>$u=3Kq>`7u9QnChi8>CjONBN-b+W_UQIuN#{N$Q<$}IOvpQP zB&5ZrY{V&D=4)voh;6<1U`PFA>V%XUW73S9D^J>cQYfzIyIV5i35WNb5K9c^|M}=* zN_C3rnjCZP1^v{;EaGK7Tp5z~B#?f5NZaAsFUOLK)mI~bJTaL8DF_eRikE{%^J?y9-n_U32EKHPCkB^ZN2*zk{bC=GM%_I z61}nkr+Plg6S0V=mY>H_KQU&)P~=y3$#$*U8FunXkb_e1O-7t@m$5re%u!_G%^?_| zRIJzg+lX$}+ba|qx)Ec6c^ip;`_QfQrD~SPa4MoyRUOtX&~^XWcO^a}KBkXK9J{ZFOA~rovYa0!7btTC*=xNQrwJ)$Eu`TT$;%V&2@y@$ISdNn ztbM7|nO+U9r;ae{{;QiNEYpe4nrFq_x3 z4Tvf^b(I@_3odwhVe!aC0X&~inrYFu# zh)+eF__8ly&nLr4KlLWl%B_ZMo=zCH2QfO^$lJ zBvU*LQ#M(5HQ}2Z9_^y~i@C#h)1C*?N3v68pY+7DD09nxowdG#_AAM5z&*|-9NcB{ z_xKUY>Ya7>TO#Bat}yM}o(~8Ck^!QHnIj8N9}c*uyIs}IEqGn`xP;q3vhW6gsqUe>`m1 z)~ad@y1=?H`1SNl?ANCs5ZD`8tG&Hi=j|R%pP(%gB8pd)Q--E?hWU@)e?>SLV4s(- z!_I^oVC0x97@I(;cnEm$ttKBnI3gXE>>`K?vAq~SK?0YSBsx{@s1ZdiKfFb|zf}ju z7@rJb3mC{U`$R`YS(Z#KyxQx_*nU`kf;}QL%bw17%5~6!mMao^-{FFmX}|ItFuR~F zAAvTF%f4XKYo>2-PJ~ro@Ly#t@Sf69CrA+rmMRpihqH7V&SXX+$Sw`HZF`I*_3Vjz z%kPMyN0J3sl>X{-h12)j&XRhAAI;Aou%%z}gI>G+32z*qpZg{m`CezFrzg#&yc<1` z%j~}PN!F5Ddq(>R{+t0v{j6v^0XwWGu@5+`-$m`_>pCzM`r}wz*8Qv=$|P0R$%tJp z>D+N4GZ|Tg>XL<6XP9_wQRGDs^1icY*5GP4>*7mGMr;V zI%kT_^_SQml6$#uRE4Ps>}?ES)_XI8m-%GN{o^itb^S7e_bM$-wo_Ws)W? zx4_6#*X;T$n2N==N0#xzb~BQU#%^NF6|~898JGDbQxjK(ex;Q}_Qn@?Y>!kkUYUeY z&VclG1#eDPU78K@^p3tAUvZi1(nFfk6AAVHWt)Wbi7dPbjA4isOY~?*1&asp!wg#Q zSpSI6*!TGn3|-%vuJE<9V_1EKkz_0%z}Mb7;E!uz)+0^k;@x+<5tzj5 z!InbRtc`YwNCbCac{plY&Y}hWp#PC{o@5UsBj#tv3f^ns^`;$MVN?>q!pW+MYeC7= zkWr1kAX(0xVQ<{qny&CO*|g1{Mk_yE>1t}_YT<5#p8P7QXf;o|s>XQ#SoA&!ddE+8 zOM&VsxsRGS(Spli?P$^pK7Ty{v86RP_6h|MU^J z`J>vn0|BG3Vf!uR0zM|GwtiTPZNb;a@@1+V5+$P4GI_&$%6m!YRGL=lz5kh?z#5f55 z76COi1`R(5p69;ThuQnJ$R3w?I?jigai2arApagd=^tT~oMUWp^u|H_@zXBjpI)Dv zEFc^_`mVu5U*;ClT?x-t9{#fto_+92GF^dotz0sFWTDwZ`s40AY@mv+Qh5c-Ts8Zp z!(v7!zPvFhUZ-xkR!IvaW`{PqN|k)L4*anbtmK+UU&K*awl?DhxRalbtmDw`$#VzK zYFaG}?$F)1j`Qx7wbn|XzMJ&g@3Ai#u5M?%CLPghk;lD^)-|21{Sr+M(suBU4}6CMTMxc_tD;X;z<1-{FeHte=kh1B9O6Hl z!v2i$d1VFC&z&58zU0`G#7^K3Cs@9LYN16O%Vz)?-iQL!G6&sg6aaX>DBZmm@lFrRJpcL{K3(;+`$9GDFDw62Mud@LZjabzVC=w$dx>TQa}U z-{dhKYTYx*C=Fio`ez@wrzx+p%Fk3i&v?6ENXMb3p^?;_&huLLueDwr zpRqHbU%i;9TmexFxCS8F1rPo-ea3!}!ew7{(($76Rdnfa`~$9{8H@f7U&0&HjZ3TZ zuBc||%FljS_e&wNZ$1ezT$*})XAfm??$_cY_?13vM^tT0EKY2ptb+v5P10}a%aTk_ zh8@_T{ns2@jTFhv`)-Vxh}u(0DiL0MUi(We_eic$;gCoqj(T_S{jDo^PahnKJUp3@ zMOk+%weP*c%K6VFXR2icY`J~-&fVMYUg6fsFI->jlA|9`+07y~$Fsz}^;w;mNk$ms zu?y)VA@QH__tvYDudhEWuDD20H&uvrf_boY{($?5{s-SDjyRxSC%%2Xs5d2dpjdk$ zU*NURD#ovwIfd^H{fXR@UuaooJtQr7$d0+(K+1UEwtG9_T?sb$ExV$e-bpf}a@YUe zuzInI59w!x;<)>Be;a7ukLW>V=8~J6nKU<0@H+SQ!Be;1Za_pw#hiuW_PMPBo8W2G z*WDtiIAN<>HQOmh)DMi{s-0H^GmV3QMf4Zu(zXT!-c;2)uv4gUwt(-}-N*|KUOo$h z+Ak^R)h8yB5UD8 zsSjHgY}KguNi?xV=tdCWqJR!~dDpFQoRJOwxrWH^vfRq4%)v;sDfIjsLXF^)uy>!i z*S8Njd7yfa`+7(|8H9j73Rh|TwFpF(8H-p;RLLIU>k<*qI%A*SL{u$%<=X@Jm1QFe zVkQ(X8P4Tohl?_tSO__^aqaI?k$CC8uNLv2mp_zD@4oDaZfEN5;3#XY!L{8B!;Dtt zb~Zge@JF|#Gsk^5$-|(OPI73po|WZh<`UxaH#Y2!&p05Ph?H)d3Bc3J4sDi$f(6K`?&D&~eHVuE@_Prkt>_&8&aq=OzoN!ANkvho;qIX(g|d#EKQbJ@;-%_iARmgSF1fEK z@B4W@5mDME7AzfL**c&2#B7xO9>rA4x$rM{N=%0=goumK1kL{TF@CSk0yvqR2oo&m z)?nyiL$9~Jt(qnEuWt9Hc_duim%|zJQYiaF*~orVNDvJB;`%ZW_2x%Uu01LeX-JP& zD&fas6d3=igAgcfeki79{5!XPHHYR#nfLYRKv^wkv~cnEbLHMwQ8%yCZI^rK!D2qT zk40Vg;e!_!3d56&umIuidN?6MTZFzHot}AdqKzDh#w0s`)cV!2A74RSH1@lDXtC38 z+UhO4A9?oZEOV{bIgGd1{2qMR&xT+}q!=I8m)W23v!W2WPC?Tf!F!e%_(m^lQZtq* zYwi}gY(KZ*Y^OWRNj$Ph#uEEBM+wtN8QFQ@^`GDOln^ioNrmtvzNNi*qS5lPHxI96#sMil*teLVaa%$msF>@5p#SjT%q8|<4ZOUB#!-kG+|eFSED z!|3c8fXaym9qH`L;pmqTWcG}WE$(h1sZ3seM>)E3ptoP<;~h~qe6XA)lGVanf&->P zjZwi;_;Dt+bYdAeD_XSQ-DgXRXqLv`3Wcgl}myA-JlzBBIh zWq4Q*9#(zjAk_H8VS_AJ`?OS*^gB-rp|~qt;v(C5ef=SErv;~zL64hW`#g!UZQcvZ zF6Ra@S@YhVSkSWVAY=Z1w)w-hfJDRwKTUH0o-OG5TlW0HDH36hIjnP=?A+8u1)Qyy5U8Gi$! zt^!vy|f=YHfQ`ZRK?D zXXn*kItRg50vr2+_hV5kjOleg#s~z(J2p#`=1Tq4#JS`MC^e4p&s7Ir=3m(K$LW#` z=ULCoWtna!so+QQ*JHb~6Ps9_&Ag>9qsUskp0pKbi`n?(u3&@QT!?}N}rXn z>1eHi6(@LicU*AR1obe+nbzTCD#VTJ`PFLRT(nc$NWrhsgRwFni*D(#?W^x=J6?|b zENSc^D}s>Y55)PzFs2d_2;yh89E0ZIgs&>6JV=pL6k9g_(`$04EoY+Zjn}}8e#n83 zJ=zB>BU<253Erdo$wE4^+@QQJFZyAj#(InFlN;!UGg96R@{Y&%OlGG;dM)^X8=Ddw@&2Vx?zui$tO z-{zgaU7&F!xs=e`Mn}r+xrdIAmkraRN_7P1?qu1|TZ%1QR(Mn?k+pq`Xys2v9Gs=a z?r@g&;UKcM#?36r9k*eVD(}9qe8?irotsn0+eHH8*4 zPX@Lusr)$J%8jarx5ssEJ?twFyu4kAbrf`96_z{6at^&UkyDzFa69RXP>PeK+dAWqE5<5P+aHa zs<<*+OO_2ObTXau%y)Nn{(p5`XIPWlvi|asjYcui;E@)Ig{YKBXi}spqC!-P5owwL z3L*+9;0C0G!xoN;4KNfDaElv>1#DMDglI&MAVoK2+c2Pr8&sl*1dYj=^>NRS`{O&%YV25@5*eoOvpD_(xdKsnqb^`T}bm;n0BN9ben1Ynyi*OOf;qLpf^ z!T{}GzkXSszN_Xqzp>}S*Im)_Y8~2|B*ybw(U=Q)5_NcMkT;)1&52YQJB)Tn%kPK! z@3;^AI){B(&UOv<{v9KKJrInkdcXV0%O1%1=7vYV*j?v(Kp~arZio$#(A@$kYB3aM zRdm4!^Je15%66($EkCIWGhi@=kNAyLJ3ydlJnCpPuxH0+OA}J)+t8d7nT->##Nz4w-L=S7ExQt=Rx}S*mpT91(>t~qe7tM%e|O)TIO^dP zfo61GNS=cJbLutqUh84?7X#bq)bv57s&D_zm{+xNv7vHjb=_}j-Lrj-Ss*pcD@ts$ z)5Dol8Z_&*1@JdAQE7SL$*!TXI|YE7q=YGkIiUeLvT0)14Q-ivs|+cqeT6DTi9eQ)h?Pu9pqmH51B* zFMd|;l2@D4*56|EhMFlDxl2i<8qq=c+AhMYS3(A28#3DZ;_Ln>RA3q#IAdJq7M#N> zTZ8t=_>lq0=W&w|bdQ^sy&m^@KR)mNi3|1<6|OL(0KLtP#I6ix$2b{-Y9GP5I7 z8AJUSCnlia5vWawX%ZLWTC2UV$cn^sfv68W!6)QO;ZjnX=7#`$ZPRG~irfl)ZUJ^D z{lUk?(*SU7XIiS^H{Lpxn%542#PgxdeG)Ociej#(uvX)z;Z3)<16Yhd z-sv?qQ5D4a)ZYoYPRep2Zvom@U)HKq*54ZEwdaEq^FZG#(CyG!=Vw(0j8CCmP~`_z z=OR^i&WkDCf2cLvWm@d?)mEgme{hA(o#xAL023LZ3(82SGRg6jJF7$kZ4! z6*FTm4y6v~CP!3$+fxg{QeFo24<3iucgI!oyjV|9Dsx}r~4X@lt^VaH$u zD?87}1Jh=?G8OYg*ts2k;X9{f*Za?yu8IUUfyuQ**wbcWT+KncjD^qQ3h&w2+S(Mj zZM~?Ot%ggTIHwkBkL-4&jI5R=B+MCOR42bKzC2M>l?1%x2Iv7amIfQ1B#wwfD`z|m z+E?G+o(tde*Ws?;Wo4p#Yy>Nnf|*b<nj@-s(rZ)-U@ z(Xe(qZ1(_dH|J3yWu|bAPINK}DwF(kZ>FKx(?ZmU^KFC6*bh$;FKGh~pH1 zozA+kgcIk9@2aAwEJ=VYizT!sxDXX$N?XDiGKaaT-OU@Ib=~4DmgEk&{2D@IvyjF* zuF@sDcuuqx_FAgx;B@@8gqjMh!kQeEKA*y4+q+^4&uc0|>M;$Xb+ z@X%eUx1m%$WSP}Qchx68NQ?dO!h`6;Quq+A1(RORsQ-;6bZ90vj#^0(7>cLR+-_;9 zCd@b~B5V>$tpjkQU#BD%9^zu7-l>U8nzt+XuX5cYDCHYaX5t~~3?lpa;)Mr>q;5XW zu(Th;fr}-GkP`K)u97(#UB|L3f;H7Cd#Pox+auV`=m?a=mSv1v)(V!E=$%gkIJZ;` zZj{Lb@bhs%bRa znZw9cD$cDFVHPtpXwY1K)wys@LS~;!qdqkR>@&RtP>?M^>xe{4N#EtZy4zZ5Ar$ZF zV=X=(!xin-58MC<+b~;jk8Q|3B3THGIA$cM8Bg)Yd6ygP#i?4VrX3OvP_k5i{Cppw z-{$XwrJ-+X$ccJ(Q{|?T@U9=-?qlsfA43%8t247KZn?`+C4e`b-e^(df*iW66=Oc2 z3w9UhohfdY@pH1MZ}vc<1osV(2CGG)Ree$E-T;8>$zw*>x-505b&4(shMGIjbAfLS zEZ3ys(`SmCWc(75)^=aKer}>67qj^nGKtCK{35I|tA}wQa!uM!suX%Gb~ylORGGc( ze^|m|N!}G0#Ph|;wSXz`SByQM>lPM#8>mdSQs`7RxkXaSAADYA24u6xWqkIXY?o%z z%TEFL+wNW^&nrvaA1_#P%&Hbzrjl!*hIft>F0@g0IVydUU4MJgS3_3Js8{*>|G2jC z4%n#cOy9b2Xf&Pw=14;0Dtf00C^Z$I-v05OqtvN9>sAC&oV1Tk;;ku7VR`sQK4oFq zQ8)yoZNuTwV$t13|GCUIC{ID_r7M5&R*zhsxbrkg;EgMtL|9ne=^}BM!dxV!KDeXkWA^MfQTkQEt8~t>JznNh%ULvn@dbQ2cyf} z|C%ns#NJU}SHU(7Pg$<&8uDK>d5GZJ&`;CcfGP(~b-#UusXevc^q!km1X6_wVMqGk z^m&ZS6#42?p4c_t1TA$_+}h1L2c<<=$k%;v+D!<@j5hs|{>d18>~~v#oq4yGyS@QP zgTX2oJbEy@eJbo-f{ZQ>-nmB-#AqWcHbMQXFi*T)0n!(HIexz=pp<(O*DMh7CMupX z)ei1ZYuIW~E={-ND*nD;okiZdm!?^|LjLZhs*FHZvWld5TDj zcvWB)`-1Me9bu`*4M=CO6ye=pMgxlgYvsh2rV#5Z$hFKw0GX30%oufb=hJ0BFIJH` z+Fii4gQ+7!)8K^yc*PVEW^#f!|BW0Q5*`IewQ5YDFh?{x1L7tlaUAX@3Y+D>6FPVf zJzOGex~H34`8eq+TL$FsHm+27RS>3$CG;>0Jj4*1ukX$za})*b^S5p}I2jbFCHLsA zzYwAyftMz`uo2c8ieQcy-p&9iP3fMk(uRw+OlBPm`KCLei6g!|Vnk*-kjs>A25MTE z5GLDMV$70AC0j-tx*0sCruvKh{fSM)3X}13U>m|KeaOb`9^}v^44!$`06-JHf@L4EKyxV)M!8cL zi5p9kF97RiAT92!e?%9CP=qX3wyv^A8q!w%07d(9f-U))uDgsr4FDVL;|%r)fw}-@ zlB$F79X^EKYF%8J7mU?3VzJoYQ0<;NczW1jH4=4kEh_)q|^9wj zIsn-SsmRx0_EJ7(6WypwptIwZ)-T<__UgUu?BXt zoIf|a!5`?&JEb$w2PZSqhA>J;GIA^rJ-Cpz8MKX~bcqZNOUzPtu|NMvEP>+cO;V*W zNQ8YPENkr!)lN+tlxB79RUD20$)+_P6Jc`+4q@%Kno{F+#1qR*zrj%T>nTSceO?a5 zyqGDa59#G6k*RXu6+#=e=e!~i1Y&15!cHmE6sLh_K%Ppv$tFE-Le3RQs-nx5LB>gy z5A))kwkxWSy73{@I{%{DY8X+2o{CLJb~R$3r=oT^P~Xo$2lKz8?Z!3QLn$5l#L2k2 zb1=?UT&c<8!&9gW1M&jI!5%dhJbD3nQXpaeNJ>=zR+EL!4iY(nMBQI+|2J+Hw-WMr z08Mt9h8(PGbY?zKtk=cqw(yW}1A#htn* z8&}5Y>$uc>Lv!bSuWQ5UB&ct7*jiZAFpxz|%xO&5kg zzlf?6xy7H3G^*wvP5scW*Wf(<&eP!YIUf%&HT?K)RWmKg$G^=mSoi~;&9dU%{o}WV z#BX;9+q)fpVU`>Vdo~AtYK)`7z*H;dc-e|q6Qt;3J0APUL!~g&Q literal 0 HcmV?d00001 diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4cc16421680a50164ba74381b4b35ceaa0ccfc GIT binary patch literal 3276 zcmZ`*X*|?x8~)E?#xi3t91%vcMKbnsIy2_j%QE2ziLq8HEtbf{7%?Q-9a%z_Y^9`> zEHh*&vUG%uWkg7pKTS-`$veH@-Vg8ZdG7oAJ@<88AMX3Z{d}TU-4*=KI1-hF6u>DKF2moPt09c{` zfN3rO$X+gJI&oA$AbgKoTL8PiPI1eFOhHBDvW+$&oPl1s$+O5y3$30Jx9nC_?fg%8Om)@;^P;Ee~8ibejUNlSR{FL7-+ zCzU}3UT98m{kYI^@`mgCOJ))+D#erb#$UWt&((j-5*t1id2Zak{`aS^W*K5^gM02# zUAhZn-JAUK>i+SNuFbWWd*7n1^!}>7qZ1CqCl*T+WoAy&z9pm~0AUt1cCV24f z3M@&G~UKrjVHa zjcE@a`2;M>eV&ocly&W3h{`Kt`1Fpp?_h~9!Uj5>0eXw@$opV(@!pixIux}s5pvEqF5$OEMG0;c zAfMxC(-;nx_`}8!F?OqK19MeaswOomKeifCG-!9PiHSU$yamJhcjXiq)-}9`M<&Au|H!nKY(0`^x16f205i2i;E%(4!?0lLq0sH_%)Wzij)B{HZxYWRl3DLaN5`)L zx=x=|^RA?d*TRCwF%`zN6wn_1C4n;lZG(9kT;2Uhl&2jQYtC1TbwQlP^BZHY!MoHm zjQ9)uu_K)ObgvvPb}!SIXFCtN!-%sBQe{6NU=&AtZJS%}eE$i}FIll!r>~b$6gt)V z7x>OFE}YetHPc-tWeu!P@qIWb@Z$bd!*!*udxwO6&gJ)q24$RSU^2Mb%-_`dR2`nW z)}7_4=iR`Tp$TPfd+uieo)8B}Q9#?Szmy!`gcROB@NIehK|?!3`r^1>av?}e<$Qo` zo{Qn#X4ktRy<-+f#c@vILAm;*sfS}r(3rl+{op?Hx|~DU#qsDcQDTvP*!c>h*nXU6 zR=Un;i9D!LcnC(AQ$lTUv^pgv4Z`T@vRP3{&xb^drmjvOruIBJ%3rQAFLl7d9_S64 zN-Uv?R`EzkbYIo)af7_M=X$2p`!u?nr?XqQ_*F-@@(V zFbNeVEzbr;i2fefJ@Gir3-s`syC93he_krL1eb;r(}0yUkuEK34aYvC@(yGi`*oq? zw5g_abg=`5Fdh1Z+clSv*N*Jifmh&3Ghm0A=^s4be*z5N!i^FzLiShgkrkwsHfMjf z*7&-G@W>p6En#dk<^s@G?$7gi_l)y7k`ZY=?ThvvVKL~kM{ehG7-q6=#%Q8F&VsB* zeW^I zUq+tV(~D&Ii_=gn-2QbF3;Fx#%ajjgO05lfF8#kIllzHc=P}a3$S_XsuZI0?0__%O zjiL!@(C0$Nr+r$>bHk(_oc!BUz;)>Xm!s*C!32m1W<*z$^&xRwa+AaAG= z9t4X~7UJht1-z88yEKjJ68HSze5|nKKF9(Chw`{OoG{eG0mo`^93gaJmAP_i_jF8a z({|&fX70PXVE(#wb11j&g4f{_n>)wUYIY#vo>Rit(J=`A-NYYowTnl(N6&9XKIV(G z1aD!>hY!RCd^Sy#GL^0IgYF~)b-lczn+X}+eaa)%FFw41P#f8n2fm9=-4j7}ULi@Z zm=H8~9;)ShkOUAitb!1fvv%;2Q+o)<;_YA1O=??ie>JmIiTy6g+1B-1#A(NAr$JNL znVhfBc8=aoz&yqgrN|{VlpAniZVM?>0%bwB6>}S1n_OURps$}g1t%)YmCA6+5)W#B z=G^KX>C7x|X|$~;K;cc2x8RGO2{{zmjPFrfkr6AVEeW2$J9*~H-4~G&}~b+Pb}JJdODU|$n1<7GPa_>l>;{NmA^y_eXTiv z)T61teOA9Q$_5GEA_ox`1gjz>3lT2b?YY_0UJayin z64qq|Nb7^UhikaEz3M8BKhNDhLIf};)NMeS8(8?3U$ThSMIh0HG;;CW$lAp0db@s0 zu&jbmCCLGE*NktXVfP3NB;MQ>p?;*$-|htv>R`#4>OG<$_n)YvUN7bwzbWEsxAGF~ zn0Vfs?Dn4}Vd|Cf5T-#a52Knf0f*#2D4Lq>-Su4g`$q={+5L$Ta|N8yfZ}rgQm;&b z0A4?$Hg5UkzI)29=>XSzdH4wH8B@_KE{mSc>e3{yGbeiBY_+?^t_a#2^*x_AmN&J$ zf9@<5N15~ty+uwrz0g5k$sL9*mKQazK2h19UW~#H_X83ap-GAGf#8Q5b8n@B8N2HvTiZu&Mg+xhthyG3#0uIny33r?t&kzBuyI$igd`%RIcO8{s$$R3+Z zt{ENUO)pqm_&<(vPf*$q1FvC}W&G)HQOJd%x4PbxogX2a4eW-%KqA5+x#x`g)fN&@ zLjG8|!rCj3y0%N)NkbJVJgDu5tOdMWS|y|Tsb)Z04-oAVZ%Mb311P}}SG#!q_ffMV z@*L#25zW6Ho?-x~8pKw4u9X)qFI7TRC)LlEL6oQ9#!*0k{=p?Vf_^?4YR(M z`uD+8&I-M*`sz5af#gd$8rr|oRMVgeI~soPKB{Q{FwV-FW)>BlS?inI8girWs=mo5b18{#~CJz!miCgQYU>KtCPt()StN;x)c2P3bMVB$o(QUh z$cRQlo_?#k`7A{Tw z!~_YKSd(%1dBM+KE!5I2)ZZsGz|`+*fB*n}yxtKVyx14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>GbI`Jdw*pGcA%L+*Q#&*YQOJ$_%U#(BDn``;rKxi&&)LfRxIZ*98z8UWRslDo@Xu)QVh}rB>bKwe@Bjzwg%m$hd zG)gFMgHZlPxGcm3paLLb44yHI|Ag0wdp!_yD5R<|B29Ui~27`?vfy#ktk_KyHWMDA42{J=Uq-o}i z*%kZ@45mQ-Rw?0?K+z{&5KFc}xc5Q%1PFAbL_xCmpj?JNAm>L6SjrCMpiK}5LG0ZE zO>_%)r1c48n{Iv*t(u1=&kH zeO=ifbFy+6aSK)V_5t;NKhE#$Iz=+Oii|KDJ}W>g}0%`Svgra*tnS6TRU4iTH*e=dj~I` zym|EM*}I1?pT2#3`oZ(|3I-Y$DkeHMN=8~%YSR?;>=X?(Emci*ZIz9+t<|S1>hE8$ zVa1LmTh{DZv}x6@Wz!a}+qZDz%AHHMuHCzM^XlEpr!QPzf9QzkS_0!&1MPx*ICxe}RFdTH+c}l9E`G zYL#4+3Zxi}3=A!G4S>ir#L(2r)WFKnP}jiR%D`ZOPH`@ZhTQy=%(P0}8ZH)|z6jL7 N;OXk;vd$@?2>?>Ex^Vyi literal 0 HcmV?d00001 diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..bcbf36df2f2aaaa0a63c7dabc94e600184229d0d GIT binary patch literal 5933 zcmZ{Idpwix|Np(&m_yAF>K&UIn{t*2ZOdsShYs(MibU!|=pZCJq~7E>B$QJr)hC5| zmk?V?ES039lQ~RC!kjkl-TU4?|NZ{>J$CPLUH9vHy`Hbhhnc~SD_vpzBp6Xw4`$%jfmPw(;etLCccvfU-s)1A zLl8-RiSx!#?Kwzd0E&>h;Fc z^;S84cUH7gMe#2}MHYcDXgbkI+Qh^X4BV~6y<@s`gMSNX!4@g8?ojjj5hZj5X4g9D zavr_NoeZ=4vim%!Y`GnF-?2_Gb)g$xAo>#zCOLB-jPww8a%c|r&DC=eVdE;y+HwH@ zy`JK(oq+Yw^-hLvWO4B8orWwLiKT!hX!?xw`kz%INd5f)>k1PZ`ZfM&&Ngw)HiXA| ze=+%KkiLe1hd>h!ZO2O$45alH0O|E+>G2oCiJ|3y2c$;XedBozx93BprOr$#d{W5sb*hQQ~M@+v_m!8s?9+{Q0adM?ip3qQ*P5$R~dFvP+5KOH_^A+l-qu5flE*KLJp!rtjqTVqJsmpc1 zo>T>*ja-V&ma7)K?CE9RTsKQKk7lhx$L`9d6-Gq`_zKDa6*>csToQ{&0rWf$mD7x~S3{oA z1wUZl&^{qbX>y*T71~3NWd1Wfgjg)<~BnK96Ro#om&~8mU{}D!Fu# zTrKKSM8gY^*47b2Vr|ZZe&m9Y`n+Y8lHvtlBbIjNl3pGxU{!#Crl5RPIO~!L5Y({ym~8%Ox-9g>IW8 zSz2G6D#F|L^lcotrZx4cFdfw6f){tqITj6>HSW&ijlgTJTGbc7Q#=)*Be0-s0$fCk z^YaG;7Q1dfJq#p|EJ~YYmqjs`M0jPl=E`Id{+h%Lo*|8xp6K7yfgjqiH7{61$4x~A zNnH+65?QCtL;_w(|mDNJXybin=rOy-i7A@lXEu z&jY(5jhjlP{TsjMe$*b^2kp8LeAXu~*q&5;|3v|4w4Ij_4c{4GG8={;=K#lh{#C8v z&t9d7bf{@9aUaE94V~4wtQ|LMT*Ruuu0Ndjj*vh2pWW@|KeeXi(vt!YXi~I6?r5PG z$_{M*wrccE6x42nPaJUO#tBu$l#MInrZhej_Tqki{;BT0VZeb$Ba%;>L!##cvieb2 zwn(_+o!zhMk@l~$$}hivyebloEnNQmOy6biopy`GL?=hN&2)hsA0@fj=A^uEv~TFE z<|ZJIWplBEmufYI)<>IXMv(c+I^y6qBthESbAnk?0N(PI>4{ASayV1ErZ&dsM4Z@E-)F&V0>tIF+Oubl zin^4Qx@`Un4kRiPq+LX5{4*+twI#F~PE7g{FpJ`{)K()FH+VG^>)C-VgK>S=PH!m^ zE$+Cfz!Ja`s^Vo(fd&+U{W|K$e(|{YG;^9{D|UdadmUW;j;&V!rU)W_@kqQj*Frp~ z7=kRxk)d1$$38B03-E_|v=<*~p3>)2w*eXo(vk%HCXeT5lf_Z+D}(Uju=(WdZ4xa( zg>98lC^Z_`s-=ra9ZC^lAF?rIvQZpAMz8-#EgX;`lc6*53ckpxG}(pJp~0XBd9?RP zq!J-f`h0dC*nWxKUh~8YqN{SjiJ6vLBkMRo?;|eA(I!akhGm^}JXoL_sHYkGEQWWf zTR_u*Ga~Y!hUuqb`h|`DS-T)yCiF#s<KR}hC~F%m)?xjzj6w#Za%~XsXFS@P0E3t*qs)tR43%!OUxs(|FTR4Sjz(N zppN>{Ip2l3esk9rtB#+To92s~*WGK`G+ECt6D>Bvm|0`>Img`jUr$r@##&!1Ud{r| zgC@cPkNL_na`74%fIk)NaP-0UGq`|9gB}oHRoRU7U>Uqe!U61fY7*Nj(JiFa-B7Av z;VNDv7Xx&CTwh(C2ZT{ot`!E~1i1kK;VtIh?;a1iLWifv8121n6X!{C%kw|h-Z8_U z9Y8M38M2QG^=h+dW*$CJFmuVcrvD*0hbFOD=~wU?C5VqNiIgAs#4axofE*WFYd|K;Et18?xaI|v-0hN#D#7j z5I{XH)+v0)ZYF=-qloGQ>!)q_2S(Lg3<=UsLn%O)V-mhI-nc_cJZu(QWRY)*1il%n zOR5Kdi)zL-5w~lOixilSSF9YQ29*H+Br2*T2lJ?aSLKBwv7}*ZfICEb$t>z&A+O3C z^@_rpf0S7MO<3?73G5{LWrDWfhy-c7%M}E>0!Q(Iu71MYB(|gk$2`jH?!>ND0?xZu z1V|&*VsEG9U zm)!4#oTcgOO6Hqt3^vcHx>n}%pyf|NSNyTZX*f+TODT`F%IyvCpY?BGELP#s<|D{U z9lUTj%P6>^0Y$fvIdSj5*=&VVMy&nms=!=2y<5DP8x;Z13#YXf7}G)sc$_TQQ=4BD zQ1Le^y+BwHl7T6)`Q&9H&A2fJ@IPa;On5n!VNqWUiA*XXOnvoSjEIKW<$V~1?#zts>enlSTQaG2A|Ck4WkZWQoeOu(te znV;souKbA2W=)YWldqW@fV^$6EuB`lFmXYm%WqI}X?I1I7(mQ8U-pm+Ya* z|7o6wac&1>GuQfIvzU7YHIz_|V;J*CMLJolXMx^9CI;I+{Nph?sf2pX@%OKT;N@Uz9Y zzuNq11Ccdwtr(TDLx}N!>?weLLkv~i!xfI0HGWff*!12E*?7QzzZT%TX{5b7{8^*A z3ut^C4uxSDf=~t4wZ%L%gO_WS7SR4Ok7hJ;tvZ9QBfVE%2)6hE>xu9y*2%X5y%g$8 z*8&(XxwN?dO?2b4VSa@On~5A?zZZ{^s3rXm54Cfi-%4hBFSk|zY9u(3d1ButJuZ1@ zfOHtpSt)uJnL`zg9bBvUkjbPO0xNr{^{h0~$I$XQzel_OIEkgT5L!dW1uSnKsEMVp z9t^dfkxq=BneR9`%b#nWSdj)u1G=Ehv0$L@xe_eG$Ac%f7 zy`*X(p0r3FdCTa1AX^BtmPJNR4%S1nyu-AM-8)~t-KII9GEJU)W^ng7C@3%&3lj$2 z4niLa8)fJ2g>%`;;!re+Vh{3V^}9osx@pH8>b0#d8p`Dgm{I?y@dUJ4QcSB<+FAuT)O9gMlwrERIy z6)DFLaEhJkQ7S4^Qr!JA6*SYni$THFtE)0@%!vAw%X7y~!#k0?-|&6VIpFY9>5GhK zr;nM-Z`Omh>1>7;&?VC5JQoKi<`!BU_&GLzR%92V$kMohNpMDB=&NzMB&w-^SF~_# zNsTca>J{Y555+z|IT75yW;wi5A1Z zyzv|4l|xZ-Oy8r8_c8X)h%|a8#(oWcgS5P6gtuCA_vA!t=)IFTL{nnh8iW!B$i=Kd zj1ILrL;ht_4aRKF(l1%^dUyVxgK!2QsL)-{x$`q5wWjjN6B!Cj)jB=bii;9&Ee-;< zJfVk(8EOrbM&5mUciP49{Z43|TLoE#j(nQN_MaKt16dp#T6jF7z?^5*KwoT-Y`rs$ z?}8)#5Dg-Rx!PTa2R5; zx0zhW{BOpx_wKPlTu;4ev-0dUwp;g3qqIi|UMC@A?zEb3RXY`z_}gbwju zzlNht0WR%g@R5CVvg#+fb)o!I*Zpe?{_+oGq*wOmCWQ=(Ra-Q9mx#6SsqWAp*-Jzb zKvuPthpH(Fn_k>2XPu!=+C{vZsF8<9p!T}U+ICbNtO}IAqxa57*L&T>M6I0ogt&l> z^3k#b#S1--$byAaU&sZL$6(6mrf)OqZXpUPbVW%T|4T}20q9SQ&;3?oRz6rSDP4`b z(}J^?+mzbp>MQDD{ziSS0K(2^V4_anz9JV|Y_5{kF3spgW%EO6JpJ(rnnIN%;xkKf zn~;I&OGHKII3ZQ&?sHlEy)jqCyfeusjPMo7sLVr~??NAknqCbuDmo+7tp8vrKykMb z(y`R)pVp}ZgTErmi+z`UyQU*G5stQRsx*J^XW}LHi_af?(bJ8DPho0b)^PT|(`_A$ zFCYCCF={BknK&KYTAVaHE{lqJs4g6B@O&^5oTPLkmqAB#T#m!l9?wz!C}#a6w)Z~Z z6jx{dsXhI(|D)x%Yu49%ioD-~4}+hCA8Q;w_A$79%n+X84jbf?Nh?kRNRzyAi{_oV zU)LqH-yRdPxp;>vBAWqH4E z(WL)}-rb<_R^B~fI%ddj?Qxhp^5_~)6-aB`D~Nd$S`LY_O&&Fme>Id)+iI>%9V-68 z3crl=15^%0qA~}ksw@^dpZ`p;m=ury;-OV63*;zQyRs4?1?8lbUL!bR+C~2Zz1O+E@6ZQW!wvv z|NLqSP0^*J2Twq@yws%~V0^h05B8BMNHv_ZZT+=d%T#i{faiqN+ut5Bc`uQPM zgO+b1uj;)i!N94RJ>5RjTNXN{gAZel|L8S4r!NT{7)_=|`}D~ElU#2er}8~UE$Q>g zZryBhOd|J-U72{1q;Lb!^3mf+H$x6(hJHn$ZJRqCp^In_PD+>6KWnCnCXA35(}g!X z;3YI1luR&*1IvESL~*aF8(?4deU`9!cxB{8IO?PpZ{O5&uY<0DIERh2wEoAP@bayv z#$WTjR*$bN8^~AGZu+85uHo&AulFjmh*pupai?o?+>rZ7@@Xk4muI}ZqH`n&<@_Vn zvT!GF-_Ngd$B7kLge~&3qC;TE=tEid(nQB*qzXI0m46ma*2d(Sd*M%@Zc{kCFcs;1 zky%U)Pyg3wm_g12J`lS4n+Sg=L)-Y`bU705E5wk&zVEZw`eM#~AHHW96@D>bz#7?- zV`xlac^e`Zh_O+B5-kO=$04{<cKUG?R&#bnF}-?4(Jq+?Ph!9g zx@s~F)Uwub>Ratv&v85!6}3{n$bYb+p!w(l8Na6cSyEx#{r7>^YvIj8L?c*{mcB^x zqnv*lu-B1ORFtrmhfe}$I8~h*3!Ys%FNQv!P2tA^wjbH f$KZHO*s&vt|9^w-6P?|#0pRK8NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGJ{c&& zS>O>_%)r1c48n{Iv*t(u1=&kHeO=ifbFy+6aSK)V_AxLppYn8Z42d|rc6w}vOsL55 z`t&mC&y2@JTEyg!eDiFX^k#CC!jq%>erB=yHqUP0XcDOTw6ko}L zX;EmMrq(fKk*eygEuA616;0)>@A{TK|55PV@70 z$OfzS*(VJxQev3J?yY?O=ul(v`fp}?u9z`JK3ugibK>)DyCwImZOF4d{xK%%Ks1*} zv$oa)9anR%lXIBUqYnhLmT>VOzHfNP?ZwJNZ!5$s9M08RynIvaXw>@G^T9@r9^KH1 zVy??F&uuk)bH9Y4pQY!hP58i_H6 znl-NcuCpLV6ZWU;4C zu@9exF&OZi`Bovq_m%T+WhU2kvkz@^_LpycBvqm3bMpLw8X-Or5sL>0AKE1$(k_L=_Zc=CUq#=x1-QZf)G7nHu@fmsQ1eN_N3+nTEz`4HI4Z6uVlE zJH+X&det8JU?tO?upcM4Z=cV!JV;yF>FfL5Q$M|W_2Z!P`S=}Wzp|_1^#d%e?_H`> zV@%vA$+bFVqhw9`U;TfP|5|PD{||OiYdor8P*i??|NJcb%kzT_73*7WE?Ua5hAnR2 z=7WE=PhTlJ#ZeRznjTUb;`E(wkMZrj4e|Hilz-mK>9cZHQY**5TUPw~u}k;u73KI}xAx!0m-)GVia|x^d3p~s_9gh83jA&Ra<8rM%`>U3x69t&NzbwWY}7Ar?)FK#IZ0z|d0H0EkRO w3{9;}4Xg|ebq&m|3=9_N6z8I7$jwj5OsmAL;bP(Gi$Dzwp00i_>zopr02+f8CIA2c literal 0 HcmV?d00001 diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..e71a726136a47ed24125c7efc79d68a4a01961b4 GIT binary patch literal 14800 zcmZ{Lc|26@`~R6Crm_qwyCLMMh!)vm)F@HWt|+6V6lE=CaHfcnn4;2x(VilEl9-V} zsce-cGK|WaF}4{T=lt&J`Fy_L-|vs#>v^7+XU=`!*L|PszSj43o%o$Dj`9mM7C;ar z@3hrnHw59q|KcHn4EQr~{_70*BYk4yj*SqM&s>NcnFoIBdT-sm1A@YrK@dF#f+SPu z{Sb8441xx|AjtYQ1gQq5z1g(^49Fba=I8)nl7BMGpQeB(^8>dY41u79Dw6+j(A_jO z@K83?X~$;S-ud$gYZfZg5|bdvlI`TMaqs!>e}3%9HXev<6;dZZT8Yx`&;pKnN*iCJ z&x_ycWo9{*O}Gc$JHU`%s*$C%@v73hd+Mf%%9ph_Y1juXamcTAHd9tkwoua7yBu?V zgROzw>LbxAw3^;bZU~ZGnnHW?=7r9ZAK#wxT;0O<*z~_>^uV+VCU9B@)|r z*z^v>$!oH7%WZYrwf)zjGU|(8I%9PoktcsH8`z^%$48u z(O_}1U25s@Q*9{-3O!+t?w*QHo;~P99;6-KTGO{Cb#ADDYWF!eATsx{xh-!YMBiuE z%bJc7j^^B$Sa|27XRxg(XTaxWoFI}VFfV>0py8mMM;b^vH}49j;kwCA+Lw=q8lptk z?Pe`{wHI39A&xYkltf5*y%;-DF>5v`-lm0vydYtmqo0sClh5ueHCLJ+6$0y67Z zO-_LCT|JXi3tN7fB-!0_Kn#I+=tyUj87uR5*0>|SZ zy3x2;aql87`{aPZ@UbBwY0;Z-a*lYL90YApOAMKur7YgOiqA~Cne6%b&{V-t>Am2c z{eyEuKl!GsA*jF2H_gvX?bP~v46%3ax$r~B$HnZQ;UiCmRl`ROK8v>;Zs~upH9}qu1ZA3kn-AY2k2@CaH=Qh7K6`nU z3ib(Bk%H*^_omL6N4_G5NpY20UXGi}a$!}#lf<&J4~nhRwRM5cCB3Zvv#6+N1$g@W zj9?qmQ`zz-G9HTpoNl~bCOaEQqlTVYi7G0WmB5E34;f{SGcLvFpOb`+Zm)C(wjqLA z2;+nmB6~QDXbxZGWKLt38I%X$Q!;h zup9S~byxKv=$x|^YEV;l0l67jH~E8BU45ft_7xomac-48oq4PZpSNJbw<7DTM4mmz z!$)z#04cy%b8w@cOvjmb36o;gwYIOLwy+{I#3dJj#W4QdOWwJQ2#20AL49`hSFUa7 zFNAN3OD==G3_kbr1d96>l`_cI`<=thKNh5>hgg7FV>5TfC6d#u)9BNXi@p1K*;2Is zz+x;l4GbSt#*%>1iq}jGIebXYJY5;PGG0y(^{>SSuZY89aL`sDghOM&&pyP6ABJ#w zYwK~4^1eUQD)4!GL>`zrWeHV z-W!6JZbW*Ngo;Edhp_cOysYr!uhKS}vIg_UC}x z=jXxQfV@4B3`5 z!u#byBVXV5GtrSx_8bnT@iKv=Uc6n)Zpa`<9N>+!J~Loxptl5$Z`!u<3a)-+P)say z#=jc7^mJzPMI2;yMhCmN7YN78E7-^S(t8E}FklC;z|4PL{bO|JieM#p1mBjwyZMEm zkX^A1RXPGeS2YqtPMX~~t^$~oeFfWAU#jVLi%Z@l2hle^3|e(q?(uS=BVauF?VF{j z(owKLJuze;_@5p1OtRyrT`EFXf)NfMYb-)E8RVVdr<@}M>4R&~P=;B`c1L%o|8YfB z-a(LB-i8jc5!&B5cowyI2~M^YID&@Xt(D9v{|DB z959W z*vEA77fh3*w*UJ`4Y(bxsoEy6hm7_Wc5gT0^cvso%Ow>9<&@9Q>mxb6-^pv)5yc>n zQ~^!qY(lPQ1EDGkr%_*y*D8T^YbCa52^MVqYpTLhgJ;N5PfCQ{SXk|plD#Sm+g4c- zFeL2Dih35W4{_qb75U`4Rb#S0FEo%F85dOhXSX0huPOxdAid{&p6P;+9}I)XU7^=3RZu9M(g0dLyz_7$8K{`AddBLOfU&B_QNHtmsnNXq`hy~% zvJ{vtz~Yt9X|o}5vXX)9ZCHaRq8iAb zUDj8%(MpzJN39LferYKvIc!)z^5T-eW@j3h9a6d%WZ!%@2^@4+6%Z9W1GHZbOj|sb z0cU$}*~G$fYvDC|XulSC_;m}?KC2jg5pxES$Bt!hA|@EX*2+O!UEb5sn_^d>z;>;r~ zmO3BivdXboPY*}amsO&`xk|e)S*u=`o67MC(1WTB;OwG+ua4UV7T5Wvy%?U{Pa5cO zMoLG>#@chO{Oc72XPyX8f3jC7P`$j4$)0wc(b50COaDP3_Cm}aPAglUa7kRXAqmo5 z0KDD7G>Gmnpons40WJNYn+pxko92GXy@PvSErKE-Ou3)3UiRr7!L4+0%+5}sD{bf)uj^ounQ-Yn2%%JoZ%FjUv%yjS?Ks4u_88Jh%tNliYW~817IV@fqd1T zi(?;Fv-s3rQEn=9G*E-QzSl%YS|^fe*yn}Aqh!&P<5%#oB?*{wZMa5$PYa*A{VA8! zbOfS1W!W}cTo%g~iP$>WhE_x7#O4?h$jq=>{M77>bTAK_ z6uU0tl6HARboGi}=4krr6WP`9`aAt&P5ON1v(+H{T?jZuJ}B{L-=z3VX)}mZwzrqH zpf?T!k&$?{&{0_p>b`kdJbSb(p~tFcuG4zh6}hfl@ues6CfJu<-P+!>FlYMlD_3!E z9$6VE==tlxNYe(s;@8@+4c4jQ$R2g8t0QwE>Et|)5)@kJj6^yaqFYY?0LEM2C!+7+ z+FN|UxR1GCy1KA`{T_%24U+Vserchr5h`;U7TZPr@43x#MMN{@vV?KSII}R@5k`7cVK}E;c)$f~_{ZLDOoL|-01p~oafxi4F zG$?Wha&a*rTnz-nTI-bAJ*SLb!5(L!#iRdvLEyo>7D_=H78-qZrm=6{hkUR{tR{H! z`ZTOV$Oi6^qX5=_{f}V9h}WJAO%h9)kEUF#*-JyYDbOGZ>Nfs%7L}4p zopIul&&Bbn!C9o83ypC6W4F$X=_|pex$V4!Whm#48Wfm3*oAW0Gc&#&b+oq<8>aZR z2BLpouQQwyf$aHpQUK3pMRj(mS^^t#s$IC3{j*m9&l7sQt@RU{o_}N-xI_lh`rND^ zX~-8$o(;p^wf3_5-WZ^qgW`e8T@37{`J)e2KJdSSCUpX6KZu0Ga&U*+u3*PDAs1uK zpl)40+fROA@Vo#vK?^@Pq%w8DO9HdfmH+~vNinZ$5GRz?sD|k246NepqZd`>81P^P z#x#3kUS-}x4k%&~iEUrsb&-X#_;;?y9oCP4crMkC`=q58#NxQ| z*NXNA;GR4X=GiGXwab5=&M3j04fQw%2UxM`S(aE)_PlgJttBX96$$lY@Q%0xV^IbcHqzw^Uk&E=vFB;EQ@kzVIeM8lDIW_Q_ zrfy)l6s2QBApF;J2xTD_@wuNMlwDfsdfMyzRq)<>qG{M)Yt}9F1{1HaI_X7=F=7>& zYB54VaKlxu0lIgS;Ac&25Aw(tcf@K~(cvPi8(OChzhlYp6}#<_MVhU95sD&)n0FtL zmxm4w$~s(S9jmHOgyovpG!x4uLfJsMsJn^QMraKAa1Ix?{zkV!a7{f%-!u2{NqZ&) zo+^XB`eFQ4 zk-(;_>T#pTKyvW${yL|XXbcv?CE2Tp<3(PjeXhu^Jrp6^Mj}lg_)jamK{g;C+q^Da ztb!gV!q5)B7G1%lVanA2b>Xs?%hzCgJ{Hc!ldr9dnz7k^xG#4pDpr|0ZmxxiUVl}j zbD_rg3yAFQ>nnc)0>71D==715jRj4XsRb2#_lJoSOwky&c4957V-|m)@>b^Nak1!8 z@DsIOS8>Oe^T>tgB)WX3Y^I^65Uae+2M;$RxX_C)Aoo0dltvoRRIVQkpnegWj;D#G z+TwFIRUN%bZW3(K{8yN8!(1i0O!X3YN?Zo08L5D~)_tWQA8&|CvuQb8Od?p_x=GMF z-B@v9iNLYS1lUsbb`!%f5+1ev8RFPk7xyx5*G;ybRw(PW*yEZ$unu2`wpH)7b@ZXEz4Jr{?KZKYl!+3^)Q z)~^g?KlPGtT!{yQU&(Z&^rVjPu>ueeZN86AnhRwc)m|;5NvM&W3xD%n`+Hjg5$e8M zKh1Ju82L~&^ z-IQ5bYhsjqJfr38iwi~8<{oeREh|3l)*Enj4&Q$+mM$15YqwXeufK9P^(O=pj=F-1 zD+&REgwY~!W#ZPccSEi(*jiKJ5)Q|zX;hP}S2T9j_);epH9JQs{n>RG}{Nak)vIbfa zFQm?H;D+tzrBN2)6{?Mo%fzN6;6d_h0Qyn61)+XT63=!T*WQyRUoB_x0_)Ir`$FtS zak07C(mOaWN5m%bk?F9X&@mEVKN%{R6obt(9qw&p>w&p;R*l2th9$D^*`pC}NmB+v z>bk;OJ(C8p$G;jNvRsBbt=a!!tKnjJ`9*yQFgjEN1HcC<&>u9aStT3>Oq=MOQV!#WOZ6{cv$YVmlJdovPRV}<=IZUPeBVh5DC z91-?kimq3JUr;UMQ@0?h52gupvG=~(5AVdP(2(%*sL8!#K1-L$9B7MrWGdt(h&whR@vz~0oEHF8u3U1Q zdGdaIytJj4x@eF*E+^zgi{nPCA8tkjN}UoR8WhDzM3-zLqx0z?2tTdDKyENM={fp8VC@3Dt`AiK$;K#H$K2{08mrHG%jgEOLX3MCsG>afZm_0mLPS4jmYUJp~Dm! z5AUe_vEaOAT3zWdwl#cLvqwd1^lwW?gt7(92wEsOE6c#<0}{szFV4(uO70?3>=((! zQr}1{J?Wx2ZmjxYL_8OB*m&mimfojzYn~PiJ2g8R&ZRx-i^yF#sdhEWXAUIZ@J?T$ zs3PgT2<&Ki>Bob_n(@S>kUIvE+nY~ti9~6j;O9VAG#{oZ!DZCW)}i6iA!Tgsyz+hC z1VVyvbQ_nwgdZSEP=U4d#U`2*`e~d4y8uM4Bcmm%!jidaee#4WqN!ZnlBmbYpuaO! z!rU3`Kl2 z0O7PD&fQ|_b)Ub!g9^s;C2e>1i*2&?1$6yEn?~Y zI)-WIN8N(5s9;grW+J@K@I%g#?G&hzmlgV=L}ZA{f>3YCMx^P{u@c5Z;U1qmdk#)L zvX6z1!sL>+@vxO8qVn#k3YxYi?8ggV){?Rn@j$+Fd4-QkuH1@)j#3-=f82GZ!nl~{ zzZ(?kO`ANttVeHSo%xmH!NmNZECh*{s!-8S>ALoe5xOPs>|P5BbUmP@rlV8`d(c=7 zypcpLaI*FM^;GM%@q`GAb8kO`$oE|R48yn)?p(c1t>5;Wwn5r6ck&uw4}TnT80jI`IS~J%q8CpaVgIze<8IykSpVBg8~E! zW_tGqB;GO47r_er05y+Kwrcn{VLxL*1;HMv@*sd}MB6DH4zaP~u4Y;>@Nw7?F8S?c zfVIY(^ntnGgWlD|idzGz$Y+Oh(Ra=&VIf4!K2W*a)(%5%78s}8qxOknAGtDAq+HMO zM+Nu;0OgQRn36 zA@~a8`uVQ~v9?d!BxnsVaB-z-djypO44BjQAmg7&eVoaew|~)wH$SgefJ2$7_RiY+ z_7ACGoFM6Lhvho+eUG@pU&0X(Uy(*j;9pr?ET?FHTXadlfXC|MReZoU5>AG`mTM<% zc~*I@E*u0|hwVTdFA~4^b2VT7_~}~tCueNY{de3og=ASFQ`)0dhC2~Ne<}}Rc?ptA zi}+bQE%N9o*hpSUMH)9xt%Zlz&^p&5=cW}{m#f85iVX64^{!(vhClT<I)+c)RuiyrZqIw4v`z%YK&;_Fh4_+0B?qAGxMfAM`LzG_bjD>ib4;KGT4_1I>sxvL&&qp40ajgQOqIE^9=Az4w#ymo)bW-Vg{T!n=l&|nR_ zw+wcH|FxUH63)~{M;goHepmD{Fe?W9sO|eJP9L$G<{e_7FxxuXQ+)(Z^@;X8I1=%k zTK$gbHA1^4W<`q~ubQ0M_C^CA5#Z&*nGc(T?4Y_2jLu&FJDQYpCSiRny->$+nC9Jl z?avTW`ZXYT51%SrEq!}dXNM&!pM6nmL^lce=%S7{_TS)ckN8;{p*LT~LMgmlE~dpL zEBQy-jDj%cSK6N3)|CCR0LQ$N6iDM~+-1Oz|LAdkip(VZcO`gqCuJ+(Mm{m6@P%_; zBtF|MMVMP;E`5NJ{&@4j^JE5j&}(Jq{lCGL(P^#uqvbD`2)FVyfNgy|pvT!XY;02Z zZWbgGsvi6#!*$Zxwd{Xk6_M{+^yV_K@%_SAW(x)Lg|*AuG-%g2#GQYk8F?W&8|2dU z;00ppzrQnnYXnT`(S%_qF2#QNz&@Y$zcq+O8p>Gto2&4z8(^#cY?DuQwBQP4Fe?qUK_-yh4xT{8O@gb`uh` z>Q%jrgPAnANn4_)->n;w{Mei#J)F+`12&+-MLKSRzF6bL3;4O~oy~v7 zL0K-=m?>>(^qDCgvFRLBI@`04EGdTxe5}xBg#7#Wb!aUED;?5BLDEvZ@tai4*Rh8& z4V)cOr}DJ0&(FjWH%50Y+&=WtB42^eEVsmaHG)Il#j265oK&Bot(+-IIn`6InmuE# z;)qXs+X{fSb8^rYb#46X5?KCzH9X0>ppBQi(aKS--;4yA%0N|D<#8RZlOS(8n26=u zv~y;KC>`ypW=aqj`&x9 z0Zm>NKp}hPJu1+QDo(_U(Gt0SZ`IJWnp%QK`pye>Bm!w{sG>;VU^2 z4lZhV1}tCE8(?zu#j99|l3-qRBcz3bG+DlyxPGB$^6B^ssc_qYQ6lG0q~EAI?1$?( zahfn%etVvuKwB7R=>JDQluP97nLDM6*5;b0Ox#b{4nIgZA*+?IvyDN{K9WGnlA=Ju z+)6hjr}{;GxQQIDr3*lf32lRp{nHP8uiz^Fa|K+dUc@wD4Kf5RPxVkUZFCdtZH{+=c$AC)G2T-Qn@BPbr zZigIhKhKrVYy`!Mlc#HVr=CURVrhUjExhI~gZ%a=WM9BwvnN?=z!_ZQ$(sP?X;2Jy zyI$}H^^SvH2tf6+Uk$pJww@ngzPp856-l9g6WtW+%Yf>N^A}->#1W2n=WJ%sZ0<){Z&#% z^Kzl$>Km)sIxKLFjtc;}bZeoaZSpL4>`jCmAeRM-NP9sQ&-mi@p0j7Iq>1n&z@8?M z%dM7K^SgE5z)@i5w#rLE4+8%|^J`a6wYr`3BlvdD>7xW?Dd>`0HC0o{w7r_ot~h*G z2gI7Y!AUZ6YN+z$=GNzns@Tu7BxgAb3MBha30-ZG7a%rckU5}y{df`lj@^+34kr5> z988PPbWYdHye~=?>uZ4N&MN@4RBLk_?9W*b$}jqt0j%>yO9QOV(*!#cX~=wRdVL&S zhPQ{${0CGU-rfdS&b@u|IK{hV2Z=(*B2d0?&jwWfT=?Gk`4T9TfMQ)CfNgpLQa#>Q z%6A$w#QNc&qOtrHAbqY>J782@!X{9Y@N(HMSr;PP^;0DlJNxfC`oMB%Ocg zC*hnEsF|p*=CVe^dT)>BTL0yff)uo!U<+_2o3p)CE8quU1JI(=6)9$KxVdJYD*S*~ zzNeSkzFIQyqK}578+qq6X8rrRdgX z4k&R=AGex~a)MoB0pK&|yA<(*J#P&tR?ImBVD)ZTA4VH5L5DxXe<-*s`Aox%H1{-^Qa`kG_DGXD%QX-;l1#&#IVQP6>kir ztO@~ZvJDPnTvKt>fc*(j$W^)JhWk{4kWwbpFIXzuPt2V%M4H19-i5Gn*6(D`4_c1+ zYoI1@yT^~9JF~t>2eVM6p=GP3b*;daJpQOhAMNO|LKnwE2B5n8y9mf;q=)-L_FfD0 z<}YIRBO{k)6AHAn8iG>pYT+3bJ7jvP9}LSMR1nZW$5HR%PD1rFz z{4XE^Vmi-QX#?|Farz=CYS_8!%$E#G%4j2+;Avz|9QBj|YIExYk?y-1(j}0h{$$MnC_*F0U2*ExSi1ZCb_S9aV zTgyGP0Cl=m`emxM4Qih1E{`J{4oJo8K}WnH`@js^pR7Z-vTBK5F5JIFCDN}7pU^_nV>NTz@2$|Kcc5o+L&^Db_AQ);F?)X5BF*QJRCdLI-a%gW z++DZM)x=6*fNrSaUA&hf&CUqC$F*y^CJC-MAm9gd*5#^mh;-dR1?a&<3-hp3@}XN! z&8dcwo6=MQua%0KFvYbi>O{j)RrbDQo3S*y!oEJ~2=}^-v%zn~@hnmKGOvX6JLr;>DNC3)={8OM9n5Zs*(DlS*|%JTniJX2Uav7sOFT0vdIiUOC5pEtY?EF)@Fh9pCfD%N zXskZ8b^ldI{HHj{-l?iWo@IW6Nr`hAS>f8S*8FGc*gmcK^f2JS+>I&r#Gcewy=-JM zv0*w<5qBa6UQB@`esOG*4*t@7c9AkrTpM`v=eY?cO#z17H9B%Xy4m!}LhW}*iZ27w1?HrevgB1SZ1q2X$mm@FK@Qt7o z!s~Lio^IRdwzyvQ80{5iYeTV@mAo=2o5>KepRH0d{*Szlg~n%w2)S5v2|K8}pj;c{ zoDRLvYJO1@?x-=mq+LVhD{l-1-Dw4`7M?3@+ z`fu7?1#9W++6Y46N=H0+bD|CJH~q*CdEBm8D##VS7`cXy4~+x=ZC17rJeBh zI~qW^&FU`+e!{AKO3(>z5Ghh14bUT$=4B>@DVm(cj* zSLA*j!?z!=SLuVvAPh_EFKx}JE8T8;Gx)LH^H136=#Jn3Bo*@?=S`5M{WJPY&~ODs z+^V57DhJ2kD^Z|&;H}eoN~sxS8~cN5u1eW{t&y{!ouH`%p4(yDZaqw$%dlm4A0f0| z8H}XZFDs?3QuqI^PEy}T;r!5+QpfKEt&V|D)Z*xoJ?XXZ+k!sU2X!rcTF4tg8vWPM zr-JE>iu9DZK`#R5gQO{nyGDALY!l@M&eZsc*j*H~l4lD)8S?R*nrdxn?ELUR4kxK? zH(t9IM~^mfPs9WxR>J{agadQg@N6%=tUQ8Bn++TC|Hbqn*q;WydeNIS@gt|3j!P`w zxCKoeKQ*WBlF%l4-apIhERKl(hXS1vVk$U?Wifi)&lL6vF@bmFXmQEe{=$iG)Zt*l z0df@_)B-P_^K2P7h=>OIQ6f0Q-E@|M?$Z5n^oN>2_sBCpN>q(LnqUoef{tm^5^L$# z{<SL zKmH78cHX`4cBKIY8u1x*lwrgP^fJ%E&&AmHrRY7^hH*=2OA9K?!+|~Aeia=nAA`5~ z#zI=h#I>@FXaGk(n)0uqelNY;A5I9obE~OjsuW!%^NxK*52CfBPWYuw--v<1v|B>h z8R=#$TS-Pt3?d@P+xqmYpL4oB8- z>w99}%xqy9W!A^ODfLq8iA@z}10u?o#nG#MXumSaybi(S{`wIM z&nE3n2gWWMu93EvtofWzvG2{v;$ysuw^8q?3n}y=pB1vUr5gi++PjiyBH3jzKBRny zSO~O++1ZLdy7v7VzS&$yY;^Z7*j_#BI`PK`dAzJa9G1{9ahPqPi1C}ti+L)WHii*= z+RZ^+at-tlatc4|akPa&9H;%gn9aS`X_kfb>n>#NTyUVM6m4NCIfLm(28>qaYv7}t zn`M;XcONtXoa3#u3{L-ytd_&g z2mO$8CnE?460w#eSm|smlnNwFHM;A&IxSKLzVkV7nNVqZ*A`)eI{Nbg6WxsarAFuc=FFf1z|%#eTvBgUhY}N zsCT>`_YO>14i^vFX0KXbARLItzT{TeD%N~=ovGtZ6j{>PxkuYlHNTe0!u>rgw#?td z{)n=QrGvgCDE6BUem$Rh(1y!$@(Bn!k3E0|>PQ(8O==zN`?yBhAqlWyq+c%+h?p^- zE&OtLind}^_=>pbhxOgOIC0q9{cLK6p6*eg_|S+p9$W~_u4wzx@N?$QmFg2S)m~^R znni$X{U*!lHgdS@fI;|Owl=9Gwi?dr0m#>yL<8<}bLW_Kpl| zSGesADX&n?qmHC`2GyIev^hi~ka}ISZ^Y4w-yUzyPxaJB0mm%ww^>if3<;P^U+L5=s+cifT-ct*;!dOOk#SOZNv@a^J|DrS3YtSn8EEAlabX1NV3RfHwZn_41Xa z4;$taa6JJR()-FQ<#0G~WlML<l5I+IPnqDpW(PP>hRcQ+S2zU?tbG^(y z1K_?1R){jF;OKGw0WYjnm>aPxnmr5?bP?^B-|Fv`TT4ecH3O`Z3`X_r;vgFn>t1tE zGE6W2PODPKUj+@a%3lB;lS?srE5lp(tZ;uvzrPb){f~n7v_^z! z=16!Vdm!Q0q#?jy0qY%#0d^J8D9o)A;Rj!~j%u>KPs-tB08{4s1ry9VS>gW~5o^L; z7vyjmfXDGRVFa@-mis2!a$GI@9kE*pe3y_C3-$iVGUTQzZE+%>vT0=r|2%xMDBC@>WlkGU4CjoWs@D(rZ zS1NB#e69fvI^O#5r$Hj;bhHPEE4)4q5*t5Gyjzyc{)o459VkEhJ$%hJUC&67k z7gdo`Q*Jm3R&?ueqBezPTa}OI9wqcc;FRTcfVXob^z|dNIB0hMkHV26$zA%YgR$sM zTKM61S}#wJ#u+0UDE3N+U*~Tz1nnV;W<8Akz&6M7-6mIF(Pq`wJ1A%loYL( zIS;&2((xbyL7zoyaY2Sa%BBYBxo6Aa*53`~e@|RA`MP+?iI4KZ+y4EU&I zS_|(#*&j2hxpELa3r0O7ok&5!ijRiRu9i-_3cdnydZU9Mp6Y);skv%!$~`i-J7e-g zj@EoHf+gtcrKf;tY5`4iLnWSHa)9brUM$XmEzG3T0BXTG_+0}p7uGLs^(uYh0j$;~ zT1&~S%_Y5VImvf1EkD7vP-@F%hRlBe{a@T!SW(4WEQd1!O47*Crf@u-TS==48iR5x z!*`Ul4AJI^vIVaN3u5UifXBX{fJ@z>4Q2#1?jpcdLocwymBgKrZ+^Cb@QuIxl58B* zD{t-W3;M;{MGHm_@&n(6A-AsD;JO#>J3o4ru{hy;k;8?=rkp0tadEEcHNECoTI(W31`El-CI0eWQ zWD4&2ehvACkLCjG`82T`L^cNNC4Oo2IH(T4e;C75IwkJ&`|ArqSKD}TX_-E*eeiU& ziUuAC)A?d>-;@9Jcmsdca>@q1`6vzo^3etEH%1Gco&gvC{;Y-qyJ$Re`#A!5Kd((5 z6sSiKnA20uPX0**Mu&6tNgTunUR1sodoNmDst1&wz8v7AG3=^huypTi`S7+GrO$D6 z)0Ja-y5r?QQ+&jVQBjitIZ`z2Ia}iXWf#=#>nU+ zL29$)Q>f#o<#4deo!Kuo@WX{G(`eLaf%(_Nc}E`q=BXHMS(Os{!g%(|&tTDIczE_# z5y%wjCp9S?&*8bS3imJi_9_COC)-_;6D9~8Om@?U2PGQpM^7LKG7Q~(AoSRgP#tZfVDF_zr;_U*!F9qsbVQ@un9O2>T4M5tr0B~~v_@a=w^8h510a#=L z;8+9zhV}57uajb+9DbZm1G`_NqOuKN`bQ2fw9A*v*Kdb_E-SA`?2 z)OFIY-%uD`JZUZg?D4lHtNegKgWr!1m%hOpu5`R+bZ2K#&)*R-7ElKYo0$0xYxIL8 zLg%u|4oZixz}ILB-@aS4=XOe)z!VL6@?dX{LW^YCPjKtyw44)xT=H;h(fmFr>R?p%r5*}W z7_bo0drVDRq9V9QL4_!dazughK6t}tVVvBq={T0+3(1zmb>f+|;{D%J?^xnZcqio5 z%H?@L+L-CIdO=x6QrALL9&PwvjrZi5NS)1e<*%V8ntw~S2PF}zH}B5f_DHyB=I3m@ z_;^TpN|sesCU}qxQ`~jIwF>#8wGvxg9kdMT$}us8BM&W>OzZ|ry2BB)+UY*_yH+&L zl_=Jy9BNzIZs}D~Yv_H%HPjVGNV=xT3xpIW!Np1F^G#9Y8X zl)c_V1(DhYu-v%H3-m&n%M_}}c{E5Wu+6*>R24gW_A7$(U=9D|H$r;;;@o zJ)c_CmVf9l*;4SyJ}E{+4)}^C>SIJ*_bul7OJ{v&0oO>jG(5xzYP0$I%*YH|Mwu#r zubNW5VZ9^X#Phw<;?=^G?Kg&C)^x1FVsKGZ*n+{C1znj~YHSP?6PS(k5e9qGvS4X* z=1kA_27(iV65a(i+Sicmd@Vzf^2@*Wed-`aYQ~em=-h%Pu`gHfz)&@$hpr<&mNO={ zl^kI0HP0wTbbh{d(>5a#;zT2_=ppef?;D4;2^}&kZjB^yl%LBJ;|> zkLc)JEg*5rpQ;_)w?PnKynWtv!@ z>}+am{@(g$KKM+e$ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/macos/Runner/Configs/AppInfo.xcconfig b/app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..38c93ba6 --- /dev/null +++ b/app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.app + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. diff --git a/app/macos/Runner/Configs/Debug.xcconfig b/app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..b3988237 --- /dev/null +++ b/app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/app/macos/Runner/Configs/Release.xcconfig b/app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..d93e5dc4 --- /dev/null +++ b/app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/app/macos/Runner/Configs/Warnings.xcconfig b/app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..fb4d7d3f --- /dev/null +++ b/app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/app/macos/Runner/DebugProfile.entitlements b/app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..51d09670 --- /dev/null +++ b/app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/app/macos/Runner/Info.plist b/app/macos/Runner/Info.plist new file mode 100644 index 00000000..3733c1a8 --- /dev/null +++ b/app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/app/macos/Runner/MainFlutterWindow.swift b/app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..4cb95dc9 --- /dev/null +++ b/app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/app/macos/Runner/Release.entitlements b/app/macos/Runner/Release.entitlements new file mode 100644 index 00000000..04336df3 --- /dev/null +++ b/app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/app/windows/.gitignore b/app/windows/.gitignore new file mode 100644 index 00000000..ec4098aa --- /dev/null +++ b/app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/app/windows/CMakeLists.txt b/app/windows/CMakeLists.txt new file mode 100644 index 00000000..c4363c4c --- /dev/null +++ b/app/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +project(app LANGUAGES CXX) + +set(BINARY_NAME "app") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/app/windows/flutter/CMakeLists.txt b/app/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..c10f4f62 --- /dev/null +++ b/app/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.15) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..ddfcf7c3 --- /dev/null +++ b/app/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherPlugin")); +} diff --git a/app/windows/flutter/generated_plugin_registrant.h b/app/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..9846246b --- /dev/null +++ b/app/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,13 @@ +// +// Generated file. Do not edit. +// + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/app/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..411af46d --- /dev/null +++ b/app/windows/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/app/windows/runner/CMakeLists.txt b/app/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..e9932176 --- /dev/null +++ b/app/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/app/windows/runner/Runner.rc b/app/windows/runner/Runner.rc new file mode 100644 index 00000000..3ac0062f --- /dev/null +++ b/app/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "A new Flutter project." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "app.exe" "\0" + VALUE "ProductName", "app" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/app/windows/runner/flutter_window.cpp b/app/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..ac04f779 --- /dev/null +++ b/app/windows/runner/flutter_window.cpp @@ -0,0 +1,64 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opporutunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/app/windows/runner/flutter_window.h b/app/windows/runner/flutter_window.h new file mode 100644 index 00000000..ba86031c --- /dev/null +++ b/app/windows/runner/flutter_window.h @@ -0,0 +1,39 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "run_loop.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The run loop driving events for this window. + RunLoop* run_loop_; + + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/app/windows/runner/main.cpp b/app/windows/runner/main.cpp new file mode 100644 index 00000000..81da1d1c --- /dev/null +++ b/app/windows/runner/main.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"app", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/app/windows/runner/resource.h b/app/windows/runner/resource.h new file mode 100644 index 00000000..ddc7f3ef --- /dev/null +++ b/app/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/app/windows/runner/resources/app_icon.ico b/app/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 zcmeHQc|26z|35SKE&G-*mXah&B~fFkXr)DEO&hIfqby^T&>|8^_Ub8Vp#`BLl3lbZ zvPO!8k!2X>cg~Elr=IVxo~J*a`+9wR=A83c-k-DFd(XM&UI1VKCqM@V;DDtJ09WB} zRaHKiW(GT00brH|0EeTeKVbpbGZg?nK6-j827q-+NFM34gXjqWxJ*a#{b_apGN<-L_m3#8Z26atkEn& ze87Bvv^6vVmM+p+cQ~{u%=NJF>#(d;8{7Q{^rWKWNtf14H}>#&y7$lqmY6xmZryI& z($uy?c5-+cPnt2%)R&(KIWEXww>Cnz{OUpT>W$CbO$h1= z#4BPMkFG1Y)x}Ui+WXr?Z!w!t_hjRq8qTaWpu}FH{MsHlU{>;08goVLm{V<&`itk~ zE_Ys=D(hjiy+5=?=$HGii=Y5)jMe9|wWoD_K07(}edAxh`~LBorOJ!Cf@f{_gNCC| z%{*04ViE!#>@hc1t5bb+NO>ncf@@Dv01K!NxH$3Eg1%)|wLyMDF8^d44lV!_Sr}iEWefOaL z8f?ud3Q%Sen39u|%00W<#!E=-RpGa+H8}{ulxVl4mwpjaU+%2pzmi{3HM)%8vb*~-M9rPUAfGCSos8GUXp02|o~0BTV2l#`>>aFV&_P$ejS;nGwSVP8 zMbOaG7<7eKD>c12VdGH;?2@q7535sa7MN*L@&!m?L`ASG%boY7(&L5imY#EQ$KrBB z4@_tfP5m50(T--qv1BJcD&aiH#b-QC>8#7Fx@3yXlonJI#aEIi=8&ChiVpc#N=5le zM*?rDIdcpawoc5kizv$GEjnveyrp3sY>+5_R5;>`>erS%JolimF=A^EIsAK zsPoVyyUHCgf0aYr&alx`<)eb6Be$m&`JYSuBu=p8j%QlNNp$-5C{b4#RubPb|CAIS zGE=9OFLP7?Hgc{?k45)84biT0k&-C6C%Q}aI~q<(7BL`C#<6HyxaR%!dFx7*o^laG z=!GBF^cwK$IA(sn9y6>60Rw{mYRYkp%$jH z*xQM~+bp)G$_RhtFPYx2HTsWk80+p(uqv9@I9)y{b$7NK53rYL$ezbmRjdXS?V}fj zWxX_feWoLFNm3MG7pMUuFPs$qrQWO9!l2B(SIuy2}S|lHNbHzoE+M2|Zxhjq9+Ws8c{*}x^VAib7SbxJ*Q3EnY5lgI9 z=U^f3IW6T=TWaVj+2N%K3<%Un;CF(wUp`TC&Y|ZjyFu6co^uqDDB#EP?DV5v_dw~E zIRK*BoY9y-G_ToU2V_XCX4nJ32~`czdjT!zwme zGgJ0nOk3U4@IE5JwtM}pwimLjk{ln^*4HMU%Fl4~n(cnsLB}Ja-jUM>xIB%aY;Nq8 z)Fp8dv1tkqKanv<68o@cN|%thj$+f;zGSO7H#b+eMAV8xH$hLggtt?O?;oYEgbq@= zV(u9bbd12^%;?nyk6&$GPI%|+<_mEpJGNfl*`!KV;VfmZWw{n{rnZ51?}FDh8we_L z8OI9nE31skDqJ5Oa_ybn7|5@ui>aC`s34p4ZEu6-s!%{uU45$Zd1=p$^^dZBh zu<*pDDPLW+c>iWO$&Z_*{VSQKg7=YEpS3PssPn1U!lSm6eZIho*{@&20e4Y_lRklKDTUCKI%o4Pc<|G^Xgu$J^Q|B87U;`c1zGwf^-zH*VQ^x+i^OUWE0yd z;{FJq)2w!%`x7yg@>uGFFf-XJl4H`YtUG%0slGKOlXV`q?RP>AEWg#x!b{0RicxGhS!3$p7 zij;{gm!_u@D4$Ox%>>bPtLJ> zwKtYz?T_DR1jN>DkkfGU^<#6sGz|~p*I{y`aZ>^Di#TC|Z!7j_O1=Wo8thuit?WxR zh9_S>kw^{V^|g}HRUF=dcq>?q(pHxw!8rx4dC6vbQVmIhmICF#zU!HkHpQ>9S%Uo( zMw{eC+`&pb=GZRou|3;Po1}m46H6NGd$t<2mQh}kaK-WFfmj_66_17BX0|j-E2fe3Jat}ijpc53 zJV$$;PC<5aW`{*^Z6e5##^`Ed#a0nwJDT#Qq~^e8^JTA=z^Kl>La|(UQ!bI@#ge{Dzz@61p-I)kc2?ZxFt^QQ}f%ldLjO*GPj(5)V9IyuUakJX=~GnTgZ4$5!3E=V#t`yOG4U z(gphZB6u2zsj=qNFLYShhg$}lNpO`P9xOSnO*$@@UdMYES*{jJVj|9z-}F^riksLK zbsU+4-{281P9e2UjY6tse^&a)WM1MFw;p#_dHhWI7p&U*9TR0zKdVuQed%6{otTsq z$f~S!;wg#Bd9kez=Br{m|66Wv z#g1xMup<0)H;c2ZO6su_ii&m8j&+jJz4iKnGZ&wxoQX|5a>v&_e#6WA!MB_4asTxLRGQCC5cI(em z%$ZfeqP>!*q5kU>a+BO&ln=4Jm>Ef(QE8o&RgLkk%2}4Tf}U%IFP&uS7}&|Q-)`5< z+e>;s#4cJ-z%&-^&!xsYx777Wt(wZY9(3(avmr|gRe4cD+a8&!LY`1^T?7x{E<=kdY9NYw>A;FtTvQ=Y&1M%lyZPl$ss1oY^Sl8we}n}Aob#6 zl4jERwnt9BlSoWb@3HxYgga(752Vu6Y)k4yk9u~Kw>cA5&LHcrvn1Y-HoIuFWg~}4 zEw4bR`mXZQIyOAzo)FYqg?$5W<;^+XX%Uz61{-L6@eP|lLH%|w?g=rFc;OvEW;^qh z&iYXGhVt(G-q<+_j}CTbPS_=K>RKN0&;dubh0NxJyDOHFF;<1k!{k#7b{|Qok9hac z;gHz}6>H6C6RnB`Tt#oaSrX0p-j-oRJ;_WvS-qS--P*8}V943RT6kou-G=A+7QPGQ z!ze^UGxtW3FC0$|(lY9^L!Lx^?Q8cny(rR`es5U;-xBhphF%_WNu|aO<+e9%6LuZq zt(0PoagJG<%hyuf;te}n+qIl_Ej;czWdc{LX^pS>77s9t*2b4s5dvP_!L^3cwlc)E!(!kGrg~FescVT zZCLeua3f4;d;Tk4iXzt}g}O@nlK3?_o91_~@UMIl?@77Qc$IAlLE95#Z=TES>2E%z zxUKpK{_HvGF;5%Q7n&vA?`{%8ohlYT_?(3A$cZSi)MvIJygXD}TS-3UwyUxGLGiJP znblO~G|*uA^|ac8E-w#}uBtg|s_~s&t>-g0X%zIZ@;o_wNMr_;{KDg^O=rg`fhDZu zFp(VKd1Edj%F zWHPl+)FGj%J1BO3bOHVfH^3d1F{)*PL&sRX`~(-Zy3&9UQX)Z;c51tvaI2E*E7!)q zcz|{vpK7bjxix(k&6=OEIBJC!9lTkUbgg?4-yE{9+pFS)$Ar@vrIf`D0Bnsed(Cf? zObt2CJ>BKOl>q8PyFO6w)+6Iz`LW%T5^R`U_NIW0r1dWv6OY=TVF?N=EfA(k(~7VBW(S;Tu5m4Lg8emDG-(mOSSs=M9Q&N8jc^Y4&9RqIsk(yO_P(mcCr}rCs%1MW1VBrn=0-oQN(Xj!k%iKV zb%ricBF3G4S1;+8lzg5PbZ|$Se$)I=PwiK=cDpHYdov2QO1_a-*dL4KUi|g&oh>(* zq$<`dQ^fat`+VW?m)?_KLn&mp^-@d=&7yGDt<=XwZZC=1scwxO2^RRI7n@g-1o8ps z)&+et_~)vr8aIF1VY1Qrq~Xe``KJrQSnAZ{CSq3yP;V*JC;mmCT6oRLSs7=GA?@6g zUooM}@tKtx(^|aKK8vbaHlUQqwE0}>j&~YlN3H#vKGm@u)xxS?n9XrOWUfCRa< z`20Fld2f&;gg7zpo{Adh+mqNntMc-D$N^yWZAZRI+u1T1zWHPxk{+?vcS1D>08>@6 zLhE@`gt1Y9mAK6Z4p|u(5I%EkfU7rKFSM=E4?VG9tI;a*@?6!ey{lzN5=Y-!$WFSe z&2dtO>^0@V4WRc#L&P%R(?@KfSblMS+N+?xUN$u3K4Ys%OmEh+tq}fnU}i>6YHM?< zlnL2gl~sF!j!Y4E;j3eIU-lfa`RsOL*Tt<%EFC0gPzoHfNWAfKFIKZN8}w~(Yi~=q z>=VNLO2|CjkxP}RkutxjV#4fWYR1KNrPYq5ha9Wl+u>ipsk*I(HS@iLnmGH9MFlTU zaFZ*KSR0px>o+pL7BbhB2EC1%PJ{67_ z#kY&#O4@P=OV#-79y_W>Gv2dxL*@G7%LksNSqgId9v;2xJ zrh8uR!F-eU$NMx@S*+sk=C~Dxr9Qn7TfWnTupuHKuQ$;gGiBcU>GF5sWx(~4IP3`f zWE;YFO*?jGwYh%C3X<>RKHC-DZ!*r;cIr}GLOno^3U4tFSSoJp%oHPiSa%nh=Zgn% z14+8v@ygy0>UgEN1bczD6wK45%M>psM)y^)IfG*>3ItX|TzV*0i%@>L(VN!zdKb8S?Qf7BhjNpziA zR}?={-eu>9JDcl*R=OP9B8N$IcCETXah9SUDhr{yrld{G;PnCWRsPD7!eOOFBTWUQ=LrA_~)mFf&!zJX!Oc-_=kT<}m|K52 z)M=G#;p;Rdb@~h5D{q^K;^fX-m5V}L%!wVC2iZ1uu401Ll}#rocTeK|7FAeBRhNdQ zCc2d^aQnQp=MpOmak60N$OgS}a;p(l9CL`o4r(e-nN}mQ?M&isv-P&d$!8|1D1I(3-z!wi zTgoo)*Mv`gC?~bm?S|@}I|m-E2yqPEvYybiD5azInexpK8?9q*$9Yy9-t%5jU8~ym zgZDx>!@ujQ=|HJnwp^wv-FdD{RtzO9SnyfB{mH_(c!jHL*$>0o-(h(eqe*ZwF6Lvu z{7rkk%PEqaA>o+f{H02tzZ@TWy&su?VNw43! z-X+rN`6llvpUms3ZiSt)JMeztB~>9{J8SPmYs&qohxdYFi!ra8KR$35Zp9oR)eFC4 zE;P31#3V)n`w$fZ|4X-|%MX`xZDM~gJyl2W;O$H25*=+1S#%|53>|LyH za@yh+;325%Gq3;J&a)?%7X%t@WXcWL*BaaR*7UEZad4I8iDt7^R_Fd`XeUo256;sAo2F!HcIQKk;h})QxEsPE5BcKc7WyerTchgKmrfRX z!x#H_%cL#B9TWAqkA4I$R^8{%do3Y*&(;WFmJ zU7Dih{t1<{($VtJRl9|&EB?|cJ)xse!;}>6mSO$o5XIx@V|AA8ZcoD88ZM?C*;{|f zZVmf94_l1OmaICt`2sTyG!$^UeTHx9YuUP!omj(r|7zpm5475|yXI=rR>>fteLI+| z)MoiGho0oEt=*J(;?VY0QzwCqw@cVm?d7Y!z0A@u#H?sCJ*ecvyhj& z-F77lO;SH^dmf?L>3i>?Z*U}Em4ZYV_CjgfvzYsRZ+1B!Uo6H6mbS<-FFL`ytqvb& zE7+)2ahv-~dz(Hs+f})z{*4|{)b=2!RZK;PWwOnO=hG7xG`JU5>bAvUbdYd_CjvtHBHgtGdlO+s^9ca^Bv3`t@VRX2_AD$Ckg36OcQRF zXD6QtGfHdw*hx~V(MV-;;ZZF#dJ-piEF+s27z4X1qi5$!o~xBnvf=uopcn7ftfsZc zy@(PuOk`4GL_n(H9(E2)VUjqRCk9kR?w)v@xO6Jm_Mx})&WGEl=GS0#)0FAq^J*o! zAClhvoTsNP*-b~rN{8Yym3g{01}Ep^^Omf=SKqvN?{Q*C4HNNAcrowIa^mf+3PRy! z*_G-|3i8a;+q;iP@~Of_$(vtFkB8yOyWt2*K)vAn9El>=D;A$CEx6b*XF@4y_6M+2 zpeW`RHoI_p(B{%(&jTHI->hmNmZjHUj<@;7w0mx3&koy!2$@cfX{sN19Y}euYJFn& z1?)+?HCkD0MRI$~uB2UWri})0bru_B;klFdwsLc!ne4YUE;t41JqfG# zZJq6%vbsdx!wYeE<~?>o4V`A3?lN%MnKQ`z=uUivQN^vzJ|C;sdQ37Qn?;lpzg})y z)_2~rUdH}zNwX;Tp0tJ78+&I=IwOQ-fl30R79O8@?Ub8IIA(6I`yHn%lARVL`%b8+ z4$8D-|MZZWxc_)vu6@VZN!HsI$*2NOV&uMxBNzIbRgy%ob_ zhwEH{J9r$!dEix9XM7n&c{S(h>nGm?el;gaX0@|QnzFD@bne`el^CO$yXC?BDJ|Qg z+y$GRoR`?ST1z^e*>;!IS@5Ovb7*RlN>BV_UC!7E_F;N#ky%1J{+iixp(dUJj93aK zzHNN>R-oN7>kykHClPnoPTIj7zc6KM(Pnlb(|s??)SMb)4!sMHU^-ntJwY5Big7xv zb1Ew`Xj;|D2kzGja*C$eS44(d&RMU~c_Y14V9_TLTz0J#uHlsx`S6{nhsA0dWZ#cG zJ?`fO50E>*X4TQLv#nl%3GOk*UkAgt=IY+u0LNXqeln3Z zv$~&Li`ZJOKkFuS)dJRA>)b_Da%Q~axwA_8zNK{BH{#}#m}zGcuckz}riDE-z_Ms> zR8-EqAMcfyGJCtvTpaUVQtajhUS%c@Yj}&6Zz;-M7MZzqv3kA7{SuW$oW#=0az2wQ zg-WG@Vb4|D`pl~Il54N7Hmsauc_ne-a!o5#j3WaBBh@Wuefb!QJIOn5;d)%A#s+5% zuD$H=VNux9bE-}1&bcYGZ+>1Fo;3Z@e&zX^n!?JK*adSbONm$XW9z;Q^L>9U!}Toj2WdafJ%oL#h|yWWwyAGxzfrAWdDTtaKl zK4`5tDpPg5>z$MNv=X0LZ0d6l%D{(D8oT@+w0?ce$DZ6pv>{1&Ok67Ix1 zH}3=IEhPJEhItCC8E=`T`N5(k?G=B4+xzZ?<4!~ ze~z6Wk9!CHTI(0rLJ4{JU?E-puc;xusR?>G?;4vt;q~iI9=kDL=z0Rr%O$vU`30X$ zDZRFyZ`(omOy@u|i6h;wtJlP;+}$|Ak|k2dea7n?U1*$T!sXqqOjq^NxLPMmk~&qI zYg0W?yK8T(6+Ea+$YyspKK?kP$+B`~t3^Pib_`!6xCs32!i@pqXfFV6PmBIR<-QW= zN8L{pt0Vap0x`Gzn#E@zh@H)0FfVfA_Iu4fjYZ+umO1LXIbVc$pY+E234u)ttcrl$ z>s92z4vT%n6cMb>=XT6;l0+9e(|CZG)$@C7t7Z7Ez@a)h)!hyuV&B5K%%)P5?Lk|C zZZSVzdXp{@OXSP0hoU-gF8s8Um(#xzjP2Vem zec#-^JqTa&Y#QJ>-FBxd7tf`XB6e^JPUgagB8iBSEps;92KG`!#mvVcPQ5yNC-GEG zTiHEDYfH+0O15}r^+ z#jxj=@x8iNHWALe!P3R67TwmhItn**0JwnzSV2O&KE8KcT+0hWH^OPD1pwiuyx=b@ zNf5Jh0{9X)8;~Es)$t@%(3!OnbY+`@?i{mGX7Yy}8T_*0a6g;kaFPq;*=px5EhO{Cp%1kI<0?*|h8v!6WnO3cCJRF2-CRrU3JiLJnj@6;L)!0kWYAc_}F{2P))3HmCrz zQ&N&gE70;`!6*eJ4^1IR{f6j4(-l&X!tjHxkbHA^Zhrnhr9g{exN|xrS`5Pq=#Xf& zG%P=#ra-TyVFfgW%cZo5OSIwFL9WtXAlFOa+ubmI5t*3=g#Y zF%;70p5;{ZeFL}&}yOY1N1*Q;*<(kTB!7vM$QokF)yr2FlIU@$Ph58$Bz z0J?xQG=MlS4L6jA22eS42g|9*9pX@$#*sUeM(z+t?hr@r5J&D1rx}2pW&m*_`VDCW zUYY@v-;bAO0HqoAgbbiGGC<=ryf96}3pouhy3XJrX+!!u*O_>Si38V{uJmQ&USptX zKp#l(?>%^7;2%h(q@YWS#9;a!JhKlkR#Vd)ERILlgu!Hr@jA@V;sk4BJ-H#p*4EqC zDGjC*tl=@3Oi6)Bn^QwFpul18fpkbpg0+peH$xyPBqb%`$OUhPKyWb32o7clB*9Z< zN=i~NLjavrLtwgJ01bufP+>p-jR2I95|TpmKpQL2!oV>g(4RvS2pK4*ou%m(h6r3A zX#s&`9LU1ZG&;{CkOK!4fLDTnBys`M!vuz>Q&9OZ0hGQl!~!jSDg|~s*w52opC{sB ze|Cf2luD(*G13LcOAGA!s2FjSK8&IE5#W%J25w!vM0^VyQM!t)inj&RTiJ!wXzFgz z3^IqzB7I0L$llljsGq})thBy9UOyjtFO_*hYM_sgcMk>44jeH0V1FDyELc{S1F-;A zS;T^k^~4biG&V*Irq}O;e}j$$+E_#G?HKIn05iP3j|87TkGK~SqG!-KBg5+mN(aLm z8ybhIM`%C19UX$H$KY6JgXbY$0AT%rEpHC;u`rQ$Y=rxUdsc5*Kvc8jaYaO$^)cI6){P6K0r)I6DY4Wr4&B zLQUBraey#0HV|&c4v7PVo3n$zHj99(TZO^3?Ly%C4nYvJTL9eLBLHsM3WKKD>5!B` zQ=BsR3aR6PD(Fa>327E2HAu5TM~Wusc!)>~(gM)+3~m;92Jd;FnSib=M5d6;;5{%R zb4V7DEJ0V!CP-F*oU?gkc>ksUtAYP&V4ND5J>J2^jt*vcFflQWCrB&fLdT%O59PVJ zhid#toR=FNgD!q3&r8#wEBr`!wzvQu5zX?Q>nlSJ4i@WC*CN*-xU66F^V5crWevQ9gsq$I@z1o(a=k7LL~ z7m_~`o;_Ozha1$8Q}{WBehvAlO4EL60y5}8GDrZ< zXh&F}71JbW2A~8KfEWj&UWV#4+Z4p`b{uAj4&WC zha`}X@3~+Iz^WRlOHU&KngK>#j}+_o@LdBC1H-`gT+krWX3-;!)6?{FBp~%20a}FL zFP9%Emqcwa#(`=G>BBZ0qZDQhmZKJg_g8<=bBFKWr!dyg(YkpE+|R*SGpDVU!+VlU zFC54^DLv}`qa%49T>nNiA9Q7Ips#!Xx90tCU2gvK`(F+GPcL=J^>No{)~we#o@&mUb6c$ zCc*<|NJBk-#+{j9xkQ&ujB zI~`#kN~7W!f*-}wkG~Ld!JqZ@tK}eeSnsS5J1fMFXm|`LJx&}5`@dK3W^7#Wnm+_P zBZkp&j1fa2Y=eIjJ0}gh85jt43kaIXXv?xmo@eHrka!Z|vQv12HN#+!I5E z`(fbuW>gFiJL|uXJ!vKt#z3e3HlVdboH7;e#i3(2<)Fg-I@BR!qY#eof3MFZ&*Y@l zI|KJf&ge@p2Dq09Vu$$Qxb7!}{m-iRk@!)%KL)txi3;~Z4Pb}u@GsW;ELiWeG9V51 znX#}B&4Y2E7-H=OpNE@q{%hFLxwIpBF2t{vPREa8_{linXT;#1vMRWjOzLOP$-hf( z>=?$0;~~PnkqY;~K{EM6Vo-T(0K{A0}VUGmu*hR z{tw3hvBN%N3G3Yw`X5Te+F{J`(3w1s3-+1EbnFQKcrgrX1Jqvs@ADGe%M0s$EbK$$ zK)=y=upBc6SjGYAACCcI=Y*6Fi8_jgwZlLxD26fnQfJmb8^gHRN5(TemhX@0e=vr> zg`W}6U>x6VhoA3DqsGGD9uL1DhB3!OXO=k}59TqD@(0Nb{)Ut_luTioK_>7wjc!5C zIr@w}b`Fez3)0wQfKl&bae7;PcTA7%?f2xucM0G)wt_KO!Ewx>F~;=BI0j=Fb4>pp zv}0R^xM4eti~+^+gE$6b81p(kwzuDti(-K9bc|?+pJEl@H+jSYuxZQV8rl8 zjp@M{#%qItIUFN~KcO9Hed*`$5A-2~pAo~K&<-Q+`9`$CK>rzqAI4w~$F%vs9s{~x zg4BP%Gy*@m?;D6=SRX?888Q6peF@_4Z->8wAH~Cn!R$|Hhq2cIzFYqT_+cDourHbY z0qroxJnrZ4Gh+Ay+F`_c%+KRT>y3qw{)89?=hJ@=KO=@ep)aBJ$c!JHfBMJpsP*3G za7|)VJJ8B;4?n{~ldJF7%jmb`-ftIvNd~ekoufG(`K(3=LNc;HBY& z(lp#q8XAD#cIf}k49zX_i`*fO+#!zKA&%T3j@%)R+#yag067CU%yUEe47>wzGU8^` z1EXFT^@I!{J!F8!X?S6ph8J=gUi5tl93*W>7}_uR<2N2~e}FaG?}KPyugQ=-OGEZs z!GBoyYY+H*ANn4?Z)X4l+7H%`17i5~zRlRIX?t)6_eu=g2Q`3WBhxSUeea+M-S?RL zX9oBGKn%a!H+*hx4d2(I!gsi+@SQK%<{X22M~2tMulJoa)0*+z9=-YO+;DFEm5eE1U9b^B(Z}2^9!Qk`!A$wUE z7$Ar5?NRg2&G!AZqnmE64eh^Anss3i!{}%6@Et+4rr!=}!SBF8eZ2*J3ujCWbl;3; z48H~goPSv(8X61fKKdpP!Z7$88NL^Z?j`!^*I?-P4X^pMxyWz~@$(UeAcTSDd(`vO z{~rc;9|GfMJcApU3k}22a!&)k4{CU!e_ny^Y3cO;tOvOMKEyWz!vG(Kp*;hB?d|R3`2X~=5a6#^o5@qn?J-bI8Ppip{-yG z!k|VcGsq!jF~}7DMr49Wap-s&>o=U^T0!Lcy}!(bhtYsPQy z4|EJe{12QL#=c(suQ89Mhw9<`bui%nx7Nep`C&*M3~vMEACmcRYYRGtANq$F%zh&V zc)cEVeHz*Z1N)L7k-(k3np#{GcDh2Q@ya0YHl*n7fl*ZPAsbU-a94MYYtA#&!c`xGIaV;yzsmrjfieTEtqB_WgZp2*NplHx=$O{M~2#i_vJ{ps-NgK zQsxKK_CBM2PP_je+Xft`(vYfXXgIUr{=PA=7a8`2EHk)Ym2QKIforz# tySWtj{oF3N9@_;i*Fv5S)9x^z=nlWP>jpp-9)52ZmLVA=i*%6g{{fxOO~wEK literal 0 HcmV?d00001 diff --git a/app/windows/runner/run_loop.cpp b/app/windows/runner/run_loop.cpp new file mode 100644 index 00000000..0d912118 --- /dev/null +++ b/app/windows/runner/run_loop.cpp @@ -0,0 +1,66 @@ +#include "run_loop.h" + +#include + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto instance : flutter_instances_) { + std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/app/windows/runner/run_loop.h b/app/windows/runner/run_loop.h new file mode 100644 index 00000000..54927f97 --- /dev/null +++ b/app/windows/runner/run_loop.h @@ -0,0 +1,40 @@ +#ifndef RUNNER_RUN_LOOP_H_ +#define RUNNER_RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUNNER_RUN_LOOP_H_ diff --git a/app/windows/runner/runner.exe.manifest b/app/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..2c680b8b --- /dev/null +++ b/app/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/app/windows/runner/utils.cpp b/app/windows/runner/utils.cpp new file mode 100644 index 00000000..05b53c01 --- /dev/null +++ b/app/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/app/windows/runner/utils.h b/app/windows/runner/utils.h new file mode 100644 index 00000000..3f0e05cb --- /dev/null +++ b/app/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/app/windows/runner/win32_window.cpp b/app/windows/runner/win32_window.cpp new file mode 100644 index 00000000..97f4439c --- /dev/null +++ b/app/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/app/windows/runner/win32_window.h b/app/windows/runner/win32_window.h new file mode 100644 index 00000000..d9bcac1b --- /dev/null +++ b/app/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 2876c3fb..b091314c 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -699,14 +700,21 @@ class RawEditorState extends EditorState handleDelete, ); - _keyboardVisibilityController = KeyboardVisibilityController(); - _keyboardVisibilitySubscription = - _keyboardVisibilityController.onChange.listen((bool visible) { - _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(); - } - }); + if (Platform.isWindows || + Platform.isMacOS || + Platform.isLinux || + Platform.isFuchsia) { + _keyboardVisible = true; + } else { + _keyboardVisibilityController = KeyboardVisibilityController(); + _keyboardVisibilitySubscription = + _keyboardVisibilityController.onChange.listen((bool visible) { + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(); + } + }); + } _focusAttachment = widget.focusNode.attach(context, onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); @@ -868,7 +876,7 @@ class RawEditorState extends EditorState @override void dispose() { closeConnectionIfNeeded(); - _keyboardVisibilitySubscription.cancel(); + _keyboardVisibilitySubscription?.cancel(); assert(!hasConnection); _selectionOverlay?.dispose(); _selectionOverlay = null; diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 9ca7ee80..74ee989b 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; +import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,6 +11,7 @@ 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 'controller.dart'; @@ -17,6 +19,7 @@ double iconSize = 18.0; double kToolbarHeight = iconSize * 2; typedef OnImagePickCallback = Future Function(File file); +typedef ImagePickImpl = Future Function(ImageSource source); class InsertEmbedButton extends StatelessWidget { final QuillController controller; @@ -494,6 +497,8 @@ class ImageButton extends StatefulWidget { final OnImagePickCallback onImagePickCallback; + final ImagePickImpl imagePickImpl; + final ImageSource imageSource; ImageButton( @@ -501,7 +506,8 @@ class ImageButton extends StatefulWidget { @required this.icon, @required this.controller, @required this.imageSource, - this.onImagePickCallback}) + this.onImagePickCallback, + this.imagePickImpl}) : assert(icon != null), assert(controller != null), super(key: key); @@ -570,6 +576,26 @@ class _ImageButtonState extends State { return null; } + Future _pickImageDesktop() async { + try { + var filePath = await FilesystemPicker.open( + context: context, + rootDirectory: await getApplicationDocumentsDirectory(), + fsType: FilesystemType.file, + fileTileSelectMode: FileTileSelectMode.wholeTile, + ); + if (filePath == null || filePath.isEmpty) return null; + + final File file = File(filePath); + String url = await widget.onImagePickCallback(file); + print('Image uploaded and its url is $url'); + return url; + } catch (error) { + print('Upload image error $error'); + } + return null; + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -584,7 +610,18 @@ class _ImageButtonState extends State { onPressed: () { final index = widget.controller.selection.baseOffset; final length = widget.controller.selection.extentOffset - index; - final image = kIsWeb ? _pickImageWeb() : _pickImage(widget.imageSource); + Future image; + if (widget.imagePickImpl != null) { + image = widget.imagePickImpl(widget.imageSource); + } else { + if (kIsWeb) { + image = _pickImageWeb(); + } else if (Platform.isAndroid || Platform.isIOS) { + image = _pickImage(widget.imageSource); + } else { + image = _pickImageDesktop(); + } + } image.then((imageUploadUrl) => { if (imageUploadUrl != null) { diff --git a/pubspec.lock b/pubspec.lock index 7540db7d..8085e487 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -43,20 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.0" csslib: dependency: transitive description: @@ -71,6 +64,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" file_picker: dependency: "direct main" description: @@ -78,6 +85,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + filesystem_picker: + dependency: "direct main" + description: + name: filesystem_picker + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" flutter: dependency: "direct main" description: flutter @@ -155,7 +169,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.7.2" + version: "0.7.2+1" image_picker_platform_interface: dependency: transitive description: @@ -191,13 +205,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" pedantic: dependency: transitive description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.10.0" + version: "1.11.0" photo_view: dependency: "direct main" description: @@ -205,6 +254,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.11.1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" plugin_platform_interface: dependency: transitive description: @@ -212,6 +268,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" quiver: dependency: transitive description: @@ -307,7 +370,7 @@ packages: name: universal_io url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "2.0.0" universal_ui: dependency: "direct main" description: @@ -364,6 +427,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" zone_local: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 14f7123a..01de66d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,8 @@ dependencies: photo_view: ^0.11.0 universal_html: ^1.2.4 file_picker: ^3.0.0 + filesystem_picker: ^1.0.4 + path_provider: ^2.0.1 string_validator: ^0.1.4 flutter_keyboard_visibility: ^5.0.0 universal_ui: ^0.0.8 From f7d05ba28a73a6f31848631d8a4a370f94f438f0 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 13 Mar 2021 00:19:42 -0800 Subject: [PATCH 069/306] Upgrade version to [1.0.6] Add desktop support - WINDOWS, MACOS and LINUX --- CHANGELOG.md | 3 +++ app/pubspec.lock | 9 ++++++++- lib/widgets/raw_editor.dart | 10 +++++----- pubspec.yaml | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d3b37b..4a20bd70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.0.6] +* Add desktop support - WINDOWS, MACOS and LINUX. + ## [1.0.5] * Bug fix: Can not insert newline when Bold is toggled ON. diff --git a/app/pubspec.lock b/app/pubspec.lock index 116d24bf..c5f94fa3 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -99,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + filesystem_picker: + dependency: transitive + description: + name: filesystem_picker + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" flutter: dependency: "direct main" description: flutter @@ -267,7 +274,7 @@ packages: name: photo_view url: "https://pub.dartlang.org" source: hosted - version: "0.10.3" + version: "0.11.1" platform: dependency: transitive description: diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index b091314c..ca1a1d6e 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -709,11 +709,11 @@ class RawEditorState extends EditorState _keyboardVisibilityController = KeyboardVisibilityController(); _keyboardVisibilitySubscription = _keyboardVisibilityController.onChange.listen((bool visible) { - _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(); - } - }); + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(); + } + }); } _focusAttachment = widget.focusNode.attach(context, diff --git a/pubspec.yaml b/pubspec.yaml index 01de66d3..0cf36eb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.5 +version: 1.0.6 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From 498dba598d5ab3c9fa9cdf85c3751148a93c1b4c Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 13 Mar 2021 20:49:33 +0800 Subject: [PATCH 070/306] fix crash on web (dart:io) --- lib/widgets/raw_editor.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index ca1a1d6e..c282d2e5 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -700,10 +699,10 @@ class RawEditorState extends EditorState handleDelete, ); - if (Platform.isWindows || - Platform.isMacOS || - Platform.isLinux || - Platform.isFuchsia) { + if (defaultTargetPlatform == TargetPlatform.windows || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux || + defaultTargetPlatform == TargetPlatform.fuchsia) { _keyboardVisible = true; } else { _keyboardVisibilityController = KeyboardVisibilityController(); From cdfce302ac36c97176947fdda23e6d556cb131a8 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 13 Mar 2021 08:58:04 -0800 Subject: [PATCH 071/306] Upgrade version to 1.0.7 - Fix crash on web (dart:io) --- CHANGELOG.md | 3 +++ app/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a20bd70..de08eca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.0.7] +* Fix crash on web (dart:io). + ## [1.0.6] * Add desktop support - WINDOWS, MACOS and LINUX. diff --git a/app/pubspec.lock b/app/pubspec.lock index c5f94fa3..1ac7adf0 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -152,7 +152,7 @@ packages: path: ".." relative: true source: path - version: "1.0.5" + version: "1.0.7" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 0cf36eb9..b84de7bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.6 +version: 1.0.7 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From a5277ffd3cc132b5675e7df2b0a9f03682d237be Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 13 Mar 2021 09:58:24 -0800 Subject: [PATCH 072/306] Support token attribute --- CHANGELOG.md | 3 +++ app/assets/sample_data.json | 18 ++++++++++++++++++ app/pubspec.lock | 2 +- lib/models/documents/attribute.dart | 7 +++++++ pubspec.yaml | 2 +- 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de08eca2..c83c8746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.0.8] +* Support token attribute. + ## [1.0.7] * Fix crash on web (dart:io). diff --git a/app/assets/sample_data.json b/app/assets/sample_data.json index 7fe41833..a0b44f50 100644 --- a/app/assets/sample_data.json +++ b/app/assets/sample_data.json @@ -497,6 +497,24 @@ }, "insert": "font size 20" }, + { + "attributes":{ + "token":"built_in" + }, + "insert":" diff" + }, + { + "attributes":{ + "token":"operator" + }, + "insert":"-match" + }, + { + "attributes":{ + "token":"literal" + }, + "insert":"-patch" + }, { "insert": "\n" } diff --git a/app/pubspec.lock b/app/pubspec.lock index 1ac7adf0..a37fd0d0 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -152,7 +152,7 @@ packages: path: ".." relative: true source: path - version: "1.0.7" + version: "1.0.8" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 7b363bed..38a6b522 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -34,6 +34,7 @@ class Attribute { Attribute.width.key: Attribute.width, Attribute.height.key: Attribute.height, Attribute.style.key: Attribute.style, + Attribute.token.key: Attribute.token, }; static final BoldAttribute bold = BoldAttribute(); @@ -74,6 +75,8 @@ class Attribute { static final StyleAttribute style = StyleAttribute(null); + static final TokenAttribute token = TokenAttribute(null); + static final Set inlineKeys = { Attribute.bold.key, Attribute.italic.key, @@ -266,3 +269,7 @@ class HeightAttribute extends Attribute { class StyleAttribute extends Attribute { StyleAttribute(String val) : super('style', AttributeScope.IGNORE, val); } + +class TokenAttribute extends Attribute { + TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val); +} diff --git a/pubspec.yaml b/pubspec.yaml index b84de7bc..b79ea315 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.7 +version: 1.0.8 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From 701c56c312e440b52ec2031583de2160dc1e008c Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 15 Mar 2021 16:50:23 -0700 Subject: [PATCH 073/306] Update quiver hashcode dependency (#88) Development on the quiver_* subpackages has been discontinued in favour of the main quiver package at https://github.com/google/quiver-dart. The code itself is identical, but is more actively maintained. Updated the dependency on the tuple package to 2.0.0, since it also depends on quiver 3.0.0. Both packages has been migrated to null-safety at those versions. --- lib/models/documents/attribute.dart | 2 +- lib/models/documents/style.dart | 2 +- lib/models/quill_delta.dart | 2 +- pubspec.lock | 15 ++++----------- pubspec.yaml | 4 ++-- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 38a6b522..2821661a 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,4 +1,4 @@ -import 'package:quiver_hashcode/hashcode.dart'; +import 'package:quiver/core.dart'; enum AttributeScope { INLINE, // refer to https://quilljs.com/docs/formats/#inline diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index cd05cd03..aa3588d6 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter_quill/models/documents/attribute.dart'; -import 'package:quiver_hashcode/hashcode.dart'; +import 'package:quiver/core.dart'; /* Collection of style attributes */ class Style { diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index 3bbda2eb..cbfee575 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -7,7 +7,7 @@ library quill_delta; import 'dart:math' as math; import 'package:collection/collection.dart'; -import 'package:quiver_hashcode/hashcode.dart'; +import 'package:quiver/core.dart'; const _attributeEquality = DeepCollectionEquality(); const _valueEquality = DeepCollectionEquality(); diff --git a/pubspec.lock b/pubspec.lock index 8085e487..c582bed2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -276,19 +276,12 @@ packages: source: hosted version: "4.1.0" quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.5" - quiver_hashcode: dependency: "direct main" description: - name: quiver_hashcode + name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -300,7 +293,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -349,7 +342,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.0.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b79ea315..b49566bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,9 +12,9 @@ environment: dependencies: flutter: sdk: flutter - quiver_hashcode: ^2.0.0 + quiver: ^3.0.0 collection: ^1.15.0 - tuple: ^1.0.3 + tuple: ^2.0.0 url_launcher: ^6.0.0 flutter_colorpicker: ^0.3.5 image_picker: ^0.7.2 From 54c3eb446e2b90ec0a977d29eb0f21e2752dd07f Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 16 Mar 2021 17:24:37 -0700 Subject: [PATCH 074/306] Web support for raw editor and keyboard listener --- lib/widgets/keyboard_listener.dart | 6 ++++++ lib/widgets/raw_editor.dart | 32 ++++++++++++++++++++++++++---- pubspec.lock | 2 +- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart index 329132ab..c0dc0c27 100644 --- a/lib/widgets/keyboard_listener.dart +++ b/lib/widgets/keyboard_listener.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL } @@ -64,6 +65,11 @@ class KeyboardListener { assert(onDelete != null); bool handleRawKeyEvent(RawKeyEvent event) { + if (kIsWeb) { + // On web platform, we should ignore the key because it's processed already. + return false; + } + if (event is! RawKeyDownEvent) { return false; } diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index c282d2e5..e3cb78af 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -131,11 +131,27 @@ class RawEditorState extends EditorState bool _didAutoFocus = false; bool _keyboardVisible = false; DefaultStyles _styles; - final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); + final ClipboardStatusNotifier _clipboardStatus = + kIsWeb ? null : ClipboardStatusNotifier(); final LayerLink _toolbarLayerLink = LayerLink(); final LayerLink _startHandleLayerLink = LayerLink(); final LayerLink _endHandleLayerLink = LayerLink(); + /// Whether to create an input connection with the platform for text editing + /// or not. + /// + /// Read-only input fields do not need a connection with the platform since + /// there's no need for text editing capabilities (e.g. virtual keyboard). + /// + /// On the web, we always need a connection because we want some browser + /// functionalities to continue to work on read-only input fields like: + /// + /// - Relevant context menu. + /// - cmd/ctrl+c shortcut to copy. + /// - cmd/ctrl+a to select all. + /// - Changing the selection using a physical keyboard. + bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; + bool get _hasFocus => widget.focusNode.hasFocus; TextDirection get _textDirection { @@ -371,7 +387,7 @@ class RawEditorState extends EditorState _textInputConnection != null && _textInputConnection.attached; openConnectionIfNeeded() { - if (widget.readOnly) { + if (!shouldCreateInputConnection) { return; } @@ -437,7 +453,7 @@ class RawEditorState extends EditorState @override void updateEditingValue(TextEditingValue value) { - if (widget.readOnly) { + if (!shouldCreateInputConnection) { return; } @@ -773,7 +789,7 @@ class RawEditorState extends EditorState } _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); - if (widget.readOnly) { + if (!shouldCreateInputConnection) { closeConnectionIfNeeded(); } else { if (oldWidget.readOnly && _hasFocus) { @@ -1101,6 +1117,14 @@ class RawEditorState extends EditorState @override bool showToolbar() { + // Web is using native dom elements to enable clipboard functionality of the + // toolbar: copy, paste, select, cut. It might also provide additional + // functionality depending on the browser (such as translate). Due to this + // we should not show a Flutter toolbar for the editable text elements. + if (kIsWeb) { + return false; + } + if (_selectionOverlay == null || _selectionOverlay.toolbar != null) { return false; } diff --git a/pubspec.lock b/pubspec.lock index c582bed2..080b2b98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -293,7 +293,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0" stack_trace: dependency: transitive description: From 8f73a14830a4af6f4976bee13afb4f8751157d6e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 16 Mar 2021 17:59:14 -0700 Subject: [PATCH 075/306] Upgrade version --- CHANGELOG.md | 3 +++ app/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83c8746..6854232b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.0.9] +* Web support for raw editor and keyboard listener. + ## [1.0.8] * Support token attribute. diff --git a/app/pubspec.lock b/app/pubspec.lock index a37fd0d0..73de09ad 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -152,7 +152,7 @@ packages: path: ".." relative: true source: path - version: "1.0.8" + version: "1.0.9" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index b49566bc..63185344 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.8 +version: 1.0.9 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill.git From e36033929cab2909152d681c89aa7bcfe81acbac Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 18 Mar 2021 12:10:03 +0100 Subject: [PATCH 076/306] Fix link on pub.dev to "View/report issues" (#91) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 63185344..aac2f51e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: One client and affiliated collaborator of Flutter Quill is Bullet J version: 1.0.9 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html -repository: https://github.com/singerdmx/flutter-quill.git +repository: https://github.com/singerdmx/flutter-quill environment: sdk: ">=2.7.0 <3.0.0" From dda5805935568d42b71a1a1faf7fbdf078d72633 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Fri, 19 Mar 2021 17:15:22 +0100 Subject: [PATCH 077/306] Add hitTesting to allow changing click behavior in embeds The code change allows to override the default behaviour of clicking on images and thus solves #90. Now the only thing left to do is to use a `RawGestureDetector` in the `EmbedBuilder` to deactivate the "old" behaviour (by entering the GestureArena and win). The `embedBuilder` could look like this: ```dart Widget embedBuilder(BuildContext context, Embed node, bool readOnly) { return FutureBuilder( future: File('image_path.png'), builder: (context, snapshot) { final gestures = { ReadDependentGestureRecognizer: GestureRecognizerFactoryWithHandlers< ReadDependentGestureRecognizer>( () => ReadDependentGestureRecognizer(readOnly: readOnly), (instance) => instance ..onTapUp = ((_) => print('Test')) ) }; return RawGestureDetector( gestures: gestures, child: Image.file(snapshot.data), ); } ); } class ReadDependentGestureRecognizer extends TapGestureRecognizer { ReadDependentGestureRecognizer({@required this.readOnly}); final bool readOnly; @override bool isPointerAllowed(PointerDownEvent event) { if (!readOnly) { return false; } return super.isPointerAllowed(event); } } ``` **Explanation of the code:** When a `PointerDownEvent` is received by the `GestureBinding` a hit test is performed to determine which `HitTestTarget` nodes are affected. For this the (I think) element tree is traversed downwards to find out which elements has been hit. To find out which elements were hit, the method `hitTestSelf` and `hitTestChildren` are used. Since the `hitTestChildren` method of `TextLine` has not been implemented yet, `false` was always returned, which is why the hit test never arrived at the `embedBuilder`. With the code change, the hit is passed on and can be processed correctly in the embed. Additionally I removed the class `_TransparentTapGestureRecognizer`, because this class always accepted the gesture, even if the recognizer lost in the arena. --- lib/widgets/text_line.dart | 5 +++++ lib/widgets/text_selection.dart | 23 ++++------------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index ffe7ee93..a9260e8f 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -785,6 +785,11 @@ class RenderEditableTextLine extends RenderEditableBox { ); _cursorPainter.paint(context.canvas, effectiveOffset, position); } + + @override + bool hitTestChildren(BoxHitTestResult result, {Offset position}) { + return this._children.first.hitTest(result, position: position); + } } class _TextLineElement extends RenderObjectElement { diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index e74318ac..0790cbd0 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -670,10 +670,10 @@ class _EditorTextSelectionGestureDetectorState final Map gestures = {}; - gestures[_TransparentTapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers<_TransparentTapGestureRecognizer>( - () => _TransparentTapGestureRecognizer(debugOwner: this), - (_TransparentTapGestureRecognizer instance) { + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { instance ..onTapDown = _handleTapDown ..onTapUp = _handleTapUp @@ -735,18 +735,3 @@ class _EditorTextSelectionGestureDetectorState ); } } - -class _TransparentTapGestureRecognizer extends TapGestureRecognizer { - _TransparentTapGestureRecognizer({ - Object debugOwner, - }) : super(debugOwner: debugOwner); - - @override - void rejectGesture(int pointer) { - if (state == GestureRecognizerState.ready) { - acceptGesture(pointer); - } else { - super.rejectGesture(pointer); - } - } -} From c593026fd90003c30bae1b0b3d9e8b858b1a490a Mon Sep 17 00:00:00 2001 From: Miller Adulu Date: Fri, 19 Mar 2021 21:03:19 +0300 Subject: [PATCH 078/306] Sound null safety migration (#87) * Upgrade upgradable packages * Apply default null-safety migrations * Remove hashnode as a dependency and localize its functionality to the package. Maintenance was done long time ago hence no need to wait for the update * Localize ui package to reduce maintenance burdens * Replace universal html with dart:html * Remove unnecessary checks * Fix formatting * Migrate app to null safety * Enable methods to be nullable * Fix non-nullable issue with node methods * Cast as Node * Use universal html * Use universal html package to bring in the ImageElement class * Remove unused imports * Fix imports on the editor file * Add key to quill editor * Remove final from the GlobalKey * Remove final on GlobalKey * Remove custom util implementation in favor of quiver * Fix issue with null on token attrivute * Remove final hashcode that is replaced by quiver functionality * Fix merge request * Fix hit test position in text_line.dart * Fix null safety errors on text_selection.dart * Fix sound null safe errors in toolbar.dart * Import null safe file picker --- analysis_options.yaml | 3 + app/lib/pages/home_page.dart | 15 +- app/lib/pages/read_only_page.dart | 4 +- app/lib/widgets/demo_scaffold.dart | 16 +- app/lib/widgets/field.dart | 50 ++-- app/pubspec.lock | 75 +----- app/pubspec.yaml | 2 +- lib/models/documents/attribute.dart | 82 +++--- lib/models/documents/document.dart | 20 +- lib/models/documents/history.dart | 4 +- lib/models/documents/nodes/block.dart | 16 +- lib/models/documents/nodes/container.dart | 44 ++-- lib/models/documents/nodes/embed.dart | 4 +- lib/models/documents/nodes/leaf.dart | 51 ++-- lib/models/documents/nodes/line.dart | 70 +++--- lib/models/documents/nodes/node.dart | 23 +- lib/models/documents/style.dart | 6 +- lib/models/quill_delta.dart | 126 +++++----- lib/models/rules/delete.dart | 56 +++-- lib/models/rules/format.dart | 52 ++-- lib/models/rules/insert.dart | 123 +++++---- lib/models/rules/rule.dart | 14 +- lib/utils/color.dart | 4 +- lib/utils/diff_delta.dart | 14 +- lib/utils/universal_ui/fake_ui.dart | 3 + lib/utils/universal_ui/real_ui.dart | 9 + lib/utils/universal_ui/universal_ui.dart | 22 ++ lib/widgets/box.dart | 8 +- lib/widgets/controller.dart | 44 ++-- lib/widgets/cursor.dart | 47 ++-- lib/widgets/default_styles.dart | 60 +++-- lib/widgets/delegate.dart | 47 ++-- lib/widgets/editor.dart | 266 +++++++++----------- lib/widgets/image.dart | 2 +- lib/widgets/keyboard_listener.dart | 7 +- lib/widgets/proxy.dart | 75 +++--- lib/widgets/raw_editor.dart | 294 ++++++++++------------ lib/widgets/responsive_widget.dart | 8 +- lib/widgets/text_block.dart | 180 ++++++------- lib/widgets/text_line.dart | 246 +++++++++--------- lib/widgets/text_selection.dart | 238 +++++++++--------- lib/widgets/toolbar.dart | 262 +++++++++---------- pubspec.lock | 43 ++-- pubspec.yaml | 88 +++---- 44 files changed, 1315 insertions(+), 1508 deletions(-) create mode 100644 analysis_options.yaml create mode 100644 lib/utils/universal_ui/fake_ui.dart create mode 100644 lib/utils/universal_ui/real_ui.dart create mode 100644 lib/utils/universal_ui/universal_ui.dart diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..cb1ea33c --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + errors: + undefined_prefixed_name: ignore \ No newline at end of file diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 60a42a64..f7299805 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -23,7 +23,7 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - QuillController _controller; + QuillController? _controller; final FocusNode _focusNode = FocusNode(); @override @@ -75,15 +75,15 @@ class _HomePageState extends State { focusNode: FocusNode(), onKey: (RawKeyEvent event) { if (event.data.isControlPressed && event.character == 'b') { - if (_controller + if (_controller! .getSelectionStyle() .attributes .keys .contains("bold")) { - _controller + _controller! .formatSelection(Attribute.clone(Attribute.bold, null)); } else { - _controller.formatSelection(Attribute.bold); + _controller!.formatSelection(Attribute.bold); print("not bold"); } } @@ -104,7 +104,7 @@ class _HomePageState extends State { color: Colors.white, padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: QuillEditor( - controller: _controller, + controller: _controller!, scrollController: ScrollController(), scrollable: true, focusNode: _focusNode, @@ -135,12 +135,12 @@ class _HomePageState extends State { child: Container( padding: EdgeInsets.symmetric(vertical: 16, horizontal: 8), child: QuillToolbar.basic( - controller: _controller, + controller: _controller!, onImagePickCallback: _onImagePickCallback), )) : Container( child: QuillToolbar.basic( - controller: _controller, + controller: _controller!, onImagePickCallback: _onImagePickCallback), ), ], @@ -151,7 +151,6 @@ class _HomePageState extends State { // Renders the image picked by imagePicker from local file storage // 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 { - if (file == null) return null; // Copies the picked file from temporary cache to applications directory Directory appDocDir = await getApplicationDocumentsDirectory(); File copiedFile = diff --git a/app/lib/pages/read_only_page.dart b/app/lib/pages/read_only_page.dart index c72313ad..8f03d45c 100644 --- a/app/lib/pages/read_only_page.dart +++ b/app/lib/pages/read_only_page.dart @@ -27,7 +27,7 @@ class _ReadOnlyPageState extends State { ); } - Widget _buildContent(BuildContext context, QuillController controller) { + Widget _buildContent(BuildContext context, QuillController? controller) { return Padding( padding: const EdgeInsets.all(8.0), child: Container( @@ -36,7 +36,7 @@ class _ReadOnlyPageState extends State { border: Border.all(color: Colors.grey.shade200), ), child: QuillEditor( - controller: controller, + controller: controller!, scrollController: ScrollController(), scrollable: true, focusNode: _focusNode, diff --git a/app/lib/widgets/demo_scaffold.dart b/app/lib/widgets/demo_scaffold.dart index 3caa00d5..1533da28 100644 --- a/app/lib/widgets/demo_scaffold.dart +++ b/app/lib/widgets/demo_scaffold.dart @@ -7,21 +7,21 @@ import 'package:flutter_quill/widgets/controller.dart'; import 'package:flutter_quill/widgets/toolbar.dart'; typedef DemoContentBuilder = Widget Function( - BuildContext context, QuillController controller); + BuildContext context, QuillController? controller); // 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 List? actions; + final Widget? floatingActionButton; final bool showToolbar; const DemoScaffold({ - Key key, - @required this.documentFilename, - @required this.builder, + Key? key, + required this.documentFilename, + required this.builder, this.actions, this.showToolbar = true, this.floatingActionButton, @@ -33,7 +33,7 @@ class DemoScaffold extends StatefulWidget { class _DemoScaffoldState extends State { final _scaffoldKey = GlobalKey(); - QuillController _controller; + QuillController? _controller; bool _loading = false; @@ -92,7 +92,7 @@ class _DemoScaffoldState extends State { ), title: _loading || widget.showToolbar == false ? null - : QuillToolbar.basic(controller: _controller), + : QuillToolbar.basic(controller: _controller!), actions: actions, ), floatingActionButton: widget.floatingActionButton, diff --git a/app/lib/widgets/field.dart b/app/lib/widgets/field.dart index dd8b4bf4..35e710f2 100644 --- a/app/lib/widgets/field.dart +++ b/app/lib/widgets/field.dart @@ -6,28 +6,28 @@ import 'package:flutter_quill/widgets/editor.dart'; class QuillField extends StatefulWidget { final QuillController controller; - final FocusNode focusNode; - final ScrollController scrollController; + final FocusNode? focusNode; + final ScrollController? scrollController; final bool scrollable; final EdgeInsetsGeometry padding; final bool autofocus; final bool showCursor; final bool readOnly; final bool enableInteractiveSelection; - final double minHeight; - final double maxHeight; + final double? minHeight; + final double? maxHeight; final bool expands; final TextCapitalization textCapitalization; final Brightness keyboardAppearance; - final ScrollPhysics scrollPhysics; - final ValueChanged onLaunchUrl; - final InputDecoration decoration; - final Widget toolbar; - final EmbedBuilder embedBuilder; + final ScrollPhysics? scrollPhysics; + final ValueChanged? onLaunchUrl; + final InputDecoration? decoration; + final Widget? toolbar; + final EmbedBuilder? embedBuilder; QuillField({ - Key key, - @required this.controller, + Key? key, + required this.controller, this.focusNode, this.scrollController, this.scrollable = true, @@ -53,28 +53,28 @@ class QuillField extends StatefulWidget { } class _QuillFieldState extends State { - bool _focused; + late bool _focused; void _editorFocusChanged() { setState(() { - _focused = widget.focusNode.hasFocus; + _focused = widget.focusNode!.hasFocus; }); } @override void initState() { super.initState(); - _focused = widget.focusNode.hasFocus; - widget.focusNode.addListener(_editorFocusChanged); + _focused = widget.focusNode!.hasFocus; + widget.focusNode!.addListener(_editorFocusChanged); } @override void didUpdateWidget(covariant QuillField oldWidget) { super.didUpdateWidget(oldWidget); if (widget.focusNode != oldWidget.focusNode) { - oldWidget.focusNode.removeListener(_editorFocusChanged); - widget.focusNode.addListener(_editorFocusChanged); - _focused = widget.focusNode.hasFocus; + oldWidget.focusNode!.removeListener(_editorFocusChanged); + widget.focusNode!.addListener(_editorFocusChanged); + _focused = widget.focusNode!.hasFocus; } } @@ -82,8 +82,8 @@ class _QuillFieldState extends State { Widget build(BuildContext context) { Widget child = QuillEditor( controller: widget.controller, - focusNode: widget.focusNode, - scrollController: widget.scrollController, + focusNode: widget.focusNode!, + scrollController: widget.scrollController!, scrollable: widget.scrollable, padding: widget.padding, autoFocus: widget.autofocus, @@ -97,7 +97,7 @@ class _QuillFieldState extends State { keyboardAppearance: widget.keyboardAppearance, scrollPhysics: widget.scrollPhysics, onLaunchUrl: widget.onLaunchUrl, - embedBuilder: widget.embedBuilder, + embedBuilder: widget.embedBuilder!, ); if (widget.toolbar != null) { @@ -105,7 +105,7 @@ class _QuillFieldState extends State { children: [ child, Visibility( - child: widget.toolbar, + child: widget.toolbar!, visible: _focused, maintainSize: true, maintainAnimation: true, @@ -117,11 +117,11 @@ class _QuillFieldState extends State { return AnimatedBuilder( animation: - Listenable.merge([widget.focusNode, widget.controller]), - builder: (BuildContext context, Widget child) { + Listenable.merge([widget.focusNode, widget.controller]), + builder: (BuildContext context, Widget? child) { return InputDecorator( decoration: _getEffectiveDecoration(), - isFocused: widget.focusNode.hasFocus, + isFocused: widget.focusNode!.hasFocus, // TODO: Document should be considered empty of it has single empty line with no styles applied isEmpty: widget.controller.document.length == 1, child: child, diff --git a/app/pubspec.lock b/app/pubspec.lock index 73de09ad..e7326d61 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -43,27 +43,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.5" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.dartlang.org" - source: hosted - version: "0.16.2" cupertino_icons: dependency: "direct main" description: @@ -117,7 +96,7 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "0.4.0-nullsafety.0" flutter_keyboard_visibility: dependency: transitive description: @@ -163,13 +142,6 @@ packages: description: flutter source: sdk version: "0.0.0" - html: - dependency: transitive - description: - name: html - url: "https://pub.dartlang.org" - source: hosted - version: "0.14.0+4" http: dependency: transitive description: @@ -190,7 +162,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.7.2" + version: "0.7.2+1" image_picker_platform_interface: dependency: transitive description: @@ -302,14 +274,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" - quiver_hashcode: - dependency: transitive - description: - name: quiver_hashcode - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -349,7 +314,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.2.0-nullsafety.0" term_glyph: dependency: transitive description: @@ -370,7 +335,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "2.0.0" typed_data: dependency: transitive description: @@ -378,27 +343,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" - universal_html: - dependency: transitive - description: - name: universal_html - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.4" - universal_io: - dependency: transitive - description: - name: universal_io - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - universal_ui: - dependency: transitive - description: - name: universal_ui - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.8" url_launcher: dependency: transitive description: @@ -462,13 +406,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" - zone_local: - dependency: transitive - description: - name: zone_local - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.22.0" + flutter: ">=1.24.0-10.2.pre" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8858a8dc..3a31d347 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: flutter: diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 2821661a..8d43e805 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -75,7 +75,7 @@ class Attribute { static final StyleAttribute style = StyleAttribute(null); - static final TokenAttribute token = TokenAttribute(null); + static final TokenAttribute token = TokenAttribute(''); static final Set inlineKeys = { Attribute.bold.key, @@ -105,46 +105,46 @@ class Attribute { Attribute.blockQuote.key, }; - static Attribute get h1 => HeaderAttribute(level: 1); + static Attribute get h1 => HeaderAttribute(level: 1); - static Attribute get h2 => HeaderAttribute(level: 2); + static Attribute get h2 => HeaderAttribute(level: 2); - static Attribute get h3 => HeaderAttribute(level: 3); + static Attribute get h3 => HeaderAttribute(level: 3); // "attributes":{"align":"left"} - static Attribute get leftAlignment => AlignAttribute('left'); + static Attribute get leftAlignment => AlignAttribute('left'); // "attributes":{"align":"center"} - static Attribute get centerAlignment => AlignAttribute('center'); + static Attribute get centerAlignment => AlignAttribute('center'); // "attributes":{"align":"right"} - static Attribute get rightAlignment => AlignAttribute('right'); + static Attribute get rightAlignment => AlignAttribute('right'); // "attributes":{"align":"justify"} - static Attribute get justifyAlignment => AlignAttribute('justify'); + static Attribute get justifyAlignment => AlignAttribute('justify'); // "attributes":{"list":"bullet"} - static Attribute get ul => ListAttribute('bullet'); + static Attribute get ul => ListAttribute('bullet'); // "attributes":{"list":"ordered"} - static Attribute get ol => ListAttribute('ordered'); + static Attribute get ol => ListAttribute('ordered'); // "attributes":{"list":"checked"} - static Attribute get checked => ListAttribute('checked'); + static Attribute get checked => ListAttribute('checked'); // "attributes":{"list":"unchecked"} - static Attribute get unchecked => ListAttribute('unchecked'); + static Attribute get unchecked => ListAttribute('unchecked'); // "attributes":{"indent":1"} - static Attribute get indentL1 => IndentAttribute(level: 1); + static Attribute get indentL1 => IndentAttribute(level: 1); // "attributes":{"indent":2"} - static Attribute get indentL2 => IndentAttribute(level: 2); + static Attribute get indentL2 => IndentAttribute(level: 2); // "attributes":{"indent":3"} - static Attribute get indentL3 => IndentAttribute(level: 3); + static Attribute get indentL3 => IndentAttribute(level: 3); - static Attribute getIndentLevel(int level) { + static Attribute getIndentLevel(int? level) { if (level == 1) { return indentL1; } @@ -164,7 +164,7 @@ class Attribute { if (!_registry.containsKey(key)) { throw ArgumentError.value(key, 'key "$key" not found.'); } - Attribute origin = _registry[key]; + Attribute origin = _registry[key]!; Attribute attribute = clone(origin, value); return attribute; } @@ -208,24 +208,24 @@ class StrikeThroughAttribute extends Attribute { StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true); } -class FontAttribute extends Attribute { - FontAttribute(String val) : super('font', AttributeScope.INLINE, val); +class FontAttribute extends Attribute { + FontAttribute(String? val) : super('font', AttributeScope.INLINE, val); } -class SizeAttribute extends Attribute { - SizeAttribute(String val) : super('size', AttributeScope.INLINE, val); +class SizeAttribute extends Attribute { + SizeAttribute(String? val) : super('size', AttributeScope.INLINE, val); } -class LinkAttribute extends Attribute { - LinkAttribute(String val) : super('link', AttributeScope.INLINE, val); +class LinkAttribute extends Attribute { + LinkAttribute(String? val) : super('link', AttributeScope.INLINE, val); } -class ColorAttribute extends Attribute { - ColorAttribute(String val) : super('color', AttributeScope.INLINE, val); +class ColorAttribute extends Attribute { + ColorAttribute(String? val) : super('color', AttributeScope.INLINE, val); } -class BackgroundAttribute extends Attribute { - BackgroundAttribute(String val) +class BackgroundAttribute extends Attribute { + BackgroundAttribute(String? val) : super('background', AttributeScope.INLINE, val); } @@ -234,20 +234,20 @@ class PlaceholderAttribute extends Attribute { PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true); } -class HeaderAttribute extends Attribute { - HeaderAttribute({int level}) : super('header', AttributeScope.BLOCK, level); +class HeaderAttribute extends Attribute { + HeaderAttribute({int? level}) : super('header', AttributeScope.BLOCK, level); } -class IndentAttribute extends Attribute { - IndentAttribute({int level}) : super('indent', AttributeScope.BLOCK, level); +class IndentAttribute extends Attribute { + IndentAttribute({int? level}) : super('indent', AttributeScope.BLOCK, level); } -class AlignAttribute extends Attribute { - AlignAttribute(String val) : super('align', AttributeScope.BLOCK, val); +class AlignAttribute extends Attribute { + AlignAttribute(String? val) : super('align', AttributeScope.BLOCK, val); } -class ListAttribute extends Attribute { - ListAttribute(String val) : super('list', AttributeScope.BLOCK, val); +class ListAttribute extends Attribute { + ListAttribute(String? val) : super('list', AttributeScope.BLOCK, val); } class CodeBlockAttribute extends Attribute { @@ -258,16 +258,16 @@ class BlockQuoteAttribute extends Attribute { BlockQuoteAttribute() : super('blockquote', AttributeScope.BLOCK, true); } -class WidthAttribute extends Attribute { - WidthAttribute(String val) : super('width', AttributeScope.IGNORE, val); +class WidthAttribute extends Attribute { + WidthAttribute(String? val) : super('width', AttributeScope.IGNORE, val); } -class HeightAttribute extends Attribute { - HeightAttribute(String val) : super('height', AttributeScope.IGNORE, val); +class HeightAttribute extends Attribute { + HeightAttribute(String? val) : super('height', AttributeScope.IGNORE, val); } -class StyleAttribute extends Attribute { - StyleAttribute(String val) : super('style', AttributeScope.IGNORE, val); +class StyleAttribute extends Attribute { + StyleAttribute(String? val) : super('style', AttributeScope.IGNORE, val); } class TokenAttribute extends Attribute { diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 73391d7b..38813510 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -43,11 +43,11 @@ class Document { _loadDocument(_delta); } - Delta insert(int index, Object data) { + Delta insert(int index, Object? data) { assert(index >= 0); assert(data is String || data is Embeddable); if (data is Embeddable) { - data = (data as Embeddable).toJson(); + data = data.toJson(); } else if ((data as String).isEmpty) { return Delta(); } @@ -66,7 +66,7 @@ class Document { return delta; } - Delta replace(int index, int len, Object data) { + Delta replace(int index, int len, Object? data) { assert(index >= 0); assert(data is String || data is Embeddable); @@ -88,7 +88,7 @@ class Document { return delta; } - Delta format(int index, int len, Attribute attribute) { + Delta format(int index, int len, Attribute? attribute) { assert(index >= 0 && len >= 0 && attribute != null); Delta delta = Delta(); @@ -113,7 +113,7 @@ class Document { if (res.node is Line) { return res; } - Block block = res.node; + Block block = res.node as Block; return block.queryChild(res.offset, true); } @@ -126,7 +126,7 @@ class Document { delta = _transform(delta); Delta originalDelta = toDelta(); for (Operation op in delta.toList()) { - Style style = + Style? style = op.attributes != null ? Style.fromJson(op.attributes) : null; if (op.isInsert) { @@ -138,7 +138,7 @@ class Document { } if (!op.isDelete) { - offset += op.length; + offset += op.length!; } } try { @@ -197,7 +197,7 @@ class Document { } } - Object _normalize(Object data) { + Object _normalize(Object? data) { if (data is String) { return data; } @@ -205,7 +205,7 @@ class Document { if (data is Embeddable) { return data; } - return Embeddable.fromJson(data); + return Embeddable.fromJson(data as Map); } close() { @@ -227,7 +227,7 @@ class Document { op.attributes != null ? Style.fromJson(op.attributes) : null; final data = _normalize(op.data); _root.insert(offset, data, style); - offset += op.length; + offset += op.length!; } final node = _root.last; if (node is Line && diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index ea71afe9..1bb92105 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -90,13 +90,13 @@ class History { } Delta delta = source.removeLast(); // look for insert or delete - int len = 0; + int? len = 0; List ops = delta.toList(); for (var i = 0; i < ops.length; i++) { if (ops[i].key == Operation.insertKey) { len = ops[i].length; } else if (ops[i].key == Operation.deleteKey) { - len = ops[i].length * -1; + len = ops[i].length! * -1; } } Delta base = Delta.from(doc.toDelta()); diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index d48dc38d..6ab8d331 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -4,7 +4,7 @@ import 'container.dart'; import 'line.dart'; import 'node.dart'; -class Block extends Container { +class Block extends Container { @override Line get defaultChild => Line(); @@ -18,7 +18,7 @@ class Block extends Container { @override adjust() { if (isEmpty) { - Node sibling = previous; + Node? sibling = previous; unlink(); if (sibling != null) { sibling.adjust(); @@ -27,18 +27,18 @@ class Block extends Container { } Block block = this; - Node prev = block.previous; + Node? 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); + prev!.style == block.style) { + block.moveChildToNewParent(prev as Container?); block.unlink(); - block = prev; + block = prev as Block; } - Node next = block.next; + Node? next = block.next; // merging it with next block if style is the same - if (!block.isLast && block.next is Block && next.style == block.style) { + if (!block.isLast && block.next is Block && next!.style == block.style) { (next as Block).moveChildToNewParent(block); next.unlink(); } diff --git a/lib/models/documents/nodes/container.dart b/lib/models/documents/nodes/container.dart index b4a4aa58..2e1c29fa 100644 --- a/lib/models/documents/nodes/container.dart +++ b/lib/models/documents/nodes/container.dart @@ -4,7 +4,7 @@ import '../style.dart'; import 'node.dart'; /* Container of multiple nodes */ -abstract class Container extends Node { +abstract class Container extends Node { final LinkedList _children = LinkedList(); LinkedList get children => _children; @@ -26,32 +26,32 @@ abstract class Container extends Node { /// abstract methods end add(T node) { - assert(node.parent == null); - node.parent = this; - _children.add(node); + assert(node?.parent == null); + node?.parent = this; + _children.add(node as Node); } addFirst(T node) { - assert(node.parent == null); - node.parent = this; - _children.addFirst(node); + assert(node?.parent == null); + node?.parent = this; + _children.addFirst(node as Node); } void remove(T node) { - assert(node.parent == this); - node.parent = null; - _children.remove(node); + assert(node?.parent == this); + node?.parent = null; + _children.remove(node as Node); } - void moveChildToNewParent(Container newParent) { + void moveChildToNewParent(Container? newParent) { if (isEmpty) { return; } - T last = newParent.isEmpty ? null : newParent.last; + T? last = newParent!.isEmpty ? null : newParent.last as T?; while (isNotEmpty) { - T child = first; - child.unlink(); + T child = first as T; + child?.unlink(); newParent.add(child); } @@ -80,12 +80,12 @@ abstract class Container extends Node { int get length => _children.fold(0, (cur, node) => cur + node.length); @override - insert(int index, Object data, Style style) { + insert(int index, Object data, Style? style) { assert(index == 0 || (index > 0 && index < length)); if (isNotEmpty) { ChildQuery child = queryChild(index, false); - child.node.insert(child.offset, data, style); + child.node!.insert(child.offset, data, style); return; } @@ -93,21 +93,21 @@ abstract class Container extends Node { assert(index == 0); T node = defaultChild; add(node); - node.insert(index, data, style); + node?.insert(index, data, style); } @override - retain(int index, int length, Style attributes) { + retain(int index, int? length, Style? attributes) { assert(isNotEmpty); ChildQuery child = queryChild(index, false); - child.node.retain(child.offset, length, attributes); + child.node!.retain(child.offset, length, attributes); } @override - delete(int index, int length) { + delete(int index, int? length) { assert(isNotEmpty); ChildQuery child = queryChild(index, false); - child.node.delete(child.offset, length); + child.node!.delete(child.offset, length); } @override @@ -116,7 +116,7 @@ abstract class Container extends Node { /// Query of a child in a Container class ChildQuery { - final Node node; // null if not found + final Node? node; // null if not found final int offset; diff --git a/lib/models/documents/nodes/embed.dart b/lib/models/documents/nodes/embed.dart index a8dff833..3268e257 100644 --- a/lib/models/documents/nodes/embed.dart +++ b/lib/models/documents/nodes/embed.dart @@ -2,9 +2,7 @@ class Embeddable { final String type; final dynamic data; - Embeddable(this.type, this.data) - : assert(type != null), - assert(data != null); + Embeddable(this.type, this.data); Map toJson() { Map m = {type: data}; diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index 13c7b6f0..804c9a10 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -13,13 +13,9 @@ abstract class Leaf extends Node { Object get value => _value; - Leaf.val(Object val) - : assert(val != null), - _value = val; - - factory Leaf([Object data]) { - assert(data != null); + Leaf.val(Object val) : _value = val; + factory Leaf(Object data) { if (data is Embeddable) { return Embed(data); } @@ -30,14 +26,13 @@ abstract class Leaf extends Node { @override void applyStyle(Style value) { - assert( - value != null && (value.isInline || value.isIgnored || value.isEmpty), + assert((value.isInline || value.isIgnored || value.isEmpty), 'Unable to apply Style to leaf: $value'); super.applyStyle(value); } @override - Line get parent => super.parent as Line; + Line? get parent => super.parent as Line?; @override int get length { @@ -55,11 +50,11 @@ abstract class Leaf extends Node { } @override - insert(int index, Object data, Style style) { - assert(data != null && index >= 0 && index <= length); + insert(int index, Object data, Style? style) { + assert(index >= 0 && index <= length); Leaf node = Leaf(data); if (index < length) { - splitAt(index).insertBefore(node); + splitAt(index)!.insertBefore(node); } else { insertAfter(node); } @@ -67,36 +62,36 @@ abstract class Leaf extends Node { } @override - retain(int index, int len, Style style) { + retain(int index, int? len, Style? style) { if (style == null) { return; } - int local = math.min(this.length - index, len); + int local = math.min(this.length - index, len!); int remain = len - local; Leaf node = _isolate(index, local); if (remain > 0) { assert(node.next != null); - node.next.retain(0, remain, style); + node.next!.retain(0, remain, style); } node.format(style); } @override - delete(int index, int len) { + delete(int index, int? len) { assert(index < this.length); - int local = math.min(this.length - index, len); + int local = math.min(this.length - index, len!); Leaf target = _isolate(index, local); - Leaf prev = target.previous; - Leaf next = target.next; + Leaf? prev = target.previous as Leaf?; + Leaf? next = target.next as Leaf?; target.unlink(); int remain = len - local; if (remain > 0) { assert(next != null); - next.delete(0, remain); + next!.delete(0, remain); } if (prev != null) { @@ -112,7 +107,7 @@ abstract class Leaf extends Node { Text node = this as Text; // merging it with previous node if style is the same - Node prev = node.previous; + Node? prev = node.previous; if (!node.isFirst && prev is Text && prev.style == node.style) { prev._value = prev.value + node.value; node.unlink(); @@ -120,27 +115,27 @@ abstract class Leaf extends Node { } // merging it with next node if style is the same - Node next = node.next; + Node? 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) { + Leaf? cutAt(int index) { assert(index >= 0 && index <= length); - Leaf cut = splitAt(index); + Leaf? cut = splitAt(index); cut?.unlink(); return cut; } - Leaf splitAt(int index) { + Leaf? splitAt(int index) { assert(index >= 0 && index <= length); if (index == 0) { return this; } if (index == length) { - return isLast ? null : next as Leaf; + return isLast ? null : next as Leaf?; } assert(this is Text); @@ -152,7 +147,7 @@ abstract class Leaf extends Node { return split; } - format(Style style) { + format(Style? style) { if (style != null && style.isNotEmpty) { applyStyle(style); } @@ -163,7 +158,7 @@ abstract class Leaf extends Node { Leaf _isolate(int index, int length) { assert( index >= 0 && index < this.length && (index + length <= this.length)); - Leaf target = splitAt(index); + Leaf target = splitAt(index)!; target.splitAt(length); return target; } diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 90b19f03..4e3b1ac0 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -10,7 +10,7 @@ import 'container.dart'; import 'embed.dart'; import 'leaf.dart'; -class Line extends Container { +class Line extends Container { @override Leaf get defaultChild => Text(); @@ -25,28 +25,30 @@ class Line extends Container { return children.single is Embed; } - Line get nextLine { + Line? get nextLine { if (!isLast) { - return next is Block ? (next as Block).first : next; + return next is Block ? (next as Block).first as Line? : next as Line?; } if (parent is! Block) { return null; } - if (parent.isLast) { + if (parent!.isLast) { return null; } - return parent.next is Block ? (parent.next as Block).first : parent.next; + return parent!.next is Block + ? (parent!.next as Block).first as Line? + : parent!.next as Line?; } @override Delta toDelta() { final delta = children .map((child) => child.toDelta()) - .fold(Delta(), (a, b) => a.concat(b)); + .fold(Delta(), (dynamic a, b) => a.concat(b)); var attributes = style; if (parent is Block) { - Block block = parent; + Block block = parent as Block; attributes = attributes.mergeAll(block.style); } delta.insert('\n', attributes.toJson()); @@ -64,7 +66,7 @@ class Line extends Container { } @override - insert(int index, Object data, Style style) { + insert(int index, Object data, Style? style) { if (data is Embeddable) { _insert(index, data, style); return; @@ -99,13 +101,13 @@ class Line extends Container { } @override - retain(int index, int len, Style style) { + retain(int index, int? len, Style? style) { if (style == null) { return; } int thisLen = this.length; - int local = math.min(thisLen - index, len); + int local = math.min(thisLen - index, len!); if (index + local == thisLen && local == 1) { assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK)); @@ -119,13 +121,13 @@ class Line extends Container { int remain = len - local; if (remain > 0) { assert(nextLine != null); - nextLine.retain(0, remain, style); + nextLine!.retain(0, remain, style); } } @override - delete(int index, int len) { - int local = math.min(this.length - index, len); + delete(int index, int? len) { + int local = math.min(this.length - index, len!); bool deleted = index + local == this.length; if (deleted) { clearStyle(); @@ -139,35 +141,35 @@ class Line extends Container { int remain = len - local; if (remain > 0) { assert(nextLine != null); - nextLine.delete(0, remain); + nextLine!.delete(0, remain); } if (deleted && isNotEmpty) { assert(nextLine != null); - nextLine.moveChildToNewParent(this); + nextLine!.moveChildToNewParent(this); moveChildToNewParent(nextLine); } if (deleted) { - Node p = parent; + Node p = parent!; unlink(); p.adjust(); } } - void _format(Style newStyle) { + void _format(Style? newStyle) { if (newStyle == null || newStyle.isEmpty) { return; } applyStyle(newStyle); - Attribute blockStyle = newStyle.getBlockExceptHeader(); + Attribute? blockStyle = newStyle.getBlockExceptHeader(); if (blockStyle == null) { return; } if (parent is Block) { - Attribute parentStyle = (parent as Block).style.getBlockExceptHeader(); + Attribute? parentStyle = (parent as Block).style.getBlockExceptHeader(); if (blockStyle.value == null) { _unwrap(); } else if (blockStyle != parentStyle) { @@ -196,7 +198,7 @@ class Line extends Container { if (parent is! Block) { throw ArgumentError('Invalid parent'); } - Block block = parent; + Block block = parent as Block; assert(block.children.contains(this)); @@ -207,10 +209,10 @@ class Line extends Container { unlink(); block.insertAfter(this); } else { - Block before = block.clone(); + Block before = block.clone() as Block; block.insertBefore(before); - Line child = block.first; + Line child = block.first as Line; while (child != this) { child.unlink(); before.add(child); @@ -232,19 +234,19 @@ class Line extends Container { } ChildQuery query = queryChild(index, false); - while (!query.node.isLast) { - Leaf next = last; + while (!query.node!.isLast) { + Leaf next = last as Leaf; next.unlink(); line.addFirst(next); } - Leaf child = query.node; - Leaf cut = child.splitAt(query.offset); + Leaf child = query.node as Leaf; + Leaf? cut = child.splitAt(query.offset); cut?.unlink(); line.addFirst(cut); return line; } - _insert(int index, Object data, Style style) { + _insert(int index, Object data, Style? style) { assert(index == 0 || (index > 0 && index < length)); if (data is String) { @@ -256,7 +258,7 @@ class Line extends Container { if (isNotEmpty) { ChildQuery result = queryChild(index, true); - result.node.insert(result.offset, data, style); + result.node!.insert(result.offset, data, style); return; } @@ -291,26 +293,26 @@ class Line extends Container { } ChildQuery data = queryChild(offset, true); - Leaf node = data.node; + Leaf? node = data.node as Leaf?; if (node != null) { res = res.mergeAll(node.style); int pos = node.length - data.offset; - while (!node.isLast && pos < local) { - node = node.next as Leaf; - _handle(node.style); + while (!node!.isLast && pos < local) { + node = node.next as Leaf?; + _handle(node!.style); pos += node.length; } } res = res.mergeAll(style); if (parent is Block) { - Block block = parent; + Block block = parent as Block; res = res.mergeAll(block.style); } int remain = len - local; if (remain > 0) { - _handle(nextLine.collectStyle(0, remain)); + _handle(nextLine!.collectStyle(0, remain)); } return res; diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index 9c52f9c7..984ed0a1 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -9,7 +9,7 @@ import 'line.dart'; /* node in a document tree */ abstract class Node extends LinkedListEntry { - Container parent; + Container? parent; Style _style = Style(); Style get style => _style; @@ -19,9 +19,6 @@ abstract class Node extends LinkedListEntry { } void applyStyle(Style value) { - if (value == null) { - throw ArgumentError('null value'); - } _style = _style.mergeAll(value); } @@ -29,9 +26,9 @@ abstract class Node extends LinkedListEntry { _style = Style(); } - bool get isFirst => list.first == this; + bool get isFirst => list!.first == this; - bool get isLast => list.last == this; + bool get isLast => list!.last == this; int get length; @@ -50,14 +47,14 @@ abstract class Node extends LinkedListEntry { Node cur = this; do { - cur = cur.previous; + cur = cur.previous!; offset += cur.length; } while (!cur.isFirst); return offset; } int getDocumentOffset() { - final parentOffset = (parent is! Root) ? parent.getDocumentOffset() : 0; + final parentOffset = (parent is! Root) ? parent!.getDocumentOffset() : 0; return parentOffset + getOffset(); } @@ -99,20 +96,20 @@ abstract class Node extends LinkedListEntry { Delta toDelta(); - insert(int index, Object data, Style style); + insert(int index, Object data, Style? style); - retain(int index, int len, Style style); + retain(int index, int? len, Style? style); - delete(int index, int len); + delete(int index, int? len); /// abstract methods end } /* Root node of document tree */ -class Root extends Container> { +class Root extends Container> { @override - Container get defaultChild => Line(); + Container get defaultChild => Line(); @override Delta toDelta() => children diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index aa3588d6..7b9b050b 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -10,7 +10,7 @@ class Style { Style() : _attributes = {}; - static Style fromJson(Map attributes) { + static Style fromJson(Map? attributes) { if (attributes == null) { return Style(); } @@ -22,7 +22,7 @@ class Style { return Style.attr(result); } - Map toJson() => _attributes.isEmpty + Map? toJson() => _attributes.isEmpty ? null : _attributes.map((String _, Attribute attribute) => MapEntry(attribute.key, attribute.value)); @@ -46,7 +46,7 @@ class Style { bool containsKey(String key) => _attributes.containsKey(key); - Attribute getBlockExceptHeader() { + Attribute? getBlockExceptHeader() { for (Attribute val in values) { if (val.isBlockExceptHeader) { return val; diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index cbfee575..bced9b88 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -15,10 +15,10 @@ const _valueEquality = DeepCollectionEquality(); /// Decoder function to convert raw `data` object into a user-defined data type. /// /// Useful with embedded content. -typedef DataDecoder = Object Function(Object data); +typedef DataDecoder = Object? Function(Object data); /// Default data decoder which simply passes through the original value. -Object _passThroughDataDecoder(Object data) => data; +Object? _passThroughDataDecoder(Object? data) => data; /// Operation performed on a rich-text document. class Operation { @@ -40,19 +40,18 @@ class Operation { final String key; /// Length of this operation. - final int length; + final int? length; /// Payload of "insert" operation, for other types is set to empty string. - final Object data; + final Object? data; /// Rich-text attributes set by this operation, can be `null`. - Map get attributes => - _attributes == null ? null : Map.from(_attributes); - final Map _attributes; + Map? get attributes => + _attributes == null ? null : Map.from(_attributes!); + final Map? _attributes; - Operation._(this.key, this.length, this.data, Map attributes) - : assert(key != null && length != null && data != null), - assert(_validKeys.contains(key), 'Invalid operation key "$key".'), + 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; @@ -64,7 +63,7 @@ class Operation { /// /// If `dataDecoder` parameter is not null then it is used to additionally /// decode the operation's data object. Only applied to insert operations. - static Operation fromJson(Map data, {DataDecoder dataDecoder}) { + static Operation fromJson(Map data, {DataDecoder? dataDecoder}) { dataDecoder ??= _passThroughDataDecoder; final map = Map.from(data); if (map.containsKey(Operation.insertKey)) { @@ -73,10 +72,10 @@ class Operation { return Operation._( Operation.insertKey, dataLength, data, map[Operation.attributesKey]); } else if (map.containsKey(Operation.deleteKey)) { - final int length = map[Operation.deleteKey]; + final int? length = map[Operation.deleteKey]; return Operation._(Operation.deleteKey, length, '', null); } else if (map.containsKey(Operation.retainKey)) { - final int length = map[Operation.retainKey]; + final int? length = map[Operation.retainKey]; return Operation._( Operation.retainKey, length, '', map[Operation.attributesKey]); } @@ -95,13 +94,13 @@ class Operation { Operation._(Operation.deleteKey, length, '', null); /// Creates operation which inserts [text] with optional [attributes]. - factory Operation.insert(dynamic data, [Map 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]) => + factory Operation.retain(int? length, [Map? attributes]) => Operation._(Operation.retainKey, length, '', attributes); /// Returns value of this operation. @@ -119,7 +118,7 @@ class Operation { bool get isRetain => key == Operation.retainKey; /// Returns `true` if this operation has no attributes, e.g. is plain text. - bool get isPlain => (_attributes == null || _attributes.isEmpty); + bool get isPlain => (_attributes == null || _attributes!.isEmpty); /// Returns `true` if this operation sets at least one attribute. bool get isNotPlain => !isPlain; @@ -130,7 +129,7 @@ class Operation { bool get isEmpty => length == 0; /// Returns `true` is this operation is not empty. - bool get isNotEmpty => length > 0; + bool get isNotEmpty => length! > 0; @override bool operator ==(other) { @@ -144,7 +143,8 @@ class Operation { } /// Returns `true` if this operation has attribute specified by [name]. - bool hasAttribute(String name) => isNotPlain && _attributes.containsKey(name); + bool hasAttribute(String name) => + isNotPlain && _attributes!.containsKey(name); /// Returns `true` if [other] operation has the same attributes as this one. bool hasSameAttributes(Operation other) { @@ -153,9 +153,9 @@ class Operation { @override int get hashCode { - if (_attributes != null && _attributes.isNotEmpty) { + if (_attributes != null && _attributes!.isNotEmpty) { final attrsHash = - hashObjects(_attributes.entries.map((e) => hash2(e.key, e.value))); + hashObjects(_attributes!.entries.map((e) => hash2(e.key, e.value))); return hash3(key, value, attrsHash); } return hash2(key, value); @@ -181,8 +181,8 @@ class Operation { /// it is a "change delta". class Delta { /// Transforms two attribute sets. - static Map transformAttributes( - Map a, Map b, bool priority) { + static Map? transformAttributes( + Map? a, Map? b, bool priority) { if (a == null) return b; if (b == null) return null; @@ -197,8 +197,8 @@ class Delta { } /// Composes two attribute sets. - static Map composeAttributes( - Map a, Map b, + static Map? composeAttributes( + Map? a, Map? b, {bool keepNull = false}) { a ??= const {}; b ??= const {}; @@ -217,12 +217,12 @@ class Delta { ///get anti-attr result base on base static Map invertAttributes( - Map attr, Map base) { + Map? attr, Map? base) { attr ??= const {}; base ??= const {}; - var baseInverted = base.keys.fold({}, (memo, key) { - if (base[key] != attr[key] && attr.containsKey(key)) { + var baseInverted = base.keys.fold({}, (dynamic memo, key) { + if (base![key] != attr![key] && attr.containsKey(key)) { memo[key] = base[key]; } return memo; @@ -230,7 +230,7 @@ class Delta { var inverted = Map.from(attr.keys.fold(baseInverted, (memo, key) { - if (base[key] != attr[key] && !base.containsKey(key)) { + if (base![key] != attr![key] && !base.containsKey(key)) { memo[key] = null; } return memo; @@ -242,9 +242,7 @@ class Delta { int _modificationCount = 0; - Delta._(List operations) - : assert(operations != null), - _operations = operations; + Delta._(List operations) : _operations = operations; /// Creates new empty [Delta]. factory Delta() => Delta._([]); @@ -257,7 +255,7 @@ class Delta { /// /// If `dataDecoder` parameter is not null then it is used to additionally /// decode the operation's data object. Only applied to insert operations. - static Delta fromJson(List data, {DataDecoder dataDecoder}) { + static Delta fromJson(List data, {DataDecoder? dataDecoder}) { return Delta._(data .map((op) => Operation.fromJson(op, dataDecoder: dataDecoder)) .toList()); @@ -304,15 +302,14 @@ class Delta { int get hashCode => hashObjects(_operations); /// Retain [count] of characters from current position. - void retain(int count, [Map attributes]) { + void retain(int count, [Map? attributes]) { assert(count >= 0); if (count == 0) return; // no-op push(Operation.retain(count, attributes)); } /// Insert [data] at current position. - void insert(dynamic data, [Map attributes]) { - assert(data != null); + void insert(dynamic data, [Map? attributes]) { if (data is String && data.isEmpty) return; // no-op push(Operation.insert(data, attributes)); } @@ -326,10 +323,10 @@ class Delta { void _mergeWithTail(Operation operation) { assert(isNotEmpty); - assert(operation != null && last.key == operation.key); + assert(last.key == operation.key); assert(operation.data is String && last.data is String); - final length = operation.length + last.length; + final length = operation.length! + last.length!; final lastText = last.data as String; final opText = operation.data as String; final resultText = lastText + opText; @@ -396,12 +393,13 @@ class Delta { /// Returns new operation or `null` if operations from [thisIter] and /// [otherIter] nullify each other. For instance, for the pair `insert('abc')` /// and `delete(3)` composition result would be empty string. - Operation _composeOperation(DeltaIterator thisIter, DeltaIterator otherIter) { + Operation? _composeOperation( + DeltaIterator thisIter, DeltaIterator otherIter) { if (otherIter.isNextInsert) return otherIter.next(); if (thisIter.isNextDelete) return thisIter.next(); final length = math.min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter.next(length); + final thisOp = thisIter.next(length as int); final otherOp = otherIter.next(length); assert(thisOp.length == otherOp.length); @@ -448,7 +446,7 @@ class Delta { /// [thisIter]. /// /// Returns `null` if both operations nullify each other. - Operation _transformOperation( + Operation? _transformOperation( DeltaIterator thisIter, DeltaIterator otherIter, bool priority) { if (thisIter.isNextInsert && (priority || !otherIter.isNextInsert)) { return Operation.retain(thisIter.next().length); @@ -457,7 +455,7 @@ class Delta { } final length = math.min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter.next(length); + final thisOp = thisIter.next(length as int); final otherOp = otherIter.next(length); assert(thisOp.length == otherOp.length); @@ -520,12 +518,12 @@ class Delta { var baseIndex = 0; for (final op in _operations) { if (op.isInsert) { - inverted.delete(op.length); + inverted.delete(op.length!); } else if (op.isRetain && op.isPlain) { - inverted.retain(op.length, null); - baseIndex += op.length; + inverted.retain(op.length!, null); + baseIndex += op.length!; } else if (op.isDelete || (op.isRetain && op.isNotPlain)) { - final length = op.length; + final length = op.length!; final sliceDelta = base.slice(baseIndex, baseIndex + length); sliceDelta.toList().forEach((baseOp) { if (op.isDelete) { @@ -533,7 +531,7 @@ class Delta { } else if (op.isRetain && op.isNotPlain) { var invertAttr = invertAttributes(op.attributes, baseOp.attributes); inverted.retain( - baseOp.length, invertAttr.isEmpty ? null : invertAttr); + baseOp.length!, invertAttr.isEmpty ? null : invertAttr); } }); baseIndex += length; @@ -547,7 +545,7 @@ class Delta { /// Returns slice of this delta from [start] index (inclusive) to [end] /// (exclusive). - Delta slice(int start, [int end]) { + Delta slice(int start, [int? end]) { final delta = Delta(); var index = 0; var opIterator = DeltaIterator(this); @@ -559,10 +557,10 @@ class Delta { if (index < start) { op = opIterator.next(start - index); } else { - op = opIterator.next(actualEnd - index); + op = opIterator.next(actualEnd - index as int); delta.push(op); } - index += op.length; + index += op.length!; } return delta; } @@ -585,12 +583,12 @@ class Delta { while (iter.hasNext && offset <= index) { final op = iter.next(); if (op.isDelete) { - index -= math.min(op.length, index - offset); + index -= math.min(op.length!, index - offset); continue; } else if (op.isInsert && (offset < index || force)) { - index += op.length; + index += op.length!; } - offset += op.length; + offset += op.length!; } return index; } @@ -614,7 +612,7 @@ class DeltaIterator { bool get isNextRetain => nextOperationKey == Operation.retainKey; - String get nextOperationKey { + String? get nextOperationKey { if (_index < delta.length) { return delta.elementAt(_index).key; } else { @@ -630,7 +628,7 @@ class DeltaIterator { num peekLength() { if (_index < delta.length) { final operation = delta._operations[_index]; - return operation.length - _offset; + return operation.length! - _offset; } return double.infinity; } @@ -640,8 +638,6 @@ class DeltaIterator { /// Optional [length] specifies maximum length of operation to return. Note /// that actual length of returned operation may be less than specified value. Operation next([int length = 4294967296]) { - assert(length != null); - if (_modificationCount != delta._modificationCount) { throw ConcurrentModificationError(delta); } @@ -651,21 +647,21 @@ class DeltaIterator { final opKey = op.key; final opAttributes = op.attributes; final _currentOffset = _offset; - final actualLength = math.min(op.length - _currentOffset, length); - if (actualLength == op.length - _currentOffset) { + final actualLength = math.min(op.length! - _currentOffset, length); + if (actualLength == op.length! - _currentOffset) { _index++; _offset = 0; } else { _offset += actualLength; } final opData = op.isInsert && op.data is String - ? (op.data as String) - .substring(_currentOffset, _currentOffset + actualLength) + ? (op.data as String).substring( + _currentOffset as int, _currentOffset + (actualLength as int)) : op.data; 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; + final int opActualLength = opIsNotEmpty ? opLength : actualLength as int; return Operation._(opKey, opActualLength, opData, opAttributes); } return Operation.retain(length); @@ -674,14 +670,14 @@ class DeltaIterator { /// Skips [length] characters in source delta. /// /// Returns last skipped operation, or `null` if there was nothing to skip. - Operation skip(int length) { + Operation? skip(int length) { var skipped = 0; - Operation op; + Operation? op; while (skipped < length && hasNext) { final opLength = peekLength(); final skip = math.min(length - skipped, opLength); - op = next(skip); - skipped += op.length; + op = next(skip as int); + skipped += op.length!; } return op; } diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index 6cbb28ff..8f590231 100644 --- a/lib/models/rules/delete.dart +++ b/lib/models/rules/delete.dart @@ -9,7 +9,7 @@ abstract class DeleteRule extends Rule { RuleType get type => RuleType.DELETE; @override - validateArgs(int len, Object data, Attribute attribute) { + validateArgs(int? len, Object? data, Attribute? attribute) { assert(len != null); assert(data == null); assert(attribute == null); @@ -21,10 +21,10 @@ class CatchAllDeleteRule extends DeleteRule { @override Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { + {int? len, Object? data, Attribute? attribute}) { return Delta() ..retain(index) - ..delete(len); + ..delete(len!); } } @@ -32,8 +32,8 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { const PreserveLineStyleOnMergeRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { DeltaIterator itr = DeltaIterator(document); itr.skip(index); Operation op = itr.next(1); @@ -42,30 +42,30 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { } bool isNotPlain = op.isNotPlain; - Map attrs = op.attributes; + Map? attrs = op.attributes; - itr.skip(len - 1); + itr.skip(len! - 1); Delta delta = Delta() ..retain(index) ..delete(len); while (itr.hasNext) { op = itr.next(); - String text = op.data is String ? op.data as String : ''; + String text = op.data is String ? (op.data as String?)! : ''; int lineBreak = text.indexOf('\n'); if (lineBreak == -1) { - delta..retain(op.length); + delta..retain(op.length!); continue; } - Map attributes = op.attributes == null + Map? attributes = op.attributes == null ? null - : op.attributes.map((String key, dynamic value) => + : op.attributes!.map((String key, dynamic value) => MapEntry(key, null)); if (isNotPlain) { attributes ??= {}; - attributes.addAll(attrs); + attributes.addAll(attrs!); } delta..retain(lineBreak)..retain(1, attributes); break; @@ -78,33 +78,35 @@ class EnsureEmbedLineRule extends DeleteRule { const EnsureEmbedLineRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { DeltaIterator itr = DeltaIterator(document); - Operation op = itr.skip(index); - int indexDelta = 0, lengthDelta = 0, remain = len; + Operation? op = itr.skip(index); + int? indexDelta = 0, lengthDelta = 0, remain = len; bool embedFound = op != null && op.data is! String; bool hasLineBreakBefore = - !embedFound && (op == null || (op?.data as String).endsWith('\n')); + !embedFound && (op == null || (op.data as String).endsWith('\n')); if (embedFound) { Operation candidate = itr.next(1); - remain--; - if (candidate.data == '\n') { - indexDelta++; - lengthDelta--; - - candidate = itr.next(1); + if (remain != null) { remain--; if (candidate.data == '\n') { - lengthDelta++; + indexDelta++; + lengthDelta--; + + candidate = itr.next(1); + remain--; + if (candidate.data == '\n') { + lengthDelta++; + } } } } - op = itr.skip(remain); + op = itr.skip(remain!); if (op != null && - (op?.data is String ? op.data as String : '').endsWith('\n')) { + (op.data is String ? op.data as String? : '')!.endsWith('\n')) { Operation candidate = itr.next(1); if (candidate.data is! String && !hasLineBreakBefore) { embedFound = true; @@ -118,6 +120,6 @@ class EnsureEmbedLineRule extends DeleteRule { return Delta() ..retain(index + indexDelta) - ..delete(len + lengthDelta); + ..delete(len! + lengthDelta); } } diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart index 755bc0bb..f5875833 100644 --- a/lib/models/rules/format.dart +++ b/lib/models/rules/format.dart @@ -9,7 +9,7 @@ abstract class FormatRule extends Rule { RuleType get type => RuleType.FORMAT; @override - validateArgs(int len, Object data, Attribute attribute) { + validateArgs(int? len, Object? data, Attribute? attribute) { assert(len != null); assert(data == null); assert(attribute != null); @@ -20,9 +20,9 @@ class ResolveLineFormatRule extends FormatRule { const ResolveLineFormatRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (attribute.scope != AttributeScope.BLOCK) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (attribute!.scope != AttributeScope.BLOCK) { return null; } @@ -30,13 +30,13 @@ class ResolveLineFormatRule extends FormatRule { DeltaIterator itr = DeltaIterator(document); itr.skip(index); Operation op; - for (int cur = 0; cur < len && itr.hasNext; cur += op.length) { + for (int 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); + delta.retain(op.length!); continue; } - String text = op.data; + String text = op.data as String; Delta tmp = Delta(); int offset = 0; @@ -52,10 +52,10 @@ class ResolveLineFormatRule extends FormatRule { while (itr.hasNext) { op = itr.next(); - String text = op.data is String ? op.data as String : ''; + String text = op.data is String ? (op.data as String?)! : ''; int lineBreak = text.indexOf('\n'); if (lineBreak < 0) { - delta..retain(op.length); + delta..retain(op.length!); continue; } delta..retain(lineBreak)..retain(1, attribute.toJson()); @@ -69,28 +69,28 @@ class FormatLinkAtCaretPositionRule extends FormatRule { const FormatLinkAtCaretPositionRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (attribute.key != Attribute.link.key || len > 0) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (attribute!.key != Attribute.link.key || len! > 0) { return null; } Delta delta = Delta(); DeltaIterator itr = DeltaIterator(document); - Operation before = itr.skip(index), after = itr.next(); - int beg = index, retain = 0; + Operation? before = itr.skip(index), after = itr.next(); + int? beg = index, retain = 0; if (before != null && before.hasAttribute(attribute.key)) { - beg -= before.length; + beg -= before.length!; retain = before.length; } - if (after != null && after.hasAttribute(attribute.key)) { - retain += after.length; + if (after.hasAttribute(attribute.key)) { + if (retain != null) retain += after.length!; } if (retain == 0) { return null; } - delta..retain(beg)..retain(retain, attribute.toJson()); + delta..retain(beg)..retain(retain!, attribute.toJson()); return delta; } } @@ -99,9 +99,9 @@ class ResolveInlineFormatRule extends FormatRule { const ResolveInlineFormatRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (attribute.scope != AttributeScope.INLINE) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (attribute!.scope != AttributeScope.INLINE) { return null; } @@ -110,12 +110,12 @@ class ResolveInlineFormatRule extends FormatRule { itr.skip(index); Operation op; - for (int cur = 0; cur < len && itr.hasNext; cur += op.length) { + for (int cur = 0; cur < len! && itr.hasNext; cur += op.length!) { op = itr.next(len - cur); - String text = op.data is String ? op.data as String : ''; + String text = op.data is String ? (op.data as String?)! : ''; int lineBreak = text.indexOf('\n'); if (lineBreak < 0) { - delta.retain(op.length, attribute.toJson()); + delta.retain(op.length!, attribute.toJson()); continue; } int pos = 0; @@ -124,8 +124,8 @@ class ResolveInlineFormatRule extends FormatRule { pos = lineBreak + 1; lineBreak = text.indexOf('\n', pos); } - if (pos < op.length) { - delta.retain(op.length - pos, attribute.toJson()); + if (pos < op.length!) { + delta.retain(op.length! - pos, attribute.toJson()); } } diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 1973d981..61829972 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -11,7 +11,7 @@ abstract class InsertRule extends Rule { RuleType get type => RuleType.INSERT; @override - validateArgs(int len, Object data, Attribute attribute) { + validateArgs(int? len, Object? data, Attribute? attribute) { assert(len == null); assert(data != null); assert(attribute == null); @@ -22,23 +22,21 @@ class PreserveLineStyleOnSplitRule extends InsertRule { const PreserveLineStyleOnSplitRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (data is! String || (data as String) != '\n') { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data != '\n') { return null; } DeltaIterator itr = DeltaIterator(document); - Operation before = itr.skip(index); + Operation? before = itr.skip(index); if (before == null || before.data is! String || (before.data as String).endsWith('\n')) { return null; } Operation after = itr.next(); - if (after == null || - after.data is! String || - (after.data as String).startsWith('\n')) { + if (after.data is! String || (after.data as String).startsWith('\n')) { return null; } @@ -50,8 +48,8 @@ class PreserveLineStyleOnSplitRule extends InsertRule { delta..insert('\n'); return delta; } - Tuple2 nextNewLine = _getNextNewLine(itr); - Map attributes = nextNewLine?.item1?.attributes; + Tuple2 nextNewLine = _getNextNewLine(itr); + Map? attributes = nextNewLine.item1?.attributes; return delta..insert('\n', attributes); } @@ -61,33 +59,33 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { const PreserveBlockStyleOnInsertRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (data is! String || !(data as String).contains('\n')) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || !data.contains('\n')) { return null; } DeltaIterator itr = DeltaIterator(document); itr.skip(index); - Tuple2 nextNewLine = _getNextNewLine(itr); + Tuple2 nextNewLine = _getNextNewLine(itr); Style lineStyle = Style.fromJson(nextNewLine.item1?.attributes ?? {}); - Attribute attribute = lineStyle.getBlockExceptHeader(); + Attribute? attribute = lineStyle.getBlockExceptHeader(); if (attribute == null) { return null; } var blockStyle = {attribute.key: attribute.value}; - Map resetStyle; + Map? resetStyle; if (lineStyle.containsKey(Attribute.header.key)) { resetStyle = Attribute.header.toJson(); } - List lines = (data as String).split('\n'); + List lines = data.split('\n'); Delta delta = Delta()..retain(index); for (int i = 0; i < lines.length; i++) { String line = lines[i]; @@ -102,9 +100,9 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { } if (resetStyle != null) { - delta.retain(nextNewLine.item2); + delta.retain(nextNewLine.item2!); delta - ..retain((nextNewLine.item1.data as String).indexOf('\n')) + ..retain((nextNewLine.item1!.data as String).indexOf('\n')) ..retain(1, resetStyle); } @@ -115,26 +113,26 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { class AutoExitBlockRule extends InsertRule { const AutoExitBlockRule(); - bool _isEmptyLine(Operation before, Operation after) { + bool _isEmptyLine(Operation? before, Operation? after) { if (before == null) { return true; } return before.data is String && (before.data as String).endsWith('\n') && - after.data is String && + after!.data is String && (after.data as String).startsWith('\n'); } @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (data is! String || (data as String) != '\n') { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data != '\n') { return null; } DeltaIterator itr = DeltaIterator(document); - Operation prev = itr.skip(index), cur = itr.next(); - Attribute blockStyle = + Operation? prev = itr.skip(index), cur = itr.next(); + Attribute? blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader(); if (cur.isPlain || blockStyle == null) { return null; @@ -147,10 +145,10 @@ class AutoExitBlockRule extends InsertRule { return null; } - Tuple2 nextNewLine = _getNextNewLine(itr); + Tuple2 nextNewLine = _getNextNewLine(itr); if (nextNewLine.item1 != null && - nextNewLine.item1.attributes != null && - Style.fromJson(nextNewLine.item1.attributes).getBlockExceptHeader() == + nextNewLine.item1!.attributes != null && + Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() == blockStyle) { return null; } @@ -168,9 +166,9 @@ class ResetLineFormatOnNewLineRule extends InsertRule { const ResetLineFormatOnNewLineRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (data is! String || (data as String) != '\n') { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data != '\n') { return null; } @@ -181,9 +179,9 @@ class ResetLineFormatOnNewLineRule extends InsertRule { return null; } - Map resetStyle; + Map? resetStyle; if (cur.attributes != null && - cur.attributes.containsKey(Attribute.header.key)) { + cur.attributes!.containsKey(Attribute.header.key)) { resetStyle = Attribute.header.toJson(); } return Delta() @@ -198,33 +196,33 @@ class InsertEmbedsRule extends InsertRule { const InsertEmbedsRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { if (data is String) { return null; } Delta delta = Delta()..retain(index); DeltaIterator itr = DeltaIterator(document); - Operation prev = itr.skip(index), cur = itr.next(); + Operation? 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 : ''; + String? textBefore = prev?.data is String ? prev!.data as String? : ''; + String textAfter = cur.data is String ? (cur.data as String?)! : ''; - final isNewlineBefore = prev == null || textBefore.endsWith('\n'); + final isNewlineBefore = prev == null || textBefore!.endsWith('\n'); final isNewlineAfter = textAfter.startsWith('\n'); if (isNewlineBefore && isNewlineAfter) { return delta..insert(data); } - Map lineStyle; + Map? lineStyle; if (textAfter.contains('\n')) { lineStyle = cur.attributes; } else { while (itr.hasNext) { Operation op = itr.next(); - if ((op.data is String ? op.data as String : '').indexOf('\n') >= 0) { + if ((op.data is String ? op.data as String? : '')!.indexOf('\n') >= 0) { lineStyle = op.attributes; break; } @@ -246,13 +244,13 @@ class ForceNewlineForInsertsAroundEmbedRule extends InsertRule { const ForceNewlineForInsertsAroundEmbedRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { if (data is! String) { return null; } - String text = data as String; + String text = data; DeltaIterator itr = DeltaIterator(document); final prev = itr.skip(index); final cur = itr.next(); @@ -277,14 +275,14 @@ class AutoFormatLinksRule extends InsertRule { const AutoFormatLinksRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (data is! String || (data as String) != ' ') { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data != ' ') { return null; } DeltaIterator itr = DeltaIterator(document); - Operation prev = itr.skip(index); + Operation? prev = itr.skip(index); if (prev == null || prev.data is! String) { return null; } @@ -305,7 +303,7 @@ class AutoFormatLinksRule extends InsertRule { return Delta() ..retain(index - cand.length) ..retain(cand.length, attributes) - ..insert(data as String, prev.attributes); + ..insert(data, prev.attributes); } on FormatException { return null; } @@ -316,22 +314,22 @@ class PreserveInlineStylesRule extends InsertRule { const PreserveInlineStylesRule(); @override - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { - if (data is! String || (data as String).contains('\n')) { + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data.contains('\n')) { return null; } DeltaIterator itr = DeltaIterator(document); - Operation prev = itr.skip(index); + Operation? 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 as String; + Map? attributes = prev.attributes; + String text = data; if (attributes == null || !attributes.containsKey(Attribute.link.key)) { return Delta() ..retain(index) @@ -343,9 +341,7 @@ class PreserveInlineStylesRule extends InsertRule { ..retain(index) ..insert(text, attributes.isEmpty ? null : attributes); Operation next = itr.next(); - if (next == null) { - return delta; - } + Map nextAttributes = next.attributes ?? const {}; if (!nextAttributes.containsKey(Attribute.link.key)) { @@ -365,18 +361,19 @@ class CatchAllInsertRule extends InsertRule { @override Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}) { + {int? len, Object? data, Attribute? attribute}) { return Delta() ..retain(index) ..insert(data); } } -Tuple2 _getNextNewLine(DeltaIterator iterator) { +Tuple2 _getNextNewLine(DeltaIterator iterator) { Operation op; - for (int skipped = 0; iterator.hasNext; skipped += op.length) { + for (int skipped = 0; iterator.hasNext; skipped += op.length!) { op = iterator.next(); - int lineBreak = (op.data is String ? op.data as String : '').indexOf('\n'); + int 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 13c7d12e..a549c22f 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -11,19 +11,17 @@ enum RuleType { INSERT, DELETE, FORMAT } abstract class Rule { const Rule(); - Delta apply(Delta document, int index, - {int len, Object data, Attribute attribute}) { - assert(document != null); - assert(index != null); + Delta? apply(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { validateArgs(len, data, attribute); return applyRule(document, index, len: len, data: data, attribute: attribute); } - validateArgs(int len, Object data, Attribute attribute); + validateArgs(int? len, Object? data, Attribute? attribute); - Delta applyRule(Delta document, int index, - {int len, Object data, Attribute attribute}); + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}); RuleType get type; } @@ -53,7 +51,7 @@ class Rules { static Rules getInstance() => _instance; Delta apply(RuleType ruleType, Document document, int index, - {int len, Object data, Attribute attribute}) { + {int? len, Object? data, Attribute? attribute}) { final delta = document.toDelta(); for (var rule in _rules) { if (rule.type != ruleType) { diff --git a/lib/utils/color.dart b/lib/utils/color.dart index f37906ff..c7f467fa 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -2,7 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -Color stringToColor(String s) { +Color stringToColor(String? s) { switch (s) { case 'transparent': return Colors.transparent; @@ -106,7 +106,7 @@ Color stringToColor(String s) { return Colors.brown; } - if (s.startsWith('rgba')) { + if (s!.startsWith('rgba')) { s = s.substring(5); // trim left 'rgba(' s = s.substring(0, s.length - 1); // trim right ')' final arr = s.split(',').map((e) => e.trim()).toList(); diff --git a/lib/utils/diff_delta.dart b/lib/utils/diff_delta.dart index 673dacd2..8733c48f 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -76,7 +76,7 @@ int getPositionDelta(Delta user, Delta actual) { int diff = 0; while (userItr.hasNext || actualItr.hasNext) { final length = math.min(userItr.peekLength(), actualItr.peekLength()); - Operation userOperation = userItr.next(length); + Operation userOperation = userItr.next(length as int); Operation actualOperation = actualItr.next(length); if (userOperation.length != actualOperation.length) { throw ('userOp ' + @@ -88,18 +88,18 @@ int getPositionDelta(Delta user, Delta actual) { if (userOperation.key == actualOperation.key) { continue; } else if (userOperation.isInsert && actualOperation.isRetain) { - diff -= userOperation.length; + diff -= userOperation.length!; } else if (userOperation.isDelete && actualOperation.isRetain) { - diff += userOperation.length; + diff += userOperation.length!; } else if (userOperation.isRetain && actualOperation.isInsert) { - String operationTxt = ''; + String? operationTxt = ''; if (actualOperation.data is String) { - operationTxt = actualOperation.data as String; + operationTxt = actualOperation.data as String?; } - if (operationTxt.startsWith('\n')) { + if (operationTxt!.startsWith('\n')) { continue; } - diff += actualOperation.length; + diff += actualOperation.length!; } } return diff; diff --git a/lib/utils/universal_ui/fake_ui.dart b/lib/utils/universal_ui/fake_ui.dart new file mode 100644 index 00000000..da0f9a32 --- /dev/null +++ b/lib/utils/universal_ui/fake_ui.dart @@ -0,0 +1,3 @@ +class platformViewRegistry { + static registerViewFactory(String viewId, dynamic cb) {} +} diff --git a/lib/utils/universal_ui/real_ui.dart b/lib/utils/universal_ui/real_ui.dart new file mode 100644 index 00000000..c2b8ea23 --- /dev/null +++ b/lib/utils/universal_ui/real_ui.dart @@ -0,0 +1,9 @@ +import 'dart:ui' as ui; + +// ignore: camel_case_types +class platformViewRegistry { + static registerViewFactory(String viewId, dynamic cb) { + // ignore:undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory(viewId, cb); + } +} diff --git a/lib/utils/universal_ui/universal_ui.dart b/lib/utils/universal_ui/universal_ui.dart new file mode 100644 index 00000000..d97aff1f --- /dev/null +++ b/lib/utils/universal_ui/universal_ui.dart @@ -0,0 +1,22 @@ +library universal_ui; + +import 'package:flutter/foundation.dart'; +import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; + +class PlatformViewRegistryFix { + registerViewFactory(dynamic x, dynamic y) { + if (kIsWeb) { + // ignore: undefined_prefixed_name + ui_instance.platformViewRegistry.registerViewFactory( + x, + y, + ); + } else {} + } +} + +class UniversalUI { + PlatformViewRegistryFix platformViewRegistry = PlatformViewRegistryFix(); +} + +var ui = UniversalUI(); diff --git a/lib/widgets/box.dart b/lib/widgets/box.dart index 9a28d1e1..5e43b841 100644 --- a/lib/widgets/box.dart +++ b/lib/widgets/box.dart @@ -4,11 +4,11 @@ import 'package:flutter_quill/models/documents/nodes/container.dart'; abstract class RenderContentProxyBox implements RenderBox { double getPreferredLineHeight(); - Offset getOffsetForCaret(TextPosition position, Rect caretPrototype); + Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype); TextPosition getPositionForOffset(Offset offset); - double getFullHeightForCaret(TextPosition position); + double? getFullHeightForCaret(TextPosition position); TextRange getWordBoundary(TextPosition position); @@ -24,9 +24,9 @@ abstract class RenderEditableBox extends RenderBox { TextPosition getPositionForOffset(Offset offset); - TextPosition getPositionAbove(TextPosition position); + TextPosition? getPositionAbove(TextPosition position); - TextPosition getPositionBelow(TextPosition position); + TextPosition? getPositionBelow(TextPosition position); TextRange getWordBoundary(TextPosition position); diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 9c23d327..2ea56e32 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -14,9 +14,7 @@ class QuillController extends ChangeNotifier { TextSelection selection; Style toggledStyle = Style(); - QuillController({@required this.document, @required this.selection}) - : assert(document != null), - assert(selection != null); + QuillController({required this.document, required this.selection}); factory QuillController.basic() { return QuillController( @@ -49,14 +47,14 @@ class QuillController extends ChangeNotifier { } } - void _handleHistoryChange(int len) { + void _handleHistoryChange(int? len) { if (len != 0) { // if (this.selection.extentOffset >= document.length) { // // cursor exceeds the length of document, position it in the end // updateSelection( // TextSelection.collapsed(offset: document.length), ChangeSource.LOCAL); updateSelection( - TextSelection.collapsed(offset: this.selection.baseOffset + len), + TextSelection.collapsed(offset: this.selection.baseOffset + len!), ChangeSource.LOCAL); } else { // no need to move cursor @@ -75,19 +73,18 @@ class QuillController extends ChangeNotifier { get hasRedo => document.hasRedo; - replaceText(int index, int len, Object data, TextSelection textSelection) { + replaceText(int index, int len, Object? data, TextSelection? textSelection) { assert(data is String || data is Embeddable); - Delta delta; - if (len > 0 || data is! String || (data as String).isNotEmpty) { + Delta? delta; + if (len > 0 || data is! String || data.isNotEmpty) { try { delta = document.replace(index, len, data); } catch (e) { print('document.replace failed: $e'); throw e; } - bool shouldRetainDelta = delta != null && - toggledStyle.isNotEmpty && + bool shouldRetainDelta = toggledStyle.isNotEmpty && delta.isNotEmpty && delta.length <= 2 && delta.last.isInsert; @@ -137,8 +134,10 @@ class QuillController extends ChangeNotifier { notifyListeners(); } - formatText(int index, int len, Attribute attribute) { - if (len == 0 && attribute.isInline && attribute.key != Attribute.link.key) { + formatText(int index, int len, Attribute? attribute) { + if (len == 0 && + attribute!.isInline && + attribute.key != Attribute.link.key) { toggledStyle = toggledStyle.put(attribute); } @@ -152,7 +151,7 @@ class QuillController extends ChangeNotifier { notifyListeners(); } - formatSelection(Attribute attribute) { + formatSelection(Attribute? attribute) { formatText(selection.start, selection.end - selection.start, attribute); } @@ -165,18 +164,15 @@ class QuillController extends ChangeNotifier { if (delta.isNotEmpty) { document.compose(delta, source); } - if (textSelection != null) { + + textSelection = selection.copyWith( + baseOffset: delta.transformPosition(selection.baseOffset, force: false), + extentOffset: + delta.transformPosition(selection.extentOffset, force: false)); + if (selection != textSelection) { _updateSelection(textSelection, source); - } else { - textSelection = selection.copyWith( - baseOffset: - delta.transformPosition(selection.baseOffset, force: false), - extentOffset: - delta.transformPosition(selection.extentOffset, force: false)); - if (selection != textSelection) { - _updateSelection(textSelection, source); - } } + notifyListeners(); } @@ -187,8 +183,6 @@ class QuillController extends ChangeNotifier { } _updateSelection(TextSelection textSelection, ChangeSource source) { - assert(textSelection != null); - assert(source != null); selection = textSelection; int end = document.length - 1; selection = selection.copyWith( diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index 6318dba6..d6305569 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -11,25 +11,22 @@ class CursorStyle { final Color color; final Color backgroundColor; final double width; - final double height; - final Radius radius; - final Offset offset; + final double? height; + final Radius? radius; + final Offset? offset; final bool opacityAnimates; final bool paintAboveText; const CursorStyle({ - @required this.color, - @required this.backgroundColor, + required this.color, + required this.backgroundColor, this.width = 1.0, this.height, this.radius, this.offset, this.opacityAnimates = false, this.paintAboveText = false, - }) : assert(color != null), - assert(backgroundColor != null), - assert(opacityAnimates != null), - assert(paintAboveText != null); + }); @override bool operator ==(Object other) => @@ -61,19 +58,16 @@ class CursorCont extends ChangeNotifier { final ValueNotifier show; final ValueNotifier _blink; final ValueNotifier color; - AnimationController _blinkOpacityCont; - Timer _cursorTimer; + late AnimationController _blinkOpacityCont; + Timer? _cursorTimer; bool _targetCursorVisibility = false; CursorStyle _style; CursorCont({ - @required ValueNotifier show, - @required CursorStyle style, - @required TickerProvider tickerProvider, - }) : assert(show != null), - assert(style != null), - assert(tickerProvider != null), - show = show ?? ValueNotifier(false), + required ValueNotifier show, + required CursorStyle style, + required TickerProvider tickerProvider, + }) : show = show, _style = style, _blink = ValueNotifier(false), color = ValueNotifier(style.color) { @@ -89,7 +83,6 @@ class CursorCont extends ChangeNotifier { CursorStyle get style => _style; set style(CursorStyle value) { - assert(value != null); if (_style == value) return; _style = value; notifyListeners(); @@ -161,9 +154,9 @@ class CursorCont extends ChangeNotifier { } class CursorPainter { - final RenderContentProxyBox editable; + final RenderContentProxyBox? editable; final CursorStyle style; - final Rect prototype; + final Rect? prototype; final Color color; final double devicePixelRatio; @@ -174,17 +167,17 @@ class CursorPainter { assert(prototype != null); Offset caretOffset = - editable.getOffsetForCaret(position, prototype) + offset; - Rect caretRect = prototype.shift(caretOffset); + editable!.getOffsetForCaret(position, prototype) + offset; + Rect caretRect = prototype!.shift(caretOffset); if (style.offset != null) { - caretRect = caretRect.shift(style.offset); + caretRect = caretRect.shift(style.offset!); } if (caretRect.left < 0.0) { caretRect = caretRect.shift(Offset(-caretRect.left, 0.0)); } - double caretHeight = editable.getFullHeightForCaret(position); + double? caretHeight = editable!.getFullHeightForCaret(position); if (caretHeight != null) { switch (defaultTargetPlatform) { case TargetPlatform.android: @@ -212,7 +205,7 @@ class CursorPainter { } } - Offset caretPosition = editable.localToGlobal(caretRect.topLeft); + Offset caretPosition = editable!.localToGlobal(caretRect.topLeft); double pixelMultiple = 1.0 / devicePixelRatio; caretRect = caretRect.shift(Offset( caretPosition.dx.isFinite @@ -230,7 +223,7 @@ class CursorPainter { return; } - RRect caretRRect = RRect.fromRectAndRadius(caretRect, style.radius); + RRect 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 4cad7bbb..7beb9f5c 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -6,25 +6,23 @@ class QuillStyles extends InheritedWidget { final DefaultStyles data; QuillStyles({ - Key key, - @required this.data, - @required Widget child, - }) : assert(data != null), - assert(child != null), - super(key: key, child: child); + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); @override bool updateShouldNotify(QuillStyles oldWidget) { return data != oldWidget.data; } - static DefaultStyles getStyles(BuildContext context, bool nullOk) { + static DefaultStyles? getStyles(BuildContext context, bool nullOk) { var widget = context.dependOnInheritedWidgetOfExactType(); if (widget == null && nullOk) { return null; } assert(widget != null); - return widget.data; + return widget!.data; } } @@ -35,31 +33,31 @@ class DefaultTextBlockStyle { final Tuple2 lineSpacing; - final BoxDecoration decoration; + final BoxDecoration? decoration; DefaultTextBlockStyle( this.style, this.verticalSpacing, this.lineSpacing, this.decoration); } class DefaultStyles { - final DefaultTextBlockStyle h1; - final DefaultTextBlockStyle h2; - final DefaultTextBlockStyle h3; - final DefaultTextBlockStyle paragraph; - final TextStyle bold; - final TextStyle italic; - final TextStyle underline; - final TextStyle strikeThrough; - final TextStyle sizeSmall; // 'small' - final TextStyle sizeLarge; // 'large' - final TextStyle sizeHuge; // 'huge' - final TextStyle link; - final DefaultTextBlockStyle placeHolder; - final DefaultTextBlockStyle lists; - final DefaultTextBlockStyle quote; - final DefaultTextBlockStyle code; - final DefaultTextBlockStyle indent; - final DefaultTextBlockStyle align; + final DefaultTextBlockStyle? h1; + final DefaultTextBlockStyle? h2; + final DefaultTextBlockStyle? h3; + final DefaultTextBlockStyle? paragraph; + final TextStyle? bold; + final TextStyle? italic; + final TextStyle? underline; + final TextStyle? strikeThrough; + final TextStyle? sizeSmall; // 'small' + final TextStyle? sizeLarge; // 'large' + final TextStyle? sizeHuge; // 'huge' + final TextStyle? link; + final DefaultTextBlockStyle? placeHolder; + final DefaultTextBlockStyle? lists; + final DefaultTextBlockStyle? quote; + final DefaultTextBlockStyle? code; + final DefaultTextBlockStyle? indent; + final DefaultTextBlockStyle? align; DefaultStyles( {this.h1, @@ -109,7 +107,7 @@ class DefaultStyles { h1: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( fontSize: 34.0, - color: defaultTextStyle.style.color.withOpacity(0.70), + color: defaultTextStyle.style.color!.withOpacity(0.70), height: 1.15, fontWeight: FontWeight.w300, ), @@ -119,7 +117,7 @@ class DefaultStyles { h2: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( fontSize: 24.0, - color: defaultTextStyle.style.color.withOpacity(0.70), + color: defaultTextStyle.style.color!.withOpacity(0.70), height: 1.15, fontWeight: FontWeight.normal, ), @@ -129,7 +127,7 @@ class DefaultStyles { h3: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( fontSize: 20.0, - color: defaultTextStyle.style.color.withOpacity(0.70), + color: defaultTextStyle.style.color!.withOpacity(0.70), height: 1.25, fontWeight: FontWeight.w500, ), @@ -158,7 +156,7 @@ class DefaultStyles { lists: DefaultTextBlockStyle( baseStyle, baseSpacing, Tuple2(0.0, 6.0), null), quote: DefaultTextBlockStyle( - TextStyle(color: baseStyle.color.withOpacity(0.6)), + TextStyle(color: baseStyle.color!.withOpacity(0.6)), baseSpacing, Tuple2(6.0, 2.0), BoxDecoration( diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index aee7a058..a8278246 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -14,28 +14,27 @@ abstract class EditorTextSelectionGestureDetectorBuilderDelegate { bool getForcePressEnabled(); - bool getSelectionEnabled(); + bool? getSelectionEnabled(); } class EditorTextSelectionGestureDetectorBuilder { final EditorTextSelectionGestureDetectorBuilderDelegate delegate; bool shouldShowSelectionToolbar = true; - EditorTextSelectionGestureDetectorBuilder(this.delegate) - : assert(delegate != null); + EditorTextSelectionGestureDetectorBuilder(this.delegate); - EditorState getEditor() { + EditorState? getEditor() { return delegate.getEditableTextKey().currentState; } - RenderEditor getRenderEditor() { - return this.getEditor().getRenderEditor(); + RenderEditor? getRenderEditor() { + return this.getEditor()!.getRenderEditor(); } onTapDown(TapDownDetails details) { - getRenderEditor().handleTapDown(details); + getRenderEditor()!.handleTapDown(details); - PointerDeviceKind kind = details.kind; + PointerDeviceKind? kind = details.kind; shouldShowSelectionToolbar = kind == null || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; @@ -44,8 +43,8 @@ class EditorTextSelectionGestureDetectorBuilder { onForcePressStart(ForcePressDetails details) { assert(delegate.getForcePressEnabled()); shouldShowSelectionToolbar = true; - if (delegate.getSelectionEnabled()) { - getRenderEditor().selectWordsInRange( + if (delegate.getSelectionEnabled()!) { + getRenderEditor()!.selectWordsInRange( details.globalPosition, null, SelectionChangedCause.forcePress, @@ -55,27 +54,27 @@ class EditorTextSelectionGestureDetectorBuilder { onForcePressEnd(ForcePressDetails details) { assert(delegate.getForcePressEnabled()); - getRenderEditor().selectWordsInRange( + getRenderEditor()!.selectWordsInRange( details.globalPosition, null, SelectionChangedCause.forcePress, ); if (shouldShowSelectionToolbar) { - getEditor().showToolbar(); + getEditor()!.showToolbar(); } } onSingleTapUp(TapUpDetails details) { - if (delegate.getSelectionEnabled()) { - getRenderEditor().selectWordEdge(SelectionChangedCause.tap); + if (delegate.getSelectionEnabled()!) { + getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); } } onSingleTapCancel() {} onSingleLongTapStart(LongPressStartDetails details) { - if (delegate.getSelectionEnabled()) { - getRenderEditor().selectPositionAt( + if (delegate.getSelectionEnabled()!) { + getRenderEditor()!.selectPositionAt( details.globalPosition, null, SelectionChangedCause.longPress, @@ -84,8 +83,8 @@ class EditorTextSelectionGestureDetectorBuilder { } onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (delegate.getSelectionEnabled()) { - getRenderEditor().selectPositionAt( + if (delegate.getSelectionEnabled()!) { + getRenderEditor()!.selectPositionAt( details.globalPosition, null, SelectionChangedCause.longPress, @@ -95,21 +94,21 @@ class EditorTextSelectionGestureDetectorBuilder { onSingleLongTapEnd(LongPressEndDetails details) { if (shouldShowSelectionToolbar) { - getEditor().showToolbar(); + getEditor()!.showToolbar(); } } onDoubleTapDown(TapDownDetails details) { - if (delegate.getSelectionEnabled()) { - getRenderEditor().selectWord(SelectionChangedCause.tap); + if (delegate.getSelectionEnabled()!) { + getRenderEditor()!.selectWord(SelectionChangedCause.tap); if (shouldShowSelectionToolbar) { - getEditor().showToolbar(); + getEditor()!.showToolbar(); } } } onDragSelectionStart(DragStartDetails details) { - getRenderEditor().selectPositionAt( + getRenderEditor()!.selectPositionAt( details.globalPosition, null, SelectionChangedCause.drag, @@ -118,7 +117,7 @@ class EditorTextSelectionGestureDetectorBuilder { onDragSelectionUpdate( DragStartDetails startDetails, DragUpdateDetails updateDetails) { - getRenderEditor().selectPositionAt( + getRenderEditor()!.selectPositionAt( startDetails.globalPosition, updateDetails.globalPosition, SelectionChangedCause.drag, diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 6c355b22..12b698bf 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -16,13 +16,13 @@ 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/utils/universal_ui/universal_ui.dart'; import 'package:flutter_quill/widgets/image.dart'; import 'package:flutter_quill/widgets/raw_editor.dart'; import 'package:flutter_quill/widgets/responsive_widget.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; import 'package:string_validator/string_validator.dart'; -import 'package:universal_html/prefer_universal/html.dart' as html; -import 'package:universal_ui/universal_ui.dart'; +import 'package:universal_html/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; import 'box.dart'; @@ -53,9 +53,9 @@ abstract class EditorState extends State { void setTextEditingValue(TextEditingValue value); - RenderEditor getRenderEditor(); + RenderEditor? getRenderEditor(); - EditorTextSelectionOverlay getSelectionOverlay(); + EditorTextSelectionOverlay? getSelectionOverlay(); bool showToolbar(); @@ -146,51 +146,45 @@ class QuillEditor extends StatefulWidget { final bool scrollable; final EdgeInsetsGeometry padding; final bool autoFocus; - final bool showCursor; + final bool? showCursor; final bool readOnly; - final String placeholder; - final bool enableInteractiveSelection; - final double minHeight; - final double maxHeight; - final DefaultStyles customStyles; + final String? placeholder; + final bool? enableInteractiveSelection; + final double? minHeight; + final double? maxHeight; + final DefaultStyles? customStyles; final bool expands; final TextCapitalization textCapitalization; final Brightness keyboardAppearance; - final ScrollPhysics scrollPhysics; - final ValueChanged onLaunchUrl; + final ScrollPhysics? scrollPhysics; + final ValueChanged? onLaunchUrl; final EmbedBuilder embedBuilder; QuillEditor( - {@required this.controller, - @required this.focusNode, - @required this.scrollController, - @required this.scrollable, - @required this.padding, - @required this.autoFocus, + {Key? key, + required this.controller, + required this.focusNode, + required this.scrollController, + required this.scrollable, + required this.padding, + required this.autoFocus, this.showCursor, - @required this.readOnly, + required this.readOnly, this.placeholder, this.enableInteractiveSelection, this.minHeight, this.maxHeight, this.customStyles, - @required this.expands, + required this.expands, this.textCapitalization = TextCapitalization.sentences, this.keyboardAppearance = Brightness.light, this.scrollPhysics, this.onLaunchUrl, this.embedBuilder = - kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder}) - : assert(controller != null), - assert(scrollController != null), - assert(scrollable != null), - assert(focusNode != null), - assert(autoFocus != null), - assert(readOnly != null), - assert(embedBuilder != null); + kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder}); factory QuillEditor.basic( - {@required QuillController controller, bool readOnly}) { + {Key? key, required QuillController controller, required bool readOnly}) { return QuillEditor( controller: controller, scrollController: ScrollController(), @@ -209,8 +203,9 @@ class QuillEditor extends StatefulWidget { class _QuillEditorState extends State implements EditorTextSelectionGestureDetectorBuilderDelegate { - final GlobalKey _editorKey = GlobalKey(); - EditorTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; + GlobalKey _editorKey = GlobalKey(); + late EditorTextSelectionGestureDetectorBuilder + _selectionGestureDetectorBuilder; @override void initState() { @@ -227,10 +222,10 @@ class _QuillEditorState extends State TextSelectionControls textSelectionControls; bool paintCursorAboveText; bool cursorOpacityAnimates; - Offset cursorOffset; - Color cursorColor; + Offset? cursorOffset; + Color? cursorColor; Color selectionColor; - Radius cursorRadius; + Radius? cursorRadius; switch (theme.platform) { case TargetPlatform.android: @@ -301,7 +296,7 @@ class _QuillEditorState extends State selectionColor, textSelectionControls, widget.keyboardAppearance, - widget.enableInteractiveSelection, + widget.enableInteractiveSelection!, widget.scrollPhysics, widget.embedBuilder), ); @@ -318,12 +313,12 @@ class _QuillEditorState extends State } @override - bool getSelectionEnabled() { + bool? getSelectionEnabled() { return widget.enableInteractiveSelection; } _requestKeyboard() { - _editorKey.currentState.requestKeyboard(); + _editorKey.currentState!.requestKeyboard(); } } @@ -336,8 +331,8 @@ class _QuillEditorSelectionGestureDetectorBuilder @override onForcePressStart(ForcePressDetails details) { super.onForcePressStart(details); - if (delegate.getSelectionEnabled() && shouldShowSelectionToolbar) { - getEditor().showToolbar(); + if (delegate.getSelectionEnabled()! && shouldShowSelectionToolbar) { + getEditor()!.showToolbar(); } } @@ -346,13 +341,13 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (!delegate.getSelectionEnabled()) { + if (!delegate.getSelectionEnabled()!) { return; } switch (Theme.of(_state.context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: - getRenderEditor().selectPositionAt( + getRenderEditor()!.selectPositionAt( details.globalPosition, null, SelectionChangedCause.longPress, @@ -362,7 +357,7 @@ class _QuillEditorSelectionGestureDetectorBuilder case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - getRenderEditor().selectWordsInRange( + getRenderEditor()!.selectWordsInRange( details.globalPosition - details.offsetFromOrigin, details.globalPosition, SelectionChangedCause.longPress, @@ -378,9 +373,9 @@ class _QuillEditorSelectionGestureDetectorBuilder return false; } TextPosition pos = - getRenderEditor().getPositionForOffset(details.globalPosition); + getRenderEditor()!.getPositionForOffset(details.globalPosition); containerNode.ChildQuery result = - getEditor().widget.controller.document.queryChild(pos.offset); + getEditor()!.widget.controller.document.queryChild(pos.offset); if (result.node == null) { return false; } @@ -391,7 +386,7 @@ class _QuillEditorSelectionGestureDetectorBuilder if (line.length == 1) { // tapping when no text yet on this line _flipListCheckbox(pos, line, segmentResult); - getEditor().widget.controller.updateSelection( + getEditor()!.widget.controller.updateSelection( TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); return true; } @@ -399,33 +394,34 @@ class _QuillEditorSelectionGestureDetectorBuilder } leaf.Leaf segment = segmentResult.node as leaf.Leaf; if (segment.style.containsKey(Attribute.link.key)) { - var launchUrl = getEditor().widget.onLaunchUrl; + var launchUrl = getEditor()!.widget.onLaunchUrl; if (launchUrl == null) { launchUrl = _launchUrl; } - String link = segment.style.attributes[Attribute.link.key].value; - if (getEditor().widget.readOnly && link != null) { + String? link = segment.style.attributes[Attribute.link.key]!.value; + if (getEditor()!.widget.readOnly && link != null) { link = link.trim(); if (!linkPrefixes - .any((linkPrefix) => link.toLowerCase().startsWith(linkPrefix))) { + .any((linkPrefix) => link!.toLowerCase().startsWith(linkPrefix))) { link = 'https://$link'; } launchUrl(link); } return false; } - if (getEditor().widget.readOnly && segment.value is BlockEmbed) { + if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) { BlockEmbed blockEmbed = segment.value as BlockEmbed; if (blockEmbed.type == 'image') { final String imageUrl = blockEmbed.data; Navigator.push( - getEditor().context, + getEditor()!.context, MaterialPageRoute( builder: (context) => ImageTapWrapper( imageProvider: imageUrl.startsWith('http') ? NetworkImage(imageUrl) : isBase64(imageUrl) ? Image.memory(base64.decode(imageUrl)) + as ImageProvider? : FileImage(io.File(imageUrl)), ), ), @@ -441,25 +437,25 @@ class _QuillEditorSelectionGestureDetectorBuilder bool _flipListCheckbox( TextPosition pos, Line line, containerNode.ChildQuery segmentResult) { - if (getEditor().widget.readOnly || + if (getEditor()!.widget.readOnly || !line.style.containsKey(Attribute.list.key) || segmentResult.offset != 0) { return false; } // segmentResult.offset == 0 means tap at the beginning of the TextLine - String listVal = line.style.attributes[Attribute.list.key].value; + String? listVal = line.style.attributes[Attribute.list.key]!.value; if (listVal == Attribute.unchecked.value) { - getEditor() + getEditor()! .widget .controller .formatText(pos.offset, 0, Attribute.checked); } else if (listVal == Attribute.checked.value) { - getEditor() + getEditor()! .widget .controller .formatText(pos.offset, 0, Attribute.unchecked); } - getEditor().widget.controller.updateSelection( + getEditor()!.widget.controller.updateSelection( TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); return true; } @@ -470,11 +466,11 @@ class _QuillEditorSelectionGestureDetectorBuilder @override onSingleTapUp(TapUpDetails details) { - getEditor().hideToolbar(); + getEditor()!.hideToolbar(); bool positionSelected = _onTapping(details); - if (delegate.getSelectionEnabled() && !positionSelected) { + if (delegate.getSelectionEnabled()! && !positionSelected) { switch (Theme.of(_state.context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: @@ -482,11 +478,11 @@ class _QuillEditorSelectionGestureDetectorBuilder case PointerDeviceKind.mouse: case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: - getRenderEditor().selectPosition(SelectionChangedCause.tap); + getRenderEditor()!.selectPosition(SelectionChangedCause.tap); break; case PointerDeviceKind.touch: case PointerDeviceKind.unknown: - getRenderEditor().selectWordEdge(SelectionChangedCause.tap); + getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); break; } break; @@ -494,7 +490,7 @@ class _QuillEditorSelectionGestureDetectorBuilder case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - getRenderEditor().selectPosition(SelectionChangedCause.tap); + getRenderEditor()!.selectPosition(SelectionChangedCause.tap); break; } } @@ -503,11 +499,11 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapStart(LongPressStartDetails details) { - if (delegate.getSelectionEnabled()) { + if (delegate.getSelectionEnabled()!) { switch (Theme.of(_state.context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: - getRenderEditor().selectPositionAt( + getRenderEditor()!.selectPositionAt( details.globalPosition, null, SelectionChangedCause.longPress, @@ -517,7 +513,7 @@ class _QuillEditorSelectionGestureDetectorBuilder case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - getRenderEditor().selectWord(SelectionChangedCause.longPress); + getRenderEditor()!.selectWord(SelectionChangedCause.longPress); Feedback.forLongPress(_state.context); break; default: @@ -548,7 +544,7 @@ class RenderEditor extends RenderEditableContainerBox final ValueNotifier _selectionEndInViewport = ValueNotifier(true); RenderEditor( - List children, + List? children, TextDirection textDirection, EdgeInsetsGeometry padding, this.document, @@ -558,11 +554,7 @@ class RenderEditor extends RenderEditableContainerBox this._startHandleLayerLink, this._endHandleLayerLink, EdgeInsets floatingCursorAddedMargin) - : assert(document != null), - assert(textDirection != null), - assert(_hasFocus != null), - assert(floatingCursorAddedMargin != null), - super( + : super( children, document.root, textDirection, @@ -570,7 +562,6 @@ class RenderEditor extends RenderEditableContainerBox ); setDocument(Document doc) { - assert(doc != null); if (document == doc) { return; } @@ -579,7 +570,6 @@ class RenderEditor extends RenderEditableContainerBox } setHasFocus(bool h) { - assert(h != null); if (_hasFocus == h) { return; } @@ -614,15 +604,13 @@ class RenderEditor extends RenderEditableContainerBox @override List getEndpointsForSelection( TextSelection textSelection) { - assert(constraints != null); - 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; + BoxParentData parentData = child.parentData as BoxParentData; return [ TextSelectionPoint( Offset(0.0, child.preferredLineHeight(localPosition)) + @@ -632,7 +620,7 @@ class RenderEditor extends RenderEditableContainerBox ]; } - Node baseNode = _container.queryChild(textSelection.start, false).node; + Node? baseNode = _container.queryChild(textSelection.start, false).node; var baseChild = firstChild; while (baseChild != null) { @@ -643,7 +631,7 @@ class RenderEditor extends RenderEditableContainerBox } assert(baseChild != null); - BoxParentData baseParentData = baseChild.parentData; + BoxParentData baseParentData = baseChild!.parentData as BoxParentData; TextSelection baseSelection = localSelection(baseChild.getContainer(), textSelection, true); TextSelectionPoint basePoint = @@ -651,8 +639,8 @@ class RenderEditor extends RenderEditableContainerBox basePoint = TextSelectionPoint( basePoint.point + baseParentData.offset, basePoint.direction); - Node extentNode = _container.queryChild(textSelection.end, false).node; - var extentChild = baseChild; + Node? extentNode = _container.queryChild(textSelection.end, false).node; + RenderEditableBox? extentChild = baseChild; while (extentChild != null) { if (extentChild.getContainer() == extentNode) { break; @@ -661,7 +649,7 @@ class RenderEditor extends RenderEditableContainerBox } assert(extentChild != null); - BoxParentData extentParentData = extentChild.parentData; + BoxParentData extentParentData = extentChild!.parentData as BoxParentData; TextSelection extentSelection = localSelection(extentChild.getContainer(), textSelection, true); TextSelectionPoint extentPoint = @@ -672,7 +660,7 @@ class RenderEditor extends RenderEditableContainerBox return [basePoint, extentPoint]; } - Offset _lastTapDownPosition; + Offset? _lastTapDownPosition; @override handleTapDown(TapDownDetails details) { @@ -682,14 +670,9 @@ class RenderEditor extends RenderEditableContainerBox @override selectWordsInRange( Offset from, - Offset to, + Offset? to, SelectionChangedCause cause, ) { - assert(cause != null); - assert(from != null); - if (onSelectionChanged == null) { - return; - } TextPosition firstPosition = getPositionForOffset(from); TextSelection firstWord = selectWordAtPosition(firstPosition); TextSelection lastWord = @@ -717,19 +700,13 @@ class RenderEditor extends RenderEditableContainerBox !focusingEmpty) { return; } - if (onSelectionChanged != null) { - onSelectionChanged(nextSelection, cause); - } + onSelectionChanged(nextSelection, cause); } @override selectWordEdge(SelectionChangedCause cause) { - assert(cause != null); assert(_lastTapDownPosition != null); - if (onSelectionChanged == null) { - return; - } - TextPosition position = getPositionForOffset(_lastTapDownPosition); + TextPosition position = getPositionForOffset(_lastTapDownPosition!); RenderEditableBox child = childAtPosition(position); int nodeOffset = child.getContainer().getOffset(); TextPosition localPosition = TextPosition( @@ -759,16 +736,11 @@ class RenderEditor extends RenderEditableContainerBox @override selectPositionAt( Offset from, - Offset to, + Offset? to, SelectionChangedCause cause, ) { - assert(cause != null); - assert(from != null); - if (onSelectionChanged == null) { - return; - } TextPosition fromPosition = getPositionForOffset(from); - TextPosition toPosition = to == null ? null : getPositionForOffset(to); + TextPosition? toPosition = to == null ? null : getPositionForOffset(to); int baseOffset = fromPosition.offset; int extentOffset = fromPosition.offset; @@ -787,12 +759,12 @@ class RenderEditor extends RenderEditableContainerBox @override selectWord(SelectionChangedCause cause) { - selectWordsInRange(_lastTapDownPosition, null, cause); + selectWordsInRange(_lastTapDownPosition!, null, cause); } @override selectPosition(SelectionChangedCause cause) { - selectPositionAt(_lastTapDownPosition, null, cause); + selectPositionAt(_lastTapDownPosition!, null, cause); } @override @@ -837,7 +809,7 @@ class RenderEditor extends RenderEditableContainerBox } @override - bool hitTestChildren(BoxHitTestResult result, {Offset position}) { + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { return defaultHitTestChildren(result, position: position); } @@ -877,9 +849,9 @@ class RenderEditor extends RenderEditableContainerBox @override TextPosition getPositionForOffset(Offset offset) { Offset local = globalToLocal(offset); - RenderEditableBox child = childAtOffset(local); + RenderEditableBox child = childAtOffset(local)!; - BoxParentData parentData = child.parentData; + BoxParentData parentData = child.parentData as BoxParentData; Offset localOffset = local - parentData.offset; TextPosition localPosition = child.getPositionForOffset(localOffset); return TextPosition( @@ -888,7 +860,7 @@ class RenderEditor extends RenderEditableContainerBox ); } - double getOffsetToRevealCursor( + double? getOffsetToRevealCursor( double viewportHeight, double scrollOffset, double offsetInViewport) { List endpoints = getEndpointsForSelection(selection); TextSelectionPoint endpoint = endpoints.first; @@ -902,7 +874,7 @@ class RenderEditor extends RenderEditableContainerBox kMargin + offsetInViewport; final caretBottom = endpoint.point.dy + kMargin + offsetInViewport; - double dy; + double? dy; if (caretTop < scrollOffset) { dy = caretTop; } else if (caretBottom > scrollOffset + viewportHeight) { @@ -927,14 +899,11 @@ class RenderEditableContainerBox extends RenderBox containerNode.Container _container; TextDirection textDirection; EdgeInsetsGeometry _padding; - EdgeInsets _resolvedPadding; + EdgeInsets? _resolvedPadding; - RenderEditableContainerBox(List children, this._container, + RenderEditableContainerBox(List? children, this._container, this.textDirection, this._padding) - : assert(_container != null), - assert(textDirection != null), - assert(_padding != null), - assert(_padding.isNonNegative) { + : assert(_padding.isNonNegative) { addAll(children); } @@ -943,7 +912,6 @@ class RenderEditableContainerBox extends RenderBox } setContainer(containerNode.Container c) { - assert(c != null); if (_container == c) { return; } @@ -954,7 +922,6 @@ class RenderEditableContainerBox extends RenderBox EdgeInsetsGeometry getPadding() => _padding; setPadding(EdgeInsetsGeometry value) { - assert(value != null); assert(value.isNonNegative); if (_padding == value) { return; @@ -963,22 +930,22 @@ class RenderEditableContainerBox extends RenderBox _markNeedsPaddingResolution(); } - EdgeInsets get resolvedPadding => _resolvedPadding; + EdgeInsets? get resolvedPadding => _resolvedPadding; _resolvePadding() { if (_resolvedPadding != null) { return; } _resolvedPadding = _padding.resolve(textDirection); - _resolvedPadding = _resolvedPadding.copyWith(left: _resolvedPadding.left); + _resolvedPadding = _resolvedPadding!.copyWith(left: _resolvedPadding!.left); - assert(_resolvedPadding.isNonNegative); + assert(_resolvedPadding!.isNonNegative); } RenderEditableBox childAtPosition(TextPosition position) { assert(firstChild != null); - Node targetNode = _container.queryChild(position.offset, false).node; + Node? targetNode = _container.queryChild(position.offset, false).node; var targetChild = firstChild; while (targetChild != null) { @@ -998,19 +965,19 @@ class RenderEditableContainerBox extends RenderBox markNeedsLayout(); } - RenderEditableBox childAtOffset(Offset offset) { + RenderEditableBox? childAtOffset(Offset offset) { assert(firstChild != null); _resolvePadding(); - if (offset.dy <= _resolvedPadding.top) { + if (offset.dy <= _resolvedPadding!.top) { return firstChild; } - if (offset.dy >= size.height - _resolvedPadding.bottom) { + if (offset.dy >= size.height - _resolvedPadding!.bottom) { return lastChild; } var child = firstChild; - double dx = -offset.dx, dy = _resolvedPadding.top; + double dx = -offset.dx, dy = _resolvedPadding!.top; while (child != null) { if (child.size.contains(offset.translate(dx, -dy))) { return child; @@ -1037,20 +1004,21 @@ class RenderEditableContainerBox extends RenderBox _resolvePadding(); assert(_resolvedPadding != null); - double mainAxisExtent = _resolvedPadding.top; + double mainAxisExtent = _resolvedPadding!.top; var child = firstChild; BoxConstraints innerConstraints = BoxConstraints.tightFor(width: constraints.maxWidth) - .deflate(_resolvedPadding); + .deflate(_resolvedPadding!); while (child != null) { child.layout(innerConstraints, parentUsesSize: true); - final EditableContainerParentData childParentData = child.parentData; - childParentData.offset = Offset(_resolvedPadding.left, mainAxisExtent); + final EditableContainerParentData childParentData = + child.parentData as EditableContainerParentData; + childParentData.offset = Offset(_resolvedPadding!.left, mainAxisExtent); mainAxisExtent += child.size.height; assert(child.parentData == childParentData); child = childParentData.nextSibling; } - mainAxisExtent += _resolvedPadding.bottom; + mainAxisExtent += _resolvedPadding!.bottom; size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent)); assert(size.isFinite); @@ -1061,7 +1029,8 @@ class RenderEditableContainerBox extends RenderBox var child = firstChild; while (child != null) { extent = math.max(extent, childSize(child)); - EditableContainerParentData childParentData = child.parentData; + EditableContainerParentData childParentData = + child.parentData as EditableContainerParentData; child = childParentData.nextSibling; } return extent; @@ -1072,7 +1041,8 @@ class RenderEditableContainerBox extends RenderBox var child = firstChild; while (child != null) { extent += childSize(child); - EditableContainerParentData childParentData = child.parentData; + EditableContainerParentData childParentData = + child.parentData as EditableContainerParentData; child = childParentData.nextSibling; } return extent; @@ -1083,10 +1053,10 @@ class RenderEditableContainerBox extends RenderBox _resolvePadding(); return _getIntrinsicCrossAxis((RenderBox child) { double childHeight = math.max( - 0.0, height - _resolvedPadding.top + _resolvedPadding.bottom); + 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMinIntrinsicWidth(childHeight) + - _resolvedPadding.left + - _resolvedPadding.right; + _resolvedPadding!.left + + _resolvedPadding!.right; }); } @@ -1095,10 +1065,10 @@ class RenderEditableContainerBox extends RenderBox _resolvePadding(); return _getIntrinsicCrossAxis((RenderBox child) { double childHeight = math.max( - 0.0, height - _resolvedPadding.top + _resolvedPadding.bottom); + 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMaxIntrinsicWidth(childHeight) + - _resolvedPadding.left + - _resolvedPadding.right; + _resolvedPadding!.left + + _resolvedPadding!.right; }); } @@ -1106,11 +1076,11 @@ class RenderEditableContainerBox extends RenderBox double computeMinIntrinsicHeight(double width) { _resolvePadding(); return _getIntrinsicMainAxis((RenderBox child) { - double childWidth = - math.max(0.0, width - _resolvedPadding.left + _resolvedPadding.right); + double childWidth = math.max( + 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMinIntrinsicHeight(childWidth) + - _resolvedPadding.top + - _resolvedPadding.bottom; + _resolvedPadding!.top + + _resolvedPadding!.bottom; }); } @@ -1118,18 +1088,18 @@ class RenderEditableContainerBox extends RenderBox double computeMaxIntrinsicHeight(double width) { _resolvePadding(); return _getIntrinsicMainAxis((RenderBox child) { - final childWidth = - math.max(0.0, width - _resolvedPadding.left + _resolvedPadding.right); + final childWidth = math.max( + 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMaxIntrinsicHeight(childWidth) + - _resolvedPadding.top + - _resolvedPadding.bottom; + _resolvedPadding!.top + + _resolvedPadding!.bottom; }); } @override double computeDistanceToActualBaseline(TextBaseline baseline) { _resolvePadding(); - return defaultComputeDistanceToFirstActualBaseline(baseline) + - _resolvedPadding.top; + return defaultComputeDistanceToFirstActualBaseline(baseline)! + + _resolvedPadding!.top; } } diff --git a/lib/widgets/image.dart b/lib/widgets/image.dart index 2a6785f0..b9df48ce 100644 --- a/lib/widgets/image.dart +++ b/lib/widgets/image.dart @@ -8,7 +8,7 @@ class ImageTapWrapper extends StatelessWidget { this.imageProvider, }); - final ImageProvider imageProvider; + final ImageProvider? imageProvider; @override Widget build(BuildContext context) { diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart index c0dc0c27..7311b345 100644 --- a/lib/widgets/keyboard_listener.dart +++ b/lib/widgets/keyboard_listener.dart @@ -5,7 +5,7 @@ enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL } typedef CursorMoveCallback = void Function( LogicalKeyboardKey key, bool wordModifier, bool lineModifier, bool shift); -typedef InputShortcutCallback = void Function(InputShortcut shortcut); +typedef InputShortcutCallback = void Function(InputShortcut? shortcut); typedef OnDeleteCallback = void Function(bool forward); class KeyboardListener { @@ -59,10 +59,7 @@ class KeyboardListener { LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, }; - KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete) - : assert(onCursorMove != null), - assert(onShortcut != null), - assert(onDelete != null); + KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete); bool handleRawKeyEvent(RawKeyEvent event) { if (kIsWeb) { diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index b710e8d3..9f2e0bb3 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -4,17 +4,17 @@ import 'package:flutter/widgets.dart'; import 'box.dart'; class BaselineProxy extends SingleChildRenderObjectWidget { - final TextStyle textStyle; - final EdgeInsets padding; + final TextStyle? textStyle; + final EdgeInsets? padding; - BaselineProxy({Key key, Widget child, this.textStyle, this.padding}) + BaselineProxy({Key? key, Widget? child, this.textStyle, this.padding}) : super(key: key, child: child); @override RenderBaselineProxy createRenderObject(BuildContext context) { return RenderBaselineProxy( null, - textStyle, + textStyle!, padding, ); } @@ -23,16 +23,16 @@ class BaselineProxy extends SingleChildRenderObjectWidget { void updateRenderObject( BuildContext context, covariant RenderBaselineProxy renderObject) { renderObject - ..textStyle = textStyle - ..padding = padding; + ..textStyle = textStyle! + ..padding = padding!; } } class RenderBaselineProxy extends RenderProxyBox { RenderBaselineProxy( - RenderParagraph child, + RenderParagraph? child, TextStyle textStyle, - EdgeInsets padding, + EdgeInsets? padding, ) : _prototypePainter = TextPainter( text: TextSpan(text: ' ', style: textStyle), textDirection: TextDirection.ltr, @@ -43,18 +43,16 @@ class RenderBaselineProxy extends RenderProxyBox { final TextPainter _prototypePainter; set textStyle(TextStyle value) { - assert(value != null); - if (_prototypePainter.text.style == value) { + if (_prototypePainter.text!.style == value) { return; } _prototypePainter.text = TextSpan(text: ' ', style: value); markNeedsLayout(); } - EdgeInsets _padding; + EdgeInsets? _padding; set padding(EdgeInsets value) { - assert(value != null); if (_padding == value) { return; } @@ -64,9 +62,8 @@ class RenderBaselineProxy extends RenderProxyBox { @override double computeDistanceToActualBaseline(TextBaseline baseline) => - _prototypePainter.computeDistanceToActualBaseline(baseline) + - _padding?.top ?? - 0.0; + _prototypePainter.computeDistanceToActualBaseline(baseline); + // SEE What happens + _padding?.top; @override performLayout() { @@ -84,7 +81,7 @@ class EmbedProxy extends SingleChildRenderObjectWidget { } class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { - RenderEmbedProxy(RenderBox child) : super(child); + RenderEmbedProxy(RenderBox? child) : super(child); @override List getBoxesForSelection(TextSelection selection) { @@ -105,10 +102,8 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { double getFullHeightForCaret(TextPosition position) => size.height; @override - Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { - assert(position.offset != null && - position.offset <= 1 && - position.offset >= 0); + Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) { + assert(position.offset <= 1 && position.offset >= 0); return position.offset == 0 ? Offset.zero : Offset(size.width, 0.0); } @@ -134,7 +129,7 @@ class RichTextProxy extends SingleChildRenderObjectWidget { final Locale locale; final StrutStyle strutStyle; final TextWidthBasis textWidthBasis; - final TextHeightBehavior textHeightBehavior; + final TextHeightBehavior? textHeightBehavior; @override RenderParagraphProxy createRenderObject(BuildContext context) { @@ -160,13 +155,7 @@ class RichTextProxy extends SingleChildRenderObjectWidget { this.strutStyle, this.textWidthBasis, this.textHeightBehavior) - : assert(child != null), - assert(textStyle != null), - assert(textAlign != null), - assert(textDirection != null), - assert(locale != null), - assert(strutStyle != null), - super(child: child); + : super(child: child); @override void updateRenderObject( @@ -185,7 +174,7 @@ class RichTextProxy extends SingleChildRenderObjectWidget { class RenderParagraphProxy extends RenderProxyBox implements RenderContentProxyBox { RenderParagraphProxy( - RenderParagraph child, + RenderParagraph? child, TextStyle textStyle, TextAlign textAlign, TextDirection textDirection, @@ -193,7 +182,7 @@ class RenderParagraphProxy extends RenderProxyBox StrutStyle strutStyle, Locale locale, TextWidthBasis textWidthBasis, - TextHeightBehavior textHeightBehavior, + TextHeightBehavior? textHeightBehavior, ) : _prototypePainter = TextPainter( text: TextSpan(text: ' ', style: textStyle), textAlign: textAlign, @@ -208,8 +197,7 @@ class RenderParagraphProxy extends RenderProxyBox final TextPainter _prototypePainter; set textStyle(TextStyle value) { - assert(value != null); - if (_prototypePainter.text.style == value) { + if (_prototypePainter.text!.style == value) { return; } _prototypePainter.text = TextSpan(text: ' ', style: value); @@ -217,7 +205,6 @@ class RenderParagraphProxy extends RenderProxyBox } set textAlign(TextAlign value) { - assert(value != null); if (_prototypePainter.textAlign == value) { return; } @@ -226,7 +213,6 @@ class RenderParagraphProxy extends RenderProxyBox } set textDirection(TextDirection value) { - assert(value != null); if (_prototypePainter.textDirection == value) { return; } @@ -235,7 +221,6 @@ class RenderParagraphProxy extends RenderProxyBox } set textScaleFactor(double value) { - assert(value != null); if (_prototypePainter.textScaleFactor == value) { return; } @@ -244,7 +229,6 @@ class RenderParagraphProxy extends RenderProxyBox } set strutStyle(StrutStyle value) { - assert(value != null); if (_prototypePainter.strutStyle == value) { return; } @@ -261,7 +245,6 @@ class RenderParagraphProxy extends RenderProxyBox } set textWidthBasis(TextWidthBasis value) { - assert(value != null); if (_prototypePainter.textWidthBasis == value) { return; } @@ -269,7 +252,7 @@ class RenderParagraphProxy extends RenderProxyBox markNeedsLayout(); } - set textHeightBehavior(TextHeightBehavior value) { + set textHeightBehavior(TextHeightBehavior? value) { if (_prototypePainter.textHeightBehavior == value) { return; } @@ -278,7 +261,7 @@ class RenderParagraphProxy extends RenderProxyBox } @override - RenderParagraph get child => super.child; + RenderParagraph? get child => super.child as RenderParagraph?; @override double getPreferredLineHeight() { @@ -286,24 +269,24 @@ class RenderParagraphProxy extends RenderProxyBox } @override - Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) => - child.getOffsetForCaret(position, caretPrototype); + Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) => + child!.getOffsetForCaret(position, caretPrototype!); @override TextPosition getPositionForOffset(Offset offset) => - child.getPositionForOffset(offset); + child!.getPositionForOffset(offset); @override - double getFullHeightForCaret(TextPosition position) => - child.getFullHeightForCaret(position); + double? getFullHeightForCaret(TextPosition position) => + child!.getFullHeightForCaret(position); @override TextRange getWordBoundary(TextPosition position) => - child.getWordBoundary(position); + child!.getWordBoundary(position); @override List getBoxesForSelection(TextSelection selection) => - child.getBoxesForSelection(selection); + child!.getBoxesForSelection(selection); @override performLayout() { diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index e3cb78af..24f9c397 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -36,23 +36,23 @@ class RawEditor extends StatefulWidget { final bool scrollable; final EdgeInsetsGeometry padding; final bool readOnly; - final String placeholder; - final ValueChanged onLaunchUrl; + final String? placeholder; + final ValueChanged? onLaunchUrl; final ToolbarOptions toolbarOptions; final bool showSelectionHandles; final bool showCursor; final CursorStyle cursorStyle; final TextCapitalization textCapitalization; - final double maxHeight; - final double minHeight; - final DefaultStyles customStyles; + final double? maxHeight; + final double? minHeight; + final DefaultStyles? customStyles; final bool expands; final bool autoFocus; final Color selectionColor; final TextSelectionControls selectionCtrls; final Brightness keyboardAppearance; final bool enableInteractiveSelection; - final ScrollPhysics scrollPhysics; + final ScrollPhysics? scrollPhysics; final EmbedBuilder embedBuilder; RawEditor( @@ -67,7 +67,7 @@ class RawEditor extends StatefulWidget { this.onLaunchUrl, this.toolbarOptions, this.showSelectionHandles, - bool showCursor, + bool? showCursor, this.cursorStyle, this.textCapitalization, this.maxHeight, @@ -81,26 +81,11 @@ class RawEditor extends StatefulWidget { this.enableInteractiveSelection, this.scrollPhysics, this.embedBuilder) - : assert(controller != null, 'controller cannot be null'), - assert(focusNode != null, 'focusNode cannot be null'), - assert(scrollable || scrollController != null, - 'scrollController cannot be null'), - assert(selectionColor != null, 'selectionColor cannot be null'), - assert(enableInteractiveSelection != null, - 'enableInteractiveSelection cannot be null'), - assert(showSelectionHandles != null, - 'showSelectionHandles cannot be null'), - assert(readOnly != null, 'readOnly cannot be null'), - assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), + : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, 'maxHeight cannot be null'), - assert(autoFocus != null, 'autoFocus cannot be null'), - assert(toolbarOptions != null, 'toolbarOptions cannot be null'), showCursor = showCursor ?? !readOnly, - assert(embedBuilder != null, 'embedBuilder cannot be null'), - assert(expands != null, 'expands cannot be null'), - assert(padding != null), super(key: key); @override @@ -115,23 +100,23 @@ class RawEditorState extends EditorState WidgetsBindingObserver, TickerProviderStateMixin implements TextSelectionDelegate, TextInputClient { - final GlobalKey _editorKey = GlobalKey(); + GlobalKey _editorKey = GlobalKey(); final List _sentRemoteValues = []; - TextInputConnection _textInputConnection; - TextEditingValue _lastKnownRemoteTextEditingValue; + TextInputConnection? _textInputConnection; + TextEditingValue? _lastKnownRemoteTextEditingValue; int _cursorResetLocation = -1; bool _wasSelectingVerticallyWithKeyboard = false; - EditorTextSelectionOverlay _selectionOverlay; - FocusAttachment _focusAttachment; - CursorCont _cursorCont; - ScrollController _scrollController; - KeyboardVisibilityController _keyboardVisibilityController; - StreamSubscription _keyboardVisibilitySubscription; - KeyboardListener _keyboardListener; + EditorTextSelectionOverlay? _selectionOverlay; + FocusAttachment? _focusAttachment; + late CursorCont _cursorCont; + ScrollController? _scrollController; + late KeyboardVisibilityController _keyboardVisibilityController; + late StreamSubscription _keyboardVisibilitySubscription; + late KeyboardListener _keyboardListener; bool _didAutoFocus = false; bool _keyboardVisible = false; - DefaultStyles _styles; - final ClipboardStatusNotifier _clipboardStatus = + DefaultStyles? _styles; + final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier(); final LayerLink _toolbarLayerLink = LayerLink(); final LayerLink _startHandleLayerLink = LayerLink(); @@ -156,7 +141,6 @@ class RawEditorState extends EditorState TextDirection get _textDirection { TextDirection result = Directionality.of(context); - assert(result != null); return result; } @@ -170,7 +154,6 @@ class RawEditorState extends EditorState return; } TextSelection selection = widget.controller.selection; - assert(selection != null); TextSelection newSelection = widget.controller.selection; @@ -226,19 +209,20 @@ class RawEditorState extends EditorState TextPosition originPosition = TextPosition( offset: upKey ? selection.baseOffset : selection.extentOffset); - RenderEditableBox child = getRenderEditor().childAtPosition(originPosition); + RenderEditableBox child = + getRenderEditor()!.childAtPosition(originPosition); TextPosition localPosition = TextPosition( offset: originPosition.offset - child.getContainer().getDocumentOffset()); - TextPosition position = upKey + TextPosition? position = upKey ? child.getPositionAbove(localPosition) : child.getPositionBelow(localPosition); if (position == null) { var sibling = upKey - ? getRenderEditor().childBefore(child) - : getRenderEditor().childAfter(child); + ? getRenderEditor()!.childBefore(child) + : getRenderEditor()!.childAfter(child); if (sibling == null) { position = TextPosition(offset: upKey ? 0 : plainText.length - 1); } else { @@ -289,20 +273,20 @@ class RawEditorState extends EditorState bool shift) { if (wordModifier) { if (leftKey) { - TextSelection textSelection = getRenderEditor().selectWordAtPosition( + TextSelection textSelection = getRenderEditor()!.selectWordAtPosition( TextPosition( offset: _previousCharacter( newSelection.extentOffset, plainText, false))); return newSelection.copyWith(extentOffset: textSelection.baseOffset); } - TextSelection textSelection = getRenderEditor().selectWordAtPosition( + TextSelection 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( + TextSelection textSelection = getRenderEditor()!.selectLineAtPosition( TextPosition( offset: _previousCharacter( newSelection.extentOffset, plainText, false))); @@ -310,7 +294,7 @@ class RawEditorState extends EditorState } int startPoint = newSelection.extentOffset; if (startPoint < plainText.length) { - TextSelection textSelection = getRenderEditor() + TextSelection textSelection = getRenderEditor()! .selectLineAtPosition(TextPosition(offset: startPoint)); return newSelection.copyWith(extentOffset: textSelection.extentOffset); } @@ -368,7 +352,7 @@ class RawEditorState extends EditorState } int count = 0; - int lastNonWhitespace; + int? lastNonWhitespace; for (String currentString in string.characters) { if (!includeWhitespace && !WHITE_SPACE.contains( @@ -384,7 +368,7 @@ class RawEditorState extends EditorState } bool get hasConnection => - _textInputConnection != null && _textInputConnection.attached; + _textInputConnection != null && _textInputConnection!.attached; openConnectionIfNeeded() { if (!shouldCreateInputConnection) { @@ -406,17 +390,17 @@ class RawEditorState extends EditorState ), ); - _textInputConnection.setEditingState(_lastKnownRemoteTextEditingValue); + _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); } - _textInputConnection.show(); + _textInputConnection!.show(); } closeConnectionIfNeeded() { if (!hasConnection) { return; } - _textInputConnection.close(); + _textInputConnection!.close(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; _sentRemoteValues.clear(); @@ -428,7 +412,7 @@ class RawEditorState extends EditorState } TextEditingValue actualValue = textEditingValue.copyWith( - composing: _lastKnownRemoteTextEditingValue.composing, + composing: _lastKnownRemoteTextEditingValue!.composing, ); if (actualValue == _lastKnownRemoteTextEditingValue) { @@ -436,20 +420,20 @@ class RawEditorState extends EditorState } bool shouldRemember = - textEditingValue.text != _lastKnownRemoteTextEditingValue.text; + textEditingValue.text != _lastKnownRemoteTextEditingValue!.text; _lastKnownRemoteTextEditingValue = actualValue; - _textInputConnection.setEditingState(actualValue); + _textInputConnection!.setEditingState(actualValue); if (shouldRemember) { _sentRemoteValues.add(actualValue); } } @override - TextEditingValue get currentTextEditingValue => + TextEditingValue? get currentTextEditingValue => _lastKnownRemoteTextEditingValue; @override - AutofillScope get currentAutofillScope => null; + AutofillScope? get currentAutofillScope => null; @override void updateEditingValue(TextEditingValue value) { @@ -466,13 +450,14 @@ class RawEditorState extends EditorState return; } - if (_lastKnownRemoteTextEditingValue.text == value.text && - _lastKnownRemoteTextEditingValue.selection == value.selection) { + if (_lastKnownRemoteTextEditingValue!.text == value.text && + _lastKnownRemoteTextEditingValue!.selection == value.selection) { _lastKnownRemoteTextEditingValue = value; return; } - TextEditingValue effectiveLastKnownValue = _lastKnownRemoteTextEditingValue; + TextEditingValue effectiveLastKnownValue = + _lastKnownRemoteTextEditingValue!; _lastKnownRemoteTextEditingValue = value; String oldText = effectiveLastKnownValue.text; String text = value.text; @@ -516,7 +501,7 @@ class RawEditorState extends EditorState if (!hasConnection) { return; } - _textInputConnection.connectionClosedReceived(); + _textInputConnection!.connectionClosedReceived(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; _sentRemoteValues.clear(); @@ -525,7 +510,7 @@ class RawEditorState extends EditorState @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - _focusAttachment.reparent(); + _focusAttachment!.reparent(); super.build(context); Document _doc = widget.controller.document; @@ -556,9 +541,9 @@ class RawEditorState extends EditorState if (widget.scrollable) { EdgeInsets baselinePadding = - EdgeInsets.only(top: _styles.paragraph.verticalSpacing.item1); + EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1); child = BaselineProxy( - textStyle: _styles.paragraph.style, + textStyle: _styles!.paragraph!.style, padding: baselinePadding, child: SingleChildScrollView( controller: _scrollController, @@ -575,7 +560,7 @@ class RawEditorState extends EditorState maxHeight: widget.maxHeight ?? double.infinity); return QuillStyles( - data: _styles, + data: _styles!, child: MouseRegion( cursor: SystemMouseCursors.text, child: Container( @@ -636,7 +621,7 @@ class RawEditorState extends EditorState line: node, textDirection: _textDirection, embedBuilder: widget.embedBuilder, - styles: _styles, + styles: _styles!, ); EditableTextLine editableTextLine = EditableTextLine( node, @@ -655,36 +640,36 @@ class RawEditorState extends EditorState } Tuple2 _getVerticalSpacingForLine( - Line line, DefaultStyles defaultStyles) { + Line line, DefaultStyles? defaultStyles) { Map attrs = line.style.attributes; if (attrs.containsKey(Attribute.header.key)) { - int level = attrs[Attribute.header.key].value; + int? level = attrs[Attribute.header.key]!.value; switch (level) { case 1: - return defaultStyles.h1.verticalSpacing; + return defaultStyles!.h1!.verticalSpacing; case 2: - return defaultStyles.h2.verticalSpacing; + return defaultStyles!.h2!.verticalSpacing; case 3: - return defaultStyles.h3.verticalSpacing; + return defaultStyles!.h3!.verticalSpacing; default: throw ('Invalid level $level'); } } - return defaultStyles.paragraph.verticalSpacing; + return defaultStyles!.paragraph!.verticalSpacing; } Tuple2 _getVerticalSpacingForBlock( - Block node, DefaultStyles defaultStyles) { + Block node, DefaultStyles? defaultStyles) { Map attrs = node.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { - return defaultStyles.quote.verticalSpacing; + return defaultStyles!.quote!.verticalSpacing; } else if (attrs.containsKey(Attribute.codeBlock.key)) { - return defaultStyles.code.verticalSpacing; + return defaultStyles!.code!.verticalSpacing; } else if (attrs.containsKey(Attribute.indent.key)) { - return defaultStyles.indent.verticalSpacing; + return defaultStyles!.indent!.verticalSpacing; } - return defaultStyles.lists.verticalSpacing; + return defaultStyles!.lists!.verticalSpacing; } @override @@ -695,17 +680,12 @@ class RawEditorState extends EditorState widget.controller.addListener(_didChangeTextEditingValue); - _scrollController = widget.scrollController ?? ScrollController(); - _scrollController.addListener(_updateSelectionOverlayForScroll); + _scrollController = widget.scrollController; + _scrollController!.addListener(_updateSelectionOverlayForScroll); _cursorCont = CursorCont( - show: ValueNotifier(widget.showCursor ?? false), - style: widget.cursorStyle ?? - CursorStyle( - color: Colors.blueAccent, - backgroundColor: Colors.grey, - width: 2.0, - ), + show: ValueNotifier(widget.showCursor), + style: widget.cursorStyle, tickerProvider: this, ); @@ -739,14 +719,14 @@ class RawEditorState extends EditorState @override didChangeDependencies() { super.didChangeDependencies(); - DefaultStyles parentStyles = QuillStyles.getStyles(context, true); + DefaultStyles? parentStyles = QuillStyles.getStyles(context, true); DefaultStyles defaultStyles = DefaultStyles.getInstance(context); _styles = (parentStyles != null) ? defaultStyles.merge(parentStyles) : defaultStyles; if (widget.customStyles != null) { - _styles = _styles.merge(widget.customStyles); + _styles = _styles!.merge(widget.customStyles!); } if (!_didAutoFocus && widget.autoFocus) { @@ -768,11 +748,10 @@ class RawEditorState extends EditorState updateRemoteValueIfNeeded(); } - if (widget.scrollController != null && - widget.scrollController != _scrollController) { - _scrollController.removeListener(_updateSelectionOverlayForScroll); + if (widget.scrollController != _scrollController) { + _scrollController!.removeListener(_updateSelectionOverlayForScroll); _scrollController = widget.scrollController; - _scrollController.addListener(_updateSelectionOverlayForScroll); + _scrollController!.addListener(_updateSelectionOverlayForScroll); } if (widget.focusNode != oldWidget.focusNode) { @@ -806,7 +785,6 @@ class RawEditorState extends EditorState handleDelete(bool forward) { TextSelection selection = widget.controller.selection; String plainText = textEditingValue.text; - assert(selection != null); int cursorPosition = selection.start; String textBefore = selection.textBefore(plainText); String textAfter = selection.textAfter(plainText); @@ -834,9 +812,8 @@ class RawEditorState extends EditorState ); } - Future handleShortcut(InputShortcut shortcut) async { + void handleShortcut(InputShortcut? shortcut) async { TextSelection selection = widget.controller.selection; - assert(selection != null); String plainText = textEditingValue.text; if (shortcut == InputShortcut.COPY) { if (!selection.isCollapsed) { @@ -865,13 +842,13 @@ class RawEditorState extends EditorState return; } if (shortcut == InputShortcut.PASTE && !widget.readOnly) { - ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain); + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null) { widget.controller.replaceText( selection.start, selection.end - selection.start, data.text, - TextSelection.collapsed(offset: selection.start + data.text.length), + TextSelection.collapsed(offset: selection.start + data.text!.length), ); } return; @@ -891,13 +868,13 @@ class RawEditorState extends EditorState @override void dispose() { closeConnectionIfNeeded(); - _keyboardVisibilitySubscription?.cancel(); + _keyboardVisibilitySubscription.cancel(); assert(!hasConnection); _selectionOverlay?.dispose(); _selectionOverlay = null; widget.controller.removeListener(_didChangeTextEditingValue); widget.focusNode.removeListener(_handleFocusChanged); - _focusAttachment.detach(); + _focusAttachment!.detach(); _cursorCont.dispose(); _clipboardStatus?.removeListener(_onChangedClipboardStatus); _clipboardStatus?.dispose(); @@ -932,7 +909,7 @@ class RawEditorState extends EditorState _cursorCont.startCursorTimer(); } - SchedulerBinding.instance.addPostFrameCallback( + SchedulerBinding.instance!.addPostFrameCallback( (Duration _) => _updateOrDisposeSelectionOverlayIfNeeded()); if (!mounted) return; setState(() { @@ -944,33 +921,32 @@ class RawEditorState extends EditorState _updateOrDisposeSelectionOverlayIfNeeded() { if (_selectionOverlay != null) { if (_hasFocus) { - _selectionOverlay.update(textEditingValue); + _selectionOverlay!.update(textEditingValue); } else { - _selectionOverlay.dispose(); + _selectionOverlay!.dispose(); _selectionOverlay = null; } } else if (_hasFocus) { _selectionOverlay?.hide(); _selectionOverlay = null; - if (widget.selectionCtrls != null) { - _selectionOverlay = EditorTextSelectionOverlay( - textEditingValue, - false, - context, - widget, - _toolbarLayerLink, - _startHandleLayerLink, - _endHandleLayerLink, - getRenderEditor(), - widget.selectionCtrls, - this, - DragStartBehavior.start, - null, - _clipboardStatus); - _selectionOverlay.handlesVisible = _shouldShowSelectionHandles(); - _selectionOverlay.showHandles(); - } + _selectionOverlay = EditorTextSelectionOverlay( + textEditingValue, + false, + context, + widget, + _toolbarLayerLink, + _startHandleLayerLink, + _endHandleLayerLink, + getRenderEditor(), + widget.selectionCtrls, + this, + DragStartBehavior.start, + null, + _clipboardStatus!, + ); + _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); + _selectionOverlay!.showHandles(); } } @@ -980,10 +956,10 @@ class RawEditorState extends EditorState _hasFocus, widget.controller.selection); _updateOrDisposeSelectionOverlayIfNeeded(); if (_hasFocus) { - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance!.addObserver(this); _showCaretOnScreen(); } else { - WidgetsBinding.instance.removeObserver(this); + WidgetsBinding.instance!.removeObserver(this); } updateKeepAlive(); } @@ -1004,23 +980,22 @@ class RawEditorState extends EditorState } _showCaretOnScreenScheduled = true; - SchedulerBinding.instance.addPostFrameCallback((Duration _) { + SchedulerBinding.instance!.addPostFrameCallback((Duration _) { _showCaretOnScreenScheduled = false; - final viewport = RenderAbstractViewport.of(getRenderEditor()); - assert(viewport != null); - final editorOffset = - getRenderEditor().localToGlobal(Offset(0.0, 0.0), ancestor: viewport); - final offsetInViewport = _scrollController.offset + editorOffset.dy; + final viewport = RenderAbstractViewport.of(getRenderEditor())!; + final editorOffset = getRenderEditor()! + .localToGlobal(Offset(0.0, 0.0), ancestor: viewport); + final offsetInViewport = _scrollController!.offset + editorOffset.dy; - final offset = getRenderEditor().getOffsetToRevealCursor( - _scrollController.position.viewportDimension, - _scrollController.offset, + final offset = getRenderEditor()!.getOffsetToRevealCursor( + _scrollController!.position.viewportDimension, + _scrollController!.offset, offsetInViewport, ); if (offset != null) { - _scrollController.animateTo( + _scrollController!.animateTo( offset, duration: Duration(milliseconds: 100), curve: Curves.fastOutSlowIn, @@ -1030,12 +1005,12 @@ class RawEditorState extends EditorState } @override - RenderEditor getRenderEditor() { - return _editorKey.currentContext.findRenderObject(); + RenderEditor? getRenderEditor() { + return _editorKey.currentContext!.findRenderObject() as RenderEditor?; } @override - EditorTextSelectionOverlay getSelectionOverlay() { + EditorTextSelectionOverlay? getSelectionOverlay() { return _selectionOverlay; } @@ -1091,7 +1066,7 @@ class RawEditorState extends EditorState ); } else { final TextEditingValue value = textEditingValue; - final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain); + final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null) { final length = textEditingValue.selection.end - textEditingValue.selection.start; @@ -1104,32 +1079,37 @@ class RawEditorState extends EditorState // move cursor to the end of pasted text selection widget.controller.updateSelection( TextSelection.collapsed( - offset: value.selection.start + data.text.length), + offset: value.selection.start + data.text!.length), ChangeSource.LOCAL); } } } Future __isItCut(TextEditingValue value) async { - final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain); - return textEditingValue.text.length - value.text.length == data.text.length; + final ClipboardData data = await (Clipboard.getData(Clipboard.kTextPlain) + as FutureOr); + return textEditingValue.text.length - value.text.length == + data.text!.length; } @override bool showToolbar() { - // Web is using native dom elements to enable clipboard functionality of the - // toolbar: copy, paste, select, cut. It might also provide additional - // functionality depending on the browser (such as translate). Due to this - // we should not show a Flutter toolbar for the editable text elements. - if (kIsWeb) { - return false; - } + if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { + // Web is using native dom elements to enable clipboard functionality of the + // toolbar: copy, paste, select, cut. It might also provide additional + // functionality depending on the browser (such as translate). Due to this + // we should not show a Flutter toolbar for the editable text elements. + if (kIsWeb) { + return false; + } - if (_selectionOverlay == null || _selectionOverlay.toolbar != null) { - return false; - } + if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { + return false; + } - _selectionOverlay.showToolbar(); + _selectionOverlay!.showToolbar(); + return true; + } return true; } @@ -1147,15 +1127,15 @@ class RawEditorState extends EditorState class _Editor extends MultiChildRenderObjectWidget { _Editor({ - @required Key key, - @required List children, - @required this.document, - @required this.textDirection, - @required this.hasFocus, - @required this.selection, - @required this.startHandleLayerLink, - @required this.endHandleLayerLink, - @required this.onSelectionChanged, + required Key key, + required List children, + required this.document, + required this.textDirection, + required this.hasFocus, + required this.selection, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.onSelectionChanged, this.padding = EdgeInsets.zero, }) : super(key: key, children: children); diff --git a/lib/widgets/responsive_widget.dart b/lib/widgets/responsive_widget.dart index a047deab..806883ec 100644 --- a/lib/widgets/responsive_widget.dart +++ b/lib/widgets/responsive_widget.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; class ResponsiveWidget extends StatelessWidget { final Widget largeScreen; - final Widget mediumScreen; - final Widget smallScreen; + final Widget? mediumScreen; + final Widget? smallScreen; const ResponsiveWidget( - {Key key, - @required this.largeScreen, + {Key? key, + required this.largeScreen, this.mediumScreen, this.smallScreen}) : super(key: key); diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index f0bc6487..6a12559f 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -53,10 +53,10 @@ class EditableTextBlock extends StatelessWidget { final Tuple2 verticalSpacing; final TextSelection textSelection; final Color color; - final DefaultStyles styles; + final DefaultStyles? styles; final bool enableInteractiveSelection; final bool hasFocus; - final EdgeInsets contentPadding; + final EdgeInsets? contentPadding; final EmbedBuilder embedBuilder; final CursorCont cursorCont; final Map indentLevelCounts; @@ -73,44 +73,41 @@ class EditableTextBlock extends StatelessWidget { this.contentPadding, this.embedBuilder, this.cursorCont, - this.indentLevelCounts) - : assert(hasFocus != null), - assert(embedBuilder != null), - assert(cursorCont != null); + this.indentLevelCounts); @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - DefaultStyles defaultStyles = QuillStyles.getStyles(context, false); + DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); return _EditableBlock( block, textDirection, - verticalSpacing, + verticalSpacing as Tuple2, _getDecorationForBlock(block, defaultStyles) ?? BoxDecoration(), contentPadding, _buildChildren(context, this.indentLevelCounts)); } - BoxDecoration _getDecorationForBlock( - Block node, DefaultStyles defaultStyles) { + BoxDecoration? _getDecorationForBlock( + Block node, DefaultStyles? defaultStyles) { Map attrs = block.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { - return defaultStyles.quote.decoration; + return defaultStyles!.quote!.decoration; } if (attrs.containsKey(Attribute.codeBlock.key)) { - return defaultStyles.code.decoration; + return defaultStyles!.code!.decoration; } return null; } List _buildChildren( BuildContext context, Map indentLevelCounts) { - DefaultStyles defaultStyles = QuillStyles.getStyles(context, false); + DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); int count = block.children.length; var children = []; int index = 0; - for (Line line in block.children) { + for (Line line in block.children as Iterable) { index++; EditableTextLine editableTextLine = EditableTextLine( line, @@ -119,7 +116,7 @@ class EditableTextBlock extends StatelessWidget { line: line, textDirection: textDirection, embedBuilder: embedBuilder, - styles: styles, + styles: styles!, ), _getIndentWidth(), _getSpacingForLine(line, index, count, defaultStyles), @@ -135,16 +132,16 @@ class EditableTextBlock extends StatelessWidget { return children.toList(growable: false); } - Widget _buildLeading(BuildContext context, Line line, int index, + Widget? _buildLeading(BuildContext context, Line line, int index, Map indentLevelCounts, int count) { - DefaultStyles defaultStyles = QuillStyles.getStyles(context, false); + DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false); Map attrs = line.style.attributes; if (attrs[Attribute.list.key] == Attribute.ol) { return _NumberPoint( index: index, indentLevelCounts: indentLevelCounts, count: count, - style: defaultStyles.paragraph.style, + style: defaultStyles!.paragraph!.style, attrs: attrs, width: 32.0, padding: 8.0, @@ -153,20 +150,20 @@ class EditableTextBlock extends StatelessWidget { if (attrs[Attribute.list.key] == Attribute.ul) { return _BulletPoint( - style: - defaultStyles.paragraph.style.copyWith(fontWeight: FontWeight.bold), + style: defaultStyles!.paragraph!.style + .copyWith(fontWeight: FontWeight.bold), width: 32, ); } if (attrs[Attribute.list.key] == Attribute.checked) { return _Checkbox( - style: defaultStyles.paragraph.style, width: 32, isChecked: true); + style: defaultStyles!.paragraph!.style, width: 32, isChecked: true); } if (attrs[Attribute.list.key] == Attribute.unchecked) { return _Checkbox( - style: defaultStyles.paragraph.style, width: 32, isChecked: false); + style: defaultStyles!.paragraph!.style, width: 32, isChecked: false); } if (attrs.containsKey(Attribute.codeBlock.key)) { @@ -174,8 +171,8 @@ class EditableTextBlock extends StatelessWidget { index: index, indentLevelCounts: indentLevelCounts, count: count, - style: defaultStyles.code.style - .copyWith(color: defaultStyles.code.style.color.withOpacity(0.4)), + style: defaultStyles!.code!.style + .copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)), width: 32.0, attrs: attrs, padding: 16.0, @@ -188,7 +185,7 @@ class EditableTextBlock extends StatelessWidget { double _getIndentWidth() { Map attrs = block.style.attributes; - Attribute indent = attrs[Attribute.indent.key]; + Attribute? indent = attrs[Attribute.indent.key]; double extraIndent = 0.0; if (indent != null && indent.value != null) { extraIndent = 16.0 * indent.value; @@ -202,40 +199,40 @@ class EditableTextBlock extends StatelessWidget { } Tuple2 _getSpacingForLine( - Line node, int index, int count, DefaultStyles defaultStyles) { + Line node, int index, int count, DefaultStyles? defaultStyles) { double top = 0.0, bottom = 0.0; Map attrs = block.style.attributes; if (attrs.containsKey(Attribute.header.key)) { - int level = attrs[Attribute.header.key].value; + int? level = attrs[Attribute.header.key]!.value; switch (level) { case 1: - top = defaultStyles.h1.verticalSpacing.item1; - bottom = defaultStyles.h1.verticalSpacing.item2; + top = defaultStyles!.h1!.verticalSpacing.item1; + bottom = defaultStyles.h1!.verticalSpacing.item2; break; case 2: - top = defaultStyles.h2.verticalSpacing.item1; - bottom = defaultStyles.h2.verticalSpacing.item2; + top = defaultStyles!.h2!.verticalSpacing.item1; + bottom = defaultStyles.h2!.verticalSpacing.item2; break; case 3: - top = defaultStyles.h3.verticalSpacing.item1; - bottom = defaultStyles.h3.verticalSpacing.item2; + top = defaultStyles!.h3!.verticalSpacing.item1; + bottom = defaultStyles.h3!.verticalSpacing.item2; break; default: throw ('Invalid level $level'); } } else { - Tuple2 lineSpacing; + late Tuple2 lineSpacing; if (attrs.containsKey(Attribute.blockQuote.key)) { - lineSpacing = defaultStyles.quote.lineSpacing; + lineSpacing = defaultStyles!.quote!.lineSpacing; } else if (attrs.containsKey(Attribute.indent.key)) { - lineSpacing = defaultStyles.indent.lineSpacing; + lineSpacing = defaultStyles!.indent!.lineSpacing; } else if (attrs.containsKey(Attribute.list.key)) { - lineSpacing = defaultStyles.lists.lineSpacing; + lineSpacing = defaultStyles!.lists!.lineSpacing; } else if (attrs.containsKey(Attribute.codeBlock.key)) { - lineSpacing = defaultStyles.code.lineSpacing; + lineSpacing = defaultStyles!.code!.lineSpacing; } else if (attrs.containsKey(Attribute.align.key)) { - lineSpacing = defaultStyles.align.lineSpacing; + lineSpacing = defaultStyles!.align!.lineSpacing; } top = lineSpacing.item1; bottom = lineSpacing.item2; @@ -256,19 +253,14 @@ class EditableTextBlock extends StatelessWidget { class RenderEditableTextBlock extends RenderEditableContainerBox implements RenderEditableBox { RenderEditableTextBlock({ - List children, - @required Block block, - @required TextDirection textDirection, - @required EdgeInsetsGeometry padding, - @required Decoration decoration, + List? children, + required Block block, + required TextDirection textDirection, + required EdgeInsetsGeometry padding, + required Decoration decoration, ImageConfiguration configuration = ImageConfiguration.empty, EdgeInsets contentPadding = EdgeInsets.zero, - }) : assert(block != null), - assert(textDirection != null), - assert(decoration != null), - assert(padding != null), - assert(contentPadding != null), - _decoration = decoration, + }) : _decoration = decoration, _configuration = configuration, _savedPadding = padding, _contentPadding = contentPadding, @@ -283,7 +275,6 @@ class RenderEditableTextBlock extends RenderEditableContainerBox EdgeInsets _contentPadding; set contentPadding(EdgeInsets value) { - assert(value != null); if (_contentPadding == value) return; _contentPadding = value; super.setPadding(_savedPadding.add(_contentPadding)); @@ -295,13 +286,12 @@ class RenderEditableTextBlock extends RenderEditableContainerBox _savedPadding = value; } - BoxPainter _painter; + BoxPainter? _painter; Decoration get decoration => _decoration; Decoration _decoration; set decoration(Decoration value) { - assert(value != null); if (value == _decoration) return; _painter?.dispose(); _painter = null; @@ -313,7 +303,6 @@ class RenderEditableTextBlock extends RenderEditableContainerBox ImageConfiguration _configuration; set configuration(ImageConfiguration value) { - assert(value != null); if (value == _configuration) return; _configuration = value; markNeedsPaint(); @@ -344,8 +333,8 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @override TextPosition getPositionForOffset(Offset offset) { - RenderEditableBox child = childAtOffset(offset); - BoxParentData parentData = child.parentData; + RenderEditableBox child = childAtOffset(offset)!; + BoxParentData parentData = child.parentData as BoxParentData; TextPosition localPosition = child.getPositionForOffset(offset - parentData.offset); return TextPosition( @@ -367,19 +356,19 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } @override - TextPosition getPositionAbove(TextPosition position) { + 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); + TextPosition? result = child.getPositionAbove(childLocalPosition); if (result != null) { return TextPosition( offset: result.offset + child.getContainer().getOffset()); } - RenderEditableBox sibling = childBefore(child); + RenderEditableBox? sibling = childBefore(child); if (sibling == null) { return null; } @@ -395,19 +384,19 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } @override - TextPosition getPositionBelow(TextPosition position) { + 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); + TextPosition? result = child.getPositionBelow(childLocalPosition); if (result != null) { return TextPosition( offset: result.offset + child.getContainer().getOffset()); } - RenderEditableBox sibling = childAfter(child); + RenderEditableBox? sibling = childAfter(child); if (sibling == null) { return null; } @@ -436,7 +425,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox null); } - Node baseNode = getContainer().queryChild(selection.start, false).node; + Node? baseNode = getContainer().queryChild(selection.start, false).node; var baseChild = firstChild; while (baseChild != null) { if (baseChild.getContainer() == baseNode) { @@ -446,7 +435,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } assert(baseChild != null); - TextSelectionPoint basePoint = baseChild.getBaseEndpointForSelection( + TextSelectionPoint basePoint = baseChild!.getBaseEndpointForSelection( localSelection(baseChild.getContainer(), selection, true)); return TextSelectionPoint( basePoint.point + (baseChild.parentData as BoxParentData).offset, @@ -462,7 +451,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox null); } - Node extentNode = getContainer().queryChild(selection.end, false).node; + Node? extentNode = getContainer().queryChild(selection.end, false).node; var extentChild = firstChild; while (extentChild != null) { @@ -473,7 +462,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } assert(extentChild != null); - TextSelectionPoint extentPoint = extentChild.getExtentEndpointForSelection( + TextSelectionPoint extentPoint = extentChild!.getExtentEndpointForSelection( localSelection(extentChild.getContainer(), selection, true)); return TextSelectionPoint( extentPoint.point + (extentChild.parentData as BoxParentData).offset, @@ -495,11 +484,9 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } _paintDecoration(PaintingContext context, Offset offset) { - assert(size.width != null); - assert(size.height != null); _painter ??= _decoration.createBoxPainter(markNeedsPaint); - EdgeInsets decorationPadding = resolvedPadding - _contentPadding; + EdgeInsets decorationPadding = resolvedPadding! - _contentPadding; ImageConfiguration filledConfiguration = configuration.copyWith(size: decorationPadding.deflateSize(size)); @@ -507,7 +494,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox final decorationOffset = offset.translate(decorationPadding.left, decorationPadding.top); - _painter.paint(context.canvas, decorationOffset, filledConfiguration); + _painter!.paint(context.canvas, decorationOffset, filledConfiguration); if (debugSaveCount != context.canvas.getSaveCount()) { throw ('${_decoration.runtimeType} painter had mismatching save and restore calls.'); } @@ -517,7 +504,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } @override - bool hitTestChildren(BoxHitTestResult result, {Offset position}) { + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { return defaultHitTestChildren(result, position: position); } } @@ -527,16 +514,11 @@ class _EditableBlock extends MultiChildRenderObjectWidget { final TextDirection textDirection; final Tuple2 padding; final Decoration decoration; - final EdgeInsets contentPadding; + final EdgeInsets? contentPadding; _EditableBlock(this.block, this.textDirection, this.padding, this.decoration, this.contentPadding, List children) - : assert(block != null), - assert(textDirection != null), - assert(padding != null), - assert(decoration != null), - assert(children != null), - super(children: children); + : super(children: children); EdgeInsets get _padding => EdgeInsets.only(top: padding.item1, bottom: padding.item2); @@ -567,7 +549,7 @@ class _EditableBlock extends MultiChildRenderObjectWidget { class _NumberPoint extends StatelessWidget { final int index; - final Map indentLevelCounts; + final Map indentLevelCounts; final int count; final TextStyle style; final double width; @@ -576,13 +558,13 @@ class _NumberPoint extends StatelessWidget { final double padding; const _NumberPoint({ - Key key, - @required this.index, - @required this.indentLevelCounts, - @required this.count, - @required this.style, - @required this.width, - @required this.attrs, + Key? key, + required this.index, + required this.indentLevelCounts, + required this.count, + required this.style, + required this.width, + required this.attrs, this.withDot = true, this.padding = 0.0, }) : super(key: key); @@ -590,7 +572,7 @@ class _NumberPoint extends StatelessWidget { @override Widget build(BuildContext context) { String s = this.index.toString(); - int level = 0; + int? level = 0; if (!this.attrs.containsKey(Attribute.indent.key) && !this.indentLevelCounts.containsKey(1)) { this.indentLevelCounts.clear(); @@ -602,13 +584,13 @@ class _NumberPoint extends StatelessWidget { ); } if (this.attrs.containsKey(Attribute.indent.key)) { - level = this.attrs[Attribute.indent.key].value; + level = this.attrs[Attribute.indent.key]!.value; } else { // first level but is back from previous indent level // supposed to be "2." this.indentLevelCounts[0] = 1; } - if (this.indentLevelCounts.containsKey(level + 1)) { + if (this.indentLevelCounts.containsKey(level! + 1)) { // last visited level is done, going up this.indentLevelCounts.remove(level + 1); } @@ -674,9 +656,9 @@ class _BulletPoint extends StatelessWidget { final double width; const _BulletPoint({ - Key key, - @required this.style, - @required this.width, + Key? key, + required this.style, + required this.width, }) : super(key: key); @override @@ -691,23 +673,23 @@ class _BulletPoint extends StatelessWidget { } class _Checkbox extends StatefulWidget { - final TextStyle style; - final double width; - final bool isChecked; + final TextStyle? style; + final double? width; + final bool? isChecked; - const _Checkbox({Key key, this.style, this.width, this.isChecked}) + const _Checkbox({Key? key, this.style, this.width, this.isChecked}) : super(key: key); @override __CheckboxState createState() => __CheckboxState(); } class __CheckboxState extends State<_Checkbox> { - bool isChecked; + bool? isChecked; - void _onCheckboxClicked(bool newValue) => setState(() { + void _onCheckboxClicked(bool? newValue) => setState(() { isChecked = newValue; - if (isChecked) { + if (isChecked!) { // check list } else { // uncheck list diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index a9260e8f..8eb7207e 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -23,16 +23,17 @@ import 'delegate.dart'; class TextLine extends StatelessWidget { final Line line; - final TextDirection textDirection; + final TextDirection? textDirection; final EmbedBuilder embedBuilder; final DefaultStyles styles; const TextLine( - {Key key, this.line, this.textDirection, this.embedBuilder, this.styles}) - : assert(line != null), - assert(embedBuilder != null), - assert(styles != null), - super(key: key); + {Key? key, + required this.line, + this.textDirection, + required this.embedBuilder, + required this.styles}) + : super(key: key); @override Widget build(BuildContext context) { @@ -45,7 +46,7 @@ class TextLine extends StatelessWidget { TextSpan textSpan = _buildTextSpan(context); StrutStyle strutStyle = - StrutStyle.fromTextStyle(textSpan.style, forceStrutHeight: true); + StrutStyle.fromTextStyle(textSpan.style!, forceStrutHeight: true); final textAlign = _getTextAlign(); RichText child = RichText( text: TextSpan(children: [textSpan]), @@ -56,9 +57,9 @@ class TextLine extends StatelessWidget { ); return RichTextProxy( child, - textSpan.style, + textSpan.style!, textAlign, - textDirection, + textDirection!, 1.0, Localizations.localeOf(context), strutStyle, @@ -89,27 +90,27 @@ class TextLine extends StatelessWidget { TextStyle textStyle = TextStyle(); if (line.style.containsKey(Attribute.placeholder.key)) { - textStyle = defaultStyles.placeHolder.style; + textStyle = defaultStyles.placeHolder!.style; return TextSpan(children: children, style: textStyle); } - Attribute header = line.style.attributes[Attribute.header.key]; + Attribute? header = line.style.attributes[Attribute.header.key]; Map m = { - Attribute.h1: defaultStyles.h1.style, - Attribute.h2: defaultStyles.h2.style, - Attribute.h3: defaultStyles.h3.style, + Attribute.h1: defaultStyles.h1!.style, + Attribute.h2: defaultStyles.h2!.style, + Attribute.h3: defaultStyles.h3!.style, }; - textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph.style); + textStyle = textStyle.merge(m[header!] ?? defaultStyles.paragraph!.style); - Attribute block = line.style.getBlockExceptHeader(); - TextStyle toMerge; + Attribute? block = line.style.getBlockExceptHeader(); + TextStyle? toMerge; if (block == Attribute.blockQuote) { - toMerge = defaultStyles.quote.style; + toMerge = defaultStyles.quote!.style; } else if (block == Attribute.codeBlock) { - toMerge = defaultStyles.code.style; + toMerge = defaultStyles.code!.style; } else if (block != null) { - toMerge = defaultStyles.lists.style; + toMerge = defaultStyles.lists!.style; } textStyle = textStyle.merge(toMerge); @@ -122,7 +123,7 @@ class TextLine extends StatelessWidget { Style style = textNode.style; TextStyle res = TextStyle(); - Map m = { + Map m = { Attribute.bold.key: defaultStyles.bold, Attribute.italic.key: defaultStyles.italic, Attribute.link.key: defaultStyles.link, @@ -131,16 +132,16 @@ class TextLine extends StatelessWidget { }; m.forEach((k, s) { if (style.values.any((v) => v.key == k)) { - res = _merge(res, s); + res = _merge(res, s!); } }); - Attribute font = textNode.style.attributes[Attribute.font.key]; + Attribute? 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]; + Attribute? size = textNode.style.attributes[Attribute.size.key]; if (size != null && size.value != null) { switch (size.value) { case 'small': @@ -153,7 +154,7 @@ class TextLine extends StatelessWidget { res = res.merge(defaultStyles.sizeHuge); break; default: - double fontSize = double.tryParse(size.value); + double? fontSize = double.tryParse(size.value); if (fontSize != null) { res = res.merge(TextStyle(fontSize: fontSize)); } else { @@ -162,13 +163,13 @@ class TextLine extends StatelessWidget { } } - Attribute color = textNode.style.attributes[Attribute.color.key]; + Attribute? color = textNode.style.attributes[Attribute.color.key]; if (color != null && color.value != null) { final textColor = stringToColor(color.value); res = res.merge(new TextStyle(color: textColor)); } - Attribute background = textNode.style.attributes[Attribute.background.key]; + Attribute? background = textNode.style.attributes[Attribute.background.key]; if (background != null && background.value != null) { final backgroundColor = stringToColor(background.value); res = res.merge(new TextStyle(backgroundColor: backgroundColor)); @@ -178,20 +179,22 @@ class TextLine extends StatelessWidget { } TextStyle _merge(TextStyle a, TextStyle b) { - final decorations = []; + final decorations = []; if (a.decoration != null) { decorations.add(a.decoration); } if (b.decoration != null) { decorations.add(b.decoration); } - return a.merge(b).apply(decoration: TextDecoration.combine(decorations)); + return a.merge(b).apply( + decoration: + TextDecoration.combine(decorations as List)); } } class EditableTextLine extends RenderObjectWidget { final Line line; - final Widget leading; + final Widget? leading; final Widget body; final double indentWidth; final Tuple2 verticalSpacing; @@ -215,14 +218,7 @@ class EditableTextLine extends RenderObjectWidget { this.enableInteractiveSelection, this.hasFocus, this.devicePixelRatio, - this.cursorCont) - : assert(line != null), - assert(indentWidth != null), - assert(textSelection != null), - assert(color != null), - assert(enableInteractiveSelection != null), - assert(hasFocus != null), - assert(cursorCont != null); + this.cursorCont); @override RenderObjectElement createElement() { @@ -268,8 +264,8 @@ class EditableTextLine extends RenderObjectWidget { enum TextLineSlot { LEADING, BODY } class RenderEditableTextLine extends RenderEditableBox { - RenderBox _leading; - RenderContentProxyBox _body; + RenderBox? _leading; + RenderContentProxyBox? _body; Line line; TextDirection textDirection; TextSelection textSelection; @@ -279,10 +275,10 @@ class RenderEditableTextLine extends RenderEditableBox { double devicePixelRatio; EdgeInsetsGeometry padding; CursorCont cursorCont; - EdgeInsets _resolvedPadding; - bool _containsCursor; - List _selectedRects; - Rect _caretPrototype; + EdgeInsets? _resolvedPadding; + bool? _containsCursor; + List? _selectedRects; + Rect? _caretPrototype; final Map children = {}; RenderEditableTextLine( @@ -294,26 +290,18 @@ class RenderEditableTextLine extends RenderEditableBox { this.devicePixelRatio, this.padding, this.color, - this.cursorCont) - : assert(line != null), - assert(padding != null), - assert(padding.isNonNegative), - assert(devicePixelRatio != null), - assert(hasFocus != null), - assert(color != null), - assert(cursorCont != null); + this.cursorCont); Iterable get _children sync* { if (_leading != null) { - yield _leading; + yield _leading!; } if (_body != null) { - yield _body; + yield _body!; } } setCursorCont(CursorCont c) { - assert(c != null); if (cursorCont == c) { return; } @@ -383,7 +371,6 @@ class RenderEditableTextLine extends RenderEditableBox { } setLine(Line l) { - assert(l != null); if (line == l) { return; } @@ -393,7 +380,6 @@ class RenderEditableTextLine extends RenderEditableBox { } setPadding(EdgeInsetsGeometry p) { - assert(p != null); assert(p.isNonNegative); if (padding == p) { return; @@ -403,12 +389,12 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsLayout(); } - setLeading(RenderBox l) { + setLeading(RenderBox? l) { _leading = _updateChild(_leading, l, TextLineSlot.LEADING); } - setBody(RenderContentProxyBox b) { - _body = _updateChild(_body, b, TextLineSlot.BODY); + setBody(RenderContentProxyBox? b) { + _body = _updateChild(_body, b, TextLineSlot.BODY) as RenderContentProxyBox?; } bool containsTextSelection() { @@ -421,7 +407,8 @@ class RenderEditableTextLine extends RenderEditableBox { line.containsOffset(textSelection.baseOffset); } - RenderBox _updateChild(RenderBox old, RenderBox newChild, TextLineSlot slot) { + RenderBox? _updateChild( + RenderBox? old, RenderBox? newChild, TextLineSlot slot) { if (old != null) { dropChild(old); children.remove(slot); @@ -434,10 +421,10 @@ class RenderEditableTextLine extends RenderEditableBox { } List _getBoxes(TextSelection textSelection) { - BoxParentData parentData = _body.parentData as BoxParentData; - return _body.getBoxesForSelection(textSelection).map((box) { + BoxParentData? parentData = _body!.parentData as BoxParentData?; + return _body!.getBoxesForSelection(textSelection).map((box) { return TextBox.fromLTRBD( - box.left + parentData.offset.dx, + box.left + parentData!.offset.dx, box.top + parentData.offset.dy, box.right + parentData.offset.dx, box.bottom + parentData.offset.dy, @@ -451,7 +438,7 @@ class RenderEditableTextLine extends RenderEditableBox { return; } _resolvedPadding = padding.resolve(textDirection); - assert(_resolvedPadding.isNonNegative); + assert(_resolvedPadding!.isNonNegative); } @override @@ -498,26 +485,26 @@ class RenderEditableTextLine extends RenderEditableBox { @override Offset getOffsetForCaret(TextPosition position) { - return _body.getOffsetForCaret(position, _caretPrototype) + - (_body.parentData as BoxParentData).offset; + return _body!.getOffsetForCaret(position, _caretPrototype) + + (_body!.parentData as BoxParentData).offset; } @override - TextPosition getPositionAbove(TextPosition position) { + TextPosition? getPositionAbove(TextPosition position) { return _getPosition(position, -0.5); } @override - TextPosition getPositionBelow(TextPosition position) { + TextPosition? getPositionBelow(TextPosition position) { return _getPosition(position, 1.5); } - TextPosition _getPosition(TextPosition textPosition, double dyScale) { + TextPosition? _getPosition(TextPosition textPosition, double dyScale) { assert(textPosition.offset < line.length); Offset offset = getOffsetForCaret(textPosition) .translate(0, dyScale * preferredLineHeight(textPosition)); - if (_body.size - .contains(offset - (_body.parentData as BoxParentData).offset)) { + if (_body!.size + .contains(offset - (_body!.parentData as BoxParentData).offset)) { return getPositionForOffset(offset); } return null; @@ -525,18 +512,18 @@ class RenderEditableTextLine extends RenderEditableBox { @override TextPosition getPositionForOffset(Offset offset) { - return _body.getPositionForOffset( - offset - (_body.parentData as BoxParentData).offset); + return _body!.getPositionForOffset( + offset - (_body!.parentData as BoxParentData).offset); } @override TextRange getWordBoundary(TextPosition position) { - return _body.getWordBoundary(position); + return _body!.getWordBoundary(position); } @override double preferredLineHeight(TextPosition position) { - return _body.getPreferredLineHeight(); + return _body!.getPreferredLineHeight(); } @override @@ -550,7 +537,6 @@ class RenderEditableTextLine extends RenderEditableBox { cursorCont.style.height ?? preferredLineHeight(TextPosition(offset: 0)); _computeCaretPrototype() { - assert(defaultTargetPlatform != null); switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: @@ -606,7 +592,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override List debugDescribeChildren() { var value = []; - void add(RenderBox child, String name) { + void add(RenderBox? child, String name) { if (child != null) { value.add(child.toDiagnosticsNode(name: name)); } @@ -623,38 +609,40 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMinIntrinsicWidth(double height) { _resolvePadding(); - double horizontalPadding = _resolvedPadding.left + _resolvedPadding.right; - double verticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; + double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; int leadingWidth = _leading == null ? 0 - : _leading.getMinIntrinsicWidth(height - verticalPadding); + : _leading!.getMinIntrinsicWidth(height - verticalPadding) as int; int bodyWidth = _body == null ? 0 - : _body.getMinIntrinsicWidth(math.max(0.0, height - verticalPadding)); + : _body!.getMinIntrinsicWidth(math.max(0.0, height - verticalPadding)) + as int; return horizontalPadding + leadingWidth + bodyWidth; } @override double computeMaxIntrinsicWidth(double height) { _resolvePadding(); - double horizontalPadding = _resolvedPadding.left + _resolvedPadding.right; - double verticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; + double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; int leadingWidth = _leading == null ? 0 - : _leading.getMaxIntrinsicWidth(height - verticalPadding); + : _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int; int bodyWidth = _body == null ? 0 - : _body.getMaxIntrinsicWidth(math.max(0.0, height - verticalPadding)); + : _body!.getMaxIntrinsicWidth(math.max(0.0, height - verticalPadding)) + as int; return horizontalPadding + leadingWidth + bodyWidth; } @override double computeMinIntrinsicHeight(double width) { _resolvePadding(); - double horizontalPadding = _resolvedPadding.left + _resolvedPadding.right; - double verticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; + double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (_body != null) { - return _body + return _body! .getMinIntrinsicHeight(math.max(0.0, width - horizontalPadding)) + verticalPadding; } @@ -664,10 +652,10 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeMaxIntrinsicHeight(double width) { _resolvePadding(); - double horizontalPadding = _resolvedPadding.left + _resolvedPadding.right; - double verticalPadding = _resolvedPadding.top + _resolvedPadding.bottom; + double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (_body != null) { - return _body + return _body! .getMaxIntrinsicHeight(math.max(0.0, width - horizontalPadding)) + verticalPadding; } @@ -677,7 +665,8 @@ class RenderEditableTextLine extends RenderEditableBox { @override double computeDistanceToActualBaseline(TextBaseline baseline) { _resolvePadding(); - return _body.getDistanceToActualBaseline(baseline) + _resolvedPadding.top; + return _body!.getDistanceToActualBaseline(baseline)! + + _resolvedPadding!.top; } @override @@ -690,34 +679,35 @@ class RenderEditableTextLine extends RenderEditableBox { if (_body == null && _leading == null) { size = constraints.constrain(Size( - _resolvedPadding.left + _resolvedPadding.right, - _resolvedPadding.top + _resolvedPadding.bottom, + _resolvedPadding!.left + _resolvedPadding!.right, + _resolvedPadding!.top + _resolvedPadding!.bottom, )); return; } - final innerConstraints = constraints.deflate(_resolvedPadding); + final innerConstraints = constraints.deflate(_resolvedPadding!); final indentWidth = textDirection == TextDirection.ltr - ? _resolvedPadding.left - : _resolvedPadding.right; + ? _resolvedPadding!.left + : _resolvedPadding!.right; - _body.layout(innerConstraints, parentUsesSize: true); - final bodyParentData = _body.parentData as BoxParentData; - bodyParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top); + _body!.layout(innerConstraints, parentUsesSize: true); + final bodyParentData = _body!.parentData as BoxParentData; + bodyParentData.offset = + Offset(_resolvedPadding!.left, _resolvedPadding!.top); if (_leading != null) { final leadingConstraints = innerConstraints.copyWith( minWidth: indentWidth, maxWidth: indentWidth, - maxHeight: _body.size.height); - _leading.layout(leadingConstraints, parentUsesSize: true); - final parentData = _leading.parentData as BoxParentData; - parentData.offset = Offset(0.0, _resolvedPadding.top); + maxHeight: _body!.size.height); + _leading!.layout(leadingConstraints, parentUsesSize: true); + final parentData = _leading!.parentData as BoxParentData; + parentData.offset = Offset(0.0, _resolvedPadding!.top); } size = constraints.constrain(Size( - _resolvedPadding.left + _body.size.width + _resolvedPadding.right, - _resolvedPadding.top + _body.size.height + _resolvedPadding.bottom, + _resolvedPadding!.left + _body!.size.width + _resolvedPadding!.right, + _resolvedPadding!.top + _body!.size.height + _resolvedPadding!.bottom, )); _computeCaretPrototype(); @@ -734,19 +724,19 @@ class RenderEditableTextLine extends RenderEditableBox { @override paint(PaintingContext context, Offset offset) { if (_leading != null) { - final parentData = _leading.parentData as BoxParentData; + final parentData = _leading!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; - context.paintChild(_leading, effectiveOffset); + context.paintChild(_leading!, effectiveOffset); } if (_body != null) { - final parentData = _body.parentData as BoxParentData; + final parentData = _body!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; - if ((enableInteractiveSelection ?? true) && + if ((enableInteractiveSelection) && line.getDocumentOffset() <= textSelection.end && textSelection.start <= line.getDocumentOffset() + line.length - 1) { final local = localSelection(line, textSelection, false); - _selectedRects ??= _body.getBoxesForSelection( + _selectedRects ??= _body!.getBoxesForSelection( local, ); _paintSelection(context, effectiveOffset); @@ -759,7 +749,7 @@ class RenderEditableTextLine extends RenderEditableBox { _paintCursor(context, effectiveOffset); } - context.paintChild(_body, effectiveOffset); + context.paintChild(_body!, effectiveOffset); if (hasFocus && cursorCont.show.value && @@ -773,7 +763,7 @@ class RenderEditableTextLine extends RenderEditableBox { _paintSelection(PaintingContext context, Offset effectiveOffset) { assert(_selectedRects != null); final paint = Paint()..color = color; - for (final box in _selectedRects) { + for (final box in _selectedRects!) { context.canvas.drawRect(box.toRect().shift(effectiveOffset), paint); } } @@ -787,7 +777,7 @@ class RenderEditableTextLine extends RenderEditableBox { } @override - bool hitTestChildren(BoxHitTestResult result, {Offset position}) { + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { return this._children.first.hitTest(result, position: position); } } @@ -819,7 +809,7 @@ class _TextLineElement extends RenderObjectElement { } @override - mount(Element parent, dynamic newSlot) { + mount(Element? parent, dynamic newSlot) { super.mount(parent, newSlot); _mountChild(widget.leading, TextLineSlot.LEADING); _mountChild(widget.body, TextLineSlot.BODY); @@ -834,16 +824,16 @@ class _TextLineElement extends RenderObjectElement { } @override - insertRenderObjectChild(RenderObject child, TextLineSlot slot) { + insertRenderObjectChild(RenderObject child, TextLineSlot? slot) { assert(child is RenderBox); _updateRenderObject(child, slot); assert(renderObject.children.keys.contains(slot)); } @override - removeRenderObjectChild(RenderObject child, TextLineSlot slot) { + removeRenderObjectChild(RenderObject child, TextLineSlot? slot) { assert(child is RenderBox); - assert(renderObject.children[slot] == child); + assert(renderObject.children[slot!] == child); _updateRenderObject(null, slot); assert(!renderObject.children.keys.contains(slot)); } @@ -853,9 +843,9 @@ class _TextLineElement extends RenderObjectElement { throw UnimplementedError(); } - _mountChild(Widget widget, TextLineSlot slot) { - Element oldChild = _slotToChildren[slot]; - Element newChild = updateChild(oldChild, widget, slot); + _mountChild(Widget? widget, TextLineSlot slot) { + Element? oldChild = _slotToChildren[slot]; + Element? newChild = updateChild(oldChild, widget, slot); if (oldChild != null) { _slotToChildren.remove(slot); } @@ -864,22 +854,22 @@ class _TextLineElement extends RenderObjectElement { } } - _updateRenderObject(RenderObject child, TextLineSlot slot) { + _updateRenderObject(RenderObject? child, TextLineSlot? slot) { switch (slot) { case TextLineSlot.LEADING: - renderObject.setLeading(child as RenderBox); + renderObject.setLeading(child as RenderBox?); break; case TextLineSlot.BODY: - renderObject.setBody(child as RenderBox); + renderObject.setBody((child as RenderBox?) as RenderContentProxyBox?); break; default: throw UnimplementedError(); } } - _updateChild(Widget widget, TextLineSlot slot) { - Element oldChild = _slotToChildren[slot]; - Element newChild = updateChild(oldChild, widget, slot); + _updateChild(Widget? widget, TextLineSlot slot) { + Element? oldChild = _slotToChildren[slot]; + Element? 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 0790cbd0..22b687b2 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -31,15 +31,15 @@ class EditorTextSelectionOverlay { final LayerLink toolbarLayerLink; final LayerLink startHandleLayerLink; final LayerLink endHandleLayerLink; - final RenderEditor renderObject; + final RenderEditor? renderObject; final TextSelectionControls selectionCtrls; final TextSelectionDelegate selectionDelegate; final DragStartBehavior dragStartBehavior; - final VoidCallback onSelectionHandleTapped; + final VoidCallback? onSelectionHandleTapped; final ClipboardStatusNotifier clipboardStatus; - AnimationController _toolbarController; - List _handles; - OverlayEntry toolbar; + late AnimationController _toolbarController; + List? _handles; + OverlayEntry? toolbar; EditorTextSelectionOverlay( this.value, @@ -54,14 +54,9 @@ class EditorTextSelectionOverlay { this.selectionDelegate, this.dragStartBehavior, this.onSelectionHandleTapped, - this.clipboardStatus) - : assert(value != null), - assert(context != null), - assert(handlesVisible != null) { - OverlayState overlay = Overlay.of(context, rootOverlay: true); - assert( - overlay != null, - ); + this.clipboardStatus) { + OverlayState overlay = Overlay.of(context, rootOverlay: true)!; + _toolbarController = AnimationController( duration: Duration(milliseconds: 150), vsync: overlay); } @@ -71,14 +66,13 @@ class EditorTextSelectionOverlay { Animation get _toolbarOpacity => _toolbarController.view; setHandlesVisible(bool visible) { - assert(visible != null); if (handlesVisible == visible) { return; } handlesVisible = visible; - if (SchedulerBinding.instance.schedulerPhase == + if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) { - SchedulerBinding.instance.addPostFrameCallback(markNeedsBuild); + SchedulerBinding.instance!.addPostFrameCallback(markNeedsBuild); } else { markNeedsBuild(); } @@ -88,37 +82,36 @@ class EditorTextSelectionOverlay { if (_handles == null) { return; } - _handles[0].remove(); - _handles[1].remove(); + _handles![0].remove(); + _handles![1].remove(); _handles = null; } hideToolbar() { assert(toolbar != null); _toolbarController.stop(); - toolbar.remove(); + toolbar!.remove(); toolbar = null; } showToolbar() { assert(toolbar == null); toolbar = OverlayEntry(builder: _buildToolbar); - Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor) - .insert(toolbar); + Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! + .insert(toolbar!); _toolbarController.forward(from: 0.0); } Widget _buildHandle( BuildContext context, _TextSelectionHandlePosition position) { if ((_selection.isCollapsed && - position == _TextSelectionHandlePosition.END) || - selectionCtrls == null) { + position == _TextSelectionHandlePosition.END)) { return Container(); } return Visibility( visible: handlesVisible, child: _TextSelectionHandleOverlay( - onSelectionHandleChanged: (TextSelection newSelection) { + onSelectionHandleChanged: (TextSelection? newSelection) { _handleSelectionHandleChanged(newSelection, position); }, onSelectionHandleTapped: onSelectionHandleTapped, @@ -137,23 +130,26 @@ class EditorTextSelectionOverlay { return; } value = newValue; - if (SchedulerBinding.instance.schedulerPhase == + if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) { - SchedulerBinding.instance.addPostFrameCallback(markNeedsBuild); + SchedulerBinding.instance!.addPostFrameCallback(markNeedsBuild); } else { markNeedsBuild(); } } _handleSelectionHandleChanged( - TextSelection newSelection, _TextSelectionHandlePosition position) { + TextSelection? newSelection, _TextSelectionHandlePosition position) { TextPosition textPosition; switch (position) { case _TextSelectionHandlePosition.START: - textPosition = newSelection.base; + textPosition = + newSelection != null ? newSelection.base : TextPosition(offset: 0); break; case _TextSelectionHandlePosition.END: - textPosition = newSelection.extent; + textPosition = newSelection != null + ? newSelection.extent + : TextPosition(offset: 0); break; default: throw ('Invalid position'); @@ -164,21 +160,17 @@ class EditorTextSelectionOverlay { } Widget _buildToolbar(BuildContext context) { - if (selectionCtrls == null) { - return Container(); - } - List endpoints = - renderObject.getEndpointsForSelection(_selection); + renderObject!.getEndpointsForSelection(_selection); Rect editingRegion = Rect.fromPoints( - renderObject.localToGlobal(Offset.zero), - renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)), + renderObject!.localToGlobal(Offset.zero), + renderObject!.localToGlobal(renderObject!.size.bottomRight(Offset.zero)), ); - double baseLineHeight = renderObject.preferredLineHeight(_selection.base); + double baseLineHeight = renderObject!.preferredLineHeight(_selection.base); double extentLineHeight = - renderObject.preferredLineHeight(_selection.extent); + renderObject!.preferredLineHeight(_selection.extent); double smallestLineHeight = math.min(baseLineHeight, extentLineHeight); bool isMultiline = endpoints.last.point.dy - endpoints.first.point.dy > smallestLineHeight / 2; @@ -211,18 +203,18 @@ class EditorTextSelectionOverlay { ); } - markNeedsBuild([Duration duration]) { + markNeedsBuild([Duration? duration]) { if (_handles != null) { - _handles[0].markNeedsBuild(); - _handles[1].markNeedsBuild(); + _handles![0].markNeedsBuild(); + _handles![1].markNeedsBuild(); } toolbar?.markNeedsBuild(); } hide() { if (_handles != null) { - _handles[0].remove(); - _handles[1].remove(); + _handles![0].remove(); + _handles![1].remove(); _handles = null; } if (toolbar != null) { @@ -246,22 +238,22 @@ class EditorTextSelectionOverlay { _buildHandle(context, _TextSelectionHandlePosition.END)), ]; - Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor) - .insertAll(_handles); + Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! + .insertAll(_handles!); } } class _TextSelectionHandleOverlay extends StatefulWidget { const _TextSelectionHandleOverlay({ - Key key, - @required this.selection, - @required this.position, - @required this.startHandleLayerLink, - @required this.endHandleLayerLink, - @required this.renderObject, - @required this.onSelectionHandleChanged, - @required this.onSelectionHandleTapped, - @required this.selectionControls, + Key? key, + required this.selection, + required this.position, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.renderObject, + required this.onSelectionHandleChanged, + required this.onSelectionHandleTapped, + required this.selectionControls, this.dragStartBehavior = DragStartBehavior.start, }) : super(key: key); @@ -269,9 +261,9 @@ class _TextSelectionHandleOverlay extends StatefulWidget { final _TextSelectionHandlePosition position; final LayerLink startHandleLayerLink; final LayerLink endHandleLayerLink; - final RenderEditor renderObject; - final ValueChanged onSelectionHandleChanged; - final VoidCallback onSelectionHandleTapped; + final RenderEditor? renderObject; + final ValueChanged onSelectionHandleChanged; + final VoidCallback? onSelectionHandleTapped; final TextSelectionControls selectionControls; final DragStartBehavior dragStartBehavior; @@ -279,21 +271,20 @@ class _TextSelectionHandleOverlay extends StatefulWidget { _TextSelectionHandleOverlayState createState() => _TextSelectionHandleOverlayState(); - ValueListenable get _visibility { + ValueListenable? get _visibility { switch (position) { case _TextSelectionHandlePosition.START: - return renderObject.selectionStartInViewport; + return renderObject!.selectionStartInViewport; case _TextSelectionHandlePosition.END: - return renderObject.selectionEndInViewport; + return renderObject!.selectionEndInViewport; } - return null; } } class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay> with SingleTickerProviderStateMixin { - AnimationController _controller; + late AnimationController _controller; Animation get _opacity => _controller.view; @@ -305,11 +296,11 @@ class _TextSelectionHandleOverlayState AnimationController(duration: Duration(milliseconds: 150), vsync: this); _handleVisibilityChanged(); - widget._visibility.addListener(_handleVisibilityChanged); + widget._visibility!.addListener(_handleVisibilityChanged); } _handleVisibilityChanged() { - if (widget._visibility.value) { + if (widget._visibility!.value) { _controller.forward(); } else { _controller.reverse(); @@ -319,14 +310,14 @@ class _TextSelectionHandleOverlayState @override didUpdateWidget(_TextSelectionHandleOverlay oldWidget) { super.didUpdateWidget(oldWidget); - oldWidget._visibility.removeListener(_handleVisibilityChanged); + oldWidget._visibility!.removeListener(_handleVisibilityChanged); _handleVisibilityChanged(); - widget._visibility.addListener(_handleVisibilityChanged); + widget._visibility!.addListener(_handleVisibilityChanged); } @override void dispose() { - widget._visibility.removeListener(_handleVisibilityChanged); + widget._visibility!.removeListener(_handleVisibilityChanged); _controller.dispose(); super.dispose(); } @@ -335,7 +326,7 @@ class _TextSelectionHandleOverlayState _handleDragUpdate(DragUpdateDetails details) { TextPosition position = - widget.renderObject.getPositionForOffset(details.globalPosition); + widget.renderObject!.getPositionForOffset(details.globalPosition); if (widget.selection.isCollapsed) { widget.onSelectionHandleChanged(TextSelection.fromPosition(position)); return; @@ -343,7 +334,7 @@ class _TextSelectionHandleOverlayState bool isNormalized = widget.selection.extentOffset >= widget.selection.baseOffset; - TextSelection newSelection; + TextSelection? newSelection; switch (widget.position) { case _TextSelectionHandlePosition.START: newSelection = TextSelection( @@ -368,19 +359,19 @@ class _TextSelectionHandleOverlayState _handleTap() { if (widget.onSelectionHandleTapped != null) - widget.onSelectionHandleTapped(); + widget.onSelectionHandleTapped!(); } @override Widget build(BuildContext context) { - LayerLink layerLink; - TextSelectionHandleType type; + late LayerLink layerLink; + TextSelectionHandleType? type; switch (widget.position) { case _TextSelectionHandlePosition.START: layerLink = widget.startHandleLayerLink; type = _chooseType( - widget.renderObject.textDirection, + widget.renderObject!.textDirection, TextSelectionHandleType.left, TextSelectionHandleType.right, ); @@ -389,7 +380,7 @@ class _TextSelectionHandleOverlayState assert(!widget.selection.isCollapsed); layerLink = widget.endHandleLayerLink; type = _chooseType( - widget.renderObject.textDirection, + widget.renderObject!.textDirection, TextSelectionHandleType.right, TextSelectionHandleType.left, ); @@ -400,9 +391,9 @@ class _TextSelectionHandleOverlayState widget.position == _TextSelectionHandlePosition.START ? widget.selection.base : widget.selection.extent; - double lineHeight = widget.renderObject.preferredLineHeight(textPosition); + double lineHeight = widget.renderObject!.preferredLineHeight(textPosition); Offset handleAnchor = - widget.selectionControls.getHandleAnchor(type, lineHeight); + widget.selectionControls.getHandleAnchor(type!, lineHeight); Size handleSize = widget.selectionControls.getHandleSize(lineHeight); Rect handleRect = Rect.fromLTWH( @@ -458,27 +449,25 @@ class _TextSelectionHandleOverlayState ); } - TextSelectionHandleType _chooseType( + TextSelectionHandleType? _chooseType( TextDirection textDirection, TextSelectionHandleType ltrType, TextSelectionHandleType rtlType, ) { if (widget.selection.isCollapsed) return TextSelectionHandleType.collapsed; - assert(textDirection != null); switch (textDirection) { case TextDirection.ltr: return ltrType; case TextDirection.rtl: return rtlType; } - return null; } } class EditorTextSelectionGestureDetector extends StatefulWidget { const EditorTextSelectionGestureDetector({ - Key key, + Key? key, this.onTapDown, this.onForcePressStart, this.onForcePressEnd, @@ -492,35 +481,34 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { this.onDragSelectionUpdate, this.onDragSelectionEnd, this.behavior, - @required this.child, - }) : assert(child != null), - super(key: key); + required this.child, + }) : super(key: key); - final GestureTapDownCallback onTapDown; + final GestureTapDownCallback? onTapDown; - final GestureForcePressStartCallback onForcePressStart; + final GestureForcePressStartCallback? onForcePressStart; - final GestureForcePressEndCallback onForcePressEnd; + final GestureForcePressEndCallback? onForcePressEnd; - final GestureTapUpCallback onSingleTapUp; + final GestureTapUpCallback? onSingleTapUp; - final GestureTapCancelCallback onSingleTapCancel; + final GestureTapCancelCallback? onSingleTapCancel; - final GestureLongPressStartCallback onSingleLongTapStart; + final GestureLongPressStartCallback? onSingleLongTapStart; - final GestureLongPressMoveUpdateCallback onSingleLongTapMoveUpdate; + final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate; - final GestureLongPressEndCallback onSingleLongTapEnd; + final GestureLongPressEndCallback? onSingleLongTapEnd; - final GestureTapDownCallback onDoubleTapDown; + final GestureTapDownCallback? onDoubleTapDown; - final GestureDragStartCallback onDragSelectionStart; + final GestureDragStartCallback? onDragSelectionStart; - final DragSelectionUpdateCallback onDragSelectionUpdate; + final DragSelectionUpdateCallback? onDragSelectionUpdate; - final GestureDragEndCallback onDragSelectionEnd; + final GestureDragEndCallback? onDragSelectionEnd; - final HitTestBehavior behavior; + final HitTestBehavior? behavior; final Widget child; @@ -531,8 +519,8 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { class _EditorTextSelectionGestureDetectorState extends State { - Timer _doubleTapTimer; - Offset _lastTapOffset; + Timer? _doubleTapTimer; + Offset? _lastTapOffset; bool _isDoubleTap = false; @override @@ -544,15 +532,15 @@ class _EditorTextSelectionGestureDetectorState _handleTapDown(TapDownDetails details) { if (widget.onTapDown != null) { - widget.onTapDown(details); + widget.onTapDown!(details); } if (_doubleTapTimer != null && _isWithinDoubleTapTolerance(details.globalPosition)) { if (widget.onDoubleTapDown != null) { - widget.onDoubleTapDown(details); + widget.onDoubleTapDown!(details); } - _doubleTapTimer.cancel(); + _doubleTapTimer!.cancel(); _doubleTapTimeout(); _isDoubleTap = true; } @@ -561,7 +549,7 @@ class _EditorTextSelectionGestureDetectorState _handleTapUp(TapUpDetails details) { if (!_isDoubleTap) { if (widget.onSingleTapUp != null) { - widget.onSingleTapUp(details); + widget.onSingleTapUp!(details); } _lastTapOffset = details.globalPosition; _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); @@ -571,19 +559,19 @@ class _EditorTextSelectionGestureDetectorState _handleTapCancel() { if (widget.onSingleTapCancel != null) { - widget.onSingleTapCancel(); + widget.onSingleTapCancel!(); } } - DragStartDetails _lastDragStartDetails; - DragUpdateDetails _lastDragUpdateDetails; - Timer _dragUpdateThrottleTimer; + DragStartDetails? _lastDragStartDetails; + DragUpdateDetails? _lastDragUpdateDetails; + Timer? _dragUpdateThrottleTimer; _handleDragStart(DragStartDetails details) { assert(_lastDragStartDetails == null); _lastDragStartDetails = details; if (widget.onDragSelectionStart != null) { - widget.onDragSelectionStart(details); + widget.onDragSelectionStart!(details); } } @@ -597,8 +585,8 @@ class _EditorTextSelectionGestureDetectorState assert(_lastDragStartDetails != null); assert(_lastDragUpdateDetails != null); if (widget.onDragSelectionUpdate != null) { - widget.onDragSelectionUpdate( - _lastDragStartDetails, _lastDragUpdateDetails); + widget.onDragSelectionUpdate!( + _lastDragStartDetails!, _lastDragUpdateDetails!); } _dragUpdateThrottleTimer = null; _lastDragUpdateDetails = null; @@ -607,11 +595,11 @@ class _EditorTextSelectionGestureDetectorState _handleDragEnd(DragEndDetails details) { assert(_lastDragStartDetails != null); if (_dragUpdateThrottleTimer != null) { - _dragUpdateThrottleTimer.cancel(); + _dragUpdateThrottleTimer!.cancel(); _handleDragUpdateThrottled(); } if (widget.onDragSelectionEnd != null) { - widget.onDragSelectionEnd(details); + widget.onDragSelectionEnd!(details); } _dragUpdateThrottleTimer = null; _lastDragStartDetails = null; @@ -622,31 +610,31 @@ class _EditorTextSelectionGestureDetectorState _doubleTapTimer?.cancel(); _doubleTapTimer = null; if (widget.onForcePressStart != null) { - widget.onForcePressStart(details); + widget.onForcePressStart!(details); } } _forcePressEnded(ForcePressDetails details) { if (widget.onForcePressEnd != null) { - widget.onForcePressEnd(details); + widget.onForcePressEnd!(details); } } _handleLongPressStart(LongPressStartDetails details) { if (!_isDoubleTap && widget.onSingleLongTapStart != null) { - widget.onSingleLongTapStart(details); + widget.onSingleLongTapStart!(details); } } _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { if (!_isDoubleTap && widget.onSingleLongTapMoveUpdate != null) { - widget.onSingleLongTapMoveUpdate(details); + widget.onSingleLongTapMoveUpdate!(details); } } _handleLongPressEnd(LongPressEndDetails details) { if (!_isDoubleTap && widget.onSingleLongTapEnd != null) { - widget.onSingleLongTapEnd(details); + widget.onSingleLongTapEnd!(details); } _isDoubleTap = false; } @@ -657,12 +645,11 @@ class _EditorTextSelectionGestureDetectorState } bool _isWithinDoubleTapTolerance(Offset secondTapOffset) { - assert(secondTapOffset != null); if (_lastTapOffset == null) { return false; } - return (secondTapOffset - _lastTapOffset).distance <= kDoubleTapSlop; + return (secondTapOffset - _lastTapOffset!).distance <= kDoubleTapSlop; } @override @@ -735,3 +722,18 @@ class _EditorTextSelectionGestureDetectorState ); } } + +class _TransparentTapGestureRecognizer extends TapGestureRecognizer { + _TransparentTapGestureRecognizer({ + Object? debugOwner, + }) : super(debugOwner: debugOwner); + + @override + void rejectGesture(int pointer) { + if (state == GestureRecognizerState.ready) { + acceptGesture(pointer); + } else { + super.rejectGesture(pointer); + } + } +} diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 74ee989b..2f6e0115 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -26,9 +26,9 @@ class InsertEmbedButton extends StatelessWidget { final IconData icon; const InsertEmbedButton({ - Key key, - @required this.controller, - @required this.icon, + Key? key, + required this.controller, + required this.icon, }) : super(key: key); @override @@ -54,11 +54,11 @@ class InsertEmbedButton extends StatelessWidget { class LinkStyleButton extends StatefulWidget { final QuillController controller; - final IconData icon; + final IconData? icon; const LinkStyleButton({ - Key key, - @required this.controller, + Key? key, + required this.controller, this.icon, }) : super(key: key); @@ -120,7 +120,7 @@ class _LinkStyleButtonState extends State { ).then(_linkSubmitted); } - void _linkSubmitted(String value) { + void _linkSubmitted(String? value) { if (value == null || value.isEmpty) { return; } @@ -129,7 +129,7 @@ class _LinkStyleButtonState extends State { } class _LinkDialog extends StatefulWidget { - const _LinkDialog({Key key}) : super(key: key); + const _LinkDialog({Key? key}) : super(key: key); @override _LinkDialogState createState() => _LinkDialogState(); @@ -170,8 +170,8 @@ typedef ToggleStyleButtonBuilder = Widget Function( BuildContext context, Attribute attribute, IconData icon, - bool isToggled, - VoidCallback onPressed, + bool? isToggled, + VoidCallback? onPressed, ); class ToggleStyleButton extends StatefulWidget { @@ -184,23 +184,19 @@ class ToggleStyleButton extends StatefulWidget { final ToggleStyleButtonBuilder childBuilder; ToggleStyleButton({ - Key key, - @required this.attribute, - @required this.icon, - @required this.controller, + Key? key, + required this.attribute, + required this.icon, + required this.controller, this.childBuilder = defaultToggleStyleButtonBuilder, - }) : assert(attribute.value != null), - assert(icon != null), - assert(controller != null), - assert(childBuilder != null), - super(key: key); + }) : super(key: key); @override _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); } class _ToggleStyleButtonState extends State { - bool _isToggled; + bool? _isToggled; Style get _selectionStyle => widget.controller.getSelectionStyle(); @@ -220,7 +216,7 @@ class _ToggleStyleButtonState extends State { bool _getIsToggled(Map attrs) { if (widget.attribute.key == Attribute.list.key) { - Attribute attribute = attrs[widget.attribute.key]; + Attribute? attribute = attrs[widget.attribute.key]; if (attribute == null) { return false; } @@ -256,7 +252,7 @@ class _ToggleStyleButtonState extends State { } _toggleAttribute() { - widget.controller.formatSelection(_isToggled + widget.controller.formatSelection(_isToggled! ? Attribute.clone(widget.attribute, null) : widget.attribute); } @@ -272,22 +268,19 @@ class ToggleCheckListButton extends StatefulWidget { final Attribute attribute; ToggleCheckListButton({ - Key key, - @required this.icon, - @required this.controller, + Key? key, + required this.icon, + required this.controller, this.childBuilder = defaultToggleStyleButtonBuilder, - @required this.attribute, - }) : assert(icon != null), - assert(controller != null), - assert(childBuilder != null), - super(key: key); + required this.attribute, + }) : super(key: key); @override _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); } class _ToggleCheckListButtonState extends State { - bool _isToggled; + bool? _isToggled; Style get _selectionStyle => widget.controller.getSelectionStyle(); @@ -307,7 +300,7 @@ class _ToggleCheckListButtonState extends State { bool _getIsToggled(Map attrs) { if (widget.attribute.key == Attribute.list.key) { - Attribute attribute = attrs[widget.attribute.key]; + Attribute? attribute = attrs[widget.attribute.key]; if (attribute == null) { return false; } @@ -344,7 +337,7 @@ class _ToggleCheckListButtonState extends State { } _toggleAttribute() { - widget.controller.formatSelection(_isToggled + widget.controller.formatSelection(_isToggled! ? Attribute.clone(Attribute.unchecked, null) : Attribute.unchecked); } @@ -354,17 +347,18 @@ Widget defaultToggleStyleButtonBuilder( BuildContext context, Attribute attribute, IconData icon, - bool isToggled, - VoidCallback onPressed, + bool? isToggled, + VoidCallback? onPressed, ) { final theme = Theme.of(context); final isEnabled = onPressed != null; final iconColor = isEnabled - ? isToggled + ? isToggled != null ? theme.primaryIconTheme.color : theme.iconTheme.color : theme.disabledColor; - final fillColor = isToggled ? theme.toggleableActiveColor : theme.canvasColor; + final fillColor = + isToggled != null ? theme.toggleableActiveColor : theme.canvasColor; return QuillIconButton( highlightElevation: 0, hoverElevation: 0, @@ -378,7 +372,7 @@ Widget defaultToggleStyleButtonBuilder( class SelectHeaderStyleButton extends StatefulWidget { final QuillController controller; - const SelectHeaderStyleButton({Key key, @required this.controller}) + const SelectHeaderStyleButton({Key? key, required this.controller}) : super(key: key); @override @@ -387,7 +381,7 @@ class SelectHeaderStyleButton extends StatefulWidget { } class _SelectHeaderStyleButtonState extends State { - Attribute _value; + Attribute? _value; Style get _selectionStyle => widget.controller.getSelectionStyle(); @@ -435,8 +429,8 @@ class _SelectHeaderStyleButtonState extends State { } } -Widget _selectHeadingStyleButtonBuilder( - BuildContext context, Attribute value, ValueChanged onSelected) { +Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, + ValueChanged onSelected) { final style = TextStyle(fontSize: 13); final Map _valueToText = { @@ -446,42 +440,42 @@ Widget _selectHeadingStyleButtonBuilder( Attribute.h3: 'Heading 3', }; - return QuillDropdownButton( + return QuillDropdownButton( highlightElevation: 0, hoverElevation: 0, height: iconSize * 1.77, fillColor: Theme.of(context).canvasColor, child: Text( !kIsWeb - ? _valueToText[value] - : _valueToText[value.key == "header" + ? _valueToText[value!]! + : _valueToText[value!.key == "header" ? Attribute.header : (value.key == "h1") ? Attribute.h1 : (value.key == "h2") ? Attribute.h2 - : Attribute.h3], + : Attribute.h3]!, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600), ), initialValue: value, items: [ PopupMenuItem( - child: Text(_valueToText[Attribute.header], style: style), + child: Text(_valueToText[Attribute.header]!, style: style), value: Attribute.header, height: iconSize * 1.77, ), PopupMenuItem( - child: Text(_valueToText[Attribute.h1], style: style), + child: Text(_valueToText[Attribute.h1]!, style: style), value: Attribute.h1, height: iconSize * 1.77, ), PopupMenuItem( - child: Text(_valueToText[Attribute.h2], style: style), + child: Text(_valueToText[Attribute.h2]!, style: style), value: Attribute.h2, height: iconSize * 1.77, ), PopupMenuItem( - child: Text(_valueToText[Attribute.h3], style: style), + child: Text(_valueToText[Attribute.h3]!, style: style), value: Attribute.h3, height: iconSize * 1.77, ), @@ -495,43 +489,40 @@ class ImageButton extends StatefulWidget { final QuillController controller; - final OnImagePickCallback onImagePickCallback; + final OnImagePickCallback? onImagePickCallback; - final ImagePickImpl imagePickImpl; + final ImagePickImpl? imagePickImpl; final ImageSource imageSource; ImageButton( - {Key key, - @required this.icon, - @required this.controller, - @required this.imageSource, + {Key? key, + required this.icon, + required this.controller, + required this.imageSource, this.onImagePickCallback, this.imagePickImpl}) - : assert(icon != null), - assert(controller != null), - super(key: key); + : super(key: key); @override _ImageButtonState createState() => _ImageButtonState(); } class _ImageButtonState extends State { - List _paths; - String _extension; + List? _paths; + String? _extension; final _picker = ImagePicker(); FileType _pickingType = FileType.any; - Future _pickImage(ImageSource source) async { - final PickedFile pickedFile = await _picker.getImage(source: source); + Future _pickImage(ImageSource source) async { + final PickedFile? pickedFile = await _picker.getImage(source: source); if (pickedFile == null) return null; final File file = File(pickedFile.path); - if (file == null || widget.onImagePickCallback == null) return null; // We simply return the absolute path to selected file. try { - String url = await widget.onImagePickCallback(file); + String url = await widget.onImagePickCallback!(file); print('Image uploaded and its url is $url'); return url; } catch (error) { @@ -540,13 +531,13 @@ class _ImageButtonState extends State { return null; } - Future _pickImageWeb() async { + Future _pickImageWeb() async { try { _paths = (await FilePicker.platform.pickFiles( type: _pickingType, allowMultiple: false, allowedExtensions: (_extension?.isNotEmpty ?? false) - ? _extension?.replaceAll(' ', '')?.split(',') + ? _extension?.replaceAll(' ', '').split(',') : null, )) ?.files; @@ -556,14 +547,13 @@ class _ImageButtonState extends State { print(ex); } var _fileName = - _paths != null ? _paths.map((e) => e.name).toString() : '...'; + _paths != null ? _paths!.map((e) => e.name).toString() : '...'; if (_paths != null) { File file = File(_fileName); - if (file == null || widget.onImagePickCallback == null) return null; // We simply return the absolute path to selected file. try { - String url = await widget.onImagePickCallback(file); + String url = await widget.onImagePickCallback!(file); print('Image uploaded and its url is $url'); return url; } catch (error) { @@ -584,16 +574,16 @@ class _ImageButtonState extends State { fsType: FilesystemType.file, fileTileSelectMode: FileTileSelectMode.wholeTile, ); - if (filePath == null || filePath.isEmpty) return null; + if (filePath != null && filePath.isEmpty) return ''; - final File file = File(filePath); - String url = await widget.onImagePickCallback(file); + final File file = File(filePath!); + String url = await widget.onImagePickCallback!(file); print('Image uploaded and its url is $url'); return url; } catch (error) { print('Upload image error $error'); } - return null; + return ''; } @override @@ -610,9 +600,9 @@ class _ImageButtonState extends State { onPressed: () { final index = widget.controller.selection.baseOffset; final length = widget.controller.selection.extentOffset - index; - Future image; + Future image; if (widget.imagePickImpl != null) { - image = widget.imagePickImpl(widget.imageSource); + image = widget.imagePickImpl!(widget.imageSource); } else { if (kIsWeb) { image = _pickImageWeb(); @@ -623,11 +613,8 @@ class _ImageButtonState extends State { } } image.then((imageUploadUrl) => { - if (imageUploadUrl != null) - { - widget.controller.replaceText( - index, length, BlockEmbed.image(imageUploadUrl), null) - } + widget.controller.replaceText( + index, length, BlockEmbed.image(imageUploadUrl!), null) }); }, ); @@ -644,24 +631,21 @@ class ColorButton extends StatefulWidget { final QuillController controller; ColorButton( - {Key key, - @required this.icon, - @required this.controller, - @required this.background}) - : assert(icon != null), - assert(controller != null), - assert(background != null), - super(key: key); + {Key? key, + required this.icon, + required this.controller, + required this.background}) + : super(key: key); @override _ColorButtonState createState() => _ColorButtonState(); } class _ColorButtonState extends State { - bool _isToggledColor; - bool _isToggledBackground; - bool _isWhite; - bool _isWhitebackground; + late bool _isToggledColor; + late bool _isToggledBackground; + late bool _isWhite; + late bool _isWhitebackground; Style get _selectionStyle => widget.controller.getSelectionStyle(); @@ -672,9 +656,9 @@ class _ColorButtonState extends State { _isToggledBackground = _getIsToggledBackground( widget.controller.getSelectionStyle().attributes); _isWhite = _isToggledColor && - _selectionStyle.attributes["color"].value == '#ffffff'; + _selectionStyle.attributes["color"]!.value == '#ffffff'; _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes["background"].value == '#ffffff'; + _selectionStyle.attributes["background"]!.value == '#ffffff'; }); } @@ -684,9 +668,9 @@ class _ColorButtonState extends State { _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); _isWhite = _isToggledColor && - _selectionStyle.attributes["color"].value == '#ffffff'; + _selectionStyle.attributes["color"]!.value == '#ffffff'; _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes["background"].value == '#ffffff'; + _selectionStyle.attributes["background"]!.value == '#ffffff'; widget.controller.addListener(_didChangeEditingValue); } @@ -708,9 +692,9 @@ class _ColorButtonState extends State { _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); _isWhite = _isToggledColor && - _selectionStyle.attributes["color"].value == '#ffffff'; + _selectionStyle.attributes["color"]!.value == '#ffffff'; _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes["background"].value == '#ffffff'; + _selectionStyle.attributes["background"]!.value == '#ffffff'; } } @@ -723,13 +707,13 @@ class _ColorButtonState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - Color iconColor = _isToggledColor && !widget.background && !_isWhite - ? stringToColor(_selectionStyle.attributes["color"].value) + Color? iconColor = _isToggledColor && !widget.background && !_isWhite + ? stringToColor(_selectionStyle.attributes["color"]!.value) : theme.iconTheme.color; - Color iconColorBackground = + Color? iconColorBackground = _isToggledBackground && widget.background && !_isWhitebackground - ? stringToColor(_selectionStyle.attributes["background"].value) + ? stringToColor(_selectionStyle.attributes["background"]!.value) : theme.iconTheme.color; Color fillColor = _isToggledColor && !widget.background && _isWhite @@ -785,22 +769,19 @@ class HistoryButton extends StatefulWidget { final QuillController controller; HistoryButton( - {Key key, - @required this.icon, - @required this.controller, - @required this.undo}) - : assert(icon != null), - assert(controller != null), - assert(undo != null), - super(key: key); + {Key? key, + required this.icon, + required this.controller, + required this.undo}) + : super(key: key); @override _HistoryButtonState createState() => _HistoryButtonState(); } class _HistoryButtonState extends State { - Color _iconColor; - ThemeData theme; + Color? _iconColor; + late ThemeData theme; @override Widget build(BuildContext context) { @@ -860,14 +841,11 @@ class IndentButton extends StatefulWidget { final bool isIncrease; IndentButton( - {Key key, - @required this.icon, - @required this.controller, - @required this.isIncrease}) - : assert(icon != null), - assert(controller != null), - assert(isIncrease != null), - super(key: key); + {Key? key, + required this.icon, + required this.controller, + required this.isIncrease}) + : super(key: key); @override _IndentButtonState createState() => _IndentButtonState(); @@ -917,10 +895,8 @@ class ClearFormatButton extends StatefulWidget { final QuillController controller; - ClearFormatButton({Key key, @required this.icon, @required this.controller}) - : assert(icon != null), - assert(controller != null), - super(key: key); + ClearFormatButton({Key? key, required this.icon, required this.controller}) + : super(key: key); @override _ClearFormatButtonState createState() => _ClearFormatButtonState(); @@ -950,11 +926,11 @@ class _ClearFormatButtonState extends State { class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { final List children; - const QuillToolbar({Key key, @required this.children}) : super(key: key); + const QuillToolbar({Key? key, required this.children}) : super(key: key); factory QuillToolbar.basic( - {Key key, - @required QuillController controller, + {Key? key, + required QuillController controller, double toolbarIconSize = 18.0, bool showBoldButton = true, bool showItalicButton = true, @@ -973,7 +949,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { bool showLink = true, bool showHistory = true, bool showHorizontalRule = false, - OnImagePickCallback onImagePickCallback}) { + OnImagePickCallback? onImagePickCallback}) { iconSize = toolbarIconSize; return QuillToolbar(key: key, children: [ Visibility( @@ -1192,16 +1168,16 @@ class _QuillToolbarState extends State { } class QuillIconButton extends StatelessWidget { - final VoidCallback onPressed; - final Widget icon; + final VoidCallback? onPressed; + final Widget? icon; final double size; - final Color fillColor; + final Color? fillColor; final double hoverElevation; final double highlightElevation; const QuillIconButton({ - Key key, - @required this.onPressed, + Key? key, + required this.onPressed, this.icon, this.size = 40, this.fillColor, @@ -1230,7 +1206,7 @@ class QuillIconButton extends StatelessWidget { class QuillDropdownButton extends StatefulWidget { final double height; - final Color fillColor; + final Color? fillColor; final double hoverElevation; final double highlightElevation; final Widget child; @@ -1239,15 +1215,15 @@ class QuillDropdownButton extends StatefulWidget { final ValueChanged onSelected; const QuillDropdownButton({ - Key key, + Key? key, this.height = 40, this.fillColor, this.hoverElevation = 1, this.highlightElevation = 1, - @required this.child, - @required this.initialValue, - @required this.items, - @required this.onSelected, + required this.child, + required this.initialValue, + required this.items, + required this.onSelected, }) : super(key: key); @override @@ -1276,7 +1252,8 @@ class _QuillDropdownButtonState extends State> { void _showMenu() { final popupMenuTheme = PopupMenuTheme.of(context); final button = context.findRenderObject() as RenderBox; - final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final overlay = + Overlay.of(context)!.context.findRenderObject() as RenderBox; final position = RelativeRect.fromRect( Rect.fromPoints( button.localToGlobal(Offset.zero, ancestor: overlay), @@ -1296,15 +1273,12 @@ class _QuillDropdownButtonState extends State> { // widget.shape ?? popupMenuTheme.shape, color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color, // captureInheritedThemes: widget.captureInheritedThemes, - ).then((T newValue) { + ).then((T? newValue) { if (!mounted) return null; if (newValue == null) { // if (widget.onCanceled != null) widget.onCanceled(); return null; } - if (widget.onSelected != null) { - widget.onSelected(newValue); - } }); } diff --git a/pubspec.lock b/pubspec.lock index 080b2b98..aac0d9cd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -56,7 +56,7 @@ packages: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.2" + version: "0.17.0" fake_async: dependency: transitive description: @@ -91,7 +91,7 @@ packages: name: filesystem_picker url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.0-nullsafety.0" flutter: dependency: "direct main" description: flutter @@ -103,7 +103,7 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "0.4.0-nullsafety.0" flutter_keyboard_visibility: dependency: "direct main" description: @@ -148,14 +148,14 @@ packages: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.14.0+4" + version: "0.15.0" http: dependency: transitive description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.0" + version: "0.13.1" http_parser: dependency: transitive description: @@ -169,7 +169,14 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.7.2+1" + version: "0.7.3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" image_picker_platform_interface: dependency: transitive description: @@ -321,7 +328,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.3.0" term_glyph: dependency: transitive description: @@ -356,21 +363,14 @@ packages: name: universal_html url: "https://pub.dartlang.org" source: hosted - version: "1.2.4" + version: "2.0.4" universal_io: dependency: transitive description: name: universal_io url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" - universal_ui: - dependency: "direct main" - description: - name: universal_ui - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.8" + version: "2.0.1" url_launcher: dependency: "direct main" description: @@ -426,7 +426,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" xdg_directories: dependency: transitive description: @@ -434,13 +434,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" - zone_local: - dependency: transitive - description: - name: zone_local - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.22.0" + flutter: ">=1.24.0-10.2.pre" diff --git a/pubspec.yaml b/pubspec.yaml index aac2f51e..682eb412 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,28 +6,25 @@ homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - quiver: ^3.0.0 collection: ^1.15.0 - tuple: ^2.0.0 - url_launcher: ^6.0.0 - flutter_colorpicker: ^0.3.5 - image_picker: ^0.7.2 - photo_view: ^0.11.0 - universal_html: ^1.2.4 file_picker: ^3.0.0 - filesystem_picker: ^1.0.4 - path_provider: ^2.0.1 - string_validator: ^0.1.4 + filesystem_picker: ^2.0.0-nullsafety.0 + flutter_colorpicker: ^0.4.0-nullsafety.0 flutter_keyboard_visibility: ^5.0.0 - universal_ui: ^0.0.8 - - + image_picker: ^0.7.3 + path_provider: ^2.0.1 + photo_view: ^0.11.1 + quiver: ^3.0.0 + string_validator: ^0.3.0 + tuple: ^2.0.0 + universal_html: ^2.0.4 + url_launcher: ^6.0.2 dev_dependencies: flutter_test: @@ -35,37 +32,34 @@ dev_dependencies: # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec - # The following section is specific to Flutter. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages +flutter: null +# To add assets to your package, add an assets section, like this: +# assets: +# - images/a_dot_burr.jpeg +# - images/a_dot_ham.jpeg +# +# For details regarding assets in packages, see +# https://flutter.dev/assets-and-images/#from-packages +# +# An image asset can refer to one or more resolution-specific "variants", see +# https://flutter.dev/assets-and-images/#resolution-aware. +# To add custom fonts to your package, add a fonts section here, +# in this "flutter" section. Each entry in this list should have a +# "family" key with the font family name, and a "fonts" key with a +# list giving the asset and other descriptors for the font. For +# example: +# fonts: +# - family: Schyler +# fonts: +# - asset: fonts/Schyler-Regular.ttf +# - asset: fonts/Schyler-Italic.ttf +# style: italic +# - family: Trajan Pro +# fonts: +# - asset: fonts/TrajanPro.ttf +# - asset: fonts/TrajanPro_Bold.ttf +# weight: 700 +# +# For details regarding fonts in packages, see +# https://flutter.dev/custom-fonts/#from-packages From 3021fbf06024f3b7fc64d142ed03db282ce86321 Mon Sep 17 00:00:00 2001 From: Miller Adulu Date: Sat, 20 Mar 2021 01:03:19 +0300 Subject: [PATCH 079/306] Fix basic widget rendering and editor usage (#95) * Upgrade upgradable packages * Apply default null-safety migrations * Remove hashnode as a dependency and localize its functionality to the package. Maintenance was done long time ago hence no need to wait for the update * Localize ui package to reduce maintenance burdens * Replace universal html with dart:html * Remove unnecessary checks * Fix formatting * Migrate app to null safety * Enable methods to be nullable * Fix non-nullable issue with node methods * Cast as Node * Use universal html * Use universal html package to bring in the ImageElement class * Remove unused imports * Fix imports on the editor file * Add key to quill editor * Remove final from the GlobalKey * Remove final on GlobalKey * Remove custom util implementation in favor of quiver * Fix issue with null on token attrivute * Remove final hashcode that is replaced by quiver functionality * Fix merge request * Fix hit test position in text_line.dart * Fix null safety errors on text_selection.dart * Fix sound null safe errors in toolbar.dart * Import null safe file picker * Fix issue with basic text editing and editor display * Fix issues with basic widget rendering and editing --- lib/widgets/delegate.dart | 12 ++++++------ lib/widgets/editor.dart | 29 ++++++++++++++--------------- lib/widgets/text_block.dart | 2 +- lib/widgets/text_line.dart | 16 ++++++++-------- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index a8278246..3fa51fab 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -14,7 +14,7 @@ abstract class EditorTextSelectionGestureDetectorBuilderDelegate { bool getForcePressEnabled(); - bool? getSelectionEnabled(); + bool getSelectionEnabled(); } class EditorTextSelectionGestureDetectorBuilder { @@ -43,7 +43,7 @@ class EditorTextSelectionGestureDetectorBuilder { onForcePressStart(ForcePressDetails details) { assert(delegate.getForcePressEnabled()); shouldShowSelectionToolbar = true; - if (delegate.getSelectionEnabled()!) { + if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectWordsInRange( details.globalPosition, null, @@ -65,7 +65,7 @@ class EditorTextSelectionGestureDetectorBuilder { } onSingleTapUp(TapUpDetails details) { - if (delegate.getSelectionEnabled()!) { + if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); } } @@ -73,7 +73,7 @@ class EditorTextSelectionGestureDetectorBuilder { onSingleTapCancel() {} onSingleLongTapStart(LongPressStartDetails details) { - if (delegate.getSelectionEnabled()!) { + if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectPositionAt( details.globalPosition, null, @@ -83,7 +83,7 @@ class EditorTextSelectionGestureDetectorBuilder { } onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (delegate.getSelectionEnabled()!) { + if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectPositionAt( details.globalPosition, null, @@ -99,7 +99,7 @@ class EditorTextSelectionGestureDetectorBuilder { } onDoubleTapDown(TapDownDetails details) { - if (delegate.getSelectionEnabled()!) { + if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectWord(SelectionChangedCause.tap); if (shouldShowSelectionToolbar) { getEditor()!.showToolbar(); diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 12b698bf..2cc6258c 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -149,7 +149,7 @@ class QuillEditor extends StatefulWidget { final bool? showCursor; final bool readOnly; final String? placeholder; - final bool? enableInteractiveSelection; + final bool enableInteractiveSelection; final double? minHeight; final double? maxHeight; final DefaultStyles? customStyles; @@ -161,8 +161,7 @@ class QuillEditor extends StatefulWidget { final EmbedBuilder embedBuilder; QuillEditor( - {Key? key, - required this.controller, + {required this.controller, required this.focusNode, required this.scrollController, required this.scrollable, @@ -171,7 +170,7 @@ class QuillEditor extends StatefulWidget { this.showCursor, required this.readOnly, this.placeholder, - this.enableInteractiveSelection, + this.enableInteractiveSelection = true, this.minHeight, this.maxHeight, this.customStyles, @@ -184,7 +183,7 @@ class QuillEditor extends StatefulWidget { kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder}); factory QuillEditor.basic( - {Key? key, required QuillController controller, required bool readOnly}) { + {required QuillController controller, required bool readOnly}) { return QuillEditor( controller: controller, scrollController: ScrollController(), @@ -270,10 +269,10 @@ class _QuillEditorState extends State widget.placeholder, widget.onLaunchUrl, ToolbarOptions( - copy: widget.enableInteractiveSelection ?? true, - cut: widget.enableInteractiveSelection ?? true, - paste: widget.enableInteractiveSelection ?? true, - selectAll: widget.enableInteractiveSelection ?? true, + copy: widget.enableInteractiveSelection, + cut: widget.enableInteractiveSelection, + paste: widget.enableInteractiveSelection, + selectAll: widget.enableInteractiveSelection, ), theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.android, @@ -296,7 +295,7 @@ class _QuillEditorState extends State selectionColor, textSelectionControls, widget.keyboardAppearance, - widget.enableInteractiveSelection!, + widget.enableInteractiveSelection, widget.scrollPhysics, widget.embedBuilder), ); @@ -313,7 +312,7 @@ class _QuillEditorState extends State } @override - bool? getSelectionEnabled() { + bool getSelectionEnabled() { return widget.enableInteractiveSelection; } @@ -331,7 +330,7 @@ class _QuillEditorSelectionGestureDetectorBuilder @override onForcePressStart(ForcePressDetails details) { super.onForcePressStart(details); - if (delegate.getSelectionEnabled()! && shouldShowSelectionToolbar) { + if (delegate.getSelectionEnabled() && shouldShowSelectionToolbar) { getEditor()!.showToolbar(); } } @@ -341,7 +340,7 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (!delegate.getSelectionEnabled()!) { + if (!delegate.getSelectionEnabled()) { return; } switch (Theme.of(_state.context).platform) { @@ -470,7 +469,7 @@ class _QuillEditorSelectionGestureDetectorBuilder bool positionSelected = _onTapping(details); - if (delegate.getSelectionEnabled()! && !positionSelected) { + if (delegate.getSelectionEnabled() && !positionSelected) { switch (Theme.of(_state.context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: @@ -499,7 +498,7 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapStart(LongPressStartDetails details) { - if (delegate.getSelectionEnabled()!) { + if (delegate.getSelectionEnabled()) { switch (Theme.of(_state.context).platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 6a12559f..ee71e6e5 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -107,7 +107,7 @@ class EditableTextBlock extends StatelessWidget { int count = block.children.length; var children = []; int index = 0; - for (Line line in block.children as Iterable) { + for (Line line in Iterable.castFrom(block.children)) { index++; EditableTextLine editableTextLine = EditableTextLine( line, diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 8eb7207e..ea676d64 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -101,7 +101,7 @@ class TextLine extends StatelessWidget { Attribute.h3: defaultStyles.h3!.style, }; - textStyle = textStyle.merge(m[header!] ?? defaultStyles.paragraph!.style); + textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style); Attribute? block = line.style.getBlockExceptHeader(); TextStyle? toMerge; @@ -187,8 +187,8 @@ class TextLine extends StatelessWidget { decorations.add(b.decoration); } return a.merge(b).apply( - decoration: - TextDecoration.combine(decorations as List)); + decoration: TextDecoration.combine( + List.castFrom(decorations))); } } @@ -824,8 +824,8 @@ class _TextLineElement extends RenderObjectElement { } @override - insertRenderObjectChild(RenderObject child, TextLineSlot? slot) { - assert(child is RenderBox); + insertRenderObjectChild(RenderBox child, TextLineSlot? slot) { + // assert(child is RenderBox); _updateRenderObject(child, slot); assert(renderObject.children.keys.contains(slot)); } @@ -854,13 +854,13 @@ class _TextLineElement extends RenderObjectElement { } } - _updateRenderObject(RenderObject? child, TextLineSlot? slot) { + _updateRenderObject(RenderBox? child, TextLineSlot? slot) { switch (slot) { case TextLineSlot.LEADING: - renderObject.setLeading(child as RenderBox?); + renderObject.setLeading(child); break; case TextLineSlot.BODY: - renderObject.setBody((child as RenderBox?) as RenderContentProxyBox?); + renderObject.setBody((child) as RenderContentProxyBox?); break; default: throw UnimplementedError(); From 60514cbd24fbde3a4500ae04b930a039ee5851f4 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 19 Mar 2021 15:53:35 -0700 Subject: [PATCH 080/306] Upgrade version to 1.1.0 --- CHANGELOG.md | 3 +++ app/pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6854232b..43608373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.0] +* Support null safety. + ## [1.0.9] * Web support for raw editor and keyboard listener. diff --git a/app/pubspec.lock b/app/pubspec.lock index e7326d61..6d3c6caf 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -131,7 +131,7 @@ packages: path: ".." relative: true source: path - version: "1.0.9" + version: "1.1.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 682eb412..3e1f49c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.0.9 +version: 1.1.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 4ebf5da5c4d0d9e14599401342839bdfb00145cd Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 19 Mar 2021 17:56:59 -0700 Subject: [PATCH 081/306] Remove class _TransparentTapGestureRecognizer --- lib/widgets/text_selection.dart | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 22b687b2..2b625e90 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -722,18 +722,3 @@ class _EditorTextSelectionGestureDetectorState ); } } - -class _TransparentTapGestureRecognizer extends TapGestureRecognizer { - _TransparentTapGestureRecognizer({ - Object? debugOwner, - }) : super(debugOwner: debugOwner); - - @override - void rejectGesture(int pointer) { - if (state == GestureRecognizerState.ready) { - acceptGesture(pointer); - } else { - super.rejectGesture(pointer); - } - } -} From 743cf20e7741f51b7a64a3b731768729bc7c7ccb Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 19 Mar 2021 18:03:05 -0700 Subject: [PATCH 082/306] Update app lock file --- app/pubspec.lock | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/app/pubspec.lock b/app/pubspec.lock index 6d3c6caf..b4d844be 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -43,6 +43,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" cupertino_icons: dependency: "direct main" description: @@ -84,7 +98,7 @@ packages: name: filesystem_picker url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.0-nullsafety.0" flutter: dependency: "direct main" description: flutter @@ -142,6 +156,13 @@ packages: description: flutter source: sdk version: "0.0.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.0" http: dependency: transitive description: @@ -162,7 +183,14 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.7.2+1" + version: "0.7.3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" image_picker_platform_interface: dependency: transitive description: @@ -314,7 +342,7 @@ packages: name: string_validator url: "https://pub.dartlang.org" source: hosted - version: "0.2.0-nullsafety.0" + version: "0.3.0" term_glyph: dependency: transitive description: @@ -343,6 +371,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + universal_html: + dependency: transitive + description: + name: universal_html + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" url_launcher: dependency: transitive description: From 975bc3b31054cfa227d474b7d9bc40eadbd89e44 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 20 Mar 2021 12:06:08 +0800 Subject: [PATCH 083/306] ignore .lock files update flutter_colorpicker to stable --- .gitignore | 1 + app/.gitignore | 1 + app/ios/Flutter/Debug.xcconfig | 1 + app/ios/Flutter/Release.xcconfig | 1 + app/macos/.gitignore | 1 + app/macos/Flutter/Flutter-Debug.xcconfig | 1 + app/macos/Flutter/Flutter-Release.xcconfig | 1 + app/macos/Podfile | 40 + app/macos/Runner.xcodeproj/project.pbxproj | 1204 +++++++++-------- .../contents.xcworkspacedata | 17 +- app/pubspec.lock | 453 ------- pubspec.lock | 439 ------ pubspec.yaml | 2 +- 13 files changed, 690 insertions(+), 1472 deletions(-) create mode 100644 app/macos/Podfile delete mode 100644 app/pubspec.lock delete mode 100644 pubspec.lock diff --git a/.gitignore b/.gitignore index 1985397a..e73faed9 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ build/ !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 +pubspec.lock \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore index 9d532b18..15e3a7c7 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -39,3 +39,4 @@ app.*.symbols # Obfuscation related app.*.map.json +pubspec.lock \ No newline at end of file diff --git a/app/ios/Flutter/Debug.xcconfig b/app/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/app/ios/Flutter/Debug.xcconfig +++ b/app/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/app/ios/Flutter/Release.xcconfig b/app/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/app/ios/Flutter/Release.xcconfig +++ b/app/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/app/macos/.gitignore b/app/macos/.gitignore index e146f77e..e72996ef 100644 --- a/app/macos/.gitignore +++ b/app/macos/.gitignore @@ -4,3 +4,4 @@ # Xcode-related **/xcuserdata/ +Podfile.lock \ No newline at end of file diff --git a/app/macos/Flutter/Flutter-Debug.xcconfig b/app/macos/Flutter/Flutter-Debug.xcconfig index f022c34e..df4c964c 100644 --- a/app/macos/Flutter/Flutter-Debug.xcconfig +++ b/app/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/app/macos/Flutter/Flutter-Release.xcconfig b/app/macos/Flutter/Flutter-Release.xcconfig index f022c34e..e79501e2 100644 --- a/app/macos/Flutter/Flutter-Release.xcconfig +++ b/app/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/app/macos/Podfile b/app/macos/Podfile new file mode 100644 index 00000000..dade8dfa --- /dev/null +++ b/app/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/app/macos/Runner.xcodeproj/project.pbxproj b/app/macos/Runner.xcodeproj/project.pbxproj index 9dfe210b..fa640cf1 100644 --- a/app/macos/Runner.xcodeproj/project.pbxproj +++ b/app/macos/Runner.xcodeproj/project.pbxproj @@ -1,572 +1,632 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 51; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* app.app */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* app.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 7C7D9700842BA0E01048B7B5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93FCB7C0C7A612A19EB2D2C2 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C1FD22CA47E09B485C5D2F0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4B8510D07EAD5FFD466A09A7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 93FCB7C0C7A612A19EB2D2C2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + F44289D50723316BB0B4ADA1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C7D9700842BA0E01048B7B5 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + D4C5240DF6823DEC9AB0AD3D /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* app.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D4C5240DF6823DEC9AB0AD3D /* Pods */ = { + isa = PBXGroup; + children = ( + 0C1FD22CA47E09B485C5D2F0 /* Pods-Runner.debug.xcconfig */, + 4B8510D07EAD5FFD466A09A7 /* Pods-Runner.release.xcconfig */, + F44289D50723316BB0B4ADA1 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 93FCB7C0C7A612A19EB2D2C2 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + ECE07D1381B49CCA8A8E0C5F /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 678C02007C3A278E6325D529 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 678C02007C3A278E6325D529 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + ECE07D1381B49CCA8A8E0C5F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/app/macos/Runner.xcworkspace/contents.xcworkspacedata b/app/macos/Runner.xcworkspace/contents.xcworkspacedata index 59c6d394..21a3cc14 100644 --- a/app/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -1,7 +1,10 @@ - - - - - + + + + + + + diff --git a/app/pubspec.lock b/app/pubspec.lock deleted file mode 100644 index b4d844be..00000000 --- a/app/pubspec.lock +++ /dev/null @@ -1,453 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.dartlang.org" - source: hosted - version: "0.17.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - ffi: - dependency: transitive - description: - name: ffi - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.0" - file_picker: - dependency: transitive - description: - name: file_picker - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - filesystem_picker: - dependency: transitive - description: - name: filesystem_picker - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0-nullsafety.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_colorpicker: - dependency: transitive - description: - name: flutter_colorpicker - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.0-nullsafety.0" - flutter_keyboard_visibility: - dependency: transitive - description: - name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" - source: hosted - version: "5.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_web: - dependency: transitive - description: - name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_quill: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "1.1.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - html: - dependency: transitive - description: - name: html - url: "https://pub.dartlang.org" - source: hosted - version: "0.15.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.0" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - image_picker: - dependency: transitive - description: - name: image_picker - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.3" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.3" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - path_provider: - dependency: "direct main" - description: - name: path_provider - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.0" - photo_view: - dependency: transitive - description: - name: photo_view - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.1" - platform: - dependency: transitive - description: - name: platform - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - process: - dependency: transitive - description: - name: process - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.0" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - string_validator: - dependency: transitive - description: - name: string_validator - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.19" - tuple: - dependency: transitive - description: - name: tuple - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - universal_html: - dependency: transitive - description: - name: universal_html - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" - universal_io: - dependency: transitive - description: - name: universal_io - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - url_launcher: - dependency: transitive - description: - name: url_launcher - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.2" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - win32: - dependency: transitive - description: - name: win32 - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0" -sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.24.0-10.2.pre" diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index aac0d9cd..00000000 --- a/pubspec.lock +++ /dev/null @@ -1,439 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: "direct main" - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.dartlang.org" - source: hosted - version: "0.17.0" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - ffi: - dependency: transitive - description: - name: ffi - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.0" - file_picker: - dependency: "direct main" - description: - name: file_picker - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - filesystem_picker: - dependency: "direct main" - description: - name: filesystem_picker - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0-nullsafety.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_colorpicker: - dependency: "direct main" - description: - name: flutter_colorpicker - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.0-nullsafety.0" - flutter_keyboard_visibility: - dependency: "direct main" - description: - name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" - source: hosted - version: "5.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_web: - dependency: transitive - description: - name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - html: - dependency: transitive - description: - name: html - url: "https://pub.dartlang.org" - source: hosted - version: "0.15.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.1" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - image_picker: - dependency: "direct main" - description: - name: image_picker - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.3" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.3" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - path_provider: - dependency: "direct main" - description: - name: path_provider - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.0" - photo_view: - dependency: "direct main" - description: - name: photo_view - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.1" - platform: - dependency: transitive - description: - name: platform - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - process: - dependency: transitive - description: - name: process - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.0" - quiver: - dependency: "direct main" - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - string_validator: - dependency: "direct main" - description: - name: string_validator - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.19" - tuple: - dependency: "direct main" - description: - name: tuple - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - universal_html: - dependency: "direct main" - description: - name: universal_html - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" - universal_io: - dependency: transitive - description: - name: universal_io - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.2" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - win32: - dependency: transitive - description: - name: win32 - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0" -sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.24.0-10.2.pre" diff --git a/pubspec.yaml b/pubspec.yaml index 3e1f49c3..29983ba0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: collection: ^1.15.0 file_picker: ^3.0.0 filesystem_picker: ^2.0.0-nullsafety.0 - flutter_colorpicker: ^0.4.0-nullsafety.0 + flutter_colorpicker: ^0.4.0 flutter_keyboard_visibility: ^5.0.0 image_picker: ^0.7.3 path_provider: ^2.0.1 From 4ac00bd912b982dec50e58df5abc95503fce9225 Mon Sep 17 00:00:00 2001 From: Thea Choem Date: Sat, 20 Mar 2021 18:59:32 +0700 Subject: [PATCH 084/306] Fix toggle color doesn't work (#97) --- lib/widgets/toolbar.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 2f6e0115..58f367d5 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -353,12 +353,12 @@ Widget defaultToggleStyleButtonBuilder( final theme = Theme.of(context); final isEnabled = onPressed != null; final iconColor = isEnabled - ? isToggled != null + ? isToggled == true ? theme.primaryIconTheme.color : theme.iconTheme.color : theme.disabledColor; final fillColor = - isToggled != null ? theme.toggleableActiveColor : theme.canvasColor; + isToggled == true ? theme.toggleableActiveColor : theme.canvasColor; return QuillIconButton( highlightElevation: 0, hoverElevation: 0, From fe7e0a6ba0503437a70baa665219077b60050385 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 22 Mar 2021 13:44:48 +0800 Subject: [PATCH 085/306] fix --- lib/widgets/raw_editor.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 24f9c397..1b9f4ad0 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -110,8 +110,8 @@ class RawEditorState extends EditorState FocusAttachment? _focusAttachment; late CursorCont _cursorCont; ScrollController? _scrollController; - late KeyboardVisibilityController _keyboardVisibilityController; - late StreamSubscription _keyboardVisibilitySubscription; + KeyboardVisibilityController? _keyboardVisibilityController; + StreamSubscription? _keyboardVisibilitySubscription; late KeyboardListener _keyboardListener; bool _didAutoFocus = false; bool _keyboardVisible = false; @@ -703,7 +703,7 @@ class RawEditorState extends EditorState } else { _keyboardVisibilityController = KeyboardVisibilityController(); _keyboardVisibilitySubscription = - _keyboardVisibilityController.onChange.listen((bool visible) { + _keyboardVisibilityController?.onChange.listen((bool visible) { _keyboardVisible = visible; if (visible) { _onChangeTextEditingValue(); @@ -868,7 +868,7 @@ class RawEditorState extends EditorState @override void dispose() { closeConnectionIfNeeded(); - _keyboardVisibilitySubscription.cancel(); + _keyboardVisibilitySubscription?.cancel(); assert(!hasConnection); _selectionOverlay?.dispose(); _selectionOverlay = null; From f62447ef65234d1e73edc6af1b5a823be2e44445 Mon Sep 17 00:00:00 2001 From: yahiaIB Date: Thu, 25 Mar 2021 04:58:32 +0200 Subject: [PATCH 086/306] add base64 image support in _defaultEmbedBuilder and Remove base64 header from image url (#107) * Remove base64 header from image url * change removeBase64Header to _standardizeImageUrl and format code Co-authored-by: Yahia --- lib/widgets/editor.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 2cc6258c..53eef90b 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -93,13 +93,22 @@ abstract class RenderAbstractEditor { void selectPosition(SelectionChangedCause cause); } +String _standardizeImageUrl(String url) { + if (url.contains("base64")) { + return url.split(",")[1]; + } + return url; +} + Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { switch (node.value.type) { case 'image': - String imageUrl = node.value.data; + String imageUrl = _standardizeImageUrl(node.value.data); return imageUrl.startsWith('http') ? Image.network(imageUrl) - : Image.file(io.File(imageUrl)); + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + : Image.file(io.File(imageUrl)); default: throw UnimplementedError( 'Embeddable type "${node.value.type}" is not supported by default embed ' @@ -411,7 +420,7 @@ class _QuillEditorSelectionGestureDetectorBuilder if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) { BlockEmbed blockEmbed = segment.value as BlockEmbed; if (blockEmbed.type == 'image') { - final String imageUrl = blockEmbed.data; + final String imageUrl = _standardizeImageUrl(blockEmbed.data); Navigator.push( getEditor()!.context, MaterialPageRoute( From 1c9cc2978cd249f67c207dd5a51940f921815df5 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 24 Mar 2021 22:38:34 -0700 Subject: [PATCH 087/306] Upgrade version to 1.1.1 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43608373..03fbb31d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.1] +* Base64 image support. + ## [1.1.0] * Support null safety. diff --git a/pubspec.yaml b/pubspec.yaml index 29983ba0..e54c69ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.1.0 +version: 1.1.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 25eff9cd0755cb0172963724b001c90ab76dc951 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 24 Mar 2021 23:20:02 -0700 Subject: [PATCH 088/306] Update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3cb37a95..cfdbd4f2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ FlutterQuill is a rich text editor and a [Quill] component for [Flutter]. This library is a WYSIWYG editor built for the modern mobile platform, with web compatibility under development. You can join our [Slack Group] for discussion. -https://pub.dev/packages/flutter_quill +Demo App: https://bulletjournal.us/home/index.html + +Pub: https://pub.dev/packages/flutter_quill ## Usage @@ -77,9 +79,7 @@ For web development, use `flutter config --enable-web` for flutter and use [Reac 1 1 1 -1 - -One client and affiliated collaborator of **[FlutterQuill]** is Bullet Journal App: https://bulletjournal.us/home/index.html +1 [Quill]: https://quilljs.com/docs/formats [Flutter]: https://github.com/flutter/flutter From 811cb341bf25e17291ee9512be800da2bbea9c80 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 24 Mar 2021 23:48:23 -0700 Subject: [PATCH 089/306] Add pedantic --- CHANGELOG.md | 3 ++ analysis_options.yaml | 2 + example/main.dart | 2 +- lib/models/documents/document.dart | 6 +-- lib/models/documents/history.dart | 16 +++--- lib/models/documents/nodes/block.dart | 2 +- lib/utils/universal_ui/fake_ui.dart | 2 +- lib/utils/universal_ui/real_ui.dart | 4 +- lib/utils/universal_ui/universal_ui.dart | 5 +- lib/widgets/text_line.dart | 6 +-- lib/widgets/text_selection.dart | 55 ++++++++++---------- lib/widgets/toolbar.dart | 66 ++++++++++++------------ pubspec.yaml | 5 +- 13 files changed, 89 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fbb31d..61e2cf0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.2] +* Add pedantic. + ## [1.1.1] * Base64 image support. diff --git a/analysis_options.yaml b/analysis_options.yaml index cb1ea33c..98c97294 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,5 @@ +include: package:pedantic/analysis_options.yaml + analyzer: errors: undefined_prefixed_name: ignore \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index a523c5a6..1b31b0bb 100644 --- a/example/main.dart +++ b/example/main.dart @@ -9,7 +9,7 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - QuillController _controller = QuillController.basic(); + final QuillController _controller = QuillController.basic(); @override Widget build(BuildContext context) { diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 38813510..2e715f3f 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -117,7 +117,7 @@ class Document { return block.queryChild(res.offset, true); } - compose(Delta delta, ChangeSource changeSource) { + void compose(Delta delta, ChangeSource changeSource) { assert(!_observer.isClosed); delta.trim(); assert(delta.isNotEmpty); @@ -208,14 +208,14 @@ class Document { return Embeddable.fromJson(data as Map); } - close() { + void close() { _observer.close(); _history.clear(); } String toPlainText() => _root.children.map((e) => e.toPlainText()).join(''); - _loadDocument(Delta doc) { + void _loadDocument(Delta doc) { assert((doc.last.data as String).endsWith('\n')); int offset = 0; for (final op in doc.toList()) { diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index 1bb92105..99798d71 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -69,8 +69,8 @@ class History { ///It will override pre local undo delta,replaced by remote change /// void transform(Delta delta) { - transformStack(this.stack.undo, delta); - transformStack(this.stack.redo, delta); + transformStack(stack.undo, delta); + transformStack(stack.redo, delta); } void transformStack(List stack, Delta delta) { @@ -85,8 +85,8 @@ class History { } Tuple2 _change(Document doc, List source, List dest) { - if (source.length == 0) { - return new Tuple2(false, 0); + if (source.isEmpty) { + return Tuple2(false, 0); } Delta delta = source.removeLast(); // look for insert or delete @@ -102,11 +102,11 @@ class History { Delta base = Delta.from(doc.toDelta()); Delta inverseDelta = delta.invert(base); dest.add(inverseDelta); - this.lastRecorded = 0; - this.ignoreChange = true; + lastRecorded = 0; + ignoreChange = true; doc.compose(delta, ChangeSource.LOCAL); - this.ignoreChange = false; - return new Tuple2(true, len); + ignoreChange = false; + return Tuple2(true, len); } Tuple2 undo(Document doc) { diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index 6ab8d331..4d569cc7 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -16,7 +16,7 @@ class Block extends Container { } @override - adjust() { + void adjust() { if (isEmpty) { Node? sibling = previous; unlink(); diff --git a/lib/utils/universal_ui/fake_ui.dart b/lib/utils/universal_ui/fake_ui.dart index da0f9a32..3c59b5d2 100644 --- a/lib/utils/universal_ui/fake_ui.dart +++ b/lib/utils/universal_ui/fake_ui.dart @@ -1,3 +1,3 @@ -class platformViewRegistry { +class PlatformViewRegistry { static registerViewFactory(String viewId, dynamic cb) {} } diff --git a/lib/utils/universal_ui/real_ui.dart b/lib/utils/universal_ui/real_ui.dart index c2b8ea23..f699c9c0 100644 --- a/lib/utils/universal_ui/real_ui.dart +++ b/lib/utils/universal_ui/real_ui.dart @@ -1,9 +1,7 @@ import 'dart:ui' as ui; -// ignore: camel_case_types -class platformViewRegistry { +class PlatformViewRegistry { static registerViewFactory(String viewId, dynamic cb) { - // ignore:undefined_prefixed_name ui.platformViewRegistry.registerViewFactory(viewId, cb); } } diff --git a/lib/utils/universal_ui/universal_ui.dart b/lib/utils/universal_ui/universal_ui.dart index d97aff1f..307520c5 100644 --- a/lib/utils/universal_ui/universal_ui.dart +++ b/lib/utils/universal_ui/universal_ui.dart @@ -6,12 +6,11 @@ import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; class PlatformViewRegistryFix { registerViewFactory(dynamic x, dynamic y) { if (kIsWeb) { - // ignore: undefined_prefixed_name - ui_instance.platformViewRegistry.registerViewFactory( + ui_instance.PlatformViewRegistry.registerViewFactory( x, y, ); - } else {} + } } } diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index ea676d64..e7f0975c 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -843,7 +843,7 @@ class _TextLineElement extends RenderObjectElement { throw UnimplementedError(); } - _mountChild(Widget? widget, TextLineSlot slot) { + void _mountChild(Widget? widget, TextLineSlot slot) { Element? oldChild = _slotToChildren[slot]; Element? newChild = updateChild(oldChild, widget, slot); if (oldChild != null) { @@ -854,7 +854,7 @@ class _TextLineElement extends RenderObjectElement { } } - _updateRenderObject(RenderBox? child, TextLineSlot? slot) { + void _updateRenderObject(RenderBox? child, TextLineSlot? slot) { switch (slot) { case TextLineSlot.LEADING: renderObject.setLeading(child); @@ -867,7 +867,7 @@ class _TextLineElement extends RenderObjectElement { } } - _updateChild(Widget? widget, TextLineSlot slot) { + void _updateChild(Widget? widget, TextLineSlot slot) { Element? oldChild = _slotToChildren[slot]; Element? newChild = updateChild(oldChild, widget, slot); if (oldChild != null) { diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 2b625e90..4d9cb564 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -65,7 +65,7 @@ class EditorTextSelectionOverlay { Animation get _toolbarOpacity => _toolbarController.view; - setHandlesVisible(bool visible) { + void setHandlesVisible(bool visible) { if (handlesVisible == visible) { return; } @@ -78,7 +78,7 @@ class EditorTextSelectionOverlay { } } - hideHandles() { + void hideHandles() { if (_handles == null) { return; } @@ -87,14 +87,14 @@ class EditorTextSelectionOverlay { _handles = null; } - hideToolbar() { + void hideToolbar() { assert(toolbar != null); _toolbarController.stop(); toolbar!.remove(); toolbar = null; } - showToolbar() { + void showToolbar() { assert(toolbar == null); toolbar = OverlayEntry(builder: _buildToolbar); Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! @@ -125,7 +125,7 @@ class EditorTextSelectionOverlay { )); } - update(TextEditingValue newValue) { + void update(TextEditingValue newValue) { if (value == newValue) { return; } @@ -138,7 +138,7 @@ class EditorTextSelectionOverlay { } } - _handleSelectionHandleChanged( + void _handleSelectionHandleChanged( TextSelection? newSelection, _TextSelectionHandlePosition position) { TextPosition textPosition; switch (position) { @@ -203,7 +203,7 @@ class EditorTextSelectionOverlay { ); } - markNeedsBuild([Duration? duration]) { + void markNeedsBuild([Duration? duration]) { if (_handles != null) { _handles![0].markNeedsBuild(); _handles![1].markNeedsBuild(); @@ -211,7 +211,7 @@ class EditorTextSelectionOverlay { toolbar?.markNeedsBuild(); } - hide() { + void hide() { if (_handles != null) { _handles![0].remove(); _handles![1].remove(); @@ -222,7 +222,7 @@ class EditorTextSelectionOverlay { } } - dispose() { + void dispose() { hide(); _toolbarController.dispose(); } @@ -299,7 +299,7 @@ class _TextSelectionHandleOverlayState widget._visibility!.addListener(_handleVisibilityChanged); } - _handleVisibilityChanged() { + void _handleVisibilityChanged() { if (widget._visibility!.value) { _controller.forward(); } else { @@ -308,7 +308,7 @@ class _TextSelectionHandleOverlayState } @override - didUpdateWidget(_TextSelectionHandleOverlay oldWidget) { + void didUpdateWidget(_TextSelectionHandleOverlay oldWidget) { super.didUpdateWidget(oldWidget); oldWidget._visibility!.removeListener(_handleVisibilityChanged); _handleVisibilityChanged(); @@ -322,9 +322,9 @@ class _TextSelectionHandleOverlayState super.dispose(); } - _handleDragStart(DragStartDetails details) {} + void _handleDragStart(DragStartDetails details) {} - _handleDragUpdate(DragUpdateDetails details) { + void _handleDragUpdate(DragUpdateDetails details) { TextPosition position = widget.renderObject!.getPositionForOffset(details.globalPosition); if (widget.selection.isCollapsed) { @@ -357,9 +357,10 @@ class _TextSelectionHandleOverlayState widget.onSelectionHandleChanged(newSelection); } - _handleTap() { - if (widget.onSelectionHandleTapped != null) + void _handleTap() { + if (widget.onSelectionHandleTapped != null) { widget.onSelectionHandleTapped!(); + } } @override @@ -530,7 +531,7 @@ class _EditorTextSelectionGestureDetectorState super.dispose(); } - _handleTapDown(TapDownDetails details) { + void _handleTapDown(TapDownDetails details) { if (widget.onTapDown != null) { widget.onTapDown!(details); } @@ -546,7 +547,7 @@ class _EditorTextSelectionGestureDetectorState } } - _handleTapUp(TapUpDetails details) { + void _handleTapUp(TapUpDetails details) { if (!_isDoubleTap) { if (widget.onSingleTapUp != null) { widget.onSingleTapUp!(details); @@ -557,7 +558,7 @@ class _EditorTextSelectionGestureDetectorState _isDoubleTap = false; } - _handleTapCancel() { + void _handleTapCancel() { if (widget.onSingleTapCancel != null) { widget.onSingleTapCancel!(); } @@ -567,7 +568,7 @@ class _EditorTextSelectionGestureDetectorState DragUpdateDetails? _lastDragUpdateDetails; Timer? _dragUpdateThrottleTimer; - _handleDragStart(DragStartDetails details) { + void _handleDragStart(DragStartDetails details) { assert(_lastDragStartDetails == null); _lastDragStartDetails = details; if (widget.onDragSelectionStart != null) { @@ -575,13 +576,13 @@ class _EditorTextSelectionGestureDetectorState } } - _handleDragUpdate(DragUpdateDetails details) { + void _handleDragUpdate(DragUpdateDetails details) { _lastDragUpdateDetails = details; _dragUpdateThrottleTimer ??= Timer(Duration(milliseconds: 50), _handleDragUpdateThrottled); } - _handleDragUpdateThrottled() { + void _handleDragUpdateThrottled() { assert(_lastDragStartDetails != null); assert(_lastDragUpdateDetails != null); if (widget.onDragSelectionUpdate != null) { @@ -592,7 +593,7 @@ class _EditorTextSelectionGestureDetectorState _lastDragUpdateDetails = null; } - _handleDragEnd(DragEndDetails details) { + void _handleDragEnd(DragEndDetails details) { assert(_lastDragStartDetails != null); if (_dragUpdateThrottleTimer != null) { _dragUpdateThrottleTimer!.cancel(); @@ -606,7 +607,7 @@ class _EditorTextSelectionGestureDetectorState _lastDragUpdateDetails = null; } - _forcePressStarted(ForcePressDetails details) { + void _forcePressStarted(ForcePressDetails details) { _doubleTapTimer?.cancel(); _doubleTapTimer = null; if (widget.onForcePressStart != null) { @@ -614,25 +615,25 @@ class _EditorTextSelectionGestureDetectorState } } - _forcePressEnded(ForcePressDetails details) { + void _forcePressEnded(ForcePressDetails details) { if (widget.onForcePressEnd != null) { widget.onForcePressEnd!(details); } } - _handleLongPressStart(LongPressStartDetails details) { + void _handleLongPressStart(LongPressStartDetails details) { if (!_isDoubleTap && widget.onSingleLongTapStart != null) { widget.onSingleLongTapStart!(details); } } - _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { + void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { if (!_isDoubleTap && widget.onSingleLongTapMoveUpdate != null) { widget.onSingleLongTapMoveUpdate!(details); } } - _handleLongPressEnd(LongPressEndDetails details) { + void _handleLongPressEnd(LongPressEndDetails details) { if (!_isDoubleTap && widget.onSingleLongTapEnd != null) { widget.onSingleLongTapEnd!(details); } diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 58f367d5..3a6a72b5 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -251,7 +251,7 @@ class _ToggleStyleButtonState extends State { _isToggled, isEnabled ? _toggleAttribute : null); } - _toggleAttribute() { + void _toggleAttribute() { widget.controller.formatSelection(_isToggled! ? Attribute.clone(widget.attribute, null) : widget.attribute); @@ -336,7 +336,7 @@ class _ToggleCheckListButtonState extends State { _isToggled, isEnabled ? _toggleAttribute : null); } - _toggleAttribute() { + void _toggleAttribute() { widget.controller.formatSelection(_isToggled! ? Attribute.clone(Attribute.unchecked, null) : Attribute.unchecked); @@ -445,42 +445,42 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, hoverElevation: 0, height: iconSize * 1.77, fillColor: Theme.of(context).canvasColor, - child: Text( - !kIsWeb - ? _valueToText[value!]! - : _valueToText[value!.key == "header" - ? Attribute.header - : (value.key == "h1") - ? Attribute.h1 - : (value.key == "h2") - ? Attribute.h2 - : Attribute.h3]!, - style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600), - ), initialValue: value, items: [ PopupMenuItem( - child: Text(_valueToText[Attribute.header]!, style: style), value: Attribute.header, height: iconSize * 1.77, + child: Text(_valueToText[Attribute.header]!, style: style), ), PopupMenuItem( - child: Text(_valueToText[Attribute.h1]!, style: style), value: Attribute.h1, height: iconSize * 1.77, + child: Text(_valueToText[Attribute.h1]!, style: style), ), PopupMenuItem( - child: Text(_valueToText[Attribute.h2]!, style: style), value: Attribute.h2, height: iconSize * 1.77, + child: Text(_valueToText[Attribute.h2]!, style: style), ), PopupMenuItem( - child: Text(_valueToText[Attribute.h3]!, style: style), value: Attribute.h3, height: iconSize * 1.77, + child: Text(_valueToText[Attribute.h3]!, style: style), ), ], onSelected: onSelected, + child: Text( + !kIsWeb + ? _valueToText[value!]! + : _valueToText[value!.key == 'header' + ? Attribute.header + : (value.key == 'h1') + ? Attribute.h1 + : (value.key == 'h2') + ? Attribute.h2 + : Attribute.h3]!, + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600), + ), ); } @@ -512,7 +512,7 @@ class _ImageButtonState extends State { List? _paths; String? _extension; final _picker = ImagePicker(); - FileType _pickingType = FileType.any; + final FileType _pickingType = FileType.any; Future _pickImage(ImageSource source) async { final PickedFile? pickedFile = await _picker.getImage(source: source); @@ -542,7 +542,7 @@ class _ImageButtonState extends State { )) ?.files; } on PlatformException catch (e) { - print("Unsupported operation" + e.toString()); + print('Unsupported operation' + e.toString()); } catch (ex) { print(ex); } @@ -656,9 +656,9 @@ class _ColorButtonState extends State { _isToggledBackground = _getIsToggledBackground( widget.controller.getSelectionStyle().attributes); _isWhite = _isToggledColor && - _selectionStyle.attributes["color"]!.value == '#ffffff'; + _selectionStyle.attributes['color']!.value == '#ffffff'; _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes["background"]!.value == '#ffffff'; + _selectionStyle.attributes['background']!.value == '#ffffff'; }); } @@ -668,9 +668,9 @@ class _ColorButtonState extends State { _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); _isWhite = _isToggledColor && - _selectionStyle.attributes["color"]!.value == '#ffffff'; + _selectionStyle.attributes['color']!.value == '#ffffff'; _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes["background"]!.value == '#ffffff'; + _selectionStyle.attributes['background']!.value == '#ffffff'; widget.controller.addListener(_didChangeEditingValue); } @@ -692,9 +692,9 @@ class _ColorButtonState extends State { _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); _isWhite = _isToggledColor && - _selectionStyle.attributes["color"]!.value == '#ffffff'; + _selectionStyle.attributes['color']!.value == '#ffffff'; _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes["background"]!.value == '#ffffff'; + _selectionStyle.attributes['background']!.value == '#ffffff'; } } @@ -708,12 +708,12 @@ class _ColorButtonState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); Color? iconColor = _isToggledColor && !widget.background && !_isWhite - ? stringToColor(_selectionStyle.attributes["color"]!.value) + ? stringToColor(_selectionStyle.attributes['color']!.value) : theme.iconTheme.color; - Color? iconColorBackground = + var iconColorBackground = _isToggledBackground && widget.background && !_isWhitebackground - ? stringToColor(_selectionStyle.attributes["background"]!.value) + ? stringToColor(_selectionStyle.attributes['background']!.value) : theme.iconTheme.color; Color fillColor = _isToggledColor && !widget.background && _isWhite @@ -747,7 +747,7 @@ class _ColorButtonState extends State { Navigator.of(context).pop(); } - _showColorPicker() { + void _showColorPicker() { showDialog( context: context, builder: (_) => AlertDialog( @@ -755,7 +755,7 @@ class _ColorButtonState extends State { backgroundColor: Theme.of(context).canvasColor, content: SingleChildScrollView( child: MaterialPicker( - pickerColor: Color(0), + pickerColor: Color(0x00000000), onColorChanged: _changeColor, ), )), @@ -1190,7 +1190,6 @@ class QuillIconButton extends StatelessWidget { return ConstrainedBox( constraints: BoxConstraints.tightFor(width: size, height: size), child: RawMaterialButton( - child: icon, visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), padding: EdgeInsets.zero, @@ -1199,6 +1198,7 @@ class QuillIconButton extends StatelessWidget { hoverElevation: hoverElevation, highlightElevation: hoverElevation, onPressed: onPressed, + child: icon, ), ); } @@ -1236,7 +1236,6 @@ class _QuillDropdownButtonState extends State> { return ConstrainedBox( constraints: BoxConstraints.tightFor(height: widget.height), child: RawMaterialButton( - child: _buildContent(context), visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), padding: EdgeInsets.zero, @@ -1245,6 +1244,7 @@ class _QuillDropdownButtonState extends State> { hoverElevation: widget.hoverElevation, highlightElevation: widget.hoverElevation, onPressed: _showMenu, + child: _buildContent(context), ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index e54c69ee..7fb8925c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill -description: One client and affiliated collaborator of Flutter Quill is Bullet Journal App. -version: 1.1.1 +description: Rich text editor (Demo App https://bulletjournal.us/home/index.html ). +version: 1.1.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill @@ -25,6 +25,7 @@ dependencies: tuple: ^2.0.0 universal_html: ^2.0.4 url_launcher: ^6.0.2 + pedantic: ^1.11.0 dev_dependencies: flutter_test: From 718912bc29524da9f603f6d406ba2a319c8bab95 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 25 Mar 2021 00:07:30 -0700 Subject: [PATCH 090/306] Clear hints --- lib/models/documents/nodes/container.dart | 10 ++-- lib/models/documents/nodes/leaf.dart | 16 +++--- lib/models/documents/nodes/line.dart | 20 +++---- lib/models/documents/nodes/node.dart | 8 +-- lib/models/rules/delete.dart | 4 +- lib/models/rules/format.dart | 4 +- lib/models/rules/insert.dart | 12 ++-- lib/models/rules/rule.dart | 6 +- lib/utils/color.dart | 2 +- lib/utils/universal_ui/fake_ui.dart | 2 +- lib/utils/universal_ui/real_ui.dart | 2 +- lib/utils/universal_ui/universal_ui.dart | 2 +- lib/widgets/controller.dart | 18 +++--- lib/widgets/cursor.dart | 14 ++--- lib/widgets/default_styles.dart | 36 ++++++------ lib/widgets/delegate.dart | 4 +- lib/widgets/editor.dart | 60 ++++++++++--------- lib/widgets/proxy.dart | 4 +- lib/widgets/raw_editor.dart | 46 +++++++-------- lib/widgets/text_block.dart | 70 +++++++++++------------ lib/widgets/text_line.dart | 64 ++++++++++----------- pubspec.yaml | 2 +- 22 files changed, 202 insertions(+), 204 deletions(-) diff --git a/lib/models/documents/nodes/container.dart b/lib/models/documents/nodes/container.dart index 2e1c29fa..c7c10390 100644 --- a/lib/models/documents/nodes/container.dart +++ b/lib/models/documents/nodes/container.dart @@ -25,13 +25,13 @@ abstract class Container extends Node { /// abstract methods end - add(T node) { + void add(T node) { assert(node?.parent == null); node?.parent = this; _children.add(node as Node); } - addFirst(T node) { + void addFirst(T node) { assert(node?.parent == null); node?.parent = this; _children.addFirst(node as Node); @@ -80,7 +80,7 @@ abstract class Container extends Node { int get length => _children.fold(0, (cur, node) => cur + node.length); @override - insert(int index, Object data, Style? style) { + void insert(int index, Object data, Style? style) { assert(index == 0 || (index > 0 && index < length)); if (isNotEmpty) { @@ -97,14 +97,14 @@ abstract class Container extends Node { } @override - retain(int index, int? length, Style? attributes) { + void retain(int index, int? length, Style? attributes) { assert(isNotEmpty); ChildQuery child = queryChild(index, false); child.node!.retain(child.offset, length, attributes); } @override - delete(int index, int? length) { + void delete(int index, int? length) { assert(isNotEmpty); ChildQuery child = queryChild(index, false); child.node!.delete(child.offset, length); diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index 804c9a10..efb2ccde 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -50,7 +50,7 @@ abstract class Leaf extends Node { } @override - insert(int index, Object data, Style? style) { + void insert(int index, Object data, Style? style) { assert(index >= 0 && index <= length); Leaf node = Leaf(data); if (index < length) { @@ -62,12 +62,12 @@ abstract class Leaf extends Node { } @override - retain(int index, int? len, Style? style) { + void retain(int index, int? len, Style? style) { if (style == null) { return; } - int local = math.min(this.length - index, len!); + int local = math.min(length - index, len!); int remain = len - local; Leaf node = _isolate(index, local); @@ -79,10 +79,10 @@ abstract class Leaf extends Node { } @override - delete(int index, int? len) { - assert(index < this.length); + void delete(int index, int? len) { + assert(index < length); - int local = math.min(this.length - index, len!); + int local = math.min(length - index, len!); Leaf target = _isolate(index, local); Leaf? prev = target.previous as Leaf?; Leaf? next = target.next as Leaf?; @@ -100,7 +100,7 @@ abstract class Leaf extends Node { } @override - adjust() { + void adjust() { if (this is Embed) { return; } @@ -147,7 +147,7 @@ abstract class Leaf extends Node { return split; } - format(Style? style) { + void format(Style? style) { if (style != null && style.isNotEmpty) { applyStyle(style); } diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 4e3b1ac0..574549ea 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -66,7 +66,7 @@ class Line extends Container { } @override - insert(int index, Object data, Style? style) { + void insert(int index, Object data, Style? style) { if (data is Embeddable) { _insert(index, data, style); return; @@ -101,11 +101,11 @@ class Line extends Container { } @override - retain(int index, int? len, Style? style) { + void retain(int index, int? len, Style? style) { if (style == null) { return; } - int thisLen = this.length; + int thisLen = length; int local = math.min(thisLen - index, len!); @@ -126,9 +126,9 @@ class Line extends Container { } @override - delete(int index, int? len) { - int local = math.min(this.length - index, len!); - bool deleted = index + local == this.length; + void delete(int index, int? len) { + int local = math.min(length - index, len!); + bool deleted = index + local == length; if (deleted) { clearStyle(); if (local > 1) { @@ -187,14 +187,14 @@ class Line extends Container { } } - _wrap(Block block) { + void _wrap(Block block) { assert(parent != null && parent is! Block); insertAfter(block); unlink(); block.add(this); } - _unwrap() { + void _unwrap() { if (parent is! Block) { throw ArgumentError('Invalid parent'); } @@ -246,7 +246,7 @@ class Line extends Container { return line; } - _insert(int index, Object data, Style? style) { + void _insert(int index, Object data, Style? style) { assert(index == 0 || (index > 0 && index < length)); if (data is String) { @@ -273,7 +273,7 @@ class Line extends Container { } Style collectStyle(int offset, int len) { - int local = math.min(this.length - offset, len); + int local = math.min(length - offset, len); Style res = Style(); var excluded = {}; diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index 984ed0a1..abc093c3 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -84,7 +84,7 @@ abstract class Node extends LinkedListEntry { super.unlink(); } - adjust() { + void adjust() { // do nothing } @@ -96,11 +96,11 @@ abstract class Node extends LinkedListEntry { Delta toDelta(); - insert(int index, Object data, Style? style); + void insert(int index, Object data, Style? style); - retain(int index, int? len, Style? style); + void retain(int index, int? len, Style? style); - delete(int index, int? len); + void delete(int index, int? len); /// abstract methods end diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index 8f590231..15aa3862 100644 --- a/lib/models/rules/delete.dart +++ b/lib/models/rules/delete.dart @@ -9,7 +9,7 @@ abstract class DeleteRule extends Rule { RuleType get type => RuleType.DELETE; @override - validateArgs(int? len, Object? data, Attribute? attribute) { + void validateArgs(int? len, Object? data, Attribute? attribute) { assert(len != null); assert(data == null); assert(attribute == null); @@ -54,7 +54,7 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { String text = op.data is String ? (op.data as String?)! : ''; int lineBreak = text.indexOf('\n'); if (lineBreak == -1) { - delta..retain(op.length!); + delta.retain(op.length!); continue; } diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart index f5875833..755f4137 100644 --- a/lib/models/rules/format.dart +++ b/lib/models/rules/format.dart @@ -9,7 +9,7 @@ abstract class FormatRule extends Rule { RuleType get type => RuleType.FORMAT; @override - validateArgs(int? len, Object? data, Attribute? attribute) { + void validateArgs(int? len, Object? data, Attribute? attribute) { assert(len != null); assert(data == null); assert(attribute != null); @@ -55,7 +55,7 @@ class ResolveLineFormatRule extends FormatRule { String text = op.data is String ? (op.data as String?)! : ''; int lineBreak = text.indexOf('\n'); if (lineBreak < 0) { - delta..retain(op.length!); + delta.retain(op.length!); continue; } delta..retain(lineBreak)..retain(1, attribute.toJson()); diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 61829972..62585ec3 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -11,7 +11,7 @@ abstract class InsertRule extends Rule { RuleType get type => RuleType.INSERT; @override - validateArgs(int? len, Object? data, Attribute? attribute) { + void validateArgs(int? len, Object? data, Attribute? attribute) { assert(len == null); assert(data != null); assert(attribute == null); @@ -45,7 +45,7 @@ class PreserveLineStyleOnSplitRule extends InsertRule { Delta delta = Delta()..retain(index); if (text.contains('\n')) { assert(after.isPlain); - delta..insert('\n'); + delta.insert('\n'); return delta; } Tuple2 nextNewLine = _getNextNewLine(itr); @@ -222,7 +222,7 @@ class InsertEmbedsRule extends InsertRule { } else { while (itr.hasNext) { Operation op = itr.next(); - if ((op.data is String ? op.data as String? : '')!.indexOf('\n') >= 0) { + if ((op.data is String ? op.data as String? : '')!.contains('\n')) { lineStyle = op.attributes; break; } @@ -230,11 +230,11 @@ class InsertEmbedsRule extends InsertRule { } if (!isNewlineBefore) { - delta..insert('\n', lineStyle); + delta.insert('\n', lineStyle); } - delta..insert(data); + delta.insert(data); if (!isNewlineAfter) { - delta..insert('\n'); + delta.insert('\n'); } return delta; } diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index a549c22f..da83797b 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -18,7 +18,7 @@ abstract class Rule { len: len, data: data, attribute: attribute); } - validateArgs(int? len, Object? data, Attribute? attribute); + void validateArgs(int? len, Object? data, Attribute? attribute); Delta? applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}); @@ -61,11 +61,11 @@ class Rules { final result = rule.apply(delta, index, len: len, data: data, attribute: attribute); if (result != null) { - print("Rule $rule applied"); + print('Rule $rule applied'); return result..trim(); } } catch (e) { - throw e; + rethrow; } } throw ('Apply rules failed'); diff --git a/lib/utils/color.dart b/lib/utils/color.dart index c7f467fa..4bc1c55a 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -115,7 +115,7 @@ Color stringToColor(String? s) { } if (!s.startsWith('#')) { - throw ("Color code not supported"); + throw ('Color code not supported'); } String hex = s.replaceFirst('#', ''); diff --git a/lib/utils/universal_ui/fake_ui.dart b/lib/utils/universal_ui/fake_ui.dart index 3c59b5d2..1711ad5f 100644 --- a/lib/utils/universal_ui/fake_ui.dart +++ b/lib/utils/universal_ui/fake_ui.dart @@ -1,3 +1,3 @@ class PlatformViewRegistry { - static registerViewFactory(String viewId, dynamic cb) {} + static void registerViewFactory(String viewId, dynamic cb) {} } diff --git a/lib/utils/universal_ui/real_ui.dart b/lib/utils/universal_ui/real_ui.dart index f699c9c0..b701caf4 100644 --- a/lib/utils/universal_ui/real_ui.dart +++ b/lib/utils/universal_ui/real_ui.dart @@ -1,7 +1,7 @@ import 'dart:ui' as ui; class PlatformViewRegistry { - static registerViewFactory(String viewId, dynamic cb) { + static void registerViewFactory(String viewId, dynamic cb) { ui.platformViewRegistry.registerViewFactory(viewId, cb); } } diff --git a/lib/utils/universal_ui/universal_ui.dart b/lib/utils/universal_ui/universal_ui.dart index 307520c5..5fe7c428 100644 --- a/lib/utils/universal_ui/universal_ui.dart +++ b/lib/utils/universal_ui/universal_ui.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; class PlatformViewRegistryFix { - registerViewFactory(dynamic x, dynamic y) { + void registerViewFactory(dynamic x, dynamic y) { if (kIsWeb) { ui_instance.PlatformViewRegistry.registerViewFactory( x, diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 2ea56e32..665211a9 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -54,7 +54,7 @@ class QuillController extends ChangeNotifier { // updateSelection( // TextSelection.collapsed(offset: document.length), ChangeSource.LOCAL); updateSelection( - TextSelection.collapsed(offset: this.selection.baseOffset + len!), + TextSelection.collapsed(offset: selection.baseOffset + len!), ChangeSource.LOCAL); } else { // no need to move cursor @@ -73,7 +73,7 @@ class QuillController extends ChangeNotifier { get hasRedo => document.hasRedo; - replaceText(int index, int len, Object? data, TextSelection? textSelection) { + void replaceText(int index, int len, Object? data, TextSelection? textSelection) { assert(data is String || data is Embeddable); Delta? delta; @@ -82,7 +82,7 @@ class QuillController extends ChangeNotifier { delta = document.replace(index, len, data); } catch (e) { print('document.replace failed: $e'); - throw e; + rethrow; } bool shouldRetainDelta = toggledStyle.isNotEmpty && delta.isNotEmpty && @@ -127,14 +127,14 @@ class QuillController extends ChangeNotifier { ); } catch (e) { print('getPositionDelta or getPositionDelta error: $e'); - throw e; + rethrow; } } } notifyListeners(); } - formatText(int index, int len, Attribute? attribute) { + void formatText(int index, int len, Attribute? attribute) { if (len == 0 && attribute!.isInline && attribute.key != Attribute.link.key) { @@ -151,16 +151,16 @@ class QuillController extends ChangeNotifier { notifyListeners(); } - formatSelection(Attribute? attribute) { + void formatSelection(Attribute? attribute) { formatText(selection.start, selection.end - selection.start, attribute); } - updateSelection(TextSelection textSelection, ChangeSource source) { + void updateSelection(TextSelection textSelection, ChangeSource source) { _updateSelection(textSelection, source); notifyListeners(); } - compose(Delta delta, TextSelection textSelection, ChangeSource source) { + void compose(Delta delta, TextSelection textSelection, ChangeSource source) { if (delta.isNotEmpty) { document.compose(delta, source); } @@ -182,7 +182,7 @@ class QuillController extends ChangeNotifier { super.dispose(); } - _updateSelection(TextSelection textSelection, ChangeSource source) { + void _updateSelection(TextSelection textSelection, ChangeSource source) { selection = textSelection; int end = document.length - 1; selection = selection.copyWith( diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index d6305569..8fa45437 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -89,7 +89,7 @@ class CursorCont extends ChangeNotifier { } @override - dispose() { + void dispose() { _blinkOpacityCont.removeListener(_onColorTick); stopCursorTimer(); _blinkOpacityCont.dispose(); @@ -97,7 +97,7 @@ class CursorCont extends ChangeNotifier { super.dispose(); } - _cursorTick(Timer timer) { + void _cursorTick(Timer timer) { _targetCursorVisibility = !_targetCursorVisibility; double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; if (style.opacityAnimates) { @@ -107,7 +107,7 @@ class CursorCont extends ChangeNotifier { } } - _cursorWaitForStart(Timer timer) { + void _cursorWaitForStart(Timer timer) { _cursorTimer?.cancel(); _cursorTimer = Timer.periodic(Duration(milliseconds: 500), _cursorTick); } @@ -124,7 +124,7 @@ class CursorCont extends ChangeNotifier { } } - stopCursorTimer({bool resetCharTicks = true}) { + void stopCursorTimer({bool resetCharTicks = true}) { _cursorTimer?.cancel(); _cursorTimer = null; _targetCursorVisibility = false; @@ -136,7 +136,7 @@ class CursorCont extends ChangeNotifier { } } - startOrStopCursorTimerIfNeeded(bool hasFocus, TextSelection selection) { + void startOrStopCursorTimerIfNeeded(bool hasFocus, TextSelection selection) { if (show.value && _cursorTimer == null && hasFocus && @@ -147,7 +147,7 @@ class CursorCont extends ChangeNotifier { } } - _onColorTick() { + void _onColorTick() { color.value = _style.color.withOpacity(_blinkOpacityCont.value); _blink.value = show.value && _blinkOpacityCont.value > 0; } @@ -163,7 +163,7 @@ class CursorPainter { CursorPainter(this.editable, this.style, this.prototype, this.color, this.devicePixelRatio); - paint(Canvas canvas, Offset offset, TextPosition position) { + void paint(Canvas canvas, Offset offset, TextPosition position) { assert(prototype != null); Offset caretOffset = diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 7beb9f5c..bbe8db10 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -188,23 +188,23 @@ class DefaultStyles { DefaultStyles merge(DefaultStyles other) { return DefaultStyles( - h1: other.h1 ?? this.h1, - h2: other.h2 ?? this.h2, - h3: other.h3 ?? this.h3, - paragraph: other.paragraph ?? this.paragraph, - bold: other.bold ?? this.bold, - italic: other.italic ?? this.italic, - underline: other.underline ?? this.underline, - strikeThrough: other.strikeThrough ?? this.strikeThrough, - link: other.link ?? this.link, - placeHolder: other.placeHolder ?? this.placeHolder, - lists: other.lists ?? this.lists, - quote: other.quote ?? this.quote, - code: other.code ?? this.code, - indent: other.indent ?? this.indent, - align: other.align ?? this.align, - sizeSmall: other.sizeSmall ?? this.sizeSmall, - sizeLarge: other.sizeLarge ?? this.sizeLarge, - sizeHuge: other.sizeHuge ?? this.sizeHuge); + h1: other.h1 ?? h1, + h2: other.h2 ?? h2, + h3: other.h3 ?? h3, + paragraph: other.paragraph ?? paragraph, + bold: other.bold ?? bold, + italic: other.italic ?? italic, + underline: other.underline ?? underline, + strikeThrough: other.strikeThrough ?? strikeThrough, + link: other.link ?? link, + placeHolder: other.placeHolder ?? placeHolder, + lists: other.lists ?? lists, + quote: other.quote ?? quote, + code: other.code ?? code, + indent: other.indent ?? indent, + align: other.align ?? align, + sizeSmall: other.sizeSmall ?? sizeSmall, + sizeLarge: other.sizeLarge ?? sizeLarge, + sizeHuge: other.sizeHuge ?? sizeHuge); } } diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index 3fa51fab..13cf999d 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -28,7 +28,7 @@ class EditorTextSelectionGestureDetectorBuilder { } RenderEditor? getRenderEditor() { - return this.getEditor()!.getRenderEditor(); + return getEditor()!.getRenderEditor(); } onTapDown(TapDownDetails details) { @@ -124,7 +124,7 @@ class EditorTextSelectionGestureDetectorBuilder { ); } - onDragSelectionEnd(DragEndDetails details) {} + void onDragSelectionEnd(DragEndDetails details) {} Widget build(HitTestBehavior behavior, Widget child) { return EditorTextSelectionGestureDetector( diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 53eef90b..c939d5fe 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -11,7 +11,7 @@ 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 containerNode; + 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'; @@ -94,8 +94,8 @@ abstract class RenderAbstractEditor { } String _standardizeImageUrl(String url) { - if (url.contains("base64")) { - return url.split(",")[1]; + if (url.contains('base64')) { + return url.split(',')[1]; } return url; } @@ -211,7 +211,7 @@ class QuillEditor extends StatefulWidget { class _QuillEditorState extends State implements EditorTextSelectionGestureDetectorBuilderDelegate { - GlobalKey _editorKey = GlobalKey(); + final GlobalKey _editorKey = GlobalKey(); late EditorTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; @@ -382,13 +382,13 @@ class _QuillEditorSelectionGestureDetectorBuilder } TextPosition pos = getRenderEditor()!.getPositionForOffset(details.globalPosition); - containerNode.ChildQuery result = + container_node.ChildQuery result = getEditor()!.widget.controller.document.queryChild(pos.offset); if (result.node == null) { return false; } Line line = result.node as Line; - containerNode.ChildQuery segmentResult = + container_node.ChildQuery segmentResult = line.queryChild(result.offset, false); if (segmentResult.node == null) { if (line.length == 1) { @@ -403,9 +403,7 @@ class _QuillEditorSelectionGestureDetectorBuilder leaf.Leaf segment = segmentResult.node as leaf.Leaf; if (segment.style.containsKey(Attribute.link.key)) { var launchUrl = getEditor()!.widget.onLaunchUrl; - if (launchUrl == null) { - launchUrl = _launchUrl; - } + launchUrl ??= _launchUrl; String? link = segment.style.attributes[Attribute.link.key]!.value; if (getEditor()!.widget.readOnly && link != null) { link = link.trim(); @@ -444,7 +442,7 @@ class _QuillEditorSelectionGestureDetectorBuilder } bool _flipListCheckbox( - TextPosition pos, Line line, containerNode.ChildQuery segmentResult) { + TextPosition pos, Line line, container_node.ChildQuery segmentResult) { if (getEditor()!.widget.readOnly || !line.style.containsKey(Attribute.list.key) || segmentResult.offset != 0) { @@ -473,7 +471,7 @@ class _QuillEditorSelectionGestureDetectorBuilder } @override - onSingleTapUp(TapUpDetails details) { + void onSingleTapUp(TapUpDetails details) { getEditor()!.hideToolbar(); bool positionSelected = _onTapping(details); @@ -569,7 +567,7 @@ class RenderEditor extends RenderEditableContainerBox padding, ); - setDocument(Document doc) { + void setDocument(Document doc) { if (document == doc) { return; } @@ -577,7 +575,7 @@ class RenderEditor extends RenderEditableContainerBox markNeedsLayout(); } - setHasFocus(bool h) { + void setHasFocus(bool h) { if (_hasFocus == h) { return; } @@ -585,7 +583,7 @@ class RenderEditor extends RenderEditableContainerBox markNeedsSemanticsUpdate(); } - setSelection(TextSelection t) { + void setSelection(TextSelection t) { if (selection == t) { return; } @@ -593,7 +591,7 @@ class RenderEditor extends RenderEditableContainerBox markNeedsPaint(); } - setStartHandleLayerLink(LayerLink value) { + void setStartHandleLayerLink(LayerLink value) { if (_startHandleLayerLink == value) { return; } @@ -601,7 +599,7 @@ class RenderEditor extends RenderEditableContainerBox markNeedsPaint(); } - setEndHandleLayerLink(LayerLink value) { + void setEndHandleLayerLink(LayerLink value) { if (_endHandleLayerLink == value) { return; } @@ -671,12 +669,12 @@ class RenderEditor extends RenderEditableContainerBox Offset? _lastTapDownPosition; @override - handleTapDown(TapDownDetails details) { + void handleTapDown(TapDownDetails details) { _lastTapDownPosition = details.globalPosition; } @override - selectWordsInRange( + void selectWordsInRange( Offset from, Offset? to, SelectionChangedCause cause, @@ -696,7 +694,7 @@ class RenderEditor extends RenderEditableContainerBox ); } - _handleSelectionChange( + void _handleSelectionChange( TextSelection nextSelection, SelectionChangedCause cause, ) { @@ -712,7 +710,7 @@ class RenderEditor extends RenderEditableContainerBox } @override - selectWordEdge(SelectionChangedCause cause) { + void selectWordEdge(SelectionChangedCause cause) { assert(_lastTapDownPosition != null); TextPosition position = getPositionForOffset(_lastTapDownPosition!); RenderEditableBox child = childAtPosition(position); @@ -742,7 +740,7 @@ class RenderEditor extends RenderEditableContainerBox } @override - selectPositionAt( + void selectPositionAt( Offset from, Offset? to, SelectionChangedCause cause, @@ -766,12 +764,12 @@ class RenderEditor extends RenderEditableContainerBox } @override - selectWord(SelectionChangedCause cause) { + void selectWord(SelectionChangedCause cause) { selectWordsInRange(_lastTapDownPosition!, null, cause); } @override - selectPosition(SelectionChangedCause cause) { + void selectPosition(SelectionChangedCause cause) { selectPositionAt(_lastTapDownPosition!, null, cause); } @@ -821,7 +819,7 @@ class RenderEditor extends RenderEditableContainerBox return defaultHitTestChildren(result, position: position); } - _paintHandleLayers( + void _paintHandleLayers( PaintingContext context, List endpoints) { var startPoint = endpoints[0].point; startPoint = Offset( @@ -904,7 +902,7 @@ class RenderEditableContainerBox extends RenderBox EditableContainerParentData>, RenderBoxContainerDefaultsMixin { - containerNode.Container _container; + container_node.Container _container; TextDirection textDirection; EdgeInsetsGeometry _padding; EdgeInsets? _resolvedPadding; @@ -915,11 +913,11 @@ class RenderEditableContainerBox extends RenderBox addAll(children); } - containerNode.Container getContainer() { + container_node.Container getContainer() { return _container; } - setContainer(containerNode.Container c) { + void setContainer(container_node.Container c) { if (_container == c) { return; } @@ -929,7 +927,7 @@ class RenderEditableContainerBox extends RenderBox EdgeInsetsGeometry getPadding() => _padding; - setPadding(EdgeInsetsGeometry value) { + void setPadding(EdgeInsetsGeometry value) { assert(value.isNonNegative); if (_padding == value) { return; @@ -940,7 +938,7 @@ class RenderEditableContainerBox extends RenderBox EdgeInsets? get resolvedPadding => _resolvedPadding; - _resolvePadding() { + void _resolvePadding() { if (_resolvedPadding != null) { return; } @@ -968,7 +966,7 @@ class RenderEditableContainerBox extends RenderBox return targetChild; } - _markNeedsPaddingResolution() { + void _markNeedsPaddingResolution() { _resolvedPadding = null; markNeedsLayout(); } @@ -997,7 +995,7 @@ class RenderEditableContainerBox extends RenderBox } @override - setupParentData(RenderBox child) { + void setupParentData(RenderBox child) { if (child.parentData is EditableContainerParentData) { return; } diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index 9f2e0bb3..f3e38c4e 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -66,7 +66,7 @@ class RenderBaselineProxy extends RenderProxyBox { // SEE What happens + _padding?.top; @override - performLayout() { + void performLayout() { super.performLayout(); _prototypePainter.layout(); } @@ -289,7 +289,7 @@ class RenderParagraphProxy extends RenderProxyBox child!.getBoxesForSelection(selection); @override - performLayout() { + void performLayout() { super.performLayout(); _prototypePainter.layout( minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 1b9f4ad0..04b4aaf8 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -100,7 +100,7 @@ class RawEditorState extends EditorState WidgetsBindingObserver, TickerProviderStateMixin implements TextSelectionDelegate, TextInputClient { - GlobalKey _editorKey = GlobalKey(); + final GlobalKey _editorKey = GlobalKey(); final List _sentRemoteValues = []; TextInputConnection? _textInputConnection; TextEditingValue? _lastKnownRemoteTextEditingValue; @@ -144,7 +144,7 @@ class RawEditorState extends EditorState return result; } - handleCursorMovement( + void handleCursorMovement( LogicalKeyboardKey key, bool wordModifier, bool lineModifier, @@ -370,7 +370,7 @@ class RawEditorState extends EditorState bool get hasConnection => _textInputConnection != null && _textInputConnection!.attached; - openConnectionIfNeeded() { + void openConnectionIfNeeded() { if (!shouldCreateInputConnection) { return; } @@ -396,7 +396,7 @@ class RawEditorState extends EditorState _textInputConnection!.show(); } - closeConnectionIfNeeded() { + void closeConnectionIfNeeded() { if (!hasConnection) { return; } @@ -406,7 +406,7 @@ class RawEditorState extends EditorState _sentRemoteValues.clear(); } - updateRemoteValueIfNeeded() { + void updateRemoteValueIfNeeded() { if (!hasConnection) { return; } @@ -526,7 +526,6 @@ class RawEditorState extends EditorState child: Semantics( child: _Editor( key: _editorKey, - children: _buildChildren(_doc, context), document: _doc, selection: widget.controller.selection, hasFocus: _hasFocus, @@ -535,6 +534,7 @@ class RawEditorState extends EditorState endHandleLayerLink: _endHandleLayerLink, onSelectionChanged: _handleSelectionChanged, padding: widget.padding, + children: _buildChildren(_doc, context), ), ), ); @@ -571,7 +571,7 @@ class RawEditorState extends EditorState ); } - _handleSelectionChanged( + void _handleSelectionChanged( TextSelection selection, SelectionChangedCause cause) { widget.controller.updateSelection(selection, ChangeSource.LOCAL); @@ -582,7 +582,7 @@ class RawEditorState extends EditorState } } - _buildChildren(Document doc, BuildContext context) { + List _buildChildren(Document doc, BuildContext context) { final result = []; Map indentLevelCounts = {}; for (Node node in doc.root.children) { @@ -717,7 +717,7 @@ class RawEditorState extends EditorState } @override - didChangeDependencies() { + void didChangeDependencies() { super.didChangeDependencies(); DefaultStyles? parentStyles = QuillStyles.getStyles(context, true); DefaultStyles defaultStyles = DefaultStyles.getInstance(context); @@ -782,7 +782,7 @@ class RawEditorState extends EditorState !widget.controller.selection.isCollapsed; } - handleDelete(bool forward) { + void handleDelete(bool forward) { TextSelection selection = widget.controller.selection; String plainText = textEditingValue.text; int cursorPosition = selection.start; @@ -817,14 +817,14 @@ class RawEditorState extends EditorState String plainText = textEditingValue.text; if (shortcut == InputShortcut.COPY) { if (!selection.isCollapsed) { - Clipboard.setData(ClipboardData(text: selection.textInside(plainText))); + await Clipboard.setData(ClipboardData(text: selection.textInside(plainText))); } return; } if (shortcut == InputShortcut.CUT && !widget.readOnly) { if (!selection.isCollapsed) { final data = selection.textInside(plainText); - Clipboard.setData(ClipboardData(text: data)); + await Clipboard.setData(ClipboardData(text: data)); widget.controller.replaceText( selection.start, @@ -881,11 +881,11 @@ class RawEditorState extends EditorState super.dispose(); } - _updateSelectionOverlayForScroll() { + void _updateSelectionOverlayForScroll() { _selectionOverlay?.markNeedsBuild(); } - _didChangeTextEditingValue() { + void _didChangeTextEditingValue() { if (kIsWeb) { _onChangeTextEditingValue(); requestKeyboard(); @@ -899,7 +899,7 @@ class RawEditorState extends EditorState } } - _onChangeTextEditingValue() { + void _onChangeTextEditingValue() { _showCaretOnScreen(); updateRemoteValueIfNeeded(); _cursorCont.startOrStopCursorTimerIfNeeded( @@ -918,7 +918,7 @@ class RawEditorState extends EditorState }); } - _updateOrDisposeSelectionOverlayIfNeeded() { + void _updateOrDisposeSelectionOverlayIfNeeded() { if (_selectionOverlay != null) { if (_hasFocus) { _selectionOverlay!.update(textEditingValue); @@ -950,7 +950,7 @@ class RawEditorState extends EditorState } } - _handleFocusChanged() { + void _handleFocusChanged() { openOrCloseConnection(); _cursorCont.startOrStopCursorTimerIfNeeded( _hasFocus, widget.controller.selection); @@ -964,7 +964,7 @@ class RawEditorState extends EditorState updateKeepAlive(); } - _onChangedClipboardStatus() { + void _onChangedClipboardStatus() { if (!mounted) return; setState(() { // Inform the widget that the value of clipboardStatus has changed. @@ -974,7 +974,7 @@ class RawEditorState extends EditorState bool _showCaretOnScreenScheduled = false; - _showCaretOnScreen() { + void _showCaretOnScreen() { if (!widget.showCursor || _showCaretOnScreenScheduled) { return; } @@ -1039,7 +1039,7 @@ class RawEditorState extends EditorState bool get selectAllEnabled => widget.toolbarOptions.selectAll; @override - requestKeyboard() { + void requestKeyboard() { if (_hasFocus) { openConnectionIfNeeded(); } else { @@ -1048,7 +1048,7 @@ class RawEditorState extends EditorState } @override - setTextEditingValue(TextEditingValue value) { + void setTextEditingValue(TextEditingValue value) { if (value.text == textEditingValue.text) { widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); } else { @@ -1116,7 +1116,7 @@ class RawEditorState extends EditorState @override bool get wantKeepAlive => widget.focusNode.hasFocus; - openOrCloseConnection() { + void openOrCloseConnection() { if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { openConnectionIfNeeded(); } else if (!widget.focusNode.hasFocus) { @@ -1164,7 +1164,7 @@ class _Editor extends MultiChildRenderObjectWidget { } @override - updateRenderObject( + void updateRenderObject( BuildContext context, covariant RenderEditor renderObject) { renderObject.document = document; renderObject.setContainer(document.root); diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index ee71e6e5..4d2a5cc3 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -32,19 +32,19 @@ const List arabianRomanNumbers = [ ]; const List romanNumbers = [ - "M", - "CM", - "D", - "CD", - "C", - "XC", - "L", - "XL", - "X", - "IX", - "V", - "IV", - "I" + 'M', + 'CM', + 'D', + 'CD', + 'C', + 'XC', + 'L', + 'XL', + 'X', + 'IX', + 'V', + 'IV', + 'I' ]; class EditableTextBlock extends StatelessWidget { @@ -86,7 +86,7 @@ class EditableTextBlock extends StatelessWidget { verticalSpacing as Tuple2, _getDecorationForBlock(block, defaultStyles) ?? BoxDecoration(), contentPadding, - _buildChildren(context, this.indentLevelCounts)); + _buildChildren(context, indentLevelCounts)); } BoxDecoration? _getDecorationForBlock( @@ -281,7 +281,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } @override - setPadding(EdgeInsetsGeometry value) { + void setPadding(EdgeInsetsGeometry value) { super.setPadding(value.add(_contentPadding)); _savedPadding = value; } @@ -478,12 +478,12 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } @override - paint(PaintingContext context, Offset offset) { + void paint(PaintingContext context, Offset offset) { _paintDecoration(context, offset); defaultPaint(context, offset); } - _paintDecoration(PaintingContext context, Offset offset) { + void _paintDecoration(PaintingContext context, Offset offset) { _painter ??= _decoration.createBoxPainter(markNeedsPaint); EdgeInsets decorationPadding = resolvedPadding! - _contentPadding; @@ -571,31 +571,31 @@ class _NumberPoint extends StatelessWidget { @override Widget build(BuildContext context) { - String s = this.index.toString(); + String s = index.toString(); int? level = 0; - if (!this.attrs.containsKey(Attribute.indent.key) && - !this.indentLevelCounts.containsKey(1)) { - this.indentLevelCounts.clear(); + if (!attrs.containsKey(Attribute.indent.key) && + !indentLevelCounts.containsKey(1)) { + indentLevelCounts.clear(); return Container( alignment: AlignmentDirectional.topEnd, - child: Text(withDot ? '$s.' : '$s', style: style), width: width, padding: EdgeInsetsDirectional.only(end: padding), + child: Text(withDot ? '$s.' : '$s', style: style), ); } - if (this.attrs.containsKey(Attribute.indent.key)) { - level = this.attrs[Attribute.indent.key]!.value; + if (attrs.containsKey(Attribute.indent.key)) { + level = attrs[Attribute.indent.key]!.value; } else { // first level but is back from previous indent level // supposed to be "2." - this.indentLevelCounts[0] = 1; + indentLevelCounts[0] = 1; } - if (this.indentLevelCounts.containsKey(level! + 1)) { + if (indentLevelCounts.containsKey(level! + 1)) { // last visited level is done, going up - this.indentLevelCounts.remove(level + 1); + indentLevelCounts.remove(level + 1); } - int count = (this.indentLevelCounts[level] ?? 0) + 1; - this.indentLevelCounts[level] = count; + int count = (indentLevelCounts[level] ?? 0) + 1; + indentLevelCounts[level] = count; s = count.toString(); if (level % 3 == 1) { @@ -609,9 +609,9 @@ class _NumberPoint extends StatelessWidget { return Container( alignment: AlignmentDirectional.topEnd, - child: Text(withDot ? '$s.' : '$s', style: style), width: width, padding: EdgeInsetsDirectional.only(end: padding), + child: Text(withDot ? '$s.' : '$s', style: style), ); } @@ -630,9 +630,9 @@ class _NumberPoint extends StatelessWidget { var num = input; if (num < 0) { - return ""; + return ''; } else if (num == 0) { - return "nulla"; + return 'nulla'; } final builder = StringBuffer(); @@ -665,9 +665,9 @@ class _BulletPoint extends StatelessWidget { Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.topEnd, - child: Text('•', style: style), width: width, padding: EdgeInsetsDirectional.only(end: 13.0), + child: Text('•', style: style), ); } } @@ -706,12 +706,12 @@ class __CheckboxState extends State<_Checkbox> { Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.topEnd, + width: widget.width, + padding: EdgeInsetsDirectional.only(end: 13.0), child: Checkbox( value: widget.isChecked, onChanged: _onCheckboxClicked, ), - width: widget.width, - padding: EdgeInsetsDirectional.only(end: 13.0), ); } } diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index e7f0975c..0da1e64f 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -158,7 +158,7 @@ class TextLine extends StatelessWidget { if (fontSize != null) { res = res.merge(TextStyle(fontSize: fontSize)); } else { - throw "Invalid size ${size.value}"; + throw 'Invalid size ${size.value}'; } } } @@ -166,13 +166,13 @@ class TextLine extends StatelessWidget { Attribute? color = textNode.style.attributes[Attribute.color.key]; if (color != null && color.value != null) { final textColor = stringToColor(color.value); - res = res.merge(new TextStyle(color: textColor)); + res = res.merge(TextStyle(color: textColor)); } Attribute? background = textNode.style.attributes[Attribute.background.key]; if (background != null && background.value != null) { final backgroundColor = stringToColor(background.value); - res = res.merge(new TextStyle(backgroundColor: backgroundColor)); + res = res.merge(TextStyle(backgroundColor: backgroundColor)); } return TextSpan(text: textNode.value, style: res); @@ -235,12 +235,12 @@ class EditableTextLine extends RenderObjectWidget { hasFocus, devicePixelRatio, _getPadding(), - this.color, + color, cursorCont); } @override - updateRenderObject( + void updateRenderObject( BuildContext context, covariant RenderEditableTextLine renderObject) { renderObject.setLine(line); renderObject.setPadding(_getPadding()); @@ -301,7 +301,7 @@ class RenderEditableTextLine extends RenderEditableBox { } } - setCursorCont(CursorCont c) { + void setCursorCont(CursorCont c) { if (cursorCont == c) { return; } @@ -309,7 +309,7 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsLayout(); } - setDevicePixelRatio(double d) { + void setDevicePixelRatio(double d) { if (devicePixelRatio == d) { return; } @@ -317,7 +317,7 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsLayout(); } - setEnableInteractiveSelection(bool val) { + void setEnableInteractiveSelection(bool val) { if (enableInteractiveSelection == val) { return; } @@ -326,7 +326,7 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsSemanticsUpdate(); } - setColor(Color c) { + void setColor(Color c) { if (color == c) { return; } @@ -337,7 +337,7 @@ class RenderEditableTextLine extends RenderEditableBox { } } - setTextSelection(TextSelection t) { + void setTextSelection(TextSelection t) { if (textSelection == t) { return; } @@ -361,7 +361,7 @@ class RenderEditableTextLine extends RenderEditableBox { } } - setTextDirection(TextDirection t) { + void setTextDirection(TextDirection t) { if (textDirection == t) { return; } @@ -370,7 +370,7 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsLayout(); } - setLine(Line l) { + void setLine(Line l) { if (line == l) { return; } @@ -379,7 +379,7 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsLayout(); } - setPadding(EdgeInsetsGeometry p) { + void setPadding(EdgeInsetsGeometry p) { assert(p.isNonNegative); if (padding == p) { return; @@ -389,11 +389,11 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsLayout(); } - setLeading(RenderBox? l) { + void setLeading(RenderBox? l) { _leading = _updateChild(_leading, l, TextLineSlot.LEADING); } - setBody(RenderContentProxyBox? b) { + void setBody(RenderContentProxyBox? b) { _body = _updateChild(_body, b, TextLineSlot.BODY) as RenderContentProxyBox?; } @@ -433,7 +433,7 @@ class RenderEditableTextLine extends RenderEditableBox { }).toList(growable: false); } - _resolvePadding() { + void _resolvePadding() { if (_resolvedPadding != null) { return; } @@ -536,7 +536,7 @@ class RenderEditableTextLine extends RenderEditableBox { double get cursorHeight => cursorCont.style.height ?? preferredLineHeight(TextPosition(offset: 0)); - _computeCaretPrototype() { + void _computeCaretPrototype() { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: @@ -556,7 +556,7 @@ class RenderEditableTextLine extends RenderEditableBox { } @override - attach(covariant PipelineOwner owner) { + void attach(covariant PipelineOwner owner) { super.attach(owner); for (final child in _children) { child.attach(owner); @@ -568,7 +568,7 @@ class RenderEditableTextLine extends RenderEditableBox { } @override - detach() { + void detach() { super.detach(); for (RenderBox child in _children) { child.detach(); @@ -580,12 +580,12 @@ class RenderEditableTextLine extends RenderEditableBox { } @override - redepthChildren() { + void redepthChildren() { _children.forEach(redepthChild); } @override - visitChildren(RenderObjectVisitor visitor) { + void visitChildren(RenderObjectVisitor visitor) { _children.forEach(visitor); } @@ -722,7 +722,7 @@ class RenderEditableTextLine extends RenderEditableBox { ); @override - paint(PaintingContext context, Offset offset) { + void paint(PaintingContext context, Offset offset) { if (_leading != null) { final parentData = _leading!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; @@ -760,7 +760,7 @@ class RenderEditableTextLine extends RenderEditableBox { } } - _paintSelection(PaintingContext context, Offset effectiveOffset) { + void _paintSelection(PaintingContext context, Offset effectiveOffset) { assert(_selectedRects != null); final paint = Paint()..color = color; for (final box in _selectedRects!) { @@ -768,7 +768,7 @@ class RenderEditableTextLine extends RenderEditableBox { } } - _paintCursor(PaintingContext context, Offset effectiveOffset) { + void _paintCursor(PaintingContext context, Offset effectiveOffset) { final position = TextPosition( offset: textSelection.extentOffset - line.getDocumentOffset(), affinity: textSelection.base.affinity, @@ -778,7 +778,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - return this._children.first.hitTest(result, position: position); + return _children.first.hitTest(result, position: position); } } @@ -795,12 +795,12 @@ class _TextLineElement extends RenderObjectElement { super.renderObject as RenderEditableTextLine; @override - visitChildren(ElementVisitor visitor) { + void visitChildren(ElementVisitor visitor) { _slotToChildren.values.forEach(visitor); } @override - forgetChild(Element child) { + void forgetChild(Element child) { assert(_slotToChildren.containsValue(child)); assert(child.slot is TextLineSlot); assert(_slotToChildren.containsKey(child.slot)); @@ -809,14 +809,14 @@ class _TextLineElement extends RenderObjectElement { } @override - mount(Element? parent, dynamic newSlot) { + void mount(Element? parent, dynamic newSlot) { super.mount(parent, newSlot); _mountChild(widget.leading, TextLineSlot.LEADING); _mountChild(widget.body, TextLineSlot.BODY); } @override - update(EditableTextLine newWidget) { + void update(EditableTextLine newWidget) { super.update(newWidget); assert(widget == newWidget); _updateChild(widget.leading, TextLineSlot.LEADING); @@ -824,14 +824,14 @@ class _TextLineElement extends RenderObjectElement { } @override - insertRenderObjectChild(RenderBox child, TextLineSlot? slot) { + void insertRenderObjectChild(RenderBox child, TextLineSlot? slot) { // assert(child is RenderBox); _updateRenderObject(child, slot); assert(renderObject.children.keys.contains(slot)); } @override - removeRenderObjectChild(RenderObject child, TextLineSlot? slot) { + void removeRenderObjectChild(RenderObject child, TextLineSlot? slot) { assert(child is RenderBox); assert(renderObject.children[slot!] == child); _updateRenderObject(null, slot); @@ -839,7 +839,7 @@ class _TextLineElement extends RenderObjectElement { } @override - moveRenderObjectChild(RenderObject child, dynamic oldSlot, dynamic newSlot) { + void moveRenderObjectChild(RenderObject child, dynamic oldSlot, dynamic newSlot) { throw UnimplementedError(); } diff --git a/pubspec.yaml b/pubspec.yaml index 7fb8925c..4c1e457f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_quill -description: Rich text editor (Demo App https://bulletjournal.us/home/index.html ). +description: A rich text editor. version: 1.1.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html From 6722759441e995793d821bff3c448939459a783f Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 25 Mar 2021 00:12:50 -0700 Subject: [PATCH 091/306] Clear warnings --- analysis_options.yaml | 4 +++- lib/models/documents/document.dart | 4 ++-- lib/models/documents/history.dart | 4 ++-- lib/widgets/controller.dart | 4 ++-- lib/widgets/delegate.dart | 22 +++++++++++----------- lib/widgets/editor.dart | 6 +++--- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 98c97294..4679e597 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,4 +2,6 @@ include: package:pedantic/analysis_options.yaml analyzer: errors: - undefined_prefixed_name: ignore \ No newline at end of file + undefined_prefixed_name: ignore + omit_local_variable_types: ignore + unsafe_html: ignore \ No newline at end of file diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 2e715f3f..d52e89a4 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -163,9 +163,9 @@ class Document { return _history.redo(this); } - get hasUndo => _history.hasUndo; + bool get hasUndo => _history.hasUndo; - get hasRedo => _history.hasRedo; + bool get hasRedo => _history.hasRedo; static Delta _transform(Delta delta) { Delta res = Delta(); diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index 99798d71..32e780c9 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -6,9 +6,9 @@ import 'document.dart'; class History { final HistoryStack stack = HistoryStack.empty(); - get hasUndo => stack.undo.isNotEmpty; + bool get hasUndo => stack.undo.isNotEmpty; - get hasRedo => stack.redo.isNotEmpty; + bool get hasRedo => stack.redo.isNotEmpty; /// used for disable redo or undo function bool ignoreChange; diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 665211a9..d36a73b5 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -69,9 +69,9 @@ class QuillController extends ChangeNotifier { } } - get hasUndo => document.hasUndo; + bool get hasUndo => document.hasUndo; - get hasRedo => document.hasRedo; + bool get hasRedo => document.hasRedo; void replaceText(int index, int len, Object? data, TextSelection? textSelection) { assert(data is String || data is Embeddable); diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index 13cf999d..3cfb9cd6 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -31,7 +31,7 @@ class EditorTextSelectionGestureDetectorBuilder { return getEditor()!.getRenderEditor(); } - onTapDown(TapDownDetails details) { + void onTapDown(TapDownDetails details) { getRenderEditor()!.handleTapDown(details); PointerDeviceKind? kind = details.kind; @@ -40,7 +40,7 @@ class EditorTextSelectionGestureDetectorBuilder { kind == PointerDeviceKind.stylus; } - onForcePressStart(ForcePressDetails details) { + void onForcePressStart(ForcePressDetails details) { assert(delegate.getForcePressEnabled()); shouldShowSelectionToolbar = true; if (delegate.getSelectionEnabled()) { @@ -52,7 +52,7 @@ class EditorTextSelectionGestureDetectorBuilder { } } - onForcePressEnd(ForcePressDetails details) { + void onForcePressEnd(ForcePressDetails details) { assert(delegate.getForcePressEnabled()); getRenderEditor()!.selectWordsInRange( details.globalPosition, @@ -64,15 +64,15 @@ class EditorTextSelectionGestureDetectorBuilder { } } - onSingleTapUp(TapUpDetails details) { + void onSingleTapUp(TapUpDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); } } - onSingleTapCancel() {} + void onSingleTapCancel() {} - onSingleLongTapStart(LongPressStartDetails details) { + void onSingleLongTapStart(LongPressStartDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectPositionAt( details.globalPosition, @@ -82,7 +82,7 @@ class EditorTextSelectionGestureDetectorBuilder { } } - onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { + void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectPositionAt( details.globalPosition, @@ -92,13 +92,13 @@ class EditorTextSelectionGestureDetectorBuilder { } } - onSingleLongTapEnd(LongPressEndDetails details) { + void onSingleLongTapEnd(LongPressEndDetails details) { if (shouldShowSelectionToolbar) { getEditor()!.showToolbar(); } } - onDoubleTapDown(TapDownDetails details) { + void onDoubleTapDown(TapDownDetails details) { if (delegate.getSelectionEnabled()) { getRenderEditor()!.selectWord(SelectionChangedCause.tap); if (shouldShowSelectionToolbar) { @@ -107,7 +107,7 @@ class EditorTextSelectionGestureDetectorBuilder { } } - onDragSelectionStart(DragStartDetails details) { + void onDragSelectionStart(DragStartDetails details) { getRenderEditor()!.selectPositionAt( details.globalPosition, null, @@ -115,7 +115,7 @@ class EditorTextSelectionGestureDetectorBuilder { ); } - onDragSelectionUpdate( + void onDragSelectionUpdate( DragStartDetails startDetails, DragUpdateDetails updateDetails) { getRenderEditor()!.selectPositionAt( startDetails.globalPosition, diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index c939d5fe..305a58c8 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -325,7 +325,7 @@ class _QuillEditorState extends State return widget.enableInteractiveSelection; } - _requestKeyboard() { + void _requestKeyboard() { _editorKey.currentState!.requestKeyboard(); } } @@ -337,7 +337,7 @@ class _QuillEditorSelectionGestureDetectorBuilder _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); @override - onForcePressStart(ForcePressDetails details) { + void onForcePressStart(ForcePressDetails details) { super.onForcePressStart(details); if (delegate.getSelectionEnabled() && shouldShowSelectionToolbar) { getEditor()!.showToolbar(); @@ -345,7 +345,7 @@ class _QuillEditorSelectionGestureDetectorBuilder } @override - onForcePressEnd(ForcePressDetails details) {} + void onForcePressEnd(ForcePressDetails details) {} @override void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { From 997f3a212a0f929b8e02dcc3125f9416998c63b8 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 25 Mar 2021 00:23:44 -0700 Subject: [PATCH 092/306] Update description --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4c1e457f..3fe29f53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_quill -description: A rich text editor. +description: A rich text editor version: 1.1.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html From ceb191b26ba0f3e55326af255d9acd98fd2f8dc4 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Fri, 26 Mar 2021 14:39:03 +0100 Subject: [PATCH 093/306] Rename app folder to example (#109) --- {app => example}/.gitignore | 0 {app => example}/.metadata | 0 {app => example}/README.md | 0 {app => example}/android/.gitignore | 0 {app => example}/android/app/build.gradle | 0 .../android/app/src/debug/AndroidManifest.xml | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../java/com/example/app/MainActivity.java | 0 .../kotlin/com/example/app/MainActivity.kt | 0 .../res/drawable-v21/launch_background.xml | 0 .../main/res/drawable/launch_background.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values-night/styles.xml | 0 .../app/src/main/res/values/styles.xml | 0 .../app/src/profile/AndroidManifest.xml | 0 {app => example}/android/build.gradle | 0 {app => example}/android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 {app => example}/android/settings.gradle | 0 {app => example}/android/settings_aar.gradle | 0 {app => example}/assets/sample_data.json | 0 {app => example}/ios/.gitignore | 0 .../ios/Flutter/AppFrameworkInfo.plist | 0 {app => example}/ios/Flutter/Debug.xcconfig | 0 {app => example}/ios/Flutter/Release.xcconfig | 0 {app => example}/ios/Podfile | 0 .../ios/Runner.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 {app => example}/ios/Runner/AppDelegate.h | 0 {app => example}/ios/Runner/AppDelegate.m | 0 {app => example}/ios/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios/Runner/Base.lproj/Main.storyboard | 0 {app => example}/ios/Runner/Info.plist | 0 .../ios/Runner/Runner-Bridging-Header.h | 0 {app => example}/ios/Runner/main.m | 0 {app => example}/lib/main.dart | 0 {app => example}/lib/pages/home_page.dart | 0 .../lib/pages/read_only_page.dart | 0 .../lib/widgets/demo_scaffold.dart | 0 {app => example}/lib/widgets/field.dart | 0 {app => example}/linux/.gitignore | 0 {app => example}/linux/CMakeLists.txt | 0 {app => example}/linux/flutter/CMakeLists.txt | 0 .../flutter/generated_plugin_registrant.cc | 0 .../flutter/generated_plugin_registrant.h | 0 .../linux/flutter/generated_plugins.cmake | 0 {app => example}/linux/main.cc | 0 {app => example}/linux/my_application.cc | 0 {app => example}/linux/my_application.h | 0 {app => example}/macos/.gitignore | 0 .../macos/Flutter/Flutter-Debug.xcconfig | 0 .../macos/Flutter/Flutter-Release.xcconfig | 0 .../Flutter/GeneratedPluginRegistrant.swift | 0 {app => example}/macos/Podfile | 0 .../macos/Runner.xcodeproj/project.pbxproj | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../macos/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app_icon_1024.png | Bin .../AppIcon.appiconset/app_icon_128.png | Bin .../AppIcon.appiconset/app_icon_16.png | Bin .../AppIcon.appiconset/app_icon_256.png | Bin .../AppIcon.appiconset/app_icon_32.png | Bin .../AppIcon.appiconset/app_icon_512.png | Bin .../AppIcon.appiconset/app_icon_64.png | Bin .../macos/Runner/Base.lproj/MainMenu.xib | 0 .../macos/Runner/Configs/AppInfo.xcconfig | 0 .../macos/Runner/Configs/Debug.xcconfig | 0 .../macos/Runner/Configs/Release.xcconfig | 0 .../macos/Runner/Configs/Warnings.xcconfig | 0 .../macos/Runner/DebugProfile.entitlements | 0 {app => example}/macos/Runner/Info.plist | 0 .../macos/Runner/MainFlutterWindow.swift | 0 .../macos/Runner/Release.entitlements | 0 example/main.dart | 31 ------------------ {app => example}/pubspec.yaml | 0 {app => example}/test/widget_test.dart | 0 {app => example}/web/favicon.png | Bin {app => example}/web/icons/Icon-192.png | Bin {app => example}/web/icons/Icon-512.png | Bin {app => example}/web/index.html | 0 {app => example}/web/manifest.json | 0 {app => example}/windows/.gitignore | 0 {app => example}/windows/CMakeLists.txt | 0 .../windows/flutter/CMakeLists.txt | 0 .../flutter/generated_plugin_registrant.cc | 0 .../flutter/generated_plugin_registrant.h | 0 .../windows/flutter/generated_plugins.cmake | 0 .../windows/runner/CMakeLists.txt | 0 {app => example}/windows/runner/Runner.rc | 0 .../windows/runner/flutter_window.cpp | 0 .../windows/runner/flutter_window.h | 0 {app => example}/windows/runner/main.cpp | 0 {app => example}/windows/runner/resource.h | 0 .../windows/runner/resources/app_icon.ico | Bin {app => example}/windows/runner/run_loop.cpp | 0 {app => example}/windows/runner/run_loop.h | 0 .../windows/runner/runner.exe.manifest | 0 {app => example}/windows/runner/utils.cpp | 0 {app => example}/windows/runner/utils.h | 0 .../windows/runner/win32_window.cpp | 0 .../windows/runner/win32_window.h | 0 137 files changed, 31 deletions(-) rename {app => example}/.gitignore (100%) rename {app => example}/.metadata (100%) rename {app => example}/README.md (100%) rename {app => example}/android/.gitignore (100%) rename {app => example}/android/app/build.gradle (100%) rename {app => example}/android/app/src/debug/AndroidManifest.xml (100%) rename {app => example}/android/app/src/main/AndroidManifest.xml (100%) rename {app => example}/android/app/src/main/java/com/example/app/MainActivity.java (100%) rename {app => example}/android/app/src/main/kotlin/com/example/app/MainActivity.kt (100%) rename {app => example}/android/app/src/main/res/drawable-v21/launch_background.xml (100%) rename {app => example}/android/app/src/main/res/drawable/launch_background.xml (100%) rename {app => example}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {app => example}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {app => example}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {app => example}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {app => example}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {app => example}/android/app/src/main/res/values-night/styles.xml (100%) rename {app => example}/android/app/src/main/res/values/styles.xml (100%) rename {app => example}/android/app/src/profile/AndroidManifest.xml (100%) rename {app => example}/android/build.gradle (100%) rename {app => example}/android/gradle.properties (100%) rename {app => example}/android/gradle/wrapper/gradle-wrapper.properties (100%) rename {app => example}/android/settings.gradle (100%) rename {app => example}/android/settings_aar.gradle (100%) rename {app => example}/assets/sample_data.json (100%) rename {app => example}/ios/.gitignore (100%) rename {app => example}/ios/Flutter/AppFrameworkInfo.plist (100%) rename {app => example}/ios/Flutter/Debug.xcconfig (100%) rename {app => example}/ios/Flutter/Release.xcconfig (100%) rename {app => example}/ios/Podfile (100%) rename {app => example}/ios/Runner.xcodeproj/project.pbxproj (100%) rename {app => example}/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {app => example}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {app => example}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {app => example}/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {app => example}/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {app => example}/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {app => example}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {app => example}/ios/Runner/AppDelegate.h (100%) rename {app => example}/ios/Runner/AppDelegate.m (100%) rename {app => example}/ios/Runner/AppDelegate.swift (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename {app => example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename {app => example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename {app => example}/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename {app => example}/ios/Runner/Base.lproj/Main.storyboard (100%) rename {app => example}/ios/Runner/Info.plist (100%) rename {app => example}/ios/Runner/Runner-Bridging-Header.h (100%) rename {app => example}/ios/Runner/main.m (100%) rename {app => example}/lib/main.dart (100%) rename {app => example}/lib/pages/home_page.dart (100%) rename {app => example}/lib/pages/read_only_page.dart (100%) rename {app => example}/lib/widgets/demo_scaffold.dart (100%) rename {app => example}/lib/widgets/field.dart (100%) rename {app => example}/linux/.gitignore (100%) rename {app => example}/linux/CMakeLists.txt (100%) rename {app => example}/linux/flutter/CMakeLists.txt (100%) rename {app => example}/linux/flutter/generated_plugin_registrant.cc (100%) rename {app => example}/linux/flutter/generated_plugin_registrant.h (100%) rename {app => example}/linux/flutter/generated_plugins.cmake (100%) rename {app => example}/linux/main.cc (100%) rename {app => example}/linux/my_application.cc (100%) rename {app => example}/linux/my_application.h (100%) rename {app => example}/macos/.gitignore (100%) rename {app => example}/macos/Flutter/Flutter-Debug.xcconfig (100%) rename {app => example}/macos/Flutter/Flutter-Release.xcconfig (100%) rename {app => example}/macos/Flutter/GeneratedPluginRegistrant.swift (100%) rename {app => example}/macos/Podfile (100%) rename {app => example}/macos/Runner.xcodeproj/project.pbxproj (100%) rename {app => example}/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {app => example}/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {app => example}/macos/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {app => example}/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {app => example}/macos/Runner/AppDelegate.swift (100%) rename {app => example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {app => example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%) rename {app => example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%) rename {app => example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%) rename {app => example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%) rename {app => example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%) rename {app => example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%) rename {app => example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%) rename {app => example}/macos/Runner/Base.lproj/MainMenu.xib (100%) rename {app => example}/macos/Runner/Configs/AppInfo.xcconfig (100%) rename {app => example}/macos/Runner/Configs/Debug.xcconfig (100%) rename {app => example}/macos/Runner/Configs/Release.xcconfig (100%) rename {app => example}/macos/Runner/Configs/Warnings.xcconfig (100%) rename {app => example}/macos/Runner/DebugProfile.entitlements (100%) rename {app => example}/macos/Runner/Info.plist (100%) rename {app => example}/macos/Runner/MainFlutterWindow.swift (100%) rename {app => example}/macos/Runner/Release.entitlements (100%) delete mode 100644 example/main.dart rename {app => example}/pubspec.yaml (100%) rename {app => example}/test/widget_test.dart (100%) rename {app => example}/web/favicon.png (100%) rename {app => example}/web/icons/Icon-192.png (100%) rename {app => example}/web/icons/Icon-512.png (100%) rename {app => example}/web/index.html (100%) rename {app => example}/web/manifest.json (100%) rename {app => example}/windows/.gitignore (100%) rename {app => example}/windows/CMakeLists.txt (100%) rename {app => example}/windows/flutter/CMakeLists.txt (100%) rename {app => example}/windows/flutter/generated_plugin_registrant.cc (100%) rename {app => example}/windows/flutter/generated_plugin_registrant.h (100%) rename {app => example}/windows/flutter/generated_plugins.cmake (100%) rename {app => example}/windows/runner/CMakeLists.txt (100%) rename {app => example}/windows/runner/Runner.rc (100%) rename {app => example}/windows/runner/flutter_window.cpp (100%) rename {app => example}/windows/runner/flutter_window.h (100%) rename {app => example}/windows/runner/main.cpp (100%) rename {app => example}/windows/runner/resource.h (100%) rename {app => example}/windows/runner/resources/app_icon.ico (100%) rename {app => example}/windows/runner/run_loop.cpp (100%) rename {app => example}/windows/runner/run_loop.h (100%) rename {app => example}/windows/runner/runner.exe.manifest (100%) rename {app => example}/windows/runner/utils.cpp (100%) rename {app => example}/windows/runner/utils.h (100%) rename {app => example}/windows/runner/win32_window.cpp (100%) rename {app => example}/windows/runner/win32_window.h (100%) diff --git a/app/.gitignore b/example/.gitignore similarity index 100% rename from app/.gitignore rename to example/.gitignore diff --git a/app/.metadata b/example/.metadata similarity index 100% rename from app/.metadata rename to example/.metadata diff --git a/app/README.md b/example/README.md similarity index 100% rename from app/README.md rename to example/README.md diff --git a/app/android/.gitignore b/example/android/.gitignore similarity index 100% rename from app/android/.gitignore rename to example/android/.gitignore diff --git a/app/android/app/build.gradle b/example/android/app/build.gradle similarity index 100% rename from app/android/app/build.gradle rename to example/android/app/build.gradle diff --git a/app/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from app/android/app/src/debug/AndroidManifest.xml rename to example/android/app/src/debug/AndroidManifest.xml diff --git a/app/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from app/android/app/src/main/AndroidManifest.xml rename to example/android/app/src/main/AndroidManifest.xml diff --git a/app/android/app/src/main/java/com/example/app/MainActivity.java b/example/android/app/src/main/java/com/example/app/MainActivity.java similarity index 100% rename from app/android/app/src/main/java/com/example/app/MainActivity.java rename to example/android/app/src/main/java/com/example/app/MainActivity.java diff --git a/app/android/app/src/main/kotlin/com/example/app/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/app/MainActivity.kt similarity index 100% rename from app/android/app/src/main/kotlin/com/example/app/MainActivity.kt rename to example/android/app/src/main/kotlin/com/example/app/MainActivity.kt diff --git a/app/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from app/android/app/src/main/res/drawable-v21/launch_background.xml rename to example/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/app/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from app/android/app/src/main/res/drawable/launch_background.xml rename to example/android/app/src/main/res/drawable/launch_background.xml diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/app/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from app/android/app/src/main/res/values-night/styles.xml rename to example/android/app/src/main/res/values-night/styles.xml diff --git a/app/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from app/android/app/src/main/res/values/styles.xml rename to example/android/app/src/main/res/values/styles.xml diff --git a/app/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from app/android/app/src/profile/AndroidManifest.xml rename to example/android/app/src/profile/AndroidManifest.xml diff --git a/app/android/build.gradle b/example/android/build.gradle similarity index 100% rename from app/android/build.gradle rename to example/android/build.gradle diff --git a/app/android/gradle.properties b/example/android/gradle.properties similarity index 100% rename from app/android/gradle.properties rename to example/android/gradle.properties diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from app/android/gradle/wrapper/gradle-wrapper.properties rename to example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/app/android/settings.gradle b/example/android/settings.gradle similarity index 100% rename from app/android/settings.gradle rename to example/android/settings.gradle diff --git a/app/android/settings_aar.gradle b/example/android/settings_aar.gradle similarity index 100% rename from app/android/settings_aar.gradle rename to example/android/settings_aar.gradle diff --git a/app/assets/sample_data.json b/example/assets/sample_data.json similarity index 100% rename from app/assets/sample_data.json rename to example/assets/sample_data.json diff --git a/app/ios/.gitignore b/example/ios/.gitignore similarity index 100% rename from app/ios/.gitignore rename to example/ios/.gitignore diff --git a/app/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from app/ios/Flutter/AppFrameworkInfo.plist rename to example/ios/Flutter/AppFrameworkInfo.plist diff --git a/app/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from app/ios/Flutter/Debug.xcconfig rename to example/ios/Flutter/Debug.xcconfig diff --git a/app/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig similarity index 100% rename from app/ios/Flutter/Release.xcconfig rename to example/ios/Flutter/Release.xcconfig diff --git a/app/ios/Podfile b/example/ios/Podfile similarity index 100% rename from app/ios/Podfile rename to example/ios/Podfile diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from app/ios/Runner.xcodeproj/project.pbxproj rename to example/ios/Runner.xcodeproj/project.pbxproj diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/app/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from app/ios/Runner.xcworkspace/contents.xcworkspacedata rename to example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/app/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h similarity index 100% rename from app/ios/Runner/AppDelegate.h rename to example/ios/Runner/AppDelegate.h diff --git a/app/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m similarity index 100% rename from app/ios/Runner/AppDelegate.m rename to example/ios/Runner/AppDelegate.m diff --git a/app/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift similarity index 100% rename from app/ios/Runner/AppDelegate.swift rename to example/ios/Runner/AppDelegate.swift diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from app/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/app/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from app/ios/Runner/Base.lproj/Main.storyboard rename to example/ios/Runner/Base.lproj/Main.storyboard diff --git a/app/ios/Runner/Info.plist b/example/ios/Runner/Info.plist similarity index 100% rename from app/ios/Runner/Info.plist rename to example/ios/Runner/Info.plist diff --git a/app/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from app/ios/Runner/Runner-Bridging-Header.h rename to example/ios/Runner/Runner-Bridging-Header.h diff --git a/app/ios/Runner/main.m b/example/ios/Runner/main.m similarity index 100% rename from app/ios/Runner/main.m rename to example/ios/Runner/main.m diff --git a/app/lib/main.dart b/example/lib/main.dart similarity index 100% rename from app/lib/main.dart rename to example/lib/main.dart diff --git a/app/lib/pages/home_page.dart b/example/lib/pages/home_page.dart similarity index 100% rename from app/lib/pages/home_page.dart rename to example/lib/pages/home_page.dart diff --git a/app/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart similarity index 100% rename from app/lib/pages/read_only_page.dart rename to example/lib/pages/read_only_page.dart diff --git a/app/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart similarity index 100% rename from app/lib/widgets/demo_scaffold.dart rename to example/lib/widgets/demo_scaffold.dart diff --git a/app/lib/widgets/field.dart b/example/lib/widgets/field.dart similarity index 100% rename from app/lib/widgets/field.dart rename to example/lib/widgets/field.dart diff --git a/app/linux/.gitignore b/example/linux/.gitignore similarity index 100% rename from app/linux/.gitignore rename to example/linux/.gitignore diff --git a/app/linux/CMakeLists.txt b/example/linux/CMakeLists.txt similarity index 100% rename from app/linux/CMakeLists.txt rename to example/linux/CMakeLists.txt diff --git a/app/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt similarity index 100% rename from app/linux/flutter/CMakeLists.txt rename to example/linux/flutter/CMakeLists.txt diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc similarity index 100% rename from app/linux/flutter/generated_plugin_registrant.cc rename to example/linux/flutter/generated_plugin_registrant.cc diff --git a/app/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h similarity index 100% rename from app/linux/flutter/generated_plugin_registrant.h rename to example/linux/flutter/generated_plugin_registrant.h diff --git a/app/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake similarity index 100% rename from app/linux/flutter/generated_plugins.cmake rename to example/linux/flutter/generated_plugins.cmake diff --git a/app/linux/main.cc b/example/linux/main.cc similarity index 100% rename from app/linux/main.cc rename to example/linux/main.cc diff --git a/app/linux/my_application.cc b/example/linux/my_application.cc similarity index 100% rename from app/linux/my_application.cc rename to example/linux/my_application.cc diff --git a/app/linux/my_application.h b/example/linux/my_application.h similarity index 100% rename from app/linux/my_application.h rename to example/linux/my_application.h diff --git a/app/macos/.gitignore b/example/macos/.gitignore similarity index 100% rename from app/macos/.gitignore rename to example/macos/.gitignore diff --git a/app/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from app/macos/Flutter/Flutter-Debug.xcconfig rename to example/macos/Flutter/Flutter-Debug.xcconfig diff --git a/app/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from app/macos/Flutter/Flutter-Release.xcconfig rename to example/macos/Flutter/Flutter-Release.xcconfig diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift similarity index 100% rename from app/macos/Flutter/GeneratedPluginRegistrant.swift rename to example/macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/app/macos/Podfile b/example/macos/Podfile similarity index 100% rename from app/macos/Podfile rename to example/macos/Podfile diff --git a/app/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj similarity index 100% rename from app/macos/Runner.xcodeproj/project.pbxproj rename to example/macos/Runner.xcodeproj/project.pbxproj diff --git a/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/app/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from app/macos/Runner.xcworkspace/contents.xcworkspacedata rename to example/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/app/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift similarity index 100% rename from app/macos/Runner/AppDelegate.swift rename to example/macos/Runner/AppDelegate.swift diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/app/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from app/macos/Runner/Base.lproj/MainMenu.xib rename to example/macos/Runner/Base.lproj/MainMenu.xib diff --git a/app/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from app/macos/Runner/Configs/AppInfo.xcconfig rename to example/macos/Runner/Configs/AppInfo.xcconfig diff --git a/app/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from app/macos/Runner/Configs/Debug.xcconfig rename to example/macos/Runner/Configs/Debug.xcconfig diff --git a/app/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from app/macos/Runner/Configs/Release.xcconfig rename to example/macos/Runner/Configs/Release.xcconfig diff --git a/app/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from app/macos/Runner/Configs/Warnings.xcconfig rename to example/macos/Runner/Configs/Warnings.xcconfig diff --git a/app/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements similarity index 100% rename from app/macos/Runner/DebugProfile.entitlements rename to example/macos/Runner/DebugProfile.entitlements diff --git a/app/macos/Runner/Info.plist b/example/macos/Runner/Info.plist similarity index 100% rename from app/macos/Runner/Info.plist rename to example/macos/Runner/Info.plist diff --git a/app/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from app/macos/Runner/MainFlutterWindow.swift rename to example/macos/Runner/MainFlutterWindow.swift diff --git a/app/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements similarity index 100% rename from app/macos/Runner/Release.entitlements rename to example/macos/Runner/Release.entitlements diff --git a/example/main.dart b/example/main.dart deleted file mode 100644 index 1b31b0bb..00000000 --- a/example/main.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/widgets/controller.dart'; -import 'package:flutter_quill/widgets/editor.dart'; -import 'package:flutter_quill/widgets/toolbar.dart'; - -class HomePage extends StatefulWidget { - @override - _HomePageState createState() => _HomePageState(); -} - -class _HomePageState extends State { - final QuillController _controller = QuillController.basic(); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - QuillToolbar.basic(controller: _controller), - Expanded( - child: Container( - child: QuillEditor.basic( - controller: _controller, - readOnly: false, // change to true to be view only mode - ), - ), - ) - ], - )); - } -} diff --git a/app/pubspec.yaml b/example/pubspec.yaml similarity index 100% rename from app/pubspec.yaml rename to example/pubspec.yaml diff --git a/app/test/widget_test.dart b/example/test/widget_test.dart similarity index 100% rename from app/test/widget_test.dart rename to example/test/widget_test.dart diff --git a/app/web/favicon.png b/example/web/favicon.png similarity index 100% rename from app/web/favicon.png rename to example/web/favicon.png diff --git a/app/web/icons/Icon-192.png b/example/web/icons/Icon-192.png similarity index 100% rename from app/web/icons/Icon-192.png rename to example/web/icons/Icon-192.png diff --git a/app/web/icons/Icon-512.png b/example/web/icons/Icon-512.png similarity index 100% rename from app/web/icons/Icon-512.png rename to example/web/icons/Icon-512.png diff --git a/app/web/index.html b/example/web/index.html similarity index 100% rename from app/web/index.html rename to example/web/index.html diff --git a/app/web/manifest.json b/example/web/manifest.json similarity index 100% rename from app/web/manifest.json rename to example/web/manifest.json diff --git a/app/windows/.gitignore b/example/windows/.gitignore similarity index 100% rename from app/windows/.gitignore rename to example/windows/.gitignore diff --git a/app/windows/CMakeLists.txt b/example/windows/CMakeLists.txt similarity index 100% rename from app/windows/CMakeLists.txt rename to example/windows/CMakeLists.txt diff --git a/app/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt similarity index 100% rename from app/windows/flutter/CMakeLists.txt rename to example/windows/flutter/CMakeLists.txt diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc similarity index 100% rename from app/windows/flutter/generated_plugin_registrant.cc rename to example/windows/flutter/generated_plugin_registrant.cc diff --git a/app/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h similarity index 100% rename from app/windows/flutter/generated_plugin_registrant.h rename to example/windows/flutter/generated_plugin_registrant.h diff --git a/app/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake similarity index 100% rename from app/windows/flutter/generated_plugins.cmake rename to example/windows/flutter/generated_plugins.cmake diff --git a/app/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt similarity index 100% rename from app/windows/runner/CMakeLists.txt rename to example/windows/runner/CMakeLists.txt diff --git a/app/windows/runner/Runner.rc b/example/windows/runner/Runner.rc similarity index 100% rename from app/windows/runner/Runner.rc rename to example/windows/runner/Runner.rc diff --git a/app/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp similarity index 100% rename from app/windows/runner/flutter_window.cpp rename to example/windows/runner/flutter_window.cpp diff --git a/app/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h similarity index 100% rename from app/windows/runner/flutter_window.h rename to example/windows/runner/flutter_window.h diff --git a/app/windows/runner/main.cpp b/example/windows/runner/main.cpp similarity index 100% rename from app/windows/runner/main.cpp rename to example/windows/runner/main.cpp diff --git a/app/windows/runner/resource.h b/example/windows/runner/resource.h similarity index 100% rename from app/windows/runner/resource.h rename to example/windows/runner/resource.h diff --git a/app/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico similarity index 100% rename from app/windows/runner/resources/app_icon.ico rename to example/windows/runner/resources/app_icon.ico diff --git a/app/windows/runner/run_loop.cpp b/example/windows/runner/run_loop.cpp similarity index 100% rename from app/windows/runner/run_loop.cpp rename to example/windows/runner/run_loop.cpp diff --git a/app/windows/runner/run_loop.h b/example/windows/runner/run_loop.h similarity index 100% rename from app/windows/runner/run_loop.h rename to example/windows/runner/run_loop.h diff --git a/app/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest similarity index 100% rename from app/windows/runner/runner.exe.manifest rename to example/windows/runner/runner.exe.manifest diff --git a/app/windows/runner/utils.cpp b/example/windows/runner/utils.cpp similarity index 100% rename from app/windows/runner/utils.cpp rename to example/windows/runner/utils.cpp diff --git a/app/windows/runner/utils.h b/example/windows/runner/utils.h similarity index 100% rename from app/windows/runner/utils.h rename to example/windows/runner/utils.h diff --git a/app/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp similarity index 100% rename from app/windows/runner/win32_window.cpp rename to example/windows/runner/win32_window.cpp diff --git a/app/windows/runner/win32_window.h b/example/windows/runner/win32_window.h similarity index 100% rename from app/windows/runner/win32_window.h rename to example/windows/runner/win32_window.h From 8efa97ec12f43ff654418c80a5108da6de78fbf9 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Fri, 26 Mar 2021 16:50:21 +0100 Subject: [PATCH 094/306] Remove prints (#110) --- analysis_options.yaml | 5 ++- lib/models/rules/rule.dart | 1 - lib/widgets/controller.dart | 36 +++++++------------ lib/widgets/toolbar.dart | 69 +++++++++++-------------------------- 4 files changed, 37 insertions(+), 74 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 4679e597..57f3028e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,4 +4,7 @@ analyzer: errors: undefined_prefixed_name: ignore omit_local_variable_types: ignore - unsafe_html: ignore \ No newline at end of file + unsafe_html: ignore +linter: + rules: + - avoid_print diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index da83797b..bff58b44 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -61,7 +61,6 @@ class Rules { final result = rule.apply(delta, index, len: len, data: data, attribute: attribute); if (result != null) { - print('Rule $rule applied'); return result..trim(); } } catch (e) { diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index d36a73b5..29206059 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -78,12 +78,7 @@ class QuillController extends ChangeNotifier { Delta? delta; if (len > 0 || data is! String || data.isNotEmpty) { - try { - delta = document.replace(index, len, data); - } catch (e) { - print('document.replace failed: $e'); - rethrow; - } + delta = document.replace(index, len, data); bool shouldRetainDelta = toggledStyle.isNotEmpty && delta.isNotEmpty && delta.length <= 2 && @@ -112,23 +107,18 @@ class QuillController extends ChangeNotifier { if (delta == null || delta.isEmpty) { _updateSelection(textSelection, ChangeSource.LOCAL); } else { - try { - Delta user = Delta() - ..retain(index) - ..insert(data) - ..delete(len); - int positionDelta = getPositionDelta(user, delta); - _updateSelection( - textSelection.copyWith( - baseOffset: textSelection.baseOffset + positionDelta, - extentOffset: textSelection.extentOffset + positionDelta, - ), - ChangeSource.LOCAL, - ); - } catch (e) { - print('getPositionDelta or getPositionDelta error: $e'); - rethrow; - } + Delta user = Delta() + ..retain(index) + ..insert(data) + ..delete(len); + int positionDelta = getPositionDelta(user, delta); + _updateSelection( + textSelection.copyWith( + baseOffset: textSelection.baseOffset + positionDelta, + extentOffset: textSelection.extentOffset + positionDelta, + ), + ChangeSource.LOCAL, + ); } } notifyListeners(); diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 3a6a72b5..cb23a19e 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -4,7 +4,6 @@ import 'package:file_picker/file_picker.dart'; import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.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'; @@ -520,46 +519,25 @@ class _ImageButtonState extends State { final File file = File(pickedFile.path); - // We simply return the absolute path to selected file. - try { - String url = await widget.onImagePickCallback!(file); - print('Image uploaded and its url is $url'); - return url; - } catch (error) { - print('Upload image error $error'); - } - return null; + return widget.onImagePickCallback!(file); } Future _pickImageWeb() async { - try { - _paths = (await FilePicker.platform.pickFiles( - type: _pickingType, - allowMultiple: false, - allowedExtensions: (_extension?.isNotEmpty ?? false) - ? _extension?.replaceAll(' ', '').split(',') - : null, - )) - ?.files; - } on PlatformException catch (e) { - print('Unsupported operation' + e.toString()); - } catch (ex) { - print(ex); - } + _paths = (await FilePicker.platform.pickFiles( + type: _pickingType, + allowMultiple: false, + allowedExtensions: (_extension?.isNotEmpty ?? false) + ? _extension?.replaceAll(' ', '').split(',') + : null, + )) + ?.files; var _fileName = _paths != null ? _paths!.map((e) => e.name).toString() : '...'; if (_paths != null) { File file = File(_fileName); // We simply return the absolute path to selected file. - try { - String url = await widget.onImagePickCallback!(file); - print('Image uploaded and its url is $url'); - return url; - } catch (error) { - print('Upload image error $error'); - } - return null; + return widget.onImagePickCallback!(file); } else { // User canceled the picker } @@ -567,23 +545,16 @@ class _ImageButtonState extends State { } Future _pickImageDesktop() async { - try { - var filePath = await FilesystemPicker.open( - context: context, - rootDirectory: await getApplicationDocumentsDirectory(), - fsType: FilesystemType.file, - fileTileSelectMode: FileTileSelectMode.wholeTile, - ); - if (filePath != null && filePath.isEmpty) return ''; - - final File file = File(filePath!); - String url = await widget.onImagePickCallback!(file); - print('Image uploaded and its url is $url'); - return url; - } catch (error) { - print('Upload image error $error'); - } - return ''; + var filePath = await FilesystemPicker.open( + context: context, + rootDirectory: await getApplicationDocumentsDirectory(), + fsType: FilesystemType.file, + fileTileSelectMode: FileTileSelectMode.wholeTile, + ); + if (filePath != null && filePath.isEmpty) return ''; + + final File file = File(filePath!); + return widget.onImagePickCallback!(file); } @override From 02e5fb0ba20fd70bb145d5bdd1eb8eec10465420 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Mar 2021 09:07:57 -0700 Subject: [PATCH 095/306] Fix home page --- README.md | 2 +- example/lib/pages/home_page.dart | 3 +-- example/lib/widgets/field.dart | 2 +- example/test/widget_test.dart | 5 +---- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cfdbd4f2..ac307c4e 100644 --- a/README.md +++ b/README.md @@ -86,4 +86,4 @@ For web development, use `flutter config --enable-web` for flutter and use [Reac [FlutterQuill]: https://pub.dev/packages/flutter_quill [ReactQuill]: https://github.com/zenoamaro/react-quill [Slack Group]: https://join.slack.com/t/bulletjournal1024/shared_invite/zt-fys7t9hi-ITVU5PGDen1rNRyCjdcQ2g -[Sample Page]: https://github.com/singerdmx/flutter-quill/blob/master/app/lib/pages/home_page.dart +[Sample Page]: https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index f7299805..2e044fd7 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -79,12 +79,11 @@ class _HomePageState extends State { .getSelectionStyle() .attributes .keys - .contains("bold")) { + .contains('bold')) { _controller! .formatSelection(Attribute.clone(Attribute.bold, null)); } else { _controller!.formatSelection(Attribute.bold); - print("not bold"); } } }, diff --git a/example/lib/widgets/field.dart b/example/lib/widgets/field.dart index 35e710f2..5a6badfe 100644 --- a/example/lib/widgets/field.dart +++ b/example/lib/widgets/field.dart @@ -105,11 +105,11 @@ class _QuillFieldState extends State { children: [ child, Visibility( - child: widget.toolbar!, visible: _focused, maintainSize: true, maintainAnimation: true, maintainState: true, + child: widget.toolbar!, ), ], ); diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 4088468f..5029542e 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -7,10 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import '../lib/main.dart'; - -// import 'package:app/main.dart'; +import 'package:app/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { From 7ea11f0aea666a5fc624e91f27fa664afb1a1947 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Mar 2021 12:06:24 -0700 Subject: [PATCH 096/306] Upgrade version to 1.1.3 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e2cf0f..667ea419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.3] +* Update example folder. + ## [1.1.2] * Add pedantic. diff --git a/pubspec.yaml b/pubspec.yaml index 3fe29f53..015e037c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor -version: 1.1.2 +version: 1.1.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From cfbe25475401c238f8d018ed4ae467996b6afa16 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Mar 2021 12:15:49 -0700 Subject: [PATCH 097/306] Update README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ac307c4e..18172233 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,11 @@ The `QuillToolbar` class lets you customise which formatting options are availab ## Web -For web development, use `flutter config --enable-web` for flutter and use [ReactQuill] for React. +For web development, use `flutter config --enable-web` for flutter and use [ReactQuill] for React. + +## Migrate Zefyr Data + +Check out [code](https://github.com/jwehrle/zefyr_quill_convert) and [doc](https://docs.google.com/document/d/1FUSrpbarHnilb7uDN5J5DDahaI0v1RMXBjj4fFSpSuY/edit?usp=sharing). --- From 19e86fa781bb55c0b50180e024ee69181aac705e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Mar 2021 13:24:32 -0700 Subject: [PATCH 098/306] Update description --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 015e037c..553ea465 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_quill -description: A rich text editor +description: A rich text editor supporting mobile and web version: 1.1.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html From bdb8f1c22f722601f0842e916129bcea38465eb1 Mon Sep 17 00:00:00 2001 From: Diego Garcia Date: Fri, 26 Mar 2021 20:25:22 -0700 Subject: [PATCH 099/306] Added QuillEditor Key (#113) --- lib/widgets/editor.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 305a58c8..86a80df0 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -189,7 +189,9 @@ class QuillEditor extends StatefulWidget { this.scrollPhysics, this.onLaunchUrl, this.embedBuilder = - kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder}); + kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder, + Key? key, + }) : super(key: key) factory QuillEditor.basic( {required QuillController controller, required bool readOnly}) { From 60c755d86d01e2321953178c4ac96b105f8a622c Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 26 Mar 2021 20:28:03 -0700 Subject: [PATCH 100/306] Revert "Added QuillEditor Key (#113)" This reverts commit bdb8f1c22f722601f0842e916129bcea38465eb1. --- lib/widgets/editor.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 86a80df0..305a58c8 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -189,9 +189,7 @@ class QuillEditor extends StatefulWidget { this.scrollPhysics, this.onLaunchUrl, this.embedBuilder = - kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder, - Key? key, - }) : super(key: key) + kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder}); factory QuillEditor.basic( {required QuillController controller, required bool readOnly}) { From cd05dfaeb8ec3d3a860ba0bc8ccc563728d7eba8 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 27 Mar 2021 18:06:58 +0100 Subject: [PATCH 101/306] Avoid redundant argument values (#114) If it's ok, I would spend the day enabling linter rules so that the codebase is more compliant with [Effective Dart](https://dart.dev/guides/language/effective-dart) and thus more readable for new developers. --- analysis_options.yaml | 1 + example/lib/pages/home_page.dart | 1 - example/lib/pages/read_only_page.dart | 1 - lib/models/documents/document.dart | 6 +++--- lib/models/quill_delta.dart | 2 +- lib/widgets/controller.dart | 1 - lib/widgets/editor.dart | 4 +--- lib/widgets/raw_editor.dart | 2 -- lib/widgets/text_block.dart | 2 +- lib/widgets/toolbar.dart | 3 --- 10 files changed, 7 insertions(+), 16 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 57f3028e..33b1634a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,3 +8,4 @@ analyzer: linter: rules: - avoid_print + - avoid_redundant_argument_values diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 2e044fd7..9e91cde4 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -110,7 +110,6 @@ class _HomePageState extends State { autoFocus: false, readOnly: false, placeholder: 'Add content', - enableInteractiveSelection: true, expands: false, padding: EdgeInsets.zero, customStyles: DefaultStyles( diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index 8f03d45c..6e2ec0e0 100644 --- a/example/lib/pages/read_only_page.dart +++ b/example/lib/pages/read_only_page.dart @@ -42,7 +42,6 @@ class _ReadOnlyPageState extends State { focusNode: _focusNode, autoFocus: true, readOnly: !_edit, - enableInteractiveSelection: true, expands: false, padding: EdgeInsets.zero, ), diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index d52e89a4..126ff32b 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -183,7 +183,7 @@ class Document { bool 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', null)); + res.push(Operation.insert('\n')); } // Currently embed is equivalent to image and hence `is! String` bool opInsertImage = op.isInsert && op.data is! String; @@ -193,7 +193,7 @@ class Document { (ops[i + 1].data as String).startsWith('\n'); if (opInsertImage && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) { // automatically append '\n' for image - res.push(Operation.insert('\n', null)); + res.push(Operation.insert('\n')); } } @@ -213,7 +213,7 @@ class Document { _history.clear(); } - String toPlainText() => _root.children.map((e) => e.toPlainText()).join(''); + String toPlainText() => _root.children.map((e) => e.toPlainText()).join(); void _loadDocument(Delta doc) { assert((doc.last.data as String).endsWith('\n')); diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index bced9b88..a1fe1f0f 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -520,7 +520,7 @@ class Delta { if (op.isInsert) { inverted.delete(op.length!); } else if (op.isRetain && op.isPlain) { - inverted.retain(op.length!, null); + inverted.retain(op.length!); baseIndex += op.length!; } else if (op.isDelete || (op.isRetain && op.isNotPlain)) { final length = op.length!; diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 29206059..f613af69 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -31,7 +31,6 @@ class QuillController extends ChangeNotifier { TextEditingValue get plainTextEditingValue => TextEditingValue( text: document.toPlainText(), selection: selection, - composing: TextRange.empty, ); Style getSelectionStyle() { diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 305a58c8..f3d3bd1a 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -200,7 +200,6 @@ class QuillEditor extends StatefulWidget { focusNode: FocusNode(), autoFocus: true, readOnly: readOnly, - enableInteractiveSelection: true, expands: false, padding: EdgeInsets.zero); } @@ -726,8 +725,7 @@ class RenderEditor extends RenderEditableContainerBox ); if (position.offset - word.start <= 1) { _handleSelectionChange( - TextSelection.collapsed( - offset: word.start, affinity: TextAffinity.downstream), + TextSelection.collapsed(offset: word.start), cause, ); } else { diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 04b4aaf8..11c9c568 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -382,8 +382,6 @@ class RawEditorState extends EditorState TextInputConfiguration( inputType: TextInputType.multiline, readOnly: widget.readOnly, - obscureText: false, - autocorrect: true, inputAction: TextInputAction.newline, keyboardAppearance: widget.keyboardAppearance, textCapitalization: widget.textCapitalization, diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 4d2a5cc3..1b085957 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -623,7 +623,7 @@ class _NumberPoint extends StatelessWidget { n = (n / 26).floor(); } - return result.toString().split('').reversed.join(''); + return result.toString().split('').reversed.join(); } String _intToRoman(int input) { diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index cb23a19e..0859cad3 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -525,7 +525,6 @@ class _ImageButtonState extends State { Future _pickImageWeb() async { _paths = (await FilePicker.platform.pickFiles( type: _pickingType, - allowMultiple: false, allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '').split(',') : null, @@ -1163,7 +1162,6 @@ class QuillIconButton extends StatelessWidget { child: RawMaterialButton( visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - padding: EdgeInsets.zero, fillColor: fillColor, elevation: 0, hoverElevation: hoverElevation, @@ -1209,7 +1207,6 @@ class _QuillDropdownButtonState extends State> { child: RawMaterialButton( visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - padding: EdgeInsets.zero, fillColor: widget.fillColor, elevation: 0, hoverElevation: widget.hoverElevation, From 25f1582f70f11c5e0ff643f3f33b5a84e7cdbbab Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 27 Mar 2021 18:08:16 +0100 Subject: [PATCH 102/306] Always put required named parameters first (#115) --- analysis_options.yaml | 1 + lib/widgets/default_styles.dart | 2 +- lib/widgets/editor.dart | 4 +- lib/widgets/responsive_widget.dart | 12 +-- lib/widgets/text_block.dart | 6 +- lib/widgets/text_line.dart | 17 ++-- lib/widgets/text_selection.dart | 6 +- lib/widgets/toolbar.dart | 127 +++++++++++++++-------------- 8 files changed, 89 insertions(+), 86 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 33b1634a..451c1799 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,5 +7,6 @@ analyzer: unsafe_html: ignore linter: rules: + - always_put_required_named_parameters_first - avoid_print - avoid_redundant_argument_values diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index bbe8db10..30b6b7fe 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -6,9 +6,9 @@ class QuillStyles extends InheritedWidget { final DefaultStyles data; QuillStyles({ - Key? key, required this.data, required Widget child, + Key? key, }) : super(key: key, child: child); @override diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index f3d3bd1a..ef2221c0 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -176,14 +176,14 @@ class QuillEditor extends StatefulWidget { required this.scrollable, required this.padding, required this.autoFocus, - this.showCursor, required this.readOnly, + required this.expands, + this.showCursor, this.placeholder, this.enableInteractiveSelection = true, this.minHeight, this.maxHeight, this.customStyles, - required this.expands, this.textCapitalization = TextCapitalization.sentences, this.keyboardAppearance = Brightness.light, this.scrollPhysics, diff --git a/lib/widgets/responsive_widget.dart b/lib/widgets/responsive_widget.dart index 806883ec..dbfec8d5 100644 --- a/lib/widgets/responsive_widget.dart +++ b/lib/widgets/responsive_widget.dart @@ -5,12 +5,12 @@ class ResponsiveWidget extends StatelessWidget { final Widget? mediumScreen; final Widget? smallScreen; - const ResponsiveWidget( - {Key? key, - required this.largeScreen, - this.mediumScreen, - this.smallScreen}) - : super(key: key); + const ResponsiveWidget({ + required this.largeScreen, + this.mediumScreen, + this.smallScreen, + Key? key, + }) : super(key: key); 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 1b085957..ef226b1e 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -253,11 +253,11 @@ class EditableTextBlock extends StatelessWidget { class RenderEditableTextBlock extends RenderEditableContainerBox implements RenderEditableBox { RenderEditableTextBlock({ - List? children, required Block block, required TextDirection textDirection, required EdgeInsetsGeometry padding, required Decoration decoration, + List? children, ImageConfiguration configuration = ImageConfiguration.empty, EdgeInsets contentPadding = EdgeInsets.zero, }) : _decoration = decoration, @@ -558,7 +558,6 @@ class _NumberPoint extends StatelessWidget { final double padding; const _NumberPoint({ - Key? key, required this.index, required this.indentLevelCounts, required this.count, @@ -567,6 +566,7 @@ class _NumberPoint extends StatelessWidget { required this.attrs, this.withDot = true, this.padding = 0.0, + Key? key, }) : super(key: key); @override @@ -656,9 +656,9 @@ class _BulletPoint extends StatelessWidget { final double width; const _BulletPoint({ - Key? key, required this.style, required this.width, + Key? key, }) : super(key: key); @override diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 0da1e64f..82418ca3 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -27,13 +27,13 @@ class TextLine extends StatelessWidget { final EmbedBuilder embedBuilder; final DefaultStyles styles; - const TextLine( - {Key? key, - required this.line, - this.textDirection, - required this.embedBuilder, - required this.styles}) - : super(key: key); + const TextLine({ + required this.line, + required this.embedBuilder, + required this.styles, + this.textDirection, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -839,7 +839,8 @@ class _TextLineElement extends RenderObjectElement { } @override - void moveRenderObjectChild(RenderObject child, dynamic oldSlot, dynamic newSlot) { + void moveRenderObjectChild( + RenderObject child, dynamic oldSlot, dynamic newSlot) { throw UnimplementedError(); } diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 4d9cb564..99f381fa 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -245,7 +245,6 @@ class EditorTextSelectionOverlay { class _TextSelectionHandleOverlay extends StatefulWidget { const _TextSelectionHandleOverlay({ - Key? key, required this.selection, required this.position, required this.startHandleLayerLink, @@ -255,6 +254,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget { required this.onSelectionHandleTapped, required this.selectionControls, this.dragStartBehavior = DragStartBehavior.start, + Key? key, }) : super(key: key); final TextSelection selection; @@ -468,7 +468,7 @@ class _TextSelectionHandleOverlayState class EditorTextSelectionGestureDetector extends StatefulWidget { const EditorTextSelectionGestureDetector({ - Key? key, + required this.child, this.onTapDown, this.onForcePressStart, this.onForcePressEnd, @@ -482,7 +482,7 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { this.onDragSelectionUpdate, this.onDragSelectionEnd, this.behavior, - required this.child, + Key? key, }) : super(key: key); final GestureTapDownCallback? onTapDown; diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 0859cad3..8eeb35ae 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -25,9 +25,9 @@ class InsertEmbedButton extends StatelessWidget { final IconData icon; const InsertEmbedButton({ - Key? key, required this.controller, required this.icon, + Key? key, }) : super(key: key); @override @@ -56,9 +56,9 @@ class LinkStyleButton extends StatefulWidget { final IconData? icon; const LinkStyleButton({ - Key? key, required this.controller, this.icon, + Key? key, }) : super(key: key); @override @@ -183,11 +183,11 @@ class ToggleStyleButton extends StatefulWidget { final ToggleStyleButtonBuilder childBuilder; ToggleStyleButton({ - Key? key, required this.attribute, required this.icon, required this.controller, this.childBuilder = defaultToggleStyleButtonBuilder, + Key? key, }) : super(key: key); @override @@ -267,11 +267,11 @@ class ToggleCheckListButton extends StatefulWidget { final Attribute attribute; ToggleCheckListButton({ - Key? key, required this.icon, required this.controller, - this.childBuilder = defaultToggleStyleButtonBuilder, required this.attribute, + this.childBuilder = defaultToggleStyleButtonBuilder, + Key? key, }) : super(key: key); @override @@ -371,7 +371,7 @@ Widget defaultToggleStyleButtonBuilder( class SelectHeaderStyleButton extends StatefulWidget { final QuillController controller; - const SelectHeaderStyleButton({Key? key, required this.controller}) + const SelectHeaderStyleButton({required this.controller, Key? key}) : super(key: key); @override @@ -494,14 +494,14 @@ class ImageButton extends StatefulWidget { final ImageSource imageSource; - ImageButton( - {Key? key, - required this.icon, - required this.controller, - required this.imageSource, - this.onImagePickCallback, - this.imagePickImpl}) - : super(key: key); + ImageButton({ + required this.icon, + required this.controller, + required this.imageSource, + this.onImagePickCallback, + this.imagePickImpl, + Key? key, + }) : super(key: key); @override _ImageButtonState createState() => _ImageButtonState(); @@ -600,12 +600,12 @@ class ColorButton extends StatefulWidget { final bool background; final QuillController controller; - ColorButton( - {Key? key, - required this.icon, - required this.controller, - required this.background}) - : super(key: key); + ColorButton({ + required this.icon, + required this.controller, + required this.background, + Key? key, + }) : super(key: key); @override _ColorButtonState createState() => _ColorButtonState(); @@ -738,12 +738,12 @@ class HistoryButton extends StatefulWidget { final bool undo; final QuillController controller; - HistoryButton( - {Key? key, - required this.icon, - required this.controller, - required this.undo}) - : super(key: key); + HistoryButton({ + required this.icon, + required this.controller, + required this.undo, + Key? key, + }) : super(key: key); @override _HistoryButtonState createState() => _HistoryButtonState(); @@ -810,12 +810,12 @@ class IndentButton extends StatefulWidget { final QuillController controller; final bool isIncrease; - IndentButton( - {Key? key, - required this.icon, - required this.controller, - required this.isIncrease}) - : super(key: key); + IndentButton({ + required this.icon, + required this.controller, + required this.isIncrease, + Key? key, + }) : super(key: key); @override _IndentButtonState createState() => _IndentButtonState(); @@ -865,7 +865,7 @@ class ClearFormatButton extends StatefulWidget { final QuillController controller; - ClearFormatButton({Key? key, required this.icon, required this.controller}) + ClearFormatButton({required this.icon, required this.controller, Key? key}) : super(key: key); @override @@ -896,30 +896,31 @@ class _ClearFormatButtonState extends State { class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { final List children; - const QuillToolbar({Key? key, required this.children}) : super(key: key); - - factory QuillToolbar.basic( - {Key? key, - required QuillController controller, - double toolbarIconSize = 18.0, - bool showBoldButton = true, - bool showItalicButton = true, - bool showUnderLineButton = true, - bool showStrikeThrough = true, - bool showColorButton = true, - bool showBackgroundColorButton = true, - bool showClearFormat = true, - bool showHeaderStyle = true, - bool showListNumbers = true, - bool showListBullets = true, - bool showListCheck = true, - bool showCodeBlock = true, - bool showQuote = true, - bool showIndent = true, - bool showLink = true, - bool showHistory = true, - bool showHorizontalRule = false, - OnImagePickCallback? onImagePickCallback}) { + const QuillToolbar({required this.children, Key? key}) : super(key: key); + + factory QuillToolbar.basic({ + required QuillController controller, + double toolbarIconSize = 18.0, + bool showBoldButton = true, + bool showItalicButton = true, + bool showUnderLineButton = true, + bool showStrikeThrough = true, + bool showColorButton = true, + bool showBackgroundColorButton = true, + bool showClearFormat = true, + bool showHeaderStyle = true, + bool showListNumbers = true, + bool showListBullets = true, + bool showListCheck = true, + bool showCodeBlock = true, + bool showQuote = true, + bool showIndent = true, + bool showLink = true, + bool showHistory = true, + bool showHorizontalRule = false, + OnImagePickCallback? onImagePickCallback, + Key? key, + }) { iconSize = toolbarIconSize; return QuillToolbar(key: key, children: [ Visibility( @@ -1146,13 +1147,13 @@ class QuillIconButton extends StatelessWidget { final double highlightElevation; const QuillIconButton({ - Key? key, required this.onPressed, this.icon, this.size = 40, this.fillColor, this.hoverElevation = 1, this.highlightElevation = 1, + Key? key, }) : super(key: key); @override @@ -1184,15 +1185,15 @@ class QuillDropdownButton extends StatefulWidget { final ValueChanged onSelected; const QuillDropdownButton({ - Key? key, - this.height = 40, - this.fillColor, - this.hoverElevation = 1, - this.highlightElevation = 1, required this.child, required this.initialValue, required this.items, required this.onSelected, + this.height = 40, + this.fillColor, + this.hoverElevation = 1, + this.highlightElevation = 1, + Key? key, }) : super(key: key); @override From ace4fd4deb929e37128f78252a9d5310dafe7d33 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 27 Mar 2021 10:46:52 -0700 Subject: [PATCH 103/306] Fix selection not working --- lib/widgets/toolbar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 8eeb35ae..514946a3 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -1248,6 +1248,7 @@ class _QuillDropdownButtonState extends State> { // if (widget.onCanceled != null) widget.onCanceled(); return null; } + widget.onSelected(newValue); }); } From 1cf37c1824197f4907cf820705de07cf92c77b2d Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 27 Mar 2021 18:57:40 +0100 Subject: [PATCH 104/306] Rebuild editor when keyboard is already open (#111) * Rebuild editor when keyboard is already open If the keyboard is already open, but the editor thinks that the keyboard is not open, the text will not be updated when writing. This can easily happen if one has a `TabBarView` with two children, each with an `QuillEditor`, see the code for an example:
Example ```dart import 'package:flutter/material.dart'; import 'package:flutter_quill/widgets/controller.dart'; import 'package:flutter_quill/widgets/editor.dart'; void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { late QuillController _controller1; late QuillController _controller2; @override void initState() { _controller1 = QuillController.basic(); _controller2 = QuillController.basic(); super.initState(); } @override Widget build(BuildContext context) { return MaterialApp( home: DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: Text('Flutter Quill tabs demo'), bottom: TabBar( tabs: [ Tab(text: 'First'), Tab(text: 'Second'), ], ), ), body: TabBarView( children: [ QuillEditor.basic(controller: _controller1, readOnly: false), QuillEditor.basic(controller: _controller2, readOnly: false), ], ), ), ), ); } }
Video
* Add documentation comment for getOffsetToRevealCursor * Set initial keyboard visibility --- lib/widgets/editor.dart | 5 +++++ lib/widgets/raw_editor.dart | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index ef2221c0..0913ffe0 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -864,6 +864,11 @@ class RenderEditor extends RenderEditableContainerBox ); } + /// Returns the y-offset of the editor at which [selection] is visible. + /// + /// The offset is the distance from the top of the editor and is the minimum + /// from the current scroll position until [selection] becomes visible. + /// Returns null if [selection] is already visible. double? getOffsetToRevealCursor( double viewportHeight, double scrollOffset, double offsetInViewport) { List endpoints = getEndpointsForSelection(selection); diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 11c9c568..73a17e25 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -700,6 +700,7 @@ class RawEditorState extends EditorState _keyboardVisible = true; } else { _keyboardVisibilityController = KeyboardVisibilityController(); + _keyboardVisible = _keyboardVisibilityController!.isVisible; _keyboardVisibilitySubscription = _keyboardVisibilityController?.onChange.listen((bool visible) { _keyboardVisible = visible; From d6b21586a41e57e74309ad604da97d4d2185c1aa Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 27 Mar 2021 19:08:34 +0100 Subject: [PATCH 105/306] Prefer const constructors (#116) --- analysis_options.yaml | 1 + lib/models/documents/history.dart | 2 +- lib/models/quill_delta.dart | 2 +- lib/models/rules/insert.dart | 2 +- lib/models/rules/rule.dart | 30 ++++++++++----------- lib/widgets/controller.dart | 7 +++-- lib/widgets/cursor.dart | 10 ++++--- lib/widgets/default_styles.dart | 44 +++++++++++++++---------------- lib/widgets/proxy.dart | 2 +- lib/widgets/raw_editor.dart | 32 +++++++++++----------- lib/widgets/text_block.dart | 9 ++++--- lib/widgets/text_line.dart | 7 ++--- lib/widgets/text_selection.dart | 17 ++++++------ lib/widgets/toolbar.dart | 36 ++++++++++++------------- 14 files changed, 106 insertions(+), 95 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 451c1799..74ce67a1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,3 +10,4 @@ linter: - always_put_required_named_parameters_first - avoid_print - avoid_redundant_argument_values + - prefer_const_constructors diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index 32e780c9..6d89b389 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -86,7 +86,7 @@ class History { Tuple2 _change(Document doc, List source, List dest) { if (source.isEmpty) { - return Tuple2(false, 0); + return const Tuple2(false, 0); } Delta delta = source.removeLast(); // look for insert or delete diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index a1fe1f0f..72d26846 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -294,7 +294,7 @@ class Delta { if (other is! Delta) return false; Delta typedOther = other; final comparator = - ListEquality(const DefaultEquality()); + const ListEquality(DefaultEquality()); return comparator.equals(_operations, typedOther._operations); } diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 62585ec3..5ea7f215 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -378,5 +378,5 @@ Tuple2 _getNextNewLine(DeltaIterator iterator) { return Tuple2(op, skipped); } } - return Tuple2(null, null); + return const Tuple2(null, null); } diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index bff58b44..8141aaf3 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -29,21 +29,21 @@ abstract class Rule { class Rules { final List _rules; static final Rules _instance = Rules([ - FormatLinkAtCaretPositionRule(), - ResolveLineFormatRule(), - ResolveInlineFormatRule(), - InsertEmbedsRule(), - ForceNewlineForInsertsAroundEmbedRule(), - AutoExitBlockRule(), - PreserveBlockStyleOnInsertRule(), - PreserveLineStyleOnSplitRule(), - ResetLineFormatOnNewLineRule(), - AutoFormatLinksRule(), - PreserveInlineStylesRule(), - CatchAllInsertRule(), - EnsureEmbedLineRule(), - PreserveLineStyleOnMergeRule(), - CatchAllDeleteRule(), + const FormatLinkAtCaretPositionRule(), + const ResolveLineFormatRule(), + const ResolveInlineFormatRule(), + const InsertEmbedsRule(), + const ForceNewlineForInsertsAroundEmbedRule(), + const AutoExitBlockRule(), + const PreserveBlockStyleOnInsertRule(), + const PreserveLineStyleOnSplitRule(), + const ResetLineFormatOnNewLineRule(), + const AutoFormatLinksRule(), + const PreserveInlineStylesRule(), + const CatchAllInsertRule(), + const EnsureEmbedLineRule(), + const PreserveLineStyleOnMergeRule(), + const CatchAllDeleteRule(), ]); Rules(this._rules); diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index f613af69..da876bf1 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -18,7 +18,9 @@ class QuillController extends ChangeNotifier { factory QuillController.basic() { return QuillController( - document: Document(), selection: TextSelection.collapsed(offset: 0)); + document: Document(), + selection: const TextSelection.collapsed(offset: 0), + ); } // item1: Document state before [change]. @@ -72,7 +74,8 @@ class QuillController extends ChangeNotifier { bool get hasRedo => document.hasRedo; - void replaceText(int index, int len, Object? data, TextSelection? textSelection) { + void replaceText( + int index, int len, Object? data, TextSelection? textSelection) { assert(data is String || data is Embeddable); Delta? delta; diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index 8fa45437..4195d947 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -109,7 +109,8 @@ class CursorCont extends ChangeNotifier { void _cursorWaitForStart(Timer timer) { _cursorTimer?.cancel(); - _cursorTimer = Timer.periodic(Duration(milliseconds: 500), _cursorTick); + _cursorTimer = + Timer.periodic(const Duration(milliseconds: 500), _cursorTick); } void startCursorTimer() { @@ -117,10 +118,11 @@ class CursorCont extends ChangeNotifier { _blinkOpacityCont.value = 1.0; if (style.opacityAnimates) { - _cursorTimer = - Timer.periodic(Duration(milliseconds: 150), _cursorWaitForStart); + _cursorTimer = Timer.periodic( + const Duration(milliseconds: 150), _cursorWaitForStart); } else { - _cursorTimer = Timer.periodic(Duration(milliseconds: 500), _cursorTick); + _cursorTimer = + Timer.periodic(const Duration(milliseconds: 500), _cursorTick); } } diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 30b6b7fe..2e3ad80c 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -86,7 +86,7 @@ class DefaultStyles { fontSize: 16.0, height: 1.3, ); - Tuple2 baseSpacing = Tuple2(6.0, 0); + Tuple2 baseSpacing = const Tuple2(6.0, 0); String fontFamily; switch (themeData.platform) { case TargetPlatform.iOS: @@ -111,8 +111,8 @@ class DefaultStyles { height: 1.15, fontWeight: FontWeight.w300, ), - Tuple2(16.0, 0.0), - Tuple2(0.0, 0.0), + const Tuple2(16.0, 0.0), + const Tuple2(0.0, 0.0), null), h2: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( @@ -121,8 +121,8 @@ class DefaultStyles { height: 1.15, fontWeight: FontWeight.normal, ), - Tuple2(8.0, 0.0), - Tuple2(0.0, 0.0), + const Tuple2(8.0, 0.0), + const Tuple2(0.0, 0.0), null), h3: DefaultTextBlockStyle( defaultTextStyle.style.copyWith( @@ -131,15 +131,15 @@ class DefaultStyles { height: 1.25, fontWeight: FontWeight.w500, ), - Tuple2(8.0, 0.0), - Tuple2(0.0, 0.0), + const Tuple2(8.0, 0.0), + const Tuple2(0.0, 0.0), null), paragraph: DefaultTextBlockStyle( - baseStyle, Tuple2(0.0, 0.0), Tuple2(0.0, 0.0), null), - bold: TextStyle(fontWeight: FontWeight.bold), - italic: TextStyle(fontStyle: FontStyle.italic), - underline: TextStyle(decoration: TextDecoration.underline), - strikeThrough: TextStyle(decoration: TextDecoration.lineThrough), + baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null), + bold: const TextStyle(fontWeight: FontWeight.bold), + italic: const TextStyle(fontStyle: FontStyle.italic), + underline: const TextStyle(decoration: TextDecoration.underline), + strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), link: TextStyle( color: themeData.accentColor, decoration: TextDecoration.underline, @@ -150,15 +150,15 @@ class DefaultStyles { height: 1.5, color: Colors.grey.withOpacity(0.6), ), - Tuple2(0.0, 0.0), - Tuple2(0.0, 0.0), + const Tuple2(0.0, 0.0), + const Tuple2(0.0, 0.0), null), lists: DefaultTextBlockStyle( - baseStyle, baseSpacing, Tuple2(0.0, 6.0), null), + baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null), quote: DefaultTextBlockStyle( TextStyle(color: baseStyle.color!.withOpacity(0.6)), baseSpacing, - Tuple2(6.0, 2.0), + const Tuple2(6.0, 2.0), BoxDecoration( border: Border( left: BorderSide(width: 4, color: Colors.grey.shade300), @@ -172,18 +172,18 @@ class DefaultStyles { height: 1.15, ), baseSpacing, - Tuple2(0.0, 0.0), + const Tuple2(0.0, 0.0), BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(2), )), indent: DefaultTextBlockStyle( - baseStyle, baseSpacing, Tuple2(0.0, 6.0), null), + baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null), align: DefaultTextBlockStyle( - baseStyle, Tuple2(0.0, 0.0), Tuple2(0.0, 0.0), null), - sizeSmall: TextStyle(fontSize: 10.0), - sizeLarge: TextStyle(fontSize: 18.0), - sizeHuge: TextStyle(fontSize: 22.0)); + 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)); } DefaultStyles merge(DefaultStyles other) { diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index f3e38c4e..7b918b9d 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -113,7 +113,7 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { @override TextRange getWordBoundary(TextPosition position) => - TextRange(start: 0, end: 1); + const TextRange(start: 0, end: 1); @override double getPreferredLineHeight() { diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 73a17e25..9ba3587a 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -552,7 +552,7 @@ class RawEditorState extends EditorState } BoxConstraints constraints = widget.expands - ? BoxConstraints.expand() + ? const BoxConstraints.expand() : BoxConstraints( minHeight: widget.minHeight ?? 0.0, maxHeight: widget.maxHeight ?? double.infinity); @@ -600,7 +600,7 @@ class RawEditorState extends EditorState widget.enableInteractiveSelection, _hasFocus, attrs.containsKey(Attribute.codeBlock.key) - ? EdgeInsets.all(16.0) + ? const EdgeInsets.all(16.0) : null, widget.embedBuilder, _cursorCont, @@ -816,7 +816,8 @@ class RawEditorState extends EditorState String plainText = textEditingValue.text; if (shortcut == InputShortcut.COPY) { if (!selection.isCollapsed) { - await Clipboard.setData(ClipboardData(text: selection.textInside(plainText))); + await Clipboard.setData( + ClipboardData(text: selection.textInside(plainText))); } return; } @@ -984,7 +985,7 @@ class RawEditorState extends EditorState final viewport = RenderAbstractViewport.of(getRenderEditor())!; final editorOffset = getRenderEditor()! - .localToGlobal(Offset(0.0, 0.0), ancestor: viewport); + .localToGlobal(const Offset(0.0, 0.0), ancestor: viewport); final offsetInViewport = _scrollController!.offset + editorOffset.dy; final offset = getRenderEditor()!.getOffsetToRevealCursor( @@ -996,7 +997,7 @@ class RawEditorState extends EditorState if (offset != null) { _scrollController!.animateTo( offset, - duration: Duration(milliseconds: 100), + duration: const Duration(milliseconds: 100), curve: Curves.fastOutSlowIn, ); } @@ -1150,16 +1151,17 @@ class _Editor extends MultiChildRenderObjectWidget { @override RenderEditor createRenderObject(BuildContext context) { return RenderEditor( - null, - textDirection, - padding, - document, - selection, - hasFocus, - onSelectionChanged, - startHandleLayerLink, - endHandleLayerLink, - EdgeInsets.fromLTRB(4, 4, 4, 5)); + null, + textDirection, + padding, + document, + selection, + hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + const EdgeInsets.fromLTRB(4, 4, 4, 5), + ); } @override diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index ef226b1e..7edb3add 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -84,7 +84,7 @@ class EditableTextBlock extends StatelessWidget { block, textDirection, verticalSpacing as Tuple2, - _getDecorationForBlock(block, defaultStyles) ?? BoxDecoration(), + _getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(), contentPadding, _buildChildren(context, indentLevelCounts)); } @@ -402,7 +402,8 @@ class RenderEditableTextBlock extends RenderEditableContainerBox } Offset caretOffset = child.getOffsetForCaret(childLocalPosition); - Offset testOffset = sibling.getOffsetForCaret(TextPosition(offset: 0)); + Offset testOffset = + sibling.getOffsetForCaret(const TextPosition(offset: 0)); Offset finalOffset = Offset(caretOffset.dx, testOffset.dy); return TextPosition( offset: sibling.getContainer().getOffset() + @@ -666,7 +667,7 @@ class _BulletPoint extends StatelessWidget { return Container( alignment: AlignmentDirectional.topEnd, width: width, - padding: EdgeInsetsDirectional.only(end: 13.0), + padding: const EdgeInsetsDirectional.only(end: 13.0), child: Text('•', style: style), ); } @@ -707,7 +708,7 @@ class __CheckboxState extends State<_Checkbox> { return Container( alignment: AlignmentDirectional.topEnd, width: widget.width, - padding: EdgeInsetsDirectional.only(end: 13.0), + padding: const EdgeInsetsDirectional.only(end: 13.0), child: Checkbox( value: widget.isChecked, onChanged: _onCheckboxClicked, diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 82418ca3..73090786 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -87,7 +87,7 @@ class TextLine extends StatelessWidget { .map((node) => _getTextSpanFromNode(defaultStyles, node)) .toList(growable: false); - TextStyle textStyle = TextStyle(); + TextStyle textStyle = const TextStyle(); if (line.style.containsKey(Attribute.placeholder.key)) { textStyle = defaultStyles.placeHolder!.style; @@ -121,7 +121,7 @@ class TextLine extends StatelessWidget { TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { leaf.Text textNode = node as leaf.Text; Style style = textNode.style; - TextStyle res = TextStyle(); + TextStyle res = const TextStyle(); Map m = { Attribute.bold.key: defaultStyles.bold, @@ -534,7 +534,8 @@ class RenderEditableTextLine extends RenderEditableBox { double get cursorWidth => cursorCont.style.width; double get cursorHeight => - cursorCont.style.height ?? preferredLineHeight(TextPosition(offset: 0)); + cursorCont.style.height ?? + preferredLineHeight(const TextPosition(offset: 0)); void _computeCaretPrototype() { switch (defaultTargetPlatform) { diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 99f381fa..366c7d0b 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -58,7 +58,7 @@ class EditorTextSelectionOverlay { OverlayState overlay = Overlay.of(context, rootOverlay: true)!; _toolbarController = AnimationController( - duration: Duration(milliseconds: 150), vsync: overlay); + duration: const Duration(milliseconds: 150), vsync: overlay); } TextSelection get _selection => value.selection; @@ -143,13 +143,14 @@ class EditorTextSelectionOverlay { TextPosition textPosition; switch (position) { case _TextSelectionHandlePosition.START: - textPosition = - newSelection != null ? newSelection.base : TextPosition(offset: 0); + textPosition = newSelection != null + ? newSelection.base + : const TextPosition(offset: 0); break; case _TextSelectionHandlePosition.END: textPosition = newSelection != null ? newSelection.extent - : TextPosition(offset: 0); + : const TextPosition(offset: 0); break; default: throw ('Invalid position'); @@ -198,7 +199,7 @@ class EditorTextSelectionOverlay { endpoints, selectionDelegate, clipboardStatus, - Offset(0, 0)), + const Offset(0, 0)), ), ); } @@ -292,8 +293,8 @@ class _TextSelectionHandleOverlayState void initState() { super.initState(); - _controller = - AnimationController(duration: Duration(milliseconds: 150), vsync: this); + _controller = AnimationController( + duration: const Duration(milliseconds: 150), vsync: this); _handleVisibilityChanged(); widget._visibility!.addListener(_handleVisibilityChanged); @@ -579,7 +580,7 @@ class _EditorTextSelectionGestureDetectorState void _handleDragUpdate(DragUpdateDetails details) { _lastDragUpdateDetails = details; _dragUpdateThrottleTimer ??= - Timer(Duration(milliseconds: 50), _handleDragUpdateThrottled); + Timer(const Duration(milliseconds: 50), _handleDragUpdateThrottled); } void _handleDragUpdateThrottled() { diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 514946a3..bff3ff9e 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -114,7 +114,7 @@ class _LinkStyleButtonState extends State { showDialog( context: context, builder: (ctx) { - return _LinkDialog(); + return const _LinkDialog(); }, ).then(_linkSubmitted); } @@ -141,14 +141,14 @@ class _LinkDialogState extends State<_LinkDialog> { Widget build(BuildContext context) { return AlertDialog( content: TextField( - decoration: InputDecoration(labelText: 'Paste a link'), + decoration: const InputDecoration(labelText: 'Paste a link'), autofocus: true, onChanged: _linkChanged, ), actions: [ TextButton( onPressed: _link.isNotEmpty ? _applyLink : null, - child: Text('Apply'), + child: const Text('Apply'), ), ], ); @@ -430,7 +430,7 @@ class _SelectHeaderStyleButtonState extends State { Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, ValueChanged onSelected) { - final style = TextStyle(fontSize: 13); + final style = const TextStyle(fontSize: 13); final Map _valueToText = { Attribute.header: 'Normal text', @@ -478,7 +478,7 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, : (value.key == 'h2') ? Attribute.h2 : Attribute.h3]!, - style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600), + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), ), ); } @@ -725,7 +725,7 @@ class _ColorButtonState extends State { backgroundColor: Theme.of(context).canvasColor, content: SingleChildScrollView( child: MaterialPicker( - pickerColor: Color(0x00000000), + pickerColor: const Color(0x00000000), onColorChanged: _changeColor, ), )), @@ -939,7 +939,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { undo: false, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: showBoldButton, child: ToggleStyleButton( @@ -948,7 +948,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { controller: controller, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: showItalicButton, child: ToggleStyleButton( @@ -957,7 +957,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { controller: controller, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: showUnderLineButton, child: ToggleStyleButton( @@ -966,7 +966,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { controller: controller, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: showStrikeThrough, child: ToggleStyleButton( @@ -975,7 +975,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { controller: controller, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: showColorButton, child: ColorButton( @@ -984,7 +984,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { background: false, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: showBackgroundColorButton, child: ColorButton( @@ -993,7 +993,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { background: true, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: showClearFormat, child: ClearFormatButton( @@ -1001,7 +1001,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { controller: controller, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: onImagePickCallback != null, child: ImageButton( @@ -1011,7 +1011,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { onImagePickCallback: onImagePickCallback, ), ), - SizedBox(width: 0.6), + const SizedBox(width: 0.6), Visibility( visible: onImagePickCallback != null, child: ImageButton( @@ -1119,7 +1119,7 @@ class _QuillToolbarState extends State { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), constraints: BoxConstraints.tightFor(height: widget.preferredSize.height), color: Theme.of(context).canvasColor, child: CustomScrollView( @@ -1254,14 +1254,14 @@ class _QuillDropdownButtonState extends State> { Widget _buildContent(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints.tightFor(width: 110), + constraints: const BoxConstraints.tightFor(width: 110), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( children: [ widget.child, Expanded(child: Container()), - Icon(Icons.arrow_drop_down, size: 15) + const Icon(Icons.arrow_drop_down, size: 15) ], ), ), From dfeae914b169a65b92ef987aa2be0e70540ba58d Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 27 Mar 2021 19:17:22 +0100 Subject: [PATCH 106/306] Prefer const constructors in immutables (#117) --- analysis_options.yaml | 1 + lib/widgets/default_styles.dart | 2 +- lib/widgets/editor.dart | 2 +- lib/widgets/proxy.dart | 6 +++--- lib/widgets/raw_editor.dart | 2 +- lib/widgets/text_block.dart | 2 +- lib/widgets/text_line.dart | 2 +- lib/widgets/toolbar.dart | 15 ++++++++------- 8 files changed, 17 insertions(+), 15 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 74ce67a1..8f139635 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,3 +11,4 @@ linter: - avoid_print - avoid_redundant_argument_values - prefer_const_constructors + - prefer_const_constructors_in_immutables diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 2e3ad80c..5583307d 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -5,7 +5,7 @@ import 'package:tuple/tuple.dart'; class QuillStyles extends InheritedWidget { final DefaultStyles data; - QuillStyles({ + const QuillStyles({ required this.data, required Widget child, Key? key, diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 0913ffe0..84a4c9d5 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -169,7 +169,7 @@ class QuillEditor extends StatefulWidget { final ValueChanged? onLaunchUrl; final EmbedBuilder embedBuilder; - QuillEditor( + const QuillEditor( {required this.controller, required this.focusNode, required this.scrollController, diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index 7b918b9d..8717d12a 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -7,7 +7,7 @@ class BaselineProxy extends SingleChildRenderObjectWidget { final TextStyle? textStyle; final EdgeInsets? padding; - BaselineProxy({Key? key, Widget? child, this.textStyle, this.padding}) + const BaselineProxy({Key? key, Widget? child, this.textStyle, this.padding}) : super(key: key, child: child); @override @@ -73,7 +73,7 @@ class RenderBaselineProxy extends RenderProxyBox { } class EmbedProxy extends SingleChildRenderObjectWidget { - EmbedProxy(Widget child) : super(child: child); + const EmbedProxy(Widget child) : super(child: child); @override RenderEmbedProxy createRenderObject(BuildContext context) => @@ -145,7 +145,7 @@ class RichTextProxy extends SingleChildRenderObjectWidget { textHeightBehavior); } - RichTextProxy( + const RichTextProxy( RichText child, this.textStyle, this.textAlign, diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 9ba3587a..b922f2be 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -55,7 +55,7 @@ class RawEditor extends StatefulWidget { final ScrollPhysics? scrollPhysics; final EmbedBuilder embedBuilder; - RawEditor( + const RawEditor( Key key, this.controller, this.focusNode, diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 7edb3add..92b82ae5 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -61,7 +61,7 @@ class EditableTextBlock extends StatelessWidget { final CursorCont cursorCont; final Map indentLevelCounts; - EditableTextBlock( + const EditableTextBlock( this.block, this.textDirection, this.verticalSpacing, diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 73090786..1b2be8e6 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -206,7 +206,7 @@ class EditableTextLine extends RenderObjectWidget { final double devicePixelRatio; final CursorCont cursorCont; - EditableTextLine( + const EditableTextLine( this.line, this.leading, this.body, diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index bff3ff9e..47a490d8 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -182,7 +182,7 @@ class ToggleStyleButton extends StatefulWidget { final ToggleStyleButtonBuilder childBuilder; - ToggleStyleButton({ + const ToggleStyleButton({ required this.attribute, required this.icon, required this.controller, @@ -266,7 +266,7 @@ class ToggleCheckListButton extends StatefulWidget { final Attribute attribute; - ToggleCheckListButton({ + const ToggleCheckListButton({ required this.icon, required this.controller, required this.attribute, @@ -494,7 +494,7 @@ class ImageButton extends StatefulWidget { final ImageSource imageSource; - ImageButton({ + const ImageButton({ required this.icon, required this.controller, required this.imageSource, @@ -600,7 +600,7 @@ class ColorButton extends StatefulWidget { final bool background; final QuillController controller; - ColorButton({ + const ColorButton({ required this.icon, required this.controller, required this.background, @@ -738,7 +738,7 @@ class HistoryButton extends StatefulWidget { final bool undo; final QuillController controller; - HistoryButton({ + const HistoryButton({ required this.icon, required this.controller, required this.undo, @@ -810,7 +810,7 @@ class IndentButton extends StatefulWidget { final QuillController controller; final bool isIncrease; - IndentButton({ + const IndentButton({ required this.icon, required this.controller, required this.isIncrease, @@ -865,7 +865,8 @@ class ClearFormatButton extends StatefulWidget { final QuillController controller; - ClearFormatButton({required this.icon, required this.controller, Key? key}) + const ClearFormatButton( + {required this.icon, required this.controller, Key? key,}) : super(key: key); @override From 4c16af44207a61c088e478975034d194071ecb64 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 27 Mar 2021 19:20:26 +0100 Subject: [PATCH 107/306] Remove unnecessary parenthesis (#118) --- analysis_options.yaml | 1 + lib/models/documents/document.dart | 4 ++-- lib/models/documents/nodes/leaf.dart | 2 +- lib/models/quill_delta.dart | 2 +- lib/models/rules/rule.dart | 2 +- lib/utils/color.dart | 2 +- lib/utils/diff_delta.dart | 4 ++-- lib/widgets/editor.dart | 8 ++++---- lib/widgets/raw_editor.dart | 2 +- lib/widgets/text_block.dart | 4 ++-- lib/widgets/text_line.dart | 6 +++--- lib/widgets/text_selection.dart | 6 +++--- lib/widgets/toolbar.dart | 8 +++++--- 13 files changed, 27 insertions(+), 24 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 8f139635..306d335b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,3 +12,4 @@ linter: - avoid_redundant_argument_values - prefer_const_constructors - prefer_const_constructors_in_immutables + - unnecessary_parenthesis diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 126ff32b..0ff76faf 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -144,11 +144,11 @@ class Document { try { _delta = _delta.compose(delta); } catch (e) { - throw ('_delta compose failed'); + throw '_delta compose failed'; } if (_delta != _root.toDelta()) { - throw ('Compose failed'); + throw 'Compose failed'; } final change = Tuple3(originalDelta, delta, changeSource); _observer.add(change); diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index efb2ccde..6cb9af24 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -26,7 +26,7 @@ abstract class Leaf extends Node { @override void applyStyle(Style value) { - assert((value.isInline || value.isIgnored || value.isEmpty), + assert(value.isInline || value.isIgnored || value.isEmpty, 'Unable to apply Style to leaf: $value'); super.applyStyle(value); } diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index 72d26846..ddccb710 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -118,7 +118,7 @@ class Operation { bool get isRetain => key == Operation.retainKey; /// Returns `true` if this operation has no attributes, e.g. is plain text. - bool get isPlain => (_attributes == null || _attributes!.isEmpty); + bool get isPlain => _attributes == null || _attributes!.isEmpty; /// Returns `true` if this operation sets at least one attribute. bool get isNotPlain => !isPlain; diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index 8141aaf3..70a5aa74 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -67,6 +67,6 @@ class Rules { rethrow; } } - throw ('Apply rules failed'); + throw 'Apply rules failed'; } } diff --git a/lib/utils/color.dart b/lib/utils/color.dart index 4bc1c55a..4e206644 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -115,7 +115,7 @@ Color stringToColor(String? s) { } if (!s.startsWith('#')) { - throw ('Color code not supported'); + throw 'Color code not supported'; } String hex = s.replaceFirst('#', ''); diff --git a/lib/utils/diff_delta.dart b/lib/utils/diff_delta.dart index 8733c48f..127c2d8b 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -79,11 +79,11 @@ int getPositionDelta(Delta user, Delta actual) { Operation userOperation = userItr.next(length as int); Operation actualOperation = actualItr.next(length); if (userOperation.length != actualOperation.length) { - throw ('userOp ' + + throw 'userOp ' + userOperation.length.toString() + ' does not match ' + ' actualOp ' + - actualOperation.length.toString()); + actualOperation.length.toString(); } if (userOperation.key == actualOperation.key) { continue; diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 84a4c9d5..5bab5fb4 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -371,7 +371,7 @@ class _QuillEditorSelectionGestureDetectorBuilder ); break; default: - throw ('Invalid platform'); + throw 'Invalid platform'; } } @@ -522,7 +522,7 @@ class _QuillEditorSelectionGestureDetectorBuilder Feedback.forLongPress(_state.context); break; default: - throw ('Invalid platform'); + throw 'Invalid platform'; } } } @@ -964,7 +964,7 @@ class RenderEditableContainerBox extends RenderBox targetChild = childAfter(targetChild); } if (targetChild == null) { - throw ('targetChild should not be null'); + throw 'targetChild should not be null'; } return targetChild; } @@ -994,7 +994,7 @@ class RenderEditableContainerBox extends RenderBox dy += child.size.height; child = childAfter(child); } - throw ('No child'); + throw 'No child'; } @override diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index b922f2be..e0774208 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -650,7 +650,7 @@ class RawEditorState extends EditorState case 3: return defaultStyles!.h3!.verticalSpacing; default: - throw ('Invalid level $level'); + throw 'Invalid level $level'; } } diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 92b82ae5..7ea5b278 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -219,7 +219,7 @@ class EditableTextBlock extends StatelessWidget { bottom = defaultStyles.h3!.verticalSpacing.item2; break; default: - throw ('Invalid level $level'); + throw 'Invalid level $level'; } } else { late Tuple2 lineSpacing; @@ -497,7 +497,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox offset.translate(decorationPadding.left, decorationPadding.top); _painter!.paint(context.canvas, decorationOffset, filledConfiguration); if (debugSaveCount != context.canvas.getSaveCount()) { - throw ('${_decoration.runtimeType} painter had mismatching save and restore calls.'); + throw '${_decoration.runtimeType} painter had mismatching save and restore calls.'; } if (decoration.isComplex) { context.setIsComplexHint(); diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 1b2be8e6..0a9ef94e 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -552,7 +552,7 @@ class RenderEditableTextLine extends RenderEditableBox { Rect.fromLTWH(0.0, 2.0, cursorWidth, cursorHeight - 4.0); break; default: - throw ('Invalid platform'); + throw 'Invalid platform'; } } @@ -733,7 +733,7 @@ class RenderEditableTextLine extends RenderEditableBox { if (_body != null) { final parentData = _body!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; - if ((enableInteractiveSelection) && + if (enableInteractiveSelection && line.getDocumentOffset() <= textSelection.end && textSelection.start <= line.getDocumentOffset() + line.length - 1) { final local = localSelection(line, textSelection, false); @@ -862,7 +862,7 @@ class _TextLineElement extends RenderObjectElement { renderObject.setLeading(child); break; case TextLineSlot.BODY: - renderObject.setBody((child) as RenderContentProxyBox?); + renderObject.setBody(child as RenderContentProxyBox?); break; default: throw UnimplementedError(); diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 366c7d0b..8aa29d25 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -104,8 +104,8 @@ class EditorTextSelectionOverlay { Widget _buildHandle( BuildContext context, _TextSelectionHandlePosition position) { - if ((_selection.isCollapsed && - position == _TextSelectionHandlePosition.END)) { + if (_selection.isCollapsed && + position == _TextSelectionHandlePosition.END) { return Container(); } return Visibility( @@ -153,7 +153,7 @@ class EditorTextSelectionOverlay { : const TextPosition(offset: 0); break; default: - throw ('Invalid position'); + throw 'Invalid position'; } selectionDelegate.textEditingValue = value.copyWith(selection: newSelection, composing: TextRange.empty); diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 47a490d8..d3440cfa 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -865,9 +865,11 @@ class ClearFormatButton extends StatefulWidget { final QuillController controller; - const ClearFormatButton( - {required this.icon, required this.controller, Key? key,}) - : super(key: key); + const ClearFormatButton({ + required this.icon, + required this.controller, + Key? key, + }) : super(key: key); @override _ClearFormatButtonState createState() => _ClearFormatButtonState(); From 977f94b347f9812392f9bf3f0d6af04f98a4c93d Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 27 Mar 2021 19:39:27 +0100 Subject: [PATCH 108/306] Fix "Show toolbar" bug (#120) --- lib/widgets/raw_editor.dart | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index e0774208..d48fea6b 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1094,22 +1094,18 @@ class RawEditorState extends EditorState @override bool showToolbar() { + // Web is using native dom elements to enable clipboard functionality of the + // toolbar: copy, paste, select, cut. It might also provide additional + // functionality depending on the browser (such as translate). Due to this + // we should not show a Flutter toolbar for the editable text elements. + if (kIsWeb) { + return false; + } if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { - // Web is using native dom elements to enable clipboard functionality of the - // toolbar: copy, paste, select, cut. It might also provide additional - // functionality depending on the browser (such as translate). Due to this - // we should not show a Flutter toolbar for the editable text elements. - if (kIsWeb) { - return false; - } - - if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { - return false; - } - - _selectionOverlay!.showToolbar(); - return true; + return false; } + + _selectionOverlay!.showToolbar(); return true; } From 4ed236f6ed4f7d58f63c78a05bbb4b0a400d541d Mon Sep 17 00:00:00 2001 From: pengw00 Date: Sat, 27 Mar 2021 14:27:01 -0500 Subject: [PATCH 109/306] Release v1.1.4 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 667ea419..96d611ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.4] +* Fix text selection issue. + ## [1.1.3] * Update example folder. diff --git a/pubspec.yaml b/pubspec.yaml index 553ea465..437bd0b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web -version: 1.1.3 +version: 1.1.4 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 60431235bbb65bc6ac7a95ed5fe3bdab6a5da918 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 27 Mar 2021 12:50:44 -0700 Subject: [PATCH 110/306] Update description --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 437bd0b6..69bb7a14 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_quill -description: A rich text editor supporting mobile and web +description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) version: 1.1.4 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html From 2bf7b8b585a040bf0769ec443d501546ade14d26 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 28 Mar 2021 22:21:17 -0700 Subject: [PATCH 111/306] Enable "Select", "Select All" and "Copy" in read-only mode --- CHANGELOG.md | 3 +++ lib/widgets/editor.dart | 4 ++-- lib/widgets/raw_editor.dart | 4 ---- pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d611ee..84dcc862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.5] +* Enable "Select", "Select All" and "Copy" in read-only mode. + ## [1.1.4] * Fix text selection issue. diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 5bab5fb4..56b41641 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -277,10 +277,10 @@ class _QuillEditorState extends State widget.placeholder, widget.onLaunchUrl, ToolbarOptions( - copy: widget.enableInteractiveSelection, + copy: true, cut: widget.enableInteractiveSelection, paste: widget.enableInteractiveSelection, - selectAll: widget.enableInteractiveSelection, + selectAll: true, ), theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.android, diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index d48fea6b..c4b15c88 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -371,10 +371,6 @@ class RawEditorState extends EditorState _textInputConnection != null && _textInputConnection!.attached; void openConnectionIfNeeded() { - if (!shouldCreateInputConnection) { - return; - } - if (!hasConnection) { _lastKnownRemoteTextEditingValue = textEditingValue; _textInputConnection = TextInput.attach( diff --git a/pubspec.yaml b/pubspec.yaml index 69bb7a14..a06bd297 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.4 +version: 1.1.5 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 03e587b970ab51612d07adf7aefab7345ec558dd Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 28 Mar 2021 23:02:55 -0700 Subject: [PATCH 112/306] Upgrade universal_html --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index a06bd297..99a201aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: quiver: ^3.0.0 string_validator: ^0.3.0 tuple: ^2.0.0 - universal_html: ^2.0.4 + universal_html: ^2.0.7 url_launcher: ^6.0.2 pedantic: ^1.11.0 From 63a616e7ebea3d6a8b5aa8386fd7341cefe6f33a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 29 Mar 2021 01:20:00 -0700 Subject: [PATCH 113/306] Remove universal_html dependency --- example/lib/pages/home_page.dart | 88 +++++++---- example/lib/pages/read_only_page.dart | 33 ++-- .../lib}/universal_ui/fake_ui.dart | 0 .../lib}/universal_ui/real_ui.dart | 0 example/lib/universal_ui/universal_ui.dart | 57 +++++++ example/lib/widgets/field.dart | 142 ------------------ example/pubspec.yaml | 2 +- lib/utils/universal_ui/universal_ui.dart | 21 --- lib/widgets/editor.dart | 38 +---- pubspec.yaml | 1 - 10 files changed, 142 insertions(+), 240 deletions(-) rename {lib/utils => example/lib}/universal_ui/fake_ui.dart (100%) rename {lib/utils => example/lib}/universal_ui/real_ui.dart (100%) create mode 100644 example/lib/universal_ui/universal_ui.dart delete mode 100644 example/lib/widgets/field.dart delete mode 100644 lib/utils/universal_ui/universal_ui.dart diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 9e91cde4..65c2f165 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -2,6 +2,7 @@ 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'; @@ -38,13 +39,13 @@ class _HomePageState extends State { final doc = Document.fromJson(jsonDecode(result)); setState(() { _controller = QuillController( - document: doc, selection: TextSelection.collapsed(offset: 0)); + document: doc, selection: const TextSelection.collapsed(offset: 0)); }); } catch (error) { final doc = Document()..insert(0, 'Empty asset'); setState(() { _controller = QuillController( - document: doc, selection: TextSelection.collapsed(offset: 0)); + document: doc, selection: const TextSelection.collapsed(offset: 0)); }); } } @@ -52,7 +53,7 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { if (_controller == null) { - return Scaffold(body: Center(child: Text('Loading...'))); + return const Scaffold(body: Center(child: Text('Loading...'))); } return Scaffold( @@ -60,7 +61,7 @@ class _HomePageState extends State { backgroundColor: Colors.grey.shade800, elevation: 0, centerTitle: false, - title: Text( + title: const Text( 'Flutter Quill', ), actions: [], @@ -93,6 +94,55 @@ class _HomePageState extends State { } Widget _buildWelcomeEditor(BuildContext context) { + var quillEditor = QuillEditor( + controller: _controller!, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: false, + readOnly: false, + placeholder: 'Add content', + expands: false, + padding: EdgeInsets.zero, + customStyles: DefaultStyles( + h1: DefaultTextBlockStyle( + const TextStyle( + fontSize: 32.0, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w300, + ), + const Tuple2(16.0, 0.0), + const Tuple2(0.0, 0.0), + null), + sizeSmall: const TextStyle(fontSize: 9.0), + )); + if (kIsWeb) { + quillEditor = QuillEditor( + controller: _controller!, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: false, + readOnly: false, + placeholder: 'Add content', + expands: false, + padding: EdgeInsets.zero, + customStyles: DefaultStyles( + h1: DefaultTextBlockStyle( + const TextStyle( + fontSize: 32.0, + color: Colors.black, + height: 1.15, + fontWeight: FontWeight.w300, + ), + const Tuple2(16.0, 0.0), + const Tuple2(0.0, 0.0), + null), + sizeSmall: const TextStyle(fontSize: 9.0), + ), + embedBuilder: defaultEmbedBuilderWeb); + } return SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -102,36 +152,14 @@ class _HomePageState extends State { child: Container( color: Colors.white, padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: QuillEditor( - controller: _controller!, - scrollController: ScrollController(), - scrollable: true, - focusNode: _focusNode, - autoFocus: false, - readOnly: false, - placeholder: 'Add content', - expands: false, - padding: EdgeInsets.zero, - customStyles: DefaultStyles( - h1: DefaultTextBlockStyle( - TextStyle( - fontSize: 32.0, - color: Colors.black, - height: 1.15, - fontWeight: FontWeight.w300, - ), - Tuple2(16.0, 0.0), - Tuple2(0.0, 0.0), - null), - sizeSmall: TextStyle(fontSize: 9.0), - ), - ), + child: quillEditor, ), ), kIsWeb ? Expanded( child: Container( - padding: EdgeInsets.symmetric(vertical: 16, horizontal: 8), + padding: + const EdgeInsets.symmetric(vertical: 16, horizontal: 8), child: QuillToolbar.basic( controller: _controller!, onImagePickCallback: _onImagePickCallback), @@ -158,7 +186,7 @@ class _HomePageState extends State { Widget _buildMenuBar(BuildContext context) { Size size = MediaQuery.of(context).size; - final itemStyle = TextStyle( + final itemStyle = const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index 6e2ec0e0..e87e4f8f 100644 --- a/example/lib/pages/read_only_page.dart +++ b/example/lib/pages/read_only_page.dart @@ -1,3 +1,5 @@ +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'; @@ -28,15 +30,19 @@ class _ReadOnlyPageState extends State { } Widget _buildContent(BuildContext context, QuillController? controller) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade200), - ), - child: QuillEditor( - controller: controller!, + var quillEditor = QuillEditor( + controller: controller!, + scrollController: ScrollController(), + scrollable: true, + focusNode: _focusNode, + autoFocus: true, + readOnly: !_edit, + expands: false, + padding: EdgeInsets.zero, + ); + if (kIsWeb) { + quillEditor = QuillEditor( + controller: controller, scrollController: ScrollController(), scrollable: true, focusNode: _focusNode, @@ -44,7 +50,16 @@ class _ReadOnlyPageState extends State { readOnly: !_edit, expands: false, padding: EdgeInsets.zero, + embedBuilder: defaultEmbedBuilderWeb); + } + return Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), ), + child: quillEditor, ), ); } diff --git a/lib/utils/universal_ui/fake_ui.dart b/example/lib/universal_ui/fake_ui.dart similarity index 100% rename from lib/utils/universal_ui/fake_ui.dart rename to example/lib/universal_ui/fake_ui.dart diff --git a/lib/utils/universal_ui/real_ui.dart b/example/lib/universal_ui/real_ui.dart similarity index 100% rename from lib/utils/universal_ui/real_ui.dart rename to example/lib/universal_ui/real_ui.dart diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart new file mode 100644 index 00000000..15eeb9f8 --- /dev/null +++ b/example/lib/universal_ui/universal_ui.dart @@ -0,0 +1,57 @@ +library universal_ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf; +import 'package:flutter_quill/widgets/responsive_widget.dart'; +import 'package:universal_html/html.dart' as html; + +import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; + +class PlatformViewRegistryFix { + void registerViewFactory(dynamic x, dynamic y) { + if (kIsWeb) { + ui_instance.PlatformViewRegistry.registerViewFactory( + x, + y, + ); + } + } +} + +class UniversalUI { + PlatformViewRegistryFix platformViewRegistry = PlatformViewRegistryFix(); +} + +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; + UniversalUI().platformViewRegistry.registerViewFactory( + imageUrl, (int viewId) => html.ImageElement()..src = imageUrl); + return Padding( + padding: EdgeInsets.only( + right: ResponsiveWidget.isMediumScreen(context) + ? size.width * 0.5 + : (ResponsiveWidget.isLargeScreen(context)) + ? size.width * 0.75 + : size.width * 0.2, + ), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.45, + child: HtmlElementView( + viewType: imageUrl, + ), + ), + ); + + default: + throw UnimplementedError( + 'Embeddable type "${node.value.type}" is not supported by default embed ' + 'builder of QuillEditor. You must pass your own builder function to ' + 'embedBuilder property of QuillEditor or QuillField widgets.'); + } +} diff --git a/example/lib/widgets/field.dart b/example/lib/widgets/field.dart deleted file mode 100644 index 5a6badfe..00000000 --- a/example/lib/widgets/field.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_quill/widgets/controller.dart'; -import 'package:flutter_quill/widgets/delegate.dart'; -import 'package:flutter_quill/widgets/editor.dart'; - -class QuillField extends StatefulWidget { - final QuillController controller; - final FocusNode? focusNode; - final ScrollController? scrollController; - final bool scrollable; - final EdgeInsetsGeometry padding; - final bool autofocus; - final bool showCursor; - final bool readOnly; - final bool enableInteractiveSelection; - final double? minHeight; - final double? maxHeight; - final bool expands; - final TextCapitalization textCapitalization; - final Brightness keyboardAppearance; - final ScrollPhysics? scrollPhysics; - final ValueChanged? onLaunchUrl; - final InputDecoration? decoration; - final Widget? toolbar; - final EmbedBuilder? embedBuilder; - - QuillField({ - Key? key, - required this.controller, - this.focusNode, - this.scrollController, - this.scrollable = true, - this.padding = EdgeInsets.zero, - this.autofocus = false, - this.showCursor = true, - this.readOnly = false, - this.enableInteractiveSelection = true, - this.minHeight, - this.maxHeight, - this.expands = false, - this.textCapitalization = TextCapitalization.sentences, - this.keyboardAppearance = Brightness.light, - this.scrollPhysics, - this.onLaunchUrl, - this.decoration, - this.toolbar, - this.embedBuilder, - }) : super(key: key); - - @override - _QuillFieldState createState() => _QuillFieldState(); -} - -class _QuillFieldState extends State { - late bool _focused; - - void _editorFocusChanged() { - setState(() { - _focused = widget.focusNode!.hasFocus; - }); - } - - @override - void initState() { - super.initState(); - _focused = widget.focusNode!.hasFocus; - widget.focusNode!.addListener(_editorFocusChanged); - } - - @override - void didUpdateWidget(covariant QuillField oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.focusNode != oldWidget.focusNode) { - oldWidget.focusNode!.removeListener(_editorFocusChanged); - widget.focusNode!.addListener(_editorFocusChanged); - _focused = widget.focusNode!.hasFocus; - } - } - - @override - Widget build(BuildContext context) { - Widget child = QuillEditor( - controller: widget.controller, - focusNode: widget.focusNode!, - scrollController: widget.scrollController!, - scrollable: widget.scrollable, - padding: widget.padding, - autoFocus: widget.autofocus, - showCursor: widget.showCursor, - readOnly: widget.readOnly, - enableInteractiveSelection: widget.enableInteractiveSelection, - minHeight: widget.minHeight, - maxHeight: widget.maxHeight, - expands: widget.expands, - textCapitalization: widget.textCapitalization, - keyboardAppearance: widget.keyboardAppearance, - scrollPhysics: widget.scrollPhysics, - onLaunchUrl: widget.onLaunchUrl, - embedBuilder: widget.embedBuilder!, - ); - - if (widget.toolbar != null) { - child = Column( - children: [ - child, - Visibility( - visible: _focused, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: widget.toolbar!, - ), - ], - ); - } - - return AnimatedBuilder( - animation: - Listenable.merge([widget.focusNode, widget.controller]), - builder: (BuildContext context, Widget? child) { - return InputDecorator( - decoration: _getEffectiveDecoration(), - isFocused: widget.focusNode!.hasFocus, - // TODO: Document should be considered empty of it has single empty line with no styles applied - isEmpty: widget.controller.document.length == 1, - child: child, - ); - }, - child: child, - ); - } - - InputDecoration _getEffectiveDecoration() { - return (widget.decoration ?? const InputDecoration()) - .applyDefaults(Theme.of(context).inputDecorationTheme) - .copyWith( - enabled: !widget.readOnly, - hintMaxLines: widget.decoration?.hintMaxLines, - ); - } -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3a31d347..e1bd45f5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -23,7 +23,7 @@ environment: dependencies: flutter: sdk: flutter - + universal_html: ^2.0.7 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/lib/utils/universal_ui/universal_ui.dart b/lib/utils/universal_ui/universal_ui.dart deleted file mode 100644 index 5fe7c428..00000000 --- a/lib/utils/universal_ui/universal_ui.dart +++ /dev/null @@ -1,21 +0,0 @@ -library universal_ui; - -import 'package:flutter/foundation.dart'; -import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; - -class PlatformViewRegistryFix { - void registerViewFactory(dynamic x, dynamic y) { - if (kIsWeb) { - ui_instance.PlatformViewRegistry.registerViewFactory( - x, - y, - ); - } - } -} - -class UniversalUI { - PlatformViewRegistryFix platformViewRegistry = PlatformViewRegistryFix(); -} - -var ui = UniversalUI(); diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 56b41641..df74e2e7 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -16,13 +16,10 @@ 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/utils/universal_ui/universal_ui.dart'; import 'package:flutter_quill/widgets/image.dart'; import 'package:flutter_quill/widgets/raw_editor.dart'; -import 'package:flutter_quill/widgets/responsive_widget.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; import 'package:string_validator/string_validator.dart'; -import 'package:universal_html/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; import 'box.dart'; @@ -101,6 +98,7 @@ String _standardizeImageUrl(String url) { } 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); @@ -117,37 +115,6 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { } } -Widget _defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) { - switch (node.value.type) { - case 'image': - String imageUrl = node.value.data; - Size size = MediaQuery.of(context).size; - UniversalUI().platformViewRegistry.registerViewFactory( - imageUrl, (int viewId) => html.ImageElement()..src = imageUrl); - return Padding( - padding: EdgeInsets.only( - right: ResponsiveWidget.isMediumScreen(context) - ? size.width * 0.5 - : (ResponsiveWidget.isLargeScreen(context)) - ? size.width * 0.75 - : size.width * 0.2, - ), - child: SizedBox( - height: MediaQuery.of(context).size.height * 0.45, - child: HtmlElementView( - viewType: imageUrl, - ), - ), - ); - - default: - throw UnimplementedError( - 'Embeddable type "${node.value.type}" is not supported by default embed ' - 'builder of QuillEditor. You must pass your own builder function to ' - 'embedBuilder property of QuillEditor or QuillField widgets.'); - } -} - class QuillEditor extends StatefulWidget { final QuillController controller; final FocusNode focusNode; @@ -188,8 +155,7 @@ class QuillEditor extends StatefulWidget { this.keyboardAppearance = Brightness.light, this.scrollPhysics, this.onLaunchUrl, - this.embedBuilder = - kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder}); + this.embedBuilder = _defaultEmbedBuilder}); factory QuillEditor.basic( {required QuillController controller, required bool readOnly}) { diff --git a/pubspec.yaml b/pubspec.yaml index 99a201aa..41f9fb06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: quiver: ^3.0.0 string_validator: ^0.3.0 tuple: ^2.0.0 - universal_html: ^2.0.7 url_launcher: ^6.0.2 pedantic: ^1.11.0 From 96cb7effc66471a06b9e25dcaa4492cfa38b25b7 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 29 Mar 2021 01:25:49 -0700 Subject: [PATCH 114/306] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 18172233..b7c1a247 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ The `QuillToolbar` class lets you customise which formatting options are availab For web development, use `flutter config --enable-web` for flutter and use [ReactQuill] for React. +It is required to provide EmbedBuilder, e.g. [defaultEmbedBuilderWeb](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/universal_ui/universal_ui.dart#L28). + ## Migrate Zefyr Data Check out [code](https://github.com/jwehrle/zefyr_quill_convert) and [doc](https://docs.google.com/document/d/1FUSrpbarHnilb7uDN5J5DDahaI0v1RMXBjj4fFSpSuY/edit?usp=sharing). From 30bd6342ead137c6d810a18c97c3b6cf60858a8a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 29 Mar 2021 01:37:20 -0700 Subject: [PATCH 115/306] Upgrade to 1.1.6 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84dcc862..bee97937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.6] +* Remove universal_html dependency. + ## [1.1.5] * Enable "Select", "Select All" and "Copy" in read-only mode. diff --git a/pubspec.yaml b/pubspec.yaml index 41f9fb06..040d3d33 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.5 +version: 1.1.6 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 5b9fb5280446cf7fe0fb5391ea270461f864396e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 29 Mar 2021 01:47:23 -0700 Subject: [PATCH 116/306] Make showCursor default to true --- lib/widgets/raw_editor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index c4b15c88..b5139bc5 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -85,7 +85,7 @@ class RawEditor extends StatefulWidget { assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, 'maxHeight cannot be null'), - showCursor = showCursor ?? !readOnly, + showCursor = showCursor ?? true, super(key: key); @override From f0dcca00d90cd75d7165c0daf803523c81559743 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 29 Mar 2021 02:02:48 -0700 Subject: [PATCH 117/306] Readonly mode - temporary hack to dismiss keyboard --- lib/widgets/raw_editor.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index b5139bc5..9d2a5fe4 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -379,6 +379,7 @@ class RawEditorState extends EditorState inputType: TextInputType.multiline, readOnly: widget.readOnly, inputAction: TextInputAction.newline, + enableSuggestions: !widget.readOnly, keyboardAppearance: widget.keyboardAppearance, textCapitalization: widget.textCapitalization, ), @@ -388,6 +389,10 @@ class RawEditorState extends EditorState // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); } _textInputConnection!.show(); + if (widget.readOnly) { + // temporary hack to dismiss keyboard + SystemChannels.textInput.invokeMethod('TextInput.hide'); + } } void closeConnectionIfNeeded() { From 047d0f729b4d1e2074fe74148d19f69ed1e73f46 Mon Sep 17 00:00:00 2001 From: hyouuu Date: Mon, 29 Mar 2021 15:07:12 -0700 Subject: [PATCH 118/306] default color, Document.fromDelta, fix isItCut null check (#123) * default color, Document.fromDelta, fix isItCut null check * fix merge conflict --- lib/models/documents/document.dart | 4 ++++ lib/widgets/default_styles.dart | 3 +++ lib/widgets/raw_editor.dart | 6 ++++-- lib/widgets/text_line.dart | 9 +++++++-- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 0ff76faf..8a620446 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -43,6 +43,10 @@ class Document { _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); diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 5583307d..4f26d9d3 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -52,6 +52,7 @@ class DefaultStyles { final TextStyle? sizeLarge; // 'large' final TextStyle? sizeHuge; // 'huge' final TextStyle? link; + final Color? color; final DefaultTextBlockStyle? placeHolder; final DefaultTextBlockStyle? lists; final DefaultTextBlockStyle? quote; @@ -69,6 +70,7 @@ class DefaultStyles { this.underline, this.strikeThrough, this.link, + this.color, this.placeHolder, this.lists, this.quote, @@ -197,6 +199,7 @@ class DefaultStyles { underline: other.underline ?? underline, strikeThrough: other.strikeThrough ?? strikeThrough, link: other.link ?? link, + color: other.color ?? color, placeHolder: other.placeHolder ?? placeHolder, lists: other.lists ?? lists, quote: other.quote ?? quote, diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 9d2a5fe4..130af8b4 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1087,8 +1087,10 @@ class RawEditorState extends EditorState } Future __isItCut(TextEditingValue value) async { - final ClipboardData data = await (Clipboard.getData(Clipboard.kTextPlain) - as FutureOr); + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + if (data == null) { + return false; + } return textEditingValue.text.length - value.text.length == data.text!.length; } diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 0a9ef94e..0b6788cf 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -165,8 +165,13 @@ class TextLine extends StatelessWidget { Attribute? color = textNode.style.attributes[Attribute.color.key]; if (color != null && color.value != null) { - final textColor = stringToColor(color.value); - res = res.merge(TextStyle(color: textColor)); + var textColor = defaultStyles.color; + if (color.value is String) { + textColor = stringToColor(color.value); + } + if (textColor != null) { + res = res.merge(TextStyle(color: textColor)); + } } Attribute? background = textNode.style.attributes[Attribute.background.key]; From 8e845c5b0185f87253f140be244ed1180b0ce197 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 30 Mar 2021 23:30:16 -0700 Subject: [PATCH 119/306] Update issue templates --- .github/ISSUE_TEMPLATE/issue-template.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/issue-template.md diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md new file mode 100644 index 00000000..1b5e77ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -0,0 +1,12 @@ +--- +name: Issue template +about: Common things to fill +title: "[Web] or [Mobile] or [Desktop]" +labels: '' +assignees: '' + +--- + +My issue is about [Web] +My issue is about [Mobile] +My issue is about [Desktop] From 2e87ac922a74018d7b1cd059acce6dbba5e0ae2a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 30 Mar 2021 23:34:46 -0700 Subject: [PATCH 120/306] Update issue templates --- .github/ISSUE_TEMPLATE/issue-template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md index 1b5e77ed..1dfda1d3 100644 --- a/.github/ISSUE_TEMPLATE/issue-template.md +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -10,3 +10,5 @@ assignees: '' 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. From 7791364c5a1ba71f4eec77cbcd2335eaead6c1e8 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 2 Apr 2021 00:03:19 -0700 Subject: [PATCH 121/306] Fix warnings in example --- example/lib/widgets/demo_scaffold.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 1533da28..52d44e4f 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -19,12 +19,12 @@ class DemoScaffold extends StatefulWidget { final bool showToolbar; const DemoScaffold({ - Key? key, required this.documentFilename, required this.builder, this.actions, this.showToolbar = true, this.floatingActionButton, + Key? key, }) : super(key: key); @override @@ -59,14 +59,14 @@ class _DemoScaffoldState extends State { final doc = Document.fromJson(jsonDecode(result)); setState(() { _controller = QuillController( - document: doc, selection: TextSelection.collapsed(offset: 0)); + document: doc, selection: const TextSelection.collapsed(offset: 0)); _loading = false; }); } catch (error) { final doc = Document()..insert(0, 'Empty asset'); setState(() { _controller = QuillController( - document: doc, selection: TextSelection.collapsed(offset: 0)); + document: doc, selection: const TextSelection.collapsed(offset: 0)); _loading = false; }); } @@ -97,7 +97,7 @@ class _DemoScaffoldState extends State { ), floatingActionButton: widget.floatingActionButton, body: _loading - ? Center(child: Text('Loading...')) + ? const Center(child: Text('Loading...')) : widget.builder(context, _controller), ); } From 53eaf34ad95e497432ae28179227174c425829c1 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 2 Apr 2021 00:10:21 -0700 Subject: [PATCH 122/306] Revert change in openConnectionIfNeeded for readonly mode --- lib/widgets/raw_editor.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 130af8b4..7dbd46ed 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -371,6 +371,10 @@ class RawEditorState extends EditorState _textInputConnection != null && _textInputConnection!.attached; void openConnectionIfNeeded() { + if (!shouldCreateInputConnection) { + return; + } + if (!hasConnection) { _lastKnownRemoteTextEditingValue = textEditingValue; _textInputConnection = TextInput.attach( @@ -388,11 +392,8 @@ class RawEditorState extends EditorState _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); } + _textInputConnection!.show(); - if (widget.readOnly) { - // temporary hack to dismiss keyboard - SystemChannels.textInput.invokeMethod('TextInput.hide'); - } } void closeConnectionIfNeeded() { From 8ab586c318b64e289a8556ea2725e584e728d80b Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 2 Apr 2021 19:32:15 -0700 Subject: [PATCH 123/306] Fix text selection in read-only mode --- CHANGELOG.md | 3 +++ lib/widgets/raw_editor.dart | 7 +++++++ pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bee97937..3550e11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.1.7] +* Fix text selection in read-only mode. + ## [1.1.6] * Remove universal_html dependency. diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 7dbd46ed..1b649274 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -898,6 +898,12 @@ class RawEditorState extends EditorState _onChangeTextEditingValue(); } else { requestKeyboard(); + if (mounted) { + setState(() { + // Use widget.controller.value in build() + // Trigger build and updateChildren + }); + } } } @@ -1109,6 +1115,7 @@ class RawEditorState extends EditorState return false; } + _selectionOverlay!.update(textEditingValue); _selectionOverlay!.showToolbar(); return true; } diff --git a/pubspec.yaml b/pubspec.yaml index 040d3d33..504f96d1 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.6 +version: 1.1.7 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 084adcb1e798a7ace469b6192d898c9371680742 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 2 Apr 2021 21:07:48 -0700 Subject: [PATCH 124/306] Refactor code --- lib/widgets/raw_editor.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 1b649274..6a24f39c 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -919,11 +919,12 @@ class RawEditorState extends EditorState SchedulerBinding.instance!.addPostFrameCallback( (Duration _) => _updateOrDisposeSelectionOverlayIfNeeded()); - if (!mounted) return; - setState(() { - // Use widget.controller.value in build() - // Trigger build and updateChildren - }); + if (mounted) { + setState(() { + // Use widget.controller.value in build() + // Trigger build and updateChildren + }); + } } void _updateOrDisposeSelectionOverlayIfNeeded() { From 2f550d5e85ba6b16da139727c087c0d79db122fc Mon Sep 17 00:00:00 2001 From: Gagan Yadav Date: Sat, 3 Apr 2021 13:11:18 +0530 Subject: [PATCH 125/306] Replace header dropdown with buttons (#132) * Replace header dropdown with buttons * variable name fix and code format Co-authored-by: Gagan --- lib/widgets/toolbar.dart | 110 +++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index d3440cfa..8e6fbada 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -370,8 +370,10 @@ Widget defaultToggleStyleButtonBuilder( class SelectHeaderStyleButton extends StatefulWidget { final QuillController controller; + final double headerFontSize; - const SelectHeaderStyleButton({required this.controller, Key? key}) + const SelectHeaderStyleButton( + {required this.controller, this.headerFontSize = 18.0, Key? key}) : super(key: key); @override @@ -424,62 +426,59 @@ class _SelectHeaderStyleButtonState extends State { @override Widget build(BuildContext context) { - return _selectHeadingStyleButtonBuilder(context, _value, _selectAttribute); + return _selectHeadingStyleButtonBuilder( + context, _value, _selectAttribute, widget.headerFontSize); } } Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, - ValueChanged onSelected) { - final style = const TextStyle(fontSize: 13); - + ValueChanged onSelected, double headerFontSize) { final Map _valueToText = { - Attribute.header: 'Normal text', - Attribute.h1: 'Heading 1', - Attribute.h2: 'Heading 2', - Attribute.h3: 'Heading 3', + Attribute.header: 'N', + Attribute.h1: 'H1', + Attribute.h2: 'H2', + Attribute.h3: 'H3', }; - return QuillDropdownButton( - highlightElevation: 0, - hoverElevation: 0, - height: iconSize * 1.77, - fillColor: Theme.of(context).canvasColor, - initialValue: value, - items: [ - PopupMenuItem( - value: Attribute.header, - height: iconSize * 1.77, - child: Text(_valueToText[Attribute.header]!, style: style), - ), - PopupMenuItem( - value: Attribute.h1, - height: iconSize * 1.77, - child: Text(_valueToText[Attribute.h1]!, style: style), - ), - PopupMenuItem( - value: Attribute.h2, - height: iconSize * 1.77, - child: Text(_valueToText[Attribute.h2]!, style: style), - ), - PopupMenuItem( - value: Attribute.h3, - height: iconSize * 1.77, - child: Text(_valueToText[Attribute.h3]!, style: style), - ), - ], - onSelected: onSelected, - child: Text( - !kIsWeb - ? _valueToText[value!]! - : _valueToText[value!.key == 'header' - ? Attribute.header - : (value.key == 'h1') - ? Attribute.h1 - : (value.key == 'h2') - ? Attribute.h2 - : Attribute.h3]!, - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), - ), + List _valueAttribute = [ + Attribute.header, + Attribute.h1, + Attribute.h2, + Attribute.h3 + ]; + List _valueString = ['N', 'H1', 'H2', 'H3']; + + final theme = Theme.of(context); + final style = theme.textTheme.caption?.copyWith( + fontWeight: FontWeight.bold, + fontSize: (15.0 / iconSize) * headerFontSize, + ); + final width = theme.buttonTheme.constraints.minHeight + 4.0; + final constraints = theme.buttonTheme.constraints.copyWith( + minWidth: width, + maxHeight: theme.buttonTheme.constraints.minHeight, + ); + final radius = const BorderRadius.all(Radius.circular(3.0)); + + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(4, (index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 6.0), + child: RawMaterialButton( + shape: RoundedRectangleBorder(borderRadius: radius), + elevation: 0.0, + fillColor: _valueToText[value] == _valueString[index] + ? Theme.of(context).accentColor.withOpacity(0.4) + : Colors.white, + constraints: constraints, + onPressed: () { + onSelected(_valueAttribute[index]); + }, + child: Text(_valueString[index], style: style), + ), + ); + }), ); } @@ -1027,11 +1026,12 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { Visibility( visible: showHeaderStyle, child: VerticalDivider( - indent: 16, endIndent: 16, color: Colors.grey.shade400)), + indent: 12, endIndent: 12, color: Colors.grey.shade400)), Visibility( visible: showHeaderStyle, - child: SelectHeaderStyleButton(controller: controller)), - VerticalDivider(indent: 16, endIndent: 16, color: Colors.grey.shade400), + child: SelectHeaderStyleButton( + controller: controller, headerFontSize: toolbarIconSize)), + VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400), Visibility( visible: showListNumbers, child: ToggleStyleButton( @@ -1070,7 +1070,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { !showListCheck && !showCodeBlock, child: VerticalDivider( - indent: 16, endIndent: 16, color: Colors.grey.shade400)), + indent: 12, endIndent: 12, color: Colors.grey.shade400)), Visibility( visible: showQuote, child: ToggleStyleButton( @@ -1098,7 +1098,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { Visibility( visible: showQuote, child: VerticalDivider( - indent: 16, endIndent: 16, color: Colors.grey.shade400)), + indent: 12, endIndent: 12, color: Colors.grey.shade400)), Visibility( visible: showLink, child: LinkStyleButton(controller: controller)), Visibility( From 96b5be1518e9ff4ef7b7ae19663eb88e2d22af01 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 3 Apr 2021 00:54:25 -0700 Subject: [PATCH 126/306] Add style for leading --- lib/widgets/default_styles.dart | 5 +++++ lib/widgets/text_block.dart | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 4f26d9d3..9490bebd 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -59,6 +59,7 @@ class DefaultStyles { final DefaultTextBlockStyle? code; final DefaultTextBlockStyle? indent; final DefaultTextBlockStyle? align; + final DefaultTextBlockStyle? leading; DefaultStyles( {this.h1, @@ -77,6 +78,7 @@ class DefaultStyles { this.code, this.indent, this.align, + this.leading, this.sizeSmall, this.sizeLarge, this.sizeHuge}); @@ -183,6 +185,8 @@ class DefaultStyles { baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null), align: DefaultTextBlockStyle( baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 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)); @@ -206,6 +210,7 @@ class DefaultStyles { code: other.code ?? code, indent: other.indent ?? indent, align: other.align ?? align, + leading: other.leading ?? leading, sizeSmall: other.sizeSmall ?? sizeSmall, sizeLarge: other.sizeLarge ?? sizeLarge, sizeHuge: other.sizeHuge ?? sizeHuge); diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 7ea5b278..85912431 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -141,7 +141,7 @@ class EditableTextBlock extends StatelessWidget { index: index, indentLevelCounts: indentLevelCounts, count: count, - style: defaultStyles!.paragraph!.style, + style: defaultStyles!.leading!.style, attrs: attrs, width: 32.0, padding: 8.0, @@ -150,20 +150,20 @@ class EditableTextBlock extends StatelessWidget { if (attrs[Attribute.list.key] == Attribute.ul) { return _BulletPoint( - style: defaultStyles!.paragraph!.style - .copyWith(fontWeight: FontWeight.bold), + style: + defaultStyles!.leading!.style.copyWith(fontWeight: FontWeight.bold), width: 32, ); } if (attrs[Attribute.list.key] == Attribute.checked) { return _Checkbox( - style: defaultStyles!.paragraph!.style, width: 32, isChecked: true); + style: defaultStyles!.leading!.style, width: 32, isChecked: true); } if (attrs[Attribute.list.key] == Attribute.unchecked) { return _Checkbox( - style: defaultStyles!.paragraph!.style, width: 32, isChecked: false); + style: defaultStyles!.leading!.style, width: 32, isChecked: false); } if (attrs.containsKey(Attribute.codeBlock.key)) { From 2cbc99287b37165bc343270ec641a136e4c4d0ad Mon Sep 17 00:00:00 2001 From: Gagan Yadav Date: Sat, 3 Apr 2021 22:39:50 +0530 Subject: [PATCH 127/306] Match new header buttons UI to ToggleStyleButtons. (#135) --- lib/widgets/toolbar.dart | 64 ++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 8e6fbada..6b4b4147 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -370,10 +370,8 @@ Widget defaultToggleStyleButtonBuilder( class SelectHeaderStyleButton extends StatefulWidget { final QuillController controller; - final double headerFontSize; - const SelectHeaderStyleButton( - {required this.controller, this.headerFontSize = 18.0, Key? key}) + const SelectHeaderStyleButton({required this.controller, Key? key}) : super(key: key); @override @@ -426,13 +424,12 @@ class _SelectHeaderStyleButtonState extends State { @override Widget build(BuildContext context) { - return _selectHeadingStyleButtonBuilder( - context, _value, _selectAttribute, widget.headerFontSize); + return _selectHeadingStyleButtonBuilder(context, _value, _selectAttribute); } } Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, - ValueChanged onSelected, double headerFontSize) { + ValueChanged onSelected) { final Map _valueToText = { Attribute.header: 'N', Attribute.h1: 'H1', @@ -449,33 +446,43 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, List _valueString = ['N', 'H1', 'H2', 'H3']; final theme = Theme.of(context); - final style = theme.textTheme.caption?.copyWith( - fontWeight: FontWeight.bold, - fontSize: (15.0 / iconSize) * headerFontSize, + final style = TextStyle( + fontWeight: FontWeight.w600, + fontSize: iconSize * 0.7, ); - final width = theme.buttonTheme.constraints.minHeight + 4.0; - final constraints = theme.buttonTheme.constraints.copyWith( - minWidth: width, - maxHeight: theme.buttonTheme.constraints.minHeight, - ); - final radius = const BorderRadius.all(Radius.circular(3.0)); return Row( mainAxisSize: MainAxisSize.min, children: List.generate(4, (index) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 6.0), - child: RawMaterialButton( - shape: RoundedRectangleBorder(borderRadius: radius), - elevation: 0.0, - fillColor: _valueToText[value] == _valueString[index] - ? Theme.of(context).accentColor.withOpacity(0.4) - : Colors.white, - constraints: constraints, - onPressed: () { - onSelected(_valueAttribute[index]); - }, - child: Text(_valueString[index], style: style), + padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), + child: ConstrainedBox( + constraints: BoxConstraints.tightFor( + width: iconSize * 1.77, + height: iconSize * 1.77, + ), + child: RawMaterialButton( + hoverElevation: 0, + highlightElevation: 0, + elevation: 0.0, + visualDensity: VisualDensity.compact, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), + fillColor: _valueToText[value] == _valueString[index] + ? theme.toggleableActiveColor + : theme.canvasColor, + onPressed: () { + onSelected(_valueAttribute[index]); + }, + child: Text( + _valueString[index], + style: style.copyWith( + color: _valueToText[value] == _valueString[index] + ? theme.primaryIconTheme.color + : theme.iconTheme.color, + ), + ), + ), ), ); }), @@ -1029,8 +1036,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { indent: 12, endIndent: 12, color: Colors.grey.shade400)), Visibility( visible: showHeaderStyle, - child: SelectHeaderStyleButton( - controller: controller, headerFontSize: toolbarIconSize)), + child: SelectHeaderStyleButton(controller: controller)), VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400), Visibility( visible: showListNumbers, From d62f15d88968a732b27eb0bb412d9d8c0a47eab0 Mon Sep 17 00:00:00 2001 From: Jeremie Corpinot Date: Sat, 3 Apr 2021 21:03:57 +0200 Subject: [PATCH 128/306] =?UTF-8?q?=E2=98=9D=EF=B8=8F=20Update=20deprecate?= =?UTF-8?q?d=20code=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/widgets/raw_editor.dart | 6 +++++- lib/widgets/text_selection.dart | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 6a24f39c..eac1cbcf 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1029,7 +1029,7 @@ class RawEditorState extends EditorState } @override - void hideToolbar() { + void hideToolbar([bool hideHandles = true]) { if (getSelectionOverlay()?.toolbar != null) { getSelectionOverlay()?.hideToolbar(); } @@ -1131,6 +1131,10 @@ class RawEditorState extends EditorState closeConnectionIfNeeded(); } } + + @override + void userUpdateTextEditingValue( + TextEditingValue value, SelectionChangedCause cause) {} } class _Editor extends MultiChildRenderObjectWidget { diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 8aa29d25..f223ef00 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -155,8 +155,12 @@ class EditorTextSelectionOverlay { default: throw 'Invalid position'; } - selectionDelegate.textEditingValue = - value.copyWith(selection: newSelection, composing: TextRange.empty); + + selectionDelegate.userUpdateTextEditingValue( + value.copyWith(selection: newSelection, composing: TextRange.empty), + SelectionChangedCause.drag, + ); + selectionDelegate.bringIntoView(textPosition); } From 41056bcc4cd938a87b4280a4034ae15a34425d8f Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 3 Apr 2021 12:09:30 -0700 Subject: [PATCH 129/306] =?UTF-8?q?Revert=20"=E2=98=9D=EF=B8=8F=20Update?= =?UTF-8?q?=20deprecated=20code=20(#136)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d62f15d88968a732b27eb0bb412d9d8c0a47eab0. --- lib/widgets/raw_editor.dart | 6 +----- lib/widgets/text_selection.dart | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index eac1cbcf..6a24f39c 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1029,7 +1029,7 @@ class RawEditorState extends EditorState } @override - void hideToolbar([bool hideHandles = true]) { + void hideToolbar() { if (getSelectionOverlay()?.toolbar != null) { getSelectionOverlay()?.hideToolbar(); } @@ -1131,10 +1131,6 @@ class RawEditorState extends EditorState closeConnectionIfNeeded(); } } - - @override - void userUpdateTextEditingValue( - TextEditingValue value, SelectionChangedCause cause) {} } class _Editor extends MultiChildRenderObjectWidget { diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index f223ef00..8aa29d25 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -155,12 +155,8 @@ class EditorTextSelectionOverlay { default: throw 'Invalid position'; } - - selectionDelegate.userUpdateTextEditingValue( - value.copyWith(selection: newSelection, composing: TextRange.empty), - SelectionChangedCause.drag, - ); - + selectionDelegate.textEditingValue = + value.copyWith(selection: newSelection, composing: TextRange.empty); selectionDelegate.bringIntoView(textPosition); } From 041d24cea02847de249eafc1b1726c200ff47d5d Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 3 Apr 2021 18:55:56 -0700 Subject: [PATCH 130/306] Use widget.enableInteractiveSelection for copy/selectAll in ToobarOptions --- lib/widgets/editor.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index df74e2e7..f9bab1c1 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -243,10 +243,10 @@ class _QuillEditorState extends State widget.placeholder, widget.onLaunchUrl, ToolbarOptions( - copy: true, + copy: widget.enableInteractiveSelection, cut: widget.enableInteractiveSelection, paste: widget.enableInteractiveSelection, - selectAll: true, + selectAll: widget.enableInteractiveSelection, ), theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.android, From ac2ca07bb04401642a863b75af4149fba1b987be Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 4 Apr 2021 12:14:25 -0700 Subject: [PATCH 131/306] Update issue templates --- .github/ISSUE_TEMPLATE/issue-template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md index 1dfda1d3..a908ed53 100644 --- a/.github/ISSUE_TEMPLATE/issue-template.md +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -12,3 +12,5 @@ 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. + +Please note that we are using stable channel. If you are using beta or master channel, those are not supported. From b68fc5a4659cb8af7b068ca3ba2b1527a4ccebde Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Mon, 5 Apr 2021 01:29:17 -0700 Subject: [PATCH 132/306] tap handles --- lib/widgets/editor.dart | 68 +++++++++++++++++++++++++++++++++ lib/widgets/text_selection.dart | 1 + 2 files changed, 69 insertions(+) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index df74e2e7..9991c2a6 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -134,6 +134,16 @@ class QuillEditor extends StatefulWidget { final Brightness keyboardAppearance; final ScrollPhysics? scrollPhysics; final ValueChanged? onLaunchUrl; + // Returns whether gesture is handled + final bool Function(TapDownDetails details, TextPosition textPosition)? onTapDown; + // Returns whether gesture is handled + final bool Function(TapUpDetails details, TextPosition textPosition)? onTapUp; + // Returns whether gesture is handled + final bool Function(LongPressStartDetails details, TextPosition textPosition)? onSingleLongTapStart; + // Returns whether gesture is handled + final bool Function(LongPressMoveUpdateDetails details, TextPosition textPosition)? onSingleLongTapMoveUpdate; + // Returns whether gesture is handled + final bool Function(LongPressEndDetails details, TextPosition textPosition)? onSingleLongTapEnd; final EmbedBuilder embedBuilder; const QuillEditor( @@ -155,6 +165,11 @@ class QuillEditor extends StatefulWidget { this.keyboardAppearance = Brightness.light, this.scrollPhysics, this.onLaunchUrl, + this.onTapDown, + this.onTapUp, + this.onSingleLongTapStart, + this.onSingleLongTapMoveUpdate, + this.onSingleLongTapEnd, this.embedBuilder = _defaultEmbedBuilder}); factory QuillEditor.basic( @@ -314,6 +329,15 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { + if (_state.widget.onSingleLongTapMoveUpdate != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onSingleLongTapMoveUpdate!(details, renderEditor.getPositionForOffset(details.globalPosition) + )) { + return; + } + } + } if (!delegate.getSelectionEnabled()) { return; } @@ -435,8 +459,30 @@ class _QuillEditorSelectionGestureDetectorBuilder await launch(url); } + @override + void onTapDown(TapDownDetails details) { + if (_state.widget.onTapDown != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onTapDown!(details, renderEditor.getPositionForOffset(details.globalPosition))) { + return; + } + } + } + super.onTapDown(details); + } + @override void onSingleTapUp(TapUpDetails details) { + if (_state.widget.onTapUp != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onTapUp!(details, renderEditor.getPositionForOffset(details.globalPosition))) { + return; + } + } + } + getEditor()!.hideToolbar(); bool positionSelected = _onTapping(details); @@ -470,6 +516,15 @@ class _QuillEditorSelectionGestureDetectorBuilder @override void onSingleLongTapStart(LongPressStartDetails details) { + if (_state.widget.onSingleLongTapStart != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onSingleLongTapStart!(details, renderEditor.getPositionForOffset(details.globalPosition))) { + return; + } + } + } + if (delegate.getSelectionEnabled()) { switch (Theme.of(_state.context).platform) { case TargetPlatform.iOS: @@ -492,6 +547,19 @@ class _QuillEditorSelectionGestureDetectorBuilder } } } + + @override + void onSingleLongTapEnd(LongPressEndDetails details) { + if (_state.widget.onSingleLongTapEnd != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onSingleLongTapEnd!(details, renderEditor.getPositionForOffset(details.globalPosition))) { + return; + } + } + } + super.onSingleLongTapEnd(details); + } } typedef TextSelectionChangedHandler = void Function( diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 8aa29d25..ff733de2 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -533,6 +533,7 @@ class _EditorTextSelectionGestureDetectorState } void _handleTapDown(TapDownDetails details) { + // renderObject.resetTapDownStatus(); if (widget.onTapDown != null) { widget.onTapDown!(details); } From 7cba9a85234e38731db86cbdd10543970623e5b7 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 18:01:14 +0200 Subject: [PATCH 133/306] Fix height of empty line bug (#141) --- lib/widgets/text_line.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 0b6788cf..57455d5b 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -44,12 +44,11 @@ class TextLine extends StatelessWidget { 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]), + text: textSpan, textAlign: textAlign, textDirection: textDirection, strutStyle: strutStyle, From 31ec14c114214eb16862ae1cb36c842885205593 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 18:07:22 +0200 Subject: [PATCH 134/306] Prefer relative imports This streamlines our imports. --- analysis_options.yaml | 2 ++ lib/models/documents/document.dart | 10 +++++----- lib/models/documents/history.dart | 2 +- lib/models/documents/nodes/block.dart | 3 +-- lib/models/documents/nodes/leaf.dart | 3 +-- lib/models/documents/nodes/line.dart | 7 +++---- lib/models/documents/nodes/node.dart | 5 ++--- lib/models/documents/style.dart | 3 ++- lib/models/rules/delete.dart | 6 +++--- lib/models/rules/format.dart | 6 +++--- lib/models/rules/insert.dart | 9 +++++---- lib/models/rules/rule.dart | 7 +++---- lib/utils/diff_delta.dart | 2 +- lib/widgets/box.dart | 3 ++- lib/widgets/controller.dart | 13 +++++++------ lib/widgets/delegate.dart | 4 ++-- lib/widgets/editor.dart | 21 ++++++++++----------- lib/widgets/raw_editor.dart | 22 +++++++++++----------- lib/widgets/text_block.dart | 16 ++++++++-------- lib/widgets/text_line.dart | 23 +++++++++++------------ lib/widgets/text_selection.dart | 2 +- lib/widgets/toolbar.dart | 8 ++++---- 22 files changed, 88 insertions(+), 89 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 306d335b..fb75bccd 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,6 +10,8 @@ linter: - always_put_required_named_parameters_first - avoid_print - avoid_redundant_argument_values + - directives_ordering - prefer_const_constructors - prefer_const_constructors_in_immutables + - prefer_relative_imports - unnecessary_parenthesis diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 8a620446..a8810079 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -1,17 +1,17 @@ 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 { diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index 6d89b389..d3e36e32 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -1,6 +1,6 @@ -import 'package:flutter_quill/models/quill_delta.dart'; import 'package:tuple/tuple.dart'; +import '../quill_delta.dart'; import 'document.dart'; class History { diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index 4d569cc7..acae321b 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -1,5 +1,4 @@ -import 'package:flutter_quill/models/quill_delta.dart'; - +import '../../quill_delta.dart'; import 'container.dart'; import 'line.dart'; import 'node.dart'; diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index 6cb9af24..fab88b53 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -1,7 +1,6 @@ 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'; diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 574549ea..ceb95801 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -1,14 +1,13 @@ 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'; class Line extends Container { @override diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index abc093c3..2d694222 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -1,9 +1,8 @@ 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'; diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 7b9b050b..90c03df0 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -1,7 +1,8 @@ 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; diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index 15aa3862..535504d5 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(); diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart index 755f4137..0d574c94 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(); diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 5ea7f215..f0f7d001 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(); diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index 70a5aa74..23eb3b9d 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'; diff --git a/lib/utils/diff_delta.dart b/lib/utils/diff_delta.dart index 127c2d8b..d3b97116 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, 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..510db24c 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -1,14 +1,15 @@ 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'; +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 { final Document document; TextSelection selection; diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index 3cfb9cd6..f134a77e 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); diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index f9bab1c1..654daf1e 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -8,25 +8,24 @@ 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 '../models/documents/nodes/node.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 diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 6a24f39c..155a90ff 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -9,25 +9,25 @@ 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 '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/line.dart'; +import '../models/documents/nodes/node.dart'; +import '../utils/diff_delta.dart'; import 'box.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 { final QuillController controller; diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 85912431..031ecbd5 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -1,19 +1,19 @@ 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 '../models/documents/nodes/node.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, diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 57455d5b..c10c6c8e 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -3,23 +3,22 @@ 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 '../models/documents/style.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; diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 8aa29d25..7873c99d 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -7,8 +7,8 @@ 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) { diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 6b4b4147..07bdd421 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -5,13 +5,13 @@ 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; From e4a3c11702ca9982b9928b955901fecf4e72fb20 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 8 Apr 2021 09:35:59 -0700 Subject: [PATCH 135/306] Update example code --- example/lib/pages/home_page.dart | 2 +- example/lib/pages/read_only_page.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 65c2f165..5d02d71e 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 { diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index e87e4f8f..395c20f5 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 { From bc494bc50f6c3bacb31d76abfe2b41c5e3ae04ea Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:08:26 +0200 Subject: [PATCH 136/306] Prefer final Declaring variables as final when possible is a good practice because it helps avoid accidental reassignments and allows the compiler to do optimizations. --- analysis_options.yaml | 5 +- example/lib/pages/home_page.dart | 6 +- example/lib/universal_ui/universal_ui.dart | 4 +- lib/models/documents/attribute.dart | 6 +- lib/models/documents/document.dart | 48 +++---- lib/models/documents/history.dart | 12 +- lib/models/documents/nodes/block.dart | 10 +- lib/models/documents/nodes/container.dart | 16 +-- lib/models/documents/nodes/embed.dart | 4 +- lib/models/documents/nodes/leaf.dart | 37 ++--- lib/models/documents/nodes/line.dart | 74 +++++----- lib/models/documents/nodes/node.dart | 6 +- lib/models/documents/style.dart | 18 +-- lib/models/quill_delta.dart | 15 +- lib/models/rules/delete.dart | 28 ++-- lib/models/rules/format.dart | 36 ++--- lib/models/rules/insert.dart | 96 +++++++------ lib/models/rules/rule.dart | 2 +- lib/utils/color.dart | 4 +- lib/utils/diff_delta.dart | 24 ++-- lib/widgets/controller.dart | 18 +-- lib/widgets/cursor.dart | 16 +-- lib/widgets/default_styles.dart | 10 +- lib/widgets/delegate.dart | 2 +- lib/widgets/editor.dart | 152 ++++++++++----------- lib/widgets/keyboard_listener.dart | 6 +- lib/widgets/proxy.dart | 4 +- lib/widgets/raw_editor.dart | 135 +++++++++--------- lib/widgets/text_block.dart | 102 +++++++------- lib/widgets/text_line.dart | 85 ++++++------ lib/widgets/text_selection.dart | 49 ++++--- lib/widgets/toolbar.dart | 34 ++--- 32 files changed, 524 insertions(+), 540 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index fb75bccd..50b1b8fc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,7 +3,6 @@ include: package:pedantic/analysis_options.yaml analyzer: errors: undefined_prefixed_name: ignore - omit_local_variable_types: ignore unsafe_html: ignore linter: rules: @@ -11,7 +10,11 @@ linter: - avoid_print - avoid_redundant_argument_values - directives_ordering + - omit_local_variable_types - prefer_const_constructors - prefer_const_constructors_in_immutables + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals - prefer_relative_imports - unnecessary_parenthesis diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 5d02d71e..a613c085 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -178,14 +178,14 @@ 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 size = MediaQuery.of(context).size; final itemStyle = const TextStyle( color: Colors.white, fontSize: 18, diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart index 15eeb9f8..9c833aa2 100644 --- a/example/lib/universal_ui/universal_ui.dart +++ b/example/lib/universal_ui/universal_ui.dart @@ -28,8 +28,8 @@ 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); return Padding( diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 8d43e805..7683b9ed 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -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 a8810079..232642fb 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -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 d3e36e32..9398ad1c 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -47,7 +47,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 +74,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 +88,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 +99,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; diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index acae321b..0919806f 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -17,7 +17,7 @@ class Block extends Container { @override void adjust() { if (isEmpty) { - Node? sibling = previous; + final sibling = previous; unlink(); if (sibling != null) { sibling.adjust(); @@ -25,8 +25,8 @@ 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 && @@ -35,7 +35,7 @@ class Block extends Container { block.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); @@ -47,7 +47,7 @@ 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(); diff --git a/lib/models/documents/nodes/container.dart b/lib/models/documents/nodes/container.dart index c7c10390..8b3fd0c3 100644 --- a/lib/models/documents/nodes/container.dart +++ b/lib/models/documents/nodes/container.dart @@ -48,9 +48,9 @@ abstract class Container extends Node { 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); } @@ -63,8 +63,8 @@ abstract class Container extends Node { 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); } @@ -84,14 +84,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 +99,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); } diff --git a/lib/models/documents/nodes/embed.dart b/lib/models/documents/nodes/embed.dart index 3268e257..cc0ecaea 100644 --- a/lib/models/documents/nodes/embed.dart +++ b/lib/models/documents/nodes/embed.dart @@ -5,12 +5,12 @@ class Embeddable { Embeddable(this.type, this.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); diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index fab88b53..88aeca6c 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -18,7 +18,7 @@ abstract class Leaf extends Node { if (data is Embeddable) { return Embed(data); } - String text = data as String; + final text = data as String; assert(text.isNotEmpty); return Text(text); } @@ -44,14 +44,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 { @@ -66,9 +67,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); @@ -81,13 +82,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); @@ -104,9 +105,9 @@ abstract class Leaf extends Node { return; } - Text node = this as Text; + var node = this as Text; // merging it with previous node if style is the same - Node? prev = node.previous; + final prev = node.previous; if (!node.isFirst && prev is Text && prev.style == node.style) { prev._value = prev.value + node.value; node.unlink(); @@ -114,7 +115,7 @@ abstract class Leaf extends Node { } // merging it with next node if style is the same - Node? next = node.next; + final next = node.next; if (!node.isLast && next is Text && next.style == node.style) { node._value = node.value + next.value; next.unlink(); @@ -123,7 +124,7 @@ abstract class Leaf extends Node { Leaf? cutAt(int index) { assert(index >= 0 && index <= length); - Leaf? cut = splitAt(index); + final cut = splitAt(index); cut?.unlink(); return cut; } @@ -138,9 +139,9 @@ 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)); + final split = Leaf(text.substring(index)); split.applyStyle(style); insertAfter(split); return split; @@ -157,7 +158,7 @@ abstract class Leaf extends Node { Leaf _isolate(int index, int length) { assert( index >= 0 && index < this.length && (index + length <= this.length)); - Leaf target = splitAt(index)!; + final target = splitAt(index)!; target.splitAt(length); return target; } diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index ceb95801..7dba241f 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -47,7 +47,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()); @@ -71,20 +71,20 @@ class Line extends Container { 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); return; } - String prefix = text.substring(0, lineBreak); + final prefix = text.substring(0, lineBreak); _insert(index, prefix, style); if (prefix.isNotEmpty) { index += prefix.length; } - Line nextLine = _getNextLine(index); + final nextLine = _getNextLine(index); clearStyle(); @@ -95,7 +95,7 @@ class Line extends Container { _format(style); // Continue with the remaining - String remain = text.substring(lineBreak + 1); + final remain = text.substring(lineBreak + 1); nextLine.insert(0, remain, style); } @@ -104,9 +104,9 @@ class Line extends Container { if (style == null) { return; } - int thisLen = length; + final thisLen = length; - int local = math.min(thisLen - index, len!); + final local = math.min(thisLen - index, len!); if (index + local == thisLen && local == 1) { assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK)); @@ -117,7 +117,7 @@ class Line extends Container { super.retain(index, local, style); } - int remain = len - local; + final remain = len - local; if (remain > 0) { assert(nextLine != null); nextLine!.retain(0, remain, style); @@ -126,8 +126,8 @@ class Line extends Container { @override void delete(int index, int? len) { - int local = math.min(length - index, len!); - bool deleted = index + local == length; + final local = math.min(length - index, len!); + final deleted = index + local == length; if (deleted) { clearStyle(); if (local > 1) { @@ -137,7 +137,7 @@ class Line extends Container { super.delete(index, local); } - int remain = len - local; + final remain = len - local; if (remain > 0) { assert(nextLine != null); nextLine!.delete(0, remain); @@ -150,7 +150,7 @@ class Line extends Container { } if (deleted) { - Node p = parent!; + final Node p = parent!; unlink(); p.adjust(); } @@ -162,24 +162,24 @@ class Line extends Container { } applyStyle(newStyle); - Attribute? blockStyle = newStyle.getBlockExceptHeader(); + final blockStyle = newStyle.getBlockExceptHeader(); if (blockStyle == null) { return; } 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(); + final block = Block(); block.applyAttribute(blockStyle); _wrap(block); block.adjust(); } } else if (blockStyle.value != null) { - Block block = Block(); + final block = Block(); block.applyAttribute(blockStyle); _wrap(block); block.adjust(); @@ -197,7 +197,7 @@ class Line extends Container { if (parent is! Block) { throw ArgumentError('Invalid parent'); } - Block block = parent as Block; + final block = parent as Block; assert(block.children.contains(this)); @@ -208,10 +208,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); @@ -226,20 +226,20 @@ 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; + final next = last as Leaf; next.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; @@ -256,12 +256,12 @@ class Line extends Container { } if (isNotEmpty) { - ChildQuery result = queryChild(index, true); + final result = queryChild(index, true); result.node!.insert(result.offset, data, style); return; } - Leaf child = Leaf(data); + final child = Leaf(data); add(child); child.format(style); } @@ -272,30 +272,30 @@ class Line extends Container { } 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 res = Style(); + final excluded = {}; void _handle(Style style) { if (res.isEmpty) { excluded.addAll(style.values); } else { - for (Attribute attr in res.values) { + for (final attr in res.values) { if (!style.containsKey(attr.key)) { excluded.add(attr); } } } - Style remain = style.removeAll(excluded); + final remain = style.removeAll(excluded); res = res.removeAll(excluded); res = res.mergeAll(remain); } - 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; + var pos = node.length - data.offset; while (!node!.isLast && pos < local) { node = node.next as Leaf?; _handle(node!.style); @@ -305,11 +305,11 @@ class Line extends Container { res = res.mergeAll(style); if (parent is Block) { - Block block = parent as Block; + final block = parent as Block; res = res.mergeAll(block.style); } - int remain = len - local; + final remain = len - local; if (remain > 0) { _handle(nextLine!.collectStyle(0, remain)); } diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index 2d694222..d46b4647 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -32,19 +32,19 @@ abstract class Node extends LinkedListEntry { int get length; Node clone() { - Node node = newInstance(); + final node = newInstance(); node.applyStyle(style); return node; } int getOffset() { - int offset = 0; + var offset = 0; if (list == null || isFirst) { return offset; } - Node cur = this; + var cur = this; do { cur = cur.previous!; offset += cur.length; diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 90c03df0..4becc57e 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -16,8 +16,8 @@ class Style { return Style(); } - Map result = attributes.map((String key, dynamic value) { - Attribute attr = Attribute.fromKeyValue(key, value); + final result = attributes.map((String key, dynamic value) { + final attr = Attribute.fromKeyValue(key, value); return MapEntry(key, attr); }); return Style.attr(result); @@ -48,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; } @@ -57,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 { @@ -67,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); } @@ -94,7 +94,7 @@ class Style { if (other is! Style) { return false; } - Style typedOther = other; + final typedOther = other; final eq = const MapEquality(); return eq.equals(_attributes, typedOther._attributes); } diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index ddccb710..c8707ee0 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -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) && @@ -221,14 +221,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; @@ -292,7 +292,7 @@ class Delta { bool operator ==(dynamic other) { if (identical(this, other)) return true; if (other is! Delta) return false; - Delta typedOther = other; + final typedOther = other; final comparator = const ListEquality(DefaultEquality()); return comparator.equals(_operations, typedOther._operations); @@ -529,7 +529,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 +549,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; @@ -661,7 +662,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 535504d5..60cee64d 100644 --- a/lib/models/rules/delete.dart +++ b/lib/models/rules/delete.dart @@ -34,31 +34,31 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { @override Delta? applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}) { - DeltaIterator itr = DeltaIterator(document); + final itr = DeltaIterator(document); itr.skip(index); - Operation op = itr.next(1); + 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)); @@ -80,15 +80,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 +107,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 0d574c94..470ca200 100644 --- a/lib/models/rules/format.dart +++ b/lib/models/rules/format.dart @@ -26,21 +26,21 @@ class ResolveLineFormatRule extends FormatRule { return null; } - Delta delta = Delta()..retain(index); - DeltaIterator itr = DeltaIterator(document); + var delta = Delta()..retain(index); + final itr = DeltaIterator(document); itr.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 +52,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 +75,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 +105,20 @@ class ResolveInlineFormatRule extends FormatRule { return null; } - Delta delta = Delta()..retain(index); - DeltaIterator itr = DeltaIterator(document); + final delta = Delta()..retain(index); + final itr = DeltaIterator(document); itr.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 f0f7d001..97f386d5 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -29,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); } @@ -66,19 +66,19 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { return null; } - DeltaIterator itr = DeltaIterator(document); + final itr = DeltaIterator(document); itr.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; @@ -86,10 +86,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); } @@ -131,10 +131,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; } @@ -146,7 +145,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() == @@ -155,7 +154,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 @@ -173,9 +172,9 @@ class ResetLineFormatOnNewLineRule extends InsertRule { return null; } - DeltaIterator itr = DeltaIterator(document); + final itr = DeltaIterator(document); itr.skip(index); - Operation cur = itr.next(); + final cur = itr.next(); if (cur.data is! String || !(cur.data as String).startsWith('\n')) { return null; } @@ -203,12 +202,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'); @@ -222,7 +221,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; @@ -251,17 +250,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'); } @@ -282,19 +281,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; @@ -321,16 +320,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) @@ -338,13 +337,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; } @@ -371,9 +369,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 23eb3b9d..19bc9177 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -52,7 +52,7 @@ class Rules { 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..f4c26040 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('#', ''); + var hex = s.replaceFirst('#', ''); hex = hex.length == 6 ? 'ff' + hex : hex; - int val = int.parse(hex, radix: 16); + 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 d3b97116..a3653eb0 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -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,13 +71,13 @@ 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() + diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 510db24c..faabf4c2 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -43,7 +43,7 @@ class QuillController extends ChangeNotifier { } void undo() { - Tuple2 tup = document.undo(); + final tup = document.undo(); if (tup.item1) { _handleHistoryChange(tup.item2); } @@ -65,7 +65,7 @@ class QuillController extends ChangeNotifier { } void redo() { - Tuple2 tup = document.redo(); + final tup = document.redo(); if (tup.item1) { _handleHistoryChange(tup.item2); } @@ -82,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; @@ -98,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); @@ -110,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, @@ -134,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) { @@ -177,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..8a22c7c2 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -99,7 +99,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 { @@ -168,9 +168,9 @@ class CursorPainter { 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!); } @@ -179,7 +179,7 @@ class CursorPainter { caretRect = caretRect.shift(Offset(-caretRect.left, 0.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..7f908114 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -17,7 +17,7 @@ class QuillStyles extends InheritedWidget { } static DefaultStyles? getStyles(BuildContext context, bool nullOk) { - var widget = context.dependOnInheritedWidgetOfExactType(); + final widget = context.dependOnInheritedWidgetOfExactType(); if (widget == null && nullOk) { return null; } @@ -84,13 +84,13 @@ class DefaultStyles { this.sizeHuge}); static DefaultStyles getInstance(BuildContext context) { - ThemeData themeData = Theme.of(context); - DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); - TextStyle baseStyle = defaultTextStyle.style.copyWith( + final themeData = Theme.of(context); + final defaultTextStyle = DefaultTextStyle.of(context); + final baseStyle = defaultTextStyle.style.copyWith( fontSize: 16.0, height: 1.3, ); - Tuple2 baseSpacing = const Tuple2(6.0, 0); + final baseSpacing = const Tuple2(6.0, 0); String fontFamily; switch (themeData.platform) { case TargetPlatform.iOS: diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index f134a77e..8adf8ce7 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -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 654daf1e..c3f24d98 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -17,7 +17,6 @@ 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 '../models/documents/nodes/node.dart'; import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; @@ -100,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) @@ -188,8 +187,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; @@ -213,7 +212,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; @@ -344,16 +343,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 @@ -364,7 +361,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; @@ -380,9 +377,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( @@ -413,7 +410,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 @@ -438,7 +435,7 @@ class _QuillEditorSelectionGestureDetectorBuilder void onSingleTapUp(TapUpDetails details) { getEditor()!.hideToolbar(); - bool positionSelected = _onTapping(details); + final positionSelected = _onTapping(details); if (delegate.getSelectionEnabled() && !positionSelected) { switch (Theme.of(_state.context).platform) { @@ -575,12 +572,12 @@ class RenderEditor extends RenderEditableContainerBox List getEndpointsForSelection( TextSelection textSelection) { if (textSelection.isCollapsed) { - RenderEditableBox child = childAtPosition(textSelection.extent); - TextPosition localPosition = TextPosition( + final child = childAtPosition(textSelection.extent); + final localPosition = TextPosition( offset: textSelection.extentOffset - child.getContainer().getOffset()); - Offset localOffset = child.getOffsetForCaret(localPosition); - BoxParentData parentData = child.parentData as BoxParentData; + final localOffset = child.getOffsetForCaret(localPosition); + final parentData = child.parentData as BoxParentData; return [ TextSelectionPoint( Offset(0.0, child.preferredLineHeight(localPosition)) + @@ -590,7 +587,7 @@ class RenderEditor extends RenderEditableContainerBox ]; } - Node? baseNode = _container.queryChild(textSelection.start, false).node; + final baseNode = _container.queryChild(textSelection.start, false).node; var baseChild = firstChild; while (baseChild != null) { @@ -601,15 +598,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) { @@ -619,10 +615,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); @@ -643,9 +639,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( @@ -662,7 +658,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 && @@ -676,15 +672,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().getOffset(); + 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, ); @@ -708,17 +704,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, @@ -738,12 +734,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().getOffset(); + 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, ); @@ -755,12 +751,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().getOffset(); + 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, ); @@ -810,19 +806,19 @@ class RenderEditor extends RenderEditableContainerBox @override double preferredLineHeight(TextPosition position) { - RenderEditableBox child = childAtPosition(position); + final child = childAtPosition(position); return child.preferredLineHeight(TextPosition( offset: position.offset - child.getContainer().getOffset())); } @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(), affinity: localPosition.affinity, @@ -836,12 +832,12 @@ 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())) - @@ -919,7 +915,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) { @@ -951,7 +947,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; @@ -978,15 +975,14 @@ 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; + final childParentData = child.parentData as EditableContainerParentData; childParentData.offset = Offset(_resolvedPadding!.left, mainAxisExtent); mainAxisExtent += child.size.height; assert(child.parentData == childParentData); @@ -999,24 +995,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; @@ -1026,7 +1020,7 @@ class RenderEditableContainerBox extends RenderBox double computeMinIntrinsicWidth(double height) { _resolvePadding(); return _getIntrinsicCrossAxis((RenderBox child) { - double childHeight = math.max( + final childHeight = math.max( 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMinIntrinsicWidth(childHeight) + _resolvedPadding!.left + @@ -1038,7 +1032,7 @@ class RenderEditableContainerBox extends RenderBox double computeMaxIntrinsicWidth(double height) { _resolvePadding(); return _getIntrinsicCrossAxis((RenderBox child) { - double childHeight = math.max( + final childHeight = math.max( 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMaxIntrinsicWidth(childHeight) + _resolvedPadding!.left + @@ -1050,7 +1044,7 @@ class RenderEditableContainerBox extends RenderBox double computeMinIntrinsicHeight(double width) { _resolvePadding(); return _getIntrinsicMainAxis((RenderBox child) { - double childWidth = math.max( + final childWidth = math.max( 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMinIntrinsicHeight(childWidth) + _resolvedPadding!.top + diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart index 7311b345..f952c3dd 100644 --- a/lib/widgets/keyboard_listener.dart +++ b/lib/widgets/keyboard_listener.dart @@ -71,10 +71,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..71fc219d 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -91,8 +91,8 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { ]; } - 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) ]; diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 155a90ff..8fc9babd 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -15,9 +15,7 @@ import '../models/documents/attribute.dart'; import '../models/documents/document.dart'; import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; -import '../models/documents/nodes/node.dart'; import '../utils/diff_delta.dart'; -import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; import 'default_styles.dart'; @@ -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,34 +204,32 @@ 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( + final child = getRenderEditor()!.childAtPosition(originPosition); + final localPosition = TextPosition( offset: originPosition.offset - child.getContainer().getDocumentOffset()); - 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); @@ -273,28 +269,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 +298,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 +309,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 +327,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((String currentString) { if (count <= index) { count += currentString.length; return true; @@ -351,9 +347,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 +407,7 @@ class RawEditorState extends EditorState return; } - TextEditingValue actualValue = textEditingValue.copyWith( + final actualValue = textEditingValue.copyWith( composing: _lastKnownRemoteTextEditingValue!.composing, ); @@ -419,7 +415,7 @@ class RawEditorState extends EditorState return; } - bool shouldRemember = + final shouldRemember = textEditingValue.text != _lastKnownRemoteTextEditingValue!.text; _lastKnownRemoteTextEditingValue = actualValue; _textInputConnection!.setEditingState(actualValue); @@ -456,13 +452,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 +508,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 +535,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 +548,7 @@ class RawEditorState extends EditorState ); } - BoxConstraints constraints = widget.expands + final constraints = widget.expands ? const BoxConstraints.expand() : BoxConstraints( minHeight: widget.minHeight ?? 0.0, @@ -584,15 +579,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), @@ -617,13 +611,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 +635,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 +655,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)) { @@ -720,8 +714,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 +778,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, @@ -814,8 +807,8 @@ class RawEditorState extends EditorState } void handleShortcut(InputShortcut? shortcut) async { - TextSelection selection = widget.controller.selection; - String plainText = textEditingValue.text; + final selection = widget.controller.selection; + final plainText = textEditingValue.text; if (shortcut == InputShortcut.COPY) { if (!selection.isCollapsed) { await Clipboard.setData( @@ -844,7 +837,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, @@ -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; } diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 031ecbd5..b9808fad 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -6,7 +6,6 @@ import 'package:tuple/tuple.dart'; import '../models/documents/attribute.dart'; import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; -import '../models/documents/nodes/node.dart'; import 'box.dart'; import 'cursor.dart'; import 'default_styles.dart'; @@ -79,7 +78,7 @@ class EditableTextBlock extends StatelessWidget { 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 +90,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 +102,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 +133,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, @@ -183,10 +182,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 +199,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,8 +309,8 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @override TextRange getLineBoundary(TextPosition position) { - RenderEditableBox child = childAtPosition(position); - TextRange rangeInChild = child.getLineBoundary(TextPosition( + final child = childAtPosition(position); + final rangeInChild = child.getLineBoundary(TextPosition( offset: position.offset - child.getContainer().getOffset(), affinity: position.affinity, )); @@ -323,7 +322,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @override Offset getOffsetForCaret(TextPosition position) { - RenderEditableBox child = childAtPosition(position); + final child = childAtPosition(position); return child.getOffsetForCaret(TextPosition( offset: position.offset - child.getContainer().getOffset(), affinity: position.affinity, @@ -333,9 +332,9 @@ 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(), @@ -345,9 +344,9 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @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().getOffset(); + final childWord = child .getWordBoundary(TextPosition(offset: position.offset - nodeOffset)); return TextRange( start: childWord.start + nodeOffset, @@ -359,25 +358,25 @@ class RenderEditableTextBlock extends RenderEditableContainerBox TextPosition? getPositionAbove(TextPosition position) { assert(position.offset < getContainer().length); - RenderEditableBox child = childAtPosition(position); - TextPosition childLocalPosition = TextPosition( + final child = childAtPosition(position); + final childLocalPosition = TextPosition( offset: position.offset - child.getContainer().getOffset()); - TextPosition? result = child.getPositionAbove(childLocalPosition); + final result = child.getPositionAbove(childLocalPosition); if (result != null) { return TextPosition( offset: result.offset + child.getContainer().getOffset()); } - 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() + sibling.getPositionForOffset(finalOffset).offset); @@ -387,24 +386,23 @@ class RenderEditableTextBlock extends RenderEditableContainerBox TextPosition? getPositionBelow(TextPosition position) { assert(position.offset < getContainer().length); - RenderEditableBox child = childAtPosition(position); - TextPosition childLocalPosition = TextPosition( + final child = childAtPosition(position); + final childLocalPosition = TextPosition( offset: position.offset - child.getContainer().getOffset()); - TextPosition? result = child.getPositionBelow(childLocalPosition); + final result = child.getPositionBelow(childLocalPosition); if (result != null) { return TextPosition( offset: result.offset + child.getContainer().getOffset()); } - 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() + sibling.getPositionForOffset(finalOffset).offset); @@ -412,7 +410,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @override double preferredLineHeight(TextPosition position) { - RenderEditableBox child = childAtPosition(position); + final child = childAtPosition(position); return child.preferredLineHeight(TextPosition( offset: position.offset - child.getContainer().getOffset())); } @@ -426,7 +424,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox 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 +434,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, @@ -452,7 +450,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox 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 +461,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 +485,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); @@ -572,7 +570,7 @@ class _NumberPoint extends StatelessWidget { @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)) { @@ -595,7 +593,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(); diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index c10c6c8e..99075411 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -11,7 +11,6 @@ import '../models/documents/nodes/leaf.dart' as leaf; import '../models/documents/nodes/leaf.dart'; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; -import '../models/documents/style.dart'; import '../utils/color.dart'; import 'box.dart'; import 'cursor.dart'; @@ -39,14 +38,14 @@ class TextLine extends StatelessWidget { 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)); } final textSpan = _buildTextSpan(context); final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); final textAlign = _getTextAlign(); - RichText child = RichText( + final child = RichText( text: textSpan, textAlign: textAlign, textDirection: textDirection, @@ -80,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, @@ -101,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; @@ -117,11 +116,11 @@ 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 = { + final m = { Attribute.bold.key: defaultStyles.bold, Attribute.italic.key: defaultStyles.italic, Attribute.link.key: defaultStyles.link, @@ -134,12 +133,12 @@ class TextLine extends StatelessWidget { } }); - 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': @@ -152,7 +151,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 { @@ -161,7 +160,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) { @@ -172,7 +171,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)); @@ -345,7 +344,7 @@ class RenderEditableTextLine extends RenderEditableBox { return; } - bool containsSelection = containsTextSelection(); + final containsSelection = containsTextSelection(); if (attached && containsCursor()) { cursorCont.removeListener(markNeedsLayout); cursorCont.color.removeListener(markNeedsPaint); @@ -424,7 +423,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, @@ -463,9 +462,9 @@ class RenderEditableTextLine extends RenderEditableBox { 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); @@ -473,10 +472,10 @@ class RenderEditableTextLine extends RenderEditableBox { @override TextRange getLineBoundary(TextPosition position) { - double lineDy = getOffsetForCaret(position) + final lineDy = getOffsetForCaret(position) .translate(0.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); @@ -504,7 +503,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)) { @@ -574,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()) { @@ -595,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)); @@ -613,12 +612,12 @@ 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)) as int; @@ -628,12 +627,12 @@ 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)) as int; @@ -643,8 +642,8 @@ 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)) + @@ -656,8 +655,8 @@ 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)) + @@ -849,8 +848,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); } @@ -873,8 +872,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 7873c99d..cf963d9a 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -12,10 +12,10 @@ 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.getOffset() : node.getDocumentOffset(); assert(base <= selection.end && selection.start <= base + node.length - 1); - int offset = fromParent ? node.getOffset() : node.getDocumentOffset(); + final offset = fromParent ? node.getOffset() : node.getDocumentOffset(); return selection.copyWith( baseOffset: math.max(selection.start - offset, 0), extentOffset: math.min(selection.end - offset, node.length - 1)); @@ -55,7 +55,7 @@ class EditorTextSelectionOverlay { this.dragStartBehavior, this.onSelectionHandleTapped, this.clipboardStatus) { - OverlayState overlay = Overlay.of(context, rootOverlay: true)!; + final overlay = Overlay.of(context, rootOverlay: true)!; _toolbarController = AnimationController( duration: const Duration(milliseconds: 150), vsync: overlay); @@ -161,26 +161,25 @@ class EditorTextSelectionOverlay { } 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, ); @@ -326,14 +325,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 +388,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), @@ -656,8 +654,7 @@ class _EditorTextSelectionGestureDetectorState @override Widget build(BuildContext context) { - final Map gestures = - {}; + final gestures = {}; gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 07bdd421..0941760f 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -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; } @@ -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; } @@ -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( @@ -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); } @@ -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); } @@ -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)); } From 15a2f1d76f7c81b62adca20ff5b9bfd13f0100be Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:20:09 +0200 Subject: [PATCH 137/306] Prefer const declarations Const declarations are more hot-reload friendly and allow to use const constructors if an instantiation references this declaration. --- analysis_options.yaml | 1 + lib/models/documents/style.dart | 2 +- lib/models/quill_delta.dart | 3 +-- lib/widgets/default_styles.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 50b1b8fc..ac173063 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,6 +13,7 @@ linter: - 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 diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 4becc57e..37f6a0b9 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -95,7 +95,7 @@ class Style { return false; } final typedOther = other; - final eq = const MapEquality(); + const eq = MapEquality(); return eq.equals(_attributes, typedOther._attributes); } diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index c8707ee0..0135bd6a 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -293,8 +293,7 @@ class Delta { if (identical(this, other)) return true; if (other is! Delta) return false; final typedOther = other; - final comparator = - const ListEquality(DefaultEquality()); + const comparator = ListEquality(DefaultEquality()); return comparator.equals(_operations, typedOther._operations); } diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 7f908114..2f2ea02c 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -90,7 +90,7 @@ class DefaultStyles { fontSize: 16.0, height: 1.3, ); - final baseSpacing = const Tuple2(6.0, 0); + const baseSpacing = Tuple2(6.0, 0); String fontFamily; switch (themeData.platform) { case TargetPlatform.iOS: From b47d22856b82cb66c9b19610d023a347b403bd35 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:24:07 +0200 Subject: [PATCH 138/306] Add already complied rules --- analysis_options.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index ac173063..eac60d7e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -6,7 +6,11 @@ analyzer: 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 - directives_ordering @@ -18,4 +22,5 @@ linter: - prefer_final_in_for_each - prefer_final_locals - prefer_relative_imports + - prefer_single_quotes - unnecessary_parenthesis From 6c31fb1bc3df3318273385bd05cd3a422e09402c Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:27:07 +0200 Subject: [PATCH 139/306] Avoid types on closure parameters Annotating types for function expression parameters is usually unnecessary because the parameter types can almost always be inferred from the context, thus making the practice redundant. --- analysis_options.yaml | 1 + lib/models/documents/style.dart | 4 ++-- lib/models/rules/delete.dart | 4 ++-- lib/widgets/editor.dart | 8 ++++---- lib/widgets/raw_editor.dart | 8 ++++---- lib/widgets/text_selection.dart | 14 +++++++------- lib/widgets/toolbar.dart | 2 +- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index eac60d7e..a7b7b075 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -24,3 +24,4 @@ linter: - prefer_relative_imports - prefer_single_quotes - unnecessary_parenthesis + - avoid_types_on_closure_parameters diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 37f6a0b9..f8a8ee74 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -16,7 +16,7 @@ class Style { return Style(); } - final result = attributes.map((String key, dynamic value) { + final result = attributes.map((key, dynamic value) { final attr = Attribute.fromKeyValue(key, value); return MapEntry(key, attr); }); @@ -25,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; diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index 60cee64d..def2e8c7 100644 --- a/lib/models/rules/delete.dart +++ b/lib/models/rules/delete.dart @@ -60,8 +60,8 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { 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 ??= {}; diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index c3f24d98..f1faaec6 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1019,7 +1019,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeMinIntrinsicWidth(double height) { _resolvePadding(); - return _getIntrinsicCrossAxis((RenderBox child) { + return _getIntrinsicCrossAxis((child) { final childHeight = math.max( 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMinIntrinsicWidth(childHeight) + @@ -1031,7 +1031,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeMaxIntrinsicWidth(double height) { _resolvePadding(); - return _getIntrinsicCrossAxis((RenderBox child) { + return _getIntrinsicCrossAxis((child) { final childHeight = math.max( 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMaxIntrinsicWidth(childHeight) + @@ -1043,7 +1043,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeMinIntrinsicHeight(double width) { _resolvePadding(); - return _getIntrinsicMainAxis((RenderBox child) { + return _getIntrinsicMainAxis((child) { final childWidth = math.max( 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMinIntrinsicHeight(childWidth) + @@ -1055,7 +1055,7 @@ class RenderEditableContainerBox extends RenderBox @override double computeMaxIntrinsicHeight(double width) { _resolvePadding(); - return _getIntrinsicMainAxis((RenderBox child) { + return _getIntrinsicMainAxis((child) { final childWidth = math.max( 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMaxIntrinsicHeight(childWidth) + diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 8fc9babd..9747901f 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -328,7 +328,7 @@ class RawEditorState extends EditorState } var count = 0; - final remain = string.characters.skipWhile((String currentString) { + final remain = string.characters.skipWhile((currentString) { if (count <= index) { count += currentString.length; return true; @@ -698,7 +698,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(); @@ -911,7 +911,7 @@ class RawEditorState extends EditorState } SchedulerBinding.instance!.addPostFrameCallback( - (Duration _) => _updateOrDisposeSelectionOverlayIfNeeded()); + (_) => _updateOrDisposeSelectionOverlayIfNeeded()); if (mounted) { setState(() { // Use widget.controller.value in build() @@ -982,7 +982,7 @@ class RawEditorState extends EditorState } _showCaretOnScreenScheduled = true; - SchedulerBinding.instance!.addPostFrameCallback((Duration _) { + SchedulerBinding.instance!.addPostFrameCallback((_) { _showCaretOnScreenScheduled = false; final viewport = RenderAbstractViewport.of(getRenderEditor())!; diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index cf963d9a..c113faeb 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -111,7 +111,7 @@ class EditorTextSelectionOverlay { return Visibility( visible: handlesVisible, child: _TextSelectionHandleOverlay( - onSelectionHandleChanged: (TextSelection? newSelection) { + onSelectionHandleChanged: (newSelection) { _handleSelectionHandleChanged(newSelection, position); }, onSelectionHandleTapped: onSelectionHandleTapped, @@ -231,10 +231,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)), ]; @@ -659,7 +659,7 @@ class _EditorTextSelectionGestureDetectorState gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { + (instance) { instance ..onTapDown = _handleTapDown ..onTapUp = _handleTapUp @@ -674,7 +674,7 @@ class _EditorTextSelectionGestureDetectorState GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer( debugOwner: this, kind: PointerDeviceKind.touch), - (LongPressGestureRecognizer instance) { + (instance) { instance ..onLongPressStart = _handleLongPressStart ..onLongPressMoveUpdate = _handleLongPressMoveUpdate @@ -690,7 +690,7 @@ class _EditorTextSelectionGestureDetectorState GestureRecognizerFactoryWithHandlers( () => HorizontalDragGestureRecognizer( debugOwner: this, kind: PointerDeviceKind.mouse), - (HorizontalDragGestureRecognizer instance) { + (instance) { instance ..dragStartBehavior = DragStartBehavior.down ..onStart = _handleDragStart @@ -704,7 +704,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 0941760f..9348c3af 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -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(); From 0a136edb8667255ec8c392b2a007d2d718b570ba Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:29:01 +0200 Subject: [PATCH 140/306] Avoid void async When declaring an async method or function which does not return a value, declare that it returns Future and not just void. --- analysis_options.yaml | 3 ++- lib/widgets/editor.dart | 2 +- lib/widgets/raw_editor.dart | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index a7b7b075..b00ad5ad 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,6 +13,8 @@ linter: - avoid_escaping_inner_quotes - avoid_print - avoid_redundant_argument_values + - avoid_types_on_closure_parameters + - avoid_void_async - directives_ordering - omit_local_variable_types - prefer_const_constructors @@ -24,4 +26,3 @@ linter: - prefer_relative_imports - prefer_single_quotes - unnecessary_parenthesis - - avoid_types_on_closure_parameters diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index f1faaec6..1b92dece 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -427,7 +427,7 @@ class _QuillEditorSelectionGestureDetectorBuilder return true; } - void _launchUrl(String url) async { + Future _launchUrl(String url) async { await launch(url); } diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 9747901f..fc07a110 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -806,7 +806,7 @@ class RawEditorState extends EditorState ); } - void handleShortcut(InputShortcut? shortcut) async { + Future handleShortcut(InputShortcut? shortcut) async { final selection = widget.controller.selection; final plainText = textEditingValue.text; if (shortcut == InputShortcut.COPY) { @@ -1058,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, From 78594aa266eb3eb895f93e2dab6073fafb845c58 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:36:59 +0200 Subject: [PATCH 141/306] Cascade invocations Use the cascading style when succesively invoking methods on the same reference. --- analysis_options.yaml | 1 + lib/models/documents/nodes/block.dart | 5 +++-- lib/models/documents/nodes/leaf.dart | 6 ++---- lib/models/documents/nodes/line.dart | 9 +++----- lib/models/documents/nodes/node.dart | 4 +--- lib/models/rules/delete.dart | 3 +-- lib/models/rules/format.dart | 6 ++---- lib/models/rules/insert.dart | 8 +++---- lib/widgets/cursor.dart | 5 +++-- lib/widgets/editor.dart | 4 ++-- lib/widgets/proxy.dart | 17 ++++++++------- lib/widgets/raw_editor.dart | 24 +++++++++++---------- lib/widgets/text_block.dart | 11 +++++----- lib/widgets/text_line.dart | 31 +++++++++++++-------------- lib/widgets/text_selection.dart | 7 +++--- 15 files changed, 68 insertions(+), 73 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index b00ad5ad..5e1da9dc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -15,6 +15,7 @@ linter: - avoid_redundant_argument_values - avoid_types_on_closure_parameters - avoid_void_async + - cascade_invocations - directives_ordering - omit_local_variable_types - prefer_const_constructors diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index 0919806f..5128f598 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -31,8 +31,9 @@ class Block extends Container { 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; } final next = block.next; diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index 88aeca6c..3d98060e 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -141,8 +141,7 @@ abstract class Leaf extends Node { assert(this is Text); final text = _value as String; _value = text.substring(0, index); - final split = Leaf(text.substring(index)); - split.applyStyle(style); + final split = Leaf(text.substring(index))..applyStyle(style); insertAfter(split); return split; } @@ -158,8 +157,7 @@ abstract class Leaf extends Node { Leaf _isolate(int index, int length) { assert( index >= 0 && index < this.length && (index + length <= this.length)); - final target = splitAt(index)!; - target.splitAt(length); + final target = splitAt(index)!..splitAt(length); return target; } } diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 7dba241f..08ec39ee 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -173,14 +173,12 @@ class Line extends Container { _unwrap(); } else if (blockStyle != parentStyle) { _unwrap(); - final block = Block(); - block.applyAttribute(blockStyle); + final block = Block()..applyAttribute(blockStyle); _wrap(block); block.adjust(); } } else if (blockStyle.value != null) { - final block = Block(); - block.applyAttribute(blockStyle); + final block = Block()..applyAttribute(blockStyle); _wrap(block); block.adjust(); } @@ -234,8 +232,7 @@ class Line extends Container { final query = queryChild(index, false); while (!query.node!.isLast) { - final next = last as Leaf; - next.unlink(); + final next = (last as Leaf)..unlink(); line.addFirst(next); } final child = query.node as Leaf; diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index d46b4647..ddd1078c 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -32,9 +32,7 @@ abstract class Node extends LinkedListEntry { int get length; Node clone() { - final node = newInstance(); - node.applyStyle(style); - return node; + return newInstance()..applyStyle(style); } int getOffset() { diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index def2e8c7..e6682f94 100644 --- a/lib/models/rules/delete.dart +++ b/lib/models/rules/delete.dart @@ -34,8 +34,7 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { @override Delta? applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}) { - final itr = DeltaIterator(document); - itr.skip(index); + final itr = DeltaIterator(document)..skip(index); var op = itr.next(1); if (op.data != '\n') { return null; diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart index 470ca200..be201925 100644 --- a/lib/models/rules/format.dart +++ b/lib/models/rules/format.dart @@ -27,8 +27,7 @@ class ResolveLineFormatRule extends FormatRule { } var delta = Delta()..retain(index); - final itr = DeltaIterator(document); - itr.skip(index); + final itr = DeltaIterator(document)..skip(index); Operation op; for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { op = itr.next(len - cur); @@ -106,8 +105,7 @@ class ResolveInlineFormatRule extends FormatRule { } final delta = Delta()..retain(index); - final itr = DeltaIterator(document); - itr.skip(index); + final itr = DeltaIterator(document)..skip(index); Operation op; for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 97f386d5..f7d029a4 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -66,8 +66,7 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { return null; } - final itr = DeltaIterator(document); - itr.skip(index); + final itr = DeltaIterator(document)..skip(index); final nextNewLine = _getNextNewLine(itr); final lineStyle = @@ -101,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); } @@ -172,8 +171,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule { return null; } - final itr = DeltaIterator(document); - itr.skip(index); + final itr = DeltaIterator(document)..skip(index); final cur = itr.next(); if (cur.data is! String || !(cur.data as String).startsWith('\n')) { return null; diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index 8a22c7c2..6c9ce734 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -133,8 +133,9 @@ class CursorCont extends ChangeNotifier { _blinkOpacityCont.value = 0.0; if (style.opacityAnimates) { - _blinkOpacityCont.stop(); - _blinkOpacityCont.value = 0.0; + _blinkOpacityCont + ..stop() + ..value = 0.0; } } diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 1b92dece..b77b4673 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -982,8 +982,8 @@ class RenderEditableContainerBox extends RenderBox .deflate(_resolvedPadding!); while (child != null) { child.layout(innerConstraints, parentUsesSize: true); - final 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; diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index 71fc219d..ef763b9b 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -160,14 +160,15 @@ class RichTextProxy extends SingleChildRenderObjectWidget { @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 fc07a110..2f076a20 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -906,8 +906,9 @@ 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( @@ -1168,14 +1169,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/text_block.dart b/lib/widgets/text_block.dart index b9808fad..9cb40275 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -538,11 +538,12 @@ 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; } } diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 99075411..8eff21c3 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -120,14 +120,13 @@ class TextLine extends StatelessWidget { final style = textNode.style; var res = const TextStyle(); - final 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!); } @@ -244,15 +243,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() { @@ -694,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) { @@ -704,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.0, _resolvedPadding!.top); } size = constraints.constrain(Size( diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index c113faeb..ff008742 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -155,9 +155,10 @@ 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) { From 32b6c9169a1fd749a48d66567dd82397f23b43e4 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:38:34 +0200 Subject: [PATCH 142/306] Prefer initializing formals --- analysis_options.yaml | 1 + lib/widgets/cursor.dart | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 5e1da9dc..9b96f35d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -24,6 +24,7 @@ linter: - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals + - prefer_initializing_formals - prefer_relative_imports - prefer_single_quotes - unnecessary_parenthesis diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index 6c9ce734..a2d7488b 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -64,11 +64,10 @@ class CursorCont extends ChangeNotifier { 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 = From 5c18ca1dc3977b0d2c69fc9e48e7670a4ec937c5 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:45:49 +0200 Subject: [PATCH 143/306] Prefer int literals --- analysis_options.yaml | 1 + lib/widgets/cursor.dart | 2 +- lib/widgets/default_styles.dart | 50 ++++++++++++++++----------------- lib/widgets/editor.dart | 24 ++++++++-------- lib/widgets/proxy.dart | 6 ++-- lib/widgets/raw_editor.dart | 4 +-- lib/widgets/text_block.dart | 16 +++++------ lib/widgets/text_line.dart | 22 +++++++-------- lib/widgets/text_selection.dart | 2 +- lib/widgets/toolbar.dart | 6 ++-- 10 files changed, 66 insertions(+), 67 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 9b96f35d..22b167da 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -25,6 +25,7 @@ linter: - prefer_final_in_for_each - prefer_final_locals - prefer_initializing_formals + - prefer_int_literals - prefer_relative_imports - prefer_single_quotes - unnecessary_parenthesis diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index a2d7488b..307e70a5 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -176,7 +176,7 @@ class CursorPainter { } if (caretRect.left < 0.0) { - caretRect = caretRect.shift(Offset(-caretRect.left, 0.0)); + caretRect = caretRect.shift(Offset(-caretRect.left, 0)); } final caretHeight = editable!.getFullHeightForCaret(position); diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 2f2ea02c..8612d92b 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -87,10 +87,10 @@ class DefaultStyles { final themeData = Theme.of(context); final defaultTextStyle = DefaultTextStyle.of(context); final baseStyle = defaultTextStyle.style.copyWith( - fontSize: 16.0, + fontSize: 16, height: 1.3, ); - const baseSpacing = Tuple2(6.0, 0); + const baseSpacing = Tuple2(6, 0); String fontFamily; switch (themeData.platform) { case TargetPlatform.iOS: @@ -110,36 +110,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 +150,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 +172,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/editor.dart b/lib/widgets/editor.dart index b77b4673..c3d5d2cb 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -220,7 +220,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; @@ -252,7 +252,7 @@ class _QuillEditorState extends State CursorStyle( color: cursorColor, backgroundColor: Colors.grey, - width: 2.0, + width: 2, radius: cursorRadius, offset: cursorOffset, paintAboveText: paintCursorAboveText, @@ -580,7 +580,7 @@ class RenderEditor extends RenderEditableContainerBox final parentData = child.parentData as BoxParentData; return [ TextSelectionPoint( - Offset(0.0, child.preferredLineHeight(localPosition)) + + Offset(0, child.preferredLineHeight(localPosition)) + localOffset + parentData.offset, null) @@ -853,7 +853,7 @@ class RenderEditor extends RenderEditableContainerBox if (dy == null) { return null; } - return math.max(dy, 0.0); + return math.max(dy, 0); } } @@ -1020,8 +1020,8 @@ class RenderEditableContainerBox extends RenderBox double computeMinIntrinsicWidth(double height) { _resolvePadding(); return _getIntrinsicCrossAxis((child) { - final childHeight = math.max( - 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); + final childHeight = math.max( + 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMinIntrinsicWidth(childHeight) + _resolvedPadding!.left + _resolvedPadding!.right; @@ -1032,8 +1032,8 @@ class RenderEditableContainerBox extends RenderBox double computeMaxIntrinsicWidth(double height) { _resolvePadding(); return _getIntrinsicCrossAxis((child) { - final childHeight = math.max( - 0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); + final childHeight = math.max( + 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); return child.getMaxIntrinsicWidth(childHeight) + _resolvedPadding!.left + _resolvedPadding!.right; @@ -1044,8 +1044,8 @@ class RenderEditableContainerBox extends RenderBox double computeMinIntrinsicHeight(double width) { _resolvePadding(); return _getIntrinsicMainAxis((child) { - final childWidth = math.max( - 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); + final childWidth = math.max( + 0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMinIntrinsicHeight(childWidth) + _resolvedPadding!.top + _resolvedPadding!.bottom; @@ -1056,8 +1056,8 @@ class RenderEditableContainerBox extends RenderBox double computeMaxIntrinsicHeight(double width) { _resolvePadding(); return _getIntrinsicMainAxis((child) { - final childWidth = math.max( - 0.0, width - _resolvedPadding!.left + _resolvedPadding!.right); + final childWidth = math.max( + 0, width - _resolvedPadding!.left + _resolvedPadding!.right); return child.getMaxIntrinsicHeight(childWidth) + _resolvedPadding!.top + _resolvedPadding!.bottom; diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index ef763b9b..bae59f1b 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -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) ]; } 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 diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 2f076a20..cf293aba 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -596,7 +596,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, @@ -988,7 +988,7 @@ class RawEditorState extends EditorState 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( diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 9cb40275..757a2c9c 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -142,8 +142,8 @@ class EditableTextBlock extends StatelessWidget { count: count, style: defaultStyles!.leading!.style, attrs: attrs, - width: 32.0, - padding: 8.0, + width: 32, + padding: 8, ); } @@ -172,9 +172,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, ); } @@ -419,7 +419,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) { if (selection.isCollapsed) { return TextSelectionPoint( - Offset(0.0, preferredLineHeight(selection.extent)) + + Offset(0, preferredLineHeight(selection.extent)) + getOffsetForCaret(selection.extent), null); } @@ -445,7 +445,7 @@ 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); } @@ -666,7 +666,7 @@ class _BulletPoint extends StatelessWidget { return Container( alignment: AlignmentDirectional.topEnd, width: width, - padding: const EdgeInsetsDirectional.only(end: 13.0), + padding: const EdgeInsetsDirectional.only(end: 13), child: Text('•', style: style), ); } @@ -707,7 +707,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 8eff21c3..304742ac 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -57,7 +57,7 @@ class TextLine extends StatelessWidget { textSpan.style!, textAlign, textDirection!, - 1.0, + 1, Localizations.localeOf(context), strutStyle, TextWidthBasis.parent, @@ -458,7 +458,7 @@ 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); } @@ -473,7 +473,7 @@ class RenderEditableTextLine extends RenderEditableBox { @override TextRange getLineBoundary(TextPosition position) { final lineDy = getOffsetForCaret(position) - .translate(0.0, 0.5 * preferredLineHeight(position)) + .translate(0, 0.5 * preferredLineHeight(position)) .dy; final lineBoxes = _getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1)) @@ -543,15 +543,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'; @@ -619,7 +617,7 @@ class RenderEditableTextLine extends RenderEditableBox { : _leading!.getMinIntrinsicWidth(height - verticalPadding) as int; 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; } @@ -634,7 +632,7 @@ class RenderEditableTextLine extends RenderEditableBox { : _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int; 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; } @@ -646,7 +644,7 @@ class RenderEditableTextLine extends RenderEditableBox { 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; @@ -659,7 +657,7 @@ class RenderEditableTextLine extends RenderEditableBox { 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; @@ -704,7 +702,7 @@ class RenderEditableTextLine extends RenderEditableBox { maxHeight: _body!.size.height); _leading!.layout(leadingConstraints, parentUsesSize: true); (_leading!.parentData as BoxParentData).offset = - Offset(0.0, _resolvedPadding!.top); + Offset(0, _resolvedPadding!.top); } size = constraints.constrain(Size( diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index ff008742..d6d24467 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -99,7 +99,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( diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 9348c3af..73e0c9a7 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -14,7 +14,7 @@ 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); @@ -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)), @@ -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, From 5bab9188608b60d0237021365268e05128d2b720 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:47:14 +0200 Subject: [PATCH 144/306] Prefer interpolation to compose strings Using interpolation when composing strings and values is usually easier to write and read than concatenation. --- analysis_options.yaml | 1 + lib/models/documents/nodes/line.dart | 2 +- lib/utils/color.dart | 2 +- lib/utils/diff_delta.dart | 6 +----- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 22b167da..df405fed 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,6 +26,7 @@ linter: - prefer_final_locals - prefer_initializing_formals - prefer_int_literals + - prefer_interpolation_to_compose_strings - prefer_relative_imports - prefer_single_quotes - unnecessary_parenthesis diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 08ec39ee..f3473ac9 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -55,7 +55,7 @@ class Line extends Container { } @override - String toPlainText() => super.toPlainText() + '\n'; + String toPlainText() => '${super.toPlainText()}\n'; @override String toString() { diff --git a/lib/utils/color.dart b/lib/utils/color.dart index f4c26040..93b6e12b 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -119,7 +119,7 @@ Color stringToColor(String? s) { } var hex = s.replaceFirst('#', ''); - hex = hex.length == 6 ? 'ff' + hex : hex; + 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 a3653eb0..1e6d913e 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -79,11 +79,7 @@ int getPositionDelta(Delta user, Delta actual) { 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; From 483dba0e4a113d62e7252f881dfe28fd5a4dc709 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 20:49:27 +0200 Subject: [PATCH 145/306] Remove unnecessary string interpolations --- analysis_options.yaml | 1 + lib/widgets/text_block.dart | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index df405fed..e62a6e5c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -30,3 +30,4 @@ linter: - prefer_relative_imports - prefer_single_quotes - unnecessary_parenthesis + - unnecessary_string_interpolations diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 757a2c9c..61448d5a 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -580,7 +580,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)) { @@ -611,7 +611,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), ); } From e1793ead612a6a896ca07790e0edcab66a446f88 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 21:03:54 +0200 Subject: [PATCH 146/306] Fix linter warnings in example --- example/lib/pages/home_page.dart | 10 +++++----- example/lib/pages/read_only_page.dart | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index a613c085..34441303 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -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( diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index 395c20f5..42957b52 100644 --- a/example/lib/pages/read_only_page.dart +++ b/example/lib/pages/read_only_page.dart @@ -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, From ff61b1a95111be7bea17d55f0fdd8e4fb8ea1105 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 21:15:58 +0200 Subject: [PATCH 147/306] Fix more warnings (forgot to save the files) --- example/lib/pages/home_page.dart | 16 ++++++++-------- example/lib/universal_ui/universal_ui.dart | 2 +- example/test/widget_test.dart | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 34441303..075f3f9b 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -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, ), ), @@ -186,7 +186,7 @@ class _HomePageState extends State { Widget _buildMenuBar(BuildContext context) { final size = MediaQuery.of(context).size; - final itemStyle = const TextStyle( + 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/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart index 9c833aa2..b242af34 100644 --- a/example/lib/universal_ui/universal_ui.dart +++ b/example/lib/universal_ui/universal_ui.dart @@ -31,7 +31,7 @@ Widget defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) { 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/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()); From 9a29446c97d58ba525bb227d4e7ede7fd62db105 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 21:21:23 +0200 Subject: [PATCH 148/306] Make the issue template a little more friendly --- .github/ISSUE_TEMPLATE/issue-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md index 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. From 873bfbcee112d18143cfc3b589524f0ba4f6e49b Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 21:46:10 +0200 Subject: [PATCH 149/306] Sort constructors first --- analysis_options.yaml | 2 + example/lib/widgets/demo_scaffold.dart | 14 +-- lib/models/documents/attribute.dart | 4 +- lib/models/documents/document.dart | 24 ++-- lib/models/documents/history.dart | 21 ++-- lib/models/documents/nodes/container.dart | 4 +- lib/models/documents/nodes/embed.dart | 4 +- lib/models/documents/nodes/leaf.dart | 12 +- lib/models/documents/style.dart | 4 +- lib/models/quill_delta.dart | 68 +++++------ lib/models/rules/rule.dart | 4 +- lib/utils/diff_delta.dart | 4 +- lib/widgets/controller.dart | 8 +- lib/widgets/cursor.dart | 40 +++---- lib/widgets/default_styles.dart | 59 +++++----- lib/widgets/delegate.dart | 4 +- lib/widgets/editor.dart | 126 ++++++++++---------- lib/widgets/keyboard_listener.dart | 5 +- lib/widgets/proxy.dart | 30 ++--- lib/widgets/raw_editor.dart | 66 +++++------ lib/widgets/responsive_widget.dart | 8 +- lib/widgets/text_block.dart | 71 ++++++------ lib/widgets/text_line.dart | 62 +++++----- lib/widgets/text_selection.dart | 41 +++---- lib/widgets/toolbar.dart | 134 +++++++++++----------- 25 files changed, 422 insertions(+), 397 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index e62a6e5c..2fc4fec6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -29,5 +29,7 @@ linter: - 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/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/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 7683b9ed..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, diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 232642fb..0362665e 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -15,6 +15,18 @@ 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); diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index 9398ad1c..d406505e 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -4,6 +4,14 @@ 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) { @@ -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/container.dart b/lib/models/documents/nodes/container.dart index 8b3fd0c3..28fc2475 100644 --- a/lib/models/documents/nodes/container.dart +++ b/lib/models/documents/nodes/container.dart @@ -116,9 +116,9 @@ abstract class Container extends Node { /// Query of a child in a Container class ChildQuery { + ChildQuery(this.node, this.offset); + final Node? node; // null if not found final int offset; - - ChildQuery(this.node, this.offset); } diff --git a/lib/models/documents/nodes/embed.dart b/lib/models/documents/nodes/embed.dart index cc0ecaea..81ca6005 100644 --- a/lib/models/documents/nodes/embed.dart +++ b/lib/models/documents/nodes/embed.dart @@ -1,9 +1,9 @@ class Embeddable { + Embeddable(this.type, this.data); + final String type; final dynamic data; - Embeddable(this.type, this.data); - Map toJson() { final m = {type: data}; return m; diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index 3d98060e..24431fc9 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -8,12 +8,6 @@ import 'node.dart'; /* A leaf node in document tree */ abstract class Leaf extends Node { - Object _value; - - Object get value => _value; - - Leaf.val(Object val) : _value = val; - factory Leaf(Object data) { if (data is Embeddable) { return Embed(data); @@ -23,6 +17,12 @@ abstract class Leaf extends Node { return Text(text); } + Leaf.val(Object val) : _value = val; + + Object _value; + + Object get value => _value; + @override void applyStyle(Style value) { assert(value.isInline || value.isIgnored || value.isEmpty, diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index f8a8ee74..c805280d 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -5,11 +5,11 @@ 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) { diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index 0135bd6a..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. @@ -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) { @@ -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 @@ -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; diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index 19bc9177..4ee6c278 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -26,6 +26,8 @@ abstract class Rule { } class Rules { + Rules(this._rules); + final List _rules; static final Rules _instance = Rules([ const FormatLinkAtCaretPositionRule(), @@ -45,8 +47,6 @@ class Rules { const CatchAllDeleteRule(), ]); - Rules(this._rules); - static Rules getInstance() => _instance; Delta apply(RuleType ruleType, Document document, int index, diff --git a/lib/utils/diff_delta.dart b/lib/utils/diff_delta.dart index 1e6d913e..003bae47 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -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"]'; diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index faabf4c2..3bd66b6f 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -11,10 +11,6 @@ import '../models/quill_delta.dart'; import '../utils/diff_delta.dart'; class QuillController extends ChangeNotifier { - final Document document; - TextSelection selection; - Style toggledStyle = Style(); - QuillController({required this.document, required this.selection}); factory QuillController.basic() { @@ -24,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. diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index 307e70a5..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,14 +55,6 @@ 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 this.show, required CursorStyle style, @@ -75,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; @@ -156,15 +156,15 @@ 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); diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 8612d92b..1cebe135 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -3,14 +3,14 @@ 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; @@ -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,28 +88,6 @@ 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) { final themeData = Theme.of(context); final defaultTextStyle = DefaultTextStyle.of(context); diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index 8adf8ce7..4b4bdea7 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -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; } diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index c3d5d2cb..3b33211c 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -114,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; @@ -134,40 +171,6 @@ class QuillEditor extends StatefulWidget { final ValueChanged? onLaunchUrl; 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.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(); } @@ -295,10 +298,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); @@ -495,6 +498,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; @@ -510,24 +531,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; @@ -866,17 +869,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; } diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart index f952c3dd..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. diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index bae59f1b..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( @@ -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,18 +157,6 @@ 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) { diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index cf293aba..72422991 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -28,6 +28,39 @@ 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; @@ -53,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(); 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 61448d5a..25ea8d9c 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -47,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; @@ -60,20 +75,6 @@ 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)); @@ -509,16 +510,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); @@ -548,15 +554,6 @@ class _EditableBlock extends MultiChildRenderObjectWidget { } 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, @@ -569,6 +566,15 @@ 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) { var s = index.toString(); @@ -652,15 +658,15 @@ 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( @@ -673,12 +679,13 @@ class _BulletPoint extends StatelessWidget { } 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(); } diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 304742ac..43cc6969 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -20,11 +20,6 @@ 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, @@ -33,6 +28,11 @@ 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)); @@ -194,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; @@ -207,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); @@ -266,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; @@ -283,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!; diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index d6d24467..fe28741b 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -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) { - final 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; diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 73e0c9a7..71ddec1d 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -21,15 +21,15 @@ 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(); } @@ -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(); } @@ -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(); @@ -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(); } @@ -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(); } @@ -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(); } @@ -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(); } From 8c663ad9f5d9258137dc05ec055109e02db245e4 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 8 Apr 2021 23:30:54 +0200 Subject: [PATCH 150/306] Add comments from Zefyr to nodes folder Since quite a lot of code was taken from Zefyr, the license should be changed to BSD to meet the requirements of Zefyr. --- lib/models/documents/nodes/block.dart | 19 ++- lib/models/documents/nodes/container.dart | 50 ++++++-- lib/models/documents/nodes/embed.dart | 16 ++- lib/models/documents/nodes/leaf.dart | 108 ++++++++++++----- lib/models/documents/nodes/line.dart | 139 +++++++++++++++------- lib/models/documents/nodes/node.dart | 75 +++++++----- lib/widgets/editor.dart | 18 ++- lib/widgets/raw_editor.dart | 9 +- lib/widgets/text_block.dart | 34 +++--- lib/widgets/text_line.dart | 10 +- lib/widgets/text_selection.dart | 4 +- 11 files changed, 323 insertions(+), 159 deletions(-) diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index 5128f598..095f1183 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -3,7 +3,21 @@ 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(); @@ -55,9 +69,4 @@ class Block extends Container { } 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 28fc2475..dbdd12d1 100644 --- a/lib/models/documents/nodes/container.dart +++ b/lib/models/documents/nodes/container.dart @@ -1,48 +1,67 @@ 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; @@ -55,9 +74,19 @@ abstract class Container extends Node { 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); @@ -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); @@ -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 { ChildQuery(this.node, this.offset); - final Node? node; // null if not found + /// The child node if found, otherwise `null`. + final Node? node; + /// 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 81ca6005..d6fe628a 100644 --- a/lib/models/documents/nodes/embed.dart +++ b/lib/models/documents/nodes/embed.dart @@ -1,7 +1,15 @@ +/// 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; + + /// The data payload of this object. final dynamic data; Map toJson() { @@ -17,10 +25,16 @@ class Embeddable { } } +/// 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 24431fc9..bd9292f5 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -6,8 +6,9 @@ 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 { + /// Creates a new [Leaf] with specified [data]. factory Leaf(Object data) { if (data is Embeddable) { return Embed(data); @@ -19,9 +20,10 @@ abstract class Leaf extends Node { Leaf.val(Object val) : _value = val; - Object _value; - + /// 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) { @@ -99,14 +101,21 @@ 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; } + // 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 + + // 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; @@ -114,7 +123,7 @@ abstract class Leaf extends Node { node = prev; } - // merging it with next node if style is the same + // 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; @@ -122,13 +131,17 @@ abstract class Leaf extends Node { } } - Leaf? cutAt(int index) { - assert(index >= 0 && index <= length); - final 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) { @@ -146,14 +159,33 @@ abstract class Leaf extends Node { 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)); @@ -162,39 +194,59 @@ abstract class Leaf extends Node { } } +/// 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 f3473ac9..ec933b52 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -9,6 +9,12 @@ 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(); @@ -16,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; @@ -24,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?; @@ -40,6 +48,9 @@ class Line extends Container { : parent!.next as Line?; } + @override + Node newInstance() => Line(); + @override Delta toDelta() { final delta = children @@ -67,34 +78,42 @@ 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; } 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; } final prefix = text.substring(0, lineBreak); - _insert(index, prefix, style); + _insertSafe(index, prefix, style); if (prefix.isNotEmpty) { index += prefix.length; } + // 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 + // Continue with remaining part. final remain = text.substring(lineBreak + 1); nextLine.insert(0, remain, style); } @@ -104,16 +123,20 @@ class Line extends Container { if (style == null) { return; } - final thisLen = length; + final thisLength = length; - final 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); } @@ -127,35 +150,47 @@ class Line extends Container { @override void delete(int index, int? len) { final local = math.min(length - index, len!); - final deleted = index + local == length; - if (deleted) { + 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); } - final 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) { - final 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; @@ -165,7 +200,7 @@ class Line extends Container { final blockStyle = newStyle.getBlockExceptHeader(); if (blockStyle == null) { return; - } + } // No block-level changes if (parent is Block) { final parentStyle = (parent as Block).style.getBlockExceptHeader(); @@ -176,14 +211,18 @@ class Line extends Container { final block = Block()..applyAttribute(blockStyle); _wrap(block); block.adjust(); - } + } // else the same style, no-op. } else if (blockStyle.value != null) { + // Only wrap with a new block if this is not an unset final block = Block()..applyAttribute(blockStyle); _wrap(block); block.adjust(); } } + /// 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); @@ -191,6 +230,9 @@ 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'); @@ -242,7 +284,7 @@ class Line extends Container { 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) { @@ -252,46 +294,50 @@ class Line extends Container { } } - if (isNotEmpty) { + 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; } - - final 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) { final local = math.min(length - offset, len); - var res = Style(); + var result = Style(); final excluded = {}; void _handle(Style style) { - if (res.isEmpty) { + if (result.isEmpty) { excluded.addAll(style.values); } else { - for (final attr in res.values) { + for (final attr in result.values) { if (!style.containsKey(attr.key)) { excluded.add(attr); } } } - final 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); } final data = queryChild(offset, true); var node = data.node as Leaf?; if (node != null) { - res = res.mergeAll(node.style); + result = result.mergeAll(node.style); var pos = node.length - data.offset; while (!node!.isLast && pos < local) { node = node.next as Leaf?; @@ -300,17 +346,18 @@ class Line extends Container { } } - res = res.mergeAll(style); + result = result.mergeAll(style); if (parent is Block) { final block = parent as Block; - res = res.mergeAll(block.style); + result = result.mergeAll(block.style); } - final 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 ddd1078c..6bb0fb97 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -6,36 +6,37 @@ 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() { - return newInstance()..applyStyle(style); - } + Node clone() => newInstance()..applyStyle(style); - int getOffset() { + /// 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) { @@ -50,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); @@ -81,9 +97,7 @@ abstract class Node extends LinkedListEntry { super.unlink(); } - void adjust() { - // do nothing - } + void adjust() {/* no-op */} /// abstract methods begin @@ -100,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(); @@ -112,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/widgets/editor.dart b/lib/widgets/editor.dart index 3b33211c..4d2ce846 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -577,8 +577,7 @@ class RenderEditor extends RenderEditableContainerBox if (textSelection.isCollapsed) { final child = childAtPosition(textSelection.extent); final localPosition = TextPosition( - offset: - textSelection.extentOffset - child.getContainer().getOffset()); + offset: textSelection.extentOffset - child.getContainer().offset); final localOffset = child.getOffsetForCaret(localPosition); final parentData = child.parentData as BoxParentData; return [ @@ -677,7 +676,7 @@ class RenderEditor extends RenderEditableContainerBox assert(_lastTapDownPosition != null); final position = getPositionForOffset(_lastTapDownPosition!); final child = childAtPosition(position); - final nodeOffset = child.getContainer().getOffset(); + final nodeOffset = child.getContainer().offset; final localPosition = TextPosition( offset: position.offset - nodeOffset, affinity: position.affinity, @@ -738,7 +737,7 @@ class RenderEditor extends RenderEditableContainerBox @override TextSelection selectWordAtPosition(TextPosition position) { final child = childAtPosition(position); - final nodeOffset = child.getContainer().getOffset(); + final nodeOffset = child.getContainer().offset; final localPosition = TextPosition( offset: position.offset - nodeOffset, affinity: position.affinity); final localWord = child.getWordBoundary(localPosition); @@ -755,7 +754,7 @@ class RenderEditor extends RenderEditableContainerBox @override TextSelection selectLineAtPosition(TextPosition position) { final child = childAtPosition(position); - final nodeOffset = child.getContainer().getOffset(); + final nodeOffset = child.getContainer().offset; final localPosition = TextPosition( offset: position.offset - nodeOffset, affinity: position.affinity); final localLineRange = child.getLineBoundary(localPosition); @@ -810,8 +809,8 @@ class RenderEditor extends RenderEditableContainerBox @override double preferredLineHeight(TextPosition position) { final child = childAtPosition(position); - return child.preferredLineHeight(TextPosition( - offset: position.offset - child.getContainer().getOffset())); + return child.preferredLineHeight( + TextPosition(offset: position.offset - child.getContainer().offset)); } @override @@ -823,7 +822,7 @@ class RenderEditor extends RenderEditableContainerBox 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, ); } @@ -842,8 +841,7 @@ class RenderEditor extends RenderEditableContainerBox 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; diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 72422991..0ae9e82d 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -209,8 +209,7 @@ class RawEditorState extends EditorState final child = getRenderEditor()!.childAtPosition(originPosition); final localPosition = TextPosition( - offset: - originPosition.offset - child.getContainer().getDocumentOffset()); + offset: originPosition.offset - child.getContainer().documentOffset); var position = upKey ? child.getPositionAbove(localPosition) @@ -231,12 +230,12 @@ class RawEditorState extends EditorState .dy); 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) { diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 25ea8d9c..1c81f1ac 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -312,12 +312,12 @@ class RenderEditableTextBlock extends RenderEditableContainerBox TextRange getLineBoundary(TextPosition position) { final child = childAtPosition(position); final rangeInChild = child.getLineBoundary(TextPosition( - offset: position.offset - child.getContainer().getOffset(), + 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, ); } @@ -325,7 +325,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox Offset getOffsetForCaret(TextPosition 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; @@ -338,7 +338,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox final localPosition = child.getPositionForOffset(offset - parentData.offset); return TextPosition( - offset: localPosition.offset + child.getContainer().getOffset(), + offset: localPosition.offset + child.getContainer().offset, affinity: localPosition.affinity, ); } @@ -346,7 +346,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @override TextRange getWordBoundary(TextPosition position) { final child = childAtPosition(position); - final nodeOffset = child.getContainer().getOffset(); + final nodeOffset = child.getContainer().offset; final childWord = child .getWordBoundary(TextPosition(offset: position.offset - nodeOffset)); return TextRange( @@ -360,12 +360,11 @@ class RenderEditableTextBlock extends RenderEditableContainerBox assert(position.offset < getContainer().length); final child = childAtPosition(position); - final childLocalPosition = TextPosition( - offset: position.offset - child.getContainer().getOffset()); + 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); } final sibling = childBefore(child); @@ -379,7 +378,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox 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); } @@ -388,12 +387,11 @@ class RenderEditableTextBlock extends RenderEditableContainerBox assert(position.offset < getContainer().length); final child = childAtPosition(position); - final childLocalPosition = TextPosition( - offset: position.offset - child.getContainer().getOffset()); + 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); } final sibling = childAfter(child); @@ -405,15 +403,15 @@ class RenderEditableTextBlock extends RenderEditableContainerBox 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) { final child = childAtPosition(position); - return child.preferredLineHeight(TextPosition( - offset: position.offset - child.getContainer().getOffset())); + return child.preferredLineHeight( + TextPosition(offset: position.offset - child.getContainer().offset)); } @override diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 43cc6969..3106a08d 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -402,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() { @@ -735,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, @@ -772,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); diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index fe28741b..dbf9d856 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -12,10 +12,10 @@ import '../models/documents/nodes/node.dart'; import 'editor.dart'; TextSelection localSelection(Node node, TextSelection selection, fromParent) { - final base = fromParent ? node.getOffset() : node.getDocumentOffset(); + final base = fromParent ? node.offset : node.documentOffset; assert(base <= selection.end && selection.start <= base + node.length - 1); - final 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)); From ce60ed3b4bcfef07eb60611a0770e6dc2aa4b667 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 8 Apr 2021 16:54:46 -0700 Subject: [PATCH 151/306] Bump version to 1.1.8 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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/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 From 106e54c3fcd901979a9d498ec74780d96a2064ff Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Fri, 9 Apr 2021 01:19:07 -0700 Subject: [PATCH 152/306] add scrollBottomInset --- lib/widgets/editor.dart | 33 +++++++++++++++++++++++++-------- lib/widgets/raw_editor.dart | 8 ++++++++ lib/widgets/text_block.dart | 11 ++++++++++- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index d11653a1..53540eb4 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -119,6 +119,7 @@ class QuillEditor extends StatefulWidget { required this.focusNode, required this.scrollController, required this.scrollable, + required this.scrollBottomInset, required this.padding, required this.autoFocus, required this.readOnly, @@ -133,7 +134,12 @@ class QuillEditor extends StatefulWidget { this.keyboardAppearance = Brightness.light, this.scrollPhysics, this.onLaunchUrl, - this.embedBuilder = _defaultEmbedBuilder, + this.onTapDown, + this.onTapUp, + this.onSingleLongTapStart, + this.onSingleLongTapMoveUpdate, + this.onSingleLongTapEnd, + this.embedBuilder = _defaultEmbedBuilder }); factory QuillEditor.basic({ @@ -148,6 +154,7 @@ class QuillEditor extends StatefulWidget { autoFocus: true, readOnly: readOnly, expands: false, + scrollBottomInset: 0, padding: EdgeInsets.zero); } @@ -155,6 +162,7 @@ class QuillEditor extends StatefulWidget { final FocusNode focusNode; final ScrollController scrollController; final bool scrollable; + final double scrollBottomInset; final EdgeInsetsGeometry padding; final bool autoFocus; final bool? showCursor; @@ -249,6 +257,7 @@ class _QuillEditorState extends State widget.focusNode, widget.scrollController, widget.scrollable, + widget.scrollBottomInset, widget.padding, widget.readOnly, widget.placeholder, @@ -282,11 +291,6 @@ class _QuillEditorState extends State widget.keyboardAppearance, widget.enableInteractiveSelection, widget.scrollPhysics, - widget.onTapDown, - widget.onTapUp, - widget.onSingleLongTapStart, - widget.onSingleLongTapMoveUpdate, - widget.onSingleLongTapEnd, widget.embedBuilder), ); } @@ -569,6 +573,7 @@ class RenderEditor extends RenderEditableContainerBox RenderEditor( List? children, TextDirection textDirection, + double scrollBottomInset, EdgeInsetsGeometry padding, this.document, this.selection, @@ -581,6 +586,7 @@ class RenderEditor extends RenderEditableContainerBox children, document.root, textDirection, + scrollBottomInset, padding, ); @@ -639,6 +645,14 @@ class RenderEditor extends RenderEditableContainerBox markNeedsPaint(); } + void setScrollBottomInset(double value) { + if (scrollBottomInset == value) { + return; + } + scrollBottomInset = value; + markNeedsPaint(); + } + @override List getEndpointsForSelection( TextSelection textSelection) { @@ -911,8 +925,9 @@ class RenderEditor extends RenderEditableContainerBox child.preferredLineHeight(TextPosition( offset: selection.extentOffset - child.getContainer().offset)) - kMargin + - offsetInViewport; - final caretBottom = endpoint.point.dy + kMargin + offsetInViewport; + offsetInViewport + + scrollBottomInset; + final caretBottom = endpoint.point.dy + kMargin + offsetInViewport + scrollBottomInset; double? dy; if (caretTop < scrollOffset) { dy = caretTop; @@ -939,6 +954,7 @@ class RenderEditableContainerBox extends RenderBox List? children, this._container, this.textDirection, + this.scrollBottomInset, this._padding, ) : assert(_padding.isNonNegative) { addAll(children); @@ -947,6 +963,7 @@ class RenderEditableContainerBox extends RenderBox container_node.Container _container; TextDirection textDirection; EdgeInsetsGeometry _padding; + double scrollBottomInset; EdgeInsets? _resolvedPadding; container_node.Container getContainer() { diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 0ae9e82d..1627ebc3 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -34,6 +34,7 @@ class RawEditor extends StatefulWidget { this.focusNode, this.scrollController, this.scrollable, + this.scrollBottomInset, this.padding, this.readOnly, this.placeholder, @@ -65,6 +66,7 @@ class RawEditor extends StatefulWidget { final FocusNode focusNode; final ScrollController scrollController; final bool scrollable; + final double scrollBottomInset; final EdgeInsetsGeometry padding; final bool readOnly; final String? placeholder; @@ -527,6 +529,7 @@ class RawEditorState extends EditorState startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, onSelectionChanged: _handleSelectionChanged, + scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, children: _buildChildren(_doc, context), ), @@ -588,6 +591,7 @@ class RawEditorState extends EditorState final editableTextBlock = EditableTextBlock( node, _textDirection, + widget.scrollBottomInset, _getVerticalSpacingForBlock(node, _styles), widget.controller.selection, widget.selectionColor, @@ -1137,6 +1141,7 @@ class _Editor extends MultiChildRenderObjectWidget { required this.startHandleLayerLink, required this.endHandleLayerLink, required this.onSelectionChanged, + required this.scrollBottomInset, this.padding = EdgeInsets.zero, }) : super(key: key, children: children); @@ -1147,6 +1152,7 @@ class _Editor extends MultiChildRenderObjectWidget { final LayerLink startHandleLayerLink; final LayerLink endHandleLayerLink; final TextSelectionChangedHandler onSelectionChanged; + final double scrollBottomInset; final EdgeInsetsGeometry padding; @override @@ -1154,6 +1160,7 @@ class _Editor extends MultiChildRenderObjectWidget { return RenderEditor( null, textDirection, + scrollBottomInset, padding, document, selection, @@ -1177,6 +1184,7 @@ class _Editor extends MultiChildRenderObjectWidget { ..setStartHandleLayerLink(startHandleLayerLink) ..setEndHandleLayerLink(endHandleLayerLink) ..onSelectionChanged = onSelectionChanged + ..setScrollBottomInset(scrollBottomInset) ..setPadding(padding); } } diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 1c81f1ac..d826c9e5 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -50,6 +50,7 @@ class EditableTextBlock extends StatelessWidget { const EditableTextBlock( this.block, this.textDirection, + this.scrollBottomInset, this.verticalSpacing, this.textSelection, this.color, @@ -64,6 +65,7 @@ class EditableTextBlock extends StatelessWidget { final Block block; final TextDirection textDirection; + final double scrollBottomInset; final Tuple2 verticalSpacing; final TextSelection textSelection; final Color color; @@ -84,6 +86,7 @@ class EditableTextBlock extends StatelessWidget { block, textDirection, verticalSpacing as Tuple2, + scrollBottomInset, _getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(), contentPadding, _buildChildren(context, indentLevelCounts)); @@ -256,6 +259,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox required Block block, required TextDirection textDirection, required EdgeInsetsGeometry padding, + required double scrollBottomInset, required Decoration decoration, List? children, ImageConfiguration configuration = ImageConfiguration.empty, @@ -268,6 +272,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox children, block, textDirection, + scrollBottomInset, padding.add(contentPadding), ); @@ -512,14 +517,16 @@ class _EditableBlock extends MultiChildRenderObjectWidget { this.block, this.textDirection, this.padding, + this.scrollBottomInset, this.decoration, this.contentPadding, - List children, + List children ) : super(children: children); final Block block; final TextDirection textDirection; final Tuple2 padding; + final double scrollBottomInset; final Decoration decoration; final EdgeInsets? contentPadding; @@ -534,6 +541,7 @@ class _EditableBlock extends MultiChildRenderObjectWidget { block: block, textDirection: textDirection, padding: _padding, + scrollBottomInset: scrollBottomInset, decoration: decoration, contentPadding: _contentPadding, ); @@ -545,6 +553,7 @@ class _EditableBlock extends MultiChildRenderObjectWidget { renderObject ..setContainer(block) ..textDirection = textDirection + ..scrollBottomInset = scrollBottomInset ..setPadding(_padding) ..decoration = decoration ..contentPadding = _contentPadding; From e08009e3ff8dd1f26857a233a67fdecf06bd7d9d Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Fri, 9 Apr 2021 23:43:28 -0700 Subject: [PATCH 153/306] len fix --- lib/models/documents/document.dart | 8 +++++--- lib/models/rules/insert.dart | 23 +++++++++++------------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 0362665e..072b3a44 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -47,7 +47,7 @@ class Document { Stream> get changes => _observer.stream; - Delta insert(int index, Object? data) { + Delta insert(int index, Object? data, {int replaceLength = 0}) { assert(index >= 0); assert(data is String || data is Embeddable); if (data is Embeddable) { @@ -56,7 +56,7 @@ class Document { return Delta(); } - final delta = _rules.apply(RuleType.INSERT, this, index, data: data); + final delta = _rules.apply(RuleType.INSERT, this, index, data: data, len: replaceLength); compose(delta, ChangeSource.LOCAL); return delta; } @@ -80,8 +80,10 @@ class Document { var delta = Delta(); + // We have to insert before applying delete rules + // Otherwise delete would be operating on stale document snapshot. if (dataIsNotEmpty) { - delta = insert(index + len, data); + delta = insert(index, data, replaceLength: len); } if (len > 0) { diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index f7d029a4..b83c8838 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -13,7 +13,6 @@ abstract class InsertRule extends Rule { @override void validateArgs(int? len, Object? data, Attribute? attribute) { - assert(len == null); assert(data != null); assert(attribute == null); } @@ -43,7 +42,7 @@ class PreserveLineStyleOnSplitRule extends InsertRule { final text = after.data as String; - final delta = Delta()..retain(index); + final delta = Delta()..retain(index + (len ?? 0)); if (text.contains('\n')) { assert(after.isPlain); delta.insert('\n'); @@ -86,7 +85,7 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { } final lines = data.split('\n'); - final delta = Delta()..retain(index); + final delta = Delta()..retain(index + (len ?? 0)); for (var i = 0; i < lines.length; i++) { final line = lines[i]; if (line.isNotEmpty) { @@ -157,7 +156,7 @@ class AutoExitBlockRule extends InsertRule { .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k)); attributes[k] = null; // retain(1) should be '\n', set it with no attribute - return Delta()..retain(index)..retain(1, attributes); + return Delta()..retain(index + (len ?? 0))..retain(1, attributes); } } @@ -183,7 +182,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule { resetStyle = Attribute.header.toJson(); } return Delta() - ..retain(index) + ..retain(index + (len ?? 0)) ..insert('\n', cur.attributes) ..retain(1, resetStyle) ..trim(); @@ -200,7 +199,7 @@ class InsertEmbedsRule extends InsertRule { return null; } - final delta = Delta()..retain(index); + final delta = Delta()..retain(index + (len ?? 0)); final itr = DeltaIterator(document); final prev = itr.skip(index), cur = itr.next(); @@ -258,7 +257,7 @@ class ForceNewlineForInsertsAroundEmbedRule extends InsertRule { if (!cursorBeforeEmbed && !cursorAfterEmbed) { return null; } - final delta = Delta()..retain(index); + final delta = Delta()..retain(index + (len ?? 0)); if (cursorBeforeEmbed && !text.endsWith('\n')) { return delta..insert(text)..insert('\n'); } @@ -299,7 +298,7 @@ class AutoFormatLinksRule extends InsertRule { attributes.addAll(LinkAttribute(link.toString()).toJson()); return Delta() - ..retain(index - cand.length) + ..retain(index + (len ?? 0) - cand.length) ..retain(cand.length, attributes) ..insert(data, prev.attributes); } on FormatException { @@ -330,13 +329,13 @@ class PreserveInlineStylesRule extends InsertRule { final text = data; if (attributes == null || !attributes.containsKey(Attribute.link.key)) { return Delta() - ..retain(index) + ..retain(index + (len ?? 0)) ..insert(text, attributes); } attributes.remove(Attribute.link.key); final delta = Delta() - ..retain(index) + ..retain(index + (len ?? 0)) ..insert(text, attributes.isEmpty ? null : attributes); final next = itr.next(); @@ -346,7 +345,7 @@ class PreserveInlineStylesRule extends InsertRule { } if (attributes[Attribute.link.key] == nextAttributes[Attribute.link.key]) { return Delta() - ..retain(index) + ..retain(index + (len ?? 0)) ..insert(text, attributes); } return delta; @@ -360,7 +359,7 @@ class CatchAllInsertRule extends InsertRule { Delta applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}) { return Delta() - ..retain(index) + ..retain(index + (len ?? 0)) ..insert(data); } } From b7063f00ba79de71cac6318c394197613d350a2e Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Sat, 10 Apr 2021 01:41:54 -0700 Subject: [PATCH 154/306] toolbar button fillColor --- lib/widgets/toolbar.dart | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 71ddec1d..11753e8c 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -24,11 +24,13 @@ class InsertEmbedButton extends StatelessWidget { const InsertEmbedButton({ required this.controller, required this.icon, + this.fillColor, Key? key, }) : super(key: key); final QuillController controller; final IconData icon; + final Color? fillColor; @override Widget build(BuildContext context) { @@ -41,7 +43,7 @@ class InsertEmbedButton extends StatelessWidget { size: iconSize, color: Theme.of(context).iconTheme.color, ), - fillColor: Theme.of(context).canvasColor, + fillColor: fillColor ?? Theme.of(context).canvasColor, onPressed: () { final index = controller.selection.baseOffset; final length = controller.selection.extentOffset - index; @@ -169,6 +171,7 @@ typedef ToggleStyleButtonBuilder = Widget Function( BuildContext context, Attribute attribute, IconData icon, + Color? fillColor, bool? isToggled, VoidCallback? onPressed, ); @@ -178,6 +181,7 @@ class ToggleStyleButton extends StatefulWidget { required this.attribute, required this.icon, required this.controller, + this.fillColor, this.childBuilder = defaultToggleStyleButtonBuilder, Key? key, }) : super(key: key); @@ -186,6 +190,8 @@ class ToggleStyleButton extends StatefulWidget { final IconData icon; + final Color? fillColor; + final QuillController controller; final ToggleStyleButtonBuilder childBuilder; @@ -246,8 +252,7 @@ class _ToggleStyleButtonState extends State { _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); final isEnabled = !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key; - return widget.childBuilder(context, widget.attribute, widget.icon, - _isToggled, isEnabled ? _toggleAttribute : null); + return widget.childBuilder(context, widget.attribute, widget.icon, widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); } void _toggleAttribute() { @@ -262,12 +267,15 @@ class ToggleCheckListButton extends StatefulWidget { required this.icon, required this.controller, required this.attribute, + this.fillColor, this.childBuilder = defaultToggleStyleButtonBuilder, Key? key, }) : super(key: key); final IconData icon; + final Color? fillColor; + final QuillController controller; final ToggleStyleButtonBuilder childBuilder; @@ -331,8 +339,7 @@ class _ToggleCheckListButtonState extends State { _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); final isEnabled = !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key; - return widget.childBuilder(context, Attribute.unchecked, widget.icon, - _isToggled, isEnabled ? _toggleAttribute : null); + return widget.childBuilder(context, Attribute.unchecked, widget.icon, widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); } void _toggleAttribute() { @@ -346,6 +353,7 @@ Widget defaultToggleStyleButtonBuilder( BuildContext context, Attribute attribute, IconData icon, + Color? fillColor, bool? isToggled, VoidCallback? onPressed, ) { @@ -356,14 +364,14 @@ Widget defaultToggleStyleButtonBuilder( ? theme.primaryIconTheme.color : theme.iconTheme.color : theme.disabledColor; - final fillColor = - isToggled == true ? theme.toggleableActiveColor : theme.canvasColor; + final fill = + isToggled == true ? theme.toggleableActiveColor : fillColor ?? theme.canvasColor; return QuillIconButton( highlightElevation: 0, hoverElevation: 0, size: iconSize * 1.77, icon: Icon(icon, size: iconSize, color: iconColor), - fillColor: fillColor, + fillColor: fill, onPressed: onPressed, ); } From 3080fc2ad152bda16fca387ca6d23a0f16648e07 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Mon, 12 Apr 2021 12:05:10 +0200 Subject: [PATCH 155/306] Fix image button cancel causes crash (#145) I also restructured the code slightly to make it more readable and fixed some minor bugs in the desktop and web version. I also noticed that the image button doesn't work in the desktop version as well as in the web version. This is not part of the PR. Closes #137. --- lib/widgets/toolbar.dart | 133 +++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 11753e8c..82ab6125 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -18,7 +18,7 @@ double iconSize = 18; double kToolbarHeight = iconSize * 2; typedef OnImagePickCallback = Future Function(File file); -typedef ImagePickImpl = Future Function(ImageSource source); +typedef ImagePickImpl = Future Function(ImageSource source); class InsertEmbedButton extends StatelessWidget { const InsertEmbedButton({ @@ -252,7 +252,8 @@ class _ToggleStyleButtonState extends State { _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); final isEnabled = !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key; - return widget.childBuilder(context, widget.attribute, widget.icon, widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); + return widget.childBuilder(context, widget.attribute, widget.icon, + widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); } void _toggleAttribute() { @@ -339,7 +340,8 @@ class _ToggleCheckListButtonState extends State { _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); final isEnabled = !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key; - return widget.childBuilder(context, Attribute.unchecked, widget.icon, widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); + return widget.childBuilder(context, Attribute.unchecked, widget.icon, + widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); } void _toggleAttribute() { @@ -364,8 +366,9 @@ Widget defaultToggleStyleButtonBuilder( ? theme.primaryIconTheme.color : theme.iconTheme.color : theme.disabledColor; - final fill = - isToggled == true ? theme.toggleableActiveColor : fillColor ?? theme.canvasColor; + final fill = isToggled == true + ? theme.toggleableActiveColor + : fillColor ?? theme.canvasColor; return QuillIconButton( highlightElevation: 0, hoverElevation: 0, @@ -522,87 +525,81 @@ class ImageButton extends StatefulWidget { } class _ImageButtonState extends State { - List? _paths; - String? _extension; - final _picker = ImagePicker(); - final FileType _pickingType = FileType.any; + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); - Future _pickImage(ImageSource source) async { - final pickedFile = await _picker.getImage(source: source); - if (pickedFile == null) return null; + return QuillIconButton( + icon: Icon( + widget.icon, + size: iconSize, + color: theme.iconTheme.color, + ), + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: theme.canvasColor, + onPressed: _handleImageButtonTap, + ); + } - final file = File(pickedFile.path); + Future _handleImageButtonTap() async { + final index = widget.controller.selection.baseOffset; + final length = widget.controller.selection.extentOffset - index; - return widget.onImagePickCallback!(file); + String? imageUrl; + if (widget.imagePickImpl != null) { + imageUrl = await widget.imagePickImpl!(widget.imageSource); + } else { + if (kIsWeb) { + imageUrl = await _pickImageWeb(); + } else if (Platform.isAndroid || Platform.isIOS) { + imageUrl = await _pickImage(widget.imageSource); + } else { + imageUrl = await _pickImageDesktop(); + } + } + + if (imageUrl != null) { + widget.controller + .replaceText(index, length, BlockEmbed.image(imageUrl), null); + } } Future _pickImageWeb() async { - _paths = (await FilePicker.platform.pickFiles( - type: _pickingType, - allowedExtensions: (_extension?.isNotEmpty ?? false) - ? _extension?.replaceAll(' ', '').split(',') - : null, - )) - ?.files; - final _fileName = - _paths != null ? _paths!.map((e) => e.name).toString() : '...'; - - if (_paths != null) { - final file = File(_fileName); - // We simply return the absolute path to selected file. - return widget.onImagePickCallback!(file); - } else { - // User canceled the picker + final result = await FilePicker.platform.pickFiles(); + if (result == null) { + return null; + } + + // Take first, because we don't allow picking multiple files. + final fileName = result.files.first.name!; + final file = File(fileName); + + return widget.onImagePickCallback!(file); + } + + Future _pickImage(ImageSource source) async { + final pickedFile = await ImagePicker().getImage(source: source); + if (pickedFile == null) { + return null; } - return null; + + return widget.onImagePickCallback!(File(pickedFile.path)); } - Future _pickImageDesktop() async { + Future _pickImageDesktop() async { final filePath = await FilesystemPicker.open( context: context, rootDirectory: await getApplicationDocumentsDirectory(), fsType: FilesystemType.file, fileTileSelectMode: FileTileSelectMode.wholeTile, ); - if (filePath != null && filePath.isEmpty) return ''; + if (filePath == null || filePath.isEmpty) return null; - final file = File(filePath!); + final file = File(filePath); return widget.onImagePickCallback!(file); } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = theme.iconTheme.color; - final fillColor = theme.canvasColor; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * 1.77, - icon: Icon(widget.icon, size: iconSize, color: iconColor), - fillColor: fillColor, - onPressed: () { - final index = widget.controller.selection.baseOffset; - final length = widget.controller.selection.extentOffset - index; - Future image; - if (widget.imagePickImpl != null) { - image = widget.imagePickImpl!(widget.imageSource); - } else { - if (kIsWeb) { - image = _pickImageWeb(); - } else if (Platform.isAndroid || Platform.isIOS) { - image = _pickImage(widget.imageSource); - } else { - image = _pickImageDesktop(); - } - } - image.then((imageUploadUrl) => { - widget.controller.replaceText( - index, length, BlockEmbed.image(imageUploadUrl!), null) - }); - }, - ); - } } /// Controls color styles. From 63c2ef9d0e2211a7ef2eefe940111d7ae02df71c Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 12 Apr 2021 08:30:02 -0700 Subject: [PATCH 156/306] Upgrade to 1.2.0 --- CHANGELOG.md | 3 ++ lib/models/documents/document.dart | 3 +- lib/widgets/editor.dart | 86 ++++++++++++++++-------------- lib/widgets/text_block.dart | 16 +++--- pubspec.yaml | 2 +- 5 files changed, 61 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01145d24..8937de42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.2.0] +* Fix image button cancel causes crash. + ## [1.1.8] * Fix height of empty line bug. diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 072b3a44..83f7a0fc 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -56,7 +56,8 @@ class Document { return Delta(); } - final delta = _rules.apply(RuleType.INSERT, this, index, data: data, len: replaceLength); + final delta = _rules.apply(RuleType.INSERT, this, index, + data: data, len: replaceLength); compose(delta, ChangeSource.LOCAL); return delta; } diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 53540eb4..5be0dd24 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -114,33 +114,32 @@ 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.scrollBottomInset, - 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 - }); + 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.scrollBottomInset = 0, + 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, @@ -154,7 +153,6 @@ class QuillEditor extends StatefulWidget { autoFocus: true, readOnly: readOnly, expands: false, - scrollBottomInset: 0, padding: EdgeInsets.zero); } @@ -178,15 +176,20 @@ class QuillEditor extends StatefulWidget { final ScrollPhysics? scrollPhysics; final ValueChanged? onLaunchUrl; // Returns whether gesture is handled - final bool Function(TapDownDetails details, TextPosition textPosition)? onTapDown; + final bool Function(TapDownDetails details, TextPosition textPosition)? + onTapDown; // Returns whether gesture is handled final bool Function(TapUpDetails details, TextPosition textPosition)? onTapUp; // Returns whether gesture is handled - final bool Function(LongPressStartDetails details, TextPosition textPosition)? onSingleLongTapStart; + final bool Function(LongPressStartDetails details, TextPosition textPosition)? + onSingleLongTapStart; // Returns whether gesture is handled - final bool Function(LongPressMoveUpdateDetails details, TextPosition textPosition)? onSingleLongTapMoveUpdate; + final bool Function( + LongPressMoveUpdateDetails details, TextPosition textPosition)? + onSingleLongTapMoveUpdate; // Returns whether gesture is handled - final bool Function(LongPressEndDetails details, TextPosition textPosition)? onSingleLongTapEnd; + final bool Function(LongPressEndDetails details, TextPosition textPosition)? + onSingleLongTapEnd; final EmbedBuilder embedBuilder; @override @@ -337,8 +340,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapMoveUpdate != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapMoveUpdate!(details, renderEditor.getPositionForOffset(details.globalPosition) - )) { + if (_state.widget.onSingleLongTapMoveUpdate!(details, + renderEditor.getPositionForOffset(details.globalPosition))) { return; } } @@ -467,7 +470,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onTapDown != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onTapDown!(details, renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onTapDown!(details, + renderEditor.getPositionForOffset(details.globalPosition))) { return; } } @@ -480,7 +484,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onTapUp != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onTapUp!(details, renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onTapUp!(details, + renderEditor.getPositionForOffset(details.globalPosition))) { return; } } @@ -522,7 +527,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapStart != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapStart!(details, renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onSingleLongTapStart!(details, + renderEditor.getPositionForOffset(details.globalPosition))) { return; } } @@ -556,7 +562,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapEnd != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapEnd!(details, renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onSingleLongTapEnd!(details, + renderEditor.getPositionForOffset(details.globalPosition))) { return; } } @@ -927,7 +934,8 @@ class RenderEditor extends RenderEditableContainerBox kMargin + offsetInViewport + scrollBottomInset; - final caretBottom = endpoint.point.dy + kMargin + offsetInViewport + scrollBottomInset; + final caretBottom = + endpoint.point.dy + kMargin + offsetInViewport + scrollBottomInset; double? dy; if (caretTop < scrollOffset) { dy = caretTop; diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index d826c9e5..309b9cf8 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -514,14 +514,14 @@ class RenderEditableTextBlock extends RenderEditableContainerBox class _EditableBlock extends MultiChildRenderObjectWidget { _EditableBlock( - this.block, - this.textDirection, - this.padding, - this.scrollBottomInset, - this.decoration, - this.contentPadding, - List children - ) : super(children: children); + this.block, + this.textDirection, + this.padding, + this.scrollBottomInset, + this.decoration, + this.contentPadding, + List children) + : super(children: children); final Block block; final TextDirection textDirection; diff --git a/pubspec.yaml b/pubspec.yaml index 15c4de29..93bf71be 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.8 +version: 1.2.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 14817ec752f530cafcd12c10827bfb8142ea6bbe Mon Sep 17 00:00:00 2001 From: Thea Choem Date: Thu, 15 Apr 2021 21:55:42 +0700 Subject: [PATCH 157/306] close #149 underline strike through color according to text color (#150) --- lib/widgets/text_line.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 3106a08d..ef80a024 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -119,6 +119,7 @@ class TextLine extends StatelessWidget { final textNode = node as leaf.Text; final style = textNode.style; var res = const TextStyle(); + final color = textNode.style.attributes[Attribute.color.key]; { Attribute.bold.key: defaultStyles.bold, @@ -128,7 +129,16 @@ class TextLine extends StatelessWidget { Attribute.strikeThrough.key: defaultStyles.strikeThrough, }.forEach((k, s) { if (style.values.any((v) => v.key == k)) { - res = _merge(res, s!); + if (k == Attribute.underline.key || k == Attribute.strikeThrough.key) { + var textColor = defaultStyles.color; + if (color?.value is String) { + textColor = stringToColor(color?.value); + } + res = _merge(res.copyWith(decorationColor: textColor), + s!.copyWith(decorationColor: textColor)); + } else { + res = _merge(res, s!); + } } }); @@ -159,7 +169,6 @@ class TextLine extends StatelessWidget { } } - final color = textNode.style.attributes[Attribute.color.key]; if (color != null && color.value != null) { var textColor = defaultStyles.color; if (color.value is String) { From 273340a1f6610abf0c760ace45261629392f3cf8 Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Mon, 19 Apr 2021 17:49:22 -0700 Subject: [PATCH 158/306] Pass getTextPosition instead of passing the tap text position to be more flexible; optional ignoreFocus on replaceText --- lib/widgets/controller.dart | 8 +++++++- lib/widgets/editor.dart | 34 ++++++++++++++-------------------- lib/widgets/raw_editor.dart | 29 +++++++++++++++++------------ 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 3bd66b6f..a17fe92d 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -23,6 +23,7 @@ class QuillController extends ChangeNotifier { final Document document; TextSelection selection; Style toggledStyle = Style(); + bool ignoreFocusOnTextChange = false; // item1: Document state before [change]. // @@ -76,7 +77,7 @@ class QuillController extends ChangeNotifier { bool get hasRedo => document.hasRedo; void replaceText( - int index, int len, Object? data, TextSelection? textSelection) { + int index, int len, Object? data, TextSelection? textSelection, {bool ignoreFocus = false}) { assert(data is String || data is Embeddable); Delta? delta; @@ -124,7 +125,12 @@ class QuillController extends ChangeNotifier { ); } } + + if (ignoreFocus) { + ignoreFocusOnTextChange = true; + } notifyListeners(); + ignoreFocusOnTextChange = false; } void formatText(int index, int len, Attribute? attribute) { diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 5be0dd24..42fd769c 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -176,20 +176,19 @@ class QuillEditor extends StatefulWidget { final ScrollPhysics? scrollPhysics; final ValueChanged? onLaunchUrl; // Returns whether gesture is handled - final bool Function(TapDownDetails details, TextPosition textPosition)? - onTapDown; + final bool Function(TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; + // Returns whether gesture is handled - final bool Function(TapUpDetails details, TextPosition textPosition)? onTapUp; + final bool Function(TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; + // Returns whether gesture is handled - final bool Function(LongPressStartDetails details, TextPosition textPosition)? - onSingleLongTapStart; + final bool Function(LongPressStartDetails details, TextPosition Function(Offset offset))? onSingleLongTapStart; + // Returns whether gesture is handled - final bool Function( - LongPressMoveUpdateDetails details, TextPosition textPosition)? - onSingleLongTapMoveUpdate; + final bool Function(LongPressMoveUpdateDetails details, TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; // Returns whether gesture is handled - final bool Function(LongPressEndDetails details, TextPosition textPosition)? - onSingleLongTapEnd; + final bool Function(LongPressEndDetails details, TextPosition Function(Offset offset))? onSingleLongTapEnd; + final EmbedBuilder embedBuilder; @override @@ -340,8 +339,7 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapMoveUpdate != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapMoveUpdate!(details, - renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onSingleLongTapMoveUpdate!(details, renderEditor.getPositionForOffset)) { return; } } @@ -470,8 +468,7 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onTapDown != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onTapDown!(details, - renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onTapDown!(details, renderEditor.getPositionForOffset)) { return; } } @@ -484,8 +481,7 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onTapUp != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onTapUp!(details, - renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onTapUp!(details, renderEditor.getPositionForOffset)) { return; } } @@ -527,8 +523,7 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapStart != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapStart!(details, - renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onSingleLongTapStart!(details, renderEditor.getPositionForOffset)) { return; } } @@ -562,8 +557,7 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapEnd != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapEnd!(details, - renderEditor.getPositionForOffset(details.globalPosition))) { + if (_state.widget.onSingleLongTapEnd!(details, renderEditor.getPositionForOffset)) { return; } } diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 1627ebc3..7114e670 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -675,7 +675,9 @@ class RawEditorState extends EditorState _clipboardStatus?.addListener(_onChangedClipboardStatus); - widget.controller.addListener(_didChangeTextEditingValue); + widget.controller.addListener(() { + _didChangeTextEditingValue(widget.controller.ignoreFocusOnTextChange); + }); _scrollController = widget.scrollController; _scrollController!.addListener(_updateSelectionOverlayForScroll); @@ -883,15 +885,17 @@ class RawEditorState extends EditorState _selectionOverlay?.markNeedsBuild(); } - void _didChangeTextEditingValue() { + void _didChangeTextEditingValue([bool ignoreFocus = false]) { if (kIsWeb) { - _onChangeTextEditingValue(); - requestKeyboard(); + _onChangeTextEditingValue(ignoreFocus); + if (!ignoreFocus) { + requestKeyboard(); + } return; } - if (_keyboardVisible) { - _onChangeTextEditingValue(); + if (ignoreFocus || _keyboardVisible) { + _onChangeTextEditingValue(ignoreFocus); } else { requestKeyboard(); if (mounted) { @@ -903,19 +907,20 @@ class RawEditorState extends EditorState } } - void _onChangeTextEditingValue() { - _showCaretOnScreen(); + void _onChangeTextEditingValue([bool ignoreCaret = false]) { updateRemoteValueIfNeeded(); - _cursorCont.startOrStopCursorTimerIfNeeded( - _hasFocus, widget.controller.selection); + if (ignoreCaret) { + return; + } + _showCaretOnScreen(); + _cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, widget.controller.selection); if (hasConnection) { _cursorCont ..stopCursorTimer(resetCharTicks: false) ..startCursorTimer(); } - SchedulerBinding.instance!.addPostFrameCallback( - (_) => _updateOrDisposeSelectionOverlayIfNeeded()); + SchedulerBinding.instance!.addPostFrameCallback((_) => _updateOrDisposeSelectionOverlayIfNeeded()); if (mounted) { setState(() { // Use widget.controller.value in build() From 049ada2febeb9452e2f11a919dadc4e76e543470 Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Mon, 19 Apr 2021 20:27:08 -0700 Subject: [PATCH 159/306] adjustable toolbar button & bar size --- lib/widgets/controller.dart | 5 ++- lib/widgets/toolbar.dart | 73 +++++++++++++++---------------------- 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index a17fe92d..1b7a8180 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -11,7 +11,7 @@ import '../models/quill_delta.dart'; import '../utils/diff_delta.dart'; class QuillController extends ChangeNotifier { - QuillController({required this.document, required this.selection}); + QuillController({required this.document, required this.selection, this.iconSize = 18, this.toolbarHeightFactor = 2}); factory QuillController.basic() { return QuillController( @@ -22,6 +22,9 @@ class QuillController extends ChangeNotifier { final Document document; TextSelection selection; + double iconSize; + double toolbarHeightFactor; + Style toggledStyle = Style(); bool ignoreFocusOnTextChange = false; diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 82ab6125..8bcb3329 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -14,9 +14,6 @@ import '../models/documents/style.dart'; import '../utils/color.dart'; import 'controller.dart'; -double iconSize = 18; -double kToolbarHeight = iconSize * 2; - typedef OnImagePickCallback = Future Function(File file); typedef ImagePickImpl = Future Function(ImageSource source); @@ -37,10 +34,10 @@ class InsertEmbedButton extends StatelessWidget { return QuillIconButton( highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, + size: controller.iconSize * 1.77, icon: Icon( icon, - size: iconSize, + size: controller.iconSize, color: Theme.of(context).iconTheme.color, ), fillColor: fillColor ?? Theme.of(context).canvasColor, @@ -101,10 +98,10 @@ class _LinkStyleButtonState extends State { return QuillIconButton( highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, + size: widget.controller.iconSize * 1.77, icon: Icon( widget.icon ?? Icons.link, - size: iconSize, + size: widget.controller.iconSize, color: isEnabled ? theme.iconTheme.color : theme.disabledColor, ), fillColor: Theme.of(context).canvasColor, @@ -372,8 +369,8 @@ Widget defaultToggleStyleButtonBuilder( return QuillIconButton( highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, - icon: Icon(icon, size: iconSize, color: iconColor), + size: 18 * 1.77, + icon: Icon(icon, size: 18, color: iconColor), fillColor: fill, onPressed: onPressed, ); @@ -435,12 +432,12 @@ class _SelectHeaderStyleButtonState extends State { @override Widget build(BuildContext context) { - return _selectHeadingStyleButtonBuilder(context, _value, _selectAttribute); + return _selectHeadingStyleButtonBuilder(context, _value, _selectAttribute, widget.controller.iconSize); } } Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, - ValueChanged onSelected) { + ValueChanged onSelected, double iconSize) { final _valueToText = { Attribute.header: 'N', Attribute.h1: 'H1', @@ -532,12 +529,12 @@ class _ImageButtonState extends State { return QuillIconButton( icon: Icon( widget.icon, - size: iconSize, + size: widget.controller.iconSize, color: theme.iconTheme.color, ), highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, + size: widget.controller.iconSize * 1.77, fillColor: theme.canvasColor, onPressed: _handleImageButtonTap, ); @@ -708,9 +705,9 @@ class _ColorButtonState extends State { return QuillIconButton( highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, + size: widget.controller.iconSize * 1.77, icon: Icon(widget.icon, - size: iconSize, + size: widget.controller.iconSize, color: widget.background ? iconColorBackground : iconColor), fillColor: widget.background ? fillColorBackground : fillColor, onPressed: _showColorPicker, @@ -776,8 +773,8 @@ class _HistoryButtonState extends State { return QuillIconButton( highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, - icon: Icon(widget.icon, size: iconSize, color: _iconColor), + size: widget.controller.iconSize * 1.77, + icon: Icon(widget.icon, size: widget.controller.iconSize, color: _iconColor), fillColor: fillColor, onPressed: _changeHistory, ); @@ -841,8 +838,8 @@ class _IndentButtonState extends State { return QuillIconButton( highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, - icon: Icon(widget.icon, size: iconSize, color: iconColor), + size: widget.controller.iconSize * 1.77, + icon: Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), fillColor: fillColor, onPressed: () { final indent = widget.controller @@ -895,8 +892,8 @@ class _ClearFormatButtonState extends State { return QuillIconButton( highlightElevation: 0, hoverElevation: 0, - size: iconSize * 1.77, - icon: Icon(widget.icon, size: iconSize, color: iconColor), + size: widget.controller.iconSize * 1.77, + icon: Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), fillColor: fillColor, onPressed: () { for (final k @@ -908,7 +905,7 @@ class _ClearFormatButtonState extends State { } class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { - const QuillToolbar({required this.children, Key? key}) : super(key: key); + const QuillToolbar({required this.children, this.toolBarHeight = 36, Key? key}) : super(key: key); factory QuillToolbar.basic({ required QuillController controller, @@ -933,8 +930,9 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { OnImagePickCallback? onImagePickCallback, Key? key, }) { - iconSize = toolbarIconSize; - return QuillToolbar(key: key, children: [ + controller.iconSize = toolbarIconSize; + + return QuillToolbar(key: key, toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor, children: [ Visibility( visible: showHistory, child: HistoryButton( @@ -1034,12 +1032,8 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { ), ), Visibility( - visible: showHeaderStyle, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility( - visible: showHeaderStyle, - child: SelectHeaderStyleButton(controller: controller)), + visible: showHeaderStyle, child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility(visible: showHeaderStyle, child: SelectHeaderStyleButton(controller: controller)), VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400), Visibility( visible: showListNumbers, @@ -1074,12 +1068,8 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { ), ), Visibility( - visible: !showListNumbers && - !showListBullets && - !showListCheck && - !showCodeBlock, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), + visible: !showListNumbers && !showListBullets && !showListCheck && !showCodeBlock, + child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), Visibility( visible: showQuote, child: ToggleStyleButton( @@ -1104,12 +1094,8 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { isIncrease: false, ), ), - Visibility( - visible: showQuote, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility( - visible: showLink, child: LinkStyleButton(controller: controller)), + Visibility(visible: showQuote, child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility(visible: showLink, child: LinkStyleButton(controller: controller)), Visibility( visible: showHorizontalRule, child: InsertEmbedButton( @@ -1121,12 +1107,13 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { } final List children; + final double toolBarHeight; @override _QuillToolbarState createState() => _QuillToolbarState(); @override - Size get preferredSize => Size.fromHeight(kToolbarHeight); + Size get preferredSize => Size.fromHeight(toolBarHeight); } class _QuillToolbarState extends State { From e4cf28d054773d9c869c6829cbdef4bc17b0f180 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 12:40:55 -0700 Subject: [PATCH 160/306] Update getIndentLevel --- lib/models/documents/attribute.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 18822e01..09564e4e 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -151,7 +151,10 @@ class Attribute { if (level == 2) { return indentL2; } - return indentL3; + if (level == 3) { + return indentL3; + } + return IndentAttribute(level: level); } bool get isInline => scope == AttributeScope.INLINE; From 4c5f72826c0aee09cd8a0bade2b5ac8f990efdfc Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 15:14:54 -0700 Subject: [PATCH 161/306] Indent attribute is consider block but may have null value --- lib/models/documents/attribute.dart | 32 +++++++++++++++++++++-------- lib/models/documents/style.dart | 3 ++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 09564e4e..1b9043b9 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:quiver/core.dart'; enum AttributeScope { @@ -14,7 +16,7 @@ class Attribute { final AttributeScope scope; final T value; - static final Map _registry = { + static final Map _registry = LinkedHashMap.of({ Attribute.bold.key: Attribute.bold, Attribute.italic.key: Attribute.italic, Attribute.underline.key: Attribute.underline, @@ -26,16 +28,16 @@ class Attribute { Attribute.background.key: Attribute.background, Attribute.placeholder.key: Attribute.placeholder, Attribute.header.key: Attribute.header, - Attribute.indent.key: Attribute.indent, Attribute.align.key: Attribute.align, Attribute.list.key: Attribute.list, Attribute.codeBlock.key: Attribute.codeBlock, Attribute.blockQuote.key: Attribute.blockQuote, + Attribute.indent.key: Attribute.indent, Attribute.width.key: Attribute.width, Attribute.height.key: Attribute.height, Attribute.style.key: Attribute.style, Attribute.token.key: Attribute.token, - }; + }); static final BoldAttribute bold = BoldAttribute(); @@ -88,22 +90,22 @@ class Attribute { Attribute.placeholder.key, }; - static final Set blockKeys = { + static final Set blockKeys = LinkedHashSet.of({ Attribute.header.key, - Attribute.indent.key, Attribute.align.key, Attribute.list.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - }; + Attribute.indent.key, + }); - static final Set blockKeysExceptHeader = { + static final Set blockKeysExceptHeader = LinkedHashSet.of({ Attribute.list.key, - Attribute.indent.key, Attribute.align.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - }; + Attribute.indent.key, + }); static Attribute get h1 => HeaderAttribute(level: 1); @@ -172,6 +174,18 @@ class Attribute { return attribute; } + static int getRegistryOrder(Attribute attribute) { + var order = 0; + for (final attr in _registry.values) { + if (attr.key == attribute.key) { + break; + } + order++; + } + + return order; + } + static Attribute clone(Attribute origin, dynamic value) { return Attribute(origin.key, origin.scope, value); } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index c805280d..7f3a39ac 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -30,7 +30,8 @@ class Style { Iterable get keys => _attributes.keys; - Iterable get values => _attributes.values; + Iterable get values => _attributes.values.sorted( + (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); Map get attributes => _attributes; From b6763fe2fcda949e28b8c429582aca0c4781801a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 16:10:46 -0700 Subject: [PATCH 162/306] Revert "Indent attribute is consider block but may have null value" This reverts commit 4c5f72826c0aee09cd8a0bade2b5ac8f990efdfc. --- lib/models/documents/attribute.dart | 32 ++++++++--------------------- lib/models/documents/style.dart | 3 +-- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 1b9043b9..09564e4e 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:quiver/core.dart'; enum AttributeScope { @@ -16,7 +14,7 @@ class Attribute { final AttributeScope scope; final T value; - static final Map _registry = LinkedHashMap.of({ + static final Map _registry = { Attribute.bold.key: Attribute.bold, Attribute.italic.key: Attribute.italic, Attribute.underline.key: Attribute.underline, @@ -28,16 +26,16 @@ class Attribute { Attribute.background.key: Attribute.background, Attribute.placeholder.key: Attribute.placeholder, Attribute.header.key: Attribute.header, + Attribute.indent.key: Attribute.indent, Attribute.align.key: Attribute.align, Attribute.list.key: Attribute.list, Attribute.codeBlock.key: Attribute.codeBlock, Attribute.blockQuote.key: Attribute.blockQuote, - Attribute.indent.key: Attribute.indent, Attribute.width.key: Attribute.width, Attribute.height.key: Attribute.height, Attribute.style.key: Attribute.style, Attribute.token.key: Attribute.token, - }); + }; static final BoldAttribute bold = BoldAttribute(); @@ -90,22 +88,22 @@ class Attribute { Attribute.placeholder.key, }; - static final Set blockKeys = LinkedHashSet.of({ + static final Set blockKeys = { Attribute.header.key, + Attribute.indent.key, Attribute.align.key, Attribute.list.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - Attribute.indent.key, - }); + }; - static final Set blockKeysExceptHeader = LinkedHashSet.of({ + static final Set blockKeysExceptHeader = { Attribute.list.key, + Attribute.indent.key, Attribute.align.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - Attribute.indent.key, - }); + }; static Attribute get h1 => HeaderAttribute(level: 1); @@ -174,18 +172,6 @@ class Attribute { return attribute; } - static int getRegistryOrder(Attribute attribute) { - var order = 0; - for (final attr in _registry.values) { - if (attr.key == attribute.key) { - break; - } - order++; - } - - return order; - } - static Attribute clone(Attribute origin, dynamic value) { return Attribute(origin.key, origin.scope, value); } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 7f3a39ac..c805280d 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -30,8 +30,7 @@ class Style { Iterable get keys => _attributes.keys; - Iterable get values => _attributes.values.sorted( - (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); + Iterable get values => _attributes.values; Map get attributes => _attributes; From bc5eb86a8f623bb2426a095835b2a72bb117ebd3 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 16:25:35 -0700 Subject: [PATCH 163/306] Indent attribute is consider block but may have null value --- lib/models/documents/nodes/line.dart | 2 +- lib/models/documents/style.dart | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index ec933b52..632d44d1 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -197,7 +197,7 @@ class Line extends Container { } applyStyle(newStyle); - final blockStyle = newStyle.getBlockExceptHeader(); + final blockStyle = newStyle.getNotNullValueBlockExceptHeader(); if (blockStyle == null) { return; } // No block-level changes diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index c805280d..4efce0f2 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -56,6 +56,15 @@ class Style { return null; } + Attribute? getNotNullValueBlockExceptHeader() { + for (final val in values) { + if (val.isBlockExceptHeader && val.value != null) { + return val; + } + } + return null; + } + Style merge(Attribute attribute) { final merged = Map.from(_attributes); if (attribute.value == null) { From df602cfa9b762d9c0248c081dbd5ddf308f7805f Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 21:50:50 -0700 Subject: [PATCH 164/306] Format code --- lib/models/documents/nodes/line.dart | 2 +- lib/models/documents/style.dart | 8 +- lib/widgets/controller.dart | 9 +- lib/widgets/editor.dart | 32 ++- lib/widgets/raw_editor.dart | 6 +- lib/widgets/toolbar.dart | 377 ++++++++++++++------------- 6 files changed, 236 insertions(+), 198 deletions(-) diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 632d44d1..ec933b52 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -197,7 +197,7 @@ class Line extends Container { } applyStyle(newStyle); - final blockStyle = newStyle.getNotNullValueBlockExceptHeader(); + final blockStyle = newStyle.getBlockExceptHeader(); if (blockStyle == null) { return; } // No block-level changes diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 4efce0f2..ff78d9ed 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -49,16 +49,12 @@ class Style { Attribute? getBlockExceptHeader() { for (final val in values) { - if (val.isBlockExceptHeader) { + if (val.isBlockExceptHeader && val.value != null) { return val; } } - return null; - } - - Attribute? getNotNullValueBlockExceptHeader() { for (final val in values) { - if (val.isBlockExceptHeader && val.value != null) { + if (val.isBlockExceptHeader) { return val; } } diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 1b7a8180..59aa1e09 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -11,7 +11,11 @@ import '../models/quill_delta.dart'; import '../utils/diff_delta.dart'; class QuillController extends ChangeNotifier { - QuillController({required this.document, required this.selection, this.iconSize = 18, this.toolbarHeightFactor = 2}); + QuillController( + {required this.document, + required this.selection, + this.iconSize = 18, + this.toolbarHeightFactor = 2}); factory QuillController.basic() { return QuillController( @@ -80,7 +84,8 @@ class QuillController extends ChangeNotifier { bool get hasRedo => document.hasRedo; void replaceText( - int index, int len, Object? data, TextSelection? textSelection, {bool ignoreFocus = false}) { + int index, int len, Object? data, TextSelection? textSelection, + {bool ignoreFocus = false}) { assert(data is String || data is Embeddable); Delta? delta; diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 42fd769c..d15a8789 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -176,18 +176,25 @@ class QuillEditor extends StatefulWidget { final ScrollPhysics? scrollPhysics; final ValueChanged? onLaunchUrl; // Returns whether gesture is handled - final bool Function(TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; + final bool Function( + TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; // Returns whether gesture is handled - final bool Function(TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; + final bool Function( + TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; // Returns whether gesture is handled - final bool Function(LongPressStartDetails details, TextPosition Function(Offset offset))? onSingleLongTapStart; + final bool Function( + LongPressStartDetails details, TextPosition Function(Offset offset))? + onSingleLongTapStart; // Returns whether gesture is handled - final bool Function(LongPressMoveUpdateDetails details, TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; + final bool Function(LongPressMoveUpdateDetails details, + TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; // Returns whether gesture is handled - final bool Function(LongPressEndDetails details, TextPosition Function(Offset offset))? onSingleLongTapEnd; + final bool Function( + LongPressEndDetails details, TextPosition Function(Offset offset))? + onSingleLongTapEnd; final EmbedBuilder embedBuilder; @@ -339,7 +346,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapMoveUpdate != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapMoveUpdate!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onSingleLongTapMoveUpdate!( + details, renderEditor.getPositionForOffset)) { return; } } @@ -468,7 +476,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onTapDown != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onTapDown!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onTapDown!( + details, renderEditor.getPositionForOffset)) { return; } } @@ -481,7 +490,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onTapUp != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onTapUp!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onTapUp!( + details, renderEditor.getPositionForOffset)) { return; } } @@ -523,7 +533,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapStart != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapStart!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onSingleLongTapStart!( + details, renderEditor.getPositionForOffset)) { return; } } @@ -557,7 +568,8 @@ class _QuillEditorSelectionGestureDetectorBuilder if (_state.widget.onSingleLongTapEnd != null) { final renderEditor = getRenderEditor(); if (renderEditor != null) { - if (_state.widget.onSingleLongTapEnd!(details, renderEditor.getPositionForOffset)) { + if (_state.widget.onSingleLongTapEnd!( + details, renderEditor.getPositionForOffset)) { return; } } diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 7114e670..35e3aa68 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -913,14 +913,16 @@ class RawEditorState extends EditorState return; } _showCaretOnScreen(); - _cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, widget.controller.selection); + _cursorCont.startOrStopCursorTimerIfNeeded( + _hasFocus, widget.controller.selection); if (hasConnection) { _cursorCont ..stopCursorTimer(resetCharTicks: false) ..startCursorTimer(); } - SchedulerBinding.instance!.addPostFrameCallback((_) => _updateOrDisposeSelectionOverlayIfNeeded()); + SchedulerBinding.instance!.addPostFrameCallback( + (_) => _updateOrDisposeSelectionOverlayIfNeeded()); if (mounted) { setState(() { // Use widget.controller.value in build() diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index 8bcb3329..c5fe2bab 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -432,7 +432,8 @@ class _SelectHeaderStyleButtonState extends State { @override Widget build(BuildContext context) { - return _selectHeadingStyleButtonBuilder(context, _value, _selectAttribute, widget.controller.iconSize); + return _selectHeadingStyleButtonBuilder( + context, _value, _selectAttribute, widget.controller.iconSize); } } @@ -774,7 +775,8 @@ class _HistoryButtonState extends State { highlightElevation: 0, hoverElevation: 0, size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, size: widget.controller.iconSize, color: _iconColor), + icon: Icon(widget.icon, + size: widget.controller.iconSize, color: _iconColor), fillColor: fillColor, onPressed: _changeHistory, ); @@ -839,7 +841,8 @@ class _IndentButtonState extends State { highlightElevation: 0, hoverElevation: 0, size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), + icon: + Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), fillColor: fillColor, onPressed: () { final indent = widget.controller @@ -893,7 +896,8 @@ class _ClearFormatButtonState extends State { highlightElevation: 0, hoverElevation: 0, size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), + icon: Icon(widget.icon, + size: widget.controller.iconSize, color: iconColor), fillColor: fillColor, onPressed: () { for (final k @@ -905,7 +909,9 @@ class _ClearFormatButtonState extends State { } class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { - const QuillToolbar({required this.children, this.toolBarHeight = 36, Key? key}) : super(key: key); + const QuillToolbar( + {required this.children, this.toolBarHeight = 36, Key? key}) + : super(key: key); factory QuillToolbar.basic({ required QuillController controller, @@ -932,178 +938,195 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { }) { controller.iconSize = toolbarIconSize; - return QuillToolbar(key: key, toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor, children: [ - Visibility( - visible: showHistory, - child: HistoryButton( - icon: Icons.undo_outlined, - controller: controller, - undo: true, - ), - ), - Visibility( - visible: showHistory, - child: HistoryButton( - icon: Icons.redo_outlined, - controller: controller, - undo: false, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showBoldButton, - child: ToggleStyleButton( - attribute: Attribute.bold, - icon: Icons.format_bold, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showItalicButton, - child: ToggleStyleButton( - attribute: Attribute.italic, - icon: Icons.format_italic, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showUnderLineButton, - child: ToggleStyleButton( - attribute: Attribute.underline, - icon: Icons.format_underline, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showStrikeThrough, - child: ToggleStyleButton( - attribute: Attribute.strikeThrough, - icon: Icons.format_strikethrough, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showColorButton, - child: ColorButton( - icon: Icons.color_lens, - controller: controller, - background: false, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showBackgroundColorButton, - child: ColorButton( - icon: Icons.format_color_fill, - controller: controller, - background: true, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showClearFormat, - child: ClearFormatButton( - icon: Icons.format_clear, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: onImagePickCallback != null, - child: ImageButton( - icon: Icons.image, - controller: controller, - imageSource: ImageSource.gallery, - onImagePickCallback: onImagePickCallback, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: onImagePickCallback != null, - child: ImageButton( - icon: Icons.photo_camera, - controller: controller, - imageSource: ImageSource.camera, - onImagePickCallback: onImagePickCallback, - ), - ), - Visibility( - visible: showHeaderStyle, child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility(visible: showHeaderStyle, child: SelectHeaderStyleButton(controller: controller)), - VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400), - Visibility( - visible: showListNumbers, - child: ToggleStyleButton( - attribute: Attribute.ol, - controller: controller, - icon: Icons.format_list_numbered, - ), - ), - Visibility( - visible: showListBullets, - child: ToggleStyleButton( - attribute: Attribute.ul, - controller: controller, - icon: Icons.format_list_bulleted, - ), - ), - Visibility( - visible: showListCheck, - child: ToggleCheckListButton( - attribute: Attribute.unchecked, - controller: controller, - icon: Icons.check_box, - ), - ), - Visibility( - visible: showCodeBlock, - child: ToggleStyleButton( - attribute: Attribute.codeBlock, - controller: controller, - icon: Icons.code, - ), - ), - Visibility( - visible: !showListNumbers && !showListBullets && !showListCheck && !showCodeBlock, - child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility( - visible: showQuote, - child: ToggleStyleButton( - attribute: Attribute.blockQuote, - controller: controller, - icon: Icons.format_quote, - ), - ), - Visibility( - visible: showIndent, - child: IndentButton( - icon: Icons.format_indent_increase, - controller: controller, - isIncrease: true, - ), - ), - Visibility( - visible: showIndent, - child: IndentButton( - icon: Icons.format_indent_decrease, - controller: controller, - isIncrease: false, - ), - ), - Visibility(visible: showQuote, child: VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility(visible: showLink, child: LinkStyleButton(controller: controller)), - Visibility( - visible: showHorizontalRule, - child: InsertEmbedButton( - controller: controller, - icon: Icons.horizontal_rule, - ), - ), - ]); + return QuillToolbar( + key: key, + toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor, + children: [ + Visibility( + visible: showHistory, + child: HistoryButton( + icon: Icons.undo_outlined, + controller: controller, + undo: true, + ), + ), + Visibility( + visible: showHistory, + child: HistoryButton( + icon: Icons.redo_outlined, + controller: controller, + undo: false, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showBoldButton, + child: ToggleStyleButton( + attribute: Attribute.bold, + icon: Icons.format_bold, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showItalicButton, + child: ToggleStyleButton( + attribute: Attribute.italic, + icon: Icons.format_italic, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showUnderLineButton, + child: ToggleStyleButton( + attribute: Attribute.underline, + icon: Icons.format_underline, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showStrikeThrough, + child: ToggleStyleButton( + attribute: Attribute.strikeThrough, + icon: Icons.format_strikethrough, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showColorButton, + child: ColorButton( + icon: Icons.color_lens, + controller: controller, + background: false, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showBackgroundColorButton, + child: ColorButton( + icon: Icons.format_color_fill, + controller: controller, + background: true, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showClearFormat, + child: ClearFormatButton( + icon: Icons.format_clear, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: onImagePickCallback != null, + child: ImageButton( + icon: Icons.image, + controller: controller, + imageSource: ImageSource.gallery, + onImagePickCallback: onImagePickCallback, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: onImagePickCallback != null, + child: ImageButton( + icon: Icons.photo_camera, + controller: controller, + imageSource: ImageSource.camera, + onImagePickCallback: onImagePickCallback, + ), + ), + Visibility( + visible: showHeaderStyle, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showHeaderStyle, + child: SelectHeaderStyleButton(controller: controller)), + VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400), + Visibility( + visible: showListNumbers, + child: ToggleStyleButton( + attribute: Attribute.ol, + controller: controller, + icon: Icons.format_list_numbered, + ), + ), + Visibility( + visible: showListBullets, + child: ToggleStyleButton( + attribute: Attribute.ul, + controller: controller, + icon: Icons.format_list_bulleted, + ), + ), + Visibility( + visible: showListCheck, + child: ToggleCheckListButton( + attribute: Attribute.unchecked, + controller: controller, + icon: Icons.check_box, + ), + ), + Visibility( + visible: showCodeBlock, + child: ToggleStyleButton( + attribute: Attribute.codeBlock, + controller: controller, + icon: Icons.code, + ), + ), + Visibility( + visible: !showListNumbers && + !showListBullets && + !showListCheck && + !showCodeBlock, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showQuote, + child: ToggleStyleButton( + attribute: Attribute.blockQuote, + controller: controller, + icon: Icons.format_quote, + ), + ), + Visibility( + visible: showIndent, + child: IndentButton( + icon: Icons.format_indent_increase, + controller: controller, + isIncrease: true, + ), + ), + Visibility( + visible: showIndent, + child: IndentButton( + icon: Icons.format_indent_decrease, + controller: controller, + isIncrease: false, + ), + ), + Visibility( + visible: showQuote, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showLink, + child: LinkStyleButton(controller: controller)), + Visibility( + visible: showHorizontalRule, + child: InsertEmbedButton( + controller: controller, + icon: Icons.horizontal_rule, + ), + ), + ]); } final List children; From 80d918ef9ca6c39c1c38f8727dbf826cb64b7017 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 22:20:54 -0700 Subject: [PATCH 165/306] Update PreserveBlockStyleOnInsertRule to apply all block styles --- lib/models/documents/style.dart | 10 ++++++++++ lib/models/rules/insert.dart | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index ff78d9ed..8174aeca 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -61,6 +61,16 @@ class Style { return null; } + Map getBlocksExceptHeader() { + final m = {}; + attributes.forEach((key, value) { + if (Attribute.blockKeysExceptHeader.contains(key)) { + m[key] = value; + } + }); + return m; + } + Style merge(Attribute attribute) { final merged = Map.from(_attributes); if (attribute.value == null) { diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index b83c8838..cc7a3a58 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -62,28 +62,32 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { Delta? applyRule(Delta document, int index, {int? len, Object? data, Attribute? attribute}) { if (data is! String || !data.contains('\n')) { + // Only interested in text containing at least one newline character. return null; } final itr = DeltaIterator(document)..skip(index); + // Look for the next newline. final nextNewLine = _getNextNewLine(itr); final lineStyle = Style.fromJson(nextNewLine.item1?.attributes ?? {}); - final attribute = lineStyle.getBlockExceptHeader(); - if (attribute == null) { + final blockStyle = lineStyle.getBlocksExceptHeader(); + // Are we currently in a block? If not then ignore. + if (blockStyle.isEmpty) { return null; } - final blockStyle = {attribute.key: attribute.value}; - Map? resetStyle; - + // If current line had heading style applied to it we'll need to move this + // style to the newly inserted line before it and reset style of the + // original line. if (lineStyle.containsKey(Attribute.header.key)) { resetStyle = Attribute.header.toJson(); } + // Go over each inserted line and ensure block style is applied. final lines = data.split('\n'); final delta = Delta()..retain(index + (len ?? 0)); for (var i = 0; i < lines.length; i++) { @@ -92,12 +96,15 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { delta.insert(line); } if (i == 0) { + // The first line should inherit the lineStyle entirely. delta.insert('\n', lineStyle.toJson()); } else if (i < lines.length - 1) { + // we don't want to insert a newline after the last chunk of text, so -1 delta.insert('\n', blockStyle); } } + // Reset style of the original newline character if needed. if (resetStyle != null) { delta ..retain(nextNewLine.item2!) From f7d47a12db381d1799e7e886e6634a78cd075b64 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 22:26:59 -0700 Subject: [PATCH 166/306] Add comments to AutoExitBlockRule --- lib/models/rules/insert.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index cc7a3a58..5801a10e 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -55,6 +55,14 @@ class PreserveLineStyleOnSplitRule extends InsertRule { } } +/// Preserves block style when user inserts text containing newlines. +/// +/// This rule handles: +/// +/// * inserting a new line in a block +/// * pasting text containing multiple lines of text in a block +/// +/// This rule may also be activated for changes triggered by auto-correct. class PreserveBlockStyleOnInsertRule extends InsertRule { const PreserveBlockStyleOnInsertRule(); @@ -116,6 +124,12 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { } } +/// Heuristic rule to exit current block when user inserts two consecutive +/// newlines. +/// +/// This rule is only applied when the cursor is on the last line of a block. +/// When the cursor is in the middle of a block we allow adding empty lines +/// and preserving the block's style. class AutoExitBlockRule extends InsertRule { const AutoExitBlockRule(); @@ -139,25 +153,39 @@ class AutoExitBlockRule extends InsertRule { final itr = DeltaIterator(document); final prev = itr.skip(index), cur = itr.next(); final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader(); + // We are not in a block, ignore. if (cur.isPlain || blockStyle == null) { return null; } + // We are not on an empty line, ignore. if (!_isEmptyLine(prev, cur)) { return null; } + // We are on an empty line. Now we need to determine if we are on the + // last line of a block. + // First check if `cur` length is greater than 1, this would indicate + // that it contains multiple newline characters which share the same style. + // This would mean we are not on the last line yet. + // `cur.value as String` is safe since we already called isEmptyLine and know it contains a newline if ((cur.value as String).length > 1) { + // We are not on the last line of this block, ignore. return null; } + // Keep looking for the next newline character to see if it shares the same + // block style as `cur`. final nextNewLine = _getNextNewLine(itr); if (nextNewLine.item1 != null && nextNewLine.item1!.attributes != null && Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() == blockStyle) { + // We are not at the end of this block, ignore. return null; } + // Here we now know that the line after `cur` is not in the same block + // therefore we can exit this block. final attributes = cur.attributes ?? {}; final k = attributes.keys .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k)); From 900d0f2489ed2c731b52ae8594af80da617df1a4 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 22:40:01 -0700 Subject: [PATCH 167/306] Fix: Indented position not holding while editing --- CHANGELOG.md | 3 +++ lib/models/documents/nodes/line.dart | 17 +++++++++++------ pubspec.yaml | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8937de42..d96c8386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.2.1] +* Indented position not holding while editing. + ## [1.2.0] * Fix image button cancel causes crash. diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index ec933b52..c34a8ed8 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -208,18 +208,23 @@ class Line extends Container { _unwrap(); } else if (blockStyle != parentStyle) { _unwrap(); - final block = Block()..applyAttribute(blockStyle); - _wrap(block); - block.adjust(); + _applyBlockStyles(newStyle); } // else the same style, no-op. } else if (blockStyle.value != null) { // Only wrap with a new block if this is not an unset - final block = Block()..applyAttribute(blockStyle); - _wrap(block); - block.adjust(); + _applyBlockStyles(newStyle); } } + void _applyBlockStyles(Style newStyle) { + var block = Block(); + for (final style in newStyle.getBlocksExceptHeader().values) { + block = block..applyAttribute(style); + } + _wrap(block); + block.adjust(); + } + /// Wraps this line with new parent [block]. /// /// This line can not be in a [Block] when this method is called. diff --git a/pubspec.yaml b/pubspec.yaml index 93bf71be..5e87f6d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.2.0 +version: 1.2.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 7cdbdd9a6a10e1f5bdbaf22955b5bc59ac52b8d0 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 22 Apr 2021 23:56:53 -0700 Subject: [PATCH 168/306] Make attribute registry ordered --- lib/models/documents/attribute.dart | 32 +++++++++++++++++++++-------- lib/models/documents/style.dart | 3 ++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 09564e4e..1b9043b9 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:quiver/core.dart'; enum AttributeScope { @@ -14,7 +16,7 @@ class Attribute { final AttributeScope scope; final T value; - static final Map _registry = { + static final Map _registry = LinkedHashMap.of({ Attribute.bold.key: Attribute.bold, Attribute.italic.key: Attribute.italic, Attribute.underline.key: Attribute.underline, @@ -26,16 +28,16 @@ class Attribute { Attribute.background.key: Attribute.background, Attribute.placeholder.key: Attribute.placeholder, Attribute.header.key: Attribute.header, - Attribute.indent.key: Attribute.indent, Attribute.align.key: Attribute.align, Attribute.list.key: Attribute.list, Attribute.codeBlock.key: Attribute.codeBlock, Attribute.blockQuote.key: Attribute.blockQuote, + Attribute.indent.key: Attribute.indent, Attribute.width.key: Attribute.width, Attribute.height.key: Attribute.height, Attribute.style.key: Attribute.style, Attribute.token.key: Attribute.token, - }; + }); static final BoldAttribute bold = BoldAttribute(); @@ -88,22 +90,22 @@ class Attribute { Attribute.placeholder.key, }; - static final Set blockKeys = { + static final Set blockKeys = LinkedHashSet.of({ Attribute.header.key, - Attribute.indent.key, Attribute.align.key, Attribute.list.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - }; + Attribute.indent.key, + }); - static final Set blockKeysExceptHeader = { + static final Set blockKeysExceptHeader = LinkedHashSet.of({ Attribute.list.key, - Attribute.indent.key, Attribute.align.key, Attribute.codeBlock.key, Attribute.blockQuote.key, - }; + Attribute.indent.key, + }); static Attribute get h1 => HeaderAttribute(level: 1); @@ -172,6 +174,18 @@ class Attribute { return attribute; } + static int getRegistryOrder(Attribute attribute) { + var order = 0; + for (final attr in _registry.values) { + if (attr.key == attribute.key) { + break; + } + order++; + } + + return order; + } + static Attribute clone(Attribute origin, dynamic value) { return Attribute(origin.key, origin.scope, value); } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 8174aeca..fade1bb5 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -30,7 +30,8 @@ class Style { Iterable get keys => _attributes.keys; - Iterable get values => _attributes.values; + Iterable get values => _attributes.values.sorted( + (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); Map get attributes => _attributes; From 3e576aeb9a0bb76e7da9e2cc67b1204a78bd4664 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 23 Apr 2021 18:34:16 -0300 Subject: [PATCH 169/306] Fixes `Document.toJson` to map from raw operations (#162) --- lib/models/quill_delta.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index 895b27fe..a0e608be 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -265,7 +265,7 @@ class Delta { List toList() => List.from(_operations); /// Returns JSON-serializable version of this delta. - List toJson() => toList(); + List toJson() => toList().map((operation) => operation.toJson()).toList(); /// Returns `true` if this delta is empty. bool get isEmpty => _operations.isEmpty; From 60127aeb048730e32a8b31d2250fc3f07c411bbb Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 23 Apr 2021 18:44:02 -0300 Subject: [PATCH 170/306] Adds `doc.isEmpty` check to `Document._loadDocument(Delta doc)` (#163) --- lib/models/documents/document.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 83f7a0fc..68dbee4d 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -223,7 +223,12 @@ class Document { String toPlainText() => _root.children.map((e) => e.toPlainText()).join(); void _loadDocument(Delta doc) { + if (doc.isEmpty) { + throw ArgumentError.value(doc, 'Document Delta cannot be empty.'); + } + assert((doc.last.data as String).endsWith('\n')); + var offset = 0; for (final op in doc.toList()) { if (!op.isInsert) { From 451dffc4cdb96ec4a851cb2e7b0f8bb2f70f626a Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Fri, 23 Apr 2021 19:14:33 -0300 Subject: [PATCH 171/306] Fixes crashing disposed listeners on a bunch of widgets (#164) --- lib/widgets/controller.dart | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 59aa1e09..a0c2aa78 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -32,6 +32,13 @@ class QuillController extends ChangeNotifier { Style toggledStyle = Style(); bool ignoreFocusOnTextChange = false; + /// Controls whether this [QuillController] instance has already been disposed + /// of + /// + /// This is a safe approach to make sure that listeners don't crash when + /// adding, removing or listeners to this instance. + bool _isDisposed = false; + // item1: Document state before [change]. // // item2: Change delta applied to the document. @@ -183,9 +190,31 @@ class QuillController extends ChangeNotifier { notifyListeners(); } + @override + void addListener(VoidCallback listener) { + // By using `_isDisposed`, make sure that `addListener` won't be called on a + // disposed `ChangeListener` + if (!_isDisposed) { + super.addListener(listener); + } + } + + @override + void removeListener(VoidCallback listener) { + // By using `_isDisposed`, make sure that `removeListener` won't be called + // on a disposed `ChangeListener` + if (!_isDisposed) { + super.removeListener(listener); + } + } + @override void dispose() { - document.close(); + if (!_isDisposed) { + document.close(); + } + + _isDisposed = true; super.dispose(); } From ca9a13b1505589c8df104a78f2b6d8a36bb32ff4 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 23 Apr 2021 16:23:10 -0700 Subject: [PATCH 172/306] Update Line _format method --- lib/models/documents/nodes/line.dart | 7 +++++-- lib/widgets/controller.dart | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index c34a8ed8..fabfad4d 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -1,5 +1,7 @@ import 'dart:math' as math; +import 'package:collection/collection.dart'; + import '../../quill_delta.dart'; import '../attribute.dart'; import '../style.dart'; @@ -203,10 +205,11 @@ class Line extends Container { } // No block-level changes if (parent is Block) { - final parentStyle = (parent as Block).style.getBlockExceptHeader(); + final parentStyle = (parent as Block).style.getBlocksExceptHeader(); if (blockStyle.value == null) { _unwrap(); - } else if (blockStyle != parentStyle) { + } else if (!const MapEquality() + .equals(newStyle.getBlocksExceptHeader(), parentStyle)) { _unwrap(); _applyBlockStyles(newStyle); } // else the same style, no-op. diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index a0c2aa78..cb5136df 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -34,7 +34,7 @@ class QuillController extends ChangeNotifier { /// Controls whether this [QuillController] instance has already been disposed /// of - /// + /// /// This is a safe approach to make sure that listeners don't crash when /// adding, removing or listeners to this instance. bool _isDisposed = false; From da2a05aaa0baef057750426f5b4ca749060c2be2 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 26 Apr 2021 00:45:40 -0700 Subject: [PATCH 173/306] Update issue templates --- .github/ISSUE_TEMPLATE/issue-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md index ff759a4b..077b59c0 100644 --- a/.github/ISSUE_TEMPLATE/issue-template.md +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -13,4 +13,4 @@ My issue is about [Desktop] I have tried running `example` directory successfully before creating an issue here. -Please note that we are using stable channel. If you are using beta or master channel, those are not supported. +Please note that we are using stable channel on branch master. If you are using beta or master channel, use branch dev. From e49421f48c8dbd30b40ed056c8edd5ab24dc5688 Mon Sep 17 00:00:00 2001 From: kevinDespoulains <46108869+kevinDespoulains@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:50:46 +0200 Subject: [PATCH 174/306] Updating checkbox to handle tap (#186) * updating checkbox to handle tap * updating checkbox to handle long press and using UniqueKey() to avoid weird side effects * removed useless doc Co-authored-by: Kevin Despoulains --- lib/widgets/editor.dart | 32 +---------------- lib/widgets/raw_editor.dart | 44 +++++++++++++++-------- lib/widgets/text_block.dart | 70 ++++++++++++++++++++----------------- 3 files changed, 68 insertions(+), 78 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index d15a8789..f6dd8e40 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -393,8 +393,6 @@ class _QuillEditorSelectionGestureDetectorBuilder final segmentResult = line.queryChild(result.offset, false); if (segmentResult.node == null) { if (line.length == 1) { - // tapping when no text yet on this line - _flipListCheckbox(pos, line, segmentResult); getEditor()!.widget.controller.updateSelection( TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); return true; @@ -434,37 +432,9 @@ class _QuillEditorSelectionGestureDetectorBuilder ), ); } - return false; - } - if (_flipListCheckbox(pos, line, segmentResult)) { - return true; } - return false; - } - bool _flipListCheckbox( - TextPosition pos, Line line, container_node.ChildQuery segmentResult) { - if (getEditor()!.widget.readOnly || - !line.style.containsKey(Attribute.list.key) || - segmentResult.offset != 0) { - return false; - } - // segmentResult.offset == 0 means tap at the beginning of the TextLine - final String? listVal = line.style.attributes[Attribute.list.key]!.value; - if (listVal == Attribute.unchecked.value) { - getEditor()! - .widget - .controller - .formatText(pos.offset, 0, Attribute.checked); - } else if (listVal == Attribute.checked.value) { - getEditor()! - .widget - .controller - .formatText(pos.offset, 0, Attribute.unchecked); - } - getEditor()!.widget.controller.updateSelection( - TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); - return true; + return false; } Future _launchUrl(String url) async { diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 35e3aa68..f93eabe2 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -579,6 +579,18 @@ class RawEditorState extends EditorState } } + /// Updates the checkbox positioned at [offset] in document + /// by changing its attribute according to [value]. + void _handleCheckboxTap(int offset, bool value) { + if (!widget.readOnly) { + if (value) { + widget.controller.formatText(offset, 0, Attribute.checked); + } else { + widget.controller.formatText(offset, 0, Attribute.unchecked); + } + } + } + List _buildChildren(Document doc, BuildContext context) { final result = []; final indentLevelCounts = {}; @@ -589,21 +601,23 @@ class RawEditorState extends EditorState } else if (node is Block) { final attrs = node.style.attributes; final editableTextBlock = EditableTextBlock( - node, - _textDirection, - widget.scrollBottomInset, - _getVerticalSpacingForBlock(node, _styles), - widget.controller.selection, - widget.selectionColor, - _styles, - widget.enableInteractiveSelection, - _hasFocus, - attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - widget.embedBuilder, - _cursorCont, - indentLevelCounts); + node, + _textDirection, + widget.scrollBottomInset, + _getVerticalSpacingForBlock(node, _styles), + widget.controller.selection, + widget.selectionColor, + _styles, + widget.enableInteractiveSelection, + _hasFocus, + attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + widget.embedBuilder, + _cursorCont, + indentLevelCounts, + _handleCheckboxTap, + ); result.add(editableTextBlock); } else { throw StateError('Unreachable.'); diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 309b9cf8..f533a160 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -61,6 +61,7 @@ class EditableTextBlock extends StatelessWidget { this.embedBuilder, this.cursorCont, this.indentLevelCounts, + this.onCheckboxTap, ); final Block block; @@ -76,6 +77,7 @@ class EditableTextBlock extends StatelessWidget { final EmbedBuilder embedBuilder; final CursorCont cursorCont; final Map indentLevelCounts; + final Function(int, bool) onCheckboxTap; @override Widget build(BuildContext context) { @@ -161,12 +163,23 @@ class EditableTextBlock extends StatelessWidget { if (attrs[Attribute.list.key] == Attribute.checked) { return _Checkbox( - style: defaultStyles!.leading!.style, width: 32, isChecked: true); + key: UniqueKey(), + style: defaultStyles!.leading!.style, + width: 32, + isChecked: true, + offset: block.offset + line.offset, + onTap: onCheckboxTap, + ); } if (attrs[Attribute.list.key] == Attribute.unchecked) { return _Checkbox( - style: defaultStyles!.leading!.style, width: 32, isChecked: false); + key: UniqueKey(), + style: defaultStyles!.leading!.style, + width: 32, + offset: block.offset + line.offset, + onTap: onCheckboxTap, + ); } if (attrs.containsKey(Attribute.codeBlock.key)) { @@ -685,46 +698,39 @@ class _BulletPoint extends StatelessWidget { } } -class _Checkbox extends StatefulWidget { - const _Checkbox({Key? key, this.style, this.width, this.isChecked}) - : super(key: key); - +class _Checkbox extends StatelessWidget { + const _Checkbox({ + Key? key, + this.style, + this.width, + this.isChecked = false, + this.offset, + this.onTap, + }) : super(key: key); final TextStyle? style; final double? width; - final bool? isChecked; + final bool isChecked; + final int? offset; + final Function(int, bool)? onTap; - @override - __CheckboxState createState() => __CheckboxState(); -} - -class __CheckboxState extends State<_Checkbox> { - bool? isChecked; - - void _onCheckboxClicked(bool? newValue) => setState(() { - isChecked = newValue; - - if (isChecked!) { - // check list - } else { - // uncheck list - } - }); - - @override - void initState() { - super.initState(); - isChecked = widget.isChecked; + void _onCheckboxClicked(bool? newValue) { + if (onTap != null && newValue != null && offset != null) { + onTap!(offset!, newValue); + } } @override Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.topEnd, - width: widget.width, + width: width, padding: const EdgeInsetsDirectional.only(end: 13), - child: Checkbox( - value: widget.isChecked, - onChanged: _onCheckboxClicked, + child: GestureDetector( + onLongPress: () => _onCheckboxClicked(!isChecked), + child: Checkbox( + value: isChecked, + onChanged: _onCheckboxClicked, + ), ), ); } From 760b4def7ea0c4ef39857572d16e329aad3f6dfb Mon Sep 17 00:00:00 2001 From: em6m6e <50019687+em6m6e@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:52:37 +0200 Subject: [PATCH 175/306] Simple viewer (#187) * 2021-04-25 * 2021-04-26 --- lib/widgets/simple_viewer.dart | 337 +++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 lib/widgets/simple_viewer.dart diff --git a/lib/widgets/simple_viewer.dart b/lib/widgets/simple_viewer.dart new file mode 100644 index 00000000..97cdcedd --- /dev/null +++ b/lib/widgets/simple_viewer.dart @@ -0,0 +1,337 @@ +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:tuple/tuple.dart'; + +import '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/leaf.dart' as leaf; +import '../models/documents/nodes/line.dart'; +import 'controller.dart'; +import 'cursor.dart'; +import 'default_styles.dart'; +import 'delegate.dart'; +import 'editor.dart'; +import 'text_block.dart'; +import 'text_line.dart'; + +class QuillSimpleViewer extends StatefulWidget { + const QuillSimpleViewer({ + required this.controller, + this.customStyles, + this.truncate = false, + this.truncateScale, + this.truncateAlignment, + this.truncateHeight, + this.truncateWidth, + this.scrollBottomInset = 0, + this.padding = EdgeInsets.zero, + this.embedBuilder, + Key? key, + }) : assert(truncate || + ((truncateScale == null) && + (truncateAlignment == null) && + (truncateHeight == null) && + (truncateWidth == null))), + super(key: key); + + final QuillController controller; + final DefaultStyles? customStyles; + final bool truncate; + final double? truncateScale; + final Alignment? truncateAlignment; + final double? truncateHeight; + final double? truncateWidth; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + final EmbedBuilder? embedBuilder; + + @override + _QuillSimpleViewerState createState() => _QuillSimpleViewerState(); +} + +class _QuillSimpleViewerState extends State + with SingleTickerProviderStateMixin { + late DefaultStyles _styles; + final LayerLink _toolbarLayerLink = LayerLink(); + final LayerLink _startHandleLayerLink = LayerLink(); + final LayerLink _endHandleLayerLink = LayerLink(); + late CursorCont _cursorCont; + + @override + void initState() { + super.initState(); + + _cursorCont = CursorCont( + show: ValueNotifier(false), + style: const CursorStyle( + color: Colors.black, + backgroundColor: Colors.grey, + width: 2, + radius: Radius.zero, + offset: Offset.zero, + ), + tickerProvider: this, + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final parentStyles = QuillStyles.getStyles(context, true); + final defaultStyles = DefaultStyles.getInstance(context); + _styles = (parentStyles != null) + ? defaultStyles.merge(parentStyles) + : defaultStyles; + + if (widget.customStyles != null) { + _styles = _styles.merge(widget.customStyles!); + } + } + + EmbedBuilder get embedBuilder => widget.embedBuilder ?? _defaultEmbedBuilder; + + Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { + assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); + switch (node.value.type) { + case 'image': + final imageUrl = _standardizeImageUrl(node.value.data); + return imageUrl.startsWith('http') + ? Image.network(imageUrl) + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + : Image.file(io.File(imageUrl)); + default: + throw UnimplementedError( + 'Embeddable type "${node.value.type}" is not supported by default embed ' + 'builder of QuillEditor. You must pass your own builder function to ' + 'embedBuilder property of QuillEditor or QuillField widgets.'); + } + } + + String _standardizeImageUrl(String url) { + if (url.contains('base64')) { + return url.split(',')[1]; + } + return url; + } + + @override + Widget build(BuildContext context) { + final _doc = widget.controller.document; + // if (_doc.isEmpty() && + // !widget.focusNode.hasFocus && + // widget.placeholder != null) { + // _doc = Document.fromJson(jsonDecode( + // '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); + // } + + Widget child = CompositedTransformTarget( + link: _toolbarLayerLink, + child: Semantics( + child: _SimpleViewer( + document: _doc, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _nullSelectionChanged, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + children: _buildChildren(_doc, context), + ), + ), + ); + + if (widget.truncate) { + if (widget.truncateScale != null) { + child = Container( + height: widget.truncateHeight, + child: Align( + heightFactor: widget.truncateScale, + widthFactor: widget.truncateScale, + alignment: widget.truncateAlignment ?? Alignment.topLeft, + child: Container( + width: widget.truncateWidth! / widget.truncateScale!, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Transform.scale( + scale: widget.truncateScale!, + alignment: + widget.truncateAlignment ?? Alignment.topLeft, + child: child))))); + } else { + child = Container( + height: widget.truncateHeight, + width: widget.truncateWidth, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), child: child)); + } + } + + return QuillStyles(data: _styles, child: child); + } + + List _buildChildren(Document doc, BuildContext context) { + final result = []; + final indentLevelCounts = {}; + for (final node in doc.root.children) { + if (node is Line) { + final editableTextLine = _getEditableTextLineFromNode(node, context); + result.add(editableTextLine); + } else if (node is Block) { + final attrs = node.style.attributes; + final editableTextBlock = EditableTextBlock( + node, + _textDirection, + widget.scrollBottomInset, + _getVerticalSpacingForBlock(node, _styles), + widget.controller.selection, + Colors.black, + // selectionColor, + _styles, + false, + // enableInteractiveSelection, + false, + // hasFocus, + attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + embedBuilder, + _cursorCont, + indentLevelCounts); + result.add(editableTextBlock); + } else { + throw StateError('Unreachable.'); + } + } + return result; + } + + TextDirection get _textDirection { + final result = Directionality.of(context); + return result; + } + + EditableTextLine _getEditableTextLineFromNode( + Line node, BuildContext context) { + final textLine = TextLine( + line: node, + textDirection: _textDirection, + embedBuilder: embedBuilder, + styles: _styles, + ); + final editableTextLine = EditableTextLine( + node, + null, + textLine, + 0, + _getVerticalSpacingForLine(node, _styles), + _textDirection, + widget.controller.selection, + Colors.black, + //widget.selectionColor, + false, + //enableInteractiveSelection, + false, + //_hasFocus, + MediaQuery.of(context).devicePixelRatio, + _cursorCont); + return editableTextLine; + } + + Tuple2 _getVerticalSpacingForLine( + Line line, DefaultStyles? defaultStyles) { + final attrs = line.style.attributes; + if (attrs.containsKey(Attribute.header.key)) { + final int? level = attrs[Attribute.header.key]!.value; + switch (level) { + case 1: + return defaultStyles!.h1!.verticalSpacing; + case 2: + return defaultStyles!.h2!.verticalSpacing; + case 3: + return defaultStyles!.h3!.verticalSpacing; + default: + throw 'Invalid level $level'; + } + } + + return defaultStyles!.paragraph!.verticalSpacing; + } + + Tuple2 _getVerticalSpacingForBlock( + Block node, DefaultStyles? defaultStyles) { + final attrs = node.style.attributes; + if (attrs.containsKey(Attribute.blockQuote.key)) { + return defaultStyles!.quote!.verticalSpacing; + } else if (attrs.containsKey(Attribute.codeBlock.key)) { + return defaultStyles!.code!.verticalSpacing; + } else if (attrs.containsKey(Attribute.indent.key)) { + return defaultStyles!.indent!.verticalSpacing; + } + return defaultStyles!.lists!.verticalSpacing; + } + + void _nullSelectionChanged( + TextSelection selection, SelectionChangedCause cause) {} +} + +class _SimpleViewer extends MultiChildRenderObjectWidget { + _SimpleViewer({ + required List children, + required this.document, + required this.textDirection, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.onSelectionChanged, + required this.scrollBottomInset, + this.padding = EdgeInsets.zero, + Key? key, + }) : super(key: key, children: children); + + final Document document; + final TextDirection textDirection; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final TextSelectionChangedHandler onSelectionChanged; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + + @override + RenderEditor createRenderObject(BuildContext context) { + return RenderEditor( + null, + textDirection, + scrollBottomInset, + padding, + document, + const TextSelection(baseOffset: 0, extentOffset: 0), + false, + // hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + const EdgeInsets.fromLTRB(4, 4, 4, 5), + ); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderEditor renderObject) { + renderObject + ..document = document + ..setContainer(document.root) + ..textDirection = textDirection + ..setStartHandleLayerLink(startHandleLayerLink) + ..setEndHandleLayerLink(endHandleLayerLink) + ..onSelectionChanged = onSelectionChanged + ..setScrollBottomInset(scrollBottomInset) + ..setPadding(padding); + } +} From 06c75637682053f2f71250d6622e2d91b1213527 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 26 Apr 2021 09:07:04 -0700 Subject: [PATCH 176/306] Fix simple viewer compilation error --- lib/widgets/simple_viewer.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/widgets/simple_viewer.dart b/lib/widgets/simple_viewer.dart index 97cdcedd..ee1a7732 100644 --- a/lib/widgets/simple_viewer.dart +++ b/lib/widgets/simple_viewer.dart @@ -204,7 +204,8 @@ class _QuillSimpleViewerState extends State : null, embedBuilder, _cursorCont, - indentLevelCounts); + indentLevelCounts, + _handleCheckboxTap); result.add(editableTextBlock); } else { throw StateError('Unreachable.'); @@ -213,6 +214,12 @@ class _QuillSimpleViewerState extends State return result; } + /// Updates the checkbox positioned at [offset] in document + /// by changing its attribute according to [value]. + void _handleCheckboxTap(int offset, bool value) { + // readonly - do nothing + } + TextDirection get _textDirection { final result = Directionality.of(context); return result; From 2adebbe11bbb54126ef9322f959adb02ad2a770a Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 26 Apr 2021 09:08:47 -0700 Subject: [PATCH 177/306] Upgrade version - checkbox supports tapping --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96c8386..62e3a30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.2.2] +* Checkbox supports tapping. + ## [1.2.1] * Indented position not holding while editing. diff --git a/pubspec.yaml b/pubspec.yaml index 5e87f6d7..de0226ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.2.1 +version: 1.2.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 08412c167a85310293df854be1a1c4d51bf0e9b2 Mon Sep 17 00:00:00 2001 From: Gyuri Majercsik Date: Mon, 26 Apr 2021 20:09:03 +0300 Subject: [PATCH 178/306] 171: support for non-scrollable text editor (#188) Co-authored-by: Gyuri Majercsik --- lib/widgets/editor.dart | 1 - lib/widgets/raw_editor.dart | 33 ++++++++++++++++++--------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index f6dd8e40..662a018c 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1043,7 +1043,6 @@ class RenderEditableContainerBox extends RenderBox @override void performLayout() { - assert(!constraints.hasBoundedHeight); assert(constraints.hasBoundedWidth); _resolvePadding(); assert(_resolvedPadding != null); diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index f93eabe2..4df93a4d 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1008,25 +1008,28 @@ class RawEditorState extends EditorState _showCaretOnScreenScheduled = true; SchedulerBinding.instance!.addPostFrameCallback((_) { - _showCaretOnScreenScheduled = false; + if (widget.scrollable) { + _showCaretOnScreenScheduled = false; - final viewport = RenderAbstractViewport.of(getRenderEditor())!; - final editorOffset = getRenderEditor()! - .localToGlobal(const Offset(0, 0), ancestor: viewport); - final offsetInViewport = _scrollController!.offset + editorOffset.dy; + final viewport = RenderAbstractViewport.of(getRenderEditor()); - final offset = getRenderEditor()!.getOffsetToRevealCursor( - _scrollController!.position.viewportDimension, - _scrollController!.offset, - offsetInViewport, - ); + final editorOffset = getRenderEditor()! + .localToGlobal(const Offset(0, 0), ancestor: viewport); + final offsetInViewport = _scrollController!.offset + editorOffset.dy; - if (offset != null) { - _scrollController!.animateTo( - offset, - duration: const Duration(milliseconds: 100), - curve: Curves.fastOutSlowIn, + final offset = getRenderEditor()!.getOffsetToRevealCursor( + _scrollController!.position.viewportDimension, + _scrollController!.offset, + offsetInViewport, ); + + if (offset != null) { + _scrollController!.animateTo( + offset, + duration: const Duration(milliseconds: 100), + curve: Curves.fastOutSlowIn, + ); + } } }); } From f816ad7ec84a2166c28d12b3a85acc88d0429e0e Mon Sep 17 00:00:00 2001 From: hyouuu Date: Mon, 3 May 2021 01:34:01 -0700 Subject: [PATCH 179/306] custom rules & optionally auto add newline for image embeds (#205) --- lib/models/documents/document.dart | 31 ++++++++++++++++++++---------- lib/models/rules/rule.dart | 8 +++++++- lib/widgets/controller.dart | 4 ++-- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 68dbee4d..9d966f32 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -40,6 +40,10 @@ class Document { final Rules _rules = Rules.getInstance(); + void setCustomRules(List customRules) { + _rules.setCustomRules(customRules); + } + final StreamController> _observer = StreamController.broadcast(); @@ -47,7 +51,7 @@ class Document { Stream> get changes => _observer.stream; - Delta insert(int index, Object? data, {int replaceLength = 0}) { + Delta insert(int index, Object? data, {int replaceLength = 0, bool autoAppendNewlineAfterImage = true}) { assert(index >= 0); assert(data is String || data is Embeddable); if (data is Embeddable) { @@ -58,7 +62,7 @@ class Document { final delta = _rules.apply(RuleType.INSERT, this, index, data: data, len: replaceLength); - compose(delta, ChangeSource.LOCAL); + compose(delta, ChangeSource.LOCAL, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); return delta; } @@ -71,7 +75,7 @@ class Document { return delta; } - Delta replace(int index, int len, Object? data) { + Delta replace(int index, int len, Object? data, {bool autoAppendNewlineAfterImage = true}) { assert(index >= 0); assert(data is String || data is Embeddable); @@ -84,7 +88,8 @@ class Document { // We have to insert before applying delete rules // Otherwise delete would be operating on stale document snapshot. if (dataIsNotEmpty) { - delta = insert(index, data, replaceLength: len); + delta = insert(index, data, replaceLength: len, + autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); } if (len > 0) { @@ -124,13 +129,13 @@ class Document { return block.queryChild(res.offset, true); } - void compose(Delta delta, ChangeSource changeSource) { + void compose(Delta delta, ChangeSource changeSource, {bool autoAppendNewlineAfterImage = true}) { assert(!_observer.isClosed); delta.trim(); assert(delta.isNotEmpty); var offset = 0; - delta = _transform(delta); + delta = _transform(delta, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); final originalDelta = toDelta(); for (final op in delta.toList()) { final style = @@ -174,22 +179,28 @@ class Document { bool get hasRedo => _history.hasRedo; - static Delta _transform(Delta delta) { + static Delta _transform(Delta delta, {bool autoAppendNewlineAfterImage = true}) { 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); + if (autoAppendNewlineAfterImage) { + _autoAppendNewlineAfterImage(i, ops, op, res); + } } return res; } - static void _handleImageInsert( + static void _autoAppendNewlineAfterImage( int i, List ops, Operation op, Delta res) { final nextOpIsImage = i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String; - if (nextOpIsImage && !(op.data as String).endsWith('\n')) { + if (nextOpIsImage && + op.data is String && + (op.data as String).isNotEmpty && + !(op.data as String).endsWith('\n')) + { res.push(Operation.insert('\n')); } // Currently embed is equivalent to image and hence `is! String` diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index 4ee6c278..042f1aaa 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -28,6 +28,8 @@ abstract class Rule { class Rules { Rules(this._rules); + List _customRules = []; + final List _rules; static final Rules _instance = Rules([ const FormatLinkAtCaretPositionRule(), @@ -49,10 +51,14 @@ class Rules { static Rules getInstance() => _instance; + void setCustomRules(List customRules) { + _customRules = customRules; + } + Delta apply(RuleType ruleType, Document document, int index, {int? len, Object? data, Attribute? attribute}) { final delta = document.toDelta(); - for (final rule in _rules) { + for (final rule in _customRules + _rules) { if (rule.type != ruleType) { continue; } diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index cb5136df..4820b3f9 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -92,12 +92,12 @@ class QuillController extends ChangeNotifier { void replaceText( int index, int len, Object? data, TextSelection? textSelection, - {bool ignoreFocus = false}) { + {bool ignoreFocus = false, bool autoAppendNewlineAfterImage = true}) { assert(data is String || data is Embeddable); Delta? delta; if (len > 0 || data is! String || data.isNotEmpty) { - delta = document.replace(index, len, data); + delta = document.replace(index, len, data, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); var shouldRetainDelta = toggledStyle.isNotEmpty && delta.isNotEmpty && delta.length <= 2 && From 1ac73b7cae0245f9a69bb002bcfcd334091bdaca Mon Sep 17 00:00:00 2001 From: kevinDespoulains <46108869+kevinDespoulains@users.noreply.github.com> Date: Wed, 19 May 2021 13:29:38 +0200 Subject: [PATCH 180/306] Adding missing overrides to make package work with Flutter 2.2.0 (#226) --- lib/widgets/raw_editor.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 4df93a4d..cfd98c42 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1050,7 +1050,7 @@ class RawEditorState extends EditorState } @override - void hideToolbar() { + void hideToolbar([bool hideHandles = true]) { if (getSelectionOverlay()?.toolbar != null) { getSelectionOverlay()?.hideToolbar(); } @@ -1152,6 +1152,11 @@ class RawEditorState extends EditorState closeConnectionIfNeeded(); } } + + @override + void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { + // TODO: implement userUpdateTextEditingValue + } } class _Editor extends MultiChildRenderObjectWidget { From 5655920937e9964d7d10bf60740b71db69bba1af Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Wed, 19 May 2021 18:47:57 +0200 Subject: [PATCH 181/306] Improve SOC of raw editor (#227) Improve separation of concerns for RawEditor by moving the code for the text input client to a separate class, furthermore add more comments. --- ..._editor_state_text_input_client_mixin.dart | 200 ++++++++++++++++++ lib/widgets/raw_editor.dart | 159 +------------- 2 files changed, 205 insertions(+), 154 deletions(-) create mode 100644 lib/widgets/controller/raw_editor_state_text_input_client_mixin.dart diff --git a/lib/widgets/controller/raw_editor_state_text_input_client_mixin.dart b/lib/widgets/controller/raw_editor_state_text_input_client_mixin.dart new file mode 100644 index 00000000..527df582 --- /dev/null +++ b/lib/widgets/controller/raw_editor_state_text_input_client_mixin.dart @@ -0,0 +1,200 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import '../../utils/diff_delta.dart'; +import '../editor.dart'; + +mixin RawEditorStateTextInputClientMixin on EditorState + implements TextInputClient { + final List _sentRemoteValues = []; + TextInputConnection? _textInputConnection; + TextEditingValue? _lastKnownRemoteTextEditingValue; + + /// Whether to create an input connection with the platform for text editing + /// or not. + /// + /// Read-only input fields do not need a connection with the platform since + /// there's no need for text editing capabilities (e.g. virtual keyboard). + /// + /// On the web, we always need a connection because we want some browser + /// functionalities to continue to work on read-only input fields like: + /// + /// - Relevant context menu. + /// - cmd/ctrl+c shortcut to copy. + /// - cmd/ctrl+a to select all. + /// - Changing the selection using a physical keyboard. + bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; + + /// Returns `true` if there is open input connection. + bool get hasConnection => + _textInputConnection != null && _textInputConnection!.attached; + + /// Opens or closes input connection based on the current state of + /// [focusNode] and [value]. + void openOrCloseConnection() { + if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { + openConnectionIfNeeded(); + } else if (!widget.focusNode.hasFocus) { + closeConnectionIfNeeded(); + } + } + + void openConnectionIfNeeded() { + if (!shouldCreateInputConnection) { + return; + } + + if (!hasConnection) { + _lastKnownRemoteTextEditingValue = getTextEditingValue(); + _textInputConnection = TextInput.attach( + this, + TextInputConfiguration( + inputType: TextInputType.multiline, + readOnly: widget.readOnly, + inputAction: TextInputAction.newline, + enableSuggestions: !widget.readOnly, + keyboardAppearance: widget.keyboardAppearance, + textCapitalization: widget.textCapitalization, + ), + ); + + _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); + // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); + } + + _textInputConnection!.show(); + } + + /// Closes input connection if it's currently open. Otherwise does nothing. + void closeConnectionIfNeeded() { + if (!hasConnection) { + return; + } + _textInputConnection!.close(); + _textInputConnection = null; + _lastKnownRemoteTextEditingValue = null; + _sentRemoteValues.clear(); + } + + /// Updates remote value based on current state of [document] and + /// [selection]. + /// + /// This method may not actually send an update to native side if it thinks + /// remote value is up to date or identical. + void updateRemoteValueIfNeeded() { + if (!hasConnection) { + return; + } + + // Since we don't keep track of the composing range in value provided + // by the Controller we need to add it here manually before comparing + // with the last known remote value. + // It is important to prevent excessive remote updates as it can cause + // race conditions. + final actualValue = getTextEditingValue().copyWith( + composing: _lastKnownRemoteTextEditingValue!.composing, + ); + + if (actualValue == _lastKnownRemoteTextEditingValue) { + return; + } + + final shouldRemember = + getTextEditingValue().text != _lastKnownRemoteTextEditingValue!.text; + _lastKnownRemoteTextEditingValue = actualValue; + _textInputConnection!.setEditingState(actualValue); + if (shouldRemember) { + // Only keep track if text changed (selection changes are not relevant) + _sentRemoteValues.add(actualValue); + } + } + + @override + TextEditingValue? get currentTextEditingValue => + _lastKnownRemoteTextEditingValue; + + // autofill is not needed + @override + AutofillScope? get currentAutofillScope => null; + + @override + void updateEditingValue(TextEditingValue value) { + if (!shouldCreateInputConnection) { + return; + } + + if (_sentRemoteValues.contains(value)) { + /// There is a race condition in Flutter text input plugin where sending + /// updates to native side too often results in broken behavior. + /// TextInputConnection.setEditingValue is an async call to native side. + /// For each such call native side _always_ sends an update which triggers + /// this method (updateEditingValue) with the same value we've sent it. + /// If multiple calls to setEditingValue happen too fast and we only + /// track the last sent value then there is no way for us to filter out + /// automatic callbacks from native side. + /// Therefore we have to keep track of all values we send to the native + /// side and when we see this same value appear here we skip it. + /// This is fragile but it's probably the only available option. + _sentRemoteValues.remove(value); + return; + } + + if (_lastKnownRemoteTextEditingValue == value) { + // There is no difference between this value and the last known value. + return; + } + + // Check if only composing range changed. + if (_lastKnownRemoteTextEditingValue!.text == value.text && + _lastKnownRemoteTextEditingValue!.selection == value.selection) { + // This update only modifies composing range. Since we don't keep track + // of composing range we just need to update last known value here. + // This check fixes an issue on Android when it sends + // composing updates separately from regular changes for text and + // selection. + _lastKnownRemoteTextEditingValue = value; + return; + } + + final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; + _lastKnownRemoteTextEditingValue = value; + final oldText = effectiveLastKnownValue.text; + final text = value.text; + final cursorPosition = value.selection.extentOffset; + final diff = getDiff(oldText, text, cursorPosition); + widget.controller.replaceText( + diff.start, diff.deleted.length, diff.inserted, value.selection); + } + + @override + void performAction(TextInputAction action) { + // no-op + } + + @override + void performPrivateCommand(String action, Map data) { + // no-op + } + + @override + void updateFloatingCursor(RawFloatingCursorPoint point) { + throw UnimplementedError(); + } + + @override + void showAutocorrectionPromptRect(int start, int end) { + throw UnimplementedError(); + } + + @override + void connectionClosed() { + if (!hasConnection) { + return; + } + _textInputConnection!.connectionClosedReceived(); + _textInputConnection = null; + _lastKnownRemoteTextEditingValue = null; + _sentRemoteValues.clear(); + } +} diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index cfd98c42..d9df13d5 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -17,6 +17,7 @@ import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; import '../utils/diff_delta.dart'; import 'controller.dart'; +import 'controller/raw_editor_state_text_input_client_mixin.dart'; import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; @@ -98,12 +99,10 @@ class RawEditorState extends EditorState with AutomaticKeepAliveClientMixin, WidgetsBindingObserver, - TickerProviderStateMixin + TickerProviderStateMixin, + RawEditorStateTextInputClientMixin implements TextSelectionDelegate, TextInputClient { final GlobalKey _editorKey = GlobalKey(); - final List _sentRemoteValues = []; - TextInputConnection? _textInputConnection; - TextEditingValue? _lastKnownRemoteTextEditingValue; int _cursorResetLocation = -1; bool _wasSelectingVerticallyWithKeyboard = false; EditorTextSelectionOverlay? _selectionOverlay; @@ -122,21 +121,6 @@ class RawEditorState extends EditorState final LayerLink _startHandleLayerLink = LayerLink(); final LayerLink _endHandleLayerLink = LayerLink(); - /// Whether to create an input connection with the platform for text editing - /// or not. - /// - /// Read-only input fields do not need a connection with the platform since - /// there's no need for text editing capabilities (e.g. virtual keyboard). - /// - /// On the web, we always need a connection because we want some browser - /// functionalities to continue to work on read-only input fields like: - /// - /// - Relevant context menu. - /// - cmd/ctrl+c shortcut to copy. - /// - cmd/ctrl+a to select all. - /// - Changing the selection using a physical keyboard. - bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; - bool get _hasFocus => widget.focusNode.hasFocus; TextDirection get _textDirection { @@ -364,105 +348,6 @@ class RawEditorState extends EditorState return 0; } - bool get hasConnection => - _textInputConnection != null && _textInputConnection!.attached; - - void openConnectionIfNeeded() { - if (!shouldCreateInputConnection) { - return; - } - - if (!hasConnection) { - _lastKnownRemoteTextEditingValue = textEditingValue; - _textInputConnection = TextInput.attach( - this, - TextInputConfiguration( - inputType: TextInputType.multiline, - readOnly: widget.readOnly, - inputAction: TextInputAction.newline, - enableSuggestions: !widget.readOnly, - keyboardAppearance: widget.keyboardAppearance, - textCapitalization: widget.textCapitalization, - ), - ); - - _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); - // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); - } - - _textInputConnection!.show(); - } - - void closeConnectionIfNeeded() { - if (!hasConnection) { - return; - } - _textInputConnection!.close(); - _textInputConnection = null; - _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); - } - - void updateRemoteValueIfNeeded() { - if (!hasConnection) { - return; - } - - final actualValue = textEditingValue.copyWith( - composing: _lastKnownRemoteTextEditingValue!.composing, - ); - - if (actualValue == _lastKnownRemoteTextEditingValue) { - return; - } - - final shouldRemember = - textEditingValue.text != _lastKnownRemoteTextEditingValue!.text; - _lastKnownRemoteTextEditingValue = actualValue; - _textInputConnection!.setEditingState(actualValue); - if (shouldRemember) { - _sentRemoteValues.add(actualValue); - } - } - - @override - TextEditingValue? get currentTextEditingValue => - _lastKnownRemoteTextEditingValue; - - @override - AutofillScope? get currentAutofillScope => null; - - @override - void updateEditingValue(TextEditingValue value) { - if (!shouldCreateInputConnection) { - return; - } - - if (_sentRemoteValues.contains(value)) { - _sentRemoteValues.remove(value); - return; - } - - if (_lastKnownRemoteTextEditingValue == value) { - return; - } - - if (_lastKnownRemoteTextEditingValue!.text == value.text && - _lastKnownRemoteTextEditingValue!.selection == value.selection) { - _lastKnownRemoteTextEditingValue = value; - return; - } - - final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; - _lastKnownRemoteTextEditingValue = value; - final oldText = effectiveLastKnownValue.text; - final text = value.text; - final cursorPosition = value.selection.extentOffset; - final diff = getDiff(oldText, text, cursorPosition); - widget.controller.replaceText( - diff.start, diff.deleted.length, diff.inserted, value.selection); - } - @override TextEditingValue get textEditingValue { return getTextEditingValue(); @@ -473,36 +358,9 @@ class RawEditorState extends EditorState setTextEditingValue(value); } - @override - void performAction(TextInputAction action) {} - - @override - void performPrivateCommand(String action, Map data) {} - - @override - void updateFloatingCursor(RawFloatingCursorPoint point) { - throw UnimplementedError(); - } - - @override - void showAutocorrectionPromptRect(int start, int end) { - throw UnimplementedError(); - } - @override void bringIntoView(TextPosition position) {} - @override - void connectionClosed() { - if (!hasConnection) { - return; - } - _textInputConnection!.connectionClosedReceived(); - _textInputConnection = null; - _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); - } - @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); @@ -1145,16 +1003,9 @@ class RawEditorState extends EditorState @override bool get wantKeepAlive => widget.focusNode.hasFocus; - void openOrCloseConnection() { - if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { - openConnectionIfNeeded(); - } else if (!widget.focusNode.hasFocus) { - closeConnectionIfNeeded(); - } - } - @override - void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { + void userUpdateTextEditingValue( + TextEditingValue value, SelectionChangedCause cause) { // TODO: implement userUpdateTextEditingValue } } From 90e7f0ef434502ce23da1a49067b2f4eda2ec189 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 19 May 2021 09:50:28 -0700 Subject: [PATCH 182/306] Upgrade version --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62e3a30e..323da506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.3.0] +* Support flutter 2.2.0. + ## [1.2.2] * Checkbox supports tapping. diff --git a/pubspec.yaml b/pubspec.yaml index de0226ba..7cf52633 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.2.2 +version: 1.3.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 0c05c530e3f0815d3fce06b5e5b380394dfee8ee Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 20 May 2021 13:43:05 +0200 Subject: [PATCH 183/306] Improve SOC of raw editor (#228) Improve separation of concerns for `RawEditor` by moving the code for the keyboard to a separate class, furthermore add more comments. The PR does not change the functionality of the code. --- lib/widgets/raw_editor.dart | 310 +-------------- .../raw_editor_state_keyboard_mixin.dart | 354 ++++++++++++++++++ ..._editor_state_text_input_client_mixin.dart | 0 3 files changed, 357 insertions(+), 307 deletions(-) create mode 100644 lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart rename lib/widgets/{controller => raw_editor}/raw_editor_state_text_input_client_mixin.dart (100%) diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index d9df13d5..580dd5b7 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -15,15 +15,15 @@ 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 'controller/raw_editor_state_text_input_client_mixin.dart'; import 'cursor.dart'; import 'default_styles.dart'; import 'delegate.dart'; import 'editor.dart'; import 'keyboard_listener.dart'; import 'proxy.dart'; +import 'raw_editor/raw_editor_state_keyboard_mixin.dart'; +import 'raw_editor/raw_editor_state_text_input_client_mixin.dart'; import 'text_block.dart'; import 'text_line.dart'; import 'text_selection.dart'; @@ -100,11 +100,10 @@ class RawEditorState extends EditorState AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin, + RawEditorStateKeyboardMixin, RawEditorStateTextInputClientMixin implements TextSelectionDelegate, TextInputClient { final GlobalKey _editorKey = GlobalKey(); - int _cursorResetLocation = -1; - bool _wasSelectingVerticallyWithKeyboard = false; EditorTextSelectionOverlay? _selectionOverlay; FocusAttachment? _focusAttachment; late CursorCont _cursorCont; @@ -128,226 +127,6 @@ class RawEditorState extends EditorState return result; } - void handleCursorMovement( - LogicalKeyboardKey key, - bool wordModifier, - bool lineModifier, - bool shift, - ) { - if (wordModifier && lineModifier) { - return; - } - final selection = widget.controller.selection; - - var newSelection = widget.controller.selection; - - final plainText = textEditingValue.text; - - final rightKey = key == LogicalKeyboardKey.arrowRight, - leftKey = key == LogicalKeyboardKey.arrowLeft, - upKey = key == LogicalKeyboardKey.arrowUp, - downKey = key == LogicalKeyboardKey.arrowDown; - - if ((rightKey || leftKey) && !(rightKey && leftKey)) { - newSelection = _jumpToBeginOrEndOfWord(newSelection, wordModifier, - leftKey, rightKey, plainText, lineModifier, shift); - } - - if (downKey || upKey) { - newSelection = _handleMovingCursorVertically( - upKey, downKey, shift, selection, newSelection, plainText); - } - - if (!shift) { - newSelection = - _placeCollapsedSelection(selection, newSelection, leftKey, rightKey); - } - - widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); - } - - TextSelection _placeCollapsedSelection(TextSelection selection, - TextSelection newSelection, bool leftKey, bool rightKey) { - var newOffset = newSelection.extentOffset; - if (!selection.isCollapsed) { - if (leftKey) { - newOffset = newSelection.baseOffset < newSelection.extentOffset - ? newSelection.baseOffset - : newSelection.extentOffset; - } else if (rightKey) { - newOffset = newSelection.baseOffset > newSelection.extentOffset - ? newSelection.baseOffset - : newSelection.extentOffset; - } - } - return TextSelection.fromPosition(TextPosition(offset: newOffset)); - } - - TextSelection _handleMovingCursorVertically( - bool upKey, - bool downKey, - bool shift, - TextSelection selection, - TextSelection newSelection, - String plainText) { - final originPosition = TextPosition( - offset: upKey ? selection.baseOffset : selection.extentOffset); - - final child = getRenderEditor()!.childAtPosition(originPosition); - final localPosition = TextPosition( - offset: originPosition.offset - child.getContainer().documentOffset); - - var position = upKey - ? child.getPositionAbove(localPosition) - : child.getPositionBelow(localPosition); - - if (position == null) { - final sibling = upKey - ? getRenderEditor()!.childBefore(child) - : getRenderEditor()!.childAfter(child); - if (sibling == null) { - position = TextPosition(offset: upKey ? 0 : plainText.length - 1); - } else { - final finalOffset = Offset( - child.getOffsetForCaret(localPosition).dx, - sibling - .getOffsetForCaret(TextPosition( - offset: upKey ? sibling.getContainer().length - 1 : 0)) - .dy); - final siblingPosition = sibling.getPositionForOffset(finalOffset); - position = TextPosition( - offset: - sibling.getContainer().documentOffset + siblingPosition.offset); - } - } else { - position = TextPosition( - offset: child.getContainer().documentOffset + position.offset); - } - - if (position.offset == newSelection.extentOffset) { - if (downKey) { - newSelection = newSelection.copyWith(extentOffset: plainText.length); - } else if (upKey) { - newSelection = newSelection.copyWith(extentOffset: 0); - } - _wasSelectingVerticallyWithKeyboard = shift; - return newSelection; - } - - if (_wasSelectingVerticallyWithKeyboard && shift) { - newSelection = newSelection.copyWith(extentOffset: _cursorResetLocation); - _wasSelectingVerticallyWithKeyboard = false; - return newSelection; - } - newSelection = newSelection.copyWith(extentOffset: position.offset); - _cursorResetLocation = newSelection.extentOffset; - return newSelection; - } - - TextSelection _jumpToBeginOrEndOfWord( - TextSelection newSelection, - bool wordModifier, - bool leftKey, - bool rightKey, - String plainText, - bool lineModifier, - bool shift) { - if (wordModifier) { - if (leftKey) { - final textSelection = getRenderEditor()!.selectWordAtPosition( - TextPosition( - offset: _previousCharacter( - newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.baseOffset); - } - final textSelection = getRenderEditor()!.selectWordAtPosition( - TextPosition( - offset: - _nextCharacter(newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.extentOffset); - } else if (lineModifier) { - if (leftKey) { - final textSelection = getRenderEditor()!.selectLineAtPosition( - TextPosition( - offset: _previousCharacter( - newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.baseOffset); - } - final startPoint = newSelection.extentOffset; - if (startPoint < plainText.length) { - final textSelection = getRenderEditor()! - .selectLineAtPosition(TextPosition(offset: startPoint)); - return newSelection.copyWith(extentOffset: textSelection.extentOffset); - } - return newSelection; - } - - if (rightKey && newSelection.extentOffset < plainText.length) { - final nextExtent = - _nextCharacter(newSelection.extentOffset, plainText, true); - final distance = nextExtent - newSelection.extentOffset; - newSelection = newSelection.copyWith(extentOffset: nextExtent); - if (shift) { - _cursorResetLocation += distance; - } - return newSelection; - } - - if (leftKey && newSelection.extentOffset > 0) { - final previousExtent = - _previousCharacter(newSelection.extentOffset, plainText, true); - final distance = newSelection.extentOffset - previousExtent; - newSelection = newSelection.copyWith(extentOffset: previousExtent); - if (shift) { - _cursorResetLocation -= distance; - } - return newSelection; - } - return newSelection; - } - - int _nextCharacter(int index, String string, bool includeWhitespace) { - assert(index >= 0 && index <= string.length); - if (index == string.length) { - return string.length; - } - - var count = 0; - final remain = string.characters.skipWhile((currentString) { - if (count <= index) { - count += currentString.length; - return true; - } - if (includeWhitespace) { - return false; - } - return WHITE_SPACE.contains(currentString.codeUnitAt(0)); - }); - return string.length - remain.toString().length; - } - - int _previousCharacter(int index, String string, includeWhitespace) { - assert(index >= 0 && index <= string.length); - if (index == 0) { - return 0; - } - - var count = 0; - int? lastNonWhitespace; - for (final currentString in string.characters) { - if (!includeWhitespace && - !WHITE_SPACE.contains( - currentString.characters.first.toString().codeUnitAt(0))) { - lastNonWhitespace = count; - } - if (count + currentString.length >= index) { - return includeWhitespace ? count : lastNonWhitespace ?? 0; - } - count += currentString.length; - } - return 0; - } - @override TextEditingValue get textEditingValue { return getTextEditingValue(); @@ -654,89 +433,6 @@ class RawEditorState extends EditorState !widget.controller.selection.isCollapsed; } - void handleDelete(bool forward) { - final selection = widget.controller.selection; - final plainText = textEditingValue.text; - var cursorPosition = selection.start; - var textBefore = selection.textBefore(plainText); - var textAfter = selection.textAfter(plainText); - if (selection.isCollapsed) { - if (!forward && textBefore.isNotEmpty) { - final characterBoundary = - _previousCharacter(textBefore.length, textBefore, true); - textBefore = textBefore.substring(0, characterBoundary); - cursorPosition = characterBoundary; - } - if (forward && textAfter.isNotEmpty && textAfter != '\n') { - final deleteCount = _nextCharacter(0, textAfter, true); - textAfter = textAfter.substring(deleteCount); - } - } - final newSelection = TextSelection.collapsed(offset: cursorPosition); - final newText = textBefore + textAfter; - final size = plainText.length - newText.length; - widget.controller.replaceText( - cursorPosition, - size, - '', - newSelection, - ); - } - - Future handleShortcut(InputShortcut? shortcut) async { - final selection = widget.controller.selection; - final plainText = textEditingValue.text; - if (shortcut == InputShortcut.COPY) { - if (!selection.isCollapsed) { - await Clipboard.setData( - ClipboardData(text: selection.textInside(plainText))); - } - return; - } - if (shortcut == InputShortcut.CUT && !widget.readOnly) { - if (!selection.isCollapsed) { - final data = selection.textInside(plainText); - await Clipboard.setData(ClipboardData(text: data)); - - widget.controller.replaceText( - selection.start, - data.length, - '', - TextSelection.collapsed(offset: selection.start), - ); - - textEditingValue = TextEditingValue( - text: - selection.textBefore(plainText) + selection.textAfter(plainText), - selection: TextSelection.collapsed(offset: selection.start), - ); - } - return; - } - if (shortcut == InputShortcut.PASTE && !widget.readOnly) { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { - widget.controller.replaceText( - selection.start, - selection.end - selection.start, - data.text, - TextSelection.collapsed(offset: selection.start + data.text!.length), - ); - } - return; - } - if (shortcut == InputShortcut.SELECT_ALL && - widget.enableInteractiveSelection) { - widget.controller.updateSelection( - selection.copyWith( - baseOffset: 0, - extentOffset: textEditingValue.text.length, - ), - ChangeSource.REMOTE); - return; - } - } - @override void dispose() { closeConnectionIfNeeded(); diff --git a/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart new file mode 100644 index 00000000..0eb7f955 --- /dev/null +++ b/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart @@ -0,0 +1,354 @@ +import 'dart:ui'; + +import 'package:characters/characters.dart'; +import 'package:flutter/services.dart'; + +import '../../models/documents/document.dart'; +import '../../utils/diff_delta.dart'; +import '../editor.dart'; +import '../keyboard_listener.dart'; + +mixin RawEditorStateKeyboardMixin on EditorState { + // Holds the last cursor location the user selected in the case the user tries + // to select vertically past the end or beginning of the field. If they do, + // then we need to keep the old cursor location so that we can go back to it + // if they change their minds. Only used for moving selection up and down in a + // multiline text field when selecting using the keyboard. + int _cursorResetLocation = -1; + + // Whether we should reset the location of the cursor in the case the user + // tries to select vertically past the end or beginning of the field. If they + // do, then we need to keep the old cursor location so that we can go back to + // it if they change their minds. Only used for resetting selection up and + // down in a multiline text field when selecting using the keyboard. + bool _wasSelectingVerticallyWithKeyboard = false; + + void handleCursorMovement( + LogicalKeyboardKey key, + bool wordModifier, + bool lineModifier, + bool shift, + ) { + if (wordModifier && lineModifier) { + // If both modifiers are down, nothing happens on any of the platforms. + return; + } + final selection = widget.controller.selection; + + var newSelection = widget.controller.selection; + + final plainText = getTextEditingValue().text; + + final rightKey = key == LogicalKeyboardKey.arrowRight, + leftKey = key == LogicalKeyboardKey.arrowLeft, + upKey = key == LogicalKeyboardKey.arrowUp, + downKey = key == LogicalKeyboardKey.arrowDown; + + if ((rightKey || leftKey) && !(rightKey && leftKey)) { + newSelection = _jumpToBeginOrEndOfWord(newSelection, wordModifier, + leftKey, rightKey, plainText, lineModifier, shift); + } + + if (downKey || upKey) { + newSelection = _handleMovingCursorVertically( + upKey, downKey, shift, selection, newSelection, plainText); + } + + if (!shift) { + newSelection = + _placeCollapsedSelection(selection, newSelection, leftKey, rightKey); + } + + widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); + } + + // Handles shortcut functionality including cut, copy, paste and select all + // using control/command + (X, C, V, A). + // TODO: Add support for formatting shortcuts: Cmd+B (bold), Cmd+I (italic) + Future handleShortcut(InputShortcut? shortcut) async { + final selection = widget.controller.selection; + final plainText = getTextEditingValue().text; + if (shortcut == InputShortcut.COPY) { + if (!selection.isCollapsed) { + await Clipboard.setData( + ClipboardData(text: selection.textInside(plainText))); + } + return; + } + if (shortcut == InputShortcut.CUT && !widget.readOnly) { + if (!selection.isCollapsed) { + final data = selection.textInside(plainText); + await Clipboard.setData(ClipboardData(text: data)); + + widget.controller.replaceText( + selection.start, + data.length, + '', + TextSelection.collapsed(offset: selection.start), + ); + + setTextEditingValue(TextEditingValue( + text: + selection.textBefore(plainText) + selection.textAfter(plainText), + selection: TextSelection.collapsed(offset: selection.start), + )); + } + return; + } + if (shortcut == InputShortcut.PASTE && !widget.readOnly) { + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null) { + widget.controller.replaceText( + selection.start, + selection.end - selection.start, + data.text, + TextSelection.collapsed(offset: selection.start + data.text!.length), + ); + } + return; + } + if (shortcut == InputShortcut.SELECT_ALL && + widget.enableInteractiveSelection) { + widget.controller.updateSelection( + selection.copyWith( + baseOffset: 0, + extentOffset: getTextEditingValue().text.length, + ), + ChangeSource.REMOTE); + return; + } + } + + void handleDelete(bool forward) { + final selection = widget.controller.selection; + final plainText = getTextEditingValue().text; + var cursorPosition = selection.start; + var textBefore = selection.textBefore(plainText); + var textAfter = selection.textAfter(plainText); + if (selection.isCollapsed) { + if (!forward && textBefore.isNotEmpty) { + final characterBoundary = + _previousCharacter(textBefore.length, textBefore, true); + textBefore = textBefore.substring(0, characterBoundary); + cursorPosition = characterBoundary; + } + if (forward && textAfter.isNotEmpty && textAfter != '\n') { + final deleteCount = _nextCharacter(0, textAfter, true); + textAfter = textAfter.substring(deleteCount); + } + } + final newSelection = TextSelection.collapsed(offset: cursorPosition); + final newText = textBefore + textAfter; + final size = plainText.length - newText.length; + widget.controller.replaceText( + cursorPosition, + size, + '', + newSelection, + ); + } + + TextSelection _jumpToBeginOrEndOfWord( + TextSelection newSelection, + bool wordModifier, + bool leftKey, + bool rightKey, + String plainText, + bool lineModifier, + bool shift) { + if (wordModifier) { + if (leftKey) { + final textSelection = getRenderEditor()!.selectWordAtPosition( + TextPosition( + offset: _previousCharacter( + newSelection.extentOffset, plainText, false))); + return newSelection.copyWith(extentOffset: textSelection.baseOffset); + } + final textSelection = getRenderEditor()!.selectWordAtPosition( + TextPosition( + offset: + _nextCharacter(newSelection.extentOffset, plainText, false))); + return newSelection.copyWith(extentOffset: textSelection.extentOffset); + } else if (lineModifier) { + if (leftKey) { + final textSelection = getRenderEditor()!.selectLineAtPosition( + TextPosition( + offset: _previousCharacter( + newSelection.extentOffset, plainText, false))); + return newSelection.copyWith(extentOffset: textSelection.baseOffset); + } + final startPoint = newSelection.extentOffset; + if (startPoint < plainText.length) { + final textSelection = getRenderEditor()! + .selectLineAtPosition(TextPosition(offset: startPoint)); + return newSelection.copyWith(extentOffset: textSelection.extentOffset); + } + return newSelection; + } + + if (rightKey && newSelection.extentOffset < plainText.length) { + final nextExtent = + _nextCharacter(newSelection.extentOffset, plainText, true); + final distance = nextExtent - newSelection.extentOffset; + newSelection = newSelection.copyWith(extentOffset: nextExtent); + if (shift) { + _cursorResetLocation += distance; + } + return newSelection; + } + + if (leftKey && newSelection.extentOffset > 0) { + final previousExtent = + _previousCharacter(newSelection.extentOffset, plainText, true); + final distance = newSelection.extentOffset - previousExtent; + newSelection = newSelection.copyWith(extentOffset: previousExtent); + if (shift) { + _cursorResetLocation -= distance; + } + return newSelection; + } + return newSelection; + } + + /// Returns the index into the string of the next character boundary after the + /// given index. + /// + /// The character boundary is determined by the characters package, so + /// surrogate pairs and extended grapheme clusters are considered. + /// + /// The index must be between 0 and string.length, inclusive. If given + /// string.length, string.length is returned. + /// + /// Setting includeWhitespace to false will only return the index of non-space + /// characters. + int _nextCharacter(int index, String string, bool includeWhitespace) { + assert(index >= 0 && index <= string.length); + if (index == string.length) { + return string.length; + } + + var count = 0; + final remain = string.characters.skipWhile((currentString) { + if (count <= index) { + count += currentString.length; + return true; + } + if (includeWhitespace) { + return false; + } + return WHITE_SPACE.contains(currentString.codeUnitAt(0)); + }); + return string.length - remain.toString().length; + } + + /// Returns the index into the string of the previous character boundary + /// before the given index. + /// + /// The character boundary is determined by the characters package, so + /// surrogate pairs and extended grapheme clusters are considered. + /// + /// The index must be between 0 and string.length, inclusive. If index is 0, + /// 0 will be returned. + /// + /// Setting includeWhitespace to false will only return the index of non-space + /// characters. + int _previousCharacter(int index, String string, includeWhitespace) { + assert(index >= 0 && index <= string.length); + if (index == 0) { + return 0; + } + + var count = 0; + int? lastNonWhitespace; + for (final currentString in string.characters) { + if (!includeWhitespace && + !WHITE_SPACE.contains( + currentString.characters.first.toString().codeUnitAt(0))) { + lastNonWhitespace = count; + } + if (count + currentString.length >= index) { + return includeWhitespace ? count : lastNonWhitespace ?? 0; + } + count += currentString.length; + } + return 0; + } + + TextSelection _handleMovingCursorVertically( + bool upKey, + bool downKey, + bool shift, + TextSelection selection, + TextSelection newSelection, + String plainText) { + final originPosition = TextPosition( + offset: upKey ? selection.baseOffset : selection.extentOffset); + + final child = getRenderEditor()!.childAtPosition(originPosition); + final localPosition = TextPosition( + offset: originPosition.offset - child.getContainer().documentOffset); + + var position = upKey + ? child.getPositionAbove(localPosition) + : child.getPositionBelow(localPosition); + + if (position == null) { + final sibling = upKey + ? getRenderEditor()!.childBefore(child) + : getRenderEditor()!.childAfter(child); + if (sibling == null) { + position = TextPosition(offset: upKey ? 0 : plainText.length - 1); + } else { + final finalOffset = Offset( + child.getOffsetForCaret(localPosition).dx, + sibling + .getOffsetForCaret(TextPosition( + offset: upKey ? sibling.getContainer().length - 1 : 0)) + .dy); + final siblingPosition = sibling.getPositionForOffset(finalOffset); + position = TextPosition( + offset: + sibling.getContainer().documentOffset + siblingPosition.offset); + } + } else { + position = TextPosition( + offset: child.getContainer().documentOffset + position.offset); + } + + if (position.offset == newSelection.extentOffset) { + if (downKey) { + newSelection = newSelection.copyWith(extentOffset: plainText.length); + } else if (upKey) { + newSelection = newSelection.copyWith(extentOffset: 0); + } + _wasSelectingVerticallyWithKeyboard = shift; + return newSelection; + } + + if (_wasSelectingVerticallyWithKeyboard && shift) { + newSelection = newSelection.copyWith(extentOffset: _cursorResetLocation); + _wasSelectingVerticallyWithKeyboard = false; + return newSelection; + } + newSelection = newSelection.copyWith(extentOffset: position.offset); + _cursorResetLocation = newSelection.extentOffset; + return newSelection; + } + + TextSelection _placeCollapsedSelection(TextSelection selection, + TextSelection newSelection, bool leftKey, bool rightKey) { + var newOffset = newSelection.extentOffset; + if (!selection.isCollapsed) { + if (leftKey) { + newOffset = newSelection.baseOffset < newSelection.extentOffset + ? newSelection.baseOffset + : newSelection.extentOffset; + } else if (rightKey) { + newOffset = newSelection.baseOffset > newSelection.extentOffset + ? newSelection.baseOffset + : newSelection.extentOffset; + } + } + return TextSelection.fromPosition(TextPosition(offset: newOffset)); + } +} diff --git a/lib/widgets/controller/raw_editor_state_text_input_client_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart similarity index 100% rename from lib/widgets/controller/raw_editor_state_text_input_client_mixin.dart rename to lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart From ac68c2373d04aa223efe39b552e30ada81ee10a5 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Thu, 20 May 2021 13:56:46 +0200 Subject: [PATCH 184/306] Improve further SOC of raw editor This improves separation of concerns for the RawEditor by moving the code for the text selection delegate to a separate class, furthermore add more comments. The PR does not change the functionality of the code. --- lib/widgets/raw_editor.dart | 78 ++++++------------- ...editor_state_selection_delegate_mixin.dart | 40 ++++++++++ 2 files changed, 64 insertions(+), 54 deletions(-) create mode 100644 lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index 580dd5b7..cd4f379c 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -23,6 +23,7 @@ import 'editor.dart'; import 'keyboard_listener.dart'; import 'proxy.dart'; import 'raw_editor/raw_editor_state_keyboard_mixin.dart'; +import 'raw_editor/raw_editor_state_selection_delegate_mixin.dart'; import 'raw_editor/raw_editor_state_text_input_client_mixin.dart'; import 'text_block.dart'; import 'text_line.dart'; @@ -90,9 +91,7 @@ class RawEditor extends StatefulWidget { final EmbedBuilder embedBuilder; @override - State createState() { - return RawEditorState(); - } + State createState() => RawEditorState(); } class RawEditorState extends EditorState @@ -101,44 +100,39 @@ class RawEditorState extends EditorState WidgetsBindingObserver, TickerProviderStateMixin, RawEditorStateKeyboardMixin, - RawEditorStateTextInputClientMixin - implements TextSelectionDelegate, TextInputClient { + RawEditorStateTextInputClientMixin, + RawEditorStateSelectionDelegateMixin { final GlobalKey _editorKey = GlobalKey(); - EditorTextSelectionOverlay? _selectionOverlay; - FocusAttachment? _focusAttachment; - late CursorCont _cursorCont; - ScrollController? _scrollController; + + // Keyboard + late KeyboardListener _keyboardListener; KeyboardVisibilityController? _keyboardVisibilityController; StreamSubscription? _keyboardVisibilitySubscription; - late KeyboardListener _keyboardListener; - bool _didAutoFocus = false; bool _keyboardVisible = false; + + // Selection overlay + @override + EditorTextSelectionOverlay? getSelectionOverlay() => _selectionOverlay; + EditorTextSelectionOverlay? _selectionOverlay; + + ScrollController? _scrollController; + + late CursorCont _cursorCont; + + // Focus + bool _didAutoFocus = false; + FocusAttachment? _focusAttachment; + bool get _hasFocus => widget.focusNode.hasFocus; + DefaultStyles? _styles; + final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier(); final LayerLink _toolbarLayerLink = LayerLink(); final LayerLink _startHandleLayerLink = LayerLink(); final LayerLink _endHandleLayerLink = LayerLink(); - bool get _hasFocus => widget.focusNode.hasFocus; - - TextDirection get _textDirection { - final result = Directionality.of(context); - return result; - } - - @override - TextEditingValue get textEditingValue { - return getTextEditingValue(); - } - - @override - set textEditingValue(TextEditingValue value) { - setTextEditingValue(value); - } - - @override - void bringIntoView(TextPosition position) {} + TextDirection get _textDirection => Directionality.of(context); @override Widget build(BuildContext context) { @@ -593,35 +587,11 @@ class RawEditorState extends EditorState return _editorKey.currentContext!.findRenderObject() as RenderEditor?; } - @override - EditorTextSelectionOverlay? getSelectionOverlay() { - return _selectionOverlay; - } - @override TextEditingValue getTextEditingValue() { return widget.controller.plainTextEditingValue; } - @override - void hideToolbar([bool hideHandles = true]) { - if (getSelectionOverlay()?.toolbar != null) { - getSelectionOverlay()?.hideToolbar(); - } - } - - @override - bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; - - @override - bool get copyEnabled => widget.toolbarOptions.copy; - - @override - bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly; - - @override - bool get selectAllEnabled => widget.toolbarOptions.selectAll; - @override void requestKeyboard() { if (_hasFocus) { diff --git a/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart new file mode 100644 index 00000000..cda991cc --- /dev/null +++ b/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart'; + +import '../editor.dart'; + +mixin RawEditorStateSelectionDelegateMixin on EditorState + implements TextSelectionDelegate { + @override + TextEditingValue get textEditingValue { + return getTextEditingValue(); + } + + @override + set textEditingValue(TextEditingValue value) { + setTextEditingValue(value); + } + + @override + void bringIntoView(TextPosition position) { + // TODO: implement bringIntoView + } + + @override + void hideToolbar([bool hideHandles = true]) { + if (getSelectionOverlay()?.toolbar != null) { + getSelectionOverlay()?.hideToolbar(); + } + } + + @override + bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; + + @override + bool get copyEnabled => widget.toolbarOptions.copy; + + @override + bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly; + + @override + bool get selectAllEnabled => widget.toolbarOptions.selectAll; +} From d4e4b0d507140ed0f7745f51a0769c2f4908bef3 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Fri, 21 May 2021 18:23:21 +0200 Subject: [PATCH 185/306] Hide implementation files (#233) --- example/lib/main.dart | 1 - example/lib/pages/home_page.dart | 7 +- example/lib/pages/read_only_page.dart | 3 +- example/lib/universal_ui/universal_ui.dart | 6 +- example/lib/widgets/demo_scaffold.dart | 4 +- lib/flutter_quill.dart | 11 + lib/models/documents/attribute.dart | 295 +--- lib/models/documents/document.dart | 287 +--- lib/models/documents/history.dart | 137 +- lib/models/documents/nodes/block.dart | 75 +- lib/models/documents/nodes/container.dart | 163 +-- lib/models/documents/nodes/embed.dart | 43 +- lib/models/documents/nodes/leaf.dart | 255 +--- lib/models/documents/nodes/line.dart | 374 +---- lib/models/documents/nodes/node.dart | 134 +- lib/models/documents/style.dart | 130 +- lib/models/quill_delta.dart | 687 +-------- lib/models/rules/delete.dart | 127 +- lib/models/rules/format.dart | 135 +- lib/models/rules/insert.dart | 416 +----- lib/models/rules/rule.dart | 80 +- lib/src/models/documents/attribute.dart | 292 ++++ lib/src/models/documents/document.dart | 290 ++++ lib/src/models/documents/history.dart | 134 ++ lib/src/models/documents/nodes/block.dart | 72 + lib/src/models/documents/nodes/container.dart | 160 ++ lib/src/models/documents/nodes/embed.dart | 40 + lib/src/models/documents/nodes/leaf.dart | 252 ++++ lib/src/models/documents/nodes/line.dart | 371 +++++ lib/src/models/documents/nodes/node.dart | 131 ++ lib/src/models/documents/style.dart | 127 ++ lib/src/models/quill_delta.dart | 684 +++++++++ lib/src/models/rules/delete.dart | 124 ++ lib/src/models/rules/format.dart | 132 ++ lib/src/models/rules/insert.dart | 413 ++++++ lib/src/models/rules/rule.dart | 77 + lib/src/utils/color.dart | 125 ++ lib/src/utils/diff_delta.dart | 102 ++ lib/src/widgets/box.dart | 39 + lib/src/widgets/controller.dart | 229 +++ lib/src/widgets/cursor.dart | 231 +++ lib/src/widgets/default_styles.dart | 223 +++ lib/src/widgets/delegate.dart | 148 ++ lib/src/widgets/editor.dart | 1145 +++++++++++++++ lib/src/widgets/image.dart | 31 + lib/src/widgets/keyboard_listener.dart | 105 ++ lib/src/widgets/proxy.dart | 298 ++++ lib/src/widgets/raw_editor.dart | 736 ++++++++++ .../raw_editor_state_keyboard_mixin.dart | 354 +++++ ...editor_state_selection_delegate_mixin.dart | 40 + ..._editor_state_text_input_client_mixin.dart | 200 +++ lib/src/widgets/responsive_widget.dart | 43 + lib/src/widgets/simple_viewer.dart | 344 +++++ lib/src/widgets/text_block.dart | 737 ++++++++++ lib/src/widgets/text_line.dart | 892 ++++++++++++ lib/src/widgets/text_selection.dart | 726 +++++++++ lib/src/widgets/toolbar.dart | 1294 ++++++++++++++++ lib/utils/color.dart | 128 +- lib/utils/diff_delta.dart | 105 +- lib/widgets/box.dart | 42 +- lib/widgets/controller.dart | 231 +-- lib/widgets/cursor.dart | 234 +-- lib/widgets/default_styles.dart | 226 +-- lib/widgets/delegate.dart | 151 +- lib/widgets/editor.dart | 1148 +-------------- lib/widgets/image.dart | 34 +- lib/widgets/keyboard_listener.dart | 108 +- lib/widgets/proxy.dart | 301 +--- lib/widgets/raw_editor.dart | 739 +--------- .../raw_editor_state_keyboard_mixin.dart | 357 +---- ...editor_state_selection_delegate_mixin.dart | 43 +- ..._editor_state_text_input_client_mixin.dart | 203 +-- lib/widgets/responsive_widget.dart | 46 +- lib/widgets/simple_viewer.dart | 347 +---- lib/widgets/text_block.dart | 740 +--------- lib/widgets/text_line.dart | 895 +----------- lib/widgets/text_selection.dart | 729 +-------- lib/widgets/toolbar.dart | 1297 +---------------- 78 files changed, 11466 insertions(+), 11349 deletions(-) create mode 100644 lib/src/models/documents/attribute.dart create mode 100644 lib/src/models/documents/document.dart create mode 100644 lib/src/models/documents/history.dart create mode 100644 lib/src/models/documents/nodes/block.dart create mode 100644 lib/src/models/documents/nodes/container.dart create mode 100644 lib/src/models/documents/nodes/embed.dart create mode 100644 lib/src/models/documents/nodes/leaf.dart create mode 100644 lib/src/models/documents/nodes/line.dart create mode 100644 lib/src/models/documents/nodes/node.dart create mode 100644 lib/src/models/documents/style.dart create mode 100644 lib/src/models/quill_delta.dart create mode 100644 lib/src/models/rules/delete.dart create mode 100644 lib/src/models/rules/format.dart create mode 100644 lib/src/models/rules/insert.dart create mode 100644 lib/src/models/rules/rule.dart create mode 100644 lib/src/utils/color.dart create mode 100644 lib/src/utils/diff_delta.dart create mode 100644 lib/src/widgets/box.dart create mode 100644 lib/src/widgets/controller.dart create mode 100644 lib/src/widgets/cursor.dart create mode 100644 lib/src/widgets/default_styles.dart create mode 100644 lib/src/widgets/delegate.dart create mode 100644 lib/src/widgets/editor.dart create mode 100644 lib/src/widgets/image.dart create mode 100644 lib/src/widgets/keyboard_listener.dart create mode 100644 lib/src/widgets/proxy.dart create mode 100644 lib/src/widgets/raw_editor.dart create mode 100644 lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart create mode 100644 lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart create mode 100644 lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart create mode 100644 lib/src/widgets/responsive_widget.dart create mode 100644 lib/src/widgets/simple_viewer.dart create mode 100644 lib/src/widgets/text_block.dart create mode 100644 lib/src/widgets/text_line.dart create mode 100644 lib/src/widgets/text_selection.dart create mode 100644 lib/src/widgets/toolbar.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index f3ec0666..5b4feb2b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,3 @@ -// import 'package:app/pages/home_page.dart'; import 'package:flutter/material.dart'; import 'pages/home_page.dart'; diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 075f3f9b..d40b4be2 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -5,12 +5,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.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/widgets/controller.dart'; -import 'package:flutter_quill/widgets/default_styles.dart'; -import 'package:flutter_quill/widgets/editor.dart'; -import 'package:flutter_quill/widgets/toolbar.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:tuple/tuple.dart'; diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart index 42957b52..594d6123 100644 --- a/example/lib/pages/read_only_page.dart +++ b/example/lib/pages/read_only_page.dart @@ -1,7 +1,6 @@ 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 'package:flutter_quill/flutter_quill.dart' hide Text; import '../universal_ui/universal_ui.dart'; import '../widgets/demo_scaffold.dart'; diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart index b242af34..d1bdc3f5 100644 --- a/example/lib/universal_ui/universal_ui.dart +++ b/example/lib/universal_ui/universal_ui.dart @@ -2,8 +2,8 @@ library universal_ui; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf; -import 'package:flutter_quill/widgets/responsive_widget.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + import 'package:universal_html/html.dart' as html; import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; @@ -25,7 +25,7 @@ class UniversalUI { var ui = UniversalUI(); -Widget defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) { +Widget defaultEmbedBuilderWeb(BuildContext context, Embed node) { switch (node.value.type) { case 'image': final String imageUrl = node.value.data; diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 4098a5f6..944b7fe9 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -2,9 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_quill/models/documents/document.dart'; -import 'package:flutter_quill/widgets/controller.dart'; -import 'package:flutter_quill/widgets/toolbar.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; typedef DemoContentBuilder = Widget Function( BuildContext context, QuillController? controller); diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index 92e481c7..049786cd 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -1 +1,12 @@ library flutter_quill; + +export 'src/models/documents/attribute.dart'; +export 'src/models/documents/document.dart'; +export 'src/models/documents/nodes/embed.dart'; +export 'src/models/documents/nodes/leaf.dart'; +export 'src/models/quill_delta.dart'; +export 'src/widgets/controller.dart'; +export 'src/widgets/default_styles.dart'; +export 'src/widgets/editor.dart'; +export 'src/widgets/responsive_widget.dart'; +export 'src/widgets/toolbar.dart'; diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 1b9043b9..7411e232 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,292 +1,3 @@ -import 'dart:collection'; - -import 'package:quiver/core.dart'; - -enum AttributeScope { - INLINE, // refer to https://quilljs.com/docs/formats/#inline - BLOCK, // refer to https://quilljs.com/docs/formats/#block - EMBEDS, // refer to https://quilljs.com/docs/formats/#embeds - IGNORE, // attributes that can be ignored -} - -class Attribute { - Attribute(this.key, this.scope, this.value); - - final String key; - final AttributeScope scope; - final T value; - - static final Map _registry = LinkedHashMap.of({ - Attribute.bold.key: Attribute.bold, - Attribute.italic.key: Attribute.italic, - Attribute.underline.key: Attribute.underline, - Attribute.strikeThrough.key: Attribute.strikeThrough, - Attribute.font.key: Attribute.font, - Attribute.size.key: Attribute.size, - Attribute.link.key: Attribute.link, - Attribute.color.key: Attribute.color, - Attribute.background.key: Attribute.background, - Attribute.placeholder.key: Attribute.placeholder, - Attribute.header.key: Attribute.header, - Attribute.align.key: Attribute.align, - Attribute.list.key: Attribute.list, - Attribute.codeBlock.key: Attribute.codeBlock, - Attribute.blockQuote.key: Attribute.blockQuote, - Attribute.indent.key: Attribute.indent, - Attribute.width.key: Attribute.width, - Attribute.height.key: Attribute.height, - Attribute.style.key: Attribute.style, - Attribute.token.key: Attribute.token, - }); - - static final BoldAttribute bold = BoldAttribute(); - - static final ItalicAttribute italic = ItalicAttribute(); - - static final UnderlineAttribute underline = UnderlineAttribute(); - - static final StrikeThroughAttribute strikeThrough = StrikeThroughAttribute(); - - static final FontAttribute font = FontAttribute(null); - - static final SizeAttribute size = SizeAttribute(null); - - static final LinkAttribute link = LinkAttribute(null); - - static final ColorAttribute color = ColorAttribute(null); - - static final BackgroundAttribute background = BackgroundAttribute(null); - - static final PlaceholderAttribute placeholder = PlaceholderAttribute(); - - static final HeaderAttribute header = HeaderAttribute(); - - static final IndentAttribute indent = IndentAttribute(); - - static final AlignAttribute align = AlignAttribute(null); - - static final ListAttribute list = ListAttribute(null); - - static final CodeBlockAttribute codeBlock = CodeBlockAttribute(); - - static final BlockQuoteAttribute blockQuote = BlockQuoteAttribute(); - - static final WidthAttribute width = WidthAttribute(null); - - static final HeightAttribute height = HeightAttribute(null); - - static final StyleAttribute style = StyleAttribute(null); - - static final TokenAttribute token = TokenAttribute(''); - - static final Set inlineKeys = { - Attribute.bold.key, - Attribute.italic.key, - Attribute.underline.key, - Attribute.strikeThrough.key, - Attribute.link.key, - Attribute.color.key, - Attribute.background.key, - Attribute.placeholder.key, - }; - - static final Set blockKeys = LinkedHashSet.of({ - Attribute.header.key, - Attribute.align.key, - Attribute.list.key, - Attribute.codeBlock.key, - Attribute.blockQuote.key, - Attribute.indent.key, - }); - - static final Set blockKeysExceptHeader = LinkedHashSet.of({ - Attribute.list.key, - Attribute.align.key, - Attribute.codeBlock.key, - Attribute.blockQuote.key, - Attribute.indent.key, - }); - - static Attribute get h1 => HeaderAttribute(level: 1); - - static Attribute get h2 => HeaderAttribute(level: 2); - - static Attribute get h3 => HeaderAttribute(level: 3); - - // "attributes":{"align":"left"} - static Attribute get leftAlignment => AlignAttribute('left'); - - // "attributes":{"align":"center"} - static Attribute get centerAlignment => AlignAttribute('center'); - - // "attributes":{"align":"right"} - static Attribute get rightAlignment => AlignAttribute('right'); - - // "attributes":{"align":"justify"} - static Attribute get justifyAlignment => AlignAttribute('justify'); - - // "attributes":{"list":"bullet"} - static Attribute get ul => ListAttribute('bullet'); - - // "attributes":{"list":"ordered"} - static Attribute get ol => ListAttribute('ordered'); - - // "attributes":{"list":"checked"} - static Attribute get checked => ListAttribute('checked'); - - // "attributes":{"list":"unchecked"} - static Attribute get unchecked => ListAttribute('unchecked'); - - // "attributes":{"indent":1"} - static Attribute get indentL1 => IndentAttribute(level: 1); - - // "attributes":{"indent":2"} - static Attribute get indentL2 => IndentAttribute(level: 2); - - // "attributes":{"indent":3"} - static Attribute get indentL3 => IndentAttribute(level: 3); - - static Attribute getIndentLevel(int? level) { - if (level == 1) { - return indentL1; - } - if (level == 2) { - return indentL2; - } - if (level == 3) { - return indentL3; - } - return IndentAttribute(level: level); - } - - bool get isInline => scope == AttributeScope.INLINE; - - bool get isBlockExceptHeader => blockKeysExceptHeader.contains(key); - - Map toJson() => {key: value}; - - static Attribute fromKeyValue(String key, dynamic value) { - if (!_registry.containsKey(key)) { - throw ArgumentError.value(key, 'key "$key" not found.'); - } - final origin = _registry[key]!; - final attribute = clone(origin, value); - return attribute; - } - - static int getRegistryOrder(Attribute attribute) { - var order = 0; - for (final attr in _registry.values) { - if (attr.key == attribute.key) { - break; - } - order++; - } - - return order; - } - - static Attribute clone(Attribute origin, dynamic value) { - return Attribute(origin.key, origin.scope, value); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! Attribute) return false; - final typedOther = other; - return key == typedOther.key && - scope == typedOther.scope && - value == typedOther.value; - } - - @override - int get hashCode => hash3(key, scope, value); - - @override - String toString() { - return 'Attribute{key: $key, scope: $scope, value: $value}'; - } -} - -class BoldAttribute extends Attribute { - BoldAttribute() : super('bold', AttributeScope.INLINE, true); -} - -class ItalicAttribute extends Attribute { - ItalicAttribute() : super('italic', AttributeScope.INLINE, true); -} - -class UnderlineAttribute extends Attribute { - UnderlineAttribute() : super('underline', AttributeScope.INLINE, true); -} - -class StrikeThroughAttribute extends Attribute { - StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true); -} - -class FontAttribute extends Attribute { - FontAttribute(String? val) : super('font', AttributeScope.INLINE, val); -} - -class SizeAttribute extends Attribute { - SizeAttribute(String? val) : super('size', AttributeScope.INLINE, val); -} - -class LinkAttribute extends Attribute { - LinkAttribute(String? val) : super('link', AttributeScope.INLINE, val); -} - -class ColorAttribute extends Attribute { - ColorAttribute(String? val) : super('color', AttributeScope.INLINE, val); -} - -class BackgroundAttribute extends Attribute { - BackgroundAttribute(String? val) - : super('background', AttributeScope.INLINE, val); -} - -/// This is custom attribute for hint -class PlaceholderAttribute extends Attribute { - PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true); -} - -class HeaderAttribute extends Attribute { - HeaderAttribute({int? level}) : super('header', AttributeScope.BLOCK, level); -} - -class IndentAttribute extends Attribute { - IndentAttribute({int? level}) : super('indent', AttributeScope.BLOCK, level); -} - -class AlignAttribute extends Attribute { - AlignAttribute(String? val) : super('align', AttributeScope.BLOCK, val); -} - -class ListAttribute extends Attribute { - ListAttribute(String? val) : super('list', AttributeScope.BLOCK, val); -} - -class CodeBlockAttribute extends Attribute { - CodeBlockAttribute() : super('code-block', AttributeScope.BLOCK, true); -} - -class BlockQuoteAttribute extends Attribute { - BlockQuoteAttribute() : super('blockquote', AttributeScope.BLOCK, true); -} - -class WidthAttribute extends Attribute { - WidthAttribute(String? val) : super('width', AttributeScope.IGNORE, val); -} - -class HeightAttribute extends Attribute { - HeightAttribute(String? val) : super('height', AttributeScope.IGNORE, val); -} - -class StyleAttribute extends Attribute { - StyleAttribute(String? val) : super('style', AttributeScope.IGNORE, val); -} - -class TokenAttribute extends Attribute { - TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val); -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/attribute.dart'; diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index 9d966f32..d946618a 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -1,284 +1,3 @@ -import 'dart:async'; - -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(); - - Root get root => _root; - - int get length => _root.length; - - Delta _delta; - - Delta toDelta() => Delta.from(_delta); - - final Rules _rules = Rules.getInstance(); - - void setCustomRules(List customRules) { - _rules.setCustomRules(customRules); - } - - final StreamController> _observer = - StreamController.broadcast(); - - final History _history = History(); - - Stream> get changes => _observer.stream; - - Delta insert(int index, Object? data, {int replaceLength = 0, bool autoAppendNewlineAfterImage = true}) { - assert(index >= 0); - assert(data is String || data is Embeddable); - if (data is Embeddable) { - data = data.toJson(); - } else if ((data as String).isEmpty) { - return Delta(); - } - - final delta = _rules.apply(RuleType.INSERT, this, index, - data: data, len: replaceLength); - compose(delta, ChangeSource.LOCAL, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); - return delta; - } - - Delta delete(int index, int len) { - assert(index >= 0 && len > 0); - final delta = _rules.apply(RuleType.DELETE, this, index, len: len); - if (delta.isNotEmpty) { - compose(delta, ChangeSource.LOCAL); - } - return delta; - } - - Delta replace(int index, int len, Object? data, {bool autoAppendNewlineAfterImage = true}) { - assert(index >= 0); - assert(data is String || data is Embeddable); - - final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true; - - assert(dataIsNotEmpty || len > 0); - - var delta = Delta(); - - // We have to insert before applying delete rules - // Otherwise delete would be operating on stale document snapshot. - if (dataIsNotEmpty) { - delta = insert(index, data, replaceLength: len, - autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); - } - - if (len > 0) { - final deleteDelta = delete(index, len); - delta = delta.compose(deleteDelta); - } - - return delta; - } - - Delta format(int index, int len, Attribute? attribute) { - assert(index >= 0 && len >= 0 && attribute != null); - - var delta = Delta(); - - final formatDelta = _rules.apply(RuleType.FORMAT, this, index, - len: len, attribute: attribute); - if (formatDelta.isNotEmpty) { - compose(formatDelta, ChangeSource.LOCAL); - delta = delta.compose(formatDelta); - } - - return delta; - } - - Style collectStyle(int index, int len) { - final res = queryChild(index); - return (res.node as Line).collectStyle(res.offset, len); - } - - ChildQuery queryChild(int offset) { - final res = _root.queryChild(offset, true); - if (res.node is Line) { - return res; - } - final block = res.node as Block; - return block.queryChild(res.offset, true); - } - - void compose(Delta delta, ChangeSource changeSource, {bool autoAppendNewlineAfterImage = true}) { - assert(!_observer.isClosed); - delta.trim(); - assert(delta.isNotEmpty); - - var offset = 0; - delta = _transform(delta, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); - final originalDelta = toDelta(); - for (final op in delta.toList()) { - final style = - op.attributes != null ? Style.fromJson(op.attributes) : null; - - if (op.isInsert) { - _root.insert(offset, _normalize(op.data), style); - } else if (op.isDelete) { - _root.delete(offset, op.length); - } else if (op.attributes != null) { - _root.retain(offset, op.length, style); - } - - if (!op.isDelete) { - offset += op.length!; - } - } - try { - _delta = _delta.compose(delta); - } catch (e) { - throw '_delta compose failed'; - } - - if (_delta != _root.toDelta()) { - throw 'Compose failed'; - } - final change = Tuple3(originalDelta, delta, changeSource); - _observer.add(change); - _history.handleDocChange(change); - } - - Tuple2 undo() { - return _history.undo(this); - } - - Tuple2 redo() { - return _history.redo(this); - } - - bool get hasUndo => _history.hasUndo; - - bool get hasRedo => _history.hasRedo; - - static Delta _transform(Delta delta, {bool autoAppendNewlineAfterImage = true}) { - final res = Delta(); - final ops = delta.toList(); - for (var i = 0; i < ops.length; i++) { - final op = ops[i]; - res.push(op); - if (autoAppendNewlineAfterImage) { - _autoAppendNewlineAfterImage(i, ops, op, res); - } - } - return res; - } - - static void _autoAppendNewlineAfterImage( - int i, List ops, Operation op, Delta res) { - final nextOpIsImage = - i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String; - if (nextOpIsImage && - op.data is String && - (op.data as String).isNotEmpty && - !(op.data as String).endsWith('\n')) - { - res.push(Operation.insert('\n')); - } - // Currently embed is equivalent to image and hence `is! String` - 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'); - if (opInsertImage && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) { - // automatically append '\n' for image - res.push(Operation.insert('\n')); - } - } - - Object _normalize(Object? data) { - if (data is String) { - return data; - } - - if (data is Embeddable) { - return data; - } - return Embeddable.fromJson(data as Map); - } - - void close() { - _observer.close(); - _history.clear(); - } - - String toPlainText() => _root.children.map((e) => e.toPlainText()).join(); - - void _loadDocument(Delta doc) { - if (doc.isEmpty) { - throw ArgumentError.value(doc, 'Document Delta cannot be empty.'); - } - - assert((doc.last.data as String).endsWith('\n')); - - var offset = 0; - for (final op in doc.toList()) { - if (!op.isInsert) { - throw ArgumentError.value(doc, - 'Document Delta can only contain insert operations but ${op.key} found.'); - } - final style = - op.attributes != null ? Style.fromJson(op.attributes) : null; - final data = _normalize(op.data); - _root.insert(offset, data, style); - offset += op.length!; - } - final node = _root.last; - if (node is Line && - node.parent is! Block && - node.style.isEmpty && - _root.childCount > 1) { - _root.remove(node); - } - } - - bool isEmpty() { - if (root.children.length != 1) { - return false; - } - - final node = root.children.first; - if (!node.isLast) { - return false; - } - - final delta = node.toDelta(); - return delta.length == 1 && - delta.first.data == '\n' && - delta.first.key == 'insert'; - } -} - -enum ChangeSource { - LOCAL, - REMOTE, -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/document.dart'; diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index d406505e..3ff6870e 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -1,134 +1,3 @@ -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; - - bool get hasRedo => stack.redo.isNotEmpty; - - /// used for disable redo or undo function - bool ignoreChange; - - int lastRecorded; - - /// Collaborative editing's conditions should be true - final bool userOnly; - - ///max operation count for undo - final int maxStack; - - ///record delay - final int interval; - - void handleDocChange(Tuple3 change) { - if (ignoreChange) return; - if (!userOnly || change.item3 == ChangeSource.LOCAL) { - record(change.item2, change.item1); - } else { - transform(change.item2); - } - } - - void clear() { - stack.clear(); - } - - void record(Delta change, Delta before) { - if (change.isEmpty) return; - stack.redo.clear(); - var undoDelta = change.invert(before); - final timeStamp = DateTime.now().millisecondsSinceEpoch; - - if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) { - final lastDelta = stack.undo.removeLast(); - undoDelta = undoDelta.compose(lastDelta); - } else { - lastRecorded = timeStamp; - } - - if (undoDelta.isEmpty) return; - stack.undo.add(undoDelta); - - if (stack.undo.length > maxStack) { - stack.undo.removeAt(0); - } - } - - /// - ///It will override pre local undo delta,replaced by remote change - /// - void transform(Delta delta) { - transformStack(stack.undo, delta); - transformStack(stack.redo, delta); - } - - void transformStack(List stack, Delta delta) { - 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); - if (stack[i].length == 0) { - stack.removeAt(i); - } - } - } - - Tuple2 _change(Document doc, List source, List dest) { - if (source.isEmpty) { - return const Tuple2(false, 0); - } - final delta = source.removeLast(); - // look for insert or delete - int? len = 0; - final ops = delta.toList(); - for (var i = 0; i < ops.length; i++) { - if (ops[i].key == Operation.insertKey) { - len = ops[i].length; - } else if (ops[i].key == Operation.deleteKey) { - len = ops[i].length! * -1; - } - } - final base = Delta.from(doc.toDelta()); - final inverseDelta = delta.invert(base); - dest.add(inverseDelta); - lastRecorded = 0; - ignoreChange = true; - doc.compose(delta, ChangeSource.LOCAL); - ignoreChange = false; - return Tuple2(true, len); - } - - Tuple2 undo(Document doc) { - return _change(doc, stack.undo, stack.redo); - } - - Tuple2 redo(Document doc) { - return _change(doc, stack.redo, stack.undo); - } -} - -class HistoryStack { - HistoryStack.empty() - : undo = [], - redo = []; - - final List undo; - final List redo; - - void clear() { - undo.clear(); - redo.clear(); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/history.dart'; diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index 095f1183..6de6f743 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -1,72 +1,3 @@ -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(); - - @override - Delta toDelta() { - return children - .map((child) => child.toDelta()) - .fold(Delta(), (a, b) => a.concat(b)); - } - - @override - void adjust() { - if (isEmpty) { - final sibling = previous; - unlink(); - if (sibling != null) { - sibling.adjust(); - } - return; - } - - 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?) - ..unlink(); - block = prev as Block; - } - 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); - next.unlink(); - } - } - - @override - String toString() { - final block = style.attributes.toString(); - final buffer = StringBuffer('§ {$block}\n'); - for (final child in children) { - final tree = child.isLast ? '└' : '├'; - buffer.write(' $tree $child'); - if (!child.isLast) buffer.writeln(); - } - return buffer.toString(); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/nodes/block.dart'; diff --git a/lib/models/documents/nodes/container.dart b/lib/models/documents/nodes/container.dart index dbdd12d1..d9a54451 100644 --- a/lib/models/documents/nodes/container.dart +++ b/lib/models/documents/nodes/container.dart @@ -1,160 +1,3 @@ -import 'dart:collection'; - -import '../style.dart'; -import 'leaf.dart'; -import 'line.dart'; -import 'node.dart'; - -/// 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; - - /// Returns an instance of default child for this container node. - /// - /// Always returns fresh instance. - T get defaultChild; - - /// 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; - } - - final last = newParent!.isEmpty ? null : newParent.last as T?; - while (isNotEmpty) { - 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 (final node in children) { - final len = node.length; - if (offset < len || (inclusive && offset == len && (node.isLast))) { - return ChildQuery(node, offset); - } - offset -= len; - } - return ChildQuery(null, 0); - } - - @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); - - @override - void insert(int index, Object data, Style? style) { - assert(index == 0 || (index > 0 && index < length)); - - if (isNotEmpty) { - final child = queryChild(index, false); - child.node!.insert(child.offset, data, style); - return; - } - - // empty - assert(index == 0); - final node = defaultChild; - add(node); - node?.insert(index, data, style); - } - - @override - void retain(int index, int? length, Style? attributes) { - assert(isNotEmpty); - final child = queryChild(index, false); - child.node!.retain(child.offset, length, attributes); - } - - @override - void delete(int index, int? length) { - assert(isNotEmpty); - final child = queryChild(index, false); - child.node!.delete(child.offset, length); - } - - @override - String toString() => _children.join('\n'); -} - -/// Result of a child query in a [Container]. -class ChildQuery { - ChildQuery(this.node, this.offset); - - /// The child node if found, otherwise `null`. - final Node? node; - - /// 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; -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/nodes/container.dart'; diff --git a/lib/models/documents/nodes/embed.dart b/lib/models/documents/nodes/embed.dart index d6fe628a..01dc357b 100644 --- a/lib/models/documents/nodes/embed.dart +++ b/lib/models/documents/nodes/embed.dart @@ -1,40 +1,3 @@ -/// 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; - - /// The data payload of this object. - final dynamic data; - - Map toJson() { - final m = {type: data}; - return m; - } - - static Embeddable fromJson(Map 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 BlockEmbed horizontalRule = BlockEmbed('divider', 'hr'); - - static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl); -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/nodes/embed.dart'; diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index bd9292f5..cc2808f2 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -1,252 +1,3 @@ -import 'dart:math' as math; - -import '../../quill_delta.dart'; -import '../style.dart'; -import 'embed.dart'; -import 'line.dart'; -import 'node.dart'; - -/// A leaf in Quill document tree. -abstract class Leaf extends Node { - /// Creates a new [Leaf] with specified [data]. - factory Leaf(Object data) { - if (data is Embeddable) { - return Embed(data); - } - 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, - 'Unable to apply Style to leaf: $value'); - super.applyStyle(value); - } - - @override - Line? get parent => super.parent as Line?; - - @override - int get length { - if (_value is String) { - return (_value as String).length; - } - // return 1 for embedded object - return 1; - } - - @override - Delta toDelta() { - 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); - final node = Leaf(data); - if (index < length) { - splitAt(index)!.insertBefore(node); - } else { - insertAfter(node); - } - node.format(style); - } - - @override - void retain(int index, int? len, Style? style) { - if (style == null) { - return; - } - - final local = math.min(length - index, len!); - final remain = len - local; - final node = _isolate(index, local); - - if (remain > 0) { - assert(node.next != null); - node.next!.retain(0, remain, style); - } - node.format(style); - } - - @override - void delete(int index, int? len) { - assert(index < length); - - 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(); - - final remain = len - local; - if (remain > 0) { - assert(next != null); - next!.delete(0, remain); - } - - if (prev != null) { - prev.adjust(); - } - } - - /// 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; - } - - // 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. - final next = node.next; - if (!node.isLast && next is Text && next.style == node.style) { - node._value = node.value + next.value; - next.unlink(); - } - } - - /// 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) { - return this; - } - if (index == length) { - return isLast ? null : next as Leaf?; - } - - assert(this is Text); - final text = _value as String; - _value = text.substring(0, index); - 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)); - 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 - Node newInstance() => Text(); - - @override - String get value => _value as String; - - @override - String toPlainText() => value; -} - -/// 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 - Node newInstance() => throw UnimplementedError(); - - @override - Embeddable get value => super.value as Embeddable; - - /// // Embed nodes are represented as unicode object replacement character in - // plain text. - @override - String toPlainText() => kObjectReplacementCharacter; -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/nodes/leaf.dart'; diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index fabfad4d..7ca2016e 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -1,371 +1,3 @@ -import 'dart:math' as math; - -import 'package:collection/collection.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(); - - @override - int get length => super.length + 1; - - /// Returns `true` if this line contains an embedded object. - bool get hasEmbed { - if (childCount != 1) { - return false; - } - - 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?; - } - if (parent is! Block) { - return null; - } - - if (parent!.isLast) { - return null; - } - return parent!.next is Block - ? (parent!.next as Block).first as Line? - : parent!.next as Line?; - } - - @override - Node newInstance() => Line(); - - @override - Delta toDelta() { - final delta = children - .map((child) => child.toDelta()) - .fold(Delta(), (dynamic a, b) => a.concat(b)); - var attributes = style; - if (parent is Block) { - final block = parent as Block; - attributes = attributes.mergeAll(block.style); - } - delta.insert('\n', attributes.toJson()); - return delta; - } - - @override - String toPlainText() => '${super.toPlainText()}\n'; - - @override - String toString() { - final body = children.join(' → '); - final styleString = style.isNotEmpty ? ' $style' : ''; - return '¶ $body ⏎$styleString'; - } - - @override - void insert(int index, Object data, Style? style) { - if (data is Embeddable) { - // 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; - } - - final text = data as String; - final lineBreak = text.indexOf('\n'); - if (lineBreak < 0) { - _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; - } - - final prefix = text.substring(0, lineBreak); - _insertSafe(index, prefix, style); - if (prefix.isNotEmpty) { - index += prefix.length; - } - - // 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 remaining part. - final remain = text.substring(lineBreak + 1); - nextLine.insert(0, remain, style); - } - - @override - void retain(int index, int? len, Style? style) { - if (style == null) { - return; - } - final thisLength = length; - - 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 (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 != thisLength); - super.retain(index, local, style); - } - - final remain = len - local; - if (remain > 0) { - assert(nextLine != null); - nextLine!.retain(0, remain, style); - } - } - - @override - void delete(int index, int? len) { - 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); - } - - final remaining = len - local; - if (remaining > 0) { - assert(nextLine != null); - nextLine!.delete(0, remaining); - } - - 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 (isLFDeleted) { - // Now we can remove this line. - final block = parent!; // remember reference before un-linking. - unlink(); - block.adjust(); - } - } - - /// Formats this line. - void _format(Style? newStyle) { - if (newStyle == null || newStyle.isEmpty) { - return; - } - - applyStyle(newStyle); - final blockStyle = newStyle.getBlockExceptHeader(); - if (blockStyle == null) { - return; - } // No block-level changes - - if (parent is Block) { - final parentStyle = (parent as Block).style.getBlocksExceptHeader(); - if (blockStyle.value == null) { - _unwrap(); - } else if (!const MapEquality() - .equals(newStyle.getBlocksExceptHeader(), parentStyle)) { - _unwrap(); - _applyBlockStyles(newStyle); - } // else the same style, no-op. - } else if (blockStyle.value != null) { - // Only wrap with a new block if this is not an unset - _applyBlockStyles(newStyle); - } - } - - void _applyBlockStyles(Style newStyle) { - var block = Block(); - for (final style in newStyle.getBlocksExceptHeader().values) { - block = block..applyAttribute(style); - } - _wrap(block); - block.adjust(); - } - - /// Wraps this line with new parent [block]. - /// - /// This line can not be in a [Block] when this method is called. - void _wrap(Block block) { - assert(parent != null && parent is! Block); - insertAfter(block); - unlink(); - 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'); - } - final block = parent as Block; - - assert(block.children.contains(this)); - - if (isFirst) { - unlink(); - block.insertBefore(this); - } else if (isLast) { - unlink(); - block.insertAfter(this); - } else { - final before = block.clone() as Block; - block.insertBefore(before); - - var child = block.first as Line; - while (child != this) { - child.unlink(); - before.add(child); - child = block.first as Line; - } - unlink(); - block.insertBefore(this); - } - block.adjust(); - } - - Line _getNextLine(int index) { - assert(index == 0 || (index > 0 && index < length)); - - final line = clone() as Line; - insertAfter(line); - if (index == length - 1) { - return line; - } - - final query = queryChild(index, false); - while (!query.node!.isLast) { - final next = (last as Leaf)..unlink(); - line.addFirst(next); - } - final child = query.node as Leaf; - final cut = child.splitAt(query.offset); - cut?.unlink(); - line.addFirst(cut); - return line; - } - - void _insertSafe(int index, Object data, Style? style) { - assert(index == 0 || (index > 0 && index < length)); - - if (data is String) { - assert(!data.contains('\n')); - if (data.isEmpty) { - return; - } - } - - if (isEmpty) { - final child = Leaf(data); - add(child); - child.format(style); - } else { - final result = queryChild(index, true); - result.node!.insert(result.offset, data, style); - } - } - - /// 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) { - final local = math.min(length - offset, len); - var result = Style(); - final excluded = {}; - - void _handle(Style style) { - if (result.isEmpty) { - excluded.addAll(style.values); - } else { - for (final attr in result.values) { - if (!style.containsKey(attr.key)) { - excluded.add(attr); - } - } - } - final remaining = style.removeAll(excluded); - result = result.removeAll(excluded); - result = result.mergeAll(remaining); - } - - final data = queryChild(offset, true); - var node = data.node as Leaf?; - if (node != null) { - result = result.mergeAll(node.style); - var pos = node.length - data.offset; - while (!node!.isLast && pos < local) { - node = node.next as Leaf?; - _handle(node!.style); - pos += node.length; - } - } - - result = result.mergeAll(style); - if (parent is Block) { - final block = parent as Block; - result = result.mergeAll(block.style); - } - - final remaining = len - local; - if (remaining > 0) { - final rest = nextLine!.collectStyle(0, remaining); - _handle(rest); - } - - return result; - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/nodes/line.dart'; diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index 6bb0fb97..210c1672 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -1,131 +1,3 @@ -import 'dart:collection'; - -import '../../quill_delta.dart'; -import '../attribute.dart'; -import '../style.dart'; -import 'container.dart'; -import 'line.dart'; - -/// 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 get style => _style; - Style _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() => newInstance()..applyStyle(style); - - /// 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; - } - - var cur = this; - do { - cur = cur.previous!; - offset += cur.length; - } while (!cur.isFirst); - return offset; - } - - /// 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 = 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); - entry.parent = parent; - super.insertBefore(entry); - } - - @override - void insertAfter(Node entry) { - assert(entry.parent == null && parent != null); - entry.parent = parent; - super.insertAfter(entry); - } - - @override - void unlink() { - assert(parent != null); - parent = null; - super.unlink(); - } - - void adjust() {/* no-op */} - - /// abstract methods begin - - Node newInstance(); - - String toPlainText(); - - Delta toDelta(); - - void insert(int index, Object data, Style? style); - - void retain(int index, int? len, Style? style); - - void delete(int index, int? len); - - /// abstract methods end -} - -/// Root node of document tree. -class Root extends Container> { - @override - Node newInstance() => Root(); - - @override - Container get defaultChild => Line(); - - @override - Delta toDelta() => children - .map((child) => child.toDelta()) - .fold(Delta(), (a, b) => a.concat(b)); -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/nodes/node.dart'; diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index fade1bb5..a4e06de4 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -1,127 +1,3 @@ -import 'package:collection/collection.dart'; -import 'package:quiver/core.dart'; - -import 'attribute.dart'; - -/* Collection of style attributes */ -class Style { - Style() : _attributes = {}; - - Style.attr(this._attributes); - - final Map _attributes; - - static Style fromJson(Map? attributes) { - if (attributes == null) { - return Style(); - } - - final result = attributes.map((key, dynamic value) { - final attr = Attribute.fromKeyValue(key, value); - return MapEntry(key, attr); - }); - return Style.attr(result); - } - - Map? toJson() => _attributes.isEmpty - ? null - : _attributes.map((_, attribute) => - MapEntry(attribute.key, attribute.value)); - - Iterable get keys => _attributes.keys; - - Iterable get values => _attributes.values.sorted( - (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); - - Map get attributes => _attributes; - - bool get isEmpty => _attributes.isEmpty; - - bool get isNotEmpty => _attributes.isNotEmpty; - - bool get isInline => isNotEmpty && values.every((item) => item.isInline); - - bool get isIgnored => - isNotEmpty && values.every((item) => item.scope == AttributeScope.IGNORE); - - Attribute get single => _attributes.values.single; - - bool containsKey(String key) => _attributes.containsKey(key); - - Attribute? getBlockExceptHeader() { - for (final val in values) { - if (val.isBlockExceptHeader && val.value != null) { - return val; - } - } - for (final val in values) { - if (val.isBlockExceptHeader) { - return val; - } - } - return null; - } - - Map getBlocksExceptHeader() { - final m = {}; - attributes.forEach((key, value) { - if (Attribute.blockKeysExceptHeader.contains(key)) { - m[key] = value; - } - }); - return m; - } - - Style merge(Attribute attribute) { - final merged = Map.from(_attributes); - if (attribute.value == null) { - merged.remove(attribute.key); - } else { - merged[attribute.key] = attribute; - } - return Style.attr(merged); - } - - Style mergeAll(Style other) { - var result = Style.attr(_attributes); - for (final attribute in other.values) { - result = result.merge(attribute); - } - return result; - } - - Style removeAll(Set attributes) { - final merged = Map.from(_attributes); - attributes.map((item) => item.key).forEach(merged.remove); - return Style.attr(merged); - } - - Style put(Attribute attribute) { - final m = Map.from(attributes); - m[attribute.key] = attribute; - return Style.attr(m); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other is! Style) { - return false; - } - final typedOther = other; - const eq = MapEquality(); - return eq.equals(_attributes, typedOther._attributes); - } - - @override - int get hashCode { - final hashes = - _attributes.entries.map((entry) => hash2(entry.key, entry.value)); - return hashObjects(hashes); - } - - @override - String toString() => "{${_attributes.values.join(', ')}}"; -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/documents/style.dart'; diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index a0e608be..477fbe33 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -1,684 +1,3 @@ -// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code -// is governed by a BSD-style license that can be found in the LICENSE file. - -/// Implementation of Quill Delta format in Dart. -library quill_delta; - -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:quiver/core.dart'; - -const _attributeEquality = DeepCollectionEquality(); -const _valueEquality = DeepCollectionEquality(); - -/// Decoder function to convert raw `data` object into a user-defined data type. -/// -/// Useful with embedded content. -typedef DataDecoder = Object? Function(Object data); - -/// Default data decoder which simply passes through the original value. -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'; - - /// Key of delete operations. - static const String deleteKey = 'delete'; - - /// Key of retain operations. - static const String retainKey = 'retain'; - - /// Key of attributes collection. - static const String attributesKey = 'attributes'; - - static const List _validKeys = [insertKey, deleteKey, retainKey]; - - /// Key of this operation, can be "insert", "delete" or "retain". - final String key; - - /// Length of this operation. - final int? length; - - /// Payload of "insert" operation, for other types is set to empty string. - final Object? data; - - /// Rich-text attributes set by this operation, can be `null`. - Map? get attributes => - _attributes == null ? null : Map.from(_attributes!); - final Map? _attributes; - - /// Creates new [Operation] from JSON payload. - /// - /// If `dataDecoder` parameter is not null then it is used to additionally - /// decode the operation's data object. Only applied to insert operations. - static Operation fromJson(Map data, {DataDecoder? dataDecoder}) { - dataDecoder ??= _passThroughDataDecoder; - final map = Map.from(data); - if (map.containsKey(Operation.insertKey)) { - final data = dataDecoder(map[Operation.insertKey]); - final dataLength = data is String ? data.length : 1; - return Operation._( - Operation.insertKey, dataLength, data, map[Operation.attributesKey]); - } else if (map.containsKey(Operation.deleteKey)) { - final int? length = map[Operation.deleteKey]; - return Operation._(Operation.deleteKey, length, '', null); - } else if (map.containsKey(Operation.retainKey)) { - final int? length = map[Operation.retainKey]; - return Operation._( - Operation.retainKey, length, '', map[Operation.attributesKey]); - } - throw ArgumentError.value(data, 'Invalid data for Delta operation.'); - } - - /// Returns JSON-serializable representation of this operation. - Map toJson() { - final json = {key: value}; - if (_attributes != null) json[Operation.attributesKey] = attributes; - return json; - } - - /// Returns value of this operation. - /// - /// For insert operations this returns text, for delete and retain - length. - dynamic get value => (key == Operation.insertKey) ? data : length; - - /// Returns `true` if this is a delete operation. - bool get isDelete => key == Operation.deleteKey; - - /// Returns `true` if this is an insert operation. - bool get isInsert => key == Operation.insertKey; - - /// Returns `true` if this is a retain operation. - bool get isRetain => key == Operation.retainKey; - - /// Returns `true` if this operation has no attributes, e.g. is plain text. - bool get isPlain => _attributes == null || _attributes!.isEmpty; - - /// Returns `true` if this operation sets at least one attribute. - bool get isNotPlain => !isPlain; - - /// Returns `true` is this operation is empty. - /// - /// An operation is considered empty if its [length] is equal to `0`. - bool get isEmpty => length == 0; - - /// Returns `true` is this operation is not empty. - bool get isNotEmpty => length! > 0; - - @override - bool operator ==(other) { - if (identical(this, other)) return true; - if (other is! Operation) return false; - final typedOther = other; - return key == typedOther.key && - length == typedOther.length && - _valueEquality.equals(data, typedOther.data) && - hasSameAttributes(typedOther); - } - - /// Returns `true` if this operation has attribute specified by [name]. - bool hasAttribute(String name) => - isNotPlain && _attributes!.containsKey(name); - - /// Returns `true` if [other] operation has the same attributes as this one. - bool hasSameAttributes(Operation other) { - return _attributeEquality.equals(_attributes, other._attributes); - } - - @override - int get hashCode { - if (_attributes != null && _attributes!.isNotEmpty) { - final attrsHash = - hashObjects(_attributes!.entries.map((e) => hash2(e.key, e.value))); - return hash3(key, value, attrsHash); - } - return hash2(key, value); - } - - @override - String toString() { - final attr = attributes == null ? '' : ' + $attributes'; - final text = isInsert - ? (data is String - ? (data as String).replaceAll('\n', '⏎') - : data.toString()) - : '$length'; - return '$key⟨ $text ⟩$attr'; - } -} - -/// Delta represents a document or a modification of a document as a sequence of -/// insert, delete and retain operations. -/// -/// Delta consisting of only "insert" operations is usually referred to as -/// "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) { - if (a == null) return b; - if (b == null) return null; - - if (!priority) return b; - - final result = b.keys.fold>({}, (attributes, key) { - if (!a.containsKey(key)) attributes[key] = b[key]; - return attributes; - }); - - return result.isEmpty ? null : result; - } - - /// Composes two attribute sets. - static Map? composeAttributes( - Map? a, Map? b, - {bool keepNull = false}) { - a ??= const {}; - b ??= const {}; - - final result = Map.from(a)..addAll(b); - final keys = result.keys.toList(growable: false); - - if (!keepNull) { - for (final key in keys) { - if (result[key] == null) result.remove(key); - } - } - - return result.isEmpty ? null : result; - } - - ///get anti-attr result base on base - static Map invertAttributes( - Map? attr, Map? base) { - attr ??= const {}; - base ??= const {}; - - final baseInverted = base.keys.fold({}, (dynamic memo, key) { - if (base![key] != attr![key] && attr.containsKey(key)) { - memo[key] = base[key]; - } - return memo; - }); - - final inverted = - Map.from(attr.keys.fold(baseInverted, (memo, key) { - if (base![key] != attr![key] && !base.containsKey(key)) { - memo[key] = null; - } - return memo; - })); - return inverted; - } - - final List _operations; - - int _modificationCount = 0; - - /// Creates [Delta] from de-serialized JSON representation. - /// - /// If `dataDecoder` parameter is not null then it is used to additionally - /// decode the operation's data object. Only applied to insert operations. - static Delta fromJson(List data, {DataDecoder? dataDecoder}) { - return Delta._(data - .map((op) => Operation.fromJson(op, dataDecoder: dataDecoder)) - .toList()); - } - - /// Returns list of operations in this delta. - List toList() => List.from(_operations); - - /// Returns JSON-serializable version of this delta. - List toJson() => toList().map((operation) => operation.toJson()).toList(); - - /// Returns `true` if this delta is empty. - bool get isEmpty => _operations.isEmpty; - - /// Returns `true` if this delta is not empty. - bool get isNotEmpty => _operations.isNotEmpty; - - /// Returns number of operations in this delta. - int get length => _operations.length; - - /// Returns [Operation] at specified [index] in this delta. - Operation operator [](int index) => _operations[index]; - - /// Returns [Operation] at specified [index] in this delta. - Operation elementAt(int index) => _operations.elementAt(index); - - /// Returns the first [Operation] in this delta. - Operation get first => _operations.first; - - /// Returns the last [Operation] in this delta. - Operation get last => _operations.last; - - @override - bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! Delta) return false; - final typedOther = other; - const comparator = ListEquality(DefaultEquality()); - return comparator.equals(_operations, typedOther._operations); - } - - @override - int get hashCode => hashObjects(_operations); - - /// Retain [count] of characters from current position. - void retain(int count, [Map? attributes]) { - assert(count >= 0); - if (count == 0) return; // no-op - push(Operation.retain(count, attributes)); - } - - /// Insert [data] at current position. - void insert(dynamic data, [Map? attributes]) { - if (data is String && data.isEmpty) return; // no-op - push(Operation.insert(data, attributes)); - } - - /// Delete [count] characters from current position. - void delete(int count) { - assert(count >= 0); - if (count == 0) return; - push(Operation.delete(count)); - } - - void _mergeWithTail(Operation operation) { - assert(isNotEmpty); - assert(last.key == operation.key); - assert(operation.data is String && last.data is String); - - final length = operation.length! + last.length!; - final lastText = last.data as String; - final opText = operation.data as String; - final resultText = lastText + opText; - final index = _operations.length; - _operations.replaceRange(index - 1, index, [ - Operation._(operation.key, length, resultText, operation.attributes), - ]); - } - - /// Pushes new operation into this delta. - /// - /// Performs compaction by composing [operation] with current tail operation - /// of this delta, when possible. For instance, if current tail is - /// `insert('abc')` and pushed operation is `insert('123')` then existing - /// tail is replaced with `insert('abc123')` - a compound result of the two - /// operations. - void push(Operation operation) { - if (operation.isEmpty) return; - - var index = _operations.length; - final lastOp = _operations.isNotEmpty ? _operations.last : null; - if (lastOp != null) { - if (lastOp.isDelete && operation.isDelete) { - _mergeWithTail(operation); - return; - } - - if (lastOp.isDelete && operation.isInsert) { - index -= 1; // Always insert before deleting - final nLastOp = (index > 0) ? _operations.elementAt(index - 1) : null; - if (nLastOp == null) { - _operations.insert(0, operation); - return; - } - } - - if (lastOp.isInsert && operation.isInsert) { - if (lastOp.hasSameAttributes(operation) && - operation.data is String && - lastOp.data is String) { - _mergeWithTail(operation); - return; - } - } - - if (lastOp.isRetain && operation.isRetain) { - if (lastOp.hasSameAttributes(operation)) { - _mergeWithTail(operation); - return; - } - } - } - if (index == _operations.length) { - _operations.add(operation); - } else { - final opAtIndex = _operations.elementAt(index); - _operations.replaceRange(index, index + 1, [operation, opAtIndex]); - } - _modificationCount++; - } - - /// Composes next operation from [thisIter] and [otherIter]. - /// - /// Returns new operation or `null` if operations from [thisIter] and - /// [otherIter] nullify each other. For instance, for the pair `insert('abc')` - /// and `delete(3)` composition result would be empty string. - Operation? _composeOperation( - DeltaIterator thisIter, DeltaIterator otherIter) { - if (otherIter.isNextInsert) return otherIter.next(); - if (thisIter.isNextDelete) return thisIter.next(); - - final length = math.min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter.next(length as int); - final otherOp = otherIter.next(length); - assert(thisOp.length == otherOp.length); - - if (otherOp.isRetain) { - final attributes = composeAttributes( - thisOp.attributes, - otherOp.attributes, - keepNull: thisOp.isRetain, - ); - if (thisOp.isRetain) { - return Operation.retain(thisOp.length, attributes); - } else if (thisOp.isInsert) { - return Operation.insert(thisOp.data, attributes); - } else { - throw StateError('Unreachable'); - } - } else { - // otherOp == delete && thisOp in [retain, insert] - assert(otherOp.isDelete); - if (thisOp.isRetain) return otherOp; - assert(thisOp.isInsert); - // otherOp(delete) + thisOp(insert) => null - } - return null; - } - - /// Composes this delta with [other] and returns new [Delta]. - /// - /// It is not required for this and [other] delta to represent a document - /// delta (consisting only of insert operations). - Delta compose(Delta other) { - final result = Delta(); - final thisIter = DeltaIterator(this); - final otherIter = DeltaIterator(other); - - while (thisIter.hasNext || otherIter.hasNext) { - final newOp = _composeOperation(thisIter, otherIter); - if (newOp != null) result.push(newOp); - } - return result..trim(); - } - - /// Transforms next operation from [otherIter] against next operation in - /// [thisIter]. - /// - /// Returns `null` if both operations nullify each other. - Operation? _transformOperation( - DeltaIterator thisIter, DeltaIterator otherIter, bool priority) { - if (thisIter.isNextInsert && (priority || !otherIter.isNextInsert)) { - return Operation.retain(thisIter.next().length); - } else if (otherIter.isNextInsert) { - return otherIter.next(); - } - - final length = math.min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter.next(length as int); - final otherOp = otherIter.next(length); - assert(thisOp.length == otherOp.length); - - // At this point only delete and retain operations are possible. - if (thisOp.isDelete) { - // otherOp is either delete or retain, so they nullify each other. - return null; - } else if (otherOp.isDelete) { - return otherOp; - } else { - // Retain otherOp which is either retain or insert. - return Operation.retain( - length, - transformAttributes(thisOp.attributes, otherOp.attributes, priority), - ); - } - } - - /// Transforms [other] delta against operations in this delta. - Delta transform(Delta other, bool priority) { - final result = Delta(); - final thisIter = DeltaIterator(this); - final otherIter = DeltaIterator(other); - - while (thisIter.hasNext || otherIter.hasNext) { - final newOp = _transformOperation(thisIter, otherIter, priority); - if (newOp != null) result.push(newOp); - } - return result..trim(); - } - - /// Removes trailing retain operation with empty attributes, if present. - void trim() { - if (isNotEmpty) { - final last = _operations.last; - if (last.isRetain && last.isPlain) _operations.removeLast(); - } - } - - /// Concatenates [other] with this delta and returns the result. - Delta concat(Delta other) { - final result = Delta.from(this); - if (other.isNotEmpty) { - // In case first operation of other can be merged with last operation in - // our list. - result.push(other._operations.first); - result._operations.addAll(other._operations.sublist(1)); - } - return result; - } - - /// Inverts this delta against [base]. - /// - /// Returns new delta which negates effect of this delta when applied to - /// [base]. This is an equivalent of "undo" operation on deltas. - Delta invert(Delta base) { - final inverted = Delta(); - if (base.isEmpty) return inverted; - - var baseIndex = 0; - for (final op in _operations) { - if (op.isInsert) { - inverted.delete(op.length!); - } else if (op.isRetain && op.isPlain) { - inverted.retain(op.length!); - baseIndex += op.length!; - } else if (op.isDelete || (op.isRetain && op.isNotPlain)) { - final length = op.length!; - final sliceDelta = base.slice(baseIndex, baseIndex + length); - sliceDelta.toList().forEach((baseOp) { - if (op.isDelete) { - inverted.push(baseOp); - } else if (op.isRetain && op.isNotPlain) { - final invertAttr = - invertAttributes(op.attributes, baseOp.attributes); - inverted.retain( - baseOp.length!, invertAttr.isEmpty ? null : invertAttr); - } - }); - baseIndex += length; - } else { - throw StateError('Unreachable'); - } - } - inverted.trim(); - return inverted; - } - - /// Returns slice of this delta from [start] index (inclusive) to [end] - /// (exclusive). - Delta slice(int start, [int? end]) { - final delta = Delta(); - var index = 0; - final opIterator = DeltaIterator(this); - - final actualEnd = end ?? double.infinity; - - while (index < actualEnd && opIterator.hasNext) { - Operation op; - if (index < start) { - op = opIterator.next(start - index); - } else { - op = opIterator.next(actualEnd - index as int); - delta.push(op); - } - index += op.length!; - } - return delta; - } - - /// Transforms [index] against this delta. - /// - /// Any "delete" operation before specified [index] shifts it backward, as - /// well as any "insert" operation shifts it forward. - /// - /// The [force] argument is used to resolve scenarios when there is an - /// insert operation at the same position as [index]. If [force] is set to - /// `true` (default) then position is forced to shift forward, otherwise - /// position stays at the same index. In other words setting [force] to - /// `false` gives higher priority to the transformed position. - /// - /// Useful to adjust caret or selection positions. - int transformPosition(int index, {bool force = true}) { - final iter = DeltaIterator(this); - var offset = 0; - while (iter.hasNext && offset <= index) { - final op = iter.next(); - if (op.isDelete) { - index -= math.min(op.length!, index - offset); - continue; - } else if (op.isInsert && (offset < index || force)) { - index += op.length!; - } - offset += op.length!; - } - return index; - } - - @override - String toString() => _operations.join('\n'); -} - -/// 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; - - bool get isNextInsert => nextOperationKey == Operation.insertKey; - - bool get isNextDelete => nextOperationKey == Operation.deleteKey; - - bool get isNextRetain => nextOperationKey == Operation.retainKey; - - String? get nextOperationKey { - if (_index < delta.length) { - return delta.elementAt(_index).key; - } else { - return null; - } - } - - bool get hasNext => peekLength() < double.infinity; - - /// Returns length of next operation without consuming it. - /// - /// Returns [double.infinity] if there is no more operations left to iterate. - num peekLength() { - if (_index < delta.length) { - final operation = delta._operations[_index]; - return operation.length! - _offset; - } - return double.infinity; - } - - /// Consumes and returns next operation. - /// - /// Optional [length] specifies maximum length of operation to return. Note - /// that actual length of returned operation may be less than specified value. - Operation next([int length = 4294967296]) { - if (_modificationCount != delta._modificationCount) { - throw ConcurrentModificationError(delta); - } - - if (_index < delta.length) { - final op = delta.elementAt(_index); - final opKey = op.key; - final opAttributes = op.attributes; - final _currentOffset = _offset; - final actualLength = math.min(op.length! - _currentOffset, length); - if (actualLength == op.length! - _currentOffset) { - _index++; - _offset = 0; - } else { - _offset += actualLength; - } - final opData = op.isInsert && op.data is String - ? (op.data as String).substring( - _currentOffset as int, _currentOffset + (actualLength as int)) - : op.data; - final opIsNotEmpty = - opData is String ? opData.isNotEmpty : true; // embeds are never empty - final opLength = opData is String ? opData.length : 1; - final opActualLength = opIsNotEmpty ? opLength : actualLength as int; - return Operation._(opKey, opActualLength, opData, opAttributes); - } - return Operation.retain(length); - } - - /// Skips [length] characters in source delta. - /// - /// Returns last skipped operation, or `null` if there was nothing to skip. - Operation? skip(int length) { - var skipped = 0; - Operation? op; - while (skipped < length && hasNext) { - final opLength = peekLength(); - final skip = math.min(length - skipped, opLength); - op = next(skip as int); - skipped += op.length!; - } - return op; - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/quill_delta.dart'; diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index e6682f94..65a27b0e 100644 --- a/lib/models/rules/delete.dart +++ b/lib/models/rules/delete.dart @@ -1,124 +1,3 @@ -import '../documents/attribute.dart'; -import '../quill_delta.dart'; -import 'rule.dart'; - -abstract class DeleteRule extends Rule { - const DeleteRule(); - - @override - RuleType get type => RuleType.DELETE; - - @override - void validateArgs(int? len, Object? data, Attribute? attribute) { - assert(len != null); - assert(data == null); - assert(attribute == null); - } -} - -class CatchAllDeleteRule extends DeleteRule { - const CatchAllDeleteRule(); - - @override - Delta applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - return Delta() - ..retain(index) - ..delete(len!); - } -} - -class PreserveLineStyleOnMergeRule extends DeleteRule { - const PreserveLineStyleOnMergeRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - final itr = DeltaIterator(document)..skip(index); - var op = itr.next(1); - if (op.data != '\n') { - return null; - } - - final isNotPlain = op.isNotPlain; - final attrs = op.attributes; - - itr.skip(len! - 1); - final delta = Delta() - ..retain(index) - ..delete(len); - - while (itr.hasNext) { - op = itr.next(); - final text = op.data is String ? (op.data as String?)! : ''; - final lineBreak = text.indexOf('\n'); - if (lineBreak == -1) { - delta.retain(op.length!); - continue; - } - - var attributes = op.attributes == null - ? null - : op.attributes!.map( - (key, dynamic value) => MapEntry(key, null)); - - if (isNotPlain) { - attributes ??= {}; - attributes.addAll(attrs!); - } - delta..retain(lineBreak)..retain(1, attributes); - break; - } - return delta; - } -} - -class EnsureEmbedLineRule extends DeleteRule { - const EnsureEmbedLineRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - final itr = DeltaIterator(document); - - var op = itr.skip(index); - int? indexDelta = 0, lengthDelta = 0, remain = len; - var embedFound = op != null && op.data is! String; - final hasLineBreakBefore = - !embedFound && (op == null || (op.data as String).endsWith('\n')); - if (embedFound) { - var candidate = itr.next(1); - if (remain != null) { - remain--; - if (candidate.data == '\n') { - indexDelta++; - lengthDelta--; - - candidate = itr.next(1); - remain--; - if (candidate.data == '\n') { - lengthDelta++; - } - } - } - } - - op = itr.skip(remain!); - if (op != null && - (op.data is String ? op.data as String? : '')!.endsWith('\n')) { - final candidate = itr.next(1); - if (candidate.data is! String && !hasLineBreakBefore) { - embedFound = true; - lengthDelta--; - } - } - - if (!embedFound) { - return null; - } - - return Delta() - ..retain(index + indexDelta) - ..delete(len! + lengthDelta); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/rules/delete.dart'; diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart index be201925..e6251d03 100644 --- a/lib/models/rules/format.dart +++ b/lib/models/rules/format.dart @@ -1,132 +1,3 @@ -import '../documents/attribute.dart'; -import '../quill_delta.dart'; -import 'rule.dart'; - -abstract class FormatRule extends Rule { - const FormatRule(); - - @override - RuleType get type => RuleType.FORMAT; - - @override - void validateArgs(int? len, Object? data, Attribute? attribute) { - assert(len != null); - assert(data == null); - assert(attribute != null); - } -} - -class ResolveLineFormatRule extends FormatRule { - const ResolveLineFormatRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (attribute!.scope != AttributeScope.BLOCK) { - return null; - } - - var delta = Delta()..retain(index); - final itr = DeltaIterator(document)..skip(index); - Operation op; - for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { - op = itr.next(len - cur); - if (op.data is! String || !(op.data as String).contains('\n')) { - delta.retain(op.length!); - continue; - } - final text = op.data as String; - final tmp = Delta(); - var offset = 0; - - for (var lineBreak = text.indexOf('\n'); - lineBreak >= 0; - lineBreak = text.indexOf('\n', offset)) { - tmp..retain(lineBreak - offset)..retain(1, attribute.toJson()); - offset = lineBreak + 1; - } - tmp.retain(text.length - offset); - delta = delta.concat(tmp); - } - - while (itr.hasNext) { - op = itr.next(); - final text = op.data is String ? (op.data as String?)! : ''; - final lineBreak = text.indexOf('\n'); - if (lineBreak < 0) { - delta.retain(op.length!); - continue; - } - delta..retain(lineBreak)..retain(1, attribute.toJson()); - break; - } - return delta; - } -} - -class FormatLinkAtCaretPositionRule extends FormatRule { - const FormatLinkAtCaretPositionRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (attribute!.key != Attribute.link.key || len! > 0) { - return null; - } - - 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!; - retain = before.length; - } - if (after.hasAttribute(attribute.key)) { - if (retain != null) retain += after.length!; - } - if (retain == 0) { - return null; - } - - delta..retain(beg)..retain(retain!, attribute.toJson()); - return delta; - } -} - -class ResolveInlineFormatRule extends FormatRule { - const ResolveInlineFormatRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (attribute!.scope != AttributeScope.INLINE) { - return null; - } - - final delta = Delta()..retain(index); - final itr = DeltaIterator(document)..skip(index); - - Operation op; - for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { - op = itr.next(len - cur); - 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; - } - var pos = 0; - while (lineBreak >= 0) { - delta..retain(lineBreak - pos, attribute.toJson())..retain(1); - pos = lineBreak + 1; - lineBreak = text.indexOf('\n', pos); - } - if (pos < op.length!) { - delta.retain(op.length! - pos, attribute.toJson()); - } - } - - return delta; - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/rules/format.dart'; diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 5801a10e..4dfe6ab7 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -1,413 +1,3 @@ -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(); - - @override - RuleType get type => RuleType.INSERT; - - @override - void validateArgs(int? len, Object? data, Attribute? attribute) { - assert(data != null); - assert(attribute == null); - } -} - -class PreserveLineStyleOnSplitRule extends InsertRule { - const PreserveLineStyleOnSplitRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is! String || data != '\n') { - return null; - } - - final itr = DeltaIterator(document); - final before = itr.skip(index); - if (before == null || - before.data is! String || - (before.data as String).endsWith('\n')) { - return null; - } - final after = itr.next(); - if (after.data is! String || (after.data as String).startsWith('\n')) { - return null; - } - - final text = after.data as String; - - final delta = Delta()..retain(index + (len ?? 0)); - if (text.contains('\n')) { - assert(after.isPlain); - delta.insert('\n'); - return delta; - } - final nextNewLine = _getNextNewLine(itr); - final attributes = nextNewLine.item1?.attributes; - - return delta..insert('\n', attributes); - } -} - -/// Preserves block style when user inserts text containing newlines. -/// -/// This rule handles: -/// -/// * inserting a new line in a block -/// * pasting text containing multiple lines of text in a block -/// -/// This rule may also be activated for changes triggered by auto-correct. -class PreserveBlockStyleOnInsertRule extends InsertRule { - const PreserveBlockStyleOnInsertRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is! String || !data.contains('\n')) { - // Only interested in text containing at least one newline character. - return null; - } - - final itr = DeltaIterator(document)..skip(index); - - // Look for the next newline. - final nextNewLine = _getNextNewLine(itr); - final lineStyle = - Style.fromJson(nextNewLine.item1?.attributes ?? {}); - - final blockStyle = lineStyle.getBlocksExceptHeader(); - // Are we currently in a block? If not then ignore. - if (blockStyle.isEmpty) { - return null; - } - - Map? resetStyle; - // If current line had heading style applied to it we'll need to move this - // style to the newly inserted line before it and reset style of the - // original line. - if (lineStyle.containsKey(Attribute.header.key)) { - resetStyle = Attribute.header.toJson(); - } - - // Go over each inserted line and ensure block style is applied. - final lines = data.split('\n'); - final delta = Delta()..retain(index + (len ?? 0)); - for (var i = 0; i < lines.length; i++) { - final line = lines[i]; - if (line.isNotEmpty) { - delta.insert(line); - } - if (i == 0) { - // The first line should inherit the lineStyle entirely. - delta.insert('\n', lineStyle.toJson()); - } else if (i < lines.length - 1) { - // we don't want to insert a newline after the last chunk of text, so -1 - delta.insert('\n', blockStyle); - } - } - - // Reset style of the original newline character if needed. - if (resetStyle != null) { - delta - ..retain(nextNewLine.item2!) - ..retain((nextNewLine.item1!.data as String).indexOf('\n')) - ..retain(1, resetStyle); - } - - return delta; - } -} - -/// Heuristic rule to exit current block when user inserts two consecutive -/// newlines. -/// -/// This rule is only applied when the cursor is on the last line of a block. -/// When the cursor is in the middle of a block we allow adding empty lines -/// and preserving the block's style. -class AutoExitBlockRule extends InsertRule { - const AutoExitBlockRule(); - - bool _isEmptyLine(Operation? before, Operation? after) { - if (before == null) { - return true; - } - return before.data is String && - (before.data as String).endsWith('\n') && - after!.data is String && - (after.data as String).startsWith('\n'); - } - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is! String || data != '\n') { - return null; - } - - final itr = DeltaIterator(document); - final prev = itr.skip(index), cur = itr.next(); - final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader(); - // We are not in a block, ignore. - if (cur.isPlain || blockStyle == null) { - return null; - } - // We are not on an empty line, ignore. - if (!_isEmptyLine(prev, cur)) { - return null; - } - - // We are on an empty line. Now we need to determine if we are on the - // last line of a block. - // First check if `cur` length is greater than 1, this would indicate - // that it contains multiple newline characters which share the same style. - // This would mean we are not on the last line yet. - // `cur.value as String` is safe since we already called isEmptyLine and know it contains a newline - if ((cur.value as String).length > 1) { - // We are not on the last line of this block, ignore. - return null; - } - - // Keep looking for the next newline character to see if it shares the same - // block style as `cur`. - final nextNewLine = _getNextNewLine(itr); - if (nextNewLine.item1 != null && - nextNewLine.item1!.attributes != null && - Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() == - blockStyle) { - // We are not at the end of this block, ignore. - return null; - } - - // Here we now know that the line after `cur` is not in the same block - // therefore we can exit this block. - final attributes = cur.attributes ?? {}; - final k = attributes.keys - .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k)); - attributes[k] = null; - // retain(1) should be '\n', set it with no attribute - return Delta()..retain(index + (len ?? 0))..retain(1, attributes); - } -} - -class ResetLineFormatOnNewLineRule extends InsertRule { - const ResetLineFormatOnNewLineRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is! String || data != '\n') { - return null; - } - - final itr = DeltaIterator(document)..skip(index); - final cur = itr.next(); - if (cur.data is! String || !(cur.data as String).startsWith('\n')) { - return null; - } - - Map? resetStyle; - if (cur.attributes != null && - cur.attributes!.containsKey(Attribute.header.key)) { - resetStyle = Attribute.header.toJson(); - } - return Delta() - ..retain(index + (len ?? 0)) - ..insert('\n', cur.attributes) - ..retain(1, resetStyle) - ..trim(); - } -} - -class InsertEmbedsRule extends InsertRule { - const InsertEmbedsRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is String) { - return null; - } - - final delta = Delta()..retain(index + (len ?? 0)); - final itr = DeltaIterator(document); - final prev = itr.skip(index), cur = itr.next(); - - 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'); - - if (isNewlineBefore && isNewlineAfter) { - return delta..insert(data); - } - - Map? lineStyle; - if (textAfter.contains('\n')) { - lineStyle = cur.attributes; - } else { - while (itr.hasNext) { - final op = itr.next(); - if ((op.data is String ? op.data as String? : '')!.contains('\n')) { - lineStyle = op.attributes; - break; - } - } - } - - if (!isNewlineBefore) { - delta.insert('\n', lineStyle); - } - delta.insert(data); - if (!isNewlineAfter) { - delta.insert('\n'); - } - return delta; - } -} - -class ForceNewlineForInsertsAroundEmbedRule extends InsertRule { - const ForceNewlineForInsertsAroundEmbedRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is! String) { - return null; - } - - final text = data; - final itr = DeltaIterator(document); - final prev = itr.skip(index); - final cur = itr.next(); - final cursorBeforeEmbed = cur.data is! String; - final cursorAfterEmbed = prev != null && prev.data is! String; - - if (!cursorBeforeEmbed && !cursorAfterEmbed) { - return null; - } - final delta = Delta()..retain(index + (len ?? 0)); - if (cursorBeforeEmbed && !text.endsWith('\n')) { - return delta..insert(text)..insert('\n'); - } - if (cursorAfterEmbed && !text.startsWith('\n')) { - return delta..insert('\n')..insert(text); - } - return delta..insert(text); - } -} - -class AutoFormatLinksRule extends InsertRule { - const AutoFormatLinksRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is! String || data != ' ') { - return null; - } - - final itr = DeltaIterator(document); - final prev = itr.skip(index); - if (prev == null || prev.data is! String) { - return null; - } - - try { - final cand = (prev.data as String).split('\n').last.split(' ').last; - final link = Uri.parse(cand); - if (!['https', 'http'].contains(link.scheme)) { - return null; - } - final attributes = prev.attributes ?? {}; - - if (attributes.containsKey(Attribute.link.key)) { - return null; - } - - attributes.addAll(LinkAttribute(link.toString()).toJson()); - return Delta() - ..retain(index + (len ?? 0) - cand.length) - ..retain(cand.length, attributes) - ..insert(data, prev.attributes); - } on FormatException { - return null; - } - } -} - -class PreserveInlineStylesRule extends InsertRule { - const PreserveInlineStylesRule(); - - @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is! String || data.contains('\n')) { - return null; - } - - final itr = DeltaIterator(document); - final prev = itr.skip(index); - if (prev == null || - prev.data is! String || - (prev.data as String).contains('\n')) { - return null; - } - - final attributes = prev.attributes; - final text = data; - if (attributes == null || !attributes.containsKey(Attribute.link.key)) { - return Delta() - ..retain(index + (len ?? 0)) - ..insert(text, attributes); - } - - attributes.remove(Attribute.link.key); - final delta = Delta() - ..retain(index + (len ?? 0)) - ..insert(text, attributes.isEmpty ? null : attributes); - final next = itr.next(); - - final nextAttributes = next.attributes ?? const {}; - if (!nextAttributes.containsKey(Attribute.link.key)) { - return delta; - } - if (attributes[Attribute.link.key] == nextAttributes[Attribute.link.key]) { - return Delta() - ..retain(index + (len ?? 0)) - ..insert(text, attributes); - } - return delta; - } -} - -class CatchAllInsertRule extends InsertRule { - const CatchAllInsertRule(); - - @override - Delta applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - return Delta() - ..retain(index + (len ?? 0)) - ..insert(data); - } -} - -Tuple2 _getNextNewLine(DeltaIterator iterator) { - Operation op; - for (var skipped = 0; iterator.hasNext; skipped += op.length!) { - op = iterator.next(); - final lineBreak = - (op.data is String ? op.data as String? : '')!.indexOf('\n'); - if (lineBreak >= 0) { - return Tuple2(op, skipped); - } - } - return const Tuple2(null, null); -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/rules/insert.dart'; diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index 042f1aaa..11026f46 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -1,77 +1,3 @@ -import '../documents/attribute.dart'; -import '../documents/document.dart'; -import '../quill_delta.dart'; -import 'delete.dart'; -import 'format.dart'; -import 'insert.dart'; - -enum RuleType { INSERT, DELETE, FORMAT } - -abstract class Rule { - const Rule(); - - Delta? apply(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - validateArgs(len, data, attribute); - return applyRule(document, index, - len: len, data: data, attribute: attribute); - } - - void validateArgs(int? len, Object? data, Attribute? attribute); - - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}); - - RuleType get type; -} - -class Rules { - Rules(this._rules); - - List _customRules = []; - - final List _rules; - static final Rules _instance = Rules([ - const FormatLinkAtCaretPositionRule(), - const ResolveLineFormatRule(), - const ResolveInlineFormatRule(), - const InsertEmbedsRule(), - const ForceNewlineForInsertsAroundEmbedRule(), - const AutoExitBlockRule(), - const PreserveBlockStyleOnInsertRule(), - const PreserveLineStyleOnSplitRule(), - const ResetLineFormatOnNewLineRule(), - const AutoFormatLinksRule(), - const PreserveInlineStylesRule(), - const CatchAllInsertRule(), - const EnsureEmbedLineRule(), - const PreserveLineStyleOnMergeRule(), - const CatchAllDeleteRule(), - ]); - - static Rules getInstance() => _instance; - - void setCustomRules(List customRules) { - _customRules = customRules; - } - - Delta apply(RuleType ruleType, Document document, int index, - {int? len, Object? data, Attribute? attribute}) { - final delta = document.toDelta(); - for (final rule in _customRules + _rules) { - if (rule.type != ruleType) { - continue; - } - try { - final result = rule.apply(delta, index, - len: len, data: data, attribute: attribute); - if (result != null) { - return result..trim(); - } - } catch (e) { - rethrow; - } - } - throw 'Apply rules failed'; - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/models/rules/rule.dart'; diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart new file mode 100644 index 00000000..1b9043b9 --- /dev/null +++ b/lib/src/models/documents/attribute.dart @@ -0,0 +1,292 @@ +import 'dart:collection'; + +import 'package:quiver/core.dart'; + +enum AttributeScope { + INLINE, // refer to https://quilljs.com/docs/formats/#inline + BLOCK, // refer to https://quilljs.com/docs/formats/#block + EMBEDS, // refer to https://quilljs.com/docs/formats/#embeds + IGNORE, // attributes that can be ignored +} + +class Attribute { + Attribute(this.key, this.scope, this.value); + + final String key; + final AttributeScope scope; + final T value; + + static final Map _registry = LinkedHashMap.of({ + Attribute.bold.key: Attribute.bold, + Attribute.italic.key: Attribute.italic, + Attribute.underline.key: Attribute.underline, + Attribute.strikeThrough.key: Attribute.strikeThrough, + Attribute.font.key: Attribute.font, + Attribute.size.key: Attribute.size, + Attribute.link.key: Attribute.link, + Attribute.color.key: Attribute.color, + Attribute.background.key: Attribute.background, + Attribute.placeholder.key: Attribute.placeholder, + Attribute.header.key: Attribute.header, + Attribute.align.key: Attribute.align, + Attribute.list.key: Attribute.list, + Attribute.codeBlock.key: Attribute.codeBlock, + Attribute.blockQuote.key: Attribute.blockQuote, + Attribute.indent.key: Attribute.indent, + Attribute.width.key: Attribute.width, + Attribute.height.key: Attribute.height, + Attribute.style.key: Attribute.style, + Attribute.token.key: Attribute.token, + }); + + static final BoldAttribute bold = BoldAttribute(); + + static final ItalicAttribute italic = ItalicAttribute(); + + static final UnderlineAttribute underline = UnderlineAttribute(); + + static final StrikeThroughAttribute strikeThrough = StrikeThroughAttribute(); + + static final FontAttribute font = FontAttribute(null); + + static final SizeAttribute size = SizeAttribute(null); + + static final LinkAttribute link = LinkAttribute(null); + + static final ColorAttribute color = ColorAttribute(null); + + static final BackgroundAttribute background = BackgroundAttribute(null); + + static final PlaceholderAttribute placeholder = PlaceholderAttribute(); + + static final HeaderAttribute header = HeaderAttribute(); + + static final IndentAttribute indent = IndentAttribute(); + + static final AlignAttribute align = AlignAttribute(null); + + static final ListAttribute list = ListAttribute(null); + + static final CodeBlockAttribute codeBlock = CodeBlockAttribute(); + + static final BlockQuoteAttribute blockQuote = BlockQuoteAttribute(); + + static final WidthAttribute width = WidthAttribute(null); + + static final HeightAttribute height = HeightAttribute(null); + + static final StyleAttribute style = StyleAttribute(null); + + static final TokenAttribute token = TokenAttribute(''); + + static final Set inlineKeys = { + Attribute.bold.key, + Attribute.italic.key, + Attribute.underline.key, + Attribute.strikeThrough.key, + Attribute.link.key, + Attribute.color.key, + Attribute.background.key, + Attribute.placeholder.key, + }; + + static final Set blockKeys = LinkedHashSet.of({ + Attribute.header.key, + Attribute.align.key, + Attribute.list.key, + Attribute.codeBlock.key, + Attribute.blockQuote.key, + Attribute.indent.key, + }); + + static final Set blockKeysExceptHeader = LinkedHashSet.of({ + Attribute.list.key, + Attribute.align.key, + Attribute.codeBlock.key, + Attribute.blockQuote.key, + Attribute.indent.key, + }); + + static Attribute get h1 => HeaderAttribute(level: 1); + + static Attribute get h2 => HeaderAttribute(level: 2); + + static Attribute get h3 => HeaderAttribute(level: 3); + + // "attributes":{"align":"left"} + static Attribute get leftAlignment => AlignAttribute('left'); + + // "attributes":{"align":"center"} + static Attribute get centerAlignment => AlignAttribute('center'); + + // "attributes":{"align":"right"} + static Attribute get rightAlignment => AlignAttribute('right'); + + // "attributes":{"align":"justify"} + static Attribute get justifyAlignment => AlignAttribute('justify'); + + // "attributes":{"list":"bullet"} + static Attribute get ul => ListAttribute('bullet'); + + // "attributes":{"list":"ordered"} + static Attribute get ol => ListAttribute('ordered'); + + // "attributes":{"list":"checked"} + static Attribute get checked => ListAttribute('checked'); + + // "attributes":{"list":"unchecked"} + static Attribute get unchecked => ListAttribute('unchecked'); + + // "attributes":{"indent":1"} + static Attribute get indentL1 => IndentAttribute(level: 1); + + // "attributes":{"indent":2"} + static Attribute get indentL2 => IndentAttribute(level: 2); + + // "attributes":{"indent":3"} + static Attribute get indentL3 => IndentAttribute(level: 3); + + static Attribute getIndentLevel(int? level) { + if (level == 1) { + return indentL1; + } + if (level == 2) { + return indentL2; + } + if (level == 3) { + return indentL3; + } + return IndentAttribute(level: level); + } + + bool get isInline => scope == AttributeScope.INLINE; + + bool get isBlockExceptHeader => blockKeysExceptHeader.contains(key); + + Map toJson() => {key: value}; + + static Attribute fromKeyValue(String key, dynamic value) { + if (!_registry.containsKey(key)) { + throw ArgumentError.value(key, 'key "$key" not found.'); + } + final origin = _registry[key]!; + final attribute = clone(origin, value); + return attribute; + } + + static int getRegistryOrder(Attribute attribute) { + var order = 0; + for (final attr in _registry.values) { + if (attr.key == attribute.key) { + break; + } + order++; + } + + return order; + } + + static Attribute clone(Attribute origin, dynamic value) { + return Attribute(origin.key, origin.scope, value); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! Attribute) return false; + final typedOther = other; + return key == typedOther.key && + scope == typedOther.scope && + value == typedOther.value; + } + + @override + int get hashCode => hash3(key, scope, value); + + @override + String toString() { + return 'Attribute{key: $key, scope: $scope, value: $value}'; + } +} + +class BoldAttribute extends Attribute { + BoldAttribute() : super('bold', AttributeScope.INLINE, true); +} + +class ItalicAttribute extends Attribute { + ItalicAttribute() : super('italic', AttributeScope.INLINE, true); +} + +class UnderlineAttribute extends Attribute { + UnderlineAttribute() : super('underline', AttributeScope.INLINE, true); +} + +class StrikeThroughAttribute extends Attribute { + StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true); +} + +class FontAttribute extends Attribute { + FontAttribute(String? val) : super('font', AttributeScope.INLINE, val); +} + +class SizeAttribute extends Attribute { + SizeAttribute(String? val) : super('size', AttributeScope.INLINE, val); +} + +class LinkAttribute extends Attribute { + LinkAttribute(String? val) : super('link', AttributeScope.INLINE, val); +} + +class ColorAttribute extends Attribute { + ColorAttribute(String? val) : super('color', AttributeScope.INLINE, val); +} + +class BackgroundAttribute extends Attribute { + BackgroundAttribute(String? val) + : super('background', AttributeScope.INLINE, val); +} + +/// This is custom attribute for hint +class PlaceholderAttribute extends Attribute { + PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true); +} + +class HeaderAttribute extends Attribute { + HeaderAttribute({int? level}) : super('header', AttributeScope.BLOCK, level); +} + +class IndentAttribute extends Attribute { + IndentAttribute({int? level}) : super('indent', AttributeScope.BLOCK, level); +} + +class AlignAttribute extends Attribute { + AlignAttribute(String? val) : super('align', AttributeScope.BLOCK, val); +} + +class ListAttribute extends Attribute { + ListAttribute(String? val) : super('list', AttributeScope.BLOCK, val); +} + +class CodeBlockAttribute extends Attribute { + CodeBlockAttribute() : super('code-block', AttributeScope.BLOCK, true); +} + +class BlockQuoteAttribute extends Attribute { + BlockQuoteAttribute() : super('blockquote', AttributeScope.BLOCK, true); +} + +class WidthAttribute extends Attribute { + WidthAttribute(String? val) : super('width', AttributeScope.IGNORE, val); +} + +class HeightAttribute extends Attribute { + HeightAttribute(String? val) : super('height', AttributeScope.IGNORE, val); +} + +class StyleAttribute extends Attribute { + StyleAttribute(String? val) : super('style', AttributeScope.IGNORE, val); +} + +class TokenAttribute extends Attribute { + TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val); +} diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart new file mode 100644 index 00000000..a26d885a --- /dev/null +++ b/lib/src/models/documents/document.dart @@ -0,0 +1,290 @@ +import 'dart:async'; + +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(); + + Root get root => _root; + + int get length => _root.length; + + Delta _delta; + + Delta toDelta() => Delta.from(_delta); + + final Rules _rules = Rules.getInstance(); + + void setCustomRules(List customRules) { + _rules.setCustomRules(customRules); + } + + final StreamController> _observer = + StreamController.broadcast(); + + final History _history = History(); + + Stream> get changes => _observer.stream; + + Delta insert(int index, Object? data, + {int replaceLength = 0, bool autoAppendNewlineAfterImage = true}) { + assert(index >= 0); + assert(data is String || data is Embeddable); + if (data is Embeddable) { + data = data.toJson(); + } else if ((data as String).isEmpty) { + return Delta(); + } + + final delta = _rules.apply(RuleType.INSERT, this, index, + data: data, len: replaceLength); + compose(delta, ChangeSource.LOCAL, + autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); + return delta; + } + + Delta delete(int index, int len) { + assert(index >= 0 && len > 0); + final delta = _rules.apply(RuleType.DELETE, this, index, len: len); + if (delta.isNotEmpty) { + compose(delta, ChangeSource.LOCAL); + } + return delta; + } + + Delta replace(int index, int len, Object? data, + {bool autoAppendNewlineAfterImage = true}) { + assert(index >= 0); + assert(data is String || data is Embeddable); + + final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true; + + assert(dataIsNotEmpty || len > 0); + + var delta = Delta(); + + // We have to insert before applying delete rules + // Otherwise delete would be operating on stale document snapshot. + if (dataIsNotEmpty) { + delta = insert(index, data, + replaceLength: len, + autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); + } + + if (len > 0) { + final deleteDelta = delete(index, len); + delta = delta.compose(deleteDelta); + } + + return delta; + } + + Delta format(int index, int len, Attribute? attribute) { + assert(index >= 0 && len >= 0 && attribute != null); + + var delta = Delta(); + + final formatDelta = _rules.apply(RuleType.FORMAT, this, index, + len: len, attribute: attribute); + if (formatDelta.isNotEmpty) { + compose(formatDelta, ChangeSource.LOCAL); + delta = delta.compose(formatDelta); + } + + return delta; + } + + Style collectStyle(int index, int len) { + final res = queryChild(index); + return (res.node as Line).collectStyle(res.offset, len); + } + + ChildQuery queryChild(int offset) { + final res = _root.queryChild(offset, true); + if (res.node is Line) { + return res; + } + final block = res.node as Block; + return block.queryChild(res.offset, true); + } + + void compose(Delta delta, ChangeSource changeSource, + {bool autoAppendNewlineAfterImage = true}) { + assert(!_observer.isClosed); + delta.trim(); + assert(delta.isNotEmpty); + + var offset = 0; + delta = _transform(delta, + autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); + final originalDelta = toDelta(); + for (final op in delta.toList()) { + final style = + op.attributes != null ? Style.fromJson(op.attributes) : null; + + if (op.isInsert) { + _root.insert(offset, _normalize(op.data), style); + } else if (op.isDelete) { + _root.delete(offset, op.length); + } else if (op.attributes != null) { + _root.retain(offset, op.length, style); + } + + if (!op.isDelete) { + offset += op.length!; + } + } + try { + _delta = _delta.compose(delta); + } catch (e) { + throw '_delta compose failed'; + } + + if (_delta != _root.toDelta()) { + throw 'Compose failed'; + } + final change = Tuple3(originalDelta, delta, changeSource); + _observer.add(change); + _history.handleDocChange(change); + } + + Tuple2 undo() { + return _history.undo(this); + } + + Tuple2 redo() { + return _history.redo(this); + } + + bool get hasUndo => _history.hasUndo; + + bool get hasRedo => _history.hasRedo; + + static Delta _transform(Delta delta, + {bool autoAppendNewlineAfterImage = true}) { + final res = Delta(); + final ops = delta.toList(); + for (var i = 0; i < ops.length; i++) { + final op = ops[i]; + res.push(op); + if (autoAppendNewlineAfterImage) { + _autoAppendNewlineAfterImage(i, ops, op, res); + } + } + return res; + } + + static void _autoAppendNewlineAfterImage( + int i, List ops, Operation op, Delta res) { + final nextOpIsImage = + i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String; + if (nextOpIsImage && + op.data is String && + (op.data as String).isNotEmpty && + !(op.data as String).endsWith('\n')) { + res.push(Operation.insert('\n')); + } + // Currently embed is equivalent to image and hence `is! String` + 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'); + if (opInsertImage && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) { + // automatically append '\n' for image + res.push(Operation.insert('\n')); + } + } + + Object _normalize(Object? data) { + if (data is String) { + return data; + } + + if (data is Embeddable) { + return data; + } + return Embeddable.fromJson(data as Map); + } + + void close() { + _observer.close(); + _history.clear(); + } + + String toPlainText() => _root.children.map((e) => e.toPlainText()).join(); + + void _loadDocument(Delta doc) { + if (doc.isEmpty) { + throw ArgumentError.value(doc, 'Document Delta cannot be empty.'); + } + + assert((doc.last.data as String).endsWith('\n')); + + var offset = 0; + for (final op in doc.toList()) { + if (!op.isInsert) { + throw ArgumentError.value(doc, + 'Document Delta can only contain insert operations but ${op.key} found.'); + } + final style = + op.attributes != null ? Style.fromJson(op.attributes) : null; + final data = _normalize(op.data); + _root.insert(offset, data, style); + offset += op.length!; + } + final node = _root.last; + if (node is Line && + node.parent is! Block && + node.style.isEmpty && + _root.childCount > 1) { + _root.remove(node); + } + } + + bool isEmpty() { + if (root.children.length != 1) { + return false; + } + + final node = root.children.first; + if (!node.isLast) { + return false; + } + + final delta = node.toDelta(); + return delta.length == 1 && + delta.first.data == '\n' && + delta.first.key == 'insert'; + } +} + +enum ChangeSource { + LOCAL, + REMOTE, +} diff --git a/lib/src/models/documents/history.dart b/lib/src/models/documents/history.dart new file mode 100644 index 00000000..d406505e --- /dev/null +++ b/lib/src/models/documents/history.dart @@ -0,0 +1,134 @@ +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; + + bool get hasRedo => stack.redo.isNotEmpty; + + /// used for disable redo or undo function + bool ignoreChange; + + int lastRecorded; + + /// Collaborative editing's conditions should be true + final bool userOnly; + + ///max operation count for undo + final int maxStack; + + ///record delay + final int interval; + + void handleDocChange(Tuple3 change) { + if (ignoreChange) return; + if (!userOnly || change.item3 == ChangeSource.LOCAL) { + record(change.item2, change.item1); + } else { + transform(change.item2); + } + } + + void clear() { + stack.clear(); + } + + void record(Delta change, Delta before) { + if (change.isEmpty) return; + stack.redo.clear(); + var undoDelta = change.invert(before); + final timeStamp = DateTime.now().millisecondsSinceEpoch; + + if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) { + final lastDelta = stack.undo.removeLast(); + undoDelta = undoDelta.compose(lastDelta); + } else { + lastRecorded = timeStamp; + } + + if (undoDelta.isEmpty) return; + stack.undo.add(undoDelta); + + if (stack.undo.length > maxStack) { + stack.undo.removeAt(0); + } + } + + /// + ///It will override pre local undo delta,replaced by remote change + /// + void transform(Delta delta) { + transformStack(stack.undo, delta); + transformStack(stack.redo, delta); + } + + void transformStack(List stack, Delta delta) { + 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); + if (stack[i].length == 0) { + stack.removeAt(i); + } + } + } + + Tuple2 _change(Document doc, List source, List dest) { + if (source.isEmpty) { + return const Tuple2(false, 0); + } + final delta = source.removeLast(); + // look for insert or delete + int? len = 0; + final ops = delta.toList(); + for (var i = 0; i < ops.length; i++) { + if (ops[i].key == Operation.insertKey) { + len = ops[i].length; + } else if (ops[i].key == Operation.deleteKey) { + len = ops[i].length! * -1; + } + } + final base = Delta.from(doc.toDelta()); + final inverseDelta = delta.invert(base); + dest.add(inverseDelta); + lastRecorded = 0; + ignoreChange = true; + doc.compose(delta, ChangeSource.LOCAL); + ignoreChange = false; + return Tuple2(true, len); + } + + Tuple2 undo(Document doc) { + return _change(doc, stack.undo, stack.redo); + } + + Tuple2 redo(Document doc) { + return _change(doc, stack.redo, stack.undo); + } +} + +class HistoryStack { + HistoryStack.empty() + : undo = [], + redo = []; + + final List undo; + final List redo; + + void clear() { + undo.clear(); + redo.clear(); + } +} diff --git a/lib/src/models/documents/nodes/block.dart b/lib/src/models/documents/nodes/block.dart new file mode 100644 index 00000000..095f1183 --- /dev/null +++ b/lib/src/models/documents/nodes/block.dart @@ -0,0 +1,72 @@ +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(); + + @override + Delta toDelta() { + return children + .map((child) => child.toDelta()) + .fold(Delta(), (a, b) => a.concat(b)); + } + + @override + void adjust() { + if (isEmpty) { + final sibling = previous; + unlink(); + if (sibling != null) { + sibling.adjust(); + } + return; + } + + 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?) + ..unlink(); + block = prev as Block; + } + 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); + next.unlink(); + } + } + + @override + String toString() { + final block = style.attributes.toString(); + final buffer = StringBuffer('§ {$block}\n'); + for (final child in children) { + final tree = child.isLast ? '└' : '├'; + buffer.write(' $tree $child'); + if (!child.isLast) buffer.writeln(); + } + return buffer.toString(); + } +} diff --git a/lib/src/models/documents/nodes/container.dart b/lib/src/models/documents/nodes/container.dart new file mode 100644 index 00000000..dbdd12d1 --- /dev/null +++ b/lib/src/models/documents/nodes/container.dart @@ -0,0 +1,160 @@ +import 'dart:collection'; + +import '../style.dart'; +import 'leaf.dart'; +import 'line.dart'; +import 'node.dart'; + +/// 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; + + /// Returns an instance of default child for this container node. + /// + /// Always returns fresh instance. + T get defaultChild; + + /// 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; + } + + final last = newParent!.isEmpty ? null : newParent.last as T?; + while (isNotEmpty) { + 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 (final node in children) { + final len = node.length; + if (offset < len || (inclusive && offset == len && (node.isLast))) { + return ChildQuery(node, offset); + } + offset -= len; + } + return ChildQuery(null, 0); + } + + @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); + + @override + void insert(int index, Object data, Style? style) { + assert(index == 0 || (index > 0 && index < length)); + + if (isNotEmpty) { + final child = queryChild(index, false); + child.node!.insert(child.offset, data, style); + return; + } + + // empty + assert(index == 0); + final node = defaultChild; + add(node); + node?.insert(index, data, style); + } + + @override + void retain(int index, int? length, Style? attributes) { + assert(isNotEmpty); + final child = queryChild(index, false); + child.node!.retain(child.offset, length, attributes); + } + + @override + void delete(int index, int? length) { + assert(isNotEmpty); + final child = queryChild(index, false); + child.node!.delete(child.offset, length); + } + + @override + String toString() => _children.join('\n'); +} + +/// Result of a child query in a [Container]. +class ChildQuery { + ChildQuery(this.node, this.offset); + + /// The child node if found, otherwise `null`. + final Node? node; + + /// 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/src/models/documents/nodes/embed.dart b/lib/src/models/documents/nodes/embed.dart new file mode 100644 index 00000000..d6fe628a --- /dev/null +++ b/lib/src/models/documents/nodes/embed.dart @@ -0,0 +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; + + /// The data payload of this object. + final dynamic data; + + Map toJson() { + final m = {type: data}; + return m; + } + + static Embeddable fromJson(Map 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 BlockEmbed horizontalRule = BlockEmbed('divider', 'hr'); + + static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl); +} diff --git a/lib/src/models/documents/nodes/leaf.dart b/lib/src/models/documents/nodes/leaf.dart new file mode 100644 index 00000000..bd9292f5 --- /dev/null +++ b/lib/src/models/documents/nodes/leaf.dart @@ -0,0 +1,252 @@ +import 'dart:math' as math; + +import '../../quill_delta.dart'; +import '../style.dart'; +import 'embed.dart'; +import 'line.dart'; +import 'node.dart'; + +/// A leaf in Quill document tree. +abstract class Leaf extends Node { + /// Creates a new [Leaf] with specified [data]. + factory Leaf(Object data) { + if (data is Embeddable) { + return Embed(data); + } + 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, + 'Unable to apply Style to leaf: $value'); + super.applyStyle(value); + } + + @override + Line? get parent => super.parent as Line?; + + @override + int get length { + if (_value is String) { + return (_value as String).length; + } + // return 1 for embedded object + return 1; + } + + @override + Delta toDelta() { + 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); + final node = Leaf(data); + if (index < length) { + splitAt(index)!.insertBefore(node); + } else { + insertAfter(node); + } + node.format(style); + } + + @override + void retain(int index, int? len, Style? style) { + if (style == null) { + return; + } + + final local = math.min(length - index, len!); + final remain = len - local; + final node = _isolate(index, local); + + if (remain > 0) { + assert(node.next != null); + node.next!.retain(0, remain, style); + } + node.format(style); + } + + @override + void delete(int index, int? len) { + assert(index < length); + + 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(); + + final remain = len - local; + if (remain > 0) { + assert(next != null); + next!.delete(0, remain); + } + + if (prev != null) { + prev.adjust(); + } + } + + /// 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; + } + + // 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. + final next = node.next; + if (!node.isLast && next is Text && next.style == node.style) { + node._value = node.value + next.value; + next.unlink(); + } + } + + /// 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) { + return this; + } + if (index == length) { + return isLast ? null : next as Leaf?; + } + + assert(this is Text); + final text = _value as String; + _value = text.substring(0, index); + 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)); + 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 + Node newInstance() => Text(); + + @override + String get value => _value as String; + + @override + String toPlainText() => value; +} + +/// 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 + Node newInstance() => throw UnimplementedError(); + + @override + Embeddable get value => super.value as Embeddable; + + /// // Embed nodes are represented as unicode object replacement character in + // plain text. + @override + String toPlainText() => kObjectReplacementCharacter; +} diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart new file mode 100644 index 00000000..fabfad4d --- /dev/null +++ b/lib/src/models/documents/nodes/line.dart @@ -0,0 +1,371 @@ +import 'dart:math' as math; + +import 'package:collection/collection.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(); + + @override + int get length => super.length + 1; + + /// Returns `true` if this line contains an embedded object. + bool get hasEmbed { + if (childCount != 1) { + return false; + } + + 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?; + } + if (parent is! Block) { + return null; + } + + if (parent!.isLast) { + return null; + } + return parent!.next is Block + ? (parent!.next as Block).first as Line? + : parent!.next as Line?; + } + + @override + Node newInstance() => Line(); + + @override + Delta toDelta() { + final delta = children + .map((child) => child.toDelta()) + .fold(Delta(), (dynamic a, b) => a.concat(b)); + var attributes = style; + if (parent is Block) { + final block = parent as Block; + attributes = attributes.mergeAll(block.style); + } + delta.insert('\n', attributes.toJson()); + return delta; + } + + @override + String toPlainText() => '${super.toPlainText()}\n'; + + @override + String toString() { + final body = children.join(' → '); + final styleString = style.isNotEmpty ? ' $style' : ''; + return '¶ $body ⏎$styleString'; + } + + @override + void insert(int index, Object data, Style? style) { + if (data is Embeddable) { + // 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; + } + + final text = data as String; + final lineBreak = text.indexOf('\n'); + if (lineBreak < 0) { + _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; + } + + final prefix = text.substring(0, lineBreak); + _insertSafe(index, prefix, style); + if (prefix.isNotEmpty) { + index += prefix.length; + } + + // 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 remaining part. + final remain = text.substring(lineBreak + 1); + nextLine.insert(0, remain, style); + } + + @override + void retain(int index, int? len, Style? style) { + if (style == null) { + return; + } + final thisLength = length; + + 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 (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 != thisLength); + super.retain(index, local, style); + } + + final remain = len - local; + if (remain > 0) { + assert(nextLine != null); + nextLine!.retain(0, remain, style); + } + } + + @override + void delete(int index, int? len) { + 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); + } + + final remaining = len - local; + if (remaining > 0) { + assert(nextLine != null); + nextLine!.delete(0, remaining); + } + + 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 (isLFDeleted) { + // Now we can remove this line. + final block = parent!; // remember reference before un-linking. + unlink(); + block.adjust(); + } + } + + /// Formats this line. + void _format(Style? newStyle) { + if (newStyle == null || newStyle.isEmpty) { + return; + } + + applyStyle(newStyle); + final blockStyle = newStyle.getBlockExceptHeader(); + if (blockStyle == null) { + return; + } // No block-level changes + + if (parent is Block) { + final parentStyle = (parent as Block).style.getBlocksExceptHeader(); + if (blockStyle.value == null) { + _unwrap(); + } else if (!const MapEquality() + .equals(newStyle.getBlocksExceptHeader(), parentStyle)) { + _unwrap(); + _applyBlockStyles(newStyle); + } // else the same style, no-op. + } else if (blockStyle.value != null) { + // Only wrap with a new block if this is not an unset + _applyBlockStyles(newStyle); + } + } + + void _applyBlockStyles(Style newStyle) { + var block = Block(); + for (final style in newStyle.getBlocksExceptHeader().values) { + block = block..applyAttribute(style); + } + _wrap(block); + block.adjust(); + } + + /// Wraps this line with new parent [block]. + /// + /// This line can not be in a [Block] when this method is called. + void _wrap(Block block) { + assert(parent != null && parent is! Block); + insertAfter(block); + unlink(); + 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'); + } + final block = parent as Block; + + assert(block.children.contains(this)); + + if (isFirst) { + unlink(); + block.insertBefore(this); + } else if (isLast) { + unlink(); + block.insertAfter(this); + } else { + final before = block.clone() as Block; + block.insertBefore(before); + + var child = block.first as Line; + while (child != this) { + child.unlink(); + before.add(child); + child = block.first as Line; + } + unlink(); + block.insertBefore(this); + } + block.adjust(); + } + + Line _getNextLine(int index) { + assert(index == 0 || (index > 0 && index < length)); + + final line = clone() as Line; + insertAfter(line); + if (index == length - 1) { + return line; + } + + final query = queryChild(index, false); + while (!query.node!.isLast) { + final next = (last as Leaf)..unlink(); + line.addFirst(next); + } + final child = query.node as Leaf; + final cut = child.splitAt(query.offset); + cut?.unlink(); + line.addFirst(cut); + return line; + } + + void _insertSafe(int index, Object data, Style? style) { + assert(index == 0 || (index > 0 && index < length)); + + if (data is String) { + assert(!data.contains('\n')); + if (data.isEmpty) { + return; + } + } + + if (isEmpty) { + final child = Leaf(data); + add(child); + child.format(style); + } else { + final result = queryChild(index, true); + result.node!.insert(result.offset, data, style); + } + } + + /// 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) { + final local = math.min(length - offset, len); + var result = Style(); + final excluded = {}; + + void _handle(Style style) { + if (result.isEmpty) { + excluded.addAll(style.values); + } else { + for (final attr in result.values) { + if (!style.containsKey(attr.key)) { + excluded.add(attr); + } + } + } + final remaining = style.removeAll(excluded); + result = result.removeAll(excluded); + result = result.mergeAll(remaining); + } + + final data = queryChild(offset, true); + var node = data.node as Leaf?; + if (node != null) { + result = result.mergeAll(node.style); + var pos = node.length - data.offset; + while (!node!.isLast && pos < local) { + node = node.next as Leaf?; + _handle(node!.style); + pos += node.length; + } + } + + result = result.mergeAll(style); + if (parent is Block) { + final block = parent as Block; + result = result.mergeAll(block.style); + } + + final remaining = len - local; + if (remaining > 0) { + final rest = nextLine!.collectStyle(0, remaining); + _handle(rest); + } + + return result; + } +} diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart new file mode 100644 index 00000000..6bb0fb97 --- /dev/null +++ b/lib/src/models/documents/nodes/node.dart @@ -0,0 +1,131 @@ +import 'dart:collection'; + +import '../../quill_delta.dart'; +import '../attribute.dart'; +import '../style.dart'; +import 'container.dart'; +import 'line.dart'; + +/// 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 get style => _style; + Style _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() => newInstance()..applyStyle(style); + + /// 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; + } + + var cur = this; + do { + cur = cur.previous!; + offset += cur.length; + } while (!cur.isFirst); + return offset; + } + + /// 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 = 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); + entry.parent = parent; + super.insertBefore(entry); + } + + @override + void insertAfter(Node entry) { + assert(entry.parent == null && parent != null); + entry.parent = parent; + super.insertAfter(entry); + } + + @override + void unlink() { + assert(parent != null); + parent = null; + super.unlink(); + } + + void adjust() {/* no-op */} + + /// abstract methods begin + + Node newInstance(); + + String toPlainText(); + + Delta toDelta(); + + void insert(int index, Object data, Style? style); + + void retain(int index, int? len, Style? style); + + void delete(int index, int? len); + + /// abstract methods end +} + +/// Root node of document tree. +class Root extends Container> { + @override + Node newInstance() => Root(); + + @override + Container get defaultChild => Line(); + + @override + Delta toDelta() => children + .map((child) => child.toDelta()) + .fold(Delta(), (a, b) => a.concat(b)); +} diff --git a/lib/src/models/documents/style.dart b/lib/src/models/documents/style.dart new file mode 100644 index 00000000..fade1bb5 --- /dev/null +++ b/lib/src/models/documents/style.dart @@ -0,0 +1,127 @@ +import 'package:collection/collection.dart'; +import 'package:quiver/core.dart'; + +import 'attribute.dart'; + +/* Collection of style attributes */ +class Style { + Style() : _attributes = {}; + + Style.attr(this._attributes); + + final Map _attributes; + + static Style fromJson(Map? attributes) { + if (attributes == null) { + return Style(); + } + + final result = attributes.map((key, dynamic value) { + final attr = Attribute.fromKeyValue(key, value); + return MapEntry(key, attr); + }); + return Style.attr(result); + } + + Map? toJson() => _attributes.isEmpty + ? null + : _attributes.map((_, attribute) => + MapEntry(attribute.key, attribute.value)); + + Iterable get keys => _attributes.keys; + + Iterable get values => _attributes.values.sorted( + (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b)); + + Map get attributes => _attributes; + + bool get isEmpty => _attributes.isEmpty; + + bool get isNotEmpty => _attributes.isNotEmpty; + + bool get isInline => isNotEmpty && values.every((item) => item.isInline); + + bool get isIgnored => + isNotEmpty && values.every((item) => item.scope == AttributeScope.IGNORE); + + Attribute get single => _attributes.values.single; + + bool containsKey(String key) => _attributes.containsKey(key); + + Attribute? getBlockExceptHeader() { + for (final val in values) { + if (val.isBlockExceptHeader && val.value != null) { + return val; + } + } + for (final val in values) { + if (val.isBlockExceptHeader) { + return val; + } + } + return null; + } + + Map getBlocksExceptHeader() { + final m = {}; + attributes.forEach((key, value) { + if (Attribute.blockKeysExceptHeader.contains(key)) { + m[key] = value; + } + }); + return m; + } + + Style merge(Attribute attribute) { + final merged = Map.from(_attributes); + if (attribute.value == null) { + merged.remove(attribute.key); + } else { + merged[attribute.key] = attribute; + } + return Style.attr(merged); + } + + Style mergeAll(Style other) { + var result = Style.attr(_attributes); + for (final attribute in other.values) { + result = result.merge(attribute); + } + return result; + } + + Style removeAll(Set attributes) { + final merged = Map.from(_attributes); + attributes.map((item) => item.key).forEach(merged.remove); + return Style.attr(merged); + } + + Style put(Attribute attribute) { + final m = Map.from(attributes); + m[attribute.key] = attribute; + return Style.attr(m); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! Style) { + return false; + } + final typedOther = other; + const eq = MapEquality(); + return eq.equals(_attributes, typedOther._attributes); + } + + @override + int get hashCode { + final hashes = + _attributes.entries.map((entry) => hash2(entry.key, entry.value)); + return hashObjects(hashes); + } + + @override + String toString() => "{${_attributes.values.join(', ')}}"; +} diff --git a/lib/src/models/quill_delta.dart b/lib/src/models/quill_delta.dart new file mode 100644 index 00000000..a0e608be --- /dev/null +++ b/lib/src/models/quill_delta.dart @@ -0,0 +1,684 @@ +// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +/// Implementation of Quill Delta format in Dart. +library quill_delta; + +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:quiver/core.dart'; + +const _attributeEquality = DeepCollectionEquality(); +const _valueEquality = DeepCollectionEquality(); + +/// Decoder function to convert raw `data` object into a user-defined data type. +/// +/// Useful with embedded content. +typedef DataDecoder = Object? Function(Object data); + +/// Default data decoder which simply passes through the original value. +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'; + + /// Key of delete operations. + static const String deleteKey = 'delete'; + + /// Key of retain operations. + static const String retainKey = 'retain'; + + /// Key of attributes collection. + static const String attributesKey = 'attributes'; + + static const List _validKeys = [insertKey, deleteKey, retainKey]; + + /// Key of this operation, can be "insert", "delete" or "retain". + final String key; + + /// Length of this operation. + final int? length; + + /// Payload of "insert" operation, for other types is set to empty string. + final Object? data; + + /// Rich-text attributes set by this operation, can be `null`. + Map? get attributes => + _attributes == null ? null : Map.from(_attributes!); + final Map? _attributes; + + /// Creates new [Operation] from JSON payload. + /// + /// If `dataDecoder` parameter is not null then it is used to additionally + /// decode the operation's data object. Only applied to insert operations. + static Operation fromJson(Map data, {DataDecoder? dataDecoder}) { + dataDecoder ??= _passThroughDataDecoder; + final map = Map.from(data); + if (map.containsKey(Operation.insertKey)) { + final data = dataDecoder(map[Operation.insertKey]); + final dataLength = data is String ? data.length : 1; + return Operation._( + Operation.insertKey, dataLength, data, map[Operation.attributesKey]); + } else if (map.containsKey(Operation.deleteKey)) { + final int? length = map[Operation.deleteKey]; + return Operation._(Operation.deleteKey, length, '', null); + } else if (map.containsKey(Operation.retainKey)) { + final int? length = map[Operation.retainKey]; + return Operation._( + Operation.retainKey, length, '', map[Operation.attributesKey]); + } + throw ArgumentError.value(data, 'Invalid data for Delta operation.'); + } + + /// Returns JSON-serializable representation of this operation. + Map toJson() { + final json = {key: value}; + if (_attributes != null) json[Operation.attributesKey] = attributes; + return json; + } + + /// Returns value of this operation. + /// + /// For insert operations this returns text, for delete and retain - length. + dynamic get value => (key == Operation.insertKey) ? data : length; + + /// Returns `true` if this is a delete operation. + bool get isDelete => key == Operation.deleteKey; + + /// Returns `true` if this is an insert operation. + bool get isInsert => key == Operation.insertKey; + + /// Returns `true` if this is a retain operation. + bool get isRetain => key == Operation.retainKey; + + /// Returns `true` if this operation has no attributes, e.g. is plain text. + bool get isPlain => _attributes == null || _attributes!.isEmpty; + + /// Returns `true` if this operation sets at least one attribute. + bool get isNotPlain => !isPlain; + + /// Returns `true` is this operation is empty. + /// + /// An operation is considered empty if its [length] is equal to `0`. + bool get isEmpty => length == 0; + + /// Returns `true` is this operation is not empty. + bool get isNotEmpty => length! > 0; + + @override + bool operator ==(other) { + if (identical(this, other)) return true; + if (other is! Operation) return false; + final typedOther = other; + return key == typedOther.key && + length == typedOther.length && + _valueEquality.equals(data, typedOther.data) && + hasSameAttributes(typedOther); + } + + /// Returns `true` if this operation has attribute specified by [name]. + bool hasAttribute(String name) => + isNotPlain && _attributes!.containsKey(name); + + /// Returns `true` if [other] operation has the same attributes as this one. + bool hasSameAttributes(Operation other) { + return _attributeEquality.equals(_attributes, other._attributes); + } + + @override + int get hashCode { + if (_attributes != null && _attributes!.isNotEmpty) { + final attrsHash = + hashObjects(_attributes!.entries.map((e) => hash2(e.key, e.value))); + return hash3(key, value, attrsHash); + } + return hash2(key, value); + } + + @override + String toString() { + final attr = attributes == null ? '' : ' + $attributes'; + final text = isInsert + ? (data is String + ? (data as String).replaceAll('\n', '⏎') + : data.toString()) + : '$length'; + return '$key⟨ $text ⟩$attr'; + } +} + +/// Delta represents a document or a modification of a document as a sequence of +/// insert, delete and retain operations. +/// +/// Delta consisting of only "insert" operations is usually referred to as +/// "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) { + if (a == null) return b; + if (b == null) return null; + + if (!priority) return b; + + final result = b.keys.fold>({}, (attributes, key) { + if (!a.containsKey(key)) attributes[key] = b[key]; + return attributes; + }); + + return result.isEmpty ? null : result; + } + + /// Composes two attribute sets. + static Map? composeAttributes( + Map? a, Map? b, + {bool keepNull = false}) { + a ??= const {}; + b ??= const {}; + + final result = Map.from(a)..addAll(b); + final keys = result.keys.toList(growable: false); + + if (!keepNull) { + for (final key in keys) { + if (result[key] == null) result.remove(key); + } + } + + return result.isEmpty ? null : result; + } + + ///get anti-attr result base on base + static Map invertAttributes( + Map? attr, Map? base) { + attr ??= const {}; + base ??= const {}; + + final baseInverted = base.keys.fold({}, (dynamic memo, key) { + if (base![key] != attr![key] && attr.containsKey(key)) { + memo[key] = base[key]; + } + return memo; + }); + + final inverted = + Map.from(attr.keys.fold(baseInverted, (memo, key) { + if (base![key] != attr![key] && !base.containsKey(key)) { + memo[key] = null; + } + return memo; + })); + return inverted; + } + + final List _operations; + + int _modificationCount = 0; + + /// Creates [Delta] from de-serialized JSON representation. + /// + /// If `dataDecoder` parameter is not null then it is used to additionally + /// decode the operation's data object. Only applied to insert operations. + static Delta fromJson(List data, {DataDecoder? dataDecoder}) { + return Delta._(data + .map((op) => Operation.fromJson(op, dataDecoder: dataDecoder)) + .toList()); + } + + /// Returns list of operations in this delta. + List toList() => List.from(_operations); + + /// Returns JSON-serializable version of this delta. + List toJson() => toList().map((operation) => operation.toJson()).toList(); + + /// Returns `true` if this delta is empty. + bool get isEmpty => _operations.isEmpty; + + /// Returns `true` if this delta is not empty. + bool get isNotEmpty => _operations.isNotEmpty; + + /// Returns number of operations in this delta. + int get length => _operations.length; + + /// Returns [Operation] at specified [index] in this delta. + Operation operator [](int index) => _operations[index]; + + /// Returns [Operation] at specified [index] in this delta. + Operation elementAt(int index) => _operations.elementAt(index); + + /// Returns the first [Operation] in this delta. + Operation get first => _operations.first; + + /// Returns the last [Operation] in this delta. + Operation get last => _operations.last; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! Delta) return false; + final typedOther = other; + const comparator = ListEquality(DefaultEquality()); + return comparator.equals(_operations, typedOther._operations); + } + + @override + int get hashCode => hashObjects(_operations); + + /// Retain [count] of characters from current position. + void retain(int count, [Map? attributes]) { + assert(count >= 0); + if (count == 0) return; // no-op + push(Operation.retain(count, attributes)); + } + + /// Insert [data] at current position. + void insert(dynamic data, [Map? attributes]) { + if (data is String && data.isEmpty) return; // no-op + push(Operation.insert(data, attributes)); + } + + /// Delete [count] characters from current position. + void delete(int count) { + assert(count >= 0); + if (count == 0) return; + push(Operation.delete(count)); + } + + void _mergeWithTail(Operation operation) { + assert(isNotEmpty); + assert(last.key == operation.key); + assert(operation.data is String && last.data is String); + + final length = operation.length! + last.length!; + final lastText = last.data as String; + final opText = operation.data as String; + final resultText = lastText + opText; + final index = _operations.length; + _operations.replaceRange(index - 1, index, [ + Operation._(operation.key, length, resultText, operation.attributes), + ]); + } + + /// Pushes new operation into this delta. + /// + /// Performs compaction by composing [operation] with current tail operation + /// of this delta, when possible. For instance, if current tail is + /// `insert('abc')` and pushed operation is `insert('123')` then existing + /// tail is replaced with `insert('abc123')` - a compound result of the two + /// operations. + void push(Operation operation) { + if (operation.isEmpty) return; + + var index = _operations.length; + final lastOp = _operations.isNotEmpty ? _operations.last : null; + if (lastOp != null) { + if (lastOp.isDelete && operation.isDelete) { + _mergeWithTail(operation); + return; + } + + if (lastOp.isDelete && operation.isInsert) { + index -= 1; // Always insert before deleting + final nLastOp = (index > 0) ? _operations.elementAt(index - 1) : null; + if (nLastOp == null) { + _operations.insert(0, operation); + return; + } + } + + if (lastOp.isInsert && operation.isInsert) { + if (lastOp.hasSameAttributes(operation) && + operation.data is String && + lastOp.data is String) { + _mergeWithTail(operation); + return; + } + } + + if (lastOp.isRetain && operation.isRetain) { + if (lastOp.hasSameAttributes(operation)) { + _mergeWithTail(operation); + return; + } + } + } + if (index == _operations.length) { + _operations.add(operation); + } else { + final opAtIndex = _operations.elementAt(index); + _operations.replaceRange(index, index + 1, [operation, opAtIndex]); + } + _modificationCount++; + } + + /// Composes next operation from [thisIter] and [otherIter]. + /// + /// Returns new operation or `null` if operations from [thisIter] and + /// [otherIter] nullify each other. For instance, for the pair `insert('abc')` + /// and `delete(3)` composition result would be empty string. + Operation? _composeOperation( + DeltaIterator thisIter, DeltaIterator otherIter) { + if (otherIter.isNextInsert) return otherIter.next(); + if (thisIter.isNextDelete) return thisIter.next(); + + final length = math.min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter.next(length as int); + final otherOp = otherIter.next(length); + assert(thisOp.length == otherOp.length); + + if (otherOp.isRetain) { + final attributes = composeAttributes( + thisOp.attributes, + otherOp.attributes, + keepNull: thisOp.isRetain, + ); + if (thisOp.isRetain) { + return Operation.retain(thisOp.length, attributes); + } else if (thisOp.isInsert) { + return Operation.insert(thisOp.data, attributes); + } else { + throw StateError('Unreachable'); + } + } else { + // otherOp == delete && thisOp in [retain, insert] + assert(otherOp.isDelete); + if (thisOp.isRetain) return otherOp; + assert(thisOp.isInsert); + // otherOp(delete) + thisOp(insert) => null + } + return null; + } + + /// Composes this delta with [other] and returns new [Delta]. + /// + /// It is not required for this and [other] delta to represent a document + /// delta (consisting only of insert operations). + Delta compose(Delta other) { + final result = Delta(); + final thisIter = DeltaIterator(this); + final otherIter = DeltaIterator(other); + + while (thisIter.hasNext || otherIter.hasNext) { + final newOp = _composeOperation(thisIter, otherIter); + if (newOp != null) result.push(newOp); + } + return result..trim(); + } + + /// Transforms next operation from [otherIter] against next operation in + /// [thisIter]. + /// + /// Returns `null` if both operations nullify each other. + Operation? _transformOperation( + DeltaIterator thisIter, DeltaIterator otherIter, bool priority) { + if (thisIter.isNextInsert && (priority || !otherIter.isNextInsert)) { + return Operation.retain(thisIter.next().length); + } else if (otherIter.isNextInsert) { + return otherIter.next(); + } + + final length = math.min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter.next(length as int); + final otherOp = otherIter.next(length); + assert(thisOp.length == otherOp.length); + + // At this point only delete and retain operations are possible. + if (thisOp.isDelete) { + // otherOp is either delete or retain, so they nullify each other. + return null; + } else if (otherOp.isDelete) { + return otherOp; + } else { + // Retain otherOp which is either retain or insert. + return Operation.retain( + length, + transformAttributes(thisOp.attributes, otherOp.attributes, priority), + ); + } + } + + /// Transforms [other] delta against operations in this delta. + Delta transform(Delta other, bool priority) { + final result = Delta(); + final thisIter = DeltaIterator(this); + final otherIter = DeltaIterator(other); + + while (thisIter.hasNext || otherIter.hasNext) { + final newOp = _transformOperation(thisIter, otherIter, priority); + if (newOp != null) result.push(newOp); + } + return result..trim(); + } + + /// Removes trailing retain operation with empty attributes, if present. + void trim() { + if (isNotEmpty) { + final last = _operations.last; + if (last.isRetain && last.isPlain) _operations.removeLast(); + } + } + + /// Concatenates [other] with this delta and returns the result. + Delta concat(Delta other) { + final result = Delta.from(this); + if (other.isNotEmpty) { + // In case first operation of other can be merged with last operation in + // our list. + result.push(other._operations.first); + result._operations.addAll(other._operations.sublist(1)); + } + return result; + } + + /// Inverts this delta against [base]. + /// + /// Returns new delta which negates effect of this delta when applied to + /// [base]. This is an equivalent of "undo" operation on deltas. + Delta invert(Delta base) { + final inverted = Delta(); + if (base.isEmpty) return inverted; + + var baseIndex = 0; + for (final op in _operations) { + if (op.isInsert) { + inverted.delete(op.length!); + } else if (op.isRetain && op.isPlain) { + inverted.retain(op.length!); + baseIndex += op.length!; + } else if (op.isDelete || (op.isRetain && op.isNotPlain)) { + final length = op.length!; + final sliceDelta = base.slice(baseIndex, baseIndex + length); + sliceDelta.toList().forEach((baseOp) { + if (op.isDelete) { + inverted.push(baseOp); + } else if (op.isRetain && op.isNotPlain) { + final invertAttr = + invertAttributes(op.attributes, baseOp.attributes); + inverted.retain( + baseOp.length!, invertAttr.isEmpty ? null : invertAttr); + } + }); + baseIndex += length; + } else { + throw StateError('Unreachable'); + } + } + inverted.trim(); + return inverted; + } + + /// Returns slice of this delta from [start] index (inclusive) to [end] + /// (exclusive). + Delta slice(int start, [int? end]) { + final delta = Delta(); + var index = 0; + final opIterator = DeltaIterator(this); + + final actualEnd = end ?? double.infinity; + + while (index < actualEnd && opIterator.hasNext) { + Operation op; + if (index < start) { + op = opIterator.next(start - index); + } else { + op = opIterator.next(actualEnd - index as int); + delta.push(op); + } + index += op.length!; + } + return delta; + } + + /// Transforms [index] against this delta. + /// + /// Any "delete" operation before specified [index] shifts it backward, as + /// well as any "insert" operation shifts it forward. + /// + /// The [force] argument is used to resolve scenarios when there is an + /// insert operation at the same position as [index]. If [force] is set to + /// `true` (default) then position is forced to shift forward, otherwise + /// position stays at the same index. In other words setting [force] to + /// `false` gives higher priority to the transformed position. + /// + /// Useful to adjust caret or selection positions. + int transformPosition(int index, {bool force = true}) { + final iter = DeltaIterator(this); + var offset = 0; + while (iter.hasNext && offset <= index) { + final op = iter.next(); + if (op.isDelete) { + index -= math.min(op.length!, index - offset); + continue; + } else if (op.isInsert && (offset < index || force)) { + index += op.length!; + } + offset += op.length!; + } + return index; + } + + @override + String toString() => _operations.join('\n'); +} + +/// 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; + + bool get isNextInsert => nextOperationKey == Operation.insertKey; + + bool get isNextDelete => nextOperationKey == Operation.deleteKey; + + bool get isNextRetain => nextOperationKey == Operation.retainKey; + + String? get nextOperationKey { + if (_index < delta.length) { + return delta.elementAt(_index).key; + } else { + return null; + } + } + + bool get hasNext => peekLength() < double.infinity; + + /// Returns length of next operation without consuming it. + /// + /// Returns [double.infinity] if there is no more operations left to iterate. + num peekLength() { + if (_index < delta.length) { + final operation = delta._operations[_index]; + return operation.length! - _offset; + } + return double.infinity; + } + + /// Consumes and returns next operation. + /// + /// Optional [length] specifies maximum length of operation to return. Note + /// that actual length of returned operation may be less than specified value. + Operation next([int length = 4294967296]) { + if (_modificationCount != delta._modificationCount) { + throw ConcurrentModificationError(delta); + } + + if (_index < delta.length) { + final op = delta.elementAt(_index); + final opKey = op.key; + final opAttributes = op.attributes; + final _currentOffset = _offset; + final actualLength = math.min(op.length! - _currentOffset, length); + if (actualLength == op.length! - _currentOffset) { + _index++; + _offset = 0; + } else { + _offset += actualLength; + } + final opData = op.isInsert && op.data is String + ? (op.data as String).substring( + _currentOffset as int, _currentOffset + (actualLength as int)) + : op.data; + final opIsNotEmpty = + opData is String ? opData.isNotEmpty : true; // embeds are never empty + final opLength = opData is String ? opData.length : 1; + final opActualLength = opIsNotEmpty ? opLength : actualLength as int; + return Operation._(opKey, opActualLength, opData, opAttributes); + } + return Operation.retain(length); + } + + /// Skips [length] characters in source delta. + /// + /// Returns last skipped operation, or `null` if there was nothing to skip. + Operation? skip(int length) { + var skipped = 0; + Operation? op; + while (skipped < length && hasNext) { + final opLength = peekLength(); + final skip = math.min(length - skipped, opLength); + op = next(skip as int); + skipped += op.length!; + } + return op; + } +} diff --git a/lib/src/models/rules/delete.dart b/lib/src/models/rules/delete.dart new file mode 100644 index 00000000..e6682f94 --- /dev/null +++ b/lib/src/models/rules/delete.dart @@ -0,0 +1,124 @@ +import '../documents/attribute.dart'; +import '../quill_delta.dart'; +import 'rule.dart'; + +abstract class DeleteRule extends Rule { + const DeleteRule(); + + @override + RuleType get type => RuleType.DELETE; + + @override + void validateArgs(int? len, Object? data, Attribute? attribute) { + assert(len != null); + assert(data == null); + assert(attribute == null); + } +} + +class CatchAllDeleteRule extends DeleteRule { + const CatchAllDeleteRule(); + + @override + Delta applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + return Delta() + ..retain(index) + ..delete(len!); + } +} + +class PreserveLineStyleOnMergeRule extends DeleteRule { + const PreserveLineStyleOnMergeRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + final itr = DeltaIterator(document)..skip(index); + var op = itr.next(1); + if (op.data != '\n') { + return null; + } + + final isNotPlain = op.isNotPlain; + final attrs = op.attributes; + + itr.skip(len! - 1); + final delta = Delta() + ..retain(index) + ..delete(len); + + while (itr.hasNext) { + op = itr.next(); + final text = op.data is String ? (op.data as String?)! : ''; + final lineBreak = text.indexOf('\n'); + if (lineBreak == -1) { + delta.retain(op.length!); + continue; + } + + var attributes = op.attributes == null + ? null + : op.attributes!.map( + (key, dynamic value) => MapEntry(key, null)); + + if (isNotPlain) { + attributes ??= {}; + attributes.addAll(attrs!); + } + delta..retain(lineBreak)..retain(1, attributes); + break; + } + return delta; + } +} + +class EnsureEmbedLineRule extends DeleteRule { + const EnsureEmbedLineRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + final itr = DeltaIterator(document); + + var op = itr.skip(index); + int? indexDelta = 0, lengthDelta = 0, remain = len; + var embedFound = op != null && op.data is! String; + final hasLineBreakBefore = + !embedFound && (op == null || (op.data as String).endsWith('\n')); + if (embedFound) { + var candidate = itr.next(1); + if (remain != null) { + remain--; + if (candidate.data == '\n') { + indexDelta++; + lengthDelta--; + + candidate = itr.next(1); + remain--; + if (candidate.data == '\n') { + lengthDelta++; + } + } + } + } + + op = itr.skip(remain!); + if (op != null && + (op.data is String ? op.data as String? : '')!.endsWith('\n')) { + final candidate = itr.next(1); + if (candidate.data is! String && !hasLineBreakBefore) { + embedFound = true; + lengthDelta--; + } + } + + if (!embedFound) { + return null; + } + + return Delta() + ..retain(index + indexDelta) + ..delete(len! + lengthDelta); + } +} diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart new file mode 100644 index 00000000..be201925 --- /dev/null +++ b/lib/src/models/rules/format.dart @@ -0,0 +1,132 @@ +import '../documents/attribute.dart'; +import '../quill_delta.dart'; +import 'rule.dart'; + +abstract class FormatRule extends Rule { + const FormatRule(); + + @override + RuleType get type => RuleType.FORMAT; + + @override + void validateArgs(int? len, Object? data, Attribute? attribute) { + assert(len != null); + assert(data == null); + assert(attribute != null); + } +} + +class ResolveLineFormatRule extends FormatRule { + const ResolveLineFormatRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (attribute!.scope != AttributeScope.BLOCK) { + return null; + } + + var delta = Delta()..retain(index); + final itr = DeltaIterator(document)..skip(index); + Operation op; + for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { + op = itr.next(len - cur); + if (op.data is! String || !(op.data as String).contains('\n')) { + delta.retain(op.length!); + continue; + } + final text = op.data as String; + final tmp = Delta(); + var offset = 0; + + for (var lineBreak = text.indexOf('\n'); + lineBreak >= 0; + lineBreak = text.indexOf('\n', offset)) { + tmp..retain(lineBreak - offset)..retain(1, attribute.toJson()); + offset = lineBreak + 1; + } + tmp.retain(text.length - offset); + delta = delta.concat(tmp); + } + + while (itr.hasNext) { + op = itr.next(); + final text = op.data is String ? (op.data as String?)! : ''; + final lineBreak = text.indexOf('\n'); + if (lineBreak < 0) { + delta.retain(op.length!); + continue; + } + delta..retain(lineBreak)..retain(1, attribute.toJson()); + break; + } + return delta; + } +} + +class FormatLinkAtCaretPositionRule extends FormatRule { + const FormatLinkAtCaretPositionRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (attribute!.key != Attribute.link.key || len! > 0) { + return null; + } + + 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!; + retain = before.length; + } + if (after.hasAttribute(attribute.key)) { + if (retain != null) retain += after.length!; + } + if (retain == 0) { + return null; + } + + delta..retain(beg)..retain(retain!, attribute.toJson()); + return delta; + } +} + +class ResolveInlineFormatRule extends FormatRule { + const ResolveInlineFormatRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (attribute!.scope != AttributeScope.INLINE) { + return null; + } + + final delta = Delta()..retain(index); + final itr = DeltaIterator(document)..skip(index); + + Operation op; + for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { + op = itr.next(len - cur); + 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; + } + var pos = 0; + while (lineBreak >= 0) { + delta..retain(lineBreak - pos, attribute.toJson())..retain(1); + pos = lineBreak + 1; + lineBreak = text.indexOf('\n', pos); + } + if (pos < op.length!) { + delta.retain(op.length! - pos, attribute.toJson()); + } + } + + return delta; + } +} diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart new file mode 100644 index 00000000..5801a10e --- /dev/null +++ b/lib/src/models/rules/insert.dart @@ -0,0 +1,413 @@ +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(); + + @override + RuleType get type => RuleType.INSERT; + + @override + void validateArgs(int? len, Object? data, Attribute? attribute) { + assert(data != null); + assert(attribute == null); + } +} + +class PreserveLineStyleOnSplitRule extends InsertRule { + const PreserveLineStyleOnSplitRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data != '\n') { + return null; + } + + final itr = DeltaIterator(document); + final before = itr.skip(index); + if (before == null || + before.data is! String || + (before.data as String).endsWith('\n')) { + return null; + } + final after = itr.next(); + if (after.data is! String || (after.data as String).startsWith('\n')) { + return null; + } + + final text = after.data as String; + + final delta = Delta()..retain(index + (len ?? 0)); + if (text.contains('\n')) { + assert(after.isPlain); + delta.insert('\n'); + return delta; + } + final nextNewLine = _getNextNewLine(itr); + final attributes = nextNewLine.item1?.attributes; + + return delta..insert('\n', attributes); + } +} + +/// Preserves block style when user inserts text containing newlines. +/// +/// This rule handles: +/// +/// * inserting a new line in a block +/// * pasting text containing multiple lines of text in a block +/// +/// This rule may also be activated for changes triggered by auto-correct. +class PreserveBlockStyleOnInsertRule extends InsertRule { + const PreserveBlockStyleOnInsertRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || !data.contains('\n')) { + // Only interested in text containing at least one newline character. + return null; + } + + final itr = DeltaIterator(document)..skip(index); + + // Look for the next newline. + final nextNewLine = _getNextNewLine(itr); + final lineStyle = + Style.fromJson(nextNewLine.item1?.attributes ?? {}); + + final blockStyle = lineStyle.getBlocksExceptHeader(); + // Are we currently in a block? If not then ignore. + if (blockStyle.isEmpty) { + return null; + } + + Map? resetStyle; + // If current line had heading style applied to it we'll need to move this + // style to the newly inserted line before it and reset style of the + // original line. + if (lineStyle.containsKey(Attribute.header.key)) { + resetStyle = Attribute.header.toJson(); + } + + // Go over each inserted line and ensure block style is applied. + final lines = data.split('\n'); + final delta = Delta()..retain(index + (len ?? 0)); + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; + if (line.isNotEmpty) { + delta.insert(line); + } + if (i == 0) { + // The first line should inherit the lineStyle entirely. + delta.insert('\n', lineStyle.toJson()); + } else if (i < lines.length - 1) { + // we don't want to insert a newline after the last chunk of text, so -1 + delta.insert('\n', blockStyle); + } + } + + // Reset style of the original newline character if needed. + if (resetStyle != null) { + delta + ..retain(nextNewLine.item2!) + ..retain((nextNewLine.item1!.data as String).indexOf('\n')) + ..retain(1, resetStyle); + } + + return delta; + } +} + +/// Heuristic rule to exit current block when user inserts two consecutive +/// newlines. +/// +/// This rule is only applied when the cursor is on the last line of a block. +/// When the cursor is in the middle of a block we allow adding empty lines +/// and preserving the block's style. +class AutoExitBlockRule extends InsertRule { + const AutoExitBlockRule(); + + bool _isEmptyLine(Operation? before, Operation? after) { + if (before == null) { + return true; + } + return before.data is String && + (before.data as String).endsWith('\n') && + after!.data is String && + (after.data as String).startsWith('\n'); + } + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data != '\n') { + return null; + } + + final itr = DeltaIterator(document); + final prev = itr.skip(index), cur = itr.next(); + final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader(); + // We are not in a block, ignore. + if (cur.isPlain || blockStyle == null) { + return null; + } + // We are not on an empty line, ignore. + if (!_isEmptyLine(prev, cur)) { + return null; + } + + // We are on an empty line. Now we need to determine if we are on the + // last line of a block. + // First check if `cur` length is greater than 1, this would indicate + // that it contains multiple newline characters which share the same style. + // This would mean we are not on the last line yet. + // `cur.value as String` is safe since we already called isEmptyLine and know it contains a newline + if ((cur.value as String).length > 1) { + // We are not on the last line of this block, ignore. + return null; + } + + // Keep looking for the next newline character to see if it shares the same + // block style as `cur`. + final nextNewLine = _getNextNewLine(itr); + if (nextNewLine.item1 != null && + nextNewLine.item1!.attributes != null && + Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() == + blockStyle) { + // We are not at the end of this block, ignore. + return null; + } + + // Here we now know that the line after `cur` is not in the same block + // therefore we can exit this block. + final attributes = cur.attributes ?? {}; + final k = attributes.keys + .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k)); + attributes[k] = null; + // retain(1) should be '\n', set it with no attribute + return Delta()..retain(index + (len ?? 0))..retain(1, attributes); + } +} + +class ResetLineFormatOnNewLineRule extends InsertRule { + const ResetLineFormatOnNewLineRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data != '\n') { + return null; + } + + final itr = DeltaIterator(document)..skip(index); + final cur = itr.next(); + if (cur.data is! String || !(cur.data as String).startsWith('\n')) { + return null; + } + + Map? resetStyle; + if (cur.attributes != null && + cur.attributes!.containsKey(Attribute.header.key)) { + resetStyle = Attribute.header.toJson(); + } + return Delta() + ..retain(index + (len ?? 0)) + ..insert('\n', cur.attributes) + ..retain(1, resetStyle) + ..trim(); + } +} + +class InsertEmbedsRule extends InsertRule { + const InsertEmbedsRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is String) { + return null; + } + + final delta = Delta()..retain(index + (len ?? 0)); + final itr = DeltaIterator(document); + final prev = itr.skip(index), cur = itr.next(); + + 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'); + + if (isNewlineBefore && isNewlineAfter) { + return delta..insert(data); + } + + Map? lineStyle; + if (textAfter.contains('\n')) { + lineStyle = cur.attributes; + } else { + while (itr.hasNext) { + final op = itr.next(); + if ((op.data is String ? op.data as String? : '')!.contains('\n')) { + lineStyle = op.attributes; + break; + } + } + } + + if (!isNewlineBefore) { + delta.insert('\n', lineStyle); + } + delta.insert(data); + if (!isNewlineAfter) { + delta.insert('\n'); + } + return delta; + } +} + +class ForceNewlineForInsertsAroundEmbedRule extends InsertRule { + const ForceNewlineForInsertsAroundEmbedRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String) { + return null; + } + + final text = data; + final itr = DeltaIterator(document); + final prev = itr.skip(index); + final cur = itr.next(); + final cursorBeforeEmbed = cur.data is! String; + final cursorAfterEmbed = prev != null && prev.data is! String; + + if (!cursorBeforeEmbed && !cursorAfterEmbed) { + return null; + } + final delta = Delta()..retain(index + (len ?? 0)); + if (cursorBeforeEmbed && !text.endsWith('\n')) { + return delta..insert(text)..insert('\n'); + } + if (cursorAfterEmbed && !text.startsWith('\n')) { + return delta..insert('\n')..insert(text); + } + return delta..insert(text); + } +} + +class AutoFormatLinksRule extends InsertRule { + const AutoFormatLinksRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data != ' ') { + return null; + } + + final itr = DeltaIterator(document); + final prev = itr.skip(index); + if (prev == null || prev.data is! String) { + return null; + } + + try { + final cand = (prev.data as String).split('\n').last.split(' ').last; + final link = Uri.parse(cand); + if (!['https', 'http'].contains(link.scheme)) { + return null; + } + final attributes = prev.attributes ?? {}; + + if (attributes.containsKey(Attribute.link.key)) { + return null; + } + + attributes.addAll(LinkAttribute(link.toString()).toJson()); + return Delta() + ..retain(index + (len ?? 0) - cand.length) + ..retain(cand.length, attributes) + ..insert(data, prev.attributes); + } on FormatException { + return null; + } + } +} + +class PreserveInlineStylesRule extends InsertRule { + const PreserveInlineStylesRule(); + + @override + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + if (data is! String || data.contains('\n')) { + return null; + } + + final itr = DeltaIterator(document); + final prev = itr.skip(index); + if (prev == null || + prev.data is! String || + (prev.data as String).contains('\n')) { + return null; + } + + final attributes = prev.attributes; + final text = data; + if (attributes == null || !attributes.containsKey(Attribute.link.key)) { + return Delta() + ..retain(index + (len ?? 0)) + ..insert(text, attributes); + } + + attributes.remove(Attribute.link.key); + final delta = Delta() + ..retain(index + (len ?? 0)) + ..insert(text, attributes.isEmpty ? null : attributes); + final next = itr.next(); + + final nextAttributes = next.attributes ?? const {}; + if (!nextAttributes.containsKey(Attribute.link.key)) { + return delta; + } + if (attributes[Attribute.link.key] == nextAttributes[Attribute.link.key]) { + return Delta() + ..retain(index + (len ?? 0)) + ..insert(text, attributes); + } + return delta; + } +} + +class CatchAllInsertRule extends InsertRule { + const CatchAllInsertRule(); + + @override + Delta applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + return Delta() + ..retain(index + (len ?? 0)) + ..insert(data); + } +} + +Tuple2 _getNextNewLine(DeltaIterator iterator) { + Operation op; + for (var skipped = 0; iterator.hasNext; skipped += op.length!) { + op = iterator.next(); + final lineBreak = + (op.data is String ? op.data as String? : '')!.indexOf('\n'); + if (lineBreak >= 0) { + return Tuple2(op, skipped); + } + } + return const Tuple2(null, null); +} diff --git a/lib/src/models/rules/rule.dart b/lib/src/models/rules/rule.dart new file mode 100644 index 00000000..042f1aaa --- /dev/null +++ b/lib/src/models/rules/rule.dart @@ -0,0 +1,77 @@ +import '../documents/attribute.dart'; +import '../documents/document.dart'; +import '../quill_delta.dart'; +import 'delete.dart'; +import 'format.dart'; +import 'insert.dart'; + +enum RuleType { INSERT, DELETE, FORMAT } + +abstract class Rule { + const Rule(); + + Delta? apply(Delta document, int index, + {int? len, Object? data, Attribute? attribute}) { + validateArgs(len, data, attribute); + return applyRule(document, index, + len: len, data: data, attribute: attribute); + } + + void validateArgs(int? len, Object? data, Attribute? attribute); + + Delta? applyRule(Delta document, int index, + {int? len, Object? data, Attribute? attribute}); + + RuleType get type; +} + +class Rules { + Rules(this._rules); + + List _customRules = []; + + final List _rules; + static final Rules _instance = Rules([ + const FormatLinkAtCaretPositionRule(), + const ResolveLineFormatRule(), + const ResolveInlineFormatRule(), + const InsertEmbedsRule(), + const ForceNewlineForInsertsAroundEmbedRule(), + const AutoExitBlockRule(), + const PreserveBlockStyleOnInsertRule(), + const PreserveLineStyleOnSplitRule(), + const ResetLineFormatOnNewLineRule(), + const AutoFormatLinksRule(), + const PreserveInlineStylesRule(), + const CatchAllInsertRule(), + const EnsureEmbedLineRule(), + const PreserveLineStyleOnMergeRule(), + const CatchAllDeleteRule(), + ]); + + static Rules getInstance() => _instance; + + void setCustomRules(List customRules) { + _customRules = customRules; + } + + Delta apply(RuleType ruleType, Document document, int index, + {int? len, Object? data, Attribute? attribute}) { + final delta = document.toDelta(); + for (final rule in _customRules + _rules) { + if (rule.type != ruleType) { + continue; + } + try { + final result = rule.apply(delta, index, + len: len, data: data, attribute: attribute); + if (result != null) { + return result..trim(); + } + } catch (e) { + rethrow; + } + } + throw 'Apply rules failed'; + } +} diff --git a/lib/src/utils/color.dart b/lib/src/utils/color.dart new file mode 100644 index 00000000..93b6e12b --- /dev/null +++ b/lib/src/utils/color.dart @@ -0,0 +1,125 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +Color stringToColor(String? s) { + switch (s) { + case 'transparent': + return Colors.transparent; + case 'black': + return Colors.black; + case 'black12': + return Colors.black12; + case 'black26': + return Colors.black26; + case 'black38': + return Colors.black38; + case 'black45': + return Colors.black45; + case 'black54': + return Colors.black54; + case 'black87': + return Colors.black87; + case 'white': + return Colors.white; + case 'white10': + return Colors.white10; + case 'white12': + return Colors.white12; + case 'white24': + return Colors.white24; + case 'white30': + return Colors.white30; + case 'white38': + return Colors.white38; + case 'white54': + return Colors.white54; + case 'white60': + return Colors.white60; + case 'white70': + return Colors.white70; + case 'red': + return Colors.red; + case 'redAccent': + return Colors.redAccent; + case 'amber': + return Colors.amber; + case 'amberAccent': + return Colors.amberAccent; + case 'yellow': + return Colors.yellow; + case 'yellowAccent': + return Colors.yellowAccent; + case 'teal': + return Colors.teal; + case 'tealAccent': + return Colors.tealAccent; + case 'purple': + return Colors.purple; + case 'purpleAccent': + return Colors.purpleAccent; + case 'pink': + return Colors.pink; + case 'pinkAccent': + return Colors.pinkAccent; + case 'orange': + return Colors.orange; + case 'orangeAccent': + return Colors.orangeAccent; + case 'deepOrange': + return Colors.deepOrange; + case 'deepOrangeAccent': + return Colors.deepOrangeAccent; + case 'indigo': + return Colors.indigo; + case 'indigoAccent': + return Colors.indigoAccent; + case 'lime': + return Colors.lime; + case 'limeAccent': + return Colors.limeAccent; + case 'grey': + return Colors.grey; + case 'blueGrey': + return Colors.blueGrey; + case 'green': + return Colors.green; + case 'greenAccent': + return Colors.greenAccent; + case 'lightGreen': + return Colors.lightGreen; + case 'lightGreenAccent': + return Colors.lightGreenAccent; + case 'blue': + return Colors.blue; + case 'blueAccent': + return Colors.blueAccent; + case 'lightBlue': + return Colors.lightBlue; + case 'lightBlueAccent': + return Colors.lightBlueAccent; + case 'cyan': + return Colors.cyan; + case 'cyanAccent': + return Colors.cyanAccent; + case 'brown': + return Colors.brown; + } + + if (s!.startsWith('rgba')) { + s = s.substring(5); // trim left 'rgba(' + s = s.substring(0, s.length - 1); // trim right ')' + final arr = s.split(',').map((e) => e.trim()).toList(); + return Color.fromRGBO(int.parse(arr[0]), int.parse(arr[1]), + int.parse(arr[2]), double.parse(arr[3])); + } + + if (!s.startsWith('#')) { + throw 'Color code not supported'; + } + + 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/src/utils/diff_delta.dart b/lib/src/utils/diff_delta.dart new file mode 100644 index 00000000..003bae47 --- /dev/null +++ b/lib/src/utils/diff_delta.dart @@ -0,0 +1,102 @@ +import 'dart:math' as math; + +import '../models/quill_delta.dart'; + +const Set WHITE_SPACE = { + 0x9, + 0xA, + 0xB, + 0xC, + 0xD, + 0x1C, + 0x1D, + 0x1E, + 0x1F, + 0x20, + 0xA0, + 0x1680, + 0x2000, + 0x2001, + 0x2002, + 0x2003, + 0x2004, + 0x2005, + 0x2006, + 0x2007, + 0x2008, + 0x2009, + 0x200A, + 0x202F, + 0x205F, + 0x3000 +}; + +// Diff between two texts - old text and new text +class Diff { + Diff(this.start, this.deleted, this.inserted); + + // Start index in old text at which changes begin. + final int start; + + /// The deleted text + final String deleted; + + // The inserted text + final String inserted; + + @override + String toString() { + return 'Diff[$start, "$deleted", "$inserted"]'; + } +} + +/* Get diff operation between old text and new text */ +Diff getDiff(String oldText, String newText, int cursorPosition) { + 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--) {} + var start = 0; + for (final startLimit = cursorPosition - math.max(0, delta); + start < startLimit && oldText[start] == newText[start]; + start++) {} + final deleted = (start >= end) ? '' : oldText.substring(start, end); + final inserted = newText.substring(start, end + delta); + return Diff(start, deleted, inserted); +} + +int getPositionDelta(Delta user, Delta actual) { + if (actual.isEmpty) { + return 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()); + final userOperation = userItr.next(length as int); + final actualOperation = actualItr.next(length); + if (userOperation.length != actualOperation.length) { + throw 'userOp ${userOperation.length} does not match actualOp ${actualOperation.length}'; + } + if (userOperation.key == actualOperation.key) { + continue; + } else if (userOperation.isInsert && actualOperation.isRetain) { + diff -= userOperation.length!; + } else if (userOperation.isDelete && actualOperation.isRetain) { + diff += userOperation.length!; + } else if (userOperation.isRetain && actualOperation.isInsert) { + String? operationTxt = ''; + if (actualOperation.data is String) { + operationTxt = actualOperation.data as String?; + } + if (operationTxt!.startsWith('\n')) { + continue; + } + diff += actualOperation.length!; + } + } + return diff; +} diff --git a/lib/src/widgets/box.dart b/lib/src/widgets/box.dart new file mode 100644 index 00000000..75547923 --- /dev/null +++ b/lib/src/widgets/box.dart @@ -0,0 +1,39 @@ +import 'package:flutter/rendering.dart'; + +import '../models/documents/nodes/container.dart'; + +abstract class RenderContentProxyBox implements RenderBox { + double getPreferredLineHeight(); + + Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype); + + TextPosition getPositionForOffset(Offset offset); + + double? getFullHeightForCaret(TextPosition position); + + TextRange getWordBoundary(TextPosition position); + + List getBoxesForSelection(TextSelection textSelection); +} + +abstract class RenderEditableBox extends RenderBox { + Container getContainer(); + + double preferredLineHeight(TextPosition position); + + Offset getOffsetForCaret(TextPosition position); + + TextPosition getPositionForOffset(Offset offset); + + TextPosition? getPositionAbove(TextPosition position); + + TextPosition? getPositionBelow(TextPosition position); + + TextRange getWordBoundary(TextPosition position); + + TextRange getLineBoundary(TextPosition position); + + TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection); + + TextSelectionPoint getExtentEndpointForSelection(TextSelection textSelection); +} diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart new file mode 100644 index 00000000..bd669171 --- /dev/null +++ b/lib/src/widgets/controller.dart @@ -0,0 +1,229 @@ +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:tuple/tuple.dart'; + +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, + this.iconSize = 18, + this.toolbarHeightFactor = 2}); + + factory QuillController.basic() { + return QuillController( + document: Document(), + selection: const TextSelection.collapsed(offset: 0), + ); + } + + final Document document; + TextSelection selection; + double iconSize; + double toolbarHeightFactor; + + Style toggledStyle = Style(); + bool ignoreFocusOnTextChange = false; + + /// Controls whether this [QuillController] instance has already been disposed + /// of + /// + /// This is a safe approach to make sure that listeners don't crash when + /// adding, removing or listeners to this instance. + bool _isDisposed = false; + + // item1: Document state before [change]. + // + // item2: Change delta applied to the document. + // + // item3: The source of this change. + Stream> get changes => document.changes; + + TextEditingValue get plainTextEditingValue => TextEditingValue( + text: document.toPlainText(), + selection: selection, + ); + + Style getSelectionStyle() { + return document + .collectStyle(selection.start, selection.end - selection.start) + .mergeAll(toggledStyle); + } + + void undo() { + final tup = document.undo(); + if (tup.item1) { + _handleHistoryChange(tup.item2); + } + } + + void _handleHistoryChange(int? len) { + if (len != 0) { + // if (this.selection.extentOffset >= document.length) { + // // cursor exceeds the length of document, position it in the end + // updateSelection( + // TextSelection.collapsed(offset: document.length), ChangeSource.LOCAL); + updateSelection( + TextSelection.collapsed(offset: selection.baseOffset + len!), + ChangeSource.LOCAL); + } else { + // no need to move cursor + notifyListeners(); + } + } + + void redo() { + final tup = document.redo(); + if (tup.item1) { + _handleHistoryChange(tup.item2); + } + } + + bool get hasUndo => document.hasUndo; + + bool get hasRedo => document.hasRedo; + + void replaceText( + int index, int len, Object? data, TextSelection? textSelection, + {bool ignoreFocus = false, bool autoAppendNewlineAfterImage = true}) { + assert(data is String || data is Embeddable); + + Delta? delta; + if (len > 0 || data is! String || data.isNotEmpty) { + delta = document.replace(index, len, data, + autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); + var shouldRetainDelta = toggledStyle.isNotEmpty && + delta.isNotEmpty && + delta.length <= 2 && + delta.last.isInsert; + if (shouldRetainDelta && + toggledStyle.isNotEmpty && + delta.length == 2 && + delta.last.data == '\n') { + // if all attributes are inline, shouldRetainDelta should be false + final anyAttributeNotInline = + toggledStyle.values.any((attr) => !attr.isInline); + if (!anyAttributeNotInline) { + shouldRetainDelta = false; + } + } + if (shouldRetainDelta) { + final retainDelta = Delta() + ..retain(index) + ..retain(data is String ? data.length : 1, toggledStyle.toJson()); + document.compose(retainDelta, ChangeSource.LOCAL); + } + } + + toggledStyle = Style(); + if (textSelection != null) { + if (delta == null || delta.isEmpty) { + _updateSelection(textSelection, ChangeSource.LOCAL); + } else { + final user = Delta() + ..retain(index) + ..insert(data) + ..delete(len); + final positionDelta = getPositionDelta(user, delta); + _updateSelection( + textSelection.copyWith( + baseOffset: textSelection.baseOffset + positionDelta, + extentOffset: textSelection.extentOffset + positionDelta, + ), + ChangeSource.LOCAL, + ); + } + } + + if (ignoreFocus) { + ignoreFocusOnTextChange = true; + } + notifyListeners(); + ignoreFocusOnTextChange = false; + } + + void formatText(int index, int len, Attribute? attribute) { + if (len == 0 && + attribute!.isInline && + attribute.key != Attribute.link.key) { + toggledStyle = toggledStyle.put(attribute); + } + + final change = document.format(index, len, attribute); + final adjustedSelection = selection.copyWith( + baseOffset: change.transformPosition(selection.baseOffset), + extentOffset: change.transformPosition(selection.extentOffset)); + if (selection != adjustedSelection) { + _updateSelection(adjustedSelection, ChangeSource.LOCAL); + } + notifyListeners(); + } + + void formatSelection(Attribute? attribute) { + formatText(selection.start, selection.end - selection.start, attribute); + } + + void updateSelection(TextSelection textSelection, ChangeSource source) { + _updateSelection(textSelection, source); + notifyListeners(); + } + + void compose(Delta delta, TextSelection textSelection, ChangeSource source) { + if (delta.isNotEmpty) { + document.compose(delta, source); + } + + textSelection = selection.copyWith( + baseOffset: delta.transformPosition(selection.baseOffset, force: false), + extentOffset: + delta.transformPosition(selection.extentOffset, force: false)); + if (selection != textSelection) { + _updateSelection(textSelection, source); + } + + notifyListeners(); + } + + @override + void addListener(VoidCallback listener) { + // By using `_isDisposed`, make sure that `addListener` won't be called on a + // disposed `ChangeListener` + if (!_isDisposed) { + super.addListener(listener); + } + } + + @override + void removeListener(VoidCallback listener) { + // By using `_isDisposed`, make sure that `removeListener` won't be called + // on a disposed `ChangeListener` + if (!_isDisposed) { + super.removeListener(listener); + } + } + + @override + void dispose() { + if (!_isDisposed) { + document.close(); + } + + _isDisposed = true; + super.dispose(); + } + + void _updateSelection(TextSelection textSelection, ChangeSource source) { + selection = textSelection; + final end = document.length - 1; + selection = selection.copyWith( + baseOffset: math.min(selection.baseOffset, end), + extentOffset: math.min(selection.extentOffset, end)); + } +} diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart new file mode 100644 index 00000000..963bc2e7 --- /dev/null +++ b/lib/src/widgets/cursor.dart @@ -0,0 +1,231 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'box.dart'; + +const Duration _FADE_DURATION = Duration(milliseconds: 250); + +class CursorStyle { + const CursorStyle({ + required this.color, + required this.backgroundColor, + this.width = 1.0, + this.height, + this.radius, + this.offset, + this.opacityAnimates = false, + 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) || + other is CursorStyle && + runtimeType == other.runtimeType && + color == other.color && + backgroundColor == other.backgroundColor && + width == other.width && + height == other.height && + radius == other.radius && + offset == other.offset && + opacityAnimates == other.opacityAnimates && + paintAboveText == other.paintAboveText; + + @override + int get hashCode => + color.hashCode ^ + backgroundColor.hashCode ^ + width.hashCode ^ + height.hashCode ^ + radius.hashCode ^ + offset.hashCode ^ + opacityAnimates.hashCode ^ + paintAboveText.hashCode; +} + +class CursorCont extends ChangeNotifier { + CursorCont({ + required this.show, + required CursorStyle style, + required TickerProvider tickerProvider, + }) : _style = style, + _blink = ValueNotifier(false), + color = ValueNotifier(style.color) { + _blinkOpacityCont = + AnimationController(vsync: tickerProvider, duration: _FADE_DURATION); + _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; + + CursorStyle get style => _style; + + set style(CursorStyle value) { + if (_style == value) return; + _style = value; + notifyListeners(); + } + + @override + void dispose() { + _blinkOpacityCont.removeListener(_onColorTick); + stopCursorTimer(); + _blinkOpacityCont.dispose(); + assert(_cursorTimer == null); + super.dispose(); + } + + void _cursorTick(Timer timer) { + _targetCursorVisibility = !_targetCursorVisibility; + final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; + if (style.opacityAnimates) { + _blinkOpacityCont.animateTo(targetOpacity, curve: Curves.easeOut); + } else { + _blinkOpacityCont.value = targetOpacity; + } + } + + void _cursorWaitForStart(Timer timer) { + _cursorTimer?.cancel(); + _cursorTimer = + Timer.periodic(const Duration(milliseconds: 500), _cursorTick); + } + + void startCursorTimer() { + _targetCursorVisibility = true; + _blinkOpacityCont.value = 1.0; + + if (style.opacityAnimates) { + _cursorTimer = Timer.periodic( + const Duration(milliseconds: 150), _cursorWaitForStart); + } else { + _cursorTimer = + Timer.periodic(const Duration(milliseconds: 500), _cursorTick); + } + } + + void stopCursorTimer({bool resetCharTicks = true}) { + _cursorTimer?.cancel(); + _cursorTimer = null; + _targetCursorVisibility = false; + _blinkOpacityCont.value = 0.0; + + if (style.opacityAnimates) { + _blinkOpacityCont + ..stop() + ..value = 0.0; + } + } + + void startOrStopCursorTimerIfNeeded(bool hasFocus, TextSelection selection) { + if (show.value && + _cursorTimer == null && + hasFocus && + selection.isCollapsed) { + startCursorTimer(); + } else if (_cursorTimer != null && (!hasFocus || !selection.isCollapsed)) { + stopCursorTimer(); + } + } + + void _onColorTick() { + color.value = _style.color.withOpacity(_blinkOpacityCont.value); + _blink.value = show.value && _blinkOpacityCont.value > 0; + } +} + +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; + + void paint(Canvas canvas, Offset offset, TextPosition position) { + assert(prototype != null); + + final caretOffset = + editable!.getOffsetForCaret(position, prototype) + offset; + 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)); + } + + final caretHeight = editable!.getFullHeightForCaret(position); + if (caretHeight != null) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + caretRect = Rect.fromLTWH( + caretRect.left, + caretRect.top - 2.0, + caretRect.width, + caretHeight, + ); + break; + case TargetPlatform.iOS: + case TargetPlatform.macOS: + caretRect = Rect.fromLTWH( + caretRect.left, + caretRect.top + (caretHeight - caretRect.height) / 2, + caretRect.width, + caretRect.height, + ); + break; + default: + throw UnimplementedError(); + } + } + + final caretPosition = editable!.localToGlobal(caretRect.topLeft); + final pixelMultiple = 1.0 / devicePixelRatio; + caretRect = caretRect.shift(Offset( + caretPosition.dx.isFinite + ? (caretPosition.dx / pixelMultiple).round() * pixelMultiple - + caretPosition.dx + : caretPosition.dx, + caretPosition.dy.isFinite + ? (caretPosition.dy / pixelMultiple).round() * pixelMultiple - + caretPosition.dy + : caretPosition.dy)); + + final paint = Paint()..color = color; + if (style.radius == null) { + canvas.drawRect(caretRect, paint); + return; + } + + final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!); + canvas.drawRRect(caretRRect, paint); + } +} diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart new file mode 100644 index 00000000..1cebe135 --- /dev/null +++ b/lib/src/widgets/default_styles.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tuple/tuple.dart'; + +class QuillStyles extends InheritedWidget { + 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) { + final widget = context.dependOnInheritedWidgetOfExactType(); + if (widget == null && nullOk) { + return null; + } + assert(widget != null); + return widget!.data; + } +} + +class DefaultTextBlockStyle { + DefaultTextBlockStyle( + this.style, + this.verticalSpacing, + this.lineSpacing, + this.decoration, + ); + + final TextStyle style; + + final Tuple2 verticalSpacing; + + final Tuple2 lineSpacing; + + final BoxDecoration? 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; + final DefaultTextBlockStyle? paragraph; + final TextStyle? bold; + final TextStyle? italic; + final TextStyle? underline; + final TextStyle? strikeThrough; + final TextStyle? sizeSmall; // 'small' + final TextStyle? sizeLarge; // 'large' + final TextStyle? sizeHuge; // 'huge' + final TextStyle? link; + final Color? color; + final DefaultTextBlockStyle? placeHolder; + final DefaultTextBlockStyle? lists; + final DefaultTextBlockStyle? quote; + final DefaultTextBlockStyle? code; + final DefaultTextBlockStyle? indent; + final DefaultTextBlockStyle? align; + final DefaultTextBlockStyle? leading; + + static DefaultStyles getInstance(BuildContext context) { + final themeData = Theme.of(context); + final defaultTextStyle = DefaultTextStyle.of(context); + final baseStyle = defaultTextStyle.style.copyWith( + fontSize: 16, + height: 1.3, + ); + const baseSpacing = Tuple2(6, 0); + String fontFamily; + switch (themeData.platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + fontFamily = 'Menlo'; + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + case TargetPlatform.linux: + fontFamily = 'Roboto Mono'; + break; + default: + throw UnimplementedError(); + } + + return DefaultStyles( + h1: DefaultTextBlockStyle( + defaultTextStyle.style.copyWith( + fontSize: 34, + color: defaultTextStyle.style.color!.withOpacity(0.70), + height: 1.15, + fontWeight: FontWeight.w300, + ), + const Tuple2(16, 0), + const Tuple2(0, 0), + null), + h2: DefaultTextBlockStyle( + defaultTextStyle.style.copyWith( + fontSize: 24, + color: defaultTextStyle.style.color!.withOpacity(0.70), + height: 1.15, + fontWeight: FontWeight.normal, + ), + const Tuple2(8, 0), + const Tuple2(0, 0), + null), + h3: DefaultTextBlockStyle( + defaultTextStyle.style.copyWith( + fontSize: 20, + color: defaultTextStyle.style.color!.withOpacity(0.70), + height: 1.25, + fontWeight: FontWeight.w500, + ), + const Tuple2(8, 0), + const Tuple2(0, 0), + null), + paragraph: DefaultTextBlockStyle( + 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), + strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), + link: TextStyle( + color: themeData.accentColor, + decoration: TextDecoration.underline, + ), + placeHolder: DefaultTextBlockStyle( + defaultTextStyle.style.copyWith( + fontSize: 20, + height: 1.5, + color: Colors.grey.withOpacity(0.6), + ), + const Tuple2(0, 0), + const Tuple2(0, 0), + null), + lists: DefaultTextBlockStyle( + baseStyle, baseSpacing, const Tuple2(0, 6), null), + quote: DefaultTextBlockStyle( + TextStyle(color: baseStyle.color!.withOpacity(0.6)), + baseSpacing, + const Tuple2(6, 2), + BoxDecoration( + border: Border( + left: BorderSide(width: 4, color: Colors.grey.shade300), + ), + )), + code: DefaultTextBlockStyle( + TextStyle( + color: Colors.blue.shade900.withOpacity(0.9), + fontFamily: fontFamily, + fontSize: 13, + height: 1.15, + ), + baseSpacing, + const Tuple2(0, 0), + BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(2), + )), + indent: DefaultTextBlockStyle( + baseStyle, baseSpacing, const Tuple2(0, 6), null), + align: DefaultTextBlockStyle( + baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), + leading: DefaultTextBlockStyle( + 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) { + return DefaultStyles( + h1: other.h1 ?? h1, + h2: other.h2 ?? h2, + h3: other.h3 ?? h3, + paragraph: other.paragraph ?? paragraph, + bold: other.bold ?? bold, + italic: other.italic ?? italic, + underline: other.underline ?? underline, + strikeThrough: other.strikeThrough ?? strikeThrough, + link: other.link ?? link, + color: other.color ?? color, + placeHolder: other.placeHolder ?? placeHolder, + lists: other.lists ?? lists, + quote: other.quote ?? quote, + code: other.code ?? code, + indent: other.indent ?? indent, + align: other.align ?? align, + leading: other.leading ?? leading, + sizeSmall: other.sizeSmall ?? sizeSmall, + sizeLarge: other.sizeLarge ?? sizeLarge, + sizeHuge: other.sizeHuge ?? sizeHuge); + } +} diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart new file mode 100644 index 00000000..4b4bdea7 --- /dev/null +++ b/lib/src/widgets/delegate.dart @@ -0,0 +1,148 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../models/documents/nodes/leaf.dart'; +import 'editor.dart'; +import 'text_selection.dart'; + +typedef EmbedBuilder = Widget Function(BuildContext context, Embed node); + +abstract class EditorTextSelectionGestureDetectorBuilderDelegate { + GlobalKey getEditableTextKey(); + + bool getForcePressEnabled(); + + bool getSelectionEnabled(); +} + +class EditorTextSelectionGestureDetectorBuilder { + EditorTextSelectionGestureDetectorBuilder(this.delegate); + + final EditorTextSelectionGestureDetectorBuilderDelegate delegate; + bool shouldShowSelectionToolbar = true; + + EditorState? getEditor() { + return delegate.getEditableTextKey().currentState; + } + + RenderEditor? getRenderEditor() { + return getEditor()!.getRenderEditor(); + } + + void onTapDown(TapDownDetails details) { + getRenderEditor()!.handleTapDown(details); + + final kind = details.kind; + shouldShowSelectionToolbar = kind == null || + kind == PointerDeviceKind.touch || + kind == PointerDeviceKind.stylus; + } + + void onForcePressStart(ForcePressDetails details) { + assert(delegate.getForcePressEnabled()); + shouldShowSelectionToolbar = true; + if (delegate.getSelectionEnabled()) { + getRenderEditor()!.selectWordsInRange( + details.globalPosition, + null, + SelectionChangedCause.forcePress, + ); + } + } + + void onForcePressEnd(ForcePressDetails details) { + assert(delegate.getForcePressEnabled()); + getRenderEditor()!.selectWordsInRange( + details.globalPosition, + null, + SelectionChangedCause.forcePress, + ); + if (shouldShowSelectionToolbar) { + getEditor()!.showToolbar(); + } + } + + void onSingleTapUp(TapUpDetails details) { + if (delegate.getSelectionEnabled()) { + getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); + } + } + + void onSingleTapCancel() {} + + void onSingleLongTapStart(LongPressStartDetails details) { + if (delegate.getSelectionEnabled()) { + getRenderEditor()!.selectPositionAt( + details.globalPosition, + null, + SelectionChangedCause.longPress, + ); + } + } + + void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { + if (delegate.getSelectionEnabled()) { + getRenderEditor()!.selectPositionAt( + details.globalPosition, + null, + SelectionChangedCause.longPress, + ); + } + } + + void onSingleLongTapEnd(LongPressEndDetails details) { + if (shouldShowSelectionToolbar) { + getEditor()!.showToolbar(); + } + } + + void onDoubleTapDown(TapDownDetails details) { + if (delegate.getSelectionEnabled()) { + getRenderEditor()!.selectWord(SelectionChangedCause.tap); + if (shouldShowSelectionToolbar) { + getEditor()!.showToolbar(); + } + } + } + + void onDragSelectionStart(DragStartDetails details) { + getRenderEditor()!.selectPositionAt( + details.globalPosition, + null, + SelectionChangedCause.drag, + ); + } + + void onDragSelectionUpdate( + DragStartDetails startDetails, DragUpdateDetails updateDetails) { + getRenderEditor()!.selectPositionAt( + startDetails.globalPosition, + updateDetails.globalPosition, + SelectionChangedCause.drag, + ); + } + + void onDragSelectionEnd(DragEndDetails details) {} + + Widget build(HitTestBehavior behavior, Widget child) { + return EditorTextSelectionGestureDetector( + onTapDown: onTapDown, + onForcePressStart: + delegate.getForcePressEnabled() ? onForcePressStart : null, + onForcePressEnd: delegate.getForcePressEnabled() ? onForcePressEnd : null, + onSingleTapUp: onSingleTapUp, + onSingleTapCancel: onSingleTapCancel, + onSingleLongTapStart: onSingleLongTapStart, + onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, + onSingleLongTapEnd: onSingleLongTapEnd, + onDoubleTapDown: onDoubleTapDown, + onDragSelectionStart: onDragSelectionStart, + onDragSelectionUpdate: onDragSelectionUpdate, + onDragSelectionEnd: onDragSelectionEnd, + behavior: behavior, + child: child, + ); + } +} diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart new file mode 100644 index 00000000..662a018c --- /dev/null +++ b/lib/src/widgets/editor.dart @@ -0,0 +1,1145 @@ +import 'dart:convert'; +import 'dart:io' as io; +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/container.dart' as container_node; +import '../models/documents/nodes/embed.dart'; +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 + 'tel:', // telephone + 'sms:', // SMS + 'callto:', + 'wtai:', + 'market:', + 'geopoint:', + 'ymsgr:', + 'msnim:', + 'gtalk:', // Google Talk + 'skype:', + 'sip:', // Lync + 'whatsapp:', + 'http' +]; + +abstract class EditorState extends State { + TextEditingValue getTextEditingValue(); + + void setTextEditingValue(TextEditingValue value); + + RenderEditor? getRenderEditor(); + + EditorTextSelectionOverlay? getSelectionOverlay(); + + bool showToolbar(); + + void hideToolbar(); + + void requestKeyboard(); +} + +abstract class RenderAbstractEditor { + TextSelection selectWordAtPosition(TextPosition position); + + TextSelection selectLineAtPosition(TextPosition position); + + double preferredLineHeight(TextPosition position); + + TextPosition getPositionForOffset(Offset offset); + + List getEndpointsForSelection( + TextSelection textSelection); + + void handleTapDown(TapDownDetails details); + + void selectWordsInRange( + Offset from, + Offset to, + SelectionChangedCause cause, + ); + + void selectWordEdge(SelectionChangedCause cause); + + void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause); + + void selectWord(SelectionChangedCause cause); + + void selectPosition(SelectionChangedCause cause); +} + +String _standardizeImageUrl(String url) { + if (url.contains('base64')) { + return url.split(',')[1]; + } + return url; +} + +Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { + assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); + switch (node.value.type) { + case 'image': + final imageUrl = _standardizeImageUrl(node.value.data); + return imageUrl.startsWith('http') + ? Image.network(imageUrl) + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + : Image.file(io.File(imageUrl)); + default: + throw UnimplementedError( + 'Embeddable type "${node.value.type}" is not supported by default embed ' + 'builder of QuillEditor. You must pass your own builder function to ' + 'embedBuilder property of QuillEditor or QuillField widgets.'); + } +} + +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.scrollBottomInset = 0, + 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); + } + + final QuillController controller; + final FocusNode focusNode; + final ScrollController scrollController; + final bool scrollable; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + final bool autoFocus; + final bool? showCursor; + final bool readOnly; + final String? placeholder; + final bool enableInteractiveSelection; + final double? minHeight; + final double? maxHeight; + final DefaultStyles? customStyles; + final bool expands; + final TextCapitalization textCapitalization; + final Brightness keyboardAppearance; + final ScrollPhysics? scrollPhysics; + final ValueChanged? onLaunchUrl; + // Returns whether gesture is handled + final bool Function( + TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; + + // Returns whether gesture is handled + final bool Function( + TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; + + // Returns whether gesture is handled + final bool Function( + LongPressStartDetails details, TextPosition Function(Offset offset))? + onSingleLongTapStart; + + // Returns whether gesture is handled + final bool Function(LongPressMoveUpdateDetails details, + TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; + // Returns whether gesture is handled + final bool Function( + LongPressEndDetails details, TextPosition Function(Offset offset))? + onSingleLongTapEnd; + + final EmbedBuilder embedBuilder; + + @override + _QuillEditorState createState() => _QuillEditorState(); +} + +class _QuillEditorState extends State + implements EditorTextSelectionGestureDetectorBuilderDelegate { + final GlobalKey _editorKey = GlobalKey(); + late EditorTextSelectionGestureDetectorBuilder + _selectionGestureDetectorBuilder; + + @override + void initState() { + super.initState(); + _selectionGestureDetectorBuilder = + _QuillEditorSelectionGestureDetectorBuilder(this); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final selectionTheme = TextSelectionTheme.of(context); + + TextSelectionControls textSelectionControls; + bool paintCursorAboveText; + bool cursorOpacityAnimates; + Offset? cursorOffset; + Color? cursorColor; + Color selectionColor; + Radius? cursorRadius; + + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + textSelectionControls = materialTextSelectionControls; + paintCursorAboveText = false; + cursorOpacityAnimates = false; + cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; + selectionColor = selectionTheme.selectionColor ?? + theme.colorScheme.primary.withOpacity(0.40); + break; + case TargetPlatform.iOS: + case TargetPlatform.macOS: + final cupertinoTheme = CupertinoTheme.of(context); + textSelectionControls = cupertinoTextSelectionControls; + paintCursorAboveText = true; + cursorOpacityAnimates = true; + cursorColor ??= + selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; + selectionColor = selectionTheme.selectionColor ?? + cupertinoTheme.primaryColor.withOpacity(0.40); + cursorRadius ??= const Radius.circular(2); + cursorOffset = Offset( + iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); + break; + default: + throw UnimplementedError(); + } + + return _selectionGestureDetectorBuilder.build( + HitTestBehavior.translucent, + RawEditor( + _editorKey, + widget.controller, + widget.focusNode, + widget.scrollController, + widget.scrollable, + widget.scrollBottomInset, + widget.padding, + widget.readOnly, + widget.placeholder, + widget.onLaunchUrl, + ToolbarOptions( + copy: widget.enableInteractiveSelection, + cut: widget.enableInteractiveSelection, + paste: widget.enableInteractiveSelection, + selectAll: widget.enableInteractiveSelection, + ), + theme.platform == TargetPlatform.iOS || + theme.platform == TargetPlatform.android, + widget.showCursor, + CursorStyle( + color: cursorColor, + backgroundColor: Colors.grey, + width: 2, + radius: cursorRadius, + offset: cursorOffset, + paintAboveText: paintCursorAboveText, + opacityAnimates: cursorOpacityAnimates, + ), + widget.textCapitalization, + widget.maxHeight, + widget.minHeight, + widget.customStyles, + widget.expands, + widget.autoFocus, + selectionColor, + textSelectionControls, + widget.keyboardAppearance, + widget.enableInteractiveSelection, + widget.scrollPhysics, + widget.embedBuilder), + ); + } + + @override + GlobalKey getEditableTextKey() { + return _editorKey; + } + + @override + bool getForcePressEnabled() { + return false; + } + + @override + bool getSelectionEnabled() { + return widget.enableInteractiveSelection; + } + + void _requestKeyboard() { + _editorKey.currentState!.requestKeyboard(); + } +} + +class _QuillEditorSelectionGestureDetectorBuilder + extends EditorTextSelectionGestureDetectorBuilder { + _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); + + final _QuillEditorState _state; + + @override + void onForcePressStart(ForcePressDetails details) { + super.onForcePressStart(details); + if (delegate.getSelectionEnabled() && shouldShowSelectionToolbar) { + getEditor()!.showToolbar(); + } + } + + @override + void onForcePressEnd(ForcePressDetails details) {} + + @override + void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { + if (_state.widget.onSingleLongTapMoveUpdate != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onSingleLongTapMoveUpdate!( + details, renderEditor.getPositionForOffset)) { + return; + } + } + } + if (!delegate.getSelectionEnabled()) { + return; + } + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + getRenderEditor()!.selectPositionAt( + details.globalPosition, + null, + SelectionChangedCause.longPress, + ); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + getRenderEditor()!.selectWordsInRange( + details.globalPosition - details.offsetFromOrigin, + details.globalPosition, + SelectionChangedCause.longPress, + ); + break; + default: + throw 'Invalid platform'; + } + } + + bool _onTapping(TapUpDetails details) { + if (_state.widget.controller.document.isEmpty()) { + return false; + } + final pos = getRenderEditor()!.getPositionForOffset(details.globalPosition); + final result = + getEditor()!.widget.controller.document.queryChild(pos.offset); + if (result.node == null) { + return false; + } + final line = result.node as Line; + final segmentResult = line.queryChild(result.offset, false); + if (segmentResult.node == null) { + if (line.length == 1) { + getEditor()!.widget.controller.updateSelection( + TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); + return true; + } + return false; + } + final segment = segmentResult.node as leaf.Leaf; + if (segment.style.containsKey(Attribute.link.key)) { + var launchUrl = getEditor()!.widget.onLaunchUrl; + launchUrl ??= _launchUrl; + String? link = segment.style.attributes[Attribute.link.key]!.value; + if (getEditor()!.widget.readOnly && link != null) { + link = link.trim(); + if (!linkPrefixes + .any((linkPrefix) => link!.toLowerCase().startsWith(linkPrefix))) { + link = 'https://$link'; + } + launchUrl(link); + } + return false; + } + if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) { + final blockEmbed = segment.value as BlockEmbed; + if (blockEmbed.type == 'image') { + final imageUrl = _standardizeImageUrl(blockEmbed.data); + Navigator.push( + getEditor()!.context, + MaterialPageRoute( + builder: (context) => ImageTapWrapper( + imageProvider: imageUrl.startsWith('http') + ? NetworkImage(imageUrl) + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + as ImageProvider? + : FileImage(io.File(imageUrl)), + ), + ), + ); + } + } + + return false; + } + + Future _launchUrl(String url) async { + await launch(url); + } + + @override + void onTapDown(TapDownDetails details) { + if (_state.widget.onTapDown != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onTapDown!( + details, renderEditor.getPositionForOffset)) { + return; + } + } + } + super.onTapDown(details); + } + + @override + void onSingleTapUp(TapUpDetails details) { + if (_state.widget.onTapUp != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onTapUp!( + details, renderEditor.getPositionForOffset)) { + return; + } + } + } + + getEditor()!.hideToolbar(); + + final positionSelected = _onTapping(details); + + if (delegate.getSelectionEnabled() && !positionSelected) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + switch (details.kind) { + case PointerDeviceKind.mouse: + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + getRenderEditor()!.selectPosition(SelectionChangedCause.tap); + break; + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); + break; + } + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + getRenderEditor()!.selectPosition(SelectionChangedCause.tap); + break; + } + } + _state._requestKeyboard(); + } + + @override + void onSingleLongTapStart(LongPressStartDetails details) { + if (_state.widget.onSingleLongTapStart != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onSingleLongTapStart!( + details, renderEditor.getPositionForOffset)) { + return; + } + } + } + + if (delegate.getSelectionEnabled()) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + getRenderEditor()!.selectPositionAt( + details.globalPosition, + null, + SelectionChangedCause.longPress, + ); + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + getRenderEditor()!.selectWord(SelectionChangedCause.longPress); + Feedback.forLongPress(_state.context); + break; + default: + throw 'Invalid platform'; + } + } + } + + @override + void onSingleLongTapEnd(LongPressEndDetails details) { + if (_state.widget.onSingleLongTapEnd != null) { + final renderEditor = getRenderEditor(); + if (renderEditor != null) { + if (_state.widget.onSingleLongTapEnd!( + details, renderEditor.getPositionForOffset)) { + return; + } + } + } + super.onSingleLongTapEnd(details); + } +} + +typedef TextSelectionChangedHandler = void Function( + TextSelection selection, SelectionChangedCause cause); + +class RenderEditor extends RenderEditableContainerBox + implements RenderAbstractEditor { + RenderEditor( + List? children, + TextDirection textDirection, + double scrollBottomInset, + EdgeInsetsGeometry padding, + this.document, + this.selection, + this._hasFocus, + this.onSelectionChanged, + this._startHandleLayerLink, + this._endHandleLayerLink, + EdgeInsets floatingCursorAddedMargin, + ) : super( + children, + document.root, + textDirection, + scrollBottomInset, + padding, + ); + + Document document; + TextSelection selection; + bool _hasFocus = false; + LayerLink _startHandleLayerLink; + LayerLink _endHandleLayerLink; + TextSelectionChangedHandler onSelectionChanged; + final ValueNotifier _selectionStartInViewport = + ValueNotifier(true); + + ValueListenable get selectionStartInViewport => + _selectionStartInViewport; + + ValueListenable get selectionEndInViewport => _selectionEndInViewport; + final ValueNotifier _selectionEndInViewport = ValueNotifier(true); + + void setDocument(Document doc) { + if (document == doc) { + return; + } + document = doc; + markNeedsLayout(); + } + + void setHasFocus(bool h) { + if (_hasFocus == h) { + return; + } + _hasFocus = h; + markNeedsSemanticsUpdate(); + } + + void setSelection(TextSelection t) { + if (selection == t) { + return; + } + selection = t; + markNeedsPaint(); + } + + void setStartHandleLayerLink(LayerLink value) { + if (_startHandleLayerLink == value) { + return; + } + _startHandleLayerLink = value; + markNeedsPaint(); + } + + void setEndHandleLayerLink(LayerLink value) { + if (_endHandleLayerLink == value) { + return; + } + _endHandleLayerLink = value; + markNeedsPaint(); + } + + void setScrollBottomInset(double value) { + if (scrollBottomInset == value) { + return; + } + scrollBottomInset = value; + markNeedsPaint(); + } + + @override + List getEndpointsForSelection( + TextSelection textSelection) { + if (textSelection.isCollapsed) { + 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, child.preferredLineHeight(localPosition)) + + localOffset + + parentData.offset, + null) + ]; + } + + final baseNode = _container.queryChild(textSelection.start, false).node; + + var baseChild = firstChild; + while (baseChild != null) { + if (baseChild.getContainer() == baseNode) { + break; + } + baseChild = childAfter(baseChild); + } + assert(baseChild != null); + + final baseParentData = baseChild!.parentData as BoxParentData; + final baseSelection = + localSelection(baseChild.getContainer(), textSelection, true); + var basePoint = baseChild.getBaseEndpointForSelection(baseSelection); + basePoint = TextSelectionPoint( + basePoint.point + baseParentData.offset, basePoint.direction); + + final extentNode = _container.queryChild(textSelection.end, false).node; + RenderEditableBox? extentChild = baseChild; + while (extentChild != null) { + if (extentChild.getContainer() == extentNode) { + break; + } + extentChild = childAfter(extentChild); + } + assert(extentChild != null); + + final extentParentData = extentChild!.parentData as BoxParentData; + final extentSelection = + localSelection(extentChild.getContainer(), textSelection, true); + var extentPoint = + extentChild.getExtentEndpointForSelection(extentSelection); + extentPoint = TextSelectionPoint( + extentPoint.point + extentParentData.offset, extentPoint.direction); + + return [basePoint, extentPoint]; + } + + Offset? _lastTapDownPosition; + + @override + void handleTapDown(TapDownDetails details) { + _lastTapDownPosition = details.globalPosition; + } + + @override + void selectWordsInRange( + Offset from, + Offset? to, + SelectionChangedCause cause, + ) { + final firstPosition = getPositionForOffset(from); + final firstWord = selectWordAtPosition(firstPosition); + final lastWord = + to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to)); + + _handleSelectionChange( + TextSelection( + baseOffset: firstWord.base.offset, + extentOffset: lastWord.extent.offset, + affinity: firstWord.affinity, + ), + cause, + ); + } + + void _handleSelectionChange( + TextSelection nextSelection, + SelectionChangedCause cause, + ) { + final focusingEmpty = nextSelection.baseOffset == 0 && + nextSelection.extentOffset == 0 && + !_hasFocus; + if (nextSelection == selection && + cause != SelectionChangedCause.keyboard && + !focusingEmpty) { + return; + } + onSelectionChanged(nextSelection, cause); + } + + @override + void selectWordEdge(SelectionChangedCause cause) { + assert(_lastTapDownPosition != null); + final position = getPositionForOffset(_lastTapDownPosition!); + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( + offset: position.offset - nodeOffset, + affinity: position.affinity, + ); + final localWord = child.getWordBoundary(localPosition); + final word = TextRange( + start: localWord.start + nodeOffset, + end: localWord.end + nodeOffset, + ); + if (position.offset - word.start <= 1) { + _handleSelectionChange( + TextSelection.collapsed(offset: word.start), + cause, + ); + } else { + _handleSelectionChange( + TextSelection.collapsed( + offset: word.end, affinity: TextAffinity.upstream), + cause, + ); + } + } + + @override + void selectPositionAt( + Offset from, + Offset? to, + SelectionChangedCause cause, + ) { + final fromPosition = getPositionForOffset(from); + final toPosition = to == null ? null : getPositionForOffset(to); + + 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); + } + + final newSelection = TextSelection( + baseOffset: baseOffset, + extentOffset: extentOffset, + affinity: fromPosition.affinity, + ); + _handleSelectionChange(newSelection, cause); + } + + @override + void selectWord(SelectionChangedCause cause) { + selectWordsInRange(_lastTapDownPosition!, null, cause); + } + + @override + void selectPosition(SelectionChangedCause cause) { + selectPositionAt(_lastTapDownPosition!, null, cause); + } + + @override + TextSelection selectWordAtPosition(TextPosition position) { + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( + offset: position.offset - nodeOffset, affinity: position.affinity); + final localWord = child.getWordBoundary(localPosition); + final word = TextRange( + start: localWord.start + nodeOffset, + end: localWord.end + nodeOffset, + ); + if (position.offset >= word.end) { + return TextSelection.fromPosition(position); + } + return TextSelection(baseOffset: word.start, extentOffset: word.end); + } + + @override + TextSelection selectLineAtPosition(TextPosition position) { + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final localPosition = TextPosition( + offset: position.offset - nodeOffset, affinity: position.affinity); + final localLineRange = child.getLineBoundary(localPosition); + final line = TextRange( + start: localLineRange.start + nodeOffset, + end: localLineRange.end + nodeOffset, + ); + + if (position.offset >= line.end) { + return TextSelection.fromPosition(position); + } + return TextSelection(baseOffset: line.start, extentOffset: line.end); + } + + @override + void paint(PaintingContext context, Offset offset) { + defaultPaint(context, offset); + _paintHandleLayers(context, getEndpointsForSelection(selection)); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return defaultHitTestChildren(result, position: position); + } + + void _paintHandleLayers( + PaintingContext context, List endpoints) { + var startPoint = endpoints[0].point; + startPoint = Offset( + startPoint.dx.clamp(0.0, size.width), + startPoint.dy.clamp(0.0, size.height), + ); + context.pushLayer( + LeaderLayer(link: _startHandleLayerLink, offset: startPoint), + super.paint, + Offset.zero, + ); + if (endpoints.length == 2) { + var endPoint = endpoints[1].point; + endPoint = Offset( + endPoint.dx.clamp(0.0, size.width), + endPoint.dy.clamp(0.0, size.height), + ); + context.pushLayer( + LeaderLayer(link: _endHandleLayerLink, offset: endPoint), + super.paint, + Offset.zero, + ); + } + } + + @override + double preferredLineHeight(TextPosition position) { + final child = childAtPosition(position); + return child.preferredLineHeight( + TextPosition(offset: position.offset - child.getContainer().offset)); + } + + @override + TextPosition getPositionForOffset(Offset offset) { + final local = globalToLocal(offset); + final child = childAtOffset(local)!; + + final parentData = child.parentData as BoxParentData; + final localOffset = local - parentData.offset; + final localPosition = child.getPositionForOffset(localOffset); + return TextPosition( + offset: localPosition.offset + child.getContainer().offset, + affinity: localPosition.affinity, + ); + } + + /// Returns the y-offset of the editor at which [selection] is visible. + /// + /// The offset is the distance from the top of the editor and is the minimum + /// from the current scroll position until [selection] becomes visible. + /// Returns null if [selection] is already visible. + double? getOffsetToRevealCursor( + double viewportHeight, double scrollOffset, double offsetInViewport) { + final endpoints = getEndpointsForSelection(selection); + final endpoint = endpoints.first; + final child = childAtPosition(selection.extent); + const kMargin = 8.0; + + final caretTop = endpoint.point.dy - + child.preferredLineHeight(TextPosition( + offset: selection.extentOffset - child.getContainer().offset)) - + kMargin + + offsetInViewport + + scrollBottomInset; + final caretBottom = + endpoint.point.dy + kMargin + offsetInViewport + scrollBottomInset; + double? dy; + if (caretTop < scrollOffset) { + dy = caretTop; + } else if (caretBottom > scrollOffset + viewportHeight) { + dy = caretBottom - viewportHeight; + } + if (dy == null) { + return null; + } + return math.max(dy, 0); + } +} + +class EditableContainerParentData + extends ContainerBoxParentData {} + +class RenderEditableContainerBox extends RenderBox + with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + RenderEditableContainerBox( + List? children, + this._container, + this.textDirection, + this.scrollBottomInset, + this._padding, + ) : assert(_padding.isNonNegative) { + addAll(children); + } + + container_node.Container _container; + TextDirection textDirection; + EdgeInsetsGeometry _padding; + double scrollBottomInset; + EdgeInsets? _resolvedPadding; + + container_node.Container getContainer() { + return _container; + } + + void setContainer(container_node.Container c) { + if (_container == c) { + return; + } + _container = c; + markNeedsLayout(); + } + + EdgeInsetsGeometry getPadding() => _padding; + + void setPadding(EdgeInsetsGeometry value) { + assert(value.isNonNegative); + if (_padding == value) { + return; + } + _padding = value; + _markNeedsPaddingResolution(); + } + + EdgeInsets? get resolvedPadding => _resolvedPadding; + + void _resolvePadding() { + if (_resolvedPadding != null) { + return; + } + _resolvedPadding = _padding.resolve(textDirection); + _resolvedPadding = _resolvedPadding!.copyWith(left: _resolvedPadding!.left); + + assert(_resolvedPadding!.isNonNegative); + } + + RenderEditableBox childAtPosition(TextPosition position) { + assert(firstChild != null); + + final targetNode = _container.queryChild(position.offset, false).node; + + var targetChild = firstChild; + while (targetChild != null) { + if (targetChild.getContainer() == targetNode) { + break; + } + targetChild = childAfter(targetChild); + } + if (targetChild == null) { + throw 'targetChild should not be null'; + } + return targetChild; + } + + void _markNeedsPaddingResolution() { + _resolvedPadding = null; + markNeedsLayout(); + } + + RenderEditableBox? childAtOffset(Offset offset) { + assert(firstChild != null); + _resolvePadding(); + + if (offset.dy <= _resolvedPadding!.top) { + return firstChild; + } + if (offset.dy >= size.height - _resolvedPadding!.bottom) { + return lastChild; + } + + var child = firstChild; + final dx = -offset.dx; + var dy = _resolvedPadding!.top; + while (child != null) { + if (child.size.contains(offset.translate(dx, -dy))) { + return child; + } + dy += child.size.height; + child = childAfter(child); + } + throw 'No child'; + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is EditableContainerParentData) { + return; + } + + child.parentData = EditableContainerParentData(); + } + + @override + void performLayout() { + assert(constraints.hasBoundedWidth); + _resolvePadding(); + assert(_resolvedPadding != null); + + var mainAxisExtent = _resolvedPadding!.top; + var child = firstChild; + final innerConstraints = + BoxConstraints.tightFor(width: constraints.maxWidth) + .deflate(_resolvedPadding!); + while (child != null) { + child.layout(innerConstraints, parentUsesSize: true); + final childParentData = (child.parentData as EditableContainerParentData) + ..offset = Offset(_resolvedPadding!.left, mainAxisExtent); + mainAxisExtent += child.size.height; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + mainAxisExtent += _resolvedPadding!.bottom; + size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent)); + + assert(size.isFinite); + } + + double _getIntrinsicCrossAxis(double Function(RenderBox child) childSize) { + var extent = 0.0; + var child = firstChild; + while (child != null) { + extent = math.max(extent, childSize(child)); + final childParentData = child.parentData as EditableContainerParentData; + child = childParentData.nextSibling; + } + return extent; + } + + double _getIntrinsicMainAxis(double Function(RenderBox child) childSize) { + var extent = 0.0; + var child = firstChild; + while (child != null) { + extent += childSize(child); + final childParentData = child.parentData as EditableContainerParentData; + child = childParentData.nextSibling; + } + return extent; + } + + @override + double computeMinIntrinsicWidth(double height) { + _resolvePadding(); + return _getIntrinsicCrossAxis((child) { + final childHeight = math.max( + 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); + return child.getMinIntrinsicWidth(childHeight) + + _resolvedPadding!.left + + _resolvedPadding!.right; + }); + } + + @override + double computeMaxIntrinsicWidth(double height) { + _resolvePadding(); + return _getIntrinsicCrossAxis((child) { + final childHeight = math.max( + 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); + return child.getMaxIntrinsicWidth(childHeight) + + _resolvedPadding!.left + + _resolvedPadding!.right; + }); + } + + @override + double computeMinIntrinsicHeight(double width) { + _resolvePadding(); + return _getIntrinsicMainAxis((child) { + final childWidth = math.max( + 0, width - _resolvedPadding!.left + _resolvedPadding!.right); + return child.getMinIntrinsicHeight(childWidth) + + _resolvedPadding!.top + + _resolvedPadding!.bottom; + }); + } + + @override + double computeMaxIntrinsicHeight(double width) { + _resolvePadding(); + return _getIntrinsicMainAxis((child) { + final childWidth = math.max( + 0, width - _resolvedPadding!.left + _resolvedPadding!.right); + return child.getMaxIntrinsicHeight(childWidth) + + _resolvedPadding!.top + + _resolvedPadding!.bottom; + }); + } + + @override + double computeDistanceToActualBaseline(TextBaseline baseline) { + _resolvePadding(); + return defaultComputeDistanceToFirstActualBaseline(baseline)! + + _resolvedPadding!.top; + } +} diff --git a/lib/src/widgets/image.dart b/lib/src/widgets/image.dart new file mode 100644 index 00000000..b9df48ce --- /dev/null +++ b/lib/src/widgets/image.dart @@ -0,0 +1,31 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:photo_view/photo_view.dart'; + +class ImageTapWrapper extends StatelessWidget { + const ImageTapWrapper({ + this.imageProvider, + }); + + final ImageProvider? imageProvider; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + constraints: BoxConstraints.expand( + height: MediaQuery.of(context).size.height, + ), + child: GestureDetector( + onTapDown: (_) { + Navigator.pop(context); + }, + child: PhotoView( + imageProvider: imageProvider, + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart new file mode 100644 index 00000000..17c47aad --- /dev/null +++ b/lib/src/widgets/keyboard_listener.dart @@ -0,0 +1,105 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL } + +typedef CursorMoveCallback = void Function( + LogicalKeyboardKey key, bool wordModifier, bool lineModifier, bool shift); +typedef InputShortcutCallback = void Function(InputShortcut? shortcut); +typedef OnDeleteCallback = void Function(bool forward); + +class 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, + LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.arrowDown, + }; + + static final Set _shortcutKeys = { + LogicalKeyboardKey.keyA, + LogicalKeyboardKey.keyC, + LogicalKeyboardKey.keyV, + LogicalKeyboardKey.keyX, + LogicalKeyboardKey.delete, + LogicalKeyboardKey.backspace, + }; + + static final Set _nonModifierKeys = { + ..._shortcutKeys, + ..._moveKeys, + }; + + static final Set _modifierKeys = { + LogicalKeyboardKey.shift, + LogicalKeyboardKey.control, + LogicalKeyboardKey.alt, + }; + + static final Set _macOsModifierKeys = + { + LogicalKeyboardKey.shift, + LogicalKeyboardKey.meta, + LogicalKeyboardKey.alt, + }; + + static final Set _interestingKeys = { + ..._modifierKeys, + ..._macOsModifierKeys, + ..._nonModifierKeys, + }; + + static final Map _keyToShortcut = { + LogicalKeyboardKey.keyX: InputShortcut.CUT, + LogicalKeyboardKey.keyC: InputShortcut.COPY, + LogicalKeyboardKey.keyV: InputShortcut.PASTE, + LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, + }; + + bool handleRawKeyEvent(RawKeyEvent event) { + if (kIsWeb) { + // On web platform, we should ignore the key because it's processed already. + return false; + } + + if (event is! RawKeyDownEvent) { + return false; + } + + final keysPressed = + LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); + final key = event.logicalKey; + final isMacOS = event.data is RawKeyEventDataMacOs; + if (!_nonModifierKeys.contains(key) || + keysPressed + .difference(isMacOS ? _macOsModifierKeys : _modifierKeys) + .length > + 1 || + keysPressed.difference(_interestingKeys).isNotEmpty) { + return false; + } + + if (_moveKeys.contains(key)) { + onCursorMove( + key, + isMacOS ? event.isAltPressed : event.isControlPressed, + isMacOS ? event.isMetaPressed : event.isAltPressed, + event.isShiftPressed); + } else if (isMacOS + ? event.isMetaPressed + : event.isControlPressed && _shortcutKeys.contains(key)) { + onShortcut(_keyToShortcut[key]); + } else if (key == LogicalKeyboardKey.delete) { + onDelete(true); + } else if (key == LogicalKeyboardKey.backspace) { + onDelete(false); + } + return false; + } +} diff --git a/lib/src/widgets/proxy.dart b/lib/src/widgets/proxy.dart new file mode 100644 index 00000000..8a04c4e1 --- /dev/null +++ b/lib/src/widgets/proxy.dart @@ -0,0 +1,298 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'box.dart'; + +class BaselineProxy extends SingleChildRenderObjectWidget { + 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( + null, + textStyle!, + padding, + ); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderBaselineProxy renderObject) { + renderObject + ..textStyle = textStyle! + ..padding = padding!; + } +} + +class RenderBaselineProxy extends RenderProxyBox { + RenderBaselineProxy( + RenderParagraph? child, + TextStyle textStyle, + EdgeInsets? padding, + ) : _prototypePainter = TextPainter( + text: TextSpan(text: ' ', style: textStyle), + textDirection: TextDirection.ltr, + strutStyle: + StrutStyle.fromTextStyle(textStyle, forceStrutHeight: true)), + super(child); + + final TextPainter _prototypePainter; + + set textStyle(TextStyle value) { + if (_prototypePainter.text!.style == value) { + return; + } + _prototypePainter.text = TextSpan(text: ' ', style: value); + markNeedsLayout(); + } + + EdgeInsets? _padding; + + set padding(EdgeInsets value) { + if (_padding == value) { + return; + } + _padding = value; + markNeedsLayout(); + } + + @override + double computeDistanceToActualBaseline(TextBaseline baseline) => + _prototypePainter.computeDistanceToActualBaseline(baseline); + // SEE What happens + _padding?.top; + + @override + void performLayout() { + super.performLayout(); + _prototypePainter.layout(); + } +} + +class EmbedProxy extends SingleChildRenderObjectWidget { + const EmbedProxy(Widget child) : super(child: child); + + @override + RenderEmbedProxy createRenderObject(BuildContext context) => + RenderEmbedProxy(null); +} + +class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { + RenderEmbedProxy(RenderBox? child) : super(child); + + @override + List getBoxesForSelection(TextSelection selection) { + if (!selection.isCollapsed) { + return [ + TextBox.fromLTRBD(0, 0, size.width, size.height, TextDirection.ltr) + ]; + } + + final left = selection.extentOffset == 0 ? 0.0 : size.width; + final right = selection.extentOffset == 0 ? 0.0 : size.width; + return [ + TextBox.fromLTRBD(left, 0, right, size.height, TextDirection.ltr) + ]; + } + + @override + double getFullHeightForCaret(TextPosition position) => size.height; + + @override + Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) { + assert(position.offset <= 1 && position.offset >= 0); + return position.offset == 0 ? Offset.zero : Offset(size.width, 0); + } + + @override + TextPosition getPositionForOffset(Offset offset) => + TextPosition(offset: offset.dx > size.width / 2 ? 1 : 0); + + @override + TextRange getWordBoundary(TextPosition position) => + const TextRange(start: 0, end: 1); + + @override + double getPreferredLineHeight() { + return size.height; + } +} + +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; + final double textScaleFactor; + final Locale locale; + final StrutStyle strutStyle; + final TextWidthBasis textWidthBasis; + final TextHeightBehavior? textHeightBehavior; + + @override + RenderParagraphProxy createRenderObject(BuildContext context) { + return RenderParagraphProxy( + null, + textStyle, + textAlign, + textDirection, + textScaleFactor, + strutStyle, + locale, + textWidthBasis, + textHeightBehavior); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderParagraphProxy renderObject) { + renderObject + ..textStyle = textStyle + ..textAlign = textAlign + ..textDirection = textDirection + ..textScaleFactor = textScaleFactor + ..locale = locale + ..strutStyle = strutStyle + ..textWidthBasis = textWidthBasis + ..textHeightBehavior = textHeightBehavior; + } +} + +class RenderParagraphProxy extends RenderProxyBox + implements RenderContentProxyBox { + RenderParagraphProxy( + RenderParagraph? child, + TextStyle textStyle, + TextAlign textAlign, + TextDirection textDirection, + double textScaleFactor, + StrutStyle strutStyle, + Locale locale, + TextWidthBasis textWidthBasis, + TextHeightBehavior? textHeightBehavior, + ) : _prototypePainter = TextPainter( + text: TextSpan(text: ' ', style: textStyle), + textAlign: textAlign, + textDirection: textDirection, + textScaleFactor: textScaleFactor, + strutStyle: strutStyle, + locale: locale, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior), + super(child); + + final TextPainter _prototypePainter; + + set textStyle(TextStyle value) { + if (_prototypePainter.text!.style == value) { + return; + } + _prototypePainter.text = TextSpan(text: ' ', style: value); + markNeedsLayout(); + } + + set textAlign(TextAlign value) { + if (_prototypePainter.textAlign == value) { + return; + } + _prototypePainter.textAlign = value; + markNeedsLayout(); + } + + set textDirection(TextDirection value) { + if (_prototypePainter.textDirection == value) { + return; + } + _prototypePainter.textDirection = value; + markNeedsLayout(); + } + + set textScaleFactor(double value) { + if (_prototypePainter.textScaleFactor == value) { + return; + } + _prototypePainter.textScaleFactor = value; + markNeedsLayout(); + } + + set strutStyle(StrutStyle value) { + if (_prototypePainter.strutStyle == value) { + return; + } + _prototypePainter.strutStyle = value; + markNeedsLayout(); + } + + set locale(Locale value) { + if (_prototypePainter.locale == value) { + return; + } + _prototypePainter.locale = value; + markNeedsLayout(); + } + + set textWidthBasis(TextWidthBasis value) { + if (_prototypePainter.textWidthBasis == value) { + return; + } + _prototypePainter.textWidthBasis = value; + markNeedsLayout(); + } + + set textHeightBehavior(TextHeightBehavior? value) { + if (_prototypePainter.textHeightBehavior == value) { + return; + } + _prototypePainter.textHeightBehavior = value; + markNeedsLayout(); + } + + @override + RenderParagraph? get child => super.child as RenderParagraph?; + + @override + double getPreferredLineHeight() { + return _prototypePainter.preferredLineHeight; + } + + @override + Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) => + child!.getOffsetForCaret(position, caretPrototype!); + + @override + TextPosition getPositionForOffset(Offset offset) => + child!.getPositionForOffset(offset); + + @override + double? getFullHeightForCaret(TextPosition position) => + child!.getFullHeightForCaret(position); + + @override + TextRange getWordBoundary(TextPosition position) => + child!.getWordBoundary(position); + + @override + List getBoxesForSelection(TextSelection selection) => + child!.getBoxesForSelection(selection); + + @override + void performLayout() { + super.performLayout(); + _prototypePainter.layout( + minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); + } +} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart new file mode 100644 index 00000000..cd4f379c --- /dev/null +++ b/lib/src/widgets/raw_editor.dart @@ -0,0 +1,736 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:tuple/tuple.dart'; + +import '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/line.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 'raw_editor/raw_editor_state_keyboard_mixin.dart'; +import 'raw_editor/raw_editor_state_selection_delegate_mixin.dart'; +import 'raw_editor/raw_editor_state_text_input_client_mixin.dart'; +import 'text_block.dart'; +import 'text_line.dart'; +import 'text_selection.dart'; + +class RawEditor extends StatefulWidget { + const RawEditor( + Key key, + this.controller, + this.focusNode, + this.scrollController, + this.scrollable, + this.scrollBottomInset, + 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; + final bool scrollable; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + final bool readOnly; + final String? placeholder; + final ValueChanged? onLaunchUrl; + final ToolbarOptions toolbarOptions; + final bool showSelectionHandles; + final bool showCursor; + final CursorStyle cursorStyle; + final TextCapitalization textCapitalization; + final double? maxHeight; + final double? minHeight; + final DefaultStyles? customStyles; + final bool expands; + final bool autoFocus; + final Color selectionColor; + final TextSelectionControls selectionCtrls; + final Brightness keyboardAppearance; + final bool enableInteractiveSelection; + final ScrollPhysics? scrollPhysics; + final EmbedBuilder embedBuilder; + + @override + State createState() => RawEditorState(); +} + +class RawEditorState extends EditorState + with + AutomaticKeepAliveClientMixin, + WidgetsBindingObserver, + TickerProviderStateMixin, + RawEditorStateKeyboardMixin, + RawEditorStateTextInputClientMixin, + RawEditorStateSelectionDelegateMixin { + final GlobalKey _editorKey = GlobalKey(); + + // Keyboard + late KeyboardListener _keyboardListener; + KeyboardVisibilityController? _keyboardVisibilityController; + StreamSubscription? _keyboardVisibilitySubscription; + bool _keyboardVisible = false; + + // Selection overlay + @override + EditorTextSelectionOverlay? getSelectionOverlay() => _selectionOverlay; + EditorTextSelectionOverlay? _selectionOverlay; + + ScrollController? _scrollController; + + late CursorCont _cursorCont; + + // Focus + bool _didAutoFocus = false; + FocusAttachment? _focusAttachment; + bool get _hasFocus => widget.focusNode.hasFocus; + + DefaultStyles? _styles; + + final ClipboardStatusNotifier? _clipboardStatus = + kIsWeb ? null : ClipboardStatusNotifier(); + final LayerLink _toolbarLayerLink = LayerLink(); + final LayerLink _startHandleLayerLink = LayerLink(); + final LayerLink _endHandleLayerLink = LayerLink(); + + TextDirection get _textDirection => Directionality.of(context); + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + _focusAttachment!.reparent(); + super.build(context); + + var _doc = widget.controller.document; + if (_doc.isEmpty() && + !widget.focusNode.hasFocus && + widget.placeholder != null) { + _doc = Document.fromJson(jsonDecode( + '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); + } + + Widget child = CompositedTransformTarget( + link: _toolbarLayerLink, + child: Semantics( + child: _Editor( + key: _editorKey, + document: _doc, + selection: widget.controller.selection, + hasFocus: _hasFocus, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _handleSelectionChanged, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + children: _buildChildren(_doc, context), + ), + ), + ); + + if (widget.scrollable) { + final baselinePadding = + EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1); + child = BaselineProxy( + textStyle: _styles!.paragraph!.style, + padding: baselinePadding, + child: SingleChildScrollView( + controller: _scrollController, + physics: widget.scrollPhysics, + child: child, + ), + ); + } + + final constraints = widget.expands + ? const BoxConstraints.expand() + : BoxConstraints( + minHeight: widget.minHeight ?? 0.0, + maxHeight: widget.maxHeight ?? double.infinity); + + return QuillStyles( + data: _styles!, + child: MouseRegion( + cursor: SystemMouseCursors.text, + child: Container( + constraints: constraints, + child: child, + ), + ), + ); + } + + void _handleSelectionChanged( + TextSelection selection, SelectionChangedCause cause) { + widget.controller.updateSelection(selection, ChangeSource.LOCAL); + + _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); + + if (!_keyboardVisible) { + requestKeyboard(); + } + } + + /// Updates the checkbox positioned at [offset] in document + /// by changing its attribute according to [value]. + void _handleCheckboxTap(int offset, bool value) { + if (!widget.readOnly) { + if (value) { + widget.controller.formatText(offset, 0, Attribute.checked); + } else { + widget.controller.formatText(offset, 0, Attribute.unchecked); + } + } + } + + List _buildChildren(Document doc, BuildContext context) { + final result = []; + final indentLevelCounts = {}; + for (final node in doc.root.children) { + if (node is Line) { + final editableTextLine = _getEditableTextLineFromNode(node, context); + result.add(editableTextLine); + } else if (node is Block) { + final attrs = node.style.attributes; + final editableTextBlock = EditableTextBlock( + node, + _textDirection, + widget.scrollBottomInset, + _getVerticalSpacingForBlock(node, _styles), + widget.controller.selection, + widget.selectionColor, + _styles, + widget.enableInteractiveSelection, + _hasFocus, + attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + widget.embedBuilder, + _cursorCont, + indentLevelCounts, + _handleCheckboxTap, + ); + result.add(editableTextBlock); + } else { + throw StateError('Unreachable.'); + } + } + return result; + } + + EditableTextLine _getEditableTextLineFromNode( + Line node, BuildContext context) { + final textLine = TextLine( + line: node, + textDirection: _textDirection, + embedBuilder: widget.embedBuilder, + styles: _styles!, + ); + final editableTextLine = EditableTextLine( + node, + null, + textLine, + 0, + _getVerticalSpacingForLine(node, _styles), + _textDirection, + widget.controller.selection, + widget.selectionColor, + widget.enableInteractiveSelection, + _hasFocus, + MediaQuery.of(context).devicePixelRatio, + _cursorCont); + return editableTextLine; + } + + Tuple2 _getVerticalSpacingForLine( + Line line, DefaultStyles? defaultStyles) { + final attrs = line.style.attributes; + if (attrs.containsKey(Attribute.header.key)) { + final int? level = attrs[Attribute.header.key]!.value; + switch (level) { + case 1: + return defaultStyles!.h1!.verticalSpacing; + case 2: + return defaultStyles!.h2!.verticalSpacing; + case 3: + return defaultStyles!.h3!.verticalSpacing; + default: + throw 'Invalid level $level'; + } + } + + return defaultStyles!.paragraph!.verticalSpacing; + } + + Tuple2 _getVerticalSpacingForBlock( + Block node, DefaultStyles? defaultStyles) { + final attrs = node.style.attributes; + if (attrs.containsKey(Attribute.blockQuote.key)) { + return defaultStyles!.quote!.verticalSpacing; + } else if (attrs.containsKey(Attribute.codeBlock.key)) { + return defaultStyles!.code!.verticalSpacing; + } else if (attrs.containsKey(Attribute.indent.key)) { + return defaultStyles!.indent!.verticalSpacing; + } + return defaultStyles!.lists!.verticalSpacing; + } + + @override + void initState() { + super.initState(); + + _clipboardStatus?.addListener(_onChangedClipboardStatus); + + widget.controller.addListener(() { + _didChangeTextEditingValue(widget.controller.ignoreFocusOnTextChange); + }); + + _scrollController = widget.scrollController; + _scrollController!.addListener(_updateSelectionOverlayForScroll); + + _cursorCont = CursorCont( + show: ValueNotifier(widget.showCursor), + style: widget.cursorStyle, + tickerProvider: this, + ); + + _keyboardListener = KeyboardListener( + handleCursorMovement, + handleShortcut, + handleDelete, + ); + + if (defaultTargetPlatform == TargetPlatform.windows || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux || + defaultTargetPlatform == TargetPlatform.fuchsia) { + _keyboardVisible = true; + } else { + _keyboardVisibilityController = KeyboardVisibilityController(); + _keyboardVisible = _keyboardVisibilityController!.isVisible; + _keyboardVisibilitySubscription = + _keyboardVisibilityController?.onChange.listen((visible) { + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(); + } + }); + } + + _focusAttachment = widget.focusNode.attach(context, + onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); + widget.focusNode.addListener(_handleFocusChanged); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final parentStyles = QuillStyles.getStyles(context, true); + final defaultStyles = DefaultStyles.getInstance(context); + _styles = (parentStyles != null) + ? defaultStyles.merge(parentStyles) + : defaultStyles; + + if (widget.customStyles != null) { + _styles = _styles!.merge(widget.customStyles!); + } + + if (!_didAutoFocus && widget.autoFocus) { + FocusScope.of(context).autofocus(widget.focusNode); + _didAutoFocus = true; + } + } + + @override + void didUpdateWidget(RawEditor oldWidget) { + super.didUpdateWidget(oldWidget); + + _cursorCont.show.value = widget.showCursor; + _cursorCont.style = widget.cursorStyle; + + if (widget.controller != oldWidget.controller) { + oldWidget.controller.removeListener(_didChangeTextEditingValue); + widget.controller.addListener(_didChangeTextEditingValue); + updateRemoteValueIfNeeded(); + } + + if (widget.scrollController != _scrollController) { + _scrollController!.removeListener(_updateSelectionOverlayForScroll); + _scrollController = widget.scrollController; + _scrollController!.addListener(_updateSelectionOverlayForScroll); + } + + if (widget.focusNode != oldWidget.focusNode) { + oldWidget.focusNode.removeListener(_handleFocusChanged); + _focusAttachment?.detach(); + _focusAttachment = widget.focusNode.attach(context, + onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); + widget.focusNode.addListener(_handleFocusChanged); + updateKeepAlive(); + } + + if (widget.controller.selection != oldWidget.controller.selection) { + _selectionOverlay?.update(textEditingValue); + } + + _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); + if (!shouldCreateInputConnection) { + closeConnectionIfNeeded(); + } else { + if (oldWidget.readOnly && _hasFocus) { + openConnectionIfNeeded(); + } + } + } + + bool _shouldShowSelectionHandles() { + return widget.showSelectionHandles && + !widget.controller.selection.isCollapsed; + } + + @override + void dispose() { + closeConnectionIfNeeded(); + _keyboardVisibilitySubscription?.cancel(); + assert(!hasConnection); + _selectionOverlay?.dispose(); + _selectionOverlay = null; + widget.controller.removeListener(_didChangeTextEditingValue); + widget.focusNode.removeListener(_handleFocusChanged); + _focusAttachment!.detach(); + _cursorCont.dispose(); + _clipboardStatus?.removeListener(_onChangedClipboardStatus); + _clipboardStatus?.dispose(); + super.dispose(); + } + + void _updateSelectionOverlayForScroll() { + _selectionOverlay?.markNeedsBuild(); + } + + void _didChangeTextEditingValue([bool ignoreFocus = false]) { + if (kIsWeb) { + _onChangeTextEditingValue(ignoreFocus); + if (!ignoreFocus) { + requestKeyboard(); + } + return; + } + + if (ignoreFocus || _keyboardVisible) { + _onChangeTextEditingValue(ignoreFocus); + } else { + requestKeyboard(); + if (mounted) { + setState(() { + // Use widget.controller.value in build() + // Trigger build and updateChildren + }); + } + } + } + + void _onChangeTextEditingValue([bool ignoreCaret = false]) { + updateRemoteValueIfNeeded(); + if (ignoreCaret) { + return; + } + _showCaretOnScreen(); + _cursorCont.startOrStopCursorTimerIfNeeded( + _hasFocus, widget.controller.selection); + if (hasConnection) { + _cursorCont + ..stopCursorTimer(resetCharTicks: false) + ..startCursorTimer(); + } + + SchedulerBinding.instance!.addPostFrameCallback( + (_) => _updateOrDisposeSelectionOverlayIfNeeded()); + if (mounted) { + setState(() { + // Use widget.controller.value in build() + // Trigger build and updateChildren + }); + } + } + + void _updateOrDisposeSelectionOverlayIfNeeded() { + if (_selectionOverlay != null) { + if (_hasFocus) { + _selectionOverlay!.update(textEditingValue); + } else { + _selectionOverlay!.dispose(); + _selectionOverlay = null; + } + } else if (_hasFocus) { + _selectionOverlay?.hide(); + _selectionOverlay = null; + + _selectionOverlay = EditorTextSelectionOverlay( + textEditingValue, + false, + context, + widget, + _toolbarLayerLink, + _startHandleLayerLink, + _endHandleLayerLink, + getRenderEditor(), + widget.selectionCtrls, + this, + DragStartBehavior.start, + null, + _clipboardStatus!, + ); + _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); + _selectionOverlay!.showHandles(); + } + } + + void _handleFocusChanged() { + openOrCloseConnection(); + _cursorCont.startOrStopCursorTimerIfNeeded( + _hasFocus, widget.controller.selection); + _updateOrDisposeSelectionOverlayIfNeeded(); + if (_hasFocus) { + WidgetsBinding.instance!.addObserver(this); + _showCaretOnScreen(); + } else { + WidgetsBinding.instance!.removeObserver(this); + } + updateKeepAlive(); + } + + void _onChangedClipboardStatus() { + if (!mounted) return; + setState(() { + // Inform the widget that the value of clipboardStatus has changed. + // Trigger build and updateChildren + }); + } + + bool _showCaretOnScreenScheduled = false; + + void _showCaretOnScreen() { + if (!widget.showCursor || _showCaretOnScreenScheduled) { + return; + } + + _showCaretOnScreenScheduled = true; + SchedulerBinding.instance!.addPostFrameCallback((_) { + if (widget.scrollable) { + _showCaretOnScreenScheduled = false; + + final viewport = RenderAbstractViewport.of(getRenderEditor()); + + final editorOffset = getRenderEditor()! + .localToGlobal(const Offset(0, 0), ancestor: viewport); + final offsetInViewport = _scrollController!.offset + editorOffset.dy; + + final offset = getRenderEditor()!.getOffsetToRevealCursor( + _scrollController!.position.viewportDimension, + _scrollController!.offset, + offsetInViewport, + ); + + if (offset != null) { + _scrollController!.animateTo( + offset, + duration: const Duration(milliseconds: 100), + curve: Curves.fastOutSlowIn, + ); + } + } + }); + } + + @override + RenderEditor? getRenderEditor() { + return _editorKey.currentContext!.findRenderObject() as RenderEditor?; + } + + @override + TextEditingValue getTextEditingValue() { + return widget.controller.plainTextEditingValue; + } + + @override + void requestKeyboard() { + if (_hasFocus) { + openConnectionIfNeeded(); + } else { + widget.focusNode.requestFocus(); + } + } + + @override + void setTextEditingValue(TextEditingValue value) { + if (value.text == textEditingValue.text) { + widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); + } else { + __setEditingValue(value); + } + } + + Future __setEditingValue(TextEditingValue value) async { + if (await __isItCut(value)) { + widget.controller.replaceText( + textEditingValue.selection.start, + textEditingValue.text.length - value.text.length, + '', + value.selection, + ); + } else { + final value = textEditingValue; + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null) { + final length = + textEditingValue.selection.end - textEditingValue.selection.start; + widget.controller.replaceText( + value.selection.start, + length, + data.text, + value.selection, + ); + // move cursor to the end of pasted text selection + widget.controller.updateSelection( + TextSelection.collapsed( + offset: value.selection.start + data.text!.length), + ChangeSource.LOCAL); + } + } + } + + Future __isItCut(TextEditingValue value) async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data == null) { + return false; + } + return textEditingValue.text.length - value.text.length == + data.text!.length; + } + + @override + bool showToolbar() { + // Web is using native dom elements to enable clipboard functionality of the + // toolbar: copy, paste, select, cut. It might also provide additional + // functionality depending on the browser (such as translate). Due to this + // we should not show a Flutter toolbar for the editable text elements. + if (kIsWeb) { + return false; + } + if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { + return false; + } + + _selectionOverlay!.update(textEditingValue); + _selectionOverlay!.showToolbar(); + return true; + } + + @override + bool get wantKeepAlive => widget.focusNode.hasFocus; + + @override + void userUpdateTextEditingValue( + TextEditingValue value, SelectionChangedCause cause) { + // TODO: implement userUpdateTextEditingValue + } +} + +class _Editor extends MultiChildRenderObjectWidget { + _Editor({ + required Key key, + required List children, + required this.document, + required this.textDirection, + required this.hasFocus, + required this.selection, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.onSelectionChanged, + required this.scrollBottomInset, + this.padding = EdgeInsets.zero, + }) : super(key: key, children: children); + + final Document document; + final TextDirection textDirection; + final bool hasFocus; + final TextSelection selection; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final TextSelectionChangedHandler onSelectionChanged; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + + @override + RenderEditor createRenderObject(BuildContext context) { + return RenderEditor( + null, + textDirection, + scrollBottomInset, + padding, + document, + selection, + hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + const EdgeInsets.fromLTRB(4, 4, 4, 5), + ); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderEditor renderObject) { + renderObject + ..document = document + ..setContainer(document.root) + ..textDirection = textDirection + ..setHasFocus(hasFocus) + ..setSelection(selection) + ..setStartHandleLayerLink(startHandleLayerLink) + ..setEndHandleLayerLink(endHandleLayerLink) + ..onSelectionChanged = onSelectionChanged + ..setScrollBottomInset(scrollBottomInset) + ..setPadding(padding); + } +} diff --git a/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart new file mode 100644 index 00000000..0eb7f955 --- /dev/null +++ b/lib/src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart @@ -0,0 +1,354 @@ +import 'dart:ui'; + +import 'package:characters/characters.dart'; +import 'package:flutter/services.dart'; + +import '../../models/documents/document.dart'; +import '../../utils/diff_delta.dart'; +import '../editor.dart'; +import '../keyboard_listener.dart'; + +mixin RawEditorStateKeyboardMixin on EditorState { + // Holds the last cursor location the user selected in the case the user tries + // to select vertically past the end or beginning of the field. If they do, + // then we need to keep the old cursor location so that we can go back to it + // if they change their minds. Only used for moving selection up and down in a + // multiline text field when selecting using the keyboard. + int _cursorResetLocation = -1; + + // Whether we should reset the location of the cursor in the case the user + // tries to select vertically past the end or beginning of the field. If they + // do, then we need to keep the old cursor location so that we can go back to + // it if they change their minds. Only used for resetting selection up and + // down in a multiline text field when selecting using the keyboard. + bool _wasSelectingVerticallyWithKeyboard = false; + + void handleCursorMovement( + LogicalKeyboardKey key, + bool wordModifier, + bool lineModifier, + bool shift, + ) { + if (wordModifier && lineModifier) { + // If both modifiers are down, nothing happens on any of the platforms. + return; + } + final selection = widget.controller.selection; + + var newSelection = widget.controller.selection; + + final plainText = getTextEditingValue().text; + + final rightKey = key == LogicalKeyboardKey.arrowRight, + leftKey = key == LogicalKeyboardKey.arrowLeft, + upKey = key == LogicalKeyboardKey.arrowUp, + downKey = key == LogicalKeyboardKey.arrowDown; + + if ((rightKey || leftKey) && !(rightKey && leftKey)) { + newSelection = _jumpToBeginOrEndOfWord(newSelection, wordModifier, + leftKey, rightKey, plainText, lineModifier, shift); + } + + if (downKey || upKey) { + newSelection = _handleMovingCursorVertically( + upKey, downKey, shift, selection, newSelection, plainText); + } + + if (!shift) { + newSelection = + _placeCollapsedSelection(selection, newSelection, leftKey, rightKey); + } + + widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); + } + + // Handles shortcut functionality including cut, copy, paste and select all + // using control/command + (X, C, V, A). + // TODO: Add support for formatting shortcuts: Cmd+B (bold), Cmd+I (italic) + Future handleShortcut(InputShortcut? shortcut) async { + final selection = widget.controller.selection; + final plainText = getTextEditingValue().text; + if (shortcut == InputShortcut.COPY) { + if (!selection.isCollapsed) { + await Clipboard.setData( + ClipboardData(text: selection.textInside(plainText))); + } + return; + } + if (shortcut == InputShortcut.CUT && !widget.readOnly) { + if (!selection.isCollapsed) { + final data = selection.textInside(plainText); + await Clipboard.setData(ClipboardData(text: data)); + + widget.controller.replaceText( + selection.start, + data.length, + '', + TextSelection.collapsed(offset: selection.start), + ); + + setTextEditingValue(TextEditingValue( + text: + selection.textBefore(plainText) + selection.textAfter(plainText), + selection: TextSelection.collapsed(offset: selection.start), + )); + } + return; + } + if (shortcut == InputShortcut.PASTE && !widget.readOnly) { + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null) { + widget.controller.replaceText( + selection.start, + selection.end - selection.start, + data.text, + TextSelection.collapsed(offset: selection.start + data.text!.length), + ); + } + return; + } + if (shortcut == InputShortcut.SELECT_ALL && + widget.enableInteractiveSelection) { + widget.controller.updateSelection( + selection.copyWith( + baseOffset: 0, + extentOffset: getTextEditingValue().text.length, + ), + ChangeSource.REMOTE); + return; + } + } + + void handleDelete(bool forward) { + final selection = widget.controller.selection; + final plainText = getTextEditingValue().text; + var cursorPosition = selection.start; + var textBefore = selection.textBefore(plainText); + var textAfter = selection.textAfter(plainText); + if (selection.isCollapsed) { + if (!forward && textBefore.isNotEmpty) { + final characterBoundary = + _previousCharacter(textBefore.length, textBefore, true); + textBefore = textBefore.substring(0, characterBoundary); + cursorPosition = characterBoundary; + } + if (forward && textAfter.isNotEmpty && textAfter != '\n') { + final deleteCount = _nextCharacter(0, textAfter, true); + textAfter = textAfter.substring(deleteCount); + } + } + final newSelection = TextSelection.collapsed(offset: cursorPosition); + final newText = textBefore + textAfter; + final size = plainText.length - newText.length; + widget.controller.replaceText( + cursorPosition, + size, + '', + newSelection, + ); + } + + TextSelection _jumpToBeginOrEndOfWord( + TextSelection newSelection, + bool wordModifier, + bool leftKey, + bool rightKey, + String plainText, + bool lineModifier, + bool shift) { + if (wordModifier) { + if (leftKey) { + final textSelection = getRenderEditor()!.selectWordAtPosition( + TextPosition( + offset: _previousCharacter( + newSelection.extentOffset, plainText, false))); + return newSelection.copyWith(extentOffset: textSelection.baseOffset); + } + final textSelection = getRenderEditor()!.selectWordAtPosition( + TextPosition( + offset: + _nextCharacter(newSelection.extentOffset, plainText, false))); + return newSelection.copyWith(extentOffset: textSelection.extentOffset); + } else if (lineModifier) { + if (leftKey) { + final textSelection = getRenderEditor()!.selectLineAtPosition( + TextPosition( + offset: _previousCharacter( + newSelection.extentOffset, plainText, false))); + return newSelection.copyWith(extentOffset: textSelection.baseOffset); + } + final startPoint = newSelection.extentOffset; + if (startPoint < plainText.length) { + final textSelection = getRenderEditor()! + .selectLineAtPosition(TextPosition(offset: startPoint)); + return newSelection.copyWith(extentOffset: textSelection.extentOffset); + } + return newSelection; + } + + if (rightKey && newSelection.extentOffset < plainText.length) { + final nextExtent = + _nextCharacter(newSelection.extentOffset, plainText, true); + final distance = nextExtent - newSelection.extentOffset; + newSelection = newSelection.copyWith(extentOffset: nextExtent); + if (shift) { + _cursorResetLocation += distance; + } + return newSelection; + } + + if (leftKey && newSelection.extentOffset > 0) { + final previousExtent = + _previousCharacter(newSelection.extentOffset, plainText, true); + final distance = newSelection.extentOffset - previousExtent; + newSelection = newSelection.copyWith(extentOffset: previousExtent); + if (shift) { + _cursorResetLocation -= distance; + } + return newSelection; + } + return newSelection; + } + + /// Returns the index into the string of the next character boundary after the + /// given index. + /// + /// The character boundary is determined by the characters package, so + /// surrogate pairs and extended grapheme clusters are considered. + /// + /// The index must be between 0 and string.length, inclusive. If given + /// string.length, string.length is returned. + /// + /// Setting includeWhitespace to false will only return the index of non-space + /// characters. + int _nextCharacter(int index, String string, bool includeWhitespace) { + assert(index >= 0 && index <= string.length); + if (index == string.length) { + return string.length; + } + + var count = 0; + final remain = string.characters.skipWhile((currentString) { + if (count <= index) { + count += currentString.length; + return true; + } + if (includeWhitespace) { + return false; + } + return WHITE_SPACE.contains(currentString.codeUnitAt(0)); + }); + return string.length - remain.toString().length; + } + + /// Returns the index into the string of the previous character boundary + /// before the given index. + /// + /// The character boundary is determined by the characters package, so + /// surrogate pairs and extended grapheme clusters are considered. + /// + /// The index must be between 0 and string.length, inclusive. If index is 0, + /// 0 will be returned. + /// + /// Setting includeWhitespace to false will only return the index of non-space + /// characters. + int _previousCharacter(int index, String string, includeWhitespace) { + assert(index >= 0 && index <= string.length); + if (index == 0) { + return 0; + } + + var count = 0; + int? lastNonWhitespace; + for (final currentString in string.characters) { + if (!includeWhitespace && + !WHITE_SPACE.contains( + currentString.characters.first.toString().codeUnitAt(0))) { + lastNonWhitespace = count; + } + if (count + currentString.length >= index) { + return includeWhitespace ? count : lastNonWhitespace ?? 0; + } + count += currentString.length; + } + return 0; + } + + TextSelection _handleMovingCursorVertically( + bool upKey, + bool downKey, + bool shift, + TextSelection selection, + TextSelection newSelection, + String plainText) { + final originPosition = TextPosition( + offset: upKey ? selection.baseOffset : selection.extentOffset); + + final child = getRenderEditor()!.childAtPosition(originPosition); + final localPosition = TextPosition( + offset: originPosition.offset - child.getContainer().documentOffset); + + var position = upKey + ? child.getPositionAbove(localPosition) + : child.getPositionBelow(localPosition); + + if (position == null) { + final sibling = upKey + ? getRenderEditor()!.childBefore(child) + : getRenderEditor()!.childAfter(child); + if (sibling == null) { + position = TextPosition(offset: upKey ? 0 : plainText.length - 1); + } else { + final finalOffset = Offset( + child.getOffsetForCaret(localPosition).dx, + sibling + .getOffsetForCaret(TextPosition( + offset: upKey ? sibling.getContainer().length - 1 : 0)) + .dy); + final siblingPosition = sibling.getPositionForOffset(finalOffset); + position = TextPosition( + offset: + sibling.getContainer().documentOffset + siblingPosition.offset); + } + } else { + position = TextPosition( + offset: child.getContainer().documentOffset + position.offset); + } + + if (position.offset == newSelection.extentOffset) { + if (downKey) { + newSelection = newSelection.copyWith(extentOffset: plainText.length); + } else if (upKey) { + newSelection = newSelection.copyWith(extentOffset: 0); + } + _wasSelectingVerticallyWithKeyboard = shift; + return newSelection; + } + + if (_wasSelectingVerticallyWithKeyboard && shift) { + newSelection = newSelection.copyWith(extentOffset: _cursorResetLocation); + _wasSelectingVerticallyWithKeyboard = false; + return newSelection; + } + newSelection = newSelection.copyWith(extentOffset: position.offset); + _cursorResetLocation = newSelection.extentOffset; + return newSelection; + } + + TextSelection _placeCollapsedSelection(TextSelection selection, + TextSelection newSelection, bool leftKey, bool rightKey) { + var newOffset = newSelection.extentOffset; + if (!selection.isCollapsed) { + if (leftKey) { + newOffset = newSelection.baseOffset < newSelection.extentOffset + ? newSelection.baseOffset + : newSelection.extentOffset; + } else if (rightKey) { + newOffset = newSelection.baseOffset > newSelection.extentOffset + ? newSelection.baseOffset + : newSelection.extentOffset; + } + } + return TextSelection.fromPosition(TextPosition(offset: newOffset)); + } +} diff --git a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart new file mode 100644 index 00000000..cda991cc --- /dev/null +++ b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart'; + +import '../editor.dart'; + +mixin RawEditorStateSelectionDelegateMixin on EditorState + implements TextSelectionDelegate { + @override + TextEditingValue get textEditingValue { + return getTextEditingValue(); + } + + @override + set textEditingValue(TextEditingValue value) { + setTextEditingValue(value); + } + + @override + void bringIntoView(TextPosition position) { + // TODO: implement bringIntoView + } + + @override + void hideToolbar([bool hideHandles = true]) { + if (getSelectionOverlay()?.toolbar != null) { + getSelectionOverlay()?.hideToolbar(); + } + } + + @override + bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; + + @override + bool get copyEnabled => widget.toolbarOptions.copy; + + @override + bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly; + + @override + bool get selectAllEnabled => widget.toolbarOptions.selectAll; +} diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart new file mode 100644 index 00000000..527df582 --- /dev/null +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -0,0 +1,200 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import '../../utils/diff_delta.dart'; +import '../editor.dart'; + +mixin RawEditorStateTextInputClientMixin on EditorState + implements TextInputClient { + final List _sentRemoteValues = []; + TextInputConnection? _textInputConnection; + TextEditingValue? _lastKnownRemoteTextEditingValue; + + /// Whether to create an input connection with the platform for text editing + /// or not. + /// + /// Read-only input fields do not need a connection with the platform since + /// there's no need for text editing capabilities (e.g. virtual keyboard). + /// + /// On the web, we always need a connection because we want some browser + /// functionalities to continue to work on read-only input fields like: + /// + /// - Relevant context menu. + /// - cmd/ctrl+c shortcut to copy. + /// - cmd/ctrl+a to select all. + /// - Changing the selection using a physical keyboard. + bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; + + /// Returns `true` if there is open input connection. + bool get hasConnection => + _textInputConnection != null && _textInputConnection!.attached; + + /// Opens or closes input connection based on the current state of + /// [focusNode] and [value]. + void openOrCloseConnection() { + if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { + openConnectionIfNeeded(); + } else if (!widget.focusNode.hasFocus) { + closeConnectionIfNeeded(); + } + } + + void openConnectionIfNeeded() { + if (!shouldCreateInputConnection) { + return; + } + + if (!hasConnection) { + _lastKnownRemoteTextEditingValue = getTextEditingValue(); + _textInputConnection = TextInput.attach( + this, + TextInputConfiguration( + inputType: TextInputType.multiline, + readOnly: widget.readOnly, + inputAction: TextInputAction.newline, + enableSuggestions: !widget.readOnly, + keyboardAppearance: widget.keyboardAppearance, + textCapitalization: widget.textCapitalization, + ), + ); + + _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); + // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); + } + + _textInputConnection!.show(); + } + + /// Closes input connection if it's currently open. Otherwise does nothing. + void closeConnectionIfNeeded() { + if (!hasConnection) { + return; + } + _textInputConnection!.close(); + _textInputConnection = null; + _lastKnownRemoteTextEditingValue = null; + _sentRemoteValues.clear(); + } + + /// Updates remote value based on current state of [document] and + /// [selection]. + /// + /// This method may not actually send an update to native side if it thinks + /// remote value is up to date or identical. + void updateRemoteValueIfNeeded() { + if (!hasConnection) { + return; + } + + // Since we don't keep track of the composing range in value provided + // by the Controller we need to add it here manually before comparing + // with the last known remote value. + // It is important to prevent excessive remote updates as it can cause + // race conditions. + final actualValue = getTextEditingValue().copyWith( + composing: _lastKnownRemoteTextEditingValue!.composing, + ); + + if (actualValue == _lastKnownRemoteTextEditingValue) { + return; + } + + final shouldRemember = + getTextEditingValue().text != _lastKnownRemoteTextEditingValue!.text; + _lastKnownRemoteTextEditingValue = actualValue; + _textInputConnection!.setEditingState(actualValue); + if (shouldRemember) { + // Only keep track if text changed (selection changes are not relevant) + _sentRemoteValues.add(actualValue); + } + } + + @override + TextEditingValue? get currentTextEditingValue => + _lastKnownRemoteTextEditingValue; + + // autofill is not needed + @override + AutofillScope? get currentAutofillScope => null; + + @override + void updateEditingValue(TextEditingValue value) { + if (!shouldCreateInputConnection) { + return; + } + + if (_sentRemoteValues.contains(value)) { + /// There is a race condition in Flutter text input plugin where sending + /// updates to native side too often results in broken behavior. + /// TextInputConnection.setEditingValue is an async call to native side. + /// For each such call native side _always_ sends an update which triggers + /// this method (updateEditingValue) with the same value we've sent it. + /// If multiple calls to setEditingValue happen too fast and we only + /// track the last sent value then there is no way for us to filter out + /// automatic callbacks from native side. + /// Therefore we have to keep track of all values we send to the native + /// side and when we see this same value appear here we skip it. + /// This is fragile but it's probably the only available option. + _sentRemoteValues.remove(value); + return; + } + + if (_lastKnownRemoteTextEditingValue == value) { + // There is no difference between this value and the last known value. + return; + } + + // Check if only composing range changed. + if (_lastKnownRemoteTextEditingValue!.text == value.text && + _lastKnownRemoteTextEditingValue!.selection == value.selection) { + // This update only modifies composing range. Since we don't keep track + // of composing range we just need to update last known value here. + // This check fixes an issue on Android when it sends + // composing updates separately from regular changes for text and + // selection. + _lastKnownRemoteTextEditingValue = value; + return; + } + + final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; + _lastKnownRemoteTextEditingValue = value; + final oldText = effectiveLastKnownValue.text; + final text = value.text; + final cursorPosition = value.selection.extentOffset; + final diff = getDiff(oldText, text, cursorPosition); + widget.controller.replaceText( + diff.start, diff.deleted.length, diff.inserted, value.selection); + } + + @override + void performAction(TextInputAction action) { + // no-op + } + + @override + void performPrivateCommand(String action, Map data) { + // no-op + } + + @override + void updateFloatingCursor(RawFloatingCursorPoint point) { + throw UnimplementedError(); + } + + @override + void showAutocorrectionPromptRect(int start, int end) { + throw UnimplementedError(); + } + + @override + void connectionClosed() { + if (!hasConnection) { + return; + } + _textInputConnection!.connectionClosedReceived(); + _textInputConnection = null; + _lastKnownRemoteTextEditingValue = null; + _sentRemoteValues.clear(); + } +} diff --git a/lib/src/widgets/responsive_widget.dart b/lib/src/widgets/responsive_widget.dart new file mode 100644 index 00000000..3829565c --- /dev/null +++ b/lib/src/widgets/responsive_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class ResponsiveWidget extends StatelessWidget { + const ResponsiveWidget({ + required this.largeScreen, + this.mediumScreen, + this.smallScreen, + 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; + } + + static bool isLargeScreen(BuildContext context) { + return MediaQuery.of(context).size.width > 1200; + } + + static bool isMediumScreen(BuildContext context) { + return MediaQuery.of(context).size.width >= 800 && + MediaQuery.of(context).size.width <= 1200; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 1200) { + return largeScreen; + } else if (constraints.maxWidth <= 1200 && + constraints.maxWidth >= 800) { + return mediumScreen ?? largeScreen; + } else { + return smallScreen ?? largeScreen; + } + }, + ); + } +} diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart new file mode 100644 index 00000000..ee1a7732 --- /dev/null +++ b/lib/src/widgets/simple_viewer.dart @@ -0,0 +1,344 @@ +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:tuple/tuple.dart'; + +import '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/leaf.dart' as leaf; +import '../models/documents/nodes/line.dart'; +import 'controller.dart'; +import 'cursor.dart'; +import 'default_styles.dart'; +import 'delegate.dart'; +import 'editor.dart'; +import 'text_block.dart'; +import 'text_line.dart'; + +class QuillSimpleViewer extends StatefulWidget { + const QuillSimpleViewer({ + required this.controller, + this.customStyles, + this.truncate = false, + this.truncateScale, + this.truncateAlignment, + this.truncateHeight, + this.truncateWidth, + this.scrollBottomInset = 0, + this.padding = EdgeInsets.zero, + this.embedBuilder, + Key? key, + }) : assert(truncate || + ((truncateScale == null) && + (truncateAlignment == null) && + (truncateHeight == null) && + (truncateWidth == null))), + super(key: key); + + final QuillController controller; + final DefaultStyles? customStyles; + final bool truncate; + final double? truncateScale; + final Alignment? truncateAlignment; + final double? truncateHeight; + final double? truncateWidth; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + final EmbedBuilder? embedBuilder; + + @override + _QuillSimpleViewerState createState() => _QuillSimpleViewerState(); +} + +class _QuillSimpleViewerState extends State + with SingleTickerProviderStateMixin { + late DefaultStyles _styles; + final LayerLink _toolbarLayerLink = LayerLink(); + final LayerLink _startHandleLayerLink = LayerLink(); + final LayerLink _endHandleLayerLink = LayerLink(); + late CursorCont _cursorCont; + + @override + void initState() { + super.initState(); + + _cursorCont = CursorCont( + show: ValueNotifier(false), + style: const CursorStyle( + color: Colors.black, + backgroundColor: Colors.grey, + width: 2, + radius: Radius.zero, + offset: Offset.zero, + ), + tickerProvider: this, + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final parentStyles = QuillStyles.getStyles(context, true); + final defaultStyles = DefaultStyles.getInstance(context); + _styles = (parentStyles != null) + ? defaultStyles.merge(parentStyles) + : defaultStyles; + + if (widget.customStyles != null) { + _styles = _styles.merge(widget.customStyles!); + } + } + + EmbedBuilder get embedBuilder => widget.embedBuilder ?? _defaultEmbedBuilder; + + Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { + assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); + switch (node.value.type) { + case 'image': + final imageUrl = _standardizeImageUrl(node.value.data); + return imageUrl.startsWith('http') + ? Image.network(imageUrl) + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + : Image.file(io.File(imageUrl)); + default: + throw UnimplementedError( + 'Embeddable type "${node.value.type}" is not supported by default embed ' + 'builder of QuillEditor. You must pass your own builder function to ' + 'embedBuilder property of QuillEditor or QuillField widgets.'); + } + } + + String _standardizeImageUrl(String url) { + if (url.contains('base64')) { + return url.split(',')[1]; + } + return url; + } + + @override + Widget build(BuildContext context) { + final _doc = widget.controller.document; + // if (_doc.isEmpty() && + // !widget.focusNode.hasFocus && + // widget.placeholder != null) { + // _doc = Document.fromJson(jsonDecode( + // '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); + // } + + Widget child = CompositedTransformTarget( + link: _toolbarLayerLink, + child: Semantics( + child: _SimpleViewer( + document: _doc, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _nullSelectionChanged, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + children: _buildChildren(_doc, context), + ), + ), + ); + + if (widget.truncate) { + if (widget.truncateScale != null) { + child = Container( + height: widget.truncateHeight, + child: Align( + heightFactor: widget.truncateScale, + widthFactor: widget.truncateScale, + alignment: widget.truncateAlignment ?? Alignment.topLeft, + child: Container( + width: widget.truncateWidth! / widget.truncateScale!, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Transform.scale( + scale: widget.truncateScale!, + alignment: + widget.truncateAlignment ?? Alignment.topLeft, + child: child))))); + } else { + child = Container( + height: widget.truncateHeight, + width: widget.truncateWidth, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), child: child)); + } + } + + return QuillStyles(data: _styles, child: child); + } + + List _buildChildren(Document doc, BuildContext context) { + final result = []; + final indentLevelCounts = {}; + for (final node in doc.root.children) { + if (node is Line) { + final editableTextLine = _getEditableTextLineFromNode(node, context); + result.add(editableTextLine); + } else if (node is Block) { + final attrs = node.style.attributes; + final editableTextBlock = EditableTextBlock( + node, + _textDirection, + widget.scrollBottomInset, + _getVerticalSpacingForBlock(node, _styles), + widget.controller.selection, + Colors.black, + // selectionColor, + _styles, + false, + // enableInteractiveSelection, + false, + // hasFocus, + attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + embedBuilder, + _cursorCont, + indentLevelCounts, + _handleCheckboxTap); + result.add(editableTextBlock); + } else { + throw StateError('Unreachable.'); + } + } + return result; + } + + /// Updates the checkbox positioned at [offset] in document + /// by changing its attribute according to [value]. + void _handleCheckboxTap(int offset, bool value) { + // readonly - do nothing + } + + TextDirection get _textDirection { + final result = Directionality.of(context); + return result; + } + + EditableTextLine _getEditableTextLineFromNode( + Line node, BuildContext context) { + final textLine = TextLine( + line: node, + textDirection: _textDirection, + embedBuilder: embedBuilder, + styles: _styles, + ); + final editableTextLine = EditableTextLine( + node, + null, + textLine, + 0, + _getVerticalSpacingForLine(node, _styles), + _textDirection, + widget.controller.selection, + Colors.black, + //widget.selectionColor, + false, + //enableInteractiveSelection, + false, + //_hasFocus, + MediaQuery.of(context).devicePixelRatio, + _cursorCont); + return editableTextLine; + } + + Tuple2 _getVerticalSpacingForLine( + Line line, DefaultStyles? defaultStyles) { + final attrs = line.style.attributes; + if (attrs.containsKey(Attribute.header.key)) { + final int? level = attrs[Attribute.header.key]!.value; + switch (level) { + case 1: + return defaultStyles!.h1!.verticalSpacing; + case 2: + return defaultStyles!.h2!.verticalSpacing; + case 3: + return defaultStyles!.h3!.verticalSpacing; + default: + throw 'Invalid level $level'; + } + } + + return defaultStyles!.paragraph!.verticalSpacing; + } + + Tuple2 _getVerticalSpacingForBlock( + Block node, DefaultStyles? defaultStyles) { + final attrs = node.style.attributes; + if (attrs.containsKey(Attribute.blockQuote.key)) { + return defaultStyles!.quote!.verticalSpacing; + } else if (attrs.containsKey(Attribute.codeBlock.key)) { + return defaultStyles!.code!.verticalSpacing; + } else if (attrs.containsKey(Attribute.indent.key)) { + return defaultStyles!.indent!.verticalSpacing; + } + return defaultStyles!.lists!.verticalSpacing; + } + + void _nullSelectionChanged( + TextSelection selection, SelectionChangedCause cause) {} +} + +class _SimpleViewer extends MultiChildRenderObjectWidget { + _SimpleViewer({ + required List children, + required this.document, + required this.textDirection, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.onSelectionChanged, + required this.scrollBottomInset, + this.padding = EdgeInsets.zero, + Key? key, + }) : super(key: key, children: children); + + final Document document; + final TextDirection textDirection; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final TextSelectionChangedHandler onSelectionChanged; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + + @override + RenderEditor createRenderObject(BuildContext context) { + return RenderEditor( + null, + textDirection, + scrollBottomInset, + padding, + document, + const TextSelection(baseOffset: 0, extentOffset: 0), + false, + // hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + const EdgeInsets.fromLTRB(4, 4, 4, 5), + ); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderEditor renderObject) { + renderObject + ..document = document + ..setContainer(document.root) + ..textDirection = textDirection + ..setStartHandleLayerLink(startHandleLayerLink) + ..setEndHandleLayerLink(endHandleLayerLink) + ..onSelectionChanged = onSelectionChanged + ..setScrollBottomInset(scrollBottomInset) + ..setPadding(padding); + } +} diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart new file mode 100644 index 00000000..f533a160 --- /dev/null +++ b/lib/src/widgets/text_block.dart @@ -0,0 +1,737 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:tuple/tuple.dart'; + +import '../models/documents/attribute.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/line.dart'; +import '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, + 900, + 500, + 400, + 100, + 90, + 50, + 40, + 10, + 9, + 5, + 4, + 1 +]; + +const List romanNumbers = [ + 'M', + 'CM', + 'D', + 'CD', + 'C', + 'XC', + 'L', + 'XL', + 'X', + 'IX', + 'V', + 'IV', + 'I' +]; + +class EditableTextBlock extends StatelessWidget { + const EditableTextBlock( + this.block, + this.textDirection, + this.scrollBottomInset, + this.verticalSpacing, + this.textSelection, + this.color, + this.styles, + this.enableInteractiveSelection, + this.hasFocus, + this.contentPadding, + this.embedBuilder, + this.cursorCont, + this.indentLevelCounts, + this.onCheckboxTap, + ); + + final Block block; + final TextDirection textDirection; + final double scrollBottomInset; + final Tuple2 verticalSpacing; + final TextSelection textSelection; + final Color color; + final DefaultStyles? styles; + final bool enableInteractiveSelection; + final bool hasFocus; + final EdgeInsets? contentPadding; + final EmbedBuilder embedBuilder; + final CursorCont cursorCont; + final Map indentLevelCounts; + final Function(int, bool) onCheckboxTap; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + + final defaultStyles = QuillStyles.getStyles(context, false); + return _EditableBlock( + block, + textDirection, + verticalSpacing as Tuple2, + scrollBottomInset, + _getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(), + contentPadding, + _buildChildren(context, indentLevelCounts)); + } + + BoxDecoration? _getDecorationForBlock( + Block node, DefaultStyles? defaultStyles) { + final attrs = block.style.attributes; + if (attrs.containsKey(Attribute.blockQuote.key)) { + return defaultStyles!.quote!.decoration; + } + if (attrs.containsKey(Attribute.codeBlock.key)) { + return defaultStyles!.code!.decoration; + } + return null; + } + + List _buildChildren( + BuildContext context, Map indentLevelCounts) { + 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++; + final editableTextLine = EditableTextLine( + line, + _buildLeading(context, line, index, indentLevelCounts, count), + TextLine( + line: line, + textDirection: textDirection, + embedBuilder: embedBuilder, + styles: styles!, + ), + _getIndentWidth(), + _getSpacingForLine(line, index, count, defaultStyles), + textDirection, + textSelection, + color, + enableInteractiveSelection, + hasFocus, + MediaQuery.of(context).devicePixelRatio, + cursorCont); + children.add(editableTextLine); + } + return children.toList(growable: false); + } + + Widget? _buildLeading(BuildContext context, Line line, int index, + Map indentLevelCounts, int count) { + final defaultStyles = QuillStyles.getStyles(context, false); + final attrs = line.style.attributes; + if (attrs[Attribute.list.key] == Attribute.ol) { + return _NumberPoint( + index: index, + indentLevelCounts: indentLevelCounts, + count: count, + style: defaultStyles!.leading!.style, + attrs: attrs, + width: 32, + padding: 8, + ); + } + + if (attrs[Attribute.list.key] == Attribute.ul) { + return _BulletPoint( + style: + defaultStyles!.leading!.style.copyWith(fontWeight: FontWeight.bold), + width: 32, + ); + } + + if (attrs[Attribute.list.key] == Attribute.checked) { + return _Checkbox( + key: UniqueKey(), + style: defaultStyles!.leading!.style, + width: 32, + isChecked: true, + offset: block.offset + line.offset, + onTap: onCheckboxTap, + ); + } + + if (attrs[Attribute.list.key] == Attribute.unchecked) { + return _Checkbox( + key: UniqueKey(), + style: defaultStyles!.leading!.style, + width: 32, + offset: block.offset + line.offset, + onTap: onCheckboxTap, + ); + } + + if (attrs.containsKey(Attribute.codeBlock.key)) { + return _NumberPoint( + index: index, + indentLevelCounts: indentLevelCounts, + count: count, + style: defaultStyles!.code!.style + .copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)), + width: 32, + attrs: attrs, + padding: 16, + withDot: false, + ); + } + return null; + } + + double _getIndentWidth() { + final attrs = block.style.attributes; + + final indent = attrs[Attribute.indent.key]; + var extraIndent = 0.0; + if (indent != null && indent.value != null) { + extraIndent = 16.0 * indent.value; + } + + if (attrs.containsKey(Attribute.blockQuote.key)) { + return 16.0 + extraIndent; + } + + return 32.0 + extraIndent; + } + + Tuple2 _getSpacingForLine( + Line node, int index, int count, DefaultStyles? defaultStyles) { + var top = 0.0, bottom = 0.0; + + final attrs = block.style.attributes; + if (attrs.containsKey(Attribute.header.key)) { + final level = attrs[Attribute.header.key]!.value; + switch (level) { + case 1: + top = defaultStyles!.h1!.verticalSpacing.item1; + bottom = defaultStyles.h1!.verticalSpacing.item2; + break; + case 2: + top = defaultStyles!.h2!.verticalSpacing.item1; + bottom = defaultStyles.h2!.verticalSpacing.item2; + break; + case 3: + top = defaultStyles!.h3!.verticalSpacing.item1; + bottom = defaultStyles.h3!.verticalSpacing.item2; + break; + default: + throw 'Invalid level $level'; + } + } else { + late Tuple2 lineSpacing; + if (attrs.containsKey(Attribute.blockQuote.key)) { + lineSpacing = defaultStyles!.quote!.lineSpacing; + } else if (attrs.containsKey(Attribute.indent.key)) { + lineSpacing = defaultStyles!.indent!.lineSpacing; + } else if (attrs.containsKey(Attribute.list.key)) { + lineSpacing = defaultStyles!.lists!.lineSpacing; + } else if (attrs.containsKey(Attribute.codeBlock.key)) { + lineSpacing = defaultStyles!.code!.lineSpacing; + } else if (attrs.containsKey(Attribute.align.key)) { + lineSpacing = defaultStyles!.align!.lineSpacing; + } + top = lineSpacing.item1; + bottom = lineSpacing.item2; + } + + if (index == 1) { + top = 0.0; + } + + if (index == count) { + bottom = 0.0; + } + + return Tuple2(top, bottom); + } +} + +class RenderEditableTextBlock extends RenderEditableContainerBox + implements RenderEditableBox { + RenderEditableTextBlock({ + required Block block, + required TextDirection textDirection, + required EdgeInsetsGeometry padding, + required double scrollBottomInset, + required Decoration decoration, + List? children, + ImageConfiguration configuration = ImageConfiguration.empty, + EdgeInsets contentPadding = EdgeInsets.zero, + }) : _decoration = decoration, + _configuration = configuration, + _savedPadding = padding, + _contentPadding = contentPadding, + super( + children, + block, + textDirection, + scrollBottomInset, + padding.add(contentPadding), + ); + + EdgeInsetsGeometry _savedPadding; + EdgeInsets _contentPadding; + + set contentPadding(EdgeInsets value) { + if (_contentPadding == value) return; + _contentPadding = value; + super.setPadding(_savedPadding.add(_contentPadding)); + } + + @override + void setPadding(EdgeInsetsGeometry value) { + super.setPadding(value.add(_contentPadding)); + _savedPadding = value; + } + + BoxPainter? _painter; + + Decoration get decoration => _decoration; + Decoration _decoration; + + set decoration(Decoration value) { + if (value == _decoration) return; + _painter?.dispose(); + _painter = null; + _decoration = value; + markNeedsPaint(); + } + + ImageConfiguration get configuration => _configuration; + ImageConfiguration _configuration; + + set configuration(ImageConfiguration value) { + if (value == _configuration) return; + _configuration = value; + markNeedsPaint(); + } + + @override + TextRange getLineBoundary(TextPosition position) { + 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().offset, + end: rangeInChild.end + child.getContainer().offset, + ); + } + + @override + Offset getOffsetForCaret(TextPosition position) { + final child = childAtPosition(position); + return child.getOffsetForCaret(TextPosition( + offset: position.offset - child.getContainer().offset, + affinity: position.affinity, + )) + + (child.parentData as BoxParentData).offset; + } + + @override + TextPosition getPositionForOffset(Offset offset) { + final child = childAtOffset(offset)!; + final parentData = child.parentData as BoxParentData; + final localPosition = + child.getPositionForOffset(offset - parentData.offset); + return TextPosition( + offset: localPosition.offset + child.getContainer().offset, + affinity: localPosition.affinity, + ); + } + + @override + TextRange getWordBoundary(TextPosition position) { + final child = childAtPosition(position); + final nodeOffset = child.getContainer().offset; + final childWord = child + .getWordBoundary(TextPosition(offset: position.offset - nodeOffset)); + return TextRange( + start: childWord.start + nodeOffset, + end: childWord.end + nodeOffset, + ); + } + + @override + TextPosition? getPositionAbove(TextPosition position) { + assert(position.offset < getContainer().length); + + 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().offset); + } + + final sibling = childBefore(child); + if (sibling == null) { + return null; + } + + final caretOffset = child.getOffsetForCaret(childLocalPosition); + final testPosition = + TextPosition(offset: sibling.getContainer().length - 1); + final testOffset = sibling.getOffsetForCaret(testPosition); + final finalOffset = Offset(caretOffset.dx, testOffset.dy); + return TextPosition( + offset: sibling.getContainer().offset + + sibling.getPositionForOffset(finalOffset).offset); + } + + @override + TextPosition? getPositionBelow(TextPosition position) { + assert(position.offset < getContainer().length); + + 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().offset); + } + + final sibling = childAfter(child); + if (sibling == null) { + return null; + } + + 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().offset + + sibling.getPositionForOffset(finalOffset).offset); + } + + @override + double preferredLineHeight(TextPosition position) { + 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, preferredLineHeight(selection.extent)) + + getOffsetForCaret(selection.extent), + null); + } + + final baseNode = getContainer().queryChild(selection.start, false).node; + var baseChild = firstChild; + while (baseChild != null) { + if (baseChild.getContainer() == baseNode) { + break; + } + baseChild = childAfter(baseChild); + } + assert(baseChild != null); + + final basePoint = baseChild!.getBaseEndpointForSelection( + localSelection(baseChild.getContainer(), selection, true)); + return TextSelectionPoint( + basePoint.point + (baseChild.parentData as BoxParentData).offset, + basePoint.direction); + } + + @override + TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) { + if (selection.isCollapsed) { + return TextSelectionPoint( + Offset(0, preferredLineHeight(selection.extent)) + + getOffsetForCaret(selection.extent), + null); + } + + final extentNode = getContainer().queryChild(selection.end, false).node; + + var extentChild = firstChild; + while (extentChild != null) { + if (extentChild.getContainer() == extentNode) { + break; + } + extentChild = childAfter(extentChild); + } + assert(extentChild != null); + + final extentPoint = extentChild!.getExtentEndpointForSelection( + localSelection(extentChild.getContainer(), selection, true)); + return TextSelectionPoint( + extentPoint.point + (extentChild.parentData as BoxParentData).offset, + extentPoint.direction); + } + + @override + void detach() { + _painter?.dispose(); + _painter = null; + super.detach(); + markNeedsPaint(); + } + + @override + void paint(PaintingContext context, Offset offset) { + _paintDecoration(context, offset); + defaultPaint(context, offset); + } + + void _paintDecoration(PaintingContext context, Offset offset) { + _painter ??= _decoration.createBoxPainter(markNeedsPaint); + + final decorationPadding = resolvedPadding! - _contentPadding; + + final filledConfiguration = + configuration.copyWith(size: decorationPadding.deflateSize(size)); + final debugSaveCount = context.canvas.getSaveCount(); + + final decorationOffset = + offset.translate(decorationPadding.left, decorationPadding.top); + _painter!.paint(context.canvas, decorationOffset, filledConfiguration); + if (debugSaveCount != context.canvas.getSaveCount()) { + throw '${_decoration.runtimeType} painter had mismatching save and restore calls.'; + } + if (decoration.isComplex) { + context.setIsComplexHint(); + } + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return defaultHitTestChildren(result, position: position); + } +} + +class _EditableBlock extends MultiChildRenderObjectWidget { + _EditableBlock( + this.block, + this.textDirection, + this.padding, + this.scrollBottomInset, + this.decoration, + this.contentPadding, + List children) + : super(children: children); + + final Block block; + final TextDirection textDirection; + final Tuple2 padding; + final double scrollBottomInset; + final Decoration decoration; + final EdgeInsets? contentPadding; + + EdgeInsets get _padding => + EdgeInsets.only(top: padding.item1, bottom: padding.item2); + + EdgeInsets get _contentPadding => contentPadding ?? EdgeInsets.zero; + + @override + RenderEditableTextBlock createRenderObject(BuildContext context) { + return RenderEditableTextBlock( + block: block, + textDirection: textDirection, + padding: _padding, + scrollBottomInset: scrollBottomInset, + decoration: decoration, + contentPadding: _contentPadding, + ); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderEditableTextBlock renderObject) { + renderObject + ..setContainer(block) + ..textDirection = textDirection + ..scrollBottomInset = scrollBottomInset + ..setPadding(_padding) + ..decoration = decoration + ..contentPadding = _contentPadding; + } +} + +class _NumberPoint extends StatelessWidget { + const _NumberPoint({ + required this.index, + required this.indentLevelCounts, + required this.count, + required this.style, + required this.width, + required this.attrs, + this.withDot = true, + this.padding = 0.0, + Key? key, + }) : super(key: key); + + final int index; + final Map indentLevelCounts; + final int count; + final TextStyle style; + final double width; + final Map attrs; + final bool withDot; + final double padding; + + @override + Widget build(BuildContext context) { + var s = index.toString(); + int? level = 0; + if (!attrs.containsKey(Attribute.indent.key) && + !indentLevelCounts.containsKey(1)) { + indentLevelCounts.clear(); + return Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: EdgeInsetsDirectional.only(end: padding), + child: Text(withDot ? '$s.' : s, style: style), + ); + } + if (attrs.containsKey(Attribute.indent.key)) { + level = attrs[Attribute.indent.key]!.value; + } else { + // first level but is back from previous indent level + // supposed to be "2." + indentLevelCounts[0] = 1; + } + if (indentLevelCounts.containsKey(level! + 1)) { + // last visited level is done, going up + indentLevelCounts.remove(level + 1); + } + final count = (indentLevelCounts[level] ?? 0) + 1; + indentLevelCounts[level] = count; + + s = count.toString(); + if (level % 3 == 1) { + // a. b. c. d. e. ... + s = _toExcelSheetColumnTitle(count); + } else if (level % 3 == 2) { + // i. ii. iii. ... + s = _intToRoman(count); + } + // level % 3 == 0 goes back to 1. 2. 3. + + return Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: EdgeInsetsDirectional.only(end: padding), + child: Text(withDot ? '$s.' : s, style: style), + ); + } + + String _toExcelSheetColumnTitle(int n) { + final result = StringBuffer(); + while (n > 0) { + n--; + result.write(String.fromCharCode((n % 26).floor() + 97)); + n = (n / 26).floor(); + } + + return result.toString().split('').reversed.join(); + } + + String _intToRoman(int input) { + var num = input; + + if (num < 0) { + return ''; + } else if (num == 0) { + return 'nulla'; + } + + final builder = StringBuffer(); + for (var a = 0; a < arabianRomanNumbers.length; a++) { + final times = (num / arabianRomanNumbers[a]) + .truncate(); // equals 1 only when arabianRomanNumbers[a] = num + // executes n times where n is the number of times you have to add + // the current roman number value to reach current num. + builder.write(romanNumbers[a] * times); + num -= times * + arabianRomanNumbers[ + a]; // subtract previous roman number value from num + } + + return builder.toString().toLowerCase(); + } +} + +class _BulletPoint extends StatelessWidget { + const _BulletPoint({ + required this.style, + required this.width, + Key? key, + }) : super(key: key); + + final TextStyle style; + final double width; + + @override + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: const EdgeInsetsDirectional.only(end: 13), + child: Text('•', style: style), + ); + } +} + +class _Checkbox extends StatelessWidget { + const _Checkbox({ + Key? key, + this.style, + this.width, + this.isChecked = false, + this.offset, + this.onTap, + }) : super(key: key); + final TextStyle? style; + final double? width; + final bool isChecked; + final int? offset; + final Function(int, bool)? onTap; + + void _onCheckboxClicked(bool? newValue) { + if (onTap != null && newValue != null && offset != null) { + onTap!(offset!, newValue); + } + } + + @override + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topEnd, + width: width, + padding: const EdgeInsetsDirectional.only(end: 13), + child: GestureDetector( + onLongPress: () => _onCheckboxClicked(!isChecked), + child: Checkbox( + value: isChecked, + onChanged: _onCheckboxClicked, + ), + ), + ); + } +} diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart new file mode 100644 index 00000000..ef80a024 --- /dev/null +++ b/lib/src/widgets/text_line.dart @@ -0,0 +1,892 @@ +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.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 { + const TextLine({ + required this.line, + required this.embedBuilder, + required this.styles, + this.textDirection, + 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) { + final embed = line.children.single as Embed; + return EmbedProxy(embedBuilder(context, embed)); + } + + final textSpan = _buildTextSpan(context); + final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); + final textAlign = _getTextAlign(); + final child = RichText( + text: textSpan, + textAlign: textAlign, + textDirection: textDirection, + strutStyle: strutStyle, + textScaleFactor: MediaQuery.textScaleFactorOf(context), + ); + return RichTextProxy( + child, + textSpan.style!, + textAlign, + textDirection!, + 1, + Localizations.localeOf(context), + strutStyle, + TextWidthBasis.parent, + null); + } + + TextAlign _getTextAlign() { + final alignment = line.style.attributes[Attribute.align.key]; + if (alignment == Attribute.leftAlignment) { + return TextAlign.left; + } else if (alignment == Attribute.centerAlignment) { + return TextAlign.center; + } else if (alignment == Attribute.rightAlignment) { + return TextAlign.right; + } else if (alignment == Attribute.justifyAlignment) { + return TextAlign.justify; + } + return TextAlign.start; + } + + TextSpan _buildTextSpan(BuildContext context) { + final defaultStyles = styles; + final children = line.children + .map((node) => _getTextSpanFromNode(defaultStyles, node)) + .toList(growable: false); + + var textStyle = const TextStyle(); + + if (line.style.containsKey(Attribute.placeholder.key)) { + textStyle = defaultStyles.placeHolder!.style; + return TextSpan(children: children, style: textStyle); + } + + 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, + }; + + textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style); + + final block = line.style.getBlockExceptHeader(); + TextStyle? toMerge; + if (block == Attribute.blockQuote) { + toMerge = defaultStyles.quote!.style; + } else if (block == Attribute.codeBlock) { + toMerge = defaultStyles.code!.style; + } else if (block != null) { + toMerge = defaultStyles.lists!.style; + } + + textStyle = textStyle.merge(toMerge); + + return TextSpan(children: children, style: textStyle); + } + + TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { + final textNode = node as leaf.Text; + final style = textNode.style; + var res = const TextStyle(); + final color = textNode.style.attributes[Attribute.color.key]; + + { + 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, + }.forEach((k, s) { + if (style.values.any((v) => v.key == k)) { + if (k == Attribute.underline.key || k == Attribute.strikeThrough.key) { + var textColor = defaultStyles.color; + if (color?.value is String) { + textColor = stringToColor(color?.value); + } + res = _merge(res.copyWith(decorationColor: textColor), + s!.copyWith(decorationColor: textColor)); + } else { + res = _merge(res, s!); + } + } + }); + + final font = textNode.style.attributes[Attribute.font.key]; + if (font != null && font.value != null) { + res = res.merge(TextStyle(fontFamily: font.value)); + } + + final size = textNode.style.attributes[Attribute.size.key]; + if (size != null && size.value != null) { + switch (size.value) { + case 'small': + res = res.merge(defaultStyles.sizeSmall); + break; + case 'large': + res = res.merge(defaultStyles.sizeLarge); + break; + case 'huge': + res = res.merge(defaultStyles.sizeHuge); + break; + default: + final fontSize = double.tryParse(size.value); + if (fontSize != null) { + res = res.merge(TextStyle(fontSize: fontSize)); + } else { + throw 'Invalid size ${size.value}'; + } + } + } + + if (color != null && color.value != null) { + var textColor = defaultStyles.color; + if (color.value is String) { + textColor = stringToColor(color.value); + } + if (textColor != null) { + res = res.merge(TextStyle(color: textColor)); + } + } + + 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)); + } + + return TextSpan(text: textNode.value, style: res); + } + + TextStyle _merge(TextStyle a, TextStyle b) { + final decorations = []; + if (a.decoration != null) { + decorations.add(a.decoration); + } + if (b.decoration != null) { + decorations.add(b.decoration); + } + return a.merge(b).apply( + decoration: TextDecoration.combine( + List.castFrom(decorations))); + } +} + +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; + final double indentWidth; + final Tuple2 verticalSpacing; + final TextDirection textDirection; + final TextSelection textSelection; + final Color color; + final bool enableInteractiveSelection; + final bool hasFocus; + final double devicePixelRatio; + final CursorCont cursorCont; + + @override + RenderObjectElement createElement() { + return _TextLineElement(this); + } + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderEditableTextLine( + line, + textDirection, + textSelection, + enableInteractiveSelection, + hasFocus, + devicePixelRatio, + _getPadding(), + color, + cursorCont); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderEditableTextLine renderObject) { + renderObject + ..setLine(line) + ..setPadding(_getPadding()) + ..setTextDirection(textDirection) + ..setTextSelection(textSelection) + ..setColor(color) + ..setEnableInteractiveSelection(enableInteractiveSelection) + ..hasFocus = hasFocus + ..setDevicePixelRatio(devicePixelRatio) + ..setCursorCont(cursorCont); + } + + EdgeInsetsGeometry _getPadding() { + return EdgeInsetsDirectional.only( + start: indentWidth, + top: verticalSpacing.item1, + bottom: verticalSpacing.item2); + } +} + +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; + TextDirection textDirection; + TextSelection textSelection; + Color color; + bool enableInteractiveSelection; + bool hasFocus = false; + double devicePixelRatio; + EdgeInsetsGeometry padding; + CursorCont cursorCont; + EdgeInsets? _resolvedPadding; + bool? _containsCursor; + List? _selectedRects; + Rect? _caretPrototype; + final Map children = {}; + + Iterable get _children sync* { + if (_leading != null) { + yield _leading!; + } + if (_body != null) { + yield _body!; + } + } + + void setCursorCont(CursorCont c) { + if (cursorCont == c) { + return; + } + cursorCont = c; + markNeedsLayout(); + } + + void setDevicePixelRatio(double d) { + if (devicePixelRatio == d) { + return; + } + devicePixelRatio = d; + markNeedsLayout(); + } + + void setEnableInteractiveSelection(bool val) { + if (enableInteractiveSelection == val) { + return; + } + + markNeedsLayout(); + markNeedsSemanticsUpdate(); + } + + void setColor(Color c) { + if (color == c) { + return; + } + + color = c; + if (containsTextSelection()) { + markNeedsPaint(); + } + } + + void setTextSelection(TextSelection t) { + if (textSelection == t) { + return; + } + + final containsSelection = containsTextSelection(); + if (attached && containsCursor()) { + cursorCont.removeListener(markNeedsLayout); + cursorCont.color.removeListener(markNeedsPaint); + } + + textSelection = t; + _selectedRects = null; + _containsCursor = null; + if (attached && containsCursor()) { + cursorCont.addListener(markNeedsLayout); + cursorCont.color.addListener(markNeedsPaint); + } + + if (containsSelection || containsTextSelection()) { + markNeedsPaint(); + } + } + + void setTextDirection(TextDirection t) { + if (textDirection == t) { + return; + } + textDirection = t; + _resolvedPadding = null; + markNeedsLayout(); + } + + void setLine(Line l) { + if (line == l) { + return; + } + line = l; + _containsCursor = null; + markNeedsLayout(); + } + + void setPadding(EdgeInsetsGeometry p) { + assert(p.isNonNegative); + if (padding == p) { + return; + } + padding = p; + _resolvedPadding = null; + markNeedsLayout(); + } + + void setLeading(RenderBox? l) { + _leading = _updateChild(_leading, l, TextLineSlot.LEADING); + } + + void setBody(RenderContentProxyBox? b) { + _body = _updateChild(_body, b, TextLineSlot.BODY) as RenderContentProxyBox?; + } + + bool containsTextSelection() { + return line.documentOffset <= textSelection.end && + textSelection.start <= line.documentOffset + line.length - 1; + } + + bool containsCursor() { + return _containsCursor ??= textSelection.isCollapsed && + line.containsOffset(textSelection.baseOffset); + } + + RenderBox? _updateChild( + RenderBox? old, RenderBox? newChild, TextLineSlot slot) { + if (old != null) { + dropChild(old); + children.remove(slot); + } + if (newChild != null) { + children[slot] = newChild; + adoptChild(newChild); + } + return newChild; + } + + List _getBoxes(TextSelection textSelection) { + final parentData = _body!.parentData as BoxParentData?; + return _body!.getBoxesForSelection(textSelection).map((box) { + return TextBox.fromLTRBD( + box.left + parentData!.offset.dx, + box.top + parentData.offset.dy, + box.right + parentData.offset.dx, + box.bottom + parentData.offset.dy, + box.direction, + ); + }).toList(growable: false); + } + + void _resolvePadding() { + if (_resolvedPadding != null) { + return; + } + _resolvedPadding = padding.resolve(textDirection); + assert(_resolvedPadding!.isNonNegative); + } + + @override + TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection) { + return _getEndpointForSelection(textSelection, true); + } + + @override + TextSelectionPoint getExtentEndpointForSelection( + TextSelection textSelection) { + return _getEndpointForSelection(textSelection, false); + } + + TextSelectionPoint _getEndpointForSelection( + TextSelection textSelection, bool first) { + if (textSelection.isCollapsed) { + return TextSelectionPoint( + Offset(0, preferredLineHeight(textSelection.extent)) + + getOffsetForCaret(textSelection.extent), + null); + } + final boxes = _getBoxes(textSelection); + assert(boxes.isNotEmpty); + final targetBox = first ? boxes.first : boxes.last; + return TextSelectionPoint( + Offset(first ? targetBox.start : targetBox.end, targetBox.bottom), + targetBox.direction); + } + + @override + TextRange getLineBoundary(TextPosition position) { + final lineDy = getOffsetForCaret(position) + .translate(0, 0.5 * preferredLineHeight(position)) + .dy; + final lineBoxes = + _getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1)) + .where((element) => element.top < lineDy && element.bottom > lineDy) + .toList(growable: false); + return TextRange( + start: + getPositionForOffset(Offset(lineBoxes.first.left, lineDy)).offset, + end: getPositionForOffset(Offset(lineBoxes.last.right, lineDy)).offset); + } + + @override + Offset getOffsetForCaret(TextPosition position) { + return _body!.getOffsetForCaret(position, _caretPrototype) + + (_body!.parentData as BoxParentData).offset; + } + + @override + TextPosition? getPositionAbove(TextPosition position) { + return _getPosition(position, -0.5); + } + + @override + TextPosition? getPositionBelow(TextPosition position) { + return _getPosition(position, 1.5); + } + + TextPosition? _getPosition(TextPosition textPosition, double dyScale) { + assert(textPosition.offset < line.length); + final offset = getOffsetForCaret(textPosition) + .translate(0, dyScale * preferredLineHeight(textPosition)); + if (_body!.size + .contains(offset - (_body!.parentData as BoxParentData).offset)) { + return getPositionForOffset(offset); + } + return null; + } + + @override + TextPosition getPositionForOffset(Offset offset) { + return _body!.getPositionForOffset( + offset - (_body!.parentData as BoxParentData).offset); + } + + @override + TextRange getWordBoundary(TextPosition position) { + return _body!.getWordBoundary(position); + } + + @override + double preferredLineHeight(TextPosition position) { + return _body!.getPreferredLineHeight(); + } + + @override + container.Container getContainer() { + return line; + } + + double get cursorWidth => cursorCont.style.width; + + double get cursorHeight => + cursorCont.style.height ?? + preferredLineHeight(const TextPosition(offset: 0)); + + void _computeCaretPrototype() { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + _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, 2, cursorWidth, cursorHeight - 4.0); + break; + default: + throw 'Invalid platform'; + } + } + + @override + void attach(covariant PipelineOwner owner) { + super.attach(owner); + for (final child in _children) { + child.attach(owner); + } + if (containsCursor()) { + cursorCont.addListener(markNeedsLayout); + cursorCont.cursorColor.addListener(markNeedsPaint); + } + } + + @override + void detach() { + super.detach(); + for (final child in _children) { + child.detach(); + } + if (containsCursor()) { + cursorCont.removeListener(markNeedsLayout); + cursorCont.cursorColor.removeListener(markNeedsPaint); + } + } + + @override + void redepthChildren() { + _children.forEach(redepthChild); + } + + @override + void visitChildren(RenderObjectVisitor visitor) { + _children.forEach(visitor); + } + + @override + List debugDescribeChildren() { + final value = []; + void add(RenderBox? child, String name) { + if (child != null) { + value.add(child.toDiagnosticsNode(name: name)); + } + } + + add(_leading, 'leading'); + add(_body, 'body'); + return value; + } + + @override + bool get sizedByParent => false; + + @override + double computeMinIntrinsicWidth(double height) { + _resolvePadding(); + final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; + final leadingWidth = _leading == null + ? 0 + : _leading!.getMinIntrinsicWidth(height - verticalPadding) as int; + final bodyWidth = _body == null + ? 0 + : _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding)) + as int; + return horizontalPadding + leadingWidth + bodyWidth; + } + + @override + double computeMaxIntrinsicWidth(double height) { + _resolvePadding(); + final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; + final leadingWidth = _leading == null + ? 0 + : _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int; + final bodyWidth = _body == null + ? 0 + : _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding)) + as int; + return horizontalPadding + leadingWidth + bodyWidth; + } + + @override + double computeMinIntrinsicHeight(double width) { + _resolvePadding(); + final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; + if (_body != null) { + return _body! + .getMinIntrinsicHeight(math.max(0, width - horizontalPadding)) + + verticalPadding; + } + return verticalPadding; + } + + @override + double computeMaxIntrinsicHeight(double width) { + _resolvePadding(); + final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; + final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; + if (_body != null) { + return _body! + .getMaxIntrinsicHeight(math.max(0, width - horizontalPadding)) + + verticalPadding; + } + return verticalPadding; + } + + @override + double computeDistanceToActualBaseline(TextBaseline baseline) { + _resolvePadding(); + return _body!.getDistanceToActualBaseline(baseline)! + + _resolvedPadding!.top; + } + + @override + void performLayout() { + final constraints = this.constraints; + _selectedRects = null; + + _resolvePadding(); + assert(_resolvedPadding != null); + + if (_body == null && _leading == null) { + size = constraints.constrain(Size( + _resolvedPadding!.left + _resolvedPadding!.right, + _resolvedPadding!.top + _resolvedPadding!.bottom, + )); + return; + } + final innerConstraints = constraints.deflate(_resolvedPadding!); + + final indentWidth = textDirection == TextDirection.ltr + ? _resolvedPadding!.left + : _resolvedPadding!.right; + + _body!.layout(innerConstraints, parentUsesSize: true); + (_body!.parentData as BoxParentData).offset = + Offset(_resolvedPadding!.left, _resolvedPadding!.top); + + if (_leading != null) { + final leadingConstraints = innerConstraints.copyWith( + minWidth: indentWidth, + maxWidth: indentWidth, + maxHeight: _body!.size.height); + _leading!.layout(leadingConstraints, parentUsesSize: true); + (_leading!.parentData as BoxParentData).offset = + Offset(0, _resolvedPadding!.top); + } + + size = constraints.constrain(Size( + _resolvedPadding!.left + _body!.size.width + _resolvedPadding!.right, + _resolvedPadding!.top + _body!.size.height + _resolvedPadding!.bottom, + )); + + _computeCaretPrototype(); + } + + CursorPainter get _cursorPainter => CursorPainter( + _body, + cursorCont.style, + _caretPrototype, + cursorCont.cursorColor.value, + devicePixelRatio, + ); + + @override + void paint(PaintingContext context, Offset offset) { + if (_leading != null) { + final parentData = _leading!.parentData as BoxParentData; + final effectiveOffset = offset + parentData.offset; + context.paintChild(_leading!, effectiveOffset); + } + + if (_body != null) { + final parentData = _body!.parentData as BoxParentData; + final effectiveOffset = offset + parentData.offset; + if (enableInteractiveSelection && + line.documentOffset <= textSelection.end && + textSelection.start <= line.documentOffset + line.length - 1) { + final local = localSelection(line, textSelection, false); + _selectedRects ??= _body!.getBoxesForSelection( + local, + ); + _paintSelection(context, effectiveOffset); + } + + if (hasFocus && + cursorCont.show.value && + containsCursor() && + !cursorCont.style.paintAboveText) { + _paintCursor(context, effectiveOffset); + } + + context.paintChild(_body!, effectiveOffset); + + if (hasFocus && + cursorCont.show.value && + containsCursor() && + cursorCont.style.paintAboveText) { + _paintCursor(context, effectiveOffset); + } + } + } + + void _paintSelection(PaintingContext context, Offset effectiveOffset) { + assert(_selectedRects != null); + final paint = Paint()..color = color; + for (final box in _selectedRects!) { + context.canvas.drawRect(box.toRect().shift(effectiveOffset), paint); + } + } + + void _paintCursor(PaintingContext context, Offset effectiveOffset) { + final position = TextPosition( + offset: textSelection.extentOffset - line.documentOffset, + affinity: textSelection.base.affinity, + ); + _cursorPainter.paint(context.canvas, effectiveOffset, position); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return _children.first.hitTest(result, position: position); + } +} + +class _TextLineElement extends RenderObjectElement { + _TextLineElement(EditableTextLine line) : super(line); + + final Map _slotToChildren = {}; + + @override + EditableTextLine get widget => super.widget as EditableTextLine; + + @override + RenderEditableTextLine get renderObject => + super.renderObject as RenderEditableTextLine; + + @override + void visitChildren(ElementVisitor visitor) { + _slotToChildren.values.forEach(visitor); + } + + @override + void forgetChild(Element child) { + assert(_slotToChildren.containsValue(child)); + assert(child.slot is TextLineSlot); + assert(_slotToChildren.containsKey(child.slot)); + _slotToChildren.remove(child.slot); + super.forgetChild(child); + } + + @override + void mount(Element? parent, dynamic newSlot) { + super.mount(parent, newSlot); + _mountChild(widget.leading, TextLineSlot.LEADING); + _mountChild(widget.body, TextLineSlot.BODY); + } + + @override + void update(EditableTextLine newWidget) { + super.update(newWidget); + assert(widget == newWidget); + _updateChild(widget.leading, TextLineSlot.LEADING); + _updateChild(widget.body, TextLineSlot.BODY); + } + + @override + void insertRenderObjectChild(RenderBox child, TextLineSlot? slot) { + // assert(child is RenderBox); + _updateRenderObject(child, slot); + assert(renderObject.children.keys.contains(slot)); + } + + @override + void removeRenderObjectChild(RenderObject child, TextLineSlot? slot) { + assert(child is RenderBox); + assert(renderObject.children[slot!] == child); + _updateRenderObject(null, slot); + assert(!renderObject.children.keys.contains(slot)); + } + + @override + void moveRenderObjectChild( + RenderObject child, dynamic oldSlot, dynamic newSlot) { + throw UnimplementedError(); + } + + void _mountChild(Widget? widget, TextLineSlot slot) { + final oldChild = _slotToChildren[slot]; + final newChild = updateChild(oldChild, widget, slot); + if (oldChild != null) { + _slotToChildren.remove(slot); + } + if (newChild != null) { + _slotToChildren[slot] = newChild; + } + } + + void _updateRenderObject(RenderBox? child, TextLineSlot? slot) { + switch (slot) { + case TextLineSlot.LEADING: + renderObject.setLeading(child); + break; + case TextLineSlot.BODY: + renderObject.setBody(child as RenderContentProxyBox?); + break; + default: + throw UnimplementedError(); + } + } + + void _updateChild(Widget? widget, TextLineSlot slot) { + final oldChild = _slotToChildren[slot]; + final newChild = updateChild(oldChild, widget, slot); + if (oldChild != null) { + _slotToChildren.remove(slot); + } + if (newChild != null) { + _slotToChildren[slot] = newChild; + } + } +} diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart new file mode 100644 index 00000000..a8748de1 --- /dev/null +++ b/lib/src/widgets/text_selection.dart @@ -0,0 +1,726 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; + +import '../models/documents/nodes/node.dart'; +import 'editor.dart'; + +TextSelection localSelection(Node node, TextSelection selection, fromParent) { + final base = fromParent ? node.offset : node.documentOffset; + assert(base <= selection.end && selection.start <= base + node.length - 1); + + 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)); +} + +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; + final Widget debugRequiredFor; + final LayerLink toolbarLayerLink; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final RenderEditor? renderObject; + final TextSelectionControls selectionCtrls; + final TextSelectionDelegate selectionDelegate; + final DragStartBehavior dragStartBehavior; + final VoidCallback? onSelectionHandleTapped; + final ClipboardStatusNotifier clipboardStatus; + late AnimationController _toolbarController; + List? _handles; + OverlayEntry? toolbar; + + TextSelection get _selection => value.selection; + + Animation get _toolbarOpacity => _toolbarController.view; + + void setHandlesVisible(bool visible) { + if (handlesVisible == visible) { + return; + } + handlesVisible = visible; + if (SchedulerBinding.instance!.schedulerPhase == + SchedulerPhase.persistentCallbacks) { + SchedulerBinding.instance!.addPostFrameCallback(markNeedsBuild); + } else { + markNeedsBuild(); + } + } + + void hideHandles() { + if (_handles == null) { + return; + } + _handles![0].remove(); + _handles![1].remove(); + _handles = null; + } + + void hideToolbar() { + assert(toolbar != null); + _toolbarController.stop(); + toolbar!.remove(); + toolbar = null; + } + + void showToolbar() { + assert(toolbar == null); + toolbar = OverlayEntry(builder: _buildToolbar); + Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! + .insert(toolbar!); + _toolbarController.forward(from: 0); + } + + Widget _buildHandle( + BuildContext context, _TextSelectionHandlePosition position) { + if (_selection.isCollapsed && + position == _TextSelectionHandlePosition.END) { + return Container(); + } + return Visibility( + visible: handlesVisible, + child: _TextSelectionHandleOverlay( + onSelectionHandleChanged: (newSelection) { + _handleSelectionHandleChanged(newSelection, position); + }, + onSelectionHandleTapped: onSelectionHandleTapped, + startHandleLayerLink: startHandleLayerLink, + endHandleLayerLink: endHandleLayerLink, + renderObject: renderObject, + selection: _selection, + selectionControls: selectionCtrls, + position: position, + dragStartBehavior: dragStartBehavior, + )); + } + + void update(TextEditingValue newValue) { + if (value == newValue) { + return; + } + value = newValue; + if (SchedulerBinding.instance!.schedulerPhase == + SchedulerPhase.persistentCallbacks) { + SchedulerBinding.instance!.addPostFrameCallback(markNeedsBuild); + } else { + markNeedsBuild(); + } + } + + void _handleSelectionHandleChanged( + TextSelection? newSelection, _TextSelectionHandlePosition position) { + TextPosition textPosition; + switch (position) { + case _TextSelectionHandlePosition.START: + textPosition = newSelection != null + ? newSelection.base + : const TextPosition(offset: 0); + break; + case _TextSelectionHandlePosition.END: + textPosition = newSelection != null + ? newSelection.extent + : const TextPosition(offset: 0); + break; + default: + throw 'Invalid position'; + } + selectionDelegate + ..textEditingValue = + value.copyWith(selection: newSelection, composing: TextRange.empty) + ..bringIntoView(textPosition); + } + + Widget _buildToolbar(BuildContext context) { + final endpoints = renderObject!.getEndpointsForSelection(_selection); + + final editingRegion = Rect.fromPoints( + renderObject!.localToGlobal(Offset.zero), + renderObject!.localToGlobal(renderObject!.size.bottomRight(Offset.zero)), + ); + + final baseLineHeight = renderObject!.preferredLineHeight(_selection.base); + final extentLineHeight = + renderObject!.preferredLineHeight(_selection.extent); + final smallestLineHeight = math.min(baseLineHeight, extentLineHeight); + final isMultiline = endpoints.last.point.dy - endpoints.first.point.dy > + smallestLineHeight / 2; + + final midX = isMultiline + ? editingRegion.width / 2 + : (endpoints.first.point.dx + endpoints.last.point.dx) / 2; + + final midpoint = Offset( + midX, + endpoints[0].point.dy - baseLineHeight, + ); + + return FadeTransition( + opacity: _toolbarOpacity, + child: CompositedTransformFollower( + link: toolbarLayerLink, + showWhenUnlinked: false, + offset: -editingRegion.topLeft, + child: selectionCtrls.buildToolbar( + context, + editingRegion, + baseLineHeight, + midpoint, + endpoints, + selectionDelegate, + clipboardStatus, + const Offset(0, 0)), + ), + ); + } + + void markNeedsBuild([Duration? duration]) { + if (_handles != null) { + _handles![0].markNeedsBuild(); + _handles![1].markNeedsBuild(); + } + toolbar?.markNeedsBuild(); + } + + void hide() { + if (_handles != null) { + _handles![0].remove(); + _handles![1].remove(); + _handles = null; + } + if (toolbar != null) { + hideToolbar(); + } + } + + void dispose() { + hide(); + _toolbarController.dispose(); + } + + void showHandles() { + assert(_handles == null); + _handles = [ + OverlayEntry( + builder: (context) => + _buildHandle(context, _TextSelectionHandlePosition.START)), + OverlayEntry( + builder: (context) => + _buildHandle(context, _TextSelectionHandlePosition.END)), + ]; + + Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! + .insertAll(_handles!); + } +} + +class _TextSelectionHandleOverlay extends StatefulWidget { + const _TextSelectionHandleOverlay({ + required this.selection, + required this.position, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.renderObject, + required this.onSelectionHandleChanged, + required this.onSelectionHandleTapped, + required this.selectionControls, + this.dragStartBehavior = DragStartBehavior.start, + Key? key, + }) : super(key: key); + + final TextSelection selection; + final _TextSelectionHandlePosition position; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final RenderEditor? renderObject; + final ValueChanged onSelectionHandleChanged; + final VoidCallback? onSelectionHandleTapped; + final TextSelectionControls selectionControls; + final DragStartBehavior dragStartBehavior; + + @override + _TextSelectionHandleOverlayState createState() => + _TextSelectionHandleOverlayState(); + + ValueListenable? get _visibility { + switch (position) { + case _TextSelectionHandlePosition.START: + return renderObject!.selectionStartInViewport; + case _TextSelectionHandlePosition.END: + return renderObject!.selectionEndInViewport; + } + } +} + +class _TextSelectionHandleOverlayState + extends State<_TextSelectionHandleOverlay> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + Animation get _opacity => _controller.view; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: const Duration(milliseconds: 150), vsync: this); + + _handleVisibilityChanged(); + widget._visibility!.addListener(_handleVisibilityChanged); + } + + void _handleVisibilityChanged() { + if (widget._visibility!.value) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + + @override + void didUpdateWidget(_TextSelectionHandleOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + oldWidget._visibility!.removeListener(_handleVisibilityChanged); + _handleVisibilityChanged(); + widget._visibility!.addListener(_handleVisibilityChanged); + } + + @override + void dispose() { + widget._visibility!.removeListener(_handleVisibilityChanged); + _controller.dispose(); + super.dispose(); + } + + void _handleDragStart(DragStartDetails details) {} + + void _handleDragUpdate(DragUpdateDetails details) { + final position = + widget.renderObject!.getPositionForOffset(details.globalPosition); + if (widget.selection.isCollapsed) { + widget.onSelectionHandleChanged(TextSelection.fromPosition(position)); + return; + } + + final isNormalized = + widget.selection.extentOffset >= widget.selection.baseOffset; + TextSelection? newSelection; + switch (widget.position) { + case _TextSelectionHandlePosition.START: + newSelection = TextSelection( + baseOffset: + isNormalized ? position.offset : widget.selection.baseOffset, + extentOffset: + isNormalized ? widget.selection.extentOffset : position.offset, + ); + break; + case _TextSelectionHandlePosition.END: + newSelection = TextSelection( + baseOffset: + isNormalized ? widget.selection.baseOffset : position.offset, + extentOffset: + isNormalized ? position.offset : widget.selection.extentOffset, + ); + break; + } + + widget.onSelectionHandleChanged(newSelection); + } + + void _handleTap() { + if (widget.onSelectionHandleTapped != null) { + widget.onSelectionHandleTapped!(); + } + } + + @override + Widget build(BuildContext context) { + late LayerLink layerLink; + TextSelectionHandleType? type; + + switch (widget.position) { + case _TextSelectionHandlePosition.START: + layerLink = widget.startHandleLayerLink; + type = _chooseType( + widget.renderObject!.textDirection, + TextSelectionHandleType.left, + TextSelectionHandleType.right, + ); + break; + case _TextSelectionHandlePosition.END: + assert(!widget.selection.isCollapsed); + layerLink = widget.endHandleLayerLink; + type = _chooseType( + widget.renderObject!.textDirection, + TextSelectionHandleType.right, + TextSelectionHandleType.left, + ); + break; + } + + 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); + final handleSize = widget.selectionControls.getHandleSize(lineHeight); + + final handleRect = Rect.fromLTWH( + -handleAnchor.dx, + -handleAnchor.dy, + handleSize.width, + handleSize.height, + ); + + final interactiveRect = handleRect.expandToInclude( + Rect.fromCircle( + center: handleRect.center, radius: kMinInteractiveDimension / 2), + ); + 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), + math.max((interactiveRect.height - handleRect.height) / 2, 0), + ); + + return CompositedTransformFollower( + link: layerLink, + offset: interactiveRect.topLeft, + showWhenUnlinked: false, + child: FadeTransition( + opacity: _opacity, + child: Container( + alignment: Alignment.topLeft, + width: interactiveRect.width, + height: interactiveRect.height, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + dragStartBehavior: widget.dragStartBehavior, + onPanStart: _handleDragStart, + onPanUpdate: _handleDragUpdate, + onTap: _handleTap, + child: Padding( + padding: EdgeInsets.only( + left: padding.left, + top: padding.top, + right: padding.right, + bottom: padding.bottom, + ), + child: widget.selectionControls.buildHandle( + context, + type, + lineHeight, + ), + ), + ), + ), + ), + ); + } + + TextSelectionHandleType? _chooseType( + TextDirection textDirection, + TextSelectionHandleType ltrType, + TextSelectionHandleType rtlType, + ) { + if (widget.selection.isCollapsed) return TextSelectionHandleType.collapsed; + + switch (textDirection) { + case TextDirection.ltr: + return ltrType; + case TextDirection.rtl: + return rtlType; + } + } +} + +class EditorTextSelectionGestureDetector extends StatefulWidget { + const EditorTextSelectionGestureDetector({ + required this.child, + this.onTapDown, + this.onForcePressStart, + this.onForcePressEnd, + this.onSingleTapUp, + this.onSingleTapCancel, + this.onSingleLongTapStart, + this.onSingleLongTapMoveUpdate, + this.onSingleLongTapEnd, + this.onDoubleTapDown, + this.onDragSelectionStart, + this.onDragSelectionUpdate, + this.onDragSelectionEnd, + this.behavior, + Key? key, + }) : super(key: key); + + final GestureTapDownCallback? onTapDown; + + final GestureForcePressStartCallback? onForcePressStart; + + final GestureForcePressEndCallback? onForcePressEnd; + + final GestureTapUpCallback? onSingleTapUp; + + final GestureTapCancelCallback? onSingleTapCancel; + + final GestureLongPressStartCallback? onSingleLongTapStart; + + final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate; + + final GestureLongPressEndCallback? onSingleLongTapEnd; + + final GestureTapDownCallback? onDoubleTapDown; + + final GestureDragStartCallback? onDragSelectionStart; + + final DragSelectionUpdateCallback? onDragSelectionUpdate; + + final GestureDragEndCallback? onDragSelectionEnd; + + final HitTestBehavior? behavior; + + final Widget child; + + @override + State createState() => + _EditorTextSelectionGestureDetectorState(); +} + +class _EditorTextSelectionGestureDetectorState + extends State { + Timer? _doubleTapTimer; + Offset? _lastTapOffset; + bool _isDoubleTap = false; + + @override + void dispose() { + _doubleTapTimer?.cancel(); + _dragUpdateThrottleTimer?.cancel(); + super.dispose(); + } + + void _handleTapDown(TapDownDetails details) { + // renderObject.resetTapDownStatus(); + if (widget.onTapDown != null) { + widget.onTapDown!(details); + } + if (_doubleTapTimer != null && + _isWithinDoubleTapTolerance(details.globalPosition)) { + if (widget.onDoubleTapDown != null) { + widget.onDoubleTapDown!(details); + } + + _doubleTapTimer!.cancel(); + _doubleTapTimeout(); + _isDoubleTap = true; + } + } + + void _handleTapUp(TapUpDetails details) { + if (!_isDoubleTap) { + if (widget.onSingleTapUp != null) { + widget.onSingleTapUp!(details); + } + _lastTapOffset = details.globalPosition; + _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); + } + _isDoubleTap = false; + } + + void _handleTapCancel() { + if (widget.onSingleTapCancel != null) { + widget.onSingleTapCancel!(); + } + } + + DragStartDetails? _lastDragStartDetails; + DragUpdateDetails? _lastDragUpdateDetails; + Timer? _dragUpdateThrottleTimer; + + void _handleDragStart(DragStartDetails details) { + assert(_lastDragStartDetails == null); + _lastDragStartDetails = details; + if (widget.onDragSelectionStart != null) { + widget.onDragSelectionStart!(details); + } + } + + void _handleDragUpdate(DragUpdateDetails details) { + _lastDragUpdateDetails = details; + _dragUpdateThrottleTimer ??= + Timer(const Duration(milliseconds: 50), _handleDragUpdateThrottled); + } + + void _handleDragUpdateThrottled() { + assert(_lastDragStartDetails != null); + assert(_lastDragUpdateDetails != null); + if (widget.onDragSelectionUpdate != null) { + widget.onDragSelectionUpdate!( + _lastDragStartDetails!, _lastDragUpdateDetails!); + } + _dragUpdateThrottleTimer = null; + _lastDragUpdateDetails = null; + } + + void _handleDragEnd(DragEndDetails details) { + assert(_lastDragStartDetails != null); + if (_dragUpdateThrottleTimer != null) { + _dragUpdateThrottleTimer!.cancel(); + _handleDragUpdateThrottled(); + } + if (widget.onDragSelectionEnd != null) { + widget.onDragSelectionEnd!(details); + } + _dragUpdateThrottleTimer = null; + _lastDragStartDetails = null; + _lastDragUpdateDetails = null; + } + + void _forcePressStarted(ForcePressDetails details) { + _doubleTapTimer?.cancel(); + _doubleTapTimer = null; + if (widget.onForcePressStart != null) { + widget.onForcePressStart!(details); + } + } + + void _forcePressEnded(ForcePressDetails details) { + if (widget.onForcePressEnd != null) { + widget.onForcePressEnd!(details); + } + } + + void _handleLongPressStart(LongPressStartDetails details) { + if (!_isDoubleTap && widget.onSingleLongTapStart != null) { + widget.onSingleLongTapStart!(details); + } + } + + void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { + if (!_isDoubleTap && widget.onSingleLongTapMoveUpdate != null) { + widget.onSingleLongTapMoveUpdate!(details); + } + } + + void _handleLongPressEnd(LongPressEndDetails details) { + if (!_isDoubleTap && widget.onSingleLongTapEnd != null) { + widget.onSingleLongTapEnd!(details); + } + _isDoubleTap = false; + } + + void _doubleTapTimeout() { + _doubleTapTimer = null; + _lastTapOffset = null; + } + + bool _isWithinDoubleTapTolerance(Offset secondTapOffset) { + if (_lastTapOffset == null) { + return false; + } + + return (secondTapOffset - _lastTapOffset!).distance <= kDoubleTapSlop; + } + + @override + Widget build(BuildContext context) { + final gestures = {}; + + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (instance) { + instance + ..onTapDown = _handleTapDown + ..onTapUp = _handleTapUp + ..onTapCancel = _handleTapCancel; + }, + ); + + if (widget.onSingleLongTapStart != null || + widget.onSingleLongTapMoveUpdate != null || + widget.onSingleLongTapEnd != null) { + gestures[LongPressGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer( + debugOwner: this, kind: PointerDeviceKind.touch), + (instance) { + instance + ..onLongPressStart = _handleLongPressStart + ..onLongPressMoveUpdate = _handleLongPressMoveUpdate + ..onLongPressEnd = _handleLongPressEnd; + }, + ); + } + + if (widget.onDragSelectionStart != null || + widget.onDragSelectionUpdate != null || + widget.onDragSelectionEnd != null) { + gestures[HorizontalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer( + debugOwner: this, kind: PointerDeviceKind.mouse), + (instance) { + instance + ..dragStartBehavior = DragStartBehavior.down + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd; + }, + ); + } + + if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { + gestures[ForcePressGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => ForcePressGestureRecognizer(debugOwner: this), + (instance) { + instance + ..onStart = + widget.onForcePressStart != null ? _forcePressStarted : null + ..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null; + }, + ); + } + + return RawGestureDetector( + gestures: gestures, + excludeFromSemantics: true, + behavior: widget.behavior, + child: widget.child, + ); + } +} diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart new file mode 100644 index 00000000..c5fe2bab --- /dev/null +++ b/lib/src/widgets/toolbar.dart @@ -0,0 +1,1294 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +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: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'; + +typedef OnImagePickCallback = Future Function(File file); +typedef ImagePickImpl = Future Function(ImageSource source); + +class InsertEmbedButton extends StatelessWidget { + const InsertEmbedButton({ + required this.controller, + required this.icon, + this.fillColor, + Key? key, + }) : super(key: key); + + final QuillController controller; + final IconData icon; + final Color? fillColor; + + @override + Widget build(BuildContext context) { + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: controller.iconSize * 1.77, + icon: Icon( + icon, + size: controller.iconSize, + color: Theme.of(context).iconTheme.color, + ), + fillColor: fillColor ?? Theme.of(context).canvasColor, + onPressed: () { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + controller.replaceText(index, length, BlockEmbed.horizontalRule, null); + }, + ); + } +} + +class LinkStyleButton extends StatefulWidget { + const LinkStyleButton({ + required this.controller, + this.icon, + Key? key, + }) : super(key: key); + + final QuillController controller; + final IconData? icon; + + @override + _LinkStyleButtonState createState() => _LinkStyleButtonState(); +} + +class _LinkStyleButtonState extends State { + void _didChangeSelection() { + setState(() {}); + } + + @override + void initState() { + super.initState(); + widget.controller.addListener(_didChangeSelection); + } + + @override + void didUpdateWidget(covariant LinkStyleButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeSelection); + widget.controller.addListener(_didChangeSelection); + } + } + + @override + void dispose() { + super.dispose(); + widget.controller.removeListener(_didChangeSelection); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isEnabled = !widget.controller.selection.isCollapsed; + final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.controller.iconSize * 1.77, + icon: Icon( + widget.icon ?? Icons.link, + size: widget.controller.iconSize, + color: isEnabled ? theme.iconTheme.color : theme.disabledColor, + ), + fillColor: Theme.of(context).canvasColor, + onPressed: pressedHandler, + ); + } + + void _openLinkDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) { + return const _LinkDialog(); + }, + ).then(_linkSubmitted); + } + + void _linkSubmitted(String? value) { + if (value == null || value.isEmpty) { + return; + } + widget.controller.formatSelection(LinkAttribute(value)); + } +} + +class _LinkDialog extends StatefulWidget { + const _LinkDialog({Key? key}) : super(key: key); + + @override + _LinkDialogState createState() => _LinkDialogState(); +} + +class _LinkDialogState extends State<_LinkDialog> { + String _link = ''; + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: TextField( + decoration: const InputDecoration(labelText: 'Paste a link'), + autofocus: true, + onChanged: _linkChanged, + ), + actions: [ + TextButton( + onPressed: _link.isNotEmpty ? _applyLink : null, + child: const Text('Apply'), + ), + ], + ); + } + + void _linkChanged(String value) { + setState(() { + _link = value; + }); + } + + void _applyLink() { + Navigator.pop(context, _link); + } +} + +typedef ToggleStyleButtonBuilder = Widget Function( + BuildContext context, + Attribute attribute, + IconData icon, + Color? fillColor, + bool? isToggled, + VoidCallback? onPressed, +); + +class ToggleStyleButton extends StatefulWidget { + const ToggleStyleButton({ + required this.attribute, + required this.icon, + required this.controller, + this.fillColor, + this.childBuilder = defaultToggleStyleButtonBuilder, + Key? key, + }) : super(key: key); + + final Attribute attribute; + + final IconData icon; + + final Color? fillColor; + + final QuillController controller; + + final ToggleStyleButtonBuilder childBuilder; + + @override + _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); +} + +class _ToggleStyleButtonState extends State { + bool? _isToggled; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + void _didChangeEditingValue() { + setState(() { + _isToggled = + _getIsToggled(widget.controller.getSelectionStyle().attributes); + }); + } + + @override + void initState() { + super.initState(); + _isToggled = _getIsToggled(_selectionStyle.attributes); + widget.controller.addListener(_didChangeEditingValue); + } + + bool _getIsToggled(Map attrs) { + if (widget.attribute.key == Attribute.list.key) { + final attribute = attrs[widget.attribute.key]; + if (attribute == null) { + return false; + } + return attribute.value == widget.attribute.value; + } + return attrs.containsKey(widget.attribute.key); + } + + @override + void didUpdateWidget(covariant ToggleStyleButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _isToggled = _getIsToggled(_selectionStyle.attributes); + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isInCodeBlock = + _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); + final isEnabled = + !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key; + return widget.childBuilder(context, widget.attribute, widget.icon, + widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); + } + + void _toggleAttribute() { + widget.controller.formatSelection(_isToggled! + ? Attribute.clone(widget.attribute, null) + : widget.attribute); + } +} + +class ToggleCheckListButton extends StatefulWidget { + const ToggleCheckListButton({ + required this.icon, + required this.controller, + required this.attribute, + this.fillColor, + this.childBuilder = defaultToggleStyleButtonBuilder, + Key? key, + }) : super(key: key); + + final IconData icon; + + final Color? fillColor; + + final QuillController controller; + + final ToggleStyleButtonBuilder childBuilder; + + final Attribute attribute; + + @override + _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); +} + +class _ToggleCheckListButtonState extends State { + bool? _isToggled; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + void _didChangeEditingValue() { + setState(() { + _isToggled = + _getIsToggled(widget.controller.getSelectionStyle().attributes); + }); + } + + @override + void initState() { + super.initState(); + _isToggled = _getIsToggled(_selectionStyle.attributes); + widget.controller.addListener(_didChangeEditingValue); + } + + bool _getIsToggled(Map attrs) { + if (widget.attribute.key == Attribute.list.key) { + final attribute = attrs[widget.attribute.key]; + if (attribute == null) { + return false; + } + return attribute.value == widget.attribute.value || + attribute.value == Attribute.checked.value; + } + return attrs.containsKey(widget.attribute.key); + } + + @override + void didUpdateWidget(covariant ToggleCheckListButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _isToggled = _getIsToggled(_selectionStyle.attributes); + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isInCodeBlock = + _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); + final isEnabled = + !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key; + return widget.childBuilder(context, Attribute.unchecked, widget.icon, + widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); + } + + void _toggleAttribute() { + widget.controller.formatSelection(_isToggled! + ? Attribute.clone(Attribute.unchecked, null) + : Attribute.unchecked); + } +} + +Widget defaultToggleStyleButtonBuilder( + BuildContext context, + Attribute attribute, + IconData icon, + Color? fillColor, + bool? isToggled, + VoidCallback? onPressed, +) { + final theme = Theme.of(context); + final isEnabled = onPressed != null; + final iconColor = isEnabled + ? isToggled == true + ? theme.primaryIconTheme.color + : theme.iconTheme.color + : theme.disabledColor; + final fill = isToggled == true + ? theme.toggleableActiveColor + : fillColor ?? theme.canvasColor; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: 18 * 1.77, + icon: Icon(icon, size: 18, color: iconColor), + fillColor: fill, + onPressed: onPressed, + ); +} + +class SelectHeaderStyleButton extends StatefulWidget { + const SelectHeaderStyleButton({required this.controller, Key? key}) + : super(key: key); + + final QuillController controller; + + @override + _SelectHeaderStyleButtonState createState() => + _SelectHeaderStyleButtonState(); +} + +class _SelectHeaderStyleButtonState extends State { + Attribute? _value; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + void _didChangeEditingValue() { + setState(() { + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + }); + } + + void _selectAttribute(value) { + widget.controller.formatSelection(value); + } + + @override + void initState() { + super.initState(); + setState(() { + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + }); + widget.controller.addListener(_didChangeEditingValue); + } + + @override + void didUpdateWidget(covariant SelectHeaderStyleButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _selectHeadingStyleButtonBuilder( + context, _value, _selectAttribute, widget.controller.iconSize); + } +} + +Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, + ValueChanged onSelected, double iconSize) { + final _valueToText = { + Attribute.header: 'N', + Attribute.h1: 'H1', + Attribute.h2: 'H2', + Attribute.h3: 'H3', + }; + + final _valueAttribute = [ + Attribute.header, + Attribute.h1, + Attribute.h2, + Attribute.h3 + ]; + final _valueString = ['N', 'H1', 'H2', 'H3']; + + final theme = Theme.of(context); + final style = TextStyle( + fontWeight: FontWeight.w600, + fontSize: iconSize * 0.7, + ); + + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(4, (index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), + child: ConstrainedBox( + constraints: BoxConstraints.tightFor( + width: iconSize * 1.77, + height: iconSize * 1.77, + ), + child: RawMaterialButton( + hoverElevation: 0, + highlightElevation: 0, + elevation: 0, + visualDensity: VisualDensity.compact, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), + fillColor: _valueToText[value] == _valueString[index] + ? theme.toggleableActiveColor + : theme.canvasColor, + onPressed: () { + onSelected(_valueAttribute[index]); + }, + child: Text( + _valueString[index], + style: style.copyWith( + color: _valueToText[value] == _valueString[index] + ? theme.primaryIconTheme.color + : theme.iconTheme.color, + ), + ), + ), + ), + ); + }), + ); +} + +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; + + final OnImagePickCallback? onImagePickCallback; + + final ImagePickImpl? imagePickImpl; + + final ImageSource imageSource; + + @override + _ImageButtonState createState() => _ImageButtonState(); +} + +class _ImageButtonState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return QuillIconButton( + icon: Icon( + widget.icon, + size: widget.controller.iconSize, + color: theme.iconTheme.color, + ), + highlightElevation: 0, + hoverElevation: 0, + size: widget.controller.iconSize * 1.77, + fillColor: theme.canvasColor, + onPressed: _handleImageButtonTap, + ); + } + + Future _handleImageButtonTap() async { + final index = widget.controller.selection.baseOffset; + final length = widget.controller.selection.extentOffset - index; + + String? imageUrl; + if (widget.imagePickImpl != null) { + imageUrl = await widget.imagePickImpl!(widget.imageSource); + } else { + if (kIsWeb) { + imageUrl = await _pickImageWeb(); + } else if (Platform.isAndroid || Platform.isIOS) { + imageUrl = await _pickImage(widget.imageSource); + } else { + imageUrl = await _pickImageDesktop(); + } + } + + if (imageUrl != null) { + widget.controller + .replaceText(index, length, BlockEmbed.image(imageUrl), null); + } + } + + Future _pickImageWeb() async { + final result = await FilePicker.platform.pickFiles(); + if (result == null) { + return null; + } + + // Take first, because we don't allow picking multiple files. + final fileName = result.files.first.name!; + final file = File(fileName); + + return widget.onImagePickCallback!(file); + } + + Future _pickImage(ImageSource source) async { + final pickedFile = await ImagePicker().getImage(source: source); + if (pickedFile == null) { + return null; + } + + return widget.onImagePickCallback!(File(pickedFile.path)); + } + + Future _pickImageDesktop() async { + final filePath = await FilesystemPicker.open( + context: context, + rootDirectory: await getApplicationDocumentsDirectory(), + fsType: FilesystemType.file, + fileTileSelectMode: FileTileSelectMode.wholeTile, + ); + if (filePath == null || filePath.isEmpty) return null; + + final file = File(filePath); + return widget.onImagePickCallback!(file); + } +} + +/// Controls color styles. +/// +/// When pressed, this button displays overlay toolbar with +/// buttons for each color. +class ColorButton extends StatefulWidget { + const ColorButton({ + required this.icon, + required this.controller, + required this.background, + Key? key, + }) : super(key: key); + + final IconData icon; + final bool background; + final QuillController controller; + + @override + _ColorButtonState createState() => _ColorButtonState(); +} + +class _ColorButtonState extends State { + late bool _isToggledColor; + late bool _isToggledBackground; + late bool _isWhite; + late bool _isWhitebackground; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + void _didChangeEditingValue() { + setState(() { + _isToggledColor = + _getIsToggledColor(widget.controller.getSelectionStyle().attributes); + _isToggledBackground = _getIsToggledBackground( + widget.controller.getSelectionStyle().attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + }); + } + + @override + void initState() { + super.initState(); + _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); + _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + widget.controller.addListener(_didChangeEditingValue); + } + + bool _getIsToggledColor(Map attrs) { + return attrs.containsKey(Attribute.color.key); + } + + bool _getIsToggledBackground(Map attrs) { + return attrs.containsKey(Attribute.background.key); + } + + @override + void didUpdateWidget(covariant ColorButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); + _isToggledBackground = + _getIsToggledBackground(_selectionStyle.attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = _isToggledColor && !widget.background && !_isWhite + ? stringToColor(_selectionStyle.attributes['color']!.value) + : theme.iconTheme.color; + + final iconColorBackground = + _isToggledBackground && widget.background && !_isWhitebackground + ? stringToColor(_selectionStyle.attributes['background']!.value) + : theme.iconTheme.color; + + final fillColor = _isToggledColor && !widget.background && _isWhite + ? stringToColor('#ffffff') + : theme.canvasColor; + final fillColorBackground = + _isToggledBackground && widget.background && _isWhitebackground + ? stringToColor('#ffffff') + : theme.canvasColor; + + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.controller.iconSize * 1.77, + icon: Icon(widget.icon, + size: widget.controller.iconSize, + color: widget.background ? iconColorBackground : iconColor), + fillColor: widget.background ? fillColorBackground : fillColor, + onPressed: _showColorPicker, + ); + } + + void _changeColor(Color color) { + var hex = color.value.toRadixString(16); + if (hex.startsWith('ff')) { + hex = hex.substring(2); + } + hex = '#$hex'; + widget.controller.formatSelection( + widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex)); + Navigator.of(context).pop(); + } + + void _showColorPicker() { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Select Color'), + backgroundColor: Theme.of(context).canvasColor, + content: SingleChildScrollView( + child: MaterialPicker( + pickerColor: const Color(0x00000000), + onColorChanged: _changeColor, + ), + )), + ); + } +} + +class HistoryButton extends StatefulWidget { + const HistoryButton({ + required this.icon, + required this.controller, + required this.undo, + Key? key, + }) : super(key: key); + + final IconData icon; + final bool undo; + final QuillController controller; + + @override + _HistoryButtonState createState() => _HistoryButtonState(); +} + +class _HistoryButtonState extends State { + Color? _iconColor; + late ThemeData theme; + + @override + Widget build(BuildContext context) { + theme = Theme.of(context); + _setIconColor(); + + final fillColor = theme.canvasColor; + widget.controller.changes.listen((event) async { + _setIconColor(); + }); + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.controller.iconSize * 1.77, + icon: Icon(widget.icon, + size: widget.controller.iconSize, color: _iconColor), + fillColor: fillColor, + onPressed: _changeHistory, + ); + } + + void _setIconColor() { + if (!mounted) return; + + if (widget.undo) { + setState(() { + _iconColor = widget.controller.hasUndo + ? theme.iconTheme.color + : theme.disabledColor; + }); + } else { + setState(() { + _iconColor = widget.controller.hasRedo + ? theme.iconTheme.color + : theme.disabledColor; + }); + } + } + + void _changeHistory() { + if (widget.undo) { + if (widget.controller.hasUndo) { + widget.controller.undo(); + } + } else { + if (widget.controller.hasRedo) { + widget.controller.redo(); + } + } + + _setIconColor(); + } +} + +class IndentButton extends StatefulWidget { + const IndentButton({ + required this.icon, + required this.controller, + required this.isIncrease, + Key? key, + }) : super(key: key); + + final IconData icon; + final QuillController controller; + final bool isIncrease; + + @override + _IndentButtonState createState() => _IndentButtonState(); +} + +class _IndentButtonState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = theme.iconTheme.color; + final fillColor = theme.canvasColor; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.controller.iconSize * 1.77, + icon: + Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), + fillColor: fillColor, + onPressed: () { + final indent = widget.controller + .getSelectionStyle() + .attributes[Attribute.indent.key]; + if (indent == null) { + if (widget.isIncrease) { + widget.controller.formatSelection(Attribute.indentL1); + } + return; + } + if (indent.value == 1 && !widget.isIncrease) { + widget.controller + .formatSelection(Attribute.clone(Attribute.indentL1, null)); + return; + } + if (widget.isIncrease) { + widget.controller + .formatSelection(Attribute.getIndentLevel(indent.value + 1)); + return; + } + widget.controller + .formatSelection(Attribute.getIndentLevel(indent.value - 1)); + }, + ); + } +} + +class ClearFormatButton extends StatefulWidget { + const ClearFormatButton({ + required this.icon, + required this.controller, + Key? key, + }) : super(key: key); + + final IconData icon; + + final QuillController controller; + + @override + _ClearFormatButtonState createState() => _ClearFormatButtonState(); +} + +class _ClearFormatButtonState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = theme.iconTheme.color; + final fillColor = theme.canvasColor; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.controller.iconSize * 1.77, + icon: Icon(widget.icon, + size: widget.controller.iconSize, color: iconColor), + fillColor: fillColor, + onPressed: () { + for (final k + in widget.controller.getSelectionStyle().attributes.values) { + widget.controller.formatSelection(Attribute.clone(k, null)); + } + }); + } +} + +class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { + const QuillToolbar( + {required this.children, this.toolBarHeight = 36, Key? key}) + : super(key: key); + + factory QuillToolbar.basic({ + required QuillController controller, + double toolbarIconSize = 18.0, + bool showBoldButton = true, + bool showItalicButton = true, + bool showUnderLineButton = true, + bool showStrikeThrough = true, + bool showColorButton = true, + bool showBackgroundColorButton = true, + bool showClearFormat = true, + bool showHeaderStyle = true, + bool showListNumbers = true, + bool showListBullets = true, + bool showListCheck = true, + bool showCodeBlock = true, + bool showQuote = true, + bool showIndent = true, + bool showLink = true, + bool showHistory = true, + bool showHorizontalRule = false, + OnImagePickCallback? onImagePickCallback, + Key? key, + }) { + controller.iconSize = toolbarIconSize; + + return QuillToolbar( + key: key, + toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor, + children: [ + Visibility( + visible: showHistory, + child: HistoryButton( + icon: Icons.undo_outlined, + controller: controller, + undo: true, + ), + ), + Visibility( + visible: showHistory, + child: HistoryButton( + icon: Icons.redo_outlined, + controller: controller, + undo: false, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showBoldButton, + child: ToggleStyleButton( + attribute: Attribute.bold, + icon: Icons.format_bold, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showItalicButton, + child: ToggleStyleButton( + attribute: Attribute.italic, + icon: Icons.format_italic, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showUnderLineButton, + child: ToggleStyleButton( + attribute: Attribute.underline, + icon: Icons.format_underline, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showStrikeThrough, + child: ToggleStyleButton( + attribute: Attribute.strikeThrough, + icon: Icons.format_strikethrough, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showColorButton, + child: ColorButton( + icon: Icons.color_lens, + controller: controller, + background: false, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showBackgroundColorButton, + child: ColorButton( + icon: Icons.format_color_fill, + controller: controller, + background: true, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: showClearFormat, + child: ClearFormatButton( + icon: Icons.format_clear, + controller: controller, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: onImagePickCallback != null, + child: ImageButton( + icon: Icons.image, + controller: controller, + imageSource: ImageSource.gallery, + onImagePickCallback: onImagePickCallback, + ), + ), + const SizedBox(width: 0.6), + Visibility( + visible: onImagePickCallback != null, + child: ImageButton( + icon: Icons.photo_camera, + controller: controller, + imageSource: ImageSource.camera, + onImagePickCallback: onImagePickCallback, + ), + ), + Visibility( + visible: showHeaderStyle, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showHeaderStyle, + child: SelectHeaderStyleButton(controller: controller)), + VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400), + Visibility( + visible: showListNumbers, + child: ToggleStyleButton( + attribute: Attribute.ol, + controller: controller, + icon: Icons.format_list_numbered, + ), + ), + Visibility( + visible: showListBullets, + child: ToggleStyleButton( + attribute: Attribute.ul, + controller: controller, + icon: Icons.format_list_bulleted, + ), + ), + Visibility( + visible: showListCheck, + child: ToggleCheckListButton( + attribute: Attribute.unchecked, + controller: controller, + icon: Icons.check_box, + ), + ), + Visibility( + visible: showCodeBlock, + child: ToggleStyleButton( + attribute: Attribute.codeBlock, + controller: controller, + icon: Icons.code, + ), + ), + Visibility( + visible: !showListNumbers && + !showListBullets && + !showListCheck && + !showCodeBlock, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showQuote, + child: ToggleStyleButton( + attribute: Attribute.blockQuote, + controller: controller, + icon: Icons.format_quote, + ), + ), + Visibility( + visible: showIndent, + child: IndentButton( + icon: Icons.format_indent_increase, + controller: controller, + isIncrease: true, + ), + ), + Visibility( + visible: showIndent, + child: IndentButton( + icon: Icons.format_indent_decrease, + controller: controller, + isIncrease: false, + ), + ), + Visibility( + visible: showQuote, + child: VerticalDivider( + indent: 12, endIndent: 12, color: Colors.grey.shade400)), + Visibility( + visible: showLink, + child: LinkStyleButton(controller: controller)), + Visibility( + visible: showHorizontalRule, + child: InsertEmbedButton( + controller: controller, + icon: Icons.horizontal_rule, + ), + ), + ]); + } + + final List children; + final double toolBarHeight; + + @override + _QuillToolbarState createState() => _QuillToolbarState(); + + @override + Size get preferredSize => Size.fromHeight(toolBarHeight); +} + +class _QuillToolbarState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + constraints: BoxConstraints.tightFor(height: widget.preferredSize.height), + color: Theme.of(context).canvasColor, + child: CustomScrollView( + scrollDirection: Axis.horizontal, + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: widget.children, + ), + ), + ], + ), + ); + } +} + +class QuillIconButton extends StatelessWidget { + const QuillIconButton({ + required this.onPressed, + this.icon, + this.size = 40, + this.fillColor, + this.hoverElevation = 1, + this.highlightElevation = 1, + 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( + constraints: BoxConstraints.tightFor(width: size, height: size), + child: RawMaterialButton( + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), + fillColor: fillColor, + elevation: 0, + hoverElevation: hoverElevation, + highlightElevation: hoverElevation, + onPressed: onPressed, + child: icon, + ), + ); + } +} + +class QuillDropdownButton extends StatefulWidget { + const QuillDropdownButton({ + required this.child, + required this.initialValue, + required this.items, + required this.onSelected, + this.height = 40, + this.fillColor, + this.hoverElevation = 1, + this.highlightElevation = 1, + 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(); +} + +class _QuillDropdownButtonState extends State> { + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints.tightFor(height: widget.height), + child: RawMaterialButton( + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), + fillColor: widget.fillColor, + elevation: 0, + hoverElevation: widget.hoverElevation, + highlightElevation: widget.hoverElevation, + onPressed: _showMenu, + child: _buildContent(context), + ), + ); + } + + void _showMenu() { + final popupMenuTheme = PopupMenuTheme.of(context); + final button = context.findRenderObject() as RenderBox; + final overlay = + Overlay.of(context)!.context.findRenderObject() as RenderBox; + final position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(Offset.zero, ancestor: overlay), + button.localToGlobal(button.size.bottomLeft(Offset.zero), + ancestor: overlay), + ), + Offset.zero & overlay.size, + ); + showMenu( + context: context, + elevation: 4, + // widget.elevation ?? popupMenuTheme.elevation, + initialValue: widget.initialValue, + items: widget.items, + position: position, + shape: popupMenuTheme.shape, + // widget.shape ?? popupMenuTheme.shape, + color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color, + // captureInheritedThemes: widget.captureInheritedThemes, + ).then((newValue) { + if (!mounted) return null; + if (newValue == null) { + // if (widget.onCanceled != null) widget.onCanceled(); + return null; + } + widget.onSelected(newValue); + }); + } + + Widget _buildContent(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 110), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + widget.child, + Expanded(child: Container()), + const Icon(Icons.arrow_drop_down, size: 15) + ], + ), + ), + ); + } +} diff --git a/lib/utils/color.dart b/lib/utils/color.dart index 93b6e12b..f126cf52 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -1,125 +1,3 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; - -Color stringToColor(String? s) { - switch (s) { - case 'transparent': - return Colors.transparent; - case 'black': - return Colors.black; - case 'black12': - return Colors.black12; - case 'black26': - return Colors.black26; - case 'black38': - return Colors.black38; - case 'black45': - return Colors.black45; - case 'black54': - return Colors.black54; - case 'black87': - return Colors.black87; - case 'white': - return Colors.white; - case 'white10': - return Colors.white10; - case 'white12': - return Colors.white12; - case 'white24': - return Colors.white24; - case 'white30': - return Colors.white30; - case 'white38': - return Colors.white38; - case 'white54': - return Colors.white54; - case 'white60': - return Colors.white60; - case 'white70': - return Colors.white70; - case 'red': - return Colors.red; - case 'redAccent': - return Colors.redAccent; - case 'amber': - return Colors.amber; - case 'amberAccent': - return Colors.amberAccent; - case 'yellow': - return Colors.yellow; - case 'yellowAccent': - return Colors.yellowAccent; - case 'teal': - return Colors.teal; - case 'tealAccent': - return Colors.tealAccent; - case 'purple': - return Colors.purple; - case 'purpleAccent': - return Colors.purpleAccent; - case 'pink': - return Colors.pink; - case 'pinkAccent': - return Colors.pinkAccent; - case 'orange': - return Colors.orange; - case 'orangeAccent': - return Colors.orangeAccent; - case 'deepOrange': - return Colors.deepOrange; - case 'deepOrangeAccent': - return Colors.deepOrangeAccent; - case 'indigo': - return Colors.indigo; - case 'indigoAccent': - return Colors.indigoAccent; - case 'lime': - return Colors.lime; - case 'limeAccent': - return Colors.limeAccent; - case 'grey': - return Colors.grey; - case 'blueGrey': - return Colors.blueGrey; - case 'green': - return Colors.green; - case 'greenAccent': - return Colors.greenAccent; - case 'lightGreen': - return Colors.lightGreen; - case 'lightGreenAccent': - return Colors.lightGreenAccent; - case 'blue': - return Colors.blue; - case 'blueAccent': - return Colors.blueAccent; - case 'lightBlue': - return Colors.lightBlue; - case 'lightBlueAccent': - return Colors.lightBlueAccent; - case 'cyan': - return Colors.cyan; - case 'cyanAccent': - return Colors.cyanAccent; - case 'brown': - return Colors.brown; - } - - if (s!.startsWith('rgba')) { - s = s.substring(5); // trim left 'rgba(' - s = s.substring(0, s.length - 1); // trim right ')' - final arr = s.split(',').map((e) => e.trim()).toList(); - return Color.fromRGBO(int.parse(arr[0]), int.parse(arr[1]), - int.parse(arr[2]), double.parse(arr[3])); - } - - if (!s.startsWith('#')) { - throw 'Color code not supported'; - } - - var hex = s.replaceFirst('#', ''); - hex = hex.length == 6 ? 'ff$hex' : hex; - final val = int.parse(hex, radix: 16); - return Color(val); -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/utils/color.dart'; diff --git a/lib/utils/diff_delta.dart b/lib/utils/diff_delta.dart index 003bae47..08d30f51 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -1,102 +1,3 @@ -import 'dart:math' as math; - -import '../models/quill_delta.dart'; - -const Set WHITE_SPACE = { - 0x9, - 0xA, - 0xB, - 0xC, - 0xD, - 0x1C, - 0x1D, - 0x1E, - 0x1F, - 0x20, - 0xA0, - 0x1680, - 0x2000, - 0x2001, - 0x2002, - 0x2003, - 0x2004, - 0x2005, - 0x2006, - 0x2007, - 0x2008, - 0x2009, - 0x200A, - 0x202F, - 0x205F, - 0x3000 -}; - -// Diff between two texts - old text and new text -class Diff { - Diff(this.start, this.deleted, this.inserted); - - // Start index in old text at which changes begin. - final int start; - - /// The deleted text - final String deleted; - - // The inserted text - final String inserted; - - @override - String toString() { - return 'Diff[$start, "$deleted", "$inserted"]'; - } -} - -/* Get diff operation between old text and new text */ -Diff getDiff(String oldText, String newText, int cursorPosition) { - 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--) {} - var start = 0; - for (final startLimit = cursorPosition - math.max(0, delta); - start < startLimit && oldText[start] == newText[start]; - start++) {} - final deleted = (start >= end) ? '' : oldText.substring(start, end); - final inserted = newText.substring(start, end + delta); - return Diff(start, deleted, inserted); -} - -int getPositionDelta(Delta user, Delta actual) { - if (actual.isEmpty) { - return 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()); - final userOperation = userItr.next(length as int); - final actualOperation = actualItr.next(length); - if (userOperation.length != actualOperation.length) { - throw 'userOp ${userOperation.length} does not match actualOp ${actualOperation.length}'; - } - if (userOperation.key == actualOperation.key) { - continue; - } else if (userOperation.isInsert && actualOperation.isRetain) { - diff -= userOperation.length!; - } else if (userOperation.isDelete && actualOperation.isRetain) { - diff += userOperation.length!; - } else if (userOperation.isRetain && actualOperation.isInsert) { - String? operationTxt = ''; - if (actualOperation.data is String) { - operationTxt = actualOperation.data as String?; - } - if (operationTxt!.startsWith('\n')) { - continue; - } - diff += actualOperation.length!; - } - } - return diff; -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../../src/utils/diff_delta.dart'; diff --git a/lib/widgets/box.dart b/lib/widgets/box.dart index 75547923..d97c610a 100644 --- a/lib/widgets/box.dart +++ b/lib/widgets/box.dart @@ -1,39 +1,3 @@ -import 'package:flutter/rendering.dart'; - -import '../models/documents/nodes/container.dart'; - -abstract class RenderContentProxyBox implements RenderBox { - double getPreferredLineHeight(); - - Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype); - - TextPosition getPositionForOffset(Offset offset); - - double? getFullHeightForCaret(TextPosition position); - - TextRange getWordBoundary(TextPosition position); - - List getBoxesForSelection(TextSelection textSelection); -} - -abstract class RenderEditableBox extends RenderBox { - Container getContainer(); - - double preferredLineHeight(TextPosition position); - - Offset getOffsetForCaret(TextPosition position); - - TextPosition getPositionForOffset(Offset offset); - - TextPosition? getPositionAbove(TextPosition position); - - TextPosition? getPositionBelow(TextPosition position); - - TextRange getWordBoundary(TextPosition position); - - TextRange getLineBoundary(TextPosition position); - - TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection); - - TextSelectionPoint getExtentEndpointForSelection(TextSelection textSelection); -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/box.dart'; diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index 4820b3f9..e1177f78 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -1,228 +1,3 @@ -import 'dart:math' as math; - -import 'package:flutter/cupertino.dart'; -import 'package:tuple/tuple.dart'; - -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, - this.iconSize = 18, - this.toolbarHeightFactor = 2}); - - factory QuillController.basic() { - return QuillController( - document: Document(), - selection: const TextSelection.collapsed(offset: 0), - ); - } - - final Document document; - TextSelection selection; - double iconSize; - double toolbarHeightFactor; - - Style toggledStyle = Style(); - bool ignoreFocusOnTextChange = false; - - /// Controls whether this [QuillController] instance has already been disposed - /// of - /// - /// This is a safe approach to make sure that listeners don't crash when - /// adding, removing or listeners to this instance. - bool _isDisposed = false; - - // item1: Document state before [change]. - // - // item2: Change delta applied to the document. - // - // item3: The source of this change. - Stream> get changes => document.changes; - - TextEditingValue get plainTextEditingValue => TextEditingValue( - text: document.toPlainText(), - selection: selection, - ); - - Style getSelectionStyle() { - return document - .collectStyle(selection.start, selection.end - selection.start) - .mergeAll(toggledStyle); - } - - void undo() { - final tup = document.undo(); - if (tup.item1) { - _handleHistoryChange(tup.item2); - } - } - - void _handleHistoryChange(int? len) { - if (len != 0) { - // if (this.selection.extentOffset >= document.length) { - // // cursor exceeds the length of document, position it in the end - // updateSelection( - // TextSelection.collapsed(offset: document.length), ChangeSource.LOCAL); - updateSelection( - TextSelection.collapsed(offset: selection.baseOffset + len!), - ChangeSource.LOCAL); - } else { - // no need to move cursor - notifyListeners(); - } - } - - void redo() { - final tup = document.redo(); - if (tup.item1) { - _handleHistoryChange(tup.item2); - } - } - - bool get hasUndo => document.hasUndo; - - bool get hasRedo => document.hasRedo; - - void replaceText( - int index, int len, Object? data, TextSelection? textSelection, - {bool ignoreFocus = false, bool autoAppendNewlineAfterImage = true}) { - assert(data is String || data is Embeddable); - - Delta? delta; - if (len > 0 || data is! String || data.isNotEmpty) { - delta = document.replace(index, len, data, autoAppendNewlineAfterImage: autoAppendNewlineAfterImage); - var shouldRetainDelta = toggledStyle.isNotEmpty && - delta.isNotEmpty && - delta.length <= 2 && - delta.last.isInsert; - if (shouldRetainDelta && - toggledStyle.isNotEmpty && - delta.length == 2 && - delta.last.data == '\n') { - // if all attributes are inline, shouldRetainDelta should be false - final anyAttributeNotInline = - toggledStyle.values.any((attr) => !attr.isInline); - if (!anyAttributeNotInline) { - shouldRetainDelta = false; - } - } - if (shouldRetainDelta) { - final retainDelta = Delta() - ..retain(index) - ..retain(data is String ? data.length : 1, toggledStyle.toJson()); - document.compose(retainDelta, ChangeSource.LOCAL); - } - } - - toggledStyle = Style(); - if (textSelection != null) { - if (delta == null || delta.isEmpty) { - _updateSelection(textSelection, ChangeSource.LOCAL); - } else { - final user = Delta() - ..retain(index) - ..insert(data) - ..delete(len); - final positionDelta = getPositionDelta(user, delta); - _updateSelection( - textSelection.copyWith( - baseOffset: textSelection.baseOffset + positionDelta, - extentOffset: textSelection.extentOffset + positionDelta, - ), - ChangeSource.LOCAL, - ); - } - } - - if (ignoreFocus) { - ignoreFocusOnTextChange = true; - } - notifyListeners(); - ignoreFocusOnTextChange = false; - } - - void formatText(int index, int len, Attribute? attribute) { - if (len == 0 && - attribute!.isInline && - attribute.key != Attribute.link.key) { - toggledStyle = toggledStyle.put(attribute); - } - - final change = document.format(index, len, attribute); - final adjustedSelection = selection.copyWith( - baseOffset: change.transformPosition(selection.baseOffset), - extentOffset: change.transformPosition(selection.extentOffset)); - if (selection != adjustedSelection) { - _updateSelection(adjustedSelection, ChangeSource.LOCAL); - } - notifyListeners(); - } - - void formatSelection(Attribute? attribute) { - formatText(selection.start, selection.end - selection.start, attribute); - } - - void updateSelection(TextSelection textSelection, ChangeSource source) { - _updateSelection(textSelection, source); - notifyListeners(); - } - - void compose(Delta delta, TextSelection textSelection, ChangeSource source) { - if (delta.isNotEmpty) { - document.compose(delta, source); - } - - textSelection = selection.copyWith( - baseOffset: delta.transformPosition(selection.baseOffset, force: false), - extentOffset: - delta.transformPosition(selection.extentOffset, force: false)); - if (selection != textSelection) { - _updateSelection(textSelection, source); - } - - notifyListeners(); - } - - @override - void addListener(VoidCallback listener) { - // By using `_isDisposed`, make sure that `addListener` won't be called on a - // disposed `ChangeListener` - if (!_isDisposed) { - super.addListener(listener); - } - } - - @override - void removeListener(VoidCallback listener) { - // By using `_isDisposed`, make sure that `removeListener` won't be called - // on a disposed `ChangeListener` - if (!_isDisposed) { - super.removeListener(listener); - } - } - - @override - void dispose() { - if (!_isDisposed) { - document.close(); - } - - _isDisposed = true; - super.dispose(); - } - - void _updateSelection(TextSelection textSelection, ChangeSource source) { - selection = textSelection; - final end = document.length - 1; - selection = selection.copyWith( - baseOffset: math.min(selection.baseOffset, end), - extentOffset: math.min(selection.extentOffset, end)); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/controller.dart'; diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index 383906d0..3528ad16 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -1,231 +1,3 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; - -import 'box.dart'; - -const Duration _FADE_DURATION = Duration(milliseconds: 250); - -class CursorStyle { - const CursorStyle({ - required this.color, - required this.backgroundColor, - this.width = 1.0, - this.height, - this.radius, - this.offset, - this.opacityAnimates = false, - 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) || - other is CursorStyle && - runtimeType == other.runtimeType && - color == other.color && - backgroundColor == other.backgroundColor && - width == other.width && - height == other.height && - radius == other.radius && - offset == other.offset && - opacityAnimates == other.opacityAnimates && - paintAboveText == other.paintAboveText; - - @override - int get hashCode => - color.hashCode ^ - backgroundColor.hashCode ^ - width.hashCode ^ - height.hashCode ^ - radius.hashCode ^ - offset.hashCode ^ - opacityAnimates.hashCode ^ - paintAboveText.hashCode; -} - -class CursorCont extends ChangeNotifier { - CursorCont({ - required this.show, - required CursorStyle style, - required TickerProvider tickerProvider, - }) : _style = style, - _blink = ValueNotifier(false), - color = ValueNotifier(style.color) { - _blinkOpacityCont = - AnimationController(vsync: tickerProvider, duration: _FADE_DURATION); - _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; - - CursorStyle get style => _style; - - set style(CursorStyle value) { - if (_style == value) return; - _style = value; - notifyListeners(); - } - - @override - void dispose() { - _blinkOpacityCont.removeListener(_onColorTick); - stopCursorTimer(); - _blinkOpacityCont.dispose(); - assert(_cursorTimer == null); - super.dispose(); - } - - void _cursorTick(Timer timer) { - _targetCursorVisibility = !_targetCursorVisibility; - final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; - if (style.opacityAnimates) { - _blinkOpacityCont.animateTo(targetOpacity, curve: Curves.easeOut); - } else { - _blinkOpacityCont.value = targetOpacity; - } - } - - void _cursorWaitForStart(Timer timer) { - _cursorTimer?.cancel(); - _cursorTimer = - Timer.periodic(const Duration(milliseconds: 500), _cursorTick); - } - - void startCursorTimer() { - _targetCursorVisibility = true; - _blinkOpacityCont.value = 1.0; - - if (style.opacityAnimates) { - _cursorTimer = Timer.periodic( - const Duration(milliseconds: 150), _cursorWaitForStart); - } else { - _cursorTimer = - Timer.periodic(const Duration(milliseconds: 500), _cursorTick); - } - } - - void stopCursorTimer({bool resetCharTicks = true}) { - _cursorTimer?.cancel(); - _cursorTimer = null; - _targetCursorVisibility = false; - _blinkOpacityCont.value = 0.0; - - if (style.opacityAnimates) { - _blinkOpacityCont - ..stop() - ..value = 0.0; - } - } - - void startOrStopCursorTimerIfNeeded(bool hasFocus, TextSelection selection) { - if (show.value && - _cursorTimer == null && - hasFocus && - selection.isCollapsed) { - startCursorTimer(); - } else if (_cursorTimer != null && (!hasFocus || !selection.isCollapsed)) { - stopCursorTimer(); - } - } - - void _onColorTick() { - color.value = _style.color.withOpacity(_blinkOpacityCont.value); - _blink.value = show.value && _blinkOpacityCont.value > 0; - } -} - -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; - - void paint(Canvas canvas, Offset offset, TextPosition position) { - assert(prototype != null); - - final caretOffset = - editable!.getOffsetForCaret(position, prototype) + offset; - 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)); - } - - final caretHeight = editable!.getFullHeightForCaret(position); - if (caretHeight != null) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - caretRect = Rect.fromLTWH( - caretRect.left, - caretRect.top - 2.0, - caretRect.width, - caretHeight, - ); - break; - case TargetPlatform.iOS: - case TargetPlatform.macOS: - caretRect = Rect.fromLTWH( - caretRect.left, - caretRect.top + (caretHeight - caretRect.height) / 2, - caretRect.width, - caretRect.height, - ); - break; - default: - throw UnimplementedError(); - } - } - - final caretPosition = editable!.localToGlobal(caretRect.topLeft); - final pixelMultiple = 1.0 / devicePixelRatio; - caretRect = caretRect.shift(Offset( - caretPosition.dx.isFinite - ? (caretPosition.dx / pixelMultiple).round() * pixelMultiple - - caretPosition.dx - : caretPosition.dx, - caretPosition.dy.isFinite - ? (caretPosition.dy / pixelMultiple).round() * pixelMultiple - - caretPosition.dy - : caretPosition.dy)); - - final paint = Paint()..color = color; - if (style.radius == null) { - canvas.drawRect(caretRect, paint); - return; - } - - final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!); - canvas.drawRRect(caretRRect, paint); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/cursor.dart'; diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 1cebe135..3fffda6f 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -1,223 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:tuple/tuple.dart'; - -class QuillStyles extends InheritedWidget { - 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) { - final widget = context.dependOnInheritedWidgetOfExactType(); - if (widget == null && nullOk) { - return null; - } - assert(widget != null); - return widget!.data; - } -} - -class DefaultTextBlockStyle { - DefaultTextBlockStyle( - this.style, - this.verticalSpacing, - this.lineSpacing, - this.decoration, - ); - - final TextStyle style; - - final Tuple2 verticalSpacing; - - final Tuple2 lineSpacing; - - final BoxDecoration? 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; - final DefaultTextBlockStyle? paragraph; - final TextStyle? bold; - final TextStyle? italic; - final TextStyle? underline; - final TextStyle? strikeThrough; - final TextStyle? sizeSmall; // 'small' - final TextStyle? sizeLarge; // 'large' - final TextStyle? sizeHuge; // 'huge' - final TextStyle? link; - final Color? color; - final DefaultTextBlockStyle? placeHolder; - final DefaultTextBlockStyle? lists; - final DefaultTextBlockStyle? quote; - final DefaultTextBlockStyle? code; - final DefaultTextBlockStyle? indent; - final DefaultTextBlockStyle? align; - final DefaultTextBlockStyle? leading; - - static DefaultStyles getInstance(BuildContext context) { - final themeData = Theme.of(context); - final defaultTextStyle = DefaultTextStyle.of(context); - final baseStyle = defaultTextStyle.style.copyWith( - fontSize: 16, - height: 1.3, - ); - const baseSpacing = Tuple2(6, 0); - String fontFamily; - switch (themeData.platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - fontFamily = 'Menlo'; - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.windows: - case TargetPlatform.linux: - fontFamily = 'Roboto Mono'; - break; - default: - throw UnimplementedError(); - } - - return DefaultStyles( - h1: DefaultTextBlockStyle( - defaultTextStyle.style.copyWith( - fontSize: 34, - color: defaultTextStyle.style.color!.withOpacity(0.70), - height: 1.15, - fontWeight: FontWeight.w300, - ), - const Tuple2(16, 0), - const Tuple2(0, 0), - null), - h2: DefaultTextBlockStyle( - defaultTextStyle.style.copyWith( - fontSize: 24, - color: defaultTextStyle.style.color!.withOpacity(0.70), - height: 1.15, - fontWeight: FontWeight.normal, - ), - const Tuple2(8, 0), - const Tuple2(0, 0), - null), - h3: DefaultTextBlockStyle( - defaultTextStyle.style.copyWith( - fontSize: 20, - color: defaultTextStyle.style.color!.withOpacity(0.70), - height: 1.25, - fontWeight: FontWeight.w500, - ), - const Tuple2(8, 0), - const Tuple2(0, 0), - null), - paragraph: DefaultTextBlockStyle( - 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), - strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough), - link: TextStyle( - color: themeData.accentColor, - decoration: TextDecoration.underline, - ), - placeHolder: DefaultTextBlockStyle( - defaultTextStyle.style.copyWith( - fontSize: 20, - height: 1.5, - color: Colors.grey.withOpacity(0.6), - ), - const Tuple2(0, 0), - const Tuple2(0, 0), - null), - lists: DefaultTextBlockStyle( - baseStyle, baseSpacing, const Tuple2(0, 6), null), - quote: DefaultTextBlockStyle( - TextStyle(color: baseStyle.color!.withOpacity(0.6)), - baseSpacing, - const Tuple2(6, 2), - BoxDecoration( - border: Border( - left: BorderSide(width: 4, color: Colors.grey.shade300), - ), - )), - code: DefaultTextBlockStyle( - TextStyle( - color: Colors.blue.shade900.withOpacity(0.9), - fontFamily: fontFamily, - fontSize: 13, - height: 1.15, - ), - baseSpacing, - const Tuple2(0, 0), - BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(2), - )), - indent: DefaultTextBlockStyle( - baseStyle, baseSpacing, const Tuple2(0, 6), null), - align: DefaultTextBlockStyle( - baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null), - leading: DefaultTextBlockStyle( - 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) { - return DefaultStyles( - h1: other.h1 ?? h1, - h2: other.h2 ?? h2, - h3: other.h3 ?? h3, - paragraph: other.paragraph ?? paragraph, - bold: other.bold ?? bold, - italic: other.italic ?? italic, - underline: other.underline ?? underline, - strikeThrough: other.strikeThrough ?? strikeThrough, - link: other.link ?? link, - color: other.color ?? color, - placeHolder: other.placeHolder ?? placeHolder, - lists: other.lists ?? lists, - quote: other.quote ?? quote, - code: other.code ?? code, - indent: other.indent ?? indent, - align: other.align ?? align, - leading: other.leading ?? leading, - sizeSmall: other.sizeSmall ?? sizeSmall, - sizeLarge: other.sizeLarge ?? sizeLarge, - sizeHuge: other.sizeHuge ?? sizeHuge); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/default_styles.dart'; diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index 4b4bdea7..c1db553e 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -1,148 +1,3 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -import '../models/documents/nodes/leaf.dart'; -import 'editor.dart'; -import 'text_selection.dart'; - -typedef EmbedBuilder = Widget Function(BuildContext context, Embed node); - -abstract class EditorTextSelectionGestureDetectorBuilderDelegate { - GlobalKey getEditableTextKey(); - - bool getForcePressEnabled(); - - bool getSelectionEnabled(); -} - -class EditorTextSelectionGestureDetectorBuilder { - EditorTextSelectionGestureDetectorBuilder(this.delegate); - - final EditorTextSelectionGestureDetectorBuilderDelegate delegate; - bool shouldShowSelectionToolbar = true; - - EditorState? getEditor() { - return delegate.getEditableTextKey().currentState; - } - - RenderEditor? getRenderEditor() { - return getEditor()!.getRenderEditor(); - } - - void onTapDown(TapDownDetails details) { - getRenderEditor()!.handleTapDown(details); - - final kind = details.kind; - shouldShowSelectionToolbar = kind == null || - kind == PointerDeviceKind.touch || - kind == PointerDeviceKind.stylus; - } - - void onForcePressStart(ForcePressDetails details) { - assert(delegate.getForcePressEnabled()); - shouldShowSelectionToolbar = true; - if (delegate.getSelectionEnabled()) { - getRenderEditor()!.selectWordsInRange( - details.globalPosition, - null, - SelectionChangedCause.forcePress, - ); - } - } - - void onForcePressEnd(ForcePressDetails details) { - assert(delegate.getForcePressEnabled()); - getRenderEditor()!.selectWordsInRange( - details.globalPosition, - null, - SelectionChangedCause.forcePress, - ); - if (shouldShowSelectionToolbar) { - getEditor()!.showToolbar(); - } - } - - void onSingleTapUp(TapUpDetails details) { - if (delegate.getSelectionEnabled()) { - getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); - } - } - - void onSingleTapCancel() {} - - void onSingleLongTapStart(LongPressStartDetails details) { - if (delegate.getSelectionEnabled()) { - getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.longPress, - ); - } - } - - void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (delegate.getSelectionEnabled()) { - getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.longPress, - ); - } - } - - void onSingleLongTapEnd(LongPressEndDetails details) { - if (shouldShowSelectionToolbar) { - getEditor()!.showToolbar(); - } - } - - void onDoubleTapDown(TapDownDetails details) { - if (delegate.getSelectionEnabled()) { - getRenderEditor()!.selectWord(SelectionChangedCause.tap); - if (shouldShowSelectionToolbar) { - getEditor()!.showToolbar(); - } - } - } - - void onDragSelectionStart(DragStartDetails details) { - getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.drag, - ); - } - - void onDragSelectionUpdate( - DragStartDetails startDetails, DragUpdateDetails updateDetails) { - getRenderEditor()!.selectPositionAt( - startDetails.globalPosition, - updateDetails.globalPosition, - SelectionChangedCause.drag, - ); - } - - void onDragSelectionEnd(DragEndDetails details) {} - - Widget build(HitTestBehavior behavior, Widget child) { - return EditorTextSelectionGestureDetector( - onTapDown: onTapDown, - onForcePressStart: - delegate.getForcePressEnabled() ? onForcePressStart : null, - onForcePressEnd: delegate.getForcePressEnabled() ? onForcePressEnd : null, - onSingleTapUp: onSingleTapUp, - onSingleTapCancel: onSingleTapCancel, - onSingleLongTapStart: onSingleLongTapStart, - onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, - onSingleLongTapEnd: onSingleLongTapEnd, - onDoubleTapDown: onDoubleTapDown, - onDragSelectionStart: onDragSelectionStart, - onDragSelectionUpdate: onDragSelectionUpdate, - onDragSelectionEnd: onDragSelectionEnd, - behavior: behavior, - child: child, - ); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/delegate.dart'; diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 662a018c..c0d754f9 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1,1145 +1,3 @@ -import 'dart:convert'; -import 'dart:io' as io; -import 'dart:math' as math; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../models/documents/attribute.dart'; -import '../models/documents/document.dart'; -import '../models/documents/nodes/container.dart' as container_node; -import '../models/documents/nodes/embed.dart'; -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 - 'tel:', // telephone - 'sms:', // SMS - 'callto:', - 'wtai:', - 'market:', - 'geopoint:', - 'ymsgr:', - 'msnim:', - 'gtalk:', // Google Talk - 'skype:', - 'sip:', // Lync - 'whatsapp:', - 'http' -]; - -abstract class EditorState extends State { - TextEditingValue getTextEditingValue(); - - void setTextEditingValue(TextEditingValue value); - - RenderEditor? getRenderEditor(); - - EditorTextSelectionOverlay? getSelectionOverlay(); - - bool showToolbar(); - - void hideToolbar(); - - void requestKeyboard(); -} - -abstract class RenderAbstractEditor { - TextSelection selectWordAtPosition(TextPosition position); - - TextSelection selectLineAtPosition(TextPosition position); - - double preferredLineHeight(TextPosition position); - - TextPosition getPositionForOffset(Offset offset); - - List getEndpointsForSelection( - TextSelection textSelection); - - void handleTapDown(TapDownDetails details); - - void selectWordsInRange( - Offset from, - Offset to, - SelectionChangedCause cause, - ); - - void selectWordEdge(SelectionChangedCause cause); - - void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause); - - void selectWord(SelectionChangedCause cause); - - void selectPosition(SelectionChangedCause cause); -} - -String _standardizeImageUrl(String url) { - if (url.contains('base64')) { - return url.split(',')[1]; - } - return url; -} - -Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { - assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); - switch (node.value.type) { - case 'image': - final imageUrl = _standardizeImageUrl(node.value.data); - return imageUrl.startsWith('http') - ? Image.network(imageUrl) - : isBase64(imageUrl) - ? Image.memory(base64.decode(imageUrl)) - : Image.file(io.File(imageUrl)); - default: - throw UnimplementedError( - 'Embeddable type "${node.value.type}" is not supported by default embed ' - 'builder of QuillEditor. You must pass your own builder function to ' - 'embedBuilder property of QuillEditor or QuillField widgets.'); - } -} - -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.scrollBottomInset = 0, - 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); - } - - final QuillController controller; - final FocusNode focusNode; - final ScrollController scrollController; - final bool scrollable; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - final bool autoFocus; - final bool? showCursor; - final bool readOnly; - final String? placeholder; - final bool enableInteractiveSelection; - final double? minHeight; - final double? maxHeight; - final DefaultStyles? customStyles; - final bool expands; - final TextCapitalization textCapitalization; - final Brightness keyboardAppearance; - final ScrollPhysics? scrollPhysics; - final ValueChanged? onLaunchUrl; - // Returns whether gesture is handled - final bool Function( - TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; - - // Returns whether gesture is handled - final bool Function( - TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; - - // Returns whether gesture is handled - final bool Function( - LongPressStartDetails details, TextPosition Function(Offset offset))? - onSingleLongTapStart; - - // Returns whether gesture is handled - final bool Function(LongPressMoveUpdateDetails details, - TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; - // Returns whether gesture is handled - final bool Function( - LongPressEndDetails details, TextPosition Function(Offset offset))? - onSingleLongTapEnd; - - final EmbedBuilder embedBuilder; - - @override - _QuillEditorState createState() => _QuillEditorState(); -} - -class _QuillEditorState extends State - implements EditorTextSelectionGestureDetectorBuilderDelegate { - final GlobalKey _editorKey = GlobalKey(); - late EditorTextSelectionGestureDetectorBuilder - _selectionGestureDetectorBuilder; - - @override - void initState() { - super.initState(); - _selectionGestureDetectorBuilder = - _QuillEditorSelectionGestureDetectorBuilder(this); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final selectionTheme = TextSelectionTheme.of(context); - - TextSelectionControls textSelectionControls; - bool paintCursorAboveText; - bool cursorOpacityAnimates; - Offset? cursorOffset; - Color? cursorColor; - Color selectionColor; - Radius? cursorRadius; - - switch (theme.platform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - textSelectionControls = materialTextSelectionControls; - paintCursorAboveText = false; - cursorOpacityAnimates = false; - cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; - selectionColor = selectionTheme.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); - break; - case TargetPlatform.iOS: - case TargetPlatform.macOS: - final cupertinoTheme = CupertinoTheme.of(context); - textSelectionControls = cupertinoTextSelectionControls; - paintCursorAboveText = true; - cursorOpacityAnimates = true; - cursorColor ??= - selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; - selectionColor = selectionTheme.selectionColor ?? - cupertinoTheme.primaryColor.withOpacity(0.40); - cursorRadius ??= const Radius.circular(2); - cursorOffset = Offset( - iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); - break; - default: - throw UnimplementedError(); - } - - return _selectionGestureDetectorBuilder.build( - HitTestBehavior.translucent, - RawEditor( - _editorKey, - widget.controller, - widget.focusNode, - widget.scrollController, - widget.scrollable, - widget.scrollBottomInset, - widget.padding, - widget.readOnly, - widget.placeholder, - widget.onLaunchUrl, - ToolbarOptions( - copy: widget.enableInteractiveSelection, - cut: widget.enableInteractiveSelection, - paste: widget.enableInteractiveSelection, - selectAll: widget.enableInteractiveSelection, - ), - theme.platform == TargetPlatform.iOS || - theme.platform == TargetPlatform.android, - widget.showCursor, - CursorStyle( - color: cursorColor, - backgroundColor: Colors.grey, - width: 2, - radius: cursorRadius, - offset: cursorOffset, - paintAboveText: paintCursorAboveText, - opacityAnimates: cursorOpacityAnimates, - ), - widget.textCapitalization, - widget.maxHeight, - widget.minHeight, - widget.customStyles, - widget.expands, - widget.autoFocus, - selectionColor, - textSelectionControls, - widget.keyboardAppearance, - widget.enableInteractiveSelection, - widget.scrollPhysics, - widget.embedBuilder), - ); - } - - @override - GlobalKey getEditableTextKey() { - return _editorKey; - } - - @override - bool getForcePressEnabled() { - return false; - } - - @override - bool getSelectionEnabled() { - return widget.enableInteractiveSelection; - } - - void _requestKeyboard() { - _editorKey.currentState!.requestKeyboard(); - } -} - -class _QuillEditorSelectionGestureDetectorBuilder - extends EditorTextSelectionGestureDetectorBuilder { - _QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state); - - final _QuillEditorState _state; - - @override - void onForcePressStart(ForcePressDetails details) { - super.onForcePressStart(details); - if (delegate.getSelectionEnabled() && shouldShowSelectionToolbar) { - getEditor()!.showToolbar(); - } - } - - @override - void onForcePressEnd(ForcePressDetails details) {} - - @override - void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - if (_state.widget.onSingleLongTapMoveUpdate != null) { - final renderEditor = getRenderEditor(); - if (renderEditor != null) { - if (_state.widget.onSingleLongTapMoveUpdate!( - details, renderEditor.getPositionForOffset)) { - return; - } - } - } - if (!delegate.getSelectionEnabled()) { - return; - } - switch (Theme.of(_state.context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.longPress, - ); - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - getRenderEditor()!.selectWordsInRange( - details.globalPosition - details.offsetFromOrigin, - details.globalPosition, - SelectionChangedCause.longPress, - ); - break; - default: - throw 'Invalid platform'; - } - } - - bool _onTapping(TapUpDetails details) { - if (_state.widget.controller.document.isEmpty()) { - return false; - } - final pos = getRenderEditor()!.getPositionForOffset(details.globalPosition); - final result = - getEditor()!.widget.controller.document.queryChild(pos.offset); - if (result.node == null) { - return false; - } - final line = result.node as Line; - final segmentResult = line.queryChild(result.offset, false); - if (segmentResult.node == null) { - if (line.length == 1) { - getEditor()!.widget.controller.updateSelection( - TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL); - return true; - } - return false; - } - final segment = segmentResult.node as leaf.Leaf; - if (segment.style.containsKey(Attribute.link.key)) { - var launchUrl = getEditor()!.widget.onLaunchUrl; - launchUrl ??= _launchUrl; - String? link = segment.style.attributes[Attribute.link.key]!.value; - if (getEditor()!.widget.readOnly && link != null) { - link = link.trim(); - if (!linkPrefixes - .any((linkPrefix) => link!.toLowerCase().startsWith(linkPrefix))) { - link = 'https://$link'; - } - launchUrl(link); - } - return false; - } - if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) { - final blockEmbed = segment.value as BlockEmbed; - if (blockEmbed.type == 'image') { - final imageUrl = _standardizeImageUrl(blockEmbed.data); - Navigator.push( - getEditor()!.context, - MaterialPageRoute( - builder: (context) => ImageTapWrapper( - imageProvider: imageUrl.startsWith('http') - ? NetworkImage(imageUrl) - : isBase64(imageUrl) - ? Image.memory(base64.decode(imageUrl)) - as ImageProvider? - : FileImage(io.File(imageUrl)), - ), - ), - ); - } - } - - return false; - } - - Future _launchUrl(String url) async { - await launch(url); - } - - @override - void onTapDown(TapDownDetails details) { - if (_state.widget.onTapDown != null) { - final renderEditor = getRenderEditor(); - if (renderEditor != null) { - if (_state.widget.onTapDown!( - details, renderEditor.getPositionForOffset)) { - return; - } - } - } - super.onTapDown(details); - } - - @override - void onSingleTapUp(TapUpDetails details) { - if (_state.widget.onTapUp != null) { - final renderEditor = getRenderEditor(); - if (renderEditor != null) { - if (_state.widget.onTapUp!( - details, renderEditor.getPositionForOffset)) { - return; - } - } - } - - getEditor()!.hideToolbar(); - - final positionSelected = _onTapping(details); - - if (delegate.getSelectionEnabled() && !positionSelected) { - switch (Theme.of(_state.context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - switch (details.kind) { - case PointerDeviceKind.mouse: - case PointerDeviceKind.stylus: - case PointerDeviceKind.invertedStylus: - getRenderEditor()!.selectPosition(SelectionChangedCause.tap); - break; - case PointerDeviceKind.touch: - case PointerDeviceKind.unknown: - getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); - break; - } - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - getRenderEditor()!.selectPosition(SelectionChangedCause.tap); - break; - } - } - _state._requestKeyboard(); - } - - @override - void onSingleLongTapStart(LongPressStartDetails details) { - if (_state.widget.onSingleLongTapStart != null) { - final renderEditor = getRenderEditor(); - if (renderEditor != null) { - if (_state.widget.onSingleLongTapStart!( - details, renderEditor.getPositionForOffset)) { - return; - } - } - } - - if (delegate.getSelectionEnabled()) { - switch (Theme.of(_state.context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - getRenderEditor()!.selectPositionAt( - details.globalPosition, - null, - SelectionChangedCause.longPress, - ); - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - getRenderEditor()!.selectWord(SelectionChangedCause.longPress); - Feedback.forLongPress(_state.context); - break; - default: - throw 'Invalid platform'; - } - } - } - - @override - void onSingleLongTapEnd(LongPressEndDetails details) { - if (_state.widget.onSingleLongTapEnd != null) { - final renderEditor = getRenderEditor(); - if (renderEditor != null) { - if (_state.widget.onSingleLongTapEnd!( - details, renderEditor.getPositionForOffset)) { - return; - } - } - } - super.onSingleLongTapEnd(details); - } -} - -typedef TextSelectionChangedHandler = void Function( - TextSelection selection, SelectionChangedCause cause); - -class RenderEditor extends RenderEditableContainerBox - implements RenderAbstractEditor { - RenderEditor( - List? children, - TextDirection textDirection, - double scrollBottomInset, - EdgeInsetsGeometry padding, - this.document, - this.selection, - this._hasFocus, - this.onSelectionChanged, - this._startHandleLayerLink, - this._endHandleLayerLink, - EdgeInsets floatingCursorAddedMargin, - ) : super( - children, - document.root, - textDirection, - scrollBottomInset, - padding, - ); - - Document document; - TextSelection selection; - bool _hasFocus = false; - LayerLink _startHandleLayerLink; - LayerLink _endHandleLayerLink; - TextSelectionChangedHandler onSelectionChanged; - final ValueNotifier _selectionStartInViewport = - ValueNotifier(true); - - ValueListenable get selectionStartInViewport => - _selectionStartInViewport; - - ValueListenable get selectionEndInViewport => _selectionEndInViewport; - final ValueNotifier _selectionEndInViewport = ValueNotifier(true); - - void setDocument(Document doc) { - if (document == doc) { - return; - } - document = doc; - markNeedsLayout(); - } - - void setHasFocus(bool h) { - if (_hasFocus == h) { - return; - } - _hasFocus = h; - markNeedsSemanticsUpdate(); - } - - void setSelection(TextSelection t) { - if (selection == t) { - return; - } - selection = t; - markNeedsPaint(); - } - - void setStartHandleLayerLink(LayerLink value) { - if (_startHandleLayerLink == value) { - return; - } - _startHandleLayerLink = value; - markNeedsPaint(); - } - - void setEndHandleLayerLink(LayerLink value) { - if (_endHandleLayerLink == value) { - return; - } - _endHandleLayerLink = value; - markNeedsPaint(); - } - - void setScrollBottomInset(double value) { - if (scrollBottomInset == value) { - return; - } - scrollBottomInset = value; - markNeedsPaint(); - } - - @override - List getEndpointsForSelection( - TextSelection textSelection) { - if (textSelection.isCollapsed) { - 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, child.preferredLineHeight(localPosition)) + - localOffset + - parentData.offset, - null) - ]; - } - - final baseNode = _container.queryChild(textSelection.start, false).node; - - var baseChild = firstChild; - while (baseChild != null) { - if (baseChild.getContainer() == baseNode) { - break; - } - baseChild = childAfter(baseChild); - } - assert(baseChild != null); - - final baseParentData = baseChild!.parentData as BoxParentData; - final baseSelection = - localSelection(baseChild.getContainer(), textSelection, true); - var basePoint = baseChild.getBaseEndpointForSelection(baseSelection); - basePoint = TextSelectionPoint( - basePoint.point + baseParentData.offset, basePoint.direction); - - final extentNode = _container.queryChild(textSelection.end, false).node; - RenderEditableBox? extentChild = baseChild; - while (extentChild != null) { - if (extentChild.getContainer() == extentNode) { - break; - } - extentChild = childAfter(extentChild); - } - assert(extentChild != null); - - final extentParentData = extentChild!.parentData as BoxParentData; - final extentSelection = - localSelection(extentChild.getContainer(), textSelection, true); - var extentPoint = - extentChild.getExtentEndpointForSelection(extentSelection); - extentPoint = TextSelectionPoint( - extentPoint.point + extentParentData.offset, extentPoint.direction); - - return [basePoint, extentPoint]; - } - - Offset? _lastTapDownPosition; - - @override - void handleTapDown(TapDownDetails details) { - _lastTapDownPosition = details.globalPosition; - } - - @override - void selectWordsInRange( - Offset from, - Offset? to, - SelectionChangedCause cause, - ) { - final firstPosition = getPositionForOffset(from); - final firstWord = selectWordAtPosition(firstPosition); - final lastWord = - to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to)); - - _handleSelectionChange( - TextSelection( - baseOffset: firstWord.base.offset, - extentOffset: lastWord.extent.offset, - affinity: firstWord.affinity, - ), - cause, - ); - } - - void _handleSelectionChange( - TextSelection nextSelection, - SelectionChangedCause cause, - ) { - final focusingEmpty = nextSelection.baseOffset == 0 && - nextSelection.extentOffset == 0 && - !_hasFocus; - if (nextSelection == selection && - cause != SelectionChangedCause.keyboard && - !focusingEmpty) { - return; - } - onSelectionChanged(nextSelection, cause); - } - - @override - void selectWordEdge(SelectionChangedCause cause) { - assert(_lastTapDownPosition != null); - final position = getPositionForOffset(_lastTapDownPosition!); - final child = childAtPosition(position); - final nodeOffset = child.getContainer().offset; - final localPosition = TextPosition( - offset: position.offset - nodeOffset, - affinity: position.affinity, - ); - final localWord = child.getWordBoundary(localPosition); - final word = TextRange( - start: localWord.start + nodeOffset, - end: localWord.end + nodeOffset, - ); - if (position.offset - word.start <= 1) { - _handleSelectionChange( - TextSelection.collapsed(offset: word.start), - cause, - ); - } else { - _handleSelectionChange( - TextSelection.collapsed( - offset: word.end, affinity: TextAffinity.upstream), - cause, - ); - } - } - - @override - void selectPositionAt( - Offset from, - Offset? to, - SelectionChangedCause cause, - ) { - final fromPosition = getPositionForOffset(from); - final toPosition = to == null ? null : getPositionForOffset(to); - - 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); - } - - final newSelection = TextSelection( - baseOffset: baseOffset, - extentOffset: extentOffset, - affinity: fromPosition.affinity, - ); - _handleSelectionChange(newSelection, cause); - } - - @override - void selectWord(SelectionChangedCause cause) { - selectWordsInRange(_lastTapDownPosition!, null, cause); - } - - @override - void selectPosition(SelectionChangedCause cause) { - selectPositionAt(_lastTapDownPosition!, null, cause); - } - - @override - TextSelection selectWordAtPosition(TextPosition position) { - final child = childAtPosition(position); - final nodeOffset = child.getContainer().offset; - final localPosition = TextPosition( - offset: position.offset - nodeOffset, affinity: position.affinity); - final localWord = child.getWordBoundary(localPosition); - final word = TextRange( - start: localWord.start + nodeOffset, - end: localWord.end + nodeOffset, - ); - if (position.offset >= word.end) { - return TextSelection.fromPosition(position); - } - return TextSelection(baseOffset: word.start, extentOffset: word.end); - } - - @override - TextSelection selectLineAtPosition(TextPosition position) { - final child = childAtPosition(position); - final nodeOffset = child.getContainer().offset; - final localPosition = TextPosition( - offset: position.offset - nodeOffset, affinity: position.affinity); - final localLineRange = child.getLineBoundary(localPosition); - final line = TextRange( - start: localLineRange.start + nodeOffset, - end: localLineRange.end + nodeOffset, - ); - - if (position.offset >= line.end) { - return TextSelection.fromPosition(position); - } - return TextSelection(baseOffset: line.start, extentOffset: line.end); - } - - @override - void paint(PaintingContext context, Offset offset) { - defaultPaint(context, offset); - _paintHandleLayers(context, getEndpointsForSelection(selection)); - } - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - return defaultHitTestChildren(result, position: position); - } - - void _paintHandleLayers( - PaintingContext context, List endpoints) { - var startPoint = endpoints[0].point; - startPoint = Offset( - startPoint.dx.clamp(0.0, size.width), - startPoint.dy.clamp(0.0, size.height), - ); - context.pushLayer( - LeaderLayer(link: _startHandleLayerLink, offset: startPoint), - super.paint, - Offset.zero, - ); - if (endpoints.length == 2) { - var endPoint = endpoints[1].point; - endPoint = Offset( - endPoint.dx.clamp(0.0, size.width), - endPoint.dy.clamp(0.0, size.height), - ); - context.pushLayer( - LeaderLayer(link: _endHandleLayerLink, offset: endPoint), - super.paint, - Offset.zero, - ); - } - } - - @override - double preferredLineHeight(TextPosition position) { - final child = childAtPosition(position); - return child.preferredLineHeight( - TextPosition(offset: position.offset - child.getContainer().offset)); - } - - @override - TextPosition getPositionForOffset(Offset offset) { - final local = globalToLocal(offset); - final child = childAtOffset(local)!; - - final parentData = child.parentData as BoxParentData; - final localOffset = local - parentData.offset; - final localPosition = child.getPositionForOffset(localOffset); - return TextPosition( - offset: localPosition.offset + child.getContainer().offset, - affinity: localPosition.affinity, - ); - } - - /// Returns the y-offset of the editor at which [selection] is visible. - /// - /// The offset is the distance from the top of the editor and is the minimum - /// from the current scroll position until [selection] becomes visible. - /// Returns null if [selection] is already visible. - double? getOffsetToRevealCursor( - double viewportHeight, double scrollOffset, double offsetInViewport) { - final endpoints = getEndpointsForSelection(selection); - final endpoint = endpoints.first; - final child = childAtPosition(selection.extent); - const kMargin = 8.0; - - final caretTop = endpoint.point.dy - - child.preferredLineHeight(TextPosition( - offset: selection.extentOffset - child.getContainer().offset)) - - kMargin + - offsetInViewport + - scrollBottomInset; - final caretBottom = - endpoint.point.dy + kMargin + offsetInViewport + scrollBottomInset; - double? dy; - if (caretTop < scrollOffset) { - dy = caretTop; - } else if (caretBottom > scrollOffset + viewportHeight) { - dy = caretBottom - viewportHeight; - } - if (dy == null) { - return null; - } - return math.max(dy, 0); - } -} - -class EditableContainerParentData - extends ContainerBoxParentData {} - -class RenderEditableContainerBox extends RenderBox - with - ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin { - RenderEditableContainerBox( - List? children, - this._container, - this.textDirection, - this.scrollBottomInset, - this._padding, - ) : assert(_padding.isNonNegative) { - addAll(children); - } - - container_node.Container _container; - TextDirection textDirection; - EdgeInsetsGeometry _padding; - double scrollBottomInset; - EdgeInsets? _resolvedPadding; - - container_node.Container getContainer() { - return _container; - } - - void setContainer(container_node.Container c) { - if (_container == c) { - return; - } - _container = c; - markNeedsLayout(); - } - - EdgeInsetsGeometry getPadding() => _padding; - - void setPadding(EdgeInsetsGeometry value) { - assert(value.isNonNegative); - if (_padding == value) { - return; - } - _padding = value; - _markNeedsPaddingResolution(); - } - - EdgeInsets? get resolvedPadding => _resolvedPadding; - - void _resolvePadding() { - if (_resolvedPadding != null) { - return; - } - _resolvedPadding = _padding.resolve(textDirection); - _resolvedPadding = _resolvedPadding!.copyWith(left: _resolvedPadding!.left); - - assert(_resolvedPadding!.isNonNegative); - } - - RenderEditableBox childAtPosition(TextPosition position) { - assert(firstChild != null); - - final targetNode = _container.queryChild(position.offset, false).node; - - var targetChild = firstChild; - while (targetChild != null) { - if (targetChild.getContainer() == targetNode) { - break; - } - targetChild = childAfter(targetChild); - } - if (targetChild == null) { - throw 'targetChild should not be null'; - } - return targetChild; - } - - void _markNeedsPaddingResolution() { - _resolvedPadding = null; - markNeedsLayout(); - } - - RenderEditableBox? childAtOffset(Offset offset) { - assert(firstChild != null); - _resolvePadding(); - - if (offset.dy <= _resolvedPadding!.top) { - return firstChild; - } - if (offset.dy >= size.height - _resolvedPadding!.bottom) { - return lastChild; - } - - var child = firstChild; - final dx = -offset.dx; - var dy = _resolvedPadding!.top; - while (child != null) { - if (child.size.contains(offset.translate(dx, -dy))) { - return child; - } - dy += child.size.height; - child = childAfter(child); - } - throw 'No child'; - } - - @override - void setupParentData(RenderBox child) { - if (child.parentData is EditableContainerParentData) { - return; - } - - child.parentData = EditableContainerParentData(); - } - - @override - void performLayout() { - assert(constraints.hasBoundedWidth); - _resolvePadding(); - assert(_resolvedPadding != null); - - var mainAxisExtent = _resolvedPadding!.top; - var child = firstChild; - final innerConstraints = - BoxConstraints.tightFor(width: constraints.maxWidth) - .deflate(_resolvedPadding!); - while (child != null) { - child.layout(innerConstraints, parentUsesSize: true); - final childParentData = (child.parentData as EditableContainerParentData) - ..offset = Offset(_resolvedPadding!.left, mainAxisExtent); - mainAxisExtent += child.size.height; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - } - mainAxisExtent += _resolvedPadding!.bottom; - size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent)); - - assert(size.isFinite); - } - - double _getIntrinsicCrossAxis(double Function(RenderBox child) childSize) { - var extent = 0.0; - var child = firstChild; - while (child != null) { - extent = math.max(extent, childSize(child)); - final childParentData = child.parentData as EditableContainerParentData; - child = childParentData.nextSibling; - } - return extent; - } - - double _getIntrinsicMainAxis(double Function(RenderBox child) childSize) { - var extent = 0.0; - var child = firstChild; - while (child != null) { - extent += childSize(child); - final childParentData = child.parentData as EditableContainerParentData; - child = childParentData.nextSibling; - } - return extent; - } - - @override - double computeMinIntrinsicWidth(double height) { - _resolvePadding(); - return _getIntrinsicCrossAxis((child) { - final childHeight = math.max( - 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); - return child.getMinIntrinsicWidth(childHeight) + - _resolvedPadding!.left + - _resolvedPadding!.right; - }); - } - - @override - double computeMaxIntrinsicWidth(double height) { - _resolvePadding(); - return _getIntrinsicCrossAxis((child) { - final childHeight = math.max( - 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom); - return child.getMaxIntrinsicWidth(childHeight) + - _resolvedPadding!.left + - _resolvedPadding!.right; - }); - } - - @override - double computeMinIntrinsicHeight(double width) { - _resolvePadding(); - return _getIntrinsicMainAxis((child) { - final childWidth = math.max( - 0, width - _resolvedPadding!.left + _resolvedPadding!.right); - return child.getMinIntrinsicHeight(childWidth) + - _resolvedPadding!.top + - _resolvedPadding!.bottom; - }); - } - - @override - double computeMaxIntrinsicHeight(double width) { - _resolvePadding(); - return _getIntrinsicMainAxis((child) { - final childWidth = math.max( - 0, width - _resolvedPadding!.left + _resolvedPadding!.right); - return child.getMaxIntrinsicHeight(childWidth) + - _resolvedPadding!.top + - _resolvedPadding!.bottom; - }); - } - - @override - double computeDistanceToActualBaseline(TextBaseline baseline) { - _resolvePadding(); - return defaultComputeDistanceToFirstActualBaseline(baseline)! + - _resolvedPadding!.top; - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/editor.dart'; diff --git a/lib/widgets/image.dart b/lib/widgets/image.dart index b9df48ce..41a8a235 100644 --- a/lib/widgets/image.dart +++ b/lib/widgets/image.dart @@ -1,31 +1,3 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:photo_view/photo_view.dart'; - -class ImageTapWrapper extends StatelessWidget { - const ImageTapWrapper({ - this.imageProvider, - }); - - final ImageProvider? imageProvider; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - constraints: BoxConstraints.expand( - height: MediaQuery.of(context).size.height, - ), - child: GestureDetector( - onTapDown: (_) { - Navigator.pop(context); - }, - child: PhotoView( - imageProvider: imageProvider, - ), - ), - ), - ); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/image.dart'; diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart index 17c47aad..0ee8d6e7 100644 --- a/lib/widgets/keyboard_listener.dart +++ b/lib/widgets/keyboard_listener.dart @@ -1,105 +1,3 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL } - -typedef CursorMoveCallback = void Function( - LogicalKeyboardKey key, bool wordModifier, bool lineModifier, bool shift); -typedef InputShortcutCallback = void Function(InputShortcut? shortcut); -typedef OnDeleteCallback = void Function(bool forward); - -class 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, - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown, - }; - - static final Set _shortcutKeys = { - LogicalKeyboardKey.keyA, - LogicalKeyboardKey.keyC, - LogicalKeyboardKey.keyV, - LogicalKeyboardKey.keyX, - LogicalKeyboardKey.delete, - LogicalKeyboardKey.backspace, - }; - - static final Set _nonModifierKeys = { - ..._shortcutKeys, - ..._moveKeys, - }; - - static final Set _modifierKeys = { - LogicalKeyboardKey.shift, - LogicalKeyboardKey.control, - LogicalKeyboardKey.alt, - }; - - static final Set _macOsModifierKeys = - { - LogicalKeyboardKey.shift, - LogicalKeyboardKey.meta, - LogicalKeyboardKey.alt, - }; - - static final Set _interestingKeys = { - ..._modifierKeys, - ..._macOsModifierKeys, - ..._nonModifierKeys, - }; - - static final Map _keyToShortcut = { - LogicalKeyboardKey.keyX: InputShortcut.CUT, - LogicalKeyboardKey.keyC: InputShortcut.COPY, - LogicalKeyboardKey.keyV: InputShortcut.PASTE, - LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, - }; - - bool handleRawKeyEvent(RawKeyEvent event) { - if (kIsWeb) { - // On web platform, we should ignore the key because it's processed already. - return false; - } - - if (event is! RawKeyDownEvent) { - return false; - } - - final keysPressed = - LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); - final key = event.logicalKey; - final isMacOS = event.data is RawKeyEventDataMacOs; - if (!_nonModifierKeys.contains(key) || - keysPressed - .difference(isMacOS ? _macOsModifierKeys : _modifierKeys) - .length > - 1 || - keysPressed.difference(_interestingKeys).isNotEmpty) { - return false; - } - - if (_moveKeys.contains(key)) { - onCursorMove( - key, - isMacOS ? event.isAltPressed : event.isControlPressed, - isMacOS ? event.isMetaPressed : event.isAltPressed, - event.isShiftPressed); - } else if (isMacOS - ? event.isMetaPressed - : event.isControlPressed && _shortcutKeys.contains(key)) { - onShortcut(_keyToShortcut[key]); - } else if (key == LogicalKeyboardKey.delete) { - onDelete(true); - } else if (key == LogicalKeyboardKey.backspace) { - onDelete(false); - } - return false; - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/keyboard_listener.dart'; diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index 8a04c4e1..6d17bb7d 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -1,298 +1,3 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -import 'box.dart'; - -class BaselineProxy extends SingleChildRenderObjectWidget { - 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( - null, - textStyle!, - padding, - ); - } - - @override - void updateRenderObject( - BuildContext context, covariant RenderBaselineProxy renderObject) { - renderObject - ..textStyle = textStyle! - ..padding = padding!; - } -} - -class RenderBaselineProxy extends RenderProxyBox { - RenderBaselineProxy( - RenderParagraph? child, - TextStyle textStyle, - EdgeInsets? padding, - ) : _prototypePainter = TextPainter( - text: TextSpan(text: ' ', style: textStyle), - textDirection: TextDirection.ltr, - strutStyle: - StrutStyle.fromTextStyle(textStyle, forceStrutHeight: true)), - super(child); - - final TextPainter _prototypePainter; - - set textStyle(TextStyle value) { - if (_prototypePainter.text!.style == value) { - return; - } - _prototypePainter.text = TextSpan(text: ' ', style: value); - markNeedsLayout(); - } - - EdgeInsets? _padding; - - set padding(EdgeInsets value) { - if (_padding == value) { - return; - } - _padding = value; - markNeedsLayout(); - } - - @override - double computeDistanceToActualBaseline(TextBaseline baseline) => - _prototypePainter.computeDistanceToActualBaseline(baseline); - // SEE What happens + _padding?.top; - - @override - void performLayout() { - super.performLayout(); - _prototypePainter.layout(); - } -} - -class EmbedProxy extends SingleChildRenderObjectWidget { - const EmbedProxy(Widget child) : super(child: child); - - @override - RenderEmbedProxy createRenderObject(BuildContext context) => - RenderEmbedProxy(null); -} - -class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { - RenderEmbedProxy(RenderBox? child) : super(child); - - @override - List getBoxesForSelection(TextSelection selection) { - if (!selection.isCollapsed) { - return [ - TextBox.fromLTRBD(0, 0, size.width, size.height, TextDirection.ltr) - ]; - } - - final left = selection.extentOffset == 0 ? 0.0 : size.width; - final right = selection.extentOffset == 0 ? 0.0 : size.width; - return [ - TextBox.fromLTRBD(left, 0, right, size.height, TextDirection.ltr) - ]; - } - - @override - double getFullHeightForCaret(TextPosition position) => size.height; - - @override - Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) { - assert(position.offset <= 1 && position.offset >= 0); - return position.offset == 0 ? Offset.zero : Offset(size.width, 0); - } - - @override - TextPosition getPositionForOffset(Offset offset) => - TextPosition(offset: offset.dx > size.width / 2 ? 1 : 0); - - @override - TextRange getWordBoundary(TextPosition position) => - const TextRange(start: 0, end: 1); - - @override - double getPreferredLineHeight() { - return size.height; - } -} - -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; - final double textScaleFactor; - final Locale locale; - final StrutStyle strutStyle; - final TextWidthBasis textWidthBasis; - final TextHeightBehavior? textHeightBehavior; - - @override - RenderParagraphProxy createRenderObject(BuildContext context) { - return RenderParagraphProxy( - null, - textStyle, - textAlign, - textDirection, - textScaleFactor, - strutStyle, - locale, - textWidthBasis, - textHeightBehavior); - } - - @override - void updateRenderObject( - BuildContext context, covariant RenderParagraphProxy renderObject) { - renderObject - ..textStyle = textStyle - ..textAlign = textAlign - ..textDirection = textDirection - ..textScaleFactor = textScaleFactor - ..locale = locale - ..strutStyle = strutStyle - ..textWidthBasis = textWidthBasis - ..textHeightBehavior = textHeightBehavior; - } -} - -class RenderParagraphProxy extends RenderProxyBox - implements RenderContentProxyBox { - RenderParagraphProxy( - RenderParagraph? child, - TextStyle textStyle, - TextAlign textAlign, - TextDirection textDirection, - double textScaleFactor, - StrutStyle strutStyle, - Locale locale, - TextWidthBasis textWidthBasis, - TextHeightBehavior? textHeightBehavior, - ) : _prototypePainter = TextPainter( - text: TextSpan(text: ' ', style: textStyle), - textAlign: textAlign, - textDirection: textDirection, - textScaleFactor: textScaleFactor, - strutStyle: strutStyle, - locale: locale, - textWidthBasis: textWidthBasis, - textHeightBehavior: textHeightBehavior), - super(child); - - final TextPainter _prototypePainter; - - set textStyle(TextStyle value) { - if (_prototypePainter.text!.style == value) { - return; - } - _prototypePainter.text = TextSpan(text: ' ', style: value); - markNeedsLayout(); - } - - set textAlign(TextAlign value) { - if (_prototypePainter.textAlign == value) { - return; - } - _prototypePainter.textAlign = value; - markNeedsLayout(); - } - - set textDirection(TextDirection value) { - if (_prototypePainter.textDirection == value) { - return; - } - _prototypePainter.textDirection = value; - markNeedsLayout(); - } - - set textScaleFactor(double value) { - if (_prototypePainter.textScaleFactor == value) { - return; - } - _prototypePainter.textScaleFactor = value; - markNeedsLayout(); - } - - set strutStyle(StrutStyle value) { - if (_prototypePainter.strutStyle == value) { - return; - } - _prototypePainter.strutStyle = value; - markNeedsLayout(); - } - - set locale(Locale value) { - if (_prototypePainter.locale == value) { - return; - } - _prototypePainter.locale = value; - markNeedsLayout(); - } - - set textWidthBasis(TextWidthBasis value) { - if (_prototypePainter.textWidthBasis == value) { - return; - } - _prototypePainter.textWidthBasis = value; - markNeedsLayout(); - } - - set textHeightBehavior(TextHeightBehavior? value) { - if (_prototypePainter.textHeightBehavior == value) { - return; - } - _prototypePainter.textHeightBehavior = value; - markNeedsLayout(); - } - - @override - RenderParagraph? get child => super.child as RenderParagraph?; - - @override - double getPreferredLineHeight() { - return _prototypePainter.preferredLineHeight; - } - - @override - Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) => - child!.getOffsetForCaret(position, caretPrototype!); - - @override - TextPosition getPositionForOffset(Offset offset) => - child!.getPositionForOffset(offset); - - @override - double? getFullHeightForCaret(TextPosition position) => - child!.getFullHeightForCaret(position); - - @override - TextRange getWordBoundary(TextPosition position) => - child!.getWordBoundary(position); - - @override - List getBoxesForSelection(TextSelection selection) => - child!.getBoxesForSelection(selection); - - @override - void performLayout() { - super.performLayout(); - _prototypePainter.layout( - minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/proxy.dart'; diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index cd4f379c..cf483dd1 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1,736 +1,3 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:tuple/tuple.dart'; - -import '../models/documents/attribute.dart'; -import '../models/documents/document.dart'; -import '../models/documents/nodes/block.dart'; -import '../models/documents/nodes/line.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 'raw_editor/raw_editor_state_keyboard_mixin.dart'; -import 'raw_editor/raw_editor_state_selection_delegate_mixin.dart'; -import 'raw_editor/raw_editor_state_text_input_client_mixin.dart'; -import 'text_block.dart'; -import 'text_line.dart'; -import 'text_selection.dart'; - -class RawEditor extends StatefulWidget { - const RawEditor( - Key key, - this.controller, - this.focusNode, - this.scrollController, - this.scrollable, - this.scrollBottomInset, - 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; - final bool scrollable; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - final bool readOnly; - final String? placeholder; - final ValueChanged? onLaunchUrl; - final ToolbarOptions toolbarOptions; - final bool showSelectionHandles; - final bool showCursor; - final CursorStyle cursorStyle; - final TextCapitalization textCapitalization; - final double? maxHeight; - final double? minHeight; - final DefaultStyles? customStyles; - final bool expands; - final bool autoFocus; - final Color selectionColor; - final TextSelectionControls selectionCtrls; - final Brightness keyboardAppearance; - final bool enableInteractiveSelection; - final ScrollPhysics? scrollPhysics; - final EmbedBuilder embedBuilder; - - @override - State createState() => RawEditorState(); -} - -class RawEditorState extends EditorState - with - AutomaticKeepAliveClientMixin, - WidgetsBindingObserver, - TickerProviderStateMixin, - RawEditorStateKeyboardMixin, - RawEditorStateTextInputClientMixin, - RawEditorStateSelectionDelegateMixin { - final GlobalKey _editorKey = GlobalKey(); - - // Keyboard - late KeyboardListener _keyboardListener; - KeyboardVisibilityController? _keyboardVisibilityController; - StreamSubscription? _keyboardVisibilitySubscription; - bool _keyboardVisible = false; - - // Selection overlay - @override - EditorTextSelectionOverlay? getSelectionOverlay() => _selectionOverlay; - EditorTextSelectionOverlay? _selectionOverlay; - - ScrollController? _scrollController; - - late CursorCont _cursorCont; - - // Focus - bool _didAutoFocus = false; - FocusAttachment? _focusAttachment; - bool get _hasFocus => widget.focusNode.hasFocus; - - DefaultStyles? _styles; - - final ClipboardStatusNotifier? _clipboardStatus = - kIsWeb ? null : ClipboardStatusNotifier(); - final LayerLink _toolbarLayerLink = LayerLink(); - final LayerLink _startHandleLayerLink = LayerLink(); - final LayerLink _endHandleLayerLink = LayerLink(); - - TextDirection get _textDirection => Directionality.of(context); - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMediaQuery(context)); - _focusAttachment!.reparent(); - super.build(context); - - var _doc = widget.controller.document; - if (_doc.isEmpty() && - !widget.focusNode.hasFocus && - widget.placeholder != null) { - _doc = Document.fromJson(jsonDecode( - '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); - } - - Widget child = CompositedTransformTarget( - link: _toolbarLayerLink, - child: Semantics( - child: _Editor( - key: _editorKey, - document: _doc, - selection: widget.controller.selection, - hasFocus: _hasFocus, - textDirection: _textDirection, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - onSelectionChanged: _handleSelectionChanged, - scrollBottomInset: widget.scrollBottomInset, - padding: widget.padding, - children: _buildChildren(_doc, context), - ), - ), - ); - - if (widget.scrollable) { - final baselinePadding = - EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1); - child = BaselineProxy( - textStyle: _styles!.paragraph!.style, - padding: baselinePadding, - child: SingleChildScrollView( - controller: _scrollController, - physics: widget.scrollPhysics, - child: child, - ), - ); - } - - final constraints = widget.expands - ? const BoxConstraints.expand() - : BoxConstraints( - minHeight: widget.minHeight ?? 0.0, - maxHeight: widget.maxHeight ?? double.infinity); - - return QuillStyles( - data: _styles!, - child: MouseRegion( - cursor: SystemMouseCursors.text, - child: Container( - constraints: constraints, - child: child, - ), - ), - ); - } - - void _handleSelectionChanged( - TextSelection selection, SelectionChangedCause cause) { - widget.controller.updateSelection(selection, ChangeSource.LOCAL); - - _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); - - if (!_keyboardVisible) { - requestKeyboard(); - } - } - - /// Updates the checkbox positioned at [offset] in document - /// by changing its attribute according to [value]. - void _handleCheckboxTap(int offset, bool value) { - if (!widget.readOnly) { - if (value) { - widget.controller.formatText(offset, 0, Attribute.checked); - } else { - widget.controller.formatText(offset, 0, Attribute.unchecked); - } - } - } - - List _buildChildren(Document doc, BuildContext context) { - final result = []; - final indentLevelCounts = {}; - for (final node in doc.root.children) { - if (node is Line) { - final editableTextLine = _getEditableTextLineFromNode(node, context); - result.add(editableTextLine); - } else if (node is Block) { - final attrs = node.style.attributes; - final editableTextBlock = EditableTextBlock( - node, - _textDirection, - widget.scrollBottomInset, - _getVerticalSpacingForBlock(node, _styles), - widget.controller.selection, - widget.selectionColor, - _styles, - widget.enableInteractiveSelection, - _hasFocus, - attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - widget.embedBuilder, - _cursorCont, - indentLevelCounts, - _handleCheckboxTap, - ); - result.add(editableTextBlock); - } else { - throw StateError('Unreachable.'); - } - } - return result; - } - - EditableTextLine _getEditableTextLineFromNode( - Line node, BuildContext context) { - final textLine = TextLine( - line: node, - textDirection: _textDirection, - embedBuilder: widget.embedBuilder, - styles: _styles!, - ); - final editableTextLine = EditableTextLine( - node, - null, - textLine, - 0, - _getVerticalSpacingForLine(node, _styles), - _textDirection, - widget.controller.selection, - widget.selectionColor, - widget.enableInteractiveSelection, - _hasFocus, - MediaQuery.of(context).devicePixelRatio, - _cursorCont); - return editableTextLine; - } - - Tuple2 _getVerticalSpacingForLine( - Line line, DefaultStyles? defaultStyles) { - final attrs = line.style.attributes; - if (attrs.containsKey(Attribute.header.key)) { - final int? level = attrs[Attribute.header.key]!.value; - switch (level) { - case 1: - return defaultStyles!.h1!.verticalSpacing; - case 2: - return defaultStyles!.h2!.verticalSpacing; - case 3: - return defaultStyles!.h3!.verticalSpacing; - default: - throw 'Invalid level $level'; - } - } - - return defaultStyles!.paragraph!.verticalSpacing; - } - - Tuple2 _getVerticalSpacingForBlock( - Block node, DefaultStyles? defaultStyles) { - final attrs = node.style.attributes; - if (attrs.containsKey(Attribute.blockQuote.key)) { - return defaultStyles!.quote!.verticalSpacing; - } else if (attrs.containsKey(Attribute.codeBlock.key)) { - return defaultStyles!.code!.verticalSpacing; - } else if (attrs.containsKey(Attribute.indent.key)) { - return defaultStyles!.indent!.verticalSpacing; - } - return defaultStyles!.lists!.verticalSpacing; - } - - @override - void initState() { - super.initState(); - - _clipboardStatus?.addListener(_onChangedClipboardStatus); - - widget.controller.addListener(() { - _didChangeTextEditingValue(widget.controller.ignoreFocusOnTextChange); - }); - - _scrollController = widget.scrollController; - _scrollController!.addListener(_updateSelectionOverlayForScroll); - - _cursorCont = CursorCont( - show: ValueNotifier(widget.showCursor), - style: widget.cursorStyle, - tickerProvider: this, - ); - - _keyboardListener = KeyboardListener( - handleCursorMovement, - handleShortcut, - handleDelete, - ); - - if (defaultTargetPlatform == TargetPlatform.windows || - defaultTargetPlatform == TargetPlatform.macOS || - defaultTargetPlatform == TargetPlatform.linux || - defaultTargetPlatform == TargetPlatform.fuchsia) { - _keyboardVisible = true; - } else { - _keyboardVisibilityController = KeyboardVisibilityController(); - _keyboardVisible = _keyboardVisibilityController!.isVisible; - _keyboardVisibilitySubscription = - _keyboardVisibilityController?.onChange.listen((visible) { - _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(); - } - }); - } - - _focusAttachment = widget.focusNode.attach(context, - onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); - widget.focusNode.addListener(_handleFocusChanged); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final parentStyles = QuillStyles.getStyles(context, true); - final defaultStyles = DefaultStyles.getInstance(context); - _styles = (parentStyles != null) - ? defaultStyles.merge(parentStyles) - : defaultStyles; - - if (widget.customStyles != null) { - _styles = _styles!.merge(widget.customStyles!); - } - - if (!_didAutoFocus && widget.autoFocus) { - FocusScope.of(context).autofocus(widget.focusNode); - _didAutoFocus = true; - } - } - - @override - void didUpdateWidget(RawEditor oldWidget) { - super.didUpdateWidget(oldWidget); - - _cursorCont.show.value = widget.showCursor; - _cursorCont.style = widget.cursorStyle; - - if (widget.controller != oldWidget.controller) { - oldWidget.controller.removeListener(_didChangeTextEditingValue); - widget.controller.addListener(_didChangeTextEditingValue); - updateRemoteValueIfNeeded(); - } - - if (widget.scrollController != _scrollController) { - _scrollController!.removeListener(_updateSelectionOverlayForScroll); - _scrollController = widget.scrollController; - _scrollController!.addListener(_updateSelectionOverlayForScroll); - } - - if (widget.focusNode != oldWidget.focusNode) { - oldWidget.focusNode.removeListener(_handleFocusChanged); - _focusAttachment?.detach(); - _focusAttachment = widget.focusNode.attach(context, - onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); - widget.focusNode.addListener(_handleFocusChanged); - updateKeepAlive(); - } - - if (widget.controller.selection != oldWidget.controller.selection) { - _selectionOverlay?.update(textEditingValue); - } - - _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); - if (!shouldCreateInputConnection) { - closeConnectionIfNeeded(); - } else { - if (oldWidget.readOnly && _hasFocus) { - openConnectionIfNeeded(); - } - } - } - - bool _shouldShowSelectionHandles() { - return widget.showSelectionHandles && - !widget.controller.selection.isCollapsed; - } - - @override - void dispose() { - closeConnectionIfNeeded(); - _keyboardVisibilitySubscription?.cancel(); - assert(!hasConnection); - _selectionOverlay?.dispose(); - _selectionOverlay = null; - widget.controller.removeListener(_didChangeTextEditingValue); - widget.focusNode.removeListener(_handleFocusChanged); - _focusAttachment!.detach(); - _cursorCont.dispose(); - _clipboardStatus?.removeListener(_onChangedClipboardStatus); - _clipboardStatus?.dispose(); - super.dispose(); - } - - void _updateSelectionOverlayForScroll() { - _selectionOverlay?.markNeedsBuild(); - } - - void _didChangeTextEditingValue([bool ignoreFocus = false]) { - if (kIsWeb) { - _onChangeTextEditingValue(ignoreFocus); - if (!ignoreFocus) { - requestKeyboard(); - } - return; - } - - if (ignoreFocus || _keyboardVisible) { - _onChangeTextEditingValue(ignoreFocus); - } else { - requestKeyboard(); - if (mounted) { - setState(() { - // Use widget.controller.value in build() - // Trigger build and updateChildren - }); - } - } - } - - void _onChangeTextEditingValue([bool ignoreCaret = false]) { - updateRemoteValueIfNeeded(); - if (ignoreCaret) { - return; - } - _showCaretOnScreen(); - _cursorCont.startOrStopCursorTimerIfNeeded( - _hasFocus, widget.controller.selection); - if (hasConnection) { - _cursorCont - ..stopCursorTimer(resetCharTicks: false) - ..startCursorTimer(); - } - - SchedulerBinding.instance!.addPostFrameCallback( - (_) => _updateOrDisposeSelectionOverlayIfNeeded()); - if (mounted) { - setState(() { - // Use widget.controller.value in build() - // Trigger build and updateChildren - }); - } - } - - void _updateOrDisposeSelectionOverlayIfNeeded() { - if (_selectionOverlay != null) { - if (_hasFocus) { - _selectionOverlay!.update(textEditingValue); - } else { - _selectionOverlay!.dispose(); - _selectionOverlay = null; - } - } else if (_hasFocus) { - _selectionOverlay?.hide(); - _selectionOverlay = null; - - _selectionOverlay = EditorTextSelectionOverlay( - textEditingValue, - false, - context, - widget, - _toolbarLayerLink, - _startHandleLayerLink, - _endHandleLayerLink, - getRenderEditor(), - widget.selectionCtrls, - this, - DragStartBehavior.start, - null, - _clipboardStatus!, - ); - _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); - _selectionOverlay!.showHandles(); - } - } - - void _handleFocusChanged() { - openOrCloseConnection(); - _cursorCont.startOrStopCursorTimerIfNeeded( - _hasFocus, widget.controller.selection); - _updateOrDisposeSelectionOverlayIfNeeded(); - if (_hasFocus) { - WidgetsBinding.instance!.addObserver(this); - _showCaretOnScreen(); - } else { - WidgetsBinding.instance!.removeObserver(this); - } - updateKeepAlive(); - } - - void _onChangedClipboardStatus() { - if (!mounted) return; - setState(() { - // Inform the widget that the value of clipboardStatus has changed. - // Trigger build and updateChildren - }); - } - - bool _showCaretOnScreenScheduled = false; - - void _showCaretOnScreen() { - if (!widget.showCursor || _showCaretOnScreenScheduled) { - return; - } - - _showCaretOnScreenScheduled = true; - SchedulerBinding.instance!.addPostFrameCallback((_) { - if (widget.scrollable) { - _showCaretOnScreenScheduled = false; - - final viewport = RenderAbstractViewport.of(getRenderEditor()); - - final editorOffset = getRenderEditor()! - .localToGlobal(const Offset(0, 0), ancestor: viewport); - final offsetInViewport = _scrollController!.offset + editorOffset.dy; - - final offset = getRenderEditor()!.getOffsetToRevealCursor( - _scrollController!.position.viewportDimension, - _scrollController!.offset, - offsetInViewport, - ); - - if (offset != null) { - _scrollController!.animateTo( - offset, - duration: const Duration(milliseconds: 100), - curve: Curves.fastOutSlowIn, - ); - } - } - }); - } - - @override - RenderEditor? getRenderEditor() { - return _editorKey.currentContext!.findRenderObject() as RenderEditor?; - } - - @override - TextEditingValue getTextEditingValue() { - return widget.controller.plainTextEditingValue; - } - - @override - void requestKeyboard() { - if (_hasFocus) { - openConnectionIfNeeded(); - } else { - widget.focusNode.requestFocus(); - } - } - - @override - void setTextEditingValue(TextEditingValue value) { - if (value.text == textEditingValue.text) { - widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); - } else { - __setEditingValue(value); - } - } - - Future __setEditingValue(TextEditingValue value) async { - if (await __isItCut(value)) { - widget.controller.replaceText( - textEditingValue.selection.start, - textEditingValue.text.length - value.text.length, - '', - value.selection, - ); - } else { - final value = textEditingValue; - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { - final length = - textEditingValue.selection.end - textEditingValue.selection.start; - widget.controller.replaceText( - value.selection.start, - length, - data.text, - value.selection, - ); - // move cursor to the end of pasted text selection - widget.controller.updateSelection( - TextSelection.collapsed( - offset: value.selection.start + data.text!.length), - ChangeSource.LOCAL); - } - } - } - - Future __isItCut(TextEditingValue value) async { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data == null) { - return false; - } - return textEditingValue.text.length - value.text.length == - data.text!.length; - } - - @override - bool showToolbar() { - // Web is using native dom elements to enable clipboard functionality of the - // toolbar: copy, paste, select, cut. It might also provide additional - // functionality depending on the browser (such as translate). Due to this - // we should not show a Flutter toolbar for the editable text elements. - if (kIsWeb) { - return false; - } - if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { - return false; - } - - _selectionOverlay!.update(textEditingValue); - _selectionOverlay!.showToolbar(); - return true; - } - - @override - bool get wantKeepAlive => widget.focusNode.hasFocus; - - @override - void userUpdateTextEditingValue( - TextEditingValue value, SelectionChangedCause cause) { - // TODO: implement userUpdateTextEditingValue - } -} - -class _Editor extends MultiChildRenderObjectWidget { - _Editor({ - required Key key, - required List children, - required this.document, - required this.textDirection, - required this.hasFocus, - required this.selection, - required this.startHandleLayerLink, - required this.endHandleLayerLink, - required this.onSelectionChanged, - required this.scrollBottomInset, - this.padding = EdgeInsets.zero, - }) : super(key: key, children: children); - - final Document document; - final TextDirection textDirection; - final bool hasFocus; - final TextSelection selection; - final LayerLink startHandleLayerLink; - final LayerLink endHandleLayerLink; - final TextSelectionChangedHandler onSelectionChanged; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - - @override - RenderEditor createRenderObject(BuildContext context) { - return RenderEditor( - null, - textDirection, - scrollBottomInset, - padding, - document, - selection, - hasFocus, - onSelectionChanged, - startHandleLayerLink, - endHandleLayerLink, - const EdgeInsets.fromLTRB(4, 4, 4, 5), - ); - } - - @override - void updateRenderObject( - BuildContext context, covariant RenderEditor renderObject) { - renderObject - ..document = document - ..setContainer(document.root) - ..textDirection = textDirection - ..setHasFocus(hasFocus) - ..setSelection(selection) - ..setStartHandleLayerLink(startHandleLayerLink) - ..setEndHandleLayerLink(endHandleLayerLink) - ..onSelectionChanged = onSelectionChanged - ..setScrollBottomInset(scrollBottomInset) - ..setPadding(padding); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/raw_editor.dart'; diff --git a/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart index 0eb7f955..ab326cec 100644 --- a/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart +++ b/lib/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart @@ -1,354 +1,3 @@ -import 'dart:ui'; - -import 'package:characters/characters.dart'; -import 'package:flutter/services.dart'; - -import '../../models/documents/document.dart'; -import '../../utils/diff_delta.dart'; -import '../editor.dart'; -import '../keyboard_listener.dart'; - -mixin RawEditorStateKeyboardMixin on EditorState { - // Holds the last cursor location the user selected in the case the user tries - // to select vertically past the end or beginning of the field. If they do, - // then we need to keep the old cursor location so that we can go back to it - // if they change their minds. Only used for moving selection up and down in a - // multiline text field when selecting using the keyboard. - int _cursorResetLocation = -1; - - // Whether we should reset the location of the cursor in the case the user - // tries to select vertically past the end or beginning of the field. If they - // do, then we need to keep the old cursor location so that we can go back to - // it if they change their minds. Only used for resetting selection up and - // down in a multiline text field when selecting using the keyboard. - bool _wasSelectingVerticallyWithKeyboard = false; - - void handleCursorMovement( - LogicalKeyboardKey key, - bool wordModifier, - bool lineModifier, - bool shift, - ) { - if (wordModifier && lineModifier) { - // If both modifiers are down, nothing happens on any of the platforms. - return; - } - final selection = widget.controller.selection; - - var newSelection = widget.controller.selection; - - final plainText = getTextEditingValue().text; - - final rightKey = key == LogicalKeyboardKey.arrowRight, - leftKey = key == LogicalKeyboardKey.arrowLeft, - upKey = key == LogicalKeyboardKey.arrowUp, - downKey = key == LogicalKeyboardKey.arrowDown; - - if ((rightKey || leftKey) && !(rightKey && leftKey)) { - newSelection = _jumpToBeginOrEndOfWord(newSelection, wordModifier, - leftKey, rightKey, plainText, lineModifier, shift); - } - - if (downKey || upKey) { - newSelection = _handleMovingCursorVertically( - upKey, downKey, shift, selection, newSelection, plainText); - } - - if (!shift) { - newSelection = - _placeCollapsedSelection(selection, newSelection, leftKey, rightKey); - } - - widget.controller.updateSelection(newSelection, ChangeSource.LOCAL); - } - - // Handles shortcut functionality including cut, copy, paste and select all - // using control/command + (X, C, V, A). - // TODO: Add support for formatting shortcuts: Cmd+B (bold), Cmd+I (italic) - Future handleShortcut(InputShortcut? shortcut) async { - final selection = widget.controller.selection; - final plainText = getTextEditingValue().text; - if (shortcut == InputShortcut.COPY) { - if (!selection.isCollapsed) { - await Clipboard.setData( - ClipboardData(text: selection.textInside(plainText))); - } - return; - } - if (shortcut == InputShortcut.CUT && !widget.readOnly) { - if (!selection.isCollapsed) { - final data = selection.textInside(plainText); - await Clipboard.setData(ClipboardData(text: data)); - - widget.controller.replaceText( - selection.start, - data.length, - '', - TextSelection.collapsed(offset: selection.start), - ); - - setTextEditingValue(TextEditingValue( - text: - selection.textBefore(plainText) + selection.textAfter(plainText), - selection: TextSelection.collapsed(offset: selection.start), - )); - } - return; - } - if (shortcut == InputShortcut.PASTE && !widget.readOnly) { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { - widget.controller.replaceText( - selection.start, - selection.end - selection.start, - data.text, - TextSelection.collapsed(offset: selection.start + data.text!.length), - ); - } - return; - } - if (shortcut == InputShortcut.SELECT_ALL && - widget.enableInteractiveSelection) { - widget.controller.updateSelection( - selection.copyWith( - baseOffset: 0, - extentOffset: getTextEditingValue().text.length, - ), - ChangeSource.REMOTE); - return; - } - } - - void handleDelete(bool forward) { - final selection = widget.controller.selection; - final plainText = getTextEditingValue().text; - var cursorPosition = selection.start; - var textBefore = selection.textBefore(plainText); - var textAfter = selection.textAfter(plainText); - if (selection.isCollapsed) { - if (!forward && textBefore.isNotEmpty) { - final characterBoundary = - _previousCharacter(textBefore.length, textBefore, true); - textBefore = textBefore.substring(0, characterBoundary); - cursorPosition = characterBoundary; - } - if (forward && textAfter.isNotEmpty && textAfter != '\n') { - final deleteCount = _nextCharacter(0, textAfter, true); - textAfter = textAfter.substring(deleteCount); - } - } - final newSelection = TextSelection.collapsed(offset: cursorPosition); - final newText = textBefore + textAfter; - final size = plainText.length - newText.length; - widget.controller.replaceText( - cursorPosition, - size, - '', - newSelection, - ); - } - - TextSelection _jumpToBeginOrEndOfWord( - TextSelection newSelection, - bool wordModifier, - bool leftKey, - bool rightKey, - String plainText, - bool lineModifier, - bool shift) { - if (wordModifier) { - if (leftKey) { - final textSelection = getRenderEditor()!.selectWordAtPosition( - TextPosition( - offset: _previousCharacter( - newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.baseOffset); - } - final textSelection = getRenderEditor()!.selectWordAtPosition( - TextPosition( - offset: - _nextCharacter(newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.extentOffset); - } else if (lineModifier) { - if (leftKey) { - final textSelection = getRenderEditor()!.selectLineAtPosition( - TextPosition( - offset: _previousCharacter( - newSelection.extentOffset, plainText, false))); - return newSelection.copyWith(extentOffset: textSelection.baseOffset); - } - final startPoint = newSelection.extentOffset; - if (startPoint < plainText.length) { - final textSelection = getRenderEditor()! - .selectLineAtPosition(TextPosition(offset: startPoint)); - return newSelection.copyWith(extentOffset: textSelection.extentOffset); - } - return newSelection; - } - - if (rightKey && newSelection.extentOffset < plainText.length) { - final nextExtent = - _nextCharacter(newSelection.extentOffset, plainText, true); - final distance = nextExtent - newSelection.extentOffset; - newSelection = newSelection.copyWith(extentOffset: nextExtent); - if (shift) { - _cursorResetLocation += distance; - } - return newSelection; - } - - if (leftKey && newSelection.extentOffset > 0) { - final previousExtent = - _previousCharacter(newSelection.extentOffset, plainText, true); - final distance = newSelection.extentOffset - previousExtent; - newSelection = newSelection.copyWith(extentOffset: previousExtent); - if (shift) { - _cursorResetLocation -= distance; - } - return newSelection; - } - return newSelection; - } - - /// Returns the index into the string of the next character boundary after the - /// given index. - /// - /// The character boundary is determined by the characters package, so - /// surrogate pairs and extended grapheme clusters are considered. - /// - /// The index must be between 0 and string.length, inclusive. If given - /// string.length, string.length is returned. - /// - /// Setting includeWhitespace to false will only return the index of non-space - /// characters. - int _nextCharacter(int index, String string, bool includeWhitespace) { - assert(index >= 0 && index <= string.length); - if (index == string.length) { - return string.length; - } - - var count = 0; - final remain = string.characters.skipWhile((currentString) { - if (count <= index) { - count += currentString.length; - return true; - } - if (includeWhitespace) { - return false; - } - return WHITE_SPACE.contains(currentString.codeUnitAt(0)); - }); - return string.length - remain.toString().length; - } - - /// Returns the index into the string of the previous character boundary - /// before the given index. - /// - /// The character boundary is determined by the characters package, so - /// surrogate pairs and extended grapheme clusters are considered. - /// - /// The index must be between 0 and string.length, inclusive. If index is 0, - /// 0 will be returned. - /// - /// Setting includeWhitespace to false will only return the index of non-space - /// characters. - int _previousCharacter(int index, String string, includeWhitespace) { - assert(index >= 0 && index <= string.length); - if (index == 0) { - return 0; - } - - var count = 0; - int? lastNonWhitespace; - for (final currentString in string.characters) { - if (!includeWhitespace && - !WHITE_SPACE.contains( - currentString.characters.first.toString().codeUnitAt(0))) { - lastNonWhitespace = count; - } - if (count + currentString.length >= index) { - return includeWhitespace ? count : lastNonWhitespace ?? 0; - } - count += currentString.length; - } - return 0; - } - - TextSelection _handleMovingCursorVertically( - bool upKey, - bool downKey, - bool shift, - TextSelection selection, - TextSelection newSelection, - String plainText) { - final originPosition = TextPosition( - offset: upKey ? selection.baseOffset : selection.extentOffset); - - final child = getRenderEditor()!.childAtPosition(originPosition); - final localPosition = TextPosition( - offset: originPosition.offset - child.getContainer().documentOffset); - - var position = upKey - ? child.getPositionAbove(localPosition) - : child.getPositionBelow(localPosition); - - if (position == null) { - final sibling = upKey - ? getRenderEditor()!.childBefore(child) - : getRenderEditor()!.childAfter(child); - if (sibling == null) { - position = TextPosition(offset: upKey ? 0 : plainText.length - 1); - } else { - final finalOffset = Offset( - child.getOffsetForCaret(localPosition).dx, - sibling - .getOffsetForCaret(TextPosition( - offset: upKey ? sibling.getContainer().length - 1 : 0)) - .dy); - final siblingPosition = sibling.getPositionForOffset(finalOffset); - position = TextPosition( - offset: - sibling.getContainer().documentOffset + siblingPosition.offset); - } - } else { - position = TextPosition( - offset: child.getContainer().documentOffset + position.offset); - } - - if (position.offset == newSelection.extentOffset) { - if (downKey) { - newSelection = newSelection.copyWith(extentOffset: plainText.length); - } else if (upKey) { - newSelection = newSelection.copyWith(extentOffset: 0); - } - _wasSelectingVerticallyWithKeyboard = shift; - return newSelection; - } - - if (_wasSelectingVerticallyWithKeyboard && shift) { - newSelection = newSelection.copyWith(extentOffset: _cursorResetLocation); - _wasSelectingVerticallyWithKeyboard = false; - return newSelection; - } - newSelection = newSelection.copyWith(extentOffset: position.offset); - _cursorResetLocation = newSelection.extentOffset; - return newSelection; - } - - TextSelection _placeCollapsedSelection(TextSelection selection, - TextSelection newSelection, bool leftKey, bool rightKey) { - var newOffset = newSelection.extentOffset; - if (!selection.isCollapsed) { - if (leftKey) { - newOffset = newSelection.baseOffset < newSelection.extentOffset - ? newSelection.baseOffset - : newSelection.extentOffset; - } else if (rightKey) { - newOffset = newSelection.baseOffset > newSelection.extentOffset - ? newSelection.baseOffset - : newSelection.extentOffset; - } - } - return TextSelection.fromPosition(TextPosition(offset: newOffset)); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/raw_editor/raw_editor_state_keyboard_mixin.dart'; diff --git a/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart index cda991cc..8ff17a7f 100644 --- a/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart +++ b/lib/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -1,40 +1,3 @@ -import 'package:flutter/widgets.dart'; - -import '../editor.dart'; - -mixin RawEditorStateSelectionDelegateMixin on EditorState - implements TextSelectionDelegate { - @override - TextEditingValue get textEditingValue { - return getTextEditingValue(); - } - - @override - set textEditingValue(TextEditingValue value) { - setTextEditingValue(value); - } - - @override - void bringIntoView(TextPosition position) { - // TODO: implement bringIntoView - } - - @override - void hideToolbar([bool hideHandles = true]) { - if (getSelectionOverlay()?.toolbar != null) { - getSelectionOverlay()?.hideToolbar(); - } - } - - @override - bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; - - @override - bool get copyEnabled => widget.toolbarOptions.copy; - - @override - bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly; - - @override - bool get selectAllEnabled => widget.toolbarOptions.selectAll; -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart'; diff --git a/lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index 527df582..e9ea33c5 100644 --- a/lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -1,200 +1,3 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../../utils/diff_delta.dart'; -import '../editor.dart'; - -mixin RawEditorStateTextInputClientMixin on EditorState - implements TextInputClient { - final List _sentRemoteValues = []; - TextInputConnection? _textInputConnection; - TextEditingValue? _lastKnownRemoteTextEditingValue; - - /// Whether to create an input connection with the platform for text editing - /// or not. - /// - /// Read-only input fields do not need a connection with the platform since - /// there's no need for text editing capabilities (e.g. virtual keyboard). - /// - /// On the web, we always need a connection because we want some browser - /// functionalities to continue to work on read-only input fields like: - /// - /// - Relevant context menu. - /// - cmd/ctrl+c shortcut to copy. - /// - cmd/ctrl+a to select all. - /// - Changing the selection using a physical keyboard. - bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; - - /// Returns `true` if there is open input connection. - bool get hasConnection => - _textInputConnection != null && _textInputConnection!.attached; - - /// Opens or closes input connection based on the current state of - /// [focusNode] and [value]. - void openOrCloseConnection() { - if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { - openConnectionIfNeeded(); - } else if (!widget.focusNode.hasFocus) { - closeConnectionIfNeeded(); - } - } - - void openConnectionIfNeeded() { - if (!shouldCreateInputConnection) { - return; - } - - if (!hasConnection) { - _lastKnownRemoteTextEditingValue = getTextEditingValue(); - _textInputConnection = TextInput.attach( - this, - TextInputConfiguration( - inputType: TextInputType.multiline, - readOnly: widget.readOnly, - inputAction: TextInputAction.newline, - enableSuggestions: !widget.readOnly, - keyboardAppearance: widget.keyboardAppearance, - textCapitalization: widget.textCapitalization, - ), - ); - - _textInputConnection!.setEditingState(_lastKnownRemoteTextEditingValue!); - // _sentRemoteValues.add(_lastKnownRemoteTextEditingValue); - } - - _textInputConnection!.show(); - } - - /// Closes input connection if it's currently open. Otherwise does nothing. - void closeConnectionIfNeeded() { - if (!hasConnection) { - return; - } - _textInputConnection!.close(); - _textInputConnection = null; - _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); - } - - /// Updates remote value based on current state of [document] and - /// [selection]. - /// - /// This method may not actually send an update to native side if it thinks - /// remote value is up to date or identical. - void updateRemoteValueIfNeeded() { - if (!hasConnection) { - return; - } - - // Since we don't keep track of the composing range in value provided - // by the Controller we need to add it here manually before comparing - // with the last known remote value. - // It is important to prevent excessive remote updates as it can cause - // race conditions. - final actualValue = getTextEditingValue().copyWith( - composing: _lastKnownRemoteTextEditingValue!.composing, - ); - - if (actualValue == _lastKnownRemoteTextEditingValue) { - return; - } - - final shouldRemember = - getTextEditingValue().text != _lastKnownRemoteTextEditingValue!.text; - _lastKnownRemoteTextEditingValue = actualValue; - _textInputConnection!.setEditingState(actualValue); - if (shouldRemember) { - // Only keep track if text changed (selection changes are not relevant) - _sentRemoteValues.add(actualValue); - } - } - - @override - TextEditingValue? get currentTextEditingValue => - _lastKnownRemoteTextEditingValue; - - // autofill is not needed - @override - AutofillScope? get currentAutofillScope => null; - - @override - void updateEditingValue(TextEditingValue value) { - if (!shouldCreateInputConnection) { - return; - } - - if (_sentRemoteValues.contains(value)) { - /// There is a race condition in Flutter text input plugin where sending - /// updates to native side too often results in broken behavior. - /// TextInputConnection.setEditingValue is an async call to native side. - /// For each such call native side _always_ sends an update which triggers - /// this method (updateEditingValue) with the same value we've sent it. - /// If multiple calls to setEditingValue happen too fast and we only - /// track the last sent value then there is no way for us to filter out - /// automatic callbacks from native side. - /// Therefore we have to keep track of all values we send to the native - /// side and when we see this same value appear here we skip it. - /// This is fragile but it's probably the only available option. - _sentRemoteValues.remove(value); - return; - } - - if (_lastKnownRemoteTextEditingValue == value) { - // There is no difference between this value and the last known value. - return; - } - - // Check if only composing range changed. - if (_lastKnownRemoteTextEditingValue!.text == value.text && - _lastKnownRemoteTextEditingValue!.selection == value.selection) { - // This update only modifies composing range. Since we don't keep track - // of composing range we just need to update last known value here. - // This check fixes an issue on Android when it sends - // composing updates separately from regular changes for text and - // selection. - _lastKnownRemoteTextEditingValue = value; - return; - } - - final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!; - _lastKnownRemoteTextEditingValue = value; - final oldText = effectiveLastKnownValue.text; - final text = value.text; - final cursorPosition = value.selection.extentOffset; - final diff = getDiff(oldText, text, cursorPosition); - widget.controller.replaceText( - diff.start, diff.deleted.length, diff.inserted, value.selection); - } - - @override - void performAction(TextInputAction action) { - // no-op - } - - @override - void performPrivateCommand(String action, Map data) { - // no-op - } - - @override - void updateFloatingCursor(RawFloatingCursorPoint point) { - throw UnimplementedError(); - } - - @override - void showAutocorrectionPromptRect(int start, int end) { - throw UnimplementedError(); - } - - @override - void connectionClosed() { - if (!hasConnection) { - return; - } - _textInputConnection!.connectionClosedReceived(); - _textInputConnection = null; - _lastKnownRemoteTextEditingValue = null; - _sentRemoteValues.clear(); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart'; diff --git a/lib/widgets/responsive_widget.dart b/lib/widgets/responsive_widget.dart index 3829565c..bb46820f 100644 --- a/lib/widgets/responsive_widget.dart +++ b/lib/widgets/responsive_widget.dart @@ -1,43 +1,3 @@ -import 'package:flutter/material.dart'; - -class ResponsiveWidget extends StatelessWidget { - const ResponsiveWidget({ - required this.largeScreen, - this.mediumScreen, - this.smallScreen, - 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; - } - - static bool isLargeScreen(BuildContext context) { - return MediaQuery.of(context).size.width > 1200; - } - - static bool isMediumScreen(BuildContext context) { - return MediaQuery.of(context).size.width >= 800 && - MediaQuery.of(context).size.width <= 1200; - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth > 1200) { - return largeScreen; - } else if (constraints.maxWidth <= 1200 && - constraints.maxWidth >= 800) { - return mediumScreen ?? largeScreen; - } else { - return smallScreen ?? largeScreen; - } - }, - ); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/responsive_widget.dart'; diff --git a/lib/widgets/simple_viewer.dart b/lib/widgets/simple_viewer.dart index ee1a7732..219babec 100644 --- a/lib/widgets/simple_viewer.dart +++ b/lib/widgets/simple_viewer.dart @@ -1,344 +1,3 @@ -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:tuple/tuple.dart'; - -import '../models/documents/attribute.dart'; -import '../models/documents/document.dart'; -import '../models/documents/nodes/block.dart'; -import '../models/documents/nodes/leaf.dart' as leaf; -import '../models/documents/nodes/line.dart'; -import 'controller.dart'; -import 'cursor.dart'; -import 'default_styles.dart'; -import 'delegate.dart'; -import 'editor.dart'; -import 'text_block.dart'; -import 'text_line.dart'; - -class QuillSimpleViewer extends StatefulWidget { - const QuillSimpleViewer({ - required this.controller, - this.customStyles, - this.truncate = false, - this.truncateScale, - this.truncateAlignment, - this.truncateHeight, - this.truncateWidth, - this.scrollBottomInset = 0, - this.padding = EdgeInsets.zero, - this.embedBuilder, - Key? key, - }) : assert(truncate || - ((truncateScale == null) && - (truncateAlignment == null) && - (truncateHeight == null) && - (truncateWidth == null))), - super(key: key); - - final QuillController controller; - final DefaultStyles? customStyles; - final bool truncate; - final double? truncateScale; - final Alignment? truncateAlignment; - final double? truncateHeight; - final double? truncateWidth; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - final EmbedBuilder? embedBuilder; - - @override - _QuillSimpleViewerState createState() => _QuillSimpleViewerState(); -} - -class _QuillSimpleViewerState extends State - with SingleTickerProviderStateMixin { - late DefaultStyles _styles; - final LayerLink _toolbarLayerLink = LayerLink(); - final LayerLink _startHandleLayerLink = LayerLink(); - final LayerLink _endHandleLayerLink = LayerLink(); - late CursorCont _cursorCont; - - @override - void initState() { - super.initState(); - - _cursorCont = CursorCont( - show: ValueNotifier(false), - style: const CursorStyle( - color: Colors.black, - backgroundColor: Colors.grey, - width: 2, - radius: Radius.zero, - offset: Offset.zero, - ), - tickerProvider: this, - ); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final parentStyles = QuillStyles.getStyles(context, true); - final defaultStyles = DefaultStyles.getInstance(context); - _styles = (parentStyles != null) - ? defaultStyles.merge(parentStyles) - : defaultStyles; - - if (widget.customStyles != null) { - _styles = _styles.merge(widget.customStyles!); - } - } - - EmbedBuilder get embedBuilder => widget.embedBuilder ?? _defaultEmbedBuilder; - - Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { - assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); - switch (node.value.type) { - case 'image': - final imageUrl = _standardizeImageUrl(node.value.data); - return imageUrl.startsWith('http') - ? Image.network(imageUrl) - : isBase64(imageUrl) - ? Image.memory(base64.decode(imageUrl)) - : Image.file(io.File(imageUrl)); - default: - throw UnimplementedError( - 'Embeddable type "${node.value.type}" is not supported by default embed ' - 'builder of QuillEditor. You must pass your own builder function to ' - 'embedBuilder property of QuillEditor or QuillField widgets.'); - } - } - - String _standardizeImageUrl(String url) { - if (url.contains('base64')) { - return url.split(',')[1]; - } - return url; - } - - @override - Widget build(BuildContext context) { - final _doc = widget.controller.document; - // if (_doc.isEmpty() && - // !widget.focusNode.hasFocus && - // widget.placeholder != null) { - // _doc = Document.fromJson(jsonDecode( - // '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); - // } - - Widget child = CompositedTransformTarget( - link: _toolbarLayerLink, - child: Semantics( - child: _SimpleViewer( - document: _doc, - textDirection: _textDirection, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - onSelectionChanged: _nullSelectionChanged, - scrollBottomInset: widget.scrollBottomInset, - padding: widget.padding, - children: _buildChildren(_doc, context), - ), - ), - ); - - if (widget.truncate) { - if (widget.truncateScale != null) { - child = Container( - height: widget.truncateHeight, - child: Align( - heightFactor: widget.truncateScale, - widthFactor: widget.truncateScale, - alignment: widget.truncateAlignment ?? Alignment.topLeft, - child: Container( - width: widget.truncateWidth! / widget.truncateScale!, - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Transform.scale( - scale: widget.truncateScale!, - alignment: - widget.truncateAlignment ?? Alignment.topLeft, - child: child))))); - } else { - child = Container( - height: widget.truncateHeight, - width: widget.truncateWidth, - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), child: child)); - } - } - - return QuillStyles(data: _styles, child: child); - } - - List _buildChildren(Document doc, BuildContext context) { - final result = []; - final indentLevelCounts = {}; - for (final node in doc.root.children) { - if (node is Line) { - final editableTextLine = _getEditableTextLineFromNode(node, context); - result.add(editableTextLine); - } else if (node is Block) { - final attrs = node.style.attributes; - final editableTextBlock = EditableTextBlock( - node, - _textDirection, - widget.scrollBottomInset, - _getVerticalSpacingForBlock(node, _styles), - widget.controller.selection, - Colors.black, - // selectionColor, - _styles, - false, - // enableInteractiveSelection, - false, - // hasFocus, - attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - embedBuilder, - _cursorCont, - indentLevelCounts, - _handleCheckboxTap); - result.add(editableTextBlock); - } else { - throw StateError('Unreachable.'); - } - } - return result; - } - - /// Updates the checkbox positioned at [offset] in document - /// by changing its attribute according to [value]. - void _handleCheckboxTap(int offset, bool value) { - // readonly - do nothing - } - - TextDirection get _textDirection { - final result = Directionality.of(context); - return result; - } - - EditableTextLine _getEditableTextLineFromNode( - Line node, BuildContext context) { - final textLine = TextLine( - line: node, - textDirection: _textDirection, - embedBuilder: embedBuilder, - styles: _styles, - ); - final editableTextLine = EditableTextLine( - node, - null, - textLine, - 0, - _getVerticalSpacingForLine(node, _styles), - _textDirection, - widget.controller.selection, - Colors.black, - //widget.selectionColor, - false, - //enableInteractiveSelection, - false, - //_hasFocus, - MediaQuery.of(context).devicePixelRatio, - _cursorCont); - return editableTextLine; - } - - Tuple2 _getVerticalSpacingForLine( - Line line, DefaultStyles? defaultStyles) { - final attrs = line.style.attributes; - if (attrs.containsKey(Attribute.header.key)) { - final int? level = attrs[Attribute.header.key]!.value; - switch (level) { - case 1: - return defaultStyles!.h1!.verticalSpacing; - case 2: - return defaultStyles!.h2!.verticalSpacing; - case 3: - return defaultStyles!.h3!.verticalSpacing; - default: - throw 'Invalid level $level'; - } - } - - return defaultStyles!.paragraph!.verticalSpacing; - } - - Tuple2 _getVerticalSpacingForBlock( - Block node, DefaultStyles? defaultStyles) { - final attrs = node.style.attributes; - if (attrs.containsKey(Attribute.blockQuote.key)) { - return defaultStyles!.quote!.verticalSpacing; - } else if (attrs.containsKey(Attribute.codeBlock.key)) { - return defaultStyles!.code!.verticalSpacing; - } else if (attrs.containsKey(Attribute.indent.key)) { - return defaultStyles!.indent!.verticalSpacing; - } - return defaultStyles!.lists!.verticalSpacing; - } - - void _nullSelectionChanged( - TextSelection selection, SelectionChangedCause cause) {} -} - -class _SimpleViewer extends MultiChildRenderObjectWidget { - _SimpleViewer({ - required List children, - required this.document, - required this.textDirection, - required this.startHandleLayerLink, - required this.endHandleLayerLink, - required this.onSelectionChanged, - required this.scrollBottomInset, - this.padding = EdgeInsets.zero, - Key? key, - }) : super(key: key, children: children); - - final Document document; - final TextDirection textDirection; - final LayerLink startHandleLayerLink; - final LayerLink endHandleLayerLink; - final TextSelectionChangedHandler onSelectionChanged; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - - @override - RenderEditor createRenderObject(BuildContext context) { - return RenderEditor( - null, - textDirection, - scrollBottomInset, - padding, - document, - const TextSelection(baseOffset: 0, extentOffset: 0), - false, - // hasFocus, - onSelectionChanged, - startHandleLayerLink, - endHandleLayerLink, - const EdgeInsets.fromLTRB(4, 4, 4, 5), - ); - } - - @override - void updateRenderObject( - BuildContext context, covariant RenderEditor renderObject) { - renderObject - ..document = document - ..setContainer(document.root) - ..textDirection = textDirection - ..setStartHandleLayerLink(startHandleLayerLink) - ..setEndHandleLayerLink(endHandleLayerLink) - ..onSelectionChanged = onSelectionChanged - ..setScrollBottomInset(scrollBottomInset) - ..setPadding(padding); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/simple_viewer.dart'; diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index f533a160..3e46ea80 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -1,737 +1,3 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:tuple/tuple.dart'; - -import '../models/documents/attribute.dart'; -import '../models/documents/nodes/block.dart'; -import '../models/documents/nodes/line.dart'; -import '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, - 900, - 500, - 400, - 100, - 90, - 50, - 40, - 10, - 9, - 5, - 4, - 1 -]; - -const List romanNumbers = [ - 'M', - 'CM', - 'D', - 'CD', - 'C', - 'XC', - 'L', - 'XL', - 'X', - 'IX', - 'V', - 'IV', - 'I' -]; - -class EditableTextBlock extends StatelessWidget { - const EditableTextBlock( - this.block, - this.textDirection, - this.scrollBottomInset, - this.verticalSpacing, - this.textSelection, - this.color, - this.styles, - this.enableInteractiveSelection, - this.hasFocus, - this.contentPadding, - this.embedBuilder, - this.cursorCont, - this.indentLevelCounts, - this.onCheckboxTap, - ); - - final Block block; - final TextDirection textDirection; - final double scrollBottomInset; - final Tuple2 verticalSpacing; - final TextSelection textSelection; - final Color color; - final DefaultStyles? styles; - final bool enableInteractiveSelection; - final bool hasFocus; - final EdgeInsets? contentPadding; - final EmbedBuilder embedBuilder; - final CursorCont cursorCont; - final Map indentLevelCounts; - final Function(int, bool) onCheckboxTap; - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMediaQuery(context)); - - final defaultStyles = QuillStyles.getStyles(context, false); - return _EditableBlock( - block, - textDirection, - verticalSpacing as Tuple2, - scrollBottomInset, - _getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(), - contentPadding, - _buildChildren(context, indentLevelCounts)); - } - - BoxDecoration? _getDecorationForBlock( - Block node, DefaultStyles? defaultStyles) { - final attrs = block.style.attributes; - if (attrs.containsKey(Attribute.blockQuote.key)) { - return defaultStyles!.quote!.decoration; - } - if (attrs.containsKey(Attribute.codeBlock.key)) { - return defaultStyles!.code!.decoration; - } - return null; - } - - List _buildChildren( - BuildContext context, Map indentLevelCounts) { - 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++; - final editableTextLine = EditableTextLine( - line, - _buildLeading(context, line, index, indentLevelCounts, count), - TextLine( - line: line, - textDirection: textDirection, - embedBuilder: embedBuilder, - styles: styles!, - ), - _getIndentWidth(), - _getSpacingForLine(line, index, count, defaultStyles), - textDirection, - textSelection, - color, - enableInteractiveSelection, - hasFocus, - MediaQuery.of(context).devicePixelRatio, - cursorCont); - children.add(editableTextLine); - } - return children.toList(growable: false); - } - - Widget? _buildLeading(BuildContext context, Line line, int index, - Map indentLevelCounts, int count) { - final defaultStyles = QuillStyles.getStyles(context, false); - final attrs = line.style.attributes; - if (attrs[Attribute.list.key] == Attribute.ol) { - return _NumberPoint( - index: index, - indentLevelCounts: indentLevelCounts, - count: count, - style: defaultStyles!.leading!.style, - attrs: attrs, - width: 32, - padding: 8, - ); - } - - if (attrs[Attribute.list.key] == Attribute.ul) { - return _BulletPoint( - style: - defaultStyles!.leading!.style.copyWith(fontWeight: FontWeight.bold), - width: 32, - ); - } - - if (attrs[Attribute.list.key] == Attribute.checked) { - return _Checkbox( - key: UniqueKey(), - style: defaultStyles!.leading!.style, - width: 32, - isChecked: true, - offset: block.offset + line.offset, - onTap: onCheckboxTap, - ); - } - - if (attrs[Attribute.list.key] == Attribute.unchecked) { - return _Checkbox( - key: UniqueKey(), - style: defaultStyles!.leading!.style, - width: 32, - offset: block.offset + line.offset, - onTap: onCheckboxTap, - ); - } - - if (attrs.containsKey(Attribute.codeBlock.key)) { - return _NumberPoint( - index: index, - indentLevelCounts: indentLevelCounts, - count: count, - style: defaultStyles!.code!.style - .copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)), - width: 32, - attrs: attrs, - padding: 16, - withDot: false, - ); - } - return null; - } - - double _getIndentWidth() { - final attrs = block.style.attributes; - - final indent = attrs[Attribute.indent.key]; - var extraIndent = 0.0; - if (indent != null && indent.value != null) { - extraIndent = 16.0 * indent.value; - } - - if (attrs.containsKey(Attribute.blockQuote.key)) { - return 16.0 + extraIndent; - } - - return 32.0 + extraIndent; - } - - Tuple2 _getSpacingForLine( - Line node, int index, int count, DefaultStyles? defaultStyles) { - var top = 0.0, bottom = 0.0; - - final attrs = block.style.attributes; - if (attrs.containsKey(Attribute.header.key)) { - final level = attrs[Attribute.header.key]!.value; - switch (level) { - case 1: - top = defaultStyles!.h1!.verticalSpacing.item1; - bottom = defaultStyles.h1!.verticalSpacing.item2; - break; - case 2: - top = defaultStyles!.h2!.verticalSpacing.item1; - bottom = defaultStyles.h2!.verticalSpacing.item2; - break; - case 3: - top = defaultStyles!.h3!.verticalSpacing.item1; - bottom = defaultStyles.h3!.verticalSpacing.item2; - break; - default: - throw 'Invalid level $level'; - } - } else { - late Tuple2 lineSpacing; - if (attrs.containsKey(Attribute.blockQuote.key)) { - lineSpacing = defaultStyles!.quote!.lineSpacing; - } else if (attrs.containsKey(Attribute.indent.key)) { - lineSpacing = defaultStyles!.indent!.lineSpacing; - } else if (attrs.containsKey(Attribute.list.key)) { - lineSpacing = defaultStyles!.lists!.lineSpacing; - } else if (attrs.containsKey(Attribute.codeBlock.key)) { - lineSpacing = defaultStyles!.code!.lineSpacing; - } else if (attrs.containsKey(Attribute.align.key)) { - lineSpacing = defaultStyles!.align!.lineSpacing; - } - top = lineSpacing.item1; - bottom = lineSpacing.item2; - } - - if (index == 1) { - top = 0.0; - } - - if (index == count) { - bottom = 0.0; - } - - return Tuple2(top, bottom); - } -} - -class RenderEditableTextBlock extends RenderEditableContainerBox - implements RenderEditableBox { - RenderEditableTextBlock({ - required Block block, - required TextDirection textDirection, - required EdgeInsetsGeometry padding, - required double scrollBottomInset, - required Decoration decoration, - List? children, - ImageConfiguration configuration = ImageConfiguration.empty, - EdgeInsets contentPadding = EdgeInsets.zero, - }) : _decoration = decoration, - _configuration = configuration, - _savedPadding = padding, - _contentPadding = contentPadding, - super( - children, - block, - textDirection, - scrollBottomInset, - padding.add(contentPadding), - ); - - EdgeInsetsGeometry _savedPadding; - EdgeInsets _contentPadding; - - set contentPadding(EdgeInsets value) { - if (_contentPadding == value) return; - _contentPadding = value; - super.setPadding(_savedPadding.add(_contentPadding)); - } - - @override - void setPadding(EdgeInsetsGeometry value) { - super.setPadding(value.add(_contentPadding)); - _savedPadding = value; - } - - BoxPainter? _painter; - - Decoration get decoration => _decoration; - Decoration _decoration; - - set decoration(Decoration value) { - if (value == _decoration) return; - _painter?.dispose(); - _painter = null; - _decoration = value; - markNeedsPaint(); - } - - ImageConfiguration get configuration => _configuration; - ImageConfiguration _configuration; - - set configuration(ImageConfiguration value) { - if (value == _configuration) return; - _configuration = value; - markNeedsPaint(); - } - - @override - TextRange getLineBoundary(TextPosition position) { - 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().offset, - end: rangeInChild.end + child.getContainer().offset, - ); - } - - @override - Offset getOffsetForCaret(TextPosition position) { - final child = childAtPosition(position); - return child.getOffsetForCaret(TextPosition( - offset: position.offset - child.getContainer().offset, - affinity: position.affinity, - )) + - (child.parentData as BoxParentData).offset; - } - - @override - TextPosition getPositionForOffset(Offset offset) { - final child = childAtOffset(offset)!; - final parentData = child.parentData as BoxParentData; - final localPosition = - child.getPositionForOffset(offset - parentData.offset); - return TextPosition( - offset: localPosition.offset + child.getContainer().offset, - affinity: localPosition.affinity, - ); - } - - @override - TextRange getWordBoundary(TextPosition position) { - final child = childAtPosition(position); - final nodeOffset = child.getContainer().offset; - final childWord = child - .getWordBoundary(TextPosition(offset: position.offset - nodeOffset)); - return TextRange( - start: childWord.start + nodeOffset, - end: childWord.end + nodeOffset, - ); - } - - @override - TextPosition? getPositionAbove(TextPosition position) { - assert(position.offset < getContainer().length); - - 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().offset); - } - - final sibling = childBefore(child); - if (sibling == null) { - return null; - } - - final caretOffset = child.getOffsetForCaret(childLocalPosition); - final testPosition = - TextPosition(offset: sibling.getContainer().length - 1); - final testOffset = sibling.getOffsetForCaret(testPosition); - final finalOffset = Offset(caretOffset.dx, testOffset.dy); - return TextPosition( - offset: sibling.getContainer().offset + - sibling.getPositionForOffset(finalOffset).offset); - } - - @override - TextPosition? getPositionBelow(TextPosition position) { - assert(position.offset < getContainer().length); - - 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().offset); - } - - final sibling = childAfter(child); - if (sibling == null) { - return null; - } - - 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().offset + - sibling.getPositionForOffset(finalOffset).offset); - } - - @override - double preferredLineHeight(TextPosition position) { - 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, preferredLineHeight(selection.extent)) + - getOffsetForCaret(selection.extent), - null); - } - - final baseNode = getContainer().queryChild(selection.start, false).node; - var baseChild = firstChild; - while (baseChild != null) { - if (baseChild.getContainer() == baseNode) { - break; - } - baseChild = childAfter(baseChild); - } - assert(baseChild != null); - - final basePoint = baseChild!.getBaseEndpointForSelection( - localSelection(baseChild.getContainer(), selection, true)); - return TextSelectionPoint( - basePoint.point + (baseChild.parentData as BoxParentData).offset, - basePoint.direction); - } - - @override - TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) { - if (selection.isCollapsed) { - return TextSelectionPoint( - Offset(0, preferredLineHeight(selection.extent)) + - getOffsetForCaret(selection.extent), - null); - } - - final extentNode = getContainer().queryChild(selection.end, false).node; - - var extentChild = firstChild; - while (extentChild != null) { - if (extentChild.getContainer() == extentNode) { - break; - } - extentChild = childAfter(extentChild); - } - assert(extentChild != null); - - final extentPoint = extentChild!.getExtentEndpointForSelection( - localSelection(extentChild.getContainer(), selection, true)); - return TextSelectionPoint( - extentPoint.point + (extentChild.parentData as BoxParentData).offset, - extentPoint.direction); - } - - @override - void detach() { - _painter?.dispose(); - _painter = null; - super.detach(); - markNeedsPaint(); - } - - @override - void paint(PaintingContext context, Offset offset) { - _paintDecoration(context, offset); - defaultPaint(context, offset); - } - - void _paintDecoration(PaintingContext context, Offset offset) { - _painter ??= _decoration.createBoxPainter(markNeedsPaint); - - final decorationPadding = resolvedPadding! - _contentPadding; - - final filledConfiguration = - configuration.copyWith(size: decorationPadding.deflateSize(size)); - final debugSaveCount = context.canvas.getSaveCount(); - - final decorationOffset = - offset.translate(decorationPadding.left, decorationPadding.top); - _painter!.paint(context.canvas, decorationOffset, filledConfiguration); - if (debugSaveCount != context.canvas.getSaveCount()) { - throw '${_decoration.runtimeType} painter had mismatching save and restore calls.'; - } - if (decoration.isComplex) { - context.setIsComplexHint(); - } - } - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - return defaultHitTestChildren(result, position: position); - } -} - -class _EditableBlock extends MultiChildRenderObjectWidget { - _EditableBlock( - this.block, - this.textDirection, - this.padding, - this.scrollBottomInset, - this.decoration, - this.contentPadding, - List children) - : super(children: children); - - final Block block; - final TextDirection textDirection; - final Tuple2 padding; - final double scrollBottomInset; - final Decoration decoration; - final EdgeInsets? contentPadding; - - EdgeInsets get _padding => - EdgeInsets.only(top: padding.item1, bottom: padding.item2); - - EdgeInsets get _contentPadding => contentPadding ?? EdgeInsets.zero; - - @override - RenderEditableTextBlock createRenderObject(BuildContext context) { - return RenderEditableTextBlock( - block: block, - textDirection: textDirection, - padding: _padding, - scrollBottomInset: scrollBottomInset, - decoration: decoration, - contentPadding: _contentPadding, - ); - } - - @override - void updateRenderObject( - BuildContext context, covariant RenderEditableTextBlock renderObject) { - renderObject - ..setContainer(block) - ..textDirection = textDirection - ..scrollBottomInset = scrollBottomInset - ..setPadding(_padding) - ..decoration = decoration - ..contentPadding = _contentPadding; - } -} - -class _NumberPoint extends StatelessWidget { - const _NumberPoint({ - required this.index, - required this.indentLevelCounts, - required this.count, - required this.style, - required this.width, - required this.attrs, - this.withDot = true, - this.padding = 0.0, - Key? key, - }) : super(key: key); - - final int index; - final Map indentLevelCounts; - final int count; - final TextStyle style; - final double width; - final Map attrs; - final bool withDot; - final double padding; - - @override - Widget build(BuildContext context) { - var s = index.toString(); - int? level = 0; - if (!attrs.containsKey(Attribute.indent.key) && - !indentLevelCounts.containsKey(1)) { - indentLevelCounts.clear(); - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: EdgeInsetsDirectional.only(end: padding), - child: Text(withDot ? '$s.' : s, style: style), - ); - } - if (attrs.containsKey(Attribute.indent.key)) { - level = attrs[Attribute.indent.key]!.value; - } else { - // first level but is back from previous indent level - // supposed to be "2." - indentLevelCounts[0] = 1; - } - if (indentLevelCounts.containsKey(level! + 1)) { - // last visited level is done, going up - indentLevelCounts.remove(level + 1); - } - final count = (indentLevelCounts[level] ?? 0) + 1; - indentLevelCounts[level] = count; - - s = count.toString(); - if (level % 3 == 1) { - // a. b. c. d. e. ... - s = _toExcelSheetColumnTitle(count); - } else if (level % 3 == 2) { - // i. ii. iii. ... - s = _intToRoman(count); - } - // level % 3 == 0 goes back to 1. 2. 3. - - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: EdgeInsetsDirectional.only(end: padding), - child: Text(withDot ? '$s.' : s, style: style), - ); - } - - String _toExcelSheetColumnTitle(int n) { - final result = StringBuffer(); - while (n > 0) { - n--; - result.write(String.fromCharCode((n % 26).floor() + 97)); - n = (n / 26).floor(); - } - - return result.toString().split('').reversed.join(); - } - - String _intToRoman(int input) { - var num = input; - - if (num < 0) { - return ''; - } else if (num == 0) { - return 'nulla'; - } - - final builder = StringBuffer(); - for (var a = 0; a < arabianRomanNumbers.length; a++) { - final times = (num / arabianRomanNumbers[a]) - .truncate(); // equals 1 only when arabianRomanNumbers[a] = num - // executes n times where n is the number of times you have to add - // the current roman number value to reach current num. - builder.write(romanNumbers[a] * times); - num -= times * - arabianRomanNumbers[ - a]; // subtract previous roman number value from num - } - - return builder.toString().toLowerCase(); - } -} - -class _BulletPoint extends StatelessWidget { - const _BulletPoint({ - required this.style, - required this.width, - Key? key, - }) : super(key: key); - - final TextStyle style; - final double width; - - @override - Widget build(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: const EdgeInsetsDirectional.only(end: 13), - child: Text('•', style: style), - ); - } -} - -class _Checkbox extends StatelessWidget { - const _Checkbox({ - Key? key, - this.style, - this.width, - this.isChecked = false, - this.offset, - this.onTap, - }) : super(key: key); - final TextStyle? style; - final double? width; - final bool isChecked; - final int? offset; - final Function(int, bool)? onTap; - - void _onCheckboxClicked(bool? newValue) { - if (onTap != null && newValue != null && offset != null) { - onTap!(offset!, newValue); - } - } - - @override - Widget build(BuildContext context) { - return Container( - alignment: AlignmentDirectional.topEnd, - width: width, - padding: const EdgeInsetsDirectional.only(end: 13), - child: GestureDetector( - onLongPress: () => _onCheckboxClicked(!isChecked), - child: Checkbox( - value: isChecked, - onChanged: _onCheckboxClicked, - ), - ), - ); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/text_block.dart'; diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index ef80a024..7c8dcc80 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -1,892 +1,3 @@ -import 'dart:math' as math; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.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 { - const TextLine({ - required this.line, - required this.embedBuilder, - required this.styles, - this.textDirection, - 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) { - final embed = line.children.single as Embed; - return EmbedProxy(embedBuilder(context, embed)); - } - - final textSpan = _buildTextSpan(context); - final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); - final textAlign = _getTextAlign(); - final child = RichText( - text: textSpan, - textAlign: textAlign, - textDirection: textDirection, - strutStyle: strutStyle, - textScaleFactor: MediaQuery.textScaleFactorOf(context), - ); - return RichTextProxy( - child, - textSpan.style!, - textAlign, - textDirection!, - 1, - Localizations.localeOf(context), - strutStyle, - TextWidthBasis.parent, - null); - } - - TextAlign _getTextAlign() { - final alignment = line.style.attributes[Attribute.align.key]; - if (alignment == Attribute.leftAlignment) { - return TextAlign.left; - } else if (alignment == Attribute.centerAlignment) { - return TextAlign.center; - } else if (alignment == Attribute.rightAlignment) { - return TextAlign.right; - } else if (alignment == Attribute.justifyAlignment) { - return TextAlign.justify; - } - return TextAlign.start; - } - - TextSpan _buildTextSpan(BuildContext context) { - final defaultStyles = styles; - final children = line.children - .map((node) => _getTextSpanFromNode(defaultStyles, node)) - .toList(growable: false); - - var textStyle = const TextStyle(); - - if (line.style.containsKey(Attribute.placeholder.key)) { - textStyle = defaultStyles.placeHolder!.style; - return TextSpan(children: children, style: textStyle); - } - - 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, - }; - - textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style); - - final block = line.style.getBlockExceptHeader(); - TextStyle? toMerge; - if (block == Attribute.blockQuote) { - toMerge = defaultStyles.quote!.style; - } else if (block == Attribute.codeBlock) { - toMerge = defaultStyles.code!.style; - } else if (block != null) { - toMerge = defaultStyles.lists!.style; - } - - textStyle = textStyle.merge(toMerge); - - return TextSpan(children: children, style: textStyle); - } - - TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { - final textNode = node as leaf.Text; - final style = textNode.style; - var res = const TextStyle(); - final color = textNode.style.attributes[Attribute.color.key]; - - { - 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, - }.forEach((k, s) { - if (style.values.any((v) => v.key == k)) { - if (k == Attribute.underline.key || k == Attribute.strikeThrough.key) { - var textColor = defaultStyles.color; - if (color?.value is String) { - textColor = stringToColor(color?.value); - } - res = _merge(res.copyWith(decorationColor: textColor), - s!.copyWith(decorationColor: textColor)); - } else { - res = _merge(res, s!); - } - } - }); - - final font = textNode.style.attributes[Attribute.font.key]; - if (font != null && font.value != null) { - res = res.merge(TextStyle(fontFamily: font.value)); - } - - final size = textNode.style.attributes[Attribute.size.key]; - if (size != null && size.value != null) { - switch (size.value) { - case 'small': - res = res.merge(defaultStyles.sizeSmall); - break; - case 'large': - res = res.merge(defaultStyles.sizeLarge); - break; - case 'huge': - res = res.merge(defaultStyles.sizeHuge); - break; - default: - final fontSize = double.tryParse(size.value); - if (fontSize != null) { - res = res.merge(TextStyle(fontSize: fontSize)); - } else { - throw 'Invalid size ${size.value}'; - } - } - } - - if (color != null && color.value != null) { - var textColor = defaultStyles.color; - if (color.value is String) { - textColor = stringToColor(color.value); - } - if (textColor != null) { - res = res.merge(TextStyle(color: textColor)); - } - } - - 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)); - } - - return TextSpan(text: textNode.value, style: res); - } - - TextStyle _merge(TextStyle a, TextStyle b) { - final decorations = []; - if (a.decoration != null) { - decorations.add(a.decoration); - } - if (b.decoration != null) { - decorations.add(b.decoration); - } - return a.merge(b).apply( - decoration: TextDecoration.combine( - List.castFrom(decorations))); - } -} - -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; - final double indentWidth; - final Tuple2 verticalSpacing; - final TextDirection textDirection; - final TextSelection textSelection; - final Color color; - final bool enableInteractiveSelection; - final bool hasFocus; - final double devicePixelRatio; - final CursorCont cursorCont; - - @override - RenderObjectElement createElement() { - return _TextLineElement(this); - } - - @override - RenderObject createRenderObject(BuildContext context) { - return RenderEditableTextLine( - line, - textDirection, - textSelection, - enableInteractiveSelection, - hasFocus, - devicePixelRatio, - _getPadding(), - color, - cursorCont); - } - - @override - void updateRenderObject( - BuildContext context, covariant RenderEditableTextLine renderObject) { - renderObject - ..setLine(line) - ..setPadding(_getPadding()) - ..setTextDirection(textDirection) - ..setTextSelection(textSelection) - ..setColor(color) - ..setEnableInteractiveSelection(enableInteractiveSelection) - ..hasFocus = hasFocus - ..setDevicePixelRatio(devicePixelRatio) - ..setCursorCont(cursorCont); - } - - EdgeInsetsGeometry _getPadding() { - return EdgeInsetsDirectional.only( - start: indentWidth, - top: verticalSpacing.item1, - bottom: verticalSpacing.item2); - } -} - -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; - TextDirection textDirection; - TextSelection textSelection; - Color color; - bool enableInteractiveSelection; - bool hasFocus = false; - double devicePixelRatio; - EdgeInsetsGeometry padding; - CursorCont cursorCont; - EdgeInsets? _resolvedPadding; - bool? _containsCursor; - List? _selectedRects; - Rect? _caretPrototype; - final Map children = {}; - - Iterable get _children sync* { - if (_leading != null) { - yield _leading!; - } - if (_body != null) { - yield _body!; - } - } - - void setCursorCont(CursorCont c) { - if (cursorCont == c) { - return; - } - cursorCont = c; - markNeedsLayout(); - } - - void setDevicePixelRatio(double d) { - if (devicePixelRatio == d) { - return; - } - devicePixelRatio = d; - markNeedsLayout(); - } - - void setEnableInteractiveSelection(bool val) { - if (enableInteractiveSelection == val) { - return; - } - - markNeedsLayout(); - markNeedsSemanticsUpdate(); - } - - void setColor(Color c) { - if (color == c) { - return; - } - - color = c; - if (containsTextSelection()) { - markNeedsPaint(); - } - } - - void setTextSelection(TextSelection t) { - if (textSelection == t) { - return; - } - - final containsSelection = containsTextSelection(); - if (attached && containsCursor()) { - cursorCont.removeListener(markNeedsLayout); - cursorCont.color.removeListener(markNeedsPaint); - } - - textSelection = t; - _selectedRects = null; - _containsCursor = null; - if (attached && containsCursor()) { - cursorCont.addListener(markNeedsLayout); - cursorCont.color.addListener(markNeedsPaint); - } - - if (containsSelection || containsTextSelection()) { - markNeedsPaint(); - } - } - - void setTextDirection(TextDirection t) { - if (textDirection == t) { - return; - } - textDirection = t; - _resolvedPadding = null; - markNeedsLayout(); - } - - void setLine(Line l) { - if (line == l) { - return; - } - line = l; - _containsCursor = null; - markNeedsLayout(); - } - - void setPadding(EdgeInsetsGeometry p) { - assert(p.isNonNegative); - if (padding == p) { - return; - } - padding = p; - _resolvedPadding = null; - markNeedsLayout(); - } - - void setLeading(RenderBox? l) { - _leading = _updateChild(_leading, l, TextLineSlot.LEADING); - } - - void setBody(RenderContentProxyBox? b) { - _body = _updateChild(_body, b, TextLineSlot.BODY) as RenderContentProxyBox?; - } - - bool containsTextSelection() { - return line.documentOffset <= textSelection.end && - textSelection.start <= line.documentOffset + line.length - 1; - } - - bool containsCursor() { - return _containsCursor ??= textSelection.isCollapsed && - line.containsOffset(textSelection.baseOffset); - } - - RenderBox? _updateChild( - RenderBox? old, RenderBox? newChild, TextLineSlot slot) { - if (old != null) { - dropChild(old); - children.remove(slot); - } - if (newChild != null) { - children[slot] = newChild; - adoptChild(newChild); - } - return newChild; - } - - List _getBoxes(TextSelection textSelection) { - final parentData = _body!.parentData as BoxParentData?; - return _body!.getBoxesForSelection(textSelection).map((box) { - return TextBox.fromLTRBD( - box.left + parentData!.offset.dx, - box.top + parentData.offset.dy, - box.right + parentData.offset.dx, - box.bottom + parentData.offset.dy, - box.direction, - ); - }).toList(growable: false); - } - - void _resolvePadding() { - if (_resolvedPadding != null) { - return; - } - _resolvedPadding = padding.resolve(textDirection); - assert(_resolvedPadding!.isNonNegative); - } - - @override - TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection) { - return _getEndpointForSelection(textSelection, true); - } - - @override - TextSelectionPoint getExtentEndpointForSelection( - TextSelection textSelection) { - return _getEndpointForSelection(textSelection, false); - } - - TextSelectionPoint _getEndpointForSelection( - TextSelection textSelection, bool first) { - if (textSelection.isCollapsed) { - return TextSelectionPoint( - Offset(0, preferredLineHeight(textSelection.extent)) + - getOffsetForCaret(textSelection.extent), - null); - } - final boxes = _getBoxes(textSelection); - assert(boxes.isNotEmpty); - final targetBox = first ? boxes.first : boxes.last; - return TextSelectionPoint( - Offset(first ? targetBox.start : targetBox.end, targetBox.bottom), - targetBox.direction); - } - - @override - TextRange getLineBoundary(TextPosition position) { - final lineDy = getOffsetForCaret(position) - .translate(0, 0.5 * preferredLineHeight(position)) - .dy; - final lineBoxes = - _getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1)) - .where((element) => element.top < lineDy && element.bottom > lineDy) - .toList(growable: false); - return TextRange( - start: - getPositionForOffset(Offset(lineBoxes.first.left, lineDy)).offset, - end: getPositionForOffset(Offset(lineBoxes.last.right, lineDy)).offset); - } - - @override - Offset getOffsetForCaret(TextPosition position) { - return _body!.getOffsetForCaret(position, _caretPrototype) + - (_body!.parentData as BoxParentData).offset; - } - - @override - TextPosition? getPositionAbove(TextPosition position) { - return _getPosition(position, -0.5); - } - - @override - TextPosition? getPositionBelow(TextPosition position) { - return _getPosition(position, 1.5); - } - - TextPosition? _getPosition(TextPosition textPosition, double dyScale) { - assert(textPosition.offset < line.length); - final offset = getOffsetForCaret(textPosition) - .translate(0, dyScale * preferredLineHeight(textPosition)); - if (_body!.size - .contains(offset - (_body!.parentData as BoxParentData).offset)) { - return getPositionForOffset(offset); - } - return null; - } - - @override - TextPosition getPositionForOffset(Offset offset) { - return _body!.getPositionForOffset( - offset - (_body!.parentData as BoxParentData).offset); - } - - @override - TextRange getWordBoundary(TextPosition position) { - return _body!.getWordBoundary(position); - } - - @override - double preferredLineHeight(TextPosition position) { - return _body!.getPreferredLineHeight(); - } - - @override - container.Container getContainer() { - return line; - } - - double get cursorWidth => cursorCont.style.width; - - double get cursorHeight => - cursorCont.style.height ?? - preferredLineHeight(const TextPosition(offset: 0)); - - void _computeCaretPrototype() { - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - _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, 2, cursorWidth, cursorHeight - 4.0); - break; - default: - throw 'Invalid platform'; - } - } - - @override - void attach(covariant PipelineOwner owner) { - super.attach(owner); - for (final child in _children) { - child.attach(owner); - } - if (containsCursor()) { - cursorCont.addListener(markNeedsLayout); - cursorCont.cursorColor.addListener(markNeedsPaint); - } - } - - @override - void detach() { - super.detach(); - for (final child in _children) { - child.detach(); - } - if (containsCursor()) { - cursorCont.removeListener(markNeedsLayout); - cursorCont.cursorColor.removeListener(markNeedsPaint); - } - } - - @override - void redepthChildren() { - _children.forEach(redepthChild); - } - - @override - void visitChildren(RenderObjectVisitor visitor) { - _children.forEach(visitor); - } - - @override - List debugDescribeChildren() { - final value = []; - void add(RenderBox? child, String name) { - if (child != null) { - value.add(child.toDiagnosticsNode(name: name)); - } - } - - add(_leading, 'leading'); - add(_body, 'body'); - return value; - } - - @override - bool get sizedByParent => false; - - @override - double computeMinIntrinsicWidth(double height) { - _resolvePadding(); - final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; - final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; - final leadingWidth = _leading == null - ? 0 - : _leading!.getMinIntrinsicWidth(height - verticalPadding) as int; - final bodyWidth = _body == null - ? 0 - : _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding)) - as int; - return horizontalPadding + leadingWidth + bodyWidth; - } - - @override - double computeMaxIntrinsicWidth(double height) { - _resolvePadding(); - final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; - final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; - final leadingWidth = _leading == null - ? 0 - : _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int; - final bodyWidth = _body == null - ? 0 - : _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding)) - as int; - return horizontalPadding + leadingWidth + bodyWidth; - } - - @override - double computeMinIntrinsicHeight(double width) { - _resolvePadding(); - final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; - final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; - if (_body != null) { - return _body! - .getMinIntrinsicHeight(math.max(0, width - horizontalPadding)) + - verticalPadding; - } - return verticalPadding; - } - - @override - double computeMaxIntrinsicHeight(double width) { - _resolvePadding(); - final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; - final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; - if (_body != null) { - return _body! - .getMaxIntrinsicHeight(math.max(0, width - horizontalPadding)) + - verticalPadding; - } - return verticalPadding; - } - - @override - double computeDistanceToActualBaseline(TextBaseline baseline) { - _resolvePadding(); - return _body!.getDistanceToActualBaseline(baseline)! + - _resolvedPadding!.top; - } - - @override - void performLayout() { - final constraints = this.constraints; - _selectedRects = null; - - _resolvePadding(); - assert(_resolvedPadding != null); - - if (_body == null && _leading == null) { - size = constraints.constrain(Size( - _resolvedPadding!.left + _resolvedPadding!.right, - _resolvedPadding!.top + _resolvedPadding!.bottom, - )); - return; - } - final innerConstraints = constraints.deflate(_resolvedPadding!); - - final indentWidth = textDirection == TextDirection.ltr - ? _resolvedPadding!.left - : _resolvedPadding!.right; - - _body!.layout(innerConstraints, parentUsesSize: true); - (_body!.parentData as BoxParentData).offset = - Offset(_resolvedPadding!.left, _resolvedPadding!.top); - - if (_leading != null) { - final leadingConstraints = innerConstraints.copyWith( - minWidth: indentWidth, - maxWidth: indentWidth, - maxHeight: _body!.size.height); - _leading!.layout(leadingConstraints, parentUsesSize: true); - (_leading!.parentData as BoxParentData).offset = - Offset(0, _resolvedPadding!.top); - } - - size = constraints.constrain(Size( - _resolvedPadding!.left + _body!.size.width + _resolvedPadding!.right, - _resolvedPadding!.top + _body!.size.height + _resolvedPadding!.bottom, - )); - - _computeCaretPrototype(); - } - - CursorPainter get _cursorPainter => CursorPainter( - _body, - cursorCont.style, - _caretPrototype, - cursorCont.cursorColor.value, - devicePixelRatio, - ); - - @override - void paint(PaintingContext context, Offset offset) { - if (_leading != null) { - final parentData = _leading!.parentData as BoxParentData; - final effectiveOffset = offset + parentData.offset; - context.paintChild(_leading!, effectiveOffset); - } - - if (_body != null) { - final parentData = _body!.parentData as BoxParentData; - final effectiveOffset = offset + parentData.offset; - if (enableInteractiveSelection && - line.documentOffset <= textSelection.end && - textSelection.start <= line.documentOffset + line.length - 1) { - final local = localSelection(line, textSelection, false); - _selectedRects ??= _body!.getBoxesForSelection( - local, - ); - _paintSelection(context, effectiveOffset); - } - - if (hasFocus && - cursorCont.show.value && - containsCursor() && - !cursorCont.style.paintAboveText) { - _paintCursor(context, effectiveOffset); - } - - context.paintChild(_body!, effectiveOffset); - - if (hasFocus && - cursorCont.show.value && - containsCursor() && - cursorCont.style.paintAboveText) { - _paintCursor(context, effectiveOffset); - } - } - } - - void _paintSelection(PaintingContext context, Offset effectiveOffset) { - assert(_selectedRects != null); - final paint = Paint()..color = color; - for (final box in _selectedRects!) { - context.canvas.drawRect(box.toRect().shift(effectiveOffset), paint); - } - } - - void _paintCursor(PaintingContext context, Offset effectiveOffset) { - final position = TextPosition( - offset: textSelection.extentOffset - line.documentOffset, - affinity: textSelection.base.affinity, - ); - _cursorPainter.paint(context.canvas, effectiveOffset, position); - } - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - return _children.first.hitTest(result, position: position); - } -} - -class _TextLineElement extends RenderObjectElement { - _TextLineElement(EditableTextLine line) : super(line); - - final Map _slotToChildren = {}; - - @override - EditableTextLine get widget => super.widget as EditableTextLine; - - @override - RenderEditableTextLine get renderObject => - super.renderObject as RenderEditableTextLine; - - @override - void visitChildren(ElementVisitor visitor) { - _slotToChildren.values.forEach(visitor); - } - - @override - void forgetChild(Element child) { - assert(_slotToChildren.containsValue(child)); - assert(child.slot is TextLineSlot); - assert(_slotToChildren.containsKey(child.slot)); - _slotToChildren.remove(child.slot); - super.forgetChild(child); - } - - @override - void mount(Element? parent, dynamic newSlot) { - super.mount(parent, newSlot); - _mountChild(widget.leading, TextLineSlot.LEADING); - _mountChild(widget.body, TextLineSlot.BODY); - } - - @override - void update(EditableTextLine newWidget) { - super.update(newWidget); - assert(widget == newWidget); - _updateChild(widget.leading, TextLineSlot.LEADING); - _updateChild(widget.body, TextLineSlot.BODY); - } - - @override - void insertRenderObjectChild(RenderBox child, TextLineSlot? slot) { - // assert(child is RenderBox); - _updateRenderObject(child, slot); - assert(renderObject.children.keys.contains(slot)); - } - - @override - void removeRenderObjectChild(RenderObject child, TextLineSlot? slot) { - assert(child is RenderBox); - assert(renderObject.children[slot!] == child); - _updateRenderObject(null, slot); - assert(!renderObject.children.keys.contains(slot)); - } - - @override - void moveRenderObjectChild( - RenderObject child, dynamic oldSlot, dynamic newSlot) { - throw UnimplementedError(); - } - - void _mountChild(Widget? widget, TextLineSlot slot) { - final oldChild = _slotToChildren[slot]; - final newChild = updateChild(oldChild, widget, slot); - if (oldChild != null) { - _slotToChildren.remove(slot); - } - if (newChild != null) { - _slotToChildren[slot] = newChild; - } - } - - void _updateRenderObject(RenderBox? child, TextLineSlot? slot) { - switch (slot) { - case TextLineSlot.LEADING: - renderObject.setLeading(child); - break; - case TextLineSlot.BODY: - renderObject.setBody(child as RenderContentProxyBox?); - break; - default: - throw UnimplementedError(); - } - } - - void _updateChild(Widget? widget, TextLineSlot slot) { - final oldChild = _slotToChildren[slot]; - final newChild = updateChild(oldChild, widget, slot); - if (oldChild != null) { - _slotToChildren.remove(slot); - } - if (newChild != null) { - _slotToChildren[slot] = newChild; - } - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/text_line.dart'; diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index a8748de1..b35db3ea 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -1,726 +1,3 @@ -import 'dart:async'; -import 'dart:math' as math; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; - -import '../models/documents/nodes/node.dart'; -import 'editor.dart'; - -TextSelection localSelection(Node node, TextSelection selection, fromParent) { - final base = fromParent ? node.offset : node.documentOffset; - assert(base <= selection.end && selection.start <= base + node.length - 1); - - 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)); -} - -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; - final Widget debugRequiredFor; - final LayerLink toolbarLayerLink; - final LayerLink startHandleLayerLink; - final LayerLink endHandleLayerLink; - final RenderEditor? renderObject; - final TextSelectionControls selectionCtrls; - final TextSelectionDelegate selectionDelegate; - final DragStartBehavior dragStartBehavior; - final VoidCallback? onSelectionHandleTapped; - final ClipboardStatusNotifier clipboardStatus; - late AnimationController _toolbarController; - List? _handles; - OverlayEntry? toolbar; - - TextSelection get _selection => value.selection; - - Animation get _toolbarOpacity => _toolbarController.view; - - void setHandlesVisible(bool visible) { - if (handlesVisible == visible) { - return; - } - handlesVisible = visible; - if (SchedulerBinding.instance!.schedulerPhase == - SchedulerPhase.persistentCallbacks) { - SchedulerBinding.instance!.addPostFrameCallback(markNeedsBuild); - } else { - markNeedsBuild(); - } - } - - void hideHandles() { - if (_handles == null) { - return; - } - _handles![0].remove(); - _handles![1].remove(); - _handles = null; - } - - void hideToolbar() { - assert(toolbar != null); - _toolbarController.stop(); - toolbar!.remove(); - toolbar = null; - } - - void showToolbar() { - assert(toolbar == null); - toolbar = OverlayEntry(builder: _buildToolbar); - Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! - .insert(toolbar!); - _toolbarController.forward(from: 0); - } - - Widget _buildHandle( - BuildContext context, _TextSelectionHandlePosition position) { - if (_selection.isCollapsed && - position == _TextSelectionHandlePosition.END) { - return Container(); - } - return Visibility( - visible: handlesVisible, - child: _TextSelectionHandleOverlay( - onSelectionHandleChanged: (newSelection) { - _handleSelectionHandleChanged(newSelection, position); - }, - onSelectionHandleTapped: onSelectionHandleTapped, - startHandleLayerLink: startHandleLayerLink, - endHandleLayerLink: endHandleLayerLink, - renderObject: renderObject, - selection: _selection, - selectionControls: selectionCtrls, - position: position, - dragStartBehavior: dragStartBehavior, - )); - } - - void update(TextEditingValue newValue) { - if (value == newValue) { - return; - } - value = newValue; - if (SchedulerBinding.instance!.schedulerPhase == - SchedulerPhase.persistentCallbacks) { - SchedulerBinding.instance!.addPostFrameCallback(markNeedsBuild); - } else { - markNeedsBuild(); - } - } - - void _handleSelectionHandleChanged( - TextSelection? newSelection, _TextSelectionHandlePosition position) { - TextPosition textPosition; - switch (position) { - case _TextSelectionHandlePosition.START: - textPosition = newSelection != null - ? newSelection.base - : const TextPosition(offset: 0); - break; - case _TextSelectionHandlePosition.END: - textPosition = newSelection != null - ? newSelection.extent - : const TextPosition(offset: 0); - break; - default: - throw 'Invalid position'; - } - selectionDelegate - ..textEditingValue = - value.copyWith(selection: newSelection, composing: TextRange.empty) - ..bringIntoView(textPosition); - } - - Widget _buildToolbar(BuildContext context) { - final endpoints = renderObject!.getEndpointsForSelection(_selection); - - final editingRegion = Rect.fromPoints( - renderObject!.localToGlobal(Offset.zero), - renderObject!.localToGlobal(renderObject!.size.bottomRight(Offset.zero)), - ); - - final baseLineHeight = renderObject!.preferredLineHeight(_selection.base); - final extentLineHeight = - renderObject!.preferredLineHeight(_selection.extent); - final smallestLineHeight = math.min(baseLineHeight, extentLineHeight); - final isMultiline = endpoints.last.point.dy - endpoints.first.point.dy > - smallestLineHeight / 2; - - final midX = isMultiline - ? editingRegion.width / 2 - : (endpoints.first.point.dx + endpoints.last.point.dx) / 2; - - final midpoint = Offset( - midX, - endpoints[0].point.dy - baseLineHeight, - ); - - return FadeTransition( - opacity: _toolbarOpacity, - child: CompositedTransformFollower( - link: toolbarLayerLink, - showWhenUnlinked: false, - offset: -editingRegion.topLeft, - child: selectionCtrls.buildToolbar( - context, - editingRegion, - baseLineHeight, - midpoint, - endpoints, - selectionDelegate, - clipboardStatus, - const Offset(0, 0)), - ), - ); - } - - void markNeedsBuild([Duration? duration]) { - if (_handles != null) { - _handles![0].markNeedsBuild(); - _handles![1].markNeedsBuild(); - } - toolbar?.markNeedsBuild(); - } - - void hide() { - if (_handles != null) { - _handles![0].remove(); - _handles![1].remove(); - _handles = null; - } - if (toolbar != null) { - hideToolbar(); - } - } - - void dispose() { - hide(); - _toolbarController.dispose(); - } - - void showHandles() { - assert(_handles == null); - _handles = [ - OverlayEntry( - builder: (context) => - _buildHandle(context, _TextSelectionHandlePosition.START)), - OverlayEntry( - builder: (context) => - _buildHandle(context, _TextSelectionHandlePosition.END)), - ]; - - Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! - .insertAll(_handles!); - } -} - -class _TextSelectionHandleOverlay extends StatefulWidget { - const _TextSelectionHandleOverlay({ - required this.selection, - required this.position, - required this.startHandleLayerLink, - required this.endHandleLayerLink, - required this.renderObject, - required this.onSelectionHandleChanged, - required this.onSelectionHandleTapped, - required this.selectionControls, - this.dragStartBehavior = DragStartBehavior.start, - Key? key, - }) : super(key: key); - - final TextSelection selection; - final _TextSelectionHandlePosition position; - final LayerLink startHandleLayerLink; - final LayerLink endHandleLayerLink; - final RenderEditor? renderObject; - final ValueChanged onSelectionHandleChanged; - final VoidCallback? onSelectionHandleTapped; - final TextSelectionControls selectionControls; - final DragStartBehavior dragStartBehavior; - - @override - _TextSelectionHandleOverlayState createState() => - _TextSelectionHandleOverlayState(); - - ValueListenable? get _visibility { - switch (position) { - case _TextSelectionHandlePosition.START: - return renderObject!.selectionStartInViewport; - case _TextSelectionHandlePosition.END: - return renderObject!.selectionEndInViewport; - } - } -} - -class _TextSelectionHandleOverlayState - extends State<_TextSelectionHandleOverlay> - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - Animation get _opacity => _controller.view; - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - duration: const Duration(milliseconds: 150), vsync: this); - - _handleVisibilityChanged(); - widget._visibility!.addListener(_handleVisibilityChanged); - } - - void _handleVisibilityChanged() { - if (widget._visibility!.value) { - _controller.forward(); - } else { - _controller.reverse(); - } - } - - @override - void didUpdateWidget(_TextSelectionHandleOverlay oldWidget) { - super.didUpdateWidget(oldWidget); - oldWidget._visibility!.removeListener(_handleVisibilityChanged); - _handleVisibilityChanged(); - widget._visibility!.addListener(_handleVisibilityChanged); - } - - @override - void dispose() { - widget._visibility!.removeListener(_handleVisibilityChanged); - _controller.dispose(); - super.dispose(); - } - - void _handleDragStart(DragStartDetails details) {} - - void _handleDragUpdate(DragUpdateDetails details) { - final position = - widget.renderObject!.getPositionForOffset(details.globalPosition); - if (widget.selection.isCollapsed) { - widget.onSelectionHandleChanged(TextSelection.fromPosition(position)); - return; - } - - final isNormalized = - widget.selection.extentOffset >= widget.selection.baseOffset; - TextSelection? newSelection; - switch (widget.position) { - case _TextSelectionHandlePosition.START: - newSelection = TextSelection( - baseOffset: - isNormalized ? position.offset : widget.selection.baseOffset, - extentOffset: - isNormalized ? widget.selection.extentOffset : position.offset, - ); - break; - case _TextSelectionHandlePosition.END: - newSelection = TextSelection( - baseOffset: - isNormalized ? widget.selection.baseOffset : position.offset, - extentOffset: - isNormalized ? position.offset : widget.selection.extentOffset, - ); - break; - } - - widget.onSelectionHandleChanged(newSelection); - } - - void _handleTap() { - if (widget.onSelectionHandleTapped != null) { - widget.onSelectionHandleTapped!(); - } - } - - @override - Widget build(BuildContext context) { - late LayerLink layerLink; - TextSelectionHandleType? type; - - switch (widget.position) { - case _TextSelectionHandlePosition.START: - layerLink = widget.startHandleLayerLink; - type = _chooseType( - widget.renderObject!.textDirection, - TextSelectionHandleType.left, - TextSelectionHandleType.right, - ); - break; - case _TextSelectionHandlePosition.END: - assert(!widget.selection.isCollapsed); - layerLink = widget.endHandleLayerLink; - type = _chooseType( - widget.renderObject!.textDirection, - TextSelectionHandleType.right, - TextSelectionHandleType.left, - ); - break; - } - - 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); - final handleSize = widget.selectionControls.getHandleSize(lineHeight); - - final handleRect = Rect.fromLTWH( - -handleAnchor.dx, - -handleAnchor.dy, - handleSize.width, - handleSize.height, - ); - - final interactiveRect = handleRect.expandToInclude( - Rect.fromCircle( - center: handleRect.center, radius: kMinInteractiveDimension / 2), - ); - 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), - math.max((interactiveRect.height - handleRect.height) / 2, 0), - ); - - return CompositedTransformFollower( - link: layerLink, - offset: interactiveRect.topLeft, - showWhenUnlinked: false, - child: FadeTransition( - opacity: _opacity, - child: Container( - alignment: Alignment.topLeft, - width: interactiveRect.width, - height: interactiveRect.height, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - dragStartBehavior: widget.dragStartBehavior, - onPanStart: _handleDragStart, - onPanUpdate: _handleDragUpdate, - onTap: _handleTap, - child: Padding( - padding: EdgeInsets.only( - left: padding.left, - top: padding.top, - right: padding.right, - bottom: padding.bottom, - ), - child: widget.selectionControls.buildHandle( - context, - type, - lineHeight, - ), - ), - ), - ), - ), - ); - } - - TextSelectionHandleType? _chooseType( - TextDirection textDirection, - TextSelectionHandleType ltrType, - TextSelectionHandleType rtlType, - ) { - if (widget.selection.isCollapsed) return TextSelectionHandleType.collapsed; - - switch (textDirection) { - case TextDirection.ltr: - return ltrType; - case TextDirection.rtl: - return rtlType; - } - } -} - -class EditorTextSelectionGestureDetector extends StatefulWidget { - const EditorTextSelectionGestureDetector({ - required this.child, - this.onTapDown, - this.onForcePressStart, - this.onForcePressEnd, - this.onSingleTapUp, - this.onSingleTapCancel, - this.onSingleLongTapStart, - this.onSingleLongTapMoveUpdate, - this.onSingleLongTapEnd, - this.onDoubleTapDown, - this.onDragSelectionStart, - this.onDragSelectionUpdate, - this.onDragSelectionEnd, - this.behavior, - Key? key, - }) : super(key: key); - - final GestureTapDownCallback? onTapDown; - - final GestureForcePressStartCallback? onForcePressStart; - - final GestureForcePressEndCallback? onForcePressEnd; - - final GestureTapUpCallback? onSingleTapUp; - - final GestureTapCancelCallback? onSingleTapCancel; - - final GestureLongPressStartCallback? onSingleLongTapStart; - - final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate; - - final GestureLongPressEndCallback? onSingleLongTapEnd; - - final GestureTapDownCallback? onDoubleTapDown; - - final GestureDragStartCallback? onDragSelectionStart; - - final DragSelectionUpdateCallback? onDragSelectionUpdate; - - final GestureDragEndCallback? onDragSelectionEnd; - - final HitTestBehavior? behavior; - - final Widget child; - - @override - State createState() => - _EditorTextSelectionGestureDetectorState(); -} - -class _EditorTextSelectionGestureDetectorState - extends State { - Timer? _doubleTapTimer; - Offset? _lastTapOffset; - bool _isDoubleTap = false; - - @override - void dispose() { - _doubleTapTimer?.cancel(); - _dragUpdateThrottleTimer?.cancel(); - super.dispose(); - } - - void _handleTapDown(TapDownDetails details) { - // renderObject.resetTapDownStatus(); - if (widget.onTapDown != null) { - widget.onTapDown!(details); - } - if (_doubleTapTimer != null && - _isWithinDoubleTapTolerance(details.globalPosition)) { - if (widget.onDoubleTapDown != null) { - widget.onDoubleTapDown!(details); - } - - _doubleTapTimer!.cancel(); - _doubleTapTimeout(); - _isDoubleTap = true; - } - } - - void _handleTapUp(TapUpDetails details) { - if (!_isDoubleTap) { - if (widget.onSingleTapUp != null) { - widget.onSingleTapUp!(details); - } - _lastTapOffset = details.globalPosition; - _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); - } - _isDoubleTap = false; - } - - void _handleTapCancel() { - if (widget.onSingleTapCancel != null) { - widget.onSingleTapCancel!(); - } - } - - DragStartDetails? _lastDragStartDetails; - DragUpdateDetails? _lastDragUpdateDetails; - Timer? _dragUpdateThrottleTimer; - - void _handleDragStart(DragStartDetails details) { - assert(_lastDragStartDetails == null); - _lastDragStartDetails = details; - if (widget.onDragSelectionStart != null) { - widget.onDragSelectionStart!(details); - } - } - - void _handleDragUpdate(DragUpdateDetails details) { - _lastDragUpdateDetails = details; - _dragUpdateThrottleTimer ??= - Timer(const Duration(milliseconds: 50), _handleDragUpdateThrottled); - } - - void _handleDragUpdateThrottled() { - assert(_lastDragStartDetails != null); - assert(_lastDragUpdateDetails != null); - if (widget.onDragSelectionUpdate != null) { - widget.onDragSelectionUpdate!( - _lastDragStartDetails!, _lastDragUpdateDetails!); - } - _dragUpdateThrottleTimer = null; - _lastDragUpdateDetails = null; - } - - void _handleDragEnd(DragEndDetails details) { - assert(_lastDragStartDetails != null); - if (_dragUpdateThrottleTimer != null) { - _dragUpdateThrottleTimer!.cancel(); - _handleDragUpdateThrottled(); - } - if (widget.onDragSelectionEnd != null) { - widget.onDragSelectionEnd!(details); - } - _dragUpdateThrottleTimer = null; - _lastDragStartDetails = null; - _lastDragUpdateDetails = null; - } - - void _forcePressStarted(ForcePressDetails details) { - _doubleTapTimer?.cancel(); - _doubleTapTimer = null; - if (widget.onForcePressStart != null) { - widget.onForcePressStart!(details); - } - } - - void _forcePressEnded(ForcePressDetails details) { - if (widget.onForcePressEnd != null) { - widget.onForcePressEnd!(details); - } - } - - void _handleLongPressStart(LongPressStartDetails details) { - if (!_isDoubleTap && widget.onSingleLongTapStart != null) { - widget.onSingleLongTapStart!(details); - } - } - - void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - if (!_isDoubleTap && widget.onSingleLongTapMoveUpdate != null) { - widget.onSingleLongTapMoveUpdate!(details); - } - } - - void _handleLongPressEnd(LongPressEndDetails details) { - if (!_isDoubleTap && widget.onSingleLongTapEnd != null) { - widget.onSingleLongTapEnd!(details); - } - _isDoubleTap = false; - } - - void _doubleTapTimeout() { - _doubleTapTimer = null; - _lastTapOffset = null; - } - - bool _isWithinDoubleTapTolerance(Offset secondTapOffset) { - if (_lastTapOffset == null) { - return false; - } - - return (secondTapOffset - _lastTapOffset!).distance <= kDoubleTapSlop; - } - - @override - Widget build(BuildContext context) { - final gestures = {}; - - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (instance) { - instance - ..onTapDown = _handleTapDown - ..onTapUp = _handleTapUp - ..onTapCancel = _handleTapCancel; - }, - ); - - if (widget.onSingleLongTapStart != null || - widget.onSingleLongTapMoveUpdate != null || - widget.onSingleLongTapEnd != null) { - gestures[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer( - debugOwner: this, kind: PointerDeviceKind.touch), - (instance) { - instance - ..onLongPressStart = _handleLongPressStart - ..onLongPressMoveUpdate = _handleLongPressMoveUpdate - ..onLongPressEnd = _handleLongPressEnd; - }, - ); - } - - if (widget.onDragSelectionStart != null || - widget.onDragSelectionUpdate != null || - widget.onDragSelectionEnd != null) { - gestures[HorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => HorizontalDragGestureRecognizer( - debugOwner: this, kind: PointerDeviceKind.mouse), - (instance) { - instance - ..dragStartBehavior = DragStartBehavior.down - ..onStart = _handleDragStart - ..onUpdate = _handleDragUpdate - ..onEnd = _handleDragEnd; - }, - ); - } - - if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { - gestures[ForcePressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => ForcePressGestureRecognizer(debugOwner: this), - (instance) { - instance - ..onStart = - widget.onForcePressStart != null ? _forcePressStarted : null - ..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null; - }, - ); - } - - return RawGestureDetector( - gestures: gestures, - excludeFromSemantics: true, - behavior: widget.behavior, - child: widget.child, - ); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/text_selection.dart'; diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index c5fe2bab..c47f1353 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -1,1294 +1,3 @@ -import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; -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: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'; - -typedef OnImagePickCallback = Future Function(File file); -typedef ImagePickImpl = Future Function(ImageSource source); - -class InsertEmbedButton extends StatelessWidget { - const InsertEmbedButton({ - required this.controller, - required this.icon, - this.fillColor, - Key? key, - }) : super(key: key); - - final QuillController controller; - final IconData icon; - final Color? fillColor; - - @override - Widget build(BuildContext context) { - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: controller.iconSize * 1.77, - icon: Icon( - icon, - size: controller.iconSize, - color: Theme.of(context).iconTheme.color, - ), - fillColor: fillColor ?? Theme.of(context).canvasColor, - onPressed: () { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - controller.replaceText(index, length, BlockEmbed.horizontalRule, null); - }, - ); - } -} - -class LinkStyleButton extends StatefulWidget { - const LinkStyleButton({ - required this.controller, - this.icon, - Key? key, - }) : super(key: key); - - final QuillController controller; - final IconData? icon; - - @override - _LinkStyleButtonState createState() => _LinkStyleButtonState(); -} - -class _LinkStyleButtonState extends State { - void _didChangeSelection() { - setState(() {}); - } - - @override - void initState() { - super.initState(); - widget.controller.addListener(_didChangeSelection); - } - - @override - void didUpdateWidget(covariant LinkStyleButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeSelection); - widget.controller.addListener(_didChangeSelection); - } - } - - @override - void dispose() { - super.dispose(); - widget.controller.removeListener(_didChangeSelection); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isEnabled = !widget.controller.selection.isCollapsed; - final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: Icon( - widget.icon ?? Icons.link, - size: widget.controller.iconSize, - color: isEnabled ? theme.iconTheme.color : theme.disabledColor, - ), - fillColor: Theme.of(context).canvasColor, - onPressed: pressedHandler, - ); - } - - void _openLinkDialog(BuildContext context) { - showDialog( - context: context, - builder: (ctx) { - return const _LinkDialog(); - }, - ).then(_linkSubmitted); - } - - void _linkSubmitted(String? value) { - if (value == null || value.isEmpty) { - return; - } - widget.controller.formatSelection(LinkAttribute(value)); - } -} - -class _LinkDialog extends StatefulWidget { - const _LinkDialog({Key? key}) : super(key: key); - - @override - _LinkDialogState createState() => _LinkDialogState(); -} - -class _LinkDialogState extends State<_LinkDialog> { - String _link = ''; - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: TextField( - decoration: const InputDecoration(labelText: 'Paste a link'), - autofocus: true, - onChanged: _linkChanged, - ), - actions: [ - TextButton( - onPressed: _link.isNotEmpty ? _applyLink : null, - child: const Text('Apply'), - ), - ], - ); - } - - void _linkChanged(String value) { - setState(() { - _link = value; - }); - } - - void _applyLink() { - Navigator.pop(context, _link); - } -} - -typedef ToggleStyleButtonBuilder = Widget Function( - BuildContext context, - Attribute attribute, - IconData icon, - Color? fillColor, - bool? isToggled, - VoidCallback? onPressed, -); - -class ToggleStyleButton extends StatefulWidget { - const ToggleStyleButton({ - required this.attribute, - required this.icon, - required this.controller, - this.fillColor, - this.childBuilder = defaultToggleStyleButtonBuilder, - Key? key, - }) : super(key: key); - - final Attribute attribute; - - final IconData icon; - - final Color? fillColor; - - final QuillController controller; - - final ToggleStyleButtonBuilder childBuilder; - - @override - _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); -} - -class _ToggleStyleButtonState extends State { - bool? _isToggled; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _isToggled = - _getIsToggled(widget.controller.getSelectionStyle().attributes); - }); - } - - @override - void initState() { - super.initState(); - _isToggled = _getIsToggled(_selectionStyle.attributes); - widget.controller.addListener(_didChangeEditingValue); - } - - bool _getIsToggled(Map attrs) { - if (widget.attribute.key == Attribute.list.key) { - final attribute = attrs[widget.attribute.key]; - if (attribute == null) { - return false; - } - return attribute.value == widget.attribute.value; - } - return attrs.containsKey(widget.attribute.key); - } - - @override - void didUpdateWidget(covariant ToggleStyleButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _isToggled = _getIsToggled(_selectionStyle.attributes); - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isInCodeBlock = - _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); - final isEnabled = - !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key; - return widget.childBuilder(context, widget.attribute, widget.icon, - widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); - } - - void _toggleAttribute() { - widget.controller.formatSelection(_isToggled! - ? Attribute.clone(widget.attribute, null) - : widget.attribute); - } -} - -class ToggleCheckListButton extends StatefulWidget { - const ToggleCheckListButton({ - required this.icon, - required this.controller, - required this.attribute, - this.fillColor, - this.childBuilder = defaultToggleStyleButtonBuilder, - Key? key, - }) : super(key: key); - - final IconData icon; - - final Color? fillColor; - - final QuillController controller; - - final ToggleStyleButtonBuilder childBuilder; - - final Attribute attribute; - - @override - _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); -} - -class _ToggleCheckListButtonState extends State { - bool? _isToggled; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _isToggled = - _getIsToggled(widget.controller.getSelectionStyle().attributes); - }); - } - - @override - void initState() { - super.initState(); - _isToggled = _getIsToggled(_selectionStyle.attributes); - widget.controller.addListener(_didChangeEditingValue); - } - - bool _getIsToggled(Map attrs) { - if (widget.attribute.key == Attribute.list.key) { - final attribute = attrs[widget.attribute.key]; - if (attribute == null) { - return false; - } - return attribute.value == widget.attribute.value || - attribute.value == Attribute.checked.value; - } - return attrs.containsKey(widget.attribute.key); - } - - @override - void didUpdateWidget(covariant ToggleCheckListButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _isToggled = _getIsToggled(_selectionStyle.attributes); - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isInCodeBlock = - _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); - final isEnabled = - !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key; - return widget.childBuilder(context, Attribute.unchecked, widget.icon, - widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); - } - - void _toggleAttribute() { - widget.controller.formatSelection(_isToggled! - ? Attribute.clone(Attribute.unchecked, null) - : Attribute.unchecked); - } -} - -Widget defaultToggleStyleButtonBuilder( - BuildContext context, - Attribute attribute, - IconData icon, - Color? fillColor, - bool? isToggled, - VoidCallback? onPressed, -) { - final theme = Theme.of(context); - final isEnabled = onPressed != null; - final iconColor = isEnabled - ? isToggled == true - ? theme.primaryIconTheme.color - : theme.iconTheme.color - : theme.disabledColor; - final fill = isToggled == true - ? theme.toggleableActiveColor - : fillColor ?? theme.canvasColor; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: 18 * 1.77, - icon: Icon(icon, size: 18, color: iconColor), - fillColor: fill, - onPressed: onPressed, - ); -} - -class SelectHeaderStyleButton extends StatefulWidget { - const SelectHeaderStyleButton({required this.controller, Key? key}) - : super(key: key); - - final QuillController controller; - - @override - _SelectHeaderStyleButtonState createState() => - _SelectHeaderStyleButtonState(); -} - -class _SelectHeaderStyleButtonState extends State { - Attribute? _value; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _value = - _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; - }); - } - - void _selectAttribute(value) { - widget.controller.formatSelection(value); - } - - @override - void initState() { - super.initState(); - setState(() { - _value = - _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; - }); - widget.controller.addListener(_didChangeEditingValue); - } - - @override - void didUpdateWidget(covariant SelectHeaderStyleButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _value = - _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return _selectHeadingStyleButtonBuilder( - context, _value, _selectAttribute, widget.controller.iconSize); - } -} - -Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, - ValueChanged onSelected, double iconSize) { - final _valueToText = { - Attribute.header: 'N', - Attribute.h1: 'H1', - Attribute.h2: 'H2', - Attribute.h3: 'H3', - }; - - final _valueAttribute = [ - Attribute.header, - Attribute.h1, - Attribute.h2, - Attribute.h3 - ]; - final _valueString = ['N', 'H1', 'H2', 'H3']; - - final theme = Theme.of(context); - final style = TextStyle( - fontWeight: FontWeight.w600, - fontSize: iconSize * 0.7, - ); - - return Row( - mainAxisSize: MainAxisSize.min, - children: List.generate(4, (index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), - child: ConstrainedBox( - constraints: BoxConstraints.tightFor( - width: iconSize * 1.77, - height: iconSize * 1.77, - ), - child: RawMaterialButton( - hoverElevation: 0, - highlightElevation: 0, - elevation: 0, - visualDensity: VisualDensity.compact, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - fillColor: _valueToText[value] == _valueString[index] - ? theme.toggleableActiveColor - : theme.canvasColor, - onPressed: () { - onSelected(_valueAttribute[index]); - }, - child: Text( - _valueString[index], - style: style.copyWith( - color: _valueToText[value] == _valueString[index] - ? theme.primaryIconTheme.color - : theme.iconTheme.color, - ), - ), - ), - ), - ); - }), - ); -} - -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; - - final OnImagePickCallback? onImagePickCallback; - - final ImagePickImpl? imagePickImpl; - - final ImageSource imageSource; - - @override - _ImageButtonState createState() => _ImageButtonState(); -} - -class _ImageButtonState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return QuillIconButton( - icon: Icon( - widget.icon, - size: widget.controller.iconSize, - color: theme.iconTheme.color, - ), - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - fillColor: theme.canvasColor, - onPressed: _handleImageButtonTap, - ); - } - - Future _handleImageButtonTap() async { - final index = widget.controller.selection.baseOffset; - final length = widget.controller.selection.extentOffset - index; - - String? imageUrl; - if (widget.imagePickImpl != null) { - imageUrl = await widget.imagePickImpl!(widget.imageSource); - } else { - if (kIsWeb) { - imageUrl = await _pickImageWeb(); - } else if (Platform.isAndroid || Platform.isIOS) { - imageUrl = await _pickImage(widget.imageSource); - } else { - imageUrl = await _pickImageDesktop(); - } - } - - if (imageUrl != null) { - widget.controller - .replaceText(index, length, BlockEmbed.image(imageUrl), null); - } - } - - Future _pickImageWeb() async { - final result = await FilePicker.platform.pickFiles(); - if (result == null) { - return null; - } - - // Take first, because we don't allow picking multiple files. - final fileName = result.files.first.name!; - final file = File(fileName); - - return widget.onImagePickCallback!(file); - } - - Future _pickImage(ImageSource source) async { - final pickedFile = await ImagePicker().getImage(source: source); - if (pickedFile == null) { - return null; - } - - return widget.onImagePickCallback!(File(pickedFile.path)); - } - - Future _pickImageDesktop() async { - final filePath = await FilesystemPicker.open( - context: context, - rootDirectory: await getApplicationDocumentsDirectory(), - fsType: FilesystemType.file, - fileTileSelectMode: FileTileSelectMode.wholeTile, - ); - if (filePath == null || filePath.isEmpty) return null; - - final file = File(filePath); - return widget.onImagePickCallback!(file); - } -} - -/// Controls color styles. -/// -/// When pressed, this button displays overlay toolbar with -/// buttons for each color. -class ColorButton extends StatefulWidget { - const ColorButton({ - required this.icon, - required this.controller, - required this.background, - Key? key, - }) : super(key: key); - - final IconData icon; - final bool background; - final QuillController controller; - - @override - _ColorButtonState createState() => _ColorButtonState(); -} - -class _ColorButtonState extends State { - late bool _isToggledColor; - late bool _isToggledBackground; - late bool _isWhite; - late bool _isWhitebackground; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _isToggledColor = - _getIsToggledColor(widget.controller.getSelectionStyle().attributes); - _isToggledBackground = _getIsToggledBackground( - widget.controller.getSelectionStyle().attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - }); - } - - @override - void initState() { - super.initState(); - _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); - _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - widget.controller.addListener(_didChangeEditingValue); - } - - bool _getIsToggledColor(Map attrs) { - return attrs.containsKey(Attribute.color.key); - } - - bool _getIsToggledBackground(Map attrs) { - return attrs.containsKey(Attribute.background.key); - } - - @override - void didUpdateWidget(covariant ColorButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); - _isToggledBackground = - _getIsToggledBackground(_selectionStyle.attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = _isToggledColor && !widget.background && !_isWhite - ? stringToColor(_selectionStyle.attributes['color']!.value) - : theme.iconTheme.color; - - final iconColorBackground = - _isToggledBackground && widget.background && !_isWhitebackground - ? stringToColor(_selectionStyle.attributes['background']!.value) - : theme.iconTheme.color; - - final fillColor = _isToggledColor && !widget.background && _isWhite - ? stringToColor('#ffffff') - : theme.canvasColor; - final fillColorBackground = - _isToggledBackground && widget.background && _isWhitebackground - ? stringToColor('#ffffff') - : theme.canvasColor; - - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, - size: widget.controller.iconSize, - color: widget.background ? iconColorBackground : iconColor), - fillColor: widget.background ? fillColorBackground : fillColor, - onPressed: _showColorPicker, - ); - } - - void _changeColor(Color color) { - var hex = color.value.toRadixString(16); - if (hex.startsWith('ff')) { - hex = hex.substring(2); - } - hex = '#$hex'; - widget.controller.formatSelection( - widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex)); - Navigator.of(context).pop(); - } - - void _showColorPicker() { - showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('Select Color'), - backgroundColor: Theme.of(context).canvasColor, - content: SingleChildScrollView( - child: MaterialPicker( - pickerColor: const Color(0x00000000), - onColorChanged: _changeColor, - ), - )), - ); - } -} - -class HistoryButton extends StatefulWidget { - const HistoryButton({ - required this.icon, - required this.controller, - required this.undo, - Key? key, - }) : super(key: key); - - final IconData icon; - final bool undo; - final QuillController controller; - - @override - _HistoryButtonState createState() => _HistoryButtonState(); -} - -class _HistoryButtonState extends State { - Color? _iconColor; - late ThemeData theme; - - @override - Widget build(BuildContext context) { - theme = Theme.of(context); - _setIconColor(); - - final fillColor = theme.canvasColor; - widget.controller.changes.listen((event) async { - _setIconColor(); - }); - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, - size: widget.controller.iconSize, color: _iconColor), - fillColor: fillColor, - onPressed: _changeHistory, - ); - } - - void _setIconColor() { - if (!mounted) return; - - if (widget.undo) { - setState(() { - _iconColor = widget.controller.hasUndo - ? theme.iconTheme.color - : theme.disabledColor; - }); - } else { - setState(() { - _iconColor = widget.controller.hasRedo - ? theme.iconTheme.color - : theme.disabledColor; - }); - } - } - - void _changeHistory() { - if (widget.undo) { - if (widget.controller.hasUndo) { - widget.controller.undo(); - } - } else { - if (widget.controller.hasRedo) { - widget.controller.redo(); - } - } - - _setIconColor(); - } -} - -class IndentButton extends StatefulWidget { - const IndentButton({ - required this.icon, - required this.controller, - required this.isIncrease, - Key? key, - }) : super(key: key); - - final IconData icon; - final QuillController controller; - final bool isIncrease; - - @override - _IndentButtonState createState() => _IndentButtonState(); -} - -class _IndentButtonState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = theme.iconTheme.color; - final fillColor = theme.canvasColor; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: - Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), - fillColor: fillColor, - onPressed: () { - final indent = widget.controller - .getSelectionStyle() - .attributes[Attribute.indent.key]; - if (indent == null) { - if (widget.isIncrease) { - widget.controller.formatSelection(Attribute.indentL1); - } - return; - } - if (indent.value == 1 && !widget.isIncrease) { - widget.controller - .formatSelection(Attribute.clone(Attribute.indentL1, null)); - return; - } - if (widget.isIncrease) { - widget.controller - .formatSelection(Attribute.getIndentLevel(indent.value + 1)); - return; - } - widget.controller - .formatSelection(Attribute.getIndentLevel(indent.value - 1)); - }, - ); - } -} - -class ClearFormatButton extends StatefulWidget { - const ClearFormatButton({ - required this.icon, - required this.controller, - Key? key, - }) : super(key: key); - - final IconData icon; - - final QuillController controller; - - @override - _ClearFormatButtonState createState() => _ClearFormatButtonState(); -} - -class _ClearFormatButtonState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = theme.iconTheme.color; - final fillColor = theme.canvasColor; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, - size: widget.controller.iconSize, color: iconColor), - fillColor: fillColor, - onPressed: () { - for (final k - in widget.controller.getSelectionStyle().attributes.values) { - widget.controller.formatSelection(Attribute.clone(k, null)); - } - }); - } -} - -class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { - const QuillToolbar( - {required this.children, this.toolBarHeight = 36, Key? key}) - : super(key: key); - - factory QuillToolbar.basic({ - required QuillController controller, - double toolbarIconSize = 18.0, - bool showBoldButton = true, - bool showItalicButton = true, - bool showUnderLineButton = true, - bool showStrikeThrough = true, - bool showColorButton = true, - bool showBackgroundColorButton = true, - bool showClearFormat = true, - bool showHeaderStyle = true, - bool showListNumbers = true, - bool showListBullets = true, - bool showListCheck = true, - bool showCodeBlock = true, - bool showQuote = true, - bool showIndent = true, - bool showLink = true, - bool showHistory = true, - bool showHorizontalRule = false, - OnImagePickCallback? onImagePickCallback, - Key? key, - }) { - controller.iconSize = toolbarIconSize; - - return QuillToolbar( - key: key, - toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor, - children: [ - Visibility( - visible: showHistory, - child: HistoryButton( - icon: Icons.undo_outlined, - controller: controller, - undo: true, - ), - ), - Visibility( - visible: showHistory, - child: HistoryButton( - icon: Icons.redo_outlined, - controller: controller, - undo: false, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showBoldButton, - child: ToggleStyleButton( - attribute: Attribute.bold, - icon: Icons.format_bold, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showItalicButton, - child: ToggleStyleButton( - attribute: Attribute.italic, - icon: Icons.format_italic, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showUnderLineButton, - child: ToggleStyleButton( - attribute: Attribute.underline, - icon: Icons.format_underline, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showStrikeThrough, - child: ToggleStyleButton( - attribute: Attribute.strikeThrough, - icon: Icons.format_strikethrough, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showColorButton, - child: ColorButton( - icon: Icons.color_lens, - controller: controller, - background: false, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showBackgroundColorButton, - child: ColorButton( - icon: Icons.format_color_fill, - controller: controller, - background: true, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: showClearFormat, - child: ClearFormatButton( - icon: Icons.format_clear, - controller: controller, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: onImagePickCallback != null, - child: ImageButton( - icon: Icons.image, - controller: controller, - imageSource: ImageSource.gallery, - onImagePickCallback: onImagePickCallback, - ), - ), - const SizedBox(width: 0.6), - Visibility( - visible: onImagePickCallback != null, - child: ImageButton( - icon: Icons.photo_camera, - controller: controller, - imageSource: ImageSource.camera, - onImagePickCallback: onImagePickCallback, - ), - ), - Visibility( - visible: showHeaderStyle, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility( - visible: showHeaderStyle, - child: SelectHeaderStyleButton(controller: controller)), - VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400), - Visibility( - visible: showListNumbers, - child: ToggleStyleButton( - attribute: Attribute.ol, - controller: controller, - icon: Icons.format_list_numbered, - ), - ), - Visibility( - visible: showListBullets, - child: ToggleStyleButton( - attribute: Attribute.ul, - controller: controller, - icon: Icons.format_list_bulleted, - ), - ), - Visibility( - visible: showListCheck, - child: ToggleCheckListButton( - attribute: Attribute.unchecked, - controller: controller, - icon: Icons.check_box, - ), - ), - Visibility( - visible: showCodeBlock, - child: ToggleStyleButton( - attribute: Attribute.codeBlock, - controller: controller, - icon: Icons.code, - ), - ), - Visibility( - visible: !showListNumbers && - !showListBullets && - !showListCheck && - !showCodeBlock, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility( - visible: showQuote, - child: ToggleStyleButton( - attribute: Attribute.blockQuote, - controller: controller, - icon: Icons.format_quote, - ), - ), - Visibility( - visible: showIndent, - child: IndentButton( - icon: Icons.format_indent_increase, - controller: controller, - isIncrease: true, - ), - ), - Visibility( - visible: showIndent, - child: IndentButton( - icon: Icons.format_indent_decrease, - controller: controller, - isIncrease: false, - ), - ), - Visibility( - visible: showQuote, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), - Visibility( - visible: showLink, - child: LinkStyleButton(controller: controller)), - Visibility( - visible: showHorizontalRule, - child: InsertEmbedButton( - controller: controller, - icon: Icons.horizontal_rule, - ), - ), - ]); - } - - final List children; - final double toolBarHeight; - - @override - _QuillToolbarState createState() => _QuillToolbarState(); - - @override - Size get preferredSize => Size.fromHeight(toolBarHeight); -} - -class _QuillToolbarState extends State { - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - constraints: BoxConstraints.tightFor(height: widget.preferredSize.height), - color: Theme.of(context).canvasColor, - child: CustomScrollView( - scrollDirection: Axis.horizontal, - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: widget.children, - ), - ), - ], - ), - ); - } -} - -class QuillIconButton extends StatelessWidget { - const QuillIconButton({ - required this.onPressed, - this.icon, - this.size = 40, - this.fillColor, - this.hoverElevation = 1, - this.highlightElevation = 1, - 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( - constraints: BoxConstraints.tightFor(width: size, height: size), - child: RawMaterialButton( - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - fillColor: fillColor, - elevation: 0, - hoverElevation: hoverElevation, - highlightElevation: hoverElevation, - onPressed: onPressed, - child: icon, - ), - ); - } -} - -class QuillDropdownButton extends StatefulWidget { - const QuillDropdownButton({ - required this.child, - required this.initialValue, - required this.items, - required this.onSelected, - this.height = 40, - this.fillColor, - this.hoverElevation = 1, - this.highlightElevation = 1, - 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(); -} - -class _QuillDropdownButtonState extends State> { - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints.tightFor(height: widget.height), - child: RawMaterialButton( - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - fillColor: widget.fillColor, - elevation: 0, - hoverElevation: widget.hoverElevation, - highlightElevation: widget.hoverElevation, - onPressed: _showMenu, - child: _buildContent(context), - ), - ); - } - - void _showMenu() { - final popupMenuTheme = PopupMenuTheme.of(context); - final button = context.findRenderObject() as RenderBox; - final overlay = - Overlay.of(context)!.context.findRenderObject() as RenderBox; - final position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(Offset.zero, ancestor: overlay), - button.localToGlobal(button.size.bottomLeft(Offset.zero), - ancestor: overlay), - ), - Offset.zero & overlay.size, - ); - showMenu( - context: context, - elevation: 4, - // widget.elevation ?? popupMenuTheme.elevation, - initialValue: widget.initialValue, - items: widget.items, - position: position, - shape: popupMenuTheme.shape, - // widget.shape ?? popupMenuTheme.shape, - color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color, - // captureInheritedThemes: widget.captureInheritedThemes, - ).then((newValue) { - if (!mounted) return null; - if (newValue == null) { - // if (widget.onCanceled != null) widget.onCanceled(); - return null; - } - widget.onSelected(newValue); - }); - } - - Widget _buildContent(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 110), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - widget.child, - Expanded(child: Container()), - const Icon(Icons.arrow_drop_down, size: 15) - ], - ), - ), - ); - } -} +/// TODO: Remove this file in the next breaking release, because implementation +/// files should be located in the src folder, https://bit.ly/3fA23Yz. +export '../../src/widgets/toolbar.dart'; From 95359f66b5690d3d300c69f90394447d9620ed29 Mon Sep 17 00:00:00 2001 From: George Date: Fri, 21 May 2021 22:48:25 +0100 Subject: [PATCH 186/306] Fixes for flutter web (#234) * Fix for Attribute object comparison * Fix for "Unexpected null value" error on web Clipboard is now supported on web, via a permission request through the browser Co-authored-by: George Johnson --- lib/src/models/documents/attribute.dart | 2 +- lib/src/widgets/raw_editor.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index 1b9043b9..acd979cb 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -193,7 +193,7 @@ class Attribute { @override bool operator ==(Object other) { if (identical(this, other)) return true; - if (other is! Attribute) return false; + if (other is! Attribute) return false; final typedOther = other; return key == typedOther.key && scope == typedOther.scope && diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index cd4f379c..e52e18cd 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -126,8 +126,7 @@ class RawEditorState extends EditorState DefaultStyles? _styles; - final ClipboardStatusNotifier? _clipboardStatus = - kIsWeb ? null : ClipboardStatusNotifier(); + final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); final LayerLink _toolbarLayerLink = LayerLink(); final LayerLink _startHandleLayerLink = LayerLink(); final LayerLink _endHandleLayerLink = LayerLink(); @@ -318,7 +317,7 @@ class RawEditorState extends EditorState void initState() { super.initState(); - _clipboardStatus?.addListener(_onChangedClipboardStatus); + _clipboardStatus.addListener(_onChangedClipboardStatus); widget.controller.addListener(() { _didChangeTextEditingValue(widget.controller.ignoreFocusOnTextChange); @@ -438,8 +437,9 @@ class RawEditorState extends EditorState widget.focusNode.removeListener(_handleFocusChanged); _focusAttachment!.detach(); _cursorCont.dispose(); - _clipboardStatus?.removeListener(_onChangedClipboardStatus); - _clipboardStatus?.dispose(); + _clipboardStatus + ..removeListener(_onChangedClipboardStatus) + ..dispose(); super.dispose(); } @@ -518,7 +518,7 @@ class RawEditorState extends EditorState this, DragStartBehavior.start, null, - _clipboardStatus!, + _clipboardStatus, ); _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); _selectionOverlay!.showHandles(); From 292609871c2e2c717cb164be5f472d39bd7981bf Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Fri, 21 May 2021 23:54:35 +0200 Subject: [PATCH 187/306] Dispose ValueNotifier of cursor controller --- lib/src/widgets/cursor.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 963bc2e7..d0bc91b2 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -92,6 +92,9 @@ class CursorCont extends ChangeNotifier { _blinkOpacityCont.removeListener(_onColorTick); stopCursorTimer(); _blinkOpacityCont.dispose(); + show.dispose(); + _blink.dispose(); + color.dispose(); assert(_cursorTimer == null); super.dispose(); } From 3b429840b6b33d799bfd52990a84c63fe93b6331 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Fri, 21 May 2021 23:58:47 +0200 Subject: [PATCH 188/306] Remove getter for final operator A getter for a final variable makes no sense, because the variable cannot be reassigned. It is better to remove the unnecessary getter and make the variable public. --- lib/src/widgets/cursor.dart | 12 ++++-------- lib/src/widgets/text_line.dart | 6 +++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index d0bc91b2..67ac45f5 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -60,7 +60,7 @@ class CursorCont extends ChangeNotifier { required CursorStyle style, required TickerProvider tickerProvider, }) : _style = style, - _blink = ValueNotifier(false), + blink = ValueNotifier(false), color = ValueNotifier(style.color) { _blinkOpacityCont = AnimationController(vsync: tickerProvider, duration: _FADE_DURATION); @@ -68,17 +68,13 @@ class CursorCont extends ChangeNotifier { } final ValueNotifier show; - final ValueNotifier _blink; + final ValueNotifier blink; final ValueNotifier color; late AnimationController _blinkOpacityCont; Timer? _cursorTimer; bool _targetCursorVisibility = false; CursorStyle _style; - ValueNotifier get cursorBlink => _blink; - - ValueNotifier get cursorColor => color; - CursorStyle get style => _style; set style(CursorStyle value) { @@ -93,7 +89,7 @@ class CursorCont extends ChangeNotifier { stopCursorTimer(); _blinkOpacityCont.dispose(); show.dispose(); - _blink.dispose(); + blink.dispose(); color.dispose(); assert(_cursorTimer == null); super.dispose(); @@ -154,7 +150,7 @@ class CursorCont extends ChangeNotifier { void _onColorTick() { color.value = _style.color.withOpacity(_blinkOpacityCont.value); - _blink.value = show.value && _blinkOpacityCont.value > 0; + blink.value = show.value && _blinkOpacityCont.value > 0; } } diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index ef80a024..2bac327b 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -575,7 +575,7 @@ class RenderEditableTextLine extends RenderEditableBox { } if (containsCursor()) { cursorCont.addListener(markNeedsLayout); - cursorCont.cursorColor.addListener(markNeedsPaint); + cursorCont.color.addListener(markNeedsPaint); } } @@ -587,7 +587,7 @@ class RenderEditableTextLine extends RenderEditableBox { } if (containsCursor()) { cursorCont.removeListener(markNeedsLayout); - cursorCont.cursorColor.removeListener(markNeedsPaint); + cursorCont.color.removeListener(markNeedsPaint); } } @@ -728,7 +728,7 @@ class RenderEditableTextLine extends RenderEditableBox { _body, cursorCont.style, _caretPrototype, - cursorCont.cursorColor.value, + cursorCont.color.value, devicePixelRatio, ); From 1fedaf0d13ec3af514df5cd1591623ed6f46e5ae Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 01:07:25 +0200 Subject: [PATCH 189/306] Add comments to cursor class --- lib/src/widgets/cursor.dart | 135 +++++++++++++++++++++++++-------- lib/src/widgets/text_line.dart | 2 +- 2 files changed, 105 insertions(+), 32 deletions(-) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 67ac45f5..29aeed11 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -5,8 +5,7 @@ import 'package:flutter/widgets.dart'; import 'box.dart'; -const Duration _FADE_DURATION = Duration(milliseconds: 250); - +/// Style properties of editing cursor. class CursorStyle { const CursorStyle({ required this.color, @@ -19,13 +18,52 @@ class CursorStyle { this.paintAboveText = false, }); + /// The color to use when painting the cursor. final Color color; + + /// The color to use when painting the background cursor aligned with the text + /// while rendering the floating cursor. final Color backgroundColor; + + /// How thick the cursor will be. + /// + /// The cursor will draw under the text. The cursor width will extend + /// to the right of the boundary between characters for left-to-right text + /// and to the left for right-to-left text. This corresponds to extending + /// downstream relative to the selected position. Negative values may be used + /// to reverse this behavior. final double width; + + /// How tall the cursor will be. + /// + /// By default, the cursor height is set to the preferred line height of the + /// text. final double? height; + + /// How rounded the corners of the cursor should be. + /// + /// By default, the cursor has no radius. final Radius? radius; + + /// The offset that is used, in pixels, when painting the cursor on screen. + /// + /// By default, the cursor position should be set to an offset of + /// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android + /// platforms. The origin from where the offset is applied to is the arbitrary + /// location where the cursor ends up being rendered from by default. final Offset? offset; + + /// Whether the cursor will animate from fully transparent to fully opaque + /// during each cursor blink. + /// + /// By default, the cursor opacity will animate on iOS platforms and will not + /// animate on Android platforms. final bool opacityAnimates; + + /// If the cursor should be painted on top of the text or underneath it. + /// + /// By default, the cursor should be painted on top for iOS platforms and + /// underneath for Android platforms. final bool paintAboveText; @override @@ -54,6 +92,10 @@ class CursorStyle { paintAboveText.hashCode; } +/// Controls the cursor of an editable widget. +/// +/// This class is a [ChangeNotifier] and allows to listen for updates on the +/// cursor [style]. class CursorCont extends ChangeNotifier { CursorCont({ required this.show, @@ -62,21 +104,35 @@ class CursorCont extends ChangeNotifier { }) : _style = style, blink = ValueNotifier(false), color = ValueNotifier(style.color) { - _blinkOpacityCont = - AnimationController(vsync: tickerProvider, duration: _FADE_DURATION); - _blinkOpacityCont.addListener(_onColorTick); + _blinkOpacityController = + AnimationController(vsync: tickerProvider, duration: _fadeDuration); + _blinkOpacityController.addListener(_onColorTick); } + // The time it takes for the cursor to fade from fully opaque to fully + // transparent and vice versa. A full cursor blink, from transparent to opaque + // to transparent, is twice this duration. + static const Duration _blinkHalfPeriod = Duration(milliseconds: 500); + + // The time the cursor is static in opacity before animating to become + // transparent. + static const Duration _blinkWaitForStart = Duration(milliseconds: 150); + + // This value is an eyeball estimation of the time it takes for the iOS cursor + // to ease in and out. + static const Duration _fadeDuration = Duration(milliseconds: 250); + final ValueNotifier show; - final ValueNotifier blink; final ValueNotifier color; - late AnimationController _blinkOpacityCont; + final ValueNotifier blink; + + late final AnimationController _blinkOpacityController; + Timer? _cursorTimer; bool _targetCursorVisibility = false; - CursorStyle _style; + CursorStyle _style; CursorStyle get style => _style; - set style(CursorStyle value) { if (_style == value) return; _style = value; @@ -85,9 +141,9 @@ class CursorCont extends ChangeNotifier { @override void dispose() { - _blinkOpacityCont.removeListener(_onColorTick); + _blinkOpacityController.removeListener(_onColorTick); stopCursorTimer(); - _blinkOpacityCont.dispose(); + _blinkOpacityController.dispose(); show.dispose(); blink.dispose(); color.dispose(); @@ -99,28 +155,32 @@ class CursorCont extends ChangeNotifier { _targetCursorVisibility = !_targetCursorVisibility; final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; if (style.opacityAnimates) { - _blinkOpacityCont.animateTo(targetOpacity, curve: Curves.easeOut); + // If we want to show the cursor, we will animate the opacity to the value + // of 1.0, and likewise if we want to make it disappear, to 0.0. An easing + // curve is used for the animation to mimic the aesthetics of the native + // iOS cursor. + // + // These values and curves have been obtained through eyeballing, so are + // likely not exactly the same as the values for native iOS. + _blinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut); } else { - _blinkOpacityCont.value = targetOpacity; + _blinkOpacityController.value = targetOpacity; } } - void _cursorWaitForStart(Timer timer) { + void _waitForStart(Timer timer) { _cursorTimer?.cancel(); - _cursorTimer = - Timer.periodic(const Duration(milliseconds: 500), _cursorTick); + _cursorTimer = Timer.periodic(_blinkHalfPeriod, _cursorTick); } void startCursorTimer() { _targetCursorVisibility = true; - _blinkOpacityCont.value = 1.0; + _blinkOpacityController.value = 1.0; if (style.opacityAnimates) { - _cursorTimer = Timer.periodic( - const Duration(milliseconds: 150), _cursorWaitForStart); + _cursorTimer = Timer.periodic(_blinkWaitForStart, _waitForStart); } else { - _cursorTimer = - Timer.periodic(const Duration(milliseconds: 500), _cursorTick); + _cursorTimer = Timer.periodic(_blinkHalfPeriod, _cursorTick); } } @@ -128,10 +188,10 @@ class CursorCont extends ChangeNotifier { _cursorTimer?.cancel(); _cursorTimer = null; _targetCursorVisibility = false; - _blinkOpacityCont.value = 0.0; + _blinkOpacityController.value = 0.0; if (style.opacityAnimates) { - _blinkOpacityCont + _blinkOpacityController ..stop() ..value = 0.0; } @@ -149,32 +209,42 @@ class CursorCont extends ChangeNotifier { } void _onColorTick() { - color.value = _style.color.withOpacity(_blinkOpacityCont.value); - blink.value = show.value && _blinkOpacityCont.value > 0; + color.value = _style.color.withOpacity(_blinkOpacityController.value); + blink.value = show.value && _blinkOpacityController.value > 0; } } +/// Paints the editing cursor. class CursorPainter { - CursorPainter(this.editable, this.style, this.prototype, this.color, - this.devicePixelRatio); + CursorPainter( + this.editable, + this.style, + this.prototype, + this.color, + this.devicePixelRatio, + ); final RenderContentProxyBox? editable; final CursorStyle style; - final Rect? prototype; + final Rect prototype; final Color color; final double devicePixelRatio; + /// Paints cursor on [canvas] at specified [position]. void paint(Canvas canvas, Offset offset, TextPosition position) { - assert(prototype != null); - final caretOffset = editable!.getOffsetForCaret(position, prototype) + offset; - var caretRect = prototype!.shift(caretOffset); + var caretRect = prototype.shift(caretOffset); if (style.offset != null) { caretRect = caretRect.shift(style.offset!); } if (caretRect.left < 0.0) { + // For iOS the cursor may get clipped by the scroll view when + // it's located at a beginning of a line. We ensure that this + // does not happen here. This may result in the cursor being painted + // closer to the character on the right, but it's arguably better + // then painting clipped cursor (or even cursor completely hidden). caretRect = caretRect.shift(Offset(-caretRect.left, 0)); } @@ -185,6 +255,8 @@ class CursorPainter { case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: + // Override the height to take the full height of the glyph at the TextPosition + // when not on iOS. iOS has special handling that creates a taller caret. caretRect = Rect.fromLTWH( caretRect.left, caretRect.top - 2.0, @@ -194,6 +266,7 @@ class CursorPainter { break; case TargetPlatform.iOS: case TargetPlatform.macOS: + // Center the caret vertically along the text. caretRect = Rect.fromLTWH( caretRect.left, caretRect.top + (caretHeight - caretRect.height) / 2, diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 2bac327b..04a9567d 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -727,7 +727,7 @@ class RenderEditableTextLine extends RenderEditableBox { CursorPainter get _cursorPainter => CursorPainter( _body, cursorCont.style, - _caretPrototype, + _caretPrototype!, cursorCont.color.value, devicePixelRatio, ); From 4c11b28cb902549d4ef7bcd0adcf24d8291ce0d4 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 01:20:51 +0200 Subject: [PATCH 190/306] Remove null exception when a disposed controller is set --- lib/src/widgets/cursor.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 29aeed11..44ba564f 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -139,10 +139,18 @@ class CursorCont extends ChangeNotifier { notifyListeners(); } + /// True when this [CursorCont] instance has been disposed. + /// + /// A safety mechanism to prevent the value of a disposed controller from + /// getting set. + bool _isDisposed = false; + @override void dispose() { _blinkOpacityController.removeListener(_onColorTick); stopCursorTimer(); + + _isDisposed = true; _blinkOpacityController.dispose(); show.dispose(); blink.dispose(); @@ -174,6 +182,10 @@ class CursorCont extends ChangeNotifier { } void startCursorTimer() { + if (_isDisposed) { + return; + } + _targetCursorVisibility = true; _blinkOpacityController.value = 1.0; From 102883dfef51f7206464114d17f016ceac7b7f6b Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 10:41:24 +0200 Subject: [PATCH 191/306] Disallow lines longer than 80 characters --- analysis_options.yaml | 1 + example/lib/pages/home_page.dart | 3 ++- example/lib/universal_ui/universal_ui.dart | 7 ++++--- lib/src/models/documents/document.dart | 2 +- lib/src/models/documents/nodes/container.dart | 2 +- lib/src/models/quill_delta.dart | 5 +++-- lib/src/models/rules/insert.dart | 3 ++- lib/src/utils/diff_delta.dart | 3 ++- lib/src/widgets/controller.dart | 2 +- lib/src/widgets/cursor.dart | 5 +++-- lib/src/widgets/editor.dart | 7 ++++--- lib/src/widgets/keyboard_listener.dart | 2 +- lib/src/widgets/simple_viewer.dart | 8 +++++--- lib/src/widgets/text_block.dart | 3 ++- 14 files changed, 32 insertions(+), 21 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 2fc4fec6..06d7ee1c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -17,6 +17,7 @@ linter: - avoid_void_async - cascade_invocations - directives_ordering + - lines_longer_than_80_chars - omit_local_variable_types - prefer_const_constructors - prefer_const_constructors_in_immutables diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index d40b4be2..e0f436f8 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -170,7 +170,8 @@ class _HomePageState extends State { } // Renders the image picked by imagePicker from local file storage - // You can also upload the picked image to any server (eg : AWS s3 or Firebase) and then return the uploaded image URL + // 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 final appDocDir = await getApplicationDocumentsDirectory(); diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart index d1bdc3f5..567aa6b4 100644 --- a/example/lib/universal_ui/universal_ui.dart +++ b/example/lib/universal_ui/universal_ui.dart @@ -50,8 +50,9 @@ Widget defaultEmbedBuilderWeb(BuildContext context, Embed node) { default: throw UnimplementedError( - 'Embeddable type "${node.value.type}" is not supported by default embed ' - 'builder of QuillEditor. You must pass your own builder function to ' - 'embedBuilder property of QuillEditor or QuillField widgets.'); + 'Embeddable type "${node.value.type}" is not supported by default ' + 'embed builder of QuillEditor. You must pass your own builder function ' + 'to embedBuilder property of QuillEditor or QuillField widgets.', + ); } } diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index a26d885a..346da93b 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -250,7 +250,7 @@ class Document { for (final op in doc.toList()) { if (!op.isInsert) { throw ArgumentError.value(doc, - 'Document Delta can only contain insert operations but ${op.key} found.'); + 'Document can only contain insert operations but ${op.key} found.'); } final style = op.attributes != null ? Style.fromJson(op.attributes) : null; diff --git a/lib/src/models/documents/nodes/container.dart b/lib/src/models/documents/nodes/container.dart index dbdd12d1..13f6aa2f 100644 --- a/lib/src/models/documents/nodes/container.dart +++ b/lib/src/models/documents/nodes/container.dart @@ -79,7 +79,7 @@ abstract class Container extends Node { if (last != null) last.adjust(); } - /// Queries the child [Node] at specified character [offset] in this container. + /// Queries the child [Node] at [offset] in this container. /// /// The result may contain the found node or `null` if no node is found /// at specified offset. diff --git a/lib/src/models/quill_delta.dart b/lib/src/models/quill_delta.dart index a0e608be..548e2afa 100644 --- a/lib/src/models/quill_delta.dart +++ b/lib/src/models/quill_delta.dart @@ -1,5 +1,6 @@ -// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code -// is governed by a BSD-style license that can be found in the LICENSE file. +// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this +// source code is governed by a BSD-style license that can be found in the +// LICENSE file. /// Implementation of Quill Delta format in Dart. library quill_delta; diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 5801a10e..9c10d422 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -167,7 +167,8 @@ class AutoExitBlockRule extends InsertRule { // First check if `cur` length is greater than 1, this would indicate // that it contains multiple newline characters which share the same style. // This would mean we are not on the last line yet. - // `cur.value as String` is safe since we already called isEmptyLine and know it contains a newline + // `cur.value as String` is safe since we already called isEmptyLine and + // know it contains a newline if ((cur.value as String).length > 1) { // We are not on the last line of this block, ignore. return null; diff --git a/lib/src/utils/diff_delta.dart b/lib/src/utils/diff_delta.dart index 003bae47..0e09946c 100644 --- a/lib/src/utils/diff_delta.dart +++ b/lib/src/utils/diff_delta.dart @@ -79,7 +79,8 @@ int getPositionDelta(Delta user, Delta actual) { final userOperation = userItr.next(length as int); final actualOperation = actualItr.next(length); if (userOperation.length != actualOperation.length) { - throw 'userOp ${userOperation.length} does not match actualOp ${actualOperation.length}'; + throw 'userOp ${userOperation.length} does not match actualOp ' + '${actualOperation.length}'; } if (userOperation.key == actualOperation.key) { continue; diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index bd669171..9edf805b 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -69,7 +69,7 @@ class QuillController extends ChangeNotifier { // if (this.selection.extentOffset >= document.length) { // // cursor exceeds the length of document, position it in the end // updateSelection( - // TextSelection.collapsed(offset: document.length), ChangeSource.LOCAL); + // TextSelection.collapsed(offset: document.length), ChangeSource.LOCAL); updateSelection( TextSelection.collapsed(offset: selection.baseOffset + len!), ChangeSource.LOCAL); diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 44ba564f..7467c0a3 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -267,8 +267,9 @@ class CursorPainter { case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - // Override the height to take the full height of the glyph at the TextPosition - // when not on iOS. iOS has special handling that creates a taller caret. + // Override the height to take the full height of the glyph at the + // TextPosition when not on iOS. iOS has special handling that + // creates a taller caret. caretRect = Rect.fromLTWH( caretRect.left, caretRect.top - 2.0, diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 662a018c..fb90db76 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -107,9 +107,10 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { : Image.file(io.File(imageUrl)); default: throw UnimplementedError( - 'Embeddable type "${node.value.type}" is not supported by default embed ' - 'builder of QuillEditor. You must pass your own builder function to ' - 'embedBuilder property of QuillEditor or QuillField widgets.'); + 'Embeddable type "${node.value.type}" is not supported by default ' + 'embed builder of QuillEditor. You must pass your own builder function ' + 'to embedBuilder property of QuillEditor or QuillField widgets.', + ); } } diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart index 17c47aad..58df1725 100644 --- a/lib/src/widgets/keyboard_listener.dart +++ b/lib/src/widgets/keyboard_listener.dart @@ -64,7 +64,7 @@ class KeyboardListener { bool handleRawKeyEvent(RawKeyEvent event) { if (kIsWeb) { - // On web platform, we should ignore the key because it's processed already. + // On web platform, we ignore the key because it's already processed. return false; } diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart index ee1a7732..559e5e1a 100644 --- a/lib/src/widgets/simple_viewer.dart +++ b/lib/src/widgets/simple_viewer.dart @@ -109,9 +109,11 @@ class _QuillSimpleViewerState extends State : Image.file(io.File(imageUrl)); default: throw UnimplementedError( - 'Embeddable type "${node.value.type}" is not supported by default embed ' - 'builder of QuillEditor. You must pass your own builder function to ' - 'embedBuilder property of QuillEditor or QuillField widgets.'); + 'Embeddable type "${node.value.type}" is not supported by default ' + 'embed builder of QuillEditor. You must pass your own builder ' + 'function to embedBuilder property of QuillEditor or QuillField ' + 'widgets.', + ); } } diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index f533a160..92c80f39 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -512,7 +512,8 @@ class RenderEditableTextBlock extends RenderEditableContainerBox offset.translate(decorationPadding.left, decorationPadding.top); _painter!.paint(context.canvas, decorationOffset, filledConfiguration); if (debugSaveCount != context.canvas.getSaveCount()) { - throw '${_decoration.runtimeType} painter had mismatching save and restore calls.'; + throw '${_decoration.runtimeType} painter had mismatching save and ' + 'restore calls.'; } if (decoration.isComplex) { context.setIsComplexHint(); From ea0dbd5ce06ad285a77c51e79131e3f185c9af8a Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 10:44:41 +0200 Subject: [PATCH 192/306] Don't create a lambda when a tear-off will do --- analysis_options.yaml | 1 + lib/src/models/rules/insert.dart | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 06d7ee1c..7749c861 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -32,5 +32,6 @@ linter: - prefer_single_quotes - sort_constructors_first - sort_unnamed_constructors_first + - unnecessary_lambdas - unnecessary_parenthesis - unnecessary_string_interpolations diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 9c10d422..f50be23f 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -189,7 +189,7 @@ class AutoExitBlockRule extends InsertRule { // therefore we can exit this block. final attributes = cur.attributes ?? {}; final k = attributes.keys - .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k)); + .firstWhere(Attribute.blockKeysExceptHeader.contains); attributes[k] = null; // retain(1) should be '\n', set it with no attribute return Delta()..retain(index + (len ?? 0))..retain(1, attributes); From 0f6fd64bedd943feec3729103dc6f802445fed30 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 10:49:38 +0200 Subject: [PATCH 193/306] Move ResponsiveWidgets to example folder This widget has nothing to do with the library and is only used in the example, so it is moved to the example. --- example/lib/universal_ui/universal_ui.dart | 2 +- {lib/src => example/lib}/widgets/responsive_widget.dart | 0 lib/flutter_quill.dart | 1 - lib/widgets/responsive_widget.dart | 3 --- 4 files changed, 1 insertion(+), 5 deletions(-) rename {lib/src => example/lib}/widgets/responsive_widget.dart (100%) delete mode 100644 lib/widgets/responsive_widget.dart diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart index 567aa6b4..95047a31 100644 --- a/example/lib/universal_ui/universal_ui.dart +++ b/example/lib/universal_ui/universal_ui.dart @@ -3,9 +3,9 @@ library universal_ui; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_quill/flutter_quill.dart'; - import 'package:universal_html/html.dart' as html; +import '../widgets/responsive_widget.dart'; import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; class PlatformViewRegistryFix { diff --git a/lib/src/widgets/responsive_widget.dart b/example/lib/widgets/responsive_widget.dart similarity index 100% rename from lib/src/widgets/responsive_widget.dart rename to example/lib/widgets/responsive_widget.dart diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index 049786cd..6b6754ce 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -8,5 +8,4 @@ export 'src/models/quill_delta.dart'; export 'src/widgets/controller.dart'; export 'src/widgets/default_styles.dart'; export 'src/widgets/editor.dart'; -export 'src/widgets/responsive_widget.dart'; export 'src/widgets/toolbar.dart'; diff --git a/lib/widgets/responsive_widget.dart b/lib/widgets/responsive_widget.dart deleted file mode 100644 index bb46820f..00000000 --- a/lib/widgets/responsive_widget.dart +++ /dev/null @@ -1,3 +0,0 @@ -/// TODO: Remove this file in the next breaking release, because implementation -/// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/responsive_widget.dart'; From 404857817b47d8e333029760fd2ab1b1f97e0e57 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 11:59:03 +0200 Subject: [PATCH 194/306] Fix null exception --- lib/src/widgets/raw_editor.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index e52e18cd..066ff675 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -559,13 +559,17 @@ class RawEditorState extends EditorState if (widget.scrollable) { _showCaretOnScreenScheduled = false; - final viewport = RenderAbstractViewport.of(getRenderEditor()); + final renderEditor = getRenderEditor(); + if (renderEditor == null) { + return; + } - final editorOffset = getRenderEditor()! - .localToGlobal(const Offset(0, 0), ancestor: viewport); + final viewport = RenderAbstractViewport.of(renderEditor); + final editorOffset = + renderEditor.localToGlobal(const Offset(0, 0), ancestor: viewport); final offsetInViewport = _scrollController!.offset + editorOffset.dy; - final offset = getRenderEditor()!.getOffsetToRevealCursor( + final offset = renderEditor.getOffsetToRevealCursor( _scrollController!.position.viewportDimension, _scrollController!.offset, offsetInViewport, @@ -584,7 +588,7 @@ class RawEditorState extends EditorState @override RenderEditor? getRenderEditor() { - return _editorKey.currentContext!.findRenderObject() as RenderEditor?; + return _editorKey.currentContext?.findRenderObject() as RenderEditor?; } @override From ee45815da522021e292fb95bd879717beecb74f9 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 12:34:09 +0200 Subject: [PATCH 195/306] Remove exception when widget is not mounted --- lib/src/widgets/raw_editor.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 066ff675..bf63dd49 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -483,8 +483,12 @@ class RawEditorState extends EditorState ..startCursorTimer(); } - SchedulerBinding.instance!.addPostFrameCallback( - (_) => _updateOrDisposeSelectionOverlayIfNeeded()); + SchedulerBinding.instance!.addPostFrameCallback((_) { + if (!mounted) { + return; + } + _updateOrDisposeSelectionOverlayIfNeeded(); + }); if (mounted) { setState(() { // Use widget.controller.value in build() From f499c478edf06205f25e6e4bad2f67b42d9ff29c Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 13:25:32 +0200 Subject: [PATCH 196/306] Fix exception when rect is not a number --- lib/src/widgets/cursor.dart | 42 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 7467c0a3..9fef16e5 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -292,25 +292,39 @@ class CursorPainter { } } - final caretPosition = editable!.localToGlobal(caretRect.topLeft); - final pixelMultiple = 1.0 / devicePixelRatio; - caretRect = caretRect.shift(Offset( - caretPosition.dx.isFinite - ? (caretPosition.dx / pixelMultiple).round() * pixelMultiple - - caretPosition.dx - : caretPosition.dx, - caretPosition.dy.isFinite - ? (caretPosition.dy / pixelMultiple).round() * pixelMultiple - - caretPosition.dy - : caretPosition.dy)); + final pixelPerfectOffset = + _getPixelPerfectCursorOffset(editable!, caretRect, devicePixelRatio); + if (!pixelPerfectOffset.isFinite) { + return; + } + caretRect = caretRect.shift(pixelPerfectOffset); final paint = Paint()..color = color; if (style.radius == null) { canvas.drawRect(caretRect, paint); - return; + } else { + final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!); + canvas.drawRRect(caretRRect, paint); } + } + + Offset _getPixelPerfectCursorOffset( + RenderContentProxyBox editable, + Rect caretRect, + double devicePixelRatio, + ) { + final caretPosition = editable.localToGlobal(caretRect.topLeft); + final pixelMultiple = 1.0 / devicePixelRatio; + + final pixelPerfectOffsetX = caretPosition.dx.isFinite + ? (caretPosition.dx / pixelMultiple).round() * pixelMultiple - + caretPosition.dx + : caretPosition.dx; + final pixelPerfectOffsetY = caretPosition.dy.isFinite + ? (caretPosition.dy / pixelMultiple).round() * pixelMultiple - + caretPosition.dy + : caretPosition.dy; - final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!); - canvas.drawRRect(caretRRect, paint); + return Offset(pixelPerfectOffsetX, pixelPerfectOffsetY); } } From 3d6a915d2684933b26fc2438456319b6c6e094ee Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 13:52:32 +0200 Subject: [PATCH 197/306] Fix paste (#236) closes #235. --- lib/src/widgets/raw_editor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index bf63dd49..b94472b6 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -680,7 +680,7 @@ class RawEditorState extends EditorState @override void userUpdateTextEditingValue( TextEditingValue value, SelectionChangedCause cause) { - // TODO: implement userUpdateTextEditingValue + updateEditingValue(value); } } From e6f1160d7107262e0fc807d31aa46d026520c9cd Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 14:48:06 +0200 Subject: [PATCH 198/306] Fix exception --- .../raw_editor_state_text_input_client_mixin.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index 527df582..bfb36106 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -103,7 +103,11 @@ mixin RawEditorStateTextInputClientMixin on EditorState final shouldRemember = getTextEditingValue().text != _lastKnownRemoteTextEditingValue!.text; _lastKnownRemoteTextEditingValue = actualValue; - _textInputConnection!.setEditingState(actualValue); + _textInputConnection!.setEditingState( + // Set composing to (-1, -1), otherwise an exception will be thrown if + // the values are different. + actualValue.copyWith(composing: const TextRange(start: -1, end: -1)), + ); if (shouldRemember) { // Only keep track if text changed (selection changes are not relevant) _sentRemoteValues.add(actualValue); From f0459ba4d5aa3fb91c643e895b3c9c3ed924e76d Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 22 May 2021 15:20:52 +0200 Subject: [PATCH 199/306] Add const types for image and divider embeds This allows to reference the type. --- lib/src/models/documents/nodes/embed.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/models/documents/nodes/embed.dart b/lib/src/models/documents/nodes/embed.dart index d6fe628a..07e6a6b3 100644 --- a/lib/src/models/documents/nodes/embed.dart +++ b/lib/src/models/documents/nodes/embed.dart @@ -4,7 +4,7 @@ /// /// * [BlockEmbed] which represents a block embed. class Embeddable { - Embeddable(this.type, this.data); + const Embeddable(this.type, this.data); /// The type of this object. final String type; @@ -32,9 +32,11 @@ class Embeddable { /// 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); + const BlockEmbed(String type, String data) : super(type, data); - static BlockEmbed horizontalRule = BlockEmbed('divider', 'hr'); + static const String horizontalRuleType = 'divider'; + static BlockEmbed horizontalRule = const BlockEmbed(horizontalRuleType, 'hr'); - static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl); + static const String imageType = 'image'; + static BlockEmbed image(String imageUrl) => BlockEmbed(imageType, imageUrl); } From c0f0ded452a8c5ff8c4066d82d59ad8de8368a60 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sat, 22 May 2021 18:26:26 -0700 Subject: [PATCH 200/306] Fix relative path --- lib/models/documents/attribute.dart | 2 +- lib/models/documents/document.dart | 2 +- lib/models/documents/history.dart | 2 +- lib/models/documents/style.dart | 2 +- lib/models/quill_delta.dart | 2 +- lib/models/rules/delete.dart | 2 +- lib/models/rules/format.dart | 2 +- lib/models/rules/insert.dart | 2 +- lib/models/rules/rule.dart | 2 +- lib/utils/color.dart | 2 +- lib/utils/diff_delta.dart | 2 +- lib/widgets/box.dart | 2 +- lib/widgets/controller.dart | 2 +- lib/widgets/cursor.dart | 2 +- lib/widgets/default_styles.dart | 2 +- lib/widgets/delegate.dart | 2 +- lib/widgets/editor.dart | 2 +- lib/widgets/image.dart | 2 +- lib/widgets/keyboard_listener.dart | 2 +- lib/widgets/proxy.dart | 2 +- lib/widgets/raw_editor.dart | 2 +- lib/widgets/simple_viewer.dart | 2 +- lib/widgets/text_block.dart | 2 +- lib/widgets/text_line.dart | 2 +- lib/widgets/text_selection.dart | 2 +- lib/widgets/toolbar.dart | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 7411e232..e106383e 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/documents/attribute.dart'; +export '../../src/models/documents/attribute.dart'; diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart index d946618a..a187d19d 100644 --- a/lib/models/documents/document.dart +++ b/lib/models/documents/document.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/documents/document.dart'; +export '../../src/models/documents/document.dart'; diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart index 3ff6870e..b07c8e33 100644 --- a/lib/models/documents/history.dart +++ b/lib/models/documents/history.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/documents/history.dart'; +export '../../src/models/documents/history.dart'; diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index a4e06de4..6df9412b 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/documents/style.dart'; +export '../../src/models/documents/style.dart'; diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart index 477fbe33..796d68ca 100644 --- a/lib/models/quill_delta.dart +++ b/lib/models/quill_delta.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/quill_delta.dart'; +export '../src/models/quill_delta.dart'; diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart index 65a27b0e..0430686b 100644 --- a/lib/models/rules/delete.dart +++ b/lib/models/rules/delete.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/rules/delete.dart'; +export '../../src/models/rules/delete.dart'; diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart index e6251d03..7d642af3 100644 --- a/lib/models/rules/format.dart +++ b/lib/models/rules/format.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/rules/format.dart'; +export '../../src/models/rules/format.dart'; diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart index 4dfe6ab7..04e7a4fa 100644 --- a/lib/models/rules/insert.dart +++ b/lib/models/rules/insert.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/rules/insert.dart'; +export '../../src/models/rules/insert.dart'; diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart index 11026f46..ccea0dda 100644 --- a/lib/models/rules/rule.dart +++ b/lib/models/rules/rule.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/models/rules/rule.dart'; +export '../../src/models/rules/rule.dart'; diff --git a/lib/utils/color.dart b/lib/utils/color.dart index f126cf52..cfd8f803 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/utils/color.dart'; +export '../src/utils/color.dart'; diff --git a/lib/utils/diff_delta.dart b/lib/utils/diff_delta.dart index 08d30f51..607093c4 100644 --- a/lib/utils/diff_delta.dart +++ b/lib/utils/diff_delta.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../../src/utils/diff_delta.dart'; +export '../src/utils/diff_delta.dart'; diff --git a/lib/widgets/box.dart b/lib/widgets/box.dart index d97c610a..511c8149 100644 --- a/lib/widgets/box.dart +++ b/lib/widgets/box.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/box.dart'; +export '../src/widgets/box.dart'; diff --git a/lib/widgets/controller.dart b/lib/widgets/controller.dart index e1177f78..82cab553 100644 --- a/lib/widgets/controller.dart +++ b/lib/widgets/controller.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/controller.dart'; +export '../src/widgets/controller.dart'; diff --git a/lib/widgets/cursor.dart b/lib/widgets/cursor.dart index 3528ad16..540c9011 100644 --- a/lib/widgets/cursor.dart +++ b/lib/widgets/cursor.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/cursor.dart'; +export '../src/widgets/cursor.dart'; diff --git a/lib/widgets/default_styles.dart b/lib/widgets/default_styles.dart index 3fffda6f..6dd63fc8 100644 --- a/lib/widgets/default_styles.dart +++ b/lib/widgets/default_styles.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/default_styles.dart'; +export '../src/widgets/default_styles.dart'; diff --git a/lib/widgets/delegate.dart b/lib/widgets/delegate.dart index c1db553e..3a5057d2 100644 --- a/lib/widgets/delegate.dart +++ b/lib/widgets/delegate.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/delegate.dart'; +export '../src/widgets/delegate.dart'; diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index c0d754f9..db30762d 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/editor.dart'; +export '../src/widgets/editor.dart'; diff --git a/lib/widgets/image.dart b/lib/widgets/image.dart index 41a8a235..547279d1 100644 --- a/lib/widgets/image.dart +++ b/lib/widgets/image.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/image.dart'; +export '../src/widgets/image.dart'; diff --git a/lib/widgets/keyboard_listener.dart b/lib/widgets/keyboard_listener.dart index 0ee8d6e7..20c72b74 100644 --- a/lib/widgets/keyboard_listener.dart +++ b/lib/widgets/keyboard_listener.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/keyboard_listener.dart'; +export '../src/widgets/keyboard_listener.dart'; diff --git a/lib/widgets/proxy.dart b/lib/widgets/proxy.dart index 6d17bb7d..247f4897 100644 --- a/lib/widgets/proxy.dart +++ b/lib/widgets/proxy.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/proxy.dart'; +export '../src/widgets/proxy.dart'; diff --git a/lib/widgets/raw_editor.dart b/lib/widgets/raw_editor.dart index cf483dd1..84ebf6f2 100644 --- a/lib/widgets/raw_editor.dart +++ b/lib/widgets/raw_editor.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/raw_editor.dart'; +export '../src/widgets/raw_editor.dart'; diff --git a/lib/widgets/simple_viewer.dart b/lib/widgets/simple_viewer.dart index 219babec..fbea8404 100644 --- a/lib/widgets/simple_viewer.dart +++ b/lib/widgets/simple_viewer.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/simple_viewer.dart'; +export '../src/widgets/simple_viewer.dart'; diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 3e46ea80..b58e01ae 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/text_block.dart'; +export '../src/widgets/text_block.dart'; diff --git a/lib/widgets/text_line.dart b/lib/widgets/text_line.dart index 7c8dcc80..0d0e4098 100644 --- a/lib/widgets/text_line.dart +++ b/lib/widgets/text_line.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/text_line.dart'; +export '../src/widgets/text_line.dart'; diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index b35db3ea..e4c3f5e4 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/text_selection.dart'; +export '../src/widgets/text_selection.dart'; diff --git a/lib/widgets/toolbar.dart b/lib/widgets/toolbar.dart index c47f1353..1f9d4827 100644 --- a/lib/widgets/toolbar.dart +++ b/lib/widgets/toolbar.dart @@ -1,3 +1,3 @@ /// TODO: Remove this file in the next breaking release, because implementation /// files should be located in the src folder, https://bit.ly/3fA23Yz. -export '../../src/widgets/toolbar.dart'; +export '../src/widgets/toolbar.dart'; From 8c3617c669567b535a74a6e4879207dd73bb1adc Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Mon, 24 May 2021 20:10:54 +0200 Subject: [PATCH 201/306] Add new logo --- README.md | 70 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b7c1a247..bbedb500 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,35 @@ +

+ +

+

A rich text editor for Flutter

+ +[![MIT License][license-badge]][license-link] +[![PRs Welcome][prs-badge]][prs-link] +[![Watch on GitHub][github-watch-badge]][github-watch-link] +[![Star on GitHub][github-star-badge]][github-star-link] +[![Watch on GitHub][github-forks-badge]][github-forks-link] + +[license-badge]: https://img.shields.io/github/license/singerdmx/flutter-quill.svg?style=for-the-badge +[license-link]: https://github.com/singerdmx/flutter-quill/blob/master/LICENSE +[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge +[prs-link]: https://github.com/singerdmx/flutter-quill/issues +[github-watch-badge]: https://img.shields.io/github/watchers/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff +[github-watch-link]: https://github.com/singerdmx/flutter-quill/watchers +[github-star-badge]: https://img.shields.io/github/stars/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff +[github-star-link]: https://github.com/singerdmx/flutter-quill/stargazers +[github-forks-badge]: https://img.shields.io/github/forks/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff +[github-forks-link]: https://github.com/singerdmx/flutter-quill/network/members + + +FlutterQuill is a rich text editor and a [Quill] component for [Flutter]. - - - -# FlutterQuill - -FlutterQuill is a rich text editor and a [Quill] component for [Flutter]. - This library is a WYSIWYG editor built for the modern mobile platform, with web compatibility under development. You can join our [Slack Group] for discussion. Demo App: https://bulletjournal.us/home/index.html Pub: https://pub.dev/packages/flutter_quill -## Usage +## Usage See the `example` directory for a minimal example of how to use FlutterQuill. You typically just need to instantiate a controller: @@ -79,17 +94,30 @@ It is required to provide EmbedBuilder, e.g. [defaultEmbedBuilderWeb](https://gi ## Migrate Zefyr Data Check out [code](https://github.com/jwehrle/zefyr_quill_convert) and [doc](https://docs.google.com/document/d/1FUSrpbarHnilb7uDN5J5DDahaI0v1RMXBjj4fFSpSuY/edit?usp=sharing). - ---- - -1 -1 -1 -1 - + +--- + +

+ 1 + 1 +

+ + +

+ 1 + 1 +

+ +## Sponsors + + + + [Quill]: https://quilljs.com/docs/formats -[Flutter]: https://github.com/flutter/flutter -[FlutterQuill]: https://pub.dev/packages/flutter_quill -[ReactQuill]: https://github.com/zenoamaro/react-quill +[Flutter]: https://github.com/flutter/flutter +[FlutterQuill]: https://pub.dev/packages/flutter_quill +[ReactQuill]: https://github.com/zenoamaro/react-quill [Slack Group]: https://join.slack.com/t/bulletjournal1024/shared_invite/zt-fys7t9hi-ITVU5PGDen1rNRyCjdcQ2g [Sample Page]: https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart From 30a9747b1d0ba41b2544b6b65bac4b78fdb2d5fa Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Mon, 24 May 2021 22:38:08 +0200 Subject: [PATCH 202/306] Fix buttons which ignore toolbariconsize Closes #189. --- lib/src/models/rules/insert.dart | 4 +- lib/src/widgets/controller.dart | 32 +- lib/src/widgets/toolbar.dart | 1142 ++--------------- .../widgets/toolbar/clear_format_button.dart | 42 + lib/src/widgets/toolbar/color_button.dart | 153 +++ lib/src/widgets/toolbar/history_button.dart | 78 ++ lib/src/widgets/toolbar/image_button.dart | 117 ++ lib/src/widgets/toolbar/indent_button.dart | 61 + .../widgets/toolbar/insert_embed_button.dart | 41 + .../widgets/toolbar/link_style_button.dart | 122 ++ .../toolbar/quill_dropdown_button.dart | 96 ++ .../widgets/toolbar/quill_icon_button.dart | 37 + .../toolbar/select_header_style_button.dart | 122 ++ .../toolbar/toggle_check_list_button.dart | 104 ++ .../widgets/toolbar/toggle_style_button.dart | 139 ++ 15 files changed, 1224 insertions(+), 1066 deletions(-) create mode 100644 lib/src/widgets/toolbar/clear_format_button.dart create mode 100644 lib/src/widgets/toolbar/color_button.dart create mode 100644 lib/src/widgets/toolbar/history_button.dart create mode 100644 lib/src/widgets/toolbar/image_button.dart create mode 100644 lib/src/widgets/toolbar/indent_button.dart create mode 100644 lib/src/widgets/toolbar/insert_embed_button.dart create mode 100644 lib/src/widgets/toolbar/link_style_button.dart create mode 100644 lib/src/widgets/toolbar/quill_dropdown_button.dart create mode 100644 lib/src/widgets/toolbar/quill_icon_button.dart create mode 100644 lib/src/widgets/toolbar/select_header_style_button.dart create mode 100644 lib/src/widgets/toolbar/toggle_check_list_button.dart create mode 100644 lib/src/widgets/toolbar/toggle_style_button.dart diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index f50be23f..60211ab2 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -188,8 +188,8 @@ class AutoExitBlockRule extends InsertRule { // Here we now know that the line after `cur` is not in the same block // therefore we can exit this block. final attributes = cur.attributes ?? {}; - final k = attributes.keys - .firstWhere(Attribute.blockKeysExceptHeader.contains); + final k = + attributes.keys.firstWhere(Attribute.blockKeysExceptHeader.contains); attributes[k] = null; // retain(1) should be '\n', set it with no attribute return Delta()..retain(index + (len ?? 0))..retain(1, attributes); diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 9edf805b..7de1a122 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -11,11 +11,10 @@ import '../models/quill_delta.dart'; import '../utils/diff_delta.dart'; class QuillController extends ChangeNotifier { - QuillController( - {required this.document, - required this.selection, - this.iconSize = 18, - this.toolbarHeightFactor = 2}); + QuillController({ + required this.document, + required TextSelection selection, + }) : _selection = selection; factory QuillController.basic() { return QuillController( @@ -24,19 +23,24 @@ class QuillController extends ChangeNotifier { ); } + /// Document managed by this controller. final Document document; - TextSelection selection; - double iconSize; - double toolbarHeightFactor; + /// Currently selected text within the [document]. + TextSelection get selection => _selection; + TextSelection _selection; + + /// Store any styles attribute that got toggled by the tap of a button + /// and that has not been applied yet. + /// It gets reset after each format action within the [document]. Style toggledStyle = Style(); + bool ignoreFocusOnTextChange = false; - /// Controls whether this [QuillController] instance has already been disposed - /// of + /// True when this [QuillController] instance has been disposed. /// - /// This is a safe approach to make sure that listeners don't crash when - /// adding, removing or listeners to this instance. + /// A safety mechanism to ensure that listeners don't crash when adding, + /// removing or listeners to this instance. bool _isDisposed = false; // item1: Document state before [change]. @@ -220,9 +224,9 @@ class QuillController extends ChangeNotifier { } void _updateSelection(TextSelection textSelection, ChangeSource source) { - selection = textSelection; + _selection = textSelection; final end = document.length - 1; - selection = selection.copyWith( + _selection = selection.copyWith( baseOffset: math.min(selection.baseOffset, end), extentOffset: math.min(selection.extentOffset, end)); } diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index c5fe2bab..d4d00f5d 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -1,921 +1,54 @@ import 'dart:io'; -import 'package:file_picker/file_picker.dart'; -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: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'; +import 'toolbar/clear_format_button.dart'; +import 'toolbar/color_button.dart'; +import 'toolbar/history_button.dart'; +import 'toolbar/image_button.dart'; +import 'toolbar/indent_button.dart'; +import 'toolbar/insert_embed_button.dart'; +import 'toolbar/link_style_button.dart'; +import 'toolbar/select_header_style_button.dart'; +import 'toolbar/toggle_check_list_button.dart'; +import 'toolbar/toggle_style_button.dart'; + +export 'toolbar/clear_format_button.dart'; +export 'toolbar/color_button.dart'; +export 'toolbar/history_button.dart'; +export 'toolbar/image_button.dart'; +export 'toolbar/indent_button.dart'; +export 'toolbar/insert_embed_button.dart'; +export 'toolbar/link_style_button.dart'; +export 'toolbar/quill_dropdown_button.dart'; +export 'toolbar/quill_icon_button.dart'; +export 'toolbar/select_header_style_button.dart'; +export 'toolbar/toggle_check_list_button.dart'; +export 'toolbar/toggle_style_button.dart'; typedef OnImagePickCallback = Future Function(File file); typedef ImagePickImpl = Future Function(ImageSource source); -class InsertEmbedButton extends StatelessWidget { - const InsertEmbedButton({ - required this.controller, - required this.icon, - this.fillColor, - Key? key, - }) : super(key: key); - - final QuillController controller; - final IconData icon; - final Color? fillColor; - - @override - Widget build(BuildContext context) { - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: controller.iconSize * 1.77, - icon: Icon( - icon, - size: controller.iconSize, - color: Theme.of(context).iconTheme.color, - ), - fillColor: fillColor ?? Theme.of(context).canvasColor, - onPressed: () { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - controller.replaceText(index, length, BlockEmbed.horizontalRule, null); - }, - ); - } -} - -class LinkStyleButton extends StatefulWidget { - const LinkStyleButton({ - required this.controller, - this.icon, - Key? key, - }) : super(key: key); - - final QuillController controller; - final IconData? icon; - - @override - _LinkStyleButtonState createState() => _LinkStyleButtonState(); -} - -class _LinkStyleButtonState extends State { - void _didChangeSelection() { - setState(() {}); - } - - @override - void initState() { - super.initState(); - widget.controller.addListener(_didChangeSelection); - } - - @override - void didUpdateWidget(covariant LinkStyleButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeSelection); - widget.controller.addListener(_didChangeSelection); - } - } - - @override - void dispose() { - super.dispose(); - widget.controller.removeListener(_didChangeSelection); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isEnabled = !widget.controller.selection.isCollapsed; - final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: Icon( - widget.icon ?? Icons.link, - size: widget.controller.iconSize, - color: isEnabled ? theme.iconTheme.color : theme.disabledColor, - ), - fillColor: Theme.of(context).canvasColor, - onPressed: pressedHandler, - ); - } - - void _openLinkDialog(BuildContext context) { - showDialog( - context: context, - builder: (ctx) { - return const _LinkDialog(); - }, - ).then(_linkSubmitted); - } - - void _linkSubmitted(String? value) { - if (value == null || value.isEmpty) { - return; - } - widget.controller.formatSelection(LinkAttribute(value)); - } -} - -class _LinkDialog extends StatefulWidget { - const _LinkDialog({Key? key}) : super(key: key); - - @override - _LinkDialogState createState() => _LinkDialogState(); -} - -class _LinkDialogState extends State<_LinkDialog> { - String _link = ''; - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: TextField( - decoration: const InputDecoration(labelText: 'Paste a link'), - autofocus: true, - onChanged: _linkChanged, - ), - actions: [ - TextButton( - onPressed: _link.isNotEmpty ? _applyLink : null, - child: const Text('Apply'), - ), - ], - ); - } - - void _linkChanged(String value) { - setState(() { - _link = value; - }); - } - - void _applyLink() { - Navigator.pop(context, _link); - } -} - -typedef ToggleStyleButtonBuilder = Widget Function( - BuildContext context, - Attribute attribute, - IconData icon, - Color? fillColor, - bool? isToggled, - VoidCallback? onPressed, -); - -class ToggleStyleButton extends StatefulWidget { - const ToggleStyleButton({ - required this.attribute, - required this.icon, - required this.controller, - this.fillColor, - this.childBuilder = defaultToggleStyleButtonBuilder, - Key? key, - }) : super(key: key); - - final Attribute attribute; - - final IconData icon; - - final Color? fillColor; - - final QuillController controller; - - final ToggleStyleButtonBuilder childBuilder; - - @override - _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); -} - -class _ToggleStyleButtonState extends State { - bool? _isToggled; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _isToggled = - _getIsToggled(widget.controller.getSelectionStyle().attributes); - }); - } - - @override - void initState() { - super.initState(); - _isToggled = _getIsToggled(_selectionStyle.attributes); - widget.controller.addListener(_didChangeEditingValue); - } - - bool _getIsToggled(Map attrs) { - if (widget.attribute.key == Attribute.list.key) { - final attribute = attrs[widget.attribute.key]; - if (attribute == null) { - return false; - } - return attribute.value == widget.attribute.value; - } - return attrs.containsKey(widget.attribute.key); - } - - @override - void didUpdateWidget(covariant ToggleStyleButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _isToggled = _getIsToggled(_selectionStyle.attributes); - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isInCodeBlock = - _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); - final isEnabled = - !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key; - return widget.childBuilder(context, widget.attribute, widget.icon, - widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); - } - - void _toggleAttribute() { - widget.controller.formatSelection(_isToggled! - ? Attribute.clone(widget.attribute, null) - : widget.attribute); - } -} - -class ToggleCheckListButton extends StatefulWidget { - const ToggleCheckListButton({ - required this.icon, - required this.controller, - required this.attribute, - this.fillColor, - this.childBuilder = defaultToggleStyleButtonBuilder, - Key? key, - }) : super(key: key); - - final IconData icon; - - final Color? fillColor; - - final QuillController controller; - - final ToggleStyleButtonBuilder childBuilder; - - final Attribute attribute; - - @override - _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); -} - -class _ToggleCheckListButtonState extends State { - bool? _isToggled; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _isToggled = - _getIsToggled(widget.controller.getSelectionStyle().attributes); - }); - } - - @override - void initState() { - super.initState(); - _isToggled = _getIsToggled(_selectionStyle.attributes); - widget.controller.addListener(_didChangeEditingValue); - } - - bool _getIsToggled(Map attrs) { - if (widget.attribute.key == Attribute.list.key) { - final attribute = attrs[widget.attribute.key]; - if (attribute == null) { - return false; - } - return attribute.value == widget.attribute.value || - attribute.value == Attribute.checked.value; - } - return attrs.containsKey(widget.attribute.key); - } - - @override - void didUpdateWidget(covariant ToggleCheckListButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _isToggled = _getIsToggled(_selectionStyle.attributes); - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isInCodeBlock = - _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); - final isEnabled = - !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key; - return widget.childBuilder(context, Attribute.unchecked, widget.icon, - widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null); - } - - void _toggleAttribute() { - widget.controller.formatSelection(_isToggled! - ? Attribute.clone(Attribute.unchecked, null) - : Attribute.unchecked); - } -} - -Widget defaultToggleStyleButtonBuilder( - BuildContext context, - Attribute attribute, - IconData icon, - Color? fillColor, - bool? isToggled, - VoidCallback? onPressed, -) { - final theme = Theme.of(context); - final isEnabled = onPressed != null; - final iconColor = isEnabled - ? isToggled == true - ? theme.primaryIconTheme.color - : theme.iconTheme.color - : theme.disabledColor; - final fill = isToggled == true - ? theme.toggleableActiveColor - : fillColor ?? theme.canvasColor; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: 18 * 1.77, - icon: Icon(icon, size: 18, color: iconColor), - fillColor: fill, - onPressed: onPressed, - ); -} - -class SelectHeaderStyleButton extends StatefulWidget { - const SelectHeaderStyleButton({required this.controller, Key? key}) - : super(key: key); - - final QuillController controller; - - @override - _SelectHeaderStyleButtonState createState() => - _SelectHeaderStyleButtonState(); -} - -class _SelectHeaderStyleButtonState extends State { - Attribute? _value; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _value = - _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; - }); - } - - void _selectAttribute(value) { - widget.controller.formatSelection(value); - } - - @override - void initState() { - super.initState(); - setState(() { - _value = - _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; - }); - widget.controller.addListener(_didChangeEditingValue); - } - - @override - void didUpdateWidget(covariant SelectHeaderStyleButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _value = - _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return _selectHeadingStyleButtonBuilder( - context, _value, _selectAttribute, widget.controller.iconSize); - } -} - -Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value, - ValueChanged onSelected, double iconSize) { - final _valueToText = { - Attribute.header: 'N', - Attribute.h1: 'H1', - Attribute.h2: 'H2', - Attribute.h3: 'H3', - }; - - final _valueAttribute = [ - Attribute.header, - Attribute.h1, - Attribute.h2, - Attribute.h3 - ]; - final _valueString = ['N', 'H1', 'H2', 'H3']; - - final theme = Theme.of(context); - final style = TextStyle( - fontWeight: FontWeight.w600, - fontSize: iconSize * 0.7, - ); - - return Row( - mainAxisSize: MainAxisSize.min, - children: List.generate(4, (index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), - child: ConstrainedBox( - constraints: BoxConstraints.tightFor( - width: iconSize * 1.77, - height: iconSize * 1.77, - ), - child: RawMaterialButton( - hoverElevation: 0, - highlightElevation: 0, - elevation: 0, - visualDensity: VisualDensity.compact, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - fillColor: _valueToText[value] == _valueString[index] - ? theme.toggleableActiveColor - : theme.canvasColor, - onPressed: () { - onSelected(_valueAttribute[index]); - }, - child: Text( - _valueString[index], - style: style.copyWith( - color: _valueToText[value] == _valueString[index] - ? theme.primaryIconTheme.color - : theme.iconTheme.color, - ), - ), - ), - ), - ); - }), - ); -} - -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; +// The default size of the icon of a button. +const double kDefaultIconSize = 18; - final OnImagePickCallback? onImagePickCallback; +// The factor of how much larger the button is in relation to the icon. +const double kIconButtonFactor = 1.77; - final ImagePickImpl? imagePickImpl; - - final ImageSource imageSource; - - @override - _ImageButtonState createState() => _ImageButtonState(); -} - -class _ImageButtonState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return QuillIconButton( - icon: Icon( - widget.icon, - size: widget.controller.iconSize, - color: theme.iconTheme.color, - ), - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - fillColor: theme.canvasColor, - onPressed: _handleImageButtonTap, - ); - } - - Future _handleImageButtonTap() async { - final index = widget.controller.selection.baseOffset; - final length = widget.controller.selection.extentOffset - index; - - String? imageUrl; - if (widget.imagePickImpl != null) { - imageUrl = await widget.imagePickImpl!(widget.imageSource); - } else { - if (kIsWeb) { - imageUrl = await _pickImageWeb(); - } else if (Platform.isAndroid || Platform.isIOS) { - imageUrl = await _pickImage(widget.imageSource); - } else { - imageUrl = await _pickImageDesktop(); - } - } - - if (imageUrl != null) { - widget.controller - .replaceText(index, length, BlockEmbed.image(imageUrl), null); - } - } - - Future _pickImageWeb() async { - final result = await FilePicker.platform.pickFiles(); - if (result == null) { - return null; - } - - // Take first, because we don't allow picking multiple files. - final fileName = result.files.first.name!; - final file = File(fileName); - - return widget.onImagePickCallback!(file); - } - - Future _pickImage(ImageSource source) async { - final pickedFile = await ImagePicker().getImage(source: source); - if (pickedFile == null) { - return null; - } - - return widget.onImagePickCallback!(File(pickedFile.path)); - } - - Future _pickImageDesktop() async { - final filePath = await FilesystemPicker.open( - context: context, - rootDirectory: await getApplicationDocumentsDirectory(), - fsType: FilesystemType.file, - fileTileSelectMode: FileTileSelectMode.wholeTile, - ); - if (filePath == null || filePath.isEmpty) return null; - - final file = File(filePath); - return widget.onImagePickCallback!(file); - } -} - -/// Controls color styles. -/// -/// When pressed, this button displays overlay toolbar with -/// buttons for each color. -class ColorButton extends StatefulWidget { - const ColorButton({ - required this.icon, - required this.controller, - required this.background, - Key? key, - }) : super(key: key); - - final IconData icon; - final bool background; - final QuillController controller; - - @override - _ColorButtonState createState() => _ColorButtonState(); -} - -class _ColorButtonState extends State { - late bool _isToggledColor; - late bool _isToggledBackground; - late bool _isWhite; - late bool _isWhitebackground; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _isToggledColor = - _getIsToggledColor(widget.controller.getSelectionStyle().attributes); - _isToggledBackground = _getIsToggledBackground( - widget.controller.getSelectionStyle().attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - }); - } - - @override - void initState() { - super.initState(); - _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); - _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - widget.controller.addListener(_didChangeEditingValue); - } - - bool _getIsToggledColor(Map attrs) { - return attrs.containsKey(Attribute.color.key); - } - - bool _getIsToggledBackground(Map attrs) { - return attrs.containsKey(Attribute.background.key); - } - - @override - void didUpdateWidget(covariant ColorButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); - _isToggledBackground = - _getIsToggledBackground(_selectionStyle.attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = _isToggledColor && !widget.background && !_isWhite - ? stringToColor(_selectionStyle.attributes['color']!.value) - : theme.iconTheme.color; - - final iconColorBackground = - _isToggledBackground && widget.background && !_isWhitebackground - ? stringToColor(_selectionStyle.attributes['background']!.value) - : theme.iconTheme.color; - - final fillColor = _isToggledColor && !widget.background && _isWhite - ? stringToColor('#ffffff') - : theme.canvasColor; - final fillColorBackground = - _isToggledBackground && widget.background && _isWhitebackground - ? stringToColor('#ffffff') - : theme.canvasColor; - - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, - size: widget.controller.iconSize, - color: widget.background ? iconColorBackground : iconColor), - fillColor: widget.background ? fillColorBackground : fillColor, - onPressed: _showColorPicker, - ); - } - - void _changeColor(Color color) { - var hex = color.value.toRadixString(16); - if (hex.startsWith('ff')) { - hex = hex.substring(2); - } - hex = '#$hex'; - widget.controller.formatSelection( - widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex)); - Navigator.of(context).pop(); - } - - void _showColorPicker() { - showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('Select Color'), - backgroundColor: Theme.of(context).canvasColor, - content: SingleChildScrollView( - child: MaterialPicker( - pickerColor: const Color(0x00000000), - onColorChanged: _changeColor, - ), - )), - ); - } -} - -class HistoryButton extends StatefulWidget { - const HistoryButton({ - required this.icon, - required this.controller, - required this.undo, - Key? key, - }) : super(key: key); - - final IconData icon; - final bool undo; - final QuillController controller; - - @override - _HistoryButtonState createState() => _HistoryButtonState(); -} - -class _HistoryButtonState extends State { - Color? _iconColor; - late ThemeData theme; - - @override - Widget build(BuildContext context) { - theme = Theme.of(context); - _setIconColor(); - - final fillColor = theme.canvasColor; - widget.controller.changes.listen((event) async { - _setIconColor(); - }); - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, - size: widget.controller.iconSize, color: _iconColor), - fillColor: fillColor, - onPressed: _changeHistory, - ); - } - - void _setIconColor() { - if (!mounted) return; - - if (widget.undo) { - setState(() { - _iconColor = widget.controller.hasUndo - ? theme.iconTheme.color - : theme.disabledColor; - }); - } else { - setState(() { - _iconColor = widget.controller.hasRedo - ? theme.iconTheme.color - : theme.disabledColor; - }); - } - } - - void _changeHistory() { - if (widget.undo) { - if (widget.controller.hasUndo) { - widget.controller.undo(); - } - } else { - if (widget.controller.hasRedo) { - widget.controller.redo(); - } - } - - _setIconColor(); - } -} - -class IndentButton extends StatefulWidget { - const IndentButton({ - required this.icon, - required this.controller, - required this.isIncrease, - Key? key, - }) : super(key: key); - - final IconData icon; - final QuillController controller; - final bool isIncrease; - - @override - _IndentButtonState createState() => _IndentButtonState(); -} - -class _IndentButtonState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = theme.iconTheme.color; - final fillColor = theme.canvasColor; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: - Icon(widget.icon, size: widget.controller.iconSize, color: iconColor), - fillColor: fillColor, - onPressed: () { - final indent = widget.controller - .getSelectionStyle() - .attributes[Attribute.indent.key]; - if (indent == null) { - if (widget.isIncrease) { - widget.controller.formatSelection(Attribute.indentL1); - } - return; - } - if (indent.value == 1 && !widget.isIncrease) { - widget.controller - .formatSelection(Attribute.clone(Attribute.indentL1, null)); - return; - } - if (widget.isIncrease) { - widget.controller - .formatSelection(Attribute.getIndentLevel(indent.value + 1)); - return; - } - widget.controller - .formatSelection(Attribute.getIndentLevel(indent.value - 1)); - }, - ); - } -} - -class ClearFormatButton extends StatefulWidget { - const ClearFormatButton({ - required this.icon, - required this.controller, +class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { + const QuillToolbar({ + required this.children, + this.toolBarHeight = 36, Key? key, }) : super(key: key); - final IconData icon; - - final QuillController controller; - - @override - _ClearFormatButtonState createState() => _ClearFormatButtonState(); -} - -class _ClearFormatButtonState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = theme.iconTheme.color; - final fillColor = theme.canvasColor; - return QuillIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: widget.controller.iconSize * 1.77, - icon: Icon(widget.icon, - size: widget.controller.iconSize, color: iconColor), - fillColor: fillColor, - onPressed: () { - for (final k - in widget.controller.getSelectionStyle().attributes.values) { - widget.controller.formatSelection(Attribute.clone(k, null)); - } - }); - } -} - -class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { - const QuillToolbar( - {required this.children, this.toolBarHeight = 36, Key? key}) - : super(key: key); - factory QuillToolbar.basic({ required QuillController controller, - double toolbarIconSize = 18.0, + double toolbarIconSize = kDefaultIconSize, bool showBoldButton = true, bool showItalicButton = true, bool showUnderLineButton = true, @@ -936,16 +69,15 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { OnImagePickCallback? onImagePickCallback, Key? key, }) { - controller.iconSize = toolbarIconSize; - return QuillToolbar( key: key, - toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor, + toolBarHeight: toolbarIconSize * 2, children: [ Visibility( visible: showHistory, child: HistoryButton( icon: Icons.undo_outlined, + iconSize: toolbarIconSize, controller: controller, undo: true, ), @@ -954,6 +86,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { visible: showHistory, child: HistoryButton( icon: Icons.redo_outlined, + iconSize: toolbarIconSize, controller: controller, undo: false, ), @@ -964,6 +97,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { child: ToggleStyleButton( attribute: Attribute.bold, icon: Icons.format_bold, + iconSize: toolbarIconSize, controller: controller, ), ), @@ -973,6 +107,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { child: ToggleStyleButton( attribute: Attribute.italic, icon: Icons.format_italic, + iconSize: toolbarIconSize, controller: controller, ), ), @@ -982,6 +117,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { child: ToggleStyleButton( attribute: Attribute.underline, icon: Icons.format_underline, + iconSize: toolbarIconSize, controller: controller, ), ), @@ -991,6 +127,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { child: ToggleStyleButton( attribute: Attribute.strikeThrough, icon: Icons.format_strikethrough, + iconSize: toolbarIconSize, controller: controller, ), ), @@ -999,6 +136,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { visible: showColorButton, child: ColorButton( icon: Icons.color_lens, + iconSize: toolbarIconSize, controller: controller, background: false, ), @@ -1008,6 +146,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { visible: showBackgroundColorButton, child: ColorButton( icon: Icons.format_color_fill, + iconSize: toolbarIconSize, controller: controller, background: true, ), @@ -1017,6 +156,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { visible: showClearFormat, child: ClearFormatButton( icon: Icons.format_clear, + iconSize: toolbarIconSize, controller: controller, ), ), @@ -1025,6 +165,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { visible: onImagePickCallback != null, child: ImageButton( icon: Icons.image, + iconSize: toolbarIconSize, controller: controller, imageSource: ImageSource.gallery, onImagePickCallback: onImagePickCallback, @@ -1035,26 +176,39 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { visible: onImagePickCallback != null, child: ImageButton( icon: Icons.photo_camera, + iconSize: toolbarIconSize, controller: controller, imageSource: ImageSource.camera, onImagePickCallback: onImagePickCallback, ), ), Visibility( - visible: showHeaderStyle, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), + visible: showHeaderStyle, + child: VerticalDivider( + indent: 12, + endIndent: 12, + color: Colors.grey.shade400, + ), + ), Visibility( - visible: showHeaderStyle, - child: SelectHeaderStyleButton(controller: controller)), + visible: showHeaderStyle, + child: SelectHeaderStyleButton( + controller: controller, + iconSize: toolbarIconSize, + ), + ), VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400), + indent: 12, + endIndent: 12, + color: Colors.grey.shade400, + ), Visibility( visible: showListNumbers, child: ToggleStyleButton( attribute: Attribute.ol, controller: controller, icon: Icons.format_list_numbered, + iconSize: toolbarIconSize, ), ), Visibility( @@ -1063,6 +217,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { attribute: Attribute.ul, controller: controller, icon: Icons.format_list_bulleted, + iconSize: toolbarIconSize, ), ), Visibility( @@ -1071,6 +226,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { attribute: Attribute.unchecked, controller: controller, icon: Icons.check_box, + iconSize: toolbarIconSize, ), ), Visibility( @@ -1079,27 +235,34 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { attribute: Attribute.codeBlock, controller: controller, icon: Icons.code, + iconSize: toolbarIconSize, ), ), Visibility( - visible: !showListNumbers && - !showListBullets && - !showListCheck && - !showCodeBlock, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), + visible: !showListNumbers && + !showListBullets && + !showListCheck && + !showCodeBlock, + child: VerticalDivider( + indent: 12, + endIndent: 12, + color: Colors.grey.shade400, + ), + ), Visibility( visible: showQuote, child: ToggleStyleButton( attribute: Attribute.blockQuote, controller: controller, icon: Icons.format_quote, + iconSize: toolbarIconSize, ), ), Visibility( visible: showIndent, child: IndentButton( icon: Icons.format_indent_increase, + iconSize: toolbarIconSize, controller: controller, isIncrease: true, ), @@ -1108,22 +271,32 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { visible: showIndent, child: IndentButton( icon: Icons.format_indent_decrease, + iconSize: toolbarIconSize, controller: controller, isIncrease: false, ), ), Visibility( - visible: showQuote, - child: VerticalDivider( - indent: 12, endIndent: 12, color: Colors.grey.shade400)), + visible: showQuote, + child: VerticalDivider( + indent: 12, + endIndent: 12, + color: Colors.grey.shade400, + ), + ), Visibility( - visible: showLink, - child: LinkStyleButton(controller: controller)), + visible: showLink, + child: LinkStyleButton( + controller: controller, + iconSize: toolbarIconSize, + ), + ), Visibility( visible: showHorizontalRule, child: InsertEmbedButton( controller: controller, icon: Icons.horizontal_rule, + iconSize: toolbarIconSize, ), ), ]); @@ -1161,134 +334,3 @@ class _QuillToolbarState extends State { ); } } - -class QuillIconButton extends StatelessWidget { - const QuillIconButton({ - required this.onPressed, - this.icon, - this.size = 40, - this.fillColor, - this.hoverElevation = 1, - this.highlightElevation = 1, - 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( - constraints: BoxConstraints.tightFor(width: size, height: size), - child: RawMaterialButton( - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - fillColor: fillColor, - elevation: 0, - hoverElevation: hoverElevation, - highlightElevation: hoverElevation, - onPressed: onPressed, - child: icon, - ), - ); - } -} - -class QuillDropdownButton extends StatefulWidget { - const QuillDropdownButton({ - required this.child, - required this.initialValue, - required this.items, - required this.onSelected, - this.height = 40, - this.fillColor, - this.hoverElevation = 1, - this.highlightElevation = 1, - 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(); -} - -class _QuillDropdownButtonState extends State> { - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints.tightFor(height: widget.height), - child: RawMaterialButton( - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - fillColor: widget.fillColor, - elevation: 0, - hoverElevation: widget.hoverElevation, - highlightElevation: widget.hoverElevation, - onPressed: _showMenu, - child: _buildContent(context), - ), - ); - } - - void _showMenu() { - final popupMenuTheme = PopupMenuTheme.of(context); - final button = context.findRenderObject() as RenderBox; - final overlay = - Overlay.of(context)!.context.findRenderObject() as RenderBox; - final position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(Offset.zero, ancestor: overlay), - button.localToGlobal(button.size.bottomLeft(Offset.zero), - ancestor: overlay), - ), - Offset.zero & overlay.size, - ); - showMenu( - context: context, - elevation: 4, - // widget.elevation ?? popupMenuTheme.elevation, - initialValue: widget.initialValue, - items: widget.items, - position: position, - shape: popupMenuTheme.shape, - // widget.shape ?? popupMenuTheme.shape, - color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color, - // captureInheritedThemes: widget.captureInheritedThemes, - ).then((newValue) { - if (!mounted) return null; - if (newValue == null) { - // if (widget.onCanceled != null) widget.onCanceled(); - return null; - } - widget.onSelected(newValue); - }); - } - - Widget _buildContent(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 110), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - widget.child, - Expanded(child: Container()), - const Icon(Icons.arrow_drop_down, size: 15) - ], - ), - ), - ); - } -} diff --git a/lib/src/widgets/toolbar/clear_format_button.dart b/lib/src/widgets/toolbar/clear_format_button.dart new file mode 100644 index 00000000..d55c21df --- /dev/null +++ b/lib/src/widgets/toolbar/clear_format_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import '../../../flutter_quill.dart'; +import 'quill_icon_button.dart'; + +class ClearFormatButton extends StatefulWidget { + const ClearFormatButton({ + required this.icon, + required this.controller, + this.iconSize = kDefaultIconSize, + Key? key, + }) : super(key: key); + + final IconData icon; + final double iconSize; + + final QuillController controller; + + @override + _ClearFormatButtonState createState() => _ClearFormatButtonState(); +} + +class _ClearFormatButtonState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = theme.iconTheme.color; + final fillColor = theme.canvasColor; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.iconSize * kIconButtonFactor, + icon: Icon(widget.icon, size: widget.iconSize, color: iconColor), + fillColor: fillColor, + onPressed: () { + for (final k + in widget.controller.getSelectionStyle().attributes.values) { + widget.controller.formatSelection(Attribute.clone(k, null)); + } + }); + } +} diff --git a/lib/src/widgets/toolbar/color_button.dart b/lib/src/widgets/toolbar/color_button.dart new file mode 100644 index 00000000..fa5bb520 --- /dev/null +++ b/lib/src/widgets/toolbar/color_button.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; + +import '../../models/documents/attribute.dart'; +import '../../models/documents/style.dart'; +import '../../utils/color.dart'; +import '../controller.dart'; +import '../toolbar.dart'; +import 'quill_icon_button.dart'; + +/// Controls color styles. +/// +/// When pressed, this button displays overlay toolbar with +/// buttons for each color. +class ColorButton extends StatefulWidget { + const ColorButton({ + required this.icon, + required this.controller, + required this.background, + this.iconSize = kDefaultIconSize, + Key? key, + }) : super(key: key); + + final IconData icon; + final double iconSize; + final bool background; + final QuillController controller; + + @override + _ColorButtonState createState() => _ColorButtonState(); +} + +class _ColorButtonState extends State { + late bool _isToggledColor; + late bool _isToggledBackground; + late bool _isWhite; + late bool _isWhitebackground; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + void _didChangeEditingValue() { + setState(() { + _isToggledColor = + _getIsToggledColor(widget.controller.getSelectionStyle().attributes); + _isToggledBackground = _getIsToggledBackground( + widget.controller.getSelectionStyle().attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + }); + } + + @override + void initState() { + super.initState(); + _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); + _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + widget.controller.addListener(_didChangeEditingValue); + } + + bool _getIsToggledColor(Map attrs) { + return attrs.containsKey(Attribute.color.key); + } + + bool _getIsToggledBackground(Map attrs) { + return attrs.containsKey(Attribute.background.key); + } + + @override + void didUpdateWidget(covariant ColorButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); + _isToggledBackground = + _getIsToggledBackground(_selectionStyle.attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = _isToggledColor && !widget.background && !_isWhite + ? stringToColor(_selectionStyle.attributes['color']!.value) + : theme.iconTheme.color; + + final iconColorBackground = + _isToggledBackground && widget.background && !_isWhitebackground + ? stringToColor(_selectionStyle.attributes['background']!.value) + : theme.iconTheme.color; + + final fillColor = _isToggledColor && !widget.background && _isWhite + ? stringToColor('#ffffff') + : theme.canvasColor; + final fillColorBackground = + _isToggledBackground && widget.background && _isWhitebackground + ? stringToColor('#ffffff') + : theme.canvasColor; + + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.iconSize * kIconButtonFactor, + icon: Icon(widget.icon, + size: widget.iconSize, + color: widget.background ? iconColorBackground : iconColor), + fillColor: widget.background ? fillColorBackground : fillColor, + onPressed: _showColorPicker, + ); + } + + void _changeColor(Color color) { + var hex = color.value.toRadixString(16); + if (hex.startsWith('ff')) { + hex = hex.substring(2); + } + hex = '#$hex'; + widget.controller.formatSelection( + widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex)); + Navigator.of(context).pop(); + } + + void _showColorPicker() { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Select Color'), + backgroundColor: Theme.of(context).canvasColor, + content: SingleChildScrollView( + child: MaterialPicker( + pickerColor: const Color(0x00000000), + onColorChanged: _changeColor, + ), + )), + ); + } +} diff --git a/lib/src/widgets/toolbar/history_button.dart b/lib/src/widgets/toolbar/history_button.dart new file mode 100644 index 00000000..2ed794c5 --- /dev/null +++ b/lib/src/widgets/toolbar/history_button.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +import '../../../flutter_quill.dart'; +import 'quill_icon_button.dart'; + +class HistoryButton extends StatefulWidget { + const HistoryButton({ + required this.icon, + required this.controller, + required this.undo, + this.iconSize = kDefaultIconSize, + Key? key, + }) : super(key: key); + + final IconData icon; + final double iconSize; + final bool undo; + final QuillController controller; + + @override + _HistoryButtonState createState() => _HistoryButtonState(); +} + +class _HistoryButtonState extends State { + Color? _iconColor; + late ThemeData theme; + + @override + Widget build(BuildContext context) { + theme = Theme.of(context); + _setIconColor(); + + final fillColor = theme.canvasColor; + widget.controller.changes.listen((event) async { + _setIconColor(); + }); + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.iconSize * 1.77, + icon: Icon(widget.icon, size: widget.iconSize, color: _iconColor), + fillColor: fillColor, + onPressed: _changeHistory, + ); + } + + void _setIconColor() { + if (!mounted) return; + + if (widget.undo) { + setState(() { + _iconColor = widget.controller.hasUndo + ? theme.iconTheme.color + : theme.disabledColor; + }); + } else { + setState(() { + _iconColor = widget.controller.hasRedo + ? theme.iconTheme.color + : theme.disabledColor; + }); + } + } + + void _changeHistory() { + if (widget.undo) { + if (widget.controller.hasUndo) { + widget.controller.undo(); + } + } else { + if (widget.controller.hasRedo) { + widget.controller.redo(); + } + } + + _setIconColor(); + } +} diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart new file mode 100644 index 00000000..33e191a9 --- /dev/null +++ b/lib/src/widgets/toolbar/image_button.dart @@ -0,0 +1,117 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:filesystem_picker/filesystem_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../models/documents/nodes/embed.dart'; +import '../controller.dart'; +import '../toolbar.dart'; +import 'quill_icon_button.dart'; + +class ImageButton extends StatefulWidget { + const ImageButton({ + required this.icon, + required this.controller, + required this.imageSource, + this.iconSize = kDefaultIconSize, + this.onImagePickCallback, + this.imagePickImpl, + Key? key, + }) : super(key: key); + + final IconData icon; + final double iconSize; + + final QuillController controller; + + final OnImagePickCallback? onImagePickCallback; + + final ImagePickImpl? imagePickImpl; + + final ImageSource imageSource; + + @override + _ImageButtonState createState() => _ImageButtonState(); +} + +class _ImageButtonState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return QuillIconButton( + icon: Icon( + widget.icon, + size: widget.iconSize, + color: theme.iconTheme.color, + ), + highlightElevation: 0, + hoverElevation: 0, + size: widget.iconSize * 1.77, + fillColor: theme.canvasColor, + onPressed: _handleImageButtonTap, + ); + } + + Future _handleImageButtonTap() async { + final index = widget.controller.selection.baseOffset; + final length = widget.controller.selection.extentOffset - index; + + String? imageUrl; + if (widget.imagePickImpl != null) { + imageUrl = await widget.imagePickImpl!(widget.imageSource); + } else { + if (kIsWeb) { + imageUrl = await _pickImageWeb(); + } else if (Platform.isAndroid || Platform.isIOS) { + imageUrl = await _pickImage(widget.imageSource); + } else { + imageUrl = await _pickImageDesktop(); + } + } + + if (imageUrl != null) { + widget.controller + .replaceText(index, length, BlockEmbed.image(imageUrl), null); + } + } + + Future _pickImageWeb() async { + final result = await FilePicker.platform.pickFiles(); + if (result == null) { + return null; + } + + // Take first, because we don't allow picking multiple files. + final fileName = result.files.first.name!; + final file = File(fileName); + + return widget.onImagePickCallback!(file); + } + + Future _pickImage(ImageSource source) async { + final pickedFile = await ImagePicker().getImage(source: source); + if (pickedFile == null) { + return null; + } + + return widget.onImagePickCallback!(File(pickedFile.path)); + } + + Future _pickImageDesktop() async { + final filePath = await FilesystemPicker.open( + context: context, + rootDirectory: await getApplicationDocumentsDirectory(), + fsType: FilesystemType.file, + fileTileSelectMode: FileTileSelectMode.wholeTile, + ); + if (filePath == null || filePath.isEmpty) return null; + + final file = File(filePath); + return widget.onImagePickCallback!(file); + } +} diff --git a/lib/src/widgets/toolbar/indent_button.dart b/lib/src/widgets/toolbar/indent_button.dart new file mode 100644 index 00000000..aa6dfadb --- /dev/null +++ b/lib/src/widgets/toolbar/indent_button.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import '../../../flutter_quill.dart'; +import 'quill_icon_button.dart'; + +class IndentButton extends StatefulWidget { + const IndentButton({ + required this.icon, + required this.controller, + required this.isIncrease, + this.iconSize = kDefaultIconSize, + Key? key, + }) : super(key: key); + + final IconData icon; + final double iconSize; + final QuillController controller; + final bool isIncrease; + + @override + _IndentButtonState createState() => _IndentButtonState(); +} + +class _IndentButtonState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = theme.iconTheme.color; + final fillColor = theme.canvasColor; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.iconSize * 1.77, + icon: Icon(widget.icon, size: widget.iconSize, color: iconColor), + fillColor: fillColor, + onPressed: () { + final indent = widget.controller + .getSelectionStyle() + .attributes[Attribute.indent.key]; + if (indent == null) { + if (widget.isIncrease) { + widget.controller.formatSelection(Attribute.indentL1); + } + return; + } + if (indent.value == 1 && !widget.isIncrease) { + widget.controller + .formatSelection(Attribute.clone(Attribute.indentL1, null)); + return; + } + if (widget.isIncrease) { + widget.controller + .formatSelection(Attribute.getIndentLevel(indent.value + 1)); + return; + } + widget.controller + .formatSelection(Attribute.getIndentLevel(indent.value - 1)); + }, + ); + } +} diff --git a/lib/src/widgets/toolbar/insert_embed_button.dart b/lib/src/widgets/toolbar/insert_embed_button.dart new file mode 100644 index 00000000..5c889b69 --- /dev/null +++ b/lib/src/widgets/toolbar/insert_embed_button.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../../models/documents/nodes/embed.dart'; +import '../controller.dart'; +import '../toolbar.dart'; +import 'quill_icon_button.dart'; + +class InsertEmbedButton extends StatelessWidget { + const InsertEmbedButton({ + required this.controller, + required this.icon, + this.iconSize = kDefaultIconSize, + this.fillColor, + Key? key, + }) : super(key: key); + + final QuillController controller; + final IconData icon; + final double iconSize; + final Color? fillColor; + + @override + Widget build(BuildContext context) { + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * kIconButtonFactor, + icon: Icon( + icon, + size: iconSize, + color: Theme.of(context).iconTheme.color, + ), + fillColor: fillColor ?? Theme.of(context).canvasColor, + onPressed: () { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + controller.replaceText(index, length, BlockEmbed.horizontalRule, null); + }, + ); + } +} diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart new file mode 100644 index 00000000..417a9972 --- /dev/null +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +import '../../models/documents/attribute.dart'; +import '../controller.dart'; +import '../toolbar.dart'; +import 'quill_icon_button.dart'; + +class LinkStyleButton extends StatefulWidget { + const LinkStyleButton({ + required this.controller, + this.iconSize = kDefaultIconSize, + this.icon, + Key? key, + }) : super(key: key); + + final QuillController controller; + final IconData? icon; + final double iconSize; + + @override + _LinkStyleButtonState createState() => _LinkStyleButtonState(); +} + +class _LinkStyleButtonState extends State { + void _didChangeSelection() { + setState(() {}); + } + + @override + void initState() { + super.initState(); + widget.controller.addListener(_didChangeSelection); + } + + @override + void didUpdateWidget(covariant LinkStyleButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeSelection); + widget.controller.addListener(_didChangeSelection); + } + } + + @override + void dispose() { + super.dispose(); + widget.controller.removeListener(_didChangeSelection); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isEnabled = !widget.controller.selection.isCollapsed; + final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: widget.iconSize * kIconButtonFactor, + icon: Icon( + widget.icon ?? Icons.link, + size: widget.iconSize, + color: isEnabled ? theme.iconTheme.color : theme.disabledColor, + ), + fillColor: Theme.of(context).canvasColor, + onPressed: pressedHandler, + ); + } + + void _openLinkDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) { + return const _LinkDialog(); + }, + ).then(_linkSubmitted); + } + + void _linkSubmitted(String? value) { + if (value == null || value.isEmpty) { + return; + } + widget.controller.formatSelection(LinkAttribute(value)); + } +} + +class _LinkDialog extends StatefulWidget { + const _LinkDialog({Key? key}) : super(key: key); + + @override + _LinkDialogState createState() => _LinkDialogState(); +} + +class _LinkDialogState extends State<_LinkDialog> { + String _link = ''; + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: TextField( + decoration: const InputDecoration(labelText: 'Paste a link'), + autofocus: true, + onChanged: _linkChanged, + ), + actions: [ + TextButton( + onPressed: _link.isNotEmpty ? _applyLink : null, + child: const Text('Apply'), + ), + ], + ); + } + + void _linkChanged(String value) { + setState(() { + _link = value; + }); + } + + void _applyLink() { + Navigator.pop(context, _link); + } +} diff --git a/lib/src/widgets/toolbar/quill_dropdown_button.dart b/lib/src/widgets/toolbar/quill_dropdown_button.dart new file mode 100644 index 00000000..be3ed092 --- /dev/null +++ b/lib/src/widgets/toolbar/quill_dropdown_button.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +class QuillDropdownButton extends StatefulWidget { + const QuillDropdownButton({ + required this.child, + required this.initialValue, + required this.items, + required this.onSelected, + this.height = 40, + this.fillColor, + this.hoverElevation = 1, + this.highlightElevation = 1, + 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(); +} + +class _QuillDropdownButtonState extends State> { + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints.tightFor(height: widget.height), + child: RawMaterialButton( + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), + fillColor: widget.fillColor, + elevation: 0, + hoverElevation: widget.hoverElevation, + highlightElevation: widget.hoverElevation, + onPressed: _showMenu, + child: _buildContent(context), + ), + ); + } + + void _showMenu() { + final popupMenuTheme = PopupMenuTheme.of(context); + final button = context.findRenderObject() as RenderBox; + final overlay = + Overlay.of(context)!.context.findRenderObject() as RenderBox; + final position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(Offset.zero, ancestor: overlay), + button.localToGlobal(button.size.bottomLeft(Offset.zero), + ancestor: overlay), + ), + Offset.zero & overlay.size, + ); + showMenu( + context: context, + elevation: 4, + // widget.elevation ?? popupMenuTheme.elevation, + initialValue: widget.initialValue, + items: widget.items, + position: position, + shape: popupMenuTheme.shape, + // widget.shape ?? popupMenuTheme.shape, + color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color, + // captureInheritedThemes: widget.captureInheritedThemes, + ).then((newValue) { + if (!mounted) return null; + if (newValue == null) { + // if (widget.onCanceled != null) widget.onCanceled(); + return null; + } + widget.onSelected(newValue); + }); + } + + Widget _buildContent(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 110), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + widget.child, + Expanded(child: Container()), + const Icon(Icons.arrow_drop_down, size: 15) + ], + ), + ), + ); + } +} diff --git a/lib/src/widgets/toolbar/quill_icon_button.dart b/lib/src/widgets/toolbar/quill_icon_button.dart new file mode 100644 index 00000000..0ffd3ef7 --- /dev/null +++ b/lib/src/widgets/toolbar/quill_icon_button.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class QuillIconButton extends StatelessWidget { + const QuillIconButton({ + required this.onPressed, + this.icon, + this.size = 40, + this.fillColor, + this.hoverElevation = 1, + this.highlightElevation = 1, + 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( + constraints: BoxConstraints.tightFor(width: size, height: size), + child: RawMaterialButton( + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), + fillColor: fillColor, + elevation: 0, + hoverElevation: hoverElevation, + highlightElevation: hoverElevation, + onPressed: onPressed, + child: icon, + ), + ); + } +} diff --git a/lib/src/widgets/toolbar/select_header_style_button.dart b/lib/src/widgets/toolbar/select_header_style_button.dart new file mode 100644 index 00000000..715e3632 --- /dev/null +++ b/lib/src/widgets/toolbar/select_header_style_button.dart @@ -0,0 +1,122 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../models/documents/attribute.dart'; +import '../../models/documents/style.dart'; +import '../controller.dart'; +import '../toolbar.dart'; + +class SelectHeaderStyleButton extends StatefulWidget { + const SelectHeaderStyleButton({ + required this.controller, + this.iconSize = kDefaultIconSize, + Key? key, + }) : super(key: key); + + final QuillController controller; + final double iconSize; + + @override + _SelectHeaderStyleButtonState createState() => + _SelectHeaderStyleButtonState(); +} + +class _SelectHeaderStyleButtonState extends State { + Attribute? _value; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + @override + void initState() { + super.initState(); + setState(() { + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + }); + widget.controller.addListener(_didChangeEditingValue); + } + + @override + Widget build(BuildContext context) { + final _valueToText = { + Attribute.header: 'N', + Attribute.h1: 'H1', + Attribute.h2: 'H2', + Attribute.h3: 'H3', + }; + + final _valueAttribute = [ + Attribute.header, + Attribute.h1, + Attribute.h2, + Attribute.h3 + ]; + final _valueString = ['N', 'H1', 'H2', 'H3']; + + final theme = Theme.of(context); + final style = TextStyle( + fontWeight: FontWeight.w600, + fontSize: widget.iconSize * 0.7, + ); + + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(4, (index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), + child: ConstrainedBox( + constraints: BoxConstraints.tightFor( + width: widget.iconSize * kIconButtonFactor, + height: widget.iconSize * kIconButtonFactor, + ), + child: RawMaterialButton( + hoverElevation: 0, + highlightElevation: 0, + elevation: 0, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2)), + fillColor: _valueToText[_value] == _valueString[index] + ? theme.toggleableActiveColor + : theme.canvasColor, + onPressed: () => + widget.controller.formatSelection(_valueAttribute[index]), + child: Text( + _valueString[index], + style: style.copyWith( + color: _valueToText[_value] == _valueString[index] + ? theme.primaryIconTheme.color + : theme.iconTheme.color, + ), + ), + ), + ), + ); + }), + ); + } + + void _didChangeEditingValue() { + setState(() { + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + }); + } + + @override + void didUpdateWidget(covariant SelectHeaderStyleButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } +} diff --git a/lib/src/widgets/toolbar/toggle_check_list_button.dart b/lib/src/widgets/toolbar/toggle_check_list_button.dart new file mode 100644 index 00000000..861da445 --- /dev/null +++ b/lib/src/widgets/toolbar/toggle_check_list_button.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +import '../../models/documents/attribute.dart'; +import '../../models/documents/style.dart'; +import '../controller.dart'; +import '../toolbar.dart'; +import 'toggle_style_button.dart'; + +class ToggleCheckListButton extends StatefulWidget { + const ToggleCheckListButton({ + required this.icon, + required this.controller, + required this.attribute, + this.iconSize = kDefaultIconSize, + this.fillColor, + this.childBuilder = defaultToggleStyleButtonBuilder, + Key? key, + }) : super(key: key); + + final IconData icon; + final double iconSize; + + final Color? fillColor; + + final QuillController controller; + + final ToggleStyleButtonBuilder childBuilder; + + final Attribute attribute; + + @override + _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState(); +} + +class _ToggleCheckListButtonState extends State { + bool? _isToggled; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + void _didChangeEditingValue() { + setState(() { + _isToggled = + _getIsToggled(widget.controller.getSelectionStyle().attributes); + }); + } + + @override + void initState() { + super.initState(); + _isToggled = _getIsToggled(_selectionStyle.attributes); + widget.controller.addListener(_didChangeEditingValue); + } + + bool _getIsToggled(Map attrs) { + if (widget.attribute.key == Attribute.list.key) { + final attribute = attrs[widget.attribute.key]; + if (attribute == null) { + return false; + } + return attribute.value == widget.attribute.value || + attribute.value == Attribute.checked.value; + } + return attrs.containsKey(widget.attribute.key); + } + + @override + void didUpdateWidget(covariant ToggleCheckListButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _isToggled = _getIsToggled(_selectionStyle.attributes); + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isInCodeBlock = + _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); + final isEnabled = + !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key; + return widget.childBuilder( + context, + Attribute.unchecked, + widget.icon, + widget.fillColor, + _isToggled, + isEnabled ? _toggleAttribute : null, + widget.iconSize, + ); + } + + void _toggleAttribute() { + widget.controller.formatSelection(_isToggled! + ? Attribute.clone(Attribute.unchecked, null) + : Attribute.unchecked); + } +} diff --git a/lib/src/widgets/toolbar/toggle_style_button.dart b/lib/src/widgets/toolbar/toggle_style_button.dart new file mode 100644 index 00000000..624a31f9 --- /dev/null +++ b/lib/src/widgets/toolbar/toggle_style_button.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; + +import '../../models/documents/attribute.dart'; +import '../../models/documents/style.dart'; +import '../controller.dart'; +import '../toolbar.dart'; +import 'quill_icon_button.dart'; + +typedef ToggleStyleButtonBuilder = Widget Function( + BuildContext context, + Attribute attribute, + IconData icon, + Color? fillColor, + bool? isToggled, + VoidCallback? onPressed, [ + double iconSize, +]); + +class ToggleStyleButton extends StatefulWidget { + const ToggleStyleButton({ + required this.attribute, + required this.icon, + required this.controller, + this.iconSize = kDefaultIconSize, + this.fillColor, + this.childBuilder = defaultToggleStyleButtonBuilder, + Key? key, + }) : super(key: key); + + final Attribute attribute; + + final IconData icon; + final double iconSize; + + final Color? fillColor; + + final QuillController controller; + + final ToggleStyleButtonBuilder childBuilder; + + @override + _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); +} + +class _ToggleStyleButtonState extends State { + bool? _isToggled; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + @override + void initState() { + super.initState(); + _isToggled = _getIsToggled(_selectionStyle.attributes); + widget.controller.addListener(_didChangeEditingValue); + } + + @override + Widget build(BuildContext context) { + final isInCodeBlock = + _selectionStyle.attributes.containsKey(Attribute.codeBlock.key); + final isEnabled = + !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key; + return widget.childBuilder( + context, + widget.attribute, + widget.icon, + widget.fillColor, + _isToggled, + isEnabled ? _toggleAttribute : null, + widget.iconSize, + ); + } + + @override + void didUpdateWidget(covariant ToggleStyleButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _isToggled = _getIsToggled(_selectionStyle.attributes); + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + void _didChangeEditingValue() { + setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes)); + } + + bool _getIsToggled(Map attrs) { + if (widget.attribute.key == Attribute.list.key) { + final attribute = attrs[widget.attribute.key]; + if (attribute == null) { + return false; + } + return attribute.value == widget.attribute.value; + } + return attrs.containsKey(widget.attribute.key); + } + + void _toggleAttribute() { + widget.controller.formatSelection(_isToggled! + ? Attribute.clone(widget.attribute, null) + : widget.attribute); + } +} + +Widget defaultToggleStyleButtonBuilder( + BuildContext context, + Attribute attribute, + IconData icon, + Color? fillColor, + bool? isToggled, + VoidCallback? onPressed, [ + double iconSize = kDefaultIconSize, +]) { + final theme = Theme.of(context); + final isEnabled = onPressed != null; + final iconColor = isEnabled + ? isToggled == true + ? theme.primaryIconTheme.color + : theme.iconTheme.color + : theme.disabledColor; + final fill = isToggled == true + ? theme.toggleableActiveColor + : fillColor ?? theme.canvasColor; + return QuillIconButton( + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * kIconButtonFactor, + icon: Icon(icon, size: iconSize, color: iconColor), + fillColor: fill, + onPressed: onPressed, + ); +} From 2b9d6bd71bc44649fd3a8bd8e8de191a87ac4e6e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Mon, 24 May 2021 13:42:20 -0700 Subject: [PATCH 203/306] Upgrade to 1.3.1 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 323da506..bfc8284b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.3.1] +* New logo. + ## [1.3.0] * Support flutter 2.2.0. diff --git a/pubspec.yaml b/pubspec.yaml index 7cf52633..e50130ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.3.0 +version: 1.3.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From c63831d01458dfe00302d06eb1182e2d6197fbc9 Mon Sep 17 00:00:00 2001 From: hyouuu Date: Tue, 25 May 2021 11:58:23 -0700 Subject: [PATCH 204/306] Fix incorrect double to int cast, and guard against optional parent (#239) --- lib/src/models/documents/nodes/node.dart | 3 +++ lib/src/widgets/text_line.dart | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 6bb0fb97..08335727 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -53,6 +53,9 @@ abstract class Node extends LinkedListEntry { /// Offset in characters of this node in the document. int get documentOffset { + if (parent == null) { + return offset; + } final parentOffset = (parent is! Root) ? parent!.documentOffset : 0; return parentOffset + offset; } diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 04a9567d..b03c0f9b 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -625,11 +625,11 @@ class RenderEditableTextLine extends RenderEditableBox { final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final leadingWidth = _leading == null ? 0 - : _leading!.getMinIntrinsicWidth(height - verticalPadding) as int; + : _leading!.getMinIntrinsicWidth(height - verticalPadding).floor(); final bodyWidth = _body == null ? 0 : _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding)) - as int; + .floor(); return horizontalPadding + leadingWidth + bodyWidth; } @@ -640,11 +640,11 @@ class RenderEditableTextLine extends RenderEditableBox { final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final leadingWidth = _leading == null ? 0 - : _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int; + : _leading!.getMaxIntrinsicWidth(height - verticalPadding).ceil(); final bodyWidth = _body == null ? 0 : _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding)) - as int; + .ceil(); return horizontalPadding + leadingWidth + bodyWidth; } From a10197dec3813944b243ab88c8149f19ab851236 Mon Sep 17 00:00:00 2001 From: Xun Gong Date: Tue, 25 May 2021 12:24:43 -0700 Subject: [PATCH 205/306] use ceil instead of floor to make sure won't cause overflow --- lib/src/widgets/text_line.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index b03c0f9b..90f7ceff 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -625,11 +625,11 @@ class RenderEditableTextLine extends RenderEditableBox { final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final leadingWidth = _leading == null ? 0 - : _leading!.getMinIntrinsicWidth(height - verticalPadding).floor(); + : _leading!.getMinIntrinsicWidth(height - verticalPadding).ceil(); final bodyWidth = _body == null ? 0 : _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding)) - .floor(); + .ceil(); return horizontalPadding + leadingWidth + bodyWidth; } From aba8032b8d12ba69e0125c9ebe9044d89a8f2f9d Mon Sep 17 00:00:00 2001 From: Ben Chung <1330575+yzxben@users.noreply.github.com> Date: Tue, 25 May 2021 16:51:34 -0400 Subject: [PATCH 206/306] Fix example project Podfile (#241) --- example/ios/Podfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/ios/Podfile b/example/ios/Podfile index f7d6a5e6..1e8c3c90 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -28,6 +28,9 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup target 'Runner' do + use_frameworks! + use_modular_headers! + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end From 16d6f243b8adb28c134fdcf79689608e98861128 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Sat, 29 May 2021 18:52:21 +0200 Subject: [PATCH 207/306] Show arrow indicator on toolbar (#245) --- lib/src/widgets/toolbar.dart | 15 +-- .../toolbar/arrow_indicated_button_list.dart | 125 ++++++++++++++++++ 2 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 lib/src/widgets/toolbar/arrow_indicated_button_list.dart diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index d4d00f5d..6b3a5e8c 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -6,6 +6,7 @@ import 'package:image_picker/image_picker.dart'; import '../models/documents/attribute.dart'; import 'controller.dart'; +import 'toolbar/arrow_indicated_button_list.dart'; import 'toolbar/clear_format_button.dart'; import 'toolbar/color_button.dart'; import 'toolbar/history_button.dart'; @@ -316,21 +317,9 @@ class _QuillToolbarState extends State { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8), constraints: BoxConstraints.tightFor(height: widget.preferredSize.height), color: Theme.of(context).canvasColor, - child: CustomScrollView( - scrollDirection: Axis.horizontal, - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: widget.children, - ), - ), - ], - ), + child: ArrowIndicatedButtonList(buttons: widget.children), ); } } diff --git a/lib/src/widgets/toolbar/arrow_indicated_button_list.dart b/lib/src/widgets/toolbar/arrow_indicated_button_list.dart new file mode 100644 index 00000000..b17157d4 --- /dev/null +++ b/lib/src/widgets/toolbar/arrow_indicated_button_list.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// Scrollable list with arrow indicators. +/// +/// The arrow indicators are automatically hidden if the list is not +/// scrollable in the direction of the respective arrow. +class ArrowIndicatedButtonList extends StatefulWidget { + const ArrowIndicatedButtonList({required this.buttons, Key? key}) + : super(key: key); + + final List buttons; + + @override + _ArrowIndicatedButtonListState createState() => + _ArrowIndicatedButtonListState(); +} + +class _ArrowIndicatedButtonListState extends State + with WidgetsBindingObserver { + final ScrollController _controller = ScrollController(); + bool _showLeftArrow = false; + bool _showRightArrow = false; + + @override + void initState() { + super.initState(); + _controller.addListener(_handleScroll); + + // Listening to the WidgetsBinding instance is necessary so that we can + // hide the arrows when the window gets a new size and thus the toolbar + // becomes scrollable/unscrollable. + WidgetsBinding.instance!.addObserver(this); + + // Workaround to allow the scroll controller attach to our ListView so that + // we can detect if overflow arrows need to be shown on init. + Timer.run(_handleScroll); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + _buildLeftArrow(), + _buildScrollableList(), + _buildRightColor(), + ], + ); + } + + @override + void didChangeMetrics() => _handleScroll(); + + @override + void dispose() { + _controller.dispose(); + WidgetsBinding.instance!.removeObserver(this); + super.dispose(); + } + + void _handleScroll() { + setState(() { + _showLeftArrow = + _controller.position.minScrollExtent != _controller.position.pixels; + _showRightArrow = + _controller.position.maxScrollExtent != _controller.position.pixels; + }); + } + + Widget _buildLeftArrow() { + return SizedBox( + width: 8, + child: Transform.translate( + // Move the icon a few pixels to center it + offset: const Offset(-5, 0), + child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null, + ), + ); + } + + Widget _buildScrollableList() { + return Expanded( + child: ScrollConfiguration( + // Remove the glowing effect, as we already have the arrow indicators + behavior: _NoGlowBehavior(), + // The CustomScrollView is necessary so that the children are not + // stretched to the height of the toolbar, https://bit.ly/3uC3bjI + child: CustomScrollView( + scrollDirection: Axis.horizontal, + controller: _controller, + physics: const ClampingScrollPhysics(), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: widget.buttons, + ), + ) + ], + ), + ), + ); + } + + Widget _buildRightColor() { + return SizedBox( + width: 8, + child: Transform.translate( + // Move the icon a few pixels to center it + offset: const Offset(-5, 0), + child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null, + ), + ); + } +} + +/// ScrollBehavior without the Material glow effect. +class _NoGlowBehavior extends ScrollBehavior { + @override + Widget buildViewportChrome(BuildContext _, Widget child, AxisDirection __) { + return child; + } +} From cdf50b579d064f37c4642b72ba203edd5b9007b4 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Mon, 31 May 2021 18:00:53 +0200 Subject: [PATCH 208/306] Add color parameter to Toolbar and ImageButton In addition, change these widgets to stateless widgets, since these widgets do not have a state and thus stateful is superfluous. --- lib/src/widgets/toolbar.dart | 18 +++++---- lib/src/widgets/toolbar/image_button.dart | 47 ++++++++++------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 6b3a5e8c..3b4b4348 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -40,10 +40,11 @@ const double kDefaultIconSize = 18; // The factor of how much larger the button is in relation to the icon. const double kIconButtonFactor = 1.77; -class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { +class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { const QuillToolbar({ required this.children, this.toolBarHeight = 36, + this.color, Key? key, }) : super(key: key); @@ -306,20 +307,21 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget { final List children; final double toolBarHeight; - @override - _QuillToolbarState createState() => _QuillToolbarState(); + /// The color of the toolbar. + /// + /// Defaults to [ThemeData.canvasColor] of the current [Theme] if no color + /// is given. + final Color? color; @override Size get preferredSize => Size.fromHeight(toolBarHeight); -} -class _QuillToolbarState extends State { @override Widget build(BuildContext context) { return Container( - constraints: BoxConstraints.tightFor(height: widget.preferredSize.height), - color: Theme.of(context).canvasColor, - child: ArrowIndicatedButtonList(buttons: widget.children), + constraints: BoxConstraints.tightFor(height: preferredSize.height), + color: color ?? Theme.of(context).canvasColor, + child: ArrowIndicatedButtonList(buttons: children), ); } } diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index 33e191a9..740ef269 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -12,12 +12,13 @@ import '../controller.dart'; import '../toolbar.dart'; import 'quill_icon_button.dart'; -class ImageButton extends StatefulWidget { +class ImageButton extends StatelessWidget { const ImageButton({ required this.icon, required this.controller, required this.imageSource, this.iconSize = kDefaultIconSize, + this.fillColor, this.onImagePickCallback, this.imagePickImpl, Key? key, @@ -26,6 +27,8 @@ class ImageButton extends StatefulWidget { final IconData icon; final double iconSize; + final Color? fillColor; + final QuillController controller; final OnImagePickCallback? onImagePickCallback; @@ -34,49 +37,39 @@ class ImageButton extends StatefulWidget { final ImageSource imageSource; - @override - _ImageButtonState createState() => _ImageButtonState(); -} - -class _ImageButtonState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); return QuillIconButton( - icon: Icon( - widget.icon, - size: widget.iconSize, - color: theme.iconTheme.color, - ), + icon: Icon(icon, size: iconSize, color: theme.iconTheme.color), highlightElevation: 0, hoverElevation: 0, - size: widget.iconSize * 1.77, - fillColor: theme.canvasColor, - onPressed: _handleImageButtonTap, + size: iconSize * 1.77, + fillColor: fillColor ?? theme.canvasColor, + onPressed: () => _handleImageButtonTap(context), ); } - Future _handleImageButtonTap() async { - final index = widget.controller.selection.baseOffset; - final length = widget.controller.selection.extentOffset - index; + Future _handleImageButtonTap(BuildContext context) async { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; String? imageUrl; - if (widget.imagePickImpl != null) { - imageUrl = await widget.imagePickImpl!(widget.imageSource); + if (imagePickImpl != null) { + imageUrl = await imagePickImpl!(imageSource); } else { if (kIsWeb) { imageUrl = await _pickImageWeb(); } else if (Platform.isAndroid || Platform.isIOS) { - imageUrl = await _pickImage(widget.imageSource); + imageUrl = await _pickImage(imageSource); } else { - imageUrl = await _pickImageDesktop(); + imageUrl = await _pickImageDesktop(context); } } if (imageUrl != null) { - widget.controller - .replaceText(index, length, BlockEmbed.image(imageUrl), null); + controller.replaceText(index, length, BlockEmbed.image(imageUrl), null); } } @@ -90,7 +83,7 @@ class _ImageButtonState extends State { final fileName = result.files.first.name!; final file = File(fileName); - return widget.onImagePickCallback!(file); + return onImagePickCallback!(file); } Future _pickImage(ImageSource source) async { @@ -99,10 +92,10 @@ class _ImageButtonState extends State { return null; } - return widget.onImagePickCallback!(File(pickedFile.path)); + return onImagePickCallback!(File(pickedFile.path)); } - Future _pickImageDesktop() async { + Future _pickImageDesktop(BuildContext context) async { final filePath = await FilesystemPicker.open( context: context, rootDirectory: await getApplicationDocumentsDirectory(), @@ -112,6 +105,6 @@ class _ImageButtonState extends State { if (filePath == null || filePath.isEmpty) return null; final file = File(filePath); - return widget.onImagePickCallback!(file); + return onImagePickCallback!(file); } } From e2ab4df8cd438a77d15700b0b0aadf8c3b5fa01d Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Tue, 1 Jun 2021 18:23:00 +0200 Subject: [PATCH 209/306] Fix paste bug --- lib/src/widgets/raw_editor.dart | 6 ------ .../raw_editor_state_selection_delegate_mixin.dart | 8 ++++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index b94472b6..7cfb9103 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -676,12 +676,6 @@ class RawEditorState extends EditorState @override bool get wantKeepAlive => widget.focusNode.hasFocus; - - @override - void userUpdateTextEditingValue( - TextEditingValue value, SelectionChangedCause cause) { - updateEditingValue(value); - } } class _Editor extends MultiChildRenderObjectWidget { diff --git a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart index cda991cc..7da1c66e 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -26,6 +26,14 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState } } + @override + void userUpdateTextEditingValue( + TextEditingValue value, + SelectionChangedCause cause, + ) { + setTextEditingValue(value); + } + @override bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; From 3ca9e966aced04b582ad6c48fbfb3ff8a64c87a3 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Tue, 1 Jun 2021 18:58:53 +0200 Subject: [PATCH 210/306] Remove extraneous toolbar dividers in certain configuration Closes #193. --- lib/src/widgets/toolbar.dart | 368 ++++++++++++++++------------------- 1 file changed, 167 insertions(+), 201 deletions(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 3b4b4348..76be1824 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -71,237 +71,203 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { OnImagePickCallback? onImagePickCallback, Key? key, }) { + final isButtonGroupShown = [ + showHistory || + showBoldButton || + showItalicButton || + showUnderLineButton || + showStrikeThrough || + showColorButton || + showBackgroundColorButton || + showClearFormat || + onImagePickCallback != null, + showHeaderStyle, + showListNumbers || showListBullets || showListCheck || showCodeBlock, + showQuote || showIndent, + showLink || showHorizontalRule + ]; + return QuillToolbar( - key: key, - toolBarHeight: toolbarIconSize * 2, - children: [ - Visibility( - visible: showHistory, - child: HistoryButton( - icon: Icons.undo_outlined, - iconSize: toolbarIconSize, - controller: controller, - undo: true, - ), + key: key, + toolBarHeight: toolbarIconSize * 2, + children: [ + if (showHistory) + HistoryButton( + icon: Icons.undo_outlined, + iconSize: toolbarIconSize, + controller: controller, + undo: true, ), - Visibility( - visible: showHistory, - child: HistoryButton( - icon: Icons.redo_outlined, - iconSize: toolbarIconSize, - controller: controller, - undo: false, - ), + if (showHistory) + HistoryButton( + icon: Icons.redo_outlined, + iconSize: toolbarIconSize, + controller: controller, + undo: false, ), - const SizedBox(width: 0.6), - Visibility( - visible: showBoldButton, - child: ToggleStyleButton( - attribute: Attribute.bold, - icon: Icons.format_bold, - iconSize: toolbarIconSize, - controller: controller, - ), + if (showBoldButton) + ToggleStyleButton( + attribute: Attribute.bold, + icon: Icons.format_bold, + iconSize: toolbarIconSize, + controller: controller, ), - const SizedBox(width: 0.6), - Visibility( - visible: showItalicButton, - child: ToggleStyleButton( - attribute: Attribute.italic, - icon: Icons.format_italic, - iconSize: toolbarIconSize, - controller: controller, - ), + if (showItalicButton) + ToggleStyleButton( + attribute: Attribute.italic, + icon: Icons.format_italic, + iconSize: toolbarIconSize, + controller: controller, ), - const SizedBox(width: 0.6), - Visibility( - visible: showUnderLineButton, - child: ToggleStyleButton( - attribute: Attribute.underline, - icon: Icons.format_underline, - iconSize: toolbarIconSize, - controller: controller, - ), + if (showUnderLineButton) + ToggleStyleButton( + attribute: Attribute.underline, + icon: Icons.format_underline, + iconSize: toolbarIconSize, + controller: controller, ), - const SizedBox(width: 0.6), - Visibility( - visible: showStrikeThrough, - child: ToggleStyleButton( - attribute: Attribute.strikeThrough, - icon: Icons.format_strikethrough, - iconSize: toolbarIconSize, - controller: controller, - ), + if (showStrikeThrough) + ToggleStyleButton( + attribute: Attribute.strikeThrough, + icon: Icons.format_strikethrough, + iconSize: toolbarIconSize, + controller: controller, ), - const SizedBox(width: 0.6), - Visibility( - visible: showColorButton, - child: ColorButton( - icon: Icons.color_lens, - iconSize: toolbarIconSize, - controller: controller, - background: false, - ), + if (showColorButton) + ColorButton( + icon: Icons.color_lens, + iconSize: toolbarIconSize, + controller: controller, + background: false, ), - const SizedBox(width: 0.6), - Visibility( - visible: showBackgroundColorButton, - child: ColorButton( - icon: Icons.format_color_fill, - iconSize: toolbarIconSize, - controller: controller, - background: true, - ), + if (showBackgroundColorButton) + ColorButton( + icon: Icons.format_color_fill, + iconSize: toolbarIconSize, + controller: controller, + background: true, ), - const SizedBox(width: 0.6), - Visibility( - visible: showClearFormat, - child: ClearFormatButton( - icon: Icons.format_clear, - iconSize: toolbarIconSize, - controller: controller, - ), + if (showClearFormat) + ClearFormatButton( + icon: Icons.format_clear, + iconSize: toolbarIconSize, + controller: controller, ), - const SizedBox(width: 0.6), - Visibility( - visible: onImagePickCallback != null, - child: ImageButton( - icon: Icons.image, - iconSize: toolbarIconSize, - controller: controller, - imageSource: ImageSource.gallery, - onImagePickCallback: onImagePickCallback, - ), + if (onImagePickCallback != null) + ImageButton( + icon: Icons.image, + iconSize: toolbarIconSize, + controller: controller, + imageSource: ImageSource.gallery, + onImagePickCallback: onImagePickCallback, ), - const SizedBox(width: 0.6), - Visibility( - visible: onImagePickCallback != null, - child: ImageButton( - icon: Icons.photo_camera, - iconSize: toolbarIconSize, - controller: controller, - imageSource: ImageSource.camera, - onImagePickCallback: onImagePickCallback, - ), + if (onImagePickCallback != null) + ImageButton( + icon: Icons.photo_camera, + iconSize: toolbarIconSize, + controller: controller, + imageSource: ImageSource.camera, + onImagePickCallback: onImagePickCallback, ), - Visibility( - visible: showHeaderStyle, - child: VerticalDivider( - indent: 12, - endIndent: 12, - color: Colors.grey.shade400, - ), + if (isButtonGroupShown[0] && + (isButtonGroupShown[1] || + isButtonGroupShown[2] || + isButtonGroupShown[3] || + isButtonGroupShown[4])) + VerticalDivider( + indent: 12, + endIndent: 12, + color: Colors.grey.shade400, ), - Visibility( - visible: showHeaderStyle, - child: SelectHeaderStyleButton( - controller: controller, - iconSize: toolbarIconSize, - ), + if (showHeaderStyle) + SelectHeaderStyleButton( + controller: controller, + iconSize: toolbarIconSize, ), + if (isButtonGroupShown[1] && + (isButtonGroupShown[2] || + isButtonGroupShown[3] || + isButtonGroupShown[4])) VerticalDivider( indent: 12, endIndent: 12, color: Colors.grey.shade400, ), - Visibility( - visible: showListNumbers, - child: ToggleStyleButton( - attribute: Attribute.ol, - controller: controller, - icon: Icons.format_list_numbered, - iconSize: toolbarIconSize, - ), + if (showListNumbers) + ToggleStyleButton( + attribute: Attribute.ol, + controller: controller, + icon: Icons.format_list_numbered, + iconSize: toolbarIconSize, ), - Visibility( - visible: showListBullets, - child: ToggleStyleButton( - attribute: Attribute.ul, - controller: controller, - icon: Icons.format_list_bulleted, - iconSize: toolbarIconSize, - ), + if (showListBullets) + ToggleStyleButton( + attribute: Attribute.ul, + controller: controller, + icon: Icons.format_list_bulleted, + iconSize: toolbarIconSize, ), - Visibility( - visible: showListCheck, - child: ToggleCheckListButton( - attribute: Attribute.unchecked, - controller: controller, - icon: Icons.check_box, - iconSize: toolbarIconSize, - ), + if (showListCheck) + ToggleCheckListButton( + attribute: Attribute.unchecked, + controller: controller, + icon: Icons.check_box, + iconSize: toolbarIconSize, ), - Visibility( - visible: showCodeBlock, - child: ToggleStyleButton( - attribute: Attribute.codeBlock, - controller: controller, - icon: Icons.code, - iconSize: toolbarIconSize, - ), + if (showCodeBlock) + ToggleStyleButton( + attribute: Attribute.codeBlock, + controller: controller, + icon: Icons.code, + iconSize: toolbarIconSize, ), - Visibility( - visible: !showListNumbers && - !showListBullets && - !showListCheck && - !showCodeBlock, - child: VerticalDivider( - indent: 12, - endIndent: 12, - color: Colors.grey.shade400, - ), + if (isButtonGroupShown[2] && + (isButtonGroupShown[3] || isButtonGroupShown[4])) + VerticalDivider( + indent: 12, + endIndent: 12, + color: Colors.grey.shade400, ), - Visibility( - visible: showQuote, - child: ToggleStyleButton( - attribute: Attribute.blockQuote, - controller: controller, - icon: Icons.format_quote, - iconSize: toolbarIconSize, - ), + if (showQuote) + ToggleStyleButton( + attribute: Attribute.blockQuote, + controller: controller, + icon: Icons.format_quote, + iconSize: toolbarIconSize, ), - Visibility( - visible: showIndent, - child: IndentButton( - icon: Icons.format_indent_increase, - iconSize: toolbarIconSize, - controller: controller, - isIncrease: true, - ), + if (showIndent) + IndentButton( + icon: Icons.format_indent_increase, + iconSize: toolbarIconSize, + controller: controller, + isIncrease: true, ), - Visibility( - visible: showIndent, - child: IndentButton( - icon: Icons.format_indent_decrease, - iconSize: toolbarIconSize, - controller: controller, - isIncrease: false, - ), + if (showIndent) + IndentButton( + icon: Icons.format_indent_decrease, + iconSize: toolbarIconSize, + controller: controller, + isIncrease: false, ), - Visibility( - visible: showQuote, - child: VerticalDivider( - indent: 12, - endIndent: 12, - color: Colors.grey.shade400, - ), + if (isButtonGroupShown[3] && isButtonGroupShown[4]) + VerticalDivider( + indent: 12, + endIndent: 12, + color: Colors.grey.shade400, ), - Visibility( - visible: showLink, - child: LinkStyleButton( - controller: controller, - iconSize: toolbarIconSize, - ), + if (showLink) + LinkStyleButton( + controller: controller, + iconSize: toolbarIconSize, ), - Visibility( - visible: showHorizontalRule, - child: InsertEmbedButton( - controller: controller, - icon: Icons.horizontal_rule, - iconSize: toolbarIconSize, - ), + if (showHorizontalRule) + InsertEmbedButton( + controller: controller, + icon: Icons.horizontal_rule, + iconSize: toolbarIconSize, ), - ]); + ], + ); } final List children; From 6707181184ffb874117ea20f6fe13c8cd33a1314 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 1 Jun 2021 11:11:48 -0700 Subject: [PATCH 211/306] Upgrade version to 1.3.2 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc8284b..40b95eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.3.2] +* Fix copy/paste bug. + ## [1.3.1] * New logo. diff --git a/pubspec.yaml b/pubspec.yaml index e50130ff..1ce0c9bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.3.1 +version: 1.3.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 4bcb73fd25984881e44cabaec4f05500c5137d0c Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 1 Jun 2021 12:19:09 -0700 Subject: [PATCH 212/306] Format code --- lib/src/widgets/cursor.dart | 2 +- lib/src/widgets/text_line.dart | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 9fef16e5..763d396d 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -101,7 +101,7 @@ class CursorCont extends ChangeNotifier { required this.show, required CursorStyle style, required TickerProvider tickerProvider, - }) : _style = style, + }) : _style = style, blink = ValueNotifier(false), color = ValueNotifier(style.color) { _blinkOpacityController = diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 90f7ceff..d95923d3 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -628,7 +628,8 @@ class RenderEditableTextLine extends RenderEditableBox { : _leading!.getMinIntrinsicWidth(height - verticalPadding).ceil(); final bodyWidth = _body == null ? 0 - : _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding)) + : _body! + .getMinIntrinsicWidth(math.max(0, height - verticalPadding)) .ceil(); return horizontalPadding + leadingWidth + bodyWidth; } @@ -643,7 +644,8 @@ class RenderEditableTextLine extends RenderEditableBox { : _leading!.getMaxIntrinsicWidth(height - verticalPadding).ceil(); final bodyWidth = _body == null ? 0 - : _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding)) + : _body! + .getMaxIntrinsicWidth(math.max(0, height - verticalPadding)) .ceil(); return horizontalPadding + leadingWidth + bodyWidth; } From ff93000041d8c4ca64a3b555cd8074441d5f1905 Mon Sep 17 00:00:00 2001 From: Till Friebe Date: Tue, 1 Jun 2021 23:51:25 +0200 Subject: [PATCH 213/306] Bump file_picker to 3.0.2+2 With version 3.0.2 `name` of the file_picker library becomes non-nullable, so a warning was issued for users who had already used version 3.0.2, as we still assumed that `name` is nullable. Increasing the version and removing the exclamation mark removes the warning. --- lib/src/widgets/toolbar/image_button.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index 740ef269..74e9ae6d 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -80,7 +80,7 @@ class ImageButton extends StatelessWidget { } // Take first, because we don't allow picking multiple files. - final fileName = result.files.first.name!; + final fileName = result.files.first.name; final file = File(fileName); return onImagePickCallback!(file); diff --git a/pubspec.yaml b/pubspec.yaml index 1ce0c9bc..f7484234 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter collection: ^1.15.0 - file_picker: ^3.0.0 + file_picker: ^3.0.2+2 filesystem_picker: ^2.0.0-nullsafety.0 flutter_colorpicker: ^0.4.0 flutter_keyboard_visibility: ^5.0.0 From 06a7f4aa2e0150b4fac4123505f7af6ce0d498b8 Mon Sep 17 00:00:00 2001 From: hyouuu Date: Wed, 2 Jun 2021 00:17:27 -0700 Subject: [PATCH 214/306] Fix a bug that Embed could be together with Text (#249) --- lib/src/widgets/text_line.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index d95923d3..c258acc0 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -37,8 +37,14 @@ class TextLine extends StatelessWidget { Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - if (line.hasEmbed) { - final embed = line.children.single as Embed; + // In rare circumstances, the line could contain an Embed & a Text of + // newline, which is unexpected and probably we should find out the + // root cause + final childCount = line.childCount; + if (line.hasEmbed || + (childCount > 1 && line.children.first is Embed)) + { + final embed = line.children.first as Embed; return EmbedProxy(embedBuilder(context, embed)); } From e69e97481bbe21f5e204668d24921b271720e220 Mon Sep 17 00:00:00 2001 From: lucasbstn <64323294+lucasbstn@users.noreply.github.com> Date: Thu, 3 Jun 2021 20:01:00 +0300 Subject: [PATCH 215/306] Fix #242 (#254) --- lib/src/widgets/toolbar/color_button.dart | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/src/widgets/toolbar/color_button.dart b/lib/src/widgets/toolbar/color_button.dart index fa5bb520..fa757e8a 100644 --- a/lib/src/widgets/toolbar/color_button.dart +++ b/lib/src/widgets/toolbar/color_button.dart @@ -125,7 +125,7 @@ class _ColorButtonState extends State { ); } - void _changeColor(Color color) { + void _changeColor(BuildContext context, Color color) { var hex = color.value.toRadixString(16); if (hex.startsWith('ff')) { hex = hex.substring(2); @@ -139,15 +139,16 @@ class _ColorButtonState extends State { void _showColorPicker() { showDialog( context: context, - builder: (_) => AlertDialog( - title: const Text('Select Color'), - backgroundColor: Theme.of(context).canvasColor, - content: SingleChildScrollView( - child: MaterialPicker( - pickerColor: const Color(0x00000000), - onColorChanged: _changeColor, - ), - )), + builder: (context) => AlertDialog( + title: const Text('Select Color'), + backgroundColor: Theme.of(context).canvasColor, + content: SingleChildScrollView( + child: MaterialPicker( + pickerColor: const Color(0x00000000), + onColorChanged: (color) => _changeColor(context, color), + ), + ), + ), ); } } From 76ef63e87c1fed0b867daec9402649112ee71d0e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 3 Jun 2021 10:45:13 -0700 Subject: [PATCH 216/306] Upgrade to 1.3.3 --- CHANGELOG.md | 3 +++ lib/src/widgets/text_line.dart | 4 +--- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b95eab..601b3bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.3.3] +* Upgrade file_picker version. + ## [1.3.2] * Fix copy/paste bug. diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index c258acc0..b44f16a9 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -41,9 +41,7 @@ class TextLine extends StatelessWidget { // newline, which is unexpected and probably we should find out the // root cause final childCount = line.childCount; - if (line.hasEmbed || - (childCount > 1 && line.children.first is Embed)) - { + if (line.hasEmbed || (childCount > 1 && line.children.first is Embed)) { final embed = line.children.first as Embed; return EmbedProxy(embedBuilder(context, embed)); } diff --git a/pubspec.yaml b/pubspec.yaml index f7484234..98818c9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.3.2 +version: 1.3.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 71a0cec282433e3f2e0828d9281a0f6321cd4514 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 3 Jun 2021 20:38:21 -0700 Subject: [PATCH 217/306] Format code --- lib/src/widgets/cursor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index 763d396d..9fef16e5 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -101,7 +101,7 @@ class CursorCont extends ChangeNotifier { required this.show, required CursorStyle style, required TickerProvider tickerProvider, - }) : _style = style, + }) : _style = style, blink = ValueNotifier(false), color = ValueNotifier(style.color) { _blinkOpacityController = From 4b4c84696f52b32bd4f9030117abdf9c3dca8d23 Mon Sep 17 00:00:00 2001 From: hyouuu Date: Sat, 12 Jun 2021 14:47:26 -0700 Subject: [PATCH 218/306] Add option to paintCursorAboveText (#265) --- lib/src/widgets/editor.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index fb90db76..89796281 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -125,6 +125,7 @@ class QuillEditor extends StatefulWidget { required this.readOnly, required this.expands, this.showCursor, + this.paintCursorAboveText, this.placeholder, this.enableInteractiveSelection = true, this.scrollBottomInset = 0, @@ -165,6 +166,7 @@ class QuillEditor extends StatefulWidget { final EdgeInsetsGeometry padding; final bool autoFocus; final bool? showCursor; + final bool? paintCursorAboveText; final bool readOnly; final String? placeholder; final bool enableInteractiveSelection; @@ -287,7 +289,7 @@ class _QuillEditorState extends State width: 2, radius: cursorRadius, offset: cursorOffset, - paintAboveText: paintCursorAboveText, + paintAboveText: widget.paintCursorAboveText ?? paintCursorAboveText, opacityAnimates: cursorOpacityAnimates, ), widget.textCapitalization, From 17257374a7163f6e63ee915dbe59216ed80e6ff6 Mon Sep 17 00:00:00 2001 From: zhaoce Date: Tue, 15 Jun 2021 11:43:28 +0800 Subject: [PATCH 219/306] =?UTF-8?q?Fix=20a=20wrong=20merge=EF=BC=9Aabd80f5?= =?UTF-8?q?=20(#266)=20(#267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/widgets/keyboard_listener.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart index 58df1725..ceabf931 100644 --- a/lib/src/widgets/keyboard_listener.dart +++ b/lib/src/widgets/keyboard_listener.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; enum InputShortcut { CUT, COPY, PASTE, SELECT_ALL } @@ -62,14 +63,14 @@ class KeyboardListener { LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, }; - bool handleRawKeyEvent(RawKeyEvent event) { + KeyEventResult handleRawKeyEvent(RawKeyEvent event) { if (kIsWeb) { // On web platform, we ignore the key because it's already processed. - return false; + return KeyEventResult.ignored; } if (event is! RawKeyDownEvent) { - return false; + return KeyEventResult.ignored; } final keysPressed = @@ -82,7 +83,7 @@ class KeyboardListener { .length > 1 || keysPressed.difference(_interestingKeys).isNotEmpty) { - return false; + return KeyEventResult.ignored; } if (_moveKeys.contains(key)) { @@ -100,6 +101,6 @@ class KeyboardListener { } else if (key == LogicalKeyboardKey.backspace) { onDelete(false); } - return false; + return KeyEventResult.ignored; } } From 9af452e92b583a1a82cb8c5ec4ba2414a1c4e3d5 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 16 Jun 2021 23:37:55 -0700 Subject: [PATCH 220/306] Upgrade version to 1.3.4 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 601b3bf2..a17d4d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.3.4] +* Add option to paintCursorAboveText. + ## [1.3.3] * Upgrade file_picker version. diff --git a/pubspec.yaml b/pubspec.yaml index 98818c9c..1b2216e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.3.3 +version: 1.3.4 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 6ac149028157c58b2c162e11074a77434d2c8cc0 Mon Sep 17 00:00:00 2001 From: li3317 Date: Fri, 18 Jun 2021 09:05:09 -0400 Subject: [PATCH 221/306] remove path_provider dependency --- README.md | 4 ++++ example/lib/pages/home_page.dart | 16 ++++++++++++++-- example/lib/widgets/demo_scaffold.dart | 13 ++++++++++++- lib/src/widgets/toolbar.dart | 6 ++++++ lib/src/widgets/toolbar/image_button.dart | 18 ++++++++++++------ pubspec.yaml | 1 - 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bbedb500..e8433608 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,10 @@ For web development, use `flutter config --enable-web` for flutter and use [Reac It is required to provide EmbedBuilder, e.g. [defaultEmbedBuilderWeb](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/universal_ui/universal_ui.dart#L28). +## Desktop + +It is required to provide application document directory for image button. See example in [example](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart). + ## Migrate Zefyr Data Check out [code](https://github.com/jwehrle/zefyr_quill_convert) and [doc](https://docs.google.com/document/d/1FUSrpbarHnilb7uDN5J5DDahaI0v1RMXBjj4fFSpSuY/edit?usp=sharing). diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index e0f436f8..0f0650ef 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -157,18 +157,30 @@ class _HomePageState extends State { const EdgeInsets.symmetric(vertical: 16, horizontal: 8), child: QuillToolbar.basic( controller: _controller!, - onImagePickCallback: _onImagePickCallback), + onImagePickCallback: _onImagePickCallback, + applicationPath: + !(kIsWeb || Platform.isAndroid || Platform.isIOS) + ? null + : getApplicationDirectoryForDesktop()), )) : Container( child: QuillToolbar.basic( controller: _controller!, - onImagePickCallback: _onImagePickCallback), + onImagePickCallback: _onImagePickCallback, + applicationPath: + !(kIsWeb || Platform.isAndroid || Platform.isIOS) + ? null + : getApplicationDirectoryForDesktop()), ), ], ), ); } + Future getApplicationDirectoryForDesktop() async { + return await getApplicationDocumentsDirectory(); + } + // Renders the image picked by imagePicker from local file storage // You can also upload the picked image to any server (eg : AWS s3 // or Firebase) and then return the uploaded image URL. diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 944b7fe9..3aec94b4 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -1,8 +1,11 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/flutter_quill.dart' hide Text; +import 'package:path_provider/path_provider.dart'; typedef DemoContentBuilder = Widget Function( BuildContext context, QuillController? controller); @@ -70,6 +73,10 @@ class _DemoScaffoldState extends State { } } + Future getApplicationDirectoryForDesktop() async { + return await getApplicationDocumentsDirectory(); + } + @override Widget build(BuildContext context) { final actions = widget.actions ?? []; @@ -90,7 +97,11 @@ class _DemoScaffoldState extends State { ), title: _loading || widget.showToolbar == false ? null - : QuillToolbar.basic(controller: _controller!), + : QuillToolbar.basic(controller: _controller!, + applicationPath: + !(kIsWeb || Platform.isAndroid || Platform.isIOS) + ? null + : getApplicationDirectoryForDesktop()), actions: actions, ), floatingActionButton: widget.floatingActionButton, diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 76be1824..27558c8d 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -45,6 +45,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { required this.children, this.toolBarHeight = 36, this.color, + this.applicationPath, Key? key, }) : super(key: key); @@ -69,6 +70,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showHistory = true, bool showHorizontalRule = false, OnImagePickCallback? onImagePickCallback, + Future? applicationPath, Key? key, }) { final isButtonGroupShown = [ @@ -160,6 +162,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, imageSource: ImageSource.gallery, onImagePickCallback: onImagePickCallback, + applicationPath: applicationPath, ), if (onImagePickCallback != null) ImageButton( @@ -168,6 +171,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, imageSource: ImageSource.camera, onImagePickCallback: onImagePickCallback, + applicationPath: applicationPath, ), if (isButtonGroupShown[0] && (isButtonGroupShown[1] || @@ -279,6 +283,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { /// is given. final Color? color; + final Directory? applicationPath; + @override Size get preferredSize => Size.fromHeight(toolBarHeight); diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index 74e9ae6d..9b20e34f 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -5,7 +5,6 @@ import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:path_provider/path_provider.dart'; import '../../models/documents/nodes/embed.dart'; import '../controller.dart'; @@ -21,6 +20,7 @@ class ImageButton extends StatelessWidget { this.fillColor, this.onImagePickCallback, this.imagePickImpl, + this.applicationPath, Key? key, }) : super(key: key); @@ -37,6 +37,8 @@ class ImageButton extends StatelessWidget { final ImageSource imageSource; + final Future? applicationPath; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -47,11 +49,12 @@ class ImageButton extends StatelessWidget { hoverElevation: 0, size: iconSize * 1.77, fillColor: fillColor ?? theme.canvasColor, - onPressed: () => _handleImageButtonTap(context), + onPressed: () => _handleImageButtonTap(context, applicationPath), ); } - Future _handleImageButtonTap(BuildContext context) async { + Future _handleImageButtonTap(BuildContext context, + [Future? applicationPath]) async { final index = controller.selection.baseOffset; final length = controller.selection.extentOffset - index; @@ -64,7 +67,9 @@ class ImageButton extends StatelessWidget { } else if (Platform.isAndroid || Platform.isIOS) { imageUrl = await _pickImage(imageSource); } else { - imageUrl = await _pickImageDesktop(context); + assert(applicationPath != null, + 'Desktop must provide application document directory'); + imageUrl = await _pickImageDesktop(context, applicationPath!); } } @@ -95,10 +100,11 @@ class ImageButton extends StatelessWidget { return onImagePickCallback!(File(pickedFile.path)); } - Future _pickImageDesktop(BuildContext context) async { + Future _pickImageDesktop(BuildContext context, + Future applicationPath) async { final filePath = await FilesystemPicker.open( context: context, - rootDirectory: await getApplicationDocumentsDirectory(), + rootDirectory: await applicationPath, fsType: FilesystemType.file, fileTileSelectMode: FileTileSelectMode.wholeTile, ); diff --git a/pubspec.yaml b/pubspec.yaml index 1b2216e7..6b181bd6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,6 @@ dependencies: flutter_colorpicker: ^0.4.0 flutter_keyboard_visibility: ^5.0.0 image_picker: ^0.7.3 - path_provider: ^2.0.1 photo_view: ^0.11.1 quiver: ^3.0.0 string_validator: ^0.3.0 From e8c0c484a498865973328a4a22253e45e8a356ef Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 18 Jun 2021 09:58:51 -0700 Subject: [PATCH 222/306] Upgrade version to 1.4.0 --- CHANGELOG.md | 3 +++ example/lib/pages/home_page.dart | 28 ++++++++++------------- example/lib/widgets/demo_scaffold.dart | 15 ++++++------ lib/src/widgets/toolbar.dart | 2 +- lib/src/widgets/toolbar/image_button.dart | 6 ++--- pubspec.yaml | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a17d4d7f..cb4229be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.4.0] +* Remove path_provider dependency. + ## [1.3.4] * Add option to paintCursorAboveText. diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 0f0650ef..57eab392 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -138,6 +138,16 @@ class _HomePageState extends State { ), embedBuilder: defaultEmbedBuilderWeb); } + var toolbar = QuillToolbar.basic( + controller: _controller!, onImagePickCallback: _onImagePickCallback); + const isDesktop = !kIsWeb && !Platform.isAndroid && !Platform.isIOS; + if (isDesktop) { + toolbar = QuillToolbar.basic( + controller: _controller!, + onImagePickCallback: _onImagePickCallback, + applicationPath: getApplicationDirectoryForDesktop()); + } + return SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -155,23 +165,9 @@ class _HomePageState extends State { child: Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8), - child: QuillToolbar.basic( - controller: _controller!, - onImagePickCallback: _onImagePickCallback, - applicationPath: - !(kIsWeb || Platform.isAndroid || Platform.isIOS) - ? null - : getApplicationDirectoryForDesktop()), + child: toolbar, )) - : Container( - child: QuillToolbar.basic( - controller: _controller!, - onImagePickCallback: _onImagePickCallback, - applicationPath: - !(kIsWeb || Platform.isAndroid || Platform.isIOS) - ? null - : getApplicationDirectoryForDesktop()), - ), + : Container(child: toolbar) ], ), ); diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 3aec94b4..fe89dbb1 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -80,6 +80,13 @@ class _DemoScaffoldState extends State { @override Widget build(BuildContext context) { final actions = widget.actions ?? []; + var toolbar = QuillToolbar.basic(controller: _controller!); + const isDesktop = !kIsWeb && !Platform.isAndroid && !Platform.isIOS; + if (isDesktop) { + toolbar = QuillToolbar.basic( + controller: _controller!, + applicationPath: getApplicationDirectoryForDesktop()); + } return Scaffold( key: _scaffoldKey, appBar: AppBar( @@ -95,13 +102,7 @@ class _DemoScaffoldState extends State { ), onPressed: () => Navigator.pop(context), ), - title: _loading || widget.showToolbar == false - ? null - : QuillToolbar.basic(controller: _controller!, - applicationPath: - !(kIsWeb || Platform.isAndroid || Platform.isIOS) - ? null - : getApplicationDirectoryForDesktop()), + title: _loading || widget.showToolbar == false ? null : toolbar, actions: actions, ), floatingActionButton: widget.floatingActionButton, diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 27558c8d..d9644d19 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -283,7 +283,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { /// is given. final Color? color; - final Directory? applicationPath; + final Future? applicationPath; @override Size get preferredSize => Size.fromHeight(toolBarHeight); diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index 9b20e34f..7cb86d51 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -68,7 +68,7 @@ class ImageButton extends StatelessWidget { imageUrl = await _pickImage(imageSource); } else { assert(applicationPath != null, - 'Desktop must provide application document directory'); + 'Desktop must provide application document directory'); imageUrl = await _pickImageDesktop(context, applicationPath!); } } @@ -100,8 +100,8 @@ class ImageButton extends StatelessWidget { return onImagePickCallback!(File(pickedFile.path)); } - Future _pickImageDesktop(BuildContext context, - Future applicationPath) async { + Future _pickImageDesktop( + BuildContext context, Future applicationPath) async { final filePath = await FilesystemPicker.open( context: context, rootDirectory: await applicationPath, diff --git a/pubspec.yaml b/pubspec.yaml index 6b181bd6..6ab72fdd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 1.3.4 +version: 1.4.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 9f89ae948a72c37a8fb4603b271bf191ed1c01e4 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 18 Jun 2021 10:01:42 -0700 Subject: [PATCH 223/306] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8433608..d0579050 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ It is required to provide EmbedBuilder, e.g. [defaultEmbedBuilderWeb](https://gi ## Desktop -It is required to provide application document directory for image button. See example in [example](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart). +It is required to provide application document directory for toolbar image button, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L148). ## Migrate Zefyr Data From 55e99e586212e4b3ebe17fc100b14690ea785555 Mon Sep 17 00:00:00 2001 From: li3317 Date: Fri, 18 Jun 2021 13:12:04 -0400 Subject: [PATCH 224/306] small fix --- example/lib/pages/home_page.dart | 2 +- example/lib/widgets/demo_scaffold.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 57eab392..898aefb9 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -140,7 +140,7 @@ class _HomePageState extends State { } var toolbar = QuillToolbar.basic( controller: _controller!, onImagePickCallback: _onImagePickCallback); - const isDesktop = !kIsWeb && !Platform.isAndroid && !Platform.isIOS; + final isDesktop = !kIsWeb && !Platform.isAndroid && !Platform.isIOS; if (isDesktop) { toolbar = QuillToolbar.basic( controller: _controller!, diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index fe89dbb1..489f3796 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -81,7 +81,7 @@ class _DemoScaffoldState extends State { Widget build(BuildContext context) { final actions = widget.actions ?? []; var toolbar = QuillToolbar.basic(controller: _controller!); - const isDesktop = !kIsWeb && !Platform.isAndroid && !Platform.isIOS; + final isDesktop = !kIsWeb && !Platform.isAndroid && !Platform.isIOS; if (isDesktop) { toolbar = QuillToolbar.basic( controller: _controller!, @@ -102,7 +102,7 @@ class _DemoScaffoldState extends State { ), onPressed: () => Navigator.pop(context), ), - title: _loading || widget.showToolbar == false ? null : toolbar, + title: _loading || !widget.showToolbar? null : toolbar, actions: actions, ), floatingActionButton: widget.floatingActionButton, From 6dc4826734933bdece3b5483d7b5c1720e8866d4 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 18 Jun 2021 10:33:02 -0700 Subject: [PATCH 225/306] Format code --- example/lib/widgets/demo_scaffold.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 489f3796..33babbe9 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -102,7 +102,7 @@ class _DemoScaffoldState extends State { ), onPressed: () => Navigator.pop(context), ), - title: _loading || !widget.showToolbar? null : toolbar, + title: _loading || !widget.showToolbar ? null : toolbar, actions: actions, ), floatingActionButton: widget.floatingActionButton, From 457c6e561ba4b6ea3e5b04c50a76e30034201c52 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 18 Jun 2021 12:00:42 -0700 Subject: [PATCH 226/306] Remove filesystem_picker dependency --- example/lib/pages/home_page.dart | 12 +++++++++--- example/lib/widgets/demo_scaffold.dart | 12 +++++++++--- example/pubspec.yaml | 1 + lib/src/widgets/toolbar.dart | 11 ++++++----- lib/src/widgets/toolbar/image_button.dart | 23 ++++++++--------------- pubspec.yaml | 1 - 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 898aefb9..a8f2cd6d 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -145,7 +146,7 @@ class _HomePageState extends State { toolbar = QuillToolbar.basic( controller: _controller!, onImagePickCallback: _onImagePickCallback, - applicationPath: getApplicationDirectoryForDesktop()); + filePickImpl: openFileSystemPickerForDesktop); } return SafeArea( @@ -173,8 +174,13 @@ class _HomePageState extends State { ); } - Future getApplicationDirectoryForDesktop() async { - return await getApplicationDocumentsDirectory(); + Future openFileSystemPickerForDesktop(BuildContext context) async { + return await FilesystemPicker.open( + context: context, + rootDirectory: await getApplicationDocumentsDirectory(), + fsType: FilesystemType.file, + fileTileSelectMode: FileTileSelectMode.wholeTile, + ); } // Renders the image picked by imagePicker from local file storage diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart index 33babbe9..b8b3b402 100644 --- a/example/lib/widgets/demo_scaffold.dart +++ b/example/lib/widgets/demo_scaffold.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -73,8 +74,13 @@ class _DemoScaffoldState extends State { } } - Future getApplicationDirectoryForDesktop() async { - return await getApplicationDocumentsDirectory(); + Future openFileSystemPickerForDesktop(BuildContext context) async { + return await FilesystemPicker.open( + context: context, + rootDirectory: await getApplicationDocumentsDirectory(), + fsType: FilesystemType.file, + fileTileSelectMode: FileTileSelectMode.wholeTile, + ); } @override @@ -85,7 +91,7 @@ class _DemoScaffoldState extends State { if (isDesktop) { toolbar = QuillToolbar.basic( controller: _controller!, - applicationPath: getApplicationDirectoryForDesktop()); + filePickImpl: openFileSystemPickerForDesktop); } return Scaffold( key: _scaffoldKey, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e1bd45f5..77cd5f64 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 path_provider: ^2.0.1 + filesystem_picker: ^2.0.0-nullsafety.0 flutter_quill: path: ../ diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index d9644d19..a539749c 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -33,6 +33,7 @@ export 'toolbar/toggle_style_button.dart'; typedef OnImagePickCallback = Future Function(File file); typedef ImagePickImpl = Future Function(ImageSource source); +typedef FilePickImpl = Future Function(BuildContext context); // The default size of the icon of a button. const double kDefaultIconSize = 18; @@ -45,7 +46,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { required this.children, this.toolBarHeight = 36, this.color, - this.applicationPath, + this.filePickImpl, Key? key, }) : super(key: key); @@ -70,7 +71,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showHistory = true, bool showHorizontalRule = false, OnImagePickCallback? onImagePickCallback, - Future? applicationPath, + FilePickImpl? filePickImpl, Key? key, }) { final isButtonGroupShown = [ @@ -162,7 +163,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, imageSource: ImageSource.gallery, onImagePickCallback: onImagePickCallback, - applicationPath: applicationPath, + filePickImpl: filePickImpl, ), if (onImagePickCallback != null) ImageButton( @@ -171,7 +172,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { controller: controller, imageSource: ImageSource.camera, onImagePickCallback: onImagePickCallback, - applicationPath: applicationPath, + filePickImpl: filePickImpl, ), if (isButtonGroupShown[0] && (isButtonGroupShown[1] || @@ -283,7 +284,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { /// is given. final Color? color; - final Future? applicationPath; + final FilePickImpl? filePickImpl; @override Size get preferredSize => Size.fromHeight(toolBarHeight); diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index 7cb86d51..e7ec6a2f 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; -import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -20,7 +19,7 @@ class ImageButton extends StatelessWidget { this.fillColor, this.onImagePickCallback, this.imagePickImpl, - this.applicationPath, + this.filePickImpl, Key? key, }) : super(key: key); @@ -37,7 +36,7 @@ class ImageButton extends StatelessWidget { final ImageSource imageSource; - final Future? applicationPath; + final FilePickImpl? filePickImpl; @override Widget build(BuildContext context) { @@ -49,12 +48,12 @@ class ImageButton extends StatelessWidget { hoverElevation: 0, size: iconSize * 1.77, fillColor: fillColor ?? theme.canvasColor, - onPressed: () => _handleImageButtonTap(context, applicationPath), + onPressed: () => _handleImageButtonTap(context, filePickImpl), ); } Future _handleImageButtonTap(BuildContext context, - [Future? applicationPath]) async { + [FilePickImpl? filePickImpl]) async { final index = controller.selection.baseOffset; final length = controller.selection.extentOffset - index; @@ -67,9 +66,8 @@ class ImageButton extends StatelessWidget { } else if (Platform.isAndroid || Platform.isIOS) { imageUrl = await _pickImage(imageSource); } else { - assert(applicationPath != null, - 'Desktop must provide application document directory'); - imageUrl = await _pickImageDesktop(context, applicationPath!); + assert(filePickImpl != null, 'Desktop must provide filePickImpl'); + imageUrl = await _pickImageDesktop(context, filePickImpl!); } } @@ -101,13 +99,8 @@ class ImageButton extends StatelessWidget { } Future _pickImageDesktop( - BuildContext context, Future applicationPath) async { - final filePath = await FilesystemPicker.open( - context: context, - rootDirectory: await applicationPath, - fsType: FilesystemType.file, - fileTileSelectMode: FileTileSelectMode.wholeTile, - ); + BuildContext context, FilePickImpl filePickImpl) async { + final filePath = await filePickImpl(context); if (filePath == null || filePath.isEmpty) return null; final file = File(filePath); diff --git a/pubspec.yaml b/pubspec.yaml index 6ab72fdd..0e0e52c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,6 @@ dependencies: sdk: flutter collection: ^1.15.0 file_picker: ^3.0.2+2 - filesystem_picker: ^2.0.0-nullsafety.0 flutter_colorpicker: ^0.4.0 flutter_keyboard_visibility: ^5.0.0 image_picker: ^0.7.3 From 7349d7c283743aea390217e7b9636f49191eb0b1 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 18 Jun 2021 12:02:09 -0700 Subject: [PATCH 227/306] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0579050..a1bfcf27 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ It is required to provide EmbedBuilder, e.g. [defaultEmbedBuilderWeb](https://gi ## Desktop -It is required to provide application document directory for toolbar image button, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L148). +It is required to provide `filePickImpl` for toolbar image button, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L149). ## Migrate Zefyr Data From 8e19042d1fa805ff66ce991a9a6cf5604009bfe7 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 18 Jun 2021 12:03:14 -0700 Subject: [PATCH 228/306] Upgrade to 1.4.1 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4229be..0d22f714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.4.1] +* Remove filesystem_picker dependency. + ## [1.4.0] * Remove path_provider dependency. diff --git a/pubspec.yaml b/pubspec.yaml index 0e0e52c9..2609d632 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.4.0 +version: 1.4.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 5318eeaa322968a77531b421c349c4dc1e868f6a Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 18 Jun 2021 15:33:45 -0700 Subject: [PATCH 229/306] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1bfcf27..02ae257b 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ The `QuillToolbar` class lets you customise which formatting options are availab For web development, use `flutter config --enable-web` for flutter and use [ReactQuill] for React. -It is required to provide EmbedBuilder, e.g. [defaultEmbedBuilderWeb](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/universal_ui/universal_ui.dart#L28). +It is required to provide `EmbedBuilder`, e.g. [defaultEmbedBuilderWeb](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/universal_ui/universal_ui.dart#L28). ## Desktop From 08fc027712b3731a86ab43dfb0cf9616e38f78ff Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 23 Jun 2021 09:00:53 -0700 Subject: [PATCH 230/306] Replace deprecated textEditingValue method (#271) * Replace deprecated textEditingValue method * Change to SelectionChangedCause.drag --- lib/src/widgets/text_selection.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index a8748de1..85362c7a 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -157,8 +157,9 @@ class EditorTextSelectionOverlay { throw 'Invalid position'; } selectionDelegate - ..textEditingValue = - value.copyWith(selection: newSelection, composing: TextRange.empty) + ..userUpdateTextEditingValue( + value.copyWith(selection: newSelection, composing: TextRange.empty), + SelectionChangedCause.drag) ..bringIntoView(textPosition); } From 195ccd7c7e2c0e5152e1fb1047c2ccc73e25f31e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 23 Jun 2021 09:52:55 -0700 Subject: [PATCH 231/306] Update _pickImageWeb method --- lib/src/widgets/toolbar/image_button.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index e7ec6a2f..a94fa89a 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -62,7 +62,7 @@ class ImageButton extends StatelessWidget { imageUrl = await imagePickImpl!(imageSource); } else { if (kIsWeb) { - imageUrl = await _pickImageWeb(); + imageUrl = await _pickImageWeb(onImagePickCallback!); } else if (Platform.isAndroid || Platform.isIOS) { imageUrl = await _pickImage(imageSource); } else { @@ -76,7 +76,7 @@ class ImageButton extends StatelessWidget { } } - Future _pickImageWeb() async { + Future _pickImageWeb(OnImagePickCallback onImagePickCallback) async { final result = await FilePicker.platform.pickFiles(); if (result == null) { return null; @@ -86,7 +86,7 @@ class ImageButton extends StatelessWidget { final fileName = result.files.first.name; final file = File(fileName); - return onImagePickCallback!(file); + return onImagePickCallback(file); } Future _pickImage(ImageSource source) async { From d9c0c53106c7881b85ae4ab2c46775ed132972c4 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 23 Jun 2021 09:54:53 -0700 Subject: [PATCH 232/306] Update _pickImageDesktop method --- lib/src/widgets/toolbar/image_button.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index a94fa89a..813015de 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -64,10 +64,11 @@ class ImageButton extends StatelessWidget { if (kIsWeb) { imageUrl = await _pickImageWeb(onImagePickCallback!); } else if (Platform.isAndroid || Platform.isIOS) { - imageUrl = await _pickImage(imageSource); + imageUrl = await _pickImage(imageSource, onImagePickCallback!); } else { assert(filePickImpl != null, 'Desktop must provide filePickImpl'); - imageUrl = await _pickImageDesktop(context, filePickImpl!); + imageUrl = await _pickImageDesktop( + context, filePickImpl!, onImagePickCallback!); } } @@ -89,21 +90,24 @@ class ImageButton extends StatelessWidget { return onImagePickCallback(file); } - Future _pickImage(ImageSource source) async { + Future _pickImage( + ImageSource source, OnImagePickCallback onImagePickCallback) async { final pickedFile = await ImagePicker().getImage(source: source); if (pickedFile == null) { return null; } - return onImagePickCallback!(File(pickedFile.path)); + return onImagePickCallback(File(pickedFile.path)); } Future _pickImageDesktop( - BuildContext context, FilePickImpl filePickImpl) async { + BuildContext context, + FilePickImpl filePickImpl, + OnImagePickCallback onImagePickCallback) async { final filePath = await filePickImpl(context); if (filePath == null || filePath.isEmpty) return null; final file = File(filePath); - return onImagePickCallback!(file); + return onImagePickCallback(file); } } From 2ba2e7e309e27c51dcaca0d37acd90ecd0117b9b Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 23 Jun 2021 10:14:15 -0700 Subject: [PATCH 233/306] Remove file_picker dependency --- example/lib/pages/home_page.dart | 21 +++++++++++++++++++++ example/pubspec.yaml | 1 + lib/src/widgets/toolbar.dart | 5 +++++ lib/src/widgets/toolbar/image_button.dart | 23 ++++++++--------------- pubspec.yaml | 1 - 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index a8f2cd6d..0dc94999 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:file_picker/file_picker.dart'; import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -141,6 +142,12 @@ class _HomePageState extends State { } var toolbar = QuillToolbar.basic( controller: _controller!, onImagePickCallback: _onImagePickCallback); + if (kIsWeb) { + toolbar = QuillToolbar.basic( + controller: _controller!, + onImagePickCallback: _onImagePickCallback, + webImagePickImpl: _webImagePickImpl); + } final isDesktop = !kIsWeb && !Platform.isAndroid && !Platform.isIOS; if (isDesktop) { toolbar = QuillToolbar.basic( @@ -194,6 +201,20 @@ class _HomePageState extends State { return copiedFile.path.toString(); } + Future _webImagePickImpl( + OnImagePickCallback onImagePickCallback) async { + final result = await FilePicker.platform.pickFiles(); + if (result == null) { + return null; + } + + // Take first, because we don't allow picking multiple files. + final fileName = result.files.first.name; + final file = File(fileName); + + return onImagePickCallback(file); + } + Widget _buildMenuBar(BuildContext context) { final size = MediaQuery.of(context).size; const itemStyle = TextStyle( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 77cd5f64..e0ec2585 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: cupertino_icons: ^1.0.2 path_provider: ^2.0.1 filesystem_picker: ^2.0.0-nullsafety.0 + file_picker: ^3.0.2+2 flutter_quill: path: ../ diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index a539749c..d5db8774 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -34,6 +34,8 @@ export 'toolbar/toggle_style_button.dart'; typedef OnImagePickCallback = Future Function(File file); typedef ImagePickImpl = Future Function(ImageSource source); typedef FilePickImpl = Future Function(BuildContext context); +typedef WebImagePickImpl = Future Function( + OnImagePickCallback onImagePickCallback); // The default size of the icon of a button. const double kDefaultIconSize = 18; @@ -72,6 +74,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showHorizontalRule = false, OnImagePickCallback? onImagePickCallback, FilePickImpl? filePickImpl, + WebImagePickImpl? webImagePickImpl, Key? key, }) { final isButtonGroupShown = [ @@ -164,6 +167,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { imageSource: ImageSource.gallery, onImagePickCallback: onImagePickCallback, filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, ), if (onImagePickCallback != null) ImageButton( @@ -173,6 +177,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { imageSource: ImageSource.camera, onImagePickCallback: onImagePickCallback, filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, ), if (isButtonGroupShown[0] && (isButtonGroupShown[1] || diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart index 813015de..1acfc40f 100644 --- a/lib/src/widgets/toolbar/image_button.dart +++ b/lib/src/widgets/toolbar/image_button.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -20,6 +19,7 @@ class ImageButton extends StatelessWidget { this.onImagePickCallback, this.imagePickImpl, this.filePickImpl, + this.webImagePickImpl, Key? key, }) : super(key: key); @@ -34,6 +34,8 @@ class ImageButton extends StatelessWidget { final ImagePickImpl? imagePickImpl; + final WebImagePickImpl? webImagePickImpl; + final ImageSource imageSource; final FilePickImpl? filePickImpl; @@ -62,7 +64,11 @@ class ImageButton extends StatelessWidget { imageUrl = await imagePickImpl!(imageSource); } else { if (kIsWeb) { - imageUrl = await _pickImageWeb(onImagePickCallback!); + assert( + webImagePickImpl != null, + 'Please provide webImagePickImpl for Web ' + '(check out example directory for how to do it)'); + imageUrl = await webImagePickImpl!(onImagePickCallback!); } else if (Platform.isAndroid || Platform.isIOS) { imageUrl = await _pickImage(imageSource, onImagePickCallback!); } else { @@ -77,19 +83,6 @@ class ImageButton extends StatelessWidget { } } - Future _pickImageWeb(OnImagePickCallback onImagePickCallback) async { - final result = await FilePicker.platform.pickFiles(); - if (result == null) { - return null; - } - - // Take first, because we don't allow picking multiple files. - final fileName = result.files.first.name; - final file = File(fileName); - - return onImagePickCallback(file); - } - Future _pickImage( ImageSource source, OnImagePickCallback onImagePickCallback) async { final pickedFile = await ImagePicker().getImage(source: source); diff --git a/pubspec.yaml b/pubspec.yaml index 2609d632..1257fe8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: flutter: sdk: flutter collection: ^1.15.0 - file_picker: ^3.0.2+2 flutter_colorpicker: ^0.4.0 flutter_keyboard_visibility: ^5.0.0 image_picker: ^0.7.3 From a65caa582ad00e75192577c588c4f8ff6776ac82 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 23 Jun 2021 10:40:57 -0700 Subject: [PATCH 234/306] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 02ae257b..597ea959 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ The `QuillToolbar` class lets you customise which formatting options are availab For web development, use `flutter config --enable-web` for flutter and use [ReactQuill] for React. It is required to provide `EmbedBuilder`, e.g. [defaultEmbedBuilderWeb](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/universal_ui/universal_ui.dart#L28). +Also it is required to provide `webImagePickImpl`, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L204). ## Desktop From 3cdf17856ac75133a60a238724186bb83a618e8e Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Wed, 23 Jun 2021 10:42:49 -0700 Subject: [PATCH 235/306] Upgrade to 1.5.0 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d22f714..45cc1fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.5.0] +* Remove file_picker dependency. + ## [1.4.1] * Remove filesystem_picker dependency. diff --git a/pubspec.yaml b/pubspec.yaml index 1257fe8c..a55996e0 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.4.1 +version: 1.5.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 40d853e6317271d029729a48ffc9de5c8fca88db Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 23 Jun 2021 10:51:14 -0700 Subject: [PATCH 236/306] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 597ea959..c205b123 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Also it is required to provide `webImagePickImpl`, e.g. [Sample Page](https://gi ## Desktop -It is required to provide `filePickImpl` for toolbar image button, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L149). +It is required to provide `filePickImpl` for toolbar image button, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L184). ## Migrate Zefyr Data From 3c1ee9eec2fa88f072e75b7b7f4f642dca126dea Mon Sep 17 00:00:00 2001 From: florianh01 <78513002+florianh01@users.noreply.github.com> Date: Thu, 24 Jun 2021 21:17:23 +0200 Subject: [PATCH 237/306] Support Multi Row Toolbar (#273) --- example/lib/pages/home_page.dart | 5 ++++- lib/src/widgets/toolbar.dart | 23 ++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 0dc94999..585e0ad2 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -141,7 +141,10 @@ class _HomePageState extends State { embedBuilder: defaultEmbedBuilderWeb); } var toolbar = QuillToolbar.basic( - controller: _controller!, onImagePickCallback: _onImagePickCallback); + controller: _controller!, + multiRowsDisplay: false, + onImagePickCallback: _onImagePickCallback, + ); if (kIsWeb) { toolbar = QuillToolbar.basic( controller: _controller!, diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index d5db8774..ddc31d42 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -49,6 +49,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { this.toolBarHeight = 36, this.color, this.filePickImpl, + this.multiRowsDisplay, Key? key, }) : super(key: key); @@ -72,6 +73,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showLink = true, bool showHistory = true, bool showHorizontalRule = false, + bool multiRowsDisplay = true, OnImagePickCallback? onImagePickCallback, FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl, @@ -96,6 +98,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { return QuillToolbar( key: key, toolBarHeight: toolbarIconSize * 2, + multiRowsDisplay: multiRowsDisplay, children: [ if (showHistory) HistoryButton( @@ -282,6 +285,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { final List children; final double toolBarHeight; + final bool? multiRowsDisplay; /// The color of the toolbar. /// @@ -296,10 +300,19 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - return Container( - constraints: BoxConstraints.tightFor(height: preferredSize.height), - color: color ?? Theme.of(context).canvasColor, - child: ArrowIndicatedButtonList(buttons: children), - ); + if (multiRowsDisplay ?? true) { + return Wrap( + alignment: WrapAlignment.center, + runSpacing: 4, + spacing: 4, + children: children, + ); + } else { + return Container( + constraints: BoxConstraints.tightFor(height: preferredSize.height), + color: color ?? Theme.of(context).canvasColor, + child: ArrowIndicatedButtonList(buttons: children), + ); + } } } From 2af193390b04360354311341848d582d7f741b5c Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Thu, 24 Jun 2021 12:31:01 -0700 Subject: [PATCH 238/306] Upgrade to 1.6.0 --- CHANGELOG.md | 3 +++ example/lib/pages/home_page.dart | 5 +---- lib/src/widgets/toolbar.dart | 11 +++++------ pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45cc1fc0..66f035b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.6.0] +* Support Multi Row Toolbar. + ## [1.5.0] * Remove file_picker dependency. diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 585e0ad2..0dc94999 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -141,10 +141,7 @@ class _HomePageState extends State { embedBuilder: defaultEmbedBuilderWeb); } var toolbar = QuillToolbar.basic( - controller: _controller!, - multiRowsDisplay: false, - onImagePickCallback: _onImagePickCallback, - ); + controller: _controller!, onImagePickCallback: _onImagePickCallback); if (kIsWeb) { toolbar = QuillToolbar.basic( controller: _controller!, diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index ddc31d42..d76ef913 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -307,12 +307,11 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { spacing: 4, children: children, ); - } else { - return Container( - constraints: BoxConstraints.tightFor(height: preferredSize.height), - color: color ?? Theme.of(context).canvasColor, - child: ArrowIndicatedButtonList(buttons: children), - ); } + return Container( + constraints: BoxConstraints.tightFor(height: preferredSize.height), + color: color ?? Theme.of(context).canvasColor, + child: ArrowIndicatedButtonList(buttons: children), + ); } } diff --git a/pubspec.yaml b/pubspec.yaml index a55996e0..081ef7a4 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.5.0 +version: 1.6.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From a74b4d539eb0b3124f44e18cefe1a1f1afd04574 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 2 Jul 2021 17:01:18 -0700 Subject: [PATCH 239/306] Upgrade image_picker and flutter_colorpicker --- CHANGELOG.md | 3 +++ pubspec.yaml | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66f035b5..2cb4482c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.6.1] +* Upgrade image_picker and flutter_colorpicker. + ## [1.6.0] * Support Multi Row Toolbar. diff --git a/pubspec.yaml b/pubspec.yaml index 081ef7a4..115d9ed3 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.6.0 +version: 1.6.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill @@ -13,9 +13,9 @@ dependencies: flutter: sdk: flutter collection: ^1.15.0 - flutter_colorpicker: ^0.4.0 + flutter_colorpicker: ^0.5.0 flutter_keyboard_visibility: ^5.0.0 - image_picker: ^0.7.3 + image_picker: ^0.8.1+3 photo_view: ^0.11.1 quiver: ^3.0.0 string_validator: ^0.3.0 From e7ea5fe632d13fcfc1badddab11049fa703d42e8 Mon Sep 17 00:00:00 2001 From: Cheryl Zhang Date: Fri, 2 Jul 2021 21:39:50 -0700 Subject: [PATCH 240/306] Downgrade flutter_colorpicker --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 115d9ed3..7b4b2e51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter collection: ^1.15.0 - flutter_colorpicker: ^0.5.0 + flutter_colorpicker: ^0.4.0 flutter_keyboard_visibility: ^5.0.0 image_picker: ^0.8.1+3 photo_view: ^0.11.1 From 5c7bede059a09234236497dbbe0784b93f573fa4 Mon Sep 17 00:00:00 2001 From: gtyhn <122523252@qq.com> Date: Tue, 6 Jul 2021 12:44:36 +0800 Subject: [PATCH 241/306] fix the OnImagePickCallback return value (#281) --- lib/src/widgets/toolbar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index d76ef913..710778ad 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -31,7 +31,7 @@ export 'toolbar/select_header_style_button.dart'; export 'toolbar/toggle_check_list_button.dart'; export 'toolbar/toggle_style_button.dart'; -typedef OnImagePickCallback = Future Function(File file); +typedef OnImagePickCallback = Future Function(File file); typedef ImagePickImpl = Future Function(ImageSource source); typedef FilePickImpl = Future Function(BuildContext context); typedef WebImagePickImpl = Future Function( From 503127dfa3f47b7d37ca2afdae5a62900c8db51b Mon Sep 17 00:00:00 2001 From: Benjamin Liii Date: Tue, 6 Jul 2021 12:50:11 +0800 Subject: [PATCH 242/306] add the parameter to hide the camera (#278) --- lib/src/widgets/toolbar.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 710778ad..af36d706 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -74,6 +74,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showHistory = true, bool showHorizontalRule = false, bool multiRowsDisplay = true, + bool showCamera = true, OnImagePickCallback? onImagePickCallback, FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl, @@ -172,7 +173,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { filePickImpl: filePickImpl, webImagePickImpl: webImagePickImpl, ), - if (onImagePickCallback != null) + if (onImagePickCallback != null && showCamera) ImageButton( icon: Icons.photo_camera, iconSize: toolbarIconSize, From 869503c2d3c5f0f15857d330e76f439080101617 Mon Sep 17 00:00:00 2001 From: gtyhn <122523252@qq.com> Date: Thu, 8 Jul 2021 00:44:56 +0800 Subject: [PATCH 243/306] solve placeholder issue, removing must focus (#286) * fix the OnImagePickCallback return value * solve placeholder issue, removing must focus --- lib/src/widgets/raw_editor.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 7cfb9103..002c62b1 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -141,7 +141,6 @@ class RawEditorState extends EditorState var _doc = widget.controller.document; if (_doc.isEmpty() && - !widget.focusNode.hasFocus && widget.placeholder != null) { _doc = Document.fromJson(jsonDecode( '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); From 6b9795ad005502fa99bbd79d5032df74dec5d3c6 Mon Sep 17 00:00:00 2001 From: gtyhn <122523252@qq.com> Date: Sat, 10 Jul 2021 01:23:16 +0800 Subject: [PATCH 244/306] Fixed the position of the selection status drag handle (#288) * fix the OnImagePickCallback return value * solve placeholder issue, removing must focus * Fixed the position of the selection status drag handle --- lib/src/widgets/text_selection.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 85362c7a..96e5310f 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -287,6 +287,9 @@ class _TextSelectionHandleOverlay extends StatefulWidget { class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay> with SingleTickerProviderStateMixin { + + late Offset _dragPosition; + late Size _handleSize; late AnimationController _controller; Animation get _opacity => _controller.view; @@ -325,11 +328,14 @@ class _TextSelectionHandleOverlayState super.dispose(); } - void _handleDragStart(DragStartDetails details) {} + void _handleDragStart(DragStartDetails details) { + _dragPosition = details.globalPosition + Offset(0.0, -_handleSize.height); + } void _handleDragUpdate(DragUpdateDetails details) { + _dragPosition += details.delta; final position = - widget.renderObject!.getPositionForOffset(details.globalPosition); + widget.renderObject!.getPositionForOffset(_dragPosition); if (widget.selection.isCollapsed) { widget.onSelectionHandleChanged(TextSelection.fromPosition(position)); return; @@ -398,6 +404,7 @@ class _TextSelectionHandleOverlayState final handleAnchor = widget.selectionControls.getHandleAnchor(type!, lineHeight); final handleSize = widget.selectionControls.getHandleSize(lineHeight); + _handleSize = handleSize; final handleRect = Rect.fromLTWH( -handleAnchor.dx, From 0c5a045d287fe0f24bef190ebe202c2a5de21e94 Mon Sep 17 00:00:00 2001 From: Cheryl Zhang Date: Fri, 9 Jul 2021 11:36:32 -0700 Subject: [PATCH 245/306] Format code --- lib/src/widgets/raw_editor.dart | 3 +-- lib/src/widgets/text_selection.dart | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 002c62b1..0629b102 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -140,8 +140,7 @@ class RawEditorState extends EditorState super.build(context); var _doc = widget.controller.document; - if (_doc.isEmpty() && - widget.placeholder != null) { + if (_doc.isEmpty() && widget.placeholder != null) { _doc = Document.fromJson(jsonDecode( '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); } diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 96e5310f..8e0b3b64 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -287,7 +287,6 @@ class _TextSelectionHandleOverlay extends StatefulWidget { class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay> with SingleTickerProviderStateMixin { - late Offset _dragPosition; late Size _handleSize; late AnimationController _controller; @@ -329,13 +328,12 @@ class _TextSelectionHandleOverlayState } void _handleDragStart(DragStartDetails details) { - _dragPosition = details.globalPosition + Offset(0.0, -_handleSize.height); + _dragPosition = details.globalPosition + Offset(0, -_handleSize.height); } void _handleDragUpdate(DragUpdateDetails details) { _dragPosition += details.delta; - final position = - widget.renderObject!.getPositionForOffset(_dragPosition); + final position = widget.renderObject!.getPositionForOffset(_dragPosition); if (widget.selection.isCollapsed) { widget.onSelectionHandleChanged(TextSelection.fromPosition(position)); return; From 3f6eca0ce90526860a7b619953fbec1b8d046ed1 Mon Sep 17 00:00:00 2001 From: Cheryl Zhang Date: Fri, 9 Jul 2021 11:40:08 -0700 Subject: [PATCH 246/306] Upgrade to 1.6.2 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb4482c..9084a319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.6.2] +* Fixed the position of the selection status drag handle. + ## [1.6.1] * Upgrade image_picker and flutter_colorpicker. diff --git a/pubspec.yaml b/pubspec.yaml index 7b4b2e51..7e4bcd12 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.6.1 +version: 1.6.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 240688f06e3e55062cbd37ffa556bc79bf5581a3 Mon Sep 17 00:00:00 2001 From: gtyhn <122523252@qq.com> Date: Sat, 10 Jul 2021 14:09:37 +0800 Subject: [PATCH 247/306] Fixed dragging right handle scrolling issue (#289) --- lib/src/widgets/editor.dart | 14 ++++++++- lib/src/widgets/text_selection.dart | 48 ++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 89796281..f5ce06e1 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -903,7 +903,19 @@ class RenderEditor extends RenderEditableContainerBox double? getOffsetToRevealCursor( double viewportHeight, double scrollOffset, double offsetInViewport) { final endpoints = getEndpointsForSelection(selection); - final endpoint = endpoints.first; + + // when we drag the right handle, we should get the last point + TextSelectionPoint endpoint; + if (selection.isCollapsed) { + endpoint = endpoints.first; + } else { + if (selection is DragTextSelection) { + endpoint = (selection as DragTextSelection).first ? endpoints.first : endpoints.last; + } else { + endpoint = endpoints.first; + } + } + final child = childAtPosition(selection.extent); const kMargin = 8.0; diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 8e0b3b64..583f7794 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -23,6 +23,41 @@ TextSelection localSelection(Node node, TextSelection selection, fromParent) { enum _TextSelectionHandlePosition { START, END } +/// internal use, used to get drag direction information +class DragTextSelection extends TextSelection { + final bool first; + + const DragTextSelection({ + int baseOffset = 0, + int extentOffset = 0, + TextAffinity affinity = TextAffinity.downstream, + bool isDirectional = false, + this.first = true, + }) : super( + baseOffset: baseOffset, + extentOffset: extentOffset, + affinity: affinity, + isDirectional: isDirectional, + ); + + @override + DragTextSelection copyWith({ + int? baseOffset, + int? extentOffset, + TextAffinity? affinity, + bool? isDirectional, + bool? first, + }) { + return DragTextSelection( + baseOffset: baseOffset ?? this.baseOffset, + extentOffset: extentOffset ?? this.extentOffset, + affinity: affinity ?? this.affinity, + isDirectional: isDirectional ?? this.isDirectional, + first: first ?? this.first, + ); + } +} + class EditorTextSelectionOverlay { EditorTextSelectionOverlay( this.value, @@ -156,9 +191,20 @@ class EditorTextSelectionOverlay { default: throw 'Invalid position'; } + + final currSelection = newSelection != null + ? DragTextSelection( + baseOffset: newSelection.baseOffset, + extentOffset: newSelection.extentOffset, + affinity: newSelection.affinity, + isDirectional: newSelection.isDirectional, + first: position == _TextSelectionHandlePosition.START, + ) + : null; + selectionDelegate ..userUpdateTextEditingValue( - value.copyWith(selection: newSelection, composing: TextRange.empty), + value.copyWith(selection: currSelection, composing: TextRange.empty), SelectionChangedCause.drag) ..bringIntoView(textPosition); } From 0aa1a9cee03dd29a8529613a279c3b485e8bf03b Mon Sep 17 00:00:00 2001 From: Cheryl Zhang Date: Fri, 9 Jul 2021 23:26:15 -0700 Subject: [PATCH 248/306] Format code --- lib/src/widgets/editor.dart | 8 +++++--- lib/src/widgets/text_selection.dart | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index f5ce06e1..59a031a0 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -903,19 +903,21 @@ class RenderEditor extends RenderEditableContainerBox double? getOffsetToRevealCursor( double viewportHeight, double scrollOffset, double offsetInViewport) { final endpoints = getEndpointsForSelection(selection); - + // when we drag the right handle, we should get the last point TextSelectionPoint endpoint; if (selection.isCollapsed) { endpoint = endpoints.first; } else { if (selection is DragTextSelection) { - endpoint = (selection as DragTextSelection).first ? endpoints.first : endpoints.last; + endpoint = (selection as DragTextSelection).first + ? endpoints.first + : endpoints.last; } else { endpoint = endpoints.first; } } - + final child = childAtPosition(selection.extent); const kMargin = 8.0; diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index 583f7794..f11a42c4 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -25,12 +25,10 @@ enum _TextSelectionHandlePosition { START, END } /// internal use, used to get drag direction information class DragTextSelection extends TextSelection { - final bool first; - const DragTextSelection({ + TextAffinity affinity = TextAffinity.downstream, int baseOffset = 0, int extentOffset = 0, - TextAffinity affinity = TextAffinity.downstream, bool isDirectional = false, this.first = true, }) : super( @@ -40,6 +38,8 @@ class DragTextSelection extends TextSelection { isDirectional: isDirectional, ); + final bool first; + @override DragTextSelection copyWith({ int? baseOffset, @@ -191,7 +191,7 @@ class EditorTextSelectionOverlay { default: throw 'Invalid position'; } - + final currSelection = newSelection != null ? DragTextSelection( baseOffset: newSelection.baseOffset, @@ -201,7 +201,7 @@ class EditorTextSelectionOverlay { first: position == _TextSelectionHandlePosition.START, ) : null; - + selectionDelegate ..userUpdateTextEditingValue( value.copyWith(selection: currSelection, composing: TextRange.empty), From a7349c66375810c278845c627c2178056c0a4a65 Mon Sep 17 00:00:00 2001 From: Cheryl Zhang Date: Fri, 9 Jul 2021 23:28:56 -0700 Subject: [PATCH 249/306] Upgrade to 1.6.3 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9084a319..3387ecca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.6.3] +* Fixed dragging right handle scrolling issue. + ## [1.6.2] * Fixed the position of the selection status drag handle. diff --git a/pubspec.yaml b/pubspec.yaml index 7e4bcd12..30020185 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.6.2 +version: 1.6.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 6ce51a66001473317ec105cdf05ce1b5cda71745 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Sun, 11 Jul 2021 21:47:24 -0700 Subject: [PATCH 250/306] Remove extra parenthesis --- lib/src/models/documents/nodes/container.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/models/documents/nodes/container.dart b/lib/src/models/documents/nodes/container.dart index 13f6aa2f..40d3ba9e 100644 --- a/lib/src/models/documents/nodes/container.dart +++ b/lib/src/models/documents/nodes/container.dart @@ -94,7 +94,7 @@ abstract class Container extends Node { for (final node in children) { final len = node.length; - if (offset < len || (inclusive && offset == len && (node.isLast))) { + if (offset < len || (inclusive && offset == len && node.isLast)) { return ChildQuery(node, offset); } offset -= len; From 6751472a55b2cc7d1aade08e253259b44e1ffbe2 Mon Sep 17 00:00:00 2001 From: li3317 Date: Tue, 13 Jul 2021 23:19:43 +0800 Subject: [PATCH 251/306] fix clear format button --- lib/src/models/documents/document.dart | 8 +++-- lib/src/models/documents/nodes/line.dart | 33 +++++++++++++++++++ lib/src/widgets/controller.dart | 8 ++++- .../widgets/toolbar/clear_format_button.dart | 2 +- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index 346da93b..010df462 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -119,9 +119,13 @@ class Document { return delta; } - Style collectStyle(int index, int len) { + Style collectStyle(int index, int len, bool collectAll) { final res = queryChild(index); - return (res.node as Line).collectStyle(res.offset, len); + if (collectAll) { + return (res.node as Line).collectAllStyle(res.offset, len); + } else { + return (res.node as Line).collectStyle(res.offset, len); + } } ChildQuery queryChild(int offset) { diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index fabfad4d..a67a17da 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -368,4 +368,37 @@ class Line extends Container { return result; } + + /// Returns all style for any character within the specified text range. + Style collectAllStyle(int offset, int len) { + final local = math.min(length - offset, len); + var result = Style(); + + final data = queryChild(offset, true); + var node = data.node as Leaf?; + if (node != null) { + result = result.mergeAll(node.style); + var pos = node.length - data.offset; + while (!node!.isLast && pos < local) { + node = node.next as Leaf?; + result = result.mergeAll(node!.style); + pos += node.length; + } + } + + result = result.mergeAll(style); + if (parent is Block) { + final block = parent as Block; + result = result.mergeAll(block.style); + } + + final remaining = len - local; + if (remaining > 0) { + final rest = nextLine!.collectAllStyle(0, remaining); + result = result.mergeAll(rest); + } + + return result; + } + } diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 7de1a122..9e2e01b0 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -57,7 +57,13 @@ class QuillController extends ChangeNotifier { Style getSelectionStyle() { return document - .collectStyle(selection.start, selection.end - selection.start) + .collectStyle(selection.start, selection.end - selection.start, false) + .mergeAll(toggledStyle); + } + + Style getAllSelectionStyle() { + return document + .collectStyle(selection.start, selection.end - selection.start, true) .mergeAll(toggledStyle); } diff --git a/lib/src/widgets/toolbar/clear_format_button.dart b/lib/src/widgets/toolbar/clear_format_button.dart index d55c21df..ee049b41 100644 --- a/lib/src/widgets/toolbar/clear_format_button.dart +++ b/lib/src/widgets/toolbar/clear_format_button.dart @@ -34,7 +34,7 @@ class _ClearFormatButtonState extends State { fillColor: fillColor, onPressed: () { for (final k - in widget.controller.getSelectionStyle().attributes.values) { + in widget.controller.getAllSelectionStyle().attributes.values) { widget.controller.formatSelection(Attribute.clone(k, null)); } }); From bbabf4e25dbc9671668984ad1a9726f209e489ea Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 13 Jul 2021 08:54:06 -0700 Subject: [PATCH 252/306] Refactor code for collectAllStyles --- lib/src/models/documents/document.dart | 16 ++++++++++------ lib/src/models/documents/nodes/line.dart | 5 ++--- lib/src/widgets/controller.dart | 9 ++++++--- lib/src/widgets/toolbar/clear_format_button.dart | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index 010df462..71cdf487 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -119,13 +119,17 @@ class Document { return delta; } - Style collectStyle(int index, int len, bool collectAll) { + /// Only attributes applied to all characters within this range are + /// included in the result. + Style collectStyle(int index, int len) { final res = queryChild(index); - if (collectAll) { - return (res.node as Line).collectAllStyle(res.offset, len); - } else { - return (res.node as Line).collectStyle(res.offset, len); - } + return (res.node as Line).collectStyle(res.offset, len); + } + + /// Returns all style for any character within the specified text range. + Style collectAllStyles(int index, int len) { + final res = queryChild(index); + return (res.node as Line).collectAllStyles(res.offset, len); } ChildQuery queryChild(int offset) { diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index a67a17da..0abbf920 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -370,7 +370,7 @@ class Line extends Container { } /// Returns all style for any character within the specified text range. - Style collectAllStyle(int offset, int len) { + Style collectAllStyles(int offset, int len) { final local = math.min(length - offset, len); var result = Style(); @@ -394,11 +394,10 @@ class Line extends Container { final remaining = len - local; if (remaining > 0) { - final rest = nextLine!.collectAllStyle(0, remaining); + final rest = nextLine!.collectAllStyles(0, remaining); result = result.mergeAll(rest); } return result; } - } diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 9e2e01b0..bed27b90 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -55,15 +55,18 @@ class QuillController extends ChangeNotifier { selection: selection, ); + /// Only attributes applied to all characters within this range are + /// included in the result. Style getSelectionStyle() { return document - .collectStyle(selection.start, selection.end - selection.start, false) + .collectStyle(selection.start, selection.end - selection.start) .mergeAll(toggledStyle); } - Style getAllSelectionStyle() { + /// Returns all style for any character within the specified text range. + Style getAllSelectionStyles() { return document - .collectStyle(selection.start, selection.end - selection.start, true) + .collectAllStyles(selection.start, selection.end - selection.start) .mergeAll(toggledStyle); } diff --git a/lib/src/widgets/toolbar/clear_format_button.dart b/lib/src/widgets/toolbar/clear_format_button.dart index ee049b41..597a211e 100644 --- a/lib/src/widgets/toolbar/clear_format_button.dart +++ b/lib/src/widgets/toolbar/clear_format_button.dart @@ -34,7 +34,7 @@ class _ClearFormatButtonState extends State { fillColor: fillColor, onPressed: () { for (final k - in widget.controller.getAllSelectionStyle().attributes.values) { + in widget.controller.getAllSelectionStyles().attributes.values) { widget.controller.formatSelection(Attribute.clone(k, null)); } }); From dad5c676cb9e8c300b5c370bac83aeff591d8b8f Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Tue, 13 Jul 2021 09:12:47 -0700 Subject: [PATCH 253/306] collectAllStyles should return List