Unverified Commit b94bf87c authored by Mouad Debbar's avatar Mouad Debbar Committed by GitHub

Text selection via mouse (#28290)

parent 1df28e8b
...@@ -12,8 +12,13 @@ import 'recognizer.dart'; ...@@ -12,8 +12,13 @@ import 'recognizer.dart';
/// all touch events inside the view bounds to the embedded Android view. /// all touch events inside the view bounds to the embedded Android view.
/// See [AndroidView.gestureRecognizers] for more details. /// See [AndroidView.gestureRecognizers] for more details.
class EagerGestureRecognizer extends OneSequenceGestureRecognizer { class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
/// Create an eager gesture recognizer.
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
EagerGestureRecognizer({ PointerDeviceKind kind }): super(kind: kind);
@override @override
void addPointer(PointerDownEvent event) { void addAllowedPointer(PointerDownEvent event) {
// We call startTrackingPointer as this is where OneSequenceGestureRecognizer joins the arena. // We call startTrackingPointer as this is where OneSequenceGestureRecognizer joins the arena.
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
......
...@@ -116,16 +116,19 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -116,16 +116,19 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
/// The [interpolation] callback must always return a value in the range 0.0 /// The [interpolation] callback must always return a value in the range 0.0
/// to 1.0 for values of `pressure` that are between `pressureMin` and /// to 1.0 for values of `pressure` that are between `pressureMin` and
/// `pressureMax`. /// `pressureMax`.
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
ForcePressGestureRecognizer({ ForcePressGestureRecognizer({
this.startPressure = 0.4, this.startPressure = 0.4,
this.peakPressure = 0.85, this.peakPressure = 0.85,
this.interpolation = _inverseLerp, this.interpolation = _inverseLerp,
Object debugOwner, Object debugOwner,
PointerDeviceKind kind,
}) : assert(startPressure != null), }) : assert(startPressure != null),
assert(peakPressure != null), assert(peakPressure != null),
assert(interpolation != null), assert(interpolation != null),
assert(peakPressure > startPressure), assert(peakPressure > startPressure),
super(debugOwner: debugOwner); super(debugOwner: debugOwner, kind: kind);
/// A pointer is in contact with the screen and has just pressed with a force /// A pointer is in contact with the screen and has just pressed with a force
/// exceeding the [startPressure]. Consequently, if there were other gesture /// exceeding the [startPressure]. Consequently, if there were other gesture
...@@ -205,7 +208,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -205,7 +208,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
_ForceState _state = _ForceState.ready; _ForceState _state = _ForceState.ready;
@override @override
void addPointer(PointerEvent event) { void addAllowedPointer(PointerEvent event) {
// If the device has a maximum pressure of less than or equal to 1, it // If the device has a maximum pressure of less than or equal to 1, it
// doesn't have touch pressure sensing capabilities. Do not participate // doesn't have touch pressure sensing capabilities. Do not participate
// in the gesture arena. // in the gesture arena.
......
...@@ -123,10 +123,12 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -123,10 +123,12 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// can be moved without limit once the long press is accepted. /// can be moved without limit once the long press is accepted.
LongPressGestureRecognizer({ LongPressGestureRecognizer({
double postAcceptSlopTolerance, double postAcceptSlopTolerance,
PointerDeviceKind kind,
Object debugOwner, Object debugOwner,
}) : super( }) : super(
deadline: kLongPressTimeout, deadline: kLongPressTimeout,
postAcceptSlopTolerance: postAcceptSlopTolerance, postAcceptSlopTolerance: postAcceptSlopTolerance,
kind: kind,
debugOwner: debugOwner, debugOwner: debugOwner,
); );
......
...@@ -52,11 +52,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -52,11 +52,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// Initialize the object. /// Initialize the object.
/// ///
/// [dragStartBehavior] must not be null. /// [dragStartBehavior] must not be null.
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
DragGestureRecognizer({ DragGestureRecognizer({
Object debugOwner, Object debugOwner,
PointerDeviceKind kind,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
}) : assert(dragStartBehavior != null), }) : assert(dragStartBehavior != null),
super(debugOwner: debugOwner); super(debugOwner: debugOwner, kind: kind);
/// Configure the behavior of offsets sent to [onStart]. /// Configure the behavior of offsets sent to [onStart].
/// ///
...@@ -147,7 +150,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -147,7 +150,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{}; final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
@override @override
void addPointer(PointerEvent event) { void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
_velocityTrackers[event.pointer] = VelocityTracker(); _velocityTrackers[event.pointer] = VelocityTracker();
if (_state == _DragState.ready) { if (_state == _DragState.ready) {
...@@ -296,7 +299,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -296,7 +299,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// track each touch point independently. /// track each touch point independently.
class VerticalDragGestureRecognizer extends DragGestureRecognizer { class VerticalDragGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for interactions in the vertical axis. /// Create a gesture recognizer for interactions in the vertical axis.
VerticalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); ///
/// {@macro flutter.gestures.gestureRecognizer.kind}
VerticalDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override @override
bool _isFlingGesture(VelocityEstimate estimate) { bool _isFlingGesture(VelocityEstimate estimate) {
...@@ -330,7 +338,12 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer { ...@@ -330,7 +338,12 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
/// track each touch point independently. /// track each touch point independently.
class HorizontalDragGestureRecognizer extends DragGestureRecognizer { class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for interactions in the horizontal axis. /// Create a gesture recognizer for interactions in the horizontal axis.
HorizontalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); ///
/// {@macro flutter.gestures.gestureRecognizer.kind}
HorizontalDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override @override
bool _isFlingGesture(VelocityEstimate estimate) { bool _isFlingGesture(VelocityEstimate estimate) {
......
...@@ -189,7 +189,10 @@ abstract class MultiDragPointerState { ...@@ -189,7 +189,10 @@ abstract class MultiDragPointerState {
/// start after a long-press gesture. /// start after a long-press gesture.
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer { abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
/// Initialize the object. /// Initialize the object.
MultiDragGestureRecognizer({ @required Object debugOwner }) : super(debugOwner: debugOwner); MultiDragGestureRecognizer({
@required Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
/// Called when this class recognizes the start of a drag gesture. /// Called when this class recognizes the start of a drag gesture.
/// ///
...@@ -200,7 +203,7 @@ abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> exten ...@@ -200,7 +203,7 @@ abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> exten
Map<int, T> _pointers = <int, T>{}; Map<int, T> _pointers = <int, T>{};
@override @override
void addPointer(PointerDownEvent event) { void addAllowedPointer(PointerDownEvent event) {
assert(_pointers != null); assert(_pointers != null);
assert(event.pointer != null); assert(event.pointer != null);
assert(event.position != null); assert(event.position != null);
...@@ -334,7 +337,10 @@ class _ImmediatePointerState extends MultiDragPointerState { ...@@ -334,7 +337,10 @@ class _ImmediatePointerState extends MultiDragPointerState {
/// start after a long-press gesture. /// start after a long-press gesture.
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> { class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once. /// Create a gesture recognizer for tracking multiple pointers at once.
ImmediateMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); ImmediateMultiDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override @override
_ImmediatePointerState createNewPointerState(PointerDownEvent event) { _ImmediatePointerState createNewPointerState(PointerDownEvent event) {
...@@ -380,7 +386,10 @@ class _HorizontalPointerState extends MultiDragPointerState { ...@@ -380,7 +386,10 @@ class _HorizontalPointerState extends MultiDragPointerState {
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> { class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once /// Create a gesture recognizer for tracking multiple pointers at once
/// but only if they first move horizontally. /// but only if they first move horizontally.
HorizontalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); HorizontalMultiDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override @override
_HorizontalPointerState createNewPointerState(PointerDownEvent event) { _HorizontalPointerState createNewPointerState(PointerDownEvent event) {
...@@ -426,7 +435,10 @@ class _VerticalPointerState extends MultiDragPointerState { ...@@ -426,7 +435,10 @@ class _VerticalPointerState extends MultiDragPointerState {
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> { class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once /// Create a gesture recognizer for tracking multiple pointers at once
/// but only if they first move vertically. /// but only if they first move vertically.
VerticalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); VerticalMultiDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override @override
_VerticalPointerState createNewPointerState(PointerDownEvent event) { _VerticalPointerState createNewPointerState(PointerDownEvent event) {
...@@ -528,8 +540,9 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela ...@@ -528,8 +540,9 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela
DelayedMultiDragGestureRecognizer({ DelayedMultiDragGestureRecognizer({
this.delay = kLongPressTimeout, this.delay = kLongPressTimeout,
Object debugOwner, Object debugOwner,
PointerDeviceKind kind,
}) : assert(delay != null), }) : assert(delay != null),
super(debugOwner: debugOwner); super(debugOwner: debugOwner, kind: kind);
/// The amount of time the pointer must remain in the same place for the drag /// The amount of time the pointer must remain in the same place for the drag
/// to be recognized. /// to be recognized.
......
...@@ -69,7 +69,12 @@ class _TapTracker { ...@@ -69,7 +69,12 @@ class _TapTracker {
/// quick succession. /// quick succession.
class DoubleTapGestureRecognizer extends GestureRecognizer { class DoubleTapGestureRecognizer extends GestureRecognizer {
/// Create a gesture recognizer for double taps. /// Create a gesture recognizer for double taps.
DoubleTapGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); ///
/// {@macro flutter.gestures.gestureRecognizer.kind}
DoubleTapGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
// Implementation notes: // Implementation notes:
// The double tap recognizer can be in one of four states. There's no // The double tap recognizer can be in one of four states. There's no
...@@ -100,7 +105,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer { ...@@ -100,7 +105,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{}; final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
@override @override
void addPointer(PointerEvent event) { void addAllowedPointer(PointerEvent event) {
// Ignore out-of-bounds second taps. // Ignore out-of-bounds second taps.
if (_firstTap != null && if (_firstTap != null &&
!_firstTap.isWithinTolerance(event, kDoubleTapSlop)) !_firstTap.isWithinTolerance(event, kDoubleTapSlop))
...@@ -318,7 +323,8 @@ class MultiTapGestureRecognizer extends GestureRecognizer { ...@@ -318,7 +323,8 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
MultiTapGestureRecognizer({ MultiTapGestureRecognizer({
this.longTapDelay = Duration.zero, this.longTapDelay = Duration.zero,
Object debugOwner, Object debugOwner,
}) : super(debugOwner: debugOwner); PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
/// A pointer that might cause a tap has contacted the screen at a particular /// A pointer that might cause a tap has contacted the screen at a particular
/// location. /// location.
...@@ -345,7 +351,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer { ...@@ -345,7 +351,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
final Map<int, _TapGesture> _gestureMap = <int, _TapGesture>{}; final Map<int, _TapGesture> _gestureMap = <int, _TapGesture>{};
@override @override
void addPointer(PointerEvent event) { void addAllowedPointer(PointerEvent event) {
assert(!_gestureMap.containsKey(event.pointer)); assert(!_gestureMap.containsKey(event.pointer));
_gestureMap[event.pointer] = _TapGesture( _gestureMap[event.pointer] = _TapGesture(
gestureRecognizer: this, gestureRecognizer: this,
......
...@@ -33,7 +33,7 @@ typedef RecognizerCallback<T> = T Function(); ...@@ -33,7 +33,7 @@ typedef RecognizerCallback<T> = T Function();
/// ///
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
enum DragStartBehavior { enum DragStartBehavior {
/// Set the initial offset, at the position where the first down even was /// Set the initial offset, at the position where the first down event was
/// detected. /// detected.
down, down,
...@@ -58,7 +58,13 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT ...@@ -58,7 +58,13 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// ///
/// The argument is optional and is only used for debug purposes (e.g. in the /// The argument is optional and is only used for debug purposes (e.g. in the
/// [toString] serialization). /// [toString] serialization).
GestureRecognizer({ this.debugOwner }); ///
/// {@template flutter.gestures.gestureRecognizer.kind}
/// It's possible to limit this recognizer to a specific [PointerDeviceKind]
/// by providing the optional [kind] argument. If [kind] is null,
/// the recognizer will accept pointer events from all device kinds.
/// {@endtemplate}
GestureRecognizer({ this.debugOwner, PointerDeviceKind kind }) : _kind = kind;
/// The recognizer's owner. /// The recognizer's owner.
/// ///
...@@ -66,6 +72,10 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT ...@@ -66,6 +72,10 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// this gesture recognizer was created, to aid in debugging. /// this gesture recognizer was created, to aid in debugging.
final Object debugOwner; final Object debugOwner;
/// The kind of device that's allowed to be recognized. If null, events from
/// all device kinds will be tracked and recognized.
final PointerDeviceKind _kind;
/// Registers a new pointer that might be relevant to this gesture /// Registers a new pointer that might be relevant to this gesture
/// detector. /// detector.
/// ///
...@@ -78,7 +88,43 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT ...@@ -78,7 +88,43 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// subsequent events for this pointer, and to add the pointer to /// subsequent events for this pointer, and to add the pointer to
/// the global gesture arena manager (see [GestureArenaManager]) to track /// the global gesture arena manager (see [GestureArenaManager]) to track
/// that pointer. /// that pointer.
void addPointer(PointerDownEvent event); ///
/// This method is called for each and all pointers being added. In
/// most cases, you want to override [addAllowedPointer] instead.
void addPointer(PointerDownEvent event) {
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
/// Registers a new pointer that's been checked to be allowed by this gesture
/// recognizer.
///
/// Subclasses of [GestureRecognizer] are supposed to override this method
/// instead of [addPointer] because [addPointer] will be called for each
/// pointer being added while [addAllowedPointer] is only called for pointers
/// that are allowed by this recognizer.
@protected
void addAllowedPointer(PointerDownEvent event) { }
/// Handles a pointer being added that's not allowed by this recognizer.
///
/// Subclasses can override this method and reject the gesture.
///
/// See:
/// - [OneSequenceGestureRecognizer.handleNonAllowedPointer].
@protected
void handleNonAllowedPointer(PointerDownEvent event) { }
/// Checks whether or not a pointer is allowed to be tracked by this recognizer.
@protected
bool isPointerAllowed(PointerDownEvent event) {
// Currently, it only checks for device kind. But in the future we could check
// for other things e.g. mouse button.
return _kind == null || _kind == event.kind;
}
/// Releases any resources used by the object. /// Releases any resources used by the object.
/// ///
...@@ -151,11 +197,21 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT ...@@ -151,11 +197,21 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// simultaneous touches to each result in a separate tap. /// simultaneous touches to each result in a separate tap.
abstract class OneSequenceGestureRecognizer extends GestureRecognizer { abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
/// Initialize the object. /// Initialize the object.
OneSequenceGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); ///
/// {@macro flutter.gestures.gestureRecognizer.kind}
OneSequenceGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{}; final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
final Set<int> _trackedPointers = HashSet<int>(); final Set<int> _trackedPointers = HashSet<int>();
@override
void handleNonAllowedPointer(PointerDownEvent event) {
resolve(GestureDisposition.rejected);
}
/// Called when a pointer event is routed to this recognizer. /// Called when a pointer event is routed to this recognizer.
@protected @protected
void handleEvent(PointerEvent event); void handleEvent(PointerEvent event);
...@@ -291,11 +347,14 @@ enum GestureRecognizerState { ...@@ -291,11 +347,14 @@ enum GestureRecognizerState {
/// in the gesture arena, the gesture will be rejected. /// in the gesture arena, the gesture will be rejected.
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer { abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
/// Initializes the [deadline] field during construction of subclasses. /// Initializes the [deadline] field during construction of subclasses.
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
PrimaryPointerGestureRecognizer({ PrimaryPointerGestureRecognizer({
this.deadline, this.deadline,
this.preAcceptSlopTolerance = kTouchSlop, this.preAcceptSlopTolerance = kTouchSlop,
this.postAcceptSlopTolerance = kTouchSlop, this.postAcceptSlopTolerance = kTouchSlop,
Object debugOwner, Object debugOwner,
PointerDeviceKind kind,
}) : assert( }) : assert(
preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0, preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
'The preAcceptSlopTolerance must be positive or null', 'The preAcceptSlopTolerance must be positive or null',
...@@ -304,7 +363,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni ...@@ -304,7 +363,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0, postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
'The postAcceptSlopTolerance must be positive or null', 'The postAcceptSlopTolerance must be positive or null',
), ),
super(debugOwner: debugOwner); super(debugOwner: debugOwner, kind: kind);
/// If non-null, the recognizer will call [didExceedDeadline] after this /// If non-null, the recognizer will call [didExceedDeadline] after this
/// amount of time has elapsed since starting to track the primary pointer. /// amount of time has elapsed since starting to track the primary pointer.
...@@ -346,7 +405,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni ...@@ -346,7 +405,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
Timer _timer; Timer _timer;
@override @override
void addPointer(PointerDownEvent event) { void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
if (state == GestureRecognizerState.ready) { if (state == GestureRecognizerState.ready) {
state = GestureRecognizerState.possible; state = GestureRecognizerState.possible;
......
...@@ -183,7 +183,12 @@ class _LineBetweenPointers{ ...@@ -183,7 +183,12 @@ class _LineBetweenPointers{
/// are no longer in contact with the screen, the recognizer calls [onEnd]. /// are no longer in contact with the screen, the recognizer calls [onEnd].
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
/// Create a gesture recognizer for interactions intended for scaling content. /// Create a gesture recognizer for interactions intended for scaling content.
ScaleGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); ///
/// {@macro flutter.gestures.gestureRecognizer.kind}
ScaleGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
/// The pointers in contact with the screen have established a focal point and /// The pointers in contact with the screen have established a focal point and
/// initial scale of 1.0. /// initial scale of 1.0.
...@@ -239,7 +244,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -239,7 +244,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
} }
@override @override
void addPointer(PointerEvent event) { void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
_velocityTrackers[event.pointer] = VelocityTracker(); _velocityTrackers[event.pointer] = VelocityTracker();
if (_state == _ScaleState.ready) { if (_state == _ScaleState.ready) {
......
...@@ -599,12 +599,12 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -599,12 +599,12 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
} }
} }
InteractiveInkFeature _createInkFeature(TapDownDetails details) { InteractiveInkFeature _createInkFeature(Offset globalPosition) {
final MaterialInkController inkController = Material.of(context); final MaterialInkController inkController = Material.of(context);
final ThemeData themeData = Theme.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 RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject();
final Offset position = referenceBox.globalToLocal(details.globalPosition); final Offset position = referenceBox.globalToLocal(globalPosition);
final Color color = themeData.splashColor; final Color color = themeData.splashColor;
InteractiveInkFeature splash; InteractiveInkFeature splash;
...@@ -637,7 +637,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -637,7 +637,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
void _handleTapDown(TapDownDetails details) { void _handleTapDown(TapDownDetails details) {
_renderEditable.handleTapDown(details); _renderEditable.handleTapDown(details);
_startSplash(details); _startSplash(details.globalPosition);
} }
void _handleForcePressStarted(ForcePressDetails details) { void _handleForcePressStarted(ForcePressDetails details) {
...@@ -723,10 +723,29 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -723,10 +723,29 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
} }
} }
void _startSplash(TapDownDetails details) { void _handleDragSelectionStart(DragStartDetails details) {
_renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
_startSplash(details.globalPosition);
}
void _handleDragSelectionUpdate(
DragStartDetails startDetails,
DragUpdateDetails updateDetails,
) {
_renderEditable.selectPositionAt(
from: startDetails.globalPosition,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
}
void _startSplash(Offset globalPosition) {
if (_effectiveFocusNode.hasFocus) if (_effectiveFocusNode.hasFocus)
return; return;
final InteractiveInkFeature splash = _createInkFeature(details); final InteractiveInkFeature splash = _createInkFeature(globalPosition);
_splashes ??= HashSet<InteractiveInkFeature>(); _splashes ??= HashSet<InteractiveInkFeature>();
_splashes.add(splash); _splashes.add(splash);
_currentSplash = splash; _currentSplash = splash;
...@@ -888,6 +907,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -888,6 +907,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onSingleLongTapEnd: _handleSingleLongTapEnd, onSingleLongTapEnd: _handleSingleLongTapEnd,
onDoubleTapDown: _handleDoubleTapDown, onDoubleTapDown: _handleDoubleTapDown,
onDragSelectionStart: _handleDragSelectionStart,
onDragSelectionUpdate: _handleDragSelectionUpdate,
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: child, child: child,
), ),
......
...@@ -55,6 +55,10 @@ enum SelectionChangedCause { ...@@ -55,6 +55,10 @@ enum SelectionChangedCause {
/// Keyboard-triggered selection changes may be caused by the IME as well as /// Keyboard-triggered selection changes may be caused by the IME as well as
/// by accessibility tools (e.g. TalkBack on Android). /// by accessibility tools (e.g. TalkBack on Android).
keyboard, keyboard,
/// The user used the mouse to change the selection by dragging over a piece
/// of text.
drag,
} }
/// Signature for the callback that reports when the caret location changes. /// Signature for the callback that reports when the caret location changes.
...@@ -1235,7 +1239,7 @@ class RenderEditable extends RenderBox { ...@@ -1235,7 +1239,7 @@ class RenderEditable extends RenderBox {
} }
/// If [ignorePointer] is false (the default) then this method is called by /// If [ignorePointer] is false (the default) then this method is called by
/// the internal gesture recognizer's [LongPressRecognizer.onLongPress] /// the internal gesture recognizer's [LongPressGestureRecognizer.onLongPress]
/// callback. /// callback.
/// ///
/// When [ignorePointer] is true, an ancestor widget must respond to long /// When [ignorePointer] is true, an ancestor widget must respond to long
......
...@@ -367,7 +367,9 @@ class RenderUiKitView extends RenderBox { ...@@ -367,7 +367,9 @@ class RenderUiKitView extends RenderBox {
// When the team wins a gesture the recognizer notifies the engine that it should release // When the team wins a gesture the recognizer notifies the engine that it should release
// the touch sequence to the embedded UIView. // the touch sequence to the embedded UIView.
class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer { class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
_UiKitViewGestureRecognizer(this.controller, this.gestureRecognizerFactories) { _UiKitViewGestureRecognizer(this.controller, this.gestureRecognizerFactories, {
PointerDeviceKind kind,
}): super(kind: kind) {
team = GestureArenaTeam(); team = GestureArenaTeam();
team.captain = this; team.captain = this;
_gestureRecognizers = gestureRecognizerFactories.map( _gestureRecognizers = gestureRecognizerFactories.map(
...@@ -387,7 +389,7 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -387,7 +389,7 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
final UiKitViewController controller; final UiKitViewController controller;
@override @override
void addPointer(PointerDownEvent event) { void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) { for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.addPointer(event); recognizer.addPointer(event);
...@@ -427,7 +429,9 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -427,7 +429,9 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
// When the team wins the recognizer sends all the cached point events to the embedded Android view, and // When the team wins the recognizer sends all the cached point events to the embedded Android view, and
// sets itself to a "forwarding mode" where it will forward any new pointer event to the Android view. // sets itself to a "forwarding mode" where it will forward any new pointer event to the Android view.
class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer { class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
_AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories) { _AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories, {
PointerDeviceKind kind,
}): super(kind: kind) {
team = GestureArenaTeam(); team = GestureArenaTeam();
team.captain = this; team.captain = this;
_gestureRecognizers = gestureRecognizerFactories.map( _gestureRecognizers = gestureRecognizerFactories.map(
...@@ -456,7 +460,7 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -456,7 +460,7 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
Set<OneSequenceGestureRecognizer> _gestureRecognizers; Set<OneSequenceGestureRecognizer> _gestureRecognizers;
@override @override
void addPointer(PointerDownEvent event) { void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) { for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.addPointer(event); recognizer.addPointer(event);
......
...@@ -8,7 +8,7 @@ import 'package:flutter/gestures.dart' show kDoubleTapTimeout, kDoubleTapSlop; ...@@ -8,7 +8,7 @@ import 'package:flutter/gestures.dart' show kDoubleTapTimeout, kDoubleTapSlop;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/gestures.dart';
import 'basic.dart'; import 'basic.dart';
import 'container.dart'; import 'container.dart';
...@@ -20,6 +20,10 @@ import 'transitions.dart'; ...@@ -20,6 +20,10 @@ import 'transitions.dart';
export 'package:flutter/services.dart' show TextSelectionDelegate; export 'package:flutter/services.dart' show TextSelectionDelegate;
/// A duration that controls how often the drag selection update callback is
/// called.
const Duration _kDragSelectionUpdateThrottle = Duration(milliseconds: 50);
/// Which type of selection handle to be displayed. /// Which type of selection handle to be displayed.
/// ///
/// With mixed-direction text, both handles may be the same type. Examples: /// With mixed-direction text, both handles may be the same type. Examples:
...@@ -64,6 +68,19 @@ enum _TextSelectionHandlePosition { start, end } ...@@ -64,6 +68,19 @@ enum _TextSelectionHandlePosition { start, end }
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged]. /// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
typedef TextSelectionOverlayChanged = void Function(TextEditingValue value, Rect caretRect); typedef TextSelectionOverlayChanged = void Function(TextEditingValue value, Rect caretRect);
/// Signature for when a pointer that's dragging to select text has moved again.
///
/// The first argument [startDetails] contains the details of the event that
/// initiated the dragging.
///
/// The second argument [updateDetails] contains the details of the current
/// pointer movement. It's the same as the one passed to [DragGestureRecognizer.onUpdate].
///
/// This signature is different from [GestureDragUpdateCallback] to make it
/// easier for various text fields to use [TextSelectionGestureDetector] without
/// having to store the start position.
typedef DragSelectionUpdateCallback = void Function(DragStartDetails startDetails, DragUpdateDetails updateDetails);
/// An interface for building the selection UI, to be provided by the /// An interface for building the selection UI, to be provided by the
/// implementor of the toolbar widget. /// implementor of the toolbar widget.
/// ///
...@@ -620,6 +637,9 @@ class TextSelectionGestureDetector extends StatefulWidget { ...@@ -620,6 +637,9 @@ class TextSelectionGestureDetector extends StatefulWidget {
this.onSingleLongTapMoveUpdate, this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd, this.onSingleLongTapEnd,
this.onDoubleTapDown, this.onDoubleTapDown,
this.onDragSelectionStart,
this.onDragSelectionUpdate,
this.onDragSelectionEnd,
this.behavior, this.behavior,
@required this.child, @required this.child,
}) : assert(child != null), }) : assert(child != null),
...@@ -664,6 +684,19 @@ class TextSelectionGestureDetector extends StatefulWidget { ...@@ -664,6 +684,19 @@ class TextSelectionGestureDetector extends StatefulWidget {
/// time (within [kDoubleTapTimeout]) to a previous short tap. /// time (within [kDoubleTapTimeout]) to a previous short tap.
final GestureTapDownCallback onDoubleTapDown; final GestureTapDownCallback onDoubleTapDown;
/// Called when a mouse starts dragging to select text.
final GestureDragStartCallback 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 DragSelectionUpdateCallback onDragSelectionUpdate;
/// Called when a mouse that was previously dragging is released.
final GestureDragEndCallback onDragSelectionEnd;
/// How this gesture detector should behave during hit testing. /// How this gesture detector should behave during hit testing.
/// ///
/// This defaults to [HitTestBehavior.deferToChild]. /// This defaults to [HitTestBehavior.deferToChild].
...@@ -687,6 +720,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec ...@@ -687,6 +720,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
@override @override
void dispose() { void dispose() {
_doubleTapTimer?.cancel(); _doubleTapTimer?.cancel();
_dragUpdateThrottleTimer?.cancel();
super.dispose(); super.dispose();
} }
...@@ -730,6 +764,56 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec ...@@ -730,6 +764,56 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
} }
} }
DragStartDetails _lastDragStartDetails;
DragUpdateDetails _lastDragUpdateDetails;
Timer _dragUpdateThrottleTimer;
void _handleDragStart(DragStartDetails details) {
assert(_lastDragStartDetails == null);
_lastDragStartDetails = details;
if (widget.onDragSelectionStart != null) {
widget.onDragSelectionStart(details);
}
}
void _handleDragUpdate(DragUpdateDetails details) {
_lastDragUpdateDetails = details;
// Only schedule a new timer if there's no one pending.
_dragUpdateThrottleTimer ??= Timer(_kDragSelectionUpdateThrottle, _handleDragUpdateThrottled);
}
/// 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();
}
if (widget.onDragSelectionEnd != null) {
widget.onDragSelectionEnd(details);
}
_dragUpdateThrottleTimer = null;
_lastDragStartDetails = null;
_lastDragUpdateDetails = null;
}
void _forcePressStarted(ForcePressDetails details) { void _forcePressStarted(ForcePressDetails details) {
_doubleTapTimer?.cancel(); _doubleTapTimer?.cancel();
_doubleTapTimer = null; _doubleTapTimer = null;
...@@ -754,7 +838,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec ...@@ -754,7 +838,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
} }
} }
void _handleLongPressUp(LongPressEndDetails details) { void _handleLongPressEnd(LongPressEndDetails details) {
if (!_isDoubleTap && widget.onSingleLongTapEnd != null) { if (!_isDoubleTap && widget.onSingleLongTapEnd != null) {
widget.onSingleLongTapEnd(details); widget.onSingleLongTapEnd(details);
} }
...@@ -778,15 +862,64 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec ...@@ -778,15 +862,64 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
onTapDown: _handleTapDown,
onTapUp: _handleTapUp, gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
onForcePressStart: widget.onForcePressStart != null ? _forcePressStarted : null, () => TapGestureRecognizer(debugOwner: this),
onForcePressEnd: widget.onForcePressEnd != null ? _forcePressEnded : null, (TapGestureRecognizer instance) {
onTapCancel: _handleTapCancel, instance
onLongPressStart: _handleLongPressStart, ..onTapDown = _handleTapDown
onLongPressMoveUpdate: _handleLongPressMoveUpdate, ..onTapUp = _handleTapUp
onLongPressEnd: _handleLongPressUp, ..onTapCancel = _handleTapCancel;
},
);
if (widget.onSingleLongTapStart != null ||
widget.onSingleLongTapMoveUpdate != null ||
widget.onSingleLongTapEnd != null) {
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.touch),
(LongPressGestureRecognizer instance) {
instance
..onLongPressStart = _handleLongPressStart
..onLongPressMoveUpdate = _handleLongPressMoveUpdate
..onLongPressEnd = _handleLongPressEnd;
},
);
}
if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) {
// TODO(mdebbar): Support dragging in any direction (for multiline text).
// https://github.com/flutter/flutter/issues/28676
gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.mouse),
(HorizontalDragGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
},
);
}
if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this),
(ForcePressGestureRecognizer instance) {
instance
..onStart = widget.onForcePressStart != null ? _forcePressStarted : null
..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null;
},
);
}
return RawGestureDetector(
gestures: gestures,
excludeFromSemantics: true, excludeFromSemantics: true,
behavior: widget.behavior, behavior: widget.behavior,
child: widget.child, child: widget.child,
......
...@@ -517,4 +517,83 @@ void main() { ...@@ -517,4 +517,83 @@ void main() {
tester.route(pointer.up()); tester.route(pointer.up());
drag.dispose(); drag.dispose();
}); });
}
\ No newline at end of file testGesture('Can filter drags based on device kind', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag =
HorizontalDragGestureRecognizer(
kind: PointerDeviceKind.mouse,
)
..dragStartBehavior = DragStartBehavior.down;
bool didStartDrag = false;
drag.onStart = (_) {
didStartDrag = true;
};
double updatedDelta;
drag.onUpdate = (DragUpdateDetails details) {
updatedDelta = details.primaryDelta;
};
bool didEndDrag = false;
drag.onEnd = (DragEndDetails details) {
didEndDrag = true;
};
// Using a touch pointer to drag shouldn't be recognized.
final TestPointer touchPointer = TestPointer(5, PointerDeviceKind.touch);
final PointerDownEvent touchDown = touchPointer.down(const Offset(10.0, 10.0));
drag.addPointer(touchDown);
tester.closeArena(5);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(touchDown);
// Still doesn't recognize the drag because it's coming from a touch pointer.
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(touchPointer.move(const Offset(20.0, 25.0)));
// Still doesn't recognize the drag because it's coming from a touch pointer.
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(touchPointer.up());
// Still doesn't recognize the drag because it's coming from a touch pointer.
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
// Using a mouse pointer to drag should be recognized.
final TestPointer mousePointer = TestPointer(5, PointerDeviceKind.mouse);
final PointerDownEvent mouseDown = mousePointer.down(const Offset(10.0, 10.0));
drag.addPointer(mouseDown);
tester.closeArena(5);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(mouseDown);
expect(didStartDrag, isTrue);
didStartDrag = false;
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(mousePointer.move(const Offset(20.0, 25.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, 10.0);
updatedDelta = null;
expect(didEndDrag, isFalse);
tester.route(mousePointer.up());
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isTrue);
didEndDrag = false;
drag.dispose();
});
}
...@@ -278,4 +278,44 @@ void main() { ...@@ -278,4 +278,44 @@ void main() {
longPressDrag.dispose(); longPressDrag.dispose();
}); });
}); });
testGesture('Can filter long press based on device kind', (GestureTester tester) {
final LongPressGestureRecognizer mouseLongPress = LongPressGestureRecognizer(kind: PointerDeviceKind.mouse);
bool mouseLongPressDown = false;
mouseLongPress.onLongPress = () {
mouseLongPressDown = true;
};
const PointerDownEvent mouseDown = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
kind: PointerDeviceKind.mouse,
);
const PointerDownEvent touchDown = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
kind: PointerDeviceKind.touch,
);
// Touch events shouldn't be recognized.
mouseLongPress.addPointer(touchDown);
tester.closeArena(5);
expect(mouseLongPressDown, isFalse);
tester.route(touchDown);
expect(mouseLongPressDown, isFalse);
tester.async.elapse(const Duration(seconds: 2));
expect(mouseLongPressDown, isFalse);
// Mouse events are still recognized.
mouseLongPress.addPointer(mouseDown);
tester.closeArena(5);
expect(mouseLongPressDown, isFalse);
tester.route(mouseDown);
expect(mouseLongPressDown, isFalse);
tester.async.elapse(const Duration(seconds: 2));
expect(mouseLongPressDown, isTrue);
mouseLongPress.dispose();
});
} }
...@@ -61,4 +61,32 @@ void main() { ...@@ -61,4 +61,32 @@ void main() {
expect(didStartDrag, isTrue); expect(didStartDrag, isTrue);
drag.dispose(); drag.dispose();
}); });
testGesture('MultiDrag: can filter based on device kind', (GestureTester tester) {
final DelayedMultiDragGestureRecognizer drag =
DelayedMultiDragGestureRecognizer(kind: PointerDeviceKind.touch);
bool didStartDrag = false;
drag.onStart = (Offset position) {
didStartDrag = true;
return TestDrag();
};
final TestPointer mousePointer = TestPointer(5, PointerDeviceKind.mouse);
final PointerDownEvent down = mousePointer.down(const Offset(10.0, 10.0));
drag.addPointer(down);
tester.closeArena(5);
expect(didStartDrag, isFalse);
tester.async.flushMicrotasks();
expect(didStartDrag, isFalse);
tester.route(mousePointer.move(const Offset(20.0, 20.0))); // move less than touch slop before delay expires
expect(didStartDrag, isFalse);
tester.async.elapse(kLongPressTimeout * 2); // expire delay
// Still false because it shouldn't recognize mouse events.
expect(didStartDrag, isFalse);
tester.route(mousePointer.move(const Offset(30.0, 70.0))); // move more than touch slop after delay expires
// And still false.
expect(didStartDrag, isFalse);
drag.dispose();
});
} }
...@@ -69,4 +69,81 @@ void main() { ...@@ -69,4 +69,81 @@ void main() {
tap.dispose(); tap.dispose();
}); });
testGesture('Can filter based on device kind', (GestureTester tester) {
final MultiTapGestureRecognizer tap =
MultiTapGestureRecognizer(
longTapDelay: kLongPressTimeout,
kind: PointerDeviceKind.touch,
);
final List<String> log = <String>[];
tap.onTapDown = (int pointer, TapDownDetails details) { log.add('tap-down $pointer'); };
tap.onTapUp = (int pointer, TapUpDetails details) { log.add('tap-up $pointer'); };
tap.onTap = (int pointer) { log.add('tap $pointer'); };
tap.onLongTapDown = (int pointer, TapDownDetails details) { log.add('long-tap-down $pointer'); };
tap.onTapCancel = (int pointer) { log.add('tap-cancel $pointer'); };
final TestPointer touchPointer5 = TestPointer(5, PointerDeviceKind.touch);
final PointerDownEvent down5 = touchPointer5.down(const Offset(10.0, 10.0));
tap.addPointer(down5);
tester.closeArena(5);
expect(log, <String>['tap-down 5']);
log.clear();
tester.route(down5);
expect(log, isEmpty);
final TestPointer mousePointer6 = TestPointer(6, PointerDeviceKind.mouse);
final PointerDownEvent down6 = mousePointer6.down(const Offset(20.0, 20.0));
tap.addPointer(down6);
tester.closeArena(6);
// Mouse down should be ignored by the recognizer.
expect(log, isEmpty);
final TestPointer touchPointer7 = TestPointer(7, PointerDeviceKind.touch);
final PointerDownEvent down7 = touchPointer7.down(const Offset(15.0, 15.0));
tap.addPointer(down7);
tester.closeArena(7);
expect(log, <String>['tap-down 7']);
log.clear();
tester.route(down7);
expect(log, isEmpty);
tester.route(touchPointer5.move(const Offset(11.0, 12.0)));
expect(log, isEmpty);
// Move within the [kTouchSlop] range.
tester.route(mousePointer6.move(const Offset(21.0, 18.0)));
// Move beyond the slop range.
tester.route(mousePointer6.move(const Offset(50.0, 40.0)));
// Neither triggers any event because they originate from a mouse.
expect(log, isEmpty);
tester.route(touchPointer7.move(const Offset(14.0, 13.0)));
expect(log, isEmpty);
tester.route(touchPointer5.up());
expect(log, <String>[
'tap-up 5',
'tap 5',
]);
log.clear();
// Mouse up should be ignored.
tester.route(mousePointer6.up());
expect(log, isEmpty);
tester.async.elapse(kLongPressTimeout + kPressTimeout);
// Only the touch pointer (7) triggers a long-tap, not the mouse pointer (6).
expect(log, <String>['long-tap-down 7']);
log.clear();
tester.route(touchPointer7.move(const Offset(40.0, 30.0))); // move more than kTouchSlop from 15.0,15.0
expect(log, <String>['tap-cancel 7']);
log.clear();
tap.dispose();
});
} }
...@@ -220,6 +220,101 @@ void main() { ...@@ -220,6 +220,101 @@ void main() {
tap.dispose(); tap.dispose();
}); });
testGesture('Rejects scale gestures from unallowed device kinds', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer(kind: PointerDeviceKind.touch);
bool didStartScale = false;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
};
double updatedScale;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
};
final TestPointer mousePointer = TestPointer(1, PointerDeviceKind.mouse);
final PointerDownEvent down = mousePointer.down(const Offset(0.0, 0.0));
scale.addPointer(down);
tester.closeArena(1);
// One-finger panning
tester.route(down);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
// Using a mouse, the scale gesture shouldn't even start.
tester.route(mousePointer.move(const Offset(20.0, 30.0)));
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
scale.dispose();
});
testGesture('Scale gestures starting from allowed device kinds cannot be ended from unallowed devices', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer(kind: PointerDeviceKind.touch);
bool didStartScale = false;
Offset updatedFocalPoint;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
};
double updatedScale;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedFocalPoint = details.focalPoint;
};
bool didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
final TestPointer touchPointer = TestPointer(1, PointerDeviceKind.touch);
final PointerDownEvent down = touchPointer.down(const Offset(0.0, 0.0));
scale.addPointer(down);
tester.closeArena(1);
// One-finger panning
tester.route(down);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, const Offset(0.0, 0.0));
expect(didEndScale, isFalse);
// The gesture can start using one touch finger.
tester.route(touchPointer.move(const Offset(20.0, 30.0)));
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedScale, 1.0);
updatedScale = null;
expect(didEndScale, isFalse);
// Two-finger scaling
final TestPointer mousePointer = TestPointer(2, PointerDeviceKind.mouse);
final PointerDownEvent down2 = mousePointer.down(const Offset(10.0, 20.0));
scale.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
// Mouse-generated events are ignored.
expect(didEndScale, isFalse);
expect(updatedScale, isNull);
expect(didStartScale, isFalse);
// Zoom in using a mouse doesn't work either.
tester.route(mousePointer.move(const Offset(0.0, 10.0)));
expect(updatedScale, isNull);
expect(didEndScale, isFalse);
scale.dispose();
});
testGesture('Scale gesture competes with drag', (GestureTester tester) { testGesture('Scale gesture competes with drag', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer(); final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
......
...@@ -12,7 +12,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -12,7 +12,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind;
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
...@@ -533,6 +533,37 @@ void main() { ...@@ -533,6 +533,37 @@ void main() {
expect(controller.selection.extentOffset, testValue.indexOf('f')+1); expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
}); });
testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
)
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press the 'e' using a mouse device.
final int eIndex = testValue.indexOf('e');
final Offset ePos = textOffsetToPosition(tester, eIndex);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
// The cursor is placed just like a regular tap.
expect(controller.selection.baseOffset, eIndex);
expect(controller.selection.extentOffset, eIndex);
});
testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async { testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
...@@ -564,6 +595,69 @@ void main() { ...@@ -564,6 +595,69 @@ void main() {
expect(controller.selection.extentOffset, -1); expect(controller.selection.extentOffset, -1);
}); });
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, testValue.indexOf('e'));
expect(controller.selection.extentOffset, testValue.indexOf('g'));
});
testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.moveTo(gPos);
await tester.pump();
await gesture.up();
expect(controller.selection.baseOffset, testValue.indexOf('e'));
expect(controller.selection.extentOffset, testValue.indexOf('g'));
});
testWidgets('Can drag handles to change selection', (WidgetTester tester) async { testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
void main() { void main() {
...@@ -13,9 +14,11 @@ void main() { ...@@ -13,9 +14,11 @@ void main() {
int doubleTapDownCount; int doubleTapDownCount;
int forcePressStartCount; int forcePressStartCount;
int forcePressEndCount; int forcePressEndCount;
int dragStartCount;
int dragUpdateCount;
int dragEndCount;
const Offset forcePressOffset = Offset(400.0, 50.0); const Offset forcePressOffset = Offset(400.0, 50.0);
void _handleTapDown(TapDownDetails details) { tapCount++; } void _handleTapDown(TapDownDetails details) { tapCount++; }
void _handleSingleTapUp(TapUpDetails details) { singleTapUpCount++; } void _handleSingleTapUp(TapUpDetails details) { singleTapUpCount++; }
void _handleSingleTapCancel() { singleTapCancelCount++; } void _handleSingleTapCancel() { singleTapCancelCount++; }
...@@ -23,6 +26,9 @@ void main() { ...@@ -23,6 +26,9 @@ void main() {
void _handleDoubleTapDown(TapDownDetails details) { doubleTapDownCount++; } void _handleDoubleTapDown(TapDownDetails details) { doubleTapDownCount++; }
void _handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; } void _handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; }
void _handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; } void _handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; }
void _handleDragSelectionStart(DragStartDetails details) { dragStartCount++; }
void _handleDragSelectionUpdate(DragStartDetails _, DragUpdateDetails details) { dragUpdateCount++; }
void _handleDragSelectionEnd(DragEndDetails details) { dragEndCount++; }
setUp(() { setUp(() {
tapCount = 0; tapCount = 0;
...@@ -32,6 +38,9 @@ void main() { ...@@ -32,6 +38,9 @@ void main() {
doubleTapDownCount = 0; doubleTapDownCount = 0;
forcePressStartCount = 0; forcePressStartCount = 0;
forcePressEndCount = 0; forcePressEndCount = 0;
dragStartCount = 0;
dragUpdateCount = 0;
dragEndCount = 0;
}); });
Future<void> pumpGestureDetector(WidgetTester tester) async { Future<void> pumpGestureDetector(WidgetTester tester) async {
...@@ -45,6 +54,9 @@ void main() { ...@@ -45,6 +54,9 @@ void main() {
onDoubleTapDown: _handleDoubleTapDown, onDoubleTapDown: _handleDoubleTapDown,
onForcePressStart: _handleForcePressStart, onForcePressStart: _handleForcePressStart,
onForcePressEnd: _handleForcePressEnd, onForcePressEnd: _handleForcePressEnd,
onDragSelectionStart: _handleDragSelectionStart,
onDragSelectionUpdate: _handleDragSelectionUpdate,
onDragSelectionEnd: _handleDragSelectionEnd,
child: Container(), child: Container(),
), ),
); );
...@@ -275,4 +287,89 @@ void main() { ...@@ -275,4 +287,89 @@ void main() {
expect(forcePressEndCount, 1); expect(forcePressEndCount, 1);
expect(doubleTapDownCount, 0); expect(doubleTapDownCount, 0);
}); });
testWidgets('a long press from a touch device is recognized as a long single tap', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.touch);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pumpAndSettle();
expect(tapCount, 1);
expect(singleTapUpCount, 0);
expect(singleLongTapStartCount, 1);
});
testWidgets('a long press from a mouse is just a tap', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pumpAndSettle();
expect(tapCount, 1);
expect(singleTapUpCount, 1);
expect(singleLongTapStartCount, 0);
});
testWidgets('a touch drag is not recognized for text selection', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.touch);
await tester.pump();
await gesture.moveBy(const Offset(210.0, 200.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(tapCount, 0);
expect(singleTapUpCount, 0);
expect(dragStartCount, 0);
expect(dragUpdateCount, 0);
expect(dragEndCount, 0);
});
testWidgets('a mouse drag is recognized for text selection', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.moveBy(const Offset(210.0, 200.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(tapCount, 0);
expect(singleTapUpCount, 0);
expect(dragStartCount, 1);
expect(dragUpdateCount, 1);
expect(dragEndCount, 1);
});
testWidgets('a slow mouse drag is still recognized for text selection', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.moveBy(const Offset(210.0, 200.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(dragStartCount, 1);
expect(dragUpdateCount, 1);
expect(dragEndCount, 1);
});
} }
...@@ -553,8 +553,12 @@ abstract class WidgetController { ...@@ -553,8 +553,12 @@ abstract class WidgetController {
/// ///
/// You can use [createGesture] if your gesture doesn't begin with an initial /// You can use [createGesture] if your gesture doesn't begin with an initial
/// down gesture. /// down gesture.
Future<TestGesture> startGesture(Offset downLocation, {int pointer}) async { Future<TestGesture> startGesture(
final TestGesture result = await createGesture(pointer: pointer); Offset downLocation, {
int pointer,
PointerDeviceKind kind = PointerDeviceKind.touch,
}) async {
final TestGesture result = await createGesture(pointer: pointer, kind: kind);
await result.down(downLocation); await result.down(downLocation);
return result; return result;
} }
......
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