Copy TapAndPanGestureRecognizer from TextField (#2128)

* [chore]: change gesture

* [chore]: change gesture

* [chore]: code fromatter

* [chore]: remove code

* [chore]: add code commentary

---------

Co-authored-by: xuyang <xuyang@qimao.com>
pull/2133/head v10.3.3
License name 8 months ago committed by GitHub
parent bb29a50050
commit 2937dc8c95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      lib/src/editor/config/editor_configurations.dart
  2. 27
      lib/src/editor/editor.dart
  3. 2
      lib/src/editor/raw_editor/raw_editor.dart
  4. 32
      lib/src/editor/raw_editor/raw_editor_state.dart
  5. 684
      lib/src/editor/widgets/delegate.dart
  6. 395
      lib/src/editor/widgets/text/text_selection.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(

@ -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();
}

@ -100,4 +100,6 @@ abstract class EditorState extends State<QuillRawEditor>
void updateMagnifier(Offset positionToShow);
void hideMagnifier();
void toggleToolbar([bool hideHandles = true]);
}

@ -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();
@ -1419,11 +1419,11 @@ 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();
_selectionOverlay = null;
}
} else if (_hasFocus) {
_selectionOverlay = _createSelectionOverlay();
@ -1601,6 +1601,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 +1845,19 @@ 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);
}
}

@ -1,7 +1,9 @@
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';
@ -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>{
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,45 @@ class EditorTextSelectionGestureDetectorBuilder {
/// * [EditorTextSelectionGestureDetector.onTapDown],
/// which triggers this callback.
@protected
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;
void onTapDown(TapDragDownDetails details) {
if (!delegate.selectionEnabled) return;
renderEditor!
.handleTapDown(TapDownDetails(globalPosition: details.globalPosition));
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 +341,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 +371,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 +450,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 +520,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 +537,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 +645,106 @@ 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;
// 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:
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 +757,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 +968,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 +977,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 +992,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,
);
}

@ -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> _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<StatefulWidget> 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<EditorTextSelectionGestureDetector> {
// 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 = <Type, GestureRecognizerFactory>{};
// 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>(
() => 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>(
() => HorizontalDragGestureRecognizer(
debugOwner: this,
supportedDevices: <PointerDeviceKind>{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>(
() => 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);
}
}
}

Loading…
Cancel
Save