Fix buttons which ignore toolbariconsize

Closes #189.
pull/239/head^2
Till Friebe 4 years ago
parent 8c3617c669
commit 30a9747b1d
  1. 4
      lib/src/models/rules/insert.dart
  2. 32
      lib/src/widgets/controller.dart
  3. 1142
      lib/src/widgets/toolbar.dart
  4. 42
      lib/src/widgets/toolbar/clear_format_button.dart
  5. 153
      lib/src/widgets/toolbar/color_button.dart
  6. 78
      lib/src/widgets/toolbar/history_button.dart
  7. 117
      lib/src/widgets/toolbar/image_button.dart
  8. 61
      lib/src/widgets/toolbar/indent_button.dart
  9. 41
      lib/src/widgets/toolbar/insert_embed_button.dart
  10. 122
      lib/src/widgets/toolbar/link_style_button.dart
  11. 96
      lib/src/widgets/toolbar/quill_dropdown_button.dart
  12. 37
      lib/src/widgets/toolbar/quill_icon_button.dart
  13. 122
      lib/src/widgets/toolbar/select_header_style_button.dart
  14. 104
      lib/src/widgets/toolbar/toggle_check_list_button.dart
  15. 139
      lib/src/widgets/toolbar/toggle_style_button.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 ?? <String, dynamic>{};
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);

@ -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));
}

File diff suppressed because it is too large Load Diff

@ -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<ClearFormatButton> {
@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));
}
});
}
}

@ -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<ColorButton> {
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<String, Attribute> attrs) {
return attrs.containsKey(Attribute.color.key);
}
bool _getIsToggledBackground(Map<String, Attribute> 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,
),
)),
);
}
}

@ -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<HistoryButton> {
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();
}
}

@ -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<ImageButton> {
@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<void> _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<String?> _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<String?> _pickImage(ImageSource source) async {
final pickedFile = await ImagePicker().getImage(source: source);
if (pickedFile == null) {
return null;
}
return widget.onImagePickCallback!(File(pickedFile.path));
}
Future<String?> _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);
}
}

@ -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<IndentButton> {
@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));
},
);
}
}

@ -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);
},
);
}
}

@ -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<LinkStyleButton> {
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<String>(
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);
}
}

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
class QuillDropdownButton<T> 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<PopupMenuEntry<T>> items;
final ValueChanged<T> onSelected;
@override
_QuillDropdownButtonState<T> createState() => _QuillDropdownButtonState<T>();
}
class _QuillDropdownButtonState<T> extends State<QuillDropdownButton<T>> {
@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<T>(
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)
],
),
),
);
}
}

@ -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,
),
);
}
}

@ -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<SelectHeaderStyleButton> {
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, String>{
Attribute.header: 'N',
Attribute.h1: 'H1',
Attribute.h2: 'H2',
Attribute.h3: 'H3',
};
final _valueAttribute = <Attribute>[
Attribute.header,
Attribute.h1,
Attribute.h2,
Attribute.h3
];
final _valueString = <String>['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();
}
}

@ -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<ToggleCheckListButton> {
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<String, Attribute> 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);
}
}

@ -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<ToggleStyleButton> {
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<String, Attribute> 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,
);
}
Loading…
Cancel
Save