import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
part 'data_table_2_resizable.dart';
bool dataTableShowLogs = true;
enum ColumnSize { S, M, L }
@immutable
class DataColumn2 extends DataColumn {
const DataColumn2(
{required super.label,
super.tooltip,
super.numeric = false,
super.onSort,
this.headingRowAlignment,
this.size = ColumnSize.M,
this.fixedWidth,
this.minWidth,
this.isResizable = false});
final MainAxisAlignment? headingRowAlignment;
final ColumnSize size;
final double? fixedWidth;
final double? minWidth;
final bool isResizable;
}
@immutable
class DataRow2 extends DataRow {
const DataRow2(
{super.key,
super.selected = false,
super.onSelectChanged,
super.color,
this.decoration,
required super.cells,
this.specificRowHeight,
this.onTap,
this.onDoubleTap,
super.onLongPress,
this.onSecondaryTap,
this.onSecondaryTapDown});
DataRow2.byIndex(
{super.index,
super.selected = false,
super.onSelectChanged,
super.color,
this.decoration,
required super.cells,
this.specificRowHeight,
this.onTap,
this.onDoubleTap,
super.onLongPress,
this.onSecondaryTap,
this.onSecondaryTapDown})
: super.byIndex();
DataRow2 clone({
LocalKey? key,
bool? selected,
ValueChanged<bool?>? onSelectChanged,
WidgetStateProperty<Color?>? color,
Decoration? decoration,
List<DataCell>? cells,
double? specificRowHeight,
GestureTapCallback? onTap,
GestureTapCallback? onDoubleTap,
GestureLongPressCallback? onLongPress,
GestureTapCallback? onSecondaryTap,
GestureTapDownCallback? onSecondaryTapDown,
}) {
return DataRow2(
key: key ?? this.key,
selected: selected ?? this.selected,
onSelectChanged: onSelectChanged ?? this.onSelectChanged,
color: color ?? this.color,
decoration: decoration ?? this.decoration,
cells: cells ?? this.cells,
specificRowHeight: specificRowHeight ?? this.specificRowHeight,
onTap: onTap ?? this.onTap,
onDoubleTap: onDoubleTap ?? this.onDoubleTap,
onLongPress: onLongPress ?? this.onLongPress,
onSecondaryTap: onSecondaryTap ?? this.onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown ?? this.onSecondaryTapDown,
);
}
final Decoration? decoration;
final double? specificRowHeight;
final GestureTapCallback? onTap;
final GestureTapCallback? onSecondaryTap;
final GestureTapDownCallback? onSecondaryTapDown;
final GestureTapCallback? onDoubleTap;
}
class DataTable2 extends DataTable {
DataTable2({
super.key,
required super.columns,
super.sortColumnIndex,
super.sortAscending = true,
super.onSelectAll,
super.decoration,
super.dataRowColor,
this.dataRowHeight,
super.dataTextStyle,
super.headingRowColor,
this.fixedColumnsColor,
this.fixedCornerColor,
super.headingRowHeight,
super.headingTextStyle,
this.headingCheckboxTheme,
this.datarowCheckboxTheme,
super.horizontalMargin,
super.checkboxHorizontalMargin,
this.checkboxAlignment = Alignment.center,
this.bottomMargin,
super.columnSpacing,
this.showHeadingCheckBox = true,
super.showCheckboxColumn = true,
super.showBottomBorder = false,
super.dividerThickness,
super.clipBehavior,
this.minWidth,
this.scrollController,
this.horizontalScrollController,
this.isVerticalScrollBarVisible,
this.isHorizontalScrollBarVisible,
this.empty,
this.border,
this.smRatio = 0.67,
this.fixedTopRows = 1,
this.fixedLeftColumns = 0,
this.lmRatio = 1.2,
this.columnResizingParameters,
this.sortArrowAnimationDuration = const Duration(milliseconds: 150),
this.sortArrowIcon = Icons.arrow_upward,
this.sortArrowIconColor,
this.sortArrowBuilder,
this.headingRowDecoration,
required super.rows,
}) : assert(fixedLeftColumns >= 0),
assert(fixedTopRows >= 0);
static final LocalKey _headingRowKey = UniqueKey();
void _handleSelectAll(bool? checked, bool someChecked) {
final bool effectiveChecked = someChecked || (checked ?? false);
if (onSelectAll != null) {
onSelectAll!(effectiveChecked);
} else {
for (final DataRow row in rows) {
if (row.onSelectChanged != null && row.selected != effectiveChecked) {
row.onSelectChanged!(effectiveChecked);
}
}
}
}
static const double _headingRowHeight = 56.0;
static const double _horizontalMargin = 24.0;
static const double _columnSpacing = 56.0;
static const double _sortArrowPadding = 2.0;
static const double _dividerThickness = 1.0;
final Duration sortArrowAnimationDuration;
final IconData sortArrowIcon;
final Color? sortArrowIconColor;
final Widget? Function(bool ascending, bool sorted)? sortArrowBuilder;
final double? minWidth;
final CheckboxThemeData? headingCheckboxTheme;
final Alignment checkboxAlignment;
final bool showHeadingCheckBox;
final CheckboxThemeData? datarowCheckboxTheme;
final double? bottomMargin;
final BoxDecoration? headingRowDecoration;
@override
final double? dataRowHeight;
final ScrollController? scrollController;
final ScrollController? horizontalScrollController;
final bool? isVerticalScrollBarVisible;
final bool? isHorizontalScrollBarVisible;
final Widget? empty;
@override
final TableBorder? border;
final double smRatio;
final double lmRatio;
final ColumnResizingParameters? columnResizingParameters;
final int fixedTopRows;
final int fixedLeftColumns;
final Color? fixedColumnsColor;
final Color? fixedCornerColor;
(double, double) getMinMaxRowHeight(DataTableThemeData dataTableTheme) {
final double effectiveDataRowMinHeight = dataRowHeight ?? dataTableTheme.dataRowMinHeight ?? kMinInteractiveDimension;
return (effectiveDataRowMinHeight, effectiveDataRowMinHeight);
}
Widget _buildCheckbox(
{required BuildContext context,
required bool? checked,
required VoidCallback? onRowTap,
required ValueChanged<bool?>? onCheckboxChanged,
required WidgetStateProperty<Color?>? overlayColor,
required CheckboxThemeData? checkboxTheme,
required bool tristate,
required double? rowHeight}) {
final DataTableThemeData dataTableTheme = DataTableTheme.of(context);
final double effectiveHorizontalMargin = horizontalMargin ?? dataTableTheme.horizontalMargin ?? _horizontalMargin;
final (effectiveDataRowMinHeight, effectiveDataRowMaxHeight) = getMinMaxRowHeight(dataTableTheme);
Widget wrapInContainer(Widget child) => Container(
alignment: checkboxAlignment,
constraints: BoxConstraints(minHeight: rowHeight ?? effectiveDataRowMinHeight, maxHeight: rowHeight ?? effectiveDataRowMaxHeight),
padding: EdgeInsetsDirectional.only(
start: checkboxHorizontalMargin ?? effectiveHorizontalMargin,
end: (checkboxHorizontalMargin ?? effectiveHorizontalMargin) / 2.0,
),
child: child);
Widget contents = Semantics(
container: true,
child: wrapInContainer(
Theme(
data: ThemeData(checkboxTheme: checkboxTheme),
child: Checkbox(
value: checked,
onChanged: onCheckboxChanged,
tristate: tristate,
)),
),
);
if (onRowTap != null) {
contents = TableRowInkWell(
onTap: onRowTap,
overlayColor: overlayColor,
child: contents,
);
}
return contents;
}
Widget _buildHeadingCell(
{required BuildContext context,
required EdgeInsetsGeometry padding,
required Widget label,
required String? tooltip,
required bool numeric,
required VoidCallback? onSort,
required bool sorted,
required bool ascending,
required double effectiveHeadingRowHeight,
required WidgetStateProperty<Color?>? overlayColor,
DataColumn2? column,
required Function(List<DataColumn>, DataColumn2, double) onColumnResized,
required MainAxisAlignment headingRowAlignment}) {
final ThemeData themeData = Theme.of(context);
var customArrows = sortArrowBuilder != null ? sortArrowBuilder!(ascending, sorted) : null;
label = Row(
textDirection: numeric ? TextDirection.rtl : null,
mainAxisAlignment: headingRowAlignment,
children: <Widget>[
Flexible(child: label),
if (onSort != null) ...<Widget>[
customArrows ??
_SortArrow(
visible: sorted,
up: sorted ? ascending : null,
duration: sortArrowAnimationDuration,
sortArrowIcon: sortArrowIcon,
sortArrowIconColor: sortArrowIconColor,
),
const SizedBox(width: _sortArrowPadding),
],
],
);
final TextStyle effectiveHeadingTextStyle = headingTextStyle ?? themeData.dataTableTheme.headingTextStyle ?? themeData.textTheme.titleSmall!;
label = Container(
padding: padding,
height: effectiveHeadingRowHeight,
alignment: numeric ? Alignment.centerRight : AlignmentDirectional.centerStart,
child: AnimatedDefaultTextStyle(
style: effectiveHeadingTextStyle,
softWrap: false,
duration: sortArrowAnimationDuration,
child: label,
),
);
if (tooltip != null) {
label = Tooltip(
message: tooltip,
child: label,
);
}
label = InkWell(
onTap: onSort,
overlayColor: overlayColor,
child: label,
);
if (column != null && column.isResizable) {
label = Row(
children: [
Expanded(child: label),
_buildResizeWidget(
column,
effectiveHeadingRowHeight,
onColumnResized,
),
],
);
}
return label;
}
Widget _buildResizeWidget(DataColumn2 column, double widgetHeight, Function(List<DataColumn>, DataColumn2, double) onColumnResized) {
ColumnResizingParameters crp = columnResizingParameters != null ? columnResizingParameters! : ColumnResizingParameters();
return ColumnResizeWidget(
height: widgetHeight,
color: crp.widgetColor,
minWidth: crp.widgetMinWidth,
maxWidth: crp.widgetMaxWidth,
onDragUpdate: (delta) {
if (column.isResizable) {
onColumnResized(columns, column, delta);
}
},
desktopMode: crp.desktopMode,
realTime: crp.realTime,
);
}
Widget _buildDataCell(
{required BuildContext context,
required EdgeInsetsGeometry padding,
required double? specificRowHeight,
required Widget label,
required bool numeric,
required bool placeholder,
required bool showEditIcon,
required GestureTapCallback? onTap,
required GestureTapCallback? onDoubleTap,
required GestureLongPressCallback? onLongPress,
required GestureTapDownCallback? onTapDown,
required GestureTapCancelCallback? onTapCancel,
required GestureTapCallback? onRowTap,
required GestureTapCallback? onRowDoubleTap,
required GestureLongPressCallback? onRowLongPress,
required GestureTapCallback? onRowSecondaryTap,
required GestureTapDownCallback? onRowSecondaryTapDown,
required VoidCallback? onSelectChanged,
required WidgetStateProperty<Color?>? overlayColor}) {
final ThemeData themeData = Theme.of(context);
final DataTableThemeData dataTableTheme = DataTableTheme.of(context);
if (showEditIcon) {
const Widget icon = Icon(Icons.edit, size: 18.0);
label = Expanded(child: label);
label = Row(
textDirection: numeric ? TextDirection.rtl : null,
children: <Widget>[label, icon],
);
}
final TextStyle effectiveDataTextStyle =
dataTextStyle ?? dataTableTheme.dataTextStyle ?? themeData.dataTableTheme.dataTextStyle ?? themeData.textTheme.bodyMedium!;
final (effectiveDataRowMinHeight, effectiveDataRowMaxHeight) = getMinMaxRowHeight(dataTableTheme);
label = Container(
padding: padding,
constraints: BoxConstraints(minHeight: specificRowHeight ?? effectiveDataRowMinHeight, maxHeight: specificRowHeight ?? effectiveDataRowMaxHeight),
alignment: numeric ? Alignment.centerRight : AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: effectiveDataTextStyle.copyWith(
color: placeholder ? effectiveDataTextStyle.color!.withOpacity(0.6) : null,
),
child: DropdownButtonHideUnderline(child: label),
),
);
if (onTap != null || onDoubleTap != null || onLongPress != null || onTapDown != null || onTapCancel != null) {
label = InkWell(
onTap: () {
onTap?.call();
onRowTap?.call();
},
onDoubleTap: () {
onDoubleTap?.call();
onRowDoubleTap?.call();
},
onLongPress: () {
onLongPress?.call();
onRowLongPress?.call();
},
onTapDown: onTapDown,
onTapCancel: onTapCancel,
onSecondaryTap: onRowSecondaryTap,
onSecondaryTapDown: onRowSecondaryTapDown,
overlayColor: overlayColor,
child: label,
);
} else if (onSelectChanged != null ||
onRowTap != null ||
onRowDoubleTap != null ||
onRowLongPress != null ||
onRowSecondaryTap != null ||
onRowSecondaryTapDown != null) {
label = TableRowInkWell(
onTap: onRowTap ?? onSelectChanged,
onDoubleTap: onRowDoubleTap,
onLongPress: onRowLongPress,
onSecondaryTap: onRowSecondaryTap,
onSecondaryTapDown: onRowSecondaryTapDown,
overlayColor: overlayColor,
child: label,
);
}
return label;
}
@override
Widget build(BuildContext context) {
var sw = Stopwatch();
sw.start();
assert(debugCheckHasMaterial(context));
final theme = Theme.of(context);
final DataTableThemeData dataTableTheme = DataTableTheme.of(context);
final effectiveHeadingRowColor = headingRowColor ?? theme.dataTableTheme.headingRowColor;
final effectiveDataRowColor = dataRowColor ?? theme.dataTableTheme.dataRowColor;
final defaultRowColor = WidgetStateProperty.resolveWith(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return theme.colorScheme.primary.withOpacity(0.08);
}
return null;
},
);
final anyRowSelectable = rows.any((DataRow row) => row.onSelectChanged != null);
final displayCheckboxColumn = showCheckboxColumn && anyRowSelectable;
final rowsWithCheckbox = displayCheckboxColumn ? rows.where((DataRow row) => row.onSelectChanged != null) : <DataRow2>[];
final rowsChecked = rowsWithCheckbox.where((DataRow row) => row.selected);
final allChecked = displayCheckboxColumn && rowsChecked.length == rowsWithCheckbox.length;
final anyChecked = displayCheckboxColumn && rowsChecked.isNotEmpty;
final someChecked = anyChecked && !allChecked;
final effectiveHorizontalMargin = horizontalMargin ?? theme.dataTableTheme.horizontalMargin ?? _horizontalMargin;
final effectiveColumnSpacing = columnSpacing ?? theme.dataTableTheme.columnSpacing ?? _columnSpacing;
final double effectiveHeadingRowHeight = headingRowHeight ?? theme.dataTableTheme.headingRowHeight ?? _headingRowHeight;
final tableColumnWidths = List<TableColumnWidth>.filled(columns.length + (displayCheckboxColumn ? 1 : 0), const _NullTableColumnWidth());
final headingRow = _buildHeadingRow(context, theme, effectiveHeadingRowColor, tableColumnWidths.length);
final actualFixedRows = math.max(0, rows.isEmpty ? 0 : math.min(fixedTopRows, rows.length + 1));
final actualFixedColumns = math.max(0, rows.isEmpty ? 0 : math.min(fixedLeftColumns, columns.length + (showCheckboxColumn ? 1 : 0)));
List<TableRow>? coreRows = rows.isEmpty || actualFixedColumns >= columns.length + (showCheckboxColumn ? 1 : 0)
? null
: _buildTableRows(
anyRowSelectable,
effectiveDataRowColor,
context,
theme,
tableColumnWidths.length - actualFixedColumns,
defaultRowColor,
actualFixedRows == 0 ? _buildHeadingRow(context, theme, effectiveHeadingRowColor, tableColumnWidths.length - actualFixedColumns) : null,
actualFixedRows > 0 ? actualFixedRows - 1 : 0);
List<TableRow>? fixedColumnsRows = rows.isEmpty
? null
: actualFixedColumns > 0
? (actualFixedRows < 1
? [
_buildHeadingRow(
context, theme, fixedColumnsColor != null ? WidgetStatePropertyAll(fixedColumnsColor) : effectiveHeadingRowColor, actualFixedColumns),
..._buildTableRows(anyRowSelectable, fixedColumnsColor != null ? WidgetStatePropertyAll(fixedColumnsColor) : effectiveDataRowColor, context,
theme, actualFixedColumns, defaultRowColor, null, 0, 0, true)
]
: _buildTableRows(anyRowSelectable, fixedColumnsColor != null ? WidgetStatePropertyAll(fixedColumnsColor) : effectiveDataRowColor, context,
theme, actualFixedColumns, defaultRowColor, null, actualFixedRows - 1, 0, true))
: null;
List<TableRow>? fixedRows = actualFixedRows > 0
? (actualFixedRows == 1
? [_buildHeadingRow(context, theme, headingRowColor ?? effectiveHeadingRowColor, tableColumnWidths.length - actualFixedColumns)]
: [
_buildHeadingRow(context, theme, headingRowColor ?? effectiveHeadingRowColor, tableColumnWidths.length - actualFixedColumns),
..._buildTableRows(anyRowSelectable, headingRowColor ?? effectiveDataRowColor, context, theme, tableColumnWidths.length - actualFixedColumns,
defaultRowColor, null, 0, actualFixedRows - 1, true)
])
: null;
List<TableRow>? fixedCornerRows = actualFixedColumns > 0 && actualFixedRows > 0
? (actualFixedRows == 1
? [
_buildHeadingRow(
context, theme, fixedCornerColor != null ? WidgetStatePropertyAll(fixedCornerColor) : effectiveHeadingRowColor, actualFixedColumns)
]
: [
_buildHeadingRow(
context, theme, fixedCornerColor != null ? WidgetStatePropertyAll(fixedCornerColor) : effectiveHeadingRowColor, actualFixedColumns),
..._buildTableRows(anyRowSelectable, fixedCornerColor != null ? WidgetStatePropertyAll(fixedCornerColor) : effectiveDataRowColor, context,
theme, actualFixedColumns, defaultRowColor, null, 0, actualFixedRows - 1, true)
])
: null;
double checkBoxWidth = _addCheckBoxes(displayCheckboxColumn, effectiveHorizontalMargin, tableColumnWidths, headingRow, effectiveHeadingRowHeight, context,
someChecked, allChecked, coreRows, fixedRows, fixedCornerRows, fixedColumnsRows, rows, actualFixedRows, effectiveDataRowColor);
var builder = LayoutBuilder(builder: (context, constraints) {
return DataTableController(
scrollController: scrollController,
sc12toSc11Position: true,
horizontalScrollController: horizontalScrollController,
sc22toSc21Position: true,
builder: (context, sc11, sc12, sc21, sc22, cdc, onColumnResized) {
var coreVerticalController = sc11;
var leftColumnVerticalContoller = sc12;
var coreHorizontalController = sc21;
var fixedRowsHorizontalController = sc22;
var displayColumnIndex = 0;
if (checkBoxWidth > 0) displayColumnIndex += 1;
final widths = _calculateDataColumnSizes(constraints, checkBoxWidth, effectiveHorizontalMargin, cdc);
for (int dataColumnIndex = 0; dataColumnIndex < columns.length; dataColumnIndex++) {
final DataColumn column = columns[dataColumnIndex];
final double paddingStart;
if (dataColumnIndex == 0 && displayCheckboxColumn) {
paddingStart = effectiveHorizontalMargin / 2.0;
} else if (dataColumnIndex == 0 && !displayCheckboxColumn) {
paddingStart = effectiveHorizontalMargin;
} else {
paddingStart = effectiveColumnSpacing / 2.0;
}
final double paddingEnd;
if (dataColumnIndex == columns.length - 1) {
paddingEnd = effectiveHorizontalMargin;
} else {
paddingEnd = effectiveColumnSpacing / 2.0;
}
final EdgeInsetsDirectional padding = EdgeInsetsDirectional.only(
start: paddingStart,
end: paddingEnd,
);
tableColumnWidths[displayColumnIndex] = FixedColumnWidth(widths[dataColumnIndex]);
var h = _buildHeadingCell(
context: context,
padding: padding,
effectiveHeadingRowHeight: effectiveHeadingRowHeight,
label: column.label,
tooltip: column.tooltip,
numeric: column.numeric,
onSort: column.onSort != null ? () => column.onSort!(dataColumnIndex, sortColumnIndex != dataColumnIndex || !sortAscending) : null,
sorted: dataColumnIndex == sortColumnIndex,
ascending: sortAscending,
overlayColor: effectiveHeadingRowColor,
headingRowAlignment: (column is DataColumn2 ? column.headingRowAlignment : null) ?? MainAxisAlignment.start,
column: column is DataColumn2 ? column : null,
onColumnResized: onColumnResized,
);
headingRow.children[displayColumnIndex] = h;
if (displayColumnIndex < actualFixedColumns) {
if (actualFixedRows < 1) {
fixedColumnsRows![0].children[displayColumnIndex] = h;
} else if (actualFixedRows > 0) {
fixedCornerRows![0].children[displayColumnIndex] = h;
}
} else {
if (actualFixedRows < 1 && coreRows != null) {
coreRows[0].children[displayColumnIndex - actualFixedColumns] = h;
} else if (actualFixedRows > 0) {
fixedRows![0].children[displayColumnIndex - actualFixedColumns] = h;
}
}
var rowIndex = 0;
var skipRows = actualFixedRows == 1
? 0
: actualFixedRows > 1
? actualFixedRows - 1
: -1;
for (final DataRow row in rows) {
final DataCell cell = row.cells[dataColumnIndex];
var c = _buildDataCell(
context: context,
padding: padding,
specificRowHeight: row is DataRow2 ? row.specificRowHeight : null,
label: cell.child,
numeric: column.numeric,
placeholder: cell.placeholder,
showEditIcon: cell.showEditIcon,
onTap: cell.onTap,
onDoubleTap: cell.onDoubleTap,
onLongPress: cell.onLongPress,
onTapDown: cell.onTapDown,
onTapCancel: cell.onTapCancel,
onRowTap: row is DataRow2 ? row.onTap : null,
onRowDoubleTap: row is DataRow2 ? row.onDoubleTap : null,
onRowLongPress: row.onLongPress,
onRowSecondaryTap: row is DataRow2 ? row.onSecondaryTap : null,
onRowSecondaryTapDown: row is DataRow2 ? row.onSecondaryTapDown : null,
onSelectChanged: row.onSelectChanged != null ? () => row.onSelectChanged!(!row.selected) : null,
overlayColor: row.color ?? effectiveDataRowColor);
if (displayColumnIndex < actualFixedColumns) {
if (rowIndex + 1 < actualFixedRows) {
fixedCornerRows![rowIndex + 1].children[displayColumnIndex] = c;
} else {
fixedColumnsRows![rowIndex - skipRows].children[displayColumnIndex] = c;
}
} else {
if (rowIndex + 1 < actualFixedRows) {
fixedRows![rowIndex + 1].children[displayColumnIndex - actualFixedColumns] = c;
} else {
coreRows![rowIndex - skipRows].children[displayColumnIndex - actualFixedColumns] = c;
}
}
rowIndex += 1;
}
displayColumnIndex += 1;
}
var widthsAsMap = tableColumnWidths.asMap();
Map<int, TableColumnWidth>? leftWidthsAsMap = actualFixedColumns > 0 ? tableColumnWidths.take(actualFixedColumns).toList().asMap() : null;
Map<int, TableColumnWidth>? rightWidthsAsMap = actualFixedColumns > 0 ? tableColumnWidths.skip(actualFixedColumns).toList().asMap() : null;
bool isRowsEmpty(List<TableRow>? rows) {
return rows == null || rows.isEmpty || rows[0].children.isEmpty;
}
var coreTable = Table(
columnWidths: actualFixedColumns > 0 ? rightWidthsAsMap : widthsAsMap,
children: coreRows ?? [],
border: border == null
? null
: isRowsEmpty(fixedRows) && isRowsEmpty(fixedColumnsRows)
? border
: !isRowsEmpty(fixedRows) && !isRowsEmpty(fixedColumnsRows)
? TableBorder(
right: border!.right,
bottom: border!.bottom,
verticalInside: border!.verticalInside,
horizontalInside: border!.horizontalInside,
borderRadius: border!.borderRadius)
: isRowsEmpty(fixedRows)
? TableBorder(
top: border!.top,
right: border!.right,
bottom: border!.bottom,
verticalInside: border!.verticalInside,
horizontalInside: border!.horizontalInside,
borderRadius: border!.borderRadius)
: TableBorder(
left: border!.left,
right: border!.right,
bottom: border!.bottom,
verticalInside: border!.verticalInside,
horizontalInside: border!.horizontalInside,
borderRadius: border!.borderRadius));
Table? fixedRowsTabel;
Table? fixedColumnsTable;
Table? fixedTopLeftCornerTable;
Widget? fixedColumnAndCornerCol;
Widget? fixedRowsAndCoreCol;
if (rows.isNotEmpty) {
if (fixedRows != null && !isRowsEmpty(fixedRows) && actualFixedColumns < columns.length + (showCheckboxColumn ? 1 : 0)) {
fixedRowsTabel = Table(
columnWidths: actualFixedColumns > 0 ? rightWidthsAsMap : widthsAsMap,
children: fixedRows,
border: border == null
? null
: isRowsEmpty(fixedCornerRows)
? border
: TableBorder(
top: border!.top,
right: border!.right,
bottom: border!.bottom,
verticalInside: border!.verticalInside,
horizontalInside: border!.horizontalInside,
borderRadius: border!.borderRadius));
}
if (fixedColumnsRows != null && !isRowsEmpty(fixedColumnsRows)) {
fixedColumnsTable = Table(
columnWidths: leftWidthsAsMap,
children: fixedColumnsRows,
border: border == null
? null
: isRowsEmpty(fixedCornerRows)
? border
: TableBorder(
left: border!.left,
right: border!.right,
bottom: border!.bottom,
verticalInside: border!.verticalInside,
horizontalInside: border!.horizontalInside,
borderRadius: border!.borderRadius));
}
if (fixedCornerRows != null && !isRowsEmpty(fixedCornerRows)) {
fixedTopLeftCornerTable = Table(columnWidths: leftWidthsAsMap, children: fixedCornerRows, border: border);
}
Widget addBottomMargin(Table t) =>
bottomMargin != null && bottomMargin! > 0 ? Column(mainAxisSize: MainAxisSize.min, children: [t, SizedBox(height: bottomMargin!)]) : t;
var scrollBarTheme = Theme.of(context).scrollbarTheme;
var isiOS = Theme.of(context).platform == TargetPlatform.iOS;
fixedRowsAndCoreCol = Scrollbar(
thumbVisibility: isHorizontalScrollBarVisible ?? (isiOS ? scrollBarTheme.thumbVisibility?.resolve({WidgetState.hovered}) : null),
thickness: (isiOS ? scrollBarTheme.thickness?.resolve({WidgetState.hovered}) : null),
controller: coreHorizontalController,
child: Column(mainAxisSize: MainAxisSize.min, children: [
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
controller: fixedRowsHorizontalController,
scrollDirection: Axis.horizontal,
child: (fixedRowsTabel != null)
? fixedRowsTabel
: SizedBox(
height: 0,
width: widths.fold<double>(0, (previousValue, value) => previousValue + value),
))),
Flexible(
fit: FlexFit.tight,
child: Scrollbar(
thumbVisibility: isVerticalScrollBarVisible ?? (isiOS ? scrollBarTheme.thumbVisibility?.resolve({WidgetState.hovered}) : null),
thickness: (isiOS ? scrollBarTheme.thickness?.resolve({WidgetState.hovered}) : null),
controller: coreVerticalController,
child: SingleChildScrollView(
controller: coreVerticalController,
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
controller: coreHorizontalController, scrollDirection: Axis.horizontal, child: addBottomMargin(coreTable)))))
]));
fixedColumnAndCornerCol = fixedTopLeftCornerTable == null && fixedColumnsTable == null
? null
: Column(mainAxisSize: MainAxisSize.min, children: [
if (fixedTopLeftCornerTable != null) fixedTopLeftCornerTable,
if (fixedColumnsTable != null)
Flexible(
fit: FlexFit.loose,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: SingleChildScrollView(
controller: leftColumnVerticalContoller, scrollDirection: Axis.vertical, child: addBottomMargin(fixedColumnsTable))))
]);
}
var completeWidget = Container(
decoration: decoration ?? theme.dataTableTheme.decoration,
child: Material(
type: MaterialType.transparency,
borderRadius: border?.borderRadius,
clipBehavior: clipBehavior,
child: rows.isEmpty
? Column(children: [
SingleChildScrollView(
controller: coreHorizontalController,
scrollDirection: Axis.horizontal,
child: Table(columnWidths: widthsAsMap, border: border, children: [headingRow])),
Flexible(fit: FlexFit.tight, child: empty ?? const SizedBox())
])
: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (fixedColumnAndCornerCol != null) fixedColumnAndCornerCol,
if (fixedRowsAndCoreCol != null) Flexible(fit: FlexFit.tight, child: fixedRowsAndCoreCol)
],
)));
return completeWidget;
});
});
sw.stop();
if (dataTableShowLogs && kDebugMode) {
debugPrint('DataTable2 built: ${sw.elapsedMilliseconds}ms');
}
return builder;
}
double _addCheckBoxes(
bool displayCheckboxColumn,
double effectiveHorizontalMargin,
List<TableColumnWidth> tableColumns,
TableRow headingRow,
double headingHeight,
BuildContext context,
bool someChecked,
bool allChecked,
List<TableRow>? coreRows,
List<TableRow>? fixedRows,
List<TableRow>? fixedCornerRows,
List<TableRow>? fixedColumnRows,
List<DataRow> rows,
int actualFixedRows,
WidgetStateProperty<Color?>? effectiveDataRowColor) {
double checkBoxWidth = 0;
if (displayCheckboxColumn) {
checkBoxWidth = effectiveHorizontalMargin + Checkbox.width + effectiveHorizontalMargin / 2.0;
tableColumns[0] = FixedColumnWidth(checkBoxWidth);
headingRow.children[0] = showHeadingCheckBox
? _buildCheckbox(
context: context,
checked: someChecked ? null : allChecked,
onRowTap: null,
onCheckboxChanged: (bool? checked) => _handleSelectAll(checked, someChecked),
overlayColor: null,
checkboxTheme: headingCheckboxTheme,
tristate: true,
rowHeight: headingHeight)
: SizedBox(
height: headingHeight,
);
if (fixedCornerRows != null) {
fixedCornerRows[0].children[0] = headingRow.children[0];
} else if (fixedColumnRows != null) {
fixedColumnRows[0].children[0] = headingRow.children[0];
} else if (fixedRows != null) {
fixedRows[0].children[0] = headingRow.children[0];
} else {
coreRows![0].children[0] = headingRow.children[0];
}
var skipRows = actualFixedRows == 1
? 0
: actualFixedRows > 1
? actualFixedRows - 1
: -1;
var rowIndex = 0;
for (final DataRow row in rows) {
var x = _buildCheckbox(
context: context,
checked: row.selected,
onRowTap: () {
if (row is DataRow2 && row.onTap != null) {
row.onTap?.call();
} else {
row.onSelectChanged?.call(!row.selected);
}
},
onCheckboxChanged: row.onSelectChanged,
overlayColor: row.color ?? effectiveDataRowColor,
checkboxTheme: datarowCheckboxTheme,
tristate: false,
rowHeight: rows[rowIndex] is DataRow2 ? (rows[rowIndex] as DataRow2).specificRowHeight : null);
if (fixedCornerRows != null && rowIndex < fixedCornerRows.length - 1) {
fixedCornerRows[rowIndex + 1].children[0] = x;
} else if (fixedColumnRows != null) {
fixedColumnRows[rowIndex - skipRows].children[0] = x;
} else if (fixedRows != null && rowIndex < fixedRows.length - 1) {
fixedRows[rowIndex + 1].children[0] = x;
} else {
coreRows![rowIndex - skipRows].children[0] = x;
}
rowIndex += 1;
}
}
return checkBoxWidth;
}
List<double> _calculateDataColumnSizes(
BoxConstraints constraints, double checkBoxWidth, double effectiveHorizontalMargin, ColumnDataController columnDataController) {
var totalColAvailableWidth = constraints.maxWidth;
double totalExtraWidth = 0;
double totalFixedWidth = 0;
for (var c in columns) {
if (c is DataColumn2) {
var extraWidth = columnDataController.getExtraWidth(columns.indexOf(c));
totalExtraWidth += extraWidth;
if (c.fixedWidth != null) {
totalFixedWidth += c.fixedWidth!;
} else if (columnDataController.hasExtraWidth(columns.indexOf(c))) {
totalFixedWidth += columnDataController.colsWidthNoExtra[columns.indexOf(c)]!;
}
}
}
if (minWidth != null && totalColAvailableWidth < minWidth!) {
totalColAvailableWidth = minWidth!;
}
var minColWidth = checkBoxWidth + effectiveHorizontalMargin + (checkBoxWidth > 0 ? effectiveHorizontalMargin / 2 : effectiveHorizontalMargin);
totalColAvailableWidth = totalColAvailableWidth - minColWidth;
if (totalExtraWidth == 0) {
assert(totalFixedWidth < totalColAvailableWidth,
"DataTable2, combined width of columns of fixed width is greater than availble parent width. Table will be clipped");
}
totalColAvailableWidth = math.max(0.0, totalColAvailableWidth - totalFixedWidth - totalExtraWidth);
var columnWidth = totalColAvailableWidth / columns.length;
var totalColCalculatedWidth = 0.0;
final widths = List<double>.generate(columns.length, (i) {
var w = columnWidth;
var column = columns[i];
if (column is DataColumn2) {
if (columnDataController.hasExtraWidth(i)) {
w = columnDataController.colsWidthNoExtra[i]!;
} else if (column.fixedWidth != null) {
w = column.fixedWidth!;
} else if (column.size == ColumnSize.S) {
w *= smRatio;
} else if (column.size == ColumnSize.L) {
w *= lmRatio;
}
}
if (!(column is DataColumn2 && column.fixedWidth != null) && !columnDataController.hasExtraWidth(i)) {
totalColCalculatedWidth += w;
}
return w;
});
var ratio = totalColCalculatedWidth != 0 ? totalColAvailableWidth / totalColCalculatedWidth : 0;
for (var i = 0; i < widths.length; i++) {
double extraWidth = columnDataController.getExtraWidth(i);
if (!(columns[i] is DataColumn2 && (columns[i] as DataColumn2).fixedWidth != null) && !columnDataController.hasExtraWidth(i)) {
widths[i] *= ratio;
}
columnDataController.colsWidthNoExtra[i] = widths[i];
widths[i] += extraWidth;
double minColWidth = ColumnDataController.minColWidth;
if ((columns[i] is DataColumn2 && (columns[i] as DataColumn2).minWidth != null)) {
minColWidth = (columns[i] as DataColumn2).minWidth!;
}
if (widths[i] < minColWidth) {
widths[i] = minColWidth;
columnDataController.colsWidthNoExtra[i] = widths[i];
}
}
if (widths.length == 1) {
widths[0] = math.max(0, widths[0] + effectiveHorizontalMargin + (checkBoxWidth > 0 ? effectiveHorizontalMargin / 2 : effectiveHorizontalMargin));
} else if (widths.length > 1) {
widths[0] = math.max(0, widths[0] + (checkBoxWidth > 0 ? effectiveHorizontalMargin / 2 : effectiveHorizontalMargin));
widths[widths.length - 1] = math.max(0, widths[widths.length - 1] + effectiveHorizontalMargin);
}
return widths;
}
List<TableRow> _buildTableRows(bool anyRowSelectable, WidgetStateProperty<Color?>? effectiveDataRowColor, BuildContext context, ThemeData theme,
int numberOfCols, WidgetStateProperty<Color?> defaultRowColor, TableRow? headingRow,
[int skipRows = 0, int takeRows = 0, bool forceEffectiveDataRowColor = false]) {
final rowStartIndex = skipRows;
final List<TableRow> tableRows = List<TableRow>.generate(
(takeRows <= 0 ? rows.length - skipRows : takeRows) + (headingRow == null ? 0 : 1),
(int index) {
var actualIndex = headingRow == null ? index : index - 1;
if (headingRow != null && index == 0) {
return headingRow;
} else {
final bool isSelected = rows[rowStartIndex + actualIndex].selected;
final bool isDisabled = anyRowSelectable && rows[rowStartIndex + actualIndex].onSelectChanged == null;
final Set<WidgetState> states = <WidgetState>{
if (isSelected) WidgetState.selected,
if (isDisabled) WidgetState.disabled,
};
final Color? resolvedDataRowColor =
(forceEffectiveDataRowColor ? effectiveDataRowColor : (rows[rowStartIndex + actualIndex].color ?? effectiveDataRowColor))?.resolve(states);
final Color? rowColor = resolvedDataRowColor;
final BorderSide borderSide = Divider.createBorderSide(
context,
width: dividerThickness ?? theme.dataTableTheme.dividerThickness ?? _dividerThickness,
);
final Border border = showBottomBorder ? Border(bottom: borderSide) : Border(top: borderSide);
Decoration? rowDecoration = rows[rowStartIndex + actualIndex] is DataRow2 ? (rows[rowStartIndex + actualIndex] as DataRow2).decoration : null;
return TableRow(
key: rows[rowStartIndex + actualIndex].key,
decoration: rowDecoration ??
BoxDecoration(
border: dividerThickness == null || (dividerThickness != null && dividerThickness != 0.0) ? border : null,
color: rowColor ?? defaultRowColor.resolve(states),
),
children: List<Widget>.filled(numberOfCols <= 0 ? numberOfCols : numberOfCols, const _NullWidget()),
);
}
},
);
return tableRows;
}
TableRow _buildHeadingRow(BuildContext context, ThemeData theme, WidgetStateProperty<Color?>? effectiveHeadingRowColor, int numberOfCols) {
var headingRow = TableRow(
key: _headingRowKey,
decoration: BoxDecoration(
border: showBottomBorder && border == null && (dividerThickness == null || (dividerThickness != null && dividerThickness != 0.0))
? Border(
bottom: Divider.createBorderSide(
context,
width: dividerThickness ?? theme.dataTableTheme.dividerThickness ?? _dividerThickness,
))
: null,
color: effectiveHeadingRowColor?.resolve(<WidgetState>{}),
).copyWith(
color: headingRowDecoration?.color,
image: headingRowDecoration?.image,
border: headingRowDecoration?.border,
borderRadius: headingRowDecoration?.borderRadius,
boxShadow: headingRowDecoration?.boxShadow,
gradient: headingRowDecoration?.gradient,
backgroundBlendMode: headingRowDecoration?.backgroundBlendMode,
shape: headingRowDecoration?.shape,
),
children: List<Widget>.filled(numberOfCols, const _NullWidget()),
);
return headingRow;
}
}
class _SortArrow extends StatefulWidget {
const _SortArrow({
required this.visible,
required this.up,
required this.duration,
required this.sortArrowIcon,
this.sortArrowIconColor,
});
final bool visible;
final bool? up;
final Duration duration;
final IconData sortArrowIcon;
final Color? sortArrowIconColor;
@override
_SortArrowState createState() => _SortArrowState();
}
class _SortArrowState extends State<_SortArrow> with TickerProviderStateMixin {
late AnimationController _opacityController;
late Animation<double> _opacityAnimation;
late AnimationController _orientationController;
late Animation<double> _orientationAnimation;
double _orientationOffset = 0.0;
bool? _up;
static final Animatable<double> _turnTween = Tween<double>(begin: 0.0, end: math.pi).chain(CurveTween(curve: Curves.easeIn));
@override
void initState() {
super.initState();
_up = widget.up;
_opacityAnimation = CurvedAnimation(
parent: _opacityController = AnimationController(
duration: widget.duration,
vsync: this,
),
curve: Curves.fastOutSlowIn,
)..addListener(_rebuild);
_opacityController.value = widget.visible ? 1.0 : 0.0;
_orientationController = AnimationController(
duration: widget.duration,
vsync: this,
);
_orientationAnimation = _orientationController.drive(_turnTween)
..addListener(_rebuild)
..addStatusListener(_resetOrientationAnimation);
if (widget.visible) _orientationOffset = widget.up! ? 0.0 : math.pi;
}
void _rebuild() {
setState(() {
});
}
void _resetOrientationAnimation(AnimationStatus status) {
if (status == AnimationStatus.completed) {
assert(_orientationAnimation.value == math.pi);
_orientationOffset += math.pi;
_orientationController.value = 0.0;
}
}
@override
void didUpdateWidget(_SortArrow oldWidget) {
super.didUpdateWidget(oldWidget);
bool skipArrow = false;
final bool? newUp = widget.up ?? _up;
if (oldWidget.visible != widget.visible) {
if (widget.visible && (_opacityController.status == AnimationStatus.dismissed)) {
_orientationController.stop();
_orientationController.value = 0.0;
_orientationOffset = newUp! ? 0.0 : math.pi;
skipArrow = true;
}
if (widget.visible) {
_opacityController.forward();
} else {
_opacityController.reverse();
}
}
if ((_up != newUp) && !skipArrow) {
if (_orientationController.status == AnimationStatus.dismissed) {
_orientationController.forward();
} else {
_orientationController.reverse();
}
}
_up = newUp;
}
@override
void dispose() {
_opacityController.dispose();
_orientationController.dispose();
super.dispose();
}
static const double _arrowIconBaselineOffset = -1.5;
static const double _arrowIconSize = 16.0;
@override
Widget build(BuildContext context) {
return Opacity(
opacity: _opacityAnimation.value,
child: Transform(
transform: Matrix4.rotationZ(_orientationOffset + _orientationAnimation.value)..setTranslationRaw(0.0, _arrowIconBaselineOffset, 0.0),
alignment: Alignment.center,
child: Icon(
widget.sortArrowIcon,
color: widget.sortArrowIconColor,
size: _arrowIconSize,
),
),
);
}
}
class _NullTableColumnWidth extends TableColumnWidth {
const _NullTableColumnWidth();
@override
double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) => throw UnimplementedError();
@override
double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) => throw UnimplementedError();
}
class _NullWidget extends Widget {
const _NullWidget();
@override
Element createElement() => throw UnimplementedError();
}
class SyncedScrollControllers extends StatefulWidget {
const SyncedScrollControllers(
{super.key,
required this.builder,
this.scrollController,
this.sc12toSc11Position = false,
this.horizontalScrollController,
this.sc22toSc21Position = false});
final ScrollController? scrollController;
final ScrollController? horizontalScrollController;
final bool sc12toSc11Position;
final bool sc22toSc21Position;
final Widget Function(BuildContext context, ScrollController sc11, ScrollController sc12, ScrollController sc21, ScrollController sc22) builder;
@override
SyncedScrollControllersState createState() => SyncedScrollControllersState();
}
class SyncedScrollControllersState extends State<SyncedScrollControllers> {
ScrollController? _sc11;
late ScrollController _sc12;
ScrollController? _sc21;
late ScrollController _sc22;
final List<void Function()> _listeners = [];
@override
void initState() {
super.initState();
_initControllers();
}
@override
void didUpdateWidget(SyncedScrollControllers oldWidget) {
super.didUpdateWidget(oldWidget);
_disposeOrUnsubscribe();
_initControllers();
}
@override
void dispose() {
_disposeOrUnsubscribe();
super.dispose();
}
void _initControllers() {
_doNotReissueJump.clear();
var offset = _sc11 == null || _sc11!.positions.isEmpty ? 0.0 : _sc11!.offset;
if (widget.scrollController != null) {
_sc11 = widget.scrollController!;
if (_sc11!.positions.isNotEmpty) {
offset = _sc11!.offset;
}
} else {
_sc11 = ScrollController();
}
var horizontalOffset = _sc21 == null || _sc21!.positions.isEmpty ? 0.0 : _sc21!.offset;
if (widget.horizontalScrollController != null) {
_sc21 = widget.horizontalScrollController!;
if (_sc21!.positions.isNotEmpty) {
offset = _sc21!.offset;
}
} else {
_sc21 = ScrollController();
}
_sc12 = ScrollController(initialScrollOffset: widget.sc12toSc11Position ? offset : 0.0);
_sc22 = ScrollController(initialScrollOffset: widget.sc22toSc21Position ? horizontalOffset : 0.0);
_syncScrollControllers(_sc11!, _sc12);
_syncScrollControllers(_sc21!, _sc22);
}
void _disposeOrUnsubscribe() {
if (widget.scrollController == _sc11) {
_sc11?.removeListener(_listeners[0]);
} else {
_sc11?.dispose();
}
_sc12.dispose();
if (widget.horizontalScrollController == _sc21) {
_sc21?.removeListener(_listeners[0]);
} else {
_sc21?.dispose();
}
_sc22.dispose();
_listeners.clear();
}
final Map<ScrollController, bool> _doNotReissueJump = {};
void _syncScrollControllers(ScrollController sc1, ScrollController sc2) {
var l = () => _jumpToNoCascade(sc1, sc2);
sc1.addListener(l);
_listeners.add(l);
l = () => _jumpToNoCascade(sc2, sc1);
sc2.addListener(l);
}
void _jumpToNoCascade(ScrollController master, ScrollController slave) {
if (!slave.hasClients || slave.position.outOfRange) {
return;
}
if (_doNotReissueJump[master] == null || _doNotReissueJump[master]! == false) {
_doNotReissueJump[slave] = true;
slave.jumpTo(master.offset);
} else {
_doNotReissueJump[master] = false;
}
}
@override
Widget build(BuildContext context) => widget.builder(context, _sc11!, _sc12, _sc21!, _sc22);
}