Add: Clipboard toolbar buttons

pull/1843/head
Douglas Ward 1 year ago
parent 55cb35d9e7
commit 9e2d3d50c0
  1. 2
      example/lib/screens/quill/quill_screen.dart
  2. 14
      lib/src/models/config/editor/editor_configurations.dart
  3. 8
      lib/src/models/config/toolbar/simple_toolbar_button_options.dart
  4. 8
      lib/src/models/config/toolbar/simple_toolbar_configurations.dart
  5. 13
      lib/src/widgets/quill/quill_controller.dart
  6. 4
      lib/src/widgets/raw_editor/raw_editor.dart
  7. 16
      lib/src/widgets/toolbar/base_button/base_value_button.dart
  8. 125
      lib/src/widgets/toolbar/buttons/clipboard_button.dart
  9. 10
      lib/src/widgets/toolbar/buttons/toggle_style_button.dart
  10. 21
      lib/src/widgets/toolbar/simple_toolbar.dart
  11. 1
      test/bug_fix_test.dart
  12. 3
      test/widgets/editor_test.dart

@ -59,6 +59,7 @@ class _QuillScreenState extends State<QuillScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_controller.readOnly = _isReadOnly;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Flutter Quill'), title: const Text('Flutter Quill'),
@ -152,7 +153,6 @@ class _QuillScreenState extends State<QuillScreen> {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
sharedConfigurations: _sharedConfigurations, sharedConfigurations: _sharedConfigurations,
controller: _controller, controller: _controller,
readOnly: _isReadOnly,
), ),
scrollController: _editorScrollController, scrollController: _editorScrollController,
focusNode: _editorFocusNode, focusNode: _editorFocusNode,

@ -32,7 +32,6 @@ class QuillEditorConfigurations extends Equatable {
this.autoFocus = false, this.autoFocus = false,
this.expands = false, this.expands = false,
this.placeholder, this.placeholder,
this.readOnly = false,
this.disableClipboard = false, this.disableClipboard = false,
this.textSelectionThemeData, this.textSelectionThemeData,
this.showCursor, this.showCursor,
@ -95,7 +94,7 @@ class QuillEditorConfigurations extends Equatable {
/// by any shortcut or keyboard operation. The text is still selectable. /// by any shortcut or keyboard operation. The text is still selectable.
/// ///
/// Defaults to `false`. Must not be `null`. /// Defaults to `false`. Must not be `null`.
final bool readOnly; bool get readOnly => controller.readOnly;
/// Disable Clipboard features /// Disable Clipboard features
/// ///
@ -132,11 +131,11 @@ class QuillEditorConfigurations extends Equatable {
/// Whether the [onTapOutside] should be triggered or not /// Whether the [onTapOutside] should be triggered or not
/// Defaults to `true` /// Defaults to `true`
/// it have default implementation, check [onTapOuside] for more /// it have default implementation, check [onTapOutside] for more
final bool isOnTapOutsideEnabled; final bool isOnTapOutsideEnabled;
/// This will run only when [isOnTapOutsideEnabled] is true /// This will run only when [isOnTapOutsideEnabled] is true
/// by default on desktop and web it will unfocus /// by default on desktop and web it will un-focus
/// on mobile it will only unFocus if the kind property of /// on mobile it will only unFocus if the kind property of
/// event [PointerDownEvent] is [PointerDeviceKind.unknown] /// event [PointerDownEvent] is [PointerDeviceKind.unknown]
/// you can override this to fit your needs /// you can override this to fit your needs
@ -303,7 +302,7 @@ class QuillEditorConfigurations extends Equatable {
/// Additional list if links prefixes, which must not be prepended /// Additional list if links prefixes, which must not be prepended
/// with "https://" when [LinkMenuAction.launch] happened /// with "https://" when [LinkMenuAction.launch] happened
/// ///
/// Useful for deeplinks /// Useful for deep-links
final List<String> customLinkPrefixes; final List<String> customLinkPrefixes;
/// Configures the dialog theme. /// Configures the dialog theme.
@ -357,10 +356,10 @@ class QuillEditorConfigurations extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
placeholder, placeholder,
readOnly, controller.readOnly,
]; ];
// We might use code generator like freezed but sometimes it can be limitied // We might use code generator like freezed but sometimes it can be limited
// instead whatever there is a change to the parameters in this class please // instead whatever there is a change to the parameters in this class please
// regenerate this function using extension in vs code or plugin in intellij // regenerate this function using extension in vs code or plugin in intellij
@ -420,7 +419,6 @@ class QuillEditorConfigurations extends Equatable {
sharedConfigurations: sharedConfigurations ?? this.sharedConfigurations, sharedConfigurations: sharedConfigurations ?? this.sharedConfigurations,
controller: controller ?? this.controller, controller: controller ?? this.controller,
placeholder: placeholder ?? this.placeholder, placeholder: placeholder ?? this.placeholder,
readOnly: readOnly ?? this.readOnly,
disableClipboard: disableClipboard ?? this.disableClipboard, disableClipboard: disableClipboard ?? this.disableClipboard,
scrollable: scrollable ?? this.scrollable, scrollable: scrollable ?? this.scrollable,
scrollBottomInset: scrollBottomInset ?? this.scrollBottomInset, scrollBottomInset: scrollBottomInset ?? this.scrollBottomInset,

@ -75,6 +75,10 @@ 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.clipboardCopy = const QuillToolbarToggleStyleButtonOptions(),
this.clipboardPaste = const QuillToolbarToggleStyleButtonOptions(),
}); });
/// The base configurations for all the buttons which will apply to all /// The base configurations for all the buttons which will apply to all
@ -113,6 +117,10 @@ class QuillSimpleToolbarButtonOptions extends Equatable {
final QuillToolbarSearchButtonOptions search; final QuillToolbarSearchButtonOptions search;
final QuillToolbarToggleStyleButtonOptions clipboardCut;
final QuillToolbarToggleStyleButtonOptions clipboardCopy;
final QuillToolbarToggleStyleButtonOptions clipboardPaste;
/// The reason we call this buttons in the end because this is responsible /// The reason we call this buttons in the end because this is responsible
/// for all the header style buttons and not just one, you still /// for all the header style buttons and not just one, you still
/// can customize it and you also have child builder /// can customize it and you also have child builder

@ -107,6 +107,11 @@ 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.showClipboardCopy = true,
this.showClipboardPaste = true,
this.linkStyleType = LinkStyleType.original, this.linkStyleType = LinkStyleType.original,
this.headerStyleType = HeaderStyleType.original, this.headerStyleType = HeaderStyleType.original,
@ -195,6 +200,9 @@ class QuillSimpleToolbarConfigurations extends QuillSharedToolbarProperties {
final bool showSearchButton; final bool showSearchButton;
final bool showSubscript; final bool showSubscript;
final bool showSuperscript; final bool showSuperscript;
final bool showClipboardCut;
final bool showClipboardCopy;
final bool showClipboardPaste;
/// Toolbar items to display for controls of embed blocks /// Toolbar items to display for controls of embed blocks
final List<EmbedButtonBuilder>? embedButtons; final List<EmbedButtonBuilder>? embedButtons;

@ -6,18 +6,10 @@ import 'package:html/parser.dart' as html_parser;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:super_clipboard/super_clipboard.dart'; import 'package:super_clipboard/super_clipboard.dart';
import '../../../flutter_quill.dart';
import '../../../quill_delta.dart'; import '../../../quill_delta.dart';
import '../../models/documents/attribute.dart';
import '../../models/documents/delta_x.dart'; import '../../models/documents/delta_x.dart';
import '../../models/documents/document.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/documents/nodes/leaf.dart';
import '../../models/documents/style.dart';
import '../../models/structs/doc_change.dart';
import '../../models/structs/image_url.dart';
import '../../models/structs/offset_value.dart';
import '../../utils/delta.dart'; import '../../utils/delta.dart';
import '../../utils/embeds.dart';
typedef ReplaceTextCallback = bool Function(int index, int len, Object? data); typedef ReplaceTextCallback = bool Function(int index, int len, Object? data);
typedef DeleteCallback = void Function(int cursorPosition, bool forward); typedef DeleteCallback = void Function(int cursorPosition, bool forward);
@ -31,6 +23,7 @@ class QuillController extends ChangeNotifier {
this.onDelete, this.onDelete,
this.onSelectionCompleted, this.onSelectionCompleted,
this.onSelectionChanged, this.onSelectionChanged,
this.readOnly = false,
}) : _document = document, }) : _document = document,
_selection = selection; _selection = selection;
@ -464,7 +457,7 @@ class QuillController extends ChangeNotifier {
String get pastePlainText => _pastePlainText; String get pastePlainText => _pastePlainText;
List<OffsetValue> get pasteStyleAndEmbed => _pasteStyleAndEmbed; List<OffsetValue> get pasteStyleAndEmbed => _pasteStyleAndEmbed;
bool readOnly = false; bool readOnly;
ImageUrl? _copiedImageUrl; ImageUrl? _copiedImageUrl;
ImageUrl? get copiedImageUrl => _copiedImageUrl; ImageUrl? get copiedImageUrl => _copiedImageUrl;

@ -29,9 +29,7 @@ class QuillRawEditor extends StatefulWidget {
configurations.maxHeight == null || configurations.maxHeight == null ||
configurations.minHeight == null || configurations.minHeight == null ||
configurations.maxHeight! >= configurations.minHeight!, configurations.maxHeight! >= configurations.minHeight!,
'maxHeight cannot be null') { 'maxHeight cannot be null');
configurations.controller.readOnly = configurations.readOnly;
}
final QuillRawEditorConfigurations configurations; final QuillRawEditorConfigurations configurations;

@ -26,7 +26,9 @@ abstract class QuillToolbarBaseValueButtonState<
QuillController get controller => widget.controller; QuillController get controller => widget.controller;
late V currentValue; V? _currentValue;
V get currentValue => _currentValue!;
set currentValue (V value) => _currentValue = value;
/// Callback to query the widget's state for the value to be assigned to currentState /// Callback to query the widget's state for the value to be assigned to currentState
V get currentStateValue; V get currentStateValue;
@ -35,6 +37,7 @@ abstract class QuillToolbarBaseValueButtonState<
void initState() { void initState() {
super.initState(); super.initState();
controller.addListener(didChangeEditingValue); controller.addListener(didChangeEditingValue);
addExtraListener();
} }
@override @override
@ -50,6 +53,7 @@ abstract class QuillToolbarBaseValueButtonState<
@override @override
void dispose() { void dispose() {
controller.removeListener(didChangeEditingValue); controller.removeListener(didChangeEditingValue);
removeExtraListener(widget);
super.dispose(); super.dispose();
} }
@ -58,11 +62,17 @@ abstract class QuillToolbarBaseValueButtonState<
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.controller != controller) { if (oldWidget.controller != controller) {
oldWidget.controller.removeListener(didChangeEditingValue); oldWidget.controller.removeListener(didChangeEditingValue);
removeExtraListener(oldWidget);
controller.addListener(didChangeEditingValue); controller.addListener(didChangeEditingValue);
addExtraListener();
currentValue = currentStateValue; currentValue = currentStateValue;
} }
} }
/// Extra listeners allow a subclass to listen to an external event that can affect its currentValue
void addExtraListener () {}
void removeExtraListener (covariant W oldWidget) {}
String get defaultTooltip; String get defaultTooltip;
String get tooltip { String get tooltip {
@ -96,3 +106,7 @@ abstract class QuillToolbarBaseValueButtonState<
baseButtonExtraOptions?.afterButtonPressed; baseButtonExtraOptions?.afterButtonPressed;
} }
} }
typedef QuillToolbarToggleStyleBaseButton = QuillToolbarBaseValueButton<QuillToolbarToggleStyleButtonOptions, QuillToolbarToggleStyleButtonExtraOptions>;
typedef QuillToolbarToggleStyleBaseButtonState<W extends QuillToolbarToggleStyleBaseButton> = QuillToolbarBaseValueButtonState<W, QuillToolbarToggleStyleButtonOptions, QuillToolbarToggleStyleButtonExtraOptions, bool>;

@ -0,0 +1,125 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../../../extensions.dart';
import '../../../../flutter_quill.dart';
import '../../../l10n/extensions/localizations.dart';
import '../base_button/base_value_button.dart';
enum ClipboardAction { cut, copy, paste }
class ClipboardMonitor {
final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier();
Timer? _timer;
void monitorClipboard ( bool add, void Function() listener ) {
if ( add ) {
_clipboardStatus.addListener(listener);
_timer = Timer.periodic(const Duration(seconds: 1), (timer) => _clipboardStatus.update());
} else {
_timer?.cancel();
_clipboardStatus.removeListener(listener);
}
}
}
class QuillToolbarClipboardButton extends QuillToolbarToggleStyleBaseButton {
QuillToolbarClipboardButton({required super.controller, required this.clipboard, required super.options, super.key});
final ClipboardAction clipboard;
final ClipboardMonitor _monitor = ClipboardMonitor();
@override
State<StatefulWidget> createState() => QuillToolbarClipboardButtonState();
}
class QuillToolbarClipboardButtonState extends QuillToolbarToggleStyleBaseButtonState<QuillToolbarClipboardButton> {
@override
bool get currentStateValue {
switch (widget.clipboard) {
case ClipboardAction.cut:
return !controller.readOnly && !controller.selection.isCollapsed;
case ClipboardAction.copy:
return !controller.selection.isCollapsed;
case ClipboardAction.paste:
return !controller.readOnly && widget._monitor._clipboardStatus.value == ClipboardStatus.pasteable;
}
}
void _listenClipboardStatus () => didChangeEditingValue();
@override
void addExtraListener () {
if ( widget.clipboard == ClipboardAction.paste) {
widget._monitor.monitorClipboard(true, _listenClipboardStatus);
}
}
@override
void removeExtraListener (covariant QuillToolbarClipboardButton oldWidget) {
if ( widget.clipboard == ClipboardAction.paste) {
oldWidget._monitor.monitorClipboard(false, _listenClipboardStatus);
}
}
@override
String get defaultTooltip => switch (widget.clipboard) {
ClipboardAction.cut => 'cut',
ClipboardAction.copy => context.loc.copy,
ClipboardAction.paste => 'paste',
};
IconData get _icon => switch (widget.clipboard) {
ClipboardAction.cut => Icons.cut_outlined,
ClipboardAction.copy => Icons.copy_outlined,
ClipboardAction.paste => Icons.paste_outlined,
};
void _onPressed() {
switch (widget.clipboard) {
case ClipboardAction.cut:
controller.clipboardSelection(false);
break;
case ClipboardAction.copy:
controller.clipboardSelection(true);
break;
case ClipboardAction.paste:
controller.clipboardPaste();
break;
}
afterButtonPressed?.call();
}
@override
Widget build(BuildContext context) {
final childBuilder = options.childBuilder ??
context.quillToolbarBaseButtonOptions?.childBuilder;
if (childBuilder != null) {
return childBuilder(
options,
QuillToolbarToggleStyleButtonExtraOptions(
context: context,
controller: controller,
onPressed: _onPressed,
isToggled: currentValue,
),
);
}
return UtilityWidgets.maybeTooltip(
message: tooltip,
child: QuillToolbarIconButton(
icon: Icon(
_icon,
size: iconSize * iconButtonFactor,
),
isSelected: false,
onPressed: currentValue ? _onPressed : null,
afterPressed: afterButtonPressed,
iconTheme: iconTheme,
));
}
}

@ -21,9 +21,7 @@ typedef ToggleStyleButtonBuilder = Widget Function(
QuillIconTheme? iconTheme, QuillIconTheme? iconTheme,
]); ]);
class QuillToolbarToggleStyleButton extends QuillToolbarBaseValueButton< class QuillToolbarToggleStyleButton extends QuillToolbarToggleStyleBaseButton {
QuillToolbarToggleStyleButtonOptions,
QuillToolbarToggleStyleButtonExtraOptions> {
const QuillToolbarToggleStyleButton({ const QuillToolbarToggleStyleButton({
required super.controller, required super.controller,
required this.attribute, required this.attribute,
@ -39,11 +37,7 @@ class QuillToolbarToggleStyleButton extends QuillToolbarBaseValueButton<
} }
class QuillToolbarToggleStyleButtonState class QuillToolbarToggleStyleButtonState
extends QuillToolbarBaseValueButtonState< extends QuillToolbarToggleStyleBaseButtonState<QuillToolbarToggleStyleButton> {
QuillToolbarToggleStyleButton,
QuillToolbarToggleStyleButtonOptions,
QuillToolbarToggleStyleButtonExtraOptions,
bool> {
Style get _selectionStyle => controller.getSelectionStyle(); Style get _selectionStyle => controller.getSelectionStyle();
@override @override

@ -7,6 +7,7 @@ import '../utils/provider.dart';
import 'base_toolbar.dart'; 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';
import 'buttons/clipboard_button.dart';
class QuillSimpleToolbar extends StatelessWidget class QuillSimpleToolbar extends StatelessWidget
implements PreferredSizeWidget { implements PreferredSizeWidget {
@ -291,6 +292,26 @@ class QuillSimpleToolbar extends StatelessWidget
controller: globalController, controller: globalController,
options: toolbarConfigurations.buttonOptions.search, options: toolbarConfigurations.buttonOptions.search,
), ),
if (configurations.showClipboardCut)
QuillToolbarClipboardButton(
options: toolbarConfigurations.buttonOptions.clipboardCut,
controller: globalController,
clipboard: ClipboardAction.cut,
),
if (configurations.showClipboardCopy)
QuillToolbarClipboardButton(
options: toolbarConfigurations.buttonOptions.clipboardCopy,
controller: globalController,
clipboard: ClipboardAction.copy,
),
if (configurations.showClipboardPaste)
QuillToolbarClipboardButton(
options: toolbarConfigurations.buttonOptions.clipboardPaste,
controller: globalController,
clipboard: ClipboardAction.paste,
),
if (configurations.customButtons.isNotEmpty) ...[ if (configurations.customButtons.isNotEmpty) ...[
if (configurations.showDividers) if (configurations.showDividers)
QuillToolbarDivider( QuillToolbarDivider(

@ -65,7 +65,6 @@ void main() {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
controller: controller, controller: controller,
// ignore: avoid_redundant_argument_values // ignore: avoid_redundant_argument_values
readOnly: false,
), ),
); );
}); });

@ -27,7 +27,6 @@ void main() {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
controller: controller, controller: controller,
// ignore: avoid_redundant_argument_values // ignore: avoid_redundant_argument_values
readOnly: false,
), ),
), ),
), ),
@ -47,7 +46,6 @@ void main() {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
controller: controller, controller: controller,
// ignore: avoid_redundant_argument_values // ignore: avoid_redundant_argument_values
readOnly: false,
autoFocus: true, autoFocus: true,
expands: true, expands: true,
contentInsertionConfiguration: ContentInsertionConfiguration( contentInsertionConfiguration: ContentInsertionConfiguration(
@ -121,7 +119,6 @@ void main() {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
controller: controller, controller: controller,
// ignore: avoid_redundant_argument_values // ignore: avoid_redundant_argument_values
readOnly: false,
autoFocus: true, autoFocus: true,
expands: true, expands: true,
contextMenuBuilder: customBuilder, contextMenuBuilder: customBuilder,

Loading…
Cancel
Save