Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 37 additions & 18 deletions lib/features/workspace/grid/grid_pointer_selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,22 @@ part of '../result_grid_view.dart';

/// Pointer, drag and tap selection for [VirtualResultGrid].
extension _GridPointerSelection on _VirtualResultGridState {
void _notifySelectionAndFocus() {
void _notifySelectionAndFocus({bool deferValues = false}) {
if (widget.onSelectionValuesChanged != null) {
if (_selection == null) {
_selectionValuesDebounce?.cancel();
widget.onSelectionValuesChanged!(const []);
} else if (deferValues) {
// During active drag gestures, defer allocating the multi-cell
// String list until the pointer stops moving or the drag ends (#1348).
_selectionValuesDebounce?.cancel();
_selectionValuesDebounce = Timer(const Duration(milliseconds: 40), () {
if (!mounted) return;
_dispatchSelectionValues();
});
} else {
final rows = _sortedRows;
final values = <String>[];
for (var r = _selection!.startRow; r <= _selection!.endRow; r++) {
if (r >= 0 && r < rows.length) {
for (var c = _selection!.startColumn;
c <= _selection!.endColumn;
c++) {
if (c >= 0 && c < rows[r].length) {
values.add(_maskedValue(c, rows[r][c]));
}
}
}
}
widget.onSelectionValuesChanged!(values);
_selectionValuesDebounce?.cancel();
_dispatchSelectionValues();
}
}

Expand All @@ -39,6 +36,24 @@ extension _GridPointerSelection on _VirtualResultGridState {
}
}

void _dispatchSelectionValues() {
if (widget.onSelectionValuesChanged == null || _selection == null) return;
final rows = _sortedRows;
final values = <String>[];
for (var r = _selection!.startRow; r <= _selection!.endRow; r++) {
if (r >= 0 && r < rows.length) {
for (var c = _selection!.startColumn;
c <= _selection!.endColumn;
c++) {
if (c >= 0 && c < rows[r].length) {
values.add(_maskedValue(c, rows[r][c]));
}
}
}
}
widget.onSelectionValuesChanged!(values);
}

ResultGridCellCoordinate? _cellAtOffset({
required Offset localPosition,
required double rowHeight,
Expand Down Expand Up @@ -127,7 +142,7 @@ extension _GridPointerSelection on _VirtualResultGridState {
_focusNode.requestFocus();
final modelRow = _toModelRowIndex(startCell.row);
widget.onRowSelected?.call(modelRow);
_notifySelectionAndFocus();
_notifySelectionAndFocus(deferValues: true);
setState(() {});
}
_startAutoScroll(rowHeight);
Expand All @@ -149,7 +164,7 @@ extension _GridPointerSelection on _VirtualResultGridState {
});
final modelRow = _toModelRowIndex(currentCell.row);
widget.onRowSelected?.call(modelRow);
_notifySelectionAndFocus();
_notifySelectionAndFocus(deferValues: true);
}
}
}
Expand All @@ -160,6 +175,7 @@ extension _GridPointerSelection on _VirtualResultGridState {
}) {
if (_isDragSelecting) {
_hasJustDragSelected = true;
_notifySelectionAndFocus(deferValues: false);
WidgetsBinding.instance.addPostFrameCallback((_) {
_hasJustDragSelected = false;
});
Expand Down Expand Up @@ -197,6 +213,9 @@ extension _GridPointerSelection on _VirtualResultGridState {
}

void _onGridPointerCancel(PointerCancelEvent event) {
if (_isDragSelecting) {
_notifySelectionAndFocus(deferValues: false);
}
_isPointerDown = false;
_isDragSelecting = false;
_stopAutoScroll();
Expand Down Expand Up @@ -288,7 +307,7 @@ extension _GridPointerSelection on _VirtualResultGridState {
});
final modelRow = _toModelRowIndex(currentCell.row);
widget.onRowSelected?.call(modelRow);
_notifySelectionAndFocus();
_notifySelectionAndFocus(deferValues: true);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lib/features/workspace/result_grid_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
DateTime? _lastTapUpTime;
ResultGridCellCoordinate? _lastTapCell;
Timer? _autoScrollTimer;
Timer? _selectionValuesDebounce;
List<double> _currentDisplayOffsets = const [0];
double _currentAvailableWidth = 0.0;

Expand Down Expand Up @@ -260,6 +261,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {

@override
void dispose() {
_selectionValuesDebounce?.cancel();
PiiMaskingController.instance.removeListener(_onPiiMaskingChanged);
_stopAutoScroll();
_rowWidgets.clear();
Expand Down
53 changes: 53 additions & 0 deletions test/features/workspace/result_grid_drag_selection_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,59 @@ void main() {
expect(selectedValues, ['1', 'Alice', '2', 'Bob']);
});

testWidgets('does not eagerly allocate and dispatch selection values on every intermediate drag move', (tester) async {
var notifications = 0;
List<String>? lastValues;

await tester.pumpWidget(
_testShell(
child: material.SizedBox(
width: 800,
height: 400,
child: VirtualResultGrid(
columns: const ['id', 'name', 'role'],
rows: const [
['1', 'Alice', 'Admin'],
['2', 'Bob', 'User'],
['3', 'Charlie', 'Manager'],
],
onSelectionValuesChanged: (vals) {
notifications++;
lastValues = vals;
},
),
),
),
);
await tester.pumpAndSettle();

final startPos = tester.getCenter(find.text('1'));
final cellAlice = tester.getCenter(find.text('Alice'));
final cellBob = tester.getCenter(find.text('Bob'));

final gesture = await tester.startGesture(startPos, kind: PointerDeviceKind.mouse);
await tester.pump();
final initialNotifications = notifications;

// Move across Alice without releasing pointer or waiting for debounce
await gesture.moveTo(cellAlice);
await tester.pump();

// Move across Bob without releasing pointer
await gesture.moveTo(cellBob);
await tester.pump();

// Intermediate drag-move steps must not trigger synchronous value list extractions (#1348)
expect(notifications, equals(initialNotifications));

// Releasing pointer flushes final selection values immediately
await gesture.up();
await tester.pump();

expect(notifications, greaterThan(initialNotifications));
expect(lastValues, ['1', 'Alice', '2', 'Bob']);
});

testWidgets('supports reverse drag selection (bottom-right to top-left)', (tester) async {
List<String>? selectedValues;

Expand Down
Loading