From c5c27cdcf1ce138fa04538fbce3a21d46ed8877b Mon Sep 17 00:00:00 2001 From: Dimo Karaivanov Date: Tue, 14 Feb 2023 10:38:28 +0200 Subject: [PATCH] TraditionalT9.class cleanup: moved the text operations into separate TextField and InputType helpers --- .../github/sspanak/tt9/ime/TraditionalT9.java | 93 ++---- .../tt9/ime/helpers/InputFieldHelper.java | 284 ----------------- .../sspanak/tt9/ime/helpers/InputType.java | 106 +++++++ .../sspanak/tt9/ime/helpers/TextField.java | 300 ++++++++++++++++++ .../sspanak/tt9/ime/helpers/TextHelper.java | 42 --- .../sspanak/tt9/ime/modes/InputMode.java | 8 +- .../sspanak/tt9/ime/modes/ModePredictive.java | 25 +- 7 files changed, 457 insertions(+), 401 deletions(-) delete mode 100644 src/io/github/sspanak/tt9/ime/helpers/InputFieldHelper.java create mode 100644 src/io/github/sspanak/tt9/ime/helpers/InputType.java create mode 100644 src/io/github/sspanak/tt9/ime/helpers/TextField.java delete mode 100644 src/io/github/sspanak/tt9/ime/helpers/TextHelper.java diff --git a/src/io/github/sspanak/tt9/ime/TraditionalT9.java b/src/io/github/sspanak/tt9/ime/TraditionalT9.java index 53bad88c..f5920cd3 100644 --- a/src/io/github/sspanak/tt9/ime/TraditionalT9.java +++ b/src/io/github/sspanak/tt9/ime/TraditionalT9.java @@ -14,9 +14,9 @@ import java.util.List; import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.db.DictionaryDb; -import io.github.sspanak.tt9.ime.helpers.InputFieldHelper; import io.github.sspanak.tt9.ime.helpers.InputModeValidator; -import io.github.sspanak.tt9.ime.helpers.TextHelper; +import io.github.sspanak.tt9.ime.helpers.InputType; +import io.github.sspanak.tt9.ime.helpers.TextField; import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; @@ -26,7 +26,8 @@ import io.github.sspanak.tt9.ui.UI; public class TraditionalT9 extends KeyPadHandler { // internal settings/data private boolean isActive = false; - private EditorInfo inputField; + private TextField textField; + private InputType inputType; // input mode private ArrayList allowedInputModes = new ArrayList<>(); @@ -97,7 +98,7 @@ public class TraditionalT9 extends KeyPadHandler { determineAllowedInputModes(); mInputMode = InputModeValidator.validateMode(settings, mInputMode, allowedInputModes); - mInputMode.setTextFieldCase(InputFieldHelper.determineTextCase(currentInputConnection, inputField)); + mInputMode.setTextFieldCase(textField.determineTextCase(inputType)); // Some modes may want to change the default text case based on grammar rules. determineNextTextCase(); InputModeValidator.validateTextCase(settings, mInputMode, settings.getTextCase()); @@ -124,8 +125,10 @@ public class TraditionalT9 extends KeyPadHandler { protected void onStart(EditorInfo input) { - this.inputField = input; - if (currentInputConnection == null || inputField == null || InputFieldHelper.isLimitedField(inputField)) { + inputType = new InputType(currentInputConnection, input); + textField = new TextField(currentInputConnection, input); + + if (!inputType.isValid() || inputType.isLimited()) { // When the input is invalid or simple, let Android handle it. onStop(); return; @@ -167,7 +170,7 @@ public class TraditionalT9 extends KeyPadHandler { // 1. Dialer fields seem to handle backspace on their own and we must ignore it, // otherwise, keyDown race condition occur for all keys. // 2. Allow the assigned key to function normally, when there is no text (e.g. "Back" navigates back) - if (mEditing == EDITING_DIALER || !InputFieldHelper.isThereText(currentInputConnection)) { + if (mEditing == EDITING_DIALER || !textField.isThereText()) { Logger.d("onBackspace", "backspace ignored"); mInputMode.reset(); return false; @@ -206,7 +209,7 @@ public class TraditionalT9 extends KeyPadHandler { protected boolean onUp() { if (previousSuggestion()) { mInputMode.setWordStem(mLanguage, mSuggestionView.getCurrentSuggestion(), true); - setComposingTextWithWordStemIndication(mSuggestionView.getCurrentSuggestion()); + textField.setComposingTextWithHighlightedStem(mSuggestionView.getCurrentSuggestion(), mInputMode); return true; } @@ -217,7 +220,7 @@ public class TraditionalT9 extends KeyPadHandler { protected boolean onDown() { if (nextSuggestion()) { mInputMode.setWordStem(mLanguage, mSuggestionView.getCurrentSuggestion(), true); - setComposingTextWithWordStemIndication(mSuggestionView.getCurrentSuggestion()); + textField.setComposingTextWithHighlightedStem(mSuggestionView.getCurrentSuggestion(), mInputMode); return true; } @@ -288,7 +291,7 @@ public class TraditionalT9 extends KeyPadHandler { currentWord = mInputMode.getWord(); mInputMode.onAcceptSuggestion(mLanguage, currentWord); - commitText(currentWord); + textField.setText(currentWord); clearSuggestions(); autoCorrectSpace(currentWord, true, key, hold, repeat > 0); resetKeyRepeat(); @@ -301,13 +304,13 @@ public class TraditionalT9 extends KeyPadHandler { protected boolean onPound() { - commitText("#"); + textField.setText("#"); return true; } protected boolean onStar() { - commitText("*"); + textField.setText("*"); return true; } @@ -377,7 +380,7 @@ public class TraditionalT9 extends KeyPadHandler { } mSuggestionView.scrollToSuggestion(-1); - setComposingTextWithWordStemIndication(mSuggestionView.getCurrentSuggestion()); + textField.setComposingTextWithHighlightedStem(mSuggestionView.getCurrentSuggestion(), mInputMode); return true; } @@ -389,7 +392,7 @@ public class TraditionalT9 extends KeyPadHandler { } mSuggestionView.scrollToSuggestion(1); - setComposingTextWithWordStemIndication(mSuggestionView.getCurrentSuggestion()); + textField.setComposingTextWithHighlightedStem(mSuggestionView.getCurrentSuggestion(), mInputMode); return true; } @@ -402,7 +405,7 @@ public class TraditionalT9 extends KeyPadHandler { private void commitCurrentSuggestion(boolean entireSuggestion) { if (!isSuggestionViewHidden() && currentInputConnection != null) { if (entireSuggestion) { - setComposingText(mSuggestionView.getCurrentSuggestion()); + textField.setComposingText(mSuggestionView.getCurrentSuggestion()); } currentInputConnection.finishComposingText(); } @@ -415,7 +418,7 @@ public class TraditionalT9 extends KeyPadHandler { setSuggestions(null); if (currentInputConnection != null) { - setComposingText(""); + textField.setComposingText(""); currentInputConnection.finishComposingText(); } } @@ -436,7 +439,7 @@ public class TraditionalT9 extends KeyPadHandler { // for a more intuitive experience. String word = mSuggestionView.getCurrentSuggestion(); word = word.substring(0, Math.min(mInputMode.getSequenceLength(), word.length())); - setComposingTextWithWordStemIndication(word); + textField.setComposingTextWithHighlightedStem(word, mInputMode); } @@ -464,13 +467,6 @@ public class TraditionalT9 extends KeyPadHandler { } - private void commitText(String text) { - if (text != null && currentInputConnection != null) { - currentInputConnection.commitText(text, 1); - } - } - - private String getComposingText() { String text = mSuggestionView.getCurrentSuggestion(); if (text.length() > 0 && text.length() > mInputMode.getSequenceLength()) { @@ -481,24 +477,8 @@ public class TraditionalT9 extends KeyPadHandler { } - private void setComposingText(CharSequence text) { - if (text != null && currentInputConnection != null) { - currentInputConnection.setComposingText(text, 1); - } - } - - - private void setComposingTextWithWordStemIndication(CharSequence word) { - if (mInputMode.getWordStem().length() > 0) { - setComposingText(TextHelper.highlightComposingText(word, 0, mInputMode.getWordStem().length(), mInputMode.isStemFilterFuzzy())); - } else { - setComposingText(word); - } - } - - private void refreshComposingText() { - setComposingText(getComposingText()); + textField.setComposingText(getComposingText()); } @@ -564,18 +544,15 @@ public class TraditionalT9 extends KeyPadHandler { private void jumpBeforeComposingText() { - if (currentInputConnection != null) { - currentInputConnection.setComposingText(getComposingText(), 0); - currentInputConnection.finishComposingText(); - } - + textField.setComposingText(getComposingText(), 0); + textField.finishComposingText(); setSuggestions(null); mInputMode.reset(); } private void determineAllowedInputModes() { - allowedInputModes = InputFieldHelper.determineInputModes(inputField); + allowedInputModes = textField.determineInputModes(inputType); int lastInputModeId = settings.getInputMode(); if (allowedInputModes.contains(lastInputModeId)) { @@ -586,23 +563,23 @@ public class TraditionalT9 extends KeyPadHandler { mInputMode = InputMode.getInstance(settings, allowedInputModes.get(0)); } - if (InputFieldHelper.isDialerField(inputField)) { + if (inputType.isDialer()) { mEditing = EDITING_DIALER; } else if (mInputMode.is123() && allowedInputModes.size() == 1) { mEditing = EDITING_STRICT_NUMERIC; } else { - mEditing = InputFieldHelper.isFilterField(inputField) ? EDITING_NOSHOW : EDITING; + mEditing = inputType.isFilter() ? EDITING_NOSHOW : EDITING; } } private void autoCorrectSpace(String currentWord, boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) { - if (mInputMode.shouldDeletePrecedingSpace(inputField)) { - InputFieldHelper.deletePrecedingSpace(currentInputConnection, currentWord); + if (mInputMode.shouldDeletePrecedingSpace(inputType)) { + textField.deletePrecedingSpace(currentWord); } - if (mInputMode.shouldAddAutoSpace(currentInputConnection, inputField, isWordAcceptedManually, incomingKey, hold, repeat)) { - commitText(" "); + if (mInputMode.shouldAddAutoSpace(inputType, textField, isWordAcceptedManually, incomingKey, hold, repeat)) { + textField.setText(" "); } } @@ -610,8 +587,8 @@ public class TraditionalT9 extends KeyPadHandler { private void determineNextTextCase() { mInputMode.determineNextWordTextCase( settings, - InputFieldHelper.isThereText(currentInputConnection), - InputFieldHelper.getTextBeforeCursor(currentInputConnection) + textField.isThereText(), + textField.getTextBeforeCursor() ); } @@ -624,7 +601,7 @@ public class TraditionalT9 extends KeyPadHandler { currentInputConnection.finishComposingText(); clearSuggestions(); - UI.showAddWordDialog(this, mLanguage.getId(), InputFieldHelper.getSurroundingWord(currentInputConnection)); + UI.showAddWordDialog(this, mLanguage.getId(), textField.getSurroundingWord()); } @@ -636,13 +613,13 @@ public class TraditionalT9 extends KeyPadHandler { String word = settings.getLastWord(); settings.clearLastWord(); - if (word.length() == 0 || word.equals(InputFieldHelper.getSurroundingWord(currentInputConnection))) { + if (word.length() == 0 || word.equals(textField.getSurroundingWord())) { return; } try { Logger.d("restoreAddedWordIfAny", "Restoring word: '" + word + "'..."); - commitText(word); + textField.setText(word); mInputMode.reset(); } catch (Exception e) { Logger.w("tt9/restoreLastWord", "Could not restore the last added word. " + e.getMessage()); diff --git a/src/io/github/sspanak/tt9/ime/helpers/InputFieldHelper.java b/src/io/github/sspanak/tt9/ime/helpers/InputFieldHelper.java deleted file mode 100644 index 79763d34..00000000 --- a/src/io/github/sspanak/tt9/ime/helpers/InputFieldHelper.java +++ /dev/null @@ -1,284 +0,0 @@ -package io.github.sspanak.tt9.ime.helpers; - -import android.text.InputType; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; -import android.view.inputmethod.InputConnection; - -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.github.sspanak.tt9.ime.modes.InputMode; - - -public class InputFieldHelper { - private static final Pattern beforeCursorWordRegex = Pattern.compile("(\\w+)(?!\n)$"); - private static final Pattern afterCursorWordRegex = Pattern.compile("^(? 0; - } - - - /** - * isThereSpaceAhead - * Checks whether there is a space after the cursor. - */ - public static boolean isThereSpaceAhead(InputConnection inputConnection) { - CharSequence after = inputConnection != null ? inputConnection.getTextAfterCursor(1, 0) : null; - return after != null && after.equals(" "); - } - - - /** - * isLimitedField - * Special or limited input type means the input connection is not rich, - * or it can not process or show things like candidate text, nor retrieve the current text. - * - * https://developer.android.com/reference/android/text/InputType#TYPE_NULL - */ - public static boolean isLimitedField(EditorInfo inputField) { - return inputField != null && inputField.inputType == InputType.TYPE_NULL; - } - - - /** - * isDialerField - * Dialer fields seem to take care of numbers and backspace on their own, - * so we need to be aware of them. - * - * NOTE: A Dialer field is not the same as a Phone field in a phone book. - */ - public static boolean isDialerField(EditorInfo inputField) { - return - inputField != null - && inputField.inputType == InputType.TYPE_CLASS_PHONE - && inputField.packageName.equals("com.android.dialer"); - } - - - 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 - || variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; - } - - - private static boolean isPersonNameField(EditorInfo inputField) { - return inputField != null && (inputField.inputType & InputType.TYPE_MASK_VARIATION) == InputType.TYPE_TEXT_VARIATION_PERSON_NAME; - } - - - public static boolean isSpecializedTextField(EditorInfo inputField) { - return isEmailField(inputField) || isPasswordField(inputField) || isUriField(inputField); - } - - - private static boolean isUriField(EditorInfo inputField) { - return inputField != null && (inputField.inputType & InputType.TYPE_MASK_VARIATION) == InputType.TYPE_TEXT_VARIATION_URI; - } - - - /** - * determineInputModes - * Determine the typing mode based on the input field being edited. Returns an ArrayList of the allowed modes. - * - * @return ArrayList - */ - public static ArrayList determineInputModes(EditorInfo inputField) { - final int INPUT_TYPE_SHARP_007H_PHONE_BOOK = 65633; - - ArrayList allowedModes = new ArrayList<>(); - - if (inputField == null) { - allowedModes.add(InputMode.MODE_123); - return allowedModes; - } - - if ( - inputField.inputType == INPUT_TYPE_SHARP_007H_PHONE_BOOK - || ( - inputField.privateImeOptions != null - && inputField.privateImeOptions.equals("io.github.sspanak.tt9.addword=true") - ) - ) { - allowedModes.add(InputMode.MODE_123); - allowedModes.add(InputMode.MODE_ABC); - return allowedModes; - } - - switch (inputField.inputType & InputType.TYPE_MASK_CLASS) { - case InputType.TYPE_CLASS_NUMBER: - case InputType.TYPE_CLASS_DATETIME: - // Numbers and dates default to the symbols keyboard, with - // no extra features. - case InputType.TYPE_CLASS_PHONE: - // Phones will also default to the symbols keyboard, though - // often you will want to have a dedicated phone keyboard. - allowedModes.add(InputMode.MODE_123); - return allowedModes; - - case InputType.TYPE_CLASS_TEXT: - // This is general text editing. We will default to the - // normal alphabetic keyboard, and assume that we should - // be doing predictive text (showing candidates as the - // user types). - if (!isPasswordField(inputField) && !isFilterField(inputField)) { - allowedModes.add(InputMode.MODE_PREDICTIVE); - } - - // ↓ fallthrough to add ABC and 123 modes ↓ - - default: - // For all unknown input types, default to the alphabetic - // keyboard with no special features. - allowedModes.add(InputMode.MODE_123); - allowedModes.add(InputMode.MODE_ABC); - - return allowedModes; - } - } - - - /** - * Helper to update the shift state of our keyboard based on the initial - * editor state. - */ - public static int determineTextCase(InputConnection inputConnection, EditorInfo inputField) { - if (inputField == null || inputConnection == null || inputField.inputType == InputType.TYPE_NULL) { - return InputMode.CASE_UNDEFINED; - } - - if (isSpecializedTextField(inputField)) { - return InputMode.CASE_LOWER; - } - - if (isPersonNameField(inputField)) { - return InputMode.CASE_CAPITALIZE; - } - - switch (inputField.inputType & InputType.TYPE_MASK_FLAGS) { - case InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS: - return InputMode.CASE_UPPER; - case InputType.TYPE_TEXT_FLAG_CAP_WORDS: - return InputMode.CASE_CAPITALIZE; - } - - return InputMode.CASE_UNDEFINED; - } - - - /** - * getTextBeforeCursor - * A simplified helper that return up to 50 characters before the cursor and "just works". - */ - public static String getTextBeforeCursor(InputConnection inputConnection) { - if (inputConnection == null) { - return ""; - } - - CharSequence before = inputConnection.getTextBeforeCursor(50, 0); - return before != null ? before.toString() : ""; - } - - - /** - * getTextBeforeCursor - * A simplified helper that return up to 50 characters after the cursor and "just works". - */ - public static String getTextAfterCursor(InputConnection inputConnection) { - if (inputConnection == null) { - return ""; - } - - CharSequence before = inputConnection.getTextAfterCursor(50, 0); - return before != null ? before.toString() : ""; - } - - - /** - * getSurroundingWord - * Returns the word next or around the cursor. Scanning length is up to 50 chars in each direction. - */ - public static String getSurroundingWord(InputConnection currentInputConnection) { - Matcher before = beforeCursorWordRegex.matcher(getTextBeforeCursor(currentInputConnection)); - Matcher after = afterCursorWordRegex.matcher(getTextAfterCursor(currentInputConnection)); - - return (before.find() ? before.group(1) : "") + (after.find() ? after.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/helpers/InputType.java b/src/io/github/sspanak/tt9/ime/helpers/InputType.java new file mode 100644 index 00000000..b73bb324 --- /dev/null +++ b/src/io/github/sspanak/tt9/ime/helpers/InputType.java @@ -0,0 +1,106 @@ +package io.github.sspanak.tt9.ime.helpers; + +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + + +public class InputType { + private final InputConnection connection; + private final EditorInfo field; + + + public InputType(InputConnection inputConnection, EditorInfo inputField) { + connection = inputConnection; + field = inputField; + } + + + public boolean isValid() { + return field != null && connection != null; + } + + + /** + * isLimited + * Special or limited input type means the input connection is not rich, + * or it can not process or show things like candidate text, nor retrieve the current text. + * + * https://developer.android.com/reference/android/text/InputType#TYPE_NULL + */ + public boolean isLimited() { + return field != null && field.inputType == android.text.InputType.TYPE_NULL; + } + + + /** + * isDialer + * Dialer fields seem to take care of numbers and backspace on their own, + * so we need to be aware of them. + * + * NOTE: A Dialer field is not the same as a Phone field in a phone book. + */ + public boolean isDialer() { + return + field != null + && field.inputType == android.text.InputType.TYPE_CLASS_PHONE + && field.packageName.equals("com.android.dialer"); + } + + + public boolean isEmail() { + if (field == null) { + return false; + } + + int variation = field.inputType & android.text.InputType.TYPE_MASK_VARIATION; + + return + variation == android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + || variation == android.text.InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; + } + + + /** + * isFilter + * handle filter list cases... do not hijack DPAD center and make sure back's go through proper + */ + public boolean isFilter() { + if (field == null) { + return false; + } + + int inputType = field.inputType & android.text.InputType.TYPE_MASK_CLASS; + int inputVariation = field.inputType & android.text.InputType.TYPE_MASK_VARIATION; + + return inputType == android.text.InputType.TYPE_CLASS_TEXT && inputVariation == android.text.InputType.TYPE_TEXT_VARIATION_FILTER; + } + + + public boolean isPassword() { + if (field == null) { + return false; + } + + int variation = field.inputType & android.text.InputType.TYPE_MASK_VARIATION; + + return + variation == android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD + || variation == android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + || variation == android.text.InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; + } + + + boolean isPersonName() { + return field != null && (field.inputType & android.text.InputType.TYPE_MASK_VARIATION) == android.text.InputType.TYPE_TEXT_VARIATION_PERSON_NAME; + } + + + public boolean isSpecialized() { + return isEmail() || isPassword() || isUri(); + } + + + private boolean isUri() { + return field != null && (field.inputType & android.text.InputType.TYPE_MASK_VARIATION) == android.text.InputType.TYPE_TEXT_VARIATION_URI; + } +} diff --git a/src/io/github/sspanak/tt9/ime/helpers/TextField.java b/src/io/github/sspanak/tt9/ime/helpers/TextField.java new file mode 100644 index 00000000..64e9d9ae --- /dev/null +++ b/src/io/github/sspanak/tt9/ime/helpers/TextField.java @@ -0,0 +1,300 @@ +package io.github.sspanak.tt9.ime.helpers; + +import android.graphics.Typeface; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.ime.modes.InputMode; + +public class TextField { + private static final Pattern beforeCursorWordRegex = Pattern.compile("(\\w+)(?!\n)$"); + private static final Pattern afterCursorWordRegex = Pattern.compile("^(? 0; + } + + + /** + * isThereSpaceAhead + * Checks whether there is a space after the cursor. + */ + public boolean isThereSpaceAhead() { + CharSequence after = connection != null ? connection.getTextAfterCursor(1, 0) : null; + return after != null && after.equals(" "); + } + + + /** + * determineInputModes + * Determine the typing mode based on the input field being edited. Returns an ArrayList of the allowed modes. + * + * @return ArrayList + */ + public ArrayList determineInputModes(InputType inputType) { + final int INPUT_TYPE_SHARP_007H_PHONE_BOOK = 65633; + + ArrayList allowedModes = new ArrayList<>(); + + if (field == null) { + allowedModes.add(InputMode.MODE_123); + return allowedModes; + } + + if ( + field.inputType == INPUT_TYPE_SHARP_007H_PHONE_BOOK + || ( + field.privateImeOptions != null + && field.privateImeOptions.equals("io.github.sspanak.tt9.addword=true") + ) + ) { + allowedModes.add(InputMode.MODE_123); + allowedModes.add(InputMode.MODE_ABC); + return allowedModes; + } + + switch (field.inputType & android.text.InputType.TYPE_MASK_CLASS) { + case android.text.InputType.TYPE_CLASS_NUMBER: + case android.text.InputType.TYPE_CLASS_DATETIME: + // Numbers and dates default to the symbols keyboard, with + // no extra features. + case android.text.InputType.TYPE_CLASS_PHONE: + // Phones will also default to the symbols keyboard, though + // often you will want to have a dedicated phone keyboard. + allowedModes.add(InputMode.MODE_123); + return allowedModes; + + case android.text.InputType.TYPE_CLASS_TEXT: + // This is general text editing. We will default to the + // normal alphabetic keyboard, and assume that we should + // be doing predictive text (showing candidates as the + // user types). + if (!inputType.isPassword() && !inputType.isFilter()) { + allowedModes.add(InputMode.MODE_PREDICTIVE); + } + + // ↓ fallthrough to add ABC and 123 modes ↓ + + default: + // For all unknown input types, default to the alphabetic + // keyboard with no special features. + allowedModes.add(InputMode.MODE_123); + allowedModes.add(InputMode.MODE_ABC); + + return allowedModes; + } + } + + + /** + * Helper to update the shift state of our keyboard based on the initial + * editor state. + */ + public int determineTextCase(InputType inputType) { + if (connection == null || field == null || field.inputType == android.text.InputType.TYPE_NULL) { + return InputMode.CASE_UNDEFINED; + } + + if (inputType.isSpecialized()) { + return InputMode.CASE_LOWER; + } + + if (inputType.isPersonName()) { + return InputMode.CASE_CAPITALIZE; + } + + switch (field.inputType & android.text.InputType.TYPE_MASK_FLAGS) { + case android.text.InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS: + return InputMode.CASE_UPPER; + case android.text.InputType.TYPE_TEXT_FLAG_CAP_WORDS: + return InputMode.CASE_CAPITALIZE; + } + + return InputMode.CASE_UNDEFINED; + } + + + /** + * getTextBeforeCursor + * A simplified helper that return up to 50 characters before the cursor and "just works". + */ + public String getTextBeforeCursor() { + if (connection == null) { + return ""; + } + + CharSequence before = connection.getTextBeforeCursor(50, 0); + return before != null ? before.toString() : ""; + } + + + /** + * getTextBeforeCursor + * A simplified helper that return up to 50 characters after the cursor and "just works". + */ + public String getTextAfterCursor() { + if (connection == null) { + return ""; + } + + CharSequence before = connection.getTextAfterCursor(50, 0); + return before != null ? before.toString() : ""; + } + + + /** + * getSurroundingWord + * Returns the word next or around the cursor. Scanning length is up to 50 chars in each direction. + */ + public String getSurroundingWord() { + Matcher before = beforeCursorWordRegex.matcher(getTextBeforeCursor()); + Matcher after = afterCursorWordRegex.matcher(getTextAfterCursor()); + + return (before.find() ? before.group(1) : "") + (after.find() ? after.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 void deletePrecedingSpace(String word) { + if (connection == null) { + return; + } + + String searchText = " " + word; + + connection.beginBatchEdit(); + CharSequence beforeText = connection.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 + ) { + connection.endBatchEdit(); + return; + } + + connection.deleteSurroundingText(searchText.length(), 0); + connection.commitText(word, 1); + + connection.endBatchEdit(); + } + + + /** + * setText + * A fail-safe setter that appends text to the field, ignoring NULL input. + */ + public void setText(String text) { + if (text != null && connection != null) { + connection.commitText(text, 1); + } + } + + + /** + * setComposingText + * A fail-safe setter for composing text, which ignores NULL input. + */ + public void setComposingText(CharSequence text, int position) { + if (text != null && connection != null) { + connection.setComposingText(text, position); + } + } + + public void setComposingText(CharSequence text) { setComposingText(text, 1); } + + + /** + * setComposingTextWithHighlightedStem + * + * Sets the composing text, but makes the "stem" substring bold. If "highlightMore" is true, + * the "stem" part will be in bold and italic. + */ + public void setComposingTextWithHighlightedStem(CharSequence word, String stem, boolean highlightMore) { + setComposingText( + stem.length() > 0 ? highlightText(word, 0, stem.length(), highlightMore) : word + ); + } + + public void setComposingTextWithHighlightedStem(CharSequence word, InputMode inputMode) { + setComposingTextWithHighlightedStem(word, inputMode.getWordStem(), inputMode.isStemFilterFuzzy()); + } + + + /** + * finishComposingText + * Finish composing text or do nothing if the text field is invalid. + */ + public void finishComposingText() { + if (connection != null) { + connection.finishComposingText(); + } + } + + + /** + * highlightText + * Makes the characters from "start" to "end" bold. If "highlightMore" is true, + * the text will be in bold and italic. + */ + private CharSequence highlightText(CharSequence word, int start, int end, boolean highlightMore) { + if (end < start || start < 0) { + Logger.w("tt9.util.highlightComposingText", "Cannot highlight invalid composing text range: [" + start + ", " + end + "]"); + return word; + } + + SpannableString styledWord = new SpannableString(word); + + // default underline style + styledWord.setSpan(new UnderlineSpan(), 0, word.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + + // highlight the requested range + styledWord.setSpan( + new StyleSpan(Typeface.BOLD), + start, + Math.min(word.length(), end), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + + if (highlightMore) { + styledWord.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + start, + Math.min(word.length(), end), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + + return styledWord; + } +} diff --git a/src/io/github/sspanak/tt9/ime/helpers/TextHelper.java b/src/io/github/sspanak/tt9/ime/helpers/TextHelper.java deleted file mode 100644 index f690a594..00000000 --- a/src/io/github/sspanak/tt9/ime/helpers/TextHelper.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.sspanak.tt9.ime.helpers; - -import android.graphics.Typeface; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.style.StyleSpan; -import android.text.style.UnderlineSpan; - -import io.github.sspanak.tt9.Logger; - -public class TextHelper { - public static CharSequence highlightComposingText(CharSequence word, int start, int end, boolean highlightMore) { - if (end < start || start < 0) { - Logger.w("tt9.util.highlightComposingText", "Cannot highlight invalid composing text range: [" + start + ", " + end + "]"); - return word; - } - - SpannableString styledWord = new SpannableString(word); - - // default underline style - styledWord.setSpan(new UnderlineSpan(), 0, word.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); - - // highlight the requested range - styledWord.setSpan( - new StyleSpan(Typeface.BOLD), - start, - Math.min(word.length(), end), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ); - - if (highlightMore) { - styledWord.setSpan( - new StyleSpan(Typeface.BOLD_ITALIC), - start, - Math.min(word.length(), end), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - - return styledWord; - } -} diff --git a/src/io/github/sspanak/tt9/ime/modes/InputMode.java b/src/io/github/sspanak/tt9/ime/modes/InputMode.java index 0a37c460..3f83b6d6 100644 --- a/src/io/github/sspanak/tt9/ime/modes/InputMode.java +++ b/src/io/github/sspanak/tt9/ime/modes/InputMode.java @@ -1,12 +1,12 @@ package io.github.sspanak.tt9.ime.modes; import android.os.Handler; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; import java.util.ArrayList; import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.ime.helpers.InputType; +import io.github.sspanak.tt9.ime.helpers.TextField; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.preferences.SettingsStore; @@ -76,8 +76,8 @@ abstract public class InputMode { // Interaction with the IME. Return "true" if it should perform the respective action. public boolean shouldAcceptCurrentSuggestion(Language language, int key, boolean hold, boolean repeat) { return false; } - public boolean shouldAddAutoSpace(InputConnection inputConnection, EditorInfo inputField, boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) { return false; } - public boolean shouldDeletePrecedingSpace(EditorInfo inputField) { return false; } + public boolean shouldAddAutoSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) { return false; } + public boolean shouldDeletePrecedingSpace(InputType inputType) { return false; } public boolean shouldSelectNextSuggestion() { return false; } public boolean shouldTrackNumPress() { return true; } public boolean shouldTrackUpDown() { return false; } diff --git a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java index 7c39fce0..41814fee 100644 --- a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java +++ b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java @@ -3,8 +3,6 @@ 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 android.view.inputmethod.InputConnection; import java.util.ArrayList; import java.util.regex.Pattern; @@ -12,7 +10,8 @@ import java.util.regex.Pattern; import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.db.DictionaryDb; import io.github.sspanak.tt9.ime.EmptyDatabaseWarning; -import io.github.sspanak.tt9.ime.helpers.InputFieldHelper; +import io.github.sspanak.tt9.ime.helpers.InputType; +import io.github.sspanak.tt9.ime.helpers.TextField; import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.Characters; @@ -516,15 +515,15 @@ public class ModePredictive extends InputMode { * See the helper functions for the list of rules. */ @Override - public boolean shouldAddAutoSpace(InputConnection inputConnection, EditorInfo inputField, boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) { + public boolean shouldAddAutoSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) { return settings.getAutoSpace() && !hold && ( - shouldAddAutoSpaceAfterPunctuation(inputField, incomingKey, repeat) - || shouldAddAutoSpaceAfterWord(inputField, isWordAcceptedManually) + shouldAddAutoSpaceAfterPunctuation(inputType, incomingKey, repeat) + || shouldAddAutoSpaceAfterWord(inputType, isWordAcceptedManually) ) - && !InputFieldHelper.isThereSpaceAhead(inputConnection); + && !textField.isThereSpaceAhead(); } @@ -534,7 +533,7 @@ public class ModePredictive extends InputMode { * 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, boolean repeat) { + private boolean shouldAddAutoSpaceAfterPunctuation(InputType inputType, int incomingKey, boolean repeat) { return (incomingKey != 0 || repeat) && ( @@ -548,7 +547,7 @@ public class ModePredictive extends InputMode { || lastAcceptedWord.endsWith("]") || lastAcceptedWord.endsWith("%") ) - && !InputFieldHelper.isSpecializedTextField(inputField); + && !inputType.isSpecialized(); } @@ -557,7 +556,7 @@ public class ModePredictive extends InputMode { * Similar to "shouldAddAutoSpaceAfterPunctuation()", but determines whether to add a space after * words. */ - private boolean shouldAddAutoSpaceAfterWord(EditorInfo inputField, boolean isWordAcceptedManually) { + private boolean shouldAddAutoSpaceAfterWord(InputType inputType, boolean isWordAcceptedManually) { return // Do not add space when auto-accepting words, because it feels very confusing when typing. isWordAcceptedManually @@ -565,7 +564,7 @@ public class ModePredictive extends InputMode { && !lastAcceptedSequence.equals("0") // Emoji && !lastAcceptedSequence.startsWith("1") - && !InputFieldHelper.isSpecializedTextField(inputField); + && !inputType.isSpecialized(); } @@ -575,7 +574,7 @@ public class ModePredictive extends InputMode { * This allows automatic conversion from: "words ." to: "words." */ @Override - public boolean shouldDeletePrecedingSpace(EditorInfo inputField) { + public boolean shouldDeletePrecedingSpace(InputType inputType) { return settings.getAutoSpace() && ( @@ -590,7 +589,7 @@ public class ModePredictive extends InputMode { || lastAcceptedWord.equals("'") || lastAcceptedWord.equals("@") ) - && !InputFieldHelper.isSpecializedTextField(inputField); + && !inputType.isSpecialized(); }