diff --git a/CHANGELOG.md b/CHANGELOG.md
index 323da506..bfc8284b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## [1.3.1]
+* New logo.
+
## [1.3.0]
* Support flutter 2.2.0.
diff --git a/README.md b/README.md
index b7c1a247..bbedb500 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,35 @@
+
+
+
+A rich text editor for Flutter
+
+[![MIT License][license-badge]][license-link]
+[![PRs Welcome][prs-badge]][prs-link]
+[![Watch on GitHub][github-watch-badge]][github-watch-link]
+[![Star on GitHub][github-star-badge]][github-star-link]
+[![Watch on GitHub][github-forks-badge]][github-forks-link]
+
+[license-badge]: https://img.shields.io/github/license/singerdmx/flutter-quill.svg?style=for-the-badge
+[license-link]: https://github.com/singerdmx/flutter-quill/blob/master/LICENSE
+[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
+[prs-link]: https://github.com/singerdmx/flutter-quill/issues
+[github-watch-badge]: https://img.shields.io/github/watchers/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff
+[github-watch-link]: https://github.com/singerdmx/flutter-quill/watchers
+[github-star-badge]: https://img.shields.io/github/stars/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff
+[github-star-link]: https://github.com/singerdmx/flutter-quill/stargazers
+[github-forks-badge]: https://img.shields.io/github/forks/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff
+[github-forks-link]: https://github.com/singerdmx/flutter-quill/network/members
+
+
+FlutterQuill is a rich text editor and a [Quill] component for [Flutter].
-
-
-
-# FlutterQuill
-
-FlutterQuill is a rich text editor and a [Quill] component for [Flutter].
-
This library is a WYSIWYG editor built for the modern mobile platform, with web compatibility under development. You can join our [Slack Group] for discussion.
Demo App: https://bulletjournal.us/home/index.html
Pub: https://pub.dev/packages/flutter_quill
-## Usage
+## Usage
See the `example` directory for a minimal example of how to use FlutterQuill. You typically just need to instantiate a controller:
@@ -79,17 +94,30 @@ It is required to provide EmbedBuilder, e.g. [defaultEmbedBuilderWeb](https://gi
## Migrate Zefyr Data
Check out [code](https://github.com/jwehrle/zefyr_quill_convert) and [doc](https://docs.google.com/document/d/1FUSrpbarHnilb7uDN5J5DDahaI0v1RMXBjj4fFSpSuY/edit?usp=sharing).
-
----
-
-
-
-
-
-
+
+---
+
+
+
+
+
+
+
+
+
+
+
+
+## Sponsors
+
+
+
+
[Quill]: https://quilljs.com/docs/formats
-[Flutter]: https://github.com/flutter/flutter
-[FlutterQuill]: https://pub.dev/packages/flutter_quill
-[ReactQuill]: https://github.com/zenoamaro/react-quill
+[Flutter]: https://github.com/flutter/flutter
+[FlutterQuill]: https://pub.dev/packages/flutter_quill
+[ReactQuill]: https://github.com/zenoamaro/react-quill
[Slack Group]: https://join.slack.com/t/bulletjournal1024/shared_invite/zt-fys7t9hi-ITVU5PGDen1rNRyCjdcQ2g
[Sample Page]: https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart
diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart
index f50be23f..60211ab2 100644
--- a/lib/src/models/rules/insert.dart
+++ b/lib/src/models/rules/insert.dart
@@ -188,8 +188,8 @@ class AutoExitBlockRule extends InsertRule {
// Here we now know that the line after `cur` is not in the same block
// therefore we can exit this block.
final attributes = cur.attributes ?? {};
- final k = attributes.keys
- .firstWhere(Attribute.blockKeysExceptHeader.contains);
+ final k =
+ attributes.keys.firstWhere(Attribute.blockKeysExceptHeader.contains);
attributes[k] = null;
// retain(1) should be '\n', set it with no attribute
return Delta()..retain(index + (len ?? 0))..retain(1, attributes);
diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart
index 9edf805b..7de1a122 100644
--- a/lib/src/widgets/controller.dart
+++ b/lib/src/widgets/controller.dart
@@ -11,11 +11,10 @@ import '../models/quill_delta.dart';
import '../utils/diff_delta.dart';
class QuillController extends ChangeNotifier {
- QuillController(
- {required this.document,
- required this.selection,
- this.iconSize = 18,
- this.toolbarHeightFactor = 2});
+ QuillController({
+ required this.document,
+ required TextSelection selection,
+ }) : _selection = selection;
factory QuillController.basic() {
return QuillController(
@@ -24,19 +23,24 @@ class QuillController extends ChangeNotifier {
);
}
+ /// Document managed by this controller.
final Document document;
- TextSelection selection;
- double iconSize;
- double toolbarHeightFactor;
+ /// Currently selected text within the [document].
+ TextSelection get selection => _selection;
+ TextSelection _selection;
+
+ /// Store any styles attribute that got toggled by the tap of a button
+ /// and that has not been applied yet.
+ /// It gets reset after each format action within the [document].
Style toggledStyle = Style();
+
bool ignoreFocusOnTextChange = false;
- /// Controls whether this [QuillController] instance has already been disposed
- /// of
+ /// True when this [QuillController] instance has been disposed.
///
- /// This is a safe approach to make sure that listeners don't crash when
- /// adding, removing or listeners to this instance.
+ /// A safety mechanism to ensure that listeners don't crash when adding,
+ /// removing or listeners to this instance.
bool _isDisposed = false;
// item1: Document state before [change].
@@ -220,9 +224,9 @@ class QuillController extends ChangeNotifier {
}
void _updateSelection(TextSelection textSelection, ChangeSource source) {
- selection = textSelection;
+ _selection = textSelection;
final end = document.length - 1;
- selection = selection.copyWith(
+ _selection = selection.copyWith(
baseOffset: math.min(selection.baseOffset, end),
extentOffset: math.min(selection.extentOffset, end));
}
diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart
index c5fe2bab..d4d00f5d 100644
--- a/lib/src/widgets/toolbar.dart
+++ b/lib/src/widgets/toolbar.dart
@@ -1,921 +1,54 @@
import 'dart:io';
-import 'package:file_picker/file_picker.dart';
-import 'package:filesystem_picker/filesystem_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:image_picker/image_picker.dart';
-import 'package:path_provider/path_provider.dart';
import '../models/documents/attribute.dart';
-import '../models/documents/nodes/embed.dart';
-import '../models/documents/style.dart';
-import '../utils/color.dart';
import 'controller.dart';
+import 'toolbar/clear_format_button.dart';
+import 'toolbar/color_button.dart';
+import 'toolbar/history_button.dart';
+import 'toolbar/image_button.dart';
+import 'toolbar/indent_button.dart';
+import 'toolbar/insert_embed_button.dart';
+import 'toolbar/link_style_button.dart';
+import 'toolbar/select_header_style_button.dart';
+import 'toolbar/toggle_check_list_button.dart';
+import 'toolbar/toggle_style_button.dart';
+
+export 'toolbar/clear_format_button.dart';
+export 'toolbar/color_button.dart';
+export 'toolbar/history_button.dart';
+export 'toolbar/image_button.dart';
+export 'toolbar/indent_button.dart';
+export 'toolbar/insert_embed_button.dart';
+export 'toolbar/link_style_button.dart';
+export 'toolbar/quill_dropdown_button.dart';
+export 'toolbar/quill_icon_button.dart';
+export 'toolbar/select_header_style_button.dart';
+export 'toolbar/toggle_check_list_button.dart';
+export 'toolbar/toggle_style_button.dart';
typedef OnImagePickCallback = Future Function(File file);
typedef ImagePickImpl = Future Function(ImageSource source);
-class InsertEmbedButton extends StatelessWidget {
- const InsertEmbedButton({
- required this.controller,
- required this.icon,
- this.fillColor,
- Key? key,
- }) : super(key: key);
-
- final QuillController controller;
- final IconData icon;
- final Color? fillColor;
-
- @override
- Widget build(BuildContext context) {
- return QuillIconButton(
- highlightElevation: 0,
- hoverElevation: 0,
- size: controller.iconSize * 1.77,
- icon: Icon(
- icon,
- size: controller.iconSize,
- color: Theme.of(context).iconTheme.color,
- ),
- fillColor: fillColor ?? Theme.of(context).canvasColor,
- onPressed: () {
- final index = controller.selection.baseOffset;
- final length = controller.selection.extentOffset - index;
- controller.replaceText(index, length, BlockEmbed.horizontalRule, null);
- },
- );
- }
-}
-
-class LinkStyleButton extends StatefulWidget {
- const LinkStyleButton({
- required this.controller,
- this.icon,
- Key? key,
- }) : super(key: key);
-
- final QuillController controller;
- final IconData? icon;
-
- @override
- _LinkStyleButtonState createState() => _LinkStyleButtonState();
-}
-
-class _LinkStyleButtonState extends State {
- void _didChangeSelection() {
- setState(() {});
- }
-
- @override
- void initState() {
- super.initState();
- widget.controller.addListener(_didChangeSelection);
- }
-
- @override
- void didUpdateWidget(covariant LinkStyleButton oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (oldWidget.controller != widget.controller) {
- oldWidget.controller.removeListener(_didChangeSelection);
- widget.controller.addListener(_didChangeSelection);
- }
- }
-
- @override
- void dispose() {
- super.dispose();
- widget.controller.removeListener(_didChangeSelection);
- }
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final isEnabled = !widget.controller.selection.isCollapsed;
- final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null;
- return QuillIconButton(
- highlightElevation: 0,
- hoverElevation: 0,
- size: widget.controller.iconSize * 1.77,
- icon: Icon(
- widget.icon ?? Icons.link,
- size: widget.controller.iconSize,
- color: isEnabled ? theme.iconTheme.color : theme.disabledColor,
- ),
- fillColor: Theme.of(context).canvasColor,
- onPressed: pressedHandler,
- );
- }
-
- void _openLinkDialog(BuildContext context) {
- showDialog(
- context: context,
- builder: (ctx) {
- return const _LinkDialog();
- },
- ).then(_linkSubmitted);
- }
-
- void _linkSubmitted(String? value) {
- if (value == null || value.isEmpty) {
- return;
- }
- widget.controller.formatSelection(LinkAttribute(value));
- }
-}
-
-class _LinkDialog extends StatefulWidget {
- const _LinkDialog({Key? key}) : super(key: key);
-
- @override
- _LinkDialogState createState() => _LinkDialogState();
-}
-
-class _LinkDialogState extends State<_LinkDialog> {
- String _link = '';
-
- @override
- Widget build(BuildContext context) {
- return AlertDialog(
- content: TextField(
- decoration: const InputDecoration(labelText: 'Paste a link'),
- autofocus: true,
- onChanged: _linkChanged,
- ),
- actions: [
- TextButton(
- onPressed: _link.isNotEmpty ? _applyLink : null,
- child: const Text('Apply'),
- ),
- ],
- );
- }
-
- void _linkChanged(String value) {
- setState(() {
- _link = value;
- });
- }
-
- void _applyLink() {
- Navigator.pop(context, _link);
- }
-}
-
-typedef ToggleStyleButtonBuilder = Widget Function(
- BuildContext context,
- Attribute attribute,
- IconData icon,
- Color? fillColor,
- bool? isToggled,
- VoidCallback? onPressed,
-);
-
-class ToggleStyleButton extends StatefulWidget {
- const ToggleStyleButton({
- required this.attribute,
- required this.icon,
- required this.controller,
- this.fillColor,
- this.childBuilder = defaultToggleStyleButtonBuilder,
- Key? key,
- }) : super(key: key);
-
- final Attribute attribute;
-
- final IconData icon;
-
- final Color? fillColor;
-
- final QuillController controller;
-
- final ToggleStyleButtonBuilder childBuilder;
-
- @override
- _ToggleStyleButtonState createState() => _ToggleStyleButtonState();
-}
-
-class _ToggleStyleButtonState extends State {
- bool? _isToggled;
-
- Style get _selectionStyle => widget.controller.getSelectionStyle();
-
- void _didChangeEditingValue() {
- setState(() {
- _isToggled =
- _getIsToggled(widget.controller.getSelectionStyle().attributes);
- });
- }
-
- @override
- void initState() {
- super.initState();
- _isToggled = _getIsToggled(_selectionStyle.attributes);
- widget.controller.addListener(_didChangeEditingValue);
- }
-
- bool _getIsToggled(Map attrs) {
- if (widget.attribute.key == Attribute.list.key) {
- final attribute = attrs[widget.attribute.key];
- if (attribute == null) {
- return false;
- }
- return attribute.value == widget.attribute.value;
- }
- return attrs.containsKey(widget.attribute.key);
- }
-
- @override
- void didUpdateWidget(covariant ToggleStyleButton oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (oldWidget.controller != widget.controller) {
- oldWidget.controller.removeListener(_didChangeEditingValue);
- widget.controller.addListener(_didChangeEditingValue);
- _isToggled = _getIsToggled(_selectionStyle.attributes);
- }
- }
-
- @override
- void dispose() {
- widget.controller.removeListener(_didChangeEditingValue);
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- final isInCodeBlock =
- _selectionStyle.attributes.containsKey(Attribute.codeBlock.key);
- final isEnabled =
- !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key;
- return widget.childBuilder(context, widget.attribute, widget.icon,
- widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null);
- }
-
- void _toggleAttribute() {
- widget.controller.formatSelection(_isToggled!
- ? Attribute.clone(widget.attribute, null)
- : widget.attribute);
- }
-}
-
-class ToggleCheckListButton extends StatefulWidget {
- const ToggleCheckListButton({
- required this.icon,
- required this.controller,
- required this.attribute,
- this.fillColor,
- this.childBuilder = defaultToggleStyleButtonBuilder,
- Key? key,
- }) : super(key: key);
-
- final IconData icon;
-
- final Color? fillColor;
-
- final QuillController controller;
-
- final ToggleStyleButtonBuilder childBuilder;
-
- final Attribute attribute;
-
- @override
- _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState();
-}
-
-class _ToggleCheckListButtonState extends State {
- bool? _isToggled;
-
- Style get _selectionStyle => widget.controller.getSelectionStyle();
-
- void _didChangeEditingValue() {
- setState(() {
- _isToggled =
- _getIsToggled(widget.controller.getSelectionStyle().attributes);
- });
- }
-
- @override
- void initState() {
- super.initState();
- _isToggled = _getIsToggled(_selectionStyle.attributes);
- widget.controller.addListener(_didChangeEditingValue);
- }
-
- bool _getIsToggled(Map attrs) {
- if (widget.attribute.key == Attribute.list.key) {
- final attribute = attrs[widget.attribute.key];
- if (attribute == null) {
- return false;
- }
- return attribute.value == widget.attribute.value ||
- attribute.value == Attribute.checked.value;
- }
- return attrs.containsKey(widget.attribute.key);
- }
-
- @override
- void didUpdateWidget(covariant ToggleCheckListButton oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (oldWidget.controller != widget.controller) {
- oldWidget.controller.removeListener(_didChangeEditingValue);
- widget.controller.addListener(_didChangeEditingValue);
- _isToggled = _getIsToggled(_selectionStyle.attributes);
- }
- }
-
- @override
- void dispose() {
- widget.controller.removeListener(_didChangeEditingValue);
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- final isInCodeBlock =
- _selectionStyle.attributes.containsKey(Attribute.codeBlock.key);
- final isEnabled =
- !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key;
- return widget.childBuilder(context, Attribute.unchecked, widget.icon,
- widget.fillColor, _isToggled, isEnabled ? _toggleAttribute : null);
- }
-
- void _toggleAttribute() {
- widget.controller.formatSelection(_isToggled!
- ? Attribute.clone(Attribute.unchecked, null)
- : Attribute.unchecked);
- }
-}
-
-Widget defaultToggleStyleButtonBuilder(
- BuildContext context,
- Attribute attribute,
- IconData icon,
- Color? fillColor,
- bool? isToggled,
- VoidCallback? onPressed,
-) {
- final theme = Theme.of(context);
- final isEnabled = onPressed != null;
- final iconColor = isEnabled
- ? isToggled == true
- ? theme.primaryIconTheme.color
- : theme.iconTheme.color
- : theme.disabledColor;
- final fill = isToggled == true
- ? theme.toggleableActiveColor
- : fillColor ?? theme.canvasColor;
- return QuillIconButton(
- highlightElevation: 0,
- hoverElevation: 0,
- size: 18 * 1.77,
- icon: Icon(icon, size: 18, color: iconColor),
- fillColor: fill,
- onPressed: onPressed,
- );
-}
-
-class SelectHeaderStyleButton extends StatefulWidget {
- const SelectHeaderStyleButton({required this.controller, Key? key})
- : super(key: key);
-
- final QuillController controller;
-
- @override
- _SelectHeaderStyleButtonState createState() =>
- _SelectHeaderStyleButtonState();
-}
-
-class _SelectHeaderStyleButtonState extends State {
- Attribute? _value;
-
- Style get _selectionStyle => widget.controller.getSelectionStyle();
-
- void _didChangeEditingValue() {
- setState(() {
- _value =
- _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
- });
- }
-
- void _selectAttribute(value) {
- widget.controller.formatSelection(value);
- }
-
- @override
- void initState() {
- super.initState();
- setState(() {
- _value =
- _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
- });
- widget.controller.addListener(_didChangeEditingValue);
- }
-
- @override
- void didUpdateWidget(covariant SelectHeaderStyleButton oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (oldWidget.controller != widget.controller) {
- oldWidget.controller.removeListener(_didChangeEditingValue);
- widget.controller.addListener(_didChangeEditingValue);
- _value =
- _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
- }
- }
-
- @override
- void dispose() {
- widget.controller.removeListener(_didChangeEditingValue);
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return _selectHeadingStyleButtonBuilder(
- context, _value, _selectAttribute, widget.controller.iconSize);
- }
-}
-
-Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value,
- ValueChanged onSelected, double iconSize) {
- final _valueToText = {
- Attribute.header: 'N',
- Attribute.h1: 'H1',
- Attribute.h2: 'H2',
- Attribute.h3: 'H3',
- };
-
- final _valueAttribute = [
- Attribute.header,
- Attribute.h1,
- Attribute.h2,
- Attribute.h3
- ];
- final _valueString = ['N', 'H1', 'H2', 'H3'];
-
- final theme = Theme.of(context);
- final style = TextStyle(
- fontWeight: FontWeight.w600,
- fontSize: iconSize * 0.7,
- );
-
- return Row(
- mainAxisSize: MainAxisSize.min,
- children: List.generate(4, (index) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0),
- child: ConstrainedBox(
- constraints: BoxConstraints.tightFor(
- width: iconSize * 1.77,
- height: iconSize * 1.77,
- ),
- child: RawMaterialButton(
- hoverElevation: 0,
- highlightElevation: 0,
- elevation: 0,
- visualDensity: VisualDensity.compact,
- shape:
- RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
- fillColor: _valueToText[value] == _valueString[index]
- ? theme.toggleableActiveColor
- : theme.canvasColor,
- onPressed: () {
- onSelected(_valueAttribute[index]);
- },
- child: Text(
- _valueString[index],
- style: style.copyWith(
- color: _valueToText[value] == _valueString[index]
- ? theme.primaryIconTheme.color
- : theme.iconTheme.color,
- ),
- ),
- ),
- ),
- );
- }),
- );
-}
-
-class ImageButton extends StatefulWidget {
- const ImageButton({
- required this.icon,
- required this.controller,
- required this.imageSource,
- this.onImagePickCallback,
- this.imagePickImpl,
- Key? key,
- }) : super(key: key);
-
- final IconData icon;
-
- final QuillController controller;
+// The default size of the icon of a button.
+const double kDefaultIconSize = 18;
- final OnImagePickCallback? onImagePickCallback;
+// The factor of how much larger the button is in relation to the icon.
+const double kIconButtonFactor = 1.77;
- final ImagePickImpl? imagePickImpl;
-
- final ImageSource imageSource;
-
- @override
- _ImageButtonState createState() => _ImageButtonState();
-}
-
-class _ImageButtonState extends State {
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
-
- return QuillIconButton(
- icon: Icon(
- widget.icon,
- size: widget.controller.iconSize,
- color: theme.iconTheme.color,
- ),
- highlightElevation: 0,
- hoverElevation: 0,
- size: widget.controller.iconSize * 1.77,
- fillColor: theme.canvasColor,
- onPressed: _handleImageButtonTap,
- );
- }
-
- Future _handleImageButtonTap() async {
- final index = widget.controller.selection.baseOffset;
- final length = widget.controller.selection.extentOffset - index;
-
- String? imageUrl;
- if (widget.imagePickImpl != null) {
- imageUrl = await widget.imagePickImpl!(widget.imageSource);
- } else {
- if (kIsWeb) {
- imageUrl = await _pickImageWeb();
- } else if (Platform.isAndroid || Platform.isIOS) {
- imageUrl = await _pickImage(widget.imageSource);
- } else {
- imageUrl = await _pickImageDesktop();
- }
- }
-
- if (imageUrl != null) {
- widget.controller
- .replaceText(index, length, BlockEmbed.image(imageUrl), null);
- }
- }
-
- Future _pickImageWeb() async {
- final result = await FilePicker.platform.pickFiles();
- if (result == null) {
- return null;
- }
-
- // Take first, because we don't allow picking multiple files.
- final fileName = result.files.first.name!;
- final file = File(fileName);
-
- return widget.onImagePickCallback!(file);
- }
-
- Future _pickImage(ImageSource source) async {
- final pickedFile = await ImagePicker().getImage(source: source);
- if (pickedFile == null) {
- return null;
- }
-
- return widget.onImagePickCallback!(File(pickedFile.path));
- }
-
- Future _pickImageDesktop() async {
- final filePath = await FilesystemPicker.open(
- context: context,
- rootDirectory: await getApplicationDocumentsDirectory(),
- fsType: FilesystemType.file,
- fileTileSelectMode: FileTileSelectMode.wholeTile,
- );
- if (filePath == null || filePath.isEmpty) return null;
-
- final file = File(filePath);
- return widget.onImagePickCallback!(file);
- }
-}
-
-/// Controls color styles.
-///
-/// When pressed, this button displays overlay toolbar with
-/// buttons for each color.
-class ColorButton extends StatefulWidget {
- const ColorButton({
- required this.icon,
- required this.controller,
- required this.background,
- Key? key,
- }) : super(key: key);
-
- final IconData icon;
- final bool background;
- final QuillController controller;
-
- @override
- _ColorButtonState createState() => _ColorButtonState();
-}
-
-class _ColorButtonState extends State {
- late bool _isToggledColor;
- late bool _isToggledBackground;
- late bool _isWhite;
- late bool _isWhitebackground;
-
- Style get _selectionStyle => widget.controller.getSelectionStyle();
-
- void _didChangeEditingValue() {
- setState(() {
- _isToggledColor =
- _getIsToggledColor(widget.controller.getSelectionStyle().attributes);
- _isToggledBackground = _getIsToggledBackground(
- widget.controller.getSelectionStyle().attributes);
- _isWhite = _isToggledColor &&
- _selectionStyle.attributes['color']!.value == '#ffffff';
- _isWhitebackground = _isToggledBackground &&
- _selectionStyle.attributes['background']!.value == '#ffffff';
- });
- }
-
- @override
- void initState() {
- super.initState();
- _isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
- _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
- _isWhite = _isToggledColor &&
- _selectionStyle.attributes['color']!.value == '#ffffff';
- _isWhitebackground = _isToggledBackground &&
- _selectionStyle.attributes['background']!.value == '#ffffff';
- widget.controller.addListener(_didChangeEditingValue);
- }
-
- bool _getIsToggledColor(Map attrs) {
- return attrs.containsKey(Attribute.color.key);
- }
-
- bool _getIsToggledBackground(Map attrs) {
- return attrs.containsKey(Attribute.background.key);
- }
-
- @override
- void didUpdateWidget(covariant ColorButton oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (oldWidget.controller != widget.controller) {
- oldWidget.controller.removeListener(_didChangeEditingValue);
- widget.controller.addListener(_didChangeEditingValue);
- _isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
- _isToggledBackground =
- _getIsToggledBackground(_selectionStyle.attributes);
- _isWhite = _isToggledColor &&
- _selectionStyle.attributes['color']!.value == '#ffffff';
- _isWhitebackground = _isToggledBackground &&
- _selectionStyle.attributes['background']!.value == '#ffffff';
- }
- }
-
- @override
- void dispose() {
- widget.controller.removeListener(_didChangeEditingValue);
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final iconColor = _isToggledColor && !widget.background && !_isWhite
- ? stringToColor(_selectionStyle.attributes['color']!.value)
- : theme.iconTheme.color;
-
- final iconColorBackground =
- _isToggledBackground && widget.background && !_isWhitebackground
- ? stringToColor(_selectionStyle.attributes['background']!.value)
- : theme.iconTheme.color;
-
- final fillColor = _isToggledColor && !widget.background && _isWhite
- ? stringToColor('#ffffff')
- : theme.canvasColor;
- final fillColorBackground =
- _isToggledBackground && widget.background && _isWhitebackground
- ? stringToColor('#ffffff')
- : theme.canvasColor;
-
- return QuillIconButton(
- highlightElevation: 0,
- hoverElevation: 0,
- size: widget.controller.iconSize * 1.77,
- icon: Icon(widget.icon,
- size: widget.controller.iconSize,
- color: widget.background ? iconColorBackground : iconColor),
- fillColor: widget.background ? fillColorBackground : fillColor,
- onPressed: _showColorPicker,
- );
- }
-
- void _changeColor(Color color) {
- var hex = color.value.toRadixString(16);
- if (hex.startsWith('ff')) {
- hex = hex.substring(2);
- }
- hex = '#$hex';
- widget.controller.formatSelection(
- widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex));
- Navigator.of(context).pop();
- }
-
- void _showColorPicker() {
- showDialog(
- context: context,
- builder: (_) => AlertDialog(
- title: const Text('Select Color'),
- backgroundColor: Theme.of(context).canvasColor,
- content: SingleChildScrollView(
- child: MaterialPicker(
- pickerColor: const Color(0x00000000),
- onColorChanged: _changeColor,
- ),
- )),
- );
- }
-}
-
-class HistoryButton extends StatefulWidget {
- const HistoryButton({
- required this.icon,
- required this.controller,
- required this.undo,
- Key? key,
- }) : super(key: key);
-
- final IconData icon;
- final bool undo;
- final QuillController controller;
-
- @override
- _HistoryButtonState createState() => _HistoryButtonState();
-}
-
-class _HistoryButtonState extends State {
- Color? _iconColor;
- late ThemeData theme;
-
- @override
- Widget build(BuildContext context) {
- theme = Theme.of(context);
- _setIconColor();
-
- final fillColor = theme.canvasColor;
- widget.controller.changes.listen((event) async {
- _setIconColor();
- });
- return QuillIconButton(
- highlightElevation: 0,
- hoverElevation: 0,
- size: widget.controller.iconSize * 1.77,
- icon: Icon(widget.icon,
- size: widget.controller.iconSize, color: _iconColor),
- fillColor: fillColor,
- onPressed: _changeHistory,
- );
- }
-
- void _setIconColor() {
- if (!mounted) return;
-
- if (widget.undo) {
- setState(() {
- _iconColor = widget.controller.hasUndo
- ? theme.iconTheme.color
- : theme.disabledColor;
- });
- } else {
- setState(() {
- _iconColor = widget.controller.hasRedo
- ? theme.iconTheme.color
- : theme.disabledColor;
- });
- }
- }
-
- void _changeHistory() {
- if (widget.undo) {
- if (widget.controller.hasUndo) {
- widget.controller.undo();
- }
- } else {
- if (widget.controller.hasRedo) {
- widget.controller.redo();
- }
- }
-
- _setIconColor();
- }
-}
-
-class IndentButton extends StatefulWidget {
- const IndentButton({
- required this.icon,
- required this.controller,
- required this.isIncrease,
- Key? key,
- }) : super(key: key);
-
- final IconData icon;
- final QuillController controller;
- final bool isIncrease;
-
- @override
- _IndentButtonState createState() => _IndentButtonState();
-}
-
-class _IndentButtonState extends State {
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final iconColor = theme.iconTheme.color;
- final fillColor = theme.canvasColor;
- return QuillIconButton(
- highlightElevation: 0,
- hoverElevation: 0,
- size: widget.controller.iconSize * 1.77,
- icon:
- Icon(widget.icon, size: widget.controller.iconSize, color: iconColor),
- fillColor: fillColor,
- onPressed: () {
- final indent = widget.controller
- .getSelectionStyle()
- .attributes[Attribute.indent.key];
- if (indent == null) {
- if (widget.isIncrease) {
- widget.controller.formatSelection(Attribute.indentL1);
- }
- return;
- }
- if (indent.value == 1 && !widget.isIncrease) {
- widget.controller
- .formatSelection(Attribute.clone(Attribute.indentL1, null));
- return;
- }
- if (widget.isIncrease) {
- widget.controller
- .formatSelection(Attribute.getIndentLevel(indent.value + 1));
- return;
- }
- widget.controller
- .formatSelection(Attribute.getIndentLevel(indent.value - 1));
- },
- );
- }
-}
-
-class ClearFormatButton extends StatefulWidget {
- const ClearFormatButton({
- required this.icon,
- required this.controller,
+class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
+ const QuillToolbar({
+ required this.children,
+ this.toolBarHeight = 36,
Key? key,
}) : super(key: key);
- final IconData icon;
-
- final QuillController controller;
-
- @override
- _ClearFormatButtonState createState() => _ClearFormatButtonState();
-}
-
-class _ClearFormatButtonState extends State {
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final iconColor = theme.iconTheme.color;
- final fillColor = theme.canvasColor;
- return QuillIconButton(
- highlightElevation: 0,
- hoverElevation: 0,
- size: widget.controller.iconSize * 1.77,
- icon: Icon(widget.icon,
- size: widget.controller.iconSize, color: iconColor),
- fillColor: fillColor,
- onPressed: () {
- for (final k
- in widget.controller.getSelectionStyle().attributes.values) {
- widget.controller.formatSelection(Attribute.clone(k, null));
- }
- });
- }
-}
-
-class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
- const QuillToolbar(
- {required this.children, this.toolBarHeight = 36, Key? key})
- : super(key: key);
-
factory QuillToolbar.basic({
required QuillController controller,
- double toolbarIconSize = 18.0,
+ double toolbarIconSize = kDefaultIconSize,
bool showBoldButton = true,
bool showItalicButton = true,
bool showUnderLineButton = true,
@@ -936,16 +69,15 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
OnImagePickCallback? onImagePickCallback,
Key? key,
}) {
- controller.iconSize = toolbarIconSize;
-
return QuillToolbar(
key: key,
- toolBarHeight: toolbarIconSize * controller.toolbarHeightFactor,
+ toolBarHeight: toolbarIconSize * 2,
children: [
Visibility(
visible: showHistory,
child: HistoryButton(
icon: Icons.undo_outlined,
+ iconSize: toolbarIconSize,
controller: controller,
undo: true,
),
@@ -954,6 +86,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
visible: showHistory,
child: HistoryButton(
icon: Icons.redo_outlined,
+ iconSize: toolbarIconSize,
controller: controller,
undo: false,
),
@@ -964,6 +97,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
child: ToggleStyleButton(
attribute: Attribute.bold,
icon: Icons.format_bold,
+ iconSize: toolbarIconSize,
controller: controller,
),
),
@@ -973,6 +107,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
child: ToggleStyleButton(
attribute: Attribute.italic,
icon: Icons.format_italic,
+ iconSize: toolbarIconSize,
controller: controller,
),
),
@@ -982,6 +117,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
child: ToggleStyleButton(
attribute: Attribute.underline,
icon: Icons.format_underline,
+ iconSize: toolbarIconSize,
controller: controller,
),
),
@@ -991,6 +127,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
child: ToggleStyleButton(
attribute: Attribute.strikeThrough,
icon: Icons.format_strikethrough,
+ iconSize: toolbarIconSize,
controller: controller,
),
),
@@ -999,6 +136,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
visible: showColorButton,
child: ColorButton(
icon: Icons.color_lens,
+ iconSize: toolbarIconSize,
controller: controller,
background: false,
),
@@ -1008,6 +146,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
visible: showBackgroundColorButton,
child: ColorButton(
icon: Icons.format_color_fill,
+ iconSize: toolbarIconSize,
controller: controller,
background: true,
),
@@ -1017,6 +156,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
visible: showClearFormat,
child: ClearFormatButton(
icon: Icons.format_clear,
+ iconSize: toolbarIconSize,
controller: controller,
),
),
@@ -1025,6 +165,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
visible: onImagePickCallback != null,
child: ImageButton(
icon: Icons.image,
+ iconSize: toolbarIconSize,
controller: controller,
imageSource: ImageSource.gallery,
onImagePickCallback: onImagePickCallback,
@@ -1035,26 +176,39 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
visible: onImagePickCallback != null,
child: ImageButton(
icon: Icons.photo_camera,
+ iconSize: toolbarIconSize,
controller: controller,
imageSource: ImageSource.camera,
onImagePickCallback: onImagePickCallback,
),
),
Visibility(
- visible: showHeaderStyle,
- child: VerticalDivider(
- indent: 12, endIndent: 12, color: Colors.grey.shade400)),
+ visible: showHeaderStyle,
+ child: VerticalDivider(
+ indent: 12,
+ endIndent: 12,
+ color: Colors.grey.shade400,
+ ),
+ ),
Visibility(
- visible: showHeaderStyle,
- child: SelectHeaderStyleButton(controller: controller)),
+ visible: showHeaderStyle,
+ child: SelectHeaderStyleButton(
+ controller: controller,
+ iconSize: toolbarIconSize,
+ ),
+ ),
VerticalDivider(
- indent: 12, endIndent: 12, color: Colors.grey.shade400),
+ indent: 12,
+ endIndent: 12,
+ color: Colors.grey.shade400,
+ ),
Visibility(
visible: showListNumbers,
child: ToggleStyleButton(
attribute: Attribute.ol,
controller: controller,
icon: Icons.format_list_numbered,
+ iconSize: toolbarIconSize,
),
),
Visibility(
@@ -1063,6 +217,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
attribute: Attribute.ul,
controller: controller,
icon: Icons.format_list_bulleted,
+ iconSize: toolbarIconSize,
),
),
Visibility(
@@ -1071,6 +226,7 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
attribute: Attribute.unchecked,
controller: controller,
icon: Icons.check_box,
+ iconSize: toolbarIconSize,
),
),
Visibility(
@@ -1079,27 +235,34 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
attribute: Attribute.codeBlock,
controller: controller,
icon: Icons.code,
+ iconSize: toolbarIconSize,
),
),
Visibility(
- visible: !showListNumbers &&
- !showListBullets &&
- !showListCheck &&
- !showCodeBlock,
- child: VerticalDivider(
- indent: 12, endIndent: 12, color: Colors.grey.shade400)),
+ visible: !showListNumbers &&
+ !showListBullets &&
+ !showListCheck &&
+ !showCodeBlock,
+ child: VerticalDivider(
+ indent: 12,
+ endIndent: 12,
+ color: Colors.grey.shade400,
+ ),
+ ),
Visibility(
visible: showQuote,
child: ToggleStyleButton(
attribute: Attribute.blockQuote,
controller: controller,
icon: Icons.format_quote,
+ iconSize: toolbarIconSize,
),
),
Visibility(
visible: showIndent,
child: IndentButton(
icon: Icons.format_indent_increase,
+ iconSize: toolbarIconSize,
controller: controller,
isIncrease: true,
),
@@ -1108,22 +271,32 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
visible: showIndent,
child: IndentButton(
icon: Icons.format_indent_decrease,
+ iconSize: toolbarIconSize,
controller: controller,
isIncrease: false,
),
),
Visibility(
- visible: showQuote,
- child: VerticalDivider(
- indent: 12, endIndent: 12, color: Colors.grey.shade400)),
+ visible: showQuote,
+ child: VerticalDivider(
+ indent: 12,
+ endIndent: 12,
+ color: Colors.grey.shade400,
+ ),
+ ),
Visibility(
- visible: showLink,
- child: LinkStyleButton(controller: controller)),
+ visible: showLink,
+ child: LinkStyleButton(
+ controller: controller,
+ iconSize: toolbarIconSize,
+ ),
+ ),
Visibility(
visible: showHorizontalRule,
child: InsertEmbedButton(
controller: controller,
icon: Icons.horizontal_rule,
+ iconSize: toolbarIconSize,
),
),
]);
@@ -1161,134 +334,3 @@ class _QuillToolbarState extends State {
);
}
}
-
-class QuillIconButton extends StatelessWidget {
- const QuillIconButton({
- required this.onPressed,
- this.icon,
- this.size = 40,
- this.fillColor,
- this.hoverElevation = 1,
- this.highlightElevation = 1,
- Key? key,
- }) : super(key: key);
-
- final VoidCallback? onPressed;
- final Widget? icon;
- final double size;
- final Color? fillColor;
- final double hoverElevation;
- final double highlightElevation;
-
- @override
- Widget build(BuildContext context) {
- return ConstrainedBox(
- constraints: BoxConstraints.tightFor(width: size, height: size),
- child: RawMaterialButton(
- visualDensity: VisualDensity.compact,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
- fillColor: fillColor,
- elevation: 0,
- hoverElevation: hoverElevation,
- highlightElevation: hoverElevation,
- onPressed: onPressed,
- child: icon,
- ),
- );
- }
-}
-
-class QuillDropdownButton extends StatefulWidget {
- const QuillDropdownButton({
- required this.child,
- required this.initialValue,
- required this.items,
- required this.onSelected,
- this.height = 40,
- this.fillColor,
- this.hoverElevation = 1,
- this.highlightElevation = 1,
- Key? key,
- }) : super(key: key);
-
- final double height;
- final Color? fillColor;
- final double hoverElevation;
- final double highlightElevation;
- final Widget child;
- final T initialValue;
- final List> items;
- final ValueChanged onSelected;
-
- @override
- _QuillDropdownButtonState createState() => _QuillDropdownButtonState();
-}
-
-class _QuillDropdownButtonState extends State> {
- @override
- Widget build(BuildContext context) {
- return ConstrainedBox(
- constraints: BoxConstraints.tightFor(height: widget.height),
- child: RawMaterialButton(
- visualDensity: VisualDensity.compact,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
- fillColor: widget.fillColor,
- elevation: 0,
- hoverElevation: widget.hoverElevation,
- highlightElevation: widget.hoverElevation,
- onPressed: _showMenu,
- child: _buildContent(context),
- ),
- );
- }
-
- void _showMenu() {
- final popupMenuTheme = PopupMenuTheme.of(context);
- final button = context.findRenderObject() as RenderBox;
- final overlay =
- Overlay.of(context)!.context.findRenderObject() as RenderBox;
- final position = RelativeRect.fromRect(
- Rect.fromPoints(
- button.localToGlobal(Offset.zero, ancestor: overlay),
- button.localToGlobal(button.size.bottomLeft(Offset.zero),
- ancestor: overlay),
- ),
- Offset.zero & overlay.size,
- );
- showMenu(
- context: context,
- elevation: 4,
- // widget.elevation ?? popupMenuTheme.elevation,
- initialValue: widget.initialValue,
- items: widget.items,
- position: position,
- shape: popupMenuTheme.shape,
- // widget.shape ?? popupMenuTheme.shape,
- color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color,
- // captureInheritedThemes: widget.captureInheritedThemes,
- ).then((newValue) {
- if (!mounted) return null;
- if (newValue == null) {
- // if (widget.onCanceled != null) widget.onCanceled();
- return null;
- }
- widget.onSelected(newValue);
- });
- }
-
- Widget _buildContent(BuildContext context) {
- return ConstrainedBox(
- constraints: const BoxConstraints.tightFor(width: 110),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: Row(
- children: [
- widget.child,
- Expanded(child: Container()),
- const Icon(Icons.arrow_drop_down, size: 15)
- ],
- ),
- ),
- );
- }
-}
diff --git a/lib/src/widgets/toolbar/clear_format_button.dart b/lib/src/widgets/toolbar/clear_format_button.dart
new file mode 100644
index 00000000..d55c21df
--- /dev/null
+++ b/lib/src/widgets/toolbar/clear_format_button.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/material.dart';
+
+import '../../../flutter_quill.dart';
+import 'quill_icon_button.dart';
+
+class ClearFormatButton extends StatefulWidget {
+ const ClearFormatButton({
+ required this.icon,
+ required this.controller,
+ this.iconSize = kDefaultIconSize,
+ Key? key,
+ }) : super(key: key);
+
+ final IconData icon;
+ final double iconSize;
+
+ final QuillController controller;
+
+ @override
+ _ClearFormatButtonState createState() => _ClearFormatButtonState();
+}
+
+class _ClearFormatButtonState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final iconColor = theme.iconTheme.color;
+ final fillColor = theme.canvasColor;
+ return QuillIconButton(
+ highlightElevation: 0,
+ hoverElevation: 0,
+ size: widget.iconSize * kIconButtonFactor,
+ icon: Icon(widget.icon, size: widget.iconSize, color: iconColor),
+ fillColor: fillColor,
+ onPressed: () {
+ for (final k
+ in widget.controller.getSelectionStyle().attributes.values) {
+ widget.controller.formatSelection(Attribute.clone(k, null));
+ }
+ });
+ }
+}
diff --git a/lib/src/widgets/toolbar/color_button.dart b/lib/src/widgets/toolbar/color_button.dart
new file mode 100644
index 00000000..fa5bb520
--- /dev/null
+++ b/lib/src/widgets/toolbar/color_button.dart
@@ -0,0 +1,153 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_colorpicker/flutter_colorpicker.dart';
+
+import '../../models/documents/attribute.dart';
+import '../../models/documents/style.dart';
+import '../../utils/color.dart';
+import '../controller.dart';
+import '../toolbar.dart';
+import 'quill_icon_button.dart';
+
+/// Controls color styles.
+///
+/// When pressed, this button displays overlay toolbar with
+/// buttons for each color.
+class ColorButton extends StatefulWidget {
+ const ColorButton({
+ required this.icon,
+ required this.controller,
+ required this.background,
+ this.iconSize = kDefaultIconSize,
+ Key? key,
+ }) : super(key: key);
+
+ final IconData icon;
+ final double iconSize;
+ final bool background;
+ final QuillController controller;
+
+ @override
+ _ColorButtonState createState() => _ColorButtonState();
+}
+
+class _ColorButtonState extends State {
+ late bool _isToggledColor;
+ late bool _isToggledBackground;
+ late bool _isWhite;
+ late bool _isWhitebackground;
+
+ Style get _selectionStyle => widget.controller.getSelectionStyle();
+
+ void _didChangeEditingValue() {
+ setState(() {
+ _isToggledColor =
+ _getIsToggledColor(widget.controller.getSelectionStyle().attributes);
+ _isToggledBackground = _getIsToggledBackground(
+ widget.controller.getSelectionStyle().attributes);
+ _isWhite = _isToggledColor &&
+ _selectionStyle.attributes['color']!.value == '#ffffff';
+ _isWhitebackground = _isToggledBackground &&
+ _selectionStyle.attributes['background']!.value == '#ffffff';
+ });
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
+ _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
+ _isWhite = _isToggledColor &&
+ _selectionStyle.attributes['color']!.value == '#ffffff';
+ _isWhitebackground = _isToggledBackground &&
+ _selectionStyle.attributes['background']!.value == '#ffffff';
+ widget.controller.addListener(_didChangeEditingValue);
+ }
+
+ bool _getIsToggledColor(Map attrs) {
+ return attrs.containsKey(Attribute.color.key);
+ }
+
+ bool _getIsToggledBackground(Map attrs) {
+ return attrs.containsKey(Attribute.background.key);
+ }
+
+ @override
+ void didUpdateWidget(covariant ColorButton oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller.removeListener(_didChangeEditingValue);
+ widget.controller.addListener(_didChangeEditingValue);
+ _isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
+ _isToggledBackground =
+ _getIsToggledBackground(_selectionStyle.attributes);
+ _isWhite = _isToggledColor &&
+ _selectionStyle.attributes['color']!.value == '#ffffff';
+ _isWhitebackground = _isToggledBackground &&
+ _selectionStyle.attributes['background']!.value == '#ffffff';
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.controller.removeListener(_didChangeEditingValue);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final iconColor = _isToggledColor && !widget.background && !_isWhite
+ ? stringToColor(_selectionStyle.attributes['color']!.value)
+ : theme.iconTheme.color;
+
+ final iconColorBackground =
+ _isToggledBackground && widget.background && !_isWhitebackground
+ ? stringToColor(_selectionStyle.attributes['background']!.value)
+ : theme.iconTheme.color;
+
+ final fillColor = _isToggledColor && !widget.background && _isWhite
+ ? stringToColor('#ffffff')
+ : theme.canvasColor;
+ final fillColorBackground =
+ _isToggledBackground && widget.background && _isWhitebackground
+ ? stringToColor('#ffffff')
+ : theme.canvasColor;
+
+ return QuillIconButton(
+ highlightElevation: 0,
+ hoverElevation: 0,
+ size: widget.iconSize * kIconButtonFactor,
+ icon: Icon(widget.icon,
+ size: widget.iconSize,
+ color: widget.background ? iconColorBackground : iconColor),
+ fillColor: widget.background ? fillColorBackground : fillColor,
+ onPressed: _showColorPicker,
+ );
+ }
+
+ void _changeColor(Color color) {
+ var hex = color.value.toRadixString(16);
+ if (hex.startsWith('ff')) {
+ hex = hex.substring(2);
+ }
+ hex = '#$hex';
+ widget.controller.formatSelection(
+ widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex));
+ Navigator.of(context).pop();
+ }
+
+ void _showColorPicker() {
+ showDialog(
+ context: context,
+ builder: (_) => AlertDialog(
+ title: const Text('Select Color'),
+ backgroundColor: Theme.of(context).canvasColor,
+ content: SingleChildScrollView(
+ child: MaterialPicker(
+ pickerColor: const Color(0x00000000),
+ onColorChanged: _changeColor,
+ ),
+ )),
+ );
+ }
+}
diff --git a/lib/src/widgets/toolbar/history_button.dart b/lib/src/widgets/toolbar/history_button.dart
new file mode 100644
index 00000000..2ed794c5
--- /dev/null
+++ b/lib/src/widgets/toolbar/history_button.dart
@@ -0,0 +1,78 @@
+import 'package:flutter/material.dart';
+
+import '../../../flutter_quill.dart';
+import 'quill_icon_button.dart';
+
+class HistoryButton extends StatefulWidget {
+ const HistoryButton({
+ required this.icon,
+ required this.controller,
+ required this.undo,
+ this.iconSize = kDefaultIconSize,
+ Key? key,
+ }) : super(key: key);
+
+ final IconData icon;
+ final double iconSize;
+ final bool undo;
+ final QuillController controller;
+
+ @override
+ _HistoryButtonState createState() => _HistoryButtonState();
+}
+
+class _HistoryButtonState extends State {
+ Color? _iconColor;
+ late ThemeData theme;
+
+ @override
+ Widget build(BuildContext context) {
+ theme = Theme.of(context);
+ _setIconColor();
+
+ final fillColor = theme.canvasColor;
+ widget.controller.changes.listen((event) async {
+ _setIconColor();
+ });
+ return QuillIconButton(
+ highlightElevation: 0,
+ hoverElevation: 0,
+ size: widget.iconSize * 1.77,
+ icon: Icon(widget.icon, size: widget.iconSize, color: _iconColor),
+ fillColor: fillColor,
+ onPressed: _changeHistory,
+ );
+ }
+
+ void _setIconColor() {
+ if (!mounted) return;
+
+ if (widget.undo) {
+ setState(() {
+ _iconColor = widget.controller.hasUndo
+ ? theme.iconTheme.color
+ : theme.disabledColor;
+ });
+ } else {
+ setState(() {
+ _iconColor = widget.controller.hasRedo
+ ? theme.iconTheme.color
+ : theme.disabledColor;
+ });
+ }
+ }
+
+ void _changeHistory() {
+ if (widget.undo) {
+ if (widget.controller.hasUndo) {
+ widget.controller.undo();
+ }
+ } else {
+ if (widget.controller.hasRedo) {
+ widget.controller.redo();
+ }
+ }
+
+ _setIconColor();
+ }
+}
diff --git a/lib/src/widgets/toolbar/image_button.dart b/lib/src/widgets/toolbar/image_button.dart
new file mode 100644
index 00000000..33e191a9
--- /dev/null
+++ b/lib/src/widgets/toolbar/image_button.dart
@@ -0,0 +1,117 @@
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:filesystem_picker/filesystem_picker.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:path_provider/path_provider.dart';
+
+import '../../models/documents/nodes/embed.dart';
+import '../controller.dart';
+import '../toolbar.dart';
+import 'quill_icon_button.dart';
+
+class ImageButton extends StatefulWidget {
+ const ImageButton({
+ required this.icon,
+ required this.controller,
+ required this.imageSource,
+ this.iconSize = kDefaultIconSize,
+ this.onImagePickCallback,
+ this.imagePickImpl,
+ Key? key,
+ }) : super(key: key);
+
+ final IconData icon;
+ final double iconSize;
+
+ final QuillController controller;
+
+ final OnImagePickCallback? onImagePickCallback;
+
+ final ImagePickImpl? imagePickImpl;
+
+ final ImageSource imageSource;
+
+ @override
+ _ImageButtonState createState() => _ImageButtonState();
+}
+
+class _ImageButtonState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+
+ return QuillIconButton(
+ icon: Icon(
+ widget.icon,
+ size: widget.iconSize,
+ color: theme.iconTheme.color,
+ ),
+ highlightElevation: 0,
+ hoverElevation: 0,
+ size: widget.iconSize * 1.77,
+ fillColor: theme.canvasColor,
+ onPressed: _handleImageButtonTap,
+ );
+ }
+
+ Future _handleImageButtonTap() async {
+ final index = widget.controller.selection.baseOffset;
+ final length = widget.controller.selection.extentOffset - index;
+
+ String? imageUrl;
+ if (widget.imagePickImpl != null) {
+ imageUrl = await widget.imagePickImpl!(widget.imageSource);
+ } else {
+ if (kIsWeb) {
+ imageUrl = await _pickImageWeb();
+ } else if (Platform.isAndroid || Platform.isIOS) {
+ imageUrl = await _pickImage(widget.imageSource);
+ } else {
+ imageUrl = await _pickImageDesktop();
+ }
+ }
+
+ if (imageUrl != null) {
+ widget.controller
+ .replaceText(index, length, BlockEmbed.image(imageUrl), null);
+ }
+ }
+
+ Future _pickImageWeb() async {
+ final result = await FilePicker.platform.pickFiles();
+ if (result == null) {
+ return null;
+ }
+
+ // Take first, because we don't allow picking multiple files.
+ final fileName = result.files.first.name!;
+ final file = File(fileName);
+
+ return widget.onImagePickCallback!(file);
+ }
+
+ Future _pickImage(ImageSource source) async {
+ final pickedFile = await ImagePicker().getImage(source: source);
+ if (pickedFile == null) {
+ return null;
+ }
+
+ return widget.onImagePickCallback!(File(pickedFile.path));
+ }
+
+ Future _pickImageDesktop() async {
+ final filePath = await FilesystemPicker.open(
+ context: context,
+ rootDirectory: await getApplicationDocumentsDirectory(),
+ fsType: FilesystemType.file,
+ fileTileSelectMode: FileTileSelectMode.wholeTile,
+ );
+ if (filePath == null || filePath.isEmpty) return null;
+
+ final file = File(filePath);
+ return widget.onImagePickCallback!(file);
+ }
+}
diff --git a/lib/src/widgets/toolbar/indent_button.dart b/lib/src/widgets/toolbar/indent_button.dart
new file mode 100644
index 00000000..aa6dfadb
--- /dev/null
+++ b/lib/src/widgets/toolbar/indent_button.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/material.dart';
+
+import '../../../flutter_quill.dart';
+import 'quill_icon_button.dart';
+
+class IndentButton extends StatefulWidget {
+ const IndentButton({
+ required this.icon,
+ required this.controller,
+ required this.isIncrease,
+ this.iconSize = kDefaultIconSize,
+ Key? key,
+ }) : super(key: key);
+
+ final IconData icon;
+ final double iconSize;
+ final QuillController controller;
+ final bool isIncrease;
+
+ @override
+ _IndentButtonState createState() => _IndentButtonState();
+}
+
+class _IndentButtonState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final iconColor = theme.iconTheme.color;
+ final fillColor = theme.canvasColor;
+ return QuillIconButton(
+ highlightElevation: 0,
+ hoverElevation: 0,
+ size: widget.iconSize * 1.77,
+ icon: Icon(widget.icon, size: widget.iconSize, color: iconColor),
+ fillColor: fillColor,
+ onPressed: () {
+ final indent = widget.controller
+ .getSelectionStyle()
+ .attributes[Attribute.indent.key];
+ if (indent == null) {
+ if (widget.isIncrease) {
+ widget.controller.formatSelection(Attribute.indentL1);
+ }
+ return;
+ }
+ if (indent.value == 1 && !widget.isIncrease) {
+ widget.controller
+ .formatSelection(Attribute.clone(Attribute.indentL1, null));
+ return;
+ }
+ if (widget.isIncrease) {
+ widget.controller
+ .formatSelection(Attribute.getIndentLevel(indent.value + 1));
+ return;
+ }
+ widget.controller
+ .formatSelection(Attribute.getIndentLevel(indent.value - 1));
+ },
+ );
+ }
+}
diff --git a/lib/src/widgets/toolbar/insert_embed_button.dart b/lib/src/widgets/toolbar/insert_embed_button.dart
new file mode 100644
index 00000000..5c889b69
--- /dev/null
+++ b/lib/src/widgets/toolbar/insert_embed_button.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+
+import '../../models/documents/nodes/embed.dart';
+import '../controller.dart';
+import '../toolbar.dart';
+import 'quill_icon_button.dart';
+
+class InsertEmbedButton extends StatelessWidget {
+ const InsertEmbedButton({
+ required this.controller,
+ required this.icon,
+ this.iconSize = kDefaultIconSize,
+ this.fillColor,
+ Key? key,
+ }) : super(key: key);
+
+ final QuillController controller;
+ final IconData icon;
+ final double iconSize;
+ final Color? fillColor;
+
+ @override
+ Widget build(BuildContext context) {
+ return QuillIconButton(
+ highlightElevation: 0,
+ hoverElevation: 0,
+ size: iconSize * kIconButtonFactor,
+ icon: Icon(
+ icon,
+ size: iconSize,
+ color: Theme.of(context).iconTheme.color,
+ ),
+ fillColor: fillColor ?? Theme.of(context).canvasColor,
+ onPressed: () {
+ final index = controller.selection.baseOffset;
+ final length = controller.selection.extentOffset - index;
+ controller.replaceText(index, length, BlockEmbed.horizontalRule, null);
+ },
+ );
+ }
+}
diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart
new file mode 100644
index 00000000..417a9972
--- /dev/null
+++ b/lib/src/widgets/toolbar/link_style_button.dart
@@ -0,0 +1,122 @@
+import 'package:flutter/material.dart';
+
+import '../../models/documents/attribute.dart';
+import '../controller.dart';
+import '../toolbar.dart';
+import 'quill_icon_button.dart';
+
+class LinkStyleButton extends StatefulWidget {
+ const LinkStyleButton({
+ required this.controller,
+ this.iconSize = kDefaultIconSize,
+ this.icon,
+ Key? key,
+ }) : super(key: key);
+
+ final QuillController controller;
+ final IconData? icon;
+ final double iconSize;
+
+ @override
+ _LinkStyleButtonState createState() => _LinkStyleButtonState();
+}
+
+class _LinkStyleButtonState extends State {
+ void _didChangeSelection() {
+ setState(() {});
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ widget.controller.addListener(_didChangeSelection);
+ }
+
+ @override
+ void didUpdateWidget(covariant LinkStyleButton oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller.removeListener(_didChangeSelection);
+ widget.controller.addListener(_didChangeSelection);
+ }
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ widget.controller.removeListener(_didChangeSelection);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final isEnabled = !widget.controller.selection.isCollapsed;
+ final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null;
+ return QuillIconButton(
+ highlightElevation: 0,
+ hoverElevation: 0,
+ size: widget.iconSize * kIconButtonFactor,
+ icon: Icon(
+ widget.icon ?? Icons.link,
+ size: widget.iconSize,
+ color: isEnabled ? theme.iconTheme.color : theme.disabledColor,
+ ),
+ fillColor: Theme.of(context).canvasColor,
+ onPressed: pressedHandler,
+ );
+ }
+
+ void _openLinkDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (ctx) {
+ return const _LinkDialog();
+ },
+ ).then(_linkSubmitted);
+ }
+
+ void _linkSubmitted(String? value) {
+ if (value == null || value.isEmpty) {
+ return;
+ }
+ widget.controller.formatSelection(LinkAttribute(value));
+ }
+}
+
+class _LinkDialog extends StatefulWidget {
+ const _LinkDialog({Key? key}) : super(key: key);
+
+ @override
+ _LinkDialogState createState() => _LinkDialogState();
+}
+
+class _LinkDialogState extends State<_LinkDialog> {
+ String _link = '';
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ content: TextField(
+ decoration: const InputDecoration(labelText: 'Paste a link'),
+ autofocus: true,
+ onChanged: _linkChanged,
+ ),
+ actions: [
+ TextButton(
+ onPressed: _link.isNotEmpty ? _applyLink : null,
+ child: const Text('Apply'),
+ ),
+ ],
+ );
+ }
+
+ void _linkChanged(String value) {
+ setState(() {
+ _link = value;
+ });
+ }
+
+ void _applyLink() {
+ Navigator.pop(context, _link);
+ }
+}
diff --git a/lib/src/widgets/toolbar/quill_dropdown_button.dart b/lib/src/widgets/toolbar/quill_dropdown_button.dart
new file mode 100644
index 00000000..be3ed092
--- /dev/null
+++ b/lib/src/widgets/toolbar/quill_dropdown_button.dart
@@ -0,0 +1,96 @@
+import 'package:flutter/material.dart';
+
+class QuillDropdownButton extends StatefulWidget {
+ const QuillDropdownButton({
+ required this.child,
+ required this.initialValue,
+ required this.items,
+ required this.onSelected,
+ this.height = 40,
+ this.fillColor,
+ this.hoverElevation = 1,
+ this.highlightElevation = 1,
+ Key? key,
+ }) : super(key: key);
+
+ final double height;
+ final Color? fillColor;
+ final double hoverElevation;
+ final double highlightElevation;
+ final Widget child;
+ final T initialValue;
+ final List> items;
+ final ValueChanged onSelected;
+
+ @override
+ _QuillDropdownButtonState createState() => _QuillDropdownButtonState();
+}
+
+class _QuillDropdownButtonState extends State> {
+ @override
+ Widget build(BuildContext context) {
+ return ConstrainedBox(
+ constraints: BoxConstraints.tightFor(height: widget.height),
+ child: RawMaterialButton(
+ visualDensity: VisualDensity.compact,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
+ fillColor: widget.fillColor,
+ elevation: 0,
+ hoverElevation: widget.hoverElevation,
+ highlightElevation: widget.hoverElevation,
+ onPressed: _showMenu,
+ child: _buildContent(context),
+ ),
+ );
+ }
+
+ void _showMenu() {
+ final popupMenuTheme = PopupMenuTheme.of(context);
+ final button = context.findRenderObject() as RenderBox;
+ final overlay =
+ Overlay.of(context)!.context.findRenderObject() as RenderBox;
+ final position = RelativeRect.fromRect(
+ Rect.fromPoints(
+ button.localToGlobal(Offset.zero, ancestor: overlay),
+ button.localToGlobal(button.size.bottomLeft(Offset.zero),
+ ancestor: overlay),
+ ),
+ Offset.zero & overlay.size,
+ );
+ showMenu(
+ context: context,
+ elevation: 4,
+ // widget.elevation ?? popupMenuTheme.elevation,
+ initialValue: widget.initialValue,
+ items: widget.items,
+ position: position,
+ shape: popupMenuTheme.shape,
+ // widget.shape ?? popupMenuTheme.shape,
+ color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color,
+ // captureInheritedThemes: widget.captureInheritedThemes,
+ ).then((newValue) {
+ if (!mounted) return null;
+ if (newValue == null) {
+ // if (widget.onCanceled != null) widget.onCanceled();
+ return null;
+ }
+ widget.onSelected(newValue);
+ });
+ }
+
+ Widget _buildContent(BuildContext context) {
+ return ConstrainedBox(
+ constraints: const BoxConstraints.tightFor(width: 110),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ child: Row(
+ children: [
+ widget.child,
+ Expanded(child: Container()),
+ const Icon(Icons.arrow_drop_down, size: 15)
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/toolbar/quill_icon_button.dart b/lib/src/widgets/toolbar/quill_icon_button.dart
new file mode 100644
index 00000000..0ffd3ef7
--- /dev/null
+++ b/lib/src/widgets/toolbar/quill_icon_button.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+
+class QuillIconButton extends StatelessWidget {
+ const QuillIconButton({
+ required this.onPressed,
+ this.icon,
+ this.size = 40,
+ this.fillColor,
+ this.hoverElevation = 1,
+ this.highlightElevation = 1,
+ Key? key,
+ }) : super(key: key);
+
+ final VoidCallback? onPressed;
+ final Widget? icon;
+ final double size;
+ final Color? fillColor;
+ final double hoverElevation;
+ final double highlightElevation;
+
+ @override
+ Widget build(BuildContext context) {
+ return ConstrainedBox(
+ constraints: BoxConstraints.tightFor(width: size, height: size),
+ child: RawMaterialButton(
+ visualDensity: VisualDensity.compact,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
+ fillColor: fillColor,
+ elevation: 0,
+ hoverElevation: hoverElevation,
+ highlightElevation: hoverElevation,
+ onPressed: onPressed,
+ child: icon,
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/toolbar/select_header_style_button.dart b/lib/src/widgets/toolbar/select_header_style_button.dart
new file mode 100644
index 00000000..715e3632
--- /dev/null
+++ b/lib/src/widgets/toolbar/select_header_style_button.dart
@@ -0,0 +1,122 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import '../../models/documents/attribute.dart';
+import '../../models/documents/style.dart';
+import '../controller.dart';
+import '../toolbar.dart';
+
+class SelectHeaderStyleButton extends StatefulWidget {
+ const SelectHeaderStyleButton({
+ required this.controller,
+ this.iconSize = kDefaultIconSize,
+ Key? key,
+ }) : super(key: key);
+
+ final QuillController controller;
+ final double iconSize;
+
+ @override
+ _SelectHeaderStyleButtonState createState() =>
+ _SelectHeaderStyleButtonState();
+}
+
+class _SelectHeaderStyleButtonState extends State {
+ Attribute? _value;
+
+ Style get _selectionStyle => widget.controller.getSelectionStyle();
+
+ @override
+ void initState() {
+ super.initState();
+ setState(() {
+ _value =
+ _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
+ });
+ widget.controller.addListener(_didChangeEditingValue);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final _valueToText = {
+ Attribute.header: 'N',
+ Attribute.h1: 'H1',
+ Attribute.h2: 'H2',
+ Attribute.h3: 'H3',
+ };
+
+ final _valueAttribute = [
+ Attribute.header,
+ Attribute.h1,
+ Attribute.h2,
+ Attribute.h3
+ ];
+ final _valueString = ['N', 'H1', 'H2', 'H3'];
+
+ final theme = Theme.of(context);
+ final style = TextStyle(
+ fontWeight: FontWeight.w600,
+ fontSize: widget.iconSize * 0.7,
+ );
+
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: List.generate(4, (index) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0),
+ child: ConstrainedBox(
+ constraints: BoxConstraints.tightFor(
+ width: widget.iconSize * kIconButtonFactor,
+ height: widget.iconSize * kIconButtonFactor,
+ ),
+ child: RawMaterialButton(
+ hoverElevation: 0,
+ highlightElevation: 0,
+ elevation: 0,
+ visualDensity: VisualDensity.compact,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(2)),
+ fillColor: _valueToText[_value] == _valueString[index]
+ ? theme.toggleableActiveColor
+ : theme.canvasColor,
+ onPressed: () =>
+ widget.controller.formatSelection(_valueAttribute[index]),
+ child: Text(
+ _valueString[index],
+ style: style.copyWith(
+ color: _valueToText[_value] == _valueString[index]
+ ? theme.primaryIconTheme.color
+ : theme.iconTheme.color,
+ ),
+ ),
+ ),
+ ),
+ );
+ }),
+ );
+ }
+
+ void _didChangeEditingValue() {
+ setState(() {
+ _value =
+ _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
+ });
+ }
+
+ @override
+ void didUpdateWidget(covariant SelectHeaderStyleButton oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller.removeListener(_didChangeEditingValue);
+ widget.controller.addListener(_didChangeEditingValue);
+ _value =
+ _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.controller.removeListener(_didChangeEditingValue);
+ super.dispose();
+ }
+}
diff --git a/lib/src/widgets/toolbar/toggle_check_list_button.dart b/lib/src/widgets/toolbar/toggle_check_list_button.dart
new file mode 100644
index 00000000..861da445
--- /dev/null
+++ b/lib/src/widgets/toolbar/toggle_check_list_button.dart
@@ -0,0 +1,104 @@
+import 'package:flutter/material.dart';
+
+import '../../models/documents/attribute.dart';
+import '../../models/documents/style.dart';
+import '../controller.dart';
+import '../toolbar.dart';
+import 'toggle_style_button.dart';
+
+class ToggleCheckListButton extends StatefulWidget {
+ const ToggleCheckListButton({
+ required this.icon,
+ required this.controller,
+ required this.attribute,
+ this.iconSize = kDefaultIconSize,
+ this.fillColor,
+ this.childBuilder = defaultToggleStyleButtonBuilder,
+ Key? key,
+ }) : super(key: key);
+
+ final IconData icon;
+ final double iconSize;
+
+ final Color? fillColor;
+
+ final QuillController controller;
+
+ final ToggleStyleButtonBuilder childBuilder;
+
+ final Attribute attribute;
+
+ @override
+ _ToggleCheckListButtonState createState() => _ToggleCheckListButtonState();
+}
+
+class _ToggleCheckListButtonState extends State {
+ bool? _isToggled;
+
+ Style get _selectionStyle => widget.controller.getSelectionStyle();
+
+ void _didChangeEditingValue() {
+ setState(() {
+ _isToggled =
+ _getIsToggled(widget.controller.getSelectionStyle().attributes);
+ });
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _isToggled = _getIsToggled(_selectionStyle.attributes);
+ widget.controller.addListener(_didChangeEditingValue);
+ }
+
+ bool _getIsToggled(Map attrs) {
+ if (widget.attribute.key == Attribute.list.key) {
+ final attribute = attrs[widget.attribute.key];
+ if (attribute == null) {
+ return false;
+ }
+ return attribute.value == widget.attribute.value ||
+ attribute.value == Attribute.checked.value;
+ }
+ return attrs.containsKey(widget.attribute.key);
+ }
+
+ @override
+ void didUpdateWidget(covariant ToggleCheckListButton oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller.removeListener(_didChangeEditingValue);
+ widget.controller.addListener(_didChangeEditingValue);
+ _isToggled = _getIsToggled(_selectionStyle.attributes);
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.controller.removeListener(_didChangeEditingValue);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isInCodeBlock =
+ _selectionStyle.attributes.containsKey(Attribute.codeBlock.key);
+ final isEnabled =
+ !isInCodeBlock || Attribute.list.key == Attribute.codeBlock.key;
+ return widget.childBuilder(
+ context,
+ Attribute.unchecked,
+ widget.icon,
+ widget.fillColor,
+ _isToggled,
+ isEnabled ? _toggleAttribute : null,
+ widget.iconSize,
+ );
+ }
+
+ void _toggleAttribute() {
+ widget.controller.formatSelection(_isToggled!
+ ? Attribute.clone(Attribute.unchecked, null)
+ : Attribute.unchecked);
+ }
+}
diff --git a/lib/src/widgets/toolbar/toggle_style_button.dart b/lib/src/widgets/toolbar/toggle_style_button.dart
new file mode 100644
index 00000000..624a31f9
--- /dev/null
+++ b/lib/src/widgets/toolbar/toggle_style_button.dart
@@ -0,0 +1,139 @@
+import 'package:flutter/material.dart';
+
+import '../../models/documents/attribute.dart';
+import '../../models/documents/style.dart';
+import '../controller.dart';
+import '../toolbar.dart';
+import 'quill_icon_button.dart';
+
+typedef ToggleStyleButtonBuilder = Widget Function(
+ BuildContext context,
+ Attribute attribute,
+ IconData icon,
+ Color? fillColor,
+ bool? isToggled,
+ VoidCallback? onPressed, [
+ double iconSize,
+]);
+
+class ToggleStyleButton extends StatefulWidget {
+ const ToggleStyleButton({
+ required this.attribute,
+ required this.icon,
+ required this.controller,
+ this.iconSize = kDefaultIconSize,
+ this.fillColor,
+ this.childBuilder = defaultToggleStyleButtonBuilder,
+ Key? key,
+ }) : super(key: key);
+
+ final Attribute attribute;
+
+ final IconData icon;
+ final double iconSize;
+
+ final Color? fillColor;
+
+ final QuillController controller;
+
+ final ToggleStyleButtonBuilder childBuilder;
+
+ @override
+ _ToggleStyleButtonState createState() => _ToggleStyleButtonState();
+}
+
+class _ToggleStyleButtonState extends State {
+ bool? _isToggled;
+
+ Style get _selectionStyle => widget.controller.getSelectionStyle();
+
+ @override
+ void initState() {
+ super.initState();
+ _isToggled = _getIsToggled(_selectionStyle.attributes);
+ widget.controller.addListener(_didChangeEditingValue);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isInCodeBlock =
+ _selectionStyle.attributes.containsKey(Attribute.codeBlock.key);
+ final isEnabled =
+ !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key;
+ return widget.childBuilder(
+ context,
+ widget.attribute,
+ widget.icon,
+ widget.fillColor,
+ _isToggled,
+ isEnabled ? _toggleAttribute : null,
+ widget.iconSize,
+ );
+ }
+
+ @override
+ void didUpdateWidget(covariant ToggleStyleButton oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller.removeListener(_didChangeEditingValue);
+ widget.controller.addListener(_didChangeEditingValue);
+ _isToggled = _getIsToggled(_selectionStyle.attributes);
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.controller.removeListener(_didChangeEditingValue);
+ super.dispose();
+ }
+
+ void _didChangeEditingValue() {
+ setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes));
+ }
+
+ bool _getIsToggled(Map attrs) {
+ if (widget.attribute.key == Attribute.list.key) {
+ final attribute = attrs[widget.attribute.key];
+ if (attribute == null) {
+ return false;
+ }
+ return attribute.value == widget.attribute.value;
+ }
+ return attrs.containsKey(widget.attribute.key);
+ }
+
+ void _toggleAttribute() {
+ widget.controller.formatSelection(_isToggled!
+ ? Attribute.clone(widget.attribute, null)
+ : widget.attribute);
+ }
+}
+
+Widget defaultToggleStyleButtonBuilder(
+ BuildContext context,
+ Attribute attribute,
+ IconData icon,
+ Color? fillColor,
+ bool? isToggled,
+ VoidCallback? onPressed, [
+ double iconSize = kDefaultIconSize,
+]) {
+ final theme = Theme.of(context);
+ final isEnabled = onPressed != null;
+ final iconColor = isEnabled
+ ? isToggled == true
+ ? theme.primaryIconTheme.color
+ : theme.iconTheme.color
+ : theme.disabledColor;
+ final fill = isToggled == true
+ ? theme.toggleableActiveColor
+ : fillColor ?? theme.canvasColor;
+ return QuillIconButton(
+ highlightElevation: 0,
+ hoverElevation: 0,
+ size: iconSize * kIconButtonFactor,
+ icon: Icon(icon, size: iconSize, color: iconColor),
+ fillColor: fill,
+ onPressed: onPressed,
+ );
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 7cf52633..e50130ff 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: flutter_quill
description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us)
-version: 1.3.0
+version: 1.3.1
#author: bulletjournal
homepage: https://bulletjournal.us/home/index.html
repository: https://github.com/singerdmx/flutter-quill