From 245e349d04074ec476054ec5e43027ded9d24c87 Mon Sep 17 00:00:00 2001 From: xuyang Date: Mon, 19 Aug 2024 19:04:24 +0800 Subject: [PATCH 1/5] [chore]: change gesture --- .../editor/config/editor_configurations.dart | 6 +- lib/src/editor/editor.dart | 27 +- lib/src/editor/raw_editor/raw_editor.dart | 2 + .../editor/raw_editor/raw_editor_state.dart | 26 +- lib/src/editor/widgets/delegate.dart | 679 +++++++++++++++++- .../editor/widgets/text/text_selection.dart | 395 +++++----- 6 files changed, 878 insertions(+), 257 deletions(-) diff --git a/lib/src/editor/config/editor_configurations.dart b/lib/src/editor/config/editor_configurations.dart index 425192f6a..6d5150d72 100644 --- a/lib/src/editor/config/editor_configurations.dart +++ b/lib/src/editor/config/editor_configurations.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart' show Brightness, Uint8List, immutable; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show TextCapitalization, TextInputAction, TextSelectionThemeData; import 'package:flutter/widgets.dart'; @@ -258,11 +259,12 @@ class QuillEditorConfigurations extends Equatable { // Returns whether gesture is handled final bool Function( - TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; + TapDragDownDetails details, TextPosition Function(Offset offset))? + onTapDown; // Returns whether gesture is handled final bool Function( - TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; + TapDragUpDetails details, TextPosition Function(Offset offset))? onTapUp; // Returns whether gesture is handled final bool Function( diff --git a/lib/src/editor/editor.dart b/lib/src/editor/editor.dart index a081bf059..fb8b84edc 100644 --- a/lib/src/editor/editor.dart +++ b/lib/src/editor/editor.dart @@ -4,7 +4,13 @@ import 'package:flutter/cupertino.dart' show CupertinoTheme, cupertinoTextSelectionControls; import 'package:flutter/foundation.dart' show ValueListenable, defaultTargetPlatform; -import 'package:flutter/gestures.dart' show PointerDeviceKind; +import 'package:flutter/gestures.dart' + show + PointerDeviceKind, + TapDragDownDetails, + TapDragEndDetails, + TapDragStartDetails, + TapDragUpDetails; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -488,7 +494,7 @@ class _QuillEditorSelectionGestureDetectorBuilder editor?.updateMagnifier(details.globalPosition); } - bool _isPositionSelected(TapUpDetails details) { + bool _isPositionSelected(TapDragUpDetails details) { if (_state.controller.document.isEmpty()) { return false; } @@ -511,7 +517,7 @@ class _QuillEditorSelectionGestureDetectorBuilder } @override - void onTapDown(TapDownDetails details) { + void onTapDown(TapDragDownDetails details) { if (_state.configurations.onTapDown != null) { if (renderEditor != null && _state.configurations.onTapDown!( @@ -532,7 +538,7 @@ class _QuillEditorSelectionGestureDetectorBuilder } @override - void onSingleTapUp(TapUpDetails details) { + void onSingleTapUp(TapDragUpDetails details) { if (_state.configurations.onTapUp != null && renderEditor != null && _state.configurations.onTapUp!( @@ -738,6 +744,7 @@ class RenderEditor extends RenderEditableContainerBox Document document; TextSelection selection; bool _hasFocus = false; + bool get hasFocus => _hasFocus; LayerLink _startHandleLayerLink; LayerLink _endHandleLayerLink; @@ -944,12 +951,20 @@ class RenderEditor extends RenderEditableContainerBox } Offset? _lastTapDownPosition; + Offset? _lastSecondaryTapDownPosition; + + Offset? get lastSecondaryTapDownPosition => _lastSecondaryTapDownPosition; // Used on Desktop (mouse and keyboard enabled platforms) as base offset // for extending selection, either with combination of `Shift` + Click or // by dragging TextSelection? _extendSelectionOrigin; + void handleSecondaryTapDown(TapDownDetails details) { + _lastTapDownPosition = details.globalPosition; + _lastSecondaryTapDownPosition = details.globalPosition; + } + @override void handleTapDown(TapDownDetails details) { _lastTapDownPosition = details.globalPosition; @@ -957,7 +972,7 @@ class RenderEditor extends RenderEditableContainerBox bool _isDragging = false; - void handleDragStart(DragStartDetails details) { + void handleDragStart(TapDragStartDetails details) { _isDragging = true; final newSelection = selectPositionAt( @@ -970,7 +985,7 @@ class RenderEditor extends RenderEditableContainerBox _extendSelectionOrigin = newSelection; } - void handleDragEnd(DragEndDetails details) { + void handleDragEnd(TapDragEndDetails details) { _isDragging = false; onSelectionCompleted(); } diff --git a/lib/src/editor/raw_editor/raw_editor.dart b/lib/src/editor/raw_editor/raw_editor.dart index 23b3130b6..8ae7d9399 100644 --- a/lib/src/editor/raw_editor/raw_editor.dart +++ b/lib/src/editor/raw_editor/raw_editor.dart @@ -100,4 +100,6 @@ abstract class EditorState extends State void updateMagnifier(Offset positionToShow); void hideMagnifier(); + + void toggleToolbar([bool hideHandles = true]); } diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index fbb6b1526..300b6d390 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -906,7 +906,7 @@ class QuillRawEditorState extends EditorState _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); _selectionOverlay?.showHandles(); - if (!_keyboardVisible) { + if (!_hasFocus) { // This will show the keyboard for all selection changes on the // editor, not just changes triggered by user gestures. requestKeyboard(); @@ -1424,6 +1424,9 @@ class QuillRawEditorState extends EditorState _selectionOverlay = null; } else if (_hasFocus) { _selectionOverlay!.update(textEditingValue); + } else { + _selectionOverlay!.dispose(); + _selectionOverlay = null; } } else if (_hasFocus) { _selectionOverlay = _createSelectionOverlay(); @@ -1601,6 +1604,16 @@ class QuillRawEditorState extends EditorState return true; } + @override + void toggleToolbar([bool hideHandles = true]) { + final selectionOverlay = _selectionOverlay ??= _createSelectionOverlay(); + if (selectionOverlay.handlesVisible) { + hideToolbar(hideHandles); + } else { + showToolbar(); + } + } + void _replaceText(ReplaceTextIntent intent) { userUpdateTextEditingValue( intent.currentTextEditingValue @@ -1835,15 +1848,20 @@ class QuillRawEditorState extends EditorState @override void showMagnifier(ui.Offset positionToShow) { + if (_hasFocus == false) return; if (_selectionOverlay == null) return; final position = renderEditor.getPositionForOffset(positionToShow); _selectionOverlay?.showMagnifier(position, positionToShow, renderEditor); + if (_selectionOverlay!.magnifierIsVisible) { + _selectionOverlay! + .updateMagnifier(position, positionToShow, renderEditor); + } else { + _selectionOverlay!.showMagnifier(position, positionToShow, renderEditor); + } } @override void updateMagnifier(ui.Offset positionToShow) { - _updateOrDisposeSelectionOverlayIfNeeded(); - final position = renderEditor.getPositionForOffset(positionToShow); - _selectionOverlay?.updateMagnifier(position, positionToShow, renderEditor); + showMagnifier(positionToShow); } } diff --git a/lib/src/editor/widgets/delegate.dart b/lib/src/editor/widgets/delegate.dart index 90f1fee33..f51f74c48 100644 --- a/lib/src/editor/widgets/delegate.dart +++ b/lib/src/editor/widgets/delegate.dart @@ -1,12 +1,14 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import '../../common/utils/platform.dart'; import '../../document/attribute.dart'; import '../../document/nodes/leaf.dart'; -import '../editor.dart'; +import '../../widgets/editor/editor.dart'; import '../raw_editor/raw_editor.dart'; import 'text/text_selection.dart'; @@ -107,6 +109,138 @@ class EditorTextSelectionGestureDetectorBuilder { @protected RenderEditor? get renderEditor => editor?.renderEditor; + /// Whether the Shift key was pressed when the most recent [PointerDownEvent] + /// was tracked by the [BaseTapAndDragGestureRecognizer]. + bool _isShiftPressed = false; + + /// The viewport offset pixels of any [Scrollable] containing the + /// [RenderEditable] at the last drag start. + double _dragStartScrollOffset = 0; + + /// The viewport offset pixels of the [RenderEditable] at the last drag start. + double _dragStartViewportOffset = 0; + + double get _scrollPosition { + final scrollableState = delegate.editableTextKey.currentContext == null + ? null + : Scrollable.maybeOf(delegate.editableTextKey.currentContext!); + return scrollableState == null ? 0.0 : scrollableState.position.pixels; + } + + // For tap + drag gesture on iOS, whether the position where the drag started + // was on the previous TextSelection. iOS uses this value to determine if + // the cursor should move on drag update. + // + TextSelection? _dragStartSelection; + + // If the drag started on the previous selection then the cursor will move on + // drag update. If the drag did not start on the previous selection then the + // cursor will not move on drag update. + bool? _dragBeganOnPreviousSelection; + + /// Returns true if lastSecondaryTapDownPosition was on selection. + bool get _lastSecondaryTapWasOnSelection { + assert(renderEditor?.lastSecondaryTapDownPosition != null); + if (renderEditor?.selection == null) { + return false; + } + renderEditor?.lastSecondaryTapDownPosition; + final textPosition = renderEditor?.getPositionForOffset( + renderEditor!.lastSecondaryTapDownPosition!, + ); + + if (textPosition == null) return false; + + return renderEditor!.selection.start <= textPosition.offset && + renderEditor!.selection.end >= textPosition.offset; + } + + /// Returns true if position was on selection. + bool _positionOnSelection(Offset position, TextSelection? targetSelection) { + if (targetSelection == null) return false; + + final textPosition = renderEditor?.getPositionForOffset(position); + + if (textPosition == null) return false; + + return targetSelection.start <= textPosition.offset && + targetSelection.end >= textPosition.offset; + } + + // Expand the selection to the given global position. + // + // Either base or extent will be moved to the last tapped position, whichever + // is closest. The selection will never shrink or pivot, only grow. + // + // If fromSelection is given, will expand from that selection instead of the + // current selection in renderEditable. + // + // See also: + // + // * [_extendSelection], which is similar but pivots the selection around + // the base. + void _expandSelection(Offset offset, SelectionChangedCause cause, + [TextSelection? fromSelection]) { + final tappedPosition = renderEditor!.getPositionForOffset(offset); + final selection = fromSelection ?? renderEditor!.selection; + final baseIsCloser = (tappedPosition.offset - selection.baseOffset).abs() < + (tappedPosition.offset - selection.extentOffset).abs(); + final nextSelection = selection.copyWith( + baseOffset: baseIsCloser ? selection.extentOffset : selection.baseOffset, + extentOffset: tappedPosition.offset, + ); + + editor?.userUpdateTextEditingValue( + editor!.textEditingValue.copyWith(selection: nextSelection), cause); + } + + // Extend the selection to the given global position. + // + // Holds the base in place and moves the extent. + // + // See also: + // + // * [_expandSelection], which is similar but always increases the size of + // the selection. + void _extendSelection(Offset offset, SelectionChangedCause cause) { + assert(renderEditor?.selection.baseOffset != null); + + final tappedPosition = renderEditor!.getPositionForOffset(offset); + final selection = renderEditor!.selection; + final nextSelection = selection.copyWith( + extentOffset: tappedPosition.offset, + ); + + editor?.userUpdateTextEditingValue( + editor!.textEditingValue.copyWith(selection: nextSelection), cause); + } + + /// Handler for [TextSelectionGestureDetector.onTapTrackStart]. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onTapTrackStart], which triggers this + /// callback. + @protected + void onTapTrackStart() { + _isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed + .intersection({ + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.shiftRight + }).isNotEmpty; + } + + /// Handler for [TextSelectionGestureDetector.onTapTrackReset]. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onTapTrackReset], which triggers this + /// callback. + @protected + void onTapTrackReset() { + _isShiftPressed = false; + } + /// Handler for [EditorTextSelectionGestureDetector.onTapDown]. /// /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets @@ -118,19 +252,44 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onTapDown], /// which triggers this callback. @protected - void onTapDown(TapDownDetails details) { + void onTapDown(TapDragDownDetails details) { + if (!delegate.selectionEnabled) return; renderEditor!.handleTapDown(details); - // The selection overlay should only be shown when the user is interacting - // through a touch screen (via either a finger or a stylus). - // A mouse shouldn't trigger the selection overlay. - // For backwards-compatibility, we treat a null kind the same as touch. - kind = details.kind; + final kind = details.kind; shouldShowSelectionToolbar = kind == null || - kind == - PointerDeviceKind - .mouse || // Enable word selection by mouse double tap kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; + final isShiftPressedValid = + _isShiftPressed && renderEditor?.selection.baseOffset != null; + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + editor?.hideToolbar(false); + case TargetPlatform.iOS: + // On mobile platforms the selection is set on tap up. + break; + case TargetPlatform.macOS: + editor?.hideToolbar(); + // On macOS, a shift-tapped unfocused field expands from 0, not from the + // previous selection. + if (isShiftPressedValid) { + final fromSelection = renderEditor?.hasFocus == true + ? null + : const TextSelection.collapsed(offset: 0); + _expandSelection( + details.globalPosition, SelectionChangedCause.tap, fromSelection); + return; + } + renderEditor?.selectPosition(cause: SelectionChangedCause.tap); + case TargetPlatform.linux: + case TargetPlatform.windows: + editor?.hideToolbar(); + if (isShiftPressedValid) { + _extendSelection(details.globalPosition, SelectionChangedCause.tap); + return; + } + renderEditor?.selectPosition(cause: SelectionChangedCause.tap); + } } /// Handler for [EditorTextSelectionGestureDetector.onForcePressStart]. @@ -181,6 +340,27 @@ class EditorTextSelectionGestureDetectorBuilder { } } + /// Whether the provided [onUserTap] callback should be dispatched on every + /// tap or only non-consecutive taps. + /// + /// Defaults to false. + @protected + bool get onUserTapAlwaysCalled => false; + + /// Handler for [TextSelectionGestureDetector.onUserTap]. + /// + /// By default, it serves as placeholder to enable subclass override. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onUserTap], which triggers this + /// callback. + /// * [TextSelectionGestureDetector.onUserTapAlwaysCalled], which controls + /// whether this callback is called only on the first tap in a series + /// of taps. + @protected + void onUserTap() {/* Subclass should override this method if needed. */} + /// Handler for [EditorTextSelectionGestureDetector.onSingleTapUp]. /// /// By default, it selects word edge if selection is enabled. @@ -190,7 +370,7 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onSingleTapUp], which triggers /// this callback. @protected - void onSingleTapUp(TapUpDetails details) { + void onSingleTapUp(TapDragUpDetails details) { if (delegate.selectionEnabled) { renderEditor!.selectWordEdge(SelectionChangedCause.tap); } @@ -269,6 +449,64 @@ class EditorTextSelectionGestureDetectorBuilder { if (checkSelectionToolbarShouldShow(isAdditionalAction: false)) { editor!.showToolbar(); } + // Q: why ? + // A: cannot access QuillRawEditorState.updateFloatingCursor + // + // if (defaultTargetPlatform == TargetPlatform.iOS && + // delegate.selectionEnabled && + // editor?.textEditingValue.selection.isCollapsed == true) { + // // Update the floating cursor. + // final cursorPoint = + // RawFloatingCursorPoint(state: FloatingCursorDragState.End); + // // !.updateFloatingCursor(cursorPoint); + // (editor as QuillRawEditorState?)?.updateFloatingCursor(cursorPoint); + // } + } + + /// Handler for [TextSelectionGestureDetector.onSecondaryTap]. + /// + /// By default, selects the word if possible and shows the toolbar. + @protected + void onSecondaryTap() { + if (!delegate.selectionEnabled) { + return; + } + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + if (!_lastSecondaryTapWasOnSelection || + renderEditor?.hasFocus == false) { + renderEditor?.selectWord(SelectionChangedCause.tap); + } + if (shouldShowSelectionToolbar) { + editor?.hideToolbar(); + editor?.showToolbar(); + } + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + if (renderEditor?.hasFocus == false) { + renderEditor?.selectPosition(cause: SelectionChangedCause.tap); + } + editor?.toggleToolbar(); + } + } + + /// Handler for [TextSelectionGestureDetector.onDoubleTapDown]. + /// + /// By default, it selects a word through [RenderEditable.selectWord] if + /// selectionEnabled and shows toolbar if necessary. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this + /// callback. + @protected + void onSecondaryTapDown(TapDownDetails details) { + renderEditor?.handleSecondaryTapDown( + TapDownDetails(globalPosition: details.globalPosition)); + shouldShowSelectionToolbar = true; } /// Handler for [EditorTextSelectionGestureDetector.onDoubleTapDown]. @@ -281,7 +519,7 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onDoubleTapDown], /// which triggers this callback. @protected - void onDoubleTapDown(TapDownDetails details) { + void onDoubleTapDown(TapDragDownDetails details) { if (delegate.selectionEnabled) { renderEditor!.selectWord(SelectionChangedCause.tap); // allow the selection to get updated before trying to bring up @@ -298,6 +536,105 @@ class EditorTextSelectionGestureDetectorBuilder { } } + // Selects the set of paragraphs in a document that intersect a given range of + // global positions. + void _selectParagraphsInRange( + {required Offset from, Offset? to, SelectionChangedCause? cause}) { + final TextBoundary paragraphBoundary = + ParagraphBoundary(editor!.textEditingValue.text); + _selectTextBoundariesInRange( + boundary: paragraphBoundary, from: from, to: to, cause: cause); + } + + // Selects the set of lines in a document that intersect a given range of + // global positions. + void _selectLinesInRange( + {required Offset from, Offset? to, SelectionChangedCause? cause}) { + final TextBoundary lineBoundary = LineBoundary(renderEditor!); + _selectTextBoundariesInRange( + boundary: lineBoundary, from: from, to: to, cause: cause); + } + + // Returns the location of a text boundary at `extent`. When `extent` is at + // the end of the text, returns the previous text boundary's location. + TextRange _moveToTextBoundary( + TextPosition extent, TextBoundary textBoundary) { + assert(extent.offset >= 0); + final start = textBoundary.getLeadingTextBoundaryAt( + extent.offset == editor!.textEditingValue.text.length + ? extent.offset - 1 + : extent.offset) ?? + 0; + final end = textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? + editor!.textEditingValue.text.length; + return TextRange(start: start, end: end); + } + + // Selects the set of text boundaries in a document that intersect a given + // range of global positions. + // + // The set of text boundaries selected are not strictly bounded by the range + // of global positions. + // + // The first and last endpoints of the selection will always be at the + // beginning and end of a text boundary respectively. + void _selectTextBoundariesInRange( + {required TextBoundary boundary, + required Offset from, + Offset? to, + SelectionChangedCause? cause}) { + final fromPosition = renderEditor!.getPositionForOffset(from); + final fromRange = _moveToTextBoundary(fromPosition, boundary); + final toPosition = + to == null ? fromPosition : renderEditor!.getPositionForOffset(to); + final toRange = toPosition == fromPosition + ? fromRange + : _moveToTextBoundary(toPosition, boundary); + final isFromBoundaryBeforeToBoundary = fromRange.start < toRange.end; + + final newSelection = isFromBoundaryBeforeToBoundary + ? TextSelection(baseOffset: fromRange.start, extentOffset: toRange.end) + : TextSelection(baseOffset: fromRange.end, extentOffset: toRange.start); + + editor!.userUpdateTextEditingValue( + editor!.textEditingValue.copyWith(selection: newSelection), + cause ?? SelectionChangedCause.drag); + } + + /// Handler for [TextSelectionGestureDetector.onTripleTapDown]. + /// + /// By default, it selects a paragraph if + /// [TextSelectionGestureDetectorBuilderDelegate.selectionEnabled] is true + /// and shows the toolbar if necessary. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onTripleTapDown], which triggers this + /// callback. + @protected + void onTripleTapDown(TapDragDownDetails details) { + if (!delegate.selectionEnabled) { + return; + } + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.windows: + _selectParagraphsInRange( + from: details.globalPosition, cause: SelectionChangedCause.tap); + case TargetPlatform.linux: + _selectLinesInRange( + from: details.globalPosition, cause: SelectionChangedCause.tap); + } + + if (shouldShowSelectionToolbar) { + editor?.showToolbar(); + } + } + /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionStart]. /// /// By default, it selects a text position specified in [details]. @@ -307,8 +644,102 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onDragSelectionStart], /// which triggers this callback. @protected - void onDragSelectionStart(DragStartDetails details) { - renderEditor!.handleDragStart(details); + void onDragSelectionStart(TapDragStartDetails details) { + if (delegate.selectionEnabled == false) return; + if (editor?.textEditingValue.selection.isCollapsed == false) return; + + final kind = details.kind; + shouldShowSelectionToolbar = kind == null || + kind == PointerDeviceKind.touch || + kind == PointerDeviceKind.stylus; + _dragStartSelection = renderEditor?.selection; + _dragStartScrollOffset = _scrollPosition; + _dragStartViewportOffset = renderEditor?.offset?.pixels ?? 0.0; + _dragBeganOnPreviousSelection = + _positionOnSelection(details.globalPosition, _dragStartSelection); + if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( + details.consecutiveTapCount) > + 1) { + // Do not set the selection on a consecutive tap and drag. + return; + } + + if (_isShiftPressed && + renderEditor?.selection != null && + renderEditor?.selection.isValid == true) { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + renderEditor?.extendSelection(details.globalPosition, + cause: SelectionChangedCause.drag); + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + renderEditor?.extendSelection(details.globalPosition, + cause: SelectionChangedCause.drag); + } + } else { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + switch (details.kind) { + case PointerDeviceKind.mouse: + case PointerDeviceKind.trackpad: + renderEditor?.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + // For iOS platforms, a touch drag does not initiate unless the + // editable has focus and the drag began on the previous selection. + assert(_dragBeganOnPreviousSelection != null); + if (renderEditor?.hasFocus == true && + _dragBeganOnPreviousSelection!) { + renderEditor?.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + editor?.showMagnifier(details.globalPosition); + } + case null: + } + case TargetPlatform.android: + case TargetPlatform.fuchsia: + switch (details.kind) { + case PointerDeviceKind.mouse: + case PointerDeviceKind.trackpad: + renderEditor?.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + // For Android, Fucshia, and iOS platforms, a touch drag + // does not initiate unless the editable has focus. + if (renderEditor?.hasFocus == true) { + renderEditor?.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + editor?.showMagnifier(details.globalPosition); + } + case null: + } + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + renderEditor?.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + } + } } /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionUpdate]. @@ -321,13 +752,206 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onDragSelectionUpdate], /// which triggers this callback./lib/src/material/text_field.dart @protected - void onDragSelectionUpdate( - //DragStartDetails startDetails, - DragUpdateDetails updateDetails) { - renderEditor!.extendSelection( - updateDetails.globalPosition, - cause: SelectionChangedCause.drag, - ); + void onDragSelectionUpdate(TapDragUpdateDetails updateDetails) { + if (delegate.selectionEnabled == false) return; + if (editor?.textEditingValue.selection.isCollapsed == false) return; + if (!_isShiftPressed) { + // Adjust the drag start offset for possible viewport offset changes. + final editableOffset = + Offset(0, renderEditor!.offset!.pixels - _dragStartViewportOffset); + final scrollableOffset = + Offset(0, _scrollPosition - _dragStartScrollOffset); + final dragStartGlobalPosition = + updateDetails.globalPosition - updateDetails.offsetFromOrigin; + + // Select word by word. + if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( + updateDetails.consecutiveTapCount) == + 2) { + renderEditor?.selectWordsInRange( + dragStartGlobalPosition - editableOffset - scrollableOffset, + updateDetails.globalPosition, + SelectionChangedCause.drag, + ); + + switch (updateDetails.kind) { + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + return editor?.updateMagnifier(updateDetails.globalPosition); + case PointerDeviceKind.mouse: + case PointerDeviceKind.trackpad: + case null: + return; + } + } + + // Select paragraph-by-paragraph. + if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( + updateDetails.consecutiveTapCount) == + 3) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + switch (updateDetails.kind) { + case PointerDeviceKind.mouse: + case PointerDeviceKind.trackpad: + return _selectParagraphsInRange( + from: dragStartGlobalPosition - + editableOffset - + scrollableOffset, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + case null: + // Triple tap to drag is not present on these platforms when using + // non-precise pointer devices at the moment. + break; + } + return; + case TargetPlatform.linux: + return _selectLinesInRange( + from: dragStartGlobalPosition - editableOffset - scrollableOffset, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + case TargetPlatform.windows: + case TargetPlatform.macOS: + return _selectParagraphsInRange( + from: dragStartGlobalPosition - editableOffset - scrollableOffset, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + } + } + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + // With a touch device, nothing should happen, unless there was a double tap, or + // there was a collapsed selection, and the tap/drag position is at the collapsed selection. + // In that case the caret should move with the drag position. + // + // With a mouse device, a drag should select the range from the origin of the drag + // to the current position of the drag. + switch (updateDetails.kind) { + case PointerDeviceKind.mouse: + case PointerDeviceKind.trackpad: + renderEditor?.selectPositionAt( + from: + dragStartGlobalPosition - editableOffset - scrollableOffset, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + return; + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + assert(_dragBeganOnPreviousSelection != null); + if (renderEditor?.hasFocus == true && + _dragStartSelection!.isCollapsed && + _dragBeganOnPreviousSelection!) { + renderEditor?.selectPositionAt( + from: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + return editor?.updateMagnifier(updateDetails.globalPosition); + } + case null: + break; + } + return; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + // With a precise pointer device, such as a mouse, trackpad, or stylus, + // the drag will select the text spanning the origin of the drag to the end of the drag. + // With a touch device, the cursor should move with the drag. + switch (updateDetails.kind) { + case PointerDeviceKind.mouse: + case PointerDeviceKind.trackpad: + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + renderEditor?.selectPositionAt( + from: + dragStartGlobalPosition - editableOffset - scrollableOffset, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + return; + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + if (renderEditor?.hasFocus == true) { + renderEditor?.selectPositionAt( + from: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + return editor?.updateMagnifier(updateDetails.globalPosition); + } + case null: + break; + } + return; + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + renderEditor?.selectPositionAt( + from: dragStartGlobalPosition - editableOffset - scrollableOffset, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); + } + } + + if (_dragStartSelection!.isCollapsed || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS)) { + return _extendSelection( + updateDetails.globalPosition, SelectionChangedCause.drag); + } + + // If the drag inverts the selection, Mac and iOS revert to the initial + // selection. + final selection = renderEditor!.selection; + final nextExtent = + renderEditor!.getPositionForOffset(updateDetails.globalPosition); + + final isShiftTapDragSelectionForward = + _dragStartSelection!.baseOffset < _dragStartSelection!.extentOffset; + final isInverted = isShiftTapDragSelectionForward + ? nextExtent.offset < _dragStartSelection!.baseOffset + : nextExtent.offset > _dragStartSelection!.baseOffset; + if (isInverted && selection.baseOffset == _dragStartSelection!.baseOffset) { + editor?.userUpdateTextEditingValue( + editor!.textEditingValue.copyWith( + selection: TextSelection( + baseOffset: _dragStartSelection!.extentOffset, + extentOffset: nextExtent.offset, + ), + ), + SelectionChangedCause.drag, + ); + } else if (!isInverted && + nextExtent.offset != _dragStartSelection!.baseOffset && + selection.baseOffset != _dragStartSelection!.baseOffset) { + editor?.userUpdateTextEditingValue( + editor!.textEditingValue.copyWith( + selection: TextSelection( + baseOffset: _dragStartSelection!.baseOffset, + extentOffset: nextExtent.offset, + ), + ), + SelectionChangedCause.drag, + ); + } else { + _extendSelection( + updateDetails.globalPosition, SelectionChangedCause.drag); + } } /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionEnd]. @@ -339,7 +963,8 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onDragSelectionEnd], /// which triggers this callback. @protected - void onDragSelectionEnd(DragEndDetails details) { + void onDragSelectionEnd(TapDragEndDetails details) { + if (editor?.textEditingValue.selection.isCollapsed == false) return; renderEditor!.handleDragEnd(details); if (isDesktop(supportWeb: true) && delegate.selectionEnabled && @@ -347,6 +972,7 @@ class EditorTextSelectionGestureDetectorBuilder { // added to show selection copy/paste toolbar after drag to select editor!.showToolbar(); } + editor?.hideMagnifier(); } /// Returns a [EditorTextSelectionGestureDetector] configured with @@ -361,21 +987,26 @@ class EditorTextSelectionGestureDetectorBuilder { }) { return EditorTextSelectionGestureDetector( key: key, + onTapTrackStart: onTapTrackStart, + onTapTrackReset: onTapTrackReset, onTapDown: onTapDown, onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null, onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, onSingleTapUp: onSingleTapUp, onSingleTapCancel: onSingleTapCancel, + onUserTap: onUserTap, onSingleLongTapStart: onSingleLongTapStart, onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, onSingleLongTapEnd: onSingleLongTapEnd, onDoubleTapDown: onDoubleTapDown, - onSecondarySingleTapUp: onSecondarySingleTapUp, + onTripleTapDown: onTripleTapDown, onDragSelectionStart: onDragSelectionStart, onDragSelectionUpdate: onDragSelectionUpdate, onDragSelectionEnd: onDragSelectionEnd, + onUserTapAlwaysCalled: onUserTapAlwaysCalled, behavior: behavior, - detectWordBoundary: detectWordBoundary, child: child, ); } diff --git a/lib/src/editor/widgets/text/text_selection.dart b/lib/src/editor/widgets/text/text_selection.dart index a22d8c553..4047cd74b 100644 --- a/lib/src/editor/widgets/text/text_selection.dart +++ b/lib/src/editor/widgets/text/text_selection.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; @@ -187,6 +186,8 @@ class EditorTextSelectionOverlay { final MagnifierController _magnifierController = MagnifierController(); + bool get magnifierIsVisible => _magnifierController.shown; + final TextMagnifierConfiguration magnifierConfiguration; final ValueNotifier _magnifierInfo = @@ -790,31 +791,39 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { /// The [child] parameter must not be null. const EditorTextSelectionGestureDetector({ required this.child, + super.key, + this.onTapTrackStart, + this.onTapTrackReset, this.onTapDown, this.onForcePressStart, this.onForcePressEnd, + this.onSecondaryTap, + this.onSecondaryTapDown, this.onSingleTapUp, this.onSingleTapCancel, - this.onSecondaryTapDown, - this.onSecondarySingleTapUp, - this.onSecondarySingleTapCancel, - this.onSecondaryDoubleTapDown, + this.onUserTap, this.onSingleLongTapStart, this.onSingleLongTapMoveUpdate, this.onSingleLongTapEnd, this.onDoubleTapDown, + this.onTripleTapDown, this.onDragSelectionStart, this.onDragSelectionUpdate, this.onDragSelectionEnd, + this.onUserTapAlwaysCalled = false, this.behavior, - this.detectWordBoundary = true, - super.key, }); + /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.onTapTrackStart} + final VoidCallback? onTapTrackStart; + + /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.onTapTrackReset} + final VoidCallback? onTapTrackReset; + /// Called for every tap down including every tap down that's part of a /// double click or a long press, except touches that include enough movement /// to not qualify as taps (e.g. pans and flings). - final GestureTapDownCallback? onTapDown; + final GestureTapDragDownCallback? onTapDown; /// Called when a pointer has tapped down and the force of the pointer has /// just become greater than [ForcePressGestureRecognizer.startPressure]. @@ -824,28 +833,31 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { /// lifted off the screen. final GestureForcePressEndCallback? onForcePressEnd; - /// Called for each distinct tap except for every second tap of a double tap. + /// Called for a tap event with the secondary mouse button. + final GestureTapCallback? onSecondaryTap; + + /// Called for a tap down event with the secondary mouse button. + final GestureTapDownCallback? onSecondaryTapDown; + + /// Called for the first tap in a series of taps, consecutive taps do not call + /// this method. + /// /// For example, if the detector was configured with [onTapDown] and /// [onDoubleTapDown], three quick taps would be recognized as a single tap - /// down, followed by a double tap down, followed by a single tap down. - final GestureTapUpCallback? onSingleTapUp; + /// down, followed by a tap up, then a double tap down, followed by a single tap down. + final GestureTapDragUpCallback? onSingleTapUp; /// Called for each touch that becomes recognized as a gesture that is not a /// short tap, such as a long tap or drag. It is called at the moment when /// another gesture from the touch is recognized. - final GestureTapCancelCallback? onSingleTapCancel; - - /// onTapDown for mouse right click - final GestureTapDownCallback? onSecondaryTapDown; - - /// onTapUp for mouse right click - final GestureTapUpCallback? onSecondarySingleTapUp; - - /// onTapCancel for mouse right click - final GestureTapCancelCallback? onSecondarySingleTapCancel; + final GestureCancelCallback? onSingleTapCancel; - /// onDoubleTap for mouse right click - final GestureTapDownCallback? onSecondaryDoubleTapDown; + /// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is + /// disabled, which is the default behavior. + /// + /// When [onUserTapAlwaysCalled] is enabled, this is called for every tap, + /// including consecutive taps. + final GestureTapCallback? onUserTap; /// Called for a single long tap that's sustained for longer than /// [kLongPressTimeout] but not necessarily lifted. Not called for a @@ -860,20 +872,25 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { /// Called after a momentary hold or a short tap that is close in space and /// time (within [kDoubleTapTimeout]) to a previous short tap. - final GestureTapDownCallback? onDoubleTapDown; + final GestureTapDragDownCallback? onDoubleTapDown; + + /// Called after a momentary hold or a short tap that is close in space and + /// time (within [kDoubleTapTimeout]) to a previous double-tap. + final GestureTapDragDownCallback? onTripleTapDown; /// Called when a mouse starts dragging to select text. - final GestureDragStartCallback? onDragSelectionStart; + final GestureTapDragStartCallback? onDragSelectionStart; /// Called repeatedly as a mouse moves while dragging. - /// - /// The frequency of calls is throttled to avoid excessive text layout - /// operations in text fields. The throttling is controlled by the constant - /// [_kDragSelectionUpdateThrottle]. - final GestureDragUpdateCallback? onDragSelectionUpdate; + final GestureTapDragUpdateCallback? onDragSelectionUpdate; /// Called when a mouse that was previously dragging is released. - final GestureDragEndCallback? onDragSelectionEnd; + final GestureTapDragEndCallback? onDragSelectionEnd; + + /// Whether [onUserTap] will be called for all taps including consecutive taps. + /// + /// Defaults to false, so [onUserTap] is only called for each distinct tap. + final bool onUserTapAlwaysCalled; /// How this gesture detector should behave during hit testing. /// @@ -883,210 +900,145 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { /// Child below this widget. final Widget child; - final bool detectWordBoundary; - @override State createState() => _EditorTextSelectionGestureDetectorState(); + + static int getEffectiveConsecutiveTapCount(int rawCount) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + // From observation, these platform's reset their tap count to 0 when + // the number of consecutive taps exceeds 3. For example on Debian Linux + // with GTK, when going past a triple click, on the fourth click the + // selection is moved to the precise click position, on the fifth click + // the word at the position is selected, and on the sixth click the + // paragraph at the position is selected. + return rawCount <= 3 + ? rawCount + : (rawCount % 3 == 0 ? 3 : rawCount % 3); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + // From observation, these platform's either hold their tap count at 3. + // For example on macOS, when going past a triple click, the selection + // should be retained at the paragraph that was first selected on triple + // click. + return math.min(rawCount, 3); + case TargetPlatform.windows: + // From observation, this platform's consecutive tap actions alternate + // between double click and triple click actions. For example, after a + // triple click has selected a paragraph, on the next click the word at + // the clicked position will be selected, and on the next click the + // paragraph at the position is selected. + return rawCount < 2 ? rawCount : 2 + rawCount % 2; + } + } } class _EditorTextSelectionGestureDetectorState extends State { - // Counts down for a short duration after a previous tap. Null otherwise. - Timer? _doubleTapTimer; - Offset? _lastTapOffset; - - // True if a second tap down of a double tap is detected. Used to discard - // subsequent tap up / tap hold of the same tap. - bool _isDoubleTap = false; - - // _isDoubleTap for mouse right click - bool _isSecondaryDoubleTap = false; + // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, + // which can grow to be infinitely large, to a value between 1 and 3. The value + // that the raw count is converted to is based on the default observed behavior + // on the native platforms. + // + // This method should be used in all instances when details.consecutiveTapCount + // would be used. + + void _handleTapTrackStart() { + widget.onTapTrackStart?.call(); + } - @override - void dispose() { - _doubleTapTimer?.cancel(); - _dragUpdateThrottleTimer?.cancel(); - super.dispose(); + void _handleTapTrackReset() { + widget.onTapTrackReset?.call(); } // The down handler is force-run on success of a single tap and optimistically // run before a long press success. - void _handleTapDown(TapDownDetails details) { + void _handleTapDown(TapDragDownDetails details) { widget.onTapDown?.call(details); - // This isn't detected as a double tap gesture in the gesture recognizer - // because it's 2 single taps, each of which may do different things - // depending on whether it's a single tap, the first tap of a double tap, - // the second tap held down, a clean double tap etc. - if (_doubleTapTimer != null && - _isWithinDoubleTapTolerance(details.globalPosition)) { - // If there was already a previous tap, the second down hold/tap is a - // double tap down. - - widget.onDoubleTapDown?.call(details); - - _doubleTapTimer!.cancel(); - _doubleTapTimeout(); - _isDoubleTap = true; + // because it's 2 single taps, each of which may do different things depending + // on whether it's a single tap, the first tap of a double tap, the second + // tap held down, a clean double tap etc. + if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( + details.consecutiveTapCount) == + 2) { + return widget.onDoubleTapDown?.call(details); + } + + if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( + details.consecutiveTapCount) == + 3) { + return widget.onTripleTapDown?.call(details); } } - void _handleTapUp(TapUpDetails details) { - if (!_isDoubleTap) { + void _handleTapUp(TapDragUpDetails details) { + if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( + details.consecutiveTapCount) == + 1) { widget.onSingleTapUp?.call(details); - _lastTapOffset = details.globalPosition; - _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); + widget.onUserTap?.call(); + } else if (widget.onUserTapAlwaysCalled) { + widget.onUserTap?.call(); } - _isDoubleTap = false; } void _handleTapCancel() { widget.onSingleTapCancel?.call(); } - // added secondary tap function for mouse right click to show toolbar - void _handleSecondaryTapDown(TapDownDetails details) { - if (widget.onSecondaryTapDown != null) { - widget.onSecondaryTapDown?.call(details); - } - if (_doubleTapTimer != null && - _isWithinDoubleTapTolerance(details.globalPosition)) { - widget.onSecondaryDoubleTapDown?.call(details); - - _doubleTapTimer!.cancel(); - _doubleTapTimeout(); - _isDoubleTap = true; - } - } - - void _handleSecondaryTapUp(TapUpDetails details) { - if (!_isSecondaryDoubleTap) { - widget.onSecondarySingleTapUp?.call(details); - _lastTapOffset = details.globalPosition; - _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); - } - _isSecondaryDoubleTap = false; - } - - void _handleSecondaryTapCancel() { - widget.onSecondarySingleTapCancel?.call(); - } - - DragStartDetails? _lastDragStartDetails; - DragUpdateDetails? _lastDragUpdateDetails; - Timer? _dragUpdateThrottleTimer; - - void _handleDragStart(DragStartDetails details) { - assert(_lastDragStartDetails == null); - _lastDragStartDetails = details; + void _handleDragStart(TapDragStartDetails details) { widget.onDragSelectionStart?.call(details); } - void _handleDragUpdate(DragUpdateDetails details) { - _lastDragUpdateDetails = details; - _dragUpdateThrottleTimer ??= Timer( - const Duration(milliseconds: 50), - _handleDragUpdateThrottled, - ); + void _handleDragUpdate(TapDragUpdateDetails details) { + widget.onDragSelectionUpdate?.call(details); } - /// Drag updates are being throttled to avoid excessive text layouts in text - /// fields. The frequency of invocations is controlled by the constant - /// [_kDragSelectionUpdateThrottle]. - /// - /// Once the drag gesture ends, any pending drag update will be fired - /// immediately. See [_handleDragEnd]. - void _handleDragUpdateThrottled() { - assert(_lastDragStartDetails != null); - assert(_lastDragUpdateDetails != null); - if (widget.onDragSelectionUpdate != null) { - widget.onDragSelectionUpdate!( - //_lastDragStartDetails!, - _lastDragUpdateDetails!); - } - _dragUpdateThrottleTimer = null; - _lastDragUpdateDetails = null; - } - - void _handleDragEnd(DragEndDetails details) { - assert(_lastDragStartDetails != null); - if (_dragUpdateThrottleTimer != null) { - // If there's already an update scheduled, trigger it immediately and - // cancel the timer. - _dragUpdateThrottleTimer!.cancel(); - _handleDragUpdateThrottled(); - } - + void _handleDragEnd(TapDragEndDetails details) { widget.onDragSelectionEnd?.call(details); - - _dragUpdateThrottleTimer = null; - _lastDragStartDetails = null; - _lastDragUpdateDetails = null; } void _forcePressStarted(ForcePressDetails details) { - _doubleTapTimer?.cancel(); - _doubleTapTimer = null; widget.onForcePressStart?.call(details); } void _forcePressEnded(ForcePressDetails details) { - if (widget.onForcePressEnd != null) { - widget.onForcePressEnd?.call(details); - } + widget.onForcePressEnd?.call(details); } void _handleLongPressStart(LongPressStartDetails details) { - if (!_isDoubleTap) { - widget.onSingleLongTapStart?.call(details); + if (widget.onSingleLongTapStart != null) { + widget.onSingleLongTapStart!(details); } } void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - if (!_isDoubleTap) { - widget.onSingleLongTapMoveUpdate?.call(details); + if (widget.onSingleLongTapMoveUpdate != null) { + widget.onSingleLongTapMoveUpdate!(details); } } void _handleLongPressEnd(LongPressEndDetails details) { - if (!_isDoubleTap) { - widget.onSingleLongTapEnd?.call(details); + if (widget.onSingleLongTapEnd != null) { + widget.onSingleLongTapEnd!(details); } - _isDoubleTap = false; - } - - void _doubleTapTimeout() { - _doubleTapTimer = null; - _lastTapOffset = null; - } - - bool _isWithinDoubleTapTolerance(Offset secondTapOffset) { - if (_lastTapOffset == null) { - return false; - } - - return (secondTapOffset - _lastTapOffset!).distance <= kDoubleTapSlop; } @override Widget build(BuildContext context) { final gestures = {}; - // Use _TransparentTapGestureRecognizer so that TextSelectionGestureDetector - // can receive the same tap events that a selection handle placed visually - // on top of it also receives. - gestures[_TransparentTapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers<_TransparentTapGestureRecognizer>( - () => _TransparentTapGestureRecognizer(debugOwner: this), + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), (instance) { instance - ..onTapDown = _handleTapDown - ..onTapUp = _handleTapUp - ..onTapCancel = _handleTapCancel - ..onSecondaryTapDown = _handleSecondaryTapDown - ..onSecondaryTapUp = _handleSecondaryTapUp - ..onSecondaryTapCancel = _handleSecondaryTapCancel; + ..onSecondaryTap = widget.onSecondaryTap + ..onSecondaryTapDown = widget.onSecondaryTapDown; }, ); @@ -1110,21 +1062,51 @@ class _EditorTextSelectionGestureDetectorState if (widget.onDragSelectionStart != null || widget.onDragSelectionUpdate != null || widget.onDragSelectionEnd != null) { - gestures[HorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => HorizontalDragGestureRecognizer( - debugOwner: this, - supportedDevices: {PointerDeviceKind.mouse}), - (instance) { - // Text selection should start from the position of the first pointer - // down event. - instance - ..dragStartBehavior = DragStartBehavior.down - ..onStart = _handleDragStart - ..onUpdate = _handleDragUpdate - ..onEnd = _handleDragEnd; - }, - ); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + gestures[TapAndHorizontalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers< + TapAndHorizontalDragGestureRecognizer>( + () => TapAndHorizontalDragGestureRecognizer(debugOwner: this), + (instance) { + instance + // Text selection should start from the position of the first pointer + // down event. + ..dragStartBehavior = DragStartBehavior.down + ..onTapTrackStart = _handleTapTrackStart + ..onTapTrackReset = _handleTapTrackReset + ..onTapDown = _handleTapDown + ..onDragStart = _handleDragStart + ..onDragUpdate = _handleDragUpdate + ..onDragEnd = _handleDragEnd + ..onTapUp = _handleTapUp + ..onCancel = _handleTapCancel; + }, + ); + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + gestures[TapAndPanGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapAndPanGestureRecognizer(debugOwner: this), + (instance) { + instance + // Text selection should start from the position of the first pointer + // down event. + ..dragStartBehavior = DragStartBehavior.down + ..onTapTrackStart = _handleTapTrackStart + ..onTapTrackReset = _handleTapTrackReset + ..onTapDown = _handleTapDown + ..onDragStart = _handleDragStart + ..onDragUpdate = _handleDragUpdate + ..onDragEnd = _handleDragEnd + ..onTapUp = _handleTapUp + ..onCancel = _handleTapCancel; + }, + ); + } } if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { @@ -1148,32 +1130,3 @@ class _EditorTextSelectionGestureDetectorState ); } } - -// A TapGestureRecognizer which allows other GestureRecognizers to win in the -// GestureArena. This means both _TransparentTapGestureRecognizer and other -// GestureRecognizers can handle the same event. -// -// This enables proper handling of events on both the selection handle and the -// underlying input, since there is significant overlap between the two given -// the handle's padded hit area. For example, the selection handle needs to -// handle single taps on itself, but double taps need to be handled by the -// underlying input. -class _TransparentTapGestureRecognizer extends TapGestureRecognizer { - _TransparentTapGestureRecognizer({ - super.debugOwner, - }); - - @override - void rejectGesture(int pointer) { - // Accept new gestures that another recognizer has already won. - // Specifically, this needs to accept taps on the text selection handle on - // behalf of the text field in order to handle double tap to select. It must - // not accept other gestures like longpresses and drags that end outside of - // the text field. - if (state == GestureRecognizerState.ready) { - acceptGesture(pointer); - } else { - super.rejectGesture(pointer); - } - } -} From 20262740cf556b17a51d6e3d500773c0e27d3512 Mon Sep 17 00:00:00 2001 From: xuyang Date: Tue, 20 Aug 2024 09:08:04 +0800 Subject: [PATCH 2/5] [chore]: change gesture --- .../editor/raw_editor/raw_editor_state.dart | 281 ++++++------------ lib/src/editor/widgets/delegate.dart | 5 +- 2 files changed, 93 insertions(+), 193 deletions(-) diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index 300b6d390..fccddbfc4 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -9,15 +9,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderAbstractViewport; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart' - show - Clipboard, - HardwareKeyboard, - KeyDownEvent, - LogicalKeyboardKey, - SystemChannels, - TextInputControl; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' - show KeyboardVisibilityController; + show Clipboard, HardwareKeyboard, KeyDownEvent, LogicalKeyboardKey, SystemChannels, TextInputControl; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' show KeyboardVisibilityController; import '../../common/structs/horizontal_spacing.dart'; import '../../common/structs/offset_value.dart'; @@ -107,11 +100,8 @@ class QuillRawEditorState extends EditorState @override void insertContent(KeyboardInsertedContent content) { - assert(widget.configurations.contentInsertionConfiguration?.allowedMimeTypes - .contains(content.mimeType) ?? - false); - widget.configurations.contentInsertionConfiguration?.onContentInserted - .call(content); + assert(widget.configurations.contentInsertionConfiguration?.allowedMimeTypes.contains(content.mimeType) ?? false); + widget.configurations.contentInsertionConfiguration?.onContentInserted.call(content); } /// Copy current selection to [Clipboard]. @@ -127,8 +117,7 @@ class QuillRawEditorState extends EditorState userUpdateTextEditingValue( TextEditingValue( text: textEditingValue.text, - selection: - TextSelection.collapsed(offset: textEditingValue.selection.end), + selection: TextSelection.collapsed(offset: textEditingValue.selection.end), ), SelectionChangedCause.toolbar, ); @@ -202,8 +191,7 @@ class QuillRawEditorState extends EditorState void selectAll(SelectionChangedCause cause) { userUpdateTextEditingValue( textEditingValue.copyWith( - selection: TextSelection( - baseOffset: 0, extentOffset: textEditingValue.text.length), + selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length), ), cause, ); @@ -218,27 +206,14 @@ class QuillRawEditorState extends EditorState /// Copied from [EditableTextState]. List get contextMenuButtonItems { return EditableText.getEditableButtonItems( - clipboardStatus: - (_clipboardStatus != null) ? _clipboardStatus!.value : 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: lookUpEnabled - ? () => lookUpSelection(SelectionChangedCause.toolbar) - : null, - onSearchWeb: searchWebEnabled - ? () => searchWebForSelection(SelectionChangedCause.toolbar) - : null, - onShare: shareEnabled - ? () => shareSelection(SelectionChangedCause.toolbar) - : null, + clipboardStatus: (_clipboardStatus != null) ? _clipboardStatus!.value : 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: lookUpEnabled ? () => lookUpSelection(SelectionChangedCause.toolbar) : null, + onSearchWeb: searchWebEnabled ? () => searchWebForSelection(SelectionChangedCause.toolbar) : null, + onShare: shareEnabled ? () => shareSelection(SelectionChangedCause.toolbar) : null, onLiveTextInput: liveTextInputEnabled ? () {} : null, ); } @@ -332,10 +307,8 @@ class QuillRawEditorState extends EditorState ); } - final startCharacterRect = - renderEditor.getLocalRectForCaret(selection.base); - final endCharacterRect = - renderEditor.getLocalRectForCaret(selection.extent); + final startCharacterRect = renderEditor.getLocalRectForCaret(selection.base); + final endCharacterRect = renderEditor.getLocalRectForCaret(selection.extent); return QuillEditorGlyphHeights( startCharacterRect.height, endCharacterRect.height, @@ -382,12 +355,9 @@ class QuillRawEditorState extends EditorState Widget _scribbleFocusable(Widget child) { return ScribbleFocusable( editorKey: _editorKey, - enabled: widget.configurations.enableScribble && - !widget.configurations.readOnly, - renderBoxForBounds: () => context - .findAncestorStateOfType() - ?.context - .findRenderObject() as RenderBox?, + enabled: widget.configurations.enableScribble && !widget.configurations.readOnly, + renderBoxForBounds: () => + context.findAncestorStateOfType()?.context.findRenderObject() as RenderBox?, onScribbleFocus: (offset) { widget.configurations.focusNode.requestFocus(); widget.configurations.onScribbleActivated?.call(); @@ -433,8 +403,7 @@ class QuillRawEditorState extends EditorState /// the scroll view with [BaselineProxy] which mimics the editor's /// baseline. // This implies that the first line has no styles applied to it. - final baselinePadding = - EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.top); + final baselinePadding = EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.top); child = BaselineProxy( textStyle: _styles!.paragraph!.style, padding: baselinePadding, @@ -464,8 +433,7 @@ class QuillRawEditorState extends EditorState padding: widget.configurations.padding, maxContentWidth: widget.configurations.maxContentWidth, cursorController: _cursorCont, - floatingCursorDisabled: - widget.configurations.floatingCursorDisabled, + floatingCursorDisabled: widget.configurations.floatingCursorDisabled, children: _buildChildren(doc, context), ), ), @@ -479,9 +447,8 @@ class QuillRawEditorState extends EditorState link: _toolbarLayerLink, child: Semantics( child: MouseRegion( - cursor: widget.configurations.readOnly - ? widget.configurations.readOnlyMouseCursor - : SystemMouseCursors.text, + cursor: + widget.configurations.readOnly ? widget.configurations.readOnlyMouseCursor : SystemMouseCursors.text, child: QuillRawEditorMultiChildRenderObject( key: _editorKey, document: doc, @@ -497,8 +464,7 @@ class QuillRawEditorState extends EditorState scrollBottomInset: widget.configurations.scrollBottomInset, padding: widget.configurations.padding, maxContentWidth: widget.configurations.maxContentWidth, - floatingCursorDisabled: - widget.configurations.floatingCursorDisabled, + floatingCursorDisabled: widget.configurations.floatingCursorDisabled, children: _buildChildren(doc, context), ), ), @@ -708,15 +674,12 @@ class QuillRawEditorState extends EditorState LogicalKeyboardKey.pageUp, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ScrollIntent( - direction: AxisDirection.up, type: ScrollIncrementType.page), + ): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page), SingleActivator( LogicalKeyboardKey.pageDown, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ScrollIntent( - direction: AxisDirection.down, - type: ScrollIncrementType.page), + ): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page), }, ), child: Actions( @@ -770,8 +733,7 @@ class QuillRawEditorState extends EditorState } KeyEventResult _handleSpaceKey(KeyEvent event) { - final child = - controller.document.queryChild(controller.selection.baseOffset); + final child = controller.document.queryChild(controller.selection.baseOffset); if (child.node == null) { return KeyEventResult.ignored; } @@ -788,8 +750,7 @@ class QuillRawEditorState extends EditorState const olKeyPhrase = '1.'; const ulKeyPhrase = '-'; - final enableMdConversion = - widget.configurations.enableMarkdownStyleConversion; + final enableMdConversion = widget.configurations.enableMarkdownStyleConversion; if (text.value == olKeyPhrase && enableMdConversion) { _updateSelectionForKeyPhrase(olKeyPhrase, Attribute.ol); @@ -803,8 +764,7 @@ class QuillRawEditorState extends EditorState } KeyEventResult _handleTabKey(KeyEvent event) { - final child = - controller.document.queryChild(controller.selection.baseOffset); + final child = controller.document.queryChild(controller.selection.baseOffset); KeyEventResult insertTabCharacter() { if (widget.configurations.readOnly) { @@ -866,23 +826,20 @@ class QuillRawEditorState extends EditorState void _moveCursor(int chars) { final selection = controller.selection; controller.updateSelection( - controller.selection.copyWith( - baseOffset: selection.baseOffset + chars, - extentOffset: selection.baseOffset + chars), + controller.selection + .copyWith(baseOffset: selection.baseOffset + chars, extentOffset: selection.baseOffset + chars), ChangeSource.local); } void _updateSelectionForKeyPhrase(String phrase, Attribute attribute) { - controller.replaceText(controller.selection.baseOffset - phrase.length, - phrase.length, '\n', null); + controller.replaceText(controller.selection.baseOffset - phrase.length, phrase.length, '\n', null); _moveCursor(-phrase.length); controller ..formatSelection(attribute) // Remove the added newline. ..replaceText(controller.selection.baseOffset + 1, 1, '', null); // - final style = - controller.document.collectStyle(controller.selection.baseOffset, 0); + final style = controller.document.collectStyle(controller.selection.baseOffset, 0); if (style.isNotEmpty) { for (final attr in style.values) { controller.formatSelection(attr); @@ -930,10 +887,8 @@ class QuillRawEditorState extends EditorState /// Updates the checkbox positioned at [offset] in document /// by changing its attribute according to [value]. void _handleCheckboxTap(int offset, bool value) { - final requestKeyboardFocusOnCheckListChanged = - widget.configurations.requestKeyboardFocusOnCheckListChanged; - if (!(widget.configurations.checkBoxReadOnly ?? - widget.configurations.readOnly)) { + final requestKeyboardFocusOnCheckListChanged = widget.configurations.requestKeyboardFocusOnCheckListChanged; + if (!(widget.configurations.checkBoxReadOnly ?? widget.configurations.readOnly)) { _disableScrollControllerAnimateOnce = true; final currentSelection = controller.selection.copyWith(); final attribute = value ? Attribute.checked : Attribute.unchecked; @@ -946,10 +901,7 @@ class QuillRawEditorState extends EditorState // Checkbox tapping causes controller.selection to go to offset 0 // Stop toggling those two toolbar buttons - ..toolbarButtonToggler = { - Attribute.list.key: attribute, - Attribute.header.key: Attribute.header - }; + ..toolbarButtonToggler = {Attribute.list.key: attribute, Attribute.header.key: Attribute.header}; // Go back from offset 0 to current selection SchedulerBinding.instance.addPostFrameCallback((_) { @@ -985,14 +937,12 @@ class QuillRawEditorState extends EditorState // and watch if the system language is a RTL language and avoid putting // to the edge of the left side any checkbox or list point/number if is a // RTL language - if (nodeTextDirection == TextDirection.ltr && - _textDirection == TextDirection.rtl) { + if (nodeTextDirection == TextDirection.ltr && _textDirection == TextDirection.rtl) { nodeTextDirection = TextDirection.rtl; } if (node is Line) { final editableTextLine = _getEditableTextLineFromNode(node, context); - result.add(Directionality( - textDirection: nodeTextDirection, child: editableTextLine)); + result.add(Directionality(textDirection: nodeTextDirection, child: editableTextLine)); } else if (node is Block) { final editableTextBlock = EditableTextBlock( block: node, @@ -1004,12 +954,9 @@ class QuillRawEditorState extends EditorState textSelection: controller.selection, color: widget.configurations.selectionColor, styles: _styles, - enableInteractiveSelection: - widget.configurations.enableInteractiveSelection, + enableInteractiveSelection: widget.configurations.enableInteractiveSelection, hasFocus: _hasFocus, - contentPadding: attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, + contentPadding: attrs.containsKey(Attribute.codeBlock.key) ? const EdgeInsets.all(16) : null, embedBuilder: widget.configurations.embedBuilder, linkActionPicker: _linkActionPicker, onLaunchUrl: widget.configurations.onLaunchUrl, @@ -1039,8 +986,7 @@ class QuillRawEditorState extends EditorState return result; } - EditableTextLine _getEditableTextLineFromNode( - Line node, BuildContext context) { + EditableTextLine _getEditableTextLineFromNode(Line node, BuildContext context) { final textLine = TextLine( line: node, textDirection: _textDirection, @@ -1136,8 +1082,7 @@ class QuillRawEditorState extends EditorState return defaultStyles!.paragraph!.verticalSpacing; } - HorizontalSpacing _getHorizontalSpacingForBlock( - Block node, DefaultStyles? defaultStyles) { + HorizontalSpacing _getHorizontalSpacingForBlock(Block node, DefaultStyles? defaultStyles) { final attrs = node.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { return defaultStyles!.quote!.horizontalSpacing; @@ -1153,8 +1098,7 @@ class QuillRawEditorState extends EditorState return HorizontalSpacing.zero; } - VerticalSpacing _getVerticalSpacingForBlock( - Block node, DefaultStyles? defaultStyles) { + VerticalSpacing _getVerticalSpacingForBlock(Block node, DefaultStyles? defaultStyles) { final attrs = node.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { return defaultStyles!.quote!.verticalSpacing; @@ -1209,8 +1153,7 @@ class QuillRawEditorState extends EditorState } else { _keyboardVisibilityController = KeyboardVisibilityController(); _keyboardVisible = _keyboardVisibilityController!.isVisible; - _keyboardVisibilitySubscription = - _keyboardVisibilityController?.onChange.listen((visible) { + _keyboardVisibilitySubscription = _keyboardVisibilityController?.onChange.listen((visible) { _keyboardVisible = visible; if (visible) { _onChangeTextEditingValue(!_hasFocus); @@ -1252,9 +1195,7 @@ class QuillRawEditorState extends EditorState super.didChangeDependencies(); final parentStyles = QuillStyles.getStyles(context, true); final defaultStyles = DefaultStyles.getInstance(context); - _styles = (parentStyles != null) - ? defaultStyles.merge(parentStyles) - : defaultStyles; + _styles = (parentStyles != null) ? defaultStyles.merge(parentStyles) : defaultStyles; if (widget.configurations.customStyles != null) { _styles = _styles!.merge(widget.configurations.customStyles!); @@ -1317,8 +1258,7 @@ class QuillRawEditorState extends EditorState } bool _shouldShowSelectionHandles() { - return widget.configurations.showSelectionHandles && - !controller.selection.isCollapsed; + return widget.configurations.showSelectionHandles && !controller.selection.isCollapsed; } @override @@ -1419,10 +1359,7 @@ class QuillRawEditorState extends EditorState void _updateOrDisposeSelectionOverlayIfNeeded() { if (_selectionOverlay != null) { - if (!_hasFocus || textEditingValue.selection.isCollapsed) { - _selectionOverlay?.dispose(); - _selectionOverlay = null; - } else if (_hasFocus) { + if (_hasFocus) { _selectionOverlay!.update(textEditingValue); } else { _selectionOverlay!.dispose(); @@ -1448,18 +1385,16 @@ class QuillRawEditorState extends EditorState clipboardStatus: _clipboardStatus, contextMenuBuilder: widget.configurations.contextMenuBuilder == null ? null - : (context) => - widget.configurations.contextMenuBuilder!(context, this), - magnifierConfiguration: widget.configurations.magnifierConfiguration ?? - TextMagnifier.adaptiveMagnifierConfiguration, + : (context) => widget.configurations.contextMenuBuilder!(context, this), + magnifierConfiguration: + widget.configurations.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, ); } void _handleFocusChanged() { if (dirty) { requestKeyboard(); - SchedulerBinding.instance - .addPostFrameCallback((_) => _handleFocusChanged()); + SchedulerBinding.instance.addPostFrameCallback((_) => _handleFocusChanged()); return; } openOrCloseConnection(); @@ -1483,8 +1418,7 @@ class QuillRawEditorState extends EditorState Future _linkActionPicker(Node linkNode) async { final link = linkNode.style.attributes[Attribute.link.key]!.value!; - return widget.configurations - .linkActionPickerDelegate(context, link, linkNode); + return widget.configurations.linkActionPickerDelegate(context, link, linkNode); } bool _showCaretOnScreenScheduled = false; @@ -1511,8 +1445,7 @@ class QuillRawEditorState extends EditorState } final viewport = RenderAbstractViewport.of(renderEditor); - final editorOffset = - renderEditor.localToGlobal(const Offset(0, 0), ancestor: viewport); + final editorOffset = renderEditor.localToGlobal(const Offset(0, 0), ancestor: viewport); final offsetInViewport = _scrollController.offset + editorOffset.dy; final offset = renderEditor.getOffsetToRevealCursor( @@ -1540,8 +1473,7 @@ class QuillRawEditorState extends EditorState /// /// This property is typically used to notify the renderer of input gestures. @override - RenderEditor get renderEditor => - _editorKey.currentContext!.findRenderObject() as RenderEditor; + RenderEditor get renderEditor => _editorKey.currentContext!.findRenderObject() as RenderEditor; /// Express interest in interacting with the keyboard. /// @@ -1616,8 +1548,7 @@ class QuillRawEditorState extends EditorState void _replaceText(ReplaceTextIntent intent) { userUpdateTextEditingValue( - intent.currentTextEditingValue - .replaced(intent.replacementRange, intent.replacementText), + intent.currentTextEditingValue.replaced(intent.replacementRange, intent.replacementText), intent.cause, ); } @@ -1626,22 +1557,18 @@ class QuillRawEditorState extends EditorState bool get wantKeepAlive => widget.configurations.focusNode.hasFocus; @override - AnimationController get floatingCursorResetController => - _floatingCursorResetController; + AnimationController get floatingCursorResetController => _floatingCursorResetController; late AnimationController _floatingCursorResetController; // --------------------------- Text Editing Actions -------------------------- - QuillEditorTextBoundary _characterBoundary( - DirectionalTextEditingIntent intent) { + QuillEditorTextBoundary _characterBoundary(DirectionalTextEditingIntent intent) { final atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); - return QuillEditorCollapsedSelectionBoundary( - atomicTextBoundary, intent.forward); + return QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, intent.forward); } - QuillEditorTextBoundary _nextWordBoundary( - DirectionalTextEditingIntent intent) { + QuillEditorTextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) { final QuillEditorTextBoundary atomicTextBoundary; final QuillEditorTextBoundary boundary; @@ -1650,8 +1577,7 @@ class QuillRawEditorState extends EditorState atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); // This isn't enough. Newline characters. boundary = QuillEditorExpandedTextBoundary( - QuillEditorWhitespaceBoundary(textEditingValue), - QuillEditorWordBoundary(renderEditor, textEditingValue)); + QuillEditorWhitespaceBoundary(textEditingValue), QuillEditorWordBoundary(renderEditor, textEditingValue)); final mixedBoundary = intent.forward ? QuillEditorMixedBoundary(atomicTextBoundary, boundary) @@ -1676,26 +1602,21 @@ class QuillRawEditorState extends EditorState // since the document boundary is unique and the linebreak boundary is // already caret-location based. return intent.forward - ? QuillEditorMixedBoundary( - QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), - boundary) + ? QuillEditorMixedBoundary(QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), boundary) : QuillEditorMixedBoundary( boundary, QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, false), ); } - QuillEditorTextBoundary _documentBoundary( - DirectionalTextEditingIntent intent) => + QuillEditorTextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => QuillEditorDocumentBoundary(textEditingValue); Action _makeOverridable(Action defaultAction) { - return Action.overridable( - context: context, defaultAction: defaultAction); + return Action.overridable(context: context, defaultAction: defaultAction); } - late final Action _replaceTextAction = - CallbackAction(onInvoke: _replaceText); + late final Action _replaceTextAction = CallbackAction(onInvoke: _replaceText); void _updateSelection(UpdateSelectionIntent intent) { userUpdateTextEditingValue( @@ -1707,27 +1628,20 @@ class QuillRawEditorState extends EditorState late final Action _updateSelectionAction = CallbackAction(onInvoke: _updateSelection); - late final QuillEditorUpdateTextSelectionToAdjacentLineAction< - ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = - QuillEditorUpdateTextSelectionToAdjacentLineAction< - ExtendSelectionVerticallyToAdjacentLineIntent>(this); + late final QuillEditorUpdateTextSelectionToAdjacentLineAction + _adjacentLineAction = + QuillEditorUpdateTextSelectionToAdjacentLineAction(this); late final _adjacentPageAction = - QuillEditorUpdateTextSelectionToAdjacentPageAction< - ExtendSelectionVerticallyToAdjacentPageIntent>(this); + QuillEditorUpdateTextSelectionToAdjacentPageAction(this); - late final QuillEditorToggleTextStyleAction _formatSelectionAction = - QuillEditorToggleTextStyleAction(this); + late final QuillEditorToggleTextStyleAction _formatSelectionAction = QuillEditorToggleTextStyleAction(this); - late final QuillEditorIndentSelectionAction _indentSelectionAction = - QuillEditorIndentSelectionAction(this); + late final QuillEditorIndentSelectionAction _indentSelectionAction = QuillEditorIndentSelectionAction(this); - late final QuillEditorOpenSearchAction _openSearchAction = - QuillEditorOpenSearchAction(this); - late final QuillEditorApplyHeaderAction _applyHeaderAction = - QuillEditorApplyHeaderAction(this); - late final QuillEditorApplyCheckListAction _applyCheckListAction = - QuillEditorApplyCheckListAction(this); + late final QuillEditorOpenSearchAction _openSearchAction = QuillEditorOpenSearchAction(this); + late final QuillEditorApplyHeaderAction _applyHeaderAction = QuillEditorApplyHeaderAction(this); + late final QuillEditorApplyCheckListAction _applyCheckListAction = QuillEditorApplyCheckListAction(this); late final Map> _actions = >{ DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), @@ -1736,48 +1650,35 @@ class QuillRawEditorState extends EditorState DirectionalFocusIntent: DirectionalFocusAction.forTextField(), // Delete - DeleteCharacterIntent: _makeOverridable( - QuillEditorDeleteTextAction( - this, _characterBoundary)), - DeleteToNextWordBoundaryIntent: _makeOverridable( - QuillEditorDeleteTextAction( - this, _nextWordBoundary)), - DeleteToLineBreakIntent: _makeOverridable( - QuillEditorDeleteTextAction(this, _linebreak)), + DeleteCharacterIntent: + _makeOverridable(QuillEditorDeleteTextAction(this, _characterBoundary)), + DeleteToNextWordBoundaryIntent: + _makeOverridable(QuillEditorDeleteTextAction(this, _nextWordBoundary)), + DeleteToLineBreakIntent: _makeOverridable(QuillEditorDeleteTextAction(this, _linebreak)), // Extend/Move Selection - ExtendSelectionByCharacterIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction( + ExtendSelectionByCharacterIntent: + _makeOverridable(QuillEditorUpdateTextSelectionAction( this, false, _characterBoundary, )), ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction< - ExtendSelectionToNextWordBoundaryIntent>( - this, true, _nextWordBoundary)), + QuillEditorUpdateTextSelectionAction(this, true, _nextWordBoundary)), ExtendSelectionToLineBreakIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction( - this, true, _linebreak)), - ExtendSelectionVerticallyToAdjacentLineIntent: - _makeOverridable(_adjacentLineAction), + QuillEditorUpdateTextSelectionAction(this, true, _linebreak)), + ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction), ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction< - ExtendSelectionToDocumentBoundaryIntent>( - this, true, _documentBoundary)), - ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( - QuillEditorExtendSelectionOrCaretPositionAction( - this, _nextWordBoundary)), + QuillEditorUpdateTextSelectionAction(this, true, _documentBoundary)), + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: + _makeOverridable(QuillEditorExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)), // Copy Paste SelectAllTextIntent: _makeOverridable(QuillEditorSelectAllAction(this)), - CopySelectionTextIntent: - _makeOverridable(QuillEditorCopySelectionAction(this)), - PasteTextIntent: _makeOverridable(CallbackAction( - onInvoke: (intent) => pasteText(intent.cause))), + CopySelectionTextIntent: _makeOverridable(QuillEditorCopySelectionAction(this)), + PasteTextIntent: _makeOverridable(CallbackAction(onInvoke: (intent) => pasteText(intent.cause))), - HideSelectionToolbarIntent: - _makeOverridable(QuillEditorHideSelectionToolbarAction(this)), + HideSelectionToolbarIntent: _makeOverridable(QuillEditorHideSelectionToolbarAction(this)), UndoTextIntent: _makeOverridable(QuillEditorUndoKeyboardAction(this)), RedoTextIntent: _makeOverridable(QuillEditorRedoKeyboardAction(this)), @@ -1851,10 +1752,8 @@ class QuillRawEditorState extends EditorState if (_hasFocus == false) return; if (_selectionOverlay == null) return; final position = renderEditor.getPositionForOffset(positionToShow); - _selectionOverlay?.showMagnifier(position, positionToShow, renderEditor); if (_selectionOverlay!.magnifierIsVisible) { - _selectionOverlay! - .updateMagnifier(position, positionToShow, renderEditor); + _selectionOverlay!.updateMagnifier(position, positionToShow, renderEditor); } else { _selectionOverlay!.showMagnifier(position, positionToShow, renderEditor); } diff --git a/lib/src/editor/widgets/delegate.dart b/lib/src/editor/widgets/delegate.dart index f51f74c48..dada74300 100644 --- a/lib/src/editor/widgets/delegate.dart +++ b/lib/src/editor/widgets/delegate.dart @@ -8,7 +8,7 @@ import 'package:flutter/services.dart'; import '../../common/utils/platform.dart'; import '../../document/attribute.dart'; import '../../document/nodes/leaf.dart'; -import '../../widgets/editor/editor.dart'; +import '../editor.dart'; import '../raw_editor/raw_editor.dart'; import 'text/text_selection.dart'; @@ -254,7 +254,8 @@ class EditorTextSelectionGestureDetectorBuilder { @protected void onTapDown(TapDragDownDetails details) { if (!delegate.selectionEnabled) return; - renderEditor!.handleTapDown(details); + renderEditor! + .handleTapDown(TapDownDetails(globalPosition: details.globalPosition)); final kind = details.kind; shouldShowSelectionToolbar = kind == null || kind == PointerDeviceKind.touch || From 9cf06302613c781b8f5f8fea58ba09c258a08b3d Mon Sep 17 00:00:00 2001 From: xuyang Date: Tue, 20 Aug 2024 09:33:46 +0800 Subject: [PATCH 3/5] [chore]: code fromatter --- .../editor/raw_editor/raw_editor_state.dart | 275 ++++++++++++------ 1 file changed, 186 insertions(+), 89 deletions(-) diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index fccddbfc4..d9eccc0a6 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -9,8 +9,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderAbstractViewport; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart' - show Clipboard, HardwareKeyboard, KeyDownEvent, LogicalKeyboardKey, SystemChannels, TextInputControl; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' show KeyboardVisibilityController; + show + Clipboard, + HardwareKeyboard, + KeyDownEvent, + LogicalKeyboardKey, + SystemChannels, + TextInputControl; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' + show KeyboardVisibilityController; import '../../common/structs/horizontal_spacing.dart'; import '../../common/structs/offset_value.dart'; @@ -100,8 +107,11 @@ class QuillRawEditorState extends EditorState @override void insertContent(KeyboardInsertedContent content) { - assert(widget.configurations.contentInsertionConfiguration?.allowedMimeTypes.contains(content.mimeType) ?? false); - widget.configurations.contentInsertionConfiguration?.onContentInserted.call(content); + assert(widget.configurations.contentInsertionConfiguration?.allowedMimeTypes + .contains(content.mimeType) ?? + false); + widget.configurations.contentInsertionConfiguration?.onContentInserted + .call(content); } /// Copy current selection to [Clipboard]. @@ -117,7 +127,8 @@ class QuillRawEditorState extends EditorState userUpdateTextEditingValue( TextEditingValue( text: textEditingValue.text, - selection: TextSelection.collapsed(offset: textEditingValue.selection.end), + selection: + TextSelection.collapsed(offset: textEditingValue.selection.end), ), SelectionChangedCause.toolbar, ); @@ -191,7 +202,8 @@ class QuillRawEditorState extends EditorState void selectAll(SelectionChangedCause cause) { userUpdateTextEditingValue( textEditingValue.copyWith( - selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length), + selection: TextSelection( + baseOffset: 0, extentOffset: textEditingValue.text.length), ), cause, ); @@ -206,14 +218,27 @@ class QuillRawEditorState extends EditorState /// Copied from [EditableTextState]. List get contextMenuButtonItems { return EditableText.getEditableButtonItems( - clipboardStatus: (_clipboardStatus != null) ? _clipboardStatus!.value : 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: lookUpEnabled ? () => lookUpSelection(SelectionChangedCause.toolbar) : null, - onSearchWeb: searchWebEnabled ? () => searchWebForSelection(SelectionChangedCause.toolbar) : null, - onShare: shareEnabled ? () => shareSelection(SelectionChangedCause.toolbar) : null, + clipboardStatus: + (_clipboardStatus != null) ? _clipboardStatus!.value : 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: lookUpEnabled + ? () => lookUpSelection(SelectionChangedCause.toolbar) + : null, + onSearchWeb: searchWebEnabled + ? () => searchWebForSelection(SelectionChangedCause.toolbar) + : null, + onShare: shareEnabled + ? () => shareSelection(SelectionChangedCause.toolbar) + : null, onLiveTextInput: liveTextInputEnabled ? () {} : null, ); } @@ -307,8 +332,10 @@ class QuillRawEditorState extends EditorState ); } - final startCharacterRect = renderEditor.getLocalRectForCaret(selection.base); - final endCharacterRect = renderEditor.getLocalRectForCaret(selection.extent); + final startCharacterRect = + renderEditor.getLocalRectForCaret(selection.base); + final endCharacterRect = + renderEditor.getLocalRectForCaret(selection.extent); return QuillEditorGlyphHeights( startCharacterRect.height, endCharacterRect.height, @@ -355,9 +382,12 @@ class QuillRawEditorState extends EditorState Widget _scribbleFocusable(Widget child) { return ScribbleFocusable( editorKey: _editorKey, - enabled: widget.configurations.enableScribble && !widget.configurations.readOnly, - renderBoxForBounds: () => - context.findAncestorStateOfType()?.context.findRenderObject() as RenderBox?, + enabled: widget.configurations.enableScribble && + !widget.configurations.readOnly, + renderBoxForBounds: () => context + .findAncestorStateOfType() + ?.context + .findRenderObject() as RenderBox?, onScribbleFocus: (offset) { widget.configurations.focusNode.requestFocus(); widget.configurations.onScribbleActivated?.call(); @@ -403,7 +433,8 @@ class QuillRawEditorState extends EditorState /// the scroll view with [BaselineProxy] which mimics the editor's /// baseline. // This implies that the first line has no styles applied to it. - final baselinePadding = EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.top); + final baselinePadding = + EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.top); child = BaselineProxy( textStyle: _styles!.paragraph!.style, padding: baselinePadding, @@ -433,7 +464,8 @@ class QuillRawEditorState extends EditorState padding: widget.configurations.padding, maxContentWidth: widget.configurations.maxContentWidth, cursorController: _cursorCont, - floatingCursorDisabled: widget.configurations.floatingCursorDisabled, + floatingCursorDisabled: + widget.configurations.floatingCursorDisabled, children: _buildChildren(doc, context), ), ), @@ -447,8 +479,9 @@ class QuillRawEditorState extends EditorState link: _toolbarLayerLink, child: Semantics( child: MouseRegion( - cursor: - widget.configurations.readOnly ? widget.configurations.readOnlyMouseCursor : SystemMouseCursors.text, + cursor: widget.configurations.readOnly + ? widget.configurations.readOnlyMouseCursor + : SystemMouseCursors.text, child: QuillRawEditorMultiChildRenderObject( key: _editorKey, document: doc, @@ -464,7 +497,8 @@ class QuillRawEditorState extends EditorState scrollBottomInset: widget.configurations.scrollBottomInset, padding: widget.configurations.padding, maxContentWidth: widget.configurations.maxContentWidth, - floatingCursorDisabled: widget.configurations.floatingCursorDisabled, + floatingCursorDisabled: + widget.configurations.floatingCursorDisabled, children: _buildChildren(doc, context), ), ), @@ -674,12 +708,15 @@ class QuillRawEditorState extends EditorState LogicalKeyboardKey.pageUp, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page), + ): const ScrollIntent( + direction: AxisDirection.up, type: ScrollIncrementType.page), SingleActivator( LogicalKeyboardKey.pageDown, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page), + ): const ScrollIntent( + direction: AxisDirection.down, + type: ScrollIncrementType.page), }, ), child: Actions( @@ -733,7 +770,8 @@ class QuillRawEditorState extends EditorState } KeyEventResult _handleSpaceKey(KeyEvent event) { - final child = controller.document.queryChild(controller.selection.baseOffset); + final child = + controller.document.queryChild(controller.selection.baseOffset); if (child.node == null) { return KeyEventResult.ignored; } @@ -750,7 +788,8 @@ class QuillRawEditorState extends EditorState const olKeyPhrase = '1.'; const ulKeyPhrase = '-'; - final enableMdConversion = widget.configurations.enableMarkdownStyleConversion; + final enableMdConversion = + widget.configurations.enableMarkdownStyleConversion; if (text.value == olKeyPhrase && enableMdConversion) { _updateSelectionForKeyPhrase(olKeyPhrase, Attribute.ol); @@ -764,7 +803,8 @@ class QuillRawEditorState extends EditorState } KeyEventResult _handleTabKey(KeyEvent event) { - final child = controller.document.queryChild(controller.selection.baseOffset); + final child = + controller.document.queryChild(controller.selection.baseOffset); KeyEventResult insertTabCharacter() { if (widget.configurations.readOnly) { @@ -826,20 +866,23 @@ class QuillRawEditorState extends EditorState void _moveCursor(int chars) { final selection = controller.selection; controller.updateSelection( - controller.selection - .copyWith(baseOffset: selection.baseOffset + chars, extentOffset: selection.baseOffset + chars), + controller.selection.copyWith( + baseOffset: selection.baseOffset + chars, + extentOffset: selection.baseOffset + chars), ChangeSource.local); } void _updateSelectionForKeyPhrase(String phrase, Attribute attribute) { - controller.replaceText(controller.selection.baseOffset - phrase.length, phrase.length, '\n', null); + controller.replaceText(controller.selection.baseOffset - phrase.length, + phrase.length, '\n', null); _moveCursor(-phrase.length); controller ..formatSelection(attribute) // Remove the added newline. ..replaceText(controller.selection.baseOffset + 1, 1, '', null); // - final style = controller.document.collectStyle(controller.selection.baseOffset, 0); + final style = + controller.document.collectStyle(controller.selection.baseOffset, 0); if (style.isNotEmpty) { for (final attr in style.values) { controller.formatSelection(attr); @@ -887,8 +930,10 @@ class QuillRawEditorState extends EditorState /// Updates the checkbox positioned at [offset] in document /// by changing its attribute according to [value]. void _handleCheckboxTap(int offset, bool value) { - final requestKeyboardFocusOnCheckListChanged = widget.configurations.requestKeyboardFocusOnCheckListChanged; - if (!(widget.configurations.checkBoxReadOnly ?? widget.configurations.readOnly)) { + final requestKeyboardFocusOnCheckListChanged = + widget.configurations.requestKeyboardFocusOnCheckListChanged; + if (!(widget.configurations.checkBoxReadOnly ?? + widget.configurations.readOnly)) { _disableScrollControllerAnimateOnce = true; final currentSelection = controller.selection.copyWith(); final attribute = value ? Attribute.checked : Attribute.unchecked; @@ -901,7 +946,10 @@ class QuillRawEditorState extends EditorState // Checkbox tapping causes controller.selection to go to offset 0 // Stop toggling those two toolbar buttons - ..toolbarButtonToggler = {Attribute.list.key: attribute, Attribute.header.key: Attribute.header}; + ..toolbarButtonToggler = { + Attribute.list.key: attribute, + Attribute.header.key: Attribute.header + }; // Go back from offset 0 to current selection SchedulerBinding.instance.addPostFrameCallback((_) { @@ -937,12 +985,14 @@ class QuillRawEditorState extends EditorState // and watch if the system language is a RTL language and avoid putting // to the edge of the left side any checkbox or list point/number if is a // RTL language - if (nodeTextDirection == TextDirection.ltr && _textDirection == TextDirection.rtl) { + if (nodeTextDirection == TextDirection.ltr && + _textDirection == TextDirection.rtl) { nodeTextDirection = TextDirection.rtl; } if (node is Line) { final editableTextLine = _getEditableTextLineFromNode(node, context); - result.add(Directionality(textDirection: nodeTextDirection, child: editableTextLine)); + result.add(Directionality( + textDirection: nodeTextDirection, child: editableTextLine)); } else if (node is Block) { final editableTextBlock = EditableTextBlock( block: node, @@ -954,9 +1004,12 @@ class QuillRawEditorState extends EditorState textSelection: controller.selection, color: widget.configurations.selectionColor, styles: _styles, - enableInteractiveSelection: widget.configurations.enableInteractiveSelection, + enableInteractiveSelection: + widget.configurations.enableInteractiveSelection, hasFocus: _hasFocus, - contentPadding: attrs.containsKey(Attribute.codeBlock.key) ? const EdgeInsets.all(16) : null, + contentPadding: attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, embedBuilder: widget.configurations.embedBuilder, linkActionPicker: _linkActionPicker, onLaunchUrl: widget.configurations.onLaunchUrl, @@ -986,7 +1039,8 @@ class QuillRawEditorState extends EditorState return result; } - EditableTextLine _getEditableTextLineFromNode(Line node, BuildContext context) { + EditableTextLine _getEditableTextLineFromNode( + Line node, BuildContext context) { final textLine = TextLine( line: node, textDirection: _textDirection, @@ -1082,7 +1136,8 @@ class QuillRawEditorState extends EditorState return defaultStyles!.paragraph!.verticalSpacing; } - HorizontalSpacing _getHorizontalSpacingForBlock(Block node, DefaultStyles? defaultStyles) { + HorizontalSpacing _getHorizontalSpacingForBlock( + Block node, DefaultStyles? defaultStyles) { final attrs = node.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { return defaultStyles!.quote!.horizontalSpacing; @@ -1098,7 +1153,8 @@ class QuillRawEditorState extends EditorState return HorizontalSpacing.zero; } - VerticalSpacing _getVerticalSpacingForBlock(Block node, DefaultStyles? defaultStyles) { + VerticalSpacing _getVerticalSpacingForBlock( + Block node, DefaultStyles? defaultStyles) { final attrs = node.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { return defaultStyles!.quote!.verticalSpacing; @@ -1153,7 +1209,8 @@ class QuillRawEditorState extends EditorState } else { _keyboardVisibilityController = KeyboardVisibilityController(); _keyboardVisible = _keyboardVisibilityController!.isVisible; - _keyboardVisibilitySubscription = _keyboardVisibilityController?.onChange.listen((visible) { + _keyboardVisibilitySubscription = + _keyboardVisibilityController?.onChange.listen((visible) { _keyboardVisible = visible; if (visible) { _onChangeTextEditingValue(!_hasFocus); @@ -1195,7 +1252,9 @@ class QuillRawEditorState extends EditorState super.didChangeDependencies(); final parentStyles = QuillStyles.getStyles(context, true); final defaultStyles = DefaultStyles.getInstance(context); - _styles = (parentStyles != null) ? defaultStyles.merge(parentStyles) : defaultStyles; + _styles = (parentStyles != null) + ? defaultStyles.merge(parentStyles) + : defaultStyles; if (widget.configurations.customStyles != null) { _styles = _styles!.merge(widget.configurations.customStyles!); @@ -1258,7 +1317,8 @@ class QuillRawEditorState extends EditorState } bool _shouldShowSelectionHandles() { - return widget.configurations.showSelectionHandles && !controller.selection.isCollapsed; + return widget.configurations.showSelectionHandles && + !controller.selection.isCollapsed; } @override @@ -1385,16 +1445,18 @@ class QuillRawEditorState extends EditorState clipboardStatus: _clipboardStatus, contextMenuBuilder: widget.configurations.contextMenuBuilder == null ? null - : (context) => widget.configurations.contextMenuBuilder!(context, this), - magnifierConfiguration: - widget.configurations.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, + : (context) => + widget.configurations.contextMenuBuilder!(context, this), + magnifierConfiguration: widget.configurations.magnifierConfiguration ?? + TextMagnifier.adaptiveMagnifierConfiguration, ); } void _handleFocusChanged() { if (dirty) { requestKeyboard(); - SchedulerBinding.instance.addPostFrameCallback((_) => _handleFocusChanged()); + SchedulerBinding.instance + .addPostFrameCallback((_) => _handleFocusChanged()); return; } openOrCloseConnection(); @@ -1418,7 +1480,8 @@ class QuillRawEditorState extends EditorState Future _linkActionPicker(Node linkNode) async { final link = linkNode.style.attributes[Attribute.link.key]!.value!; - return widget.configurations.linkActionPickerDelegate(context, link, linkNode); + return widget.configurations + .linkActionPickerDelegate(context, link, linkNode); } bool _showCaretOnScreenScheduled = false; @@ -1445,7 +1508,8 @@ class QuillRawEditorState extends EditorState } final viewport = RenderAbstractViewport.of(renderEditor); - final editorOffset = renderEditor.localToGlobal(const Offset(0, 0), ancestor: viewport); + final editorOffset = + renderEditor.localToGlobal(const Offset(0, 0), ancestor: viewport); final offsetInViewport = _scrollController.offset + editorOffset.dy; final offset = renderEditor.getOffsetToRevealCursor( @@ -1473,7 +1537,8 @@ class QuillRawEditorState extends EditorState /// /// This property is typically used to notify the renderer of input gestures. @override - RenderEditor get renderEditor => _editorKey.currentContext!.findRenderObject() as RenderEditor; + RenderEditor get renderEditor => + _editorKey.currentContext!.findRenderObject() as RenderEditor; /// Express interest in interacting with the keyboard. /// @@ -1548,7 +1613,8 @@ class QuillRawEditorState extends EditorState void _replaceText(ReplaceTextIntent intent) { userUpdateTextEditingValue( - intent.currentTextEditingValue.replaced(intent.replacementRange, intent.replacementText), + intent.currentTextEditingValue + .replaced(intent.replacementRange, intent.replacementText), intent.cause, ); } @@ -1557,18 +1623,22 @@ class QuillRawEditorState extends EditorState bool get wantKeepAlive => widget.configurations.focusNode.hasFocus; @override - AnimationController get floatingCursorResetController => _floatingCursorResetController; + AnimationController get floatingCursorResetController => + _floatingCursorResetController; late AnimationController _floatingCursorResetController; // --------------------------- Text Editing Actions -------------------------- - QuillEditorTextBoundary _characterBoundary(DirectionalTextEditingIntent intent) { + QuillEditorTextBoundary _characterBoundary( + DirectionalTextEditingIntent intent) { final atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); - return QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, intent.forward); + return QuillEditorCollapsedSelectionBoundary( + atomicTextBoundary, intent.forward); } - QuillEditorTextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) { + QuillEditorTextBoundary _nextWordBoundary( + DirectionalTextEditingIntent intent) { final QuillEditorTextBoundary atomicTextBoundary; final QuillEditorTextBoundary boundary; @@ -1577,7 +1647,8 @@ class QuillRawEditorState extends EditorState atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); // This isn't enough. Newline characters. boundary = QuillEditorExpandedTextBoundary( - QuillEditorWhitespaceBoundary(textEditingValue), QuillEditorWordBoundary(renderEditor, textEditingValue)); + QuillEditorWhitespaceBoundary(textEditingValue), + QuillEditorWordBoundary(renderEditor, textEditingValue)); final mixedBoundary = intent.forward ? QuillEditorMixedBoundary(atomicTextBoundary, boundary) @@ -1602,21 +1673,26 @@ class QuillRawEditorState extends EditorState // since the document boundary is unique and the linebreak boundary is // already caret-location based. return intent.forward - ? QuillEditorMixedBoundary(QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), boundary) + ? QuillEditorMixedBoundary( + QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), + boundary) : QuillEditorMixedBoundary( boundary, QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, false), ); } - QuillEditorTextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => + QuillEditorTextBoundary _documentBoundary( + DirectionalTextEditingIntent intent) => QuillEditorDocumentBoundary(textEditingValue); Action _makeOverridable(Action defaultAction) { - return Action.overridable(context: context, defaultAction: defaultAction); + return Action.overridable( + context: context, defaultAction: defaultAction); } - late final Action _replaceTextAction = CallbackAction(onInvoke: _replaceText); + late final Action _replaceTextAction = + CallbackAction(onInvoke: _replaceText); void _updateSelection(UpdateSelectionIntent intent) { userUpdateTextEditingValue( @@ -1628,20 +1704,27 @@ class QuillRawEditorState extends EditorState late final Action _updateSelectionAction = CallbackAction(onInvoke: _updateSelection); - late final QuillEditorUpdateTextSelectionToAdjacentLineAction - _adjacentLineAction = - QuillEditorUpdateTextSelectionToAdjacentLineAction(this); + late final QuillEditorUpdateTextSelectionToAdjacentLineAction< + ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = + QuillEditorUpdateTextSelectionToAdjacentLineAction< + ExtendSelectionVerticallyToAdjacentLineIntent>(this); late final _adjacentPageAction = - QuillEditorUpdateTextSelectionToAdjacentPageAction(this); + QuillEditorUpdateTextSelectionToAdjacentPageAction< + ExtendSelectionVerticallyToAdjacentPageIntent>(this); - late final QuillEditorToggleTextStyleAction _formatSelectionAction = QuillEditorToggleTextStyleAction(this); + late final QuillEditorToggleTextStyleAction _formatSelectionAction = + QuillEditorToggleTextStyleAction(this); - late final QuillEditorIndentSelectionAction _indentSelectionAction = QuillEditorIndentSelectionAction(this); + late final QuillEditorIndentSelectionAction _indentSelectionAction = + QuillEditorIndentSelectionAction(this); - late final QuillEditorOpenSearchAction _openSearchAction = QuillEditorOpenSearchAction(this); - late final QuillEditorApplyHeaderAction _applyHeaderAction = QuillEditorApplyHeaderAction(this); - late final QuillEditorApplyCheckListAction _applyCheckListAction = QuillEditorApplyCheckListAction(this); + late final QuillEditorOpenSearchAction _openSearchAction = + QuillEditorOpenSearchAction(this); + late final QuillEditorApplyHeaderAction _applyHeaderAction = + QuillEditorApplyHeaderAction(this); + late final QuillEditorApplyCheckListAction _applyCheckListAction = + QuillEditorApplyCheckListAction(this); late final Map> _actions = >{ DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), @@ -1650,35 +1733,48 @@ class QuillRawEditorState extends EditorState DirectionalFocusIntent: DirectionalFocusAction.forTextField(), // Delete - DeleteCharacterIntent: - _makeOverridable(QuillEditorDeleteTextAction(this, _characterBoundary)), - DeleteToNextWordBoundaryIntent: - _makeOverridable(QuillEditorDeleteTextAction(this, _nextWordBoundary)), - DeleteToLineBreakIntent: _makeOverridable(QuillEditorDeleteTextAction(this, _linebreak)), + DeleteCharacterIntent: _makeOverridable( + QuillEditorDeleteTextAction( + this, _characterBoundary)), + DeleteToNextWordBoundaryIntent: _makeOverridable( + QuillEditorDeleteTextAction( + this, _nextWordBoundary)), + DeleteToLineBreakIntent: _makeOverridable( + QuillEditorDeleteTextAction(this, _linebreak)), // Extend/Move Selection - ExtendSelectionByCharacterIntent: - _makeOverridable(QuillEditorUpdateTextSelectionAction( + ExtendSelectionByCharacterIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction( this, false, _characterBoundary, )), ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction(this, true, _nextWordBoundary)), + QuillEditorUpdateTextSelectionAction< + ExtendSelectionToNextWordBoundaryIntent>( + this, true, _nextWordBoundary)), ExtendSelectionToLineBreakIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction(this, true, _linebreak)), - ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction), + QuillEditorUpdateTextSelectionAction( + this, true, _linebreak)), + ExtendSelectionVerticallyToAdjacentLineIntent: + _makeOverridable(_adjacentLineAction), ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction(this, true, _documentBoundary)), - ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: - _makeOverridable(QuillEditorExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)), + QuillEditorUpdateTextSelectionAction< + ExtendSelectionToDocumentBoundaryIntent>( + this, true, _documentBoundary)), + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( + QuillEditorExtendSelectionOrCaretPositionAction( + this, _nextWordBoundary)), // Copy Paste SelectAllTextIntent: _makeOverridable(QuillEditorSelectAllAction(this)), - CopySelectionTextIntent: _makeOverridable(QuillEditorCopySelectionAction(this)), - PasteTextIntent: _makeOverridable(CallbackAction(onInvoke: (intent) => pasteText(intent.cause))), + CopySelectionTextIntent: + _makeOverridable(QuillEditorCopySelectionAction(this)), + PasteTextIntent: _makeOverridable(CallbackAction( + onInvoke: (intent) => pasteText(intent.cause))), - HideSelectionToolbarIntent: _makeOverridable(QuillEditorHideSelectionToolbarAction(this)), + HideSelectionToolbarIntent: + _makeOverridable(QuillEditorHideSelectionToolbarAction(this)), UndoTextIntent: _makeOverridable(QuillEditorUndoKeyboardAction(this)), RedoTextIntent: _makeOverridable(QuillEditorRedoKeyboardAction(this)), @@ -1753,7 +1849,8 @@ class QuillRawEditorState extends EditorState if (_selectionOverlay == null) return; final position = renderEditor.getPositionForOffset(positionToShow); if (_selectionOverlay!.magnifierIsVisible) { - _selectionOverlay!.updateMagnifier(position, positionToShow, renderEditor); + _selectionOverlay! + .updateMagnifier(position, positionToShow, renderEditor); } else { _selectionOverlay!.showMagnifier(position, positionToShow, renderEditor); } From 12fb345195d2b504f487e72553b3ca58158f93d2 Mon Sep 17 00:00:00 2001 From: xuyang Date: Tue, 20 Aug 2024 10:19:52 +0800 Subject: [PATCH 4/5] [chore]: remove code --- lib/src/editor/widgets/delegate.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/editor/widgets/delegate.dart b/lib/src/editor/widgets/delegate.dart index dada74300..70543529d 100644 --- a/lib/src/editor/widgets/delegate.dart +++ b/lib/src/editor/widgets/delegate.dart @@ -647,7 +647,7 @@ class EditorTextSelectionGestureDetectorBuilder { @protected void onDragSelectionStart(TapDragStartDetails details) { if (delegate.selectionEnabled == false) return; - if (editor?.textEditingValue.selection.isCollapsed == false) return; + // if (editor?.textEditingValue.selection.isCollapsed == false) return; final kind = details.kind; shouldShowSelectionToolbar = kind == null || @@ -755,7 +755,7 @@ class EditorTextSelectionGestureDetectorBuilder { @protected void onDragSelectionUpdate(TapDragUpdateDetails updateDetails) { if (delegate.selectionEnabled == false) return; - if (editor?.textEditingValue.selection.isCollapsed == false) return; + // if (editor?.textEditingValue.selection.isCollapsed == false) return; if (!_isShiftPressed) { // Adjust the drag start offset for possible viewport offset changes. final editableOffset = @@ -965,7 +965,7 @@ class EditorTextSelectionGestureDetectorBuilder { /// which triggers this callback. @protected void onDragSelectionEnd(TapDragEndDetails details) { - if (editor?.textEditingValue.selection.isCollapsed == false) return; + // if (editor?.textEditingValue.selection.isCollapsed == false) return; renderEditor!.handleDragEnd(details); if (isDesktop(supportWeb: true) && delegate.selectionEnabled && From 6ec9c79312d8f5a2e75bee810441d3eafe46979b Mon Sep 17 00:00:00 2001 From: xuyang Date: Tue, 20 Aug 2024 11:07:34 +0800 Subject: [PATCH 5/5] [chore]: add code commentary --- lib/src/editor/widgets/delegate.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/editor/widgets/delegate.dart b/lib/src/editor/widgets/delegate.dart index 70543529d..a2f81a9df 100644 --- a/lib/src/editor/widgets/delegate.dart +++ b/lib/src/editor/widgets/delegate.dart @@ -647,6 +647,10 @@ class EditorTextSelectionGestureDetectorBuilder { @protected void onDragSelectionStart(TapDragStartDetails details) { if (delegate.selectionEnabled == false) return; + // underline show open on ios and android, + // when has isCollapsed, show not reposonse to tapdarg gesture + // so that will not change texteditingvalue, + // and same issue to TextField, tap selection area, will lost selection, // if (editor?.textEditingValue.selection.isCollapsed == false) return; final kind = details.kind;