e5dee0af4a
The documentation for setComposingRegion states explicitly that it does not move the cursor. This is just a bug. This does not have any ill effects right now, but it will have in later changes if not fixed. As for the selection handling, the specific test that this code removes used to serve a purpose, but it does not any more because the code using the value has been much sanitized. Now the variable can just take the obvious value, and become so self-explanatory that the comments are unnecessary. Change-Id: I548d899b38776bd3ab5f5361aab0d89d98f12e73
742 lines
34 KiB
Java
742 lines
34 KiB
Java
/*
|
|
* Copyright (C) 2012 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.inputmethod.latin;
|
|
|
|
import android.inputmethodservice.InputMethodService;
|
|
import android.text.SpannableString;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.KeyEvent;
|
|
import android.view.inputmethod.CompletionInfo;
|
|
import android.view.inputmethod.CorrectionInfo;
|
|
import android.view.inputmethod.ExtractedText;
|
|
import android.view.inputmethod.ExtractedTextRequest;
|
|
import android.view.inputmethod.InputConnection;
|
|
|
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
|
import com.android.inputmethod.research.ResearchLogger;
|
|
|
|
import java.util.Locale;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* Enrichment class for InputConnection to simplify interaction and add functionality.
|
|
*
|
|
* This class serves as a wrapper to be able to simply add hooks to any calls to the underlying
|
|
* InputConnection. It also keeps track of a number of things to avoid having to call upon IPC
|
|
* all the time to find out what text is in the buffer, when we need it to determine caps mode
|
|
* for example.
|
|
*/
|
|
public final class RichInputConnection {
|
|
private static final String TAG = RichInputConnection.class.getSimpleName();
|
|
private static final boolean DBG = false;
|
|
private static final boolean DEBUG_PREVIOUS_TEXT = false;
|
|
private static final boolean DEBUG_BATCH_NESTING = false;
|
|
// Provision for a long word pair and a separator
|
|
private static final int LOOKBACK_CHARACTER_NUM = Constants.Dictionary.MAX_WORD_LENGTH * 2 + 1;
|
|
private static final Pattern spaceRegex = Pattern.compile("\\s+");
|
|
private static final int INVALID_CURSOR_POSITION = -1;
|
|
|
|
/**
|
|
* This variable contains the value LatinIME thinks the cursor position should be at now.
|
|
* This is a few steps in advance of what the TextView thinks it is, because TextView will
|
|
* only know after the IPC calls gets through.
|
|
*/
|
|
private int mCurrentCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points
|
|
/**
|
|
* This contains the committed text immediately preceding the cursor and the composing
|
|
* text if any. It is refreshed when the cursor moves by calling upon the TextView.
|
|
*/
|
|
private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
|
|
/**
|
|
* This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
|
|
*/
|
|
private final StringBuilder mComposingText = new StringBuilder();
|
|
// A hint on how many characters to cache from the TextView. A good value of this is given by
|
|
// how many characters we need to be able to almost always find the caps mode.
|
|
private static final int DEFAULT_TEXT_CACHE_SIZE = 100;
|
|
|
|
private final InputMethodService mParent;
|
|
InputConnection mIC;
|
|
int mNestLevel;
|
|
public RichInputConnection(final InputMethodService parent) {
|
|
mParent = parent;
|
|
mIC = null;
|
|
mNestLevel = 0;
|
|
}
|
|
|
|
private void checkConsistencyForDebug() {
|
|
final ExtractedTextRequest r = new ExtractedTextRequest();
|
|
r.hintMaxChars = 0;
|
|
r.hintMaxLines = 0;
|
|
r.token = 1;
|
|
r.flags = 0;
|
|
final ExtractedText et = mIC.getExtractedText(r, 0);
|
|
final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
|
|
final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
|
|
.append(mComposingText);
|
|
if (null == et || null == beforeCursor) return;
|
|
final int actualLength = Math.min(beforeCursor.length(), internal.length());
|
|
if (internal.length() > actualLength) {
|
|
internal.delete(0, internal.length() - actualLength);
|
|
}
|
|
final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
|
|
: beforeCursor.subSequence(beforeCursor.length() - actualLength,
|
|
beforeCursor.length()).toString();
|
|
if (et.selectionStart != mCurrentCursorPosition
|
|
|| !(reference.equals(internal.toString()))) {
|
|
final String context = "Expected cursor position = " + mCurrentCursorPosition
|
|
+ "\nActual cursor position = " + et.selectionStart
|
|
+ "\nExpected text = " + internal.length() + " " + internal
|
|
+ "\nActual text = " + reference.length() + " " + reference;
|
|
((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
|
|
} else {
|
|
Log.e(TAG, Utils.getStackTrace(2));
|
|
Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart);
|
|
}
|
|
}
|
|
|
|
public void beginBatchEdit() {
|
|
if (++mNestLevel == 1) {
|
|
mIC = mParent.getCurrentInputConnection();
|
|
if (null != mIC) {
|
|
mIC.beginBatchEdit();
|
|
}
|
|
} else {
|
|
if (DBG) {
|
|
throw new RuntimeException("Nest level too deep");
|
|
} else {
|
|
Log.e(TAG, "Nest level too deep : " + mNestLevel);
|
|
}
|
|
}
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
}
|
|
|
|
public void endBatchEdit() {
|
|
if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
|
|
if (--mNestLevel == 0 && null != mIC) {
|
|
mIC.endBatchEdit();
|
|
}
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
}
|
|
|
|
public void resetCachesUponCursorMove(final int newCursorPosition,
|
|
final boolean shouldFinishComposition) {
|
|
mCurrentCursorPosition = newCursorPosition;
|
|
mComposingText.setLength(0);
|
|
mCommittedTextBeforeComposingText.setLength(0);
|
|
final CharSequence textBeforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
|
|
if (null != textBeforeCursor) mCommittedTextBeforeComposingText.append(textBeforeCursor);
|
|
if (null != mIC && shouldFinishComposition) {
|
|
mIC.finishComposingText();
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_finishComposingText();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void checkBatchEdit() {
|
|
if (mNestLevel != 1) {
|
|
// TODO: exception instead
|
|
Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
|
|
Log.e(TAG, Utils.getStackTrace(4));
|
|
}
|
|
}
|
|
|
|
public void finishComposingText() {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
mCommittedTextBeforeComposingText.append(mComposingText);
|
|
mCurrentCursorPosition += mComposingText.length();
|
|
mComposingText.setLength(0);
|
|
if (null != mIC) {
|
|
mIC.finishComposingText();
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_finishComposingText();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void commitText(final CharSequence text, final int i) {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
mCommittedTextBeforeComposingText.append(text);
|
|
mCurrentCursorPosition += text.length() - mComposingText.length();
|
|
mComposingText.setLength(0);
|
|
if (null != mIC) {
|
|
mIC.commitText(text, i);
|
|
}
|
|
}
|
|
|
|
public CharSequence getSelectedText(final int flags) {
|
|
if (null == mIC) return null;
|
|
return mIC.getSelectedText(flags);
|
|
}
|
|
|
|
/**
|
|
* Gets the caps modes we should be in after this specific string.
|
|
*
|
|
* This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument.
|
|
* This method also supports faking an additional space after the string passed in argument,
|
|
* to support cases where a space will be added automatically, like in phantom space
|
|
* state for example.
|
|
* Note that for English, we are using American typography rules (which are not specific to
|
|
* American English, it's just the most common set of rules for English).
|
|
*
|
|
* @param inputType a mask of the caps modes to test for.
|
|
* @param locale what language should be considered.
|
|
* @param hasSpaceBefore if we should consider there should be a space after the string.
|
|
* @return the caps modes that should be on as a set of bits
|
|
*/
|
|
public int getCursorCapsMode(final int inputType, final Locale locale,
|
|
final boolean hasSpaceBefore) {
|
|
mIC = mParent.getCurrentInputConnection();
|
|
if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
|
|
if (!TextUtils.isEmpty(mComposingText)) {
|
|
if (hasSpaceBefore) {
|
|
// If we have some composing text and a space before, then we should have
|
|
// MODE_CHARACTERS and MODE_WORDS on.
|
|
return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType;
|
|
} else {
|
|
// We have some composing text - we should be in MODE_CHARACTERS only.
|
|
return TextUtils.CAP_MODE_CHARACTERS & inputType;
|
|
}
|
|
}
|
|
// TODO: this will generally work, but there may be cases where the buffer contains SOME
|
|
// information but not enough to determine the caps mode accurately. This may happen after
|
|
// heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so.
|
|
// getCapsMode should be updated to be able to return a "not enough info" result so that
|
|
// we can get more context only when needed.
|
|
if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mCurrentCursorPosition) {
|
|
mCommittedTextBeforeComposingText.append(
|
|
getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
|
|
}
|
|
// This never calls InputConnection#getCapsMode - in fact, it's a static method that
|
|
// never blocks or initiates IPC.
|
|
return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale,
|
|
hasSpaceBefore);
|
|
}
|
|
|
|
public int getCodePointBeforeCursor() {
|
|
if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE;
|
|
return Character.codePointBefore(mCommittedTextBeforeComposingText,
|
|
mCommittedTextBeforeComposingText.length());
|
|
}
|
|
|
|
public CharSequence getTextBeforeCursor(final int i, final int j) {
|
|
// TODO: use mCommittedTextBeforeComposingText if possible to improve performance
|
|
mIC = mParent.getCurrentInputConnection();
|
|
if (null != mIC) return mIC.getTextBeforeCursor(i, j);
|
|
return null;
|
|
}
|
|
|
|
public CharSequence getTextAfterCursor(final int i, final int j) {
|
|
mIC = mParent.getCurrentInputConnection();
|
|
if (null != mIC) return mIC.getTextAfterCursor(i, j);
|
|
return null;
|
|
}
|
|
|
|
public void deleteSurroundingText(final int i, final int j) {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
final int remainingChars = mComposingText.length() - i;
|
|
if (remainingChars >= 0) {
|
|
mComposingText.setLength(remainingChars);
|
|
} else {
|
|
mComposingText.setLength(0);
|
|
// Never cut under 0
|
|
final int len = Math.max(mCommittedTextBeforeComposingText.length()
|
|
+ remainingChars, 0);
|
|
mCommittedTextBeforeComposingText.setLength(len);
|
|
}
|
|
if (mCurrentCursorPosition > i) {
|
|
mCurrentCursorPosition -= i;
|
|
} else {
|
|
mCurrentCursorPosition = 0;
|
|
}
|
|
if (null != mIC) {
|
|
mIC.deleteSurroundingText(i, j);
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
|
|
}
|
|
}
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
}
|
|
|
|
public void performEditorAction(final int actionId) {
|
|
mIC = mParent.getCurrentInputConnection();
|
|
if (null != mIC) {
|
|
mIC.performEditorAction(actionId);
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_performEditorAction(actionId);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void sendKeyEvent(final KeyEvent keyEvent) {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
// This method is only called for enter or backspace when speaking to old applications
|
|
// (target SDK <= 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)), or for digits.
|
|
// When talking to new applications we never use this method because it's inherently
|
|
// racy and has unpredictable results, but for backward compatibility we continue
|
|
// sending the key events for only Enter and Backspace because some applications
|
|
// mistakenly catch them to do some stuff.
|
|
switch (keyEvent.getKeyCode()) {
|
|
case KeyEvent.KEYCODE_ENTER:
|
|
mCommittedTextBeforeComposingText.append("\n");
|
|
mCurrentCursorPosition += 1;
|
|
break;
|
|
case KeyEvent.KEYCODE_DEL:
|
|
if (0 == mComposingText.length()) {
|
|
if (mCommittedTextBeforeComposingText.length() > 0) {
|
|
mCommittedTextBeforeComposingText.delete(
|
|
mCommittedTextBeforeComposingText.length() - 1,
|
|
mCommittedTextBeforeComposingText.length());
|
|
}
|
|
} else {
|
|
mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
|
|
}
|
|
if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1;
|
|
break;
|
|
case KeyEvent.KEYCODE_UNKNOWN:
|
|
if (null != keyEvent.getCharacters()) {
|
|
mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
|
|
mCurrentCursorPosition += keyEvent.getCharacters().length();
|
|
}
|
|
break;
|
|
default:
|
|
final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
|
|
mCommittedTextBeforeComposingText.append(text);
|
|
mCurrentCursorPosition += text.length();
|
|
break;
|
|
}
|
|
}
|
|
if (null != mIC) {
|
|
mIC.sendKeyEvent(keyEvent);
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_sendKeyEvent(keyEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void setComposingRegion(final int start, final int end) {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
final CharSequence textBeforeCursor =
|
|
getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE + (end - start), 0);
|
|
mCommittedTextBeforeComposingText.setLength(0);
|
|
if (!TextUtils.isEmpty(textBeforeCursor)) {
|
|
final int indexOfStartOfComposingText =
|
|
Math.max(textBeforeCursor.length() - (end - start), 0);
|
|
mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText,
|
|
textBeforeCursor.length()));
|
|
mCommittedTextBeforeComposingText.append(
|
|
textBeforeCursor.subSequence(0, indexOfStartOfComposingText));
|
|
}
|
|
if (null != mIC) {
|
|
mIC.setComposingRegion(start, end);
|
|
}
|
|
}
|
|
|
|
public void setComposingText(final CharSequence text, final int i) {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
mCurrentCursorPosition += text.length() - mComposingText.length();
|
|
mComposingText.setLength(0);
|
|
mComposingText.append(text);
|
|
// TODO: support values of i != 1. At this time, this is never called with i != 1.
|
|
if (null != mIC) {
|
|
mIC.setComposingText(text, i);
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_setComposingText(text, i);
|
|
}
|
|
}
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
}
|
|
|
|
public void setSelection(final int from, final int to) {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
if (null != mIC) {
|
|
mIC.setSelection(from, to);
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_setSelection(from, to);
|
|
}
|
|
}
|
|
mCurrentCursorPosition = from;
|
|
mCommittedTextBeforeComposingText.setLength(0);
|
|
mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
|
|
}
|
|
|
|
public void commitCorrection(final CorrectionInfo correctionInfo) {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
// This has no effect on the text field and does not change its content. It only makes
|
|
// TextView flash the text for a second based on indices contained in the argument.
|
|
if (null != mIC) {
|
|
mIC.commitCorrection(correctionInfo);
|
|
}
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
}
|
|
|
|
public void commitCompletion(final CompletionInfo completionInfo) {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
CharSequence text = completionInfo.getText();
|
|
// text should never be null, but just in case, it's better to insert nothing than to crash
|
|
if (null == text) text = "";
|
|
mCommittedTextBeforeComposingText.append(text);
|
|
mCurrentCursorPosition += text.length() - mComposingText.length();
|
|
mComposingText.setLength(0);
|
|
if (null != mIC) {
|
|
mIC.commitCompletion(completionInfo);
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_commitCompletion(completionInfo);
|
|
}
|
|
}
|
|
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
|
}
|
|
|
|
@SuppressWarnings("unused")
|
|
public String getNthPreviousWord(final String sentenceSeperators, final int n) {
|
|
mIC = mParent.getCurrentInputConnection();
|
|
if (null == mIC) return null;
|
|
final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
|
|
if (DEBUG_PREVIOUS_TEXT && null != prev) {
|
|
final int checkLength = LOOKBACK_CHARACTER_NUM - 1;
|
|
final String reference = prev.length() <= checkLength ? prev.toString()
|
|
: prev.subSequence(prev.length() - checkLength, prev.length()).toString();
|
|
final StringBuilder internal = new StringBuilder()
|
|
.append(mCommittedTextBeforeComposingText).append(mComposingText);
|
|
if (internal.length() > checkLength) {
|
|
internal.delete(0, internal.length() - checkLength);
|
|
if (!(reference.equals(internal.toString()))) {
|
|
final String context =
|
|
"Expected text = " + internal + "\nActual text = " + reference;
|
|
((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
|
|
}
|
|
}
|
|
}
|
|
return getNthPreviousWord(prev, sentenceSeperators, n);
|
|
}
|
|
|
|
/**
|
|
* Represents a range of text, relative to the current cursor position.
|
|
*/
|
|
public static final class Range {
|
|
/** Characters before selection start */
|
|
public final int mCharsBefore;
|
|
|
|
/**
|
|
* Characters after selection start, including one trailing word
|
|
* separator.
|
|
*/
|
|
public final int mCharsAfter;
|
|
|
|
/** The actual characters that make up a word */
|
|
public final CharSequence mWord;
|
|
|
|
public Range(int charsBefore, int charsAfter, CharSequence word) {
|
|
if (charsBefore < 0 || charsAfter < 0) {
|
|
throw new IndexOutOfBoundsException();
|
|
}
|
|
this.mCharsBefore = charsBefore;
|
|
this.mCharsAfter = charsAfter;
|
|
this.mWord = word;
|
|
}
|
|
}
|
|
|
|
private static boolean isSeparator(int code, String sep) {
|
|
return sep.indexOf(code) != -1;
|
|
}
|
|
|
|
// Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor,
|
|
// n = 2 retrieves the word before that, and so on. This splits on whitespace only.
|
|
// Also, it won't return words that end in a separator (if the nth word before the cursor
|
|
// ends in a separator, it returns null).
|
|
// Example :
|
|
// (n = 1) "abc def|" -> def
|
|
// (n = 1) "abc def |" -> def
|
|
// (n = 1) "abc def. |" -> null
|
|
// (n = 1) "abc def . |" -> null
|
|
// (n = 2) "abc def|" -> abc
|
|
// (n = 2) "abc def |" -> abc
|
|
// (n = 2) "abc def. |" -> abc
|
|
// (n = 2) "abc def . |" -> def
|
|
// (n = 2) "abc|" -> null
|
|
// (n = 2) "abc |" -> null
|
|
// (n = 2) "abc. def|" -> null
|
|
public static String getNthPreviousWord(final CharSequence prev,
|
|
final String sentenceSeperators, final int n) {
|
|
if (prev == null) return null;
|
|
final String[] w = spaceRegex.split(prev);
|
|
|
|
// If we can't find n words, or we found an empty word, return null.
|
|
if (w.length < n) return null;
|
|
final String nthPrevWord = w[w.length - n];
|
|
final int length = nthPrevWord.length();
|
|
if (length <= 0) return null;
|
|
|
|
// If ends in a separator, return null
|
|
final char lastChar = nthPrevWord.charAt(length - 1);
|
|
if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
|
|
|
|
return nthPrevWord;
|
|
}
|
|
|
|
/**
|
|
* @param separators characters which may separate words
|
|
* @return the word that surrounds the cursor, including up to one trailing
|
|
* separator. For example, if the field contains "he|llo world", where |
|
|
* represents the cursor, then "hello " will be returned.
|
|
*/
|
|
public CharSequence getWordAtCursor(String separators) {
|
|
// getWordRangeAtCursor returns null if the connection is null
|
|
Range r = getWordRangeAtCursor(separators, 0);
|
|
return (r == null) ? null : r.mWord;
|
|
}
|
|
|
|
/**
|
|
* Returns the text surrounding the cursor.
|
|
*
|
|
* @param sep a string of characters that split words.
|
|
* @param additionalPrecedingWordsCount the number of words before the current word that should
|
|
* be included in the returned range
|
|
* @return a range containing the text surrounding the cursor
|
|
*/
|
|
public Range getWordRangeAtCursor(final String sep, final int additionalPrecedingWordsCount) {
|
|
mIC = mParent.getCurrentInputConnection();
|
|
if (mIC == null || sep == null) {
|
|
return null;
|
|
}
|
|
final CharSequence before = mIC.getTextBeforeCursor(1000,
|
|
InputConnection.GET_TEXT_WITH_STYLES);
|
|
final CharSequence after = mIC.getTextAfterCursor(1000,
|
|
InputConnection.GET_TEXT_WITH_STYLES);
|
|
if (before == null || after == null) {
|
|
return null;
|
|
}
|
|
|
|
// Going backward, alternate skipping non-separators and separators until enough words
|
|
// have been read.
|
|
int count = additionalPrecedingWordsCount;
|
|
int startIndexInBefore = before.length();
|
|
boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at
|
|
while (true) { // see comments below for why this is guaranteed to halt
|
|
while (startIndexInBefore > 0) {
|
|
final int codePoint = Character.codePointBefore(before, startIndexInBefore);
|
|
if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
|
|
break; // inner loop
|
|
}
|
|
--startIndexInBefore;
|
|
if (Character.isSupplementaryCodePoint(codePoint)) {
|
|
--startIndexInBefore;
|
|
}
|
|
}
|
|
// isStoppingAtWhitespace is true every other time through the loop,
|
|
// so additionalPrecedingWordsCount is guaranteed to become < 0, which
|
|
// guarantees outer loop termination
|
|
if (isStoppingAtWhitespace && (--count < 0)) {
|
|
break; // outer loop
|
|
}
|
|
isStoppingAtWhitespace = !isStoppingAtWhitespace;
|
|
}
|
|
|
|
// Find last word separator after the cursor
|
|
int endIndexInAfter = -1;
|
|
while (++endIndexInAfter < after.length()) {
|
|
final int codePoint = Character.codePointAt(after, endIndexInAfter);
|
|
if (isSeparator(codePoint, sep)) {
|
|
break;
|
|
}
|
|
if (Character.isSupplementaryCodePoint(codePoint)) {
|
|
++endIndexInAfter;
|
|
}
|
|
}
|
|
|
|
final SpannableString word = new SpannableString(TextUtils.concat(
|
|
before.subSequence(startIndexInBefore, before.length()),
|
|
after.subSequence(0, endIndexInAfter)));
|
|
return new Range(before.length() - startIndexInBefore, endIndexInAfter, word);
|
|
}
|
|
|
|
public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
|
|
final CharSequence before = getTextBeforeCursor(1, 0);
|
|
final CharSequence after = getTextAfterCursor(1, 0);
|
|
if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
|
|
&& !settingsValues.isWordConnector(before.charAt(0))) {
|
|
return true;
|
|
}
|
|
if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
|
|
&& !settingsValues.isWordConnector(after.charAt(0))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void removeTrailingSpace() {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
final CharSequence lastOne = getTextBeforeCursor(1, 0);
|
|
if (lastOne != null && lastOne.length() == 1
|
|
&& lastOne.charAt(0) == Constants.CODE_SPACE) {
|
|
deleteSurroundingText(1, 0);
|
|
}
|
|
}
|
|
|
|
public boolean sameAsTextBeforeCursor(final CharSequence text) {
|
|
final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
|
|
return TextUtils.equals(text, beforeText);
|
|
}
|
|
|
|
/* (non-javadoc)
|
|
* Returns the word before the cursor if the cursor is at the end of a word, null otherwise
|
|
*/
|
|
public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) {
|
|
// Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
|
|
// separator or end of line/text)
|
|
// Example: "test|"<EOL> "te|st" get rejected here
|
|
final CharSequence textAfterCursor = getTextAfterCursor(1, 0);
|
|
if (!TextUtils.isEmpty(textAfterCursor)
|
|
&& !settings.isWordSeparator(textAfterCursor.charAt(0))) return null;
|
|
|
|
// Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
|
|
// Example: " -|" gets rejected here but "e-|" and "e|" are okay
|
|
CharSequence word = getWordAtCursor(settings.mWordSeparators);
|
|
// We don't suggest on leading single quotes, so we have to remove them from the word if
|
|
// it starts with single quotes.
|
|
while (!TextUtils.isEmpty(word) && Constants.CODE_SINGLE_QUOTE == word.charAt(0)) {
|
|
word = word.subSequence(1, word.length());
|
|
}
|
|
if (TextUtils.isEmpty(word)) return null;
|
|
// Find the last code point of the string
|
|
final int lastCodePoint = Character.codePointBefore(word, word.length());
|
|
// If for some reason the text field contains non-unicode binary data, or if the
|
|
// charsequence is exactly one char long and the contents is a low surrogate, return null.
|
|
if (!Character.isDefined(lastCodePoint)) return null;
|
|
// Bail out if the cursor is not at the end of a word (cursor must be preceded by
|
|
// non-whitespace, non-separator, non-start-of-text)
|
|
// Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
|
|
if (settings.isWordSeparator(lastCodePoint)) return null;
|
|
final char firstChar = word.charAt(0); // we just tested that word is not empty
|
|
if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
|
|
|
|
// We don't restart suggestion if the first character is not a letter, because we don't
|
|
// start composing when the first character is not a letter.
|
|
if (!Character.isLetter(firstChar)) return null;
|
|
|
|
return word;
|
|
}
|
|
|
|
public boolean revertDoubleSpacePeriod() {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
// Here we test whether we indeed have a period and a space before us. This should not
|
|
// be needed, but it's there just in case something went wrong.
|
|
final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
|
|
final String periodSpace = ". ";
|
|
if (!periodSpace.equals(textBeforeCursor)) {
|
|
// Theoretically we should not be coming here if there isn't ". " before the
|
|
// cursor, but the application may be changing the text while we are typing, so
|
|
// anything goes. We should not crash.
|
|
Log.d(TAG, "Tried to revert double-space combo but we didn't find "
|
|
+ "\"" + periodSpace + "\" just before the cursor.");
|
|
return false;
|
|
}
|
|
deleteSurroundingText(2, 0);
|
|
final String doubleSpace = " ";
|
|
commitText(doubleSpace, 1);
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_revertDoubleSpacePeriod();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public boolean revertSwapPunctuation() {
|
|
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
|
// Here we test whether we indeed have a space and something else before us. This should not
|
|
// be needed, but it's there just in case something went wrong.
|
|
final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
|
|
// NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
|
|
// enter surrogate pairs this code will have been removed.
|
|
if (TextUtils.isEmpty(textBeforeCursor)
|
|
|| (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) {
|
|
// We may only come here if the application is changing the text while we are typing.
|
|
// This is quite a broken case, but not logically impossible, so we shouldn't crash,
|
|
// but some debugging log may be in order.
|
|
Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
|
|
+ "find a space just before the cursor.");
|
|
return false;
|
|
}
|
|
deleteSurroundingText(2, 0);
|
|
final String text = " " + textBeforeCursor.subSequence(0, 1);
|
|
commitText(text, 1);
|
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
|
ResearchLogger.richInputConnection_revertSwapPunctuation();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Heuristic to determine if this is an expected update of the cursor.
|
|
*
|
|
* Sometimes updates to the cursor position are late because of their asynchronous nature.
|
|
* This method tries to determine if this update is one, based on the values of the cursor
|
|
* position in the update, and the currently expected position of the cursor according to
|
|
* LatinIME's internal accounting. If this is not a belated expected update, then it should
|
|
* mean that the user moved the cursor explicitly.
|
|
* This is quite robust, but of course it's not perfect. In particular, it will fail in the
|
|
* case we get an update A, the user types in N characters so as to move the cursor to A+N but
|
|
* we don't get those, and then the user places the cursor between A and A+N, and we get only
|
|
* this update and not the ones in-between. This is almost impossible to achieve even trying
|
|
* very very hard.
|
|
*
|
|
* @param oldSelStart The value of the old cursor position in the update.
|
|
* @param newSelStart The value of the new cursor position in the update.
|
|
* @return whether this is a belated expected update or not.
|
|
*/
|
|
public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) {
|
|
// If this is an update that arrives at our expected position, it's a belated update.
|
|
if (newSelStart == mCurrentCursorPosition) return true;
|
|
// If this is an update that moves the cursor from our expected position, it must be
|
|
// an explicit move.
|
|
if (oldSelStart == mCurrentCursorPosition) return false;
|
|
// The following returns true if newSelStart is between oldSelStart and
|
|
// mCurrentCursorPosition. We assume that if the updated position is between the old
|
|
// position and the expected position, then it must be a belated update.
|
|
return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0;
|
|
}
|
|
|
|
/**
|
|
* The user moved the cursor by hand. Take a note of it.
|
|
* @param newCursorPosition The new cursor position.
|
|
*/
|
|
public void userMovedCursor(final int newCursorPosition) {
|
|
mCurrentCursorPosition = newCursorPosition;
|
|
}
|
|
|
|
/**
|
|
* Looks at the text just before the cursor to find out if it looks like a URL.
|
|
*
|
|
* The weakest point here is, if we don't have enough text bufferized, we may fail to realize
|
|
* we are in URL situation, but other places in this class have the same limitation and it
|
|
* does not matter too much in the practice.
|
|
*/
|
|
public boolean textBeforeCursorLooksLikeURL() {
|
|
return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText);
|
|
}
|
|
}
|