Add arrow-key controls to the guessing question tile.
Up/down step the answer by one; hold left/right to defer or save like the edge hold buttons. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e64d21dc2e
commit
4035684b4d
@ -18,6 +18,8 @@ enum _LockedDragAxis { horizontal, vertical, rejected }
|
|||||||
/// Swipeable question card: right submits, left defers (does not resolve).
|
/// Swipeable question card: right submits, left defers (does not resolve).
|
||||||
///
|
///
|
||||||
/// The center band supports vertical drag as a slider from -10 (down) to 10 (up).
|
/// The center band supports vertical drag as a slider from -10 (down) to 10 (up).
|
||||||
|
/// Arrow up/down step the value by one; hold arrow left/right to defer or save
|
||||||
|
/// (same as the edge hold buttons).
|
||||||
///
|
///
|
||||||
/// Pan gestures accumulate until [swipeAxisToleranceDegrees] identifies a
|
/// Pan gestures accumulate until [swipeAxisToleranceDegrees] identifies a
|
||||||
/// cardinal axis; diagonal swipes are ignored.
|
/// cardinal axis; diagonal swipes are ignored.
|
||||||
@ -54,7 +56,10 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
bool _acting = false;
|
bool _acting = false;
|
||||||
bool _holdSavePointerDown = false;
|
bool _holdSavePointerDown = false;
|
||||||
bool _holdDeferPointerDown = false;
|
bool _holdDeferPointerDown = false;
|
||||||
|
bool _keyboardHoldSave = false;
|
||||||
|
bool _keyboardHoldDefer = false;
|
||||||
int _lastSnappedValue = 0;
|
int _lastSnappedValue = 0;
|
||||||
|
late final FocusNode _focusNode;
|
||||||
late final AnimationController _snapController;
|
late final AnimationController _snapController;
|
||||||
late final AnimationController _holdSaveController;
|
late final AnimationController _holdSaveController;
|
||||||
late final AnimationController _holdDeferController;
|
late final AnimationController _holdDeferController;
|
||||||
@ -90,6 +95,7 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_focusNode = FocusNode()..addListener(_onFocusChange);
|
||||||
_snapController = AnimationController(
|
_snapController = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 140),
|
duration: const Duration(milliseconds: 140),
|
||||||
@ -106,12 +112,27 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_focusNode.dispose();
|
||||||
_snapController.dispose();
|
_snapController.dispose();
|
||||||
_holdSaveController.dispose();
|
_holdSaveController.dispose();
|
||||||
_holdDeferController.dispose();
|
_holdDeferController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onFocusChange() {
|
||||||
|
if (_focusNode.hasFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_keyboardHoldSave) {
|
||||||
|
_keyboardHoldSave = false;
|
||||||
|
_endHoldSave();
|
||||||
|
}
|
||||||
|
if (_keyboardHoldDefer) {
|
||||||
|
_keyboardHoldDefer = false;
|
||||||
|
_endHoldDefer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _syncHoldSaveDrag() {
|
void _syncHoldSaveDrag() {
|
||||||
if (!mounted || _acting || _holdDeferActive) {
|
if (!mounted || _acting || _holdDeferActive) {
|
||||||
return;
|
return;
|
||||||
@ -168,7 +189,7 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onHoldSavePointerDown(PointerDownEvent event) {
|
void _beginHoldSave() {
|
||||||
if (!_holdSaveEnabled || _holdDeferActive) {
|
if (!_holdSaveEnabled || _holdDeferActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -176,7 +197,7 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
_holdSaveController.forward(from: _holdSaveController.value);
|
_holdSaveController.forward(from: _holdSaveController.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onHoldSavePointerUp(PointerEvent event) {
|
void _endHoldSave() {
|
||||||
if (!_holdSavePointerDown) {
|
if (!_holdSavePointerDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -188,6 +209,14 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
unawaited(_holdSaveController.reverse());
|
unawaited(_holdSaveController.reverse());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onHoldSavePointerDown(PointerDownEvent event) {
|
||||||
|
_beginHoldSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onHoldSavePointerUp(PointerEvent event) {
|
||||||
|
_endHoldSave();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _completeHoldSave() async {
|
Future<void> _completeHoldSave() async {
|
||||||
if (_acting || widget.busy || _atZero || !mounted) {
|
if (_acting || widget.busy || _atZero || !mounted) {
|
||||||
return;
|
return;
|
||||||
@ -207,7 +236,7 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onHoldDeferPointerDown(PointerDownEvent event) {
|
void _beginHoldDefer() {
|
||||||
if (!_holdDeferEnabled || _holdSaveActive) {
|
if (!_holdDeferEnabled || _holdSaveActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -215,7 +244,7 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
_holdDeferController.forward(from: _holdDeferController.value);
|
_holdDeferController.forward(from: _holdDeferController.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onHoldDeferPointerUp(PointerEvent event) {
|
void _endHoldDefer() {
|
||||||
if (!_holdDeferPointerDown) {
|
if (!_holdDeferPointerDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -227,6 +256,14 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
unawaited(_holdDeferController.reverse());
|
unawaited(_holdDeferController.reverse());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onHoldDeferPointerDown(PointerDownEvent event) {
|
||||||
|
_beginHoldDefer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onHoldDeferPointerUp(PointerEvent event) {
|
||||||
|
_endHoldDefer();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _completeHoldDefer() async {
|
Future<void> _completeHoldDefer() async {
|
||||||
if (_acting || widget.busy || !mounted) {
|
if (_acting || widget.busy || !mounted) {
|
||||||
return;
|
return;
|
||||||
@ -400,15 +437,12 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One wheel notch / trackpad tick → one integer on the [-10, 10] scale.
|
/// Steps the slider by [delta] integers, clamped to [-10, 10].
|
||||||
void _stepSliderFromScroll(double scrollDy) {
|
void _stepSliderBy(int delta) {
|
||||||
if (scrollDy == 0) {
|
if (widget.busy || _acting || delta == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final int next = (_snappedSliderValue + (scrollDy > 0 ? -1 : 1)).clamp(
|
final int next = (_snappedSliderValue + delta).clamp(_sliderMin, _sliderMax);
|
||||||
_sliderMin,
|
|
||||||
_sliderMax,
|
|
||||||
);
|
|
||||||
if (next == _snappedSliderValue) {
|
if (next == _snappedSliderValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -421,6 +455,57 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One wheel notch / trackpad tick → one integer on the [-10, 10] scale.
|
||||||
|
void _stepSliderFromScroll(double scrollDy) {
|
||||||
|
if (scrollDy == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_stepSliderBy(scrollDy > 0 ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
||||||
|
if (widget.busy || _acting) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final LogicalKeyboardKey key = event.logicalKey;
|
||||||
|
|
||||||
|
if (key == LogicalKeyboardKey.arrowUp ||
|
||||||
|
key == LogicalKeyboardKey.arrowDown) {
|
||||||
|
if (event is KeyUpEvent) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
if (event is KeyDownEvent || event is KeyRepeatEvent) {
|
||||||
|
_stepSliderBy(key == LogicalKeyboardKey.arrowUp ? 1 : -1);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == LogicalKeyboardKey.arrowRight) {
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
_keyboardHoldSave = true;
|
||||||
|
_beginHoldSave();
|
||||||
|
} else if (event is KeyUpEvent && _keyboardHoldSave) {
|
||||||
|
_keyboardHoldSave = false;
|
||||||
|
_endHoldSave();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == LogicalKeyboardKey.arrowLeft) {
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
_keyboardHoldDefer = true;
|
||||||
|
_beginHoldDefer();
|
||||||
|
} else if (event is KeyUpEvent && _keyboardHoldDefer) {
|
||||||
|
_keyboardHoldDefer = false;
|
||||||
|
_endHoldDefer();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
void _accumulateScrollDelta(double dy) {
|
void _accumulateScrollDelta(double dy) {
|
||||||
if (widget.busy || _acting || dy == 0) {
|
if (widget.busy || _acting || dy == 0) {
|
||||||
return;
|
return;
|
||||||
@ -510,10 +595,14 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
40,
|
40,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Stack(
|
return Focus(
|
||||||
alignment: Alignment.center,
|
focusNode: _focusNode,
|
||||||
clipBehavior: Clip.none,
|
autofocus: true,
|
||||||
children: <Widget>[
|
onKeyEvent: _onKeyEvent,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: <Widget>[
|
||||||
Transform.translate(
|
Transform.translate(
|
||||||
offset: Offset(_dragOffset, 0),
|
offset: Offset(_dragOffset, 0),
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
@ -660,7 +749,8 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user