未验证 提交 9beac71a 编写于 作者: E Edman P. Anjos 提交者: GitHub

Add support for software text editing controls (#15560)

* Add support for software text editing controls

Includes selection, copy, cut, paste, as well as partial support for up
and down movement.

Text editing controls can be accessed in GBoard by:
  top-left arrow > three dots menu > text editing

Partial fix for flutter/flutter#9419 and flutter/flutter#37371.

* Introduce InputConnectionAdaptor tests

Run with:
  testing/run_tests.py --type=java --java-filter=io.flutter.plugin.editing.InputConnectionAdaptorTest

* Fix BUILD.gn comment on run_tests.py --java-filter flag
上级 96061d62
......@@ -406,7 +406,7 @@ action("pom_embedding") {
}
# To build and run:
# testing/run_tests.py [--type=java] [--filter=io.flutter.TestClassName]
# testing/run_tests.py [--type=java] [--java-filter=io.flutter.TestClassName]
action("robolectric_tests") {
script = "//build/android/gyp/javac.py"
depfile = "$target_gen_dir/$target_name.d"
......
......@@ -5,6 +5,8 @@
package io.flutter.plugin.editing;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
......@@ -267,13 +269,53 @@ class InputConnectionAdaptor extends BaseInputConnection {
}
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
int selStart = Selection.getSelectionStart(mEditable);
int newSel = Math.max(selStart - 1, 0);
setSelection(newSel, newSel);
int selEnd = Selection.getSelectionEnd(mEditable);
if (selStart == selEnd && !event.isShiftPressed()) {
int newSel = Math.max(selStart - 1, 0);
setSelection(newSel, newSel);
} else {
int newSelEnd = Math.max(selEnd - 1, 0);
setSelection(selStart, newSelEnd);
}
return true;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) {
int selStart = Selection.getSelectionStart(mEditable);
int newSel = Math.min(selStart + 1, mEditable.length());
setSelection(newSel, newSel);
int selEnd = Selection.getSelectionEnd(mEditable);
if (selStart == selEnd && !event.isShiftPressed()) {
int newSel = Math.min(selStart + 1, mEditable.length());
setSelection(newSel, newSel);
} else {
int newSelEnd = Math.min(selEnd + 1, mEditable.length());
setSelection(selStart, newSelEnd);
}
return true;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) {
int selStart = Selection.getSelectionStart(mEditable);
int selEnd = Selection.getSelectionEnd(mEditable);
if (selStart == selEnd && !event.isShiftPressed()) {
Selection.moveUp(mEditable, mLayout);
int newSelStart = Selection.getSelectionStart(mEditable);
setSelection(newSelStart, newSelStart);
} else {
Selection.extendUp(mEditable, mLayout);
int newSelStart = Selection.getSelectionStart(mEditable);
int newSelEnd = Selection.getSelectionEnd(mEditable);
setSelection(newSelStart, newSelEnd);
}
return true;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) {
int selStart = Selection.getSelectionStart(mEditable);
int selEnd = Selection.getSelectionEnd(mEditable);
if (selStart == selEnd && !event.isShiftPressed()) {
Selection.moveDown(mEditable, mLayout);
int newSelStart = Selection.getSelectionStart(mEditable);
setSelection(newSelStart, newSelStart);
} else {
Selection.extendDown(mEditable, mLayout);
int newSelStart = Selection.getSelectionStart(mEditable);
int newSelEnd = Selection.getSelectionEnd(mEditable);
setSelection(newSelStart, newSelEnd);
}
return true;
// When the enter key is pressed on a non-multiline field, consider it a
// submit instead of a newline.
......@@ -288,13 +330,75 @@ class InputConnectionAdaptor extends BaseInputConnection {
if (character != 0) {
int selStart = Math.max(0, Selection.getSelectionStart(mEditable));
int selEnd = Math.max(0, Selection.getSelectionEnd(mEditable));
if (selEnd != selStart) mEditable.delete(selStart, selEnd);
mEditable.insert(selStart, String.valueOf((char) character));
setSelection(selStart + 1, selStart + 1);
int selMin = Math.min(selStart, selEnd);
int selMax = Math.max(selStart, selEnd);
if (selMin != selMax) mEditable.delete(selMin, selMax);
mEditable.insert(selMin, String.valueOf((char) character));
setSelection(selMin + 1, selMin + 1);
}
return true;
}
}
if (event.getAction() == KeyEvent.ACTION_UP
&& (event.getKeyCode() == KeyEvent.KEYCODE_SHIFT_LEFT
|| event.getKeyCode() == KeyEvent.KEYCODE_SHIFT_RIGHT)) {
int selEnd = Selection.getSelectionEnd(mEditable);
setSelection(selEnd, selEnd);
return true;
}
return false;
}
@Override
public boolean performContextMenuAction(int id) {
if (id == android.R.id.selectAll) {
setSelection(0, mEditable.length());
return true;
} else if (id == android.R.id.cut) {
int selStart = Selection.getSelectionStart(mEditable);
int selEnd = Selection.getSelectionEnd(mEditable);
if (selStart != selEnd) {
int selMin = Math.min(selStart, selEnd);
int selMax = Math.max(selStart, selEnd);
CharSequence textToCut = mEditable.subSequence(selMin, selMax);
ClipboardManager clipboard =
(ClipboardManager)
mFlutterView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text label?", textToCut);
clipboard.setPrimaryClip(clip);
mEditable.delete(selMin, selMax);
setSelection(selMin, selMin);
}
return true;
} else if (id == android.R.id.copy) {
int selStart = Selection.getSelectionStart(mEditable);
int selEnd = Selection.getSelectionEnd(mEditable);
if (selStart != selEnd) {
CharSequence textToCopy =
mEditable.subSequence(Math.min(selStart, selEnd), Math.max(selStart, selEnd));
ClipboardManager clipboard =
(ClipboardManager)
mFlutterView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText("text label?", textToCopy));
}
return true;
} else if (id == android.R.id.paste) {
ClipboardManager clipboard =
(ClipboardManager) mFlutterView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = clipboard.getPrimaryClip();
if (clip != null) {
CharSequence textToPaste = clip.getItemAt(0).coerceToText(mFlutterView.getContext());
int selStart = Math.max(0, Selection.getSelectionStart(mEditable));
int selEnd = Math.max(0, Selection.getSelectionEnd(mEditable));
int selMin = Math.min(selStart, selEnd);
int selMax = Math.max(selStart, selEnd);
if (selMin != selMax) mEditable.delete(selMin, selMax);
mEditable.insert(selMin, textToPaste);
int newSelStart = selMin + textToPaste.length();
setSelection(newSelStart, newSelStart);
}
return true;
}
return false;
}
......
package io.flutter.plugin.editing;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
......@@ -7,9 +10,12 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.content.res.AssetManager;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
......@@ -22,8 +28,10 @@ import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowClipboardManager;
@Config(manifest = Config.NONE, sdk = 27)
@Config(manifest = Config.NONE, sdk = 27, shadows = ShadowClipboardManager.class)
@RunWith(RobolectricTestRunner.class)
public class InputConnectionAdaptorTest {
@Test
......@@ -47,4 +55,221 @@ public class InputConnectionAdaptorTest {
inputConnectionAdaptor.sendKeyEvent(keyEvent);
verify(spyEditable, times(1)).insert(eq(0), anyString());
}
@Test
public void testPerformContextMenuAction_selectAll() {
int selStart = 5;
Editable editable = sampleEditable(selStart, selStart);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
boolean didConsume = adaptor.performContextMenuAction(android.R.id.selectAll);
assertTrue(didConsume);
assertEquals(0, Selection.getSelectionStart(editable));
assertEquals(editable.length(), Selection.getSelectionEnd(editable));
}
@Test
public void testPerformContextMenuAction_cut() {
ShadowClipboardManager clipboardManager =
Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.CLIPBOARD_SERVICE));
int selStart = 6;
int selEnd = 11;
Editable editable = sampleEditable(selStart, selEnd);
CharSequence textToBeCut = editable.subSequence(selStart, selEnd);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
boolean didConsume = adaptor.performContextMenuAction(android.R.id.cut);
assertTrue(didConsume);
assertTrue(clipboardManager.hasText());
assertEquals(textToBeCut, clipboardManager.getPrimaryClip().getItemAt(0).getText());
assertFalse(editable.toString().contains(textToBeCut));
}
@Test
public void testPerformContextMenuAction_copy() {
ShadowClipboardManager clipboardManager =
Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.CLIPBOARD_SERVICE));
int selStart = 6;
int selEnd = 11;
Editable editable = sampleEditable(selStart, selEnd);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
assertFalse(clipboardManager.hasText());
boolean didConsume = adaptor.performContextMenuAction(android.R.id.copy);
assertTrue(didConsume);
assertTrue(clipboardManager.hasText());
assertEquals(
editable.subSequence(selStart, selEnd),
clipboardManager.getPrimaryClip().getItemAt(0).getText());
}
@Test
public void testPerformContextMenuAction_paste() {
ShadowClipboardManager clipboardManager =
Shadow.extract(RuntimeEnvironment.application.getSystemService(Context.CLIPBOARD_SERVICE));
String textToBePasted = "deadbeef";
clipboardManager.setText(textToBePasted);
Editable editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
boolean didConsume = adaptor.performContextMenuAction(android.R.id.paste);
assertTrue(didConsume);
assertTrue(editable.toString().startsWith(textToBePasted));
}
@Test
public void testSendKeyEvent_shiftKeyUpCancelsSelection() {
int selStart = 5;
int selEnd = 10;
Editable editable = sampleEditable(selStart, selEnd);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent shiftKeyUp = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT);
boolean didConsume = adaptor.sendKeyEvent(shiftKeyUp);
assertTrue(didConsume);
assertEquals(selEnd, Selection.getSelectionStart(editable));
assertEquals(selEnd, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_leftKeyMovesCaretLeft() {
int selStart = 5;
Editable editable = sampleEditable(selStart, selStart);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent leftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
boolean didConsume = adaptor.sendKeyEvent(leftKeyDown);
assertTrue(didConsume);
assertEquals(selStart - 1, Selection.getSelectionStart(editable));
assertEquals(selStart - 1, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_leftKeyExtendsSelectionLeft() {
int selStart = 5;
int selEnd = 40;
Editable editable = sampleEditable(selStart, selEnd);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent leftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
boolean didConsume = adaptor.sendKeyEvent(leftKeyDown);
assertTrue(didConsume);
assertEquals(selStart, Selection.getSelectionStart(editable));
assertEquals(selEnd - 1, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_shiftLeftKeyStartsSelectionLeft() {
int selStart = 5;
Editable editable = sampleEditable(selStart, selStart);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent shiftLeftKeyDown =
new KeyEvent(
0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, 0, KeyEvent.META_SHIFT_ON);
boolean didConsume = adaptor.sendKeyEvent(shiftLeftKeyDown);
assertTrue(didConsume);
assertEquals(selStart, Selection.getSelectionStart(editable));
assertEquals(selStart - 1, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_rightKeyMovesCaretRight() {
int selStart = 5;
Editable editable = sampleEditable(selStart, selStart);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent rightKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
boolean didConsume = adaptor.sendKeyEvent(rightKeyDown);
assertTrue(didConsume);
assertEquals(selStart + 1, Selection.getSelectionStart(editable));
assertEquals(selStart + 1, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_rightKeyExtendsSelectionRight() {
int selStart = 5;
int selEnd = 40;
Editable editable = sampleEditable(selStart, selEnd);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent rightKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
boolean didConsume = adaptor.sendKeyEvent(rightKeyDown);
assertTrue(didConsume);
assertEquals(selStart, Selection.getSelectionStart(editable));
assertEquals(selEnd + 1, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_shiftRightKeyStartsSelectionRight() {
int selStart = 5;
Editable editable = sampleEditable(selStart, selStart);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent shiftRightKeyDown =
new KeyEvent(
0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT, 0, KeyEvent.META_SHIFT_ON);
boolean didConsume = adaptor.sendKeyEvent(shiftRightKeyDown);
assertTrue(didConsume);
assertEquals(selStart, Selection.getSelectionStart(editable));
assertEquals(selStart + 1, Selection.getSelectionEnd(editable));
}
@Test
public void testSendKeyEvent_upKeyMovesCaretUp() {
int selStart = SAMPLE_TEXT.indexOf('\n') + 4;
Editable editable = sampleEditable(selStart, selStart);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent upKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP);
boolean didConsume = adaptor.sendKeyEvent(upKeyDown);
assertTrue(didConsume);
// Checks the caret moved left (to some previous character). Selection.moveUp() behaves
// different in tests than on a real device, we can't verify the exact position.
assertTrue(Selection.getSelectionStart(editable) < selStart);
}
@Test
public void testSendKeyEvent_downKeyMovesCaretDown() {
int selStart = 4;
Editable editable = sampleEditable(selStart, selStart);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN);
boolean didConsume = adaptor.sendKeyEvent(downKeyDown);
assertTrue(didConsume);
// Checks the caret moved right (to some following character). Selection.moveDown() behaves
// different in tests than on a real device, we can't verify the exact position.
assertTrue(Selection.getSelectionStart(editable) > selStart);
}
private static final String SAMPLE_TEXT =
"Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit.";
private static Editable sampleEditable(int selStart, int selEnd) {
SpannableStringBuilder sample = new SpannableStringBuilder(SAMPLE_TEXT);
Selection.setSelection(sample, selStart, selEnd);
return sample;
}
private static InputConnectionAdaptor sampleInputConnectionAdaptor(Editable editable) {
View testView = new View(RuntimeEnvironment.application);
int client = 0;
TextInputChannel textInputChannel = mock(TextInputChannel.class);
return new InputConnectionAdaptor(testView, client, textInputChannel, editable, null);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册