Skip to content

Commit 3e1aaf9

Browse files
fix(grid): support mouse drag range selection across cells in ResultGridView (Closes #904)
1 parent 34f07d6 commit 3e1aaf9

2 files changed

Lines changed: 421 additions & 9 deletions

File tree

‎lib/features/workspace/result_grid_view.dart‎

Lines changed: 256 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import 'dart:async';
12
import 'dart:convert';
23
import 'dart:math' as math;
34

45
import 'package:flutter/foundation.dart' as foundation;
5-
import 'package:flutter/gestures.dart' show kSecondaryMouseButton;
6+
import 'package:flutter/gestures.dart'
7+
show kPrimaryMouseButton, kSecondaryMouseButton;
68
import 'package:flutter/material.dart' as material;
79
import 'package:flutter/services.dart'
810
show Clipboard, ClipboardData, HardwareKeyboard, LogicalKeyboardKey;
@@ -705,6 +707,17 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
705707

706708
int _lastKnownStagedRowCount = -1;
707709

710+
bool _isPointerDown = false;
711+
bool _isDragSelecting = false;
712+
bool _hasJustDragSelected = false;
713+
Offset _pointerDownGlobalPos = Offset.zero;
714+
Offset _pointerDownLocalPos = Offset.zero;
715+
Offset? _lastPointerLocalPos;
716+
Timer? _autoScrollTimer;
717+
List<double> _currentDisplayOffsets = const [0];
718+
double _currentAvailableWidth = 0.0;
719+
double _currentRowsViewportHeight = 0.0;
720+
708721
@override
709722
void initState() {
710723
super.initState();
@@ -765,6 +778,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
765778

766779
@override
767780
void dispose() {
781+
_stopAutoScroll();
768782
widget.stagingBuffer?.removeListener(_onStagingBufferChanged);
769783
_horizontalController.removeListener(_onHorizontalScroll);
770784
_horizontalController.dispose();
@@ -1098,7 +1112,223 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
10981112
}
10991113
}
11001114

1115+
ResultGridCellCoordinate? _cellAtOffset({
1116+
required Offset localPosition,
1117+
required double rowHeight,
1118+
}) {
1119+
if (_sortedRows.isEmpty || widget.columns.isEmpty) return null;
1120+
final rowCount = _sortedRows.length;
1121+
final colCount = widget.columns.length;
1122+
1123+
final verticalOffset =
1124+
_verticalController.hasClients ? _verticalController.offset : 0.0;
1125+
final tableY = localPosition.dy + verticalOffset;
1126+
final rowIndex = (tableY / rowHeight).floor().clamp(0, rowCount - 1);
1127+
1128+
final tableX = localPosition.dx;
1129+
final offsets = _currentDisplayOffsets.length >= colCount + 1
1130+
? _currentDisplayOffsets
1131+
: _columnOffsets;
1132+
int colIndex = colCount - 1;
1133+
for (var i = 0; i < colCount; i++) {
1134+
if (i + 1 < offsets.length && tableX < offsets[i + 1]) {
1135+
colIndex = i;
1136+
break;
1137+
}
1138+
}
1139+
colIndex = colIndex.clamp(0, colCount - 1);
1140+
1141+
return ResultGridCellCoordinate(rowIndex, colIndex);
1142+
}
1143+
1144+
void _onGridPointerDown(
1145+
PointerDownEvent event, {
1146+
required double rowHeight,
1147+
}) {
1148+
if (event.buttons != kPrimaryMouseButton) return;
1149+
if (_sortedRows.isEmpty || widget.columns.isEmpty) return;
1150+
1151+
if (_editingCell != null) {
1152+
_cancelEdit();
1153+
}
1154+
1155+
_isPointerDown = true;
1156+
_isDragSelecting = false;
1157+
_hasJustDragSelected = false;
1158+
_pointerDownGlobalPos = event.position;
1159+
_pointerDownLocalPos = event.localPosition;
1160+
_lastPointerLocalPos = event.localPosition;
1161+
}
1162+
1163+
void _onGridPointerMove(
1164+
PointerMoveEvent event, {
1165+
required double rowHeight,
1166+
}) {
1167+
if (!_isPointerDown) return;
1168+
_lastPointerLocalPos = event.localPosition;
1169+
1170+
if (!_isDragSelecting) {
1171+
final delta = (event.position - _pointerDownGlobalPos).distance;
1172+
if (delta > 4.0) {
1173+
_isDragSelecting = true;
1174+
_hasJustDragSelected = false;
1175+
final startCell = _cellAtOffset(
1176+
localPosition: _pointerDownLocalPos,
1177+
rowHeight: rowHeight,
1178+
);
1179+
if (startCell != null) {
1180+
final isShift = HardwareKeyboard.instance.isShiftPressed;
1181+
if (!isShift || _selectionAnchor == null) {
1182+
_selectionAnchor = startCell;
1183+
}
1184+
_selectionFocus = startCell;
1185+
_selection = ResultGridSelection.fromPoints(
1186+
anchor: _selectionAnchor!,
1187+
focus: startCell,
1188+
);
1189+
_focusNode.requestFocus();
1190+
final modelRow = _toModelRowIndex(startCell.row);
1191+
widget.onRowSelected?.call(modelRow);
1192+
_notifySelectionAndFocus();
1193+
setState(() {});
1194+
}
1195+
_startAutoScroll(rowHeight);
1196+
}
1197+
}
1198+
1199+
if (_isDragSelecting) {
1200+
final currentCell = _cellAtOffset(
1201+
localPosition: event.localPosition,
1202+
rowHeight: rowHeight,
1203+
);
1204+
if (currentCell != null && currentCell != _selectionFocus) {
1205+
setState(() {
1206+
_selectionFocus = currentCell;
1207+
_selection = ResultGridSelection.fromPoints(
1208+
anchor: _selectionAnchor ?? currentCell,
1209+
focus: currentCell,
1210+
);
1211+
});
1212+
final modelRow = _toModelRowIndex(currentCell.row);
1213+
widget.onRowSelected?.call(modelRow);
1214+
_notifySelectionAndFocus();
1215+
}
1216+
}
1217+
}
1218+
1219+
void _onGridPointerUp(PointerUpEvent event) {
1220+
if (_isDragSelecting) {
1221+
_hasJustDragSelected = true;
1222+
WidgetsBinding.instance.addPostFrameCallback((_) {
1223+
_hasJustDragSelected = false;
1224+
});
1225+
}
1226+
_isPointerDown = false;
1227+
_isDragSelecting = false;
1228+
_stopAutoScroll();
1229+
}
1230+
1231+
void _onGridPointerCancel(PointerCancelEvent event) {
1232+
_isPointerDown = false;
1233+
_isDragSelecting = false;
1234+
_stopAutoScroll();
1235+
}
1236+
1237+
void _startAutoScroll(double rowHeight) {
1238+
_autoScrollTimer?.cancel();
1239+
_autoScrollTimer = Timer.periodic(const Duration(milliseconds: 20), (_) {
1240+
if (!_isDragSelecting || _lastPointerLocalPos == null || !mounted) {
1241+
_stopAutoScroll();
1242+
return;
1243+
}
1244+
_performAutoScrollAndSelection(rowHeight);
1245+
});
1246+
}
1247+
1248+
void _stopAutoScroll() {
1249+
_autoScrollTimer?.cancel();
1250+
_autoScrollTimer = null;
1251+
}
1252+
1253+
void _performAutoScrollAndSelection(double rowHeight) {
1254+
if (!mounted) {
1255+
_stopAutoScroll();
1256+
return;
1257+
}
1258+
final pos = _lastPointerLocalPos;
1259+
if (pos == null) return;
1260+
1261+
const edgeMargin = 32.0;
1262+
const maxVelocity = 15.0;
1263+
1264+
double dyScroll = 0.0;
1265+
if (pos.dy < edgeMargin) {
1266+
final factor = ((edgeMargin - pos.dy) / edgeMargin).clamp(0.0, 3.0);
1267+
dyScroll = -maxVelocity * factor;
1268+
} else if (pos.dy > _currentRowsViewportHeight - edgeMargin) {
1269+
final factor =
1270+
((pos.dy - (_currentRowsViewportHeight - edgeMargin)) / edgeMargin)
1271+
.clamp(0.0, 3.0);
1272+
dyScroll = maxVelocity * factor;
1273+
}
1274+
1275+
double dxScroll = 0.0;
1276+
final hOffset =
1277+
_horizontalController.hasClients ? _horizontalController.offset : 0.0;
1278+
final screenX = pos.dx - hOffset;
1279+
if (screenX < edgeMargin) {
1280+
final factor = ((edgeMargin - screenX) / edgeMargin).clamp(0.0, 3.0);
1281+
dxScroll = -maxVelocity * factor;
1282+
} else if (screenX > _currentAvailableWidth - edgeMargin) {
1283+
final factor =
1284+
((screenX - (_currentAvailableWidth - edgeMargin)) / edgeMargin)
1285+
.clamp(0.0, 3.0);
1286+
dxScroll = maxVelocity * factor;
1287+
}
1288+
1289+
if (dyScroll != 0.0 && _verticalController.hasClients) {
1290+
final targetY = (_verticalController.offset + dyScroll).clamp(
1291+
0.0,
1292+
_verticalController.position.maxScrollExtent,
1293+
);
1294+
if (targetY != _verticalController.offset) {
1295+
_verticalController.jumpTo(targetY);
1296+
}
1297+
}
1298+
1299+
if (dxScroll != 0.0 && _horizontalController.hasClients) {
1300+
final targetX = (_horizontalController.offset + dxScroll).clamp(
1301+
0.0,
1302+
_horizontalController.position.maxScrollExtent,
1303+
);
1304+
if (targetX != _horizontalController.offset) {
1305+
_horizontalController.jumpTo(targetX);
1306+
}
1307+
}
1308+
1309+
final currentCell = _cellAtOffset(
1310+
localPosition: pos,
1311+
rowHeight: rowHeight,
1312+
);
1313+
if (currentCell != null && currentCell != _selectionFocus) {
1314+
setState(() {
1315+
_selectionFocus = currentCell;
1316+
_selection = ResultGridSelection.fromPoints(
1317+
anchor: _selectionAnchor ?? currentCell,
1318+
focus: currentCell,
1319+
);
1320+
});
1321+
final modelRow = _toModelRowIndex(currentCell.row);
1322+
widget.onRowSelected?.call(modelRow);
1323+
_notifySelectionAndFocus();
1324+
}
1325+
}
1326+
11011327
void _onCellTap(int row, int column, {bool isShift = false}) {
1328+
if (_isDragSelecting || _hasJustDragSelected) {
1329+
_hasJustDragSelected = false;
1330+
return;
1331+
}
11021332
_focusNode.requestFocus();
11031333
final modelRow = _toModelRowIndex(row);
11041334
widget.onRowSelected?.call(modelRow);
@@ -1498,8 +1728,6 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
14981728
_widthsNeedUpdate = false;
14991729
}
15001730
final cs = Theme.of(context).colorScheme;
1501-
final rowHeight = _scaledRowHeight(context);
1502-
final headerHeight = _scaledHeaderHeight(context);
15031731

15041732
return material.CallbackShortcuts(
15051733
bindings: {
@@ -1780,6 +2008,12 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
17802008
child: material.LayoutBuilder(
17812009
builder: (context, constraints) {
17822010
final availableWidth = constraints.maxWidth;
2011+
final headerHeight = _scaledHeaderHeight(context);
2012+
final rowHeight = _scaledRowHeight(context);
2013+
final rowsViewportHeight =
2014+
math.max(0.0, constraints.maxHeight - headerHeight);
2015+
_currentAvailableWidth = availableWidth;
2016+
_currentRowsViewportHeight = rowsViewportHeight;
17832017

17842018
var displayWidths = _columnWidths;
17852019
var tableWidth = _tableWidth;
@@ -1801,6 +2035,10 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
18012035
tableWidth = availableWidth;
18022036
}
18032037

2038+
_currentDisplayOffsets = identical(displayWidths, _columnWidths)
2039+
? _columnOffsets
2040+
: computeResultGridColumnOffsets(displayWidths);
2041+
18042042
final window = _columnWindow(displayWidths, availableWidth);
18052043

18062044
return material.Scrollbar(
@@ -1831,11 +2069,19 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
18312069
child: material.Scrollbar(
18322070
controller: _verticalController,
18332071
thumbVisibility: true,
1834-
child: material.ListView.builder(
1835-
controller: _verticalController,
1836-
itemCount: _sortedRows.length,
1837-
itemExtent: rowHeight,
1838-
itemBuilder: (context, rowIndex) {
2072+
child: material.Listener(
2073+
behavior: material.HitTestBehavior.translucent,
2074+
onPointerDown: (e) =>
2075+
_onGridPointerDown(e, rowHeight: rowHeight),
2076+
onPointerMove: (e) =>
2077+
_onGridPointerMove(e, rowHeight: rowHeight),
2078+
onPointerUp: _onGridPointerUp,
2079+
onPointerCancel: _onGridPointerCancel,
2080+
child: material.ListView.builder(
2081+
controller: _verticalController,
2082+
itemCount: _sortedRows.length,
2083+
itemExtent: rowHeight,
2084+
itemBuilder: (context, rowIndex) {
18392085
final row = _sortedRows[rowIndex];
18402086
final isEven = rowIndex.isEven;
18412087
return _DataRow(
@@ -1874,7 +2120,8 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
18742120
),
18752121
),
18762122
),
1877-
],
2123+
),
2124+
],
18782125
),
18792126
),
18802127
),

0 commit comments

Comments
 (0)