Zero Improvements
* 0-key now types special/math characters. All characters normally avaialable on a computer keyboard are available now * Predictive Mode: Added many new emoji * updated user manual * Fixed the new line being invisible in the suggestions list * Predictive Mode: punctuation list on 1-key has no priorities and remains static all the time * Added 'automatic space' setting * Added 'auto capitalization' setting * Added missing translations * Unsupported emoji are no longer displayed * Code cleanup and speed optimizations * Fixed incorrect suggestion filter values, that would cause broken emoji
This commit is contained in:
parent
6a2e1806d1
commit
b637a0b9d6
22 changed files with 453 additions and 156 deletions
|
|
@ -158,7 +158,7 @@ public class DictionaryLoader {
|
|||
private void importLetters(Language language) {
|
||||
ArrayList<Word> letters = new ArrayList<>();
|
||||
|
||||
for (int key = 0; key <= 9; key++) {
|
||||
for (int key = 2; key <= 9; key++) {
|
||||
for (String langChar : language.getKeyCharacters(key)) {
|
||||
if (langChar.length() == 1 && langChar.charAt(0) >= '0' && langChar.charAt(0) <= '9') {
|
||||
// We do not want 0-9 as "word suggestions" in Predictive mode. It looks confusing
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ import java.util.regex.Pattern;
|
|||
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||
|
||||
|
||||
class InputFieldHelper {
|
||||
public class InputFieldHelper {
|
||||
private static final Pattern beforeCursorWordRegex = Pattern.compile("(\\w+)$");
|
||||
private static final Pattern afterCursorWordRegex = Pattern.compile("^(\\w+)");
|
||||
|
||||
|
||||
public static boolean isThereText(InputConnection currentInputConnection) {
|
||||
if (currentInputConnection == null) {
|
||||
return false;
|
||||
|
|
@ -24,36 +28,6 @@ class InputFieldHelper {
|
|||
}
|
||||
|
||||
|
||||
public static boolean isSpecializedTextField(EditorInfo inputField) {
|
||||
if (inputField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int variation = inputField.inputType & InputType.TYPE_MASK_VARIATION;
|
||||
|
||||
return (
|
||||
variation == InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
|| variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
|| variation == InputType.TYPE_TEXT_VARIATION_FILTER
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* isFilterTextField
|
||||
* handle filter list cases... do not hijack DPAD center and make sure back's go through proper
|
||||
*/
|
||||
public static boolean isFilterTextField(EditorInfo inputField) {
|
||||
if (inputField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int inputType = inputField.inputType & InputType.TYPE_MASK_CLASS;
|
||||
int inputVariation = inputField.inputType & InputType.TYPE_MASK_VARIATION;
|
||||
|
||||
return inputType == InputType.TYPE_CLASS_TEXT && inputVariation == InputType.TYPE_TEXT_VARIATION_FILTER;
|
||||
}
|
||||
|
||||
/**
|
||||
* isDialerField
|
||||
* Dialer fields seem to take care of numbers and backspace on their own,
|
||||
|
|
@ -67,6 +41,53 @@ class InputFieldHelper {
|
|||
}
|
||||
|
||||
|
||||
public static boolean isEmailField(EditorInfo inputField) {
|
||||
if (inputField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int variation = inputField.inputType & InputType.TYPE_MASK_VARIATION;
|
||||
|
||||
return
|
||||
variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||
|| variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* isFilterField
|
||||
* handle filter list cases... do not hijack DPAD center and make sure back's go through proper
|
||||
*/
|
||||
public static boolean isFilterField(EditorInfo inputField) {
|
||||
if (inputField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int inputType = inputField.inputType & InputType.TYPE_MASK_CLASS;
|
||||
int inputVariation = inputField.inputType & InputType.TYPE_MASK_VARIATION;
|
||||
|
||||
return inputType == InputType.TYPE_CLASS_TEXT && inputVariation == InputType.TYPE_TEXT_VARIATION_FILTER;
|
||||
}
|
||||
|
||||
|
||||
private static boolean isPasswordField(EditorInfo inputField) {
|
||||
if (inputField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int variation = inputField.inputType & InputType.TYPE_MASK_VARIATION;
|
||||
|
||||
return
|
||||
variation == InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
|| variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isRegularTextField(EditorInfo inputField) {
|
||||
return !isPasswordField(inputField) && !isEmailField(inputField);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* determineInputModes
|
||||
* Determine the typing mode based on the input field being edited. Returns an ArrayList of the allowed modes.
|
||||
|
|
@ -111,7 +132,7 @@ class InputFieldHelper {
|
|||
// normal alphabetic keyboard, and assume that we should
|
||||
// be doing predictive text (showing candidates as the
|
||||
// user types).
|
||||
if (!isSpecializedTextField(inputField)) {
|
||||
if (!isPasswordField(inputField) && !isFilterField(inputField)) {
|
||||
allowedModes.add(InputMode.MODE_PREDICTIVE);
|
||||
}
|
||||
|
||||
|
|
@ -161,15 +182,46 @@ class InputFieldHelper {
|
|||
return "";
|
||||
}
|
||||
|
||||
String before = (String) currentInputConnection.getTextBeforeCursor(50, 0);
|
||||
String after = (String) currentInputConnection.getTextAfterCursor(50, 0);
|
||||
CharSequence before = currentInputConnection.getTextBeforeCursor(50, 0);
|
||||
CharSequence after = currentInputConnection.getTextAfterCursor(50, 0);
|
||||
if (before == null || after == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Matcher beforeMatch = Pattern.compile("(\\w+)$").matcher(before);
|
||||
Matcher afterMatch = Pattern.compile("^(\\w+)").matcher(after);
|
||||
Matcher beforeMatch = beforeCursorWordRegex.matcher(before);
|
||||
Matcher afterMatch = afterCursorWordRegex.matcher(after);
|
||||
|
||||
return (beforeMatch.find() ? beforeMatch.group(1) : "") + (afterMatch.find() ? afterMatch.group(1) : "");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* deletePrecedingSpace
|
||||
* Deletes the preceding space before the given word. The word must be before the cursor.
|
||||
* No action is taken when there is double space or when it's the beginning of the text field.
|
||||
*/
|
||||
public static void deletePrecedingSpace(InputConnection inputConnection, String word) {
|
||||
if (inputConnection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String searchText = " " + word;
|
||||
|
||||
inputConnection.beginBatchEdit();
|
||||
CharSequence beforeText = inputConnection.getTextBeforeCursor(searchText.length() + 1, 0);
|
||||
if (
|
||||
beforeText == null
|
||||
|| beforeText.length() < searchText.length() + 1
|
||||
|| beforeText.charAt(1) != ' ' // preceding char must be " "
|
||||
|| beforeText.charAt(0) == ' ' // but do nothing when there is double space
|
||||
) {
|
||||
inputConnection.endBatchEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
inputConnection.deleteSurroundingText(searchText.length(), 0);
|
||||
inputConnection.commitText(word, 1);
|
||||
|
||||
inputConnection.endBatchEdit();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
private int ignoreNextKeyUp = 0;
|
||||
|
||||
private int lastKeyCode = 0;
|
||||
private boolean isKeyRepeated = false;
|
||||
private int keyRepeatCounter = 0;
|
||||
|
||||
private int lastNumKeyCode = 0;
|
||||
private boolean isNumKeyRepeated = false;
|
||||
private int numKeyRepeatCounter = 0;
|
||||
|
||||
// throttling
|
||||
private static final int BACKSPACE_DEBOUNCE_TIME = 80;
|
||||
|
|
@ -84,8 +84,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
@Override
|
||||
public void onStartInput(EditorInfo inputField, boolean restarting) {
|
||||
currentInputConnection = getCurrentInputConnection();
|
||||
// Logger.d("T9.onStartInput", "inputType: " + inputField.inputType + " fieldId: " + inputField.fieldId +
|
||||
// " fieldName: " + inputField.fieldName + " packageName: " + inputField.packageName);
|
||||
// Logger.d("T9.onStartInput", "inputType: " + inputField.inputType + " fieldId: " + inputField.fieldId + " fieldName: " + inputField.fieldName + " packageName: " + inputField.packageName);
|
||||
|
||||
mEditing = NON_EDIT;
|
||||
|
||||
|
|
@ -197,6 +196,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
return true;
|
||||
}
|
||||
|
||||
resetKeyRepeat();
|
||||
ignoreNextKeyUp = keyCode;
|
||||
|
||||
if (handleSpecialFunctionKey(keyCode, true)) {
|
||||
|
|
@ -214,7 +214,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
case KeyEvent.KEYCODE_7:
|
||||
case KeyEvent.KEYCODE_8:
|
||||
case KeyEvent.KEYCODE_9:
|
||||
return onNumber(keyCodeToKeyNumber(keyCode), true, false);
|
||||
return onNumber(keyCodeToKeyNumber(keyCode), true, 0);
|
||||
}
|
||||
|
||||
ignoreNextKeyUp = 0;
|
||||
|
|
@ -239,11 +239,11 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
return true;
|
||||
}
|
||||
|
||||
isKeyRepeated = (lastKeyCode == keyCode);
|
||||
keyRepeatCounter = (lastKeyCode == keyCode) ? keyRepeatCounter + 1 : 0;
|
||||
lastKeyCode = keyCode;
|
||||
|
||||
if (isNumber(keyCode)) {
|
||||
isNumKeyRepeated = (lastNumKeyCode == keyCode);
|
||||
numKeyRepeatCounter = (lastNumKeyCode == keyCode) ? numKeyRepeatCounter + 1 : 0;
|
||||
lastNumKeyCode = keyCode;
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_0) {
|
||||
return onNumber(0, false, isNumKeyRepeated);
|
||||
return onNumber(keyCodeToKeyNumber(keyCode), false, numKeyRepeatCounter);
|
||||
}
|
||||
|
||||
// dialer fields are similar to pure numeric fields, but for user convenience, holding "0"
|
||||
|
|
@ -281,7 +281,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
case KeyEvent.KEYCODE_DPAD_UP: return onUp();
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN: return onDown();
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT: return onLeft();
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT: return onRight(isKeyRepeated);
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT: return onRight(keyRepeatCounter > 0);
|
||||
case KeyEvent.KEYCODE_1:
|
||||
case KeyEvent.KEYCODE_2:
|
||||
case KeyEvent.KEYCODE_3:
|
||||
|
|
@ -291,7 +291,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
case KeyEvent.KEYCODE_7:
|
||||
case KeyEvent.KEYCODE_8:
|
||||
case KeyEvent.KEYCODE_9:
|
||||
return onNumber(keyCodeToKeyNumber(keyCode), false, isNumKeyRepeated);
|
||||
return onNumber(keyCodeToKeyNumber(keyCode), false, numKeyRepeatCounter);
|
||||
case KeyEvent.KEYCODE_STAR: return onStar();
|
||||
case KeyEvent.KEYCODE_POUND: return onPound();
|
||||
}
|
||||
|
|
@ -353,8 +353,10 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
|
||||
|
||||
protected void resetKeyRepeat() {
|
||||
isNumKeyRepeated = false;
|
||||
numKeyRepeatCounter = 0;
|
||||
keyRepeatCounter = 0;
|
||||
lastNumKeyCode = 0;
|
||||
lastKeyCode = 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -397,7 +399,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
abstract protected boolean onDown();
|
||||
abstract protected boolean onLeft();
|
||||
abstract protected boolean onRight(boolean repeat);
|
||||
abstract protected boolean onNumber(int key, boolean hold, boolean repeat);
|
||||
abstract protected boolean onNumber(int key, boolean hold, int repeat);
|
||||
abstract protected boolean onStar();
|
||||
abstract protected boolean onPound();
|
||||
|
||||
|
|
@ -412,27 +414,4 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
abstract protected void onRestart(EditorInfo inputField);
|
||||
abstract protected void onFinish();
|
||||
abstract protected View createSoftKeyView();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////// THE ONES BELOW MAY BE UNNECESSARY. IMPLEMENT IF NEEDED. /////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/**
|
||||
* Deal with the editor reporting movement of its cursor.
|
||||
*/
|
||||
/* @Override
|
||||
public void onUpdateSelection(
|
||||
int oldSelStart,
|
||||
int oldSelEnd,
|
||||
int newSelStart,
|
||||
int newSelEnd,
|
||||
int candidatesStart,
|
||||
int candidatesEnd
|
||||
) {
|
||||
// @todo: implement if necessary, but probably in TraditionalT9, not here
|
||||
// ... handle any interesting cursor movement
|
||||
// commitCurrentSuggestion()
|
||||
// setSuggestions(null)
|
||||
}*/
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ import io.github.sspanak.tt9.ui.SuggestionsView;
|
|||
import io.github.sspanak.tt9.ui.UI;
|
||||
|
||||
public class TraditionalT9 extends KeyPadHandler {
|
||||
private static TraditionalT9 self;
|
||||
// internal settings/data
|
||||
private EditorInfo inputField;
|
||||
|
||||
// input mode
|
||||
private ArrayList<Integer> allowedInputModes = new ArrayList<>();
|
||||
|
|
@ -36,6 +37,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
private SuggestionsView mSuggestionView = null;
|
||||
|
||||
|
||||
private static TraditionalT9 self;
|
||||
public static Context getMainContext() {
|
||||
return self.getApplicationContext();
|
||||
}
|
||||
|
|
@ -82,12 +84,14 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
|
||||
|
||||
protected void onRestart(EditorInfo inputField) {
|
||||
this.inputField = inputField;
|
||||
|
||||
// in case we are back from Settings screen, update the language list
|
||||
mEnabledLanguages = settings.getEnabledLanguageIds();
|
||||
validateLanguages();
|
||||
|
||||
// some input fields support only numbers or do not accept predictions
|
||||
determineAllowedInputModes(inputField);
|
||||
determineAllowedInputModes();
|
||||
mInputMode = InputModeValidator.validateMode(settings, mInputMode, allowedInputModes);
|
||||
|
||||
// Some modes may want to change the default text case based on grammar rules.
|
||||
|
|
@ -151,8 +155,11 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
return sendDefaultEditorAction(false);
|
||||
}
|
||||
|
||||
mInputMode.onAcceptSuggestion(mLanguage, mSuggestionView.getCurrentSuggestion());
|
||||
String word = mSuggestionView.getCurrentSuggestion();
|
||||
|
||||
mInputMode.onAcceptSuggestion(mLanguage, word);
|
||||
commitCurrentSuggestion();
|
||||
autoCorrectSpace(word, true, -1, false);
|
||||
resetKeyRepeat();
|
||||
|
||||
return true;
|
||||
|
|
@ -213,15 +220,21 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
* @param repeat If "true" we are calling the handler, because the key was pressed more than once
|
||||
* @return boolean
|
||||
*/
|
||||
protected boolean onNumber(int key, boolean hold, boolean repeat) {
|
||||
if (mInputMode.shouldAcceptCurrentSuggestion(mLanguage, key, hold, repeat)) {
|
||||
mInputMode.onAcceptSuggestion(mLanguage, getComposingText());
|
||||
protected boolean onNumber(int key, boolean hold, int repeat) {
|
||||
String currentWord = getComposingText();
|
||||
|
||||
// Automatically accept the current word, when the next one is a space or whatnot,
|
||||
// instead of requiring "OK" before that.
|
||||
if (mInputMode.shouldAcceptCurrentSuggestion(mLanguage, key, hold, repeat > 0)) {
|
||||
mInputMode.onAcceptSuggestion(mLanguage, currentWord);
|
||||
commitCurrentSuggestion(false);
|
||||
autoCorrectSpace(currentWord, false, key, hold);
|
||||
currentWord = "";
|
||||
}
|
||||
|
||||
// Auto-adjust the text case before each word, if the InputMode supports it.
|
||||
// We don't do it too often, because it is somewhat resource-intensive.
|
||||
if (getComposingText().length() == 0) {
|
||||
if (currentWord.length() == 0) {
|
||||
determineNextTextCase();
|
||||
}
|
||||
|
||||
|
|
@ -507,7 +520,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
}
|
||||
|
||||
|
||||
private void determineAllowedInputModes(EditorInfo inputField) {
|
||||
private void determineAllowedInputModes() {
|
||||
allowedInputModes = InputFieldHelper.determineInputModes(inputField);
|
||||
|
||||
int lastInputModeId = settings.getInputMode();
|
||||
|
|
@ -524,13 +537,25 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
} else if (mInputMode.is123() && allowedInputModes.size() == 1) {
|
||||
mEditing = EDITING_STRICT_NUMERIC;
|
||||
} else {
|
||||
mEditing = InputFieldHelper.isFilterTextField(inputField) ? EDITING_NOSHOW : EDITING;
|
||||
mEditing = InputFieldHelper.isFilterField(inputField) ? EDITING_NOSHOW : EDITING;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void autoCorrectSpace(String currentWord, boolean isWordAcceptedManually, int incomingKey, boolean hold) {
|
||||
if (mInputMode.shouldDeletePrecedingSpace(inputField)) {
|
||||
InputFieldHelper.deletePrecedingSpace(currentInputConnection, currentWord);
|
||||
}
|
||||
|
||||
if (mInputMode.shouldAddAutoSpace(inputField, isWordAcceptedManually, incomingKey, hold)) {
|
||||
commitText(" ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void determineNextTextCase() {
|
||||
mInputMode.determineNextWordTextCase(
|
||||
settings,
|
||||
InputFieldHelper.isThereText(currentInputConnection),
|
||||
(String) currentInputConnection.getTextBeforeCursor(50, 0)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package io.github.sspanak.tt9.ime.modes;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
|
@ -42,12 +43,12 @@ abstract public class InputMode {
|
|||
|
||||
// Key handlers. Return "true" when handling the key or "false", when is nothing to do.
|
||||
public boolean onBackspace() { return false; }
|
||||
abstract public boolean onNumber(Language language, int key, boolean hold, boolean repeat);
|
||||
abstract public boolean onNumber(Language language, int key, boolean hold, int repeat);
|
||||
|
||||
// Suggestions
|
||||
public void onAcceptSuggestion(Language language, String suggestion) {}
|
||||
protected void onSuggestionsUpdated(Handler handler) { handler.sendEmptyMessage(0); }
|
||||
public boolean loadSuggestions(Handler handler, Language language, String lastWord) { return false; }
|
||||
public boolean loadSuggestions(Handler handler, Language language, String currentWord) { return false; }
|
||||
|
||||
public ArrayList<String> getSuggestions(Language language) {
|
||||
ArrayList<String> newSuggestions = new ArrayList<>();
|
||||
|
|
@ -69,6 +70,10 @@ abstract public class InputMode {
|
|||
// Utility
|
||||
abstract public int getId();
|
||||
abstract public int getSequenceLength(); // The number of key presses for the current word.
|
||||
|
||||
public boolean shouldAddAutoSpace(EditorInfo inputField, boolean isWordAcceptedManually, int incomingKey, boolean hold) { return false; }
|
||||
public boolean shouldDeletePrecedingSpace(EditorInfo inputField) { return false; }
|
||||
|
||||
public void reset() {
|
||||
suggestions = new ArrayList<>();
|
||||
word = null;
|
||||
|
|
@ -95,7 +100,7 @@ abstract public class InputMode {
|
|||
textCase = allowedTextCases.get(nextIndex);
|
||||
}
|
||||
|
||||
public void determineNextWordTextCase(boolean isThereText, String textBeforeCursor) {}
|
||||
public void determineNextWordTextCase(SettingsStore settings, boolean isThereText, String textBeforeCursor) {}
|
||||
|
||||
// Based on the internal logic of the mode (punctuation or grammar rules), re-adjust the text case for when getSuggestions() is called.
|
||||
protected String adjustSuggestionTextCase(String word, int newTextCase, Language language) { return word; }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public class Mode123 extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
public boolean onNumber(Language l, int key, boolean hold, boolean repeat) {
|
||||
public boolean onNumber(Language l, int key, boolean hold, int repeat) {
|
||||
if (key != 0) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public class ModeABC extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
public boolean onNumber(Language language, int key, boolean hold, boolean repeat) {
|
||||
public boolean onNumber(Language language, int key, boolean hold, int repeat) {
|
||||
shouldSelectNextLetter = false;
|
||||
suggestions = language.getKeyCharacters(key);
|
||||
word = null;
|
||||
|
|
@ -23,7 +23,7 @@ public class ModeABC extends InputMode {
|
|||
if (hold) {
|
||||
suggestions = new ArrayList<>();
|
||||
word = String.valueOf(key);
|
||||
} else if (repeat) {
|
||||
} else if (repeat > 0) {
|
||||
shouldSelectNextLetter = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ package io.github.sspanak.tt9.ime.modes;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.github.sspanak.tt9.Logger;
|
||||
import io.github.sspanak.tt9.db.DictionaryDb;
|
||||
import io.github.sspanak.tt9.ime.InputFieldHelper;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.Punctuation;
|
||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||
|
|
@ -18,8 +20,9 @@ public class ModePredictive extends InputMode {
|
|||
|
||||
public int getId() { return MODE_PREDICTIVE; }
|
||||
|
||||
private boolean isEmoji = false;
|
||||
private String digitSequence = "";
|
||||
private String lastAcceptedWord = "";
|
||||
private String lastAcceptedSequence = "";
|
||||
|
||||
// stem filter
|
||||
private boolean isStemFuzzy = false;
|
||||
|
|
@ -27,11 +30,15 @@ public class ModePredictive extends InputMode {
|
|||
|
||||
// async suggestion handling
|
||||
private Language currentLanguage = null;
|
||||
private String lastInputFieldWord = "";
|
||||
private String currentInputFieldWord = "";
|
||||
private static Handler handleSuggestionsExternal;
|
||||
|
||||
// auto text case selection
|
||||
private final Pattern endOfSentence = Pattern.compile("(?<!\\.)[.?!]\\s*$");
|
||||
private final Pattern endOfSentenceRegex = Pattern.compile("(?<!\\.)[.?!]\\s*$");
|
||||
|
||||
// punctuation/emoji
|
||||
private final Pattern containsOnly1Regex = Pattern.compile("^1+$");
|
||||
private final String maxEmojiSequence;
|
||||
|
||||
|
||||
ModePredictive(SettingsStore settings) {
|
||||
|
|
@ -39,9 +46,18 @@ public class ModePredictive extends InputMode {
|
|||
allowedTextCases.add(CASE_LOWER);
|
||||
allowedTextCases.add(CASE_CAPITALIZE);
|
||||
allowedTextCases.add(CASE_UPPER);
|
||||
|
||||
// digitSequence limiter when selecting emoji
|
||||
// "11" = Emoji level 0, "111" = Emoji level 1,... up to the maximum amount of 1s
|
||||
StringBuilder maxEmojiSequenceBuilder = new StringBuilder();
|
||||
for (int i = 0; i <= Punctuation.getEmojiLevels(); i++) {
|
||||
maxEmojiSequenceBuilder.append("1");
|
||||
}
|
||||
maxEmojiSequence = maxEmojiSequenceBuilder.toString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onBackspace() {
|
||||
if (digitSequence.length() < 1) {
|
||||
clearWordStem();
|
||||
|
|
@ -59,24 +75,17 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
public boolean onNumber(Language l, int key, boolean hold, boolean repeat) {
|
||||
isEmoji = false;
|
||||
|
||||
@Override
|
||||
public boolean onNumber(Language l, int key, boolean hold, int repeat) {
|
||||
if (hold) {
|
||||
// hold to type any digit
|
||||
reset();
|
||||
word = String.valueOf(key);
|
||||
} else if (key == 0) {
|
||||
// "0" is " "
|
||||
} else if (key == 0 && repeat > 0) {
|
||||
// repeat "0" to type spaces
|
||||
reset();
|
||||
word = " ";
|
||||
} else if (key == 1 && repeat) {
|
||||
// emoticons
|
||||
reset();
|
||||
isEmoji = true;
|
||||
suggestions = Punctuation.Emoji;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// words
|
||||
super.reset();
|
||||
digitSequence += key;
|
||||
|
|
@ -86,6 +95,7 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
digitSequence = "";
|
||||
|
|
@ -93,11 +103,98 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* shouldAddAutoSpace
|
||||
* When the "auto-space" settings is enabled, this determines whether to automatically add a space
|
||||
* at the end of a sentence or after accepting a suggestion. This allows faster typing, without
|
||||
* pressing space.
|
||||
*
|
||||
* See the helper functions for the list of rules.
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldAddAutoSpace(EditorInfo inputField, boolean isWordAcceptedManually, int incomingKey, boolean hold) {
|
||||
return
|
||||
settings.getAutoSpace()
|
||||
&& !hold
|
||||
&& (
|
||||
shouldAddAutoSpaceAfterPunctuation(inputField, incomingKey)
|
||||
|| shouldAddAutoSpaceAfterWord(inputField, isWordAcceptedManually)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shouldDeletePrecedingSpace
|
||||
* When the "auto-space" settings is enabled, determine whether to delete spaces before punctuation.
|
||||
* This allows automatic conversion from: "words ." to: "words."
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldDeletePrecedingSpace(EditorInfo inputField) {
|
||||
return
|
||||
settings.getAutoSpace()
|
||||
&& (
|
||||
lastAcceptedWord.equals(".")
|
||||
|| lastAcceptedWord.equals(",")
|
||||
|| lastAcceptedWord.equals(";")
|
||||
|| lastAcceptedWord.equals(":")
|
||||
|| lastAcceptedWord.equals("!")
|
||||
|| lastAcceptedWord.equals("?")
|
||||
|| lastAcceptedWord.equals(")")
|
||||
|| lastAcceptedWord.equals("]")
|
||||
|| lastAcceptedWord.equals("'")
|
||||
|| lastAcceptedWord.equals("@")
|
||||
)
|
||||
&& InputFieldHelper.isRegularTextField(inputField);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shouldAddAutoSpaceAfterPunctuation
|
||||
* Determines whether to automatically adding a space after certain punctuation signs makes sense.
|
||||
* The rules are similar to the ones in the standard Android keyboard (with some exceptions,
|
||||
* because we are not using a QWERTY keyboard here).
|
||||
*/
|
||||
private boolean shouldAddAutoSpaceAfterPunctuation(EditorInfo inputField, int incomingKey) {
|
||||
return
|
||||
incomingKey != 0
|
||||
&& (
|
||||
lastAcceptedWord.endsWith(".")
|
||||
|| lastAcceptedWord.endsWith(",")
|
||||
|| lastAcceptedWord.endsWith(";")
|
||||
|| lastAcceptedWord.endsWith(":")
|
||||
|| lastAcceptedWord.endsWith("!")
|
||||
|| lastAcceptedWord.endsWith("?")
|
||||
|| lastAcceptedWord.endsWith(")")
|
||||
|| lastAcceptedWord.endsWith("]")
|
||||
|| lastAcceptedWord.endsWith("%")
|
||||
)
|
||||
&& InputFieldHelper.isRegularTextField(inputField);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shouldAddAutoSpaceAfterPunctuation
|
||||
* Similar to "shouldAddAutoSpaceAfterPunctuation()", but determines whether to add a space after
|
||||
* words.
|
||||
*/
|
||||
private boolean shouldAddAutoSpaceAfterWord(EditorInfo inputField, boolean isWordAcceptedManually) {
|
||||
return
|
||||
// Do not add space when auto-accepting words, because it feels very confusing when typing.
|
||||
isWordAcceptedManually
|
||||
// Secondary punctuation
|
||||
&& !lastAcceptedSequence.equals("0")
|
||||
// Emoji
|
||||
&& !lastAcceptedSequence.startsWith("1")
|
||||
&& InputFieldHelper.isRegularTextField(inputField);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shouldAcceptCurrentSuggestion
|
||||
* In this mode, In addition to confirming the suggestion in the input field,
|
||||
* we also increase its' priority. This function determines whether we want to do all this or not.
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldAcceptCurrentSuggestion(Language language, int key, boolean hold, boolean repeat) {
|
||||
return
|
||||
hold
|
||||
|
|
@ -107,7 +204,8 @@ public class ModePredictive extends InputMode {
|
|||
// Also, it must break the current word.
|
||||
|| (!language.isPunctuationPartOfWords() && key == 1 && digitSequence.length() > 0 && !digitSequence.endsWith("1"))
|
||||
// On the other hand, letters also "break" punctuation.
|
||||
|| (!language.isPunctuationPartOfWords() && key != 1 && digitSequence.endsWith("1"));
|
||||
|| (!language.isPunctuationPartOfWords() && key != 1 && digitSequence.endsWith("1"))
|
||||
|| (digitSequence.endsWith("0"));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -115,6 +213,7 @@ public class ModePredictive extends InputMode {
|
|||
* clearWordStem
|
||||
* Do not filter the suggestions by the word set using "setWordStem()", use only the digit sequence.
|
||||
*/
|
||||
@Override
|
||||
public boolean clearWordStem() {
|
||||
stem = "";
|
||||
Logger.d("tt9/setWordStem", "Stem filter cleared");
|
||||
|
|
@ -138,6 +237,7 @@ public class ModePredictive extends InputMode {
|
|||
*
|
||||
* Note that you need to manually get the suggestions again to obtain a filtered list.
|
||||
*/
|
||||
@Override
|
||||
public boolean setWordStem(Language language, String wordStem, boolean exact) {
|
||||
if (language == null || wordStem == null || wordStem.length() < 1) {
|
||||
return false;
|
||||
|
|
@ -146,9 +246,9 @@ public class ModePredictive extends InputMode {
|
|||
try {
|
||||
digitSequence = language.getDigitSequenceForWord(wordStem);
|
||||
isStemFuzzy = !exact;
|
||||
stem = wordStem.toLowerCase(language.getLocale());
|
||||
stem = digitSequence.startsWith("0") || digitSequence.startsWith("1") ? "" : wordStem.toLowerCase(language.getLocale());
|
||||
|
||||
Logger.d("tt9/setWordStem", "Stem is now: " + wordStem);
|
||||
Logger.d("tt9/setWordStem", "Stem is now: " + stem + (isStemFuzzy ? " (fuzzy)" : ""));
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
isStemFuzzy = false;
|
||||
|
|
@ -163,11 +263,37 @@ public class ModePredictive extends InputMode {
|
|||
* getWordStem
|
||||
* If "setWordStem()" has accepted a new stem by returning "true", it can be obtained using this.
|
||||
*/
|
||||
@Override
|
||||
public String getWordStem() {
|
||||
return stem;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* loadStaticSuggestions
|
||||
* Similar to "loadSuggestions()", but loads suggestions that are not in the database.
|
||||
* Returns "false", when there are no static suggestions for the current digitSequence.
|
||||
*/
|
||||
private boolean loadStaticSuggestions() {
|
||||
if (digitSequence.equals("0")) {
|
||||
stem = "";
|
||||
suggestions = Punctuation.Secondary;
|
||||
} else if (containsOnly1Regex.matcher(digitSequence).matches()) {
|
||||
stem = "";
|
||||
if (digitSequence.length() == 1) {
|
||||
suggestions = Punctuation.Main;
|
||||
} else {
|
||||
digitSequence = digitSequence.length() <= maxEmojiSequence.length() ? digitSequence : maxEmojiSequence;
|
||||
suggestions = Punctuation.getEmoji(digitSequence.length() - 2);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* loadSuggestions
|
||||
* Queries the dictionary database for a list of suggestions matching the current language and
|
||||
|
|
@ -176,19 +302,20 @@ public class ModePredictive extends InputMode {
|
|||
* "lastWord" is used for generating suggestions when there are no results.
|
||||
* See: generatePossibleCompletions()
|
||||
*/
|
||||
public boolean loadSuggestions(Handler handler, Language language, String lastWord) {
|
||||
if (isEmoji) {
|
||||
@Override
|
||||
public boolean loadSuggestions(Handler handler, Language language, String currentWord) {
|
||||
if (loadStaticSuggestions()) {
|
||||
super.onSuggestionsUpdated(handler);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (digitSequence.length() == 0) {
|
||||
suggestions.clear();
|
||||
suggestions = new ArrayList<>();
|
||||
return false;
|
||||
}
|
||||
|
||||
handleSuggestionsExternal = handler;
|
||||
lastInputFieldWord = lastWord.toLowerCase(language.getLocale());
|
||||
currentInputFieldWord = currentWord.toLowerCase(language.getLocale());
|
||||
currentLanguage = language;
|
||||
super.reset();
|
||||
|
||||
|
|
@ -217,7 +344,7 @@ public class ModePredictive extends InputMode {
|
|||
dbSuggestions = dbSuggestions == null ? new ArrayList<>() : dbSuggestions;
|
||||
|
||||
if (dbSuggestions.size() == 0 && digitSequence.length() > 0) {
|
||||
dbSuggestions = generatePossibleCompletions(currentLanguage, lastInputFieldWord);
|
||||
dbSuggestions = generatePossibleCompletions(currentLanguage, currentInputFieldWord);
|
||||
}
|
||||
|
||||
suggestions.clear();
|
||||
|
|
@ -310,7 +437,7 @@ public class ModePredictive extends InputMode {
|
|||
* Add the current stem filter to the suggestion list, when it has length of X and
|
||||
* the user has pressed X keys.
|
||||
*/
|
||||
public void suggestStem() {
|
||||
private void suggestStem() {
|
||||
if (stem.length() > 0 && stem.length() == digitSequence.length()) {
|
||||
suggestions.add(stem);
|
||||
}
|
||||
|
|
@ -321,7 +448,7 @@ public class ModePredictive extends InputMode {
|
|||
* suggestMoreWords
|
||||
* Takes a list of words and appends them to the suggestion list, if they are missing.
|
||||
*/
|
||||
public void suggestMoreWords(ArrayList<String> newSuggestions) {
|
||||
private void suggestMoreWords(ArrayList<String> newSuggestions) {
|
||||
for (String word : newSuggestions) {
|
||||
if (!suggestions.contains(word)) {
|
||||
suggestions.add(word);
|
||||
|
|
@ -334,7 +461,10 @@ public class ModePredictive extends InputMode {
|
|||
* onAcceptSuggestion
|
||||
* Bring this word up in the suggestions list next time.
|
||||
*/
|
||||
@Override
|
||||
public void onAcceptSuggestion(Language language, String currentWord) {
|
||||
lastAcceptedWord = currentWord;
|
||||
lastAcceptedSequence = digitSequence;
|
||||
reset();
|
||||
|
||||
if (currentWord.length() == 0) {
|
||||
|
|
@ -344,7 +474,12 @@ public class ModePredictive extends InputMode {
|
|||
|
||||
try {
|
||||
String sequence = language.getDigitSequenceForWord(currentWord);
|
||||
DictionaryDb.incrementWordFrequency(language, currentWord, sequence);
|
||||
|
||||
// emoji and punctuation are not in the database, so there is no point in
|
||||
// running queries that would update nothing
|
||||
if (!sequence.startsWith("11") && !sequence.equals("1") && !sequence.equals("0")) {
|
||||
DictionaryDb.incrementWordFrequency(language, currentWord, sequence);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.e("tt9/ModePredictive", "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage());
|
||||
}
|
||||
|
|
@ -360,6 +495,7 @@ public class ModePredictive extends InputMode {
|
|||
* for example: "dB", "Mb", proper names, German nouns, that always start with a capital,
|
||||
* or Dutch words such as: "'s-Hertogenbosch".
|
||||
*/
|
||||
@Override
|
||||
protected String adjustSuggestionTextCase(String word, int newTextCase, Language language) {
|
||||
switch (newTextCase) {
|
||||
case CASE_UPPER:
|
||||
|
|
@ -382,7 +518,12 @@ public class ModePredictive extends InputMode {
|
|||
* For example, this function will return CASE_LOWER by default, but CASE_UPPER at the beginning
|
||||
* of a sentence.
|
||||
*/
|
||||
public void determineNextWordTextCase(boolean isThereText, String textBeforeCursor) {
|
||||
@Override
|
||||
public void determineNextWordTextCase(SettingsStore settings, boolean isThereText, String textBeforeCursor) {
|
||||
if (!settings.getAutoTextCase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user wants to type in uppercase, this must be for a reason, so we better not override it.
|
||||
if (textCase == CASE_UPPER) {
|
||||
return;
|
||||
|
|
@ -395,7 +536,7 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
// start of sentence, excluding after "..."
|
||||
if (endOfSentence.matcher(textBeforeCursor).find()) {
|
||||
if (endOfSentenceRegex.matcher(textBeforeCursor).find()) {
|
||||
textCase = CASE_CAPITALIZE;
|
||||
return;
|
||||
}
|
||||
|
|
@ -404,9 +545,8 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
final public boolean isPredictive() { return true; }
|
||||
public int getSequenceLength() { return isEmoji ? 2 : digitSequence.length(); }
|
||||
public boolean shouldTrackUpDown() { return true; }
|
||||
public boolean shouldTrackLeftRight() { return true; }
|
||||
|
||||
@Override final public boolean isPredictive() { return true; }
|
||||
@Override public int getSequenceLength() { return digitSequence.length(); }
|
||||
@Override public boolean shouldTrackUpDown() { return true; }
|
||||
@Override public boolean shouldTrackLeftRight() { return true; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package io.github.sspanak.tt9.languages;
|
||||
|
||||
public class InvalidLanguageCharactersException extends Exception {
|
||||
private Language language;
|
||||
private final Language language;
|
||||
|
||||
public InvalidLanguageCharactersException(Language language, String extraMessage) {
|
||||
super("Some characters are not supported in language: " + language.getName() + ". " + extraMessage);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,57 @@
|
|||
package io.github.sspanak.tt9.languages;
|
||||
|
||||
import android.graphics.Paint;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Punctuation {
|
||||
final public static ArrayList<String> Main = new ArrayList<>(Arrays.asList(
|
||||
".", ",", "-", "?", "!", ")", "(", "'", "\"", "@", ":", "/", ";", "%"
|
||||
".", ",", "-", "(", ")", "[", "]", "&", "~", "`", "\"", ":", ";", "'", "!", "?"
|
||||
));
|
||||
|
||||
final public static ArrayList<String> Secondary = new ArrayList<>(Arrays.asList(
|
||||
" ", "+", "\n"
|
||||
" ", "\n", "@", "%", "#", "$", "{", "}", "^", "<", ">", "\\", "/", "=", "*", "+"
|
||||
));
|
||||
|
||||
final public static ArrayList<String> Emoji = new ArrayList<>(Arrays.asList(
|
||||
"👍", "🙂", "😀", "😉", "🙁", "😢", "😛", "😬"
|
||||
final private static ArrayList<String> TextEmoticons = new ArrayList<>(Arrays.asList(
|
||||
":)", ":D", ":P", ";)", "\\m/", ":-O", ":|", ":("
|
||||
));
|
||||
|
||||
final private static ArrayList<ArrayList<String>> Emoji = new ArrayList<>(Arrays.asList(
|
||||
new ArrayList<>(Arrays.asList(
|
||||
"🙂", "😀", "🤣", "😉", "😛", "😳", "😲", "😱", "😭", "😢", "🙁"
|
||||
)),
|
||||
new ArrayList<>(Arrays.asList(
|
||||
"👍", "👋", "✌️", "👏", "🤝", "💪", "🤘", "🖖", "👎"
|
||||
)),
|
||||
new ArrayList<>(Arrays.asList(
|
||||
"❤", "🤗", "😍", "😘", "😈", "🎉", "🤓", "😎", "🤔", "🥶", "😬"
|
||||
))
|
||||
));
|
||||
|
||||
|
||||
public static int getEmojiLevels() {
|
||||
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 1 : Emoji.size();
|
||||
}
|
||||
|
||||
|
||||
public static ArrayList<String> getEmoji(int level) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return TextEmoticons;
|
||||
}
|
||||
|
||||
level = (Emoji.size() > level) ? level : Emoji.size() - 1;
|
||||
|
||||
Paint paint = new Paint();
|
||||
ArrayList<String> availableEmoji = new ArrayList<>();
|
||||
for (String emoji : Emoji.get(level)) {
|
||||
if (paint.hasGlyph(emoji)) {
|
||||
availableEmoji.add(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
return availableEmoji.size() > 0 ? availableEmoji : TextEmoticons;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,10 +197,18 @@ public class SettingsStore {
|
|||
|
||||
public boolean getShowSoftKeys() { return prefs.getBoolean("pref_show_soft_keys", true); }
|
||||
|
||||
|
||||
|
||||
/************* typing settings *************/
|
||||
|
||||
public boolean getAutoSpace() { return prefs.getBoolean("auto_space", false); }
|
||||
public boolean getAutoTextCase() { return prefs.getBoolean("auto_text_case", true); }
|
||||
|
||||
/************* internal settings *************/
|
||||
|
||||
public int getDictionaryImportProgressUpdateInterval() { return 250; /* ms */ }
|
||||
public int getDictionaryImportWordChunkSize() { return 1000; /* words */ }
|
||||
|
||||
public int getSuggestionsMax() { return 20; }
|
||||
public int getSuggestionsMin() { return 8; }
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,11 @@ public class SuggestionsView {
|
|||
|
||||
|
||||
public String getSuggestion(int id) {
|
||||
return id >= 0 && id < suggestions.size() ? suggestions.get(id) : "";
|
||||
if (id < 0 || id >= suggestions.size()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return suggestions.get(id).equals("⏎") ? "\n" : suggestions.get(id);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -108,7 +112,10 @@ public class SuggestionsView {
|
|||
selectedIndex = 0;
|
||||
|
||||
if (newSuggestions != null) {
|
||||
suggestions.addAll(newSuggestions);
|
||||
for (String suggestion : newSuggestions) {
|
||||
// make the new line better readable
|
||||
suggestions.add(suggestion.equals("\n") ? "⏎" : suggestion);
|
||||
}
|
||||
selectedIndex = Math.max(initialSel, 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue