Improvemenets and allow to change the default text selection theme

pull/1437/head
Ahmed Hnewa 2 years ago
parent db143c9556
commit 4470a75def
No known key found for this signature in database
GPG Key ID: C488CC70BBCEF0D1
  1. 4
      example/android/app/build.gradle
  2. 24
      example/lib/pages/home_page.dart
  3. 71
      flutter_quill_extensions/lib/embeds/builders.dart
  4. 6
      flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart
  5. 54
      flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart
  6. 91
      flutter_quill_extensions/lib/embeds/toolbar/media_button.dart
  7. 27
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  8. 2
      flutter_quill_extensions/lib/utils/quill_utils.dart
  9. 4
      flutter_quill_extensions/pubspec.yaml
  10. 2
      lib/src/models/documents/nodes/line.dart
  11. 4
      lib/src/models/documents/nodes/node.dart
  12. 8
      lib/src/models/documents/style.dart
  13. 101
      lib/src/translations/toolbar.i18n.dart
  14. 6
      lib/src/utils/platform.dart
  15. 6
      lib/src/widgets/controller.dart
  16. 9
      lib/src/widgets/default_styles.dart
  17. 13
      lib/src/widgets/editor.dart
  18. 74
      lib/src/widgets/raw_editor.dart
  19. 76
      lib/src/widgets/text_block.dart
  20. 84
      lib/src/widgets/toolbar/link_style_button.dart
  21. 3
      lib/src/widgets/toolbar/quill_icon_button.dart
  22. 16
      lib/src/widgets/toolbar/search_dialog.dart
  23. 34
      test/widgets/controller_test.dart

@ -42,7 +42,7 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // I removed the todo because it's not required for this example
applicationId "com.example.app" applicationId "com.example.app"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
@ -53,7 +53,7 @@ android {
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. // I removed the todo because it's not required for this example
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }

@ -190,23 +190,23 @@ class _HomePageState extends State<HomePage> {
onTapUp: (details, p1) { onTapUp: (details, p1) {
return _onTripleClickSelection(); return _onTripleClickSelection();
}, },
customStyles: DefaultStyles( customStyles: const DefaultStyles(
h1: DefaultTextBlockStyle( h1: DefaultTextBlockStyle(
const TextStyle( TextStyle(
fontSize: 32, fontSize: 32,
color: Colors.black, color: Colors.black,
height: 1.15, height: 1.15,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
), ),
const VerticalSpacing(16, 0), VerticalSpacing(16, 0),
const VerticalSpacing(0, 0), VerticalSpacing(0, 0),
null), null),
sizeSmall: const TextStyle(fontSize: 9), sizeSmall: TextStyle(fontSize: 9),
subscript: const TextStyle( subscript: TextStyle(
fontFamily: 'SF-UI-Display', fontFamily: 'SF-UI-Display',
fontFeatures: [FontFeature.subscripts()], fontFeatures: [FontFeature.subscripts()],
), ),
superscript: const TextStyle( superscript: TextStyle(
fontFamily: 'SF-UI-Display', fontFamily: 'SF-UI-Display',
fontFeatures: [FontFeature.superscripts()], fontFeatures: [FontFeature.superscripts()],
), ),
@ -230,18 +230,18 @@ class _HomePageState extends State<HomePage> {
onTapUp: (details, p1) { onTapUp: (details, p1) {
return _onTripleClickSelection(); return _onTripleClickSelection();
}, },
customStyles: DefaultStyles( customStyles: const DefaultStyles(
h1: DefaultTextBlockStyle( h1: DefaultTextBlockStyle(
const TextStyle( TextStyle(
fontSize: 32, fontSize: 32,
color: Colors.black, color: Colors.black,
height: 1.15, height: 1.15,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
), ),
const VerticalSpacing(16, 0), VerticalSpacing(16, 0),
const VerticalSpacing(0, 0), VerticalSpacing(0, 0),
null), null),
sizeSmall: const TextStyle(fontSize: 9), sizeSmall: TextStyle(fontSize: 9),
), ),
embedBuilders: [ embedBuilders: [
...defaultEmbedBuildersWeb, ...defaultEmbedBuildersWeb,

@ -55,10 +55,6 @@ class ImageEmbedBuilder extends EmbedBuilder {
OptionalSize? imageSize; OptionalSize? imageSize;
final style = node.style.attributes['style']; final style = node.style.attributes['style'];
// TODO: Please use the one from [Attribute.margin]
const marginKey = 'margin';
// TODO: Please use the one from [Attribute.alignment]
const alignmentKey = 'alignment';
if (style != null) { if (style != null) {
final attrs = base.isMobile() final attrs = base.isMobile()
? base.parseKeyValuePairs(style.value.toString(), { ? base.parseKeyValuePairs(style.value.toString(), {
@ -70,8 +66,8 @@ class ImageEmbedBuilder extends EmbedBuilder {
: base.parseKeyValuePairs(style.value.toString(), { : base.parseKeyValuePairs(style.value.toString(), {
Attribute.width.key, Attribute.width.key,
Attribute.height.key, Attribute.height.key,
marginKey, Attribute.margin,
alignmentKey, Attribute.alignment,
}); });
if (attrs.isNotEmpty) { if (attrs.isNotEmpty) {
final width = double.tryParse( final width = double.tryParse(
@ -88,10 +84,10 @@ class ImageEmbedBuilder extends EmbedBuilder {
); );
final alignment = base.getAlignment(base.isMobile() final alignment = base.getAlignment(base.isMobile()
? attrs[Attribute.mobileAlignment] ? attrs[Attribute.mobileAlignment]
: attrs[alignmentKey]); : attrs[Attribute.alignment]);
final margin = (base.isMobile() final margin = (base.isMobile()
? double.tryParse(Attribute.mobileMargin) ? double.tryParse(Attribute.mobileMargin)
: double.tryParse(marginKey)) ?? : double.tryParse(Attribute.margin)) ??
0.0; 0.0;
assert( assert(
@ -198,57 +194,14 @@ class ImageEmbedBuilder extends EmbedBuilder {
controller, controller,
controller.selection.start, controller.selection.start,
); );
// For desktop
String _replaceStyleStringWithSize( final attr =
String s, base.replaceStyleStringWithSize(
double width, getImageStyleString(controller),
double height, width: w,
) { height: h,
final result = <String, String>{}; isMobile: base.isMobile(),
final pairs = s.split(';'); );
for (final pair in pairs) {
final _index = pair.indexOf(':');
if (_index < 0) {
continue;
}
final _key =
pair.substring(0, _index).trim();
result[_key] =
pair.substring(_index + 1).trim();
}
result[Attribute.width.key] =
width.toString();
result[Attribute.height.key] =
height.toString();
final sb = StringBuffer();
for (final pair in result.entries) {
sb
..write(pair.key)
..write(': ')
..write(pair.value)
..write('; ');
}
return sb.toString();
}
// TODO: When update flutter_quill
// we should update flutter_quill_extensions
// to use the latest version and use
// base.replaceStyleStringWithSize()
// instead of replaceStyleString
final attr = base.isMobile()
? base.replaceStyleString(
getImageStyleString(controller),
w,
h,
)
: _replaceStyleStringWithSize(
getImageStyleString(controller),
w,
h,
);
controller controller
..skipRequestKeyboard = true ..skipRequestKeyboard = true
..formatText( ..formatText(

@ -146,11 +146,13 @@ class CameraButton extends StatelessWidget {
break; break;
case MediaPickSetting.Gallery: case MediaPickSetting.Gallery:
throw ArgumentError( throw ArgumentError(
'Invalid MediaSetting for the camera button', 'Invalid MediaSetting for the camera button.\n'
'gallery is not related to camera button',
); );
case MediaPickSetting.Link: case MediaPickSetting.Link:
throw ArgumentError( throw ArgumentError(
'Invalid MediaSetting for the camera button', 'Invalid MediaSetting for the camera button.\n'
'link is not related to camera button',
); );
} }
} }

@ -35,17 +35,22 @@ class LinkDialogState extends State<LinkDialog> {
super.initState(); super.initState();
_link = widget.link ?? ''; _link = widget.link ?? '';
_controller = TextEditingController(text: _link); _controller = TextEditingController(text: _link);
// TODO: Consider replace the default Regex with this one
// Since that is not the reason I sent the changes then I will not edit it
// TODO: Consider use one of those as default or provide a final defaultLinkNonSecureRegExp = RegExp(
// way to custmize the check, that are not based on RegExp, r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)',
// I already implemented one so tell me if you are interested caseSensitive: false,
); // Not secure
// final defaultLinkRegExp = RegExp(
// r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)',
// caseSensitive: false,
// ); // Secure
_linkRegExp = widget.linkRegExp ?? defaultLinkNonSecureRegExp;
}
// final defaultLinkNonSecureRegExp = RegExp(r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Not secure @override
// final defaultLinkRegExp = RegExp(r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Secure void dispose() {
// _linkRegExp = widget.linkRegExp ?? defaultLinkRegExp; _controller.dispose();
_linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.linkRegExp; super.dispose();
} }
@override @override
@ -53,23 +58,29 @@ class LinkDialogState extends State<LinkDialog> {
return AlertDialog( return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor, backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: TextField( content: TextField(
keyboardType: TextInputType.multiline, keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
maxLines: null, maxLines: null,
style: widget.dialogTheme?.inputTextStyle, style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Paste a link'.i18n, labelText: 'Paste a link'.i18n,
hintText: 'Please enter a valid image url'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
), ),
autofocus: true, autofocus: true,
onChanged: _linkChanged, onChanged: _linkChanged,
controller: _controller, controller: _controller,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: _link.isNotEmpty && _linkRegExp.hasMatch(_link) onPressed: _canPress() ? _applyLink : null,
? _applyLink
: null,
child: Text( child: Text(
'Ok'.i18n, 'Ok'.i18n,
style: widget.dialogTheme?.labelTextStyle, style: widget.dialogTheme?.labelTextStyle,
@ -88,6 +99,10 @@ class LinkDialogState extends State<LinkDialog> {
void _applyLink() { void _applyLink() {
Navigator.pop(context, _link.trim()); Navigator.pop(context, _link.trim());
} }
bool _canPress() {
return _link.isNotEmpty && _linkRegExp.hasMatch(_link);
}
} }
class ImageVideoUtils { class ImageVideoUtils {
@ -179,12 +194,13 @@ class ImageVideoUtils {
/// For video picking logic /// For video picking logic
static Future<void> handleVideoButtonTap( static Future<void> handleVideoButtonTap(
BuildContext context, BuildContext context,
QuillController controller, QuillController controller,
ImageSource videoSource, ImageSource videoSource,
OnVideoPickCallback onVideoPickCallback, OnVideoPickCallback onVideoPickCallback, {
{FilePickImpl? filePickImpl, FilePickImpl? filePickImpl,
WebVideoPickImpl? webVideoPickImpl}) async { WebVideoPickImpl? webVideoPickImpl,
}) async {
final index = controller.selection.baseOffset; final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index; final length = controller.selection.extentOffset - index;

@ -9,6 +9,7 @@ import 'package:flutter_quill/translations.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../embed_types.dart'; import '../embed_types.dart';
import 'image_video_utils.dart';
/// Widget which combines [ImageButton] and [VideButton] widgets. This widget /// Widget which combines [ImageButton] and [VideButton] widgets. This widget
/// has more customization and uses dialog similar to one which is used /// has more customization and uses dialog similar to one which is used
@ -16,6 +17,11 @@ import '../embed_types.dart';
class MediaButton extends StatelessWidget { class MediaButton extends StatelessWidget {
const MediaButton({ const MediaButton({
required this.controller, required this.controller,
required this.onImagePickCallback,
required this.onVideoPickCallback,
required this.filePickImpl,
required this.webImagePickImpl,
required this.webVideoPickImpl,
required this.icon, required this.icon,
this.type = QuillMediaType.image, this.type = QuillMediaType.image,
this.iconSize = kDefaultIconSize, this.iconSize = kDefaultIconSize,
@ -73,6 +79,11 @@ class MediaButton extends StatelessWidget {
final AutovalidateMode autovalidateMode; final AutovalidateMode autovalidateMode;
final String? validationMessage; final String? validationMessage;
final OnImagePickCallback onImagePickCallback;
final FilePickImpl? filePickImpl;
final WebImagePickImpl? webImagePickImpl;
final OnVideoPickCallback onVideoPickCallback;
final WebVideoPickImpl? webVideoPickImpl;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -94,24 +105,48 @@ class MediaButton extends StatelessWidget {
} }
Future<void> _onPressedHandler(BuildContext context) async { Future<void> _onPressedHandler(BuildContext context) async {
if (onMediaPickedCallback != null) { if (onMediaPickedCallback == null) {
final mediaSource = await showDialog<MediaPickSetting>(
context: context,
builder: (_) => MediaSourceSelectorDialog(
dialogTheme: dialogTheme,
galleryButtonText: galleryButtonText,
linkButtonText: linkButtonText,
),
);
if (mediaSource != null) {
if (mediaSource == MediaPickSetting.Gallery) {
await _pickImage();
} else {
_inputLink(context);
}
}
} else {
_inputLink(context); _inputLink(context);
return;
}
final mediaSource = await showDialog<MediaPickSetting>(
context: context,
builder: (_) => MediaSourceSelectorDialog(
dialogTheme: dialogTheme,
galleryButtonText: galleryButtonText,
linkButtonText: linkButtonText,
),
);
if (mediaSource == null) {
return;
}
switch (mediaSource) {
case MediaPickSetting.Gallery:
await _pickImage();
break;
case MediaPickSetting.Link:
_inputLink(context);
break;
case MediaPickSetting.Camera:
await ImageVideoUtils.handleImageButtonTap(
context,
controller,
ImageSource.camera,
onImagePickCallback,
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
);
break;
case MediaPickSetting.Video:
await ImageVideoUtils.handleVideoButtonTap(
context,
controller,
ImageSource.camera,
onVideoPickCallback,
filePickImpl: filePickImpl,
webVideoPickImpl: webVideoPickImpl,
);
break;
} }
} }
@ -281,16 +316,18 @@ class _MediaLinkDialogState extends State<MediaLinkDialog> {
child: Padding( child: Padding(
padding: padding:
widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16),
child: isWrappable child: Form(
? Wrap( child: isWrappable
alignment: WrapAlignment.center, ? Wrap(
crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.center,
runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, crossAxisAlignment: WrapCrossAlignment.center,
children: children, runSpacing: widget.dialogTheme?.runSpacing ?? 0.0,
) children: children,
: Row( )
children: children, : Row(
), children: children,
),
),
), ),
), ),
); );

@ -9,6 +9,7 @@ import 'embeds/embed_types.dart';
import 'embeds/toolbar/camera_button.dart'; import 'embeds/toolbar/camera_button.dart';
import 'embeds/toolbar/formula_button.dart'; import 'embeds/toolbar/formula_button.dart';
import 'embeds/toolbar/image_button.dart'; import 'embeds/toolbar/image_button.dart';
import 'embeds/toolbar/media_button.dart';
import 'embeds/toolbar/video_button.dart'; import 'embeds/toolbar/video_button.dart';
export 'embeds/embed_types.dart'; export 'embeds/embed_types.dart';
@ -233,6 +234,7 @@ class FlutterQuillEmbeds {
bool showImageButton = true, bool showImageButton = true,
bool showVideoButton = true, bool showVideoButton = true,
bool showCameraButton = true, bool showCameraButton = true,
bool showImageMediaButton = false,
bool showFormulaButton = false, bool showFormulaButton = false,
String? imageButtonTooltip, String? imageButtonTooltip,
String? videoButtonTooltip, String? videoButtonTooltip,
@ -242,6 +244,7 @@ class FlutterQuillEmbeds {
OnVideoPickCallback? onVideoPickCallback, OnVideoPickCallback? onVideoPickCallback,
MediaPickSettingSelector? mediaPickSettingSelector, MediaPickSettingSelector? mediaPickSettingSelector,
MediaPickSettingSelector? cameraPickSettingSelector, MediaPickSettingSelector? cameraPickSettingSelector,
MediaPickedCallback? onImageMediaPickedCallback,
FilePickImpl? filePickImpl, FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl, WebImagePickImpl? webImagePickImpl,
WebVideoPickImpl? webVideoPickImpl, WebVideoPickImpl? webVideoPickImpl,
@ -292,6 +295,28 @@ class FlutterQuillEmbeds {
cameraPickSettingSelector: cameraPickSettingSelector, cameraPickSettingSelector: cameraPickSettingSelector,
iconTheme: iconTheme, iconTheme: iconTheme,
), ),
if (showImageMediaButton)
(controller, toolbarIconSize, iconTheme, dialogTheme) => MediaButton(
controller: controller,
dialogTheme: dialogTheme,
iconTheme: iconTheme,
iconSize: toolbarIconSize,
onMediaPickedCallback: onImageMediaPickedCallback,
onImagePickCallback: onImagePickCallback ??
(throw ArgumentError.notNull(
'onImagePickCallback is required when showCameraButton is'
' true',
)),
onVideoPickCallback: onVideoPickCallback ??
(throw ArgumentError.notNull(
'onVideoPickCallback is required when showCameraButton is'
' true',
)),
filePickImpl: filePickImpl,
webImagePickImpl: webImagePickImpl,
webVideoPickImpl: webVideoPickImpl,
icon: Icons.perm_media,
),
if (showFormulaButton) if (showFormulaButton)
(controller, toolbarIconSize, iconTheme, dialogTheme) => (controller, toolbarIconSize, iconTheme, dialogTheme) =>
FormulaButton( FormulaButton(
@ -301,6 +326,6 @@ class FlutterQuillEmbeds {
controller: controller, controller: controller,
iconTheme: iconTheme, iconTheme: iconTheme,
dialogTheme: dialogTheme, dialogTheme: dialogTheme,
) ),
]; ];
} }

@ -74,8 +74,6 @@ class QuillImageUtilities {
final newImageFileExtensionWithDot = path.extension(cachedImagePath); final newImageFileExtensionWithDot = path.extension(cachedImagePath);
final dateTimeAsString = DateTime.now().toIso8601String(); final dateTimeAsString = DateTime.now().toIso8601String();
// TODO: You might want to make it easier for the developer to change
// the newImageFileName, but he can rename it anyway
final newImageFileName = final newImageFileName =
'$startOfEachFile$dateTimeAsString$newImageFileExtensionWithDot'; '$startOfEachFile$dateTimeAsString$newImageFileExtensionWithDot';
final newImagePath = path.join(saveDirectory.path, newImageFileName); final newImagePath = path.join(saveDirectory.path, newImageFileName);

@ -12,7 +12,9 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_quill: ^7.4.13 # flutter_quill: ^7.4.14
flutter_quill:
path: /Users/ahmedhnewa/development/playground/framework_based/flutter/flutter-quill
http: ^1.1.0 http: ^1.1.0
image_picker: ">=1.0.4" image_picker: ">=1.0.4"

@ -348,7 +348,7 @@ class Line extends Container<Leaf?> {
/// In essence, it is INTERSECTION of each individual segment's styles /// In essence, it is INTERSECTION of each individual segment's styles
Style collectStyle(int offset, int len) { Style collectStyle(int offset, int len) {
final local = math.min(length - offset, len); final local = math.min(length - offset, len);
var result = Style(); var result = const Style();
final excluded = <Attribute>{}; final excluded = <Attribute>{};
void _handle(Style style) { void _handle(Style style) {

@ -22,7 +22,7 @@ abstract class Node extends LinkedListEntry<Node> {
Container? parent; Container? parent;
Style get style => _style; Style get style => _style;
Style _style = Style(); Style _style = const Style();
/// Returns `true` if this node is the first node in the [parent] list. /// Returns `true` if this node is the first node in the [parent] list.
bool get isFirst => list!.first == this; bool get isFirst => list!.first == this;
@ -78,7 +78,7 @@ abstract class Node extends LinkedListEntry<Node> {
} }
void clearStyle() { void clearStyle() {
_style = Style(); _style = const Style();
} }
@override @override

@ -1,19 +1,21 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart' show immutable;
import 'package:quiver/core.dart'; import 'package:quiver/core.dart';
import 'attribute.dart'; import 'attribute.dart';
/* Collection of style attributes */ /* Collection of style attributes */
@immutable
class Style { class Style {
Style() : _attributes = <String, Attribute>{}; const Style() : _attributes = const <String, Attribute>{};
Style.attr(this._attributes); const Style.attr(this._attributes);
final Map<String, Attribute> _attributes; final Map<String, Attribute> _attributes;
static Style fromJson(Map<String, dynamic>? attributes) { static Style fromJson(Map<String, dynamic>? attributes) {
if (attributes == null) { if (attributes == null) {
return Style(); return const Style();
} }
final result = attributes.map((key, dynamic value) { final result = attributes.map((key, dynamic value) {

@ -1,5 +1,7 @@
import 'package:i18n_extension/i18n_extension.dart'; import 'package:i18n_extension/i18n_extension.dart';
// TODO: The translation need to be changed and re-reviewd
extension Localization on String { extension Localization on String {
static final _t = Translations.byLocale('en') + static final _t = Translations.byLocale('en') +
{ {
@ -70,6 +72,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'en_us': { 'en_us': {
'Paste a link': 'Paste a link', 'Paste a link': 'Paste a link',
@ -138,6 +143,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'ar': { 'ar': {
'Paste a link': 'نسخ الرابط', 'Paste a link': 'نسخ الرابط',
@ -210,6 +218,9 @@ extension Localization on String {
'Saved using the local storage': 'Saved using the local storage':
'تم الحفظ باستخدام وحدة التخزين المحلية', 'تم الحفظ باستخدام وحدة التخزين المحلية',
'Error while saving image': 'حدث خطأ أثناء حفظ الصورة', 'Error while saving image': 'حدث خطأ أثناء حفظ الصورة',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'da': { 'da': {
'Paste a link': 'Indsæt link', 'Paste a link': 'Indsæt link',
@ -275,6 +286,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'de': { 'de': {
'Paste a link': 'Link hinzufügen', 'Paste a link': 'Link hinzufügen',
@ -340,6 +354,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'fr': { 'fr': {
'Paste a link': 'Coller un lien', 'Paste a link': 'Coller un lien',
@ -405,6 +422,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'zh_cn': { 'zh_cn': {
'Paste a link': '粘贴链接', 'Paste a link': '粘贴链接',
@ -470,6 +490,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'zh_hk': { 'zh_hk': {
'Paste a link': '貼上連結', 'Paste a link': '貼上連結',
@ -535,6 +558,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'ja': { 'ja': {
'Paste a link': 'リンクをペースト', 'Paste a link': 'リンクをペースト',
@ -600,6 +626,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'ko': { 'ko': {
'Paste a link': '링크를 붙여넣어 주세요.', 'Paste a link': '링크를 붙여넣어 주세요.',
@ -665,6 +694,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'ru': { 'ru': {
'Paste a link': 'Вставить ссылку', 'Paste a link': 'Вставить ссылку',
@ -730,6 +762,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'es': { 'es': {
'Paste a link': 'Pega un enlace', 'Paste a link': 'Pega un enlace',
@ -795,6 +830,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'tr': { 'tr': {
'Paste a link': 'Bağlantıyı Yapıştır', 'Paste a link': 'Bağlantıyı Yapıştır',
@ -860,6 +898,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'uk': { 'uk': {
'Paste a link': 'Вставити посилання', 'Paste a link': 'Вставити посилання',
@ -925,6 +966,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'pt': { 'pt': {
'Paste a link': 'Colar um link', 'Paste a link': 'Colar um link',
@ -991,6 +1035,9 @@ extension Localization on String {
'Saved using the local storage': 'Saved using the local storage':
'Guardado através do armazenamento local', 'Guardado através do armazenamento local',
'Error while saving image': 'Erro a gravar imagem', 'Error while saving image': 'Erro a gravar imagem',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'pt_br': { 'pt_br': {
'Paste a link': 'Colar um link', 'Paste a link': 'Colar um link',
@ -1056,6 +1103,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'pl': { 'pl': {
'Paste a link': 'Wklej link', 'Paste a link': 'Wklej link',
@ -1121,6 +1171,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'vi': { 'vi': {
'Paste a link': 'Chèn liên kết', 'Paste a link': 'Chèn liên kết',
@ -1186,6 +1239,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'ur': { 'ur': {
'Paste a link': 'لنک پیسٹ کریں', 'Paste a link': 'لنک پیسٹ کریں',
@ -1251,6 +1307,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'id': { 'id': {
'Paste a link': 'Tempel tautan', 'Paste a link': 'Tempel tautan',
@ -1316,6 +1375,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'no': { 'no': {
'Paste a link': 'Lim inn lenke', 'Paste a link': 'Lim inn lenke',
@ -1381,6 +1443,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'fa': { 'fa': {
'Paste a link': 'جایگذاری لینک', 'Paste a link': 'جایگذاری لینک',
@ -1446,6 +1511,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'hi': { 'hi': {
'Paste a link': 'िक पट कर', 'Paste a link': 'िक पट कर',
@ -1511,6 +1579,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'nl': { 'nl': {
'Paste a link': 'Plak een link', 'Paste a link': 'Plak een link',
@ -1576,6 +1647,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'sr': { 'sr': {
'Paste a link': 'Nalepi vezu', 'Paste a link': 'Nalepi vezu',
@ -1641,6 +1715,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'cs': { 'cs': {
'Paste a link': 'Vložit odkaz', 'Paste a link': 'Vložit odkaz',
@ -1709,6 +1786,9 @@ extension Localization on String {
'Saved using the network': 'Uloženo pomocí sítě', 'Saved using the network': 'Uloženo pomocí sítě',
'Saved using local storage': 'Uloženo lokálně', 'Saved using local storage': 'Uloženo lokálně',
'Error while saving image': 'Chyba při ukládání obrázku', 'Error while saving image': 'Chyba při ukládání obrázku',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'he': { 'he': {
'Paste a link': 'הדבק את הלינק', 'Paste a link': 'הדבק את הלינק',
@ -1774,6 +1854,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'ms': { 'ms': {
'Paste a link': 'Tampal Pautan', 'Paste a link': 'Tampal Pautan',
@ -1839,6 +1922,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'it': { 'it': {
'Paste a link': 'Incolla un collegamento', 'Paste a link': 'Incolla un collegamento',
@ -1904,6 +1990,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'bn': { 'bn': {
'Paste a link': 'িক পট কর', 'Paste a link': 'িক পট কর',
@ -1972,6 +2061,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'tk': { 'tk': {
'Paste a link': 'Baglanyşygy goýuň', 'Paste a link': 'Baglanyşygy goýuň',
@ -2040,6 +2132,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'bg': { 'bg': {
'Paste a link': 'Поставете връзка', 'Paste a link': 'Поставете връзка',
@ -2108,6 +2203,9 @@ extension Localization on String {
'Saved using the network': 'Saved using the network', 'Saved using the network': 'Saved using the network',
'Saved using the local storage': 'Saved using the local storage', 'Saved using the local storage': 'Saved using the local storage',
'Error while saving image': 'Error while saving image', 'Error while saving image': 'Error while saving image',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
'sw': { 'sw': {
'Paste a link': 'Bandika Kiungo', 'Paste a link': 'Bandika Kiungo',
@ -2176,6 +2274,9 @@ extension Localization on String {
'Saved using the network': 'Imehifadhiwa kwa Kutumia Mtandao', 'Saved using the network': 'Imehifadhiwa kwa Kutumia Mtandao',
'Saved using the local storage': 'Imehifadhiwa kwa Hifadhi ya Ndani', 'Saved using the local storage': 'Imehifadhiwa kwa Hifadhi ya Ndani',
'Error while saving image': 'Hitilafu Wakati wa Kuhifadhi Picha', 'Error while saving image': 'Hitilafu Wakati wa Kuhifadhi Picha',
'Please enter a text for your link': "e.g., 'Learn more)",
'Please enter the link url': "e.g., 'https://example.com'",
'Please enter a valid image url': 'Please enter a valid image url'
}, },
}; };

@ -29,6 +29,12 @@ bool isAppleOS([TargetPlatform? targetPlatform]) {
}.contains(targetPlatform); }.contains(targetPlatform);
} }
bool isMacOS([TargetPlatform? targetPlatform]) {
if (kIsWeb) return false;
targetPlatform ??= defaultTargetPlatform;
return TargetPlatform.macOS == targetPlatform;
}
Future<bool> isIOSSimulator() async { Future<bool> isIOSSimulator() async {
if (!isAppleOS()) { if (!isAppleOS()) {
return false; return false;

@ -72,7 +72,7 @@ class QuillController extends ChangeNotifier {
/// Store any styles attribute that got toggled by the tap of a button /// Store any styles attribute that got toggled by the tap of a button
/// and that has not been applied yet. /// and that has not been applied yet.
/// It gets reset after each format action within the [document]. /// It gets reset after each format action within the [document].
Style toggledStyle = Style(); Style toggledStyle = const Style();
bool ignoreFocusOnTextChange = false; bool ignoreFocusOnTextChange = false;
@ -405,7 +405,7 @@ class QuillController extends ChangeNotifier {
); );
toggledStyle = style.removeAll(ignoredStyles.toSet()); toggledStyle = style.removeAll(ignoredStyles.toSet());
} else { } else {
toggledStyle = Style(); toggledStyle = const Style();
} }
onSelectionChanged?.call(textSelection); onSelectionChanged?.call(textSelection);
} }
@ -426,5 +426,5 @@ class QuillController extends ChangeNotifier {
} }
// Notify toolbar buttons directly with attributes // Notify toolbar buttons directly with attributes
Map<String, Attribute> toolbarButtonToggler = {}; Map<String, Attribute> toolbarButtonToggler = const {};
} }

@ -34,8 +34,9 @@ class QuillStyles extends InheritedWidget {
/// Style theme applied to a block of rich text, including single-line /// Style theme applied to a block of rich text, including single-line
/// paragraphs. /// paragraphs.
@immutable
class DefaultTextBlockStyle { class DefaultTextBlockStyle {
DefaultTextBlockStyle( const DefaultTextBlockStyle(
this.style, this.style,
this.verticalSpacing, this.verticalSpacing,
this.lineSpacing, this.lineSpacing,
@ -124,8 +125,9 @@ class InlineCodeStyle {
Object.hash(style, header1, header2, header3, backgroundColor, radius); Object.hash(style, header1, header2, header3, backgroundColor, radius);
} }
@immutable
class DefaultListBlockStyle extends DefaultTextBlockStyle { class DefaultListBlockStyle extends DefaultTextBlockStyle {
DefaultListBlockStyle( const DefaultListBlockStyle(
TextStyle style, TextStyle style,
VerticalSpacing verticalSpacing, VerticalSpacing verticalSpacing,
VerticalSpacing lineSpacing, VerticalSpacing lineSpacing,
@ -136,8 +138,9 @@ class DefaultListBlockStyle extends DefaultTextBlockStyle {
final QuillCheckboxBuilder? checkboxUIBuilder; final QuillCheckboxBuilder? checkboxUIBuilder;
} }
@immutable
class DefaultStyles { class DefaultStyles {
DefaultStyles({ const DefaultStyles({
this.h1, this.h1,
this.h2, this.h2,
this.h3, this.h3,

@ -156,6 +156,7 @@ class QuillEditor extends StatefulWidget {
required this.autoFocus, required this.autoFocus,
required this.readOnly, required this.readOnly,
required this.expands, required this.expands,
this.textSelectionThemeData,
this.showCursor, this.showCursor,
this.paintCursorAboveText, this.paintCursorAboveText,
this.placeholder, this.placeholder,
@ -199,6 +200,7 @@ class QuillEditor extends StatefulWidget {
factory QuillEditor.basic({ factory QuillEditor.basic({
required QuillController controller, required QuillController controller,
required bool readOnly, required bool readOnly,
TextSelectionThemeData? textSelectionThemeData,
Brightness? keyboardAppearance, Brightness? keyboardAppearance,
Iterable<EmbedBuilder>? embedBuilders, Iterable<EmbedBuilder>? embedBuilders,
EdgeInsetsGeometry padding = EdgeInsets.zero, EdgeInsetsGeometry padding = EdgeInsets.zero,
@ -217,6 +219,7 @@ class QuillEditor extends StatefulWidget {
scrollController: ScrollController(), scrollController: ScrollController(),
scrollable: true, scrollable: true,
focusNode: focusNode ?? FocusNode(), focusNode: focusNode ?? FocusNode(),
textSelectionThemeData: textSelectionThemeData,
autoFocus: autoFocus, autoFocus: autoFocus,
readOnly: readOnly, readOnly: readOnly,
expands: expands, expands: expands,
@ -455,6 +458,13 @@ class QuillEditor extends StatefulWidget {
/// editorKey.currentState?.renderEditor.getLocalRectForCaret /// editorKey.currentState?.renderEditor.getLocalRectForCaret
final GlobalKey<EditorState>? editorKey; final GlobalKey<EditorState>? editorKey;
/// By default we will use
/// ```
/// TextSelectionTheme.of(context)
/// ```
/// to change it please pass a different value
final TextSelectionThemeData? textSelectionThemeData;
@override @override
QuillEditorState createState() => QuillEditorState(); QuillEditorState createState() => QuillEditorState();
} }
@ -477,7 +487,8 @@ class QuillEditorState extends State<QuillEditor>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final selectionTheme = TextSelectionTheme.of(context); final selectionTheme =
widget.textSelectionThemeData ?? TextSelectionTheme.of(context);
TextSelectionControls textSelectionControls; TextSelectionControls textSelectionControls;
bool paintCursorAboveText; bool paintCursorAboveText;

@ -535,7 +535,11 @@ class RawEditorState extends EditorState
minHeight: widget.minHeight ?? 0.0, minHeight: widget.minHeight ?? 0.0,
maxHeight: widget.maxHeight ?? double.infinity); maxHeight: widget.maxHeight ?? double.infinity);
final isMacOS = Theme.of(context).platform == TargetPlatform.macOS; // Please notice that this change will make the check fixed
// so if we ovveride the platform in material app theme data
// it will not depend on it and doesn't change here but I don't think
// we need to
final isMacOS = isAppleOS();
return TextFieldTapRegion( return TextFieldTapRegion(
enabled: widget.enableUnfocusOnTapOutside, enabled: widget.enableUnfocusOnTapOutside,
@ -915,31 +919,36 @@ class RawEditorState extends EditorState
textDirection: getDirectionOfNode(node), child: editableTextLine)); textDirection: getDirectionOfNode(node), child: editableTextLine));
} else if (node is Block) { } else if (node is Block) {
final editableTextBlock = EditableTextBlock( final editableTextBlock = EditableTextBlock(
block: node, block: node,
controller: controller, controller: controller,
textDirection: getDirectionOfNode(node),
scrollBottomInset: widget.scrollBottomInset,
verticalSpacing: _getVerticalSpacingForBlock(node, _styles),
textSelection: controller.selection,
color: widget.selectionColor,
styles: _styles,
enableInteractiveSelection: widget.enableInteractiveSelection,
hasFocus: _hasFocus,
contentPadding: attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16)
: null,
embedBuilder: widget.embedBuilder,
linkActionPicker: _linkActionPicker,
onLaunchUrl: widget.onLaunchUrl,
cursorCont: _cursorCont,
indentLevelCounts: indentLevelCounts,
clearIndents: clearIndents,
onCheckboxTap: _handleCheckboxTap,
readOnly: widget.readOnly,
customStyleBuilder: widget.customStyleBuilder,
customLinkPrefixes: widget.customLinkPrefixes,
);
result.add(
Directionality(
textDirection: getDirectionOfNode(node), textDirection: getDirectionOfNode(node),
scrollBottomInset: widget.scrollBottomInset, child: editableTextBlock,
verticalSpacing: _getVerticalSpacingForBlock(node, _styles), ),
textSelection: controller.selection, );
color: widget.selectionColor,
styles: _styles,
enableInteractiveSelection: widget.enableInteractiveSelection,
hasFocus: _hasFocus,
contentPadding: attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16)
: null,
embedBuilder: widget.embedBuilder,
linkActionPicker: _linkActionPicker,
onLaunchUrl: widget.onLaunchUrl,
cursorCont: _cursorCont,
indentLevelCounts: indentLevelCounts,
clearIndents: clearIndents,
onCheckboxTap: _handleCheckboxTap,
readOnly: widget.readOnly,
customStyleBuilder: widget.customStyleBuilder,
customLinkPrefixes: widget.customLinkPrefixes);
result.add(Directionality(
textDirection: getDirectionOfNode(node), child: editableTextBlock));
clearIndents = false; clearIndents = false;
} else { } else {
@ -1111,9 +1120,15 @@ class RawEditorState extends EditorState
_styles = _styles!.merge(widget.customStyles!); _styles = _styles!.merge(widget.customStyles!);
} }
// TODO: this might need some attention
_requestFocusIfShould();
}
Future<void> _requestFocusIfShould() async {
if (!_didAutoFocus && widget.autoFocus) { if (!_didAutoFocus && widget.autoFocus) {
FocusScope.of(context).autofocus(widget.focusNode);
_didAutoFocus = true; _didAutoFocus = true;
await Future.delayed(Duration.zero);
FocusScope.of(context).autofocus(widget.focusNode);
} }
} }
@ -2559,8 +2574,13 @@ class _OpenSearchAction extends ContextAction<OpenSearchIntent> {
@override @override
Future invoke(OpenSearchIntent intent, [BuildContext? context]) async { Future invoke(OpenSearchIntent intent, [BuildContext? context]) async {
if (context == null) {
throw ArgumentError(
'The context should not be null to use invoke() method',
);
}
await showDialog<String>( await showDialog<String>(
context: context!, context: context,
builder: (_) => SearchDialog(controller: state.controller, text: ''), builder: (_) => SearchDialog(controller: state.controller, text: ''),
); );
} }

@ -104,14 +104,19 @@ class EditableTextBlock extends StatelessWidget {
final defaultStyles = QuillStyles.getStyles(context, false); final defaultStyles = QuillStyles.getStyles(context, false);
return _EditableBlock( return _EditableBlock(
block: block, block: block,
textDirection: textDirection, textDirection: textDirection,
padding: verticalSpacing, padding: verticalSpacing,
scrollBottomInset: scrollBottomInset, scrollBottomInset: scrollBottomInset,
decoration: _getDecorationForBlock(block, defaultStyles) ?? decoration:
const BoxDecoration(), _getDecorationForBlock(block, defaultStyles) ?? const BoxDecoration(),
contentPadding: contentPadding, contentPadding: contentPadding,
children: _buildChildren(context, indentLevelCounts, clearIndents)); children: _buildChildren(
context,
indentLevelCounts,
clearIndents,
),
);
} }
BoxDecoration? _getDecorationForBlock( BoxDecoration? _getDecorationForBlock(
@ -138,32 +143,37 @@ class EditableTextBlock extends StatelessWidget {
for (final line in Iterable.castFrom<dynamic, Line>(block.children)) { for (final line in Iterable.castFrom<dynamic, Line>(block.children)) {
index++; index++;
final editableTextLine = EditableTextLine( final editableTextLine = EditableTextLine(
line, line,
_buildLeading(context, line, index, indentLevelCounts, count), _buildLeading(context, line, index, indentLevelCounts, count),
TextLine( TextLine(
line: line, line: line,
textDirection: textDirection, textDirection: textDirection,
embedBuilder: embedBuilder, embedBuilder: embedBuilder,
customStyleBuilder: customStyleBuilder, customStyleBuilder: customStyleBuilder,
styles: styles!, styles: styles!,
readOnly: readOnly, readOnly: readOnly,
controller: controller, controller: controller,
linkActionPicker: linkActionPicker, linkActionPicker: linkActionPicker,
onLaunchUrl: onLaunchUrl, onLaunchUrl: onLaunchUrl,
customLinkPrefixes: customLinkPrefixes, customLinkPrefixes: customLinkPrefixes,
), ),
_getIndentWidth(context, count), _getIndentWidth(context, count),
_getSpacingForLine(line, index, count, defaultStyles), _getSpacingForLine(line, index, count, defaultStyles),
textDirection, textDirection,
textSelection, textSelection,
color, color,
enableInteractiveSelection, enableInteractiveSelection,
hasFocus, hasFocus,
MediaQuery.devicePixelRatioOf(context), MediaQuery.devicePixelRatioOf(context),
cursorCont); cursorCont,
);
final nodeTextDirection = getDirectionOfNode(line); final nodeTextDirection = getDirectionOfNode(line);
children.add(Directionality( children.add(
textDirection: nodeTextDirection, child: editableTextLine)); Directionality(
textDirection: nodeTextDirection,
child: editableTextLine,
),
);
} }
return children.toList(growable: false); return children.toList(growable: false);
} }

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../models/documents/attribute.dart'; import '../../models/documents/attribute.dart';
import '../../models/rules/insert.dart'; import '../../models/rules/insert.dart';
@ -189,46 +190,77 @@ class _LinkDialogState extends State<_LinkDialog> {
_textController = TextEditingController(text: _text); _textController = TextEditingController(text: _text);
} }
@override
void dispose() {
_linkController.dispose();
_textController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor, backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: Column( content: Form(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
const SizedBox(height: 8), children: [
TextField( const SizedBox(height: 8),
keyboardType: TextInputType.multiline, TextFormField(
style: widget.dialogTheme?.inputTextStyle, keyboardType: TextInputType.text,
decoration: InputDecoration( style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: 'Text'.i18n, labelText: 'Text'.i18n,
hintText: 'Please enter a text for your link'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle), floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
autofocus: true, ),
onChanged: _textChanged, autofocus: true,
controller: _textController, onChanged: _textChanged,
), controller: _textController,
const SizedBox(height: 16), textInputAction: TextInputAction.next,
TextField( autofillHints: [
keyboardType: TextInputType.multiline, AutofillHints.name,
style: widget.dialogTheme?.inputTextStyle, AutofillHints.url,
decoration: InputDecoration( ],
),
const SizedBox(height: 16),
TextFormField(
keyboardType: TextInputType.url,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: 'Link'.i18n, labelText: 'Link'.i18n,
hintText: 'Please enter the link url'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle), floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
autofocus: true, ),
onChanged: _linkChanged, onChanged: _linkChanged,
controller: _linkController, controller: _linkController,
), textInputAction: TextInputAction.done,
], autofillHints: [AutofillHints.url],
autocorrect: false,
onEditingComplete: () {
if (!_canPress()) {
return;
}
_applyLink();
},
),
],
),
), ),
actions: [_okButton()], actions: [
_okButton(),
],
); );
} }
Widget _okButton() { Widget _okButton() {
if (widget.action != null) { if (widget.action != null) {
return widget.action!.builder(_canPress(), _applyLink); return widget.action!.builder(
_canPress(),
_applyLink,
);
} }
return TextButton( return TextButton(

@ -35,7 +35,8 @@ class QuillIconButton extends StatelessWidget {
child: RawMaterialButton( child: RawMaterialButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius)), borderRadius: BorderRadius.circular(borderRadius),
),
fillColor: fillColor, fillColor: fillColor,
elevation: 0, elevation: 0,
hoverElevation: hoverElevation, hoverElevation: hoverElevation,

@ -6,9 +6,12 @@ import '../../models/themes/quill_dialog_theme.dart';
import '../controller.dart'; import '../controller.dart';
class SearchDialog extends StatefulWidget { class SearchDialog extends StatefulWidget {
const SearchDialog( const SearchDialog({
{required this.controller, this.dialogTheme, this.text, Key? key}) required this.controller,
: super(key: key); this.dialogTheme,
this.text,
Key? key,
}) : super(key: key);
final QuillController controller; final QuillController controller;
final QuillDialogTheme? dialogTheme; final QuillDialogTheme? dialogTheme;
@ -35,6 +38,12 @@ class _SearchDialogState extends State<SearchDialog> {
_controller = TextEditingController(text: _text); _controller = TextEditingController(text: _text);
} }
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var matchShown = ''; var matchShown = '';
@ -100,6 +109,7 @@ class _SearchDialogState extends State<SearchDialog> {
autofocus: true, autofocus: true,
onChanged: _textChanged, onChanged: _textChanged,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
keyboardType: TextInputType.text,
onEditingComplete: _findText, onEditingComplete: _findText,
controller: _controller, controller: _controller,
), ),

@ -89,8 +89,12 @@ void main() {
..indentSelection(true); ..indentSelection(true);
// Should have both L1 and L2 indent attributes in selection. // Should have both L1 and L2 indent attributes in selection.
expect(controller.getAllSelectionStyles(), expect(
contains(Style().put(Attribute.indentL1).put(Attribute.indentL2))); controller.getAllSelectionStyles(),
contains(
const Style().put(Attribute.indentL1).put(Attribute.indentL2),
),
);
// Remaining lines should have no attributes. // Remaining lines should have no attributes.
controller.updateSelection( controller.updateSelection(
@ -98,7 +102,7 @@ void main() {
baseOffset: 12, baseOffset: 12,
extentOffset: controller.document.toPlainText().length - 1), extentOffset: controller.document.toPlainText().length - 1),
ChangeSource.LOCAL); ChangeSource.LOCAL);
expect(controller.getAllSelectionStyles(), everyElement(Style())); expect(controller.getAllSelectionStyles(), everyElement(const Style()));
}); });
test('getAllIndividualSelectionStylesAndEmbed', () { test('getAllIndividualSelectionStylesAndEmbed', () {
@ -110,7 +114,7 @@ void main() {
final result = controller.getAllIndividualSelectionStylesAndEmbed(); final result = controller.getAllIndividualSelectionStylesAndEmbed();
expect(result.length, 2); expect(result.length, 2);
expect(result[0].offset, 0); expect(result[0].offset, 0);
expect(result[0].value, Style().put(Attribute.bold)); expect(result[0].value, const Style().put(Attribute.bold));
expect((result[1].value as Embeddable).type, BlockEmbed.imageType); expect((result[1].value as Embeddable).type, BlockEmbed.imageType);
}); });
@ -125,7 +129,7 @@ void main() {
test('getAllSelectionStyles', () { test('getAllSelectionStyles', () {
controller.formatText(0, 2, Attribute.bold); controller.formatText(0, 2, Attribute.bold);
expect(controller.getAllSelectionStyles(), expect(controller.getAllSelectionStyles(),
contains(Style().put(Attribute.bold))); contains(const Style().put(Attribute.bold)));
}); });
test('undo', () { test('undo', () {
@ -133,7 +137,10 @@ void main() {
controller.updateSelection( controller.updateSelection(
const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL);
expect(controller.document.toDelta(), Delta()..insert('data\n')); expect(
controller.document.toDelta(),
Delta()..insert('data\n'),
);
controller controller
..addListener(() { ..addListener(() {
listenerCalled = true; listenerCalled = true;
@ -185,7 +192,7 @@ void main() {
test('formatTextStyle', () { test('formatTextStyle', () {
var listenerCalled = false; var listenerCalled = false;
final style = Style().put(Attribute.bold).put(Attribute.italic); final style = const Style().put(Attribute.bold).put(Attribute.italic);
controller controller
..addListener(() { ..addListener(() {
listenerCalled = true; listenerCalled = true;
@ -193,7 +200,8 @@ void main() {
..formatTextStyle(0, 2, style); ..formatTextStyle(0, 2, style);
expect(listenerCalled, isTrue); expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2), contains(style)); expect(controller.document.collectAllStyles(0, 2), contains(style));
expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); expect(controller.document.collectAllStyles(2, 4),
everyElement(const Style()));
}); });
test('formatText', () { test('formatText', () {
@ -205,8 +213,9 @@ void main() {
..formatText(0, 2, Attribute.bold); ..formatText(0, 2, Attribute.bold);
expect(listenerCalled, isTrue); expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2), expect(controller.document.collectAllStyles(0, 2),
contains(Style().put(Attribute.bold))); contains(const Style().put(Attribute.bold)));
expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); expect(controller.document.collectAllStyles(2, 4),
everyElement(const Style()));
}); });
test('formatSelection', () { test('formatSelection', () {
@ -220,8 +229,9 @@ void main() {
..formatSelection(Attribute.bold); ..formatSelection(Attribute.bold);
expect(listenerCalled, isTrue); expect(listenerCalled, isTrue);
expect(controller.document.collectAllStyles(0, 2), expect(controller.document.collectAllStyles(0, 2),
contains(Style().put(Attribute.bold))); contains(const Style().put(Attribute.bold)));
expect(controller.document.collectAllStyles(2, 4), everyElement(Style())); expect(controller.document.collectAllStyles(2, 4),
everyElement(const Style()));
}); });
test('moveCursorToStart', () { test('moveCursorToStart', () {

Loading…
Cancel
Save