1+ import 'dart:async' ;
12import 'dart:convert' ;
23import 'dart:math' as math;
34
45import 'package:flutter/foundation.dart' as foundation;
5- import 'package:flutter/gestures.dart' show kSecondaryMouseButton;
6+ import 'package:flutter/gestures.dart'
7+ show kPrimaryMouseButton, kSecondaryMouseButton;
68import 'package:flutter/material.dart' as material;
79import '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