1
0
Fork 0

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:
Dimo Karaivanov 2022-11-22 17:05:54 +02:00
parent 6a2e1806d1
commit b637a0b9d6
22 changed files with 453 additions and 156 deletions

View file

@ -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

View file

@ -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();
}
}

View file

@ -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)
}*/
}

View file

@ -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)
);

View file

@ -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; }

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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; }
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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; }

View file

@ -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);
}