refactor out selection handlers (#35207)
diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart
index 6a77c11..b43eb77 100644
--- a/packages/flutter/lib/src/cupertino/text_field.dart
+++ b/packages/flutter/lib/src/cupertino/text_field.dart
@@ -68,6 +68,39 @@
always,
}
+class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
+ _CupertinoTextFieldSelectionGestureDetectorBuilder({
+ @required _CupertinoTextFieldState state
+ }) : _state = state,
+ super(delegate: state);
+
+ final _CupertinoTextFieldState _state;
+
+ @override
+ void onSingleTapUp(TapUpDetails details) {
+ // Because TextSelectionGestureDetector listens to taps that happen on
+ // widgets in front of it, tapping the clear button will also trigger
+ // this handler. If the the clear button widget recognizes the up event,
+ // then do not handle it.
+ if (_state._clearGlobalKey.currentContext != null) {
+ final RenderBox renderBox = _state._clearGlobalKey.currentContext.findRenderObject();
+ final Offset localOffset = renderBox.globalToLocal(details.globalPosition);
+ if (renderBox.hitTest(BoxHitTestResult(), position: localOffset)) {
+ return;
+ }
+ }
+ super.onSingleTapUp(details);
+ _state._requestKeyboard();
+ if (_state.widget.onTap != null)
+ _state.widget.onTap();
+ }
+
+ @override
+ void onDragSelectionEnd(DragEndDetails details) {
+ _state._requestKeyboard();
+ }
+}
+
/// An iOS-style text field.
///
/// A text field lets the user enter text, either with a hardware keyboard or with
@@ -506,9 +539,8 @@
}
}
-class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin {
+class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
final GlobalKey _clearGlobalKey = GlobalKey();
- final GlobalKey<EditableTextState> _editableTextKey = GlobalKey<EditableTextState>();
TextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller;
@@ -516,17 +548,25 @@
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
- // 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.
- bool _shouldShowSelectionToolbar = true;
-
bool _showSelectionHandles = false;
+ _CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
+
+ // API for TextSelectionGestureDetectorBuilderDelegate.
+ @override
+ bool get forcePressEnabled => true;
+
+ @override
+ final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
+
+ @override
+ bool get selectionEnabled => widget.selectionEnabled;
+ // End of API for TextSelectionGestureDetectorBuilderDelegate.
+
@override
void initState() {
super.initState();
+ _selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null) {
_controller = TextEditingController();
_controller.addListener(updateKeepAlive);
@@ -556,103 +596,16 @@
super.dispose();
}
- EditableTextState get _editableText => _editableTextKey.currentState;
+ EditableTextState get _editableText => editableTextKey.currentState;
void _requestKeyboard() {
_editableText?.requestKeyboard();
}
- RenderEditable get _renderEditable => _editableText.renderEditable;
-
- void _handleTapDown(TapDownDetails details) {
- _renderEditable.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.
- final PointerDeviceKind kind = details.kind;
- _shouldShowSelectionToolbar =
- kind == null ||
- kind == PointerDeviceKind.touch ||
- kind == PointerDeviceKind.stylus;
- }
-
- void _handleForcePressStarted(ForcePressDetails details) {
- if (widget.selectionEnabled) {
- _renderEditable.selectWordsInRange(
- from: details.globalPosition,
- cause: SelectionChangedCause.forcePress,
- );
- }
- }
-
- void _handleForcePressEnded(ForcePressDetails details) {
- _renderEditable.selectWordsInRange(
- from: details.globalPosition,
- cause: SelectionChangedCause.forcePress,
- );
- if (_shouldShowSelectionToolbar)
- _editableText.showToolbar();
- }
-
- void _handleSingleTapUp(TapUpDetails details) {
- // Because TextSelectionGestureDetector listens to taps that happen on
- // widgets in front of it, tapping the clear button will also trigger
- // this handler. If the the clear button widget recognizes the up event,
- // then do not handle it.
- if (_clearGlobalKey.currentContext != null) {
- final RenderBox renderBox = _clearGlobalKey.currentContext.findRenderObject();
- final Offset localOffset = renderBox.globalToLocal(details.globalPosition);
- if(renderBox.hitTest(BoxHitTestResult(), position: localOffset)) {
- return;
- }
- }
-
- if (widget.selectionEnabled) {
- _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
- }
- _requestKeyboard();
- if (widget.onTap != null) {
- widget.onTap();
- }
- }
-
- void _handleSingleLongTapStart(LongPressStartDetails details) {
- if (widget.selectionEnabled) {
- _renderEditable.selectPositionAt(
- from: details.globalPosition,
- cause: SelectionChangedCause.longPress,
- );
- }
- }
-
- void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
- if (widget.selectionEnabled) {
- _renderEditable.selectPositionAt(
- from: details.globalPosition,
- cause: SelectionChangedCause.longPress,
- );
- }
- }
-
- void _handleSingleLongTapEnd(LongPressEndDetails details) {
- if (_shouldShowSelectionToolbar)
- _editableText.showToolbar();
- }
-
- void _handleDoubleTapDown(TapDownDetails details) {
- if (widget.selectionEnabled) {
- _renderEditable.selectWord(cause: SelectionChangedCause.tap);
- if (_shouldShowSelectionToolbar)
- _editableText.showToolbar();
- }
- }
-
bool _shouldShowSelectionHandles(SelectionChangedCause cause) {
// When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either.
- if (!_shouldShowSelectionToolbar)
+ if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
return false;
// On iOS, we don't show handles when the selection is collapsed.
@@ -668,28 +621,6 @@
return false;
}
- void _handleMouseDragSelectionStart(DragStartDetails details) {
- _renderEditable.selectPositionAt(
- from: details.globalPosition,
- cause: SelectionChangedCause.drag,
- );
- }
-
- void _handleMouseDragSelectionUpdate(
- DragStartDetails startDetails,
- DragUpdateDetails updateDetails,
- ) {
- _renderEditable.selectPositionAt(
- from: startDetails.globalPosition,
- to: updateDetails.globalPosition,
- cause: SelectionChangedCause.drag,
- );
- }
-
- void _handleMouseDragSelectionEnd(DragEndDetails details) {
- _requestKeyboard();
- }
-
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
if (cause == SelectionChangedCause.longPress) {
_editableText?.bringIntoView(selection.base);
@@ -870,7 +801,7 @@
padding: widget.padding,
child: RepaintBoundary(
child: EditableText(
- key: _editableTextKey,
+ key: editableTextKey,
controller: controller,
readOnly: widget.readOnly,
showCursor: widget.showCursor,
@@ -925,18 +856,7 @@
ignoring: !enabled,
child: Container(
decoration: effectiveDecoration,
- child: TextSelectionGestureDetector(
- onTapDown: _handleTapDown,
- onForcePressStart: _handleForcePressStarted,
- onForcePressEnd: _handleForcePressEnded,
- onSingleTapUp: _handleSingleTapUp,
- onSingleLongTapStart: _handleSingleLongTapStart,
- onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
- onSingleLongTapEnd: _handleSingleLongTapEnd,
- onDoubleTapDown: _handleDoubleTapDown,
- onDragSelectionStart: _handleMouseDragSelectionStart,
- onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
- onDragSelectionEnd: _handleMouseDragSelectionEnd,
+ child: _selectionGestureDetectorBuilder.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: Align(
alignment: Alignment(-1.0, _textAlignVertical.y),
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index 0a1d29a..ffb715b 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -35,6 +35,107 @@
@required bool isFocused,
});
+class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
+ _TextFieldSelectionGestureDetectorBuilder({
+ @required _TextFieldState state
+ }) : _state = state,
+ super(delegate: state);
+
+ final _TextFieldState _state;
+
+ @override
+ void onTapDown(TapDownDetails details) {
+ super.onTapDown(details);
+ _state._startSplash(details.globalPosition);
+ }
+
+ @override
+ void onForcePressStart(ForcePressDetails details) {
+ super.onForcePressStart(details);
+ if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
+ editableText.showToolbar();
+ }
+ }
+
+ @override
+ void onForcePressEnd(ForcePressDetails details) {
+ // Not required.
+ }
+
+ @override
+ void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
+ if (delegate.selectionEnabled) {
+ switch (Theme.of(_state.context).platform) {
+ case TargetPlatform.iOS:
+ renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.longPress,
+ );
+ break;
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ renderEditable.selectWordsInRange(
+ from: details.globalPosition - details.offsetFromOrigin,
+ to: details.globalPosition,
+ cause: SelectionChangedCause.longPress,
+ );
+ break;
+ }
+ }
+ }
+
+ @override
+ void onSingleTapUp(TapUpDetails details) {
+ editableText.hideToolbar();
+ if (delegate.selectionEnabled) {
+ switch (Theme.of(_state.context).platform) {
+ case TargetPlatform.iOS:
+ renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
+ break;
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ renderEditable.selectPosition(cause: SelectionChangedCause.tap);
+ break;
+ }
+ }
+ _state._requestKeyboard();
+ _state._confirmCurrentSplash();
+ if (_state.widget.onTap != null)
+ _state.widget.onTap();
+ }
+
+ @override
+ void onSingleTapCancel() {
+ _state._cancelCurrentSplash();
+ }
+
+ @override
+ void onSingleLongTapStart(LongPressStartDetails details) {
+ if (delegate.selectionEnabled) {
+ switch (Theme.of(_state.context).platform) {
+ case TargetPlatform.iOS:
+ renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.longPress,
+ );
+ break;
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ renderEditable.selectWord(cause: SelectionChangedCause.longPress);
+ Feedback.forLongPress(_state.context);
+ break;
+ }
+ }
+ _state._confirmCurrentSplash();
+ }
+
+ @override
+ void onDragSelectionStart(DragStartDetails details) {
+ super.onDragSelectionStart(details);
+ _state._startSplash(details.globalPosition);
+ }
+}
+
/// A material design text field.
///
/// A text field lets the user enter text, either with hardware keyboard or with
@@ -531,9 +632,7 @@
}
}
-class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin {
- final GlobalKey<EditableTextState> _editableTextKey = GlobalKey<EditableTextState>();
-
+class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
Set<InteractiveInkFeature> _splashes;
InteractiveInkFeature _currentSplash;
@@ -549,10 +648,21 @@
&& widget.decoration != null
&& widget.decoration.counterText == null;
- bool _shouldShowSelectionToolbar = true;
-
bool _showSelectionHandles = false;
+ _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
+
+ // API for TextSelectionGestureDetectorBuilderDelegate.
+ @override
+ bool forcePressEnabled;
+
+ @override
+ final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
+
+ @override
+ bool get selectionEnabled => widget.selectionEnabled;
+ // End of API for TextSelectionGestureDetectorBuilderDelegate.
+
InputDecoration _getEffectiveDecoration() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData themeData = Theme.of(context);
@@ -621,6 +731,7 @@
@override
void initState() {
super.initState();
+ _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null)
_controller = TextEditingController();
}
@@ -650,7 +761,7 @@
super.dispose();
}
- EditableTextState get _editableText => _editableTextKey.currentState;
+ EditableTextState get _editableText => editableTextKey.currentState;
void _requestKeyboard() {
_editableText?.requestKeyboard();
@@ -659,7 +770,7 @@
bool _shouldShowSelectionHandles(SelectionChangedCause cause) {
// When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either.
- if (!_shouldShowSelectionToolbar)
+ if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
return false;
if (cause == SelectionChangedCause.keyboard)
@@ -707,7 +818,7 @@
InteractiveInkFeature _createInkFeature(Offset globalPosition) {
final MaterialInkController inkController = Material.of(context);
final ThemeData themeData = Theme.of(context);
- final BuildContext editableContext = _editableTextKey.currentContext;
+ final BuildContext editableContext = editableTextKey.currentContext;
final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject();
final Offset position = referenceBox.globalToLocal(globalPosition);
final Color color = themeData.splashColor;
@@ -738,133 +849,6 @@
return splash;
}
- RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable;
-
- void _handleTapDown(TapDownDetails details) {
- _renderEditable.handleTapDown(details);
- _startSplash(details.globalPosition);
-
- // 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.
- final PointerDeviceKind kind = details.kind;
- _shouldShowSelectionToolbar =
- kind == null ||
- kind == PointerDeviceKind.touch ||
- kind == PointerDeviceKind.stylus;
- }
-
- void _handleForcePressStarted(ForcePressDetails details) {
- if (widget.selectionEnabled) {
- _renderEditable.selectWordsInRange(
- from: details.globalPosition,
- cause: SelectionChangedCause.forcePress,
- );
- if (_shouldShowSelectionToolbar) {
- _editableTextKey.currentState.showToolbar();
- }
- }
- }
-
- void _handleSingleTapUp(TapUpDetails details) {
- _editableTextKey.currentState.hideToolbar();
- if (widget.selectionEnabled) {
- switch (Theme.of(context).platform) {
- case TargetPlatform.iOS:
- _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
- break;
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- _renderEditable.selectPosition(cause: SelectionChangedCause.tap);
- break;
- }
- }
- _requestKeyboard();
- _confirmCurrentSplash();
- if (widget.onTap != null)
- widget.onTap();
- }
-
- void _handleSingleTapCancel() {
- _cancelCurrentSplash();
- }
-
- void _handleSingleLongTapStart(LongPressStartDetails details) {
- if (widget.selectionEnabled) {
- switch (Theme.of(context).platform) {
- case TargetPlatform.iOS:
- _renderEditable.selectPositionAt(
- from: details.globalPosition,
- cause: SelectionChangedCause.longPress,
- );
- break;
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- _renderEditable.selectWord(cause: SelectionChangedCause.longPress);
- Feedback.forLongPress(context);
- break;
- }
- }
- _confirmCurrentSplash();
- }
-
- void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
- if (widget.selectionEnabled) {
- switch (Theme.of(context).platform) {
- case TargetPlatform.iOS:
- _renderEditable.selectPositionAt(
- from: details.globalPosition,
- cause: SelectionChangedCause.longPress,
- );
- break;
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- _renderEditable.selectWordsInRange(
- from: details.globalPosition - details.offsetFromOrigin,
- to: details.globalPosition,
- cause: SelectionChangedCause.longPress,
- );
- break;
- }
- }
- }
-
- void _handleSingleLongTapEnd(LongPressEndDetails details) {
- if (widget.selectionEnabled) {
- if (_shouldShowSelectionToolbar)
- _editableTextKey.currentState.showToolbar();
- }
- }
-
- void _handleDoubleTapDown(TapDownDetails details) {
- if (widget.selectionEnabled) {
- _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
- if (_shouldShowSelectionToolbar) {
- _editableText.showToolbar();
- }
- }
- }
-
- void _handleMouseDragSelectionStart(DragStartDetails details) {
- _renderEditable.selectPositionAt(
- from: details.globalPosition,
- cause: SelectionChangedCause.drag,
- );
- _startSplash(details.globalPosition);
- }
-
- void _handleMouseDragSelectionUpdate(
- DragStartDetails startDetails,
- DragUpdateDetails updateDetails,
- ) {
- _renderEditable.selectPositionAt(
- from: startDetails.globalPosition,
- to: updateDetails.globalPosition,
- cause: SelectionChangedCause.drag,
- );
- }
-
void _startSplash(Offset globalPosition) {
if (_effectiveFocusNode.hasFocus)
return;
@@ -933,7 +917,6 @@
if (widget.maxLength != null && widget.maxLengthEnforced)
formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
- bool forcePressEnabled;
TextSelectionControls textSelectionControls;
bool paintCursorAboveText;
bool cursorOpacityAnimates;
@@ -971,7 +954,7 @@
Widget child = RepaintBoundary(
child: EditableText(
- key: _editableTextKey,
+ key: editableTextKey,
readOnly: widget.readOnly,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
@@ -1046,17 +1029,7 @@
onPointerExit: _handlePointerExit,
child: IgnorePointer(
ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
- child: TextSelectionGestureDetector(
- onTapDown: _handleTapDown,
- onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null,
- onSingleTapUp: _handleSingleTapUp,
- onSingleTapCancel: _handleSingleTapCancel,
- onSingleLongTapStart: _handleSingleLongTapStart,
- onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
- onSingleLongTapEnd: _handleSingleLongTapEnd,
- onDoubleTapDown: _handleDoubleTapDown,
- onDragSelectionStart: _handleMouseDragSelectionStart,
- onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
+ child: _selectionGestureDetectorBuilder.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: child,
),
diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart
index ea572c9..af17c6c 100644
--- a/packages/flutter/lib/src/widgets/text_selection.dart
+++ b/packages/flutter/lib/src/widgets/text_selection.dart
@@ -813,6 +813,318 @@
}
}
+/// Delegate interface for the [TextSelectionGestureDetectorBuilder].
+///
+/// The interface is usually implemented by textfield implementations wrapping
+/// [EditableText], that use a [TextSelectionGestureDetectorBuilder] to build a
+/// [TextSelectionGestureDetector] for their [EditableText]. The delegate provides
+/// the builder with information about the current state of the textfield.
+/// Based on these information, the builder adds the correct gesture handlers
+/// to the gesture detector.
+///
+/// See also:
+///
+/// * [TextField], which implements this delegate for the Material textfield.
+/// * [CupertinoTextField], which implements this delegate for the Cupertino textfield.
+abstract class TextSelectionGestureDetectorBuilderDelegate {
+ /// [GlobalKey] to the [EditableText] for which the
+ /// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector].
+ GlobalKey<EditableTextState> get editableTextKey;
+
+ /// Whether the textfield should respond to force presses.
+ bool get forcePressEnabled;
+
+ /// Whether the user may select text in the textfield.
+ bool get selectionEnabled;
+}
+
+/// Builds a [TextSelectionGestureDetector] to wrap an [EditableText].
+///
+/// The class implements sensible defaults for many user interactions
+/// with an [EditableText] (see the documentation of the various gesture handler
+/// methods, e.g. [onTapDown], [onFrocePress], etc.). Subclasses of
+/// [EditableTextSelectionHandlesProvider] can change the behavior performed in
+/// responds to these gesture events by overriding the corresponding handler
+/// methods of this class.
+///
+/// The resulting [TextSelectionGestureDetector] to wrap an [EditableText] is
+/// obtained by calling [buildGestureDetector].
+///
+/// See also:
+///
+/// * [TextField], which uses a subclass to implement the Material-specific
+/// gesture logic of an [EditableText].
+/// * [CupertinoTextField], which uses a subclass to implement the
+/// Cupertino-specific gesture logic of an [EditableText].
+class TextSelectionGestureDetectorBuilder {
+ /// Creates a [TextSelectionGestureDetectorBuilder].
+ ///
+ /// The [delegate] must not be null.
+ TextSelectionGestureDetectorBuilder({
+ @required this.delegate,
+ }) : assert(delegate != null);
+
+ /// The delegate for this [TextSelectionGestureDetectorBuilder].
+ ///
+ /// The delegate provides the builder with information about what actions can
+ /// currently be performed on the textfield. Based on this, the builder adds
+ /// the correct gesture handlers to the gesture detector.
+ @protected
+ final TextSelectionGestureDetectorBuilderDelegate delegate;
+
+ /// Whether to show the selection tool bar.
+ ///
+ /// It is based on the signal source when a [onTapDown] is called. This getter
+ /// will return true if current [onTapDown] event is triggered by a touch or
+ /// a stylus.
+ bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar;
+ bool _shouldShowSelectionToolbar = true;
+
+ /// The [State] of the [EditableText] for which the builder will provide a
+ /// [TextSelectionGestureDetector].
+ @protected
+ EditableTextState get editableText => delegate.editableTextKey.currentState;
+
+ /// The [RenderObject] of the [EditableText] for which the builder will
+ /// provide a [TextSelectionGestureDetector].
+ @protected
+ RenderEditable get renderEditable => editableText.renderEditable;
+
+ /// Handler for [TextSelectionGestureDetector.onTapDown].
+ ///
+ /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
+ /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger or stylus.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onTapDown], which triggers this callback.
+ @protected
+ void onTapDown(TapDownDetails details) {
+ renderEditable.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.
+ final PointerDeviceKind kind = details.kind;
+ _shouldShowSelectionToolbar = kind == null
+ || kind == PointerDeviceKind.touch
+ || kind == PointerDeviceKind.stylus;
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onForcePressStart].
+ ///
+ /// By default, it selects the word at the position of the force press,
+ /// if selection is enabled.
+ ///
+ /// This callback is only applicable when force press is enabled.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onForcePressStart], which triggers this
+ /// callback.
+ @protected
+ void onForcePressStart(ForcePressDetails details) {
+ assert(delegate.forcePressEnabled);
+ _shouldShowSelectionToolbar = true;
+ if (delegate.selectionEnabled) {
+ renderEditable.selectWordsInRange(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.forcePress,
+ );
+ }
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onForcePressEnd].
+ ///
+ /// By default, it selects words in the range specified in [details] and shows
+ /// tool bar if it is necessary.
+ ///
+ /// This callback is only applicable when force press is enabled.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onForcePressEnd], which triggers this
+ /// callback.
+ @protected
+ void onForcePressEnd(ForcePressDetails details) {
+ assert(delegate.forcePressEnabled);
+ renderEditable.selectWordsInRange(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.forcePress,
+ );
+ if (shouldShowSelectionToolbar)
+ editableText.showToolbar();
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onSingleTapUp].
+ ///
+ /// By default, it selects word edge if selection is enabled.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onSingleTapUp], which triggers
+ /// this callback.
+ @protected
+ void onSingleTapUp(TapUpDetails details) {
+ if (delegate.selectionEnabled) {
+ renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
+ }
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onSingleTapCancel].
+ ///
+ /// By default, it services as place holder to enable subclass override.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onSingleTapCancel], which triggers
+ /// this callback.
+ @protected
+ void onSingleTapCancel() {/* Subclass should override this method if needed. */}
+
+ /// Handler for [TextSelectionGestureDetector.onSingleLongTapStart].
+ ///
+ /// By default, it selects text position specified in [details] if selection
+ /// is enabled.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onSingleLongTapStart], which triggers
+ /// this callback.
+ @protected
+ void onSingleLongTapStart(LongPressStartDetails details) {
+ if (delegate.selectionEnabled) {
+ renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.longPress,
+ );
+ }
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onSingleLongTapMoveUpdate].
+ ///
+ /// By default, it updates the selection location specified in [details] if
+ /// selection is enabled.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onSingleLongTapMoveUpdate], which
+ /// triggers this callback.
+ @protected
+ void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
+ if (delegate.selectionEnabled) {
+ renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.longPress,
+ );
+ }
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
+ ///
+ /// By default, it shows tool bar if necessary.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this
+ /// callback.
+ @protected
+ void onSingleLongTapEnd(LongPressEndDetails details) {
+ if (shouldShowSelectionToolbar)
+ editableText.showToolbar();
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
+ ///
+ /// By default, it selects a word through [renderEditable.selectWord] if
+ /// selectionEnabled and shows tool bar if necessary.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
+ /// callback.
+ @protected
+ void onDoubleTapDown(TapDownDetails details) {
+ if (delegate.selectionEnabled) {
+ renderEditable.selectWord(cause: SelectionChangedCause.tap);
+ if (shouldShowSelectionToolbar)
+ editableText.showToolbar();
+ }
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onDragSelectionStart].
+ ///
+ /// By default, it selects a text position specified in [details].
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onDragSelectionStart], which triggers
+ /// this callback.
+ @protected
+ void onDragSelectionStart(DragStartDetails details) {
+ renderEditable.selectPositionAt(
+ from: details.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate].
+ ///
+ /// By default, it updates the selection location specified in [details].
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers
+ /// this callback./lib/src/material/text_field.dart
+ @protected
+ void onDragSelectionUpdate(DragStartDetails startDetails, DragUpdateDetails updateDetails) {
+ renderEditable.selectPositionAt(
+ from: startDetails.globalPosition,
+ to: updateDetails.globalPosition,
+ cause: SelectionChangedCause.drag,
+ );
+ }
+
+ /// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
+ ///
+ /// By default, it services as place holder to enable subclass override.
+ ///
+ /// See also:
+ ///
+ /// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
+ /// callback.
+ @protected
+ void onDragSelectionEnd(DragEndDetails details) {/* Subclass should override this method if needed. */}
+
+ /// Returns a [TextSelectionGestureDetector] configured with the handlers
+ /// provided by this builder.
+ ///
+ /// The [child] or its subtree should contain [EditableText].
+ Widget buildGestureDetector({
+ Key key,
+ HitTestBehavior behavior,
+ Widget child
+ }) {
+ return TextSelectionGestureDetector(
+ key: key,
+ onTapDown: onTapDown,
+ onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
+ onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
+ onSingleTapUp: onSingleTapUp,
+ onSingleTapCancel: onSingleTapCancel,
+ onSingleLongTapStart: onSingleLongTapStart,
+ onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
+ onSingleLongTapEnd: onSingleLongTapEnd,
+ onDoubleTapDown: onDoubleTapDown,
+ onDragSelectionStart: onDragSelectionStart,
+ onDragSelectionUpdate: onDragSelectionUpdate,
+ onDragSelectionEnd: onDragSelectionEnd,
+ behavior: behavior,
+ child: child,
+ );
+ }
+}
+
/// A gesture detector to respond to non-exclusive event chains for a text field.
///
/// An ordinary [GestureDetector] configured to handle events like tap and
diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart
index b978ef5..41ebfe6 100644
--- a/packages/flutter/test/widgets/text_selection_test.dart
+++ b/packages/flutter/test/widgets/text_selection_test.dart
@@ -5,6 +5,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter/widgets.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/material.dart';
void main() {
int tapCount;
@@ -62,6 +64,30 @@
);
}
+ Future<void> pumpTextSelectionGestureDetectorBuilder(
+ WidgetTester tester, {
+ bool forcePressEnabled = true,
+ bool selectionEnabled = true,
+ }) async {
+ final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
+ final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate(
+ editableTextKey: editableTextKey,
+ forcePressEnabled: forcePressEnabled,
+ selectionEnabled: selectionEnabled,
+ );
+ final TextSelectionGestureDetectorBuilder provider =
+ TextSelectionGestureDetectorBuilder(delegate: delegate);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: provider.buildGestureDetector(
+ behavior: HitTestBehavior.translucent,
+ child: FakeEditableText(key: editableTextKey)
+ )
+ )
+ );
+ }
+
testWidgets('a series of taps all call onTaps', (WidgetTester tester) async {
await pumpGestureDetector(tester);
await tester.tapAt(const Offset(200, 200));
@@ -380,4 +406,221 @@
await gesture.removePointer();
});
+
+ testWidgets('test TextSelectionGestureDetectorBuilder long press', (WidgetTester tester) async {
+ await pumpTextSelectionGestureDetectorBuilder(tester);
+ final TestGesture gesture =
+ await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
+ await tester.pump(const Duration(seconds: 2));
+ await gesture.up();
+ await tester.pumpAndSettle();
+
+ final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+ final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+ expect(state.showToolbarCalled, isTrue);
+ expect(renderEditable.selectPositionAtCalled, isTrue);
+ });
+
+ testWidgets('test TextSelectionGestureDetectorBuilder tap', (WidgetTester tester) async {
+ await pumpTextSelectionGestureDetectorBuilder(tester);
+ final TestGesture gesture =
+ await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
+ await gesture.up();
+ await tester.pumpAndSettle();
+
+ final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+ final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+ expect(state.showToolbarCalled, isFalse);
+ expect(renderEditable.selectWordEdgeCalled, isTrue);
+ });
+
+ testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async {
+ await pumpTextSelectionGestureDetectorBuilder(tester);
+ final TestGesture gesture =
+ await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
+ await tester.pump(const Duration(milliseconds: 50));
+ await gesture.up();
+ await gesture.down(const Offset(200.0, 200.0));
+ await tester.pump(const Duration(milliseconds: 50));
+ await gesture.up();
+ await tester.pumpAndSettle();
+
+ final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+ final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+ expect(state.showToolbarCalled, isTrue);
+ expect(renderEditable.selectWordCalled, isTrue);
+ });
+
+ testWidgets('test TextSelectionGestureDetectorBuilder forcePress enabled', (WidgetTester tester) async {
+ await pumpTextSelectionGestureDetectorBuilder(tester);
+ final TestGesture gesture = await tester.createGesture();
+ await gesture.downWithCustomEvent(
+ const Offset(200.0, 200.0),
+ const PointerDownEvent(
+ pointer: 0,
+ position: Offset(200.0, 200.0),
+ pressure: 3.0,
+ pressureMax: 6.0,
+ pressureMin: 0.0,
+ ),
+ );
+ await gesture.updateWithCustomEvent(
+ const PointerUpEvent(
+ pointer: 0,
+ position: Offset(200.0, 200.0),
+ pressure: 0.0,
+ pressureMax: 6.0,
+ pressureMin: 0.0,
+ ),
+ );
+ await tester.pump();
+
+ final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+ final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+ expect(state.showToolbarCalled, isTrue);
+ expect(renderEditable.selectWordsInRangeCalled, isTrue);
+ });
+
+ testWidgets('test TextSelectionGestureDetectorBuilder selection disabled', (WidgetTester tester) async {
+ await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false);
+ final TestGesture gesture =
+ await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
+ await tester.pump(const Duration(seconds: 2));
+ await gesture.up();
+ await tester.pumpAndSettle();
+
+ final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+ final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+ expect(state.showToolbarCalled, isTrue);
+ expect(renderEditable.selectWordsInRangeCalled, isFalse);
+ });
+
+ testWidgets('test TextSelectionGestureDetectorBuilder forcePress disabled', (WidgetTester tester) async {
+ await pumpTextSelectionGestureDetectorBuilder(tester, forcePressEnabled: false);
+ final TestGesture gesture = await tester.createGesture();
+ await gesture.downWithCustomEvent(
+ const Offset(200.0, 200.0),
+ const PointerDownEvent(
+ pointer: 0,
+ position: Offset(200.0, 200.0),
+ pressure: 3.0,
+ pressureMax: 6.0,
+ pressureMin: 0.0,
+ ),
+ );
+ await gesture.up();
+ await tester.pump();
+
+ final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+ final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+ expect(state.showToolbarCalled, isFalse);
+ expect(renderEditable.selectWordsInRangeCalled, isFalse);
+ });
+}
+
+class FakeTextSelectionGestureDetectorBuilderDelegate implements TextSelectionGestureDetectorBuilderDelegate {
+ FakeTextSelectionGestureDetectorBuilderDelegate({
+ this.editableTextKey,
+ this.forcePressEnabled,
+ this.selectionEnabled,
+ });
+
+ @override
+ final GlobalKey<EditableTextState> editableTextKey;
+
+ @override
+ final bool forcePressEnabled;
+
+ @override
+ final bool selectionEnabled;
+}
+
+class FakeEditableText extends EditableText {
+ FakeEditableText({Key key}): super(
+ key: key,
+ controller: TextEditingController(),
+ focusNode: FocusNode(),
+ backgroundCursorColor: Colors.white,
+ cursorColor: Colors.white,
+ style: const TextStyle(),
+ );
+
+ @override
+ FakeEditableTextState createState() => FakeEditableTextState();
+}
+
+class FakeEditableTextState extends EditableTextState {
+ final GlobalKey _editableKey = GlobalKey();
+ bool showToolbarCalled = false;
+
+ @override
+ RenderEditable get renderEditable => _editableKey.currentContext.findRenderObject();
+
+ @override
+ bool showToolbar() {
+ showToolbarCalled = true;
+ return true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return FakeEditable(this, key: _editableKey);
+ }
+}
+
+class FakeEditable extends LeafRenderObjectWidget {
+ const FakeEditable(
+ this.delegate, {
+ Key key,
+ }) : super(key: key);
+ final EditableTextState delegate;
+
+ @override
+ RenderEditable createRenderObject(BuildContext context) {
+ return FakeRenderEditable(delegate);
+ }
+}
+
+class FakeRenderEditable extends RenderEditable {
+ FakeRenderEditable(EditableTextState delegate) : super(
+ text: const TextSpan(
+ style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
+ text: 'placeholder',
+ ),
+ startHandleLayerLink: LayerLink(),
+ endHandleLayerLink: LayerLink(),
+ textAlign: TextAlign.start,
+ textDirection: TextDirection.ltr,
+ locale: const Locale('en', 'US'),
+ offset: ViewportOffset.fixed(10.0),
+ textSelectionDelegate: delegate,
+ selection: const TextSelection.collapsed(
+ offset: 0,
+ ),
+ );
+
+ bool selectWordsInRangeCalled = false;
+ @override
+ void selectWordsInRange({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
+ selectWordsInRangeCalled = true;
+ }
+
+ bool selectWordEdgeCalled = false;
+ @override
+ void selectWordEdge({ @required SelectionChangedCause cause }) {
+ selectWordEdgeCalled = true;
+ }
+
+ bool selectPositionAtCalled = false;
+ @override
+ void selectPositionAt({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
+ selectPositionAtCalled = true;
+ }
+
+ bool selectWordCalled = false;
+ @override
+ void selectWord({ @required SelectionChangedCause cause }) {
+ selectWordCalled = true;
+ }
}