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