diff --git a/CHANGELOG.md b/CHANGELOG.md index 595b23390..4e902d8b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **BREAKING**: Change the `options` parameter class type from `QuillToolbarToggleStyleButtonOptions` to `QuillToolbarClipboardButtonOptions` in `QuillToolbarClipboardButton`. To migrate, use `QuillToolbarClipboardButtonOptions` instead of `QuillToolbarToggleStyleButtonOptions` [#2433](https://github.com/singerdmx/flutter-quill/pull/2433). +- **BREAKING**: Change the `onTapDown` to accept `TapDownDetails` instead of `TapDragDownDetails` (revert [#2128](https://github.com/singerdmx/flutter-quill/pull/2128/files#diff-49ca9b0fdd0d380a06b34d5aed7674bbfb27fede500831b3e1279615a9edd06dL259-L261) due to regressions). +- **BREAKING**: Change the `onTapUp` to accept `TapUpDetails` instead of `TapDragUpDetails` (revert [#2128](https://github.com/singerdmx/flutter-quill/pull/2128/files#diff-49ca9b0fdd0d380a06b34d5aed7674bbfb27fede500831b3e1279615a9edd06dL263-L265) due to regressions). + +### Removed + +- **BREAKING**: The magnifier feature due to buggy behavior [#2413](https://github.com/singerdmx/flutter-quill/pull/2413). See [#2406](https://github.com/singerdmx/flutter-quill/issues/2406) for a list of reasons. ## [11.0.0-dev.19] - 2025-01-10 diff --git a/doc/migration/10_to_11.md b/doc/migration/10_to_11.md index cdbd377dd..23215ae23 100644 --- a/doc/migration/10_to_11.md +++ b/doc/migration/10_to_11.md @@ -352,6 +352,8 @@ required for **custom toolbars**. - Moved `onClipboardPaste` from `QuillControllerConfig` to `QuillClipboardConfig`. Added `clipboardConfig` property to `QuillControllerConfig`. - Moved `onImagePaste` and `onGifPaste` from the editor's config (`QuillEditorConfig` or `QuillRawEditorConfig`) to the clipboard's config (`QuillClipboardConfig`), which is part of the controller's config (`QuillControllerConfig`). - Changed the options type from `QuillToolbarToggleStyleButtonOptions` to `QuillToolbarClipboardButtonOptions` in `QuillToolbarClipboardButton`, use the new options class. +- Change the `onTapDown` to accept `TapDownDetails` instead of `TapDragDownDetails` (revert [#2128](https://github.com/singerdmx/flutter-quill/pull/2128/files#diff-49ca9b0fdd0d380a06b34d5aed7674bbfb27fede500831b3e1279615a9edd06dL259-L261) due to regressions). +- Change the `onTapUp` to accept `TapUpDetails` instead of `TapDragUpDetails` (revert [#2128](https://github.com/singerdmx/flutter-quill/pull/2128/files#diff-49ca9b0fdd0d380a06b34d5aed7674bbfb27fede500831b3e1279615a9edd06dL263-L265) due to regressions). ## 💥 Breaking behavior @@ -483,6 +485,23 @@ QuillSimpleToolbar( ) ``` +### 5. Removal of the magnifier feature + +Unfortunately, **due to the high volume of issues and bugs introduced by the magnifier**, this feature has been removed to ensure stability. + +This feature was introduced in [9.6.0](https://pub.dev/packages/flutter_quill/versions/9.6.0/changelog#960) which supports Android and iOS only. + +For more details, refer to [#2406](https://github.com/singerdmx/flutter-quill/issues/2406). + +```diff +QuillEditorConfig( +- magnifierConfiguration: TextMagnifierConfiguration() +) +// No longer supported, subscribe to https://github.com/singerdmx/flutter-quill/issues/1504 for updates +``` + +In the future, new features will be implemented with more caution to avoid possible issues. + ## 🚧 Experimental APIs that were indicated as stable but are now updated to indicate @@ -494,7 +513,6 @@ in non-major releases: - The `QuillEditorConfig.characterShortcutEvents` and `QuillEditorConfig.spaceShortcutEvents`. - The `QuillControllerConfig.onClipboardPaste`. - The `QuillEditorConfig.customLeadingBlockBuilder`. -- The magnifier feature including `QuillEditorConfig.magnifierConfiguration`. - The `shouldNotifyListeners` in `QuillController.replaceText()`, `QuillController.replaceText()`, `QuillController.formatSelection()`. - The `QuillController.clipboardSelection()`. - The `CopyCutServiceProvider`, `CopyCutService`, and `DefaultCopyCutService`. diff --git a/lib/src/editor/config/editor_config.dart b/lib/src/editor/config/editor_config.dart index a2bcfe5f4..72459d440 100644 --- a/lib/src/editor/config/editor_config.dart +++ b/lib/src/editor/config/editor_config.dart @@ -1,7 +1,6 @@ import 'dart:ui' as ui; import 'package:flutter/cupertino.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:meta/meta.dart' show experimental; @@ -73,7 +72,6 @@ class QuillEditorConfig { this.contextMenuBuilder, this.editorKey, this.requestKeyboardFocusOnCheckListChanged = false, - @experimental this.magnifierConfiguration, this.textInputAction = TextInputAction.newline, this.enableScribble = false, this.onScribbleActivated, @@ -338,12 +336,11 @@ class QuillEditorConfig { // Returns whether gesture is handled final bool Function( - TapDragDownDetails details, TextPosition Function(Offset offset))? - onTapDown; + TapDownDetails details, TextPosition Function(Offset offset))? onTapDown; // Returns whether gesture is handled final bool Function( - TapDragUpDetails details, TextPosition Function(Offset offset))? onTapUp; + TapUpDetails details, TextPosition Function(Offset offset))? onTapUp; // Returns whether gesture is handled final bool Function( @@ -436,11 +433,6 @@ class QuillEditorConfig { /// should we request keyboard focus?? final bool requestKeyboardFocusOnCheckListChanged; - /// This feature is currently experimental and only supported - /// on **Android** and **iOS**. - @experimental - final TextMagnifierConfiguration? magnifierConfiguration; - /// Default to [TextInputAction.newline] final TextInputAction textInputAction; @@ -488,11 +480,9 @@ class QuillEditorConfig { Brightness? keyboardAppearance, ScrollPhysics? scrollPhysics, ValueChanged? onLaunchUrl, - bool Function( - TapDragDownDetails details, TextPosition Function(Offset offset))? + bool Function(TapDownDetails details, TextPosition Function(Offset offset))? onTapDown, - bool Function( - TapDragUpDetails details, TextPosition Function(Offset offset))? + bool Function(TapUpDetails details, TextPosition Function(Offset offset))? onTapUp, Iterable? embedBuilders, EmbedBuilder? unknownEmbedBuilder, @@ -512,7 +502,6 @@ class QuillEditorConfig { GlobalKey? editorKey, TextSelectionThemeData? textSelectionThemeData, bool? requestKeyboardFocusOnCheckListChanged, - TextMagnifierConfiguration? magnifierConfiguration, TextInputAction? textInputAction, bool? enableScribble, void Function()? onScribbleActivated, @@ -581,8 +570,6 @@ class QuillEditorConfig { requestKeyboardFocusOnCheckListChanged: requestKeyboardFocusOnCheckListChanged ?? this.requestKeyboardFocusOnCheckListChanged, - magnifierConfiguration: - magnifierConfiguration ?? this.magnifierConfiguration, textInputAction: textInputAction ?? this.textInputAction, enableScribble: enableScribble ?? this.enableScribble, onScribbleActivated: onScribbleActivated ?? this.onScribbleActivated, diff --git a/lib/src/editor/editor.dart b/lib/src/editor/editor.dart index 2838a35c9..e9fa525aa 100644 --- a/lib/src/editor/editor.dart +++ b/lib/src/editor/editor.dart @@ -2,7 +2,7 @@ import 'dart:math' as math; import 'package:flutter/cupertino.dart' show CupertinoTheme, cupertinoTextSelectionControls; -import 'package:flutter/foundation.dart' show ValueListenable, kIsWeb; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -16,7 +16,6 @@ import '../document/nodes/container.dart' as container_node; import '../document/nodes/leaf.dart'; import 'config/editor_config.dart'; import 'embed/embed_editor_builder.dart'; -import 'magnifier/magnifier_platform_support.dart'; import 'raw_editor/config/raw_editor_config.dart'; import 'raw_editor/raw_editor.dart'; import 'widgets/box.dart'; @@ -320,7 +319,6 @@ class QuillEditorState extends State onScribbleActivated: configurations.onScribbleActivated, scribbleAreaInsets: configurations.scribbleAreaInsets, readOnlyMouseCursor: configurations.readOnlyMouseCursor, - magnifierConfiguration: configurations.magnifierConfiguration, textInputAction: configurations.textInputAction, onPerformAction: configurations.onPerformAction, ), @@ -448,10 +446,9 @@ class _QuillEditorSelectionGestureDetectorBuilder SelectionChangedCause.longPress, ); } - editor?.updateMagnifier(details.globalPosition); } - bool _isPositionSelected(TapDragUpDetails details) { + bool _isPositionSelected(TapUpDetails details) { if (_state.controller.document.isEmpty()) { return false; } @@ -474,7 +471,7 @@ class _QuillEditorSelectionGestureDetectorBuilder } @override - void onTapDown(TapDragDownDetails details) { + void onTapDown(TapDownDetails details) { if (_state.configurations.onTapDown != null) { if (renderEditor != null && _state.configurations.onTapDown!( @@ -495,7 +492,7 @@ class _QuillEditorSelectionGestureDetectorBuilder } @override - void onSingleTapUp(TapDragUpDetails details) { + void onSingleTapUp(TapUpDetails details) { if (_state.configurations.onTapUp != null && renderEditor != null && _state.configurations.onTapUp!( @@ -582,8 +579,6 @@ class _QuillEditorSelectionGestureDetectorBuilder Feedback.forLongPress(_state.context); } } - - _showMagnifierIfSupported(details.globalPosition); } @override @@ -602,21 +597,8 @@ class _QuillEditorSelectionGestureDetectorBuilder } } } - _hideMagnifierIfSupported(); super.onSingleLongTapEnd(details); } - - void _showMagnifierIfSupported(Offset positionToShow) { - if (magnifierSupported) { - editor?.showMagnifier(positionToShow); - } - } - - void _hideMagnifierIfSupported() { - if (magnifierSupported) { - editor?.hideMagnifier(); - } - } } /// Signature for the callback that reports when the user changes the selection @@ -689,7 +671,6 @@ class RenderEditor extends RenderEditableContainerBox Document document; TextSelection selection; bool _hasFocus = false; - bool get hasFocus => _hasFocus; LayerLink _startHandleLayerLink; LayerLink _endHandleLayerLink; @@ -896,20 +877,12 @@ 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; @@ -917,7 +890,7 @@ class RenderEditor extends RenderEditableContainerBox bool _isDragging = false; - void handleDragStart(TapDragStartDetails details) { + void handleDragStart(DragStartDetails details) { _isDragging = true; final newSelection = selectPositionAt( @@ -930,7 +903,7 @@ class RenderEditor extends RenderEditableContainerBox _extendSelectionOrigin = newSelection; } - void handleDragEnd(TapDragEndDetails details) { + void handleDragEnd(DragEndDetails details) { _isDragging = false; onSelectionCompleted(); } diff --git a/lib/src/editor/magnifier/magnifier_platform_support.dart b/lib/src/editor/magnifier/magnifier_platform_support.dart deleted file mode 100644 index 443a9f9c9..000000000 --- a/lib/src/editor/magnifier/magnifier_platform_support.dart +++ /dev/null @@ -1,4 +0,0 @@ -import '../../common/utils/platform.dart'; - -/// Whether the magnifier feature is supported on the current platform. -bool magnifierSupported = isAndroid || isIos; diff --git a/lib/src/editor/magnifier/text_selection_magnifier_ext.dart b/lib/src/editor/magnifier/text_selection_magnifier_ext.dart deleted file mode 100644 index 5132939c2..000000000 --- a/lib/src/editor/magnifier/text_selection_magnifier_ext.dart +++ /dev/null @@ -1,106 +0,0 @@ -part of '../widgets/text/text_selection.dart'; - -extension TextSelectionMagnifierExt on EditorTextSelectionOverlay { - void showMagnifier( - TextPosition position, Offset offset, RenderEditor editor) { - _showMagnifier( - _buildMagnifier( - currentTextPosition: position, - globalGesturePosition: offset, - renderEditable: editor, - ), - ); - } - - void _showMagnifier(MagnifierInfo initialMagnifierInfo) { - // Hide toolbar - if (toolbar != null) { - _restoreToolbarAfterMagnifier = true; - hideToolbar(); - } else { - _restoreToolbarAfterMagnifier = false; - } - - // Update magnifier Info - _magnifierInfo.value = initialMagnifierInfo; - - final builtMagnifier = magnifierConfiguration.magnifierBuilder( - context, - _magnifierController, - _magnifierInfo, - ); - - if (builtMagnifier == null) return; - - _magnifierController.show( - context: context, - below: magnifierConfiguration.shouldDisplayHandlesInMagnifier - ? null - : _handles?.elementAtOrNull(0), - builder: (_) => builtMagnifier, - ); - } - - void updateMagnifier( - TextPosition position, Offset offset, RenderEditor editor) { - _updateMagnifier( - _buildMagnifier( - currentTextPosition: position, - globalGesturePosition: offset, - renderEditable: editor, - ), - ); - } - - void _updateMagnifier(MagnifierInfo magnifierInfo) { - if (_magnifierController.overlayEntry == null) { - return; - } - _magnifierInfo.value = magnifierInfo; - } - - void hideMagnifier() { - if (_magnifierController.overlayEntry == null) { - return; - } - _magnifierController.hide(); - if (_restoreToolbarAfterMagnifier) { - _restoreToolbarAfterMagnifier = false; - showToolbar(); - } - } - -// build magnifier info - MagnifierInfo _buildMagnifier( - {required RenderEditor renderEditable, - required Offset globalGesturePosition, - required TextPosition currentTextPosition}) { - final globalRenderEditableTopLeft = - renderEditable.localToGlobal(Offset.zero); - final localCaretRect = - renderEditable.getLocalRectForCaret(currentTextPosition); - - final lineAtOffset = renderEditable.getLineAtOffset(currentTextPosition); - final positionAtEndOfLine = TextPosition( - offset: lineAtOffset.extentOffset, - affinity: TextAffinity.upstream, - ); - - // Default affinity is downstream. - final positionAtBeginningOfLine = TextPosition( - offset: lineAtOffset.baseOffset, - ); - - final lineBoundaries = Rect.fromPoints( - renderEditable.getLocalRectForCaret(positionAtBeginningOfLine).topCenter, - renderEditable.getLocalRectForCaret(positionAtEndOfLine).bottomCenter, - ); - - return MagnifierInfo( - fieldBounds: globalRenderEditableTopLeft & renderEditable.size, - globalGesturePosition: globalGesturePosition, - caretRect: localCaretRect.shift(globalRenderEditableTopLeft), - currentLineBoundaries: lineBoundaries.shift(globalRenderEditableTopLeft), - ); - } -} diff --git a/lib/src/editor/raw_editor/config/raw_editor_config.dart b/lib/src/editor/raw_editor/config/raw_editor_config.dart index daa7d49d7..16a5a4549 100644 --- a/lib/src/editor/raw_editor/config/raw_editor_config.dart +++ b/lib/src/editor/raw_editor/config/raw_editor_config.dart @@ -66,7 +66,6 @@ class QuillRawEditorConfig { this.onScribbleActivated, this.scribbleAreaInsets, this.readOnlyMouseCursor = SystemMouseCursors.text, - @experimental this.magnifierConfiguration, this.onPerformAction, @experimental this.customLeadingBuilder, }); @@ -402,11 +401,6 @@ class QuillRawEditorConfig { /// Optional insets for the scribble area. final EdgeInsets? scribbleAreaInsets; - /// This feature is currently experimental and only supported - /// on **Android** and **iOS**. - @experimental - final TextMagnifierConfiguration? magnifierConfiguration; - /// Called when a text input action is performed. final void Function(TextInputAction action)? onPerformAction; } diff --git a/lib/src/editor/raw_editor/raw_editor.dart b/lib/src/editor/raw_editor/raw_editor.dart index af8b8e43d..4cc9543f3 100644 --- a/lib/src/editor/raw_editor/raw_editor.dart +++ b/lib/src/editor/raw_editor/raw_editor.dart @@ -1,5 +1,4 @@ import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart'; import '../../common/structs/offset_value.dart'; import '../../controller/quill_controller.dart'; @@ -77,15 +76,4 @@ abstract class EditorState extends State bool showToolbar(); void requestKeyboard(); - - @experimental - void showMagnifier(Offset positionToShow); - - @experimental - void updateMagnifier(Offset positionToShow); - - @experimental - 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 e5e7888eb..f026c43fd 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -8,8 +8,7 @@ import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb; 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, SystemChannels, TextInputControl; +import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility_temp_fork/flutter_keyboard_visibility_temp_fork.dart' show KeyboardVisibilityController; @@ -509,15 +508,9 @@ class QuillRawEditorState extends EditorState final oldSelection = controller.selection; controller.updateSelection(selection, ChangeSource.local); - if (_selectionOverlay == null) { - _selectionOverlay = _createSelectionOverlay(); - } else { - _selectionOverlay!.update(textEditingValue); - } _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); - _selectionOverlay?.showHandles(); - if (!_hasFocus) { + if (!_keyboardVisible) { // This will show the keyboard for all selection changes on the // editor, not just changes triggered by user gestures. requestKeyboard(); @@ -938,7 +931,6 @@ class QuillRawEditorState extends EditorState @override void dispose() { - hideMagnifier(); closeConnectionIfNeeded(); _keyboardVisibilitySubscription?.cancel(); HardwareKeyboard.instance.removeHandler(_hardwareKeyboardEvent); @@ -1039,38 +1031,32 @@ class QuillRawEditorState extends EditorState void _updateOrDisposeSelectionOverlayIfNeeded() { if (_selectionOverlay != null) { - if (_hasFocus) { - _selectionOverlay!.update(textEditingValue); - } else { + if (!_hasFocus || textEditingValue.selection.isCollapsed) { _selectionOverlay!.dispose(); _selectionOverlay = null; + } else { + _selectionOverlay!.update(textEditingValue); } } else if (_hasFocus) { - _selectionOverlay = _createSelectionOverlay(); + _selectionOverlay = EditorTextSelectionOverlay( + value: textEditingValue, + context: context, + debugRequiredFor: widget, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + renderObject: renderEditor, + selectionCtrls: widget.config.selectionCtrls, + selectionDelegate: this, + clipboardStatus: _clipboardStatus, + contextMenuBuilder: widget.config.contextMenuBuilder == null + ? null + : (context) => widget.config.contextMenuBuilder!(context, this), + ); _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); _selectionOverlay!.showHandles(); } } - EditorTextSelectionOverlay _createSelectionOverlay() { - return EditorTextSelectionOverlay( - value: textEditingValue, - context: context, - debugRequiredFor: widget, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - renderObject: renderEditor, - selectionCtrls: widget.config.selectionCtrls, - selectionDelegate: this, - clipboardStatus: _clipboardStatus, - contextMenuBuilder: widget.config.contextMenuBuilder == null - ? null - : (context) => widget.config.contextMenuBuilder!(context, this), - magnifierConfiguration: widget.config.magnifierConfiguration ?? - TextMagnifier.adaptiveMagnifierConfiguration, - ); - } - void _handleFocusChanged() { if (dirty) { requestKeyboard(); @@ -1218,16 +1204,6 @@ class QuillRawEditorState extends EditorState return true; } - @override - void toggleToolbar([bool hideHandles = true]) { - final selectionOverlay = _selectionOverlay ??= _createSelectionOverlay(); - if (selectionOverlay.handlesVisible) { - hideToolbar(hideHandles); - } else { - showToolbar(); - } - } - @override bool get wantKeepAlive => widget.config.focusNode.hasFocus; @@ -1288,28 +1264,4 @@ class QuillRawEditorState extends EditorState @override bool get shareEnabled => false; - - @override - void hideMagnifier() { - if (_selectionOverlay == null) return; - _selectionOverlay?.hideMagnifier(); - } - - @override - void showMagnifier(ui.Offset positionToShow) { - if (_hasFocus == false) return; - if (_selectionOverlay == null) return; - final position = renderEditor.getPositionForOffset(positionToShow); - if (_selectionOverlay!.isMagnifierVisible) { - _selectionOverlay! - .updateMagnifier(position, positionToShow, renderEditor); - } else { - _selectionOverlay!.showMagnifier(position, positionToShow, renderEditor); - } - } - - @override - void updateMagnifier(ui.Offset positionToShow) { - showMagnifier(positionToShow); - } } diff --git a/lib/src/editor/widgets/delegate.dart b/lib/src/editor/widgets/delegate.dart index 836e7e37b..c47a2d88e 100644 --- a/lib/src/editor/widgets/delegate.dart +++ b/lib/src/editor/widgets/delegate.dart @@ -1,9 +1,7 @@ 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'; @@ -109,142 +107,6 @@ 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 = switch (cause) { - SelectionChangedCause.drag => _dragStartSelection!, - _ => 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 @@ -256,45 +118,19 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onTapDown], /// which triggers this callback. @protected - void onTapDown(TapDragDownDetails details) { - if (!delegate.selectionEnabled) return; - renderEditor! - .handleTapDown(TapDownDetails(globalPosition: details.globalPosition)); - final kind = details.kind; + void onTapDown(TapDownDetails details) { + 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; 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]. @@ -345,27 +181,6 @@ 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. @@ -375,7 +190,7 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onSingleTapUp], which triggers /// this callback. @protected - void onSingleTapUp(TapDragUpDetails details) { + void onSingleTapUp(TapUpDetails details) { if (delegate.selectionEnabled) { renderEditor!.selectWordEdge(SelectionChangedCause.tap); } @@ -454,64 +269,6 @@ 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]. @@ -524,7 +281,7 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onDoubleTapDown], /// which triggers this callback. @protected - void onDoubleTapDown(TapDragDownDetails details) { + void onDoubleTapDown(TapDownDetails details) { if (delegate.selectionEnabled) { renderEditor!.selectWord(SelectionChangedCause.tap); // allow the selection to get updated before trying to bring up @@ -541,105 +298,6 @@ 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]. @@ -649,106 +307,8 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onDragSelectionStart], /// which triggers this callback. @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; - 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: - _dragStartSelection = 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!) { - _dragStartSelection = 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: - _dragStartSelection = 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) { - _dragStartSelection = renderEditor?.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - editor?.showMagnifier(details.globalPosition); - } - case null: - } - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - _dragStartSelection = renderEditor?.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - } - } + void onDragSelectionStart(DragStartDetails details) { + renderEditor!.handleDragStart(details); } /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionUpdate]. @@ -761,206 +321,13 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onDragSelectionUpdate], /// which triggers this callback./lib/src/material/text_field.dart @protected - 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 ?? 0) - _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); - } + void onDragSelectionUpdate( + //DragStartDetails startDetails, + DragUpdateDetails updateDetails) { + renderEditor!.extendSelection( + updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); } /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionEnd]. @@ -972,8 +339,7 @@ class EditorTextSelectionGestureDetectorBuilder { /// * [EditorTextSelectionGestureDetector.onDragSelectionEnd], /// which triggers this callback. @protected - void onDragSelectionEnd(TapDragEndDetails details) { - // if (editor?.textEditingValue.selection.isCollapsed == false) return; + void onDragSelectionEnd(DragEndDetails details) { renderEditor!.handleDragEnd(details); if (isDesktop && delegate.selectionEnabled && @@ -981,7 +347,6 @@ class EditorTextSelectionGestureDetectorBuilder { // added to show selection copy/paste toolbar after drag to select editor!.showToolbar(); } - editor?.hideMagnifier(); } /// Returns a [EditorTextSelectionGestureDetector] configured with @@ -996,26 +361,21 @@ 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, - onTripleTapDown: onTripleTapDown, + onSecondarySingleTapUp: onSecondarySingleTapUp, onDragSelectionStart: onDragSelectionStart, onDragSelectionUpdate: onDragSelectionUpdate, onDragSelectionEnd: onDragSelectionEnd, - onUserTapAlwaysCalled: onUserTapAlwaysCalled, behavior: behavior, + detectWordBoundary: detectWordBoundary, child: child, ); } diff --git a/lib/src/editor/widgets/text/text_line.dart b/lib/src/editor/widgets/text/text_line.dart index 0de0ad641..fc33ae58f 100644 --- a/lib/src/editor/widgets/text/text_line.dart +++ b/lib/src/editor/widgets/text/text_line.dart @@ -984,10 +984,6 @@ class RenderEditableTextLine extends RenderEditableBox { _getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1)) .where((element) => element.top < lineDy && element.bottom > lineDy) .toList(growable: false); - if (lineBoxes.isEmpty) { - // Empty line, line box is empty - return TextRange.collapsed(position.offset); - } return TextRange( start: getPositionForOffset( Offset(lineBoxes.first.left, lineDy), diff --git a/lib/src/editor/widgets/text/text_selection.dart b/lib/src/editor/widgets/text/text_selection.dart index 66fee30cd..a748e60f5 100644 --- a/lib/src/editor/widgets/text/text_selection.dart +++ b/lib/src/editor/widgets/text/text_selection.dart @@ -1,16 +1,14 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/services.dart'; import '../../../document/nodes/node.dart'; import '../../editor.dart'; -import '../../magnifier/magnifier_platform_support.dart'; - -part '../../magnifier/text_selection_magnifier_ext.dart'; TextSelection localSelection(Node node, TextSelection selection, fromParent) { final base = fromParent ? node.offset : node.documentOffset; @@ -78,8 +76,6 @@ class EditorTextSelectionOverlay { this.onSelectionHandleTapped, this.dragStartBehavior = DragStartBehavior.start, this.handlesVisible = false, - @experimental - this.magnifierConfiguration = TextMagnifierConfiguration.disabled, }) { // Clipboard status is only checked on first instance of // ClipboardStatusNotifier @@ -186,23 +182,8 @@ class EditorTextSelectionOverlay { /// A copy/paste toolbar. OverlayEntry? toolbar; - /// Restore the selection context menu after the magnifier is dismissed. - /// Fix: https://github.com/singerdmx/flutter-quill/issues/2046 - bool _restoreToolbarAfterMagnifier = false; - TextSelection get _selection => value.selection; - final MagnifierController _magnifierController = MagnifierController(); - - @experimental - bool get isMagnifierVisible => _magnifierController.shown; - - @experimental - final TextMagnifierConfiguration magnifierConfiguration; - - final ValueNotifier _magnifierInfo = - ValueNotifier(MagnifierInfo.empty); - void setHandlesVisible(bool visible) { if (handlesVisible == visible) { return; @@ -272,9 +253,6 @@ class EditorTextSelectionOverlay { selection: _selection, selectionControls: selectionCtrls, position: position, - onHandleDragStart: _onHandleDragStart, - onHandleDragUpdate: _onHandleDragUpdate, - onHandleDragEnd: _onHandleDragEnd, dragStartBehavior: dragStartBehavior, )); } @@ -362,16 +340,11 @@ class EditorTextSelectionOverlay { /// Final cleanup. void dispose() { hide(); - _magnifierInfo.dispose(); } /// Builds the handles by inserting them into the [context]'s overlay. void showHandles() { - // TODO: Restore the assert and fix the issue or see why this change was made - // in https://github.com/singerdmx/flutter-quill/pull/2026/files#diff-ec5ab6cd2618a243ea6c82b62054455ec22ab981353b1cb50cac90b654430227L348 - // Previously it was using assertation and now it returns without any error if _handles - // is not null - if (_handles != null) return; + assert(_handles == null); _handles = [ OverlayEntry( builder: (context) => @@ -392,28 +365,8 @@ class EditorTextSelectionOverlay { void updateForScroll() { markNeedsBuild(); } - - void _onHandleDragStart(DragStartDetails details, TextPosition position) { - if (magnifierConfiguration == TextMagnifierConfiguration.disabled) return; - if (!magnifierSupported) return; - showMagnifier(position, details.globalPosition, renderObject); - } - - void _onHandleDragUpdate(DragUpdateDetails details, TextPosition position) { - if (magnifierConfiguration == TextMagnifierConfiguration.disabled) return; - if (!magnifierSupported) return; - updateMagnifier(position, details.globalPosition, renderObject); - } - - void _onHandleDragEnd(DragEndDetails details) { - if (magnifierConfiguration == TextMagnifierConfiguration.disabled) return; - if (!magnifierSupported) return; - hideMagnifier(); - } } -typedef DargHandleCallback = void Function(T details, TextPosition position); - /// This widget represents a single draggable text selection handle. class _TextSelectionHandleOverlay extends StatefulWidget { const _TextSelectionHandleOverlay({ @@ -425,9 +378,6 @@ class _TextSelectionHandleOverlay extends StatefulWidget { required this.onSelectionHandleChanged, required this.onSelectionHandleTapped, required this.selectionControls, - required this.onHandleDragStart, - required this.onHandleDragUpdate, - required this.onHandleDragEnd, this.dragStartBehavior = DragStartBehavior.start, }); @@ -437,9 +387,6 @@ class _TextSelectionHandleOverlay extends StatefulWidget { final LayerLink endHandleLayerLink; final RenderEditor renderObject; final ValueChanged onSelectionHandleChanged; - final DargHandleCallback? onHandleDragStart; - final DargHandleCallback? onHandleDragUpdate; - final ValueChanged onHandleDragEnd; final VoidCallback? onSelectionHandleTapped; final TextSelectionControls selectionControls; final DragStartBehavior dragStartBehavior; @@ -503,18 +450,15 @@ class _TextSelectionHandleOverlayState } void _handleDragStart(DragStartDetails details) { - if (!widget.renderObject.attached) return; final textPosition = widget.position == _TextSelectionHandlePosition.start ? widget.selection.base : widget.selection.extent; final lineHeight = widget.renderObject.preferredLineHeight(textPosition); final handleSize = widget.selectionControls.getHandleSize(lineHeight); _dragPosition = details.globalPosition + Offset(0, -handleSize.height); - widget.onHandleDragStart?.call(details, textPosition); } void _handleDragUpdate(DragUpdateDetails details) { - if (!widget.renderObject.attached) return; _dragPosition += details.delta; final position = widget.renderObject.getPositionForOffset(details.globalPosition); @@ -548,17 +492,8 @@ class _TextSelectionHandleOverlayState if (newSelection.baseOffset >= newSelection.extentOffset) { return; // don't allow order swapping. } - widget.onSelectionHandleChanged(newSelection); - if (widget.position == _TextSelectionHandlePosition.start) { - widget.onHandleDragUpdate?.call(details, newSelection.base); - } else if (widget.position == _TextSelectionHandlePosition.end) { - widget.onHandleDragUpdate?.call(details, newSelection.extent); - } - } - void _handleDragEnd(DragEndDetails details) { - if (!widget.renderObject.attached) return; - widget.onHandleDragEnd.call(details); + widget.onSelectionHandleChanged(newSelection); } void _handleTap() { @@ -639,7 +574,6 @@ class _TextSelectionHandleOverlayState dragStartBehavior: widget.dragStartBehavior, onPanStart: _handleDragStart, onPanUpdate: _handleDragUpdate, - onPanEnd: _handleDragEnd, onTap: _handleTap, child: Padding( padding: EdgeInsets.only( @@ -696,39 +630,31 @@ 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.onUserTap, + this.onSecondaryTapDown, + this.onSecondarySingleTapUp, + this.onSecondarySingleTapCancel, + this.onSecondaryDoubleTapDown, 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 GestureTapDragDownCallback? onTapDown; + final GestureTapDownCallback? onTapDown; /// Called when a pointer has tapped down and the force of the pointer has /// just become greater than [ForcePressGestureRecognizer.startPressure]. @@ -738,31 +664,28 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { /// lifted off the screen. final GestureForcePressEndCallback? onForcePressEnd; - /// 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. - /// + /// Called for each distinct tap except for every second tap of a double tap. /// 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 tap up, then a double tap down, followed by a single tap down. - final GestureTapDragUpCallback? onSingleTapUp; + /// down, followed by a double tap down, followed by a single tap down. + final GestureTapUpCallback? 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 GestureCancelCallback? onSingleTapCancel; + final GestureTapCancelCallback? onSingleTapCancel; - /// 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; + /// onTapDown for mouse right click + final GestureTapDownCallback? onSecondaryTapDown; + + /// onTapUp for mouse right click + final GestureTapUpCallback? onSecondarySingleTapUp; + + /// onTapCancel for mouse right click + final GestureTapCancelCallback? onSecondarySingleTapCancel; + + /// onDoubleTap for mouse right click + final GestureTapDownCallback? onSecondaryDoubleTapDown; /// Called for a single long tap that's sustained for longer than /// [kLongPressTimeout] but not necessarily lifted. Not called for a @@ -777,25 +700,20 @@ 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 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; + final GestureTapDownCallback? onDoubleTapDown; /// Called when a mouse starts dragging to select text. - final GestureTapDragStartCallback? onDragSelectionStart; + final GestureDragStartCallback? onDragSelectionStart; /// Called repeatedly as a mouse moves while dragging. - final GestureTapDragUpdateCallback? onDragSelectionUpdate; + /// + /// 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; /// Called when a mouse that was previously dragging is released. - 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; + final GestureDragEndCallback? onDragSelectionEnd; /// How this gesture detector should behave during hit testing. /// @@ -805,145 +723,210 @@ 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 { - // 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(); - } + // 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; - void _handleTapTrackReset() { - widget.onTapTrackReset?.call(); + @override + void dispose() { + _doubleTapTimer?.cancel(); + _dragUpdateThrottleTimer?.cancel(); + super.dispose(); } // The down handler is force-run on success of a single tap and optimistically // run before a long press success. - void _handleTapDown(TapDragDownDetails details) { + void _handleTapDown(TapDownDetails 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 (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( - details.consecutiveTapCount) == - 2) { - return widget.onDoubleTapDown?.call(details); - } - if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( - details.consecutiveTapCount) == - 3) { - return widget.onTripleTapDown?.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; } } - void _handleTapUp(TapDragUpDetails details) { - if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount( - details.consecutiveTapCount) == - 1) { + void _handleTapUp(TapUpDetails details) { + if (!_isDoubleTap) { widget.onSingleTapUp?.call(details); - widget.onUserTap?.call(); - } else if (widget.onUserTapAlwaysCalled) { - widget.onUserTap?.call(); + _lastTapOffset = details.globalPosition; + _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); } + _isDoubleTap = false; } void _handleTapCancel() { widget.onSingleTapCancel?.call(); } - void _handleDragStart(TapDragStartDetails details) { + // 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; widget.onDragSelectionStart?.call(details); } - void _handleDragUpdate(TapDragUpdateDetails details) { - widget.onDragSelectionUpdate?.call(details); + void _handleDragUpdate(DragUpdateDetails details) { + _lastDragUpdateDetails = details; + _dragUpdateThrottleTimer ??= Timer( + const Duration(milliseconds: 50), + _handleDragUpdateThrottled, + ); } - void _handleDragEnd(TapDragEndDetails 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(); + } + 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) { - widget.onForcePressEnd?.call(details); + if (widget.onForcePressEnd != null) { + widget.onForcePressEnd?.call(details); + } } void _handleLongPressStart(LongPressStartDetails details) { - if (widget.onSingleLongTapStart != null) { - widget.onSingleLongTapStart!(details); + if (!_isDoubleTap) { + widget.onSingleLongTapStart?.call(details); } } void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - if (widget.onSingleLongTapMoveUpdate != null) { - widget.onSingleLongTapMoveUpdate!(details); + if (!_isDoubleTap) { + widget.onSingleLongTapMoveUpdate?.call(details); } } void _handleLongPressEnd(LongPressEndDetails details) { - if (widget.onSingleLongTapEnd != null) { - widget.onSingleLongTapEnd!(details); + if (!_isDoubleTap) { + widget.onSingleLongTapEnd?.call(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 = {}; - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), + // 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), (instance) { instance - ..onSecondaryTap = widget.onSecondaryTap - ..onSecondaryTapDown = widget.onSecondaryTapDown; + ..onTapDown = _handleTapDown + ..onTapUp = _handleTapUp + ..onTapCancel = _handleTapCancel + ..onSecondaryTapDown = _handleSecondaryTapDown + ..onSecondaryTapUp = _handleSecondaryTapUp + ..onSecondaryTapCancel = _handleSecondaryTapCancel; }, ); @@ -967,51 +950,21 @@ class _EditorTextSelectionGestureDetectorState if (widget.onDragSelectionStart != null || widget.onDragSelectionUpdate != null || widget.onDragSelectionEnd != null) { - 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; - }, - ); - } + 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; + }, + ); } if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { @@ -1035,3 +988,32 @@ 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); + } + } +}