From a3e72283f804d6f1eb8289fc321bbab2292157cd Mon Sep 17 00:00:00 2001 From: Dimo Karaivanov Date: Tue, 13 Feb 2024 18:50:51 +0200 Subject: [PATCH] added currency character typing in all modes --- .../sspanak/tt9/db/DictionaryLoader.java | 2 +- .../github/sspanak/tt9/ime/TraditionalT9.java | 6 +- .../sspanak/tt9/ime/modes/InputMode.java | 38 +++++++++- .../github/sspanak/tt9/ime/modes/Mode123.java | 20 ++++- .../github/sspanak/tt9/ime/modes/ModeABC.java | 14 ++++ .../sspanak/tt9/ime/modes/ModePredictive.java | 63 ++++++++++++---- .../tt9/ime/modes/helpers/Predictions.java | 75 +++---------------- .../sspanak/tt9/languages/Characters.java | 6 +- .../sspanak/tt9/languages/Language.java | 32 +++++--- .../tt9/ui/main/keys/SoftNumberKey.java | 2 +- docs/user-manual.md | 3 + 11 files changed, 160 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java b/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java index 0815a608..090a078e 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java @@ -196,7 +196,7 @@ public class DictionaryLoader { WordBatch letters = new WordBatch(language); for (int key = 2; key <= 9; key++) { - for (String langChar : language.getKeyCharacters(key, false)) { + for (String langChar : language.getKeyCharacters(key)) { langChar = (isEnglish && langChar.equals("i")) ? langChar.toUpperCase(Locale.ENGLISH) : langChar; letters.add(langChar, 0, key); lettersCount++; diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java b/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java index c226c1b7..19956d0f 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java @@ -688,10 +688,6 @@ public class TraditionalT9 extends KeyPadHandler { } - private void refreshComposingText() { - textField.setComposingText(getComposingText()); - } - private void setComposingTextWithHighlightedStem(@NonNull String word) { if (appHacks.setComposingTextWithHighlightedStem(word)) { @@ -718,7 +714,7 @@ public class TraditionalT9 extends KeyPadHandler { for (int retries = 0; retries < 2 && mLanguage.hasUpperCase(); retries++) { mInputMode.nextTextCase(); setSuggestions(mInputMode.getSuggestions(), suggestionBar.getCurrentIndex()); - refreshComposingText(); + textField.setComposingText(suggestionBar.getCurrentSuggestion()); if (!currentSuggestionBefore.equals(getComposingText())) { break; diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/InputMode.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/InputMode.java index 02896df1..9309094b 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/InputMode.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/InputMode.java @@ -29,8 +29,10 @@ abstract public class InputMode { // data protected int autoAcceptTimeout = -1; + @NonNull protected String digitSequence = ""; protected Language language; protected final ArrayList suggestions = new ArrayList<>(); + protected int specialCharSelectedGroup = 0; public static InputMode getInstance(SettingsStore settings, Language language, InputType inputType, int mode) { @@ -44,7 +46,7 @@ abstract public class InputMode { default: Logger.w("InputMode", "Defaulting to mode: " + Mode123.class.getName() + " for unknown InputMode: " + mode); case MODE_123: - return new Mode123(inputType); + return new Mode123(inputType, language); } } @@ -102,6 +104,7 @@ abstract public class InputMode { public void reset() { autoAcceptTimeout = -1; + specialCharSelectedGroup = 0; suggestions.clear(); } @@ -125,9 +128,40 @@ abstract public class InputMode { textCase = allowedTextCases.get(0); } - public void nextTextCase() { + public boolean nextTextCase() { + if (nextSpecialCharacters()) { + return false; + } + int nextIndex = (allowedTextCases.indexOf(textCase) + 1) % allowedTextCases.size(); textCase = allowedTextCases.get(nextIndex); + return true; + } + + /** + * This is used in nextTextCase() for switching to the next set of characters. Obviously, + * special chars do not have a text case, but we use this trick to alternate the char groups. + */ + protected boolean nextSpecialCharacters() { + if (language == null || digitSequence.isEmpty()) { + return false; + } + + int key = digitSequence.charAt(0) - '0'; + + ArrayList chars = language.getKeyCharacters(key, ++specialCharSelectedGroup); + if (chars.isEmpty() && specialCharSelectedGroup == 1) { + specialCharSelectedGroup = 0; + return false; + } else if (chars.isEmpty()) { + specialCharSelectedGroup = 0; + chars = language.getKeyCharacters(key, specialCharSelectedGroup); + } + + suggestions.clear(); + suggestions.addAll(chars); + + return true; } public void determineNextWordTextCase(String textBeforeCursor) {} diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/Mode123.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/Mode123.java index c3f50de5..114a82ad 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/Mode123.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/Mode123.java @@ -8,6 +8,7 @@ import java.util.Collections; import io.github.sspanak.tt9.ime.helpers.InputType; import io.github.sspanak.tt9.languages.Characters; +import io.github.sspanak.tt9.languages.Language; public class Mode123 extends ModePassthrough { @Override public int getId() { return MODE_123; } @@ -21,7 +22,9 @@ public class Mode123 extends ModePassthrough { private final ArrayList> KEY_CHARACTERS = new ArrayList<>(); - public Mode123(InputType inputType) { + public Mode123(InputType inputType, Language language) { + this.language = language; + if (inputType.isPhoneNumber()) { getPhoneSpecialCharacters(); } else if (inputType.isNumeric()) { @@ -79,14 +82,20 @@ public class Mode123 extends ModePassthrough { } + @Override + protected boolean nextSpecialCharacters() { + return digitSequence.equals(Language.SPECIAL_CHARS_KEY) && super.nextSpecialCharacters(); + } + @Override public boolean onNumber(int number, boolean hold, int repeat) { reset(); + digitSequence = String.valueOf(number); if (hold && number < KEY_CHARACTERS.size() && KEY_CHARACTERS.get(number).size() > 0) { suggestions.addAll(KEY_CHARACTERS.get(number)); } else { autoAcceptTimeout = 0; - suggestions.add(String.valueOf(number)); + suggestions.add(digitSequence); } return true; @@ -111,4 +120,11 @@ public class Mode123 extends ModePassthrough { || (text.charAt(0) > 122 && text.charAt(0) < 127) ); } + + + @Override + public void reset() { + super.reset(); + digitSequence = ""; + } } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeABC.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeABC.java index c5bbddfe..5e918326 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeABC.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeABC.java @@ -21,6 +21,7 @@ public class ModeABC extends InputMode { public boolean onNumber(int number, boolean hold, int repeat) { if (hold) { reset(); + digitSequence = String.valueOf(number); suggestions.add(language.getKeyNumber(number)); autoAcceptTimeout = 0; } else if (repeat > 0) { @@ -28,7 +29,9 @@ public class ModeABC extends InputMode { autoAcceptTimeout = settings.getAbcAutoAcceptTimeout(); } else { reset(); + digitSequence = String.valueOf(number); suggestions.addAll(language.getKeyCharacters(number)); + suggestions.add(language.getKeyNumber(number)); autoAcceptTimeout = settings.getAbcAutoAcceptTimeout(); } @@ -40,6 +43,16 @@ public class ModeABC extends InputMode { return newTextCase == CASE_UPPER ? word.toUpperCase(language.getLocale()) : word.toLowerCase(language.getLocale()); } + @Override + protected boolean nextSpecialCharacters() { + if (digitSequence.equals(Language.SPECIAL_CHARS_KEY) && super.nextSpecialCharacters()) { + suggestions.add(language.getKeyNumber(digitSequence.charAt(0) - '0')); + return true; + } + + return false; + } + @Override public void changeLanguage(Language language) { super.changeLanguage(language); @@ -59,6 +72,7 @@ public class ModeABC extends InputMode { @Override public void reset() { super.reset(); + digitSequence = ""; shouldSelectNextLetter = false; } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModePredictive.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModePredictive.java index bec7d81f..4bb7287f 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModePredictive.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModePredictive.java @@ -12,17 +12,20 @@ import io.github.sspanak.tt9.ime.helpers.TextField; import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace; import io.github.sspanak.tt9.ime.modes.helpers.AutoTextCase; import io.github.sspanak.tt9.ime.modes.helpers.Predictions; +import io.github.sspanak.tt9.languages.Characters; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.preferences.SettingsStore; public class ModePredictive extends InputMode { private final String LOG_TAG = getClass().getSimpleName(); + private final static String PREFERRED_CHAR_SEQUENCE = "00"; + private final static String EMOJI_SEQUENCE = "11"; + private final SettingsStore settings; public int getId() { return MODE_PREDICTIVE; } - private String digitSequence = ""; private String lastAcceptedWord = ""; // stem filter @@ -46,9 +49,11 @@ public class ModePredictive extends InputMode { autoSpace = new AutoSpace(settings); autoTextCase = new AutoTextCase(settings); - predictions = new Predictions(settings); + predictions = new Predictions(); this.settings = settings; + + digitSequence = ""; } @@ -205,10 +210,8 @@ public class ModePredictive extends InputMode { /** * loadSuggestions - * Loads the possible list of suggestions for the current digitSequence. - * Returns "false" on invalid sequence. - * - * "currentWord" is used for generating suggestions when there are no results. + * Loads the possible list of suggestions for the current digitSequence. "currentWord" is used + * for generating suggestions when there are no results. * See: Predictions.generatePossibleCompletions() */ @Override @@ -218,6 +221,10 @@ public class ModePredictive extends InputMode { return; } + if (loadStaticSuggestions(onLoad)) { + return; + } + onSuggestionsUpdated = onLoad; predictions .setDigitSequence(digitSequence) @@ -229,6 +236,29 @@ public class ModePredictive extends InputMode { .load(); } + /** + * loadStatic + * Loads words that are not in the database and are supposed to be in the same order, such as + * emoji or the preferred character for double "0". Returns "false", when there are no static + * options for the current digitSequence. + */ + private boolean loadStaticSuggestions(Runnable onLoad) { + if (digitSequence.startsWith(EMOJI_SEQUENCE)) { + digitSequence = digitSequence.substring(0, Math.min(digitSequence.length(), Characters.getEmojiLevels() + 1)); + specialCharSelectedGroup = digitSequence.length() - 2; + super.nextSpecialCharacters(); + onLoad.run(); + return true; + } else if (digitSequence.startsWith(PREFERRED_CHAR_SEQUENCE)) { + suggestions.clear(); + suggestions.add(settings.getDoubleZeroChar()); + onLoad.run(); + return true; + } + + return false; + } + /** * getPredictions @@ -270,7 +300,7 @@ public class ModePredictive extends InputMode { // 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.startsWith("0")) { + if (!sequence.startsWith(Language.PUNCTUATION_KEY) && !sequence.startsWith(Language.SPECIAL_CHARS_KEY)) { WordStoreAsync.makeTopWord(language, currentWord, sequence); } } catch (Exception e) { @@ -284,6 +314,11 @@ public class ModePredictive extends InputMode { return autoTextCase.adjustSuggestionTextCase(language, word, newTextCase); } + @Override + protected boolean nextSpecialCharacters() { + return digitSequence.equals(Language.SPECIAL_CHARS_KEY) && super.nextSpecialCharacters(); + } + @Override public void determineNextWordTextCase(String textBeforeCursor) { textCase = autoTextCase.determineNextWordTextCase(textCase, textFieldTextCase, textBeforeCursor); @@ -296,12 +331,14 @@ public class ModePredictive extends InputMode { } @Override - public void nextTextCase() { - textFieldTextCase = CASE_UNDEFINED; // since it's a user's choice, the default matters no more - super.nextTextCase(); + public boolean nextTextCase() { + boolean changed = super.nextTextCase(); + textFieldTextCase = changed ? CASE_UNDEFINED : textFieldTextCase; // since it's a user's choice, the default matters no more + return changed; } + /** * shouldAcceptPreviousSuggestion * Automatic space assistance. Spaces (and special chars) cause suggestions to be accepted @@ -329,7 +366,7 @@ public class ModePredictive extends InputMode { } // special characters always break words - if (autoAcceptTimeout == 0 && !digitSequence.startsWith("0")) { + if (autoAcceptTimeout == 0 && !digitSequence.startsWith(Language.SPECIAL_CHARS_KEY)) { return true; } @@ -337,14 +374,14 @@ public class ModePredictive extends InputMode { if (language.isHebrew() || language.isUkrainian()) { return predictions.noDbWords() - && digitSequence.equals("1"); + && digitSequence.equals(Language.PUNCTUATION_KEY); } // punctuation breaks words, unless there are database matches ('s, qu', по-, etc...) return !digitSequence.isEmpty() && predictions.noDbWords() - && digitSequence.contains("1") + && digitSequence.contains(Language.PUNCTUATION_KEY) && TextTools.containsOtherThan1(digitSequence); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java index d4e8874e..7e3fff3a 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java @@ -1,17 +1,14 @@ package io.github.sspanak.tt9.ime.modes.helpers; import java.util.ArrayList; -import java.util.regex.Pattern; import io.github.sspanak.tt9.db.WordStoreAsync; import io.github.sspanak.tt9.ime.EmptyDatabaseWarning; -import io.github.sspanak.tt9.languages.Characters; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.preferences.SettingsStore; public class Predictions { private final EmptyDatabaseWarning emptyDbWarning; - private final SettingsStore settings; private Language language; private String digitSequence; @@ -26,22 +23,9 @@ public class Predictions { private boolean areThereDbWords = false; private ArrayList words = new ArrayList<>(); - // punctuation/emoji - private final Pattern containsOnly1Regex = Pattern.compile("^1+$"); - private final String maxEmojiSequence; - - public Predictions(SettingsStore settingsStore) { + public Predictions() { emptyDbWarning = new EmptyDatabaseWarning(); - settings = settingsStore; - - // digitSequence limiter when selecting emoji - // "11" = Emoji level 0, "111" = Emoji level 1,... up to the maximum amount of 1s - StringBuilder maxEmojiSequenceBuilder = new StringBuilder(); - for (int i = 0; i <= Characters.getEmojiLevels(); i++) { - maxEmojiSequenceBuilder.append("1"); - } - maxEmojiSequence = maxEmojiSequenceBuilder.toString(); } @@ -125,54 +109,15 @@ public class Predictions { return; } - if (loadStatic()) { - onWordsChanged.run(); - } else { - WordStoreAsync.getWords( - (words) -> onDbWords(words, true), - language, - digitSequence, - stem, - SettingsStore.SUGGESTIONS_MIN, - SettingsStore.SUGGESTIONS_MAX - ); - } - } + WordStoreAsync.getWords( + (words) -> onDbWords(words, true), + language, + digitSequence, + stem, + SettingsStore.SUGGESTIONS_MIN, + SettingsStore.SUGGESTIONS_MAX + ); - - /** - * loadStatic - * Similar to "load()", but loads words that are not in the database. - * Returns "false", when there are no static options for the current digitSequence. - */ - private boolean loadStatic() { - // whitespace/special/math characters - if (digitSequence.equals("0")) { - stem = ""; - words.clear(); - words.addAll(language.getKeyCharacters(0, false)); - } - // "00" is a shortcut for the preferred character - else if (digitSequence.equals("00")) { - stem = ""; - words.clear(); - words.add(settings.getDoubleZeroChar()); - } - // emoji - else if (containsOnly1Regex.matcher(digitSequence).matches()) { - stem = ""; - words.clear(); - if (digitSequence.length() == 1) { - words.addAll(language.getKeyCharacters(1, false)); - } else { - digitSequence = digitSequence.length() <= maxEmojiSequence.length() ? digitSequence : maxEmojiSequence; - words.addAll(Characters.getEmoji(digitSequence.length() - 2)); - } - } else { - return false; - } - - return true; } private void loadWithoutLeadingPunctuation() { @@ -242,7 +187,7 @@ public class Predictions { // append all letters for the last digit in the sequence (the last pressed key) int lastSequenceDigit = digitSequence.charAt(digitSequence.length() - 1) - '0'; - for (String keyLetter : language.getKeyCharacters(lastSequenceDigit, false)) { + for (String keyLetter : language.getKeyCharacters(lastSequenceDigit)) { generatedWords.add(baseWord + keyLetter); } diff --git a/app/src/main/java/io/github/sspanak/tt9/languages/Characters.java b/app/src/main/java/io/github/sspanak/tt9/languages/Characters.java index 321f98f3..25a1a982 100644 --- a/app/src/main/java/io/github/sspanak/tt9/languages/Characters.java +++ b/app/src/main/java/io/github/sspanak/tt9/languages/Characters.java @@ -27,8 +27,12 @@ public class Characters { ",", ".", "-", "„", "“", "(", ")", "[", "]", "&", "~", "`", "'", ";", ":", "!", "?" )); + final public static ArrayList Currency = new ArrayList<>(Arrays.asList( + "$", "€", "₹", "₿", "₺", "₱", "¥", "₽", "£" + )); + final public static ArrayList Special = new ArrayList<>(Arrays.asList( - " ", "\n", "@", "_", "#", "%", "$", "{", "}", "|", "^", "<", ">", "\\", "/", "=", "*", "+" + " ", "\n", "@", "_", "#", "%", "{", "}", "|", "^", "<", ">", "\\", "/", "=", "*", "+" )); final private static ArrayList TextEmoticons = new ArrayList<>(Arrays.asList( diff --git a/app/src/main/java/io/github/sspanak/tt9/languages/Language.java b/app/src/main/java/io/github/sspanak/tt9/languages/Language.java index e342838e..91a36c45 100644 --- a/app/src/main/java/io/github/sspanak/tt9/languages/Language.java +++ b/app/src/main/java/io/github/sspanak/tt9/languages/Language.java @@ -8,6 +8,9 @@ import java.util.Locale; public class Language { + public static String SPECIAL_CHARS_KEY = "0"; + public static String PUNCTUATION_KEY = "1"; + private int id; protected String name; protected Locale locale; @@ -120,7 +123,7 @@ public class Language { final public String getAbcString() { if (abcString == null) { - ArrayList lettersList = getKeyCharacters(2, false); + ArrayList lettersList = getKeyCharacters(2); abcString = ""; StringBuilder sb = new StringBuilder(); @@ -143,11 +146,11 @@ public class Language { * Returns "true" when the language is based on the Latin alphabet or "false" otherwise. */ public boolean isLatinBased() { - return getKeyCharacters(2, false).contains("a"); + return getKeyCharacters(2).contains("a"); } public boolean isCyrillic() { - return getKeyCharacters(2, false).contains("а"); + return getKeyCharacters(2).contains("а"); } public boolean isRTL() { @@ -155,19 +158,19 @@ public class Language { } public boolean isGreek() { - return getKeyCharacters(2, false).contains("α"); + return getKeyCharacters(2).contains("α"); } public boolean isArabic() { - return getKeyCharacters(3, false).contains("ا"); + return getKeyCharacters(3).contains("ا"); } public boolean isUkrainian() { - return getKeyCharacters(3, false).contains("є"); + return getKeyCharacters(3).contains("є"); } public boolean isHebrew() { - return getKeyCharacters(3, false).contains("א"); + return getKeyCharacters(3).contains("א"); } /* ************ utility ************ */ @@ -240,21 +243,27 @@ public class Language { return word != null && word.toUpperCase(locale).equals(word); } - public ArrayList getKeyCharacters(int key, boolean includeDigit) { + public ArrayList getKeyCharacters(int key, int characterGroup) { if (key < 0 || key >= layout.size()) { return new ArrayList<>(); } ArrayList chars = new ArrayList<>(layout.get(key)); - if (includeDigit && chars.size() > 0) { - chars.add(getKeyNumber(key)); + if (characterGroup > 0) { + if (key == 0 && characterGroup == 1) { + chars = new ArrayList<>(Characters.Currency); + } else if (key == 1) { + chars = new ArrayList<>(Characters.getEmoji(characterGroup - 1)); + } else { + chars = new ArrayList<>(); + } } return chars; } public ArrayList getKeyCharacters(int key) { - return getKeyCharacters(key, true); + return getKeyCharacters(key, 0); } public String getKeyNumber(int key) { @@ -285,6 +294,7 @@ public class Language { return sequence.toString(); } + @NonNull @Override public String toString() { diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftNumberKey.java b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftNumberKey.java index b10ede2b..77343dbb 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftNumberKey.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftNumberKey.java @@ -108,7 +108,7 @@ public class SoftNumberKey extends SoftKey { boolean isGreekBased = language.isGreek(); StringBuilder sb = new StringBuilder(); - ArrayList chars = language.getKeyCharacters(number, false); + ArrayList chars = language.getKeyCharacters(number); for (int i = 0; i < 5 && i < chars.size(); i++) { String currentLetter = chars.get(i); if ( diff --git a/docs/user-manual.md b/docs/user-manual.md index 9b407f36..48a77d84 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -73,13 +73,16 @@ _**Note3:** Facebook Messenger supports sending messages with the keypad only on - **In 123 mode:** - **Press:** type "0". - **Hold:** type special/math characters. + - **Hold "0", then Press "Next Mode" (Default: Hold "0", Press "#"):** type currency characters - **In ABC mode:** - **Press:** type space, newline or special/math characters. - **Hold:** type "0". + - **Press "0", then Press "Next Mode" (Default: Press "0", "#"):** type currency characters - **In Predictive mode:** - **Press:** type space, newline or special/math characters. - **Double Press:** type the character assigned in Predictive mode settings. (Default: ".") - **Hold:** type "0". + - **Press "0", then Press "Next Mode" (Default: Press "0", "#"):** type currency characters #### 1- to 9-key: - **In 123 mode:** type the respective number or hold to type punctuation.