Commit 68f92d4f authored by Ian Hickson's avatar Ian Hickson

Provide UI to paginate PaginatedDataTable (#4382)

Also:
* Make PaginatedDataTable able to scroll itself horizontally.
* Make drop down buttons support having an explicit text style and icon
  size given.
* Fix a bug with drop-down buttons asserting when opened partly
  off-screen.
* Make sure to pop the drop-down button's route if the drop-down button
  is discarded while the route is up.
* Remove extraneous padding on drop-down buttons. (Couldn't figure out
  why it was there, and it breaks alignment when a drop-down is mixed
  with other text.)
* Some docs improvements.
* Add Route.isActive
* Add a setState() method to ModalRoutes.
parent 5e6baf4a
...@@ -121,7 +121,7 @@ class DesertDataSource extends DataTableSource { ...@@ -121,7 +121,7 @@ class DesertDataSource extends DataTableSource {
int get rowCount => _deserts.length; int get rowCount => _deserts.length;
@override @override
bool get isRowCountApproximate => true; bool get isRowCountApproximate => false;
} }
class DataTableDemo extends StatefulWidget { class DataTableDemo extends StatefulWidget {
...@@ -132,6 +132,7 @@ class DataTableDemo extends StatefulWidget { ...@@ -132,6 +132,7 @@ class DataTableDemo extends StatefulWidget {
} }
class _DataTableDemoState extends State<DataTableDemo> { class _DataTableDemoState extends State<DataTableDemo> {
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
int _sortColumnIndex; int _sortColumnIndex;
bool _sortAscending = true; bool _sortAscending = true;
DesertDataSource _deserts = new DesertDataSource(); DesertDataSource _deserts = new DesertDataSource();
...@@ -149,63 +150,57 @@ class _DataTableDemoState extends State<DataTableDemo> { ...@@ -149,63 +150,57 @@ class _DataTableDemoState extends State<DataTableDemo> {
return new Scaffold( return new Scaffold(
appBar: new AppBar(title: new Text('Data tables')), appBar: new AppBar(title: new Text('Data tables')),
body: new Block( body: new Block(
padding: const EdgeInsets.all(20.0),
children: <Widget>[ children: <Widget>[
new IntrinsicHeight( new PaginatedDataTable(
child: new Block( rowsPerPage: _rowsPerPage,
padding: const EdgeInsets.all(20.0), onRowsPerPageChanged: (int value) { setState(() { _rowsPerPage = value; }); },
scrollDirection: Axis.horizontal, sortColumnIndex: _sortColumnIndex,
children: <Widget>[ sortAscending: _sortAscending,
new PaginatedDataTable( columns: <DataColumn>[
rowsPerPage: 10, new DataColumn(
sortColumnIndex: _sortColumnIndex, label: new Text('Dessert (100g serving)'),
sortAscending: _sortAscending, onSort: (int columnIndex, bool ascending) => _sort/*<String>*/((Desert d) => d.name, columnIndex, ascending)
columns: <DataColumn>[ ),
new DataColumn( new DataColumn(
label: new Text('Dessert (100g serving)'), label: new Text('Calories'),
onSort: (int columnIndex, bool ascending) => _sort/*<String>*/((Desert d) => d.name, columnIndex, ascending) tooltip: 'The total amount of food energy in the given serving size.',
), numeric: true,
new DataColumn( onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending)
label: new Text('Calories'), ),
tooltip: 'The total amount of food energy in the given serving size.', new DataColumn(
numeric: true, label: new Text('Fat (g)'),
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending) numeric: true,
), onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending)
new DataColumn( ),
label: new Text('Fat (g)'), new DataColumn(
numeric: true, label: new Text('Carbs (g)'),
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending) numeric: true,
), onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending)
new DataColumn( ),
label: new Text('Carbs (g)'), new DataColumn(
numeric: true, label: new Text('Protein (g)'),
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending) numeric: true,
), onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending)
new DataColumn( ),
label: new Text('Protein (g)'), new DataColumn(
numeric: true, label: new Text('Sodium (mg)'),
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending) numeric: true,
), onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending)
new DataColumn( ),
label: new Text('Sodium (mg)'), new DataColumn(
numeric: true, label: new Text('Calcium (%)'),
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending) tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
), numeric: true,
new DataColumn( onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending)
label: new Text('Calcium (%)'), ),
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.', new DataColumn(
numeric: true, label: new Text('Iron (%)'),
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending) numeric: true,
), onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
new DataColumn( ),
label: new Text('Iron (%)'), ],
numeric: true, source: _deserts
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
),
],
source: _deserts
)
]
)
) )
] ]
) )
......
...@@ -16,7 +16,6 @@ import 'theme.dart'; ...@@ -16,7 +16,6 @@ import 'theme.dart';
import 'material.dart'; import 'material.dart';
const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300); const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
const double _kTopMargin = 6.0;
const double _kMenuItemHeight = 48.0; const double _kMenuItemHeight = 48.0;
const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0); const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0);
const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 36.0); const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 36.0);
...@@ -136,6 +135,7 @@ class _DropDownMenu<T> extends StatusTransitionWidget { ...@@ -136,6 +135,7 @@ class _DropDownMenu<T> extends StatusTransitionWidget {
), ),
child: new Material( child: new Material(
type: MaterialType.transparency, type: MaterialType.transparency,
textStyle: route.style,
child: new ScrollableList( child: new ScrollableList(
padding: _kMenuVerticalPadding, padding: _kMenuVerticalPadding,
itemExtent: _kMenuItemHeight, itemExtent: _kMenuItemHeight,
...@@ -182,8 +182,17 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -182,8 +182,17 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit); bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit);
top = bottom - childSize.height; top = bottom - childSize.height;
} }
assert(top >= 0.0); assert(() {
assert(top + childSize.height <= size.height); final Rect container = Point.origin & size;
if (container.intersect(buttonRect) == buttonRect) {
// If the button was entirely on-screen, then verify
// that the menu is also on-screen.
// If the button was a bit off-screen, then, oh well.
assert(top >= 0.0);
assert(top + childSize.height <= size.height);
}
return true;
});
return new Offset(buttonRect.left, top); return new Offset(buttonRect.left, top);
} }
...@@ -220,14 +229,28 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> { ...@@ -220,14 +229,28 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> {
this.items, this.items,
this.buttonRect, this.buttonRect,
this.selectedIndex, this.selectedIndex,
this.elevation: 8 this.elevation: 8,
}) : super(completer: completer); TextStyle style
}) : _style = style, super(completer: completer) {
assert(style != null);
}
final List<DropDownMenuItem<T>> items; final List<DropDownMenuItem<T>> items;
final Rect buttonRect; final Rect buttonRect;
final int selectedIndex; final int selectedIndex;
final int elevation; final int elevation;
TextStyle get style => _style;
TextStyle _style;
set style (TextStyle value) {
assert(value != null);
if (_style == value)
return;
setState(() {
_style = value;
});
}
@override @override
Duration get transitionDuration => _kDropDownMenuDuration; Duration get transitionDuration => _kDropDownMenuDuration;
...@@ -277,13 +300,10 @@ class DropDownMenuItem<T> extends StatelessWidget { ...@@ -277,13 +300,10 @@ class DropDownMenuItem<T> extends StatelessWidget {
return new Container( return new Container(
height: _kMenuItemHeight, height: _kMenuItemHeight,
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: new DefaultTextStyle( child: new Baseline(
style: Theme.of(context).textTheme.subhead, baselineType: TextBaseline.alphabetic,
child: new Baseline( baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
baselineType: TextBaseline.alphabetic, child: child
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
child: child
)
) )
); );
} }
...@@ -332,12 +352,17 @@ class DropDownButton<T> extends StatefulWidget { ...@@ -332,12 +352,17 @@ class DropDownButton<T> extends StatefulWidget {
/// Creates a drop down button. /// Creates a drop down button.
/// ///
/// The [items] must have distinct values and [value] must be among them. /// The [items] must have distinct values and [value] must be among them.
///
/// The [elevation] and [iconSize] arguments must not be null (they both have
/// defaults, so do not need to be specified).
DropDownButton({ DropDownButton({
Key key, Key key,
this.items, this.items,
this.value, this.value,
this.onChanged, this.onChanged,
this.elevation: 8 this.elevation: 8,
this.style,
this.iconSize: 36.0
}) : super(key: key) { }) : super(key: key) {
assert(items != null); assert(items != null);
assert(items.where((DropDownMenuItem<T> item) => item.value == value).length == 1); assert(items.where((DropDownMenuItem<T> item) => item.value == value).length == 1);
...@@ -357,6 +382,18 @@ class DropDownButton<T> extends StatefulWidget { ...@@ -357,6 +382,18 @@ class DropDownButton<T> extends StatefulWidget {
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24 /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
final int elevation; final int elevation;
/// The text style to use for text in the drop down button and the drop down
/// menu that appears when you tap the button.
///
/// Defaults to the [TextTheme.subhead] value of the current
/// [ThemeData.textTheme] of the current [Theme].
final TextStyle style;
/// The size to use for the drop-down button's down arrow icon button.
///
/// Defaults to 36.0.
final double iconSize;
@override @override
_DropDownButtonState<T> createState() => new _DropDownButtonState<T>(); _DropDownButtonState<T> createState() => new _DropDownButtonState<T>();
} }
...@@ -388,18 +425,26 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> { ...@@ -388,18 +425,26 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
} }
} }
TextStyle get _textStyle => config.style ?? Theme.of(context).textTheme.subhead;
_DropDownRoute<T> _currentRoute;
void _handleTap() { void _handleTap() {
assert(_currentRoute == null);
final RenderBox itemBox = _itemKey.currentContext.findRenderObject(); final RenderBox itemBox = _itemKey.currentContext.findRenderObject();
final Rect itemRect = itemBox.localToGlobal(Point.origin) & itemBox.size; final Rect itemRect = itemBox.localToGlobal(Point.origin) & itemBox.size;
final Completer<_DropDownRouteResult<T>> completer = new Completer<_DropDownRouteResult<T>>(); final Completer<_DropDownRouteResult<T>> completer = new Completer<_DropDownRouteResult<T>>();
Navigator.push(context, new _DropDownRoute<T>( _currentRoute = new _DropDownRoute<T>(
completer: completer, completer: completer,
items: config.items, items: config.items,
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect), buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
selectedIndex: _selectedIndex, selectedIndex: _selectedIndex,
elevation: config.elevation elevation: config.elevation,
)); style: _textStyle
);
Navigator.push(context, _currentRoute);
completer.future.then((_DropDownRouteResult<T> newValue) { completer.future.then((_DropDownRouteResult<T> newValue) {
_currentRoute = null;
if (!mounted || newValue == null) if (!mounted || newValue == null)
return; return;
if (config.onChanged != null) if (config.onChanged != null)
...@@ -410,28 +455,28 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> { ...@@ -410,28 +455,28 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
Widget result = new Row( final TextStyle style = _textStyle;
mainAxisAlignment: MainAxisAlignment.collapse, if (_currentRoute != null)
children: <Widget>[ _currentRoute.style = style;
// We use an IndexedStack to make sure we have enough width to show any Widget result = new DefaultTextStyle(
// possible item as the selected item without changing size. style: style,
new IndexedStack( child: new Row(
children: config.items, mainAxisAlignment: MainAxisAlignment.collapse,
key: _itemKey, children: <Widget>[
index: _selectedIndex, // We use an IndexedStack to make sure we have enough width to show any
alignment: FractionalOffset.centerLeft // possible item as the selected item without changing size.
), new IndexedStack(
new Icon(icon: Icons.arrow_drop_down, size: 36.0) key: _itemKey,
] index: _selectedIndex,
alignment: FractionalOffset.centerLeft,
children: config.items
),
new Icon(icon: Icons.arrow_drop_down, size: config.iconSize)
]
)
); );
if (DropDownButtonHideUnderline.at(context)) { if (!DropDownButtonHideUnderline.at(context)) {
result = new Padding(
padding: const EdgeInsets.only(top: _kTopMargin, bottom: _kBottomBorderHeight),
child: result
);
} else {
result = new Container( result = new Container(
padding: const EdgeInsets.only(top: _kTopMargin),
decoration: const BoxDecoration(border: _kDropDownUnderline), decoration: const BoxDecoration(border: _kDropDownUnderline),
child: result child: result
); );
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'debug.dart'; import 'debug.dart';
import 'icon.dart'; import 'icon.dart';
...@@ -35,12 +36,18 @@ class IconButton extends StatelessWidget { ...@@ -35,12 +36,18 @@ class IconButton extends StatelessWidget {
/// be used in many other places as well. /// be used in many other places as well.
/// ///
/// Requires one of its ancestors to be a [Material] widget. /// Requires one of its ancestors to be a [Material] widget.
///
/// The [size], [padding], and [alignment] arguments must not be null (though
/// they each have default values).
///
/// The [icon] argument must be specified. See [Icons] for a list of icons to
/// use for this argument.
const IconButton({ const IconButton({
Key key, Key key,
this.size: 24.0, this.size: 24.0,
this.padding: const EdgeInsets.all(8.0), this.padding: const EdgeInsets.all(8.0),
this.alignment: FractionalOffset.center, this.alignment: FractionalOffset.center,
this.icon, @required this.icon,
this.color, this.color,
this.disabledColor, this.disabledColor,
this.onPressed, this.onPressed,
...@@ -48,16 +55,24 @@ class IconButton extends StatelessWidget { ...@@ -48,16 +55,24 @@ class IconButton extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
/// The size of the icon inside the button. /// The size of the icon inside the button.
///
/// This property must not be null. It defaults to 24.0.
final double size; final double size;
/// The padding around the button's icon. The entire padded icon will react /// The padding around the button's icon. The entire padded icon will react
/// to input gestures. /// to input gestures.
///
/// This property must not be null. It defaults to 8.0 padding on all sides.
final EdgeInsets padding; final EdgeInsets padding;
/// Defines how the icon is positioned within the IconButton. /// Defines how the icon is positioned within the IconButton.
///
/// This property must not be null. It defaults to [FractionalOffset.center].
final FractionalOffset alignment; final FractionalOffset alignment;
/// The icon to display inside the button. /// The icon to display inside the button, from the list in [Icons].
///
/// This property must not be null.
final IconData icon; final IconData icon;
/// The color to use for the icon inside the button, if the icon is enabled. /// The color to use for the icon inside the button, if the icon is enabled.
......
...@@ -2,12 +2,21 @@ ...@@ -2,12 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'card.dart'; import 'card.dart';
import 'data_table.dart'; import 'data_table.dart';
import 'data_table_source.dart'; import 'data_table_source.dart';
import 'drop_down.dart';
import 'icon_button.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'icons.dart';
import 'progress_indicator.dart'; import 'progress_indicator.dart';
import 'theme.dart';
/// A wrapper for [DataTable] that obtains data lazily from a [DataTableSource] /// A wrapper for [DataTable] that obtains data lazily from a [DataTableSource]
/// and displays it one page at a time. The widget is presented as a [Card]. /// and displays it one page at a time. The widget is presented as a [Card].
...@@ -33,6 +42,9 @@ class PaginatedDataTable extends StatefulWidget { ...@@ -33,6 +42,9 @@ class PaginatedDataTable extends StatefulWidget {
/// [DataTableSource] with each new instance of the [PaginatedDataTable] /// [DataTableSource] with each new instance of the [PaginatedDataTable]
/// widget unless the data table really is to now show entirely different /// widget unless the data table really is to now show entirely different
/// data from a new source. /// 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).
PaginatedDataTable({ PaginatedDataTable({
Key key, Key key,
this.columns, this.columns,
...@@ -41,7 +53,8 @@ class PaginatedDataTable extends StatefulWidget { ...@@ -41,7 +53,8 @@ class PaginatedDataTable extends StatefulWidget {
this.onSelectAll, this.onSelectAll,
this.initialFirstRowIndex: 0, this.initialFirstRowIndex: 0,
this.onPageChanged, this.onPageChanged,
this.rowsPerPage: 10, this.rowsPerPage: defaultRowsPerPage,
this.availableRowsPerPage: const <int>[defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10],
this.onRowsPerPageChanged, this.onRowsPerPageChanged,
this.source this.source
}) : super(key: key) { }) : super(key: key) {
...@@ -49,6 +62,10 @@ class PaginatedDataTable extends StatefulWidget { ...@@ -49,6 +62,10 @@ class PaginatedDataTable extends StatefulWidget {
assert(columns.length > 0); assert(columns.length > 0);
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)); assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
assert(sortAscending != null); assert(sortAscending != null);
assert(rowsPerPage != null);
assert(rowsPerPage > 0);
assert(availableRowsPerPage != null);
assert(availableRowsPerPage.contains(rowsPerPage));
assert(source != null); assert(source != null);
} }
...@@ -76,13 +93,31 @@ class PaginatedDataTable extends StatefulWidget { ...@@ -76,13 +93,31 @@ class PaginatedDataTable extends StatefulWidget {
final int initialFirstRowIndex; final int initialFirstRowIndex;
/// Invoked when the user switches to another page. /// Invoked when the user switches to another page.
///
/// The value is the index of the first row on the currently displayed page.
final ValueChanged<int> onPageChanged; final ValueChanged<int> onPageChanged;
/// The number of rows to show on each page. /// The number of rows to show on each page.
/// ///
/// See also [onRowsPerPageChanged]. /// See also:
///
/// * [onRowsPerPageChanged]
/// * [defaultRowsPerPage]
final int rowsPerPage; final int rowsPerPage;
/// The default value for [rowsPerPage].
///
/// Useful when initializing the field that will hold the current
/// [rowsPerPage], when implemented [onRowsPerPageChanged].
static const int defaultRowsPerPage = 10;
/// The options to offer for the rowsPerPage.
///
/// The current [rowsPerPage] must be a value in this list.
///
/// The values in this list should be sorted in ascending order.
final List<int> availableRowsPerPage;
/// Invoked when the user selects a different number of rows per page. /// Invoked when the user selects a different number of rows per page.
/// ///
/// If this is null, then the value given by [rowsPerPage] will be used /// If this is null, then the value given by [rowsPerPage] will be used
...@@ -142,11 +177,15 @@ class PaginatedDataTableState extends State<PaginatedDataTable> { ...@@ -142,11 +177,15 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
} }
/// Ensures that the given row is visible. /// Ensures that the given row is visible.
void pageTo(double rowIndex) { void pageTo(int rowIndex) {
final int oldFirstRowIndex = _firstRowIndex;
setState(() { setState(() {
final int rowsPerPage = config.rowsPerPage; final int rowsPerPage = config.rowsPerPage;
_firstRowIndex = (rowIndex ~/ rowsPerPage) * rowsPerPage; _firstRowIndex = (rowIndex ~/ rowsPerPage) * rowsPerPage;
}); });
if ((config.onPageChanged != null) &&
(oldFirstRowIndex != _firstRowIndex))
config.onPageChanged(_firstRowIndex);
} }
DataRow _getBlankRowFor(int index) { DataRow _getBlankRowFor(int index) {
...@@ -198,40 +237,97 @@ class PaginatedDataTableState extends State<PaginatedDataTable> { ...@@ -198,40 +237,97 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<DataRow> rows = _getRows(_firstRowIndex, config.rowsPerPage); final TextStyle textStyle = Theme.of(context).textTheme.caption;
Widget table = new DataTable( final List<Widget> footerWidgets = <Widget>[];
key: _tableKey, if (config.onRowsPerPageChanged != null) {
columns: config.columns, List<Widget> availableRowsPerPage = config.availableRowsPerPage
sortColumnIndex: config.sortColumnIndex, .where((int value) => value <= _rowCount)
sortAscending: config.sortAscending, .map/*<DropDownMenuItem<int>>*/((int value) {
onSelectAll: config.onSelectAll, return new DropDownMenuItem<int>(
rows: rows value: value,
); child: new Text('$value')
);
})
.toList();
footerWidgets.addAll(<Widget>[
new Text('Rows per page:'),
new DropDownButtonHideUnderline(
child: new DropDownButton<int>(
items: availableRowsPerPage,
value: config.rowsPerPage,
onChanged: config.onRowsPerPageChanged,
style: textStyle,
iconSize: 24.0
)
),
]);
}
footerWidgets.addAll(<Widget>[
new Container(width: 32.0),
new Text(
'${_firstRowIndex + 1}\u2013${_firstRowIndex + config.rowsPerPage} ${ _rowCountApproximate ? "of about" : "of" } $_rowCount'
),
new Container(width: 32.0),
new IconButton(
padding: EdgeInsets.zero,
icon: Icons.chevron_left,
onPressed: _firstRowIndex <= 0 ? null : () {
pageTo(math.max(_firstRowIndex - config.rowsPerPage, 0));
}
),
new Container(width: 24.0),
new IconButton(
padding: EdgeInsets.zero,
icon: Icons.chevron_right,
onPressed: (!_rowCountApproximate && (_firstRowIndex + config.rowsPerPage >= _rowCount)) ? null : () {
pageTo(_firstRowIndex + config.rowsPerPage);
}
),
new Container(width: 14.0),
]);
return new Card( return new Card(
// TODO(ianh): data table card headers // TODO(ianh): data table card headers
child: table /*
// TODO(ianh): data table card footers: prev/next page, rows per page, etc - 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 ScrollableViewport(
scrollDirection: Axis.horizontal,
child: new DataTable(
key: _tableKey,
columns: config.columns,
sortColumnIndex: config.sortColumnIndex,
sortAscending: config.sortAscending,
onSelectAll: config.onSelectAll,
rows: _getRows(_firstRowIndex, config.rowsPerPage)
)
),
new DefaultTextStyle(
style: textStyle,
child: new IconTheme(
data: new IconThemeData(
opacity: 0.54
),
child: new Container(
height: 56.0,
child: new Row(
mainAxisAlignment: MainAxisAlignment.end,
children: footerWidgets
)
)
)
)
]
)
); );
} }
} }
/*
DataTableCard
- top: 64px
- caption, 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
bottom:
- 56px
- handles pagination
- 12px Roboto Regular, black54
*/
...@@ -400,7 +400,9 @@ abstract class State<T extends StatefulWidget> { ...@@ -400,7 +400,9 @@ abstract class State<T extends StatefulWidget> {
/// Whenever you need to change internal state for a State object, make the /// Whenever you need to change internal state for a State object, make the
/// change in a function that you pass to setState(), as in: /// change in a function that you pass to setState(), as in:
/// ///
/// setState(() { myState = newValue }); /// ```dart
/// setState(() { myState = newValue });
/// ```
/// ///
/// If you just change the state directly without calling setState(), then the /// If you just change the state directly without calling setState(), then the
/// widget will not be scheduled for rebuilding, meaning that its rendering /// widget will not be scheduled for rebuilding, meaning that its rendering
......
...@@ -71,12 +71,29 @@ abstract class Route<T> { ...@@ -71,12 +71,29 @@ abstract class Route<T> {
void dispose() { } void dispose() { }
/// Whether this route is the top-most route on the navigator. /// Whether this route is the top-most route on the navigator.
///
/// If this is true, then [isActive] is also true.
bool get isCurrent { bool get isCurrent {
if (_navigator == null) if (_navigator == null)
return false; return false;
assert(_navigator._history.contains(this)); assert(_navigator._history.contains(this));
return _navigator._history.last == this; return _navigator._history.last == this;
} }
/// Whether this route is on the navigator.
///
/// If the route is not only active, but also the current route (the top-most
/// route), then [isCurrent] will also be true.
///
/// If a later route is entirely opaque, then the route will be active but not
/// rendered. In particular, it's possible for a route to be active but for
/// stateful widgets within the route to not be instantiated.
bool get isActive {
if (_navigator == null)
return false;
assert(_navigator._history.contains(this));
return true;
}
} }
/// Data that might be useful in constructing a [Route]. /// Data that might be useful in constructing a [Route].
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus.dart'; import 'focus.dart';
import 'framework.dart'; import 'framework.dart';
...@@ -400,11 +402,8 @@ class _ModalScopeState extends State<_ModalScope> { ...@@ -400,11 +402,8 @@ class _ModalScopeState extends State<_ModalScope> {
}); });
} }
void _didChangeRouteOffStage() { void _routeSetState(VoidCallback fn) {
setState(() { setState(fn);
// We use the route's offstage bool in our build function, which means our
// state has changed.
});
} }
@override @override
...@@ -466,6 +465,28 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -466,6 +465,28 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return widget?.route; return widget?.route;
} }
/// Whenever you need to change internal state for a ModalRoute object, make
/// the change in a function that you pass to setState(), as in:
///
/// ```dart
/// setState(() { myState = newValue });
/// ```
///
/// If you just change the state directly without calling setState(), then the
/// route will not be scheduled for rebuilding, meaning that its rendering
/// will not be updated.
@protected
void setState(VoidCallback fn) {
if (_scopeKey.currentState != null) {
_scopeKey.currentState._routeSetState(fn);
} else {
// The route isn't currently visible, so we don't have to call its setState
// method, but we do still need to call the fn callback, otherwise the state
// in the route won't be updated!
fn();
}
}
// The API for subclasses to override - used by _ModalScope // The API for subclasses to override - used by _ModalScope
...@@ -528,8 +549,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -528,8 +549,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
set offstage (bool value) { set offstage (bool value) {
if (_offstage == value) if (_offstage == value)
return; return;
_offstage = value; setState(() {
_scopeKey.currentState?._didChangeRouteOffStage(); _offstage = value;
});
} }
/// The build context for the subtree containing the primary content of this route. /// The build context for the subtree containing the primary content of this route.
......
...@@ -721,10 +721,11 @@ class ScrollNotification extends Notification { ...@@ -721,10 +721,11 @@ class ScrollNotification extends Notification {
/// ///
/// See also: /// See also:
/// ///
/// * [ScrollableList] /// * [ScrollableList], if you have many identically-sized children.
/// * [PageableList] /// * [PageableList], if you have children that each take the entire screen.
/// * [ScrollableGrid] /// * [ScrollableGrid], if your children are in a grid pattern.
/// * [LazyBlock] /// * [LazyBlock], if you have many children of varying sizes.
/// * [Block], if your single child is a [BlockBody] or a [Column].
class ScrollableViewport extends StatelessWidget { class ScrollableViewport extends StatelessWidget {
/// Creates a simple scrolling widget that has a single child. /// Creates a simple scrolling widget that has a single child.
/// ///
...@@ -849,14 +850,18 @@ class ScrollableViewport extends StatelessWidget { ...@@ -849,14 +850,18 @@ class ScrollableViewport extends StatelessWidget {
/// arrange in a block layout and that might exceed the height of its container /// arrange in a block layout and that might exceed the height of its container
/// (and therefore need to scroll). /// (and therefore need to scroll).
/// ///
/// If you have a large number of children, consider using [LazyBlock] (if the /// If you have a large number of children, or if you always expect this to need
/// children have variable height) or [ScrollableList] (if the children all have /// to scroll, consider using [LazyBlock] (if the children have variable height)
/// the same fixed height). /// or [ScrollableList] (if the children all have the same fixed height), as
/// they avoid doing work for children that are not visible.
///
/// If you have a single child, then use [ScrollableViewport] directly.
/// ///
/// See also: /// See also:
/// ///
/// * [ScrollableList] /// * [ScrollableViewport], if you only have one child
/// * [LazyBlock] /// * [ScrollableList], if all your children are the same height
/// * [LazyBlock], if you have children with varying heights
class Block extends StatelessWidget { class Block extends StatelessWidget {
/// Creates a scrollable array of children. /// Creates a scrollable array of children.
Block({ Block({
......
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