From a82692fe58a8ecc123070b8b6441c5257a01949a Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 4 Feb 2022 10:53:17 -0800 Subject: [PATCH 01/39] Fix: Clicking on the Link icon without any text on a new line will crash --- lib/src/widgets/toolbar/link_style_button.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index 0ccdfc77..e479481c 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -114,8 +114,9 @@ class _LinkStyleButtonState extends State { } } - text ??= widget.controller.document - .getPlainText(index, widget.controller.selection.end - index); + final len = widget.controller.selection.end - index; + text ??= + len == 0 ? '' : widget.controller.document.getPlainText(index, len); return _LinkDialog( dialogTheme: widget.dialogTheme, link: link, text: text); }, From cb458fa754abad71bce7ab8f5a5dc7de09132ad9 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 4 Feb 2022 10:56:56 -0800 Subject: [PATCH 02/39] Upgrade to 3.9.7 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e578affd..de89dec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [3.9.7] +* Fix for clicking on the Link button without any text on a new line crashes. + # [3.9.6] * Apply locale to QuillEditor(contents). diff --git a/pubspec.yaml b/pubspec.yaml index e70299e6..dec7e3f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 3.9.6 +version: 3.9.7 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 17eb892ab1109a06db6314b160d1b54011e57179 Mon Sep 17 00:00:00 2001 From: Usman Shabir <74250206+usman-97@users.noreply.github.com> Date: Sat, 5 Feb 2022 01:21:35 +0000 Subject: [PATCH 03/39] Update toolbar.i18n.dart (#650) --- lib/src/translations/toolbar.i18n.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 577f901b..a328cf20 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -308,6 +308,26 @@ extension Localization on String { 'Width': 'Width', 'Height': 'Height', }, + 'ur': { + 'Paste a link': 'لنک پیسٹ کریں', + 'Ok': 'ٹھیک ہے', + 'Select Color': 'رنگ منتخب کریں', + 'Gallery': 'گیلری', + 'Link': 'لنک', + 'Please first select some text to transform into a link.': + 'براہ کرم لنک میں تبدیل کرنے کے لیے پہلے کچھ متن منتخب کریں۔', + 'Open': 'کھولیں', + 'Copy': 'نقل', + 'Remove': 'ہٹا دیں', + 'Save': 'محفوظ کریں', + 'Zoom': 'زوم', + 'Saved': 'محفوظ کر لیا', + 'Text': 'متن', + 'What is entered is not a link': 'جو درج کیا گیا ہے وہ لنک نہیں ہے۔', + 'Resize': 'سائز تبدیل کریں۔', + 'Width': 'چوڑائی', + 'Height': 'اونچائی', + }, }; String get i18n => localize(this, _t); From 466ff255822eeae8b1e4efd5da69b3606b48f902 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 4 Feb 2022 18:39:21 -0800 Subject: [PATCH 04/39] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5c1520cf..348cb6a8 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ Currently, translations are available for these locales: * `Locale('es')` * `Locale('tr')` * `Locale('uk')` +* `Locale('ur')` * `Locale('pt')` * `Locale('pl')` * `Locale('vi')` From bd80406bf987ca38bc9b7666c1cb8c43ca9c06d2 Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 4 Feb 2022 18:42:27 -0800 Subject: [PATCH 05/39] Upgrade to 3.9.8 --- CHANGELOG.md | 3 +++ lib/src/translations/toolbar.i18n.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de89dec6..2d5ce232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [3.9.8] +* Added Urdu translation. + # [3.9.7] * Fix for clicking on the Link button without any text on a new line crashes. diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index a328cf20..06f76ff5 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -308,7 +308,7 @@ extension Localization on String { 'Width': 'Width', 'Height': 'Height', }, - 'ur': { + 'ur': { 'Paste a link': 'لنک پیسٹ کریں', 'Ok': 'ٹھیک ہے', 'Select Color': 'رنگ منتخب کریں', diff --git a/pubspec.yaml b/pubspec.yaml index dec7e3f1..5526ddd0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 3.9.7 +version: 3.9.8 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 076eaf75b5c7b4c96d5abef49640e7c4fed45358 Mon Sep 17 00:00:00 2001 From: Develeste <93141030+Develeste@users.noreply.github.com> Date: Sun, 6 Feb 2022 20:17:34 +0900 Subject: [PATCH 06/39] [For ios] Saving image that's filename does not end with it's file extension (#651) --- .../widgets/embeds/default_embed_builder.dart | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/src/widgets/embeds/default_embed_builder.dart b/lib/src/widgets/embeds/default_embed_builder.dart index dab275c5..26ffd7be 100644 --- a/lib/src/widgets/embeds/default_embed_builder.dart +++ b/lib/src/widgets/embeds/default_embed_builder.dart @@ -162,6 +162,26 @@ Widget _menuOptionsForReadonlyImage( color: Colors.greenAccent, text: 'Save'.i18n, onPressed: () { + const List imageFormats = [ + '.jpeg', + '.png', + '.jpg', + '.gif', + '.webp', + '.tif', + '.heic' + ]; + + List endsWithFormats = imageFormats.where((imageFormat) => imageUrl.toLowerCase().endsWith(imageFormat)).toList(); + if (endsWithFormats.length == 0) { + for(int i = 0; i < imageFormats.length; i++) { + if (imageUrl.toLowerCase().contains(imageFormats[i])) { + imageUrl += imageFormats[i]; + break; + } + } + } + GallerySaver.saveImage(imageUrl).then((_) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Saved'.i18n))); From ac933e07fb41ed72572476c152ed0566aadbe292 Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 6 Feb 2022 09:18:59 -0800 Subject: [PATCH 07/39] Upgrade to 3.9.9 --- CHANGELOG.md | 3 ++ .../widgets/embeds/default_embed_builder.dart | 21 +------------- lib/src/widgets/embeds/image.dart | 29 +++++++++++++++++++ pubspec.yaml | 2 +- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d5ce232..8602f221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [3.9.9] +* iOS: Save image whose filename does not end with image file extension. + # [3.9.8] * Added Urdu translation. diff --git a/lib/src/widgets/embeds/default_embed_builder.dart b/lib/src/widgets/embeds/default_embed_builder.dart index 26ffd7be..eb46585e 100644 --- a/lib/src/widgets/embeds/default_embed_builder.dart +++ b/lib/src/widgets/embeds/default_embed_builder.dart @@ -162,26 +162,7 @@ Widget _menuOptionsForReadonlyImage( color: Colors.greenAccent, text: 'Save'.i18n, onPressed: () { - const List imageFormats = [ - '.jpeg', - '.png', - '.jpg', - '.gif', - '.webp', - '.tif', - '.heic' - ]; - - List endsWithFormats = imageFormats.where((imageFormat) => imageUrl.toLowerCase().endsWith(imageFormat)).toList(); - if (endsWithFormats.length == 0) { - for(int i = 0; i < imageFormats.length; i++) { - if (imageUrl.toLowerCase().contains(imageFormats[i])) { - imageUrl += imageFormats[i]; - break; - } - } - } - + imageUrl = appendFileExtensionToImageUrl(imageUrl); GallerySaver.saveImage(imageUrl).then((_) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Saved'.i18n))); diff --git a/lib/src/widgets/embeds/image.dart b/lib/src/widgets/embeds/image.dart index c7e9f9e3..6bbedd18 100644 --- a/lib/src/widgets/embeds/image.dart +++ b/lib/src/widgets/embeds/image.dart @@ -12,6 +12,16 @@ import '../../models/documents/nodes/leaf.dart'; import '../../models/documents/style.dart'; import '../controller.dart'; +const List imageFileExtensions = [ + '.jpeg', + '.png', + '.jpg', + '.gif', + '.webp', + '.tif', + '.heic' +]; + bool isImageBase64(String imageUrl) { return !imageUrl.startsWith('http') && isBase64(imageUrl); } @@ -64,6 +74,25 @@ String standardizeImageUrl(String url) { return url; } +/// This is a bug of Gallery Saver Package. +/// It can not save image that's filename does not end with it's file extension +/// like below. +// "https://firebasestorage.googleapis.com/v0/b/eventat-4ba96.appspot.com/o/2019-Metrology-Events.jpg?alt=media&token=bfc47032-5173-4b3f-86bb-9659f46b362a" +/// If imageUrl does not end with it's file extension, +/// file extension is added to image url for saving. +String appendFileExtensionToImageUrl(String url) { + final endsWithImageFileExtension = imageFileExtensions + .firstWhere((s) => url.toLowerCase().endsWith(s), orElse: () => ''); + if (endsWithImageFileExtension.isNotEmpty) { + return url; + } + + final imageFileExtension = imageFileExtensions + .firstWhere((s) => url.toLowerCase().contains(s), orElse: () => ''); + + return url + imageFileExtension; +} + class ImageTapWrapper extends StatelessWidget { const ImageTapWrapper({ required this.imageUrl, diff --git a/pubspec.yaml b/pubspec.yaml index 5526ddd0..6823b7f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 3.9.8 +version: 3.9.9 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From a1eea4ded6330043c4275b5f40d8013cae1c54bc Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Mon, 7 Feb 2022 18:13:03 +0200 Subject: [PATCH 08/39] ios simulator cursor fix (#652) --- lib/src/utils/platform.dart | 12 ++++++++++++ lib/src/widgets/raw_editor.dart | 21 ++++++++++++++------- pubspec.yaml | 1 + 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/src/utils/platform.dart b/lib/src/utils/platform.dart index 1ee63392..b2ab2882 100644 --- a/lib/src/utils/platform.dart +++ b/lib/src/utils/platform.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; bool isMobile([TargetPlatform? targetPlatform]) { @@ -23,3 +26,12 @@ bool isAppleOS([TargetPlatform? targetPlatform]) { TargetPlatform.iOS, }.contains(targetPlatform); } + +Future isIosSimulator() async { + if (Platform.isIOS) { + final deviceInfo = DeviceInfoPlugin(); + final iosInfo = await deviceInfo.iosInfo; + return !iosInfo.isPhysicalDevice; + } + return false; +} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 473eea10..d3ff570c 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -557,13 +557,20 @@ class RawEditorState extends EditorState if (isKeyboardOS()) { _keyboardVisible = true; } else { - _keyboardVisibilityController = KeyboardVisibilityController(); - _keyboardVisible = _keyboardVisibilityController!.isVisible; - _keyboardVisibilitySubscription = - _keyboardVisibilityController?.onChange.listen((visible) { - _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(!_hasFocus); + // treat iOS Simulator like a keyboard OS + isIosSimulator().then((isIosSimulator) { + if (isIosSimulator) { + _keyboardVisible = true; + } else { + _keyboardVisibilityController = KeyboardVisibilityController(); + _keyboardVisible = _keyboardVisibilityController!.isVisible; + _keyboardVisibilitySubscription = + _keyboardVisibilityController?.onChange.listen((visible) { + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(!_hasFocus); + } + }); } }); } diff --git a/pubspec.yaml b/pubspec.yaml index 6823b7f7..1d22d9b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: diff_match_patch: ^0.4.1 i18n_extension: ^4.2.0 gallery_saver: ^2.3.2 + device_info_plus: ^3.2.1 dev_dependencies: flutter_test: From 2e4ff7b4af4b812dfe2084d66c4fd82b30909977 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 7 Feb 2022 09:07:04 -0800 Subject: [PATCH 09/39] Format code --- lib/src/widgets/raw_editor.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index d3ff570c..47dd370e 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -566,11 +566,11 @@ class RawEditorState extends EditorState _keyboardVisible = _keyboardVisibilityController!.isVisible; _keyboardVisibilitySubscription = _keyboardVisibilityController?.onChange.listen((visible) { - _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(!_hasFocus); - } - }); + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(!_hasFocus); + } + }); } }); } From 1e0b1259a183069b87570ab94ebd191c16e1e427 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 7 Feb 2022 09:21:51 -0800 Subject: [PATCH 10/39] Rename isIOSSimulator --- lib/src/utils/platform.dart | 2 +- lib/src/widgets/raw_editor.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/utils/platform.dart b/lib/src/utils/platform.dart index b2ab2882..a5c39aa1 100644 --- a/lib/src/utils/platform.dart +++ b/lib/src/utils/platform.dart @@ -27,7 +27,7 @@ bool isAppleOS([TargetPlatform? targetPlatform]) { }.contains(targetPlatform); } -Future isIosSimulator() async { +Future isIOSSimulator() async { if (Platform.isIOS) { final deviceInfo = DeviceInfoPlugin(); final iosInfo = await deviceInfo.iosInfo; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 47dd370e..4edb77e8 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -558,7 +558,7 @@ class RawEditorState extends EditorState _keyboardVisible = true; } else { // treat iOS Simulator like a keyboard OS - isIosSimulator().then((isIosSimulator) { + isIOSSimulator().then((isIosSimulator) { if (isIosSimulator) { _keyboardVisible = true; } else { From 350d7bcfb038c90079a3002e0630a0554e1322f9 Mon Sep 17 00:00:00 2001 From: Nicolas Dion-Bouchard Date: Mon, 7 Feb 2022 16:16:59 -0500 Subject: [PATCH 11/39] Fix for undoing a modification ending with an indented line (#656) Co-authored-by: Nicolas Dion Bouchard --- lib/src/models/rules/insert.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index f1db6318..70c1e63e 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -115,7 +115,11 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { 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); + final blockAttributes = blockStyle.isEmpty + ? null + : blockStyle.map((_, attribute) => + MapEntry(attribute.key, attribute.value)); + delta.insert('\n', blockAttributes); } } From fbcbddd7f899b8b0ab6c6118f003050fb9412a1c Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 7 Feb 2022 13:20:14 -0800 Subject: [PATCH 12/39] Upgrade to 3.9.10 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8602f221..2e86f367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [3.9.10] +* Fix for undoing a modification ending with an indented line. + # [3.9.9] * iOS: Save image whose filename does not end with image file extension. diff --git a/pubspec.yaml b/pubspec.yaml index 1d22d9b9..e4043e84 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 3.9.9 +version: 3.9.10 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From ef964f75f03de433e4e13ee7446ba7e0e15139aa Mon Sep 17 00:00:00 2001 From: Farhan Fadila <43161050+farhanfadila1717@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:09:44 +0700 Subject: [PATCH 13/39] Added Indonesian translation (#660) * Add id translations * link id translation * link id translation --- lib/src/translations/toolbar.i18n.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 06f76ff5..17c79084 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -328,6 +328,26 @@ extension Localization on String { 'Width': 'چوڑائی', 'Height': 'اونچائی', }, + 'id': { + 'Paste a link': 'Tempel tautan', + 'Ok': 'Oke', + 'Select Color': 'Pilih Warna', + 'Gallery': 'Galeri', + 'Link': 'Tautan', + 'Please first select some text to transform into a link.': + 'Silakan pilih dulu beberapa teks untuk diubah menjadi tautan.', + 'Open': 'Buka', + 'Copy': 'Salin', + 'Remove': 'Hapus', + 'Save': 'Simpan', + 'Zoom': 'Perbesar', + 'Saved': 'Tersimpan', + 'Text': 'Teks', + 'What is entered is not a link': 'Yang dimasukkan bukan tautan', + 'Resize': 'Ubah Ukuran', + 'Width': 'Lebar', + 'Height': 'Tinggi', + }, }; String get i18n => localize(this, _t); From 3715229fdd6262accbf24b3008ff18b3992bab35 Mon Sep 17 00:00:00 2001 From: X Code Date: Tue, 8 Feb 2022 20:10:18 -0800 Subject: [PATCH 14/39] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 348cb6a8..ed07fa3a 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ Currently, translations are available for these locales: * `Locale('pt')` * `Locale('pl')` * `Locale('vi')` +* `Locale('id')` ### Contributing to translations The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! From 95779f4dae566cbf480aa997d1484db1f4a8a661 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 9 Feb 2022 11:15:55 -0800 Subject: [PATCH 15/39] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed07fa3a..4a6569a0 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ The package offers translations for the quill toolbar, it will follow the system QuillToolbar(locale: Locale('fr'), ...) QuillEditor(locale: Locale('fr'), ...) ``` -Currently, translations are available for these locales: +Currently, translations are available for these 16 locales: * `Locale('en')` * `Locale('ar')` * `Locale('de')` From cfc5694e93bd17a26abd45b063b90c3bdd8c1c25 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 9 Feb 2022 11:17:00 -0800 Subject: [PATCH 16/39] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a6569a0..914b2231 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,8 @@ Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follo } ``` -## Translation of toolbar -The package offers translations for the quill toolbar, it will follow the system locale unless you set your own locale with: +## Translation +The package offers translations for the quill toolbar and editor, it will follow the system locale unless you set your own locale with: ``` QuillToolbar(locale: Locale('fr'), ...) QuillEditor(locale: Locale('fr'), ...) From 0edb2368d7baa6599113a9383cc96bf96fff1b2c Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 9 Feb 2022 11:19:20 -0800 Subject: [PATCH 17/39] Adjust comments --- lib/src/widgets/editor.dart | 2 +- lib/src/widgets/toolbar.dart | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 3e418287..78a6662b 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -342,7 +342,7 @@ class QuillEditor extends StatefulWidget { final CustomStyleBuilder? customStyleBuilder; /// The locale to use for the editor toolbar, defaults to system locale - /// and more https://github.com/singerdmx/flutter-quill#translation-of-toolbar + /// More https://github.com/singerdmx/flutter-quill#translation final Locale? locale; /// Delegate function responsible for showing menu with link actions on diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index 454fa415..ad350c00 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -114,13 +114,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ///shown when embedding an image, for example QuillDialogTheme? dialogTheme, - ///The locale to use for the editor toolbar, defaults to system locale - ///Currently the supported locales are: - /// * Locale('en') - /// * Locale('de') - /// * Locale('fr') - /// * Locale('zh') - /// and more https://github.com/singerdmx/flutter-quill#translation-of-toolbar + /// The locale to use for the editor toolbar, defaults to system locale + /// More at https://github.com/singerdmx/flutter-quill#translation Locale? locale, Key? key, }) { @@ -425,7 +420,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { final FilePickImpl? filePickImpl; /// The locale to use for the editor toolbar, defaults to system locale - /// and more https://github.com/singerdmx/flutter-quill#translation-of-toolbar + /// More https://github.com/singerdmx/flutter-quill#translation final Locale? locale; @override From 84d41faf2174803e8a58963bb71078e08eb18d34 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 9 Feb 2022 21:05:38 -0800 Subject: [PATCH 18/39] Upgrade to 3.9.11 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e86f367..7299d8bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [3.9.11] +* Added Indonesian translation. + # [3.9.10] * Fix for undoing a modification ending with an indented line. diff --git a/pubspec.yaml b/pubspec.yaml index e4043e84..97dfcd86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 3.9.10 +version: 3.9.11 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From a56e640e1551334154cdc02b8b3981e7e11326ab Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 9 Feb 2022 21:06:32 -0800 Subject: [PATCH 19/39] Upgrade to flutter 2.10 (#646) * Upgrade to flutter 2.10 * Remove debugAssertLayoutUpToDate * Update copy/paste * Fix analysis error --- lib/src/widgets/editor.dart | 6 -- lib/src/widgets/raw_editor.dart | 140 ++++++++++++++++++++++---------- 2 files changed, 98 insertions(+), 48 deletions(-) diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 78a6662b..619bc2bb 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -1549,12 +1549,6 @@ class RenderEditor extends RenderEditableContainerBox super.systemFontsDidChange(); markNeedsLayout(); } - - void debugAssertLayoutUpToDate() { - // no-op? - // this assert was added by Flutter TextEditingActionTarge - // so we have to comply here. - } } class EditableContainerParentData diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 4edb77e8..ad0205c6 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -233,7 +233,6 @@ class RawEditorState extends EditorState AutomaticKeepAliveClientMixin, WidgetsBindingObserver, TickerProviderStateMixin, - TextEditingActionTarget, RawEditorStateTextInputClientMixin, RawEditorStateSelectionDelegateMixin { final GlobalKey _editorKey = GlobalKey(); @@ -256,7 +255,6 @@ class RawEditorState extends EditorState // Focus bool _didAutoFocus = false; - FocusAttachment? _focusAttachment; bool get _hasFocus => widget.focusNode.hasFocus; // Theme @@ -280,7 +278,6 @@ class RawEditorState extends EditorState @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - _focusAttachment!.reparent(); super.build(context); var _doc = widget.controller.document; @@ -575,7 +572,7 @@ class RawEditorState extends EditorState }); } - _focusAttachment = widget.focusNode.attach(context); + // Focus widget.focusNode.addListener(_handleFocusChanged); } @@ -619,8 +616,6 @@ class RawEditorState extends EditorState if (widget.focusNode != oldWidget.focusNode) { oldWidget.focusNode.removeListener(_handleFocusChanged); - _focusAttachment?.detach(); - _focusAttachment = widget.focusNode.attach(context); widget.focusNode.addListener(_handleFocusChanged); updateKeepAlive(); } @@ -658,7 +653,6 @@ class RawEditorState extends EditorState _selectionOverlay = null; widget.controller.removeListener(_didChangeTextEditingValue); widget.focusNode.removeListener(_handleFocusChanged); - _focusAttachment!.detach(); _cursorCont.dispose(); _clipboardStatus ..removeListener(_onChangedClipboardStatus) @@ -835,8 +829,15 @@ class RawEditorState extends EditorState /// This property is typically used to notify the renderer of input gestures. @override RenderEditor get renderEditor => - _editorKey.currentContext?.findRenderObject() as RenderEditor; + _editorKey.currentContext!.findRenderObject() as RenderEditor; + /// Express interest in interacting with the keyboard. + /// + /// If this control is already attached to the keyboard, this function will + /// request that the keyboard become visible. Otherwise, this function will + /// ask the focus system that it become focused. If successful in acquiring + /// focus, the control will then attach to the keyboard and request that the + /// keyboard become visible. @override void requestKeyboard() { if (_hasFocus) { @@ -847,24 +848,6 @@ class RawEditorState extends EditorState } } - @override - void setTextEditingValue( - TextEditingValue value, SelectionChangedCause cause) { - if (value == textEditingValue) { - return; - } - textEditingValue = value; - userUpdateTextEditingValue(value, cause); - - // keyboard and text input force a selection completion - _handleSelectionCompleted(); - } - - @override - void debugAssertLayoutUpToDate() { - renderEditor.debugAssertLayoutUpToDate(); - } - /// Shows the selection toolbar at the location of the current cursor. /// /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar @@ -892,8 +875,19 @@ class RawEditorState extends EditorState widget.controller.copiedImageUrl = null; _pastePlainText = widget.controller.getPlainText(); _pasteStyle = widget.controller.getAllIndividualSelectionStyles(); + // Copied straight from EditableTextState - super.copySelection(cause); + void _copySelection(SelectionChangedCause cause) { + final selection = textEditingValue.selection; + final text = textEditingValue.text; + if (selection.isCollapsed || !selection.isValid) { + return; + } + Clipboard.setData(ClipboardData(text: selection.textInside(text))); + } + + _copySelection(cause); + if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); hideToolbar(false); @@ -919,7 +913,30 @@ class RawEditorState extends EditorState _pasteStyle = widget.controller.getAllIndividualSelectionStyles(); // Copied straight from EditableTextState - super.cutSelection(cause); + void _cutSelection(SelectionChangedCause cause) { + final selection = textEditingValue.selection; + if (widget.readOnly || !selection.isValid) { + return; + } + final text = textEditingValue.text; + if (selection.isCollapsed) { + return; + } + Clipboard.setData(ClipboardData(text: selection.textInside(text))); + userUpdateTextEditingValue( + TextEditingValue( + text: selection.textBefore(text) + selection.textAfter(text), + selection: TextSelection.collapsed( + offset: math.min(selection.start, selection.end), + affinity: selection.affinity, + ), + ), + cause, + ); + } + + _cutSelection(cause); + if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); hideToolbar(); @@ -946,7 +963,37 @@ class RawEditorState extends EditorState } // Copied straight from EditableTextState - super.pasteText(cause); // ignore: unawaited_futures + Future pasteText(SelectionChangedCause cause) async { + final selection = textEditingValue.selection; + if (widget.readOnly || !selection.isValid) { + return; + } + final text = textEditingValue.text; + if (!selection.isValid) { + return; + } + // Snapshot the input before using `await`. + // See https://github.com/flutter/flutter/issues/11427 + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data == null) { + return; + } + userUpdateTextEditingValue( + TextEditingValue( + text: selection.textBefore(text) + + data.text! + + selection.textAfter(text), + selection: TextSelection.collapsed( + offset: + math.min(selection.start, selection.end) + data.text!.length, + affinity: selection.affinity, + ), + ), + cause, + ); + } + + pasteText(cause); // ignore: unawaited_futures if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); @@ -954,10 +1001,31 @@ class RawEditorState extends EditorState } } + void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) { + if (nextSelection == textEditingValue.selection) { + return; + } + userUpdateTextEditingValue( + textEditingValue.copyWith(selection: nextSelection), + cause, + ); + } + @override void selectAll(SelectionChangedCause cause) { // Copied straight from EditableTextState - super.selectAll(cause); + void _selectAll(SelectionChangedCause cause) { + _setSelection( + textEditingValue.selection.copyWith( + baseOffset: 0, + extentOffset: textEditingValue.text.length, + ), + cause, + ); + } + + _selectAll(cause); + if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); } @@ -966,18 +1034,6 @@ class RawEditorState extends EditorState @override bool get wantKeepAlive => widget.focusNode.hasFocus; - @override - bool get obscureText => false; - - @override - bool get selectionEnabled => widget.enableInteractiveSelection; - - @override - bool get readOnly => widget.readOnly; - - @override - TextLayoutMetrics get textLayoutMetrics => renderEditor; - @override AnimationController get floatingCursorResetController => _floatingCursorResetController; From 802aca8f96d4b263e5d09f2c3ffbb1fb6245ab78 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 9 Feb 2022 21:11:41 -0800 Subject: [PATCH 20/39] Upgrade to 4.0.0 --- CHANGELOG.md | 3 +++ pubspec.yaml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7299d8bd..9b359e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [4.0.0] +* Upgrade for Flutter 2.10. + # [3.9.11] * Added Indonesian translation. diff --git a/pubspec.yaml b/pubspec.yaml index 97dfcd86..4428eead 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,13 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 3.9.11 +version: 4.0.0 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.5.3" + flutter: ">=2.10.0" dependencies: flutter: From 1c73a172a4334eaa2bfaaa75058d170779c5fa94 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 9 Feb 2022 21:14:53 -0800 Subject: [PATCH 21/39] Fix warnings --- lib/src/widgets/default_styles.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index 76cb50e0..4434e03a 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -202,7 +202,7 @@ class DefaultStyles { final inlineCodeStyle = TextStyle( fontSize: 14, - color: themeData.colorScheme.primaryVariant.withOpacity(0.8), + color: themeData.colorScheme.primary.withOpacity(0.8), fontFamily: fontFamily, ); From 9004ec95ee72e935d56cebbe8c4a069188bc1352 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 14 Feb 2022 06:47:34 -0800 Subject: [PATCH 22/39] Fix copy/cut/paste/selectAll not working --- lib/src/widgets/raw_editor.dart | 131 ++++++++++++-------------------- 1 file changed, 47 insertions(+), 84 deletions(-) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index ad0205c6..5b7f8fd5 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -870,23 +870,27 @@ class RawEditorState extends EditorState return true; } + void _replaceText(ReplaceTextIntent intent) { + userUpdateTextEditingValue( + intent.currentTextEditingValue + .replaced(intent.replacementRange, intent.replacementText), + intent.cause, + ); + } + + /// Copy current selection to [Clipboard]. @override void copySelection(SelectionChangedCause cause) { widget.controller.copiedImageUrl = null; _pastePlainText = widget.controller.getPlainText(); _pasteStyle = widget.controller.getAllIndividualSelectionStyles(); - // Copied straight from EditableTextState - void _copySelection(SelectionChangedCause cause) { - final selection = textEditingValue.selection; - final text = textEditingValue.text; - if (selection.isCollapsed || !selection.isValid) { - return; - } - Clipboard.setData(ClipboardData(text: selection.textInside(text))); + final selection = textEditingValue.selection; + final text = textEditingValue.text; + if (selection.isCollapsed) { + return; } - - _copySelection(cause); + Clipboard.setData(ClipboardData(text: selection.textInside(text))); if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); @@ -906,36 +910,23 @@ class RawEditorState extends EditorState } } + /// Cut current selection to [Clipboard]. @override void cutSelection(SelectionChangedCause cause) { widget.controller.copiedImageUrl = null; _pastePlainText = widget.controller.getPlainText(); _pasteStyle = widget.controller.getAllIndividualSelectionStyles(); - // Copied straight from EditableTextState - void _cutSelection(SelectionChangedCause cause) { - final selection = textEditingValue.selection; - if (widget.readOnly || !selection.isValid) { - return; - } - final text = textEditingValue.text; - if (selection.isCollapsed) { - return; - } - Clipboard.setData(ClipboardData(text: selection.textInside(text))); - userUpdateTextEditingValue( - TextEditingValue( - text: selection.textBefore(text) + selection.textAfter(text), - selection: TextSelection.collapsed( - offset: math.min(selection.start, selection.end), - affinity: selection.affinity, - ), - ), - cause, - ); + if (widget.readOnly) { + return; } - - _cutSelection(cause); + final selection = textEditingValue.selection; + final text = textEditingValue.text; + if (selection.isCollapsed) { + return; + } + Clipboard.setData(ClipboardData(text: selection.textInside(text))); + _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); @@ -943,8 +934,13 @@ class RawEditorState extends EditorState } } + /// Paste text from [Clipboard]. @override Future pasteText(SelectionChangedCause cause) async { + if (widget.readOnly) { + return; + } + if (widget.controller.copiedImageUrl != null) { final index = textEditingValue.selection.baseOffset; final length = textEditingValue.selection.extentOffset - index; @@ -962,38 +958,19 @@ class RawEditorState extends EditorState return; } - // Copied straight from EditableTextState - Future pasteText(SelectionChangedCause cause) async { - final selection = textEditingValue.selection; - if (widget.readOnly || !selection.isValid) { - return; - } - final text = textEditingValue.text; - if (!selection.isValid) { - return; - } - // Snapshot the input before using `await`. - // See https://github.com/flutter/flutter/issues/11427 - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data == null) { - return; - } - userUpdateTextEditingValue( - TextEditingValue( - text: selection.textBefore(text) + - data.text! + - selection.textAfter(text), - selection: TextSelection.collapsed( - offset: - math.min(selection.start, selection.end) + data.text!.length, - affinity: selection.affinity, - ), - ), - cause, - ); + final selection = textEditingValue.selection; + if (!selection.isValid) { + return; + } + // Snapshot the input before using `await`. + // See https://github.com/flutter/flutter/issues/11427 + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data == null) { + return; } - pasteText(cause); // ignore: unawaited_futures + _replaceText( + ReplaceTextIntent(textEditingValue, data.text!, selection, cause)); if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); @@ -1001,30 +978,16 @@ class RawEditorState extends EditorState } } - void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) { - if (nextSelection == textEditingValue.selection) { - return; - } + /// Select the entire text value. + @override + void selectAll(SelectionChangedCause cause) { userUpdateTextEditingValue( - textEditingValue.copyWith(selection: nextSelection), + textEditingValue.copyWith( + selection: TextSelection( + baseOffset: 0, extentOffset: textEditingValue.text.length), + ), cause, ); - } - - @override - void selectAll(SelectionChangedCause cause) { - // Copied straight from EditableTextState - void _selectAll(SelectionChangedCause cause) { - _setSelection( - textEditingValue.selection.copyWith( - baseOffset: 0, - extentOffset: textEditingValue.text.length, - ), - cause, - ); - } - - _selectAll(cause); if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); From 21d001c95a2bd5704d8ffb6d518f2991cf2583bf Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 14 Feb 2022 06:51:45 -0800 Subject: [PATCH 23/39] Update RawEditorState --- lib/src/widgets/raw_editor.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 5b7f8fd5..8a2d2f38 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -253,6 +253,8 @@ class RawEditorState extends EditorState // Cursors late CursorCont _cursorCont; + QuillController get controller => widget.controller; + // Focus bool _didAutoFocus = false; bool get _hasFocus => widget.focusNode.hasFocus; From 22ab369c022e0acc6d708ddb5c2a4acadc9afdbd Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 14 Feb 2022 07:08:43 -0800 Subject: [PATCH 24/39] Update RawEditor --- lib/src/widgets/raw_editor.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 8a2d2f38..39e3c8be 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -211,6 +211,8 @@ class RawEditor extends StatefulWidget { /// and paste, and moving the caret will be disabled. final bool enableInteractiveSelection; + bool get selectionEnabled => enableInteractiveSelection; + /// The [ScrollPhysics] to use when vertically scrolling the input. /// /// If not specified, it will behave according to the current platform. From f6223e062a00eed14138698ed0a0fa74100bcc83 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 14 Feb 2022 07:20:04 -0800 Subject: [PATCH 25/39] Use actions to fix showing handler --- lib/src/widgets/editor.dart | 37 ++ lib/src/widgets/raw_editor.dart | 738 +++++++++++++++++++++++++++++++- lib/src/widgets/shortcut.dart | 99 +++++ 3 files changed, 868 insertions(+), 6 deletions(-) create mode 100644 lib/src/widgets/shortcut.dart diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 619bc2bb..5cbb6e4c 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -1544,6 +1544,14 @@ class RenderEditor extends RenderEditableContainerBox // End TextLayoutMetrics implementation + QuillVerticalCaretMovementRun startVerticalCaretMovement( + TextPosition startPosition) { + return QuillVerticalCaretMovementRun._( + this, + startPosition, + ); + } + @override void systemFontsDidChange() { super.systemFontsDidChange(); @@ -1551,6 +1559,35 @@ class RenderEditor extends RenderEditableContainerBox } } +class QuillVerticalCaretMovementRun + extends BidirectionalIterator { + QuillVerticalCaretMovementRun._( + this._editor, + this._currentTextPosition, + ); + + TextPosition _currentTextPosition; + + final RenderEditor _editor; + + @override + TextPosition get current { + return _currentTextPosition; + } + + @override + bool moveNext() { + _currentTextPosition = _editor.getTextPositionBelow(_currentTextPosition); + return true; + } + + @override + bool movePrevious() { + _currentTextPosition = _editor.getTextPositionAbove(_currentTextPosition); + return true; + } +} + class EditableContainerParentData extends ContainerBoxParentData {} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 39e3c8be..b15bc247 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -363,12 +363,15 @@ class RawEditorState extends EditorState return QuillStyles( data: _styles!, - child: MouseRegion( - cursor: SystemMouseCursors.text, - child: QuillKeyboardListener( - child: Container( - constraints: constraints, - child: child, + child: Actions( + actions: _actions, + child: Focus( + focusNode: widget.focusNode, + child: QuillKeyboardListener( + child: Container( + constraints: constraints, + child: child, + ), ), ), ), @@ -1006,6 +1009,123 @@ class RawEditorState extends EditorState _floatingCursorResetController; late AnimationController _floatingCursorResetController; + + // --------------------------- Text Editing Actions -------------------------- + + _TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) { + final _TextBoundary atomicTextBoundary = + _CharacterBoundary(textEditingValue); + return _CollapsedSelectionBoundary(atomicTextBoundary, intent.forward); + } + + _TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) { + final _TextBoundary atomicTextBoundary; + final _TextBoundary boundary; + + // final TextEditingValue textEditingValue = + // _textEditingValueforTextLayoutMetrics; + atomicTextBoundary = _CharacterBoundary(textEditingValue); + // This isn't enough. Newline characters. + boundary = _ExpandedTextBoundary(_WhitespaceBoundary(textEditingValue), + _WordBoundary(renderEditor, textEditingValue)); + + final mixedBoundary = intent.forward + ? _MixedBoundary(atomicTextBoundary, boundary) + : _MixedBoundary(boundary, atomicTextBoundary); + // Use a _MixedBoundary to make sure we don't leave invalid codepoints in + // the field after deletion. + return _CollapsedSelectionBoundary(mixedBoundary, intent.forward); + } + + _TextBoundary _linebreak(DirectionalTextEditingIntent intent) { + final _TextBoundary atomicTextBoundary; + final _TextBoundary boundary; + + // final TextEditingValue textEditingValue = + // _textEditingValueforTextLayoutMetrics; + atomicTextBoundary = _CharacterBoundary(textEditingValue); + boundary = _LineBreak(renderEditor, textEditingValue); + + // The _MixedBoundary is to make sure we don't leave invalid code units in + // the field after deletion. + // `boundary` doesn't need to be wrapped in a _CollapsedSelectionBoundary, + // since the document boundary is unique and the linebreak boundary is + // already caret-location based. + return intent.forward + ? _MixedBoundary( + _CollapsedSelectionBoundary(atomicTextBoundary, true), boundary) + : _MixedBoundary( + boundary, _CollapsedSelectionBoundary(atomicTextBoundary, false)); + } + + _TextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => + _DocumentBoundary(textEditingValue); + + Action _makeOverridable(Action defaultAction) { + return Action.overridable( + context: context, defaultAction: defaultAction); + } + + late final Action _replaceTextAction = + CallbackAction(onInvoke: _replaceText); + + void _updateSelection(UpdateSelectionIntent intent) { + userUpdateTextEditingValue( + intent.currentTextEditingValue.copyWith(selection: intent.newSelection), + intent.cause, + ); + } + + late final Action _updateSelectionAction = + CallbackAction(onInvoke: _updateSelection); + + late final _UpdateTextSelectionToAdjacentLineAction< + ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = + _UpdateTextSelectionToAdjacentLineAction< + ExtendSelectionVerticallyToAdjacentLineIntent>(this); + + late final Map> _actions = >{ + DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), + ReplaceTextIntent: _replaceTextAction, + UpdateSelectionIntent: _updateSelectionAction, + DirectionalFocusIntent: DirectionalFocusAction.forTextField(), + + // Delete + DeleteCharacterIntent: _makeOverridable( + _DeleteTextAction(this, _characterBoundary)), + DeleteToNextWordBoundaryIntent: _makeOverridable( + _DeleteTextAction( + this, _nextWordBoundary)), + DeleteToLineBreakIntent: _makeOverridable( + _DeleteTextAction(this, _linebreak)), + + // Extend/Move Selection + ExtendSelectionByCharacterIntent: _makeOverridable( + _UpdateTextSelectionAction( + this, + false, + _characterBoundary, + )), + ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( + _UpdateTextSelectionAction( + this, true, _nextWordBoundary)), + ExtendSelectionToLineBreakIntent: _makeOverridable( + _UpdateTextSelectionAction( + this, true, _linebreak)), + ExtendSelectionVerticallyToAdjacentLineIntent: + _makeOverridable(_adjacentLineAction), + ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( + _UpdateTextSelectionAction( + this, true, _documentBoundary)), + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( + _ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)), + + // Copy Paste + SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), + CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), + PasteTextIntent: _makeOverridable(CallbackAction( + onInvoke: (intent) => pasteText(intent.cause))), + }; } class _Editor extends MultiChildRenderObjectWidget { @@ -1083,3 +1203,609 @@ class _Editor extends MultiChildRenderObjectWidget { ..maxContentWidth = maxContentWidth; } } + +/// An interface for retrieving the logical text boundary +/// (left-closed-right-open) +/// at a given location in a document. +/// +/// Depending on the implementation of the [_TextBoundary], the input +/// [TextPosition] can either point to a code unit, or a position between 2 code +/// units (which can be visually represented by the caret if the selection were +/// to collapse to that position). +/// +/// For example, [_LineBreak] interprets the input [TextPosition] as a caret +/// location, since in Flutter the caret is generally painted between the +/// character the [TextPosition] points to and its previous character, and +/// [_LineBreak] cares about the affinity of the input [TextPosition]. Most +/// other text boundaries however, interpret the input [TextPosition] as the +/// location of a code unit in the document, since it's easier to reason about +/// the text boundary given a code unit in the text. +/// +/// To convert a "code-unit-based" [_TextBoundary] to "caret-location-based", +/// use the [_CollapsedSelectionBoundary] combinator. +abstract class _TextBoundary { + const _TextBoundary(); + + TextEditingValue get textEditingValue; + + /// Returns the leading text boundary at the given location, inclusive. + TextPosition getLeadingTextBoundaryAt(TextPosition position); + + /// Returns the trailing text boundary at the given location, exclusive. + TextPosition getTrailingTextBoundaryAt(TextPosition position); + + TextRange getTextBoundaryAt(TextPosition position) { + return TextRange( + start: getLeadingTextBoundaryAt(position).offset, + end: getTrailingTextBoundaryAt(position).offset, + ); + } +} + +// ----------------------------- Text Boundaries ----------------------------- + +// The word modifier generally removes the word boundaries around white spaces +// (and newlines), IOW white spaces and some other punctuations are considered +// a part of the next word in the search direction. +class _WhitespaceBoundary extends _TextBoundary { + const _WhitespaceBoundary(this.textEditingValue); + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + for (var index = position.offset; index >= 0; index -= 1) { + if (!TextLayoutMetrics.isWhitespace( + textEditingValue.text.codeUnitAt(index))) { + return TextPosition(offset: index); + } + } + return const TextPosition(offset: 0); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + for (var index = position.offset; + index < textEditingValue.text.length; + index += 1) { + if (!TextLayoutMetrics.isWhitespace( + textEditingValue.text.codeUnitAt(index))) { + return TextPosition(offset: index + 1); + } + } + return TextPosition(offset: textEditingValue.text.length); + } +} + +// Most apps delete the entire grapheme when the backspace key is pressed. +// Also always put the new caret location to character boundaries to avoid +// sending malformed UTF-16 code units to the paragraph builder. +class _CharacterBoundary extends _TextBoundary { + const _CharacterBoundary(this.textEditingValue); + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + final int endOffset = + math.min(position.offset + 1, textEditingValue.text.length); + return TextPosition( + offset: + CharacterRange.at(textEditingValue.text, position.offset, endOffset) + .stringBeforeLength, + ); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + final int endOffset = + math.min(position.offset + 1, textEditingValue.text.length); + final range = + CharacterRange.at(textEditingValue.text, position.offset, endOffset); + return TextPosition( + offset: textEditingValue.text.length - range.stringAfterLength, + ); + } + + @override + TextRange getTextBoundaryAt(TextPosition position) { + final int endOffset = + math.min(position.offset + 1, textEditingValue.text.length); + final range = + CharacterRange.at(textEditingValue.text, position.offset, endOffset); + return TextRange( + start: range.stringBeforeLength, + end: textEditingValue.text.length - range.stringAfterLength, + ); + } +} + +// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries. +class _WordBoundary extends _TextBoundary { + const _WordBoundary(this.textLayout, this.textEditingValue); + + final TextLayoutMetrics textLayout; + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textLayout.getWordBoundary(position).start, + // Word boundary seems to always report downstream on many platforms. + affinity: + TextAffinity.downstream, // ignore: avoid_redundant_argument_values + ); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textLayout.getWordBoundary(position).end, + // Word boundary seems to always report downstream on many platforms. + affinity: + TextAffinity.downstream, // ignore: avoid_redundant_argument_values + ); + } +} + +// The linebreaks of the current text layout. The input [TextPosition]s are +// interpreted as caret locations because [TextPainter.getLineAtOffset] is +// text-affinity-aware. +class _LineBreak extends _TextBoundary { + const _LineBreak(this.textLayout, this.textEditingValue); + + final TextLayoutMetrics textLayout; + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textLayout.getLineAtOffset(position).start, + ); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textLayout.getLineAtOffset(position).end, + affinity: TextAffinity.upstream, + ); + } +} + +// The document boundary is unique and is a constant function of the input +// position. +class _DocumentBoundary extends _TextBoundary { + const _DocumentBoundary(this.textEditingValue); + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) => + const TextPosition(offset: 0); + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textEditingValue.text.length, + affinity: TextAffinity.upstream, + ); + } +} + +// ------------------------ Text Boundary Combinators ------------------------ + +// Expands the innerTextBoundary with outerTextBoundary. +class _ExpandedTextBoundary extends _TextBoundary { + _ExpandedTextBoundary(this.innerTextBoundary, this.outerTextBoundary); + + final _TextBoundary innerTextBoundary; + final _TextBoundary outerTextBoundary; + + @override + TextEditingValue get textEditingValue { + assert(innerTextBoundary.textEditingValue == + outerTextBoundary.textEditingValue); + return innerTextBoundary.textEditingValue; + } + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + return outerTextBoundary.getLeadingTextBoundaryAt( + innerTextBoundary.getLeadingTextBoundaryAt(position), + ); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return outerTextBoundary.getTrailingTextBoundaryAt( + innerTextBoundary.getTrailingTextBoundaryAt(position), + ); + } +} + +// Force the innerTextBoundary to interpret the input [TextPosition]s as caret +// locations instead of code unit positions. +// +// The innerTextBoundary must be a [_TextBoundary] that interprets the input +// [TextPosition]s as code unit positions. +class _CollapsedSelectionBoundary extends _TextBoundary { + _CollapsedSelectionBoundary(this.innerTextBoundary, this.isForward); + + final _TextBoundary innerTextBoundary; + final bool isForward; + + @override + TextEditingValue get textEditingValue => innerTextBoundary.textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + return isForward + ? innerTextBoundary.getLeadingTextBoundaryAt(position) + : position.offset <= 0 + ? const TextPosition(offset: 0) + : innerTextBoundary.getLeadingTextBoundaryAt( + TextPosition(offset: position.offset - 1)); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return isForward + ? innerTextBoundary.getTrailingTextBoundaryAt(position) + : position.offset <= 0 + ? const TextPosition(offset: 0) + : innerTextBoundary.getTrailingTextBoundaryAt( + TextPosition(offset: position.offset - 1)); + } +} + +// A _TextBoundary that creates a [TextRange] where its start is from the +// specified leading text boundary and its end is from the specified trailing +// text boundary. +class _MixedBoundary extends _TextBoundary { + _MixedBoundary(this.leadingTextBoundary, this.trailingTextBoundary); + + final _TextBoundary leadingTextBoundary; + final _TextBoundary trailingTextBoundary; + + @override + TextEditingValue get textEditingValue { + assert(leadingTextBoundary.textEditingValue == + trailingTextBoundary.textEditingValue); + return leadingTextBoundary.textEditingValue; + } + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) => + leadingTextBoundary.getLeadingTextBoundaryAt(position); + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) => + trailingTextBoundary.getTrailingTextBoundaryAt(position); +} + +// ------------------------------- Text Actions ------------------------------- +class _DeleteTextAction + extends ContextAction { + _DeleteTextAction(this.state, this.getTextBoundariesForIntent); + + final RawEditorState state; + final _TextBoundary Function(T intent) getTextBoundariesForIntent; + + TextRange _expandNonCollapsedRange(TextEditingValue value) { + final TextRange selection = value.selection; + assert(selection.isValid); + assert(!selection.isCollapsed); + final _TextBoundary atomicBoundary = _CharacterBoundary(value); + + return TextRange( + start: atomicBoundary + .getLeadingTextBoundaryAt(TextPosition(offset: selection.start)) + .offset, + end: atomicBoundary + .getTrailingTextBoundaryAt(TextPosition(offset: selection.end - 1)) + .offset, + ); + } + + @override + Object? invoke(T intent, [BuildContext? context]) { + final selection = state.textEditingValue.selection; + assert(selection.isValid); + + if (!selection.isCollapsed) { + return Actions.invoke( + context!, + ReplaceTextIntent( + state.textEditingValue, + '', + _expandNonCollapsedRange(state.textEditingValue), + SelectionChangedCause.keyboard), + ); + } + + final textBoundary = getTextBoundariesForIntent(intent); + if (!textBoundary.textEditingValue.selection.isValid) { + return null; + } + if (!textBoundary.textEditingValue.selection.isCollapsed) { + return Actions.invoke( + context!, + ReplaceTextIntent( + state.textEditingValue, + '', + _expandNonCollapsedRange(textBoundary.textEditingValue), + SelectionChangedCause.keyboard), + ); + } + + return Actions.invoke( + context!, + ReplaceTextIntent( + textBoundary.textEditingValue, + '', + textBoundary + .getTextBoundaryAt(textBoundary.textEditingValue.selection.base), + SelectionChangedCause.keyboard, + ), + ); + } + + @override + bool get isActionEnabled => + !state.widget.readOnly && state.textEditingValue.selection.isValid; +} + +class _UpdateTextSelectionAction + extends ContextAction { + _UpdateTextSelectionAction(this.state, this.ignoreNonCollapsedSelection, + this.getTextBoundariesForIntent); + + final RawEditorState state; + final bool ignoreNonCollapsedSelection; + final _TextBoundary Function(T intent) getTextBoundariesForIntent; + + @override + Object? invoke(T intent, [BuildContext? context]) { + final selection = state.textEditingValue.selection; + assert(selection.isValid); + + final collapseSelection = + intent.collapseSelection || !state.widget.selectionEnabled; + // Collapse to the logical start/end. + TextSelection _collapse(TextSelection selection) { + assert(selection.isValid); + assert(!selection.isCollapsed); + return selection.copyWith( + baseOffset: intent.forward ? selection.end : selection.start, + extentOffset: intent.forward ? selection.end : selection.start, + ); + } + + if (!selection.isCollapsed && + !ignoreNonCollapsedSelection && + collapseSelection) { + return Actions.invoke( + context!, + UpdateSelectionIntent(state.textEditingValue, _collapse(selection), + SelectionChangedCause.keyboard), + ); + } + + final textBoundary = getTextBoundariesForIntent(intent); + final textBoundarySelection = textBoundary.textEditingValue.selection; + if (!textBoundarySelection.isValid) { + return null; + } + if (!textBoundarySelection.isCollapsed && + !ignoreNonCollapsedSelection && + collapseSelection) { + return Actions.invoke( + context!, + UpdateSelectionIntent(state.textEditingValue, + _collapse(textBoundarySelection), SelectionChangedCause.keyboard), + ); + } + + final extent = textBoundarySelection.extent; + final newExtent = intent.forward + ? textBoundary.getTrailingTextBoundaryAt(extent) + : textBoundary.getLeadingTextBoundaryAt(extent); + + final newSelection = collapseSelection + ? TextSelection.fromPosition(newExtent) + : textBoundarySelection.extendTo(newExtent); + + // If collapseAtReversal is true and would have an effect, collapse it. + if (!selection.isCollapsed && + intent.collapseAtReversal && + (selection.baseOffset < selection.extentOffset != + newSelection.baseOffset < newSelection.extentOffset)) { + return Actions.invoke( + context!, + UpdateSelectionIntent( + state.textEditingValue, + TextSelection.fromPosition(selection.base), + SelectionChangedCause.keyboard, + ), + ); + } + + return Actions.invoke( + context!, + UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, + SelectionChangedCause.keyboard), + ); + } + + @override + bool get isActionEnabled => state.textEditingValue.selection.isValid; +} + +class _ExtendSelectionOrCaretPositionAction extends ContextAction< + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent> { + _ExtendSelectionOrCaretPositionAction( + this.state, this.getTextBoundariesForIntent); + + final RawEditorState state; + final _TextBoundary Function( + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent) + getTextBoundariesForIntent; + + @override + Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent, + [BuildContext? context]) { + final selection = state.textEditingValue.selection; + assert(selection.isValid); + + final textBoundary = getTextBoundariesForIntent(intent); + final textBoundarySelection = textBoundary.textEditingValue.selection; + if (!textBoundarySelection.isValid) { + return null; + } + + final extent = textBoundarySelection.extent; + final newExtent = intent.forward + ? textBoundary.getTrailingTextBoundaryAt(extent) + : textBoundary.getLeadingTextBoundaryAt(extent); + + final newSelection = (newExtent.offset - textBoundarySelection.baseOffset) * + (textBoundarySelection.extentOffset - + textBoundarySelection.baseOffset) < + 0 + ? textBoundarySelection.copyWith( + extentOffset: textBoundarySelection.baseOffset, + affinity: textBoundarySelection.extentOffset > + textBoundarySelection.baseOffset + ? TextAffinity.downstream + : TextAffinity.upstream, + ) + : textBoundarySelection.extendTo(newExtent); + + return Actions.invoke( + context!, + UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, + SelectionChangedCause.keyboard), + ); + } + + @override + bool get isActionEnabled => + state.widget.selectionEnabled && state.textEditingValue.selection.isValid; +} + +class _UpdateTextSelectionToAdjacentLineAction< + T extends DirectionalCaretMovementIntent> extends ContextAction { + _UpdateTextSelectionToAdjacentLineAction(this.state); + + final RawEditorState state; + + QuillVerticalCaretMovementRun? _verticalMovementRun; + TextSelection? _runSelection; + + void stopCurrentVerticalRunIfSelectionChanges() { + final runSelection = _runSelection; + if (runSelection == null) { + assert(_verticalMovementRun == null); + return; + } + _runSelection = state.textEditingValue.selection; + final currentSelection = state.widget.controller.selection; + final continueCurrentRun = currentSelection.isValid && + currentSelection.isCollapsed && + currentSelection.baseOffset == runSelection.baseOffset && + currentSelection.extentOffset == runSelection.extentOffset; + if (!continueCurrentRun) { + _verticalMovementRun = null; + _runSelection = null; + } + } + + @override + void invoke(T intent, [BuildContext? context]) { + assert(state.textEditingValue.selection.isValid); + + final collapseSelection = + intent.collapseSelection || !state.widget.selectionEnabled; + final value = state.textEditingValue; + if (!value.selection.isValid) { + return; + } + + final currentRun = _verticalMovementRun ?? + state.renderEditor + .startVerticalCaretMovement(state.renderEditor.selection.extent); + + final shouldMove = + intent.forward ? currentRun.moveNext() : currentRun.movePrevious(); + final newExtent = shouldMove + ? currentRun.current + : (intent.forward + ? TextPosition(offset: state.textEditingValue.text.length) + : const TextPosition(offset: 0)); + final newSelection = collapseSelection + ? TextSelection.fromPosition(newExtent) + : value.selection.extendTo(newExtent); + + Actions.invoke( + context!, + UpdateSelectionIntent( + value, newSelection, SelectionChangedCause.keyboard), + ); + if (state.textEditingValue.selection == newSelection) { + _verticalMovementRun = currentRun; + _runSelection = newSelection; + } + } + + @override + bool get isActionEnabled => state.textEditingValue.selection.isValid; +} + +class _SelectAllAction extends ContextAction { + _SelectAllAction(this.state); + + final RawEditorState state; + + @override + Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) { + return Actions.invoke( + context!, + UpdateSelectionIntent( + state.textEditingValue, + TextSelection( + baseOffset: 0, extentOffset: state.textEditingValue.text.length), + intent.cause, + ), + ); + } + + @override + bool get isActionEnabled => state.widget.selectionEnabled; +} + +class _CopySelectionAction extends ContextAction { + _CopySelectionAction(this.state); + + final RawEditorState state; + + @override + void invoke(CopySelectionTextIntent intent, [BuildContext? context]) { + if (intent.collapseSelection) { + state.cutSelection(intent.cause); + } else { + state.copySelection(intent.cause); + } + } + + @override + bool get isActionEnabled => + state.textEditingValue.selection.isValid && + !state.textEditingValue.selection.isCollapsed; +} diff --git a/lib/src/widgets/shortcut.dart b/lib/src/widgets/shortcut.dart new file mode 100644 index 00000000..5cf28b8a --- /dev/null +++ b/lib/src/widgets/shortcut.dart @@ -0,0 +1,99 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../models/documents/attribute.dart'; +import 'raw_editor.dart'; + +class QuillShortcuts extends Shortcuts { + QuillShortcuts({required Widget child, Key? key}) + : super( + key: key, + shortcuts: _shortcuts, + child: child, + ); + + static Map get _shortcuts { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return _defaultShortcuts; + case TargetPlatform.fuchsia: + return _defaultShortcuts; + case TargetPlatform.iOS: + return _macShortcuts; + case TargetPlatform.linux: + return _defaultShortcuts; + case TargetPlatform.macOS: + return _macShortcuts; + case TargetPlatform.windows: + return _defaultShortcuts; + } + } + + static const Map _defaultShortcuts = + { + SingleActivator(LogicalKeyboardKey.keyB, control: true): + ToggleBoldStyleIntent(), + SingleActivator(LogicalKeyboardKey.keyI, control: true): + ToggleItalicStyleIntent(), + SingleActivator(LogicalKeyboardKey.keyU, control: true): + ToggleUnderlineStyleIntent(), + }; + + static final Map _macShortcuts = + { + const SingleActivator(LogicalKeyboardKey.keyB, meta: true): + const ToggleBoldStyleIntent(), + const SingleActivator(LogicalKeyboardKey.keyI, meta: true): + const ToggleItalicStyleIntent(), + const SingleActivator(LogicalKeyboardKey.keyU, meta: true): + const ToggleUnderlineStyleIntent(), + }; +} + +class ToggleBoldStyleIntent extends Intent { + const ToggleBoldStyleIntent(); +} + +class ToggleItalicStyleIntent extends Intent { + const ToggleItalicStyleIntent(); +} + +class ToggleUnderlineStyleIntent extends Intent { + const ToggleUnderlineStyleIntent(); +} + +class QuillActions extends Actions { + QuillActions({ + required Widget child, + Key? key, + }) : super( + key: key, + actions: _shortcutsActions, + child: child, + ); + + static final Map> _shortcutsActions = + >{ + ToggleBoldStyleIntent: _ToggleInlineStyleAction(Attribute.bold), + ToggleItalicStyleIntent: _ToggleInlineStyleAction(Attribute.italic), + ToggleUnderlineStyleIntent: _ToggleInlineStyleAction(Attribute.underline), + }; +} + +class _ToggleInlineStyleAction extends ContextAction { + _ToggleInlineStyleAction(this.attribute); + + final Attribute attribute; + + @override + Object? invoke(Intent intent, [BuildContext? context]) { + final editorState = context!.findAncestorStateOfType()!; + final style = editorState.controller.getSelectionStyle(); + final actualAttr = style.containsKey(attribute.key) + ? Attribute.clone(attribute, null) + : attribute; + editorState.controller.formatSelection(actualAttr); + return null; + } +} From 3223a86c0f6a953713a726221670b65e2d17b68d Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 14 Feb 2022 07:20:58 -0800 Subject: [PATCH 26/39] Upgrade to 4.0.1 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b359e49..f7cc4184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [4.0.1] +* Fix copy/cut/paste/selectAll not working. + # [4.0.0] * Upgrade for Flutter 2.10. diff --git a/pubspec.yaml b/pubspec.yaml index 4428eead..636d2550 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: 4.0.0 +version: 4.0.1 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 75d223a7aa46e4ddeb1eed04ac87225424d252ed Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 14 Feb 2022 07:31:14 -0800 Subject: [PATCH 27/39] Update example for iOS --- .gitignore | 3 +- example/ios/Runner.xcodeproj/project.pbxproj | 88 +++++++++++++++++-- .../contents.xcworkspacedata | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../contents.xcworkspacedata | 3 + .../flutter/generated_plugin_registrant.cc | 2 + .../flutter/generated_plugin_registrant.h | 2 + 7 files changed, 93 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index e73faed9..0d11db7b 100644 --- a/.gitignore +++ b/.gitignore @@ -66,10 +66,11 @@ build/ **/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* +example/ios/Podfile.lock # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 -pubspec.lock \ No newline at end of file +pubspec.lock diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index eb73b2b0..1cce8762 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -14,6 +14,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D290BBC2BCE42906E260DD85 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC245C6D0FF6BF1D0B733C5B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,7 +33,10 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2F435AE316A2CEF9DB2FCB44 /* 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 45FB9682812B691627A14497 /* 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 = ""; }; + 48A1E3B04AC3F3F79EE01940 /* 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; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -44,6 +48,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC245C6D0FF6BF1D0B733C5B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -51,12 +56,24 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D290BBC2BCE42906E260DD85 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 54D582D77359D2F1CFEABAD6 /* Pods */ = { + isa = PBXGroup; + children = ( + 2F435AE316A2CEF9DB2FCB44 /* Pods-Runner.debug.xcconfig */, + 48A1E3B04AC3F3F79EE01940 /* Pods-Runner.release.xcconfig */, + 45FB9682812B691627A14497 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -74,7 +91,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + 54D582D77359D2F1CFEABAD6 /* Pods */, + 9B5485B940DE97CC1A7B761C /* Frameworks */, ); sourceTree = ""; }; @@ -110,6 +128,14 @@ name = "Supporting Files"; sourceTree = ""; }; + 9B5485B940DE97CC1A7B761C /* Frameworks */ = { + isa = PBXGroup; + children = ( + DC245C6D0FF6BF1D0B733C5B /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -117,12 +143,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + C781F89B47F2E7DB7E2B4898 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 869AA06D856BCA582968F111 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -139,7 +167,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -194,6 +222,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 869AA06D856BCA582968F111 /* [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; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -208,6 +253,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + C781F89B47F2E7DB7E2B4898 /* [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 */ @@ -305,7 +372,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -433,7 +503,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -456,7 +529,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16..919434a6 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..3db53b6e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 92772e9f..4f788487 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #include "generated_plugin_registrant.h" #include diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h index 9846246b..dc139d85 100644 --- a/example/windows/flutter/generated_plugin_registrant.h +++ b/example/windows/flutter/generated_plugin_registrant.h @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ From 9512a53f706536f41532f9501ab7f158ffa85cd8 Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 14 Feb 2022 07:34:14 -0800 Subject: [PATCH 28/39] Remove shortcut --- lib/src/widgets/shortcut.dart | 99 ----------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 lib/src/widgets/shortcut.dart diff --git a/lib/src/widgets/shortcut.dart b/lib/src/widgets/shortcut.dart deleted file mode 100644 index 5cf28b8a..00000000 --- a/lib/src/widgets/shortcut.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../models/documents/attribute.dart'; -import 'raw_editor.dart'; - -class QuillShortcuts extends Shortcuts { - QuillShortcuts({required Widget child, Key? key}) - : super( - key: key, - shortcuts: _shortcuts, - child: child, - ); - - static Map get _shortcuts { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return _defaultShortcuts; - case TargetPlatform.fuchsia: - return _defaultShortcuts; - case TargetPlatform.iOS: - return _macShortcuts; - case TargetPlatform.linux: - return _defaultShortcuts; - case TargetPlatform.macOS: - return _macShortcuts; - case TargetPlatform.windows: - return _defaultShortcuts; - } - } - - static const Map _defaultShortcuts = - { - SingleActivator(LogicalKeyboardKey.keyB, control: true): - ToggleBoldStyleIntent(), - SingleActivator(LogicalKeyboardKey.keyI, control: true): - ToggleItalicStyleIntent(), - SingleActivator(LogicalKeyboardKey.keyU, control: true): - ToggleUnderlineStyleIntent(), - }; - - static final Map _macShortcuts = - { - const SingleActivator(LogicalKeyboardKey.keyB, meta: true): - const ToggleBoldStyleIntent(), - const SingleActivator(LogicalKeyboardKey.keyI, meta: true): - const ToggleItalicStyleIntent(), - const SingleActivator(LogicalKeyboardKey.keyU, meta: true): - const ToggleUnderlineStyleIntent(), - }; -} - -class ToggleBoldStyleIntent extends Intent { - const ToggleBoldStyleIntent(); -} - -class ToggleItalicStyleIntent extends Intent { - const ToggleItalicStyleIntent(); -} - -class ToggleUnderlineStyleIntent extends Intent { - const ToggleUnderlineStyleIntent(); -} - -class QuillActions extends Actions { - QuillActions({ - required Widget child, - Key? key, - }) : super( - key: key, - actions: _shortcutsActions, - child: child, - ); - - static final Map> _shortcutsActions = - >{ - ToggleBoldStyleIntent: _ToggleInlineStyleAction(Attribute.bold), - ToggleItalicStyleIntent: _ToggleInlineStyleAction(Attribute.italic), - ToggleUnderlineStyleIntent: _ToggleInlineStyleAction(Attribute.underline), - }; -} - -class _ToggleInlineStyleAction extends ContextAction { - _ToggleInlineStyleAction(this.attribute); - - final Attribute attribute; - - @override - Object? invoke(Intent intent, [BuildContext? context]) { - final editorState = context!.findAncestorStateOfType()!; - final style = editorState.controller.getSelectionStyle(); - final actualAttr = style.containsKey(attribute.key) - ? Attribute.clone(attribute, null) - : attribute; - editorState.controller.formatSelection(actualAttr); - return null; - } -} From c5cc54e3f8baf9760ac8930aa41adaa76e047f0d Mon Sep 17 00:00:00 2001 From: Erlend Wiklem Date: Thu, 17 Feb 2022 00:17:32 +0100 Subject: [PATCH 29/39] no translation (#670) --- lib/src/translations/toolbar.i18n.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 17c79084..346294e8 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -348,6 +348,26 @@ extension Localization on String { 'Width': 'Lebar', 'Height': 'Tinggi', }, + 'no': { + 'Paste a link': 'Lim inn lenke', + 'Ok': 'Ok', + 'Select Color': 'Velg farge', + 'Gallery': 'Galleri', + 'Link': 'Lenke', + 'Please first select some text to transform into a link.': + 'Velg først litt tekst for å forvandle til en lenke.', + 'Open': 'Åpne', + 'Copy': 'Kopier', + 'Remove': 'Fjern', + 'Save': 'Lagre', + 'Zoom': 'Zoom', + 'Saved': 'Lagret', + 'Text': 'Tekst', + 'What is entered is not a link': 'Du har oppgitt en ugyldig lenke', + 'Resize': 'Endre størrelse', + 'Width': 'Bredde', + 'Height': 'Høyde', + }, }; String get i18n => localize(this, _t); From 7758f0a22da4f4b49bb68ead501b338c58833c02 Mon Sep 17 00:00:00 2001 From: Cheryl Zhang <40752995+xinyuezhang0402@users.noreply.github.com> Date: Wed, 16 Feb 2022 15:18:20 -0800 Subject: [PATCH 30/39] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 914b2231..3c9a31f0 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ The package offers translations for the quill toolbar and editor, it will follow QuillToolbar(locale: Locale('fr'), ...) QuillEditor(locale: Locale('fr'), ...) ``` -Currently, translations are available for these 16 locales: +Currently, translations are available for these 17 locales: * `Locale('en')` * `Locale('ar')` * `Locale('de')` @@ -133,6 +133,7 @@ Currently, translations are available for these 16 locales: * `Locale('pl')` * `Locale('vi')` * `Locale('id')` +* `Locale('no')` ### Contributing to translations The translation file is located at [lib/src/translations/toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! From b437fb014b9bcfc36b93145502cdadb3ca78a583 Mon Sep 17 00:00:00 2001 From: X Code Date: Wed, 16 Feb 2022 23:30:26 -0800 Subject: [PATCH 31/39] Update Chinese translation --- lib/src/translations/toolbar.i18n.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 346294e8..25fa6b8d 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -141,8 +141,8 @@ extension Localization on String { 'Text': '文字', 'What is entered is not a link': '输入的不是链接', 'Resize': '调整大小', - 'Width': 'Width', - 'Height': 'Height', + 'Width': '宽度', + 'Height': '高度', }, 'ko': { 'Paste a link': '링크를 붙여넣어 주세요.', From e895c8c0f731908ab9c866867efb39a810b28d43 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Fri, 18 Feb 2022 16:44:38 +0200 Subject: [PATCH 32/39] clear toggled style on selection change (#675) --- lib/src/widgets/controller.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 8d52e730..16506c10 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -327,6 +327,7 @@ class QuillController extends ChangeNotifier { _selection = selection.copyWith( baseOffset: math.min(selection.baseOffset, end), extentOffset: math.min(selection.extentOffset, end)); + toggledStyle = Style(); } /// Given offset, find its leaf node in document From a92bfe9d6d2edd32ba6a12e141e39ee47ee0af2f Mon Sep 17 00:00:00 2001 From: X Code Date: Fri, 18 Feb 2022 22:37:04 -0800 Subject: [PATCH 33/39] Upgrade to 4.0.2 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7cc4184..9a2d5883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [4.0.2] +* Clear toggled style on selection change. + # [4.0.1] * Fix copy/cut/paste/selectAll not working. diff --git a/pubspec.yaml b/pubspec.yaml index 636d2550..40556587 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: 4.0.1 +version: 4.0.2 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 4f9ee27f7dc8baf64d36784d4d74c6feb1e18ece Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 20 Feb 2022 08:23:10 -0800 Subject: [PATCH 34/39] Support text direction rtl --- lib/src/models/documents/attribute.dart | 12 ++++++++++++ lib/src/utils/delta.dart | 11 +++++++++++ lib/src/widgets/raw_editor.dart | 7 +++++-- lib/src/widgets/text_block.dart | 5 ++++- lib/src/widgets/toolbar.dart | 11 ++++++++++- 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index ed848d79..fe2832da 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -79,6 +79,8 @@ class Attribute { static final BlockQuoteAttribute blockQuote = BlockQuoteAttribute(); + static final DirectionAttribute direction = DirectionAttribute(null); + static final WidthAttribute width = WidthAttribute(null); static final HeightAttribute height = HeightAttribute(null); @@ -116,6 +118,7 @@ class Attribute { Attribute.codeBlock.key, Attribute.blockQuote.key, Attribute.indent.key, + Attribute.direction.key, }); static final Set blockKeysExceptHeader = LinkedHashSet.of({ @@ -124,6 +127,7 @@ class Attribute { Attribute.codeBlock.key, Attribute.blockQuote.key, Attribute.indent.key, + Attribute.direction.key, }); static final Set exclusiveBlockKeys = LinkedHashSet.of({ @@ -163,6 +167,9 @@ class Attribute { // "attributes":{"list":"unchecked"} static Attribute get unchecked => ListAttribute('unchecked'); + // "attributes":{"direction":"rtl"} + static Attribute get rtl => DirectionAttribute('rtl'); + // "attributes":{"indent":1"} static Attribute get indentL1 => IndentAttribute(level: 1); @@ -309,6 +316,11 @@ class BlockQuoteAttribute extends Attribute { BlockQuoteAttribute() : super('blockquote', AttributeScope.BLOCK, true); } +class DirectionAttribute extends Attribute { + DirectionAttribute(String? val) + : super('direction', AttributeScope.BLOCK, val); +} + class WidthAttribute extends Attribute { WidthAttribute(String? val) : super('width', AttributeScope.IGNORE, val); } diff --git a/lib/src/utils/delta.dart b/lib/src/utils/delta.dart index cf0e5c63..c737e1ac 100644 --- a/lib/src/utils/delta.dart +++ b/lib/src/utils/delta.dart @@ -1,5 +1,8 @@ import 'dart:math' as math; +import 'dart:ui'; +import '../models/documents/attribute.dart'; +import '../models/documents/nodes/node.dart'; import '../models/quill_delta.dart'; // Diff between two texts - old text and new text @@ -72,3 +75,11 @@ int getPositionDelta(Delta user, Delta actual) { } return diff; } + +TextDirection getDirectionOfNode(Node node) { + final direction = node.style.attributes[Attribute.direction.key]; + if (direction == Attribute.rtl) { + return TextDirection.rtl; + } + return TextDirection.ltr; +} diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index b15bc247..05cd99ef 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -19,6 +19,7 @@ import '../models/documents/nodes/embeddable.dart'; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; import '../models/documents/style.dart'; +import '../utils/delta.dart'; import '../utils/platform.dart'; import 'controller.dart'; import 'cursor.dart'; @@ -436,7 +437,8 @@ class RawEditorState extends EditorState for (final node in doc.root.children) { if (node is Line) { final editableTextLine = _getEditableTextLineFromNode(node, context); - result.add(editableTextLine); + result.add(Directionality( + textDirection: getDirectionOfNode(node), child: editableTextLine)); } else if (node is Block) { final attrs = node.style.attributes; final editableTextBlock = EditableTextBlock( @@ -461,7 +463,8 @@ class RawEditorState extends EditorState onCheckboxTap: _handleCheckboxTap, readOnly: widget.readOnly, customStyleBuilder: widget.customStyleBuilder); - result.add(editableTextBlock); + result.add(Directionality( + textDirection: getDirectionOfNode(node), child: editableTextBlock)); } else { throw StateError('Unreachable.'); } diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 3ab0f439..26305179 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -5,6 +5,7 @@ import 'package:tuple/tuple.dart'; import '../../flutter_quill.dart'; import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; +import '../utils/delta.dart'; import 'box.dart'; import 'cursor.dart'; import 'delegate.dart'; @@ -146,7 +147,9 @@ class EditableTextBlock extends StatelessWidget { hasFocus, MediaQuery.of(context).devicePixelRatio, cursorCont); - children.add(editableTextLine); + final nodeTextDirection = getDirectionOfNode(line); + children.add(Directionality( + textDirection: nodeTextDirection, child: editableTextLine)); } return children.toList(growable: false); } diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index ad350c00..e001c9d9 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -100,6 +100,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showImageButton = true, bool showVideoButton = true, bool showCameraButton = true, + bool showDirection = false, OnImagePickCallback? onImagePickCallback, OnVideoPickCallback? onVideoPickCallback, MediaPickSettingSelector? mediaPickSettingSelector, @@ -131,7 +132,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showClearFormat || onImagePickCallback != null || onVideoPickCallback != null, - showAlignmentButtons, + showAlignmentButtons || showDirection, showLeftAlignment, showCenterAlignment, showRightAlignment, @@ -296,6 +297,14 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showRightAlignment: showRightAlignment, showJustifyAlignment: showJustifyAlignment, ), + if (showDirection) + ToggleStyleButton( + attribute: Attribute.rtl, + controller: controller, + icon: Icons.format_textdirection_r_to_l, + iconSize: toolbarIconSize, + iconTheme: iconTheme, + ), if (showDividers && isButtonGroupShown[1] && (isButtonGroupShown[2] || From e667f748dd5f5845c0e061a232bbaec4b2be32b9 Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 20 Feb 2022 08:23:58 -0800 Subject: [PATCH 35/39] Upgrade to 4.0.3 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2d5883..d00c61d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [4.0.3] +* Support text direction rtl. + # [4.0.2] * Clear toggled style on selection change. diff --git a/pubspec.yaml b/pubspec.yaml index 40556587..382f7ca4 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: 4.0.2 +version: 4.0.3 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From 46a5cde77c54ff0532ff9ab3b89e0b882058f235 Mon Sep 17 00:00:00 2001 From: X Code Date: Sun, 20 Feb 2022 08:27:47 -0800 Subject: [PATCH 36/39] Bug fix for text direction rtl --- CHANGELOG.md | 3 +++ lib/src/models/documents/attribute.dart | 1 + pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00c61d0..267a26c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [4.0.4] +* Bug fix for text direction rtl. + # [4.0.3] * Support text direction rtl. diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index fe2832da..13b6b482 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -32,6 +32,7 @@ class Attribute { Attribute.placeholder.key: Attribute.placeholder, Attribute.header.key: Attribute.header, Attribute.align.key: Attribute.align, + Attribute.direction.key: Attribute.direction, Attribute.list.key: Attribute.list, Attribute.codeBlock.key: Attribute.codeBlock, Attribute.blockQuote.key: Attribute.blockQuote, diff --git a/pubspec.yaml b/pubspec.yaml index 382f7ca4..339d0ade 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: 4.0.3 +version: 4.0.4 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From dcfefa21abdd3e867d5a9d46695a39550ff93553 Mon Sep 17 00:00:00 2001 From: HONGFEI YANG Date: Tue, 22 Feb 2022 18:51:09 +1100 Subject: [PATCH 37/39] fix: fixed casting null to Tuple2 when link dialog is dismissed without any input (e.g. barrier dismissed) (#681) --- lib/src/widgets/toolbar/link_style_button.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index e479481c..9ca83fb6 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -120,7 +120,11 @@ class _LinkStyleButtonState extends State { return _LinkDialog( dialogTheme: widget.dialogTheme, link: link, text: text); }, - ).then(_linkSubmitted); + ).then( + (value) { + if (value != null) _linkSubmitted(value); + }, + ); } String? _getLinkAttributeValue() { From 5772b51d8634ff92394f5848fc52144f408e359a Mon Sep 17 00:00:00 2001 From: X Code Date: Mon, 21 Feb 2022 23:57:43 -0800 Subject: [PATCH 38/39] Upgrade to 4.0.5 --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 267a26c5..2cdeaab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [4.0.5] +* Fixed casting null to Tuple2 when link dialog is dismissed without any input (e.g. barrier dismissed). + # [4.0.4] * Bug fix for text direction rtl. diff --git a/pubspec.yaml b/pubspec.yaml index 339d0ade..bbebc8e9 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: 4.0.4 +version: 4.0.5 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill From c7be3a31a1cdbff025d49c36dd6f0064799171c2 Mon Sep 17 00:00:00 2001 From: Andy Trand Date: Tue, 22 Feb 2022 18:15:00 +0200 Subject: [PATCH 39/39] added selection change callback (#683) --- lib/src/widgets/controller.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 16506c10..f9447c0e 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -23,6 +23,7 @@ class QuillController extends ChangeNotifier { this.onReplaceText, this.onDelete, this.onSelectionCompleted, + this.onSelectionChanged, }) : _selection = selection, _keepStyleOnNewLine = keepStyleOnNewLine; @@ -52,6 +53,7 @@ class QuillController extends ChangeNotifier { DeleteCallback? onDelete; void Function()? onSelectionCompleted; + void Function(TextSelection textSelection)? onSelectionChanged; /// Store any styles attribute that got toggled by the tap of a button /// and that has not been applied yet. @@ -328,6 +330,7 @@ class QuillController extends ChangeNotifier { baseOffset: math.min(selection.baseOffset, end), extentOffset: math.min(selection.extentOffset, end)); toggledStyle = Style(); + onSelectionChanged?.call(textSelection); } /// Given offset, find its leaf node in document