Commit 52c55344 authored by Jason Simmons's avatar Jason Simmons

Change the text/selection value API of the input field

(see https://github.com/flutter/flutter/issues/1586)
parent c0ef05c3
...@@ -51,10 +51,10 @@ class MealFragment extends StatefulComponent { ...@@ -51,10 +51,10 @@ class MealFragment extends StatefulComponent {
} }
class MealFragmentState extends State<MealFragment> { class MealFragmentState extends State<MealFragment> {
String _description = ""; InputValue _description = InputValue.empty;
void _handleSave() { void _handleSave() {
config.onCreated(new Meal(when: new DateTime.now(), description: _description)); config.onCreated(new Meal(when: new DateTime.now(), description: _description.text));
Navigator.pop(context); Navigator.pop(context);
} }
...@@ -75,7 +75,7 @@ class MealFragmentState extends State<MealFragment> { ...@@ -75,7 +75,7 @@ class MealFragmentState extends State<MealFragment> {
); );
} }
void _handleDescriptionChanged(String description) { void _handleDescriptionChanged(InputValue description) {
setState(() { setState(() {
_description = description; _description = description;
}); });
......
...@@ -63,13 +63,13 @@ class MeasurementFragment extends StatefulComponent { ...@@ -63,13 +63,13 @@ class MeasurementFragment extends StatefulComponent {
} }
class MeasurementFragmentState extends State<MeasurementFragment> { class MeasurementFragmentState extends State<MeasurementFragment> {
String _weight = ""; InputValue _weight = InputValue.empty;
DateTime _when = new DateTime.now(); DateTime _when = new DateTime.now();
void _handleSave() { void _handleSave() {
double parsedWeight; double parsedWeight;
try { try {
parsedWeight = double.parse(_weight); parsedWeight = double.parse(_weight.text);
} on FormatException catch(e) { } on FormatException catch(e) {
print("Exception $e"); print("Exception $e");
Scaffold.of(context).showSnackBar(new SnackBar( Scaffold.of(context).showSnackBar(new SnackBar(
...@@ -97,7 +97,7 @@ class MeasurementFragmentState extends State<MeasurementFragment> { ...@@ -97,7 +97,7 @@ class MeasurementFragmentState extends State<MeasurementFragment> {
); );
} }
void _handleWeightChanged(String weight) { void _handleWeightChanged(InputValue weight) {
setState(() { setState(() {
_weight = weight; _weight = weight;
}); });
......
...@@ -4,6 +4,64 @@ ...@@ -4,6 +4,64 @@
part of fitness; part of fitness;
class _SettingsDialog extends StatefulComponent {
_SettingsDialogState createState() => new _SettingsDialogState();
}
class _SettingsDialogState extends State<_SettingsDialog> {
final GlobalKey weightGoalKey = new GlobalKey();
InputValue _goalWeight = InputValue.empty;
void _handleGoalWeightChanged(InputValue goalWeight) {
setState(() {
_goalWeight = goalWeight;
});
}
void _handleGoalWeightSubmitted(InputValue goalWeight) {
_goalWeight = goalWeight;
_handleSavePressed();
}
void _handleSavePressed() {
double goalWeight;
try {
goalWeight = double.parse(_goalWeight.text);
} on FormatException {
goalWeight = 0.0;
}
Navigator.pop(context, goalWeight);
}
Widget build(BuildContext context) {
return new Dialog(
title: new Text("Goal Weight"),
content: new Input(
key: weightGoalKey,
value: _goalWeight,
autofocus: true,
hintText: 'Goal weight in lbs',
keyboardType: KeyboardType.number,
onChanged: _handleGoalWeightChanged,
onSubmitted: _handleGoalWeightSubmitted
),
actions: <Widget>[
new FlatButton(
child: new Text('CANCEL'),
onPressed: () {
Navigator.pop(context);
}
),
new FlatButton(
child: new Text('SAVE'),
onPressed: _handleSavePressed
),
]
);
}
}
typedef void SettingsUpdater({ typedef void SettingsUpdater({
BackupMode backup, BackupMode backup,
double goalWeight double goalWeight
...@@ -36,52 +94,10 @@ class SettingsFragmentState extends State<SettingsFragment> { ...@@ -36,52 +94,10 @@ class SettingsFragmentState extends State<SettingsFragment> {
return "${config.userData.goalWeight}"; return "${config.userData.goalWeight}";
} }
static final GlobalKey weightGoalKey = new GlobalKey();
double _goalWeight;
void _handleGoalWeightChanged(String goalWeight) {
// TODO(jackson): Looking for null characters to detect enter key is a hack
if (goalWeight.endsWith("\u{0}")) {
Navigator.pop(context, double.parse(goalWeight.replaceAll("\u{0}", "")));
} else {
setState(() {
try {
_goalWeight = double.parse(goalWeight);
} on FormatException {
_goalWeight = 0.0;
}
});
}
}
Future _handleGoalWeightPressed() async { Future _handleGoalWeightPressed() async {
double goalWeight = await showDialog( double goalWeight = await showDialog(
context: context, context: context,
child: new Dialog( child: new _SettingsDialog()
title: new Text("Goal Weight"),
content: new Input(
key: weightGoalKey,
autofocus: true,
hintText: 'Goal weight in lbs',
keyboardType: KeyboardType.number,
onChanged: _handleGoalWeightChanged
),
actions: <Widget>[
new FlatButton(
child: new Text('CANCEL'),
onPressed: () {
Navigator.pop(context);
}
),
new FlatButton(
child: new Text('SAVE'),
onPressed: () {
Navigator.pop(context, _goalWeight);
}
),
]
)
); );
config.updater(goalWeight: goalWeight); config.updater(goalWeight: goalWeight);
} }
......
...@@ -33,14 +33,14 @@ class StockHomeState extends State<StockHome> { ...@@ -33,14 +33,14 @@ 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;
String _searchQuery; InputValue _searchQuery = InputValue.empty;
void _handleSearchBegin() { void _handleSearchBegin() {
ModalRoute.of(context).addLocalHistoryEntry(new LocalHistoryEntry( ModalRoute.of(context).addLocalHistoryEntry(new LocalHistoryEntry(
onRemove: () { onRemove: () {
setState(() { setState(() {
_isSearching = false; _isSearching = false;
_searchQuery = null; _searchQuery = InputValue.empty;
}); });
} }
)); ));
...@@ -53,7 +53,7 @@ class StockHomeState extends State<StockHome> { ...@@ -53,7 +53,7 @@ class StockHomeState extends State<StockHome> {
Navigator.pop(context); Navigator.pop(context);
} }
void _handleSearchQueryChanged(String query) { void _handleSearchQueryChanged(InputValue query) {
setState(() { setState(() {
_searchQuery = query; _searchQuery = query;
}); });
...@@ -197,9 +197,9 @@ class StockHomeState extends State<StockHome> { ...@@ -197,9 +197,9 @@ class StockHomeState extends State<StockHome> {
} }
Iterable<Stock> _filterBySearchQuery(Iterable<Stock> stocks) { Iterable<Stock> _filterBySearchQuery(Iterable<Stock> stocks) {
if (_searchQuery == null) if (_searchQuery.text.isEmpty)
return stocks; return stocks;
RegExp regexp = new RegExp(_searchQuery, caseSensitive: false); RegExp regexp = new RegExp(_searchQuery.text, caseSensitive: false);
return stocks.where((Stock stock) => stock.symbol.contains(regexp)); return stocks.where((Stock stock) => stock.symbol.contains(regexp));
} }
...@@ -254,6 +254,7 @@ class StockHomeState extends State<StockHome> { ...@@ -254,6 +254,7 @@ class StockHomeState extends State<StockHome> {
tooltip: 'Back' tooltip: 'Back'
), ),
center: new Input( center: new Input(
value: _searchQuery,
key: searchFieldKey, key: searchFieldKey,
autofocus: true, autofocus: true,
hintText: 'Search stocks', hintText: 'Search stocks',
......
...@@ -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) {
label = "Item $value"; inputValue = new InputValue(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;
String label; InputValue inputValue;
Key get key => new ObjectKey(this); Key get key => new ObjectKey(this);
} }
...@@ -305,9 +305,11 @@ class CardCollectionState extends State<CardCollection> { ...@@ -305,9 +305,11 @@ class CardCollectionState extends State<CardCollection> {
new Center( new Center(
child: new Input( child: new Input(
key: new GlobalObjectKey(cardModel), key: new GlobalObjectKey(cardModel),
initialValue: cardModel.label, value: cardModel.inputValue,
onChanged: (String value) { onChanged: (InputValue value) {
cardModel.label = value; setState(() {
cardModel.inputValue = value;
});
} }
) )
) )
...@@ -317,7 +319,7 @@ class CardCollectionState extends State<CardCollection> { ...@@ -317,7 +319,7 @@ class CardCollectionState extends State<CardCollection> {
), ),
child: new Column( child: new Column(
children: <Widget>[ children: <Widget>[
new Text(cardModel.label) new Text(cardModel.inputValue.text)
], ],
alignItems: FlexAlignItems.stretch, alignItems: FlexAlignItems.stretch,
justifyContent: FlexJustifyContent.center justifyContent: FlexJustifyContent.center
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:sky_services/editing/editing.mojom.dart' as mojom;
import 'colors.dart'; import 'colors.dart';
import 'debug.dart'; import 'debug.dart';
...@@ -17,8 +16,7 @@ export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType; ...@@ -17,8 +16,7 @@ export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
class Input extends StatefulComponent { class Input extends StatefulComponent {
Input({ Input({
GlobalKey key, GlobalKey key,
this.initialValue: '', this.value: InputValue.empty,
this.initialSelection,
this.keyboardType: KeyboardType.text, this.keyboardType: KeyboardType.text,
this.icon, this.icon,
this.labelText, this.labelText,
...@@ -34,11 +32,8 @@ class Input extends StatefulComponent { ...@@ -34,11 +32,8 @@ class Input extends StatefulComponent {
assert(key != null); assert(key != null);
} }
/// The initial editable text for the input field. /// The text of the input field.
final String initialValue; final InputValue value;
/// The initial selection for this input field.
final TextSelection initialSelection;
/// The type of keyboard to use for editing the text. /// The type of keyboard to use for editing the text.
final KeyboardType keyboardType; final KeyboardType keyboardType;
...@@ -68,10 +63,10 @@ class Input extends StatefulComponent { ...@@ -68,10 +63,10 @@ class Input extends StatefulComponent {
final bool autofocus; final bool autofocus;
/// Called when the text being edited changes. /// Called when the text being edited changes.
final ValueChanged<String> onChanged; final ValueChanged<InputValue> 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<String> onSubmitted; final ValueChanged<InputValue> onSubmitted;
_InputState createState() => new _InputState(); _InputState createState() => new _InputState();
} }
...@@ -80,89 +75,13 @@ const Duration _kTransitionDuration = const Duration(milliseconds: 200); ...@@ -80,89 +75,13 @@ const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.ease; const Curve _kTransitionCurve = Curves.ease;
class _InputState extends State<Input> { class _InputState extends State<Input> {
String _value; GlobalKey<RawInputLineState> _rawInputLineKey = new GlobalKey<RawInputLineState>();
EditableString _editableString;
KeyboardHandle _keyboardHandle;
// Used by tests.
EditableString get editableValue => _editableString;
void initState() {
super.initState();
_value = config.initialValue;
_editableString = new EditableString(
text: _value,
selection: config.initialSelection,
onUpdated: _handleTextUpdated,
onSubmitted: _handleTextSubmitted
);
}
void dispose() {
if (_isAttachedToKeyboard)
_keyboardHandle.release();
super.dispose();
}
bool get _isAttachedToKeyboard => _keyboardHandle != null && _keyboardHandle.attached;
void _attachOrDetachKeyboard(bool focused) {
if (focused && !_isAttachedToKeyboard) {
_keyboardHandle = keyboard.attach(_editableString.createStub(),
new mojom.KeyboardConfiguration()
..type = config.keyboardType);
_keyboardHandle.setEditingState(_editableString.editingState);
_keyboardHandle.show();
} else if (!focused && _isAttachedToKeyboard) {
_keyboardHandle.release();
_keyboardHandle = null;
_editableString.didDetachKeyboard();
}
}
void _requestKeyboard() {
if (Focus.at(context)) {
assert(_isAttachedToKeyboard);
_keyboardHandle.show();
} else {
Focus.moveTo(config.key);
// we'll get told to rebuild and we'll take care of the keyboard then
}
}
void _handleTextUpdated() {
if (_value != _editableString.text) {
setState(() {
_value = _editableString.text;
});
if (config.onChanged != null)
config.onChanged(_value);
}
}
void _handleTextSubmitted() {
Focus.clear(context);
if (config.onSubmitted != null)
config.onSubmitted(_value);
}
void _handleSelectionChanged(TextSelection selection) {
if (_isAttachedToKeyboard) {
_editableString.setSelection(selection);
_keyboardHandle.setEditingState(_editableString.editingState);
} else {
_editableString.setSelection(selection);
_requestKeyboard();
}
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context); ThemeData themeData = Theme.of(context);
bool focused = Focus.at(context, autofocus: config.autofocus); bool focused = Focus.at(context, autofocus: config.autofocus);
_attachOrDetachKeyboard(focused);
TextStyle textStyle = config.style ?? themeData.text.subhead; TextStyle textStyle = config.style ?? themeData.text.subhead;
Color focusHighlightColor = themeData.accentColor; Color focusHighlightColor = themeData.accentColor;
if (themeData.primarySwatch != null) if (themeData.primarySwatch != null)
...@@ -171,7 +90,7 @@ class _InputState extends State<Input> { ...@@ -171,7 +90,7 @@ class _InputState extends State<Input> {
List<Widget> stackChildren = <Widget>[]; List<Widget> stackChildren = <Widget>[];
bool hasInlineLabel = config.labelText != null && !focused && !_value.isNotEmpty; bool hasInlineLabel = config.labelText != null && !focused && !config.value.text.isNotEmpty;
if (config.labelText != null) { if (config.labelText != null) {
TextStyle labelStyle = hasInlineLabel ? TextStyle labelStyle = hasInlineLabel ?
...@@ -194,7 +113,7 @@ class _InputState extends State<Input> { ...@@ -194,7 +113,7 @@ class _InputState extends State<Input> {
topPadding += topPaddingIncrement; topPadding += topPaddingIncrement;
} }
if (config.hintText != null && _value.isEmpty && !hasInlineLabel) { if (config.hintText != null && config.value.text.isEmpty && !hasInlineLabel) {
TextStyle hintStyle = themeData.text.subhead.copyWith(color: themeData.hintColor); TextStyle hintStyle = themeData.text.subhead.copyWith(color: themeData.hintColor);
stackChildren.add(new Positioned( stackChildren.add(new Positioned(
left: 0.0, left: 0.0,
...@@ -234,14 +153,17 @@ class _InputState extends State<Input> { ...@@ -234,14 +153,17 @@ class _InputState extends State<Input> {
) )
) )
), ),
child: new RawEditableLine( child: new RawInputLine(
value: _editableString, key: _rawInputLineKey,
focused: focused, value: config.value,
focusKey: config.key,
style: textStyle, style: textStyle,
hideText: config.hideText, hideText: config.hideText,
cursorColor: cursorColor, cursorColor: cursorColor,
selectionColor: cursorColor, selectionColor: cursorColor,
onSelectionChanged: _handleSelectionChanged keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted
) )
)); ));
...@@ -278,7 +200,7 @@ class _InputState extends State<Input> { ...@@ -278,7 +200,7 @@ class _InputState extends State<Input> {
return new GestureDetector( return new GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: _requestKeyboard, onTap: () => _rawInputLineKey.currentState?.requestKeyboard(),
child: new Padding( child: new Padding(
padding: const EdgeDims.symmetric(horizontal: 16.0), padding: const EdgeDims.symmetric(horizontal: 16.0),
child: child child: child
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show TextAffinity, TextPosition; import 'dart:ui' show hashValues, TextAffinity, TextPosition;
export 'dart:ui' show TextAffinity, TextPosition; export 'dart:ui' show TextAffinity, TextPosition;
...@@ -50,6 +50,22 @@ class TextRange { ...@@ -50,6 +50,22 @@ class TextRange {
assert(isNormalized); assert(isNormalized);
return text.substring(start, end); return text.substring(start, end);
} }
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! TextRange)
return false;
TextRange typedOther = other;
return typedOther.start == start
&& typedOther.end == end;
}
int get hashCode => hashValues(
start.hashCode,
end.hashCode
);
} }
/// A range of text that represents a selection. /// A range of text that represents a selection.
...@@ -122,4 +138,23 @@ class TextSelection extends TextRange { ...@@ -122,4 +138,23 @@ class TextSelection extends TextRange {
String toString() { String toString() {
return '$runtimeType(baseOffset: $baseOffset, extentOffset: $extentOffset, affinity: $affinity, isDirectional: $isDirectional)'; return '$runtimeType(baseOffset: $baseOffset, extentOffset: $extentOffset, affinity: $affinity, isDirectional: $isDirectional)';
} }
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! TextSelection)
return false;
TextSelection typedOther = other;
return typedOther.baseOffset == baseOffset
&& typedOther.extentOffset == extentOffset
&& typedOther.affinity == affinity
&& typedOther.isDirectional == isDirectional;
}
int get hashCode => hashValues(
baseOffset.hashCode,
extentOffset.hashCode,
affinity.hashCode,
isDirectional.hashCode
);
} }
...@@ -30,15 +30,16 @@ void main() { ...@@ -30,15 +30,16 @@ void main() {
test('Editable text has consistent size', () { test('Editable text has consistent size', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
GlobalKey inputKey = new GlobalKey(); GlobalKey inputKey = new GlobalKey();
String inputValue; InputValue inputValue = InputValue.empty;
Widget builder() { Widget builder() {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Input( child: new Input(
value: inputValue,
key: inputKey, key: inputKey,
hintText: 'Placeholder', hintText: 'Placeholder',
onChanged: (String value) { inputValue = value; } onChanged: (InputValue value) { inputValue = value; }
) )
) )
); );
...@@ -58,7 +59,7 @@ void main() { ...@@ -58,7 +59,7 @@ void main() {
..composingExtent = testValue.length); ..composingExtent = testValue.length);
// Check that the onChanged event handler fired. // Check that the onChanged event handler fired.
expect(inputValue, equals(testValue)); expect(inputValue.text, equals(testValue));
tester.pumpWidget(builder()); tester.pumpWidget(builder());
} }
...@@ -88,7 +89,7 @@ void main() { ...@@ -88,7 +89,7 @@ void main() {
tester.pumpWidget(builder()); tester.pumpWidget(builder());
RawEditableTextState editableText = tester.findStateOfType(RawEditableTextState); RawInputLineState editableText = tester.findStateOfType(RawInputLineState);
// Check that the cursor visibility toggles after each blink interval. // Check that the cursor visibility toggles after each blink interval.
void checkCursorToggle() { void checkCursorToggle() {
......
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