Unverified Commit fc7bd6ad authored by chunhtai's avatar chunhtai Committed by GitHub

refactor out selection handlers (#35207)

parent e91822da
......@@ -68,6 +68,39 @@ enum OverlayVisibilityMode {
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 CupertinoTextField extends StatefulWidget {
}
}
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 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
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 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
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 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
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 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
padding: widget.padding,
child: RepaintBoundary(
child: EditableText(
key: _editableTextKey,
key: editableTextKey,
controller: controller,
readOnly: widget.readOnly,
showCursor: widget.showCursor,
......@@ -925,18 +856,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
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),
......
......@@ -35,6 +35,107 @@ typedef InputCounterWidgetBuilder = Widget Function(
@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 TextField extends StatefulWidget {
}
}
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 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
&& 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 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
@override
void initState() {
super.initState();
_selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null)
_controller = TextEditingController();
}
......@@ -650,7 +761,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
super.dispose();
}
EditableTextState get _editableText => _editableTextKey.currentState;
EditableTextState get _editableText => editableTextKey.currentState;
void _requestKeyboard() {
_editableText?.requestKeyboard();
......@@ -659,7 +770,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
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 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
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 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
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 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
if (widget.maxLength != null && widget.maxLengthEnforced)
formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
bool forcePressEnabled;
TextSelectionControls textSelectionControls;
bool paintCursorAboveText;
bool cursorOpacityAnimates;
......@@ -971,7 +954,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
Widget child = RepaintBoundary(
child: EditableText(
key: _editableTextKey,
key: editableTextKey,
readOnly: widget.readOnly,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
......@@ -1046,17 +1029,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
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,
),
......
......@@ -813,6 +813,318 @@ class _TextSelectionHandleOverlayState
}
}
/// 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
......
......@@ -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 @@ void main() {
);
}
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 @@ void main() {
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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment