Unverified Commit 3900d42b authored by jslavitz's avatar jslavitz Committed by GitHub

Control, Shift and Arrow Key functionality for Chromebook (#20204)

* added keyboard functionatliy to android builds

* Added tests

* almost ready for review

* ready for review

* Fixes

* final comments

* final commit

* removing raw keyboard changes

* removing raw keyboard changes

* removing raw keyboard changes

* actual last commit

* fixed the imports

* a few more changes

* A few more changes

* a few changes

* Final changes

* Final changes2

* final actual commit for real

* final actual commit for real2

* final actual commit for real3

* final actual commit for real4

* final

* final 2

* f

* f2

* fin

* fin 2

* fin3

* fin4
parent dc5a5c18
......@@ -198,6 +198,186 @@ class RenderEditable extends RenderBox {
Rect _lastCaretRect;
static const int _kLeftArrowCode = 21;
static const int _kRightArrowCode = 22;
static const int _kUpArrowCode = 19;
static const int _kDownArrowCode = 20;
// The extent offset of the current selection
int _extentOffset = -1;
// The base offset of the current selection
int _baseOffset = -1;
// Holds the last location the user selected in the case that he selects all
// the way to the end or beginning of the field.
int _previousCursorLocation = -1;
// Whether we should reset the location of the cursor in the case the user
// selects all the way to the end or the beginning of a field.
bool _resetCursor = false;
static const int _kShiftMask = 1; // https://developer.android.com/reference/android/view/KeyEvent.html#META_SHIFT_ON
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){
if (defaultTargetPlatform != TargetPlatform.android)
return;
if (keyEvent is RawKeyUpEvent)
return;
final RawKeyEventDataAndroid rawAndroidEvent = keyEvent.data;
final int pressedKeyCode = rawAndroidEvent.keyCode;
final int pressedKeyMetaState = rawAndroidEvent.metaState;
if (selection.isCollapsed) {
_extentOffset = selection.extentOffset;
_baseOffset = selection.baseOffset;
}
// Update current key states
final bool shift = pressedKeyMetaState & _kShiftMask > 0;
final bool ctrl = pressedKeyMetaState & _kControlMask > 0;
final bool rightArrow = pressedKeyCode == _kRightArrowCode;
final bool leftArrow = pressedKeyCode == _kLeftArrowCode;
final bool upArrow = pressedKeyCode == _kUpArrowCode;
final bool downArrow = pressedKeyCode == _kDownArrowCode;
final bool arrow = leftArrow || rightArrow || upArrow || downArrow;
// We will only move select or more the caret if an arrow is pressed
if (arrow) {
int newOffset = _extentOffset;
// Because the user can use multiple keys to change how he selects
// the new offset variable is threaded through these four functions
// and potentially changes after each one.
if (ctrl)
newOffset = _handleControl(rightArrow, leftArrow, ctrl, newOffset);
newOffset = _handleHorizontalArrows(rightArrow, leftArrow, shift, newOffset);
if (downArrow || upArrow)
newOffset = _handleVerticalArrows(upArrow, downArrow, shift, newOffset);
newOffset = _handleShift(rightArrow, leftArrow, shift, newOffset);
_extentOffset = newOffset;
}
}
// Handles full word traversal using control.
int _handleControl(bool rightArrow, bool leftArrow, bool ctrl, int newOffset) {
// If control is pressed, we will decide which way to look for a word
// based on which arrow is pressed.
if (leftArrow && _extentOffset > 2) {
final TextSelection textSelection = _selectWordAtOffset(new TextPosition(offset: _extentOffset - 2));
newOffset = textSelection.baseOffset + 1;
} else if (rightArrow && _extentOffset < text.text.length - 2) {
final TextSelection textSelection = _selectWordAtOffset(new TextPosition(offset: _extentOffset + 1));
newOffset = textSelection.extentOffset - 1;
}
return newOffset;
}
int _handleHorizontalArrows(bool rightArrow, bool leftArrow, bool shift, int newOffset) {
// Set the new offset to be +/- 1 depending on which arrow is pressed
// If shift is down, we also want to update the previous cursor location
if (rightArrow && _extentOffset < text.text.length) {
newOffset += 1;
if (shift)
_previousCursorLocation += 1;
}
if (leftArrow && _extentOffset > 0) {
newOffset -= 1;
if (shift)
_previousCursorLocation -= 1;
}
return newOffset;
}
// Handles moving the cursor vertically as well as taking care of the
// case where the user moves the cursor to the end or beginning of the text
// and then back up or down.
int _handleVerticalArrows(bool upArrow, bool downArrow, bool shift, int newOffset) {
// The caret offset gives a location in the upper left hand corner of
// the caret so the middle of the line above is a half line above that
// point and the line below is 1.5 lines below that point.
final double plh = _textPainter.preferredLineHeight;
final double verticalOffset = upArrow ? -0.5 * plh : 1.5 * plh;
final Offset caretOffset = _textPainter.getOffsetForCaret(new TextPosition(offset: _extentOffset), _caretPrototype);
final Offset caretOffsetTranslated = caretOffset.translate(0.0, verticalOffset);
final TextPosition position = _textPainter.getPositionForOffset(caretOffsetTranslated);
// To account for the possibility where the user vertically highlights
// all the way to the top or bottom of the text, we hold the previous
// cursor location. This allows us to restore to this position in the
// case that the user wants to unhighlight some text.
if (position.offset == _extentOffset) {
if (downArrow)
newOffset = text.text.length;
else if (upArrow)
newOffset = 0;
_resetCursor = shift;
} else if (_resetCursor && shift) {
newOffset = _previousCursorLocation;
_resetCursor = false;
} else {
newOffset = position.offset;
_previousCursorLocation = newOffset;
}
return newOffset;
}
// Handles the selection of text or removal of the selection and placing
// of the caret.
int _handleShift(bool rightArrow, bool leftArrow, bool shift, int newOffset) {
if (onSelectionChanged == null)
return newOffset;
// In the text_selection class, a TextSelection is defined such that the
// base offset is always less than the extent offset.
if (shift) {
if (_baseOffset < newOffset) {
onSelectionChanged(
new TextSelection(
baseOffset: _baseOffset,
extentOffset: newOffset
),
this,
SelectionChangedCause.keyboard,
);
} else {
onSelectionChanged(
new TextSelection(
baseOffset: newOffset,
extentOffset: _baseOffset
),
this,
SelectionChangedCause.keyboard,
);
}
} else {
// We want to put the cursor at the correct location depending on which
// arrow is used while there is a selection.
if (!selection.isCollapsed) {
if (leftArrow)
newOffset = _baseOffset < _extentOffset ? _baseOffset : _extentOffset;
else if (rightArrow)
newOffset = _baseOffset > _extentOffset ? _baseOffset : _extentOffset;
}
onSelectionChanged(
new TextSelection.fromPosition(
new TextPosition(
offset: newOffset
)
),
this,
SelectionChangedCause.keyboard,
);
}
return newOffset;
}
/// Marks the render object as needing to be laid out again and have its text
/// metrics recomputed.
///
......@@ -300,11 +480,23 @@ class RenderEditable extends RenderBox {
/// Whether the editable is currently focused.
bool get hasFocus => _hasFocus;
bool _hasFocus;
bool _listenerAttached = false;
set hasFocus(bool value) {
assert(value != null);
if (_hasFocus == value)
return;
_hasFocus = value;
if (_hasFocus) {
assert(!_listenerAttached);
RawKeyboard.instance.addListener(_handleKeyEvent);
_listenerAttached = true;
}
else {
assert(_listenerAttached);
RawKeyboard.instance.removeListener(_handleKeyEvent);
_listenerAttached = false;
}
markNeedsSemanticsUpdate();
}
......@@ -574,6 +766,8 @@ class RenderEditable extends RenderBox {
void detach() {
_offset.removeListener(markNeedsPaint);
_showCursor.removeListener(markNeedsPaint);
if (_listenerAttached)
RawKeyboard.instance.removeListener(_handleKeyEvent);
super.detach();
}
......
......@@ -1764,6 +1764,316 @@ void main() {
semantics.dispose();
});
void sendFakeKeyEvent(Map<String, dynamic> data) {
BinaryMessages.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data),
(ByteData data) { },
);
}
void sendKeyEventWithCode(int code, bool down, bool shiftDown, bool ctrlDown) {
int metaState = shiftDown ? 1 : 0;
if (ctrlDown)
metaState |= 1 << 12;
sendFakeKeyEvent(<String, dynamic>{
'type': down ? 'keydown' : 'keyup',
'keymap': 'android',
'keyCode' : code,
'hidUsage': 0x04,
'codePoint': 0x64,
'metaState': metaState,
});
}
group('Keyboard Tests', (){
TextEditingController controller;
setUp( () {
controller = new TextEditingController();
});
MaterialApp setupWidget() {
final FocusNode focusNode = new FocusNode();
controller = new TextEditingController();
return new MaterialApp(
home: Material(
child: new RawKeyboardListener(
focusNode: focusNode,
onKey: null,
child: TextField(
controller: controller,
maxLines: 3,
),
) ,
),
);
}
testWidgets('Shift test 1', (WidgetTester tester) async{
await tester.pumpWidget(setupWidget());
const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown, SHIFT_ON
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
});
testWidgets('Control Shift test', (WidgetTester tester) async{
await tester.pumpWidget(setupWidget());
const String testValue = 'their big house';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
await tester.pumpAndSettle();
sendKeyEventWithCode(22, true, true, true); // RIGHT_ARROW keydown SHIFT_ON, CONTROL_ON
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
});
testWidgets('Down and up test', (WidgetTester tester) async{
await tester.pumpWidget(setupWidget());
const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
sendKeyEventWithCode(20, true, true, false); // DOWN_ARROW keydown
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 11);
sendKeyEventWithCode(20, false, true, false); // DOWN_ARROW keyup
await tester.pumpAndSettle();
sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
});
testWidgets('Down and up test 2', (WidgetTester tester) async{
await tester.pumpWidget(setupWidget());
const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
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();
}
sendKeyEventWithCode(20, true, true, false); // DOWN_ARROW keydown
await tester.pumpAndSettle();
sendKeyEventWithCode(20, false, true, false); // DOWN_ARROW keyup
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);
sendKeyEventWithCode(20, true, true, false); // DOWN_ARROW keydown
await tester.pumpAndSettle();
sendKeyEventWithCode(20, false, true, false); // DOWN_ARROW keyup
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 32);
sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown
await tester.pumpAndSettle();
sendKeyEventWithCode(19, false, true, false); // UP_ARROW keyup
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 12);
sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown
await tester.pumpAndSettle();
sendKeyEventWithCode(19, false, true, false); // UP_ARROW keyup
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
sendKeyEventWithCode(19, true, true, false); // UP_ARROW keydown
await tester.pumpAndSettle();
sendKeyEventWithCode(19, false, true, false); // UP_ARROW keyup
await tester.pumpAndSettle();
expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
});
});
testWidgets('Changing positions of text fields', (WidgetTester tester) async{
final FocusNode focusNode = new FocusNode();
final List<RawKeyEvent> events = <RawKeyEvent>[];
final TextEditingController c1 = new TextEditingController();
final TextEditingController c2 = new TextEditingController();
final Key key1 = new UniqueKey();
final Key key2 = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
home:
Material(
child: new RawKeyboardListener(
focusNode: focusNode,
onKey: events.add,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
key: key1,
controller: c1,
maxLines: 3,
),
TextField(
key: key2,
controller: c2,
maxLines: 3,
),
],
),
),
),
),
);
const String testValue = 'a big house';
await tester.enterText(find.byType(TextField).first, testValue);
await tester.idle();
await tester.tap(find.byType(TextField).first);
await tester.pumpAndSettle();
for (int i = 0; i < 5; i += 1) {
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
await tester.pumpAndSettle();
}
expect(c1.selection.extentOffset - c1.selection.baseOffset, 5);
await tester.pumpWidget(
new MaterialApp(
home:
Material(
child: new RawKeyboardListener(
focusNode: focusNode,
onKey: events.add,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
key: key2,
controller: c2,
maxLines: 3,
),
TextField(
key: key1,
controller: c1,
maxLines: 3,
),
],
),
),
),
),
);
for (int i = 0; i < 5; i += 1) {
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
await tester.pumpAndSettle();
}
expect(c1.selection.extentOffset - c1.selection.baseOffset, 10);
});
testWidgets('Changing focus test', (WidgetTester tester) async {
final FocusNode focusNode = new FocusNode();
final List<RawKeyEvent> events = <RawKeyEvent>[];
final TextEditingController c1 = new TextEditingController();
final TextEditingController c2 = new TextEditingController();
final Key key1 = new UniqueKey();
final Key key2 = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
home:
Material(
child: new RawKeyboardListener(
focusNode: focusNode,
onKey: events.add,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
key: key1,
controller: c1,
maxLines: 3,
),
TextField(
key: key2,
controller: c2,
maxLines: 3,
),
],
),
),
),
),
);
await tester.idle();
await tester.tap(find.byType(TextField).first);
const String testValue = 'a big house';
await tester.enterText(find.byType(TextField).first, testValue);
await tester.pumpAndSettle();
for (int i = 0; i < 5; i += 1) {
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
await tester.pumpAndSettle();
}
expect(c1.selection.extentOffset - c1.selection.baseOffset, 5);
expect(c2.selection.extentOffset - c2.selection.baseOffset, 0);
await tester.idle();
await tester.tap(find.byType(TextField).last);
await tester.enterText(find.byType(TextField).last, testValue);
await tester.pumpAndSettle();
for (int i = 0; i < 5; i += 1) {
sendKeyEventWithCode(22, true, true, false); // RIGHT_ARROW keydown
await tester.pumpAndSettle();
}
expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
expect(c2.selection.extentOffset - c2.selection.baseOffset, 5);
});
testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
final TextEditingController controller = new TextEditingController();
......
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