diff --git a/docs/user-manual.md b/docs/user-manual.md
index 45c7bc69..02731639 100644
--- a/docs/user-manual.md
+++ b/docs/user-manual.md
@@ -47,12 +47,19 @@ _Predictive mode only._
- Clear the suggestion filter, if applied.
- When no filter is applied, accept the current word as-is, even if it does not fully match a suggestion, then jump before it.
-#### 0-key
-- **In 123 mode:** type "0" or hold it to type "+".
-- **In ABC mode:** type secondary punctuation or hold to type "0".
-- **In Predictive mode:** type space or hold to type "0".
+#### 0-key:
+- **In 123 mode:**
+ - **Press:**: type "0".
+ - **Hold:** type "+".
+- **In ABC mode:**
+ - **Press:** type space, newline or special/math characters.
+ - **Hold:** type "0".
+- **In Predictive mode:**
+ - **Press:** type space, newline or special/math characters.
+ - **Multiple Press:** type multiple spaces.
+ - **Hold:** type "0".
-#### 1- to 9-key
+#### 1- to 9-key:
- **In 123 mode:** type the respective number.
- **In ABC and Predictive mode:** type a letter/punctuation character or hold to type the respective number.
@@ -67,7 +74,7 @@ Just deletes text.
- **Short Press when there is text:** Some applications, most notably Firefox and Spotify, take full control of the "Back" key. This means, it may function as the application authors intended, instead of as backspace. In such cases, you could use the on-screen backspace instead. Unfortunately, nothing else could be done, because this is a restriction posed by Android.
- **Long Press**: Whatever the system default action is (i.e. show running applications list).
-All this does not apply, when using other keys. They will just delete text
+All this does not apply, when using other keys. They will just delete text.
#### Next Input Mode Key (Default: Press #):
- **Press when there are no suggestions:** Cycle the input modes (abc → ABC → Predictive → 123). Note that only 123 mode is available in numeric fields and Predictive mode is not available in password fields.
@@ -94,7 +101,7 @@ On the Settings screen, you can choose languages for typing, configure the keypa
To access it:
- Start typing in a text field to wake up TT9.
-- Use the on-screen gear button or hold the Settings Key.
+- Use the on-screen gear button or press the Settings Key.
## License
- The source code, the logo image and the icons are licensed under [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 879da011..81a9afee 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -15,7 +15,7 @@
За приложението
Помощ
Тъмен облик
- Избор на езици
+ Езици
Изтрий речник
Речници
@@ -41,7 +41,11 @@
Възстанови стандартните бутони
Възстановени са стандартните \"бързи\" бутони.
(задръж)
- Зареждане на речник
+ Зареждане на речник
Зареждането е отменено.
-
+ Автоматичен интервал
+ Добавяй автоматично интервал след препинателни знаци и думи.
+ Автоматични главни букви
+ Започвай автоматично изреченията с главни букви.
+ Подсказващ режим
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 1acf336b..abd9ceb3 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -13,12 +13,12 @@
Über die Anwendung
Hilfe
Dunkles Thema
- Sprachen auswählen
- Wörterbuch löschen
+ Sprachen
+ Wörterbuch löschen
Wörterbücher
Lade Wörterbuch (%1$s)…
Wörterbuch laden
Wird nicht geladen. Wörterbuch für \"%1$s\" nicht gefunden.
- Lade Wörterbuch
+ Lade Wörterbuch
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 82e698bd..83456206 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -15,7 +15,7 @@
À propos de l\'application
Aide
Thème sombre
- Choisir langues
+ Langues
Vider le dictionaire
Dictionnaires
@@ -31,6 +31,11 @@
Echec du chargement. Mot inadmissible \"%1$s\" à la ligne %2$d de langue \"%3$s\".
Le dictionaire est vidé avec succès.
Boutons à l\'écran
- Chargement du dictionnaire
+ Chargement du dictionnaire
Chargement est annulé.
+ Saisie intuitive
+ Espace automatique
+ Majuscules automatiques
+ Ajouter automatiquement un espace après signes de ponctuation et mots.
+ Commencer automatiquement les phrases avec une majuscule.
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index e26a8d2e..17c698da 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -14,8 +14,8 @@
Sull\'applicazione
Aiuto
Tema scuro
- Scegli le lingue
- Eliminare il dizionario
+ Lingue
+ Eliminare il dizionario
Dizionari
Annullare il caricamento
@@ -25,7 +25,7 @@
Carica il dizionario
Impossibile caricare. Dizionario per “%1$s” non trovato.
Scorciatoie da tastiera
- Caricamento del dizionario
+ Caricamento del dizionario
Caricamento annullato.
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index d1e6e4c2..1e0b8ccd 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -13,12 +13,12 @@
Over de applicatie
Helpen
Donker thema
- Talen kiezen
- Woordenboek wissen
+ Talen
+ Woordenboek wissen
Woordenboeken
Woordenboek laden (%1$s)…
Woordenboek laden
Laden mislukt. Woordenboek voor %1$s niet gevonden.
Woordenboek succesvol gewist.
- Woordenboek laden
+ Woordenboek laden
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 9b9bc9cd..1c4ec745 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -15,8 +15,8 @@
О приложении
Помощь
Темная тема
- Выбор языков
- Очистить словарь
+ Языки
+ Очистить словарь
Словари
Отменить загрузку
@@ -27,6 +27,7 @@
Загрузить словарь
Ошибка загрузки. Словарь «%1$s» не найден.
Словарь успешно очищен.
- Загрузка словаря
+ Загрузка словаря
Загрузка отменена.
+ Режим подсказки
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 3422b3c3..4aff2d52 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -14,8 +14,8 @@
Про додаток
Допомога
Темна тема
- Вибір мови
- Очистити словник
+ Мови
+ Очистити словник
Словники
Скасувати завантаження
@@ -25,6 +25,6 @@
Завантаження словника (%1$s)…
Завантажити словник
Помилка завантаження. Словник «%1$s» не знайдено.
- Завантаження словника
+ Завантаження словника
Завантаження скасовано.
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 94ea2fdc..76d48cb0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -17,9 +17,14 @@
About
Appearance
Dictionaries
+ Predictive Mode
Select Hotkeys
- Choose Languages
+ Automatic Space
+ Automatically add a space after punctuation or words.
+ Automatic Capitalization
+ Automatically start sentences with a capital letter.
+ Languages
Dark Theme
Show on-screen keys
Help
diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml
index 101f3737..bbfcdeb9 100644
--- a/res/xml/prefs.xml
+++ b/res/xml/prefs.xml
@@ -54,6 +54,27 @@
app:iconSpaceReserved="false"
app:key="dictionary_truncate"
app:title="@string/dictionary_truncate_title" />
+
+
+
+
+
+
+
+
+
@@ -87,7 +108,6 @@
app:key="key_show_settings"
app:title="@string/function_show_settings_key" />
-
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
diff --git a/src/io/github/sspanak/tt9/ime/InputFieldHelper.java b/src/io/github/sspanak/tt9/ime/InputFieldHelper.java
index 03d7c345..dedf11ed 100644
--- a/src/io/github/sspanak/tt9/ime/InputFieldHelper.java
+++ b/src/io/github/sspanak/tt9/ime/InputFieldHelper.java
@@ -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();
+ }
}
diff --git a/src/io/github/sspanak/tt9/ime/KeyPadHandler.java b/src/io/github/sspanak/tt9/ime/KeyPadHandler.java
index 95626d0f..9ffec7ed 100644
--- a/src/io/github/sspanak/tt9/ime/KeyPadHandler.java
+++ b/src/io/github/sspanak/tt9/ime/KeyPadHandler.java
@@ -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)
- }*/
-
}
diff --git a/src/io/github/sspanak/tt9/ime/TraditionalT9.java b/src/io/github/sspanak/tt9/ime/TraditionalT9.java
index 1cda78c2..d997aee8 100644
--- a/src/io/github/sspanak/tt9/ime/TraditionalT9.java
+++ b/src/io/github/sspanak/tt9/ime/TraditionalT9.java
@@ -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 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)
);
diff --git a/src/io/github/sspanak/tt9/ime/modes/InputMode.java b/src/io/github/sspanak/tt9/ime/modes/InputMode.java
index 39a96608..b7077710 100644
--- a/src/io/github/sspanak/tt9/ime/modes/InputMode.java
+++ b/src/io/github/sspanak/tt9/ime/modes/InputMode.java
@@ -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 getSuggestions(Language language) {
ArrayList 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; }
diff --git a/src/io/github/sspanak/tt9/ime/modes/Mode123.java b/src/io/github/sspanak/tt9/ime/modes/Mode123.java
index b308b72a..b83366c1 100644
--- a/src/io/github/sspanak/tt9/ime/modes/Mode123.java
+++ b/src/io/github/sspanak/tt9/ime/modes/Mode123.java
@@ -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;
}
diff --git a/src/io/github/sspanak/tt9/ime/modes/ModeABC.java b/src/io/github/sspanak/tt9/ime/modes/ModeABC.java
index 6a206c3f..f2e81404 100644
--- a/src/io/github/sspanak/tt9/ime/modes/ModeABC.java
+++ b/src/io/github/sspanak/tt9/ime/modes/ModeABC.java
@@ -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;
}
diff --git a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java
index d130d078..6146ba8a 100644
--- a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java
+++ b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java
@@ -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("(? 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 newSuggestions) {
+ private void suggestMoreWords(ArrayList 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; }
}
diff --git a/src/io/github/sspanak/tt9/languages/InvalidLanguageCharactersException.java b/src/io/github/sspanak/tt9/languages/InvalidLanguageCharactersException.java
index 4f5186d5..67ba09c0 100644
--- a/src/io/github/sspanak/tt9/languages/InvalidLanguageCharactersException.java
+++ b/src/io/github/sspanak/tt9/languages/InvalidLanguageCharactersException.java
@@ -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);
diff --git a/src/io/github/sspanak/tt9/languages/Punctuation.java b/src/io/github/sspanak/tt9/languages/Punctuation.java
index c37d26ab..4ea09126 100644
--- a/src/io/github/sspanak/tt9/languages/Punctuation.java
+++ b/src/io/github/sspanak/tt9/languages/Punctuation.java
@@ -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 Main = new ArrayList<>(Arrays.asList(
- ".", ",", "-", "?", "!", ")", "(", "'", "\"", "@", ":", "/", ";", "%"
+ ".", ",", "-", "(", ")", "[", "]", "&", "~", "`", "\"", ":", ";", "'", "!", "?"
));
final public static ArrayList Secondary = new ArrayList<>(Arrays.asList(
- " ", "+", "\n"
+ " ", "\n", "@", "%", "#", "$", "{", "}", "^", "<", ">", "\\", "/", "=", "*", "+"
));
- final public static ArrayList Emoji = new ArrayList<>(Arrays.asList(
- "👍", "🙂", "😀", "😉", "🙁", "😢", "😛", "😬"
+ final private static ArrayList TextEmoticons = new ArrayList<>(Arrays.asList(
+ ":)", ":D", ":P", ";)", "\\m/", ":-O", ":|", ":("
));
+
+ final private static ArrayList> 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 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 availableEmoji = new ArrayList<>();
+ for (String emoji : Emoji.get(level)) {
+ if (paint.hasGlyph(emoji)) {
+ availableEmoji.add(emoji);
+ }
+ }
+
+ return availableEmoji.size() > 0 ? availableEmoji : TextEmoticons;
+ }
}
diff --git a/src/io/github/sspanak/tt9/preferences/SettingsStore.java b/src/io/github/sspanak/tt9/preferences/SettingsStore.java
index 24a4c62d..ae869558 100644
--- a/src/io/github/sspanak/tt9/preferences/SettingsStore.java
+++ b/src/io/github/sspanak/tt9/preferences/SettingsStore.java
@@ -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; }
diff --git a/src/io/github/sspanak/tt9/ui/SuggestionsView.java b/src/io/github/sspanak/tt9/ui/SuggestionsView.java
index d2fcd144..c45aa7d9 100644
--- a/src/io/github/sspanak/tt9/ui/SuggestionsView.java
+++ b/src/io/github/sspanak/tt9/ui/SuggestionsView.java
@@ -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);
}