Add: test for QuillController clipboard

Dart Formatted
pull/1843/head
Douglas Ward 12 months ago
parent e76daa27b1
commit f1231b7851
  1. 1
      lib/src/models/config/toolbar/simple_toolbar_button_options.dart
  2. 2
      lib/src/models/config/toolbar/simple_toolbar_configurations.dart
  3. 46
      lib/src/models/documents/nodes/line.dart
  4. 68
      lib/src/widgets/quill/quill_controller.dart
  5. 4
      lib/src/widgets/raw_editor/raw_editor_state.dart
  6. 3
      lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart
  7. 11
      lib/src/widgets/toolbar/base_button/base_value_button.dart
  8. 38
      lib/src/widgets/toolbar/buttons/clipboard_button.dart
  9. 3
      lib/src/widgets/toolbar/buttons/toggle_style_button.dart
  10. 86
      lib/src/widgets/toolbar/simple_toolbar.dart
  11. 53
      test/widgets/controller_test.dart

@ -75,7 +75,6 @@ class QuillSimpleToolbarButtonOptions extends Equatable {
this.linkStyle = const QuillToolbarLinkStyleButtonOptions(), this.linkStyle = const QuillToolbarLinkStyleButtonOptions(),
this.linkStyle2 = const QuillToolbarLinkStyleButton2Options(), this.linkStyle2 = const QuillToolbarLinkStyleButton2Options(),
this.customButtons = const QuillToolbarCustomButtonOptions(), this.customButtons = const QuillToolbarCustomButtonOptions(),
this.clipboardCut = const QuillToolbarToggleStyleButtonOptions(), this.clipboardCut = const QuillToolbarToggleStyleButtonOptions(),
this.clipboardCopy = const QuillToolbarToggleStyleButtonOptions(), this.clipboardCopy = const QuillToolbarToggleStyleButtonOptions(),
this.clipboardPaste = const QuillToolbarToggleStyleButtonOptions(), this.clipboardPaste = const QuillToolbarToggleStyleButtonOptions(),

@ -107,11 +107,9 @@ class QuillSimpleToolbarConfigurations extends QuillSharedToolbarProperties {
this.showSearchButton = true, this.showSearchButton = true,
this.showSubscript = true, this.showSubscript = true,
this.showSuperscript = true, this.showSuperscript = true,
this.showClipboardCut = true, this.showClipboardCut = true,
this.showClipboardCopy = true, this.showClipboardCopy = true,
this.showClipboardPaste = true, this.showClipboardPaste = true,
this.linkStyleType = LinkStyleType.original, this.linkStyleType = LinkStyleType.original,
this.headerStyleType = HeaderStyleType.original, this.headerStyleType = HeaderStyleType.original,

@ -43,7 +43,9 @@ base class Line extends QuillContainer<Leaf?> {
if (parent!.isLast) { if (parent!.isLast) {
return null; return null;
} }
return parent!.next is Block ? (parent!.next as Block).first as Line? : parent!.next as Line?; return parent!.next is Block
? (parent!.next as Block).first as Line?
: parent!.next as Line?;
} }
@override @override
@ -51,7 +53,9 @@ base class Line extends QuillContainer<Leaf?> {
@override @override
Delta toDelta() { Delta toDelta() {
final delta = children.map((child) => child.toDelta()).fold(Delta(), (dynamic a, b) => a.concat(b)); final delta = children
.map((child) => child.toDelta())
.fold(Delta(), (dynamic a, b) => a.concat(b));
var attributes = style; var attributes = style;
if (parent is Block) { if (parent is Block) {
final block = parent as Block; final block = parent as Block;
@ -130,11 +134,17 @@ base class Line extends QuillContainer<Leaf?> {
final isLineFormat = (index + local == thisLength) && local == 1; final isLineFormat = (index + local == thisLength) && local == 1;
if (isLineFormat) { if (isLineFormat) {
assert(style.values.every((attr) => attr.scope == AttributeScope.block || attr.scope == AttributeScope.ignore), 'It is not allowed to apply inline attributes to line itself.'); assert(
style.values.every((attr) =>
attr.scope == AttributeScope.block ||
attr.scope == AttributeScope.ignore),
'It is not allowed to apply inline attributes to line itself.');
_format(style); _format(style);
} else { } else {
// Otherwise forward to children as it's an inline format update. // Otherwise forward to children as it's an inline format update.
assert(style.values.every((attr) => attr.scope == AttributeScope.inline || attr.scope == AttributeScope.ignore)); assert(style.values.every((attr) =>
attr.scope == AttributeScope.inline ||
attr.scope == AttributeScope.ignore));
assert(index + local != thisLength); assert(index + local != thisLength);
super.retain(index, local, style); super.retain(index, local, style);
} }
@ -205,15 +215,21 @@ base class Line extends QuillContainer<Leaf?> {
// Ensure that we're only unwrapping the block only if we unset a single // Ensure that we're only unwrapping the block only if we unset a single
// block format in the `parentStyle` and there are no more block formats // block format in the `parentStyle` and there are no more block formats
// left to unset. // left to unset.
if (blockStyle.value == null && parentStyle.containsKey(blockStyle.key) && parentStyle.length == 1) { if (blockStyle.value == null &&
parentStyle.containsKey(blockStyle.key) &&
parentStyle.length == 1) {
_unwrap(); _unwrap();
} else if (!const MapEquality().equals(newStyle.getBlocksExceptHeader(), parentStyle)) { } else if (!const MapEquality()
.equals(newStyle.getBlocksExceptHeader(), parentStyle)) {
_unwrap(); _unwrap();
// Block style now can contain multiple attributes // Block style now can contain multiple attributes
if (newStyle.attributes.keys.any(Attribute.exclusiveBlockKeys.contains)) { if (newStyle.attributes.keys
parentStyle.removeWhere((key, attr) => Attribute.exclusiveBlockKeys.contains(key)); .any(Attribute.exclusiveBlockKeys.contains)) {
parentStyle.removeWhere(
(key, attr) => Attribute.exclusiveBlockKeys.contains(key));
} }
parentStyle.removeWhere((key, attr) => newStyle?.attributes.keys.contains(key) ?? false); parentStyle.removeWhere(
(key, attr) => newStyle?.attributes.keys.contains(key) ?? false);
final parentStyleToMerge = Style.attr(parentStyle); final parentStyleToMerge = Style.attr(parentStyle);
newStyle = newStyle.mergeAll(parentStyleToMerge); newStyle = newStyle.mergeAll(parentStyleToMerge);
_applyBlockStyles(newStyle); _applyBlockStyles(newStyle);
@ -345,7 +361,8 @@ base class Line extends QuillContainer<Leaf?> {
void handle(Style style) { void handle(Style style) {
for (final attr in result.values) { for (final attr in result.values) {
if (!style.containsKey(attr.key) || (style.attributes[attr.key]?.value != attr.value)) { if (!style.containsKey(attr.key) ||
(style.attributes[attr.key]?.value != attr.value)) {
excluded.add(attr); excluded.add(attr);
} }
} }
@ -382,7 +399,8 @@ base class Line extends QuillContainer<Leaf?> {
/// Returns each node segment's offset in selection /// Returns each node segment's offset in selection
/// with its corresponding style or embed as a list /// with its corresponding style or embed as a list
List<OffsetValue> collectAllIndividualStylesAndEmbed(int offset, int len, {int beg = 0}) { List<OffsetValue> collectAllIndividualStylesAndEmbed(int offset, int len,
{int beg = 0}) {
final local = math.min(length - offset, len); final local = math.min(length - offset, len);
final result = <OffsetValue>[]; final result = <OffsetValue>[];
@ -414,7 +432,8 @@ base class Line extends QuillContainer<Leaf?> {
final remaining = len - local; final remaining = len - local;
if (remaining > 0 && nextLine != null) { if (remaining > 0 && nextLine != null) {
final rest = nextLine!.collectAllIndividualStylesAndEmbed(0, remaining, beg: local + beg); final rest = nextLine!
.collectAllIndividualStylesAndEmbed(0, remaining, beg: local + beg);
result.addAll(rest); result.addAll(rest);
} }
@ -484,7 +503,8 @@ base class Line extends QuillContainer<Leaf?> {
final remaining = len - local; final remaining = len - local;
if (remaining > 0 && nextLine != null) { if (remaining > 0 && nextLine != null) {
final rest = nextLine!.collectAllStylesWithOffsets(0, remaining, beg: local); final rest =
nextLine!.collectAllStylesWithOffsets(0, remaining, beg: local);
result.addAll(rest); result.addAll(rest);
} }

@ -108,7 +108,9 @@ class QuillController extends ChangeNotifier {
/// Only attributes applied to all characters within this range are /// Only attributes applied to all characters within this range are
/// included in the result. /// included in the result.
Style getSelectionStyle() { Style getSelectionStyle() {
return document.collectStyle(selection.start, selection.end - selection.start).mergeAll(toggledStyle); return document
.collectStyle(selection.start, selection.end - selection.start)
.mergeAll(toggledStyle);
} }
// Increases or decreases the indent of the current selection by 1. // Increases or decreases the indent of the current selection by 1.
@ -177,19 +179,23 @@ class QuillController extends ChangeNotifier {
/// Returns all styles and Embed for each node within selection /// Returns all styles and Embed for each node within selection
List<OffsetValue> getAllIndividualSelectionStylesAndEmbed() { List<OffsetValue> getAllIndividualSelectionStylesAndEmbed() {
final stylesAndEmbed = document.collectAllIndividualStyleAndEmbed(selection.start, selection.end - selection.start); final stylesAndEmbed = document.collectAllIndividualStyleAndEmbed(
selection.start, selection.end - selection.start);
return stylesAndEmbed; return stylesAndEmbed;
} }
/// Returns plain text for each node within selection /// Returns plain text for each node within selection
String getPlainText() { String getPlainText() {
final text = document.getPlainText(selection.start, selection.end - selection.start); final text =
document.getPlainText(selection.start, selection.end - selection.start);
return text; return text;
} }
/// Returns all styles for any character within the specified text range. /// Returns all styles for any character within the specified text range.
List<Style> getAllSelectionStyles() { List<Style> getAllSelectionStyles() {
final styles = document.collectAllStyles(selection.start, selection.end - selection.start)..add(toggledStyle); final styles = document.collectAllStyles(
selection.start, selection.end - selection.start)
..add(toggledStyle);
return styles; return styles;
} }
@ -235,7 +241,8 @@ class QuillController extends ChangeNotifier {
/// clear editor /// clear editor
void clear() { void clear() {
replaceText(0, plainTextEditingValue.text.length - 1, '', const TextSelection.collapsed(offset: 0)); replaceText(0, plainTextEditingValue.text.length - 1, '',
const TextSelection.collapsed(offset: 0));
} }
void replaceText( void replaceText(
@ -261,9 +268,13 @@ class QuillController extends ChangeNotifier {
delta.last.isInsert && delta.last.isInsert &&
// pasted text should not use toggledStyle // pasted text should not use toggledStyle
(data is! String || data.length < 2); (data is! String || data.length < 2);
if (shouldRetainDelta && toggledStyle.isNotEmpty && delta.length == 2 && delta.last.data == '\n') { if (shouldRetainDelta &&
toggledStyle.isNotEmpty &&
delta.length == 2 &&
delta.last.data == '\n') {
// if all attributes are inline, shouldRetainDelta should be false // if all attributes are inline, shouldRetainDelta should be false
final anyAttributeNotInline = toggledStyle.values.any((attr) => !attr.isInline); final anyAttributeNotInline =
toggledStyle.values.any((attr) => !attr.isInline);
if (!anyAttributeNotInline) { if (!anyAttributeNotInline) {
shouldRetainDelta = false; shouldRetainDelta = false;
} }
@ -308,7 +319,8 @@ class QuillController extends ChangeNotifier {
/// forward == true && textAfter.isEmpty /// forward == true && textAfter.isEmpty
/// Android only /// Android only
/// see https://github.com/singerdmx/flutter-quill/discussions/514 /// see https://github.com/singerdmx/flutter-quill/discussions/514
void handleDelete(int cursorPosition, bool forward) => onDelete?.call(cursorPosition, forward); void handleDelete(int cursorPosition, bool forward) =>
onDelete?.call(cursorPosition, forward);
void formatTextStyle(int index, int len, Style style) { void formatTextStyle(int index, int len, Style style) {
style.attributes.forEach((key, attr) { style.attributes.forEach((key, attr) {
@ -322,7 +334,9 @@ class QuillController extends ChangeNotifier {
Attribute? attribute, { Attribute? attribute, {
bool shouldNotifyListeners = true, bool shouldNotifyListeners = true,
}) { }) {
if (len == 0 && attribute!.isInline && attribute.key != Attribute.link.key) { if (len == 0 &&
attribute!.isInline &&
attribute.key != Attribute.link.key) {
// Add the attribute to our toggledStyle. // Add the attribute to our toggledStyle.
// It will be used later upon insertion. // It will be used later upon insertion.
toggledStyle = toggledStyle.put(attribute); toggledStyle = toggledStyle.put(attribute);
@ -332,7 +346,9 @@ class QuillController extends ChangeNotifier {
// Transform selection against the composed change and give priority to // Transform selection against the composed change and give priority to
// the change. This is needed in cases when format operation actually // the change. This is needed in cases when format operation actually
// inserts data into the document (e.g. embeds). // inserts data into the document (e.g. embeds).
final adjustedSelection = selection.copyWith(baseOffset: change.transformPosition(selection.baseOffset), extentOffset: change.transformPosition(selection.extentOffset)); final adjustedSelection = selection.copyWith(
baseOffset: change.transformPosition(selection.baseOffset),
extentOffset: change.transformPosition(selection.extentOffset));
if (selection != adjustedSelection) { if (selection != adjustedSelection) {
_updateSelection(adjustedSelection); _updateSelection(adjustedSelection);
} }
@ -341,7 +357,8 @@ class QuillController extends ChangeNotifier {
} }
} }
void formatSelection(Attribute? attribute, {bool shouldNotifyListeners = true}) { void formatSelection(Attribute? attribute,
{bool shouldNotifyListeners = true}) {
formatText( formatText(
selection.start, selection.start,
selection.end - selection.start, selection.end - selection.start,
@ -423,10 +440,13 @@ class QuillController extends ChangeNotifier {
super.dispose(); super.dispose();
} }
void _updateSelection(TextSelection textSelection, {bool insertNewline = false}) { void _updateSelection(TextSelection textSelection,
{bool insertNewline = false}) {
_selection = textSelection; _selection = textSelection;
final end = document.length - 1; final end = document.length - 1;
_selection = selection.copyWith(baseOffset: math.min(selection.baseOffset, end), extentOffset: math.min(selection.extentOffset, end)); _selection = selection.copyWith(
baseOffset: math.min(selection.baseOffset, end),
extentOffset: math.min(selection.extentOffset, end));
if (keepStyleOnNewLine) { if (keepStyleOnNewLine) {
if (insertNewline && selection.start > 0) { if (insertNewline && selection.start > 0) {
final style = document.collectStyle(selection.start - 1, 0); final style = document.collectStyle(selection.start - 1, 0);
@ -475,8 +495,10 @@ class QuillController extends ChangeNotifier {
if (!selection.isCollapsed) { if (!selection.isCollapsed) {
Clipboard.setData(ClipboardData(text: _pastePlainText)); Clipboard.setData(ClipboardData(text: _pastePlainText));
if (!copy) { if (!copy) {
if (readOnly) return false;
final sel = selection; final sel = selection;
replaceText(sel.start, sel.end - sel.start, '', TextSelection.collapsed(offset: sel.start)); replaceText(sel.start, sel.end - sel.start, '',
TextSelection.collapsed(offset: sel.start));
} }
return true; return true;
} }
@ -525,7 +547,8 @@ class QuillController extends ChangeNotifier {
selection.start, selection.start,
selection.end - selection.start, selection.end - selection.start,
plainText.text!, plainText.text!,
TextSelection.collapsed(offset: selection.start + plainText.text!.length), TextSelection.collapsed(
offset: selection.start + plainText.text!.length),
); );
updateEditor?.call(); updateEditor?.call();
return true; return true;
@ -566,16 +589,21 @@ class QuillController extends ChangeNotifier {
bool ignoreFocus = false, bool ignoreFocus = false,
bool shouldNotifyListeners = true, bool shouldNotifyListeners = true,
}) { }) {
final containsEmbed = insertedText.codeUnits.contains(Embed.kObjectReplacementInt); final containsEmbed =
insertedText = containsEmbed ? _adjustInsertedText(insertedText) : insertedText; insertedText.codeUnits.contains(Embed.kObjectReplacementInt);
insertedText =
containsEmbed ? _adjustInsertedText(insertedText) : insertedText;
replaceText(index, len, insertedText, textSelection, ignoreFocus: ignoreFocus, shouldNotifyListeners: shouldNotifyListeners); replaceText(index, len, insertedText, textSelection,
ignoreFocus: ignoreFocus, shouldNotifyListeners: shouldNotifyListeners);
_applyPasteStyleAndEmbed(insertedText, index, containsEmbed); _applyPasteStyleAndEmbed(insertedText, index, containsEmbed);
} }
void _applyPasteStyleAndEmbed(String insertedText, int start, bool containsEmbed) { void _applyPasteStyleAndEmbed(
if (insertedText == pastePlainText && pastePlainText != '' || containsEmbed) { String insertedText, int start, bool containsEmbed) {
if (insertedText == pastePlainText && pastePlainText != '' ||
containsEmbed) {
final pos = start; final pos = start;
for (final p in pasteStyleAndEmbed) { for (final p in pasteStyleAndEmbed) {
final offset = p.offset; final offset = p.offset;

@ -148,8 +148,8 @@ class QuillRawEditorState extends EditorState
/// Paste text from [Clipboard]. /// Paste text from [Clipboard].
@override @override
Future<void> pasteText(SelectionChangedCause cause) async { Future<void> pasteText(SelectionChangedCause cause) async {
if (await controller.clipboardPaste(
if ( await controller.clipboardPaste(updateEditor: () => bringIntoView(textEditingValue.selection.extent) ) ) { updateEditor: () => bringIntoView(textEditingValue.selection.extent))) {
return; return;
} }

@ -26,7 +26,8 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState
return; return;
} }
widget.configurations.controller.replaceTextWithEmbeds(diff.start, diff.deleted.length, diff.inserted, value.selection); widget.configurations.controller.replaceTextWithEmbeds(
diff.start, diff.deleted.length, diff.inserted, value.selection);
} }
@override @override

@ -107,6 +107,11 @@ abstract class QuillToolbarBaseValueButtonState<
} }
} }
typedef QuillToolbarToggleStyleBaseButton = QuillToolbarBaseValueButton<QuillToolbarToggleStyleButtonOptions, QuillToolbarToggleStyleButtonExtraOptions>; typedef QuillToolbarToggleStyleBaseButton = QuillToolbarBaseValueButton<
QuillToolbarToggleStyleButtonOptions,
typedef QuillToolbarToggleStyleBaseButtonState<W extends QuillToolbarToggleStyleBaseButton> = QuillToolbarBaseValueButtonState<W, QuillToolbarToggleStyleButtonOptions, QuillToolbarToggleStyleButtonExtraOptions, bool>; QuillToolbarToggleStyleButtonExtraOptions>;
typedef QuillToolbarToggleStyleBaseButtonState<
W extends QuillToolbarToggleStyleBaseButton>
= QuillToolbarBaseValueButtonState<W, QuillToolbarToggleStyleButtonOptions,
QuillToolbarToggleStyleButtonExtraOptions, bool>;

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../extensions.dart'; import '../../../../extensions.dart';
@ -15,9 +16,11 @@ class ClipboardMonitor {
Timer? _timer; Timer? _timer;
void monitorClipboard(bool add, void Function() listener) { void monitorClipboard(bool add, void Function() listener) {
if (kIsWeb) return;
if (add) { if (add) {
_clipboardStatus.addListener(listener); _clipboardStatus.addListener(listener);
_timer = Timer.periodic(const Duration(seconds: 1), (timer) => _clipboardStatus.update()); _timer = Timer.periodic(
const Duration(seconds: 1), (timer) => _clipboardStatus.update());
} else { } else {
_timer?.cancel(); _timer?.cancel();
_clipboardStatus.removeListener(listener); _clipboardStatus.removeListener(listener);
@ -26,9 +29,13 @@ class ClipboardMonitor {
} }
class QuillToolbarClipboardButton extends QuillToolbarToggleStyleBaseButton { class QuillToolbarClipboardButton extends QuillToolbarToggleStyleBaseButton {
QuillToolbarClipboardButton({required super.controller, required this.clipboard, required super.options, super.key}); QuillToolbarClipboardButton(
{required super.controller,
required this.clipboardAction,
super.options = const QuillToolbarToggleStyleButtonOptions(),
super.key});
final ClipboardAction clipboard; final ClipboardAction clipboardAction;
final ClipboardMonitor _monitor = ClipboardMonitor(); final ClipboardMonitor _monitor = ClipboardMonitor();
@ -36,16 +43,21 @@ class QuillToolbarClipboardButton extends QuillToolbarToggleStyleBaseButton {
State<StatefulWidget> createState() => QuillToolbarClipboardButtonState(); State<StatefulWidget> createState() => QuillToolbarClipboardButtonState();
} }
class QuillToolbarClipboardButtonState extends QuillToolbarToggleStyleBaseButtonState<QuillToolbarClipboardButton> { class QuillToolbarClipboardButtonState
extends QuillToolbarToggleStyleBaseButtonState<
QuillToolbarClipboardButton> {
@override @override
bool get currentStateValue { bool get currentStateValue {
switch (widget.clipboard) { switch (widget.clipboardAction) {
case ClipboardAction.cut: case ClipboardAction.cut:
return !controller.readOnly && !controller.selection.isCollapsed; return !controller.readOnly && !controller.selection.isCollapsed;
case ClipboardAction.copy: case ClipboardAction.copy:
return !controller.selection.isCollapsed; return !controller.selection.isCollapsed;
case ClipboardAction.paste: case ClipboardAction.paste:
return !controller.readOnly && widget._monitor._clipboardStatus.value == ClipboardStatus.pasteable; return !controller.readOnly &&
(kIsWeb ||
widget._monitor._clipboardStatus.value ==
ClipboardStatus.pasteable);
} }
} }
@ -53,33 +65,33 @@ class QuillToolbarClipboardButtonState extends QuillToolbarToggleStyleBaseButton
@override @override
void addExtraListener() { void addExtraListener() {
if ( widget.clipboard == ClipboardAction.paste) { if (widget.clipboardAction == ClipboardAction.paste) {
widget._monitor.monitorClipboard(true, _listenClipboardStatus); widget._monitor.monitorClipboard(true, _listenClipboardStatus);
} }
} }
@override @override
void removeExtraListener(covariant QuillToolbarClipboardButton oldWidget) { void removeExtraListener(covariant QuillToolbarClipboardButton oldWidget) {
if ( widget.clipboard == ClipboardAction.paste) { if (widget.clipboardAction == ClipboardAction.paste) {
oldWidget._monitor.monitorClipboard(false, _listenClipboardStatus); oldWidget._monitor.monitorClipboard(false, _listenClipboardStatus);
} }
} }
@override @override
String get defaultTooltip => switch (widget.clipboard) { String get defaultTooltip => switch (widget.clipboardAction) {
ClipboardAction.cut => 'cut', ClipboardAction.cut => 'Cut',
ClipboardAction.copy => context.loc.copy, ClipboardAction.copy => context.loc.copy,
ClipboardAction.paste => 'paste', ClipboardAction.paste => 'Paste',
}; };
IconData get _icon => switch (widget.clipboard) { IconData get _icon => switch (widget.clipboardAction) {
ClipboardAction.cut => Icons.cut_outlined, ClipboardAction.cut => Icons.cut_outlined,
ClipboardAction.copy => Icons.copy_outlined, ClipboardAction.copy => Icons.copy_outlined,
ClipboardAction.paste => Icons.paste_outlined, ClipboardAction.paste => Icons.paste_outlined,
}; };
void _onPressed() { void _onPressed() {
switch (widget.clipboard) { switch (widget.clipboardAction) {
case ClipboardAction.cut: case ClipboardAction.cut:
controller.clipboardSelection(false); controller.clipboardSelection(false);
break; break;

@ -37,7 +37,8 @@ class QuillToolbarToggleStyleButton extends QuillToolbarToggleStyleBaseButton {
} }
class QuillToolbarToggleStyleButtonState class QuillToolbarToggleStyleButtonState
extends QuillToolbarToggleStyleBaseButtonState<QuillToolbarToggleStyleButton> { extends QuillToolbarToggleStyleBaseButtonState<
QuillToolbarToggleStyleButton> {
Style get _selectionStyle => controller.getSelectionStyle(); Style get _selectionStyle => controller.getSelectionStyle();
@override @override

@ -8,7 +8,8 @@ import 'base_toolbar.dart';
import 'buttons/alignment/select_alignment_buttons.dart'; import 'buttons/alignment/select_alignment_buttons.dart';
import 'buttons/arrow_indicated_list_button.dart'; import 'buttons/arrow_indicated_list_button.dart';
class QuillSimpleToolbar extends StatelessWidget implements PreferredSizeWidget { class QuillSimpleToolbar extends StatelessWidget
implements PreferredSizeWidget {
const QuillSimpleToolbar({ const QuillSimpleToolbar({
required this.configurations, required this.configurations,
super.key, super.key,
@ -36,15 +37,23 @@ class QuillSimpleToolbar extends StatelessWidget implements PreferredSizeWidget
configurations.showBackgroundColorButton || configurations.showBackgroundColorButton ||
configurations.showClearFormat || configurations.showClearFormat ||
theEmbedButtons?.isNotEmpty == true, theEmbedButtons?.isNotEmpty == true,
configurations.showLeftAlignment || configurations.showCenterAlignment || configurations.showRightAlignment || configurations.showJustifyAlignment || configurations.showDirection, configurations.showLeftAlignment ||
configurations.showCenterAlignment ||
configurations.showRightAlignment ||
configurations.showJustifyAlignment ||
configurations.showDirection,
configurations.showHeaderStyle, configurations.showHeaderStyle,
configurations.showListNumbers || configurations.showListBullets || configurations.showListCheck || configurations.showCodeBlock, configurations.showListNumbers ||
configurations.showListBullets ||
configurations.showListCheck ||
configurations.showCodeBlock,
configurations.showQuote || configurations.showIndent, configurations.showQuote || configurations.showIndent,
configurations.showLink || configurations.showSearchButton configurations.showLink || configurations.showSearchButton
]; ];
List<Widget> childrenBuilder(BuildContext context) { List<Widget> childrenBuilder(BuildContext context) {
final toolbarConfigurations = context.requireQuillSimpleToolbarConfigurations; final toolbarConfigurations =
context.requireQuillSimpleToolbarConfigurations;
final globalIconSize = toolbarConfigurations.buttonOptions.base.iconSize; final globalIconSize = toolbarConfigurations.buttonOptions.base.iconSize;
@ -148,13 +157,25 @@ class QuillSimpleToolbar extends StatelessWidget implements PreferredSizeWidget
options: toolbarConfigurations.buttonOptions.clearFormat, options: toolbarConfigurations.buttonOptions.clearFormat,
), ),
if (theEmbedButtons != null) if (theEmbedButtons != null)
for (final builder in theEmbedButtons) builder(globalController, globalIconSize ?? kDefaultIconSize, context.quillToolbarBaseButtonOptions?.iconTheme, configurations.dialogTheme), for (final builder in theEmbedButtons)
if (configurations.showDividers && isButtonGroupShown[0] && (isButtonGroupShown[1] || isButtonGroupShown[2] || isButtonGroupShown[3] || isButtonGroupShown[4] || isButtonGroupShown[5])) builder(
globalController,
globalIconSize ?? kDefaultIconSize,
context.quillToolbarBaseButtonOptions?.iconTheme,
configurations.dialogTheme),
if (configurations.showDividers &&
isButtonGroupShown[0] &&
(isButtonGroupShown[1] ||
isButtonGroupShown[2] ||
isButtonGroupShown[3] ||
isButtonGroupShown[4] ||
isButtonGroupShown[5]))
divider, divider,
if (configurations.showAlignmentButtons) if (configurations.showAlignmentButtons)
QuillToolbarSelectAlignmentButtons( QuillToolbarSelectAlignmentButtons(
controller: globalController, controller: globalController,
options: toolbarConfigurations.buttonOptions.selectAlignmentButtons.copyWith( options: toolbarConfigurations.buttonOptions.selectAlignmentButtons
.copyWith(
showLeftAlignment: configurations.showLeftAlignment, showLeftAlignment: configurations.showLeftAlignment,
showCenterAlignment: configurations.showCenterAlignment, showCenterAlignment: configurations.showCenterAlignment,
showRightAlignment: configurations.showRightAlignment, showRightAlignment: configurations.showRightAlignment,
@ -167,20 +188,34 @@ class QuillSimpleToolbar extends StatelessWidget implements PreferredSizeWidget
options: toolbarConfigurations.buttonOptions.direction, options: toolbarConfigurations.buttonOptions.direction,
controller: globalController, controller: globalController,
), ),
if (configurations.showDividers && isButtonGroupShown[1] && (isButtonGroupShown[2] || isButtonGroupShown[3] || isButtonGroupShown[4] || isButtonGroupShown[5])) divider, if (configurations.showDividers &&
isButtonGroupShown[1] &&
(isButtonGroupShown[2] ||
isButtonGroupShown[3] ||
isButtonGroupShown[4] ||
isButtonGroupShown[5]))
divider,
if (configurations.showHeaderStyle) ...[ if (configurations.showHeaderStyle) ...[
if (configurations.headerStyleType.isOriginal) if (configurations.headerStyleType.isOriginal)
QuillToolbarSelectHeaderStyleDropdownButton( QuillToolbarSelectHeaderStyleDropdownButton(
controller: globalController, controller: globalController,
options: toolbarConfigurations.buttonOptions.selectHeaderStyleDropdownButton, options: toolbarConfigurations
.buttonOptions.selectHeaderStyleDropdownButton,
) )
else else
QuillToolbarSelectHeaderStyleButtons( QuillToolbarSelectHeaderStyleButtons(
controller: globalController, controller: globalController,
options: toolbarConfigurations.buttonOptions.selectHeaderStyleButtons, options:
toolbarConfigurations.buttonOptions.selectHeaderStyleButtons,
), ),
], ],
if (configurations.showDividers && configurations.showHeaderStyle && isButtonGroupShown[2] && (isButtonGroupShown[3] || isButtonGroupShown[4] || isButtonGroupShown[5])) divider, if (configurations.showDividers &&
configurations.showHeaderStyle &&
isButtonGroupShown[2] &&
(isButtonGroupShown[3] ||
isButtonGroupShown[4] ||
isButtonGroupShown[5]))
divider,
if (configurations.showListNumbers) if (configurations.showListNumbers)
QuillToolbarToggleStyleButton( QuillToolbarToggleStyleButton(
attribute: Attribute.ol, attribute: Attribute.ol,
@ -204,7 +239,9 @@ class QuillSimpleToolbar extends StatelessWidget implements PreferredSizeWidget
options: toolbarConfigurations.buttonOptions.codeBlock, options: toolbarConfigurations.buttonOptions.codeBlock,
controller: globalController, controller: globalController,
), ),
if (configurations.showDividers && isButtonGroupShown[3] && (isButtonGroupShown[4] || isButtonGroupShown[5])) ...[ if (configurations.showDividers &&
isButtonGroupShown[3] &&
(isButtonGroupShown[4] || isButtonGroupShown[5])) ...[
divider, divider,
], ],
if (configurations.showQuote) if (configurations.showQuote)
@ -225,7 +262,10 @@ class QuillSimpleToolbar extends StatelessWidget implements PreferredSizeWidget
isIncrease: false, isIncrease: false,
options: toolbarConfigurations.buttonOptions.indentDecrease, options: toolbarConfigurations.buttonOptions.indentDecrease,
), ),
if (configurations.showDividers && isButtonGroupShown[4] && isButtonGroupShown[5]) divider, if (configurations.showDividers &&
isButtonGroupShown[4] &&
isButtonGroupShown[5])
divider,
if (configurations.showLink) if (configurations.showLink)
toolbarConfigurations.linkStyleType.isOriginal toolbarConfigurations.linkStyleType.isOriginal
? QuillToolbarLinkStyleButton( ? QuillToolbarLinkStyleButton(
@ -305,11 +345,15 @@ class QuillSimpleToolbar extends StatelessWidget implements PreferredSizeWidget
return Container( return Container(
decoration: configurations.decoration ?? decoration: configurations.decoration ??
BoxDecoration( BoxDecoration(
color: configurations.color ?? Theme.of(context).canvasColor, color:
configurations.color ?? Theme.of(context).canvasColor,
), ),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
height: configurations.axis == Axis.horizontal ? _toolbarSize : null, height: configurations.axis == Axis.horizontal
width: configurations.axis == Axis.vertical ? _toolbarSize : null, ? _toolbarSize
: null,
width:
configurations.axis == Axis.vertical ? _toolbarSize : null,
), ),
child: QuillToolbarArrowIndicatedButtonList( child: QuillToolbarArrowIndicatedButtonList(
axis: configurations.axis, axis: configurations.axis,
@ -323,7 +367,9 @@ class QuillSimpleToolbar extends StatelessWidget implements PreferredSizeWidget
} }
@override @override
Size get preferredSize => configurations.axis == Axis.horizontal ? const Size.fromHeight(kDefaultToolbarSize) : const Size.fromWidth(kDefaultToolbarSize); Size get preferredSize => configurations.axis == Axis.horizontal
? const Size.fromHeight(kDefaultToolbarSize)
: const Size.fromWidth(kDefaultToolbarSize);
} }
/// The divider which is used for separation of buttons in the toolbar. /// The divider which is used for separation of buttons in the toolbar.
@ -339,10 +385,12 @@ class QuillToolbarDivider extends StatelessWidget {
}); });
/// Provides a horizontal divider for vertical toolbar. /// Provides a horizontal divider for vertical toolbar.
const QuillToolbarDivider.horizontal({Key? key, Color? color, double? space}) : this(Axis.horizontal, color: color, space: space, key: key); const QuillToolbarDivider.horizontal({Key? key, Color? color, double? space})
: this(Axis.horizontal, color: color, space: space, key: key);
/// Provides a horizontal divider for horizontal toolbar. /// Provides a horizontal divider for horizontal toolbar.
const QuillToolbarDivider.vertical({Key? key, Color? color, double? space}) : this(Axis.vertical, color: color, space: space, key: key); const QuillToolbarDivider.vertical({Key? key, Color? color, double? space})
: this(Axis.vertical, color: color, space: space, key: key);
/// The axis along which the toolbar is. /// The axis along which the toolbar is.
final Axis axis; final Axis axis;

@ -6,6 +6,7 @@ import 'package:test/test.dart';
void main() { void main() {
const testDocumentContents = 'data'; const testDocumentContents = 'data';
late QuillController controller; late QuillController controller;
WidgetsFlutterBinding.ensureInitialized();
setUp(() { setUp(() {
controller = QuillController.basic() controller = QuillController.basic()
@ -119,6 +120,26 @@ void main() {
expect((result[1].value as Embeddable).type, BlockEmbed.imageType); expect((result[1].value as Embeddable).type, BlockEmbed.imageType);
}); });
test('getAllIndividualSelectionStylesAndEmbed mixed', () {
controller
..replaceText(0, 4, 'bold plain italic', null)
..formatText(0, 4, Attribute.bold)
..formatText(11, 17, Attribute.italic)
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 14),
ChangeSource.local);
expect(controller.getPlainText(), 'ld plain ita',
reason: 'Selection spans 3 styles');
//
final result = controller.getAllIndividualSelectionStylesAndEmbed();
expect(result.length, 2);
expect(result[0].offset, 0);
expect(result[0].length, 2, reason: 'First style is 2 characters bold');
expect(result[0].value, const Style().put(Attribute.bold));
expect(result[1].offset, 9);
expect(result[1].length, 3, reason: 'Last style is 3 characters italic');
expect(result[1].value, const Style().put(Attribute.italic));
});
test('getPlainText', () { test('getPlainText', () {
controller.updateSelection( controller.updateSelection(
const TextSelection(baseOffset: 0, extentOffset: 4), const TextSelection(baseOffset: 0, extentOffset: 4),
@ -302,5 +323,37 @@ void main() {
expect(controller.document.toDelta(), expect(controller.document.toDelta(),
Delta()..insert('test $originalContents')); Delta()..insert('test $originalContents'));
}); });
test('clipboardSelection empty', () {
expect(controller.clipboardSelection(true), false,
reason: 'No effect when no selection');
expect(controller.clipboardSelection(false), false);
});
test('clipboardSelection', () {
controller
..replaceText(0, 4, 'bold plain italic', null)
..formatText(0, 4, Attribute.bold)
..formatText(11, 17, Attribute.italic)
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 14),
ChangeSource.local);
//
expect(controller.clipboardSelection(true), true);
expect(controller.document.length, 18,
reason: 'Copy does not change the document');
expect(controller.clipboardSelection(false), true);
expect(controller.document.length, 6, reason: 'Cut changes the document');
//
controller
..readOnly = true
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 4),
ChangeSource.local);
expect(controller.selection.isCollapsed, false);
expect(controller.clipboardSelection(true), true);
expect(controller.document.length, 6);
expect(controller.clipboardSelection(false), false);
expect(controller.document.length, 6,
reason: 'Cut not permitted on readOnly document');
});
}); });
} }

Loading…
Cancel
Save