| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import '../app_bar.dart'; |
| import '../back_button.dart'; |
| import '../color_scheme.dart'; |
| import '../debug.dart'; |
| import '../dialog.dart'; |
| import '../dialog_theme.dart'; |
| import '../icon_button.dart'; |
| import '../icons.dart'; |
| import '../material_localizations.dart'; |
| import '../scaffold.dart'; |
| import '../text_button.dart'; |
| import '../text_theme.dart'; |
| import '../theme.dart'; |
| |
| import 'calendar_date_range_picker.dart'; |
| import 'date_picker_common.dart'; |
| import 'date_picker_header.dart'; |
| import 'date_utils.dart' as utils; |
| import 'input_date_range_picker.dart'; |
| |
| const Size _inputPortraitDialogSize = Size(330.0, 270.0); |
| const Size _inputLandscapeDialogSize = Size(496, 164.0); |
| const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200); |
| const double _inputFormPortraitHeight = 98.0; |
| const double _inputFormLandscapeHeight = 108.0; |
| |
| /// Shows a full screen modal dialog containing a Material Design date range |
| /// picker. |
| /// |
| /// The returned [Future] resolves to the [DateTimeRange] selected by the user |
| /// when the user saves their selection. If the user cancels the dialog, null is |
| /// returned. |
| /// |
| /// If [initialDateRange] is non-null, then it will be used as the initially |
| /// selected date range. If it is provided, [initialDateRange.start] must be |
| /// before or on [initialDateRange.end]. |
| /// |
| /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest |
| /// allowable date. Both must be non-null. |
| /// |
| /// If an initial date range is provided, [initialDateRange.start] |
| /// and [initialDateRange.end] must both fall between or on [firstDate] and |
| /// [lastDate]. For all of these [DateTime] values, only their dates are |
| /// considered. Their time fields are ignored. |
| /// |
| /// The [currentDate] represents the current day (i.e. today). This |
| /// date will be highlighted in the day grid. If null, the date of |
| /// `DateTime.now()` will be used. |
| /// |
| /// An optional [initialEntryMode] argument can be used to display the date |
| /// picker in the [DatePickerEntryMode.calendar] (a scrollable calendar month |
| /// grid) or [DatePickerEntryMode.input] (two text input fields) mode. |
| /// It defaults to [DatePickerEntryMode.calendar] and must be non-null. |
| /// |
| /// The following optional string parameters allow you to override the default |
| /// text used for various parts of the dialog: |
| /// |
| /// * [helpText], the label displayed at the top of the dialog. |
| /// * [cancelText], the label on the cancel button for the text input mode. |
| /// * [confirmText],the label on the ok button for the text input mode. |
| /// * [saveText], the label on the save button for the fullscreen calendar |
| /// mode. |
| /// * [errorFormatText], the message used when an input text isn't in a proper |
| /// date format. |
| /// * [errorInvalidText], the message used when an input text isn't a |
| /// selectable date. |
| /// * [errorInvalidRangeText], the message used when the date range is |
| /// invalid (e.g. start date is after end date). |
| /// * [fieldStartHintText], the text used to prompt the user when no text has |
| /// been entered in the start field. |
| /// * [fieldEndHintText], the text used to prompt the user when no text has |
| /// been entered in the end field. |
| /// * [fieldStartLabelText], the label for the start date text input field. |
| /// * [fieldEndLabelText], the label for the end date text input field. |
| /// |
| /// An optional [locale] argument can be used to set the locale for the date |
| /// picker. It defaults to the ambient locale provided by [Localizations]. |
| /// |
| /// An optional [textDirection] argument can be used to set the text direction |
| /// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It |
| /// defaults to the ambient text direction provided by [Directionality]. If both |
| /// [locale] and [textDirection] are non-null, [textDirection] overrides the |
| /// direction chosen for the [locale]. |
| /// |
| /// The [context], [useRootNavigator] and [routeSettings] arguments are passed |
| /// to [showDialog], the documentation for which discusses how it is used. |
| /// [context] and [useRootNavigator] must be non-null. |
| /// |
| /// The [builder] parameter can be used to wrap the dialog widget |
| /// to add inherited widgets like [Theme]. |
| /// |
| /// See also: |
| /// |
| /// * [showDatePicker], which shows a material design date picker used to |
| /// select a single date. |
| /// * [DateTimeRange], which is used to describe a date range. |
| /// |
| Future<DateTimeRange> showDateRangePicker({ |
| required BuildContext context, |
| DateTimeRange? initialDateRange, |
| required DateTime firstDate, |
| required DateTime lastDate, |
| DateTime? currentDate, |
| DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar, |
| String? helpText, |
| String? cancelText, |
| String? confirmText, |
| String? saveText, |
| String? errorFormatText, |
| String? errorInvalidText, |
| String? errorInvalidRangeText, |
| String? fieldStartHintText, |
| String? fieldEndHintText, |
| String? fieldStartLabelText, |
| String? fieldEndLabelText, |
| Locale? locale, |
| bool useRootNavigator = true, |
| RouteSettings? routeSettings, |
| TextDirection? textDirection, |
| TransitionBuilder? builder, |
| }) async { |
| assert(context != null); |
| assert( |
| initialDateRange == null || (initialDateRange.start != null && initialDateRange.end != null), |
| 'initialDateRange must be null or have non-null start and end dates.' |
| ); |
| assert( |
| initialDateRange == null || !initialDateRange.start.isAfter(initialDateRange.end), |
| 'initialDateRange\'s start date must not be after it\'s end date.' |
| ); |
| initialDateRange = initialDateRange == null ? null : utils.datesOnly(initialDateRange); |
| assert(firstDate != null); |
| firstDate = utils.dateOnly(firstDate); |
| assert(lastDate != null); |
| lastDate = utils.dateOnly(lastDate); |
| assert( |
| !lastDate.isBefore(firstDate), |
| 'lastDate $lastDate must be on or after firstDate $firstDate.' |
| ); |
| assert( |
| initialDateRange == null || !initialDateRange.start.isBefore(firstDate), |
| 'initialDateRange\'s start date must be on or after firstDate $firstDate.' |
| ); |
| assert( |
| initialDateRange == null || !initialDateRange.end.isBefore(firstDate), |
| 'initialDateRange\'s end date must be on or after firstDate $firstDate.' |
| ); |
| assert( |
| initialDateRange == null || !initialDateRange.start.isAfter(lastDate), |
| 'initialDateRange\'s start date must be on or before lastDate $lastDate.' |
| ); |
| assert( |
| initialDateRange == null || !initialDateRange.end.isAfter(lastDate), |
| 'initialDateRange\'s end date must be on or before lastDate $lastDate.' |
| ); |
| currentDate = utils.dateOnly(currentDate ?? DateTime.now()); |
| assert(initialEntryMode != null); |
| assert(useRootNavigator != null); |
| assert(debugCheckHasMaterialLocalizations(context)); |
| |
| Widget dialog = _DateRangePickerDialog( |
| initialDateRange: initialDateRange, |
| firstDate: firstDate, |
| lastDate: lastDate, |
| currentDate: currentDate, |
| initialEntryMode: initialEntryMode, |
| helpText: helpText, |
| cancelText: cancelText, |
| confirmText: confirmText, |
| saveText: saveText, |
| errorFormatText: errorFormatText, |
| errorInvalidText: errorInvalidText, |
| errorInvalidRangeText: errorInvalidRangeText, |
| fieldStartHintText: fieldStartHintText, |
| fieldEndHintText: fieldEndHintText, |
| fieldStartLabelText: fieldStartLabelText, |
| fieldEndLabelText: fieldEndLabelText, |
| ); |
| |
| if (textDirection != null) { |
| dialog = Directionality( |
| textDirection: textDirection, |
| child: dialog, |
| ); |
| } |
| |
| if (locale != null) { |
| dialog = Localizations.override( |
| context: context, |
| locale: locale, |
| child: dialog, |
| ); |
| } |
| |
| return showDialog<DateTimeRange>( |
| context: context, |
| useRootNavigator: useRootNavigator, |
| routeSettings: routeSettings, |
| useSafeArea: false, |
| builder: (BuildContext context) { |
| return builder == null ? dialog : builder(context, dialog); |
| }, |
| ); |
| } |
| |
| class _DateRangePickerDialog extends StatefulWidget { |
| const _DateRangePickerDialog({ |
| Key? key, |
| this.initialDateRange, |
| required this.firstDate, |
| required this.lastDate, |
| this.currentDate, |
| this.initialEntryMode = DatePickerEntryMode.calendar, |
| this.helpText, |
| this.cancelText, |
| this.confirmText, |
| this.saveText, |
| this.errorInvalidRangeText, |
| this.errorFormatText, |
| this.errorInvalidText, |
| this.fieldStartHintText, |
| this.fieldEndHintText, |
| this.fieldStartLabelText, |
| this.fieldEndLabelText, |
| }) : super(key: key); |
| |
| final DateTimeRange? initialDateRange; |
| final DateTime firstDate; |
| final DateTime lastDate; |
| final DateTime? currentDate; |
| final DatePickerEntryMode initialEntryMode; |
| final String? cancelText; |
| final String? confirmText; |
| final String? saveText; |
| final String? helpText; |
| final String? errorInvalidRangeText; |
| final String? errorFormatText; |
| final String? errorInvalidText; |
| final String? fieldStartHintText; |
| final String? fieldEndHintText; |
| final String? fieldStartLabelText; |
| final String? fieldEndLabelText; |
| |
| @override |
| _DateRangePickerDialogState createState() => _DateRangePickerDialogState(); |
| } |
| |
| class _DateRangePickerDialogState extends State<_DateRangePickerDialog> { |
| late DatePickerEntryMode _entryMode; |
| DateTime? _selectedStart; |
| DateTime? _selectedEnd; |
| late bool _autoValidate; |
| final GlobalKey _calendarPickerKey = GlobalKey(); |
| final GlobalKey<InputDateRangePickerState> _inputPickerKey = GlobalKey<InputDateRangePickerState>(); |
| |
| @override |
| void initState() { |
| super.initState(); |
| _selectedStart = widget.initialDateRange?.start; |
| _selectedEnd = widget.initialDateRange?.end; |
| _entryMode = widget.initialEntryMode; |
| _autoValidate = false; |
| } |
| |
| void _handleOk() { |
| if (_entryMode == DatePickerEntryMode.input) { |
| final InputDateRangePickerState picker = _inputPickerKey.currentState!; |
| if (!picker.validate()) { |
| setState(() { |
| _autoValidate = true; |
| }); |
| return; |
| } |
| } |
| final DateTimeRange? selectedRange = _hasSelectedDateRange |
| ? DateTimeRange(start: _selectedStart!, end: _selectedEnd!) |
| : null; |
| |
| Navigator.pop(context, selectedRange); |
| } |
| |
| void _handleCancel() { |
| Navigator.pop(context); |
| } |
| |
| void _handleEntryModeToggle() { |
| setState(() { |
| switch (_entryMode) { |
| case DatePickerEntryMode.calendar: |
| _autoValidate = false; |
| _entryMode = DatePickerEntryMode.input; |
| break; |
| |
| case DatePickerEntryMode.input: |
| // Validate the range dates |
| if (_selectedStart != null && |
| (_selectedStart!.isBefore(widget.firstDate) || _selectedStart!.isAfter(widget.lastDate))) { |
| _selectedStart = null; |
| // With no valid start date, having an end date makes no sense for the UI. |
| _selectedEnd = null; |
| } |
| if (_selectedEnd != null && |
| (_selectedEnd!.isBefore(widget.firstDate) || _selectedEnd!.isAfter(widget.lastDate))) { |
| _selectedEnd = null; |
| } |
| // If invalid range (start after end), then just use the start date |
| if (_selectedStart != null && _selectedEnd != null && _selectedStart!.isAfter(_selectedEnd!)) { |
| _selectedEnd = null; |
| } |
| _entryMode = DatePickerEntryMode.calendar; |
| break; |
| } |
| }); |
| } |
| |
| void _handleStartDateChanged(DateTime? date) { |
| setState(() => _selectedStart = date); |
| } |
| |
| void _handleEndDateChanged(DateTime? date) { |
| setState(() => _selectedEnd = date); |
| } |
| |
| bool get _hasSelectedDateRange => _selectedStart != null && _selectedEnd != null; |
| |
| @override |
| Widget build(BuildContext context) { |
| final MediaQueryData mediaQuery = MediaQuery.of(context)!; |
| final Orientation orientation = mediaQuery.orientation; |
| final double textScaleFactor = math.min(mediaQuery.textScaleFactor, 1.3); |
| final MaterialLocalizations localizations = MaterialLocalizations.of(context)!; |
| |
| final Widget contents; |
| final Size size; |
| ShapeBorder? shape; |
| final double elevation; |
| final EdgeInsets insetPadding; |
| switch (_entryMode) { |
| case DatePickerEntryMode.calendar: |
| contents = _CalendarRangePickerDialog( |
| key: _calendarPickerKey, |
| selectedStartDate: _selectedStart, |
| selectedEndDate: _selectedEnd, |
| firstDate: widget.firstDate, |
| lastDate: widget.lastDate, |
| currentDate: widget.currentDate, |
| onStartDateChanged: _handleStartDateChanged, |
| onEndDateChanged: _handleEndDateChanged, |
| onConfirm: _hasSelectedDateRange ? _handleOk : null, |
| onCancel: _handleCancel, |
| onToggleEntryMode: _handleEntryModeToggle, |
| confirmText: widget.saveText ?? localizations.saveButtonLabel, |
| helpText: widget.helpText ?? localizations.dateRangePickerHelpText, |
| ); |
| size = mediaQuery.size; |
| insetPadding = const EdgeInsets.all(0.0); |
| shape = const RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.zero) |
| ); |
| elevation = 0; |
| break; |
| |
| case DatePickerEntryMode.input: |
| contents = _InputDateRangePickerDialog( |
| selectedStartDate: _selectedStart, |
| selectedEndDate: _selectedEnd, |
| currentDate: widget.currentDate, |
| picker: Container( |
| padding: const EdgeInsets.symmetric(horizontal: 24), |
| height: orientation == Orientation.portrait |
| ? _inputFormPortraitHeight |
| : _inputFormLandscapeHeight, |
| child: Column( |
| children: <Widget>[ |
| const Spacer(), |
| InputDateRangePicker( |
| key: _inputPickerKey, |
| initialStartDate: _selectedStart, |
| initialEndDate: _selectedEnd, |
| firstDate: widget.firstDate, |
| lastDate: widget.lastDate, |
| onStartDateChanged: _handleStartDateChanged, |
| onEndDateChanged: _handleEndDateChanged, |
| autofocus: true, |
| autovalidate: _autoValidate, |
| helpText: widget.helpText, |
| errorInvalidRangeText: widget.errorInvalidRangeText, |
| errorFormatText: widget.errorFormatText, |
| errorInvalidText: widget.errorInvalidText, |
| fieldStartHintText: widget.fieldStartHintText, |
| fieldEndHintText: widget.fieldEndHintText, |
| fieldStartLabelText: widget.fieldStartLabelText, |
| fieldEndLabelText: widget.fieldEndLabelText, |
| ), |
| const Spacer(), |
| ], |
| ), |
| ), |
| onConfirm: _handleOk, |
| onCancel: _handleCancel, |
| onToggleEntryMode: _handleEntryModeToggle, |
| confirmText: widget.confirmText ?? localizations.okButtonLabel, |
| cancelText: widget.cancelText ?? localizations.cancelButtonLabel, |
| helpText: widget.helpText ?? localizations.dateRangePickerHelpText, |
| ); |
| final DialogTheme dialogTheme = Theme.of(context)!.dialogTheme; |
| size = orientation == Orientation.portrait ? _inputPortraitDialogSize : _inputLandscapeDialogSize; |
| insetPadding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0); |
| shape = dialogTheme.shape; |
| elevation = dialogTheme.elevation ?? 24; |
| break; |
| } |
| |
| return Dialog( |
| child: AnimatedContainer( |
| width: size.width, |
| height: size.height, |
| duration: _dialogSizeAnimationDuration, |
| curve: Curves.easeIn, |
| child: MediaQuery( |
| data: MediaQuery.of(context)!.copyWith( |
| textScaleFactor: textScaleFactor, |
| ), |
| child: Builder(builder: (BuildContext context) { |
| return contents; |
| }), |
| ), |
| ), |
| insetPadding: insetPadding, |
| shape: shape, |
| elevation: elevation, |
| clipBehavior: Clip.antiAlias, |
| ); |
| } |
| } |
| |
| class _CalendarRangePickerDialog extends StatelessWidget { |
| const _CalendarRangePickerDialog({ |
| Key? key, |
| required this.selectedStartDate, |
| required this.selectedEndDate, |
| required this.firstDate, |
| required this.lastDate, |
| required this.currentDate, |
| required this.onStartDateChanged, |
| required this.onEndDateChanged, |
| required this.onConfirm, |
| required this.onCancel, |
| required this.onToggleEntryMode, |
| required this.confirmText, |
| required this.helpText, |
| }) : super(key: key); |
| |
| final DateTime? selectedStartDate; |
| final DateTime? selectedEndDate; |
| final DateTime firstDate; |
| final DateTime lastDate; |
| final DateTime? currentDate; |
| final ValueChanged<DateTime> onStartDateChanged; |
| final ValueChanged<DateTime?> onEndDateChanged; |
| final VoidCallback? onConfirm; |
| final VoidCallback? onCancel; |
| final VoidCallback? onToggleEntryMode; |
| final String confirmText; |
| final String helpText; |
| |
| @override |
| Widget build(BuildContext context) { |
| final ThemeData theme = Theme.of(context)!; |
| final ColorScheme colorScheme = theme.colorScheme; |
| final MaterialLocalizations localizations = MaterialLocalizations.of(context)!; |
| final Orientation orientation = MediaQuery.of(context)!.orientation; |
| final TextTheme textTheme = theme.textTheme; |
| final Color headerForeground = colorScheme.brightness == Brightness.light |
| ? colorScheme.onPrimary |
| : colorScheme.onSurface; |
| final Color headerDisabledForeground = headerForeground.withOpacity(0.38); |
| final String startDateText = utils.formatRangeStartDate(localizations, selectedStartDate, selectedEndDate); |
| final String endDateText = utils.formatRangeEndDate(localizations, selectedStartDate, selectedEndDate, DateTime.now()); |
| final TextStyle? headlineStyle = textTheme.headline5; |
| final TextStyle? startDateStyle = headlineStyle?.apply( |
| color: selectedStartDate != null ? headerForeground : headerDisabledForeground |
| ); |
| final TextStyle? endDateStyle = headlineStyle?.apply( |
| color: selectedEndDate != null ? headerForeground : headerDisabledForeground |
| ); |
| final TextStyle saveButtonStyle = textTheme.button!.apply( |
| color: onConfirm != null ? headerForeground : headerDisabledForeground |
| ); |
| |
| final IconButton entryModeIcon = IconButton( |
| padding: EdgeInsets.zero, |
| color: headerForeground, |
| icon: const Icon(Icons.edit), |
| tooltip: localizations.inputDateModeButtonLabel, |
| onPressed: onToggleEntryMode, |
| ); |
| |
| return SafeArea( |
| top: false, |
| left: false, |
| right: false, |
| child: Scaffold( |
| appBar: AppBar( |
| leading: CloseButton( |
| onPressed: onCancel, |
| ), |
| actions: <Widget>[ |
| if (orientation == Orientation.landscape) entryModeIcon, |
| TextButton( |
| onPressed: onConfirm, |
| child: Text(confirmText, style: saveButtonStyle), |
| ), |
| const SizedBox(width: 8), |
| ], |
| bottom: PreferredSize( |
| child: Row(children: <Widget>[ |
| SizedBox(width: MediaQuery.of(context)!.size.width < 360 ? 42 : 72), |
| Expanded( |
| child: Semantics( |
| label: '$helpText $startDateText to $endDateText', |
| excludeSemantics: true, |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.start, |
| children: <Widget>[ |
| Text( |
| helpText, |
| style: textTheme.overline!.apply( |
| color: headerForeground, |
| ), |
| ), |
| const SizedBox(height: 8), |
| Row( |
| children: <Widget>[ |
| Text( |
| startDateText, |
| style: startDateStyle, |
| maxLines: 1, |
| overflow: TextOverflow.ellipsis, |
| ), |
| Text(' – ', style: startDateStyle, |
| ), |
| Flexible( |
| child: Text( |
| endDateText, |
| style: endDateStyle, |
| maxLines: 1, |
| overflow: TextOverflow.ellipsis, |
| ), |
| ), |
| ], |
| ), |
| const SizedBox(height: 16), |
| ], |
| ), |
| ), |
| ), |
| if (orientation == Orientation.portrait) |
| Padding( |
| padding: const EdgeInsets.symmetric(horizontal: 8.0), |
| child: entryModeIcon, |
| ), |
| ]), |
| preferredSize: const Size(double.infinity, 64), |
| ), |
| ), |
| body: CalendarDateRangePicker( |
| initialStartDate: selectedStartDate, |
| initialEndDate: selectedEndDate, |
| firstDate: firstDate, |
| lastDate: lastDate, |
| currentDate: currentDate, |
| onStartDateChanged: onStartDateChanged, |
| onEndDateChanged: onEndDateChanged, |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class _InputDateRangePickerDialog extends StatelessWidget { |
| const _InputDateRangePickerDialog({ |
| Key? key, |
| required this.selectedStartDate, |
| required this.selectedEndDate, |
| required this.currentDate, |
| required this.picker, |
| required this.onConfirm, |
| required this.onCancel, |
| required this.onToggleEntryMode, |
| required this.confirmText, |
| required this.cancelText, |
| required this.helpText, |
| }) : super(key: key); |
| |
| final DateTime? selectedStartDate; |
| final DateTime? selectedEndDate; |
| final DateTime? currentDate; |
| final Widget picker; |
| final VoidCallback onConfirm; |
| final VoidCallback onCancel; |
| final VoidCallback onToggleEntryMode; |
| final String? confirmText; |
| final String? cancelText; |
| final String? helpText; |
| |
| String _formatDateRange(BuildContext context, DateTime? start, DateTime? end, DateTime now) { |
| final MaterialLocalizations localizations = MaterialLocalizations.of(context)!; |
| final String startText = utils.formatRangeStartDate(localizations, start, end); |
| final String endText = utils.formatRangeEndDate(localizations, start, end, now); |
| if (start == null || end == null) { |
| return localizations.unspecifiedDateRange; |
| } |
| if (Directionality.of(context) == TextDirection.ltr) { |
| return '$startText – $endText'; |
| } else { |
| return '$endText – $startText'; |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final ThemeData theme = Theme.of(context)!; |
| final ColorScheme colorScheme = theme.colorScheme; |
| final MaterialLocalizations localizations = MaterialLocalizations.of(context)!; |
| final Orientation orientation = MediaQuery.of(context)!.orientation; |
| final TextTheme textTheme = theme.textTheme; |
| |
| final Color dateColor = colorScheme.brightness == Brightness.light |
| ? colorScheme.onPrimary |
| : colorScheme.onSurface; |
| final TextStyle? dateStyle = orientation == Orientation.landscape |
| ? textTheme.headline5?.apply(color: dateColor) |
| : textTheme.headline4?.apply(color: dateColor); |
| final String dateText = _formatDateRange(context, selectedStartDate, selectedEndDate, currentDate!); |
| final String semanticDateText = selectedStartDate != null && selectedEndDate != null |
| ? '${localizations.formatMediumDate(selectedStartDate!)} – ${localizations.formatMediumDate(selectedEndDate!)}' |
| : ''; |
| |
| final Widget header = DatePickerHeader( |
| helpText: helpText ?? localizations.dateRangePickerHelpText, |
| titleText: dateText, |
| titleSemanticsLabel: semanticDateText, |
| titleStyle: dateStyle, |
| orientation: orientation, |
| isShort: orientation == Orientation.landscape, |
| icon: Icons.calendar_today, |
| iconTooltip: localizations.calendarModeButtonLabel, |
| onIconPressed: onToggleEntryMode, |
| ); |
| |
| final Widget actions = Container( |
| alignment: AlignmentDirectional.centerEnd, |
| constraints: const BoxConstraints(minHeight: 52.0), |
| padding: const EdgeInsets.symmetric(horizontal: 8), |
| child: OverflowBar( |
| spacing: 8, |
| children: <Widget>[ |
| TextButton( |
| child: Text(cancelText ?? localizations.cancelButtonLabel), |
| onPressed: onCancel, |
| ), |
| TextButton( |
| child: Text(confirmText ?? localizations.okButtonLabel), |
| onPressed: onConfirm, |
| ), |
| ], |
| ), |
| ); |
| |
| switch (orientation) { |
| case Orientation.portrait: |
| return Column( |
| mainAxisSize: MainAxisSize.min, |
| crossAxisAlignment: CrossAxisAlignment.stretch, |
| children: <Widget>[ |
| header, |
| Expanded(child: picker), |
| actions, |
| ], |
| ); |
| |
| case Orientation.landscape: |
| return Row( |
| mainAxisSize: MainAxisSize.min, |
| crossAxisAlignment: CrossAxisAlignment.stretch, |
| children: <Widget>[ |
| header, |
| Flexible( |
| child: Column( |
| mainAxisSize: MainAxisSize.min, |
| crossAxisAlignment: CrossAxisAlignment.stretch, |
| children: <Widget>[ |
| Expanded(child: picker), |
| actions, |
| ], |
| ), |
| ), |
| ], |
| ); |
| } |
| } |
| } |