Unverified Commit 9dce4b82 authored by jslavitz's avatar jslavitz Committed by GitHub

Shortcuts (#21558)

* added shortcuts and delete functionality

* added first test

*  afew chnages

* a few changes

* hope this works

* small change

* small changes

* fixed nits

* final changes:

* fixed initializing formals

* update comment

* minor change:

* added line

* final changes

* random change

* changed function to void

* one more

* removed line

* dis betta work

* final change

* actual final chnge
parent 8ebd3525
......@@ -135,6 +135,7 @@ 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),
......@@ -142,7 +143,8 @@ class RenderEditable extends RenderBox {
assert(offset != null),
assert(ignorePointer != null),
assert(obscureText != null),
_textPainter = new TextPainter(
assert(textSelectionDelegate != null),
_textPainter = new TextPainter(
text: text,
textAlign: textAlign,
textDirection: textDirection,
......@@ -196,12 +198,24 @@ 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;
......@@ -221,7 +235,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;
......@@ -246,6 +260,11 @@ 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) {
......@@ -262,7 +281,12 @@ 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.
......@@ -368,7 +392,7 @@ class RenderEditable extends RenderBox {
onSelectionChanged(
new TextSelection.fromPosition(
new TextPosition(
offset: newOffset
offset: newOffset
)
),
this,
......@@ -378,6 +402,75 @@ 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);
}
}
void _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,6 +534,23 @@ 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,6 +904,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius,
textSelectionDelegate: this,
),
),
);
......@@ -967,6 +968,7 @@ class _Editable extends LeafRenderObjectWidget {
this.rendererIgnoresPointer = false,
this.cursorWidth,
this.cursorRadius,
this.textSelectionDelegate,
}) : assert(textDirection != null),
assert(rendererIgnoresPointer != null),
super(key: key);
......@@ -990,6 +992,7 @@ class _Editable extends LeafRenderObjectWidget {
final bool rendererIgnoresPointer;
final double cursorWidth;
final Radius cursorRadius;
final TextSelectionDelegate textSelectionDelegate;
@override
RenderEditable createRenderObject(BuildContext context) {
......@@ -1012,6 +1015,7 @@ class _Editable extends LeafRenderObjectWidget {
obscureText: obscureText,
cursorWidth: cursorWidth,
cursorRadius: cursorRadius,
textSelectionDelegate: textSelectionDelegate,
);
}
......@@ -1035,6 +1039,7 @@ class _Editable extends LeafRenderObjectWidget {
..ignorePointer = rendererIgnoresPointer
..obscureText = obscureText
..cursorWidth = cursorWidth
..cursorRadius = cursorRadius;
..cursorRadius = cursorRadius
..textSelectionDelegate = textSelectionDelegate;
}
}
......@@ -16,6 +16,8 @@ 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:
......@@ -60,23 +62,6 @@ 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,6 +1919,255 @@ 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,9 +4,25 @@
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 { return const 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'),
......@@ -16,6 +32,7 @@ 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);
......
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