Add methods to InputConnection: setComposingRegion() to select a region of text for correction, and getSelectedText()

to return the selected text.

setComposingRegion:

The TextView may choose to highlight the text in some way (underline for now) to indicate
that the text is selected for correction, if the IME wants to provider alternatives.

Choosing an alternative in the IME can then call IC.commitText() to replace the highlighted
(not selected) text with a different candidate.

This change also ensures that any existing spans/styles are not wiped out. So we can now
correct rich text as well.

getSelectedText:

This is a convenience to get the selected text instead of using extracted text that is
more heavy weight. Existing getTextBeforeCursor() and getTextAfterCursor() fail to
retrieve the selected text, only what's before and after the selection.

Change-Id: Ieb5ecd5ff947ea04958589f501e7bd5228e00fb5
This commit is contained in:
Amith Yamasani
2010-08-25 18:27:20 -07:00
parent 846eb30f78
commit a90b7f0125
8 changed files with 299 additions and 34 deletions

View File

@ -198034,6 +198034,19 @@
<parameter name="flags" type="int">
</parameter>
</method>
<method name="getSelectedText"
return="java.lang.CharSequence"
abstract="false"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="flags" type="int">
</parameter>
</method>
<method name="getTextAfterCursor"
return="java.lang.CharSequence"
abstract="false"
@ -198144,6 +198157,21 @@
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="setComposingRegion"
return="boolean"
abstract="false"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="start" type="int">
</parameter>
<parameter name="end" type="int">
</parameter>
</method>
<method name="setComposingSpans"
return="void"
abstract="false"
@ -199138,6 +199166,19 @@
<parameter name="flags" type="int">
</parameter>
</method>
<method name="getSelectedText"
return="java.lang.CharSequence"
abstract="true"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="flags" type="int">
</parameter>
</method>
<method name="getTextAfterCursor"
return="java.lang.CharSequence"
abstract="true"
@ -199235,6 +199276,21 @@
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="setComposingRegion"
return="boolean"
abstract="true"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="start" type="int">
</parameter>
<parameter name="end" type="int">
</parameter>
</method>
<method name="setComposingText"
return="boolean"
abstract="true"
@ -199427,6 +199483,19 @@
<parameter name="flags" type="int">
</parameter>
</method>
<method name="getSelectedText"
return="java.lang.CharSequence"
abstract="false"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="flags" type="int">
</parameter>
</method>
<method name="getTextAfterCursor"
return="java.lang.CharSequence"
abstract="false"
@ -199524,6 +199593,21 @@
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="setComposingRegion"
return="boolean"
abstract="false"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="start" type="int">
</parameter>
<parameter name="end" type="int">
</parameter>
</method>
<method name="setComposingText"
return="boolean"
abstract="false"

View File

@ -84,9 +84,14 @@ public class BaseInputConnection implements InputConnection {
}
}
}
public static void setComposingSpans(Spannable text) {
final Object[] sps = text.getSpans(0, text.length(), Object.class);
setComposingSpans(text, 0, text.length());
}
/** @hide */
public static void setComposingSpans(Spannable text, int start, int end) {
final Object[] sps = text.getSpans(start, end, Object.class);
if (sps != null) {
for (int i=sps.length-1; i>=0; i--) {
final Object o = sps[i];
@ -94,18 +99,19 @@ public class BaseInputConnection implements InputConnection {
text.removeSpan(o);
continue;
}
final int fl = text.getSpanFlags(o);
if ((fl&(Spanned.SPAN_COMPOSING|Spanned.SPAN_POINT_MARK_MASK))
!= (Spanned.SPAN_COMPOSING|Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)) {
text.setSpan(o, text.getSpanStart(o), text.getSpanEnd(o),
(fl&Spanned.SPAN_POINT_MARK_MASK)
(fl & ~Spanned.SPAN_POINT_MARK_MASK)
| Spanned.SPAN_COMPOSING
| Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
text.setSpan(COMPOSING, 0, text.length(),
text.setSpan(COMPOSING, start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
}
@ -311,6 +317,31 @@ public class BaseInputConnection implements InputConnection {
return TextUtils.substring(content, a - length, a);
}
/**
* The default implementation returns the text currently selected, or null if none is
* selected.
*/
public CharSequence getSelectedText(int flags) {
final Editable content = getEditable();
if (content == null) return null;
int a = Selection.getSelectionStart(content);
int b = Selection.getSelectionEnd(content);
if (a > b) {
int tmp = a;
a = b;
b = tmp;
}
if (a == b) return null;
if ((flags&GET_TEXT_WITH_STYLES) != 0) {
return content.subSequence(a, b);
}
return TextUtils.substring(content, a, b);
}
/**
* The default implementation returns the given amount of text from the
* current cursor position in the buffer.
@ -385,6 +416,38 @@ public class BaseInputConnection implements InputConnection {
return true;
}
public boolean setComposingRegion(int start, int end) {
final Editable content = getEditable();
if (content != null) {
beginBatchEdit();
removeComposingSpans(content);
int a = start;
int b = end;
if (a > b) {
int tmp = a;
a = b;
b = tmp;
}
if (a < 0) a = 0;
if (b > content.length()) b = content.length();
ensureDefaultComposingSpans();
if (mDefaultComposingSpans != null) {
for (int i = 0; i < mDefaultComposingSpans.length; ++i) {
content.setSpan(mDefaultComposingSpans[i], a, b,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
}
}
content.setSpan(COMPOSING, a, b,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
endBatchEdit();
sendCurrentText();
}
return true;
}
/**
* The default implementation changes the selection position in the
* current editable text.
@ -479,7 +542,32 @@ public class BaseInputConnection implements InputConnection {
content.clear();
}
}
private void ensureDefaultComposingSpans() {
if (mDefaultComposingSpans == null) {
Context context;
if (mTargetView != null) {
context = mTargetView.getContext();
} else if (mIMM.mServedView != null) {
context = mIMM.mServedView.getContext();
} else {
context = null;
}
if (context != null) {
TypedArray ta = context.getTheme()
.obtainStyledAttributes(new int[] {
com.android.internal.R.attr.candidatesTextStyleSpans
});
CharSequence style = ta.getText(0);
ta.recycle();
if (style != null && style instanceof Spanned) {
mDefaultComposingSpans = ((Spanned)style).getSpans(
0, style.length(), Object.class);
}
}
}
}
private void replaceText(CharSequence text, int newCursorPosition,
boolean composing) {
final Editable content = getEditable();
@ -520,32 +608,11 @@ public class BaseInputConnection implements InputConnection {
if (!(text instanceof Spannable)) {
sp = new SpannableStringBuilder(text);
text = sp;
if (mDefaultComposingSpans == null) {
Context context;
if (mTargetView != null) {
context = mTargetView.getContext();
} else if (mIMM.mServedView != null) {
context = mIMM.mServedView.getContext();
} else {
context = null;
}
if (context != null) {
TypedArray ta = context.getTheme()
.obtainStyledAttributes(new int[] {
com.android.internal.R.attr.candidatesTextStyleSpans
});
CharSequence style = ta.getText(0);
ta.recycle();
if (style != null && style instanceof Spanned) {
mDefaultComposingSpans = ((Spanned)style).getSpans(
0, style.length(), Object.class);
}
}
}
ensureDefaultComposingSpans();
if (mDefaultComposingSpans != null) {
for (int i = 0; i < mDefaultComposingSpans.length; ++i) {
sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
}
}
} else {

View File

@ -79,6 +79,21 @@ public interface InputConnection {
*/
public CharSequence getTextAfterCursor(int n, int flags);
/**
* Gets the selected text, if any.
*
* <p>This method may fail if either the input connection has become
* invalid (such as its process crashing) or the client is taking too
* long to respond with the text (it is given a couple of seconds to return).
* In either case, a null is returned.
*
* @param flags Supplies additional options controlling how the text is
* returned. May be either 0 or {@link #GET_TEXT_WITH_STYLES}.
* @return Returns the text that is currently selected, if any, or null if
* no text is selected.
*/
public CharSequence getSelectedText(int flags);
/**
* Retrieve the current capitalization mode in effect at the current
* cursor position in the text. See
@ -161,6 +176,18 @@ public interface InputConnection {
*/
public boolean setComposingText(CharSequence text, int newCursorPosition);
/**
* Mark a certain region of text as composing text. Any composing text set
* previously will be removed automatically. The default style for composing
* text is used.
*
* @param start the position in the text at which the composing region begins
* @param end the position in the text at which the composing region ends
* @return Returns true on success, false if the input connection is no longer
* valid.
*/
public boolean setComposingRegion(int start, int end);
/**
* Have the text editor finish whatever composing text is currently
* active. This simply leaves the text as-is, removing any special

View File

@ -50,6 +50,10 @@ public class InputConnectionWrapper implements InputConnection {
return mTarget.getTextAfterCursor(n, flags);
}
public CharSequence getSelectedText(int flags) {
return mTarget.getSelectedText(flags);
}
public int getCursorCapsMode(int reqModes) {
return mTarget.getCursorCapsMode(reqModes);
}
@ -67,6 +71,10 @@ public class InputConnectionWrapper implements InputConnection {
return mTarget.setComposingText(text, newCursorPosition);
}
public boolean setComposingRegion(int start, int end) {
return mTarget.setComposingRegion(start, end);
}
public boolean finishComposingText() {
return mTarget.finishComposingText();
}

View File

@ -31,9 +31,10 @@ import java.lang.ref.WeakReference;
public class IInputConnectionWrapper extends IInputContext.Stub {
static final String TAG = "IInputConnectionWrapper";
private static final int DO_GET_TEXT_AFTER_CURSOR = 10;
private static final int DO_GET_TEXT_BEFORE_CURSOR = 20;
private static final int DO_GET_SELECTED_TEXT = 25;
private static final int DO_GET_CURSOR_CAPS_MODE = 30;
private static final int DO_GET_EXTRACTED_TEXT = 40;
private static final int DO_COMMIT_TEXT = 50;
@ -42,6 +43,7 @@ public class IInputConnectionWrapper extends IInputContext.Stub {
private static final int DO_PERFORM_EDITOR_ACTION = 58;
private static final int DO_PERFORM_CONTEXT_MENU_ACTION = 59;
private static final int DO_SET_COMPOSING_TEXT = 60;
private static final int DO_SET_COMPOSING_REGION = 63;
private static final int DO_FINISH_COMPOSING_TEXT = 65;
private static final int DO_SEND_KEY_EVENT = 70;
private static final int DO_DELETE_SURROUNDING_TEXT = 80;
@ -50,7 +52,7 @@ public class IInputConnectionWrapper extends IInputContext.Stub {
private static final int DO_REPORT_FULLSCREEN_MODE = 100;
private static final int DO_PERFORM_PRIVATE_COMMAND = 120;
private static final int DO_CLEAR_META_KEY_STATES = 130;
private WeakReference<InputConnection> mInputConnection;
private Looper mMainLooper;
@ -92,6 +94,10 @@ public class IInputConnectionWrapper extends IInputContext.Stub {
dispatchMessage(obtainMessageIISC(DO_GET_TEXT_BEFORE_CURSOR, length, flags, seq, callback));
}
public void getSelectedText(int flags, int seq, IInputContextCallback callback) {
dispatchMessage(obtainMessageISC(DO_GET_SELECTED_TEXT, flags, seq, callback));
}
public void getCursorCapsMode(int reqModes, int seq, IInputContextCallback callback) {
dispatchMessage(obtainMessageISC(DO_GET_CURSOR_CAPS_MODE, reqModes, seq, callback));
}
@ -122,6 +128,10 @@ public class IInputConnectionWrapper extends IInputContext.Stub {
dispatchMessage(obtainMessageII(DO_PERFORM_CONTEXT_MENU_ACTION, id, 0));
}
public void setComposingRegion(int start, int end) {
dispatchMessage(obtainMessageII(DO_SET_COMPOSING_REGION, start, end));
}
public void setComposingText(CharSequence text, int newCursorPosition) {
dispatchMessage(obtainMessageIO(DO_SET_COMPOSING_TEXT, newCursorPosition, text));
}
@ -206,6 +216,22 @@ public class IInputConnectionWrapper extends IInputContext.Stub {
}
return;
}
case DO_GET_SELECTED_TEXT: {
SomeArgs args = (SomeArgs)msg.obj;
try {
InputConnection ic = mInputConnection.get();
if (ic == null || !isActive()) {
Log.w(TAG, "getSelectedText on inactive InputConnection");
args.callback.setSelectedText(null, args.seq);
return;
}
args.callback.setSelectedText(ic.getSelectedText(
msg.arg1), args.seq);
} catch (RemoteException e) {
Log.w(TAG, "Got RemoteException calling setSelectedText", e);
}
return;
}
case DO_GET_CURSOR_CAPS_MODE: {
SomeArgs args = (SomeArgs)msg.obj;
try {
@ -292,6 +318,15 @@ public class IInputConnectionWrapper extends IInputContext.Stub {
ic.setComposingText((CharSequence)msg.obj, msg.arg1);
return;
}
case DO_SET_COMPOSING_REGION: {
InputConnection ic = mInputConnection.get();
if (ic == null || !isActive()) {
Log.w(TAG, "setComposingRegion on inactive InputConnection");
return;
}
ic.setComposingRegion(msg.arg1, msg.arg2);
return;
}
case DO_FINISH_COMPOSING_TEXT: {
InputConnection ic = mInputConnection.get();
// Note we do NOT check isActive() here, because this is safe

View File

@ -65,4 +65,8 @@ import com.android.internal.view.IInputContextCallback;
void clearMetaKeyStates(int states);
void performPrivateCommand(String action, in Bundle data);
void setComposingRegion(int start, int end);
void getSelectedText(int flags, int seq, IInputContextCallback callback);
}

View File

@ -26,4 +26,5 @@ oneway interface IInputContextCallback {
void setTextAfterCursor(CharSequence textAfterCursor, int seq);
void setCursorCapsMode(int capsMode, int seq);
void setExtractedText(in ExtractedText extractedText, int seq);
void setSelectedText(CharSequence selectedText, int seq);
}

View File

@ -16,8 +16,6 @@
package com.android.internal.view;
import com.android.internal.view.IInputContext;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
@ -38,6 +36,7 @@ public class InputConnectionWrapper implements InputConnection {
public boolean mHaveValue;
public CharSequence mTextBeforeCursor;
public CharSequence mTextAfterCursor;
public CharSequence mSelectedText;
public ExtractedText mExtractedText;
public int mCursorCapsMode;
@ -114,6 +113,19 @@ public class InputConnectionWrapper implements InputConnection {
}
}
public void setSelectedText(CharSequence selectedText, int seq) {
synchronized (this) {
if (seq == mSeq) {
mSelectedText = selectedText;
mHaveValue = true;
notifyAll();
} else {
Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
+ ") in setSelectedText, ignoring.");
}
}
}
public void setCursorCapsMode(int capsMode, int seq) {
synchronized (this) {
if (seq == mSeq) {
@ -203,6 +215,24 @@ public class InputConnectionWrapper implements InputConnection {
return value;
}
public CharSequence getSelectedText(int flags) {
CharSequence value = null;
try {
InputContextCallback callback = InputContextCallback.getInstance();
mIInputContext.getSelectedText(flags, callback.mSeq, callback);
synchronized (callback) {
callback.waitForResultLocked();
if (callback.mHaveValue) {
value = callback.mSelectedText;
}
}
callback.dispose();
} catch (RemoteException e) {
return null;
}
return value;
}
public int getCursorCapsMode(int reqModes) {
int value = 0;
try {
@ -283,7 +313,16 @@ public class InputConnectionWrapper implements InputConnection {
return false;
}
}
public boolean setComposingRegion(int start, int end) {
try {
mIInputContext.setComposingRegion(start, end);
return true;
} catch (RemoteException e) {
return false;
}
}
public boolean setComposingText(CharSequence text, int newCursorPosition) {
try {
mIInputContext.setComposingText(text, newCursorPosition);