diff --git a/lib/src/widgets/raw_editor/raw_editor_context_menu_items.dart b/lib/src/widgets/raw_editor/raw_editor_context_menu_items.dart new file mode 100644 index 00000000..39541e6c --- /dev/null +++ b/lib/src/widgets/raw_editor/raw_editor_context_menu_items.dart @@ -0,0 +1,160 @@ +// import 'package:flutter/widgets.dart' show TextSelectionDelegate; + +// class QuillEditorTextSelectionDelegate implements TextSelectionDelegate { +// /// Copy current selection to [Clipboard]. +// @override +// void copySelection(SelectionChangedCause cause) { +// controller.copiedImageUrl = null; +// _pastePlainText = controller.getPlainText(); +// _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); + +// final selection = textEditingValue.selection; +// final text = textEditingValue.text; +// if (selection.isCollapsed) { +// return; +// } +// Clipboard.setData(ClipboardData(text: selection.textInside(text))); + +// if (cause == SelectionChangedCause.toolbar) { +// bringIntoView(textEditingValue.selection.extent); + +// // Collapse the selection and hide the toolbar and handles. +// userUpdateTextEditingValue( +// TextEditingValue( +// text: textEditingValue.text, +// selection: +// TextSelection.collapsed(offset: textEditingValue.selection.end), +// ), +// SelectionChangedCause.toolbar, +// ); +// } +// } + +// /// Cut current selection to [Clipboard]. +// @override +// void cutSelection(SelectionChangedCause cause) { +// controller.copiedImageUrl = null; +// _pastePlainText = controller.getPlainText(); +// _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); + +// if (widget.configurations.readOnly) { +// return; +// } +// final selection = textEditingValue.selection; +// final text = textEditingValue.text; +// if (selection.isCollapsed) { +// return; +// } +// Clipboard.setData(ClipboardData(text: selection.textInside(text))); +// _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); + +// if (cause == SelectionChangedCause.toolbar) { +// bringIntoView(textEditingValue.selection.extent); +// hideToolbar(); +// } +// } + +// /// Paste text from [Clipboard]. +// @override +// Future pasteText(SelectionChangedCause cause) async { +// if (widget.configurations.readOnly) { +// return; +// } + +// if (controller.copiedImageUrl != null) { +// final index = textEditingValue.selection.baseOffset; +// final length = textEditingValue.selection.extentOffset - index; +// final copied = controller.copiedImageUrl!; +// controller.replaceText( +// index, +// length, +// BlockEmbed.image(copied.url), +// null, +// ); +// if (copied.styleString.isNotEmpty) { +// controller.formatText( +// getEmbedNode(controller, index + 1).offset, +// 1, +// StyleAttribute(copied.styleString), +// ); +// } +// controller.copiedImageUrl = null; +// await Clipboard.setData( +// const ClipboardData(text: ''), +// ); +// return; +// } + +// final selection = textEditingValue.selection; +// if (!selection.isValid) { +// return; +// } +// // Snapshot the input before using `await`. +// // See https://github.com/flutter/flutter/issues/11427 +// final text = await Clipboard.getData(Clipboard.kTextPlain); +// if (text != null) { +// _replaceText( +// ReplaceTextIntent( +// textEditingValue, +// text.text!, +// selection, +// cause, +// ), +// ); + +// bringIntoView(textEditingValue.selection.extent); + +// // Collapse the selection and hide the toolbar and handles. +// userUpdateTextEditingValue( +// TextEditingValue( +// text: textEditingValue.text, +// selection: TextSelection.collapsed( +// offset: textEditingValue.selection.end, +// ), +// ), +// cause, +// ); + +// return; +// } + +// final onImagePaste = widget.configurations.onImagePaste; +// if (onImagePaste != null) { +// final reader = await ClipboardReader.readClipboard(); +// if (!reader.canProvide(Formats.png)) { +// return; +// } +// reader.getFile(Formats.png, (value) async { +// final image = value; + +// final imageUrl = await onImagePaste(await image.readAll()); +// if (imageUrl == null) { +// return; +// } + +// controller.replaceText( +// textEditingValue.selection.end, +// 0, +// BlockEmbed.image(imageUrl), +// null, +// ); +// }); +// } +// } + +// /// Select the entire text value. +// @override +// void selectAll(SelectionChangedCause cause) { +// userUpdateTextEditingValue( +// textEditingValue.copyWith( +// selection: TextSelection( +// baseOffset: 0, extentOffset: textEditingValue.text.length), +// ), +// cause, +// ); + +// if (cause == SelectionChangedCause.toolbar) { +// bringIntoView(textEditingValue.selection.extent); +// } +// } +// } diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart index 4b11d967..702f312c 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -10,11 +10,12 @@ import 'package:flutter/rendering.dart' show RenderAbstractViewport; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart' show - LogicalKeyboardKey, - RawKeyDownEvent, - HardwareKeyboard, Clipboard, ClipboardData, + HardwareKeyboard, + LogicalKeyboardKey, + RawKeyDownEvent, + SystemChannels, TextInputControl; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' show KeyboardVisibilityController; @@ -119,10 +120,186 @@ class QuillRawEditorState extends EditorState /// platform's default selection menu for [QuillRawEditor]. /// /// Copied from [EditableTextState]. + // List get contextMenuButtonItems { + // return EditableText.getEditableButtonItems( + // clipboardStatus: _clipboardStatus.value, + // onLiveTextInput: null, + // onCopy: copyEnabled + // ? () => copySelection(SelectionChangedCause.toolbar) + // : null, + // onCut: + // cutEnabled ? () => cutSelection(SelectionChangedCause.toolbar) : null, + // onPaste: + // pasteEnabled ? () => pasteText(SelectionChangedCause.toolbar) : null, + // onSelectAll: selectAllEnabled + // ? () => selectAll(SelectionChangedCause.toolbar) + // : null, + // onLookUp: null, + // onSearchWeb: null, + // onShare: null, + // ); + // } + + /// Copy current selection to [Clipboard]. + @override + void copySelection(SelectionChangedCause cause) { + controller.copiedImageUrl = null; + _pastePlainText = controller.getPlainText(); + _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); + + final selection = textEditingValue.selection; + final text = textEditingValue.text; + if (selection.isCollapsed) { + return; + } + Clipboard.setData(ClipboardData(text: selection.textInside(text))); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + + // Collapse the selection and hide the toolbar and handles. + userUpdateTextEditingValue( + TextEditingValue( + text: textEditingValue.text, + selection: + TextSelection.collapsed(offset: textEditingValue.selection.end), + ), + SelectionChangedCause.toolbar, + ); + } + } + + /// Cut current selection to [Clipboard]. + @override + void cutSelection(SelectionChangedCause cause) { + controller.copiedImageUrl = null; + _pastePlainText = controller.getPlainText(); + _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); + + if (widget.configurations.readOnly) { + return; + } + final selection = textEditingValue.selection; + final text = textEditingValue.text; + if (selection.isCollapsed) { + return; + } + Clipboard.setData(ClipboardData(text: selection.textInside(text))); + _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(); + } + } + + /// Paste text from [Clipboard]. + @override + Future pasteText(SelectionChangedCause cause) async { + if (widget.configurations.readOnly) { + return; + } + + if (controller.copiedImageUrl != null) { + final index = textEditingValue.selection.baseOffset; + final length = textEditingValue.selection.extentOffset - index; + final copied = controller.copiedImageUrl!; + controller.replaceText( + index, + length, + BlockEmbed.image(copied.url), + null, + ); + if (copied.styleString.isNotEmpty) { + controller.formatText( + getEmbedNode(controller, index + 1).offset, + 1, + StyleAttribute(copied.styleString), + ); + } + controller.copiedImageUrl = null; + await Clipboard.setData( + const ClipboardData(text: ''), + ); + return; + } + + final selection = textEditingValue.selection; + if (!selection.isValid) { + return; + } + // Snapshot the input before using `await`. + // See https://github.com/flutter/flutter/issues/11427 + final text = await Clipboard.getData(Clipboard.kTextPlain); + if (text != null) { + _replaceText( + ReplaceTextIntent( + textEditingValue, + text.text!, + selection, + cause, + ), + ); + + bringIntoView(textEditingValue.selection.extent); + + // Collapse the selection and hide the toolbar and handles. + userUpdateTextEditingValue( + TextEditingValue( + text: textEditingValue.text, + selection: TextSelection.collapsed( + offset: textEditingValue.selection.end, + ), + ), + cause, + ); + + return; + } + + final onImagePaste = widget.configurations.onImagePaste; + if (onImagePaste != null) { + final reader = await ClipboardReader.readClipboard(); + if (!reader.canProvide(Formats.png)) { + return; + } + reader.getFile(Formats.png, (value) async { + final image = value; + + final imageUrl = await onImagePaste(await image.readAll()); + if (imageUrl == null) { + return; + } + + controller.replaceText( + textEditingValue.selection.end, + 0, + BlockEmbed.image(imageUrl), + null, + ); + }); + } + } + + /// Select the entire text value. + @override + void selectAll(SelectionChangedCause cause) { + userUpdateTextEditingValue( + textEditingValue.copyWith( + selection: TextSelection( + baseOffset: 0, extentOffset: textEditingValue.text.length), + ), + cause, + ); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + } + } + List get contextMenuButtonItems { return EditableText.getEditableButtonItems( clipboardStatus: _clipboardStatus.value, - onLiveTextInput: null, onCopy: copyEnabled ? () => copySelection(SelectionChangedCause.toolbar) : null, @@ -133,12 +310,70 @@ class QuillRawEditorState extends EditorState onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) : null, - onLookUp: null, - onSearchWeb: null, - onShare: null, + onLookUp: lookUpEnabled + ? () => lookUpSelection(SelectionChangedCause.toolbar) + : null, + onSearchWeb: searchWebEnabled + ? () => searchWebForSelection(SelectionChangedCause.toolbar) + : null, + onShare: shareEnabled + ? () => shareSelection(SelectionChangedCause.toolbar) + : null, + onLiveTextInput: liveTextInputEnabled ? () {} : null, ); } + /// Look up the current selection, + /// as in the "Look Up" edit menu button on iOS. + /// + /// Currently this is only implemented for iOS. + /// + /// Throws an error if the selection is empty or collapsed. + Future lookUpSelection(SelectionChangedCause cause) async { + final text = textEditingValue.selection.textInside(textEditingValue.text); + if (text.isEmpty) { + return; + } + await SystemChannels.platform.invokeMethod( + 'LookUp.invoke', + text, + ); + } + + /// Launch a web search on the current selection, + /// as in the "Search Web" edit menu button on iOS. + /// + /// Currently this is only implemented for iOS. + /// + /// When 'obscureText' is true or the selection is empty, + /// this function will not do anything + Future searchWebForSelection(SelectionChangedCause cause) async { + final text = textEditingValue.selection.textInside(textEditingValue.text); + if (text.isNotEmpty) { + await SystemChannels.platform.invokeMethod( + 'SearchWeb.invoke', + text, + ); + } + } + + /// Launch the share interface for the current selection, + /// as in the "Share" edit menu button on iOS. + /// + /// Currently this is only implemented for iOS. + /// + /// When 'obscureText' is true or the selection is empty, + /// this function will not do anything + Future shareSelection(SelectionChangedCause cause) async { + final text = textEditingValue.selection.textInside(textEditingValue.text); + if (text.isNotEmpty) { + await SystemChannels.platform.invokeMethod( + 'Share.invoke', + text, + ); + } + } + /// Returns the anchor points for the default context menu. /// /// Copied from [EditableTextState]. @@ -1262,163 +1497,6 @@ class QuillRawEditorState extends EditorState ); } - /// Copy current selection to [Clipboard]. - @override - void copySelection(SelectionChangedCause cause) { - controller.copiedImageUrl = null; - _pastePlainText = controller.getPlainText(); - _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); - - final selection = textEditingValue.selection; - final text = textEditingValue.text; - if (selection.isCollapsed) { - return; - } - Clipboard.setData(ClipboardData(text: selection.textInside(text))); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - - // Collapse the selection and hide the toolbar and handles. - userUpdateTextEditingValue( - TextEditingValue( - text: textEditingValue.text, - selection: - TextSelection.collapsed(offset: textEditingValue.selection.end), - ), - SelectionChangedCause.toolbar, - ); - } - } - - /// Cut current selection to [Clipboard]. - @override - void cutSelection(SelectionChangedCause cause) { - controller.copiedImageUrl = null; - _pastePlainText = controller.getPlainText(); - _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); - - if (widget.configurations.readOnly) { - return; - } - final selection = textEditingValue.selection; - final text = textEditingValue.text; - if (selection.isCollapsed) { - return; - } - Clipboard.setData(ClipboardData(text: selection.textInside(text))); - _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - hideToolbar(); - } - } - - /// Paste text from [Clipboard]. - @override - Future pasteText(SelectionChangedCause cause) async { - if (widget.configurations.readOnly) { - return; - } - - if (controller.copiedImageUrl != null) { - final index = textEditingValue.selection.baseOffset; - final length = textEditingValue.selection.extentOffset - index; - final copied = controller.copiedImageUrl!; - controller.replaceText( - index, - length, - BlockEmbed.image(copied.url), - null, - ); - if (copied.styleString.isNotEmpty) { - controller.formatText( - getEmbedNode(controller, index + 1).offset, - 1, - StyleAttribute(copied.styleString), - ); - } - controller.copiedImageUrl = null; - await Clipboard.setData( - const ClipboardData(text: ''), - ); - return; - } - - final selection = textEditingValue.selection; - if (!selection.isValid) { - return; - } - // Snapshot the input before using `await`. - // See https://github.com/flutter/flutter/issues/11427 - final text = await Clipboard.getData(Clipboard.kTextPlain); - if (text != null) { - _replaceText( - ReplaceTextIntent( - textEditingValue, - text.text!, - selection, - cause, - ), - ); - - bringIntoView(textEditingValue.selection.extent); - - // Collapse the selection and hide the toolbar and handles. - userUpdateTextEditingValue( - TextEditingValue( - text: textEditingValue.text, - selection: TextSelection.collapsed( - offset: textEditingValue.selection.end, - ), - ), - cause, - ); - - return; - } - - final onImagePaste = widget.configurations.onImagePaste; - if (onImagePaste != null) { - final reader = await ClipboardReader.readClipboard(); - if (!reader.canProvide(Formats.png)) { - return; - } - reader.getFile(Formats.png, (value) async { - final image = value; - - final imageUrl = await onImagePaste(await image.readAll()); - if (imageUrl == null) { - return; - } - - controller.replaceText( - textEditingValue.selection.end, - 0, - BlockEmbed.image(imageUrl), - null, - ); - }); - } - } - - /// Select the entire text value. - @override - void selectAll(SelectionChangedCause cause) { - userUpdateTextEditingValue( - textEditingValue.copyWith( - selection: TextSelection( - baseOffset: 0, extentOffset: textEditingValue.text.length), - ), - cause, - ); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - } - } - @override bool get wantKeepAlive => widget.configurations.focusNode.hasFocus; diff --git a/lib/src/widgets/toolbar/simple_toolbar.dart b/lib/src/widgets/toolbar/simple_toolbar.dart index a1da0762..c21912f7 100644 --- a/lib/src/widgets/toolbar/simple_toolbar.dart +++ b/lib/src/widgets/toolbar/simple_toolbar.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../../../flutter_quill.dart'; +import '../../../translations.dart'; import '../../models/config/toolbar/toolbar_configurations.dart'; import '../../models/documents/attribute.dart'; import '../utils/provider.dart'; @@ -49,7 +51,7 @@ class QuillSimpleToolbar extends StatelessWidget configurations.showLink || configurations.showSearchButton ]; - List childrenBuilder(context) { + List childrenBuilder(BuildContext context) { final toolbarConfigurations = context.requireQuillSimpleToolbarConfigurations;