Commit f75fd5c3 authored by Ian Hickson's avatar Ian Hickson

Header for PaginatedDataTable (#4468)

Also renames ButtonTheme.footer to ButtonTheme.bar.
parent 1fe57277
......@@ -96,7 +96,7 @@ class TravelDestinationItem extends StatelessWidget {
),
// share, explore buttons
// TODO(abarth): The theme and the bar should be part of card.
new ButtonTheme.footer(
new ButtonTheme.bar(
child: new ButtonBar(
alignment: MainAxisAlignment.start,
children: <Widget>[
......
......@@ -91,6 +91,8 @@ class DesertDataSource extends DataTableSource {
notifyListeners();
}
int _selectedCount = 0;
@override
DataRow getRow(int index) {
assert(index >= 0);
......@@ -101,8 +103,12 @@ class DesertDataSource extends DataTableSource {
index: index,
selected: desert.selected,
onSelectChanged: (bool value) {
desert.selected = value;
notifyListeners();
if (desert.selected != value) {
_selectedCount += value ? 1 : -1;
assert(_selectedCount >= 0);
desert.selected = value;
notifyListeners();
}
},
cells: <DataCell>[
new DataCell(new Text('${desert.name}')),
......@@ -122,6 +128,16 @@ class DesertDataSource extends DataTableSource {
@override
bool get isRowCountApproximate => false;
@override
int get selectedRowCount => _selectedCount;
void _selectAll(bool checked) {
for (Desert desert in _deserts)
desert.selected = checked;
_selectedCount = checked ? _deserts.length : 0;
notifyListeners();
}
}
class DataTableDemo extends StatefulWidget {
......@@ -135,10 +151,10 @@ class _DataTableDemoState extends State<DataTableDemo> {
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
int _sortColumnIndex;
bool _sortAscending = true;
DesertDataSource _deserts = new DesertDataSource();
DesertDataSource _desertsDataSource = new DesertDataSource();
void _sort/*<T>*/(Comparable<dynamic/*=T*/> getField(Desert d), int columnIndex, bool ascending) {
_deserts._sort/*<T>*/(getField, ascending);
_desertsDataSource._sort/*<T>*/(getField, ascending);
setState(() {
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
......@@ -153,10 +169,12 @@ class _DataTableDemoState extends State<DataTableDemo> {
padding: const EdgeInsets.all(20.0),
children: <Widget>[
new PaginatedDataTable(
header: new Text('Nutrition'),
rowsPerPage: _rowsPerPage,
onRowsPerPageChanged: (int value) { setState(() { _rowsPerPage = value; }); },
sortColumnIndex: _sortColumnIndex,
sortAscending: _sortAscending,
onSelectAll: _desertsDataSource._selectAll,
columns: <DataColumn>[
new DataColumn(
label: new Text('Dessert (100g serving)'),
......@@ -200,7 +218,7 @@ class _DataTableDemoState extends State<DataTableDemo> {
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
),
],
source: _deserts
source: _desertsDataSource
)
]
)
......
......@@ -50,15 +50,21 @@ class ButtonTheme extends InheritedWidget {
Widget child
}) : super(key: key, child: child);
/// Creates a button theme that is appropriate for footer buttons.
/// Creates a button theme that is appropriate for button bars, as used in
/// dialog footers and in the headers of data tables.
///
/// This theme is denser, with a smaller [minWidth] and [padding], than the
/// default theme. Also, this theme uses [ButtonTextTheme.accent] rather than
/// [ButtonTextTheme.normal].
///
/// For best effect, the label of the button at the edge of the container
/// should have text that ends up wider than 64.0 pixels. This ensures that
/// the alignment of the text matches the alignment of the edge of the
/// container.
///
/// For example, buttons at the bottom of [Dialog] or [Card] widgets use this
/// button theme.
const ButtonTheme.footer({
const ButtonTheme.bar({
Key key,
this.textTheme: ButtonTextTheme.accent,
this.minWidth: 64.0,
......
......@@ -51,7 +51,7 @@ class ButtonBar extends StatelessWidget {
),
child: new Row(
mainAxisAlignment: alignment,
children: children.map((Widget child) {
children: children.map/*<Widget>*/((Widget child) {
return new Padding(
padding: new EdgeInsets.symmetric(horizontal: paddingUnit),
child: child
......
......@@ -54,4 +54,9 @@ abstract class DataTableSource extends ChangeNotifier {
/// then later the exact number becomes available, then call
/// [notifyListeners].
bool get isRowCountApproximate;
/// Called to obtain the number of rows that are currently selected.
///
/// If the selected row count changes, call [notifyListeners].
int get selectedRowCount;
}
\ No newline at end of file
......@@ -96,7 +96,7 @@ class Dialog extends StatelessWidget {
}
if (actions != null) {
dialogBody.add(new ButtonTheme.footer(
dialogBody.add(new ButtonTheme.bar(
child: new ButtonBar(
alignment: MainAxisAlignment.end,
children: actions
......
......@@ -4,9 +4,12 @@
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'button.dart';
import 'button_bar.dart';
import 'card.dart';
import 'data_table.dart';
import 'data_table_source.dart';
......@@ -23,6 +26,9 @@ import 'theme.dart';
class PaginatedDataTable extends StatefulWidget {
/// Creates a widget describing a paginated [DataTable] on a [Card].
///
/// The [header] should give the card's header, typically a [Text] widget. It
/// must not be null.
///
/// The [columns] argument must be a list of as many [DataColumn] objects as
/// the table is to have columns, ignoring the leading checkbox column if any.
/// The [columns] argument must have a length greater than zero and cannot be
......@@ -43,10 +49,12 @@ class PaginatedDataTable extends StatefulWidget {
/// widget unless the data table really is to now show entirely different
/// data from a new source.
///
/// The [rowsPerPage] and [availableRowsPerPage] must not be null (though they
/// both have defaults, so don't have to be specified).
/// The [rowsPerPage] and [availableRowsPerPage] must not be null (they
/// both have defaults, though, so don't have to be specified).
PaginatedDataTable({
Key key,
@required this.header,
this.actions,
this.columns,
this.sortColumnIndex,
this.sortAscending: true,
......@@ -56,8 +64,9 @@ class PaginatedDataTable extends StatefulWidget {
this.rowsPerPage: defaultRowsPerPage,
this.availableRowsPerPage: const <int>[defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10],
this.onRowsPerPageChanged,
this.source
@required this.source
}) : super(key: key) {
assert(header != null);
assert(columns != null);
assert(columns.length > 0);
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
......@@ -69,6 +78,24 @@ class PaginatedDataTable extends StatefulWidget {
assert(source != null);
}
/// The table card's header.
///
/// This is typically a [Text] widget, but can also be a [ButtonBar] with
/// [FlatButton]s. Suitable defaults are automatically provided for the font,
/// button color, button padding, and so forth.
///
/// If items in the table are selectable, then, when the selection is not
/// empty, the header is replaced by a count of the selected items.
final Widget header;
/// Icon buttons to show at the top right of the table.
///
/// Typically, the exact actions included in this list will vary based on
/// whether any rows are selected or not.
///
/// These should be size 24.0 with default padding (8.0).
final List<Widget> actions;
/// The configuration and labels for the columns in the table.
final List<DataColumn> columns;
......@@ -142,6 +169,7 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
int _firstRowIndex;
int _rowCount;
bool _rowCountApproximate;
int _selectedRowCount;
final Map<int, DataRow> _rows = <int, DataRow>{};
@override
......@@ -172,6 +200,7 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
setState(() {
_rowCount = config.source.rowCount;
_rowCountApproximate = config.source.isRowCountApproximate;
_selectedRowCount = config.source.selectedRowCount;
_rows.clear();
});
}
......@@ -237,7 +266,42 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
@override
Widget build(BuildContext context) {
final TextStyle textStyle = Theme.of(context).textTheme.caption;
// TODO(ianh): This whole build function doesn't handle RTL yet.
ThemeData themeData = Theme.of(context);
// HEADER
final List<Widget> headerWidgets = <Widget>[];
double leftPadding = 24.0;
if (_selectedRowCount == 0) {
headerWidgets.add(new Flexible(child: config.header));
if (config.header is ButtonBar) {
// We adjust the padding when a button bar is present, because the
// ButtonBar introduces 2 pixels of outside padding, plus 2 pixels
// around each button on each side, and the button itself will have 8
// pixels internally on each side, yet we want the left edge of the
// inside of the button to line up with the 24.0 left inset.
// TODO(ianh): Better magic. See https://github.com/flutter/flutter/issues/4460
leftPadding = 12.0;
}
} else if (_selectedRowCount == 1) {
// TODO(ianh): Real l10n.
headerWidgets.add(new Flexible(child: new Text('1 item selected')));
} else {
headerWidgets.add(new Flexible(child: new Text('$_selectedRowCount items selected')));
}
if (config.actions != null) {
headerWidgets.addAll(
config.actions.map/*<Widget>*/((Widget widget) {
return new Padding(
// 8.0 is the default padding of an icon button
padding: new EdgeInsets.only(left: 24.0 - 8.0 * 2.0),
child: widget
);
}).toList()
);
}
// FOOTER
final TextStyle footerTextStyle = themeData.textTheme.caption;
final List<Widget> footerWidgets = <Widget>[];
if (config.onRowsPerPageChanged != null) {
List<Widget> availableRowsPerPage = config.availableRowsPerPage
......@@ -256,7 +320,7 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
items: availableRowsPerPage,
value: config.rowsPerPage,
onChanged: config.onRowsPerPageChanged,
style: textStyle,
style: footerTextStyle,
iconSize: 24.0
)
),
......@@ -285,21 +349,39 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
),
new Container(width: 14.0),
]);
// CARD
return new Card(
// TODO(ianh): data table card headers
/*
- title, top left
- 20px Roboto Regular, black87
- persistent actions, top left
- header when there's a selection
- accent 50?
- show number of selected items
- different actions
- actions, top right
- 24px icons, black54
*/
child: new BlockBody(
children: <Widget>[
new DefaultTextStyle(
// These typographic styles aren't quite the regular ones. We pick the closest ones from the regular
// list and then tweak them appropriately.
// See https://www.google.com/design/spec/components/data-tables.html#data-tables-tables-within-cards
style: _selectedRowCount > 0 ? themeData.textTheme.subhead.copyWith(color: themeData.accentColor)
: themeData.textTheme.title.copyWith(fontWeight: FontWeight.w400),
child: new IconTheme(
data: new IconThemeData(
opacity: 0.54
),
child: new ButtonTheme.bar(
child: new Container(
height: 64.0,
padding: new EdgeInsets.fromLTRB(leftPadding, 0.0, 14.0, 0.0),
// TODO(ianh): This decoration will prevent ink splashes from being visible.
// Instead, we should have a widget that prints the decoration on the material.
// See https://github.com/flutter/flutter/issues/3782
decoration: _selectedRowCount > 0 ? new BoxDecoration(
backgroundColor: themeData.secondaryHeaderColor
) : null,
child: new Row(
mainAxisAlignment: MainAxisAlignment.end,
children: headerWidgets
)
)
)
)
),
new ScrollableViewport(
scrollDirection: Axis.horizontal,
child: new DataTable(
......@@ -312,7 +394,7 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
)
),
new DefaultTextStyle(
style: textStyle,
style: footerTextStyle,
child: new IconTheme(
data: new IconThemeData(
opacity: 0.54
......
......@@ -67,6 +67,7 @@ class ThemeData {
Color unselectedWidgetColor,
Color disabledColor,
Color buttonColor,
Color secondaryHeaderColor,
Color textSelectionColor,
Color textSelectionHandleColor,
Color backgroundColor,
......@@ -93,6 +94,7 @@ class ThemeData {
unselectedWidgetColor ??= isDark ? Colors.white70 : Colors.black54;
disabledColor ??= isDark ? Colors.white30 : Colors.black26;
buttonColor ??= isDark ? primarySwatch[600] : Colors.grey[300];
secondaryHeaderColor ??= primarySwatch[50]; // TODO(ianh): dark theme support (https://github.com/flutter/flutter/issues/3370)
textSelectionColor ??= isDark ? accentColor : primarySwatch[200];
textSelectionHandleColor ??= isDark ? Colors.tealAccent[400] : primarySwatch[300];
backgroundColor ??= isDark ? Colors.grey[700] : primarySwatch[200];
......@@ -117,6 +119,7 @@ class ThemeData {
unselectedWidgetColor: unselectedWidgetColor,
disabledColor: disabledColor,
buttonColor: buttonColor,
secondaryHeaderColor: secondaryHeaderColor,
textSelectionColor: textSelectionColor,
textSelectionHandleColor: textSelectionHandleColor,
backgroundColor: backgroundColor,
......@@ -150,6 +153,7 @@ class ThemeData {
this.unselectedWidgetColor,
this.disabledColor,
this.buttonColor,
this.secondaryHeaderColor,
this.textSelectionColor,
this.textSelectionHandleColor,
this.backgroundColor,
......@@ -174,6 +178,7 @@ class ThemeData {
assert(unselectedWidgetColor != null);
assert(disabledColor != null);
assert(buttonColor != null);
assert(secondaryHeaderColor != null);
assert(textSelectionColor != null);
assert(textSelectionHandleColor != null);
assert(disabledColor != null);
......@@ -257,6 +262,12 @@ class ThemeData {
/// The default color of the [Material] used in [RaisedButton]s.
final Color buttonColor;
/// The color of the header of a [PaginatedDataTable] when there are selected rows.
// According to the spec for data tables:
// https://material.google.com/components/data-tables.html#data-tables-tables-within-cards
// ...this should be the "50-value of secondary app color".
final Color secondaryHeaderColor;
/// The color of text selections in text fields, such as [Input].
final Color textSelectionColor;
......@@ -301,6 +312,7 @@ class ThemeData {
unselectedWidgetColor: Color.lerp(begin.unselectedWidgetColor, end.unselectedWidgetColor, t),
disabledColor: Color.lerp(begin.disabledColor, end.disabledColor, t),
buttonColor: Color.lerp(begin.buttonColor, end.buttonColor, t),
secondaryHeaderColor: Color.lerp(begin.secondaryHeaderColor, end.secondaryHeaderColor, t),
textSelectionColor: Color.lerp(begin.textSelectionColor, end.textSelectionColor, t),
textSelectionHandleColor: Color.lerp(begin.textSelectionHandleColor, end.textSelectionHandleColor, t),
backgroundColor: Color.lerp(begin.backgroundColor, end.backgroundColor, t),
......@@ -332,6 +344,7 @@ class ThemeData {
(otherData.unselectedWidgetColor == unselectedWidgetColor) &&
(otherData.disabledColor == disabledColor) &&
(otherData.buttonColor == buttonColor) &&
(otherData.secondaryHeaderColor == secondaryHeaderColor) &&
(otherData.textSelectionColor == textSelectionColor) &&
(otherData.textSelectionHandleColor == textSelectionHandleColor) &&
(otherData.backgroundColor == backgroundColor) &&
......@@ -360,12 +373,13 @@ class ThemeData {
unselectedWidgetColor,
disabledColor,
buttonColor,
secondaryHeaderColor,
textSelectionColor,
textSelectionHandleColor,
backgroundColor,
accentColor,
accentColorBrightness,
hashValues( // Too many values.
accentColorBrightness,
indicatorColor,
hintColor,
errorColor,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment