mirror of
https://github.com/flutter/flutter
synced 2024-10-14 04:02:56 +00:00
parent
6e7f3e6f5a
commit
11943ce7e7
|
@ -135,7 +135,6 @@ class RenderEditable extends RenderBox {
|
|||
Locale locale,
|
||||
double cursorWidth = 1.0,
|
||||
Radius cursorRadius,
|
||||
@required this.textSelectionDelegate,
|
||||
}) : assert(textAlign != null),
|
||||
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
|
||||
assert(maxLines == null || maxLines > 0),
|
||||
|
@ -143,8 +142,7 @@ class RenderEditable extends RenderBox {
|
|||
assert(offset != null),
|
||||
assert(ignorePointer != null),
|
||||
assert(obscureText != null),
|
||||
assert(textSelectionDelegate != null),
|
||||
_textPainter = new TextPainter(
|
||||
_textPainter = new TextPainter(
|
||||
text: text,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
|
@ -198,24 +196,12 @@ class RenderEditable extends RenderBox {
|
|||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// The object that controls the text selection, used by this render object
|
||||
/// for implementing cut, copy, and paste keyboard shortcuts.
|
||||
///
|
||||
/// It must not be null. It will make cut, copy and paste functionality work
|
||||
/// with the most recently set [TextSelectionDelegate].
|
||||
TextSelectionDelegate textSelectionDelegate;
|
||||
|
||||
Rect _lastCaretRect;
|
||||
|
||||
static const int _kLeftArrowCode = 21;
|
||||
static const int _kRightArrowCode = 22;
|
||||
static const int _kUpArrowCode = 19;
|
||||
static const int _kDownArrowCode = 20;
|
||||
static const int _kXKeyCode = 52;
|
||||
static const int _kCKeyCode = 31;
|
||||
static const int _kVKeyCode = 50;
|
||||
static const int _kAKeyCode = 29;
|
||||
static const int _kDelKeyCode = 112;
|
||||
|
||||
// The extent offset of the current selection
|
||||
int _extentOffset = -1;
|
||||
|
@ -235,7 +221,7 @@ class RenderEditable extends RenderBox {
|
|||
static const int _kControlMask = 1 << 12; // https://developer.android.com/reference/android/view/KeyEvent.html#META_CTRL_ON
|
||||
|
||||
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
|
||||
void _handleKeyEvent(RawKeyEvent keyEvent) {
|
||||
void _handleKeyEvent(RawKeyEvent keyEvent){
|
||||
if (defaultTargetPlatform != TargetPlatform.android)
|
||||
return;
|
||||
|
||||
|
@ -260,11 +246,6 @@ class RenderEditable extends RenderBox {
|
|||
final bool upArrow = pressedKeyCode == _kUpArrowCode;
|
||||
final bool downArrow = pressedKeyCode == _kDownArrowCode;
|
||||
final bool arrow = leftArrow || rightArrow || upArrow || downArrow;
|
||||
final bool aKey = pressedKeyCode == _kAKeyCode;
|
||||
final bool xKey = pressedKeyCode == _kXKeyCode;
|
||||
final bool vKey = pressedKeyCode == _kVKeyCode;
|
||||
final bool cKey = pressedKeyCode == _kCKeyCode;
|
||||
final bool del = pressedKeyCode == _kDelKeyCode;
|
||||
|
||||
// We will only move select or more the caret if an arrow is pressed
|
||||
if (arrow) {
|
||||
|
@ -281,12 +262,7 @@ class RenderEditable extends RenderBox {
|
|||
newOffset = _handleShift(rightArrow, leftArrow, shift, newOffset);
|
||||
|
||||
_extentOffset = newOffset;
|
||||
} else if (ctrl && (xKey || vKey || cKey || aKey)) {
|
||||
// _handleShortcuts depends on being started in the same stack invocation as the _handleKeyEvent method
|
||||
_handleShortcuts(pressedKeyCode);
|
||||
}
|
||||
if (del)
|
||||
_handleDelete();
|
||||
}
|
||||
|
||||
// Handles full word traversal using control.
|
||||
|
@ -392,7 +368,7 @@ class RenderEditable extends RenderBox {
|
|||
onSelectionChanged(
|
||||
new TextSelection.fromPosition(
|
||||
new TextPosition(
|
||||
offset: newOffset
|
||||
offset: newOffset
|
||||
)
|
||||
),
|
||||
this,
|
||||
|
@ -402,74 +378,6 @@ class RenderEditable extends RenderBox {
|
|||
return newOffset;
|
||||
}
|
||||
|
||||
// Handles shortcut functionality including cut, copy, paste and select all
|
||||
// using control + (X, C, V, A).
|
||||
void _handleShortcuts(int pressedKeyCode) async {
|
||||
switch (pressedKeyCode) {
|
||||
case _kCKeyCode:
|
||||
if (!selection.isCollapsed) {
|
||||
Clipboard.setData(
|
||||
new ClipboardData(text: selection.textInside(text.text)));
|
||||
}
|
||||
break;
|
||||
case _kXKeyCode:
|
||||
if (!selection.isCollapsed) {
|
||||
Clipboard.setData(
|
||||
new ClipboardData(text: selection.textInside(text.text)));
|
||||
textSelectionDelegate.textEditingValue = new TextEditingValue(
|
||||
text: selection.textBefore(text.text)
|
||||
+ selection.textAfter(text.text),
|
||||
selection: new TextSelection.collapsed(offset: selection.start),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case _kVKeyCode:
|
||||
// Snapshot the input before using `await`.
|
||||
// See https://github.com/flutter/flutter/issues/11427
|
||||
final TextEditingValue value = textSelectionDelegate.textEditingValue;
|
||||
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null) {
|
||||
textSelectionDelegate.textEditingValue = new TextEditingValue(
|
||||
text: value.selection.textBefore(value.text)
|
||||
+ data.text
|
||||
+ value.selection.textAfter(value.text),
|
||||
selection: new TextSelection.collapsed(
|
||||
offset: value.selection.start + data.text.length
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case _kAKeyCode:
|
||||
_baseOffset = 0;
|
||||
_extentOffset = textSelectionDelegate.textEditingValue.text.length;
|
||||
onSelectionChanged(
|
||||
new TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: textSelectionDelegate.textEditingValue.text.length,
|
||||
),
|
||||
this,
|
||||
SelectionChangedCause.keyboard,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
int _handleDelete() {
|
||||
if (selection.textAfter(text.text).isNotEmpty) {
|
||||
textSelectionDelegate.textEditingValue = new TextEditingValue(
|
||||
text: selection.textBefore(text.text)
|
||||
+ selection.textAfter(text.text).substring(1),
|
||||
selection: new TextSelection.collapsed(offset: selection.start));
|
||||
} else {
|
||||
textSelectionDelegate.textEditingValue = new TextEditingValue(
|
||||
text: selection.textBefore(text.text),
|
||||
selection: new TextSelection.collapsed(offset: selection.start)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the render object as needing to be laid out again and have its text
|
||||
/// metrics recomputed.
|
||||
///
|
||||
|
|
|
@ -534,23 +534,6 @@ class TextEditingValue {
|
|||
);
|
||||
}
|
||||
|
||||
/// An interface for manipulating the selection, to be used by the implementor
|
||||
/// of the toolbar widget.
|
||||
abstract class TextSelectionDelegate {
|
||||
/// Gets the current text input.
|
||||
TextEditingValue get textEditingValue;
|
||||
|
||||
/// Sets the current text input (replaces the whole line).
|
||||
set textEditingValue(TextEditingValue value);
|
||||
|
||||
/// Hides the text selection toolbar.
|
||||
void hideToolbar();
|
||||
|
||||
/// Brings the provided [TextPosition] into the visible area of the text
|
||||
/// input.
|
||||
void bringIntoView(TextPosition position);
|
||||
}
|
||||
|
||||
/// An interface to receive information from [TextInput].
|
||||
///
|
||||
/// See also:
|
||||
|
|
|
@ -904,7 +904,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
rendererIgnoresPointer: widget.rendererIgnoresPointer,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorRadius: widget.cursorRadius,
|
||||
textSelectionDelegate: this,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -968,7 +967,6 @@ class _Editable extends LeafRenderObjectWidget {
|
|||
this.rendererIgnoresPointer = false,
|
||||
this.cursorWidth,
|
||||
this.cursorRadius,
|
||||
this.textSelectionDelegate,
|
||||
}) : assert(textDirection != null),
|
||||
assert(rendererIgnoresPointer != null),
|
||||
super(key: key);
|
||||
|
@ -992,7 +990,6 @@ class _Editable extends LeafRenderObjectWidget {
|
|||
final bool rendererIgnoresPointer;
|
||||
final double cursorWidth;
|
||||
final Radius cursorRadius;
|
||||
final TextSelectionDelegate textSelectionDelegate;
|
||||
|
||||
@override
|
||||
RenderEditable createRenderObject(BuildContext context) {
|
||||
|
@ -1015,7 +1012,6 @@ class _Editable extends LeafRenderObjectWidget {
|
|||
obscureText: obscureText,
|
||||
cursorWidth: cursorWidth,
|
||||
cursorRadius: cursorRadius,
|
||||
textSelectionDelegate: textSelectionDelegate,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1039,7 +1035,6 @@ class _Editable extends LeafRenderObjectWidget {
|
|||
..ignorePointer = rendererIgnoresPointer
|
||||
..obscureText = obscureText
|
||||
..cursorWidth = cursorWidth
|
||||
..cursorRadius = cursorRadius
|
||||
..textSelectionDelegate = textSelectionDelegate;
|
||||
..cursorRadius = cursorRadius;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ import 'gesture_detector.dart';
|
|||
import 'overlay.dart';
|
||||
import 'transitions.dart';
|
||||
|
||||
export 'package:flutter/services.dart' show TextSelectionDelegate;
|
||||
|
||||
/// Which type of selection handle to be displayed.
|
||||
///
|
||||
/// With mixed-direction text, both handles may be the same type. Examples:
|
||||
|
@ -62,6 +60,23 @@ enum _TextSelectionHandlePosition { start, end }
|
|||
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
|
||||
typedef void TextSelectionOverlayChanged(TextEditingValue value, Rect caretRect);
|
||||
|
||||
/// An interface for manipulating the selection, to be used by the implementor
|
||||
/// of the toolbar widget.
|
||||
abstract class TextSelectionDelegate {
|
||||
/// Gets the current text input.
|
||||
TextEditingValue get textEditingValue;
|
||||
|
||||
/// Sets the current text input (replaces the whole line).
|
||||
set textEditingValue(TextEditingValue value);
|
||||
|
||||
/// Hides the text selection toolbar.
|
||||
void hideToolbar();
|
||||
|
||||
/// Brings the provided [TextPosition] into the visible area of the text
|
||||
/// input.
|
||||
void bringIntoView(TextPosition position);
|
||||
}
|
||||
|
||||
/// An interface for building the selection UI, to be provided by the
|
||||
/// implementor of the toolbar widget.
|
||||
///
|
||||
|
|
|
@ -1919,255 +1919,6 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
const int _kXKeyCode = 52;
|
||||
const int _kCKeyCode = 31;
|
||||
const int _kVKeyCode = 50;
|
||||
const int _kAKeyCode = 29;
|
||||
const int _kDelKeyCode = 112;
|
||||
|
||||
testWidgets('Copy paste test', (WidgetTester tester) async{
|
||||
final FocusNode focusNode = new FocusNode();
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
final TextField textField =
|
||||
new TextField(
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
);
|
||||
|
||||
String clipboardContent = '';
|
||||
SystemChannels.platform
|
||||
.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
if (methodCall.method == 'Clipboard.setData')
|
||||
clipboardContent = methodCall.arguments['text'];
|
||||
else if (methodCall.method == 'Clipboard.getData')
|
||||
return <String, dynamic>{'text': clipboardContent};
|
||||
return null;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: new Material(
|
||||
child: new RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: null,
|
||||
child: textField,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.idle();
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select the first 5 characters
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown shift
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(22, false, false, false); // RIGHT_ARROW keyup
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Copy them
|
||||
sendKeyEventWithCode(_kCKeyCode, true, false, true); // keydown control
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(_kCKeyCode, false, false, false); // keyup control
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(clipboardContent, 'a big');
|
||||
|
||||
sendKeyEventWithCode(22, true, false, false); // RIGHT_ARROW keydown
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(22, false, false, false); // RIGHT_ARROW keyup
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Paste them
|
||||
sendKeyEventWithCode(_kVKeyCode, true, false, true); // Control V keydown
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
sendKeyEventWithCode(_kVKeyCode, false, false, false); // Control V keyup
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const String expected = 'a biga big house\njumped over a mouse';
|
||||
expect(find.text(expected), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Cut test', (WidgetTester tester) async{
|
||||
final FocusNode focusNode = new FocusNode();
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
final TextField textField =
|
||||
new TextField(
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
);
|
||||
String clipboardContent = '';
|
||||
SystemChannels.platform
|
||||
.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
if (methodCall.method == 'Clipboard.setData')
|
||||
clipboardContent = methodCall.arguments['text'];
|
||||
else if (methodCall.method == 'Clipboard.getData')
|
||||
return <String, dynamic>{'text': clipboardContent};
|
||||
return null;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: new Material(
|
||||
child: new RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: null,
|
||||
child: textField,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.idle();
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select the first 5 characters
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown shift
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(22, false, false, false); // RIGHT_ARROW keyup
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Cut them
|
||||
sendKeyEventWithCode(_kXKeyCode, true, false, true); // keydown control X
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(_kXKeyCode, false, false, false); // keyup control X
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(clipboardContent, 'a big');
|
||||
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
sendKeyEventWithCode(22, true, false, false); // RIGHT_ARROW keydown
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(22, false, false, false); // RIGHT_ARROW keyup
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Paste them
|
||||
sendKeyEventWithCode(_kVKeyCode, true, false, true); // Control V keydown
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
sendKeyEventWithCode(_kVKeyCode, false, false, false); // Control V keyup
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const String expected = ' housa bige\njumped over a mouse';
|
||||
expect(find.text(expected), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Select all test', (WidgetTester tester) async{
|
||||
final FocusNode focusNode = new FocusNode();
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
final TextField textField =
|
||||
new TextField(
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: new Material(
|
||||
child: new RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: null,
|
||||
child: textField,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.idle();
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select All
|
||||
sendKeyEventWithCode(_kAKeyCode, true, false, true); // keydown control A
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(_kAKeyCode, false, false, true); // keyup control A
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Delete them
|
||||
sendKeyEventWithCode(_kDelKeyCode, true, false, false); // DEL keydown
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
sendKeyEventWithCode(_kDelKeyCode, false, false, false); // DEL keyup
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const String expected = '';
|
||||
expect(find.text(expected), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Delete test', (WidgetTester tester) async{
|
||||
final FocusNode focusNode = new FocusNode();
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
final TextField textField =
|
||||
new TextField(
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: new Material(
|
||||
child: new RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: null,
|
||||
child: textField,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.idle();
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Delete
|
||||
for (int i = 0; i < 6; i += 1) {
|
||||
sendKeyEventWithCode(_kDelKeyCode, true, false, false); // keydown DEL
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(_kDelKeyCode, false, false, false); // keyup DEL
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
const String expected = 'house\njumped over a mouse';
|
||||
expect(find.text(expected), findsOneWidget);
|
||||
|
||||
sendKeyEventWithCode(_kAKeyCode, true, false, true); // keydown control A
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(_kAKeyCode, false, false, true); // keyup control A
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
||||
sendKeyEventWithCode(_kDelKeyCode, true, false, false); // keydown DEL
|
||||
await tester.pumpAndSettle();
|
||||
sendKeyEventWithCode(_kDelKeyCode, false, false, false); // keyup DEL
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const String expected2 = '';
|
||||
expect(find.text(expected2), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Changing positions of text fields', (WidgetTester tester) async{
|
||||
|
||||
final FocusNode focusNode = new FocusNode();
|
||||
|
|
|
@ -4,25 +4,9 @@
|
|||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class FakeEditableTextState extends TextSelectionDelegate {
|
||||
@override
|
||||
TextEditingValue get textEditingValue {}
|
||||
|
||||
@override
|
||||
set textEditingValue(TextEditingValue value) {}
|
||||
|
||||
@override
|
||||
void hideToolbar() {}
|
||||
|
||||
@override
|
||||
void bringIntoView(TextPosition position) {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('editable intrinsics', () {
|
||||
final TextSelectionDelegate delegate = new FakeEditableTextState();
|
||||
final RenderEditable editable = new RenderEditable(
|
||||
text: const TextSpan(
|
||||
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||||
|
@ -32,7 +16,6 @@ void main() {
|
|||
textDirection: TextDirection.ltr,
|
||||
locale: const Locale('ja', 'JP'),
|
||||
offset: new ViewportOffset.zero(),
|
||||
textSelectionDelegate: delegate,
|
||||
);
|
||||
expect(editable.getMinIntrinsicWidth(double.infinity), 50.0);
|
||||
expect(editable.getMaxIntrinsicWidth(double.infinity), 50.0);
|
||||
|
|
Loading…
Reference in a new issue