This adds multiline text widget support. (#12120)

Add multiline keyboard support to editable text widget.  Fixes #8028.
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index 09fa0f3..2638912 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -66,8 +66,10 @@
   /// null.
   ///
   /// The [maxLines] property can be set to null to remove the restriction on
-  /// the number of lines. By default, it is 1, meaning this is a single-line
-  /// text field. If it is not null, it must be greater than zero.
+  /// the number of lines. By default, it is one, meaning this is a single-line
+  /// text field. [maxLines] must not be zero. If [maxLines] is not one, then
+  /// [keyboardType] is ignored, and the [TextInputType.multiline] keyboard type
+  /// is used.
   ///
   /// The [keyboardType], [textAlign], [autofocus], [obscureText], and
   /// [autocorrect] arguments must not be null.
@@ -76,7 +78,7 @@
     this.controller,
     this.focusNode,
     this.decoration: const InputDecoration(),
-    this.keyboardType: TextInputType.text,
+    TextInputType keyboardType: TextInputType.text,
     this.style,
     this.textAlign: TextAlign.start,
     this.autofocus: false,
@@ -92,6 +94,7 @@
        assert(obscureText != null),
        assert(autocorrect != null),
        assert(maxLines == null || maxLines > 0),
+       keyboardType = maxLines == 1 ? keyboardType : TextInputType.multiline,
        super(key: key);
 
   /// Controls the text being edited.
@@ -115,7 +118,9 @@
 
   /// The type of keyboard to use for editing the text.
   ///
-  /// Defaults to [TextInputType.text]. Cannot be null.
+  /// Defaults to [TextInputType.text]. Must not be null. If
+  /// [maxLines] is not one, then [keyboardType] is ignored, and the
+  /// [TextInputType.multiline] keyboard type is used.
   final TextInputType keyboardType;
 
   /// The style to use for the text being edited.
diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart
index b36eb9f..5b6b75a 100644
--- a/packages/flutter/lib/src/services/text_input.dart
+++ b/packages/flutter/lib/src/services/text_input.dart
@@ -22,6 +22,13 @@
   /// Requests the default platform keyboard.
   text,
 
+  /// Optimize for multi-line textual information.
+  ///
+  /// Requests the default platform keyboard, but accepts newlines when the
+  /// enter key is pressed. This is the input type used for all multi-line text
+  /// fields.
+  multiline,
+
   /// Optimize for numerical information.
   ///
   /// Requests a keyboard with ready access to the decimal point and number
@@ -56,6 +63,10 @@
 enum TextInputAction {
   /// Complete the text input operation.
   done,
+
+  /// The action to take when the enter button is pressed in a multi-line
+  /// text field (which is typically to do nothing).
+  newline,
 }
 
 /// Controls the visual appearance of the text input control.
@@ -67,15 +78,18 @@
 class TextInputConfiguration {
   /// Creates configuration information for a text input control.
   ///
-  /// The [inputType], [obscureText], and [autocorrect] arguments must not be null.
+  /// All arguments have default values, except [actionLabel].  Only
+  /// [actionLabel] may be null.
   const TextInputConfiguration({
     this.inputType: TextInputType.text,
     this.obscureText: false,
     this.autocorrect: true,
     this.actionLabel,
+    this.inputAction: TextInputAction.done,
   }) : assert(inputType != null),
        assert(obscureText != null),
-       assert(autocorrect != null);
+       assert(autocorrect != null),
+       assert(inputAction != null);
 
   /// The type of information for which to optimize the text input control.
   final TextInputType inputType;
@@ -93,6 +107,9 @@
   /// What text to display in the text input control's action button.
   final String actionLabel;
 
+  /// What kind of action to request for the action button on the IME.
+  final TextInputAction inputAction;
+
   /// Returns a representation of this object as a JSON object.
   Map<String, dynamic> toJSON() {
     return <String, dynamic>{
@@ -100,6 +117,7 @@
       'obscureText': obscureText,
       'autocorrect': autocorrect,
       'actionLabel': actionLabel,
+      'inputAction': inputAction.toString(),
     };
   }
 }
@@ -278,6 +296,8 @@
   switch (action) {
     case 'TextInputAction.done':
       return TextInputAction.done;
+    case 'TextInputAction.newline':
+      return TextInputAction.newline;
   }
   throw new FlutterError('Unknown text input action: $action');
 }
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index aba7051..317d11d 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -141,8 +141,12 @@
   /// Creates a basic text input control.
   ///
   /// The [maxLines] property can be set to null to remove the restriction on
-  /// the number of lines. By default, it is 1, meaning this is a single-line
-  /// text field. If it is not null, it must be greater than zero.
+  /// the number of lines. By default, it is one, meaning this is a single-line
+  /// text field. [maxLines] must be null or greater than zero.
+  ///
+  /// If [keyboardType] is not set or is null, it will default to
+  /// [TextInputType.text] unless [maxLines] is greater than one, when it will
+  /// default to [TextInputType.multiline].
   ///
   /// The [controller], [focusNode], [style], [cursorColor], and [textAlign]
   /// arguments must not be null.
@@ -161,7 +165,7 @@
     this.autofocus: false,
     this.selectionColor,
     this.selectionControls,
-    this.keyboardType,
+    TextInputType keyboardType,
     this.onChanged,
     this.onSubmitted,
     this.onSelectionChanged,
@@ -175,6 +179,7 @@
        assert(textAlign != null),
        assert(maxLines == null || maxLines > 0),
        assert(autofocus != null),
+       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
        inputFormatters = maxLines == 1
            ? (
                <TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter]
@@ -376,10 +381,17 @@
 
   @override
   void performAction(TextInputAction action) {
-    widget.controller.clearComposing();
-    widget.focusNode.unfocus();
-    if (widget.onSubmitted != null)
-      widget.onSubmitted(_value.text);
+    switch (action) {
+      case TextInputAction.done:
+        widget.controller.clearComposing();
+        widget.focusNode.unfocus();
+        if (widget.onSubmitted != null)
+          widget.onSubmitted(_value.text);
+        break;
+      case TextInputAction.newline:
+        // Do nothing for a "newline" action: the newline is already inserted.
+        break;
+    }
   }
 
   void _updateRemoteEditingValueIfNeeded() {
@@ -419,8 +431,16 @@
     if (!_hasInputConnection) {
       final TextEditingValue localValue = _value;
       _lastKnownRemoteTextEditingValue = localValue;
-      _textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: widget.keyboardType, obscureText: widget.obscureText, autocorrect: widget.autocorrect))
-        ..setEditingState(localValue);
+      _textInputConnection = TextInput.attach(this,
+          new TextInputConfiguration(
+              inputType: widget.keyboardType,
+              obscureText: widget.obscureText,
+              autocorrect: widget.autocorrect,
+              inputAction: widget.keyboardType == TextInputType.multiline
+                  ? TextInputAction.newline
+                  : TextInputAction.done
+          )
+      )..setEditingState(localValue);
     }
     _textInputConnection.show();
   }
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
new file mode 100644
index 0000000..5947885
--- /dev/null
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -0,0 +1,156 @@
+// Copyright 2017 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_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter/services.dart';
+
+void main() {
+  final TextEditingController controller = new TextEditingController();
+  final FocusNode focusNode = new FocusNode();
+  final FocusScopeNode focusScopeNode = new FocusScopeNode();
+  final TextStyle textStyle = const TextStyle();
+  final Color cursorColor = const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
+
+  testWidgets('has expected defaults', (WidgetTester tester) async {
+    await tester.pumpWidget(new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new EditableText(
+          controller: controller,
+          focusNode: focusNode,
+          style: textStyle,
+          cursorColor: cursorColor,
+        )));
+
+    final EditableText editableText =
+        tester.firstWidget(find.byType(EditableText));
+    expect(editableText.maxLines, equals(1));
+    expect(editableText.obscureText, isFalse);
+    expect(editableText.autocorrect, isTrue);
+  });
+
+  testWidgets('text keyboard is requested when maxLines is default',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new FocusScope(
+            node: focusScopeNode,
+            autofocus: true,
+            child: new EditableText(
+              controller: controller,
+              focusNode: focusNode,
+              style: textStyle,
+              cursorColor: cursorColor,
+            ))));
+    await tester.tap(find.byType(EditableText));
+    await tester.showKeyboard(find.byType(EditableText));
+    controller.text = 'test';
+    await tester.idle();
+    final EditableText editableText =
+        tester.firstWidget(find.byType(EditableText));
+    expect(editableText.maxLines, equals(1));
+    expect(tester.testTextInput.editingState['text'], equals('test'));
+    expect(tester.testTextInput.setClientArgs['inputType'],
+        equals('TextInputType.text'));
+    expect(tester.testTextInput.setClientArgs['inputAction'],
+        equals('TextInputAction.done'));
+  });
+
+  testWidgets('multiline keyboard is requested when set explicitly',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new FocusScope(
+            node: focusScopeNode,
+            autofocus: true,
+            child: new EditableText(
+              controller: controller,
+              focusNode: focusNode,
+              keyboardType: TextInputType.multiline,
+              style: textStyle,
+              cursorColor: cursorColor,
+            ))));
+
+    await tester.tap(find.byType(EditableText));
+    await tester.showKeyboard(find.byType(EditableText));
+    controller.text = 'test';
+    await tester.idle();
+    expect(tester.testTextInput.editingState['text'], equals('test'));
+    expect(tester.testTextInput.setClientArgs['inputType'], equals('TextInputType.multiline'));
+    expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline'));
+  });
+
+  testWidgets('Correct keyboard is requested when set explicitly and maxLines > 1',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new FocusScope(
+            node: focusScopeNode,
+            autofocus: true,
+            child: new EditableText(
+              controller: controller,
+              focusNode: focusNode,
+              keyboardType: TextInputType.phone,
+              maxLines: 3,
+              style: textStyle,
+              cursorColor: cursorColor,
+            ))));
+
+    await tester.tap(find.byType(EditableText));
+    await tester.showKeyboard(find.byType(EditableText));
+    controller.text = 'test';
+    await tester.idle();
+    expect(tester.testTextInput.editingState['text'], equals('test'));
+    expect(tester.testTextInput.setClientArgs['inputType'], equals('TextInputType.phone'));
+    expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done'));
+  });
+
+  testWidgets('multiline keyboard is requested when set implicitly',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new FocusScope(
+            node: focusScopeNode,
+            autofocus: true,
+            child: new EditableText(
+              controller: controller,
+              focusNode: focusNode,
+              maxLines: 3, // Sets multiline keyboard implicitly.
+              style: textStyle,
+              cursorColor: cursorColor,
+            ))));
+
+    await tester.tap(find.byType(EditableText));
+    await tester.showKeyboard(find.byType(EditableText));
+    controller.text = 'test';
+    await tester.idle();
+    expect(tester.testTextInput.editingState['text'], equals('test'));
+    expect(tester.testTextInput.setClientArgs['inputType'], equals('TextInputType.multiline'));
+    expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline'));
+  });
+
+  testWidgets('single line inputs have correct default keyboard',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new FocusScope(
+            node: focusScopeNode,
+            autofocus: true,
+            child: new EditableText(
+              controller: controller,
+              focusNode: focusNode,
+              maxLines: 1, // Sets text keyboard implicitly.
+              style: textStyle,
+              cursorColor: cursorColor,
+            ))));
+
+    await tester.tap(find.byType(EditableText));
+    await tester.showKeyboard(find.byType(EditableText));
+    controller.text = 'test';
+    await tester.idle();
+    expect(tester.testTextInput.editingState['text'], equals('test'));
+    expect(tester.testTextInput.setClientArgs['inputType'], equals('TextInputType.text'));
+    expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done'));
+  });
+}
diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart
index ed2791b..353180c 100644
--- a/packages/flutter_test/lib/src/test_text_input.dart
+++ b/packages/flutter_test/lib/src/test_text_input.dart
@@ -28,6 +28,9 @@
 
   int _client = 0;
 
+  /// Arguments supplied to the TextInput.setClient method call.
+  Map<String, dynamic> setClientArgs;
+
   /// The last set of arguments that [TextInputConnection.setEditingState] sent
   /// to the embedder.
   ///
@@ -40,6 +43,7 @@
     switch (methodCall.method) {
       case 'TextInput.setClient':
         _client = methodCall.arguments[0];
+        setClientArgs = methodCall.arguments[1];
         break;
       case 'TextInput.clearClient':
         _client = 0;