-
Notifications
You must be signed in to change notification settings - Fork 392
Expand file tree
/
Copy path_flat_table.dart
More file actions
368 lines (327 loc) · 12.9 KB
/
_flat_table.dart
File metadata and controls
368 lines (327 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
// Copyright 2019 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
part of 'table.dart';
/// A [FlatTable] widget that is searchable.
///
/// The table requires a [SearchController], which is responsible for feeding
/// information about search matches and the active search match to the table.
///
/// This table will automatically refresh search matches on the
/// [SearchController] after sort operations that are triggered from the table.
class SearchableFlatTable<T extends SearchableDataMixin> extends FlatTable<T> {
SearchableFlatTable({
super.key,
required SearchControllerMixin<T> searchController,
required super.keyFactory,
required super.data,
required super.dataKey,
required super.columns,
required super.defaultSortColumn,
required super.defaultSortDirection,
super.secondarySortColumn,
super.sortOriginalData = false,
super.pinBehavior = FlatTablePinBehavior.none,
super.columnGroups,
super.autoScrollContent = false,
super.startScrolledAtBottom = false,
super.onItemSelected,
super.preserveVerticalScrollPosition = false,
super.includeColumnGroupHeaders = true,
super.sizeColumnsToFit = true,
super.rowHeight,
super.selectionNotifier,
super.tallHeaders,
}) : super(
searchMatchesNotifier: searchController.searchMatches,
activeSearchMatchNotifier: searchController.activeSearchMatch,
onDataSorted: () => WidgetsBinding.instance.addPostFrameCallback((_) {
// This needs to be in a post frame callback so that the search
// matches are not updated in the middle of a table build.
searchController.refreshSearchMatches();
}),
);
}
/// A table that displays in a collection of [data], based on a collection of
/// [ColumnData].
///
/// The [ColumnData] gives this table information about how to size its columns,
/// and how to present each row of `data`.
class FlatTable<T> extends StatefulWidget {
FlatTable({
super.key,
required this.keyFactory,
required this.data,
required this.dataKey,
required this.columns,
this.columnGroups,
this.autoScrollContent = false,
this.startScrolledAtBottom = false,
this.onItemSelected,
required this.defaultSortColumn,
required this.defaultSortDirection,
this.onDataSorted,
this.sortOriginalData = false,
this.pinBehavior = FlatTablePinBehavior.none,
this.secondarySortColumn,
this.searchMatchesNotifier,
this.activeSearchMatchNotifier,
this.preserveVerticalScrollPosition = false,
this.includeColumnGroupHeaders = true,
this.tallHeaders = false,
this.sizeColumnsToFit = true,
this.rowHeight,
this.headerColor,
this.fillWithEmptyRows = false,
this.enableHoverHandling = false,
ValueNotifier<T?>? selectionNotifier,
}) : selectionNotifier = selectionNotifier ?? ValueNotifier<T?>(null);
/// List of columns to display.
///
/// These [ColumnData] elements should be defined as static
/// OR if they cannot be defined as static,
/// they should not manage stateful data.
///
/// [FlatTableState.didUpdateWidget] checks if the columns have
/// changed before re-initializing the table controller,
/// and the columns are compared by title only.
/// See also [FlatTableState. _tableConfigurationChanged].
final List<ColumnData<T>> columns;
final List<ColumnGroup>? columnGroups;
/// Whether the columns for this table should be sized so that the entire
/// table fits in view (e.g. so that there is no horizontal scrolling).
final bool sizeColumnsToFit;
final double? rowHeight;
// TODO(kenz): should we enable this behavior by default? Does it ever matter
// to preserve the order of the original data passed to a flat table?
/// Whether table sorting should sort the original data list instead of
/// creating a copy.
final bool sortOriginalData;
/// Determines if the headers for column groups should be rendered.
///
/// If set to false and `columnGroups` is non-null and non-empty, only
/// the vertical dividing lines will be drawn for each column group boundary.
final bool includeColumnGroupHeaders;
/// Whether the table headers should be slightly taller than the table rows to
/// support multiline text.
final bool tallHeaders;
/// The background color of the header.
///
/// If null, defaults to `Theme.of(context).canvasColor`.
final Color? headerColor;
/// Whether to fill the table with empty rows.
final bool fillWithEmptyRows;
/// Whether to enable hover handling.
final bool enableHoverHandling;
/// Data set to show as rows in this table.
final List<T> data;
/// Unique key for the data shown in this table.
///
/// This key will be used to restore things like sort column, sort direction,
/// and scroll position for this table (when [preserveVerticalScrollPosition]
/// is true).
///
/// We use [TableUiStateStore] to store [TableUiState] by this key so that we
/// can save and restore this state without having to keep [State] or table
/// controller objects alive.
final String dataKey;
/// Auto-scrolling the table to keep new content visible.
final bool autoScrollContent;
/// Determines whether the table should be scrolled to the bottom of the
/// scrollable area on the initial build of the table.
final bool startScrolledAtBottom;
/// Factory that creates keys for each row in this table.
final Key Function(T data) keyFactory;
/// Callback that, when non-null, will be called on each table row selection.
final ItemSelectedCallback<T?>? onItemSelected;
/// Determines how elements that request to be pinned are displayed.
///
/// Defaults to [FlatTablePinBehavior.none], which disables pinning.
final FlatTablePinBehavior pinBehavior;
/// The default sort column for this table.
///
/// This sort column is passed along to [TreeTableState.tableController],
/// which uses [defaultSortColumn] for the starting value in
/// [TableControllerBase.tableUiState].
final ColumnData<T> defaultSortColumn;
/// The default [SortDirection] for this table.
///
/// This [SortDirection] is passed along to [TreeTableState.tableController],
/// which uses [defaultSortDirection] for the starting value in
/// [TableControllerBase.tableUiState].
final SortDirection defaultSortDirection;
/// The secondary sort column to be used in the sorting algorithm provided by
/// [TableControllerBase.sortDataAndNotify].
final ColumnData<T>? secondarySortColumn;
/// Callback that will be called after each table sort operation.
final VoidCallback? onDataSorted;
/// Notifies with the list of data items that should be marked as search
/// matches.
final ValueListenable<List<T>>? searchMatchesNotifier;
/// Notifies with the data item that should be marked as the active search
/// match.
final ValueListenable<T?>? activeSearchMatchNotifier;
/// Stores the selected data item (the selected row) for this table.
///
/// This notifier's value will be updated when a row of the table is selected.
final ValueNotifier<T?> selectionNotifier;
/// Whether the vertical scroll position for this table should be preserved for
/// each data set.
///
/// This should be set to true if the table is not disposed and completely
/// rebuilt when changing from one data set to another.
final bool preserveVerticalScrollPosition;
@override
FlatTableState<T> createState() => FlatTableState<T>();
}
@visibleForTesting
class FlatTableState<T> extends State<FlatTable<T>> with AutoDisposeMixin {
FlatTableController<T> get tableController => _tableController!;
FlatTableController<T>? _tableController;
@override
void initState() {
super.initState();
_setUpTableController();
addAutoDisposeListener(tableController.tableData);
if (tableController.pinBehavior != FlatTablePinBehavior.none &&
this is! State<FlatTable<PinnableListEntry>>) {
throw StateError('$T must implement PinnableListEntry');
}
}
@override
void didUpdateWidget(FlatTable<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (_tableConfigurationChanged(oldWidget, widget)) {
_setUpTableController();
} else if (!collectionEquals(oldWidget.data, widget.data)) {
_setUpTableController(reset: false);
}
}
@override
void dispose() {
_tableController?.dispose();
_tableController = null;
super.dispose();
}
/// Sets up the [tableController] for the property values in [widget].
///
/// [reset] determines whether or not we should re-initialize
/// [_tableController], which should only happen when the core table
/// configuration (columns & column groups) has changed.
///
/// See [_tableConfigurationChanged].
void _setUpTableController({bool reset = true}) {
final shouldResetController = reset || _tableController == null;
if (shouldResetController) {
_tableController = FlatTableController<T>(
columns: widget.columns,
defaultSortColumn: widget.defaultSortColumn,
defaultSortDirection: widget.defaultSortDirection,
secondarySortColumn: widget.secondarySortColumn,
columnGroups: widget.columnGroups,
includeColumnGroupHeaders: widget.includeColumnGroupHeaders,
pinBehavior: widget.pinBehavior,
sizeColumnsToFit: widget.sizeColumnsToFit,
sortOriginalData: widget.sortOriginalData,
onDataSorted: widget.onDataSorted,
);
}
if (widget.preserveVerticalScrollPosition) {
// Order matters - this must be called before [tableController.setData]
tableController.storeScrollPosition();
}
tableController
..setData(widget.data, widget.dataKey)
..pinBehavior = widget.pinBehavior;
}
/// Whether the core table configuration has changed, determined by checking
/// the equality of columns and column groups.
bool _tableConfigurationChanged(
FlatTable<T> oldWidget,
FlatTable<T> newWidget,
) {
final columnsChanged =
!collectionEquals(
oldWidget.columns.map((c) => c.config),
newWidget.columns.map((c) => c.config),
) ||
!collectionEquals(
oldWidget.columnGroups?.map((c) => c.title),
newWidget.columnGroups?.map((c) => c.title),
);
return columnsChanged;
}
@override
Widget build(BuildContext context) {
Widget buildTable(List<double> columnWidths) => DevToolsTable<T>(
tableController: tableController,
columnWidths: columnWidths,
autoScrollContent: widget.autoScrollContent,
startScrolledAtBottom: widget.startScrolledAtBottom,
rowBuilder: _buildRow,
activeSearchMatchNotifier: widget.activeSearchMatchNotifier,
rowItemExtent: widget.rowHeight ?? defaultRowHeight,
preserveVerticalScrollPosition: widget.preserveVerticalScrollPosition,
tallHeaders: widget.tallHeaders,
headerColor: widget.headerColor,
fillWithEmptyRows: widget.fillWithEmptyRows,
enableHoverHandling: widget.enableHoverHandling,
);
if (tableController.columnWidths == null) {
return LayoutBuilder(
builder: (context, constraints) => buildTable(
tableController.computeColumnWidthsSizeToFit(constraints.maxWidth),
),
);
}
return buildTable(tableController.columnWidths!);
}
Widget _buildRow({
required BuildContext context,
required int index,
required List<double> columnWidths,
required bool isPinned,
required bool enableHoverHandling,
}) {
final pinnedData = tableController.pinnedData;
final data = isPinned ? pinnedData : tableController.tableData.value.data;
if (index >= data.length) {
return TableRow<T>.filler(
columns: tableController.columns,
columnGroups: tableController.columnGroups,
columnWidths: columnWidths,
backgroundColor: alternatingColorForIndex(
index,
Theme.of(context).colorScheme,
),
);
}
final node = data[index];
return ValueListenableBuilder<T?>(
valueListenable: widget.selectionNotifier,
builder: (context, selected, _) {
return TableRow<T>(
key: widget.keyFactory(node),
node: node,
onPressed: (T? selection) {
widget.selectionNotifier.value = selection;
if (widget.onItemSelected != null && selection != null) {
widget.onItemSelected!(selection);
}
},
columns: tableController.columns,
columnGroups: tableController.columnGroups,
columnWidths: columnWidths,
backgroundColor: alternatingColorForIndex(
index,
Theme.of(context).colorScheme,
),
isSelected: node != null && node == selected,
searchMatchesNotifier: widget.searchMatchesNotifier,
activeSearchMatchNotifier: widget.activeSearchMatchNotifier,
enableHoverHandling: enableHoverHandling,
);
},
);
}
}