diff --git a/src/io/github/sspanak/tt9/ime/KeyPadHandler.java b/src/io/github/sspanak/tt9/ime/KeyPadHandler.java index 82572def..7f2997c1 100644 --- a/src/io/github/sspanak/tt9/ime/KeyPadHandler.java +++ b/src/io/github/sspanak/tt9/ime/KeyPadHandler.java @@ -177,6 +177,10 @@ abstract class KeyPadHandler extends InputMethodService { return onNumber(Key.codeToNumber(settings, keyCode), true, 0); } + if (Key.isPoundOrStar(keyCode) && onOtherKey(keyCode)) { + return true; + } + ignoreNextKeyUp = 0; return super.onKeyLongPress(keyCode, event); } @@ -234,7 +238,9 @@ abstract class KeyPadHandler extends InputMethodService { case KeyEvent.KEYCODE_DPAD_RIGHT: return onRight(keyRepeatCounter > 0); case KeyEvent.KEYCODE_STAR: case KeyEvent.KEYCODE_POUND: - return onOtherKey(keyCode); + if (onOtherKey(keyCode)) { + return true; + } } return super.onKeyUp(keyCode, event); diff --git a/src/io/github/sspanak/tt9/ime/TraditionalT9.java b/src/io/github/sspanak/tt9/ime/TraditionalT9.java index 60951a5f..68792c7c 100644 --- a/src/io/github/sspanak/tt9/ime/TraditionalT9.java +++ b/src/io/github/sspanak/tt9/ime/TraditionalT9.java @@ -14,7 +14,6 @@ import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.db.DictionaryDb; import io.github.sspanak.tt9.ime.helpers.InputModeValidator; import io.github.sspanak.tt9.ime.helpers.InputType; -import io.github.sspanak.tt9.ime.helpers.Key; import io.github.sspanak.tt9.ime.helpers.TextField; import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.languages.Language; @@ -34,9 +33,7 @@ public class TraditionalT9 extends KeyPadHandler { // editing mode protected static final int NON_EDIT = 0; protected static final int EDITING = 1; - protected static final int EDITING_STRICT_NUMERIC = 3; - protected static final int EDITING_DIALER = 4; // see: https://github.com/sspanak/tt9/issues/46 - protected int mEditing = NON_EDIT; + @Deprecated protected int mEditing = NON_EDIT; // @todo: migrate to "isActive" // input mode private ArrayList allowedInputModes = new ArrayList<>(); @@ -185,7 +182,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 || !textField.isThereText()) { + if (mInputMode.isDialer() || !textField.isThereText()) { Logger.d("onBackspace", "backspace ignored"); mInputMode.reset(); return false; @@ -316,21 +313,20 @@ public class TraditionalT9 extends KeyPadHandler { public boolean onOtherKey(int keyCode) { - if ( - keyCode <= 0 || - (mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER) && !Key.isNumber(keyCode) - ) { - return false; + String acceptedWord = acceptIncompleteSuggestion(); + if (mInputMode.onOtherKey(keyCode)) { + autoCorrectSpace(acceptedWord, false); + getSuggestions(); + resetKeyRepeat(); + return true; } - autoCorrectSpace(acceptIncompleteSuggestion(), false); - sendDownUpKeyEvents(keyCode); - return true; + return acceptedWord.length() > 0; } public boolean onText(String text) { - if (mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER || text.length() == 0) { + if (mInputMode.isNumeric() || text.length() == 0) { return false; } @@ -346,7 +342,7 @@ public class TraditionalT9 extends KeyPadHandler { public boolean onKeyAddWord() { - if (mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER) { + if (!isInputViewShown() || mInputMode.isNumeric()) { return false; } @@ -376,14 +372,19 @@ public class TraditionalT9 extends KeyPadHandler { public boolean onKeyNextInputMode() { nextInputMode(); + + if (allowedInputModes.size() == 1) { + return false; + } + mainView.render(); forceShowWindowIfHidden(); - return (mEditing != EDITING_STRICT_NUMERIC && mEditing != EDITING_DIALER); + return true; } public boolean onKeyShowSettings() { - if (mEditing == EDITING_DIALER) { + if (!isInputViewShown()) { return false; } @@ -393,11 +394,11 @@ public class TraditionalT9 extends KeyPadHandler { protected boolean shouldTrackUpDown() { - return mEditing != EDITING_STRICT_NUMERIC && !isSuggestionViewHidden() && mInputMode.shouldTrackUpDown(); + return !isSuggestionViewHidden() && mInputMode.shouldTrackUpDown(); } protected boolean shouldTrackLeftRight() { - return mEditing != EDITING_STRICT_NUMERIC && !isSuggestionViewHidden() && mInputMode.shouldTrackLeftRight(); + return !isSuggestionViewHidden() && mInputMode.shouldTrackLeftRight(); } @@ -474,7 +475,8 @@ public class TraditionalT9 extends KeyPadHandler { // key code "suggestions" take priority over words if (mInputMode.getKeyCode() > 0) { sendDownUpKeyEvents(mInputMode.getKeyCode()); - mInputMode.onAcceptSuggestion(null); + mInputMode.onAcceptSuggestion(""); + return; } // display the list of suggestions @@ -522,7 +524,9 @@ public class TraditionalT9 extends KeyPadHandler { private void nextInputMode() { - if (mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER) { + if (mInputMode.isDialer()) { + return; + } else if (allowedInputModes.size() == 1 && allowedInputModes.contains(InputMode.MODE_123)) { mInputMode = !mInputMode.is123() ? InputMode.getInstance(settings, mLanguage, InputMode.MODE_123) : mInputMode; } // when typing a word or viewing scrolling the suggestions, only change the case @@ -562,7 +566,7 @@ public class TraditionalT9 extends KeyPadHandler { private boolean nextLang() { - if (mInputMode.is123() || mEnabledLanguages.size() < 2) { + if (mInputMode.isNumeric() || mEnabledLanguages.size() < 2) { return false; } @@ -597,19 +601,15 @@ public class TraditionalT9 extends KeyPadHandler { int lastInputModeId = settings.getInputMode(); if (allowedInputModes.contains(lastInputModeId)) { mInputMode = InputMode.getInstance(settings, mLanguage, lastInputModeId); + } else if (allowedInputModes.size() == 1 && allowedInputModes.get(0) == InputMode.MODE_DIALER) { + mInputMode = InputMode.getInstance(settings, mLanguage, InputMode.MODE_DIALER); } else if (allowedInputModes.contains(InputMode.MODE_ABC)) { mInputMode = InputMode.getInstance(settings, mLanguage, InputMode.MODE_ABC); } else { mInputMode = InputMode.getInstance(settings, mLanguage, allowedInputModes.get(0)); } - if (inputType.isDialer()) { - mEditing = EDITING_DIALER; - } else if (mInputMode.is123() && allowedInputModes.size() == 1) { - mEditing = EDITING_STRICT_NUMERIC; - } else { - mEditing = EDITING; - } + mEditing = EDITING; } @@ -721,8 +721,7 @@ public class TraditionalT9 extends KeyPadHandler { */ protected void forceShowWindowIfHidden() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P - && mEditing != EDITING_STRICT_NUMERIC - && mEditing != EDITING_DIALER + && !mInputMode.isDialer() && !isInputViewShown() ) { requestShowSelf(InputMethodManager.SHOW_IMPLICIT); @@ -732,7 +731,7 @@ public class TraditionalT9 extends KeyPadHandler { @Override protected boolean shouldBeVisible() { - return mEditing != EDITING_DIALER && mEditing != NON_EDIT; + return !mInputMode.isDialer() && mEditing != NON_EDIT; } diff --git a/src/io/github/sspanak/tt9/ime/helpers/InputType.java b/src/io/github/sspanak/tt9/ime/helpers/InputType.java index a524fc23..08ed2b95 100644 --- a/src/io/github/sspanak/tt9/ime/helpers/InputType.java +++ b/src/io/github/sspanak/tt9/ime/helpers/InputType.java @@ -25,7 +25,7 @@ public class InputType { * 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. * - * ... + * More info: android docs. */ public boolean isLimited() { return field != null && field.inputType == android.text.InputType.TYPE_NULL; @@ -37,7 +37,11 @@ public class InputType { * 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. + * NOTE: A Dialer field is not the same as Phone field. Dialer is where you + * actually dial and call a phone number. While the Phone field is a text + * field in any app or a webpage, intended for typing phone numbers. + * + * More info: in this Github issue. */ public boolean isDialer() { if (field == null) { diff --git a/src/io/github/sspanak/tt9/ime/helpers/Key.java b/src/io/github/sspanak/tt9/ime/helpers/Key.java index bee0b744..9b90aaa6 100644 --- a/src/io/github/sspanak/tt9/ime/helpers/Key.java +++ b/src/io/github/sspanak/tt9/ime/helpers/Key.java @@ -21,6 +21,17 @@ public class Key { } + public static boolean isPoundOrStar(int keyCode) { + return keyCode == KeyEvent.KEYCODE_POUND || keyCode == KeyEvent.KEYCODE_STAR; + } + + public static boolean isDecimalSeparator(int keyCode) { + return + keyCode == KeyEvent.KEYCODE_COMMA + || keyCode == KeyEvent.KEYCODE_NUMPAD_DOT + || keyCode == KeyEvent.KEYCODE_PERIOD; + } + public static boolean isOK(int keyCode) { return keyCode == KeyEvent.KEYCODE_DPAD_CENTER diff --git a/src/io/github/sspanak/tt9/ime/helpers/TextField.java b/src/io/github/sspanak/tt9/ime/helpers/TextField.java index 19848374..e830e462 100644 --- a/src/io/github/sspanak/tt9/ime/helpers/TextField.java +++ b/src/io/github/sspanak/tt9/ime/helpers/TextField.java @@ -90,14 +90,19 @@ public class TextField { return allowedModes; } + // Dialer field, not to be confused with Phone text field. + // It only accepts 0-9, "#" and "*". + if (inputType.isDialer()) { + allowedModes.add(InputMode.MODE_DIALER); + 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. + // Numbers, dates and phone numbers default to the numeric keyboard, + // with no extra features. allowedModes.add(InputMode.MODE_123); return allowedModes; diff --git a/src/io/github/sspanak/tt9/ime/modes/InputMode.java b/src/io/github/sspanak/tt9/ime/modes/InputMode.java index 52d46ada..5f96cad5 100644 --- a/src/io/github/sspanak/tt9/ime/modes/InputMode.java +++ b/src/io/github/sspanak/tt9/ime/modes/InputMode.java @@ -1,5 +1,7 @@ package io.github.sspanak.tt9.ime.modes; +import androidx.annotation.NonNull; + import java.util.ArrayList; import io.github.sspanak.tt9.Logger; @@ -13,6 +15,7 @@ abstract public class InputMode { public static final int MODE_PREDICTIVE = 0; public static final int MODE_ABC = 1; public static final int MODE_123 = 2; + public static final int MODE_DIALER = 4; // text case public static final int CASE_UNDEFINED = -1; @@ -37,6 +40,8 @@ abstract public class InputMode { return new ModePredictive(settings, language); case MODE_ABC: return new ModeABC(language); + case MODE_DIALER: + return new ModeDialer(); default: Logger.w("tt9/InputMode", "Defaulting to mode: " + Mode123.class.getName() + " for unknown InputMode: " + mode); case MODE_123: @@ -47,18 +52,19 @@ 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(int number, boolean hold, int repeat); + abstract public boolean onOtherKey(int key); // Suggestions - public void onAcceptSuggestion(String suggestion) {} + public void onAcceptSuggestion(@NonNull String suggestion) {} /** * loadSuggestions * Loads the suggestions based on the current state, with optional "currentWord" filter. - * Once loading is finished the respective InputMode child will call "notification", notifying it + * Once loading is finished the respective InputMode child will call "onLoad", notifying it * the suggestions are available using "getSuggestions()". */ - public void loadSuggestions(Runnable notification, String currentWord) { - notification.run(); + public void loadSuggestions(Runnable onLoad, String currentWord) { + onLoad.run(); } public ArrayList getSuggestions() { @@ -71,9 +77,10 @@ abstract public class InputMode { } // Mode identifiers - public boolean isPredictive() { return false; } public boolean isABC() { return false; } public boolean is123() { return false; } + public boolean isDialer() { return false; } + public boolean isNumeric() { return false; } // Utility abstract public int getId(); diff --git a/src/io/github/sspanak/tt9/ime/modes/Mode123.java b/src/io/github/sspanak/tt9/ime/modes/Mode123.java index 9b9db887..8c025ab4 100644 --- a/src/io/github/sspanak/tt9/ime/modes/Mode123.java +++ b/src/io/github/sspanak/tt9/ime/modes/Mode123.java @@ -1,5 +1,7 @@ package io.github.sspanak.tt9.ime.modes; +import android.view.KeyEvent; + import androidx.annotation.NonNull; import io.github.sspanak.tt9.ime.helpers.Key; @@ -14,19 +16,23 @@ public class Mode123 extends InputMode { @Override public boolean onNumber(int number, boolean hold, int repeat) { reset(); - - if (number == 0 && hold) { - autoAcceptTimeout = 0; - suggestions.add("+"); - } else { - keyCode = Key.numberToCode(number); - } - + keyCode = (number == 0 && hold) ? KeyEvent.KEYCODE_PLUS : Key.numberToCode(number); return true; } + @Override + public boolean onOtherKey(int key) { + reset(); + if (Key.isDecimalSeparator(key) || Key.isPoundOrStar(key)) { + keyCode = key; + return true; + } - @Override final public boolean is123() { return true; } + return false; + } + + @Override public boolean is123() { return true; } + @Override final public boolean isNumeric() { return true; } @Override public int getSequenceLength() { return 0; } @NonNull diff --git a/src/io/github/sspanak/tt9/ime/modes/ModeABC.java b/src/io/github/sspanak/tt9/ime/modes/ModeABC.java index a20bb4fd..5d9e7c5e 100644 --- a/src/io/github/sspanak/tt9/ime/modes/ModeABC.java +++ b/src/io/github/sspanak/tt9/ime/modes/ModeABC.java @@ -31,11 +31,25 @@ public class ModeABC extends InputMode { } + @Override + public boolean onOtherKey(int key) { + reset(); + + if (key > 0) { + keyCode = key; + return true; + } + + return false; + } + + @Override protected String adjustSuggestionTextCase(String word, int newTextCase) { return newTextCase == CASE_UPPER ? word.toUpperCase(language.getLocale()) : word.toLowerCase(language.getLocale()); } + @Override public void changeLanguage(Language language) { super.changeLanguage(language); diff --git a/src/io/github/sspanak/tt9/ime/modes/ModeDialer.java b/src/io/github/sspanak/tt9/ime/modes/ModeDialer.java new file mode 100644 index 00000000..1e9000b6 --- /dev/null +++ b/src/io/github/sspanak/tt9/ime/modes/ModeDialer.java @@ -0,0 +1,15 @@ +package io.github.sspanak.tt9.ime.modes; + +import io.github.sspanak.tt9.ime.helpers.Key; + +// see: InputType.isDialer() +public class ModeDialer extends Mode123 { + @Override public int getId() { return MODE_DIALER; } + @Override public boolean is123() { return false; } + @Override final public boolean isDialer() { return true; } + + @Override + public boolean onOtherKey(int key) { + return !Key.isDecimalSeparator(key) && super.onOtherKey(key); + } +} diff --git a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java index c40101ac..8af9cb96 100644 --- a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java +++ b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java @@ -86,6 +86,20 @@ public class ModePredictive extends InputMode { } + @Override + public boolean onOtherKey(int key) { + reset(); + + if (key > 0) { + disablePredictions = true; + keyCode = key; + return true; + } + + return false; + } + + @Override public void changeLanguage(Language language) { super.changeLanguage(language); @@ -191,13 +205,13 @@ public class ModePredictive extends InputMode { * See: Predictions.generatePossibleCompletions() */ @Override - public void loadSuggestions(Runnable handler, String currentWord) { + public void loadSuggestions(Runnable onLoad, String currentWord) { if (disablePredictions) { - super.loadSuggestions(handler, currentWord); + super.loadSuggestions(onLoad, currentWord); return; } - onSuggestionsUpdated = handler; + onSuggestionsUpdated = onLoad; predictions .setDigitSequence(digitSequence) .setIsStemFuzzy(isStemFuzzy) @@ -227,7 +241,7 @@ public class ModePredictive extends InputMode { * Bring this word up in the suggestions list next time. */ @Override - public void onAcceptSuggestion(String currentWord) { + public void onAcceptSuggestion(@NonNull String currentWord) { lastAcceptedWord = currentWord; lastAcceptedSequence = digitSequence; reset(); @@ -314,7 +328,6 @@ public class ModePredictive extends InputMode { @Override public boolean shouldTrackUpDown() { return true; } @Override public boolean shouldTrackLeftRight() { return true; } - @Override final public boolean isPredictive() { return true; } @Override public int getSequenceLength() { return digitSequence.length(); } @NonNull diff --git a/src/io/github/sspanak/tt9/preferences/helpers/Hotkeys.java b/src/io/github/sspanak/tt9/preferences/helpers/Hotkeys.java index 6bd716be..61642fa6 100644 --- a/src/io/github/sspanak/tt9/preferences/helpers/Hotkeys.java +++ b/src/io/github/sspanak/tt9/preferences/helpers/Hotkeys.java @@ -102,18 +102,9 @@ public class Hotkeys { * * NOTE: Some TT9 functions do not support all keys. Here you just list all possible options. * Actual validation and assigning happens in SectionKeymap.populate(). - * - * NOTE 2: Holding is deliberately skipped for most of the keys. - * It's because handling holding requires short press event to be consumed in - * KeyPadHandler, as well. - * - * From user perspective, when holding is assigned to a function, - * short press will also stop performing its default system action, which may be confusing. - * And in order to avoid lengthy explanations in the documentation (that no one reads), - * the problem is avoided by simply not causing it. */ private void generateList() { - add(KeyEvent.KEYCODE_CALL, R.string.key_call, false); + add(KeyEvent.KEYCODE_CALL, R.string.key_call, true); addIfDeviceHasKey(KeyEvent.KEYCODE_BACK, R.string.key_back, false); addIfDeviceHasKey(KeyEvent.KEYCODE_CLEAR, R.string.key_clear, false); @@ -122,7 +113,7 @@ public class Hotkeys { addIfDeviceHasKey(KeyEvent.KEYCODE_F2, "F2", true); addIfDeviceHasKey(KeyEvent.KEYCODE_F3, "F3", true); addIfDeviceHasKey(KeyEvent.KEYCODE_F4, "F4", true); - addIfDeviceHasKey(KeyEvent.KEYCODE_MENU, R.string.key_menu, false); + addIfDeviceHasKey(KeyEvent.KEYCODE_MENU, R.string.key_menu, true); addIfDeviceHasKey(KeyEvent.KEYCODE_SOFT_LEFT, R.string.key_soft_left, false); addIfDeviceHasKey(KeyEvent.KEYCODE_SOFT_RIGHT, R.string.key_soft_right, false); diff --git a/src/io/github/sspanak/tt9/ui/main/keys/SoftPunctuationKey.java b/src/io/github/sspanak/tt9/ui/main/keys/SoftPunctuationKey.java index aab414c0..a5327656 100644 --- a/src/io/github/sspanak/tt9/ui/main/keys/SoftPunctuationKey.java +++ b/src/io/github/sspanak/tt9/ui/main/keys/SoftPunctuationKey.java @@ -29,8 +29,8 @@ public class SoftPunctuationKey extends SoftKey { preventRepeat(); int keyId = getId(); - if (keyId == R.id.soft_key_punctuation_1) return tt9.onText(","); - if (keyId == R.id.soft_key_punctuation_2) return tt9.onText("."); + if (keyId == R.id.soft_key_punctuation_1) return tt9.onOtherKey(KeyEvent.KEYCODE_COMMA); + if (keyId == R.id.soft_key_punctuation_2) return tt9.onOtherKey(KeyEvent.KEYCODE_PERIOD); return false; }