parent
8c3617c669
commit
30a9747b1d
15 changed files with 1224 additions and 1066 deletions
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…
Reference in new issue