diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index fcf1e9d3..c65898f2 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -467,12 +467,9 @@ class QuillEditorState extends State readOnly: widget.readOnly, placeholder: widget.placeholder, onLaunchUrl: widget.onLaunchUrl, - toolbarOptions: ToolbarOptions( - copy: showSelectionToolbar, - cut: showSelectionToolbar, - paste: showSelectionToolbar, - selectAll: showSelectionToolbar, - ), + contextMenuBuilder: showSelectionToolbar + ? RawEditor.defaultContextMenuBuilder + : null, showSelectionHandles: isMobile(theme.platform), showCursor: widget.showCursor, cursorStyle: CursorStyle( diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index b42c76bc..8c6c61af 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -57,12 +57,7 @@ class RawEditor extends StatefulWidget { this.readOnly = false, this.placeholder, this.onLaunchUrl, - this.toolbarOptions = const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), + this.contextMenuBuilder = defaultContextMenuBuilder, this.showSelectionHandles = false, bool? showCursor, this.textCapitalization = TextCapitalization.none, @@ -114,11 +109,24 @@ class RawEditor extends StatefulWidget { /// a link in the document. final ValueChanged? onLaunchUrl; - /// Configuration of toolbar options. + /// Builds the text selection toolbar when requested by the user. + /// + /// See also: + /// * [EditableText.contextMenuBuilder], which builds the default + /// text selection toolbar for [EditableText]. /// - /// By default, all options are enabled. If [readOnly] is true, - /// paste and cut will be disabled regardless. - final ToolbarOptions toolbarOptions; + /// If not provided, no context menu will be shown. + final QuillEditorContextMenuBuilder? contextMenuBuilder; + + static Widget defaultContextMenuBuilder( + BuildContext context, + RawEditorState state, + ) { + return AdaptiveTextSelectionToolbar.buttonItems( + buttonItems: state.contextMenuButtonItems, + anchors: state.contextMenuAnchors, + ); + } /// Whether to show selection handles. /// @@ -293,6 +301,76 @@ class RawEditorState extends EditorState TextDirection get _textDirection => Directionality.of(context); + /// Returns the [ContextMenuButtonItem]s representing the buttons in this + /// platform's default selection menu for [RawEditor]. + /// + /// Copied from [EditableTextState]. + List get contextMenuButtonItems { + return EditableText.getEditableButtonItems( + clipboardStatus: _clipboardStatus.value, + onCopy: copyEnabled + ? () => copySelection(SelectionChangedCause.toolbar) + : null, + onCut: cutEnabled + ? () => cutSelection(SelectionChangedCause.toolbar) + : null, + onPaste: pasteEnabled + ? () => pasteText(SelectionChangedCause.toolbar) + : null, + onSelectAll: selectAllEnabled + ? () => selectAll(SelectionChangedCause.toolbar) + : null, + ); + } + + /// Returns the anchor points for the default context menu. + /// + /// Copied from [EditableTextState]. + TextSelectionToolbarAnchors get contextMenuAnchors { + final glyphHeights = _getGlyphHeights(); + final selection = textEditingValue.selection; + final points = renderEditor.getEndpointsForSelection(selection); + return TextSelectionToolbarAnchors.fromSelection( + renderBox: renderEditor, + startGlyphHeight: glyphHeights.item1, + endGlyphHeight: glyphHeights.item2, + selectionEndpoints: points, + ); + } + + /// Gets the line heights at the start and end of the selection for the given + /// [RawEditorState]. + /// + /// Copied from [EditableTextState]. + Tuple2 _getGlyphHeights() { + final selection = textEditingValue.selection; + + // Only calculate handle rects if the text in the previous frame + // is the same as the text in the current frame. This is done because + // widget.renderObject contains the renderEditable from the previous frame. + // If the text changed between the current and previous frames then + // widget.renderObject.getRectForComposingRange might fail. In cases where + // the current frame is different from the previous we fall back to + // renderObject.preferredLineHeight. + final prevText = renderEditor.document.toPlainText(); + final currText = textEditingValue.text; + if (prevText != currText || !selection.isValid || selection.isCollapsed) { + return Tuple2( + renderEditor.preferredLineHeight(selection.base), + renderEditor.preferredLineHeight(selection.base), + ); + } + + final startCharacterRect = + renderEditor.getLocalRectForCaret(selection.base); + final endCharacterRect = + renderEditor.getLocalRectForCaret(selection.extent); + return Tuple2( + startCharacterRect.height, + endCharacterRect.height, + ); + } + @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); @@ -2333,3 +2411,15 @@ class _ApplyCheckListAction extends Action { @override bool get isActionEnabled => true; } + +/// Signature for a widget builder that builds a context menu for the given +/// [RawEditorState]. +/// +/// See also: +/// +/// * [EditableTextContextMenuBuilder], which performs the same role for +/// [EditableText] +typedef QuillEditorContextMenuBuilder = Widget Function( + BuildContext context, + RawEditorState rawEditorState, +); diff --git a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart index 8b494dd1..98cfb407 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -150,14 +150,15 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState } @override - bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; + bool get cutEnabled => widget.contextMenuBuilder != null && !widget.readOnly; @override - bool get copyEnabled => widget.toolbarOptions.copy; + bool get copyEnabled => widget.contextMenuBuilder != null; @override - bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly; + bool get pasteEnabled => widget.contextMenuBuilder != null + && !widget.readOnly; @override - bool get selectAllEnabled => widget.toolbarOptions.selectAll; + bool get selectAllEnabled => widget.contextMenuBuilder != null; }