Merge "Have Latin IME re-capitalize a selected string"

main
Jean Chalard 2013-04-15 11:29:27 +00:00 committed by Android (Google) Code Review
commit 033958330d
5 changed files with 420 additions and 1 deletions

View File

@ -161,6 +161,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
mPositionalInfoForUserDictPendingAddition = null; mPositionalInfoForUserDictPendingAddition = null;
private final WordComposer mWordComposer = new WordComposer(); private final WordComposer mWordComposer = new WordComposer();
private final RichInputConnection mConnection = new RichInputConnection(this); private final RichInputConnection mConnection = new RichInputConnection(this);
private RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(-1, -1, "",
Locale.getDefault(), ""); // Dummy object that will match no real recapitalize
// Keep track of the last selection range to decide if we need to show word alternatives // Keep track of the last selection range to decide if we need to show word alternatives
private static final int NOT_A_CURSOR_POSITION = -1; private static final int NOT_A_CURSOR_POSITION = -1;
@ -1387,8 +1389,13 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
LatinImeLogger.logOnDelete(x, y); LatinImeLogger.logOnDelete(x, y);
break; break;
case Constants.CODE_SHIFT: case Constants.CODE_SHIFT:
// Note: calling back to the keyboard on Shift key is handled in onPressKey()
// and onReleaseKey().
handleRecapitalize();
break;
case Constants.CODE_SWITCH_ALPHA_SYMBOL: case Constants.CODE_SWITCH_ALPHA_SYMBOL:
// Shift and symbol key is handled in onPressKey() and onReleaseKey(). // Note: calling back to the keyboard on symbol key is handled in onPressKey()
// and onReleaseKey().
break; break;
case Constants.CODE_SETTINGS: case Constants.CODE_SETTINGS:
onSettingsKeyPressed(); onSettingsKeyPressed();
@ -1943,6 +1950,37 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
} }
} }
private void handleRecapitalize() {
if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
// If we have a recapitalize in progress, use it; otherwise, create a new one.
if (null == mRecapitalizeStatus
|| !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
mRecapitalizeStatus =
new RecapitalizeStatus(mLastSelectionStart, mLastSelectionEnd,
mConnection.getSelectedText(0 /* flags, 0 for no styles */).toString(),
mSettings.getCurrentLocale(), mSettings.getWordSeparators());
// We trim leading and trailing whitespace.
mRecapitalizeStatus.trim();
// Trimming the object may have changed the length of the string, and we need to
// reposition the selection handles accordingly. As this result in an IPC call,
// only do it if it's actually necessary, in other words if the recapitalize status
// is not set at the same place as before.
if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
}
}
mRecapitalizeStatus.rotate();
final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
mConnection.deleteSurroundingText(numCharsDeleted, 0);
mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
}
// Returns true if we did an autocorrection, false otherwise. // Returns true if we did an autocorrection, false otherwise.
private boolean handleSeparator(final int primaryCode, final int x, final int y, private boolean handleSeparator(final int primaryCode, final int x, final int y,
final int spaceState) { final int spaceState) {

View File

@ -0,0 +1,169 @@
/*
* Copyright (C) 2013 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 com.android.inputmethod.latin.StringUtils;
import java.util.Locale;
/**
* The status of the current recapitalize process.
*/
public class RecapitalizeStatus {
public static final int CAPS_MODE_ORIGINAL_MIXED_CASE = 0;
public static final int CAPS_MODE_ALL_LOWER = 1;
public static final int CAPS_MODE_FIRST_WORD_UPPER = 2;
public static final int CAPS_MODE_ALL_UPPER = 3;
// When adding a new mode, don't forget to update the CAPS_MODE_LAST constant.
public static final int CAPS_MODE_LAST = CAPS_MODE_ALL_UPPER;
private static final int[] ROTATION_STYLE = {
CAPS_MODE_ORIGINAL_MIXED_CASE,
CAPS_MODE_ALL_LOWER,
CAPS_MODE_FIRST_WORD_UPPER,
CAPS_MODE_ALL_UPPER
};
private static final int getStringMode(final String string, final String separators) {
if (StringUtils.isIdenticalAfterUpcase(string)) {
return CAPS_MODE_ALL_UPPER;
} else if (StringUtils.isIdenticalAfterDowncase(string)) {
return CAPS_MODE_ALL_LOWER;
} else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) {
return CAPS_MODE_FIRST_WORD_UPPER;
} else {
return CAPS_MODE_ORIGINAL_MIXED_CASE;
}
}
/**
* We store the location of the cursor and the string that was there before the undoable
* action was done, and the location of the cursor and the string that was there after.
*/
private int mCursorStartBefore;
private int mCursorEndBefore;
private String mStringBefore;
private int mCursorStartAfter;
private int mCursorEndAfter;
private int mRotationStyleCurrentIndex;
private final boolean mSkipOriginalMixedCaseMode;
private final Locale mLocale;
private final String mSeparators;
private String mStringAfter;
public RecapitalizeStatus(final int cursorStart, final int cursorEnd, final String string,
final Locale locale, final String separators) {
mCursorStartBefore = cursorStart;
mCursorEndBefore = cursorEnd;
mStringBefore = string;
mCursorStartAfter = cursorStart;
mCursorEndAfter = cursorEnd;
mStringAfter = string;
final int initialMode = getStringMode(mStringBefore, separators);
mLocale = locale;
mSeparators = separators;
if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) {
mRotationStyleCurrentIndex = 0;
mSkipOriginalMixedCaseMode = false;
} else {
// Find the current mode in the array.
int currentMode;
for (currentMode = ROTATION_STYLE.length - 1; currentMode > 0; --currentMode) {
if (ROTATION_STYLE[currentMode] == initialMode) {
break;
}
}
mRotationStyleCurrentIndex = currentMode;
mSkipOriginalMixedCaseMode = true;
}
}
public boolean isSetAt(final int cursorStart, final int cursorEnd) {
return cursorStart == mCursorStartAfter && cursorEnd == mCursorEndAfter;
}
/**
* Rotate through the different possible capitalization modes.
*/
public void rotate() {
final String oldResult = mStringAfter;
int count = 0; // Protection against infinite loop.
do {
mRotationStyleCurrentIndex = (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
if (CAPS_MODE_ORIGINAL_MIXED_CASE == ROTATION_STYLE[mRotationStyleCurrentIndex]
&& mSkipOriginalMixedCaseMode) {
mRotationStyleCurrentIndex =
(mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
}
++count;
switch (ROTATION_STYLE[mRotationStyleCurrentIndex]) {
case CAPS_MODE_ORIGINAL_MIXED_CASE:
mStringAfter = mStringBefore;
break;
case CAPS_MODE_ALL_LOWER:
mStringAfter = mStringBefore.toLowerCase(mLocale);
break;
case CAPS_MODE_FIRST_WORD_UPPER:
mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators,
mLocale);
break;
case CAPS_MODE_ALL_UPPER:
mStringAfter = mStringBefore.toUpperCase(mLocale);
break;
default:
mStringAfter = mStringBefore;
}
} while (mStringAfter.equals(oldResult) && count < 5);
mCursorEndAfter = mCursorStartAfter + mStringAfter.length();
}
/**
* Remove leading/trailing whitespace from the considered string.
*/
public void trim() {
final int len = mStringBefore.length();
int nonWhitespaceStart = 0;
for (; nonWhitespaceStart < len;
nonWhitespaceStart = mStringBefore.offsetByCodePoints(nonWhitespaceStart, 1)) {
final int codePoint = mStringBefore.codePointAt(nonWhitespaceStart);
if (!Character.isWhitespace(codePoint)) break;
}
int nonWhitespaceEnd = len;
for (; nonWhitespaceEnd > 0;
nonWhitespaceEnd = mStringBefore.offsetByCodePoints(nonWhitespaceEnd, -1)) {
final int codePoint = mStringBefore.codePointBefore(nonWhitespaceEnd);
if (!Character.isWhitespace(codePoint)) break;
}
if (0 != nonWhitespaceStart || len != nonWhitespaceEnd) {
mCursorEndBefore = mCursorEndAfter = mCursorStartBefore + nonWhitespaceEnd;
mCursorStartBefore = mCursorStartAfter = mCursorStartBefore + nonWhitespaceStart;
mStringAfter = mStringBefore =
mStringBefore.substring(nonWhitespaceStart, nonWhitespaceEnd);
}
}
public String getRecapitalizedString() {
return mStringAfter;
}
public int getNewCursorStart() {
return mCursorStartAfter;
}
public int getNewCursorEnd() {
return mCursorEndAfter;
}
}

View File

@ -183,6 +183,11 @@ public final class RichInputConnection {
} }
} }
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. * Gets the caps modes we should be in after this specific string.
* *

View File

@ -138,6 +138,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return mSettingsValues.mWordSeparators; return mSettingsValues.mWordSeparators;
} }
public Locale getCurrentLocale() {
return mCurrentLocale;
}
// Accessed from the settings interface, hence public // Accessed from the settings interface, hence public
public static boolean readKeypressSoundEnabled(final SharedPreferences prefs, public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
final Resources res) { final Resources res) {

View File

@ -0,0 +1,203 @@
/*
* Copyright (C) 2013 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.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import java.util.Locale;
@SmallTest
public class RecapitalizeStatusTests extends AndroidTestCase {
public void testTrim() {
RecapitalizeStatus status = new RecapitalizeStatus(30, 40, "abcdefghij",
Locale.ENGLISH, " ");
status.trim();
assertEquals("abcdefghij", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(40, status.getNewCursorEnd());
status = new RecapitalizeStatus(30, 44, " abcdefghij",
Locale.ENGLISH, " ");
status.trim();
assertEquals("abcdefghij", status.getRecapitalizedString());
assertEquals(34, status.getNewCursorStart());
assertEquals(44, status.getNewCursorEnd());
status = new RecapitalizeStatus(30, 40, "abcdefgh ",
Locale.ENGLISH, " ");
status.trim();
assertEquals("abcdefgh", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(38, status.getNewCursorEnd());
status = new RecapitalizeStatus(30, 45, " abcdefghij ",
Locale.ENGLISH, " ");
status.trim();
assertEquals("abcdefghij", status.getRecapitalizedString());
assertEquals(33, status.getNewCursorStart());
assertEquals(43, status.getNewCursorEnd());
}
public void testRotate() {
RecapitalizeStatus status = new RecapitalizeStatus(29, 40, "abcd efghij",
Locale.ENGLISH, " ");
status.rotate();
assertEquals("Abcd Efghij", status.getRecapitalizedString());
assertEquals(29, status.getNewCursorStart());
assertEquals(40, status.getNewCursorEnd());
status.rotate();
assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
status.rotate();
assertEquals("abcd efghij", status.getRecapitalizedString());
status.rotate();
assertEquals("Abcd Efghij", status.getRecapitalizedString());
status = new RecapitalizeStatus(29, 40, "Abcd Efghij",
Locale.ENGLISH, " ");
status.rotate();
assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
assertEquals(29, status.getNewCursorStart());
assertEquals(40, status.getNewCursorEnd());
status.rotate();
assertEquals("abcd efghij", status.getRecapitalizedString());
status.rotate();
assertEquals("Abcd Efghij", status.getRecapitalizedString());
status.rotate();
assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
status = new RecapitalizeStatus(29, 40, "ABCD EFGHIJ",
Locale.ENGLISH, " ");
status.rotate();
assertEquals("abcd efghij", status.getRecapitalizedString());
assertEquals(29, status.getNewCursorStart());
assertEquals(40, status.getNewCursorEnd());
status.rotate();
assertEquals("Abcd Efghij", status.getRecapitalizedString());
status.rotate();
assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
status.rotate();
assertEquals("abcd efghij", status.getRecapitalizedString());
status = new RecapitalizeStatus(29, 39, "AbCDefghij",
Locale.ENGLISH, " ");
status.rotate();
assertEquals("abcdefghij", status.getRecapitalizedString());
assertEquals(29, status.getNewCursorStart());
assertEquals(39, status.getNewCursorEnd());
status.rotate();
assertEquals("Abcdefghij", status.getRecapitalizedString());
status.rotate();
assertEquals("ABCDEFGHIJ", status.getRecapitalizedString());
status.rotate();
assertEquals("AbCDefghij", status.getRecapitalizedString());
status.rotate();
assertEquals("abcdefghij", status.getRecapitalizedString());
status = new RecapitalizeStatus(29, 40, "Abcd efghij",
Locale.ENGLISH, " ");
status.rotate();
assertEquals("abcd efghij", status.getRecapitalizedString());
assertEquals(29, status.getNewCursorStart());
assertEquals(40, status.getNewCursorEnd());
status.rotate();
assertEquals("Abcd Efghij", status.getRecapitalizedString());
status.rotate();
assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
status.rotate();
assertEquals("Abcd efghij", status.getRecapitalizedString());
status.rotate();
assertEquals("abcd efghij", status.getRecapitalizedString());
status = new RecapitalizeStatus(30, 34, "grüß", Locale.GERMAN, " ");
status.rotate();
assertEquals("Grüß", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(34, status.getNewCursorEnd());
status.rotate();
assertEquals("GRÜSS", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(35, status.getNewCursorEnd());
status.rotate();
assertEquals("grüß", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(34, status.getNewCursorEnd());
status.rotate();
assertEquals("Grüß", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(34, status.getNewCursorEnd());
status = new RecapitalizeStatus(30, 33, "œuf", Locale.FRENCH, " ");
status.rotate();
assertEquals("Œuf", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status.rotate();
assertEquals("ŒUF", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status.rotate();
assertEquals("œuf", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status.rotate();
assertEquals("Œuf", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status = new RecapitalizeStatus(30, 33, "œUf", Locale.FRENCH, " ");
status.rotate();
assertEquals("œuf", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status.rotate();
assertEquals("Œuf", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status.rotate();
assertEquals("ŒUF", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status.rotate();
assertEquals("œUf", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status.rotate();
assertEquals("œuf", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(33, status.getNewCursorEnd());
status = new RecapitalizeStatus(30, 35, "école", Locale.FRENCH, " ");
status.rotate();
assertEquals("École", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(35, status.getNewCursorEnd());
status.rotate();
assertEquals("ÉCOLE", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(35, status.getNewCursorEnd());
status.rotate();
assertEquals("école", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(35, status.getNewCursorEnd());
status.rotate();
assertEquals("École", status.getRecapitalizedString());
assertEquals(30, status.getNewCursorStart());
assertEquals(35, status.getNewCursorEnd());
}
}