Commit ae899486 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Rationalize text input widgets (#9119)

After this patch, there are three major text input widgets:

 * EditableText. This widget is a low-level editing control that
   interacts with the IME and displays a blinking cursor.

 * TextField. This widget is a Material Design text field, with all the
   bells and whistles. It is highly configurable and can be reduced down
   to a fairly simple control by setting its `decoration` property to
   null.

 * TextFormField. This widget is a FormField that wraps a TextField.

This patch also replaces the InputValue data model for these widgets
with a Listenable TextEditingController, which is much more flexible.

Fixes #7031
parent 91dbb3c9
...@@ -7,12 +7,12 @@ import 'package:flutter/rendering.dart' show debugDumpRenderTree; ...@@ -7,12 +7,12 @@ import 'package:flutter/rendering.dart' show debugDumpRenderTree;
class CardModel { class CardModel {
CardModel(this.value, this.height) { CardModel(this.value, this.height) {
inputValue = new InputValue(text: 'Item $value'); textController = new TextEditingController(text: 'Item $value');
} }
int value; int value;
double height; double height;
int get color => ((value % 9) + 1) * 100; int get color => ((value % 9) + 1) * 100;
InputValue inputValue; TextEditingController textController;
Key get key => new ObjectKey(this); Key get key => new ObjectKey(this);
} }
...@@ -245,11 +245,7 @@ class CardCollectionState extends State<CardCollection> { ...@@ -245,11 +245,7 @@ class CardCollectionState extends State<CardCollection> {
new Center( new Center(
child: new TextField( child: new TextField(
key: new GlobalObjectKey(cardModel), key: new GlobalObjectKey(cardModel),
onChanged: (InputValue value) { controller: cardModel.textController,
setState(() {
cardModel.inputValue = value;
});
},
), ),
) )
: new DefaultTextStyle.merge( : new DefaultTextStyle.merge(
...@@ -261,7 +257,7 @@ class CardCollectionState extends State<CardCollection> { ...@@ -261,7 +257,7 @@ class CardCollectionState extends State<CardCollection> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
new Text(cardModel.inputValue.text, textAlign: _textAlign), new Text(cardModel.textController.text, textAlign: _textAlign),
], ],
), ),
), ),
......
...@@ -26,9 +26,11 @@ class _InputDropdown extends StatelessWidget { ...@@ -26,9 +26,11 @@ class _InputDropdown extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new InkWell( return new InkWell(
onTap: onPressed, onTap: onPressed,
child: new InputContainer( child: new InputDecorator(
decoration: new InputDecoration(
labelText: labelText, labelText: labelText,
style: valueStyle, ),
baseStyle: valueStyle,
child: new Row( child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
...@@ -133,11 +135,15 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> { ...@@ -133,11 +135,15 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
children: <Widget>[ children: <Widget>[
new TextField( new TextField(
decoration: const InputDecoration(
labelText: 'Event name', labelText: 'Event name',
),
style: Theme.of(context).textTheme.display1, style: Theme.of(context).textTheme.display1,
), ),
new TextField( new TextField(
decoration: const InputDecoration(
labelText: 'Location', labelText: 'Location',
),
style: Theme.of(context).textTheme.display1.copyWith(fontSize: 20.0), style: Theme.of(context).textTheme.display1.copyWith(fontSize: 20.0),
), ),
new _DateTimePicker( new _DateTimePicker(
...@@ -170,9 +176,11 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> { ...@@ -170,9 +176,11 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
}); });
}, },
), ),
new InputContainer( new InputDecorator(
decoration: const InputDecoration(
labelText: 'Activity', labelText: 'Activity',
hintText: 'Choose an activity', hintText: 'Choose an activity',
),
isEmpty: _activity == null, isEmpty: _activity == null,
child: new DropdownButton<String>( child: new DropdownButton<String>(
value: _activity, value: _activity,
......
...@@ -148,10 +148,11 @@ class DemoItem<T> { ...@@ -148,10 +148,11 @@ class DemoItem<T> {
this.hint, this.hint,
this.builder, this.builder,
this.valueToString this.valueToString
}); }) : textController = new TextEditingController(text: valueToString(value));
final String name; final String name;
final String hint; final String hint;
final TextEditingController textController;
final DemoItemBodyBuilder<T> builder; final DemoItemBodyBuilder<T> builder;
final ValueToString<T> valueToString; final ValueToString<T> valueToString;
T value; T value;
...@@ -205,18 +206,20 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> { ...@@ -205,18 +206,20 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
onCancel: () { Form.of(context).reset(); close(); }, onCancel: () { Form.of(context).reset(); close(); },
child: new Padding( child: new Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: new TextField( child: new TextFormField(
controller: item.textController,
decoration: new InputDecoration(
hintText: item.hint, hintText: item.hint,
labelText: item.name, labelText: item.name,
initialValue: new InputValue(text: item.value), ),
onSaved: (InputValue val) { item.value = val.text; }, onSaved: (String value) { item.value = value; },
), ),
), ),
); );
} },
) ),
); );
} },
), ),
new DemoItem<_Location>( new DemoItem<_Location>(
name: 'Location', name: 'Location',
...@@ -229,8 +232,6 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> { ...@@ -229,8 +232,6 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
item.isExpanded = false; item.isExpanded = false;
}); });
} }
return new Form( return new Form(
child: new Builder( child: new Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
......
...@@ -27,6 +27,6 @@ export 'slider_demo.dart'; ...@@ -27,6 +27,6 @@ export 'slider_demo.dart';
export 'snack_bar_demo.dart'; export 'snack_bar_demo.dart';
export 'tabs_demo.dart'; export 'tabs_demo.dart';
export 'tabs_fab_demo.dart'; export 'tabs_fab_demo.dart';
export 'text_field_demo.dart'; export 'text_form_field_demo.dart';
export 'tooltip_demo.dart'; export 'tooltip_demo.dart';
export 'two_level_list_demo.dart'; export 'two_level_list_demo.dart';
...@@ -6,13 +6,13 @@ import 'dart:async'; ...@@ -6,13 +6,13 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TextFieldDemo extends StatefulWidget { class TextFormFieldDemo extends StatefulWidget {
TextFieldDemo({ Key key }) : super(key: key); TextFormFieldDemo({ Key key }) : super(key: key);
static const String routeName = '/material/text-field'; static const String routeName = '/material/text-form-field';
@override @override
TextFieldDemoState createState() => new TextFieldDemoState(); TextFormFieldDemoState createState() => new TextFormFieldDemoState();
} }
class PersonData { class PersonData {
...@@ -21,7 +21,7 @@ class PersonData { ...@@ -21,7 +21,7 @@ class PersonData {
String password = ''; String password = '';
} }
class TextFieldDemoState extends State<TextFieldDemo> { class TextFormFieldDemoState extends State<TextFormFieldDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
PersonData person = new PersonData(); PersonData person = new PersonData();
...@@ -35,7 +35,7 @@ class TextFieldDemoState extends State<TextFieldDemo> { ...@@ -35,7 +35,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
bool _autovalidate = false; bool _autovalidate = false;
bool _formWasEdited = false; bool _formWasEdited = false;
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>(); final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
final GlobalKey<FormFieldState<InputValue>> _passwordFieldKey = new GlobalKey<FormFieldState<InputValue>>(); final GlobalKey<FormFieldState<String>> _passwordFieldKey = new GlobalKey<FormFieldState<String>>();
void _handleSubmitted() { void _handleSubmitted() {
final FormState form = _formKey.currentState; final FormState form = _formKey.currentState;
if (!form.validate()) { if (!form.validate()) {
...@@ -47,30 +47,30 @@ class TextFieldDemoState extends State<TextFieldDemo> { ...@@ -47,30 +47,30 @@ class TextFieldDemoState extends State<TextFieldDemo> {
} }
} }
String _validateName(InputValue value) { String _validateName(String value) {
_formWasEdited = true; _formWasEdited = true;
if (value.text.isEmpty) if (value.isEmpty)
return 'Name is required.'; return 'Name is required.';
final RegExp nameExp = new RegExp(r'^[A-za-z ]+$'); final RegExp nameExp = new RegExp(r'^[A-za-z ]+$');
if (!nameExp.hasMatch(value.text)) if (!nameExp.hasMatch(value))
return 'Please enter only alphabetical characters.'; return 'Please enter only alphabetical characters.';
return null; return null;
} }
String _validatePhoneNumber(InputValue value) { String _validatePhoneNumber(String value) {
_formWasEdited = true; _formWasEdited = true;
final RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$'); final RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$');
if (!phoneExp.hasMatch(value.text)) if (!phoneExp.hasMatch(value))
return '###-###-#### - Please enter a valid phone number.'; return '###-###-#### - Please enter a valid phone number.';
return null; return null;
} }
String _validatePassword(InputValue value) { String _validatePassword(String value) {
_formWasEdited = true; _formWasEdited = true;
final FormFieldState<InputValue> passwordField = _passwordFieldKey.currentState; final FormFieldState<String> passwordField = _passwordFieldKey.currentState;
if (passwordField.value == null || passwordField.value.text.isEmpty) if (passwordField.value == null || passwordField.value.isEmpty)
return 'Please choose a password.'; return 'Please choose a password.';
if (passwordField.value.text != value.text) if (passwordField.value != value)
return 'Passwords don\'t match'; return 'Passwords don\'t match';
return null; return null;
} }
...@@ -104,7 +104,7 @@ class TextFieldDemoState extends State<TextFieldDemo> { ...@@ -104,7 +104,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
return new Scaffold( return new Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
appBar: new AppBar( appBar: new AppBar(
title: new Text('Text fields') title: new Text('Text fields'),
), ),
body: new Form( body: new Form(
key: _formKey, key: _formKey,
...@@ -113,48 +113,58 @@ class TextFieldDemoState extends State<TextFieldDemo> { ...@@ -113,48 +113,58 @@ class TextFieldDemoState extends State<TextFieldDemo> {
child: new ListView( child: new ListView(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
children: <Widget>[ children: <Widget>[
new TextField( new TextFormField(
icon: new Icon(Icons.person), decoration: const InputDecoration(
icon: const Icon(Icons.person),
hintText: 'What do people call you?', hintText: 'What do people call you?',
labelText: 'Name *', labelText: 'Name *',
onSaved: (InputValue val) { person.name = val.text; }, ),
onSaved: (String value) { person.name = value; },
validator: _validateName, validator: _validateName,
), ),
new TextField( new TextFormField(
icon: new Icon(Icons.phone), decoration: const InputDecoration(
icon: const Icon(Icons.phone),
hintText: 'Where can we reach you?', hintText: 'Where can we reach you?',
labelText: 'Phone Number *', labelText: 'Phone Number *',
),
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
onSaved: (InputValue val) { person.phoneNumber = val.text; }, onSaved: (String value) { person.phoneNumber = value; },
validator: _validatePhoneNumber, validator: _validatePhoneNumber,
), ),
new TextField( new TextFormField(
decoration: const InputDecoration(
hintText: 'Tell us about yourself', hintText: 'Tell us about yourself',
labelText: 'Life story', labelText: 'Life story',
),
maxLines: 3, maxLines: 3,
), ),
new Row( new Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
new Expanded( new Expanded(
child: new TextField( child: new TextFormField(
key: _passwordFieldKey, key: _passwordFieldKey,
decoration: const InputDecoration(
hintText: 'How do you log in?', hintText: 'How do you log in?',
labelText: 'New Password *', labelText: 'New Password *',
),
obscureText: true, obscureText: true,
onSaved: (InputValue val) { person.password = val.text; } onSaved: (String value) { person.password = value; },
) ),
), ),
const SizedBox(width: 16.0), const SizedBox(width: 16.0),
new Expanded( new Expanded(
child: new TextField( child: new TextFormField(
decoration: const InputDecoration(
hintText: 'How do you log in?', hintText: 'How do you log in?',
labelText: 'Re-type Password *', labelText: 'Re-type Password *',
),
obscureText: true, obscureText: true,
validator: _validatePassword, validator: _validatePassword,
) ),
) ),
] ],
), ),
new Container( new Container(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
...@@ -168,9 +178,9 @@ class TextFieldDemoState extends State<TextFieldDemo> { ...@@ -168,9 +178,9 @@ class TextFieldDemoState extends State<TextFieldDemo> {
padding: const EdgeInsets.only(top: 20.0), padding: const EdgeInsets.only(top: 20.0),
child: new Text('* indicates required field', style: Theme.of(context).textTheme.caption), child: new Text('* indicates required field', style: Theme.of(context).textTheme.caption),
), ),
] ],
)
) )
),
); );
} }
} }
...@@ -261,8 +261,8 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[ ...@@ -261,8 +261,8 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[
title: 'Text fields', title: 'Text fields',
subtitle: 'Single line of editable text and numbers', subtitle: 'Single line of editable text and numbers',
category: 'Material Components', category: 'Material Components',
routeName: TextFieldDemo.routeName, routeName: TextFormFieldDemo.routeName,
buildRoute: (BuildContext context) => new TextFieldDemo(), buildRoute: (BuildContext context) => new TextFormFieldDemo(),
), ),
new GalleryItem( new GalleryItem(
title: 'Tooltips', title: 'Tooltips',
......
...@@ -29,22 +29,22 @@ class _NotImplementedDialog extends StatelessWidget { ...@@ -29,22 +29,22 @@ class _NotImplementedDialog extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new Icon( new Icon(
Icons.dvr, Icons.dvr,
size: 18.0 size: 18.0,
), ),
new Container( new Container(
width: 8.0 width: 8.0,
), ),
new Text('DUMP APP TO CONSOLE'), new Text('DUMP APP TO CONSOLE'),
] ],
) ),
), ),
new FlatButton( new FlatButton(
onPressed: () { onPressed: () {
Navigator.pop(context, false); Navigator.pop(context, false);
}, },
child: new Text('OH WELL') child: new Text('OH WELL'),
) ),
] ],
); );
} }
} }
...@@ -65,7 +65,7 @@ class StockHomeState extends State<StockHome> { ...@@ -65,7 +65,7 @@ class StockHomeState extends State<StockHome> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
bool _isSearching = false; bool _isSearching = false;
InputValue _searchQuery = InputValue.empty; final TextEditingController _searchQuery = new TextEditingController();
bool _autorefresh = false; bool _autorefresh = false;
void _handleSearchBegin() { void _handleSearchBegin() {
...@@ -73,9 +73,9 @@ class StockHomeState extends State<StockHome> { ...@@ -73,9 +73,9 @@ class StockHomeState extends State<StockHome> {
onRemove: () { onRemove: () {
setState(() { setState(() {
_isSearching = false; _isSearching = false;
_searchQuery = InputValue.empty; _searchQuery.clear();
}); });
} },
)); ));
setState(() { setState(() {
_isSearching = true; _isSearching = true;
...@@ -86,12 +86,6 @@ class StockHomeState extends State<StockHome> { ...@@ -86,12 +86,6 @@ class StockHomeState extends State<StockHome> {
Navigator.pop(context); Navigator.pop(context);
} }
void _handleSearchQueryChanged(InputValue query) {
setState(() {
_searchQuery = query;
});
}
void _handleStockModeChange(StockMode value) { void _handleStockModeChange(StockMode value) {
if (config.updater != null) if (config.updater != null)
config.updater(config.configuration.copyWith(stockMode: value)); config.updater(config.configuration.copyWith(stockMode: value));
...@@ -155,7 +149,7 @@ class StockHomeState extends State<StockHome> { ...@@ -155,7 +149,7 @@ class StockHomeState extends State<StockHome> {
trailing: new Radio<StockMode>( trailing: new Radio<StockMode>(
value: StockMode.optimistic, value: StockMode.optimistic,
groupValue: config.configuration.stockMode, groupValue: config.configuration.stockMode,
onChanged: _handleStockModeChange onChanged: _handleStockModeChange,
), ),
onTap: () { onTap: () {
_handleStockModeChange(StockMode.optimistic); _handleStockModeChange(StockMode.optimistic);
...@@ -167,7 +161,7 @@ class StockHomeState extends State<StockHome> { ...@@ -167,7 +161,7 @@ class StockHomeState extends State<StockHome> {
trailing: new Radio<StockMode>( trailing: new Radio<StockMode>(
value: StockMode.pessimistic, value: StockMode.pessimistic,
groupValue: config.configuration.stockMode, groupValue: config.configuration.stockMode,
onChanged: _handleStockModeChange onChanged: _handleStockModeChange,
), ),
onTap: () { onTap: () {
_handleStockModeChange(StockMode.pessimistic); _handleStockModeChange(StockMode.pessimistic);
...@@ -184,8 +178,8 @@ class StockHomeState extends State<StockHome> { ...@@ -184,8 +178,8 @@ class StockHomeState extends State<StockHome> {
title: new Text('About'), title: new Text('About'),
onTap: _handleShowAbout, onTap: _handleShowAbout,
), ),
] ],
) ),
); );
} }
...@@ -205,7 +199,7 @@ class StockHomeState extends State<StockHome> { ...@@ -205,7 +199,7 @@ class StockHomeState extends State<StockHome> {
new IconButton( new IconButton(
icon: new Icon(Icons.search), icon: new Icon(Icons.search),
onPressed: _handleSearchBegin, onPressed: _handleSearchBegin,
tooltip: 'Search' tooltip: 'Search',
), ),
new PopupMenuButton<_StockMenuItem>( new PopupMenuButton<_StockMenuItem>(
onSelected: (_StockMenuItem value) { _handleStockMenu(context, value); }, onSelected: (_StockMenuItem value) { _handleStockMenu(context, value); },
...@@ -213,29 +207,29 @@ class StockHomeState extends State<StockHome> { ...@@ -213,29 +207,29 @@ class StockHomeState extends State<StockHome> {
new CheckedPopupMenuItem<_StockMenuItem>( new CheckedPopupMenuItem<_StockMenuItem>(
value: _StockMenuItem.autorefresh, value: _StockMenuItem.autorefresh,
checked: _autorefresh, checked: _autorefresh,
child: new Text('Autorefresh') child: new Text('Autorefresh'),
), ),
new PopupMenuItem<_StockMenuItem>( new PopupMenuItem<_StockMenuItem>(
value: _StockMenuItem.refresh, value: _StockMenuItem.refresh,
child: new Text('Refresh') child: new Text('Refresh'),
), ),
new PopupMenuItem<_StockMenuItem>( new PopupMenuItem<_StockMenuItem>(
value: _StockMenuItem.speedUp, value: _StockMenuItem.speedUp,
child: new Text('Increase animation speed') child: new Text('Increase animation speed'),
), ),
new PopupMenuItem<_StockMenuItem>( new PopupMenuItem<_StockMenuItem>(
value: _StockMenuItem.speedDown, value: _StockMenuItem.speedDown,
child: new Text('Decrease animation speed') child: new Text('Decrease animation speed'),
) ),
] ],
) ),
], ],
bottom: new TabBar( bottom: new TabBar(
tabs: <Widget>[ tabs: <Widget>[
new Tab(text: StockStrings.of(context).market()), new Tab(text: StockStrings.of(context).market()),
new Tab(text: StockStrings.of(context).portfolio()), new Tab(text: StockStrings.of(context).portfolio()),
] ],
) ),
); );
} }
...@@ -262,8 +256,8 @@ class StockHomeState extends State<StockHome> { ...@@ -262,8 +256,8 @@ class StockHomeState extends State<StockHome> {
label: "BUY MORE", label: "BUY MORE",
onPressed: () { onPressed: () {
_buyStock(stock); _buyStock(stock);
} },
) ),
)); ));
} }
...@@ -276,14 +270,14 @@ class StockHomeState extends State<StockHome> { ...@@ -276,14 +270,14 @@ class StockHomeState extends State<StockHome> {
}, },
onShow: (Stock stock) { onShow: (Stock stock) {
_scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) => new StockSymbolBottomSheet(stock: stock)); _scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
} },
); );
} }
Widget _buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) { Widget _buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) {
return new Container( return new Container(
key: new ValueKey<StockHomeTab>(tab), key: new ValueKey<StockHomeTab>(tab),
child: _buildStockList(context, _filterBySearchQuery(_getStockList(stockSymbols)).toList(), tab) child: _buildStockList(context, _filterBySearchQuery(_getStockList(stockSymbols)).toList(), tab),
); );
} }
...@@ -296,21 +290,23 @@ class StockHomeState extends State<StockHome> { ...@@ -296,21 +290,23 @@ class StockHomeState extends State<StockHome> {
icon: new Icon(Icons.arrow_back), icon: new Icon(Icons.arrow_back),
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
onPressed: _handleSearchEnd, onPressed: _handleSearchEnd,
tooltip: 'Back' tooltip: 'Back',
), ),
title: new TextField( title: new TextField(
controller: _searchQuery,
autofocus: true, autofocus: true,
decoration: const InputDecoration(
hintText: 'Search stocks', hintText: 'Search stocks',
onChanged: _handleSearchQueryChanged
), ),
backgroundColor: Theme.of(context).canvasColor ),
backgroundColor: Theme.of(context).canvasColor,
); );
} }
void _handleCreateCompany() { void _handleCreateCompany() {
showModalBottomSheet<Null>( showModalBottomSheet<Null>(
context: context, context: context,
builder: (BuildContext context) => new _CreateCompanySheet() builder: (BuildContext context) => new _CreateCompanySheet(),
); );
} }
...@@ -319,7 +315,7 @@ class StockHomeState extends State<StockHome> { ...@@ -319,7 +315,7 @@ class StockHomeState extends State<StockHome> {
tooltip: 'Create company', tooltip: 'Create company',
child: new Icon(Icons.add), child: new Icon(Icons.add),
backgroundColor: Colors.redAccent, backgroundColor: Colors.redAccent,
onPressed: _handleCreateCompany onPressed: _handleCreateCompany,
); );
} }
...@@ -336,9 +332,9 @@ class StockHomeState extends State<StockHome> { ...@@ -336,9 +332,9 @@ class StockHomeState extends State<StockHome> {
children: <Widget>[ children: <Widget>[
_buildStockTab(context, StockHomeTab.market, config.symbols), _buildStockTab(context, StockHomeTab.market, config.symbols),
_buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols), _buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols),
] ],
) ),
) ),
); );
} }
} }
...@@ -351,9 +347,11 @@ class _CreateCompanySheet extends StatelessWidget { ...@@ -351,9 +347,11 @@ class _CreateCompanySheet extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new TextField( new TextField(
autofocus: true, autofocus: true,
decoration: const InputDecoration(
hintText: 'Company Name', hintText: 'Company Name',
), ),
] ),
],
); );
} }
} }
...@@ -52,7 +52,7 @@ export 'src/material/image_icon.dart'; ...@@ -52,7 +52,7 @@ export 'src/material/image_icon.dart';
export 'src/material/ink_highlight.dart'; export 'src/material/ink_highlight.dart';
export 'src/material/ink_splash.dart'; export 'src/material/ink_splash.dart';
export 'src/material/ink_well.dart'; export 'src/material/ink_well.dart';
export 'src/material/input.dart'; export 'src/material/input_decorator.dart';
export 'src/material/list_tile.dart'; export 'src/material/list_tile.dart';
export 'src/material/material.dart'; export 'src/material/material.dart';
export 'src/material/mergeable_material.dart'; export 'src/material/mergeable_material.dart';
...@@ -72,6 +72,9 @@ export 'src/material/stepper.dart'; ...@@ -72,6 +72,9 @@ export 'src/material/stepper.dart';
export 'src/material/switch.dart'; export 'src/material/switch.dart';
export 'src/material/tab_controller.dart'; export 'src/material/tab_controller.dart';
export 'src/material/tabs.dart'; export 'src/material/tabs.dart';
export 'src/material/text_field.dart';
export 'src/material/text_form_field.dart';
export 'src/material/text_selection.dart';
export 'src/material/theme.dart'; export 'src/material/theme.dart';
export 'src/material/theme_data.dart'; export 'src/material/theme_data.dart';
export 'src/material/time_picker.dart'; export 'src/material/time_picker.dart';
......
...@@ -158,6 +158,19 @@ class _MergingListenable extends ChangeNotifier { ...@@ -158,6 +158,19 @@ class _MergingListenable extends ChangeNotifier {
child?.removeListener(notifyListeners); child?.removeListener(notifyListeners);
super.dispose(); super.dispose();
} }
@override
String toString() {
final StringBuffer buffer = new StringBuffer();
buffer.write('_MergingListenable([');
for (int i = 0; i < _children.length; ++i) {
buffer.write(_children[i].toString());
if (i < _children.length - 1)
buffer.write(', ');
}
buffer.write('])');
return buffer.toString();
}
} }
/// A [ChangeNotifier] that holds a single value. /// A [ChangeNotifier] that holds a single value.
......
...@@ -462,7 +462,7 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -462,7 +462,7 @@ class DropdownButton<T> extends StatefulWidget {
/// By default this button's height is the same as its menu items' heights. /// By default this button's height is the same as its menu items' heights.
/// If isDense is true, the button's height is reduced by about half. This /// If isDense is true, the button's height is reduced by about half. This
/// can be useful when the button is embedded in a container that adds /// can be useful when the button is embedded in a container that adds
/// its own decorations, like [InputContainer]. /// its own decorations, like [InputDecorator].
final bool isDense; final bool isDense;
@override @override
......
// Copyright 2015 The Chromium 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 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'debug.dart';
import 'icon.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'text_selection.dart';
import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType;
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
/// A simple undecorated text input field.
///
/// If you want decorations as specified in the Material spec (most likely),
/// use [Input] instead.
///
/// This widget is comparable to [Text] in that it does not include a margin
/// or any decoration outside the text itself. It is useful for applications,
/// like a search box, that don't need any additional decoration. It should
/// also be useful in custom widgets that support text input.
///
/// The [value] field must be updated each time the [onChanged] callback is
/// invoked. Be sure to include the full [value] provided by the [onChanged]
/// callback, or information like the current selection will be lost.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [Input], which adds a label, a divider below the text field, and support for
/// an error message.
/// * [EditableText], a text field that does not require [Material] design.
class InputField extends StatefulWidget {
InputField({
Key key,
this.focusNode,
this.value,
this.keyboardType: TextInputType.text,
this.hintText,
this.style,
this.hintStyle,
this.obscureText: false,
this.maxLines: 1,
this.autofocus: false,
this.onChanged,
this.onSubmitted,
}) : super(key: key);
/// Controls whether this widget has keyboard focus.
///
/// If null, this widget will create its own [FocusNode].
final FocusNode focusNode;
/// The current state of text of the input field. This includes the selected
/// text, if any, among other things.
final InputValue value;
/// The type of keyboard to use for editing the text.
final TextInputType keyboardType;
/// Text to show inline in the input field when it would otherwise be empty.
final String hintText;
/// The style to use for the text being edited.
final TextStyle style;
/// The style to use for the hint text.
///
/// Defaults to the specified TextStyle in style with the hintColor from
/// the ThemeData
final TextStyle hintStyle;
/// Whether to hide the text being edited (e.g., for passwords).
///
/// When this is set to true, all the characters in the input are replaced by
/// U+2022 BULLET characters (•).
///
/// Defaults to false.
final bool obscureText;
/// The maximum number of lines for the text to span, wrapping if necessary.
/// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead.
final int maxLines;
/// Whether this input field should focus itself if nothing else is already focused.
///
/// Defaults to false.
final bool autofocus;
/// Called when the text being edited changes.
///
/// The [value] must be updated each time [onChanged] is invoked.
final ValueChanged<InputValue> onChanged;
/// Called when the user indicates that they are done editing the text in the field.
final ValueChanged<InputValue> onSubmitted;
@override
_InputFieldState createState() => new _InputFieldState();
}
class _InputFieldState extends State<InputField> {
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
@override
void dispose() {
_focusNode?.dispose();
super.dispose();
}
void requestKeyboard() {
_editableTextKey.currentState?.requestKeyboard();
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final InputValue value = config.value ?? InputValue.empty;
final ThemeData themeData = Theme.of(context);
final TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
final List<Widget> stackChildren = <Widget>[
new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: new EditableText(
key: _editableTextKey,
value: value,
focusNode: _effectiveFocusNode,
style: textStyle,
obscureText: config.obscureText,
maxLines: config.maxLines,
autofocus: config.autofocus,
cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor,
selectionControls: materialTextSelectionControls,
keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
),
),
];
if (config.hintText != null && value.text.isEmpty) {
final TextStyle hintStyle = config.hintStyle ??
textStyle.copyWith(color: themeData.hintColor);
stackChildren.add(
new Positioned(
left: 0.0,
top: textStyle.fontSize - hintStyle.fontSize,
child: new IgnorePointer(
child: new Text(config.hintText, style: hintStyle),
),
),
);
}
return new RepaintBoundary(child: new Stack(children: stackChildren));
}
}
/// Displays the visual elements of a material design text field around an
/// arbitrary child widget.
///
/// Use InputContainer to create widgets that look and behave like the [Input]
/// widget.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [Input], which combines an [InputContainer] with an [InputField].
class InputContainer extends StatefulWidget {
InputContainer({
Key key,
this.focused: false,
this.isEmpty: false,
this.icon,
this.labelText,
this.hintText,
this.errorText,
this.style,
this.isDense: false,
this.showDivider: true,
this.child,
}) : super(key: key);
/// An icon to show adjacent to the input field.
///
/// The size and color of the icon is configured automatically using an
/// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget.
///
/// See [Icon], [ImageIcon].
final Widget icon;
/// Text that appears above the child or over it, if isEmpty is true.
final String labelText;
/// Text that appears over the child if isEmpty is true and labelText is null.
final String hintText;
/// Text that appears below the child. If errorText is non-null the divider
/// that appears below the child is red.
final String errorText;
/// The style to use for the hint. It's also used for the label when the label
/// appears over the child.
final TextStyle style;
/// Whether the input container is part of a dense form (i.e., uses less vertical space).
///
/// Defaults to false.
final bool isDense;
/// True if the hint and label should be displayed as if the child had the focus.
///
/// Defaults to false.
final bool focused;
/// Should the hint and label be displayed as if no value had been input
/// to the child.
///
/// Defaults to false.
final bool isEmpty;
/// Whether to show a divider below the child and above the error text.
///
/// Defaults to true.
final bool showDivider;
/// The widget below this widget in the tree.
final Widget child;
@override
_InputContainerState createState() => new _InputContainerState();
}
class _InputContainerState extends State<InputContainer> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
final String errorText = config.errorText;
final TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
Color activeColor = themeData.hintColor;
if (config.focused) {
switch (themeData.brightness) {
case Brightness.dark:
activeColor = themeData.accentColor;
break;
case Brightness.light:
activeColor = themeData.primaryColor;
break;
}
}
double topPadding = config.isDense ? 12.0 : 16.0;
final List<Widget> stackChildren = <Widget>[];
// If we're not focused, there's not value, and labelText was provided,
// then the label appears where the hint would. And we will not show
// the hintText.
final bool hasInlineLabel = !config.focused && config.labelText != null && config.isEmpty;
if (config.labelText != null) {
final TextStyle labelStyle = hasInlineLabel ?
textStyle.copyWith(color: themeData.hintColor) :
themeData.textTheme.caption.copyWith(color: activeColor);
final double topPaddingIncrement = themeData.textTheme.caption.fontSize + (config.isDense ? 4.0 : 8.0);
double top = topPadding;
if (hasInlineLabel)
top += topPaddingIncrement + textStyle.fontSize - labelStyle.fontSize;
stackChildren.add(
new AnimatedPositioned(
left: 0.0,
top: top,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new _AnimatedLabel(
text: config.labelText,
style: labelStyle,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
)
),
);
topPadding += topPaddingIncrement;
}
if (config.hintText != null) {
final TextStyle hintStyle = textStyle.copyWith(color: themeData.hintColor);
stackChildren.add(
new Positioned(
left: 0.0,
top: topPadding + textStyle.fontSize - hintStyle.fontSize,
child: new AnimatedOpacity(
opacity: (config.isEmpty && !hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new IgnorePointer(
child: new Text(config.hintText, style: hintStyle),
),
),
),
);
}
final Color borderColor = errorText == null ? activeColor : themeData.errorColor;
final double bottomPadding = config.isDense ? 8.0 : 1.0;
final double bottomBorder = 2.0;
final double bottomHeight = config.isDense ? 14.0 : 18.0;
final EdgeInsets padding = new EdgeInsets.only(top: topPadding, bottom: bottomPadding);
final Border border = new Border(
bottom: new BorderSide(
color: borderColor,
width: bottomBorder,
)
);
final EdgeInsets margin = new EdgeInsets.only(bottom: bottomHeight - (bottomPadding + bottomBorder));
Widget divider;
if (!config.showDivider) {
divider = new Container(
margin: margin + new EdgeInsets.only(bottom: bottomBorder),
padding: padding,
child: config.child,
);
} else {
divider = new AnimatedContainer(
margin: margin,
padding: padding,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
decoration: new BoxDecoration(
border: border,
),
child: config.child,
);
}
stackChildren.add(divider);
if (!config.isDense) {
final TextStyle errorStyle = themeData.textTheme.caption.copyWith(color: themeData.errorColor);
stackChildren.add(new Positioned(
left: 0.0,
bottom: 0.0,
child: new Text(errorText ?? '', style: errorStyle)
));
}
Widget textField = new Stack(children: stackChildren);
if (config.icon != null) {
final double iconSize = config.isDense ? 18.0 : 24.0;
final double iconTop = topPadding + (textStyle.fontSize - iconSize) / 2.0;
textField = new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: new EdgeInsets.only(top: iconTop),
width: config.isDense ? 40.0 : 48.0,
child: new IconTheme.merge(
context: context,
data: new IconThemeData(
color: config.focused ? activeColor : Colors.black45,
size: config.isDense ? 18.0 : 24.0
),
child: config.icon
)
),
new Expanded(child: textField)
]
);
}
return textField;
}
}
/// A material design text input field.
///
/// The [value] field must be updated each time the [onChanged] callback is
/// invoked. Be sure to include the full [value] provided by the [onChanged]
/// callback, or information like the current selection will be lost.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// When using inside a [Form], consider using [TextField] instead.
///
/// Assuming that the input is already focused, the basic data flow for
/// retrieving user input is:
/// 1. User taps a character on the keyboard.
/// 2. The [onChanged] callback is called with the current [InputValue].
/// 3. Perform any necessary logic/validation on the current input value.
/// 4. Update the state of the [Input] widget accordingly through [State.setState].
///
/// For most cases, we recommend that you use the [Input] class within a
/// [StatefulWidget] so you can save and operate on the current value of the
/// input.
///
/// See also:
///
/// * <https://material.google.com/components/text-fields.html>
/// * [TextField], which simplifies steps 2-4 above.
class Input extends StatefulWidget {
/// Creates a text input field.
///
/// By default, the input uses a keyboard appropriate for text entry.
//
// If you change this constructor signature, please also update
// InputContainer, TextField, InputField.
Input({
Key key,
this.value,
this.focusNode,
this.keyboardType: TextInputType.text,
this.icon,
this.labelText,
this.hintText,
this.errorText,
this.style,
this.obscureText: false,
this.showDivider: true,
this.isDense: false,
this.autofocus: false,
this.maxLines: 1,
this.onChanged,
this.onSubmitted,
}) : super(key: key);
/// The current state of text of the input field. This includes the selected
/// text, if any, among other things.
final InputValue value;
/// Controls whether this widget has keyboard focus.
///
/// If null, this widget will create its own [FocusNode].
final FocusNode focusNode;
/// The type of keyboard to use for editing the text.
final TextInputType keyboardType;
/// An icon to show adjacent to the input field.
///
/// The size and color of the icon is configured automatically using an
/// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget.
///
/// See [Icon], [ImageIcon].
final Widget icon;
/// Text to show above the input field.
final String labelText;
/// Text to show inline in the input field when it would otherwise be empty.
final String hintText;
/// Text to show when the input text is invalid.
final String errorText;
/// The style to use for the text being edited.
final TextStyle style;
/// Whether to hide the text being edited (e.g., for passwords).
///
/// When this is set to true, all the characters in the input are replaced by
/// U+2022 BULLET characters (•).
///
/// Defaults to false.
final bool obscureText;
/// Whether to show a divider below the child and above the error text.
///
/// Defaults to true.
final bool showDivider;
/// Whether the input field is part of a dense form (i.e., uses less vertical space).
/// If true, [errorText] is not shown.
///
/// Defaults to false.
final bool isDense;
/// Whether this input field should focus itself if nothing else is already focused.
/// If true, the keyboard will open as soon as this input obtains focus. Otherwise,
/// the keyboard is only shown after the user taps the text field.
///
/// Defaults to false.
// See https://github.com/flutter/flutter/issues/7035 for the rationale for this
// keyboard behavior.
final bool autofocus;
/// The maximum number of lines for the text to span, wrapping if necessary.
/// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead.
final int maxLines;
/// Called when the text being edited changes.
///
/// The [value] must be updated each time [onChanged] is invoked.
final ValueChanged<InputValue> onChanged;
/// Called when the user indicates that they are done editing the text in the field.
final ValueChanged<InputValue> onSubmitted;
@override
_InputState createState() => new _InputState();
}
class _InputState extends State<Input> {
final GlobalKey<_InputFieldState> _inputFieldKey = new GlobalKey<_InputFieldState>();
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
@override
void dispose() {
_focusNode?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bool isEmpty = (config.value ?? InputValue.empty).text.isEmpty;
final FocusNode focusNode = _effectiveFocusNode;
return new GestureDetector(
onTap: () {
_inputFieldKey.currentState?.requestKeyboard();
},
child: new AnimatedBuilder(
animation: focusNode,
builder: (BuildContext context, Widget child) {
return new InputContainer(
focused: focusNode.hasFocus,
isEmpty: isEmpty,
icon: config.icon,
labelText: config.labelText,
hintText: config.hintText,
errorText: config.errorText,
style: config.style,
isDense: config.isDense,
showDivider: config.showDivider,
child: child,
);
},
child: new InputField(
key: _inputFieldKey,
focusNode: focusNode,
value: config.value,
style: config.style,
obscureText: config.obscureText,
maxLines: config.maxLines,
autofocus: config.autofocus,
keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
),
),
);
}
}
/// A [FormField] that contains an [Input].
///
/// This is a convenience widget that simply wraps an [Input] widget in a
/// [FormField]. The [FormField] maintains the current value of the [Input] so
/// that you don't need to manage it yourself.
///
/// A [Form] ancestor is not required. The [Form] simply makes it easier to
/// save, reset, or validate multiple fields at once. To use without a [Form],
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
/// save or reset the form field.
///
/// To see the use of [TextField], compare these two ways of a implementing
/// a simple two text field form.
///
/// Using [TextField]:
///
/// ```dart
/// String _firstName, _lastName;
/// GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
/// ...
/// new Form(
/// key: _formKey,
/// child: new Row(
/// children: <Widget>[
/// new TextField(
/// labelText: 'First Name',
/// onSaved: (InputValue value) { _firstName = value.text; }
/// ),
/// new TextField(
/// labelText: 'Last Name',
/// onSaved: (InputValue value) { _lastName = value.text; }
/// ),
/// new RaisedButton(
/// child: new Text('SUBMIT'),
/// // Instead of _formKey.currentState, you could wrap the
/// // RaisedButton in a Builder widget to get access to a BuildContext,
/// // and use Form.of(context).
/// onPressed: () { _formKey.currentState.save(); },
/// ),
/// )
/// )
/// ```
///
/// Using [Input] directly:
///
/// ```dart
/// String _firstName, _lastName;
/// InputValue _firstNameValue = const InputValue();
/// InputValue _lastNameValue = const InputValue();
/// ...
/// new Row(
/// children: <Widget>[
/// new Input(
/// value: _firstNameValue,
/// labelText: 'First Name',
/// onChanged: (InputValue value) { setState( () { _firstNameValue = value; } ); }
/// ),
/// new Input(
/// value: _lastNameValue,
/// labelText: 'Last Name',
/// onChanged: (InputValue value) { setState( () { _lastNameValue = value; } ); }
/// ),
/// new RaisedButton(
/// child: new Text('SUBMIT'),
/// onPressed: () {
/// _firstName = _firstNameValue.text;
/// _lastName = _lastNameValue.text;
/// },
/// ),
/// )
/// ```
class TextField extends FormField<InputValue> {
TextField({
Key key,
FocusNode focusNode,
TextInputType keyboardType: TextInputType.text,
Icon icon,
String labelText,
String hintText,
TextStyle style,
bool obscureText: false,
bool isDense: false,
bool autofocus: false,
int maxLines: 1,
InputValue initialValue: InputValue.empty,
FormFieldSetter<InputValue> onSaved,
FormFieldValidator<InputValue> validator,
ValueChanged<InputValue> onChanged,
}) : super(
key: key,
initialValue: initialValue,
onSaved: onSaved,
validator: validator,
builder: (FormFieldState<InputValue> field) {
return new Input(
focusNode: focusNode,
keyboardType: keyboardType,
icon: icon,
labelText: labelText,
hintText: hintText,
style: style,
obscureText: obscureText,
isDense: isDense,
autofocus: autofocus,
maxLines: maxLines,
value: field.value,
onChanged: (InputValue value) {
field.onChanged(value);
if (onChanged != null)
onChanged(value);
},
errorText: field.errorText,
);
},
);
}
// Helper widget to smoothly animate the labelText of an Input, as it
// transitions between inline and caption.
class _AnimatedLabel extends ImplicitlyAnimatedWidget {
_AnimatedLabel({
Key key,
this.text,
@required this.style,
Curve curve: Curves.linear,
Duration duration,
}) : super(key: key, curve: curve, duration: duration) {
assert(style != null);
}
final String text;
final TextStyle style;
@override
_AnimatedLabelState createState() => new _AnimatedLabelState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
'$style'.split('\n').forEach(description.add);
}
}
class _AnimatedLabelState extends AnimatedWidgetBaseState<_AnimatedLabel> {
TextStyleTween _style;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_style = visitor(_style, config.style, (dynamic value) => new TextStyleTween(begin: value));
}
@override
Widget build(BuildContext context) {
TextStyle style = _style.evaluate(animation);
double scale = 1.0;
if (style.fontSize != config.style.fontSize) {
// While the fontSize is transitioning, use a scaled Transform as a
// fraction of the original fontSize. That way we get a smooth scaling
// effect with no snapping between discrete font sizes.
scale = style.fontSize / config.style.fontSize;
style = style.copyWith(fontSize: config.style.fontSize);
}
return new Transform(
transform: new Matrix4.identity()..scale(scale),
child: new Text(
config.text,
style: style,
)
);
}
}
// Copyright 2015 The Chromium 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 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'debug.dart';
import 'icon.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'theme.dart';
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
/// Text and styles used to label an input field.
///
/// See also:
///
/// * [TextField], which is a text input widget that uses an
/// [InputDecoration].
/// * [InputDecorator], which is a widget that draws an [InputDecoration]
/// around an arbitrary child widget.
class InputDecoration {
/// Creates a bundle of text and styles used to label an input field.
///
/// Sets the [isCollapsed] property to false. To create a decoration that does
/// not reserve space for [labelText] or [errorText], use
/// [InputDecoration.collapsed].
const InputDecoration({
this.icon,
this.labelText,
this.labelStyle,
this.hintText,
this.hintStyle,
this.errorText,
this.errorStyle,
this.isDense: false,
this.hideDivider: false,
}) : isCollapsed = false;
/// Creates a decoration that is the same size as the input field.
///
/// This type of input decoration does not include a divider or an icon and
/// does not reserve space for [labelText] or [errorText].
///
/// Sets the [isCollapsed] property to true.
const InputDecoration.collapsed({
@required this.hintText,
this.hintStyle,
}) : icon = null,
labelText = null,
labelStyle = null,
errorText = null,
errorStyle = null,
isDense = false,
isCollapsed = true,
hideDivider = true;
/// An icon to show before the input field.
///
/// The size and color of the icon is configured automatically using an
/// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget.
///
/// See [Icon], [ImageIcon].
final Widget icon;
/// Text that describes the input field.
///
/// When the input field is empty and unfocused, the label is displayed on
/// top of the input field (i.e., at the same location on the screen where
/// text my be entered in the input field). When the input field receives
/// focus (or if the field is non-empty), the label moves above (i.e.,
/// vertically adjacent to) the input field.
final String labelText;
/// The style to use for the [labelText] when the label is above (i.e.,
/// vertically adjacent to) the input field.
///
/// When the [labelText] is on top of the input field, the text uses the
/// [hintStyle] instead.
///
/// If null, defaults of a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
final TextStyle labelStyle;
/// Text that suggests what sort of input the field accepts.
///
/// Displayed on top of the input field (i.e., at the same location on the
/// screen where text my be entered in the input field) when the input field
/// is empty and either (a) [labelText] is null or (b) the input field has
/// focus.
final String hintText;
/// The style to use for the [hintText].
///
/// Also used for the [labelText] when the [labelText] is displayed on
/// top of the input field (i.e., at the same location on the screen where
/// text my be entered in the input field).
///
/// If null, defaults of a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
final TextStyle hintStyle;
/// Text that appears below the input field.
///
/// If non-null the divider, that appears below the input field is red.
final String errorText;
/// The style to use for the [errorText.
///
/// If null, defaults of a value derived from the base [TextStyle] for the
/// input field and the current [Theme].
final TextStyle errorStyle;
/// Whether the input field is part of a dense form (i.e., uses less vertical
/// space).
///
/// Defaults to false.
final bool isDense;
/// Whether the decoration is the same size as the input field.
///
/// A collapsed decoration cannot have [labelText], [errorText], an [icon], or
/// a divider because those elements require extra space.
///
/// To create a collapsed input decoration, use [InputDecoration..collapsed].
final bool isCollapsed;
/// Whether to hide the divider below the input field and above the error text.
///
/// Defaults to false.
final bool hideDivider;
/// Creates a copy of this input decoration but with the given fields replaced
/// with the new values.
///
/// Always sets [isCollapsed] to false.
InputDecoration copyWith({
Widget icon,
String labelText,
TextStyle labelStyle,
String hintText,
TextStyle hintStyle,
String errorText,
TextStyle errorStyle,
bool isDense,
bool hideDivider,
}) {
return new InputDecoration(
icon: icon ?? this.icon,
labelText: labelText ?? this.labelText,
labelStyle: labelStyle ?? this.labelStyle,
hintText: hintText ?? this.hintText,
hintStyle: hintStyle ?? this.hintStyle,
errorText: errorText ?? this.errorText,
errorStyle: errorStyle ?? this.errorStyle,
isDense: isDense ?? this.isDense,
hideDivider: hideDivider ?? this.hideDivider,
);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
final InputDecoration typedOther = other;
return typedOther.icon == icon
&& typedOther.labelText == labelText
&& typedOther.labelStyle == labelStyle
&& typedOther.hintText == hintText
&& typedOther.hintStyle == hintStyle
&& typedOther.errorText == errorText
&& typedOther.errorStyle == errorStyle
&& typedOther.isDense == isDense
&& typedOther.isCollapsed == isCollapsed
&& typedOther.hideDivider == hideDivider;
}
@override
int get hashCode {
return hashValues(
icon,
labelText,
labelStyle,
hintText,
hintStyle,
errorText,
errorStyle,
isDense,
isCollapsed,
hideDivider,
);
}
@override
String toString() {
final List<String> description = <String>[];
if (icon != null)
description.add('icon: $icon');
if (labelText != null)
description.add('labelText: "$labelText"');
if (hintText != null)
description.add('hintText: "$hintText"');
if (errorText != null)
description.add('errorText: "$errorText"');
if (isDense)
description.add('isDense: $isDense');
if (isCollapsed)
description.add('isCollapsed: $isCollapsed');
if (hideDivider)
description.add('hideDivider: $hideDivider');
return 'InputDecoration(${description.join(', ')})';
}
}
/// Displays the visual elements of a Material Design text field around an
/// arbitrary widget.
///
/// Use [InputDecorator] to create widgets that look and behave like a
/// [TextField] but can be used to input information other than text.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [TextField], which uses an [InputDecorator] to draw labels and other
/// visual elements around a text entry widget.
class InputDecorator extends StatelessWidget {
/// Creates a widget that displayes labels and other visual elements similar
/// to a [TextField].
InputDecorator({
Key key,
@required this.decoration,
this.baseStyle,
this.isFocused: false,
this.isEmpty: false,
this.child,
}) : super(key: key);
/// The text and styles to use when decorating the child.
final InputDecoration decoration;
/// The style on which to base the label, hint, and error styles if the
/// [decoration] does not provide explicit styles.
///
/// If null, defaults to a text style from the current [Theme].
final TextStyle baseStyle;
/// Whether the input field has focus.
///
/// Determines the position of the label text and the color of the divider.
///
/// Defaults to false.
final bool isFocused;
/// Whether the input field is empty.
///
/// Determines the position of the label text and whether to display the hint
/// text.
///
/// Defaults to false.
final bool isEmpty;
/// The widget below this widget in the tree.
final Widget child;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('decoration: $decoration');
description.add('baseStyle: $baseStyle');
description.add('isFocused: $isFocused');
description.add('isEmpty: $isEmpty');
}
Color _getActiveColor(ThemeData themeData) {
if (isFocused) {
switch (themeData.brightness) {
case Brightness.dark:
return themeData.accentColor;
case Brightness.light:
return themeData.primaryColor;
}
}
return themeData.hintColor;
}
Widget _buildContent(Color borderColor, double topPadding, bool isDense) {
final double bottomPadding = isDense ? 8.0 : 1.0;
const double bottomBorder = 2.0;
final double bottomHeight = isDense ? 14.0 : 18.0;
final EdgeInsets padding = new EdgeInsets.only(top: topPadding, bottom: bottomPadding);
final EdgeInsets margin = new EdgeInsets.only(bottom: bottomHeight - (bottomPadding + bottomBorder));
if (decoration.hideDivider) {
return new Container(
margin: margin + new EdgeInsets.only(bottom: bottomBorder),
padding: padding,
child: child,
);
}
return new AnimatedContainer(
margin: margin,
padding: padding,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
decoration: new BoxDecoration(
border: new Border(
bottom: new BorderSide(
color: borderColor,
width: bottomBorder,
),
),
),
child: child,
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
final bool isDense = decoration.isDense;
final bool isCollapsed = decoration.isCollapsed;
assert(!isDense || !isCollapsed);
final String labelText = decoration.labelText;
final String hintText = decoration.hintText;
final String errorText = decoration.errorText;
final TextStyle baseStyle = this.baseStyle ?? themeData.textTheme.subhead;
final TextStyle hintStyle = decoration.hintStyle ?? baseStyle.copyWith(color: themeData.hintColor);
final Color activeColor = _getActiveColor(themeData);
double topPadding = isDense ? 12.0 : 16.0;
final List<Widget> stackChildren = <Widget>[];
// If we're not focused, there's not value, and labelText was provided,
// then the label appears where the hint would. And we will not show
// the hintText.
final bool hasInlineLabel = !isFocused && labelText != null && isEmpty;
if (labelText != null) {
assert(!isCollapsed);
final TextStyle labelStyle = hasInlineLabel ?
hintStyle : (decoration.labelStyle ?? themeData.textTheme.caption.copyWith(color: activeColor));
final double topPaddingIncrement = themeData.textTheme.caption.fontSize + (isDense ? 4.0 : 8.0);
double top = topPadding;
if (hasInlineLabel)
top += topPaddingIncrement + baseStyle.fontSize - labelStyle.fontSize;
stackChildren.add(
new AnimatedPositioned(
left: 0.0,
top: top,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new _AnimatedLabel(
text: labelText,
style: labelStyle,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
),
),
);
topPadding += topPaddingIncrement;
}
if (hintText != null) {
stackChildren.add(
new Positioned(
left: 0.0,
top: topPadding + baseStyle.fontSize - hintStyle.fontSize,
child: new AnimatedOpacity(
opacity: (isEmpty && !hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
child: new Text(hintText, style: hintStyle),
),
),
);
}
if (isCollapsed) {
stackChildren.add(child);
} else {
final Color borderColor = errorText == null ? activeColor : themeData.errorColor;
stackChildren.add(_buildContent(borderColor, topPadding, isDense));
}
if (!isDense && errorText != null) {
assert(!isCollapsed);
final TextStyle errorStyle = decoration.errorStyle ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor);
stackChildren.add(new Positioned(
left: 0.0,
bottom: 0.0,
child: new Text(errorText, style: errorStyle)
));
}
Widget result = new Stack(children: stackChildren);
if (decoration.icon != null) {
assert(!isCollapsed);
final double iconSize = isDense ? 18.0 : 24.0;
final double iconTop = topPadding + (baseStyle.fontSize - iconSize) / 2.0;
result = new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: new EdgeInsets.only(top: iconTop),
width: isDense ? 40.0 : 48.0,
child: new IconTheme.merge(
context: context,
data: new IconThemeData(
color: isFocused ? activeColor : Colors.black45,
size: isDense ? 18.0 : 24.0,
),
child: decoration.icon,
),
),
new Expanded(child: result),
],
);
}
return result;
}
}
// Smoothly animate the label of an InputDecorator as the label
// transitions between inline and caption.
class _AnimatedLabel extends ImplicitlyAnimatedWidget {
_AnimatedLabel({
Key key,
this.text,
@required this.style,
Curve curve: Curves.linear,
@required Duration duration,
}) : super(key: key, curve: curve, duration: duration) {
assert(style != null);
}
final String text;
final TextStyle style;
@override
_AnimatedLabelState createState() => new _AnimatedLabelState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
'$style'.split('\n').forEach(description.add);
}
}
class _AnimatedLabelState extends AnimatedWidgetBaseState<_AnimatedLabel> {
TextStyleTween _style;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_style = visitor(_style, config.style, (dynamic value) => new TextStyleTween(begin: value));
}
@override
Widget build(BuildContext context) {
TextStyle style = _style.evaluate(animation);
double scale = 1.0;
if (style.fontSize != config.style.fontSize) {
// While the fontSize is transitioning, use a scaled Transform as a
// fraction of the original fontSize. That way we get a smooth scaling
// effect with no snapping between discrete font sizes.
scale = style.fontSize / config.style.fontSize;
style = style.copyWith(fontSize: config.style.fontSize);
}
return new Transform(
transform: new Matrix4.identity()..scale(scale),
child: new Text(
config.text,
style: style,
),
);
}
}
// Copyright 2015 The Chromium 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 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'input_decorator.dart';
import 'text_selection.dart';
import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType;
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
/// A simple undecorated text input field.
///
/// If you want decorations as specified in the Material spec (most likely),
/// use [Input] instead.
///
/// This widget is comparable to [Text] in that it does not include a margin
/// or any decoration outside the text itself. It is useful for applications,
/// like a search box, that don't need any additional decoration. It should
/// also be useful in custom widgets that support text input.
///
/// The [value] field must be updated each time the [onChanged] callback is
/// invoked. Be sure to include the full [value] provided by the [onChanged]
/// callback, or information like the current selection will be lost.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [Input], which adds a label, a divider below the text field, and support for
/// an error message.
/// A Material Design text field.
///
/// A text field lets the user enter text, either with hardware keyboard or with
/// an onscreen keyboard.
///
/// The text field calls the [onChanged] callback whenever the user changes the
/// text in the field. If the user indicates that they are done typing in the
/// field (e.g., by pressing a button on the soft keyboard), the text field
/// calls the [onSubmitted] callback.
///
/// To control the text that is displayed in the text field, use the
/// [controller]. For example, to set the initial value of the text field, use
/// a [controller] that already contains some text. The [controller] can also
/// control the selection and composing region (and to observe changes to the
/// text, selection, and composing region).
///
/// By default, a text field has a [decoration] that draws a divider below the
/// text field. You can use the [decoration] property to control the decoration,
/// for example by adding a label or an icon. If you set the [decoration]
/// property to null, the decoration will be removed entirely, including the
/// extra padding introduced by the decoration to save space for the labels.
///
/// If [decoration] is non-null (which is the default), the text field requires
/// one of its ancestors to be a [Material] widget.
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
/// See also:
///
/// * <https://material.google.com/components/text-fields.html>
/// * [TextFormField], which integrates with the [Form] widget.
/// * [InputDecorator], which shows the labels and other visual elements that
/// surround the actual text editing widget.
/// * [EditableText], which is the raw text editing control at the heart of a
/// [TextField]. (The [EditableText] widget is rarely used directly unless
/// you are implementing an entirely different design language, such as
/// Cupertino.)
class TextField extends StatefulWidget {
/// Creates a Material Design text field.
///
/// If [decoration] is non-null (which is the default), the text field requires
/// one of its ancestors to be a [Material] widget.
///
/// To remove the decoration entirely (including the extra padding introduced
/// by the decoration to save space for the labels), set the [decoration] to
/// null.
TextField({
Key key,
this.controller,
this.focusNode,
this.decoration: const InputDecoration(),
this.keyboardType: TextInputType.text,
this.style,
this.autofocus: false,
this.obscureText: false,
this.maxLines: 1,
this.onChanged,
this.onSubmitted,
}) : super(key: key);
/// Controls the text being edited.
///
/// If null, this widget will creates its own [TextEditingController].
final TextEditingController controller;
/// Controls whether this widget has keyboard focus.
///
/// If null, this widget will create its own [FocusNode].
final FocusNode focusNode;
/// The decoration to show around the text field.
///
/// By default, draws a horizontal line under the input field but can be
/// configured to show an icon, label, hint text, and error text.
///
/// Set this field to null to remove the decoration entirely (including the
/// extra padding introduced by the decoration to save space for the labels).
final InputDecoration decoration;
/// The type of keyboard to use for editing the text.
final TextInputType keyboardType;
/// The style to use for the text being edited.
///
/// This text style is also used as the base style for the [decoration].
///
/// If null, defaults to a text style from the current [Theme].
final TextStyle style;
/// Whether this input field should focus itself if nothing else is already
/// focused.
///
/// If true, the keyboard will open as soon as this input obtains focus.
/// Otherwise, the keyboard is only shown after the user taps the text field.
///
/// Defaults to false.
// See https://github.com/flutter/flutter/issues/7035 for the rationale for this
// keyboard behavior.
final bool autofocus;
/// Whether to hide the text being edited (e.g., for passwords).
///
/// When this is set to true, all the characters in the input are replaced by
/// U+2022 BULLET characters (•).
///
/// Defaults to false.
final bool obscureText;
/// The maximum number of lines for the text to span, wrapping if necessary.
///
/// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead.
final int maxLines;
/// Called when the text being edited changes.
final ValueChanged<String> onChanged;
/// Called when the user indicates that they are done editing the text in the
/// field.
final ValueChanged<String> onSubmitted;
@override
_TextFieldState createState() => new _TextFieldState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (controller != null)
description.add('controller: $controller');
if (focusNode != null)
description.add('focusNode: $focusNode');
description.add('decoration: $decoration');
if (keyboardType != TextInputType.text)
description.add('keyboardType: $keyboardType');
if (style != null)
description.add('style: $style');
if (autofocus)
description.add('autofocus: $autofocus');
if (obscureText)
description.add('obscureText: $obscureText');
if (maxLines != 1)
description.add('maxLines: $maxLines');
}
}
class _TextFieldState extends State<TextField> {
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
TextEditingController _controller;
TextEditingController get _effectiveController => config.controller ?? _controller;
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
@override
void initState() {
super.initState();
if (config.controller == null)
_controller = new TextEditingController();
}
@override
void didUpdateConfig(TextField oldConfig) {
if (config.controller == null && oldConfig.controller != null)
_controller == new TextEditingController.fromValue(oldConfig.controller.value);
else if (config.controller != null && oldConfig.controller == null)
_controller = null;
}
@override
void dispose() {
_focusNode?.dispose();
super.dispose();
}
void _requestKeyboard() {
_editableTextKey.currentState?.requestKeyboard();
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TextStyle style = config.style ?? themeData.textTheme.subhead;
final TextEditingController controller = _effectiveController;
final FocusNode focusNode = _effectiveFocusNode;
Widget child = new RepaintBoundary(
child: new EditableText(
key: _editableTextKey,
controller: controller,
focusNode: focusNode,
keyboardType: config.keyboardType,
style: style,
autofocus: config.autofocus,
obscureText: config.obscureText,
maxLines: config.maxLines,
cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor,
selectionControls: materialTextSelectionControls,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
),
);
if (config.decoration != null) {
child = new AnimatedBuilder(
animation: new Listenable.merge(<Listenable>[ focusNode, controller ]),
builder: (BuildContext context, Widget child) {
return new InputDecorator(
decoration: config.decoration,
baseStyle: config.style,
isFocused: focusNode.hasFocus,
isEmpty: controller.value.text.isEmpty,
child: child,
);
},
child: child,
);
}
return new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _requestKeyboard,
child: child,
);
}
}
// Copyright 2015 The Chromium 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 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'input_decorator.dart';
import 'text_field.dart';
/// A [FormField] that contains a [TextField].
///
/// This is a convenience widget that simply wraps a [TextField] widget in a
/// [FormField].
///
/// A [Form] ancestor is not required. The [Form] simply makes it easier to
/// save, reset, or validate multiple fields at once. To use without a [Form],
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
/// save or reset the form field.
///
/// See also:
///
/// * <https://material.google.com/components/text-fields.html>
/// * [TextField], which is the underlying text field without the [Form]
/// integration.
/// * [InputDecorator], which shows the labels and other visual elements that
/// surround the actual text editing widget.
class TextFormField extends FormField<String> {
TextFormField({
Key key,
TextEditingController controller,
FocusNode focusNode,
InputDecoration decoration: const InputDecoration(),
TextInputType keyboardType: TextInputType.text,
TextStyle style,
bool autofocus: false,
bool obscureText: false,
int maxLines: 1,
FormFieldSetter<String> onSaved,
FormFieldValidator<String> validator,
}) : super(
key: key,
initialValue: controller != null ? controller.value.text : '',
onSaved: onSaved,
validator: validator,
builder: (FormFieldState<String> field) {
return new TextField(
controller: controller,
focusNode: focusNode,
decoration: decoration.copyWith(errorText: field.errorText),
keyboardType: keyboardType,
style: style,
autofocus: autofocus,
obscureText: obscureText,
maxLines: maxLines,
onChanged: (String value) {
field.onChanged(value);
},
);
},
);
}
...@@ -20,7 +20,7 @@ class _TextSelectionToolbar extends StatelessWidget { ...@@ -20,7 +20,7 @@ class _TextSelectionToolbar extends StatelessWidget {
_TextSelectionToolbar(this.delegate, {Key key}) : super(key: key); _TextSelectionToolbar(this.delegate, {Key key}) : super(key: key);
final TextSelectionDelegate delegate; final TextSelectionDelegate delegate;
InputValue get value => delegate.inputValue; TextEditingValue get value => delegate.textEditingValue;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -51,7 +51,7 @@ class _TextSelectionToolbar extends StatelessWidget { ...@@ -51,7 +51,7 @@ class _TextSelectionToolbar extends StatelessWidget {
void _handleCut() { void _handleCut() {
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text))); Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
delegate.inputValue = new InputValue( delegate.textEditingValue = new TextEditingValue(
text: value.selection.textBefore(value.text) + value.selection.textAfter(value.text), text: value.selection.textBefore(value.text) + value.selection.textAfter(value.text),
selection: new TextSelection.collapsed(offset: value.selection.start) selection: new TextSelection.collapsed(offset: value.selection.start)
); );
...@@ -60,7 +60,7 @@ class _TextSelectionToolbar extends StatelessWidget { ...@@ -60,7 +60,7 @@ class _TextSelectionToolbar extends StatelessWidget {
void _handleCopy() { void _handleCopy() {
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text))); Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
delegate.inputValue = new InputValue( delegate.textEditingValue = new TextEditingValue(
text: value.text, text: value.text,
selection: new TextSelection.collapsed(offset: value.selection.end) selection: new TextSelection.collapsed(offset: value.selection.end)
); );
...@@ -68,10 +68,10 @@ class _TextSelectionToolbar extends StatelessWidget { ...@@ -68,10 +68,10 @@ class _TextSelectionToolbar extends StatelessWidget {
} }
Future<Null> _handlePaste() async { Future<Null> _handlePaste() async {
final InputValue value = this.value; // Snapshot the input before using `await`. final TextEditingValue value = this.value; // Snapshot the input before using `await`.
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain); final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) { if (data != null) {
delegate.inputValue = new InputValue( delegate.textEditingValue = new TextEditingValue(
text: value.selection.textBefore(value.text) + data.text + value.selection.textAfter(value.text), text: value.selection.textBefore(value.text) + data.text + value.selection.textAfter(value.text),
selection: new TextSelection.collapsed(offset: value.selection.start + data.text.length) selection: new TextSelection.collapsed(offset: value.selection.start + data.text.length)
); );
...@@ -80,7 +80,7 @@ class _TextSelectionToolbar extends StatelessWidget { ...@@ -80,7 +80,7 @@ class _TextSelectionToolbar extends StatelessWidget {
} }
void _handleSelectAll() { void _handleSelectAll() {
delegate.inputValue = new InputValue( delegate.textEditingValue = new TextEditingValue(
text: value.text, text: value.text,
selection: new TextSelection(baseOffset: 0, extentOffset: value.text.length) selection: new TextSelection(baseOffset: 0, extentOffset: value.text.length)
); );
...@@ -196,4 +196,4 @@ class _MaterialTextSelectionControls extends TextSelectionControls { ...@@ -196,4 +196,4 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
} }
} }
final _MaterialTextSelectionControls materialTextSelectionControls = new _MaterialTextSelectionControls(); final TextSelectionControls materialTextSelectionControls = new _MaterialTextSelectionControls();
...@@ -44,7 +44,7 @@ class MethodCall { ...@@ -44,7 +44,7 @@ class MethodCall {
final dynamic arguments; final dynamic arguments;
@override @override
bool operator== (dynamic other) { bool operator == (dynamic other) {
if (identical(this, other)) if (identical(this, other))
return true; return true;
if (runtimeType != other.runtimeType) if (runtimeType != other.runtimeType)
......
...@@ -7,6 +7,7 @@ import 'dart:async'; ...@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
...@@ -18,91 +19,48 @@ import 'scroll_physics.dart'; ...@@ -18,91 +19,48 @@ import 'scroll_physics.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'text_selection.dart'; import 'text_selection.dart';
export 'package:flutter/services.dart' show TextSelection, TextInputType; export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType;
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500); const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
InputValue _getInputValueFromEditingValue(TextEditingValue value) { class TextEditingController extends ChangeNotifier {
return new InputValue( TextEditingController({ String text })
text: value.text, : _value = text == null ? TextEditingValue.empty : new TextEditingValue(text: text);
selection: value.selection,
composing: value.composing,
);
}
TextEditingValue _getTextEditingValueFromInputValue(InputValue value) {
return new TextEditingValue(
text: value.text,
selection: value.selection,
composing: value.composing,
);
}
/// Configuration information for a text input field.
///
/// An [InputValue] contains the text for the input field as well as the
/// selection extent and the composing range.
class InputValue {
// TODO(abarth): This class is really the same as TextEditingState.
// We should merge them into one object.
/// Creates configuration information for an input field TextEditingController.fromValue(TextEditingValue value)
/// : _value = value ?? TextEditingValue.empty;
/// The selection and composing range must be within the text.
///
/// The [text], [selection], and [composing] arguments must not be null but
/// each have default values.
const InputValue({
this.text: '',
this.selection: const TextSelection.collapsed(offset: -1),
this.composing: TextRange.empty
});
/// The current text being edited. TextEditingValue get value => _value;
final String text; TextEditingValue _value;
set value(TextEditingValue newValue) {
/// The range of text that is currently selected. assert(newValue != null);
final TextSelection selection; if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
/// The range of text that is still being composed. String get text => _value.text;
final TextRange composing; set text(String newText) {
value = value.copyWith(text: newText, composing: TextRange.empty);
}
/// An input value that corresponds to the empty string with no selection and no composing range. TextSelection get selection => _value.selection;
static const InputValue empty = const InputValue(); set selection(TextSelection newSelection) {
value = value.copyWith(selection: newSelection, composing: TextRange.empty);
}
@override void clear() {
String toString() => '$runtimeType(text: \u2524$text\u251C, selection: $selection, composing: $composing)'; value = TextEditingValue.empty;
}
@override void clearComposing() {
bool operator ==(dynamic other) { value = value.copyWith(composing: TextRange.empty);
if (identical(this, other))
return true;
if (other is! InputValue)
return false;
final InputValue typedOther = other;
return typedOther.text == text
&& typedOther.selection == selection
&& typedOther.composing == composing;
} }
@override @override
int get hashCode => hashValues( String toString() {
text.hashCode, return '$runtimeType#$hashCode($value)';
selection.hashCode,
composing.hashCode
);
/// Creates a copy of this input value but with the given fields replaced with the new values.
InputValue copyWith({
String text,
TextSelection selection,
TextRange composing
}) {
return new InputValue (
text: text ?? this.text,
selection: selection ?? this.selection,
composing: composing ?? this.composing
);
} }
} }
...@@ -126,11 +84,11 @@ class InputValue { ...@@ -126,11 +84,11 @@ class InputValue {
class EditableText extends StatefulWidget { class EditableText extends StatefulWidget {
/// Creates a basic text input control. /// Creates a basic text input control.
/// ///
/// The [value], [focusNode], [style], and [cursorColor] arguments must not /// The [controller], [focusNode], [style], and [cursorColor] arguments must
/// be null. /// not be null.
EditableText({ EditableText({
Key key, Key key,
@required this.value, @required this.controller,
@required this.focusNode, @required this.focusNode,
this.obscureText: false, this.obscureText: false,
@required this.style, @required this.style,
...@@ -144,7 +102,7 @@ class EditableText extends StatefulWidget { ...@@ -144,7 +102,7 @@ class EditableText extends StatefulWidget {
this.onChanged, this.onChanged,
this.onSubmitted, this.onSubmitted,
}) : super(key: key) { }) : super(key: key) {
assert(value != null); assert(controller != null);
assert(focusNode != null); assert(focusNode != null);
assert(obscureText != null); assert(obscureText != null);
assert(style != null); assert(style != null);
...@@ -153,8 +111,8 @@ class EditableText extends StatefulWidget { ...@@ -153,8 +111,8 @@ class EditableText extends StatefulWidget {
assert(autofocus != null); assert(autofocus != null);
} }
/// The string being displayed in this widget. /// Controls the text being edited.
final InputValue value; final TextEditingController controller;
/// Controls whether this widget has keyboard focus. /// Controls whether this widget has keyboard focus.
final FocusNode focusNode; final FocusNode focusNode;
...@@ -200,10 +158,10 @@ class EditableText extends StatefulWidget { ...@@ -200,10 +158,10 @@ class EditableText extends StatefulWidget {
final TextInputType keyboardType; final TextInputType keyboardType;
/// Called when the text being edited changes. /// Called when the text being edited changes.
final ValueChanged<InputValue> onChanged; final ValueChanged<String> onChanged;
/// Called when the user indicates that they are done editing the text in the field. /// Called when the user indicates that they are done editing the text in the field.
final ValueChanged<InputValue> onSubmitted; final ValueChanged<String> onSubmitted;
@override @override
EditableTextState createState() => new EditableTextState(); EditableTextState createState() => new EditableTextState();
...@@ -214,17 +172,18 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -214,17 +172,18 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
Timer _cursorTimer; Timer _cursorTimer;
bool _showCursor = false; bool _showCursor = false;
InputValue _currentValue;
TextInputConnection _textInputConnection; TextInputConnection _textInputConnection;
TextSelectionOverlay _selectionOverlay; TextSelectionOverlay _selectionOverlay;
final ScrollController _scrollController = new ScrollController(); final ScrollController _scrollController = new ScrollController();
bool _didAutoFocus = false; bool _didAutoFocus = false;
// State lifecycle:
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_currentValue = config.value; config.controller.addListener(_didChangeTextEditingValue);
config.focusNode.addListener(_handleFocusChanged); config.focusNode.addListener(_handleFocusChanged);
} }
...@@ -240,10 +199,11 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -240,10 +199,11 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
@override @override
void didUpdateConfig(EditableText oldConfig) { void didUpdateConfig(EditableText oldConfig) {
if (_currentValue != config.value) { if (config.controller != oldConfig.controller) {
_currentValue = config.value; oldConfig.controller.removeListener(_didChangeTextEditingValue);
if (_isAttachedToKeyboard) config.controller.addListener(_didChangeTextEditingValue);
_textInputConnection.setEditingState(_getTextEditingValueFromInputValue(_currentValue)); if (_isAttachedToKeyboard && config.controller.value != oldConfig.controller.value)
_textInputConnection.setEditingState(config.controller.value);
} }
if (config.focusNode != oldConfig.focusNode) { if (config.focusNode != oldConfig.focusNode) {
oldConfig.focusNode.removeListener(_handleFocusChanged); oldConfig.focusNode.removeListener(_handleFocusChanged);
...@@ -251,6 +211,51 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -251,6 +211,51 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
} }
} }
@override
void dispose() {
config.controller.removeListener(_didChangeTextEditingValue);
if (_isAttachedToKeyboard) {
_textInputConnection.close();
_textInputConnection = null;
}
assert(!_isAttachedToKeyboard);
if (_cursorTimer != null)
_stopCursorTimer();
assert(_cursorTimer == null);
_selectionOverlay?.dispose();
_selectionOverlay = null;
config.focusNode.removeListener(_handleFocusChanged);
super.dispose();
}
// TextInputClient implementation:
@override
void updateEditingValue(TextEditingValue value) {
if (value.text != _value.text)
_hideSelectionOverlayIfNeeded();
_value = value;
if (config.onChanged != null)
config.onChanged(value.text);
}
@override
void performAction(TextInputAction action) {
config.controller.clearComposing();
config.focusNode.unfocus();
if (config.onSubmitted != null)
config.onSubmitted(_value.text);
}
TextEditingValue get _value => config.controller.value;
set _value(TextEditingValue value) {
config.controller.value = value;
}
void _didChangeTextEditingValue() {
setState(() { /* We use config.controller.value in build(). */ });
}
bool get _isAttachedToKeyboard => _textInputConnection != null && _textInputConnection.attached; bool get _isAttachedToKeyboard => _textInputConnection != null && _textInputConnection.attached;
bool get _isMultiline => config.maxLines > 1; bool get _isMultiline => config.maxLines > 1;
...@@ -273,24 +278,18 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -273,24 +278,18 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
void _attachOrDetachKeyboard(bool focused) { void _attachOrDetachKeyboard(bool focused) {
if (focused && !_isAttachedToKeyboard && _didRequestKeyboard) { if (focused && !_isAttachedToKeyboard && _didRequestKeyboard) {
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType)) _textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType))
..setEditingState(_getTextEditingValueFromInputValue(_currentValue)) ..setEditingState(_value)
..show(); ..show();
} else if (!focused) { } else if (!focused) {
if (_isAttachedToKeyboard) { if (_isAttachedToKeyboard) {
_textInputConnection.close(); _textInputConnection.close();
_textInputConnection = null; _textInputConnection = null;
} }
_clearComposing(); config.controller.clearComposing();
} }
_didRequestKeyboard = false; _didRequestKeyboard = false;
} }
void _clearComposing() {
// TODO(abarth): We should call config.onChanged to notify our parent of
// this change in our composing range.
_currentValue = _currentValue.copyWith(composing: TextRange.empty);
}
/// Express interest in interacting with the keyboard. /// Express interest in interacting with the keyboard.
/// ///
/// If this control is already attached to the keyboard, this function will /// If this control is already attached to the keyboard, this function will
...@@ -310,69 +309,50 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -310,69 +309,50 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
} }
} }
@override void _hideSelectionOverlayIfNeeded() {
void updateEditingValue(TextEditingValue value) {
_currentValue = _getInputValueFromEditingValue(value);
if (config.onChanged != null)
config.onChanged(_currentValue);
if (_currentValue.text != config.value.text) {
_selectionOverlay?.hide(); _selectionOverlay?.hide();
_selectionOverlay = null; _selectionOverlay = null;
} }
}
@override
void performAction(TextInputAction action) {
_clearComposing();
config.focusNode.unfocus();
if (config.onSubmitted != null)
config.onSubmitted(_currentValue);
}
void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) { void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) {
// Note that this will show the keyboard for all selection changes on the // Note that this will show the keyboard for all selection changes on the
// EditableWidget, not just changes triggered by user gestures. // EditableWidget, not just changes triggered by user gestures.
requestKeyboard(); requestKeyboard();
final InputValue newInput = _currentValue.copyWith(selection: selection, composing: TextRange.empty); _hideSelectionOverlayIfNeeded();
if (config.onChanged != null) config.controller.selection = selection;
config.onChanged(newInput);
if (_selectionOverlay != null) {
_selectionOverlay.hide();
_selectionOverlay = null;
}
if (config.selectionControls != null) { if (config.selectionControls != null) {
_selectionOverlay = new TextSelectionOverlay( _selectionOverlay = new TextSelectionOverlay(
input: newInput,
context: context, context: context,
value: _value,
debugRequiredFor: config, debugRequiredFor: config,
renderObject: renderObject, renderObject: renderObject,
onSelectionOverlayChanged: _handleSelectionOverlayChanged, onSelectionOverlayChanged: _handleSelectionOverlayChanged,
selectionControls: config.selectionControls, selectionControls: config.selectionControls,
); );
if (newInput.text.isNotEmpty || longPress) if (_value.text.isNotEmpty || longPress)
_selectionOverlay.showHandles(); _selectionOverlay.showHandles();
if (longPress) if (longPress)
_selectionOverlay.showToolbar(); _selectionOverlay.showToolbar();
} }
} }
void _handleSelectionOverlayChanged(InputValue newInput, Rect caretRect) { void _handleSelectionOverlayChanged(TextEditingValue value, Rect caretRect) {
assert(!newInput.composing.isValid); // composing range must be empty while selecting assert(!value.composing.isValid); // composing range must be empty while selecting.
if (config.onChanged != null) _value = value;
config.onChanged(newInput);
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect)); _scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
} }
/// Whether the blinking cursor is actually visible at this precise moment /// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks). /// (it's hidden half the time, since it blinks).
@visibleForTesting
bool get cursorCurrentlyVisible => _showCursor; bool get cursorCurrentlyVisible => _showCursor;
/// The cursor blink interval (the amount of time the cursor is in the "on" /// The cursor blink interval (the amount of time the cursor is in the "on"
/// state or the "off" state). A complete cursor blink period is twice this /// state or the "off" state). A complete cursor blink period is twice this
/// value (half on, half off). /// value (half on, half off).
@visibleForTesting
Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod; Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
void _cursorTick(Timer timer) { void _cursorTick(Timer timer) {
...@@ -390,37 +370,21 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -390,37 +370,21 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
final bool focused = config.focusNode.hasFocus; final bool focused = config.focusNode.hasFocus;
_attachOrDetachKeyboard(focused); _attachOrDetachKeyboard(focused);
if (_cursorTimer == null && focused && config.value.selection.isCollapsed) if (_cursorTimer == null && focused && _value.selection.isCollapsed)
_startCursorTimer(); _startCursorTimer();
else if (_cursorTimer != null && (!focused || !config.value.selection.isCollapsed)) else if (_cursorTimer != null && (!focused || !_value.selection.isCollapsed))
_stopCursorTimer(); _stopCursorTimer();
if (_selectionOverlay != null) { if (_selectionOverlay != null) {
if (focused) { if (focused) {
_selectionOverlay.update(config.value); _selectionOverlay.update(_value);
} else { } else {
_selectionOverlay?.dispose(); _selectionOverlay.dispose();
_selectionOverlay = null; _selectionOverlay = null;
} }
} }
} }
@override
void dispose() {
if (_isAttachedToKeyboard) {
_textInputConnection.close();
_textInputConnection = null;
}
assert(!_isAttachedToKeyboard);
if (_cursorTimer != null)
_stopCursorTimer();
assert(_cursorTimer == null);
_selectionOverlay?.dispose();
_selectionOverlay = null;
config.focusNode.removeListener(_handleFocusChanged);
super.dispose();
}
void _stopCursorTimer() { void _stopCursorTimer() {
_cursorTimer.cancel(); _cursorTimer.cancel();
_cursorTimer = null; _cursorTimer = null;
...@@ -436,7 +400,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -436,7 +400,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new _Editable( return new _Editable(
value: _currentValue, value: _value,
style: config.style, style: config.style,
cursorColor: config.cursorColor, cursorColor: config.cursorColor,
showCursor: _showCursor, showCursor: _showCursor,
...@@ -467,7 +431,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -467,7 +431,7 @@ class _Editable extends LeafRenderObjectWidget {
this.onSelectionChanged, this.onSelectionChanged,
}) : super(key: key); }) : super(key: key);
final InputValue value; final TextEditingValue value;
final TextStyle style; final TextStyle style;
final Color cursorColor; final Color cursorColor;
final bool showCursor; final bool showCursor;
......
...@@ -43,22 +43,22 @@ enum TextSelectionHandleType { ...@@ -43,22 +43,22 @@ enum TextSelectionHandleType {
/// [start] handle always moves the [start]/[baseOffset] of the selection. /// [start] handle always moves the [start]/[baseOffset] of the selection.
enum _TextSelectionHandlePosition { start, end } enum _TextSelectionHandlePosition { start, end }
/// Signature for reporting changes to the selection component of an /// Signature for reporting changes to the selection component of a
/// [InputValue] for the purposes of a [TextSelectionOverlay]. The [caretRect] /// [TextEditingValue] for the purposes of a [TextSelectionOverlay]. The
/// argument gives the location of the caret in the coordinate space of the /// [caretRect] argument gives the location of the caret in the coordinate space
/// [RenderBox] given by the [TextSelectionOverlay.renderObject]. /// of the [RenderBox] given by the [TextSelectionOverlay.renderObject].
/// ///
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged]. /// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
typedef void TextSelectionOverlayChanged(InputValue value, Rect caretRect); typedef void TextSelectionOverlayChanged(TextEditingValue value, Rect caretRect);
/// An interface for manipulating the selection, to be used by the implementor /// An interface for manipulating the selection, to be used by the implementor
/// of the toolbar widget. /// of the toolbar widget.
abstract class TextSelectionDelegate { abstract class TextSelectionDelegate {
/// Gets the current text input. /// Gets the current text input.
InputValue get inputValue; TextEditingValue get textEditingValue;
/// Sets the current text input (replaces the whole line). /// Sets the current text input (replaces the whole line).
set inputValue(InputValue value); set textEditingValue(TextEditingValue value);
/// Hides the text selection toolbar. /// Hides the text selection toolbar.
void hideToolbar(); void hideToolbar();
...@@ -89,13 +89,14 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -89,13 +89,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// ///
/// The [context] must not be null and must have an [Overlay] as an ancestor. /// The [context] must not be null and must have an [Overlay] as an ancestor.
TextSelectionOverlay({ TextSelectionOverlay({
InputValue input, @required TextEditingValue value,
@required this.context, @required this.context,
this.debugRequiredFor, this.debugRequiredFor,
this.renderObject, this.renderObject,
this.onSelectionOverlayChanged, this.onSelectionOverlayChanged,
this.selectionControls, this.selectionControls,
}): _input = input { }): _value = value {
assert(value != null);
assert(context != null); assert(context != null);
final OverlayState overlay = Overlay.of(context); final OverlayState overlay = Overlay.of(context);
assert(overlay != null); assert(overlay != null);
...@@ -133,7 +134,7 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -133,7 +134,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
Animation<double> get _handleOpacity => _handleController.view; Animation<double> get _handleOpacity => _handleController.view;
Animation<double> get _toolbarOpacity => _toolbarController.view; Animation<double> get _toolbarOpacity => _toolbarController.view;
InputValue _input; TextEditingValue _value;
/// A pair of handles. If this is non-null, there are always 2, though the /// A pair of handles. If this is non-null, there are always 2, though the
/// second is hidden when the selection is collapsed. /// second is hidden when the selection is collapsed.
...@@ -142,7 +143,7 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -142,7 +143,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// A copy/paste toolbar. /// A copy/paste toolbar.
OverlayEntry _toolbar; OverlayEntry _toolbar;
TextSelection get _selection => _input.selection; TextSelection get _selection => _value.selection;
/// Shows the handles by inserting them into the [context]'s overlay. /// Shows the handles by inserting them into the [context]'s overlay.
void showHandles() { void showHandles() {
...@@ -172,10 +173,10 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -172,10 +173,10 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// synchronously. This means that it is safe to call during builds, but also /// synchronously. This means that it is safe to call during builds, but also
/// that if you do call this during a build, the UI will not update until the /// that if you do call this during a build, the UI will not update until the
/// next frame (i.e. many milliseconds later). /// next frame (i.e. many milliseconds later).
void update(InputValue newInput) { void update(TextEditingValue newValue) {
if (_input == newInput) if (_value == newValue)
return; return;
_input = newInput; _value = newValue;
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild); SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
} else { } else {
...@@ -259,13 +260,13 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -259,13 +260,13 @@ class TextSelectionOverlay implements TextSelectionDelegate {
caretRect = renderObject.getLocalRectForCaret(newSelection.extent); caretRect = renderObject.getLocalRectForCaret(newSelection.extent);
break; break;
} }
update(_input.copyWith(selection: newSelection, composing: TextRange.empty)); update(_value.copyWith(selection: newSelection, composing: TextRange.empty));
if (onSelectionOverlayChanged != null) if (onSelectionOverlayChanged != null)
onSelectionOverlayChanged(_input, caretRect); onSelectionOverlayChanged(_value, caretRect);
} }
void _handleSelectionHandleTapped() { void _handleSelectionHandleTapped() {
if (inputValue.selection.isCollapsed) { if (_value.selection.isCollapsed) {
if (_toolbar != null) { if (_toolbar != null) {
_toolbar?.remove(); _toolbar?.remove();
_toolbar = null; _toolbar = null;
...@@ -276,14 +277,14 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -276,14 +277,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
} }
@override @override
InputValue get inputValue => _input; TextEditingValue get textEditingValue => _value;
@override @override
set inputValue(InputValue value) { set textEditingValue(TextEditingValue newValue) {
update(value); update(newValue);
if (onSelectionOverlayChanged != null) { if (onSelectionOverlayChanged != null) {
final Rect caretRect = renderObject.getLocalRectForCaret(value.selection.extent); final Rect caretRect = renderObject.getLocalRectForCaret(newValue.selection.extent);
onSelectionOverlayChanged(value, caretRect); onSelectionOverlayChanged(newValue, caretRect);
} }
} }
......
...@@ -43,9 +43,9 @@ void main() { ...@@ -43,9 +43,9 @@ void main() {
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(new MaterialApp(
home: new Material( home: new Material(
child: new Center( child: new Center(
child: new Input(focusNode: focusNode, autofocus: true) child: new TextField(focusNode: focusNode, autofocus: true),
) ),
) ),
)); ));
expect(focusNode.hasFocus, isTrue); expect(focusNode.hasFocus, isTrue);
......
...@@ -12,7 +12,7 @@ import 'package:flutter/services.dart'; ...@@ -12,7 +12,7 @@ import 'package:flutter/services.dart';
class MockClipboard { class MockClipboard {
Object _clipboardData = <String, dynamic>{ Object _clipboardData = <String, dynamic>{
'text': null 'text': null,
}; };
Future<dynamic> handleMethodCall(MethodCall methodCall) async { Future<dynamic> handleMethodCall(MethodCall methodCall) async {
...@@ -30,9 +30,9 @@ Widget overlay(Widget child) { ...@@ -30,9 +30,9 @@ Widget overlay(Widget child) {
return new Overlay( return new Overlay(
initialEntries: <OverlayEntry>[ initialEntries: <OverlayEntry>[
new OverlayEntry( new OverlayEntry(
builder: (BuildContext context) => child builder: (BuildContext context) => child,
) ),
] ],
); );
} }
...@@ -74,28 +74,31 @@ void main() { ...@@ -74,28 +74,31 @@ void main() {
return endpoints[0].point + const Offset(0.0, -2.0); return endpoints[0].point + const Offset(0.0, -2.0);
} }
testWidgets('Editable text has consistent size', (WidgetTester tester) async { testWidgets('TextField has consistent size', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final Key textFieldKey = new UniqueKey();
InputValue inputValue = InputValue.empty; String textFieldValue;
Widget builder() { Widget builder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
value: inputValue, key: textFieldKey,
key: inputKey, decoration: new InputDecoration(
hintText: 'Placeholder', hintText: 'Placeholder',
onChanged: (InputValue value) { inputValue = value; } ),
) onChanged: (String value) {
) textFieldValue = value;
}
),
),
); );
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey)); RenderBox findTextFieldBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox(); final RenderBox inputBox = findTextFieldBox();
final Size emptyInputSize = inputBox.size; final Size emptyInputSize = inputBox.size;
Future<Null> checkText(String testValue) async { Future<Null> checkText(String testValue) async {
...@@ -103,31 +106,31 @@ void main() { ...@@ -103,31 +106,31 @@ void main() {
await tester.idle(); await tester.idle();
// Check that the onChanged event handler fired. // Check that the onChanged event handler fired.
expect(inputValue.text, equals(testValue)); expect(textFieldValue, equals(testValue));
return await tester.pumpWidget(builder()); return await tester.pumpWidget(builder());
} }
await checkText(' '); await checkText(' ');
expect(findInputBox(), equals(inputBox)); expect(findTextFieldBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize)); expect(inputBox.size, equals(emptyInputSize));
await checkText('Test'); await checkText('Test');
expect(findInputBox(), equals(inputBox)); expect(findTextFieldBox(), equals(inputBox));
expect(inputBox.size, equals(emptyInputSize)); expect(inputBox.size, equals(emptyInputSize));
}); });
testWidgets('Cursor blinks', (WidgetTester tester) async { testWidgets('Cursor blinks', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey();
Widget builder() { Widget builder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
key: inputKey, decoration: new InputDecoration(
hintText: 'Placeholder' hintText: 'Placeholder',
) ),
) ),
),
); );
} }
...@@ -163,17 +166,16 @@ void main() { ...@@ -163,17 +166,16 @@ void main() {
}); });
testWidgets('obscureText control test', (WidgetTester tester) async { testWidgets('obscureText control test', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey();
Widget builder() { Widget builder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
key: inputKey,
obscureText: true, obscureText: true,
hintText: 'Placeholder' decoration: new InputDecoration(
) hintText: 'Placeholder',
) ),
),
),
); );
} }
...@@ -190,8 +192,7 @@ void main() { ...@@ -190,8 +192,7 @@ void main() {
}); });
testWidgets('Can long press to select', (WidgetTester tester) async { testWidgets('Can long press to select', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final TextEditingController controller = new TextEditingController();
InputValue inputValue = InputValue.empty;
Widget builder() { Widget builder() {
return new Overlay( return new Overlay(
...@@ -200,16 +201,14 @@ void main() { ...@@ -200,16 +201,14 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
value: inputValue, controller: controller,
key: inputKey, ),
onChanged: (InputValue value) { inputValue = value; } ),
)
)
); );
} },
) ),
] ],
); );
} }
...@@ -218,11 +217,11 @@ void main() { ...@@ -218,11 +217,11 @@ void main() {
final String testValue = 'abc def ghi'; final String testValue = 'abc def ghi';
await tester.enterText(find.byType(EditableText), testValue); await tester.enterText(find.byType(EditableText), testValue);
await tester.idle(); await tester.idle();
expect(inputValue.text, testValue); expect(controller.value.text, testValue);
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
// Long press the 'e' to select 'def'. // Long press the 'e' to select 'def'.
final Point ePos = textOffsetToPosition(tester, testValue.indexOf('e')); final Point ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
...@@ -232,32 +231,21 @@ void main() { ...@@ -232,32 +231,21 @@ void main() {
await tester.pump(); await tester.pump();
// 'def' is selected. // 'def' is selected.
expect(inputValue.selection.baseOffset, testValue.indexOf('d')); expect(controller.selection.baseOffset, testValue.indexOf('d'));
expect(inputValue.selection.extentOffset, testValue.indexOf('f')+1); expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
}); });
testWidgets('Can drag handles to change selection', (WidgetTester tester) async { testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final TextEditingController controller = new TextEditingController();
InputValue inputValue = InputValue.empty;
Widget builder() { Widget builder() {
return new Overlay( return overlay(new Center(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) {
return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
value: inputValue, controller: controller,
key: inputKey, ),
onChanged: (InputValue value) { inputValue = value; } ),
) ));
)
);
}
)
]
);
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -275,7 +263,7 @@ void main() { ...@@ -275,7 +263,7 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
final TextSelection selection = inputValue.selection; final TextSelection selection = controller.selection;
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection( final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
...@@ -294,8 +282,8 @@ void main() { ...@@ -294,8 +282,8 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, selection.baseOffset); expect(controller.selection.baseOffset, selection.baseOffset);
expect(inputValue.selection.extentOffset, selection.extentOffset+2); expect(controller.selection.extentOffset, selection.extentOffset+2);
// Drag the left handle 2 letters to the left. // Drag the left handle 2 letters to the left.
handlePos = endpoints[0].point + const Offset(-1.0, 1.0); handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
...@@ -307,32 +295,21 @@ void main() { ...@@ -307,32 +295,21 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, selection.baseOffset-2); expect(controller.selection.baseOffset, selection.baseOffset-2);
expect(inputValue.selection.extentOffset, selection.extentOffset+2); expect(controller.selection.extentOffset, selection.extentOffset+2);
}); });
testWidgets('Can use selection toolbar', (WidgetTester tester) async { testWidgets('Can use selection toolbar', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final TextEditingController controller = new TextEditingController();
InputValue inputValue = InputValue.empty;
Widget builder() { Widget builder() {
return new Overlay( return overlay(new Center(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) {
return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
value: inputValue, controller: controller,
key: inputKey, ),
onChanged: (InputValue value) { inputValue = value; } ),
) ));
)
);
}
)
]
);
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -347,57 +324,46 @@ void main() { ...@@ -347,57 +324,46 @@ void main() {
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
RenderEditable renderEditable = findRenderEditable(tester); RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection( List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
inputValue.selection); controller.selection);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
// SELECT ALL should select all the text. // SELECT ALL should select all the text.
await tester.tap(find.text('SELECT ALL')); await tester.tap(find.text('SELECT ALL'));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, 0); expect(controller.selection.baseOffset, 0);
expect(inputValue.selection.extentOffset, testValue.length); expect(controller.selection.extentOffset, testValue.length);
// COPY should reset the selection. // COPY should reset the selection.
await tester.tap(find.text('COPY')); await tester.tap(find.text('COPY'));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
// Tap again to bring back the menu. // Tap again to bring back the menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
renderEditable = findRenderEditable(tester); renderEditable = findRenderEditable(tester);
endpoints = renderEditable.getEndpointsForSelection(inputValue.selection); endpoints = renderEditable.getEndpointsForSelection(controller.selection);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
// PASTE right before the 'e'. // PASTE right before the 'e'.
await tester.tap(find.text('PASTE')); await tester.tap(find.text('PASTE'));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.text, 'abc d${testValue}ef ghi'); expect(controller.text, 'abc d${testValue}ef ghi');
}); });
testWidgets('Selection toolbar fades in', (WidgetTester tester) async { testWidgets('Selection toolbar fades in', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final TextEditingController controller = new TextEditingController();
InputValue inputValue = InputValue.empty;
Widget builder() { Widget builder() {
return new Overlay( return overlay(new Center(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) {
return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
value: inputValue, controller: controller,
key: inputKey, ),
onChanged: (InputValue value) { inputValue = value; } ),
) ));
)
);
}
)
]
);
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -412,7 +378,7 @@ void main() { ...@@ -412,7 +378,7 @@ void main() {
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection( final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
inputValue.selection); controller.selection);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -432,27 +398,26 @@ void main() { ...@@ -432,27 +398,26 @@ void main() {
}); });
testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async { testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final Key textFieldKey = new UniqueKey();
InputValue inputValue = InputValue.empty;
Widget builder(int maxLines) { Widget builder(int maxLines) {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
value: inputValue, key: textFieldKey,
key: inputKey,
style: const TextStyle(color: Colors.black, fontSize: 34.0), style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: maxLines, maxLines: maxLines,
decoration: new InputDecoration(
hintText: 'Placeholder', hintText: 'Placeholder',
onChanged: (InputValue value) { inputValue = value; } ),
) ),
) ),
); );
} }
await tester.pumpWidget(builder(3)); await tester.pumpWidget(builder(3));
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox(); final RenderBox inputBox = findInputBox();
final Size emptyInputSize = inputBox.size; final Size emptyInputSize = inputBox.size;
...@@ -487,29 +452,18 @@ void main() { ...@@ -487,29 +452,18 @@ void main() {
}); });
testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async { testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final TextEditingController controller = new TextEditingController();
InputValue inputValue = InputValue.empty;
Widget builder() { Widget builder() {
return new Overlay( return overlay(new Center(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) {
return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
value: inputValue, controller: controller,
key: inputKey,
style: const TextStyle(color: Colors.black, fontSize: 34.0), style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: 3, maxLines: 3,
onChanged: (InputValue value) { inputValue = value; } ),
) ),
) ));
);
}
)
]
);
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -537,12 +491,12 @@ void main() { ...@@ -537,12 +491,12 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(inputValue.selection.baseOffset, 76); expect(controller.selection.baseOffset, 76);
expect(inputValue.selection.extentOffset, 81); expect(controller.selection.extentOffset, 81);
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection( final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
inputValue.selection); controller.selection);
expect(endpoints.length, 2); expect(endpoints.length, 2);
// Drag the right handle to the third line, just after 'Third'. // Drag the right handle to the third line, just after 'Third'.
...@@ -555,8 +509,8 @@ void main() { ...@@ -555,8 +509,8 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, 76); expect(controller.selection.baseOffset, 76);
expect(inputValue.selection.extentOffset, 108); expect(controller.selection.extentOffset, 108);
// Drag the left handle to the first line, just after 'First'. // Drag the left handle to the first line, just after 'First'.
handlePos = endpoints[0].point + const Offset(-1.0, 1.0); handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
...@@ -568,39 +522,30 @@ void main() { ...@@ -568,39 +522,30 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, 5); expect(controller.selection.baseOffset, 5);
expect(inputValue.selection.extentOffset, 108); expect(controller.selection.extentOffset, 108);
await tester.tap(find.text('CUT')); await tester.tap(find.text('CUT'));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
expect(inputValue.text, cutValue); expect(controller.text, cutValue);
}, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961 }, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961
testWidgets('Can scroll multiline input', (WidgetTester tester) async { testWidgets('Can scroll multiline input', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final Key textFieldKey = new UniqueKey();
InputValue inputValue = InputValue.empty; final TextEditingController controller = new TextEditingController();
Widget builder() { Widget builder() {
return new Overlay( return overlay(new Center(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) {
return new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
value: inputValue, key: textFieldKey,
key: inputKey, controller: controller,
style: const TextStyle(color: Colors.black, fontSize: 34.0), style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: 2, maxLines: 2,
onChanged: (InputValue value) { inputValue = value; } ),
) ),
) ));
);
}
)
]
);
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -610,7 +555,7 @@ void main() { ...@@ -610,7 +555,7 @@ void main() {
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox(); final RenderBox inputBox = findInputBox();
// Check that the last line of text is not displayed. // Check that the last line of text is not displayed.
...@@ -652,7 +597,7 @@ void main() { ...@@ -652,7 +597,7 @@ void main() {
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection( final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
inputValue.selection); controller.selection);
expect(endpoints.length, 2); expect(endpoints.length, 2);
// Drag the left handle to the first line, just after 'First'. // Drag the left handle to the first line, just after 'First'.
...@@ -675,17 +620,18 @@ void main() { ...@@ -675,17 +620,18 @@ void main() {
}, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961 }, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961
testWidgets('InputField smoke test', (WidgetTester tester) async { testWidgets('InputField smoke test', (WidgetTester tester) async {
InputValue inputValue = InputValue.empty; String textFieldValue;
Widget builder() { Widget builder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new InputField( child: new TextField(
value: inputValue, decoration: null,
hintText: 'Placeholder', onChanged: (String value) {
onChanged: (InputValue value) { inputValue = value; } textFieldValue = value;
) },
) ),
),
); );
} }
...@@ -696,7 +642,7 @@ void main() { ...@@ -696,7 +642,7 @@ void main() {
await tester.idle(); await tester.idle();
// Check that the onChanged event handler fired. // Check that the onChanged event handler fired.
expect(inputValue.text, equals(testValue)); expect(textFieldValue, equals(testValue));
return await tester.pumpWidget(builder()); return await tester.pumpWidget(builder());
} }
...@@ -705,19 +651,20 @@ void main() { ...@@ -705,19 +651,20 @@ void main() {
}); });
testWidgets('InputField with global key', (WidgetTester tester) async { testWidgets('InputField with global key', (WidgetTester tester) async {
final GlobalKey inputFieldKey = new GlobalKey(debugLabel: 'inputFieldKey'); final GlobalKey textFieldKey = new GlobalKey(debugLabel: 'textFieldKey');
InputValue inputValue = InputValue.empty; String textFieldValue;
Widget builder() { Widget builder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new InputField( child: new TextField(
key: inputFieldKey, key: textFieldKey,
value: inputValue, decoration: new InputDecoration(
hintText: 'Placeholder', hintText: 'Placeholder',
onChanged: (InputValue value) { inputValue = value; } ),
) onChanged: (String value) { textFieldValue = value; },
) ),
),
); );
} }
...@@ -728,7 +675,7 @@ void main() { ...@@ -728,7 +675,7 @@ void main() {
await tester.idle(); await tester.idle();
// Check that the onChanged event handler fired. // Check that the onChanged event handler fired.
expect(inputValue.text, equals(testValue)); expect(textFieldValue, equals(testValue));
return await tester.pumpWidget(builder()); return await tester.pumpWidget(builder());
} }
...@@ -736,9 +683,8 @@ void main() { ...@@ -736,9 +683,8 @@ void main() {
checkText('Hello World'); checkText('Hello World');
}); });
testWidgets('InputField with default hintStyle', (WidgetTester tester) async { testWidgets('TextField with default hintStyle', (WidgetTester tester) async {
final InputValue inputValue = InputValue.empty; final TextStyle style = new TextStyle(
final TextStyle textStyle = new TextStyle(
color: Colors.pink[500], color: Colors.pink[500],
fontSize: 10.0, fontSize: 10.0,
); );
...@@ -751,10 +697,11 @@ void main() { ...@@ -751,10 +697,11 @@ void main() {
child: new Theme( child: new Theme(
data: themeData, data: themeData,
child: new Material( child: new Material(
child: new InputField( child: new TextField(
value: inputValue, decoration: new InputDecoration(
hintText: 'Placeholder', hintText: 'Placeholder',
style: textStyle, ),
style: style,
), ),
), ),
), ),
...@@ -765,11 +712,10 @@ void main() { ...@@ -765,11 +712,10 @@ void main() {
final Text hintText = tester.widget(find.text('Placeholder')); final Text hintText = tester.widget(find.text('Placeholder'));
expect(hintText.style.color, themeData.hintColor); expect(hintText.style.color, themeData.hintColor);
expect(hintText.style.fontSize, textStyle.fontSize); expect(hintText.style.fontSize, style.fontSize);
}); });
testWidgets('InputField with specified hintStyle', (WidgetTester tester) async { testWidgets('TextField with specified hintStyle', (WidgetTester tester) async {
final InputValue inputValue = InputValue.empty;
final TextStyle hintStyle = new TextStyle( final TextStyle hintStyle = new TextStyle(
color: Colors.pink[500], color: Colors.pink[500],
fontSize: 10.0, fontSize: 10.0,
...@@ -778,12 +724,13 @@ void main() { ...@@ -778,12 +724,13 @@ void main() {
Widget builder() { Widget builder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new InputField( child: new TextField(
value: inputValue, decoration: new InputDecoration(
hintText: 'Placeholder', hintText: 'Placeholder',
hintStyle: hintStyle, hintStyle: hintStyle,
), ),
), ),
),
); );
} }
...@@ -793,21 +740,25 @@ void main() { ...@@ -793,21 +740,25 @@ void main() {
expect(hintText.style, hintStyle); expect(hintText.style, hintStyle);
}); });
testWidgets('Input label text animates', (WidgetTester tester) async { testWidgets('TextField label text animates', (WidgetTester tester) async {
final GlobalKey inputKey = new GlobalKey(); final Key secondKey = new UniqueKey();
Widget innerBuilder() { Widget innerBuilder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Column( child: new Column(
children: <Widget>[ children: <Widget>[
new Input( new TextField(
decoration: new InputDecoration(
labelText: 'First', labelText: 'First',
), ),
new Input( ),
key: inputKey, new TextField(
key: secondKey,
decoration: new InputDecoration(
labelText: 'Second', labelText: 'Second',
), ),
),
], ],
), ),
), ),
...@@ -820,7 +771,7 @@ void main() { ...@@ -820,7 +771,7 @@ void main() {
Point pos = tester.getTopLeft(find.text('Second')); Point pos = tester.getTopLeft(find.text('Second'));
// Focus the Input. The label should start animating upwards. // Focus the Input. The label should start animating upwards.
await tester.tap(find.byKey(inputKey)); await tester.tap(find.byKey(secondKey));
await tester.idle(); await tester.idle();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
...@@ -839,10 +790,11 @@ void main() { ...@@ -839,10 +790,11 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new Center( new Center(
child: new Material( child: new Material(
child: new Input( child: new TextField(
decoration: new InputDecoration(
icon: new Icon(Icons.phone), icon: new Icon(Icons.phone),
labelText: 'label', labelText: 'label',
value: InputValue.empty, ),
), ),
), ),
), ),
...@@ -850,6 +802,6 @@ void main() { ...@@ -850,6 +802,6 @@ void main() {
final double iconRight = tester.getTopRight(find.byType(Icon)).x; final double iconRight = tester.getTopRight(find.byType(Icon)).x;
expect(iconRight, equals(tester.getTopLeft(find.text('label')).x)); expect(iconRight, equals(tester.getTopLeft(find.text('label')).x));
expect(iconRight, equals(tester.getTopLeft(find.byType(InputField)).x)); expect(iconRight, equals(tester.getTopLeft(find.byType(EditableText)).x));
}); });
} }
...@@ -15,11 +15,11 @@ void main() { ...@@ -15,11 +15,11 @@ void main() {
child: new Material( child: new Material(
child: new Form( child: new Form(
key: formKey, key: formKey,
child: new TextField( child: new TextFormField(
onSaved: (InputValue value) { fieldValue = value.text; }, onSaved: (String value) { fieldValue = value; },
),
),
), ),
)
)
); );
} }
...@@ -47,10 +47,10 @@ void main() { ...@@ -47,10 +47,10 @@ void main() {
child: new Material( child: new Material(
child: new Form( child: new Form(
child: new TextField( child: new TextField(
onChanged: (InputValue value) { fieldValue = value.text; }, onChanged: (String value) { fieldValue = value; },
),
),
), ),
)
)
); );
} }
...@@ -71,8 +71,7 @@ void main() { ...@@ -71,8 +71,7 @@ void main() {
testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async { testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = new GlobalKey<FormState>(); final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
final GlobalKey inputKey = new GlobalKey(); String errorText(String value) => value + '/error';
String errorText(InputValue input) => input.text + '/error';
Widget builder(bool autovalidate) { Widget builder(bool autovalidate) {
return new Center( return new Center(
...@@ -80,12 +79,11 @@ void main() { ...@@ -80,12 +79,11 @@ void main() {
child: new Form( child: new Form(
key: formKey, key: formKey,
autovalidate: autovalidate, autovalidate: autovalidate,
child: new TextField( child: new TextFormField(
key: inputKey,
validator: errorText, validator: errorText,
), ),
) ),
) ),
); );
} }
...@@ -99,10 +97,10 @@ void main() { ...@@ -99,10 +97,10 @@ void main() {
await tester.pumpWidget(builder(false)); await tester.pumpWidget(builder(false));
// We have to manually validate if we're not autovalidating. // We have to manually validate if we're not autovalidating.
expect(find.text(errorText(new InputValue(text: testValue))), findsNothing); expect(find.text(errorText(testValue)), findsNothing);
formKey.currentState.validate(); formKey.currentState.validate();
await tester.pump(); await tester.pump();
expect(find.text(errorText(new InputValue(text: testValue))), findsOneWidget); expect(find.text(errorText(testValue)), findsOneWidget);
// Try again with autovalidation. Should validate immediately. // Try again with autovalidation. Should validate immediately.
formKey.currentState.reset(); formKey.currentState.reset();
...@@ -110,18 +108,18 @@ void main() { ...@@ -110,18 +108,18 @@ void main() {
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(true)); await tester.pumpWidget(builder(true));
expect(find.text(errorText(new InputValue(text: testValue))), findsOneWidget); expect(find.text(errorText(testValue)), findsOneWidget);
} }
await checkErrorText('Test'); await checkErrorText('Test');
await checkErrorText(''); await checkErrorText('');
}); });
testWidgets('Multiple Inputs communicate', (WidgetTester tester) async { testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = new GlobalKey<FormState>(); final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
final GlobalKey<FormFieldState<InputValue>> fieldKey = new GlobalKey<FormFieldState<InputValue>>(); final GlobalKey<FormFieldState<String>> fieldKey = new GlobalKey<FormFieldState<String>>();
// Input 2's validator depends on a input 1's value. // Input 2's validator depends on a input 1's value.
String errorText(InputValue input) => fieldKey.currentState.value?.text.toString() + '/error'; String errorText(String input) => fieldKey.currentState.value?.toString() + '/error';
Widget builder() { Widget builder() {
return new Center( return new Center(
...@@ -131,10 +129,10 @@ void main() { ...@@ -131,10 +129,10 @@ void main() {
autovalidate: true, autovalidate: true,
child: new ListView( child: new ListView(
children: <Widget>[ children: <Widget>[
new TextField( new TextFormField(
key: fieldKey, key: fieldKey,
), ),
new TextField( new TextFormField(
validator: errorText, validator: errorText,
), ),
], ],
...@@ -162,18 +160,19 @@ void main() { ...@@ -162,18 +160,19 @@ void main() {
testWidgets('Provide initial value to input', (WidgetTester tester) async { testWidgets('Provide initial value to input', (WidgetTester tester) async {
final String initialValue = 'hello'; final String initialValue = 'hello';
final GlobalKey<FormFieldState<InputValue>> inputKey = new GlobalKey<FormFieldState<InputValue>>(); final TextEditingController controller = new TextEditingController(text: initialValue);
final GlobalKey<FormFieldState<String>> inputKey = new GlobalKey<FormFieldState<String>>();
Widget builder() { Widget builder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Form( child: new Form(
child: new TextField( child: new TextFormField(
key: inputKey, key: inputKey,
initialValue: new InputValue(text: initialValue), controller: controller,
),
),
), ),
)
)
); );
} }
...@@ -186,20 +185,19 @@ void main() { ...@@ -186,20 +185,19 @@ void main() {
// initial value should also be visible in the raw input line // initial value should also be visible in the raw input line
final EditableTextState editableText = tester.state(find.byType(EditableText)); final EditableTextState editableText = tester.state(find.byType(EditableText));
expect(editableText.config.value.text, equals(initialValue)); expect(editableText.config.controller.text, equals(initialValue));
// sanity check, make sure we can still edit the text and everything updates // sanity check, make sure we can still edit the text and everything updates
expect(inputKey.currentState.value.text, equals(initialValue)); expect(inputKey.currentState.value, equals(initialValue));
await tester.enterText(find.byType(EditableText), 'world'); await tester.enterText(find.byType(EditableText), 'world');
await tester.idle(); await tester.idle();
await tester.pump(); await tester.pump();
expect(inputKey.currentState.value.text, equals('world')); expect(inputKey.currentState.value, equals('world'));
expect(editableText.config.value.text, equals('world')); expect(editableText.config.controller.text, equals('world'));
}); });
testWidgets('No crash when a FormField is removed from the tree', (WidgetTester tester) async { testWidgets('No crash when a TextFormField is removed from the tree', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = new GlobalKey<FormState>(); final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
final GlobalKey fieldKey = new GlobalKey();
String fieldValue; String fieldValue;
Widget builder(bool remove) { Widget builder(bool remove) {
...@@ -207,14 +205,13 @@ void main() { ...@@ -207,14 +205,13 @@ void main() {
child: new Material( child: new Material(
child: new Form( child: new Form(
key: formKey, key: formKey,
child: remove ? new Container() : new TextField( child: remove ? new Container() : new TextFormField(
key: fieldKey,
autofocus: true, autofocus: true,
onSaved: (InputValue value) { fieldValue = value.text; }, onSaved: (String value) { fieldValue = value; },
validator: (InputValue value) { return value.text.isEmpty ? null : 'yes'; } validator: (String value) { return value.isEmpty ? null : 'yes'; }
),
),
), ),
)
)
); );
} }
......
...@@ -321,7 +321,7 @@ class FlutterDriver { ...@@ -321,7 +321,7 @@ class FlutterDriver {
return null; return null;
} }
/// Sets the text value of the `Input` widget located by [finder]. /// Sets the text value of the [TextField] widget located by [finder].
/// ///
/// This command invokes the `onChanged` handler of the `Input` widget with /// This command invokes the `onChanged` handler of the `Input` widget with
/// the provided [text]. /// the provided [text].
...@@ -330,10 +330,10 @@ class FlutterDriver { ...@@ -330,10 +330,10 @@ class FlutterDriver {
return null; return null;
} }
/// Submits the current text value of the `Input` widget located by [finder]. /// Submits the current text value of the [TextField] widget located by [finder].
/// ///
/// This command invokes the `onSubmitted` handler of the `Input` widget and /// This command invokes the `onSubmitted` handler of the [TextField] widget
/// the returns the submitted text value. /// and the returns the submitted text value.
Future<String> submitInputText(SerializableFinder finder, {Duration timeout}) async { Future<String> submitInputText(SerializableFinder finder, {Duration timeout}) async {
final Map<String, dynamic> json = await _sendCommand(new SubmitInputText(finder, timeout: timeout)); final Map<String, dynamic> json = await _sendCommand(new SubmitInputText(finder, timeout: timeout));
return json['text']; return json['text'];
......
...@@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show RendererBinding; import 'package:flutter/rendering.dart' show RendererBinding;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -262,20 +263,29 @@ class FlutterDriverExtension { ...@@ -262,20 +263,29 @@ class FlutterDriverExtension {
return new ScrollResult(); return new ScrollResult();
} }
Finder _findEditableText(SerializableFinder finder) {
return find.descendant(of: _createFinder(finder), matching: find.byType(EditableText));
}
EditableTextState _getEditableTextState(Finder finder) {
final StatefulElement element = finder.evaluate().single;
return element.state;
}
Future<SetInputTextResult> _setInputText(Command command) async { Future<SetInputTextResult> _setInputText(Command command) async {
final SetInputText setInputTextCommand = command; final SetInputText setInputTextCommand = command;
final Finder target = await _waitForElement(_createFinder(setInputTextCommand.finder)); final Finder target = await _waitForElement(_findEditableText(setInputTextCommand.finder));
final Input input = target.evaluate().single.widget; final EditableTextState editable = _getEditableTextState(target);
input.onChanged(new InputValue(text: setInputTextCommand.text)); editable.updateEditingValue(new TextEditingValue(text: setInputTextCommand.text));
return new SetInputTextResult(); return new SetInputTextResult();
} }
Future<SubmitInputTextResult> _submitInputText(Command command) async { Future<SubmitInputTextResult> _submitInputText(Command command) async {
final SubmitInputText submitInputTextCommand = command; final SubmitInputText submitInputTextCommand = command;
final Finder target = await _waitForElement(_createFinder(submitInputTextCommand.finder)); final Finder target = await _waitForElement(_findEditableText(submitInputTextCommand.finder));
final Input input = target.evaluate().single.widget; final EditableTextState editable = _getEditableTextState(target);
input.onSubmitted(input.value); editable.performAction(TextInputAction.done);
return new SubmitInputTextResult(input.value.text); return new SubmitInputTextResult(editable.config.controller.value.text);
} }
Future<GetTextResult> _getText(Command command) async { Future<GetTextResult> _getText(Command command) async {
......
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