From 6c39fcbc54b82db2f22eeb269940fdd5fd1104ec Mon Sep 17 00:00:00 2001 From: frmatthew Date: Wed, 5 Jan 2022 22:37:23 -0800 Subject: [PATCH] Feature: add selection completed callback (#576) * Implement selection completion handlers * Selection completion customizable via callback --- lib/src/widgets/controller.dart | 3 +++ lib/src/widgets/editor.dart | 35 ++++++++++++++++++++++++++++----- lib/src/widgets/raw_editor.dart | 12 +++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 5ef115c8..4c12cfe7 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -20,6 +20,7 @@ class QuillController extends ChangeNotifier { bool keepStyleOnNewLine = false, this.onReplaceText, this.onDelete, + this.onSelectionCompleted, }) : _selection = selection, _keepStyleOnNewLine = keepStyleOnNewLine; @@ -48,6 +49,8 @@ class QuillController extends ChangeNotifier { /// Custom delete handler DeleteCallback? onDelete; + void Function()? onSelectionCompleted; + /// Store any styles attribute that got toggled by the tap of a button /// and that has not been applied yet. /// It gets reset after each format action within the [document]. diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 472f64ad..dca897fa 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -712,11 +712,14 @@ class _QuillEditorSelectionGestureDetectorBuilder // If `Shift` key is pressed then // extend current selection instead. if (isShiftClick(details.kind)) { - getRenderEditor()!.extendSelection(details.globalPosition, - cause: SelectionChangedCause.tap); + getRenderEditor()! + ..extendSelection(details.globalPosition, + cause: SelectionChangedCause.tap) + ..onSelectionCompleted(); } else { getRenderEditor()! - .selectPosition(cause: SelectionChangedCause.tap); + ..selectPosition(cause: SelectionChangedCause.tap) + ..onSelectionCompleted(); } break; @@ -725,7 +728,9 @@ class _QuillEditorSelectionGestureDetectorBuilder // On macOS/iOS/iPadOS a touch tap places the cursor at the edge // of the word. try { - getRenderEditor()!.selectWordEdge(SelectionChangedCause.tap); + getRenderEditor()! + ..selectWordEdge(SelectionChangedCause.tap) + ..onSelectionCompleted(); } finally { break; } @@ -736,7 +741,9 @@ class _QuillEditorSelectionGestureDetectorBuilder case TargetPlatform.linux: case TargetPlatform.windows: try { - getRenderEditor()!.selectPosition(cause: SelectionChangedCause.tap); + getRenderEditor()! + ..selectPosition(cause: SelectionChangedCause.tap) + ..onSelectionCompleted(); } finally { break; } @@ -788,6 +795,10 @@ class _QuillEditorSelectionGestureDetectorBuilder details, renderEditor.getPositionForOffset)) { return; } + + if (delegate.getSelectionEnabled()) { + renderEditor.onSelectionCompleted(); + } } } super.onSingleLongTapEnd(details); @@ -801,6 +812,17 @@ class _QuillEditorSelectionGestureDetectorBuilder typedef TextSelectionChangedHandler = void Function( TextSelection selection, SelectionChangedCause cause); +/// Signature for the callback that reports when a selection action is actually +/// completed and ratified. Completion is defined as when the user input has +/// concluded for an entire selection action. For simple taps and keyboard input +/// events that change the selection, this callback is invoked immediately +/// following the TextSelectionChangedHandler. For long taps, the selection is +/// considered complete at the up event of a long tap. For drag selections, the +/// selection completes once the drag/pan event ends or is interrupted. +/// +/// Used by [RenderEditor.onSelectionCompleted]. +typedef TextSelectionCompletedHandler = void Function(); + // The padding applied to text field. Used to determine the bounds when // moving the floating cursor. const EdgeInsets _kFloatingCursorAddedMargin = EdgeInsets.fromLTRB(4, 4, 4, 5); @@ -827,6 +849,7 @@ class RenderEditor extends RenderEditableContainerBox required EdgeInsetsGeometry padding, required CursorCont cursorController, required this.onSelectionChanged, + required this.onSelectionCompleted, required double scrollBottomInset, required this.floatingCursorDisabled, ViewportOffset? offset, @@ -859,6 +882,7 @@ class RenderEditor extends RenderEditableContainerBox /// Called when the selection changes. TextSelectionChangedHandler onSelectionChanged; + TextSelectionCompletedHandler onSelectionCompleted; final ValueNotifier _selectionStartInViewport = ValueNotifier(true); @@ -1067,6 +1091,7 @@ class RenderEditor extends RenderEditableContainerBox void handleDragEnd(DragEndDetails details) { _isDragging = false; + onSelectionCompleted(); } @override diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index f77f1454..20431258 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -288,6 +288,7 @@ class RawEditorState extends EditorState startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, onSelectionChanged: _handleSelectionChanged, + onSelectionCompleted: _handleSelectionCompleted, scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, maxContentWidth: widget.maxContentWidth, @@ -318,6 +319,7 @@ class RawEditorState extends EditorState startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, onSelectionChanged: _handleSelectionChanged, + onSelectionCompleted: _handleSelectionCompleted, scrollBottomInset: widget.scrollBottomInset, padding: widget.padding, maxContentWidth: widget.maxContentWidth, @@ -374,6 +376,10 @@ class RawEditorState extends EditorState } } + void _handleSelectionCompleted() { + widget.controller.onSelectionCompleted?.call(); + } + /// Updates the checkbox positioned at [offset] in document /// by changing its attribute according to [value]. void _handleCheckboxTap(int offset, bool value) { @@ -793,6 +799,9 @@ class RawEditorState extends EditorState } textEditingValue = value; userUpdateTextEditingValue(value, cause); + + // keyboard and text input force a selection completion + _handleSelectionCompleted(); } @override @@ -914,6 +923,7 @@ class _Editor extends MultiChildRenderObjectWidget { required this.startHandleLayerLink, required this.endHandleLayerLink, required this.onSelectionChanged, + required this.onSelectionCompleted, required this.scrollBottomInset, required this.cursorController, required this.floatingCursorDisabled, @@ -930,6 +940,7 @@ class _Editor extends MultiChildRenderObjectWidget { final LayerLink startHandleLayerLink; final LayerLink endHandleLayerLink; final TextSelectionChangedHandler onSelectionChanged; + final TextSelectionCompletedHandler onSelectionCompleted; final double scrollBottomInset; final EdgeInsetsGeometry padding; final double? maxContentWidth; @@ -947,6 +958,7 @@ class _Editor extends MultiChildRenderObjectWidget { startHandleLayerLink: startHandleLayerLink, endHandleLayerLink: endHandleLayerLink, onSelectionChanged: onSelectionChanged, + onSelectionCompleted: onSelectionCompleted, cursorController: cursorController, padding: padding, maxContentWidth: maxContentWidth,