Unverified Commit 0752af84 authored by Bernardo Ferrari's avatar Bernardo Ferrari Committed by GitHub

Add `allowedButtonsFilter` to prevent Draggable from appearing with secondary click. (#111852)

* DragTarget part 1.

[WIP] Change GestureRecognizer. Sorry.

[WIP] Move from GestureRecognizer to MultiDragGestureRecognizer.

Make it a `Set<int>?`

Get bitwise operations working.

Fix test. Rename to allowedInputPointers.

Convert into a builder.

Improve code with default funciton.

Refactor everything again.

Rename to buttonEventFilter.

Use static function.

Fix analyzer.

Fix private reference.

Use // in private method.

* Fix Renzo request.

* Add `allowedButtonsFilter` everywhere.

* Refactor monoDrag for multi pointer support.

* Fix tests?

* Change default to always true.

* Fix PR comments.

* Completely refactor long press.

* Add forgotten class.

* Revert "Completely refactor long press."

This reverts commit 5038e8603e250e8c928b0f1754fb794b7b75738b.

* Add default value to LongPress

* Refactor doubleTap.

* Relax double tap.

* Write comment in LongPress.

* Use template.
parent 9024a70f
......@@ -24,6 +24,7 @@ class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
});
@override
......
......@@ -133,6 +133,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
}) : assert(startPressure != null),
assert(peakPressure != null),
assert(interpolation != null),
......
......@@ -247,6 +247,8 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// The [duration] argument can be used to overwrite the default duration
/// after which the long press will be recognized.
///
/// {@macro flutter.gestures.tap.TapGestureRecognizer.allowedButtonsFilter}
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
LongPressGestureRecognizer({
Duration? duration,
......@@ -258,6 +260,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
super.kind,
super.supportedDevices,
super.debugOwner,
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
}) : super(
deadline: duration ?? kLongPressTimeout,
);
......@@ -268,6 +271,12 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
// different set of buttons, the gesture is canceled.
int? _initialButtons;
// Accept the input if, and only if, a single button is pressed.
static bool _defaultButtonAcceptBehavior(int buttons) =>
buttons == kPrimaryButton ||
buttons == kSecondaryButton ||
buttons == kTertiaryButton;
/// Called when a pointer has contacted the screen at a particular location
/// with a primary button, which might be the start of a long-press.
///
......
......@@ -59,9 +59,8 @@ typedef GestureVelocityTrackerBuilder = VelocityTracker Function(PointerEvent ev
/// consider using one of its subclasses to recognize specific types for drag
/// gestures.
///
/// [DragGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has at least one non-null callback. If it has no callbacks, it
/// is a no-op.
/// [DragGestureRecognizer] competes on pointer events only when it has at
/// least one non-null callback. If it has no callbacks, it is a no-op.
///
/// See also:
///
......@@ -84,10 +83,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
this.dragStartBehavior = DragStartBehavior.start,
this.velocityTrackerBuilder = _defaultBuilder,
super.supportedDevices,
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
}) : assert(dragStartBehavior != null);
static VelocityTracker _defaultBuilder(PointerEvent event) => VelocityTracker.withKind(event.kind);
// Accept the input if, and only if, [kPrimaryButton] is pressed.
static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
/// Configure the behavior of offsets passed to [onStart].
///
/// If set to [DragStartBehavior.start], the [onStart] callback will be called
......@@ -122,7 +125,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [DragDownDetails], which is passed as an argument to this callback.
GestureDragDownCallback? onDown;
......@@ -137,7 +140,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [DragStartDetails], which is passed as an argument to this callback.
GestureDragStartCallback? onStart;
......@@ -151,7 +154,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [DragUpdateDetails], which is passed as an argument to this callback.
GestureDragUpdateCallback? onUpdate;
......@@ -166,7 +169,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [DragEndDetails], which is passed as an argument to this callback.
GestureDragEndCallback? onEnd;
......@@ -174,7 +177,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [allowedButtonsFilter], which decides which button will be allowed.
GestureDragCancelCallback? onCancel;
/// The minimum distance an input pointer drag must have moved to
......@@ -251,18 +254,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override
bool isPointerAllowed(PointerEvent event) {
if (_initialButtons == null) {
switch (event.buttons) {
case kPrimaryButton:
if (onDown == null &&
onStart == null &&
onUpdate == null &&
onEnd == null &&
onCancel == null) {
return false;
}
break;
default:
return false;
if (onDown == null &&
onStart == null &&
onUpdate == null &&
onEnd == null &&
onCancel == null) {
return false;
}
} else {
// There can be multiple drags simultaneously. Their effects are combined.
......@@ -449,7 +446,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
}
void _checkDown() {
assert(_initialButtons == kPrimaryButton);
if (onDown != null) {
final DragDownDetails details = DragDownDetails(
globalPosition: _initialPosition.global,
......@@ -460,7 +456,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
}
void _checkStart(Duration timestamp, int pointer) {
assert(_initialButtons == kPrimaryButton);
if (onStart != null) {
final DragStartDetails details = DragStartDetails(
sourceTimeStamp: timestamp,
......@@ -479,7 +474,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
required Offset globalPosition,
Offset? localPosition,
}) {
assert(_initialButtons == kPrimaryButton);
if (onUpdate != null) {
final DragUpdateDetails details = DragUpdateDetails(
sourceTimeStamp: sourceTimeStamp,
......@@ -493,7 +487,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
}
void _checkEnd(int pointer) {
assert(_initialButtons == kPrimaryButton);
if (onEnd == null) {
return;
}
......@@ -530,7 +523,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
}
void _checkCancel() {
assert(_initialButtons == kPrimaryButton);
if (onCancel != null) {
invokeCallback<void>('onCancel', onCancel!);
}
......@@ -570,6 +562,7 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
});
@override
......@@ -616,6 +609,7 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
});
@override
......@@ -654,6 +648,7 @@ class PanGestureRecognizer extends DragGestureRecognizer {
PanGestureRecognizer({
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
});
@override
......
......@@ -223,8 +223,12 @@ abstract class MultiDragGestureRecognizer extends GestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
});
// Accept the input if, and only if, [kPrimaryButton] is pressed.
static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
/// Called when this class recognizes the start of a drag gesture.
///
/// The remaining notifications for this drag gesture are delivered to the
......@@ -382,6 +386,7 @@ class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
});
@override
......@@ -439,6 +444,7 @@ class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
});
@override
......@@ -496,6 +502,7 @@ class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
});
@override
......@@ -606,6 +613,7 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
}) : assert(delay != null);
/// The amount of time the pointer must remain in the same place for the drag
......
......@@ -113,8 +113,8 @@ class _TapTracker {
/// Recognizes when the user has tapped the screen at the same location twice in
/// quick succession.
///
/// [DoubleTapGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has a non-null callback. If it has no callbacks, it is a no-op.
/// [DoubleTapGestureRecognizer] competes on pointer events when it
/// has a non-null callback. If it has no callbacks, it is a no-op.
///
class DoubleTapGestureRecognizer extends GestureRecognizer {
/// Create a gesture recognizer for double taps.
......@@ -128,8 +128,13 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
});
// The default value for [allowedButtonsFilter].
// Accept the input if, and only if, [kPrimaryButton] is pressed.
static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
// Implementation notes:
//
// The double tap recognizer can be in one of four states. There's no
......@@ -165,7 +170,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [TapDownDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onDoubleTapDown], which exposes this callback.
GestureTapDownCallback? onDoubleTapDown;
......@@ -178,7 +183,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [GestureDetector.onDoubleTap], which exposes this callback.
GestureDoubleTapCallback? onDoubleTap;
......@@ -192,7 +197,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [allowedButtonsFilter], which decides which button will be allowed.
/// * [GestureDetector.onDoubleTapCancel], which exposes this callback.
GestureTapCancelCallback? onDoubleTapCancel;
......@@ -203,19 +208,19 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
@override
bool isPointerAllowed(PointerDownEvent event) {
if (_firstTap == null) {
switch (event.buttons) {
case kPrimaryButton:
if (onDoubleTapDown == null &&
onDoubleTap == null &&
onDoubleTapCancel == null) {
return false;
}
break;
default:
return false;
if (onDoubleTapDown == null &&
onDoubleTap == null &&
onDoubleTapCancel == null) {
return false;
}
}
return super.isPointerAllowed(event);
// If second tap is not allowed, reset the state.
final bool isPointerAllowed = super.isPointerAllowed(event);
if (isPointerAllowed == false) {
_reset();
}
return isPointerAllowed;
}
@override
......@@ -367,7 +372,6 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
}
void _checkUp(int buttons) {
assert(buttons == kPrimaryButton);
if (onDoubleTap != null) {
invokeCallback<void>('onDoubleTap', onDoubleTap!);
}
......@@ -492,6 +496,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
});
/// A pointer that might cause a tap has contacted the screen at a particular
......@@ -813,6 +818,7 @@ class SerialTapGestureRecognizer extends GestureRecognizer {
SerialTapGestureRecognizer({
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
});
/// A pointer has contacted the screen at a particular location, which might
......
......@@ -48,6 +48,11 @@ enum DragStartBehavior {
start,
}
/// Signature for `allowedButtonsFilter` in [GestureRecognizer].
/// Used to filter the input buttons of incoming pointer events.
/// The parameter `buttons` comes from [PointerEvent.buttons].
typedef AllowedButtonsFilter = bool Function(int buttons);
/// The base class that all gesture recognizers inherit from.
///
/// Provides a basic API that can be used by classes that work with
......@@ -79,8 +84,10 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
)
PointerDeviceKind? kind,
Set<PointerDeviceKind>? supportedDevices,
AllowedButtonsFilter? allowedButtonsFilter,
}) : assert(kind == null || supportedDevices == null),
_supportedDevices = kind == null ? supportedDevices : <PointerDeviceKind>{ kind };
_supportedDevices = kind == null ? supportedDevices : <PointerDeviceKind>{ kind },
_allowedButtonsFilter = allowedButtonsFilter ?? _defaultButtonAcceptBehavior;
/// The recognizer's owner.
///
......@@ -98,6 +105,29 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// tracked and recognized.
final Set<PointerDeviceKind>? _supportedDevices;
/// {@template flutter.gestures.multidrag._allowedButtonsFilter}
/// Called when interaction starts. This limits the dragging behavior
/// for custom clicks (such as scroll click). Its parameter comes
/// from [PointerEvent.buttons].
///
/// Due to how [kPrimaryButton], [kSecondaryButton], etc., use integers,
/// bitwise operations can help filter how buttons are pressed.
/// For example, if someone simultaneously presses the primary and secondary
/// buttons, the default behavior will return false. The following code
/// accepts any button press with primary:
/// `(int buttons) => buttons & kPrimaryButton != 0`.
///
/// When value is `(int buttons) => false`, allow no interactions.
/// When value is `(int buttons) => true`, allow all interactions.
///
/// Defaults to all buttons.
/// {@endtemplate}
final AllowedButtonsFilter _allowedButtonsFilter;
// The default value for [allowedButtonsFilter].
// Accept any input.
static bool _defaultButtonAcceptBehavior(int buttons) => true;
/// Holds a mapping between pointer IDs and the kind of devices they are
/// coming from.
final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};
......@@ -185,9 +215,9 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// 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 _supportedDevices == null || _supportedDevices!.contains(event.kind);
return (_supportedDevices == null ||
_supportedDevices!.contains(event.kind)) &&
_allowedButtonsFilter(event.buttons);
}
/// Handles a pointer pan/zoom being added that's not allowed by this recognizer.
......@@ -298,6 +328,7 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
});
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
......@@ -511,6 +542,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
}) : assert(
preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
'The preAcceptSlopTolerance must be positive or null',
......
......@@ -334,6 +334,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
)
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
this.dragStartBehavior = DragStartBehavior.down,
this.trackpadScrollCausesScale = false,
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
......
......@@ -149,7 +149,11 @@ abstract class BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer
/// Creates a tap gesture recognizer.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
BaseTapGestureRecognizer({ super.debugOwner, super.supportedDevices })
BaseTapGestureRecognizer({
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
})
: super(deadline: kPressTimeout);
bool _sentTapDown = false;
......@@ -354,6 +358,16 @@ abstract class BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer
/// one non-null `onTertiaryTap*` callback. If it has no callbacks, it is a
/// no-op.
///
/// {@template flutter.gestures.tap.TapGestureRecognizer.allowedButtonsFilter}
/// The [allowedButtonsFilter] argument only gives this recognizer the
/// ability to limit the buttons it accepts. It does not provide the
/// ability to recognize any buttons beyond the ones it already accepts:
/// kPrimaryButton, kSecondaryButton or kTertiaryButton. Therefore, a
/// combined value of `kPrimaryButton & kSecondaryButton` would be ignored,
/// but `kPrimaryButton | kSecondaryButton` would be allowed, as long as
/// only one of them is selected at a time.
/// {@endtemplate}
///
/// See also:
///
/// * [GestureDetector.onTap], which uses this recognizer.
......@@ -362,7 +376,11 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer {
/// Creates a tap gesture recognizer.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
TapGestureRecognizer({ super.debugOwner, super.supportedDevices });
TapGestureRecognizer({
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
});
/// {@template flutter.gestures.tap.TapGestureRecognizer.onTapDown}
/// A pointer has contacted the screen at a particular location with a primary
......
......@@ -179,6 +179,7 @@ class Draggable<T extends Object> extends StatefulWidget {
this.ignoringFeedbackPointer = true,
this.rootOverlay = false,
this.hitTestBehavior = HitTestBehavior.deferToChild,
this.allowedButtonsFilter,
}) : assert(child != null),
assert(feedback != null),
assert(ignoringFeedbackSemantics != null),
......@@ -359,6 +360,9 @@ class Draggable<T extends Object> extends StatefulWidget {
/// Defaults to [HitTestBehavior.deferToChild].
final HitTestBehavior hitTestBehavior;
/// {@macro flutter.gestures.multidrag._allowedButtonsFilter}
final AllowedButtonsFilter? allowedButtonsFilter;
/// Creates a gesture recognizer that recognizes the start of the drag.
///
/// Subclasses can override this function to customize when they start
......@@ -367,11 +371,11 @@ class Draggable<T extends Object> extends StatefulWidget {
MultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
switch (affinity) {
case Axis.horizontal:
return HorizontalMultiDragGestureRecognizer()..onStart = onStart;
return HorizontalMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
case Axis.vertical:
return VerticalMultiDragGestureRecognizer()..onStart = onStart;
return VerticalMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
case null:
return ImmediateMultiDragGestureRecognizer()..onStart = onStart;
return ImmediateMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
}
}
......@@ -409,6 +413,7 @@ class LongPressDraggable<T extends Object> extends Draggable<T> {
super.ignoringFeedbackSemantics,
super.ignoringFeedbackPointer,
this.delay = kLongPressTimeout,
super.allowedButtonsFilter,
});
/// Whether haptic feedback should be triggered on drag start.
......@@ -421,7 +426,7 @@ class LongPressDraggable<T extends Object> extends Draggable<T> {
@override
DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
return DelayedMultiDragGestureRecognizer(delay: delay)
return DelayedMultiDragGestureRecognizer(delay: delay, allowedButtonsFilter: allowedButtonsFilter)
..onStart = (Offset position) {
final Drag? result = onStart(position);
if (result != null && hapticFeedbackOnStart) {
......
......@@ -702,6 +702,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
super.debugOwner,
super.kind,
super.supportedDevices,
super.allowedButtonsFilter,
}) : _deadline = kPressTimeout,
dragStartBehavior = DragStartBehavior.start,
slopTolerance = kTouchSlop;
......
......@@ -152,6 +152,54 @@ void main() {
expect(doubleTapCanceled, isFalse);
});
testGesture('Should recognize double tap with secondaryButton', (GestureTester tester) {
final DoubleTapGestureRecognizer tapSecondary = DoubleTapGestureRecognizer(
allowedButtonsFilter: (int buttons) => buttons == kSecondaryButton,
);
tapSecondary.onDoubleTap = () {
doubleTapRecognized = true;
};
tapSecondary.onDoubleTapDown = (TapDownDetails details) {
doubleTapDownDetails = details;
};
tapSecondary.onDoubleTapCancel = () {
doubleTapCanceled = true;
};
// Down/up pair 7: normal tap sequence close to pair 6
const PointerDownEvent down7 = PointerDownEvent(
pointer: 7,
position: Offset(10.0, 10.0),
buttons: kSecondaryMouseButton,
);
const PointerUpEvent up7 = PointerUpEvent(
pointer: 7,
position: Offset(11.0, 9.0),
);
tapSecondary.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
expect(doubleTapDownDetails, isNull);
tester.async.elapse(const Duration(milliseconds: 100));
tapSecondary.addPointer(down7);
tester.closeArena(7);
expect(doubleTapDownDetails, isNotNull);
expect(doubleTapDownDetails!.globalPosition, down7.position);
expect(doubleTapDownDetails!.localPosition, down7.localPosition);
tester.route(down7);
expect(doubleTapRecognized, isFalse);
tester.route(up7);
expect(doubleTapRecognized, isTrue);
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapCanceled, isFalse);
});
testGesture('Inter-tap distance cancels double tap', (GestureTester tester) {
tap.addPointer(down1);
tester.closeArena(1);
......@@ -493,6 +541,56 @@ void main() {
expect(doubleTapCanceled, isFalse);
});
testGesture('Button change with allowedButtonsFilter should interrupt existing sequence', (GestureTester tester) {
final DoubleTapGestureRecognizer tapPrimary = DoubleTapGestureRecognizer(
allowedButtonsFilter: (int buttons) => buttons == kPrimaryButton,
);
tapPrimary.onDoubleTap = () {
doubleTapRecognized = true;
};
tapPrimary.onDoubleTapDown = (TapDownDetails details) {
doubleTapDownDetails = details;
};
tapPrimary.onDoubleTapCancel = () {
doubleTapCanceled = true;
};
// Down1 -> down6 (different button from 1) -> down2 (same button as 1)
// Down1 and down2 could've been a double tap, but is interrupted by down 6.
// Down6 gets ignored because it's not a primary button. Regardless, the state
// is reset.
const Duration interval = Duration(milliseconds: 100);
assert(interval * 2 < kDoubleTapTimeout);
assert(interval > kDoubleTapMinTime);
tapPrimary.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
tester.route(up1);
GestureBinding.instance.gestureArena.sweep(1);
tester.async.elapse(interval);
tapPrimary.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
tester.async.elapse(interval);
expect(doubleTapRecognized, isFalse);
tapPrimary.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(up2);
GestureBinding.instance.gestureArena.sweep(2);
expect(doubleTapRecognized, isFalse);
expect(doubleTapDownDetails, isNull);
expect(doubleTapCanceled, isFalse);
});
testGesture('Button change should start a valid sequence', (GestureTester tester) {
// Down6 -> down1 (different button from 6) -> down2 (same button as 1)
......@@ -624,6 +722,44 @@ void main() {
doubleTap.dispose();
});
testGesture('Buttons filter should cancel invalid taps', (GestureTester tester) {
final List<String> recognized = <String>[];
final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer(
allowedButtonsFilter: (int buttons) => false,
)
..onDoubleTap = () {
recognized.add('primary');
};
// Down/up pair 7: normal tap sequence close to pair 6
const PointerDownEvent down7 = PointerDownEvent(
pointer: 7,
position: Offset(10.0, 10.0),
);
const PointerUpEvent up7 = PointerUpEvent(
pointer: 7,
position: Offset(11.0, 9.0),
);
doubleTap.addPointer(down7);
tester.closeArena(7);
tester.route(down7);
tester.route(up7);
GestureBinding.instance.gestureArena.sweep(7);
tester.async.elapse(const Duration(milliseconds: 100));
doubleTap.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(up6);
expect(recognized, <String>[]);
recognized.clear();
doubleTap.dispose();
});
// Regression test for https://github.com/flutter/flutter/issues/73667
testGesture('Unfinished DoubleTap does not prevent competing Tap', (GestureTester tester) {
int tapCount = 0;
......
......@@ -78,6 +78,57 @@ void main() {
),
);
});
group('Recognizers on different button filters:', () {
final List<String> recognized = <String>[];
late HorizontalDragGestureRecognizer primaryRecognizer;
late HorizontalDragGestureRecognizer secondaryRecognizer;
setUp(() {
primaryRecognizer = HorizontalDragGestureRecognizer(
allowedButtonsFilter: (int buttons) => kPrimaryButton == buttons)
..onStart = (DragStartDetails details) {
recognized.add('onStartPrimary');
};
secondaryRecognizer = HorizontalDragGestureRecognizer(
allowedButtonsFilter: (int buttons) => kSecondaryButton == buttons)
..onStart = (DragStartDetails details) {
recognized.add('onStartSecondary');
};
});
tearDown(() {
recognized.clear();
primaryRecognizer.dispose();
secondaryRecognizer.dispose();
});
testGesture('Primary button works', (GestureTester tester) {
const PointerDownEvent down1 = PointerDownEvent(
pointer: 6,
position: Offset(10.0, 10.0),
);
primaryRecognizer.addPointer(down1);
secondaryRecognizer.addPointer(down1);
tester.closeArena(down1.pointer);
tester.route(down1);
expect(recognized, <String>['onStartPrimary']);
});
testGesture('Secondary button works', (GestureTester tester) {
const PointerDownEvent down1 = PointerDownEvent(
pointer: 6,
position: Offset(10.0, 10.0),
buttons: kSecondaryMouseButton,
);
primaryRecognizer.addPointer(down1);
secondaryRecognizer.addPointer(down1);
tester.closeArena(down1.pointer);
tester.route(down1);
expect(recognized, <String>['onStartSecondary']);
});
});
}
class MockHitTestTarget implements HitTestTarget {
......
......@@ -3195,6 +3195,48 @@ void main() {
expect(const LongPressDraggable<int>(feedback: widget2, child: widget1).feedback, widget2);
expect(LongPressDraggable<int>(feedback: widget2, dragAnchorStrategy: dummyStrategy, child: widget1).dragAnchorStrategy, dummyStrategy);
});
testWidgets('Test allowedButtonsFilter', (WidgetTester tester) async {
Widget build(bool Function(int buttons)? allowedButtonsFilter) {
return MaterialApp(
home: Draggable<int>(
key: UniqueKey(),
allowedButtonsFilter: allowedButtonsFilter,
feedback: const Text('Dragging'),
child: const Text('Source'),
),
);
}
await tester.pumpWidget(build(null));
final Offset firstLocation = tester.getCenter(find.text('Source'));
expect(find.text('Dragging'), findsNothing);
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
await tester.pump();
expect(find.text('Dragging'), findsOneWidget);
await gesture.up();
await tester.pumpWidget(build((int buttons) => buttons == kSecondaryButton));
expect(find.text('Dragging'), findsNothing);
final TestGesture gesture1 = await tester.startGesture(firstLocation, pointer: 8);
await tester.pump();
expect(find.text('Dragging'), findsNothing);
await gesture1.up();
await tester.pumpWidget(build((int buttons) => buttons & kTertiaryButton != 0 || buttons & kPrimaryButton != 0));
expect(find.text('Dragging'), findsNothing);
final TestGesture gesture2 = await tester.startGesture(firstLocation, pointer: 8);
await tester.pump();
expect(find.text('Dragging'), findsOneWidget);
await gesture2.up();
await tester.pumpWidget(build((int buttons) => false));
expect(find.text('Dragging'), findsNothing);
final TestGesture gesture3 = await tester.startGesture(firstLocation, pointer: 8);
await tester.pump();
expect(find.text('Dragging'), findsNothing);
await gesture3.up();
});
}
Future<void> _testLongPressDraggableHapticFeedback({ required WidgetTester tester, required bool hapticFeedbackOnStart, required int expectedHapticFeedbackCount }) async {
......
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