// 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. // @dart = 2.8 import 'dart:math' as math; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import '../button_bar.dart'; import '../button_theme.dart'; import '../color_scheme.dart'; import '../debug.dart'; import '../dialog.dart'; import '../icons.dart'; import '../material_localizations.dart'; import '../text_button.dart'; import '../text_theme.dart'; import '../theme.dart'; import 'calendar_date_picker.dart'; import 'date_picker_common.dart'; import 'date_picker_header.dart'; import 'date_utils.dart' as utils; import 'input_date_picker.dart'; const Size _calendarPortraitDialogSize = Size(330.0, 518.0); const Size _calendarLandscapeDialogSize = Size(496.0, 346.0); const Size _inputPortraitDialogSize = Size(330.0, 270.0); const Size _inputLandscapeDialogSize = Size(496, 160.0); const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200); const double _inputFormPortraitHeight = 98.0; const double _inputFormLandscapeHeight = 108.0; /// Shows a dialog containing a Material Design date picker. /// /// The returned [Future] resolves to the date selected by the user when the /// user confirms the dialog. If the user cancels the dialog, null is returned. /// /// When the date picker is first displayed, it will show the month of /// [initialDate], with [initialDate] selected. /// /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest /// allowable date. [initialDate] must either fall between these dates, /// or be equal to one of them. For each of these [DateTime] parameters, only /// their dates are considered. Their time fields are ignored. They must all /// be non-null. /// /// 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 calendar month grid) /// or [DatePickerEntryMode.input] (a text input field) mode. /// It defaults to [DatePickerEntryMode.calendar] and must be non-null. /// /// An optional [selectableDayPredicate] function can be passed in to only allow /// certain days for selection. If provided, only the days that /// [selectableDayPredicate] returns true for will be selectable. For example, /// this can be used to only allow weekdays for selection. If provided, it must /// return true for [initialDate]. /// /// The following optional string parameters allow you to override the default /// text used for various parts of the dialog: /// /// * [helpText], label displayed at the top of the dialog. /// * [cancelText], label on the cancel button. /// * [confirmText], label on the ok button. /// * [errorFormatText], message used when the input text isn't in a proper date format. /// * [errorInvalidText], message used when the input text isn't a selectable date. /// * [fieldHintText], text used to prompt the user when no text has been entered in the field. /// * [fieldLabelText], label for the 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]. /// /// An optional [initialDatePickerMode] argument can be used to have the /// calendar date picker initially appear in the [DatePickerMode.year] or /// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day], and /// must be non-null. /// /// See also: /// /// * [showDateRangePicker], which shows a material design date range picker /// used to select a range of dates. /// * [CalendarDatePicker], which provides the calendar grid used by the date picker dialog. /// * [InputDatePickerFormField], which provides a text input field for entering dates. /// Future<DateTime> showDatePicker({ @required BuildContext context, @required DateTime initialDate, @required DateTime firstDate, @required DateTime lastDate, DateTime currentDate, DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar, SelectableDayPredicate selectableDayPredicate, String helpText, String cancelText, String confirmText, Locale locale, bool useRootNavigator = true, RouteSettings routeSettings, TextDirection textDirection, TransitionBuilder builder, DatePickerMode initialDatePickerMode = DatePickerMode.day, String errorFormatText, String errorInvalidText, String fieldHintText, String fieldLabelText, }) async { assert(context != null); assert(initialDate != null); assert(firstDate != null); assert(lastDate != null); initialDate = utils.dateOnly(initialDate); firstDate = utils.dateOnly(firstDate); lastDate = utils.dateOnly(lastDate); assert( !lastDate.isBefore(firstDate), 'lastDate $lastDate must be on or after firstDate $firstDate.' ); assert( !initialDate.isBefore(firstDate), 'initialDate $initialDate must be on or after firstDate $firstDate.' ); assert( !initialDate.isAfter(lastDate), 'initialDate $initialDate must be on or before lastDate $lastDate.' ); assert( selectableDayPredicate == null || selectableDayPredicate(initialDate), 'Provided initialDate $initialDate must satisfy provided selectableDayPredicate.' ); assert(initialEntryMode != null); assert(useRootNavigator != null); assert(initialDatePickerMode != null); assert(debugCheckHasMaterialLocalizations(context)); Widget dialog = _DatePickerDialog( initialDate: initialDate, firstDate: firstDate, lastDate: lastDate, currentDate: currentDate, initialEntryMode: initialEntryMode, selectableDayPredicate: selectableDayPredicate, helpText: helpText, cancelText: cancelText, confirmText: confirmText, initialCalendarMode: initialDatePickerMode, errorFormatText: errorFormatText, errorInvalidText: errorInvalidText, fieldHintText: fieldHintText, fieldLabelText: fieldLabelText, ); if (textDirection != null) { dialog = Directionality( textDirection: textDirection, child: dialog, ); } if (locale != null) { dialog = Localizations.override( context: context, locale: locale, child: dialog, ); } return showDialog<DateTime>( context: context, useRootNavigator: useRootNavigator, routeSettings: routeSettings, builder: (BuildContext context) { return builder == null ? dialog : builder(context, dialog); }, ); } class _DatePickerDialog extends StatefulWidget { _DatePickerDialog({ Key key, @required DateTime initialDate, @required DateTime firstDate, @required DateTime lastDate, DateTime currentDate, this.initialEntryMode = DatePickerEntryMode.calendar, this.selectableDayPredicate, this.cancelText, this.confirmText, this.helpText, this.initialCalendarMode = DatePickerMode.day, this.errorFormatText, this.errorInvalidText, this.fieldHintText, this.fieldLabelText, }) : assert(initialDate != null), assert(firstDate != null), assert(lastDate != null), initialDate = utils.dateOnly(initialDate), firstDate = utils.dateOnly(firstDate), lastDate = utils.dateOnly(lastDate), currentDate = utils.dateOnly(currentDate ?? DateTime.now()), assert(initialEntryMode != null), assert(initialCalendarMode != null), super(key: key) { assert( !this.lastDate.isBefore(this.firstDate), 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.' ); assert( !this.initialDate.isBefore(this.firstDate), 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.' ); assert( !this.initialDate.isAfter(this.lastDate), 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.' ); assert( selectableDayPredicate == null || selectableDayPredicate(this.initialDate), 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate' ); } /// The initially selected [DateTime] that the picker should display. final DateTime initialDate; /// The earliest allowable [DateTime] that the user can select. final DateTime firstDate; /// The latest allowable [DateTime] that the user can select. final DateTime lastDate; /// The [DateTime] representing today. It will be highlighted in the day grid. final DateTime currentDate; final DatePickerEntryMode initialEntryMode; /// Function to provide full control over which [DateTime] can be selected. final SelectableDayPredicate selectableDayPredicate; /// The text that is displayed on the cancel button. final String cancelText; /// The text that is displayed on the confirm button. final String confirmText; /// The text that is displayed at the top of the header. /// /// This is used to indicate to the user what they are selecting a date for. final String helpText; /// The initial display of the calendar picker. final DatePickerMode initialCalendarMode; final String errorFormatText; final String errorInvalidText; final String fieldHintText; final String fieldLabelText; @override _DatePickerDialogState createState() => _DatePickerDialogState(); } class _DatePickerDialogState extends State<_DatePickerDialog> { DatePickerEntryMode _entryMode; DateTime _selectedDate; bool _autoValidate; final GlobalKey _calendarPickerKey = GlobalKey(); final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); @override void initState() { super.initState(); _entryMode = widget.initialEntryMode; _selectedDate = widget.initialDate; _autoValidate = false; } void _handleOk() { if (_entryMode == DatePickerEntryMode.input) { final FormState form = _formKey.currentState; if (!form.validate()) { setState(() => _autoValidate = true); return; } form.save(); } Navigator.pop(context, _selectedDate); } void _handleCancel() { Navigator.pop(context); } void _handleEntryModeToggle() { setState(() { switch (_entryMode) { case DatePickerEntryMode.calendar: _autoValidate = false; _entryMode = DatePickerEntryMode.input; break; case DatePickerEntryMode.input: _formKey.currentState.save(); _entryMode = DatePickerEntryMode.calendar; break; } }); } void _handleDateChanged(DateTime date) { setState(() => _selectedDate = date); } Size _dialogSize(BuildContext context) { final Orientation orientation = MediaQuery.of(context).orientation; switch (_entryMode) { case DatePickerEntryMode.calendar: switch (orientation) { case Orientation.portrait: return _calendarPortraitDialogSize; case Orientation.landscape: return _calendarLandscapeDialogSize; } break; case DatePickerEntryMode.input: switch (orientation) { case Orientation.portrait: return _inputPortraitDialogSize; case Orientation.landscape: return _inputLandscapeDialogSize; } break; } return null; } static final Map<LogicalKeySet, Intent> _formShortcutMap = <LogicalKeySet, Intent>{ // Pressing enter on the field will move focus to the next field or control. LogicalKeySet(LogicalKeyboardKey.enter): const NextFocusIntent(), }; @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; // Constrain the textScaleFactor to the largest supported value to prevent // layout issues. final double textScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 1.3); final String dateText = _selectedDate != null ? localizations.formatMediumDate(_selectedDate) : localizations.unspecifiedDate; final Color dateColor = colorScheme.brightness == Brightness.light ? colorScheme.onPrimary : colorScheme.onSurface; final TextStyle dateStyle = orientation == Orientation.landscape ? textTheme.headline5?.copyWith(color: dateColor) : textTheme.headline4?.copyWith(color: dateColor); final Widget actions = ButtonBar( buttonTextTheme: ButtonTextTheme.primary, layoutBehavior: ButtonBarLayoutBehavior.constrained, children: <Widget>[ TextButton( child: Text(widget.cancelText ?? localizations.cancelButtonLabel), onPressed: _handleCancel, ), TextButton( child: Text(widget.confirmText ?? localizations.okButtonLabel), onPressed: _handleOk, ), ], ); Widget picker; IconData entryModeIcon; String entryModeTooltip; switch (_entryMode) { case DatePickerEntryMode.calendar: picker = CalendarDatePicker( key: _calendarPickerKey, initialDate: _selectedDate, firstDate: widget.firstDate, lastDate: widget.lastDate, currentDate: widget.currentDate, onDateChanged: _handleDateChanged, selectableDayPredicate: widget.selectableDayPredicate, initialCalendarMode: widget.initialCalendarMode, ); entryModeIcon = Icons.edit; entryModeTooltip = localizations.inputDateModeButtonLabel; break; case DatePickerEntryMode.input: picker = Form( key: _formKey, autovalidate: _autoValidate, child: Container( padding: const EdgeInsets.symmetric(horizontal: 24), height: orientation == Orientation.portrait ? _inputFormPortraitHeight : _inputFormLandscapeHeight, child: Shortcuts( shortcuts: _formShortcutMap, child: Column( children: <Widget>[ const Spacer(), InputDatePickerFormField( initialDate: _selectedDate, firstDate: widget.firstDate, lastDate: widget.lastDate, onDateSubmitted: _handleDateChanged, onDateSaved: _handleDateChanged, selectableDayPredicate: widget.selectableDayPredicate, errorFormatText: widget.errorFormatText, errorInvalidText: widget.errorInvalidText, fieldHintText: widget.fieldHintText, fieldLabelText: widget.fieldLabelText, autofocus: true, ), const Spacer(), ], ), ), ), ); entryModeIcon = Icons.calendar_today; entryModeTooltip = localizations.calendarModeButtonLabel; break; } final Widget header = DatePickerHeader( helpText: widget.helpText ?? localizations.datePickerHelpText, titleText: dateText, titleStyle: dateStyle, orientation: orientation, isShort: orientation == Orientation.landscape, icon: entryModeIcon, iconTooltip: entryModeTooltip, onIconPressed: _handleEntryModeToggle, ); final Size dialogSize = _dialogSize(context) * textScaleFactor; return Dialog( child: AnimatedContainer( width: dialogSize.width, height: dialogSize.height, duration: _dialogSizeAnimationDuration, curve: Curves.easeIn, child: MediaQuery( data: MediaQuery.of(context).copyWith( textScaleFactor: textScaleFactor, ), child: Builder(builder: (BuildContext context) { 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, ], ), ), ], ); } return null; }), ), ), insetPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0), clipBehavior: Clip.antiAlias, ); } }