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