From 0e9954864ec1ae60ca0567d3ac7b573880f62f74 Mon Sep 17 00:00:00 2001 From: sspanak Date: Fri, 16 Feb 2024 12:11:48 +0200 Subject: [PATCH] emoji can now be added as custom words --- .../sspanak/tt9/db/DictionaryLoader.java | 3 ++ .../io/github/sspanak/tt9/db/WordStore.java | 4 ++ .../sspanak/tt9/db/sqlite/SQLiteOpener.java | 4 +- .../sspanak/tt9/ime/helpers/TextField.java | 8 ++-- .../sspanak/tt9/ime/modes/InputMode.java | 9 ++-- .../sspanak/tt9/ime/modes/ModePredictive.java | 48 +++++++++++++------ .../tt9/ime/modes/helpers/Predictions.java | 21 ++++---- .../sspanak/tt9/languages/Characters.java | 27 ++++++++--- .../sspanak/tt9/languages/EmojiLanguage.java | 26 ++++++++++ .../sspanak/tt9/languages/Language.java | 22 ++++----- docs/user-manual.md | 25 ++++++++-- 11 files changed, 143 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/io/github/sspanak/tt9/languages/EmojiLanguage.java 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 968bd520..a7b10808 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 @@ -21,6 +21,7 @@ import io.github.sspanak.tt9.db.sqlite.InsertOps; import io.github.sspanak.tt9.db.sqlite.SQLiteOpener; import io.github.sspanak.tt9.db.sqlite.Tables; import io.github.sspanak.tt9.ime.TraditionalT9; +import io.github.sspanak.tt9.languages.EmojiLanguage; import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException; import io.github.sspanak.tt9.languages.InvalidLanguageException; import io.github.sspanak.tt9.languages.Language; @@ -170,6 +171,7 @@ public class DictionaryLoader { start = System.currentTimeMillis(); DeleteOps.delete(sqlite, language.getId()); + DeleteOps.delete(sqlite, new EmojiLanguage().getId()); sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME); logLoadingStep("Storage cleared", language, start); @@ -180,6 +182,7 @@ public class DictionaryLoader { start = System.currentTimeMillis(); InsertOps.restoreCustomWords(sqlite.getDb(), language); + InsertOps.restoreCustomWords(sqlite.getDb(), new EmojiLanguage()); sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME); logLoadingStep("Custom words restored", language, start); diff --git a/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java b/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java index 955f9ccc..93cb2789 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java @@ -15,6 +15,8 @@ import io.github.sspanak.tt9.db.sqlite.ReadOps; import io.github.sspanak.tt9.db.sqlite.SQLiteOpener; import io.github.sspanak.tt9.db.sqlite.UpdateOps; import io.github.sspanak.tt9.ime.TraditionalT9; +import io.github.sspanak.tt9.languages.Characters; +import io.github.sspanak.tt9.languages.EmojiLanguage; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.Text; import io.github.sspanak.tt9.preferences.SettingsStore; @@ -136,6 +138,8 @@ public class WordStore { return AddWordDialog.CODE_GENERAL_ERROR; } + language = Characters.isGraphic(word) ? new EmojiLanguage() : language; + try { if (readOps.exists(sqlite.getDb(), language, word)) { return AddWordDialog.CODE_WORD_EXISTS; diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java index 4bb8e185..f725d41e 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import io.github.sspanak.tt9.BuildConfig; import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.languages.EmojiLanguage; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; @@ -23,7 +24,8 @@ public class SQLiteOpener extends SQLiteOpenHelper { public SQLiteOpener(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); - allLanguages = LanguageCollection.getAll(context); + allLanguages = new ArrayList<>(LanguageCollection.getAll(context)); + allLanguages.add(new EmojiLanguage()); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java index cc388802..b269a358 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java @@ -24,10 +24,10 @@ import io.github.sspanak.tt9.languages.Text; public class TextField { public static final int IME_ACTION_ENTER = EditorInfo.IME_MASK_ACTION + 1; - private static final Pattern beforeCursorWordRegex = Pattern.compile("(\\w+)(?!\n)$"); - private static final Pattern afterCursorWordRegex = Pattern.compile("^(? chars = language.getKeyCharacters(key, ++specialCharSelectedGroup); + ArrayList chars = altLanguage.getKeyCharacters(key, ++specialCharSelectedGroup); if (chars.isEmpty() && specialCharSelectedGroup == 1) { specialCharSelectedGroup = 0; return false; } else if (chars.isEmpty()) { specialCharSelectedGroup = 0; - chars = language.getKeyCharacters(key, specialCharSelectedGroup); + chars = altLanguage.getKeyCharacters(key, specialCharSelectedGroup); } suggestions.clear(); 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 2e0db091..b7694c6f 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 @@ -5,22 +5,21 @@ import androidx.annotation.NonNull; import java.util.ArrayList; import io.github.sspanak.tt9.Logger; -import io.github.sspanak.tt9.languages.Text; import io.github.sspanak.tt9.db.WordStoreAsync; import io.github.sspanak.tt9.ime.helpers.InputType; 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.EmojiLanguage; import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.languages.Text; 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; @@ -66,6 +65,11 @@ public class ModePredictive extends InputMode { return false; } + if (digitSequence.startsWith(EmojiLanguage.EMOJI_SEQUENCE) && specialCharSelectedGroup > 0) { + specialCharSelectedGroup -= 2; + return true; + } + digitSequence = digitSequence.substring(0, digitSequence.length() - 1); if (digitSequence.length() == 0) { clearWordStem(); @@ -88,13 +92,17 @@ public class ModePredictive extends InputMode { disablePredictions = true; suggestions.add(language.getKeyNumber(number)); } else { - // words super.reset(); - disablePredictions = false; digitSequence += number; - if (number == 0 && repeat > 0) { + disablePredictions = false; + + if (digitSequence.equals(Language.PREFERRED_CHAR_SEQUENCE)) { autoAcceptTimeout = 0; } + + // custom emoji are longest 1-key sequence, so do not allow typing any longer than that, + // to prevent side effects + digitSequence = digitSequence.startsWith(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE) ? EmojiLanguage.CUSTOM_EMOJI_SEQUENCE : digitSequence; } return true; @@ -225,12 +233,14 @@ public class ModePredictive extends InputMode { return; } + Language searchLanguage = digitSequence.startsWith(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE) ? new EmojiLanguage() : language; + onSuggestionsUpdated = onLoad; predictions .setDigitSequence(digitSequence) .setIsStemFuzzy(isStemFuzzy) .setStem(stem) - .setLanguage(language) + .setLanguage(searchLanguage) .setInputWord(currentWord) .setWordsChangedHandler(this::getPredictions) .load(); @@ -243,13 +253,16 @@ public class ModePredictive extends InputMode { * 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; + if (digitSequence.equals(Language.PUNCTUATION_KEY)) { super.nextSpecialCharacters(); onLoad.run(); return true; - } else if (digitSequence.startsWith(PREFERRED_CHAR_SEQUENCE)) { + } else if (digitSequence.equals(EmojiLanguage.EMOJI_SEQUENCE)) { + specialCharSelectedGroup = -1; + nextSpecialCharacters(new EmojiLanguage()); + onLoad.run(); + return true; + } else if (digitSequence.startsWith(Language.PREFERRED_CHAR_SEQUENCE)) { suggestions.clear(); suggestions.add(settings.getDoubleZeroChar()); onLoad.run(); @@ -265,7 +278,12 @@ public class ModePredictive extends InputMode { * Gets the currently available Predictions and sends them over to the external caller. */ private void getPredictions() { - digitSequence = predictions.getDigitSequence(); + // in case the user hasn't added any custom emoji, do not allow advancing to the empty character group + if (predictions.getList().isEmpty() && digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE)) { + digitSequence = EmojiLanguage.EMOJI_SEQUENCE; + return; + } + suggestions.clear(); suggestions.addAll(predictions.getList()); @@ -327,7 +345,9 @@ public class ModePredictive extends InputMode { @Override protected boolean nextSpecialCharacters() { - return digitSequence.equals(Language.SPECIAL_CHARS_KEY) && super.nextSpecialCharacters(); + return + (digitSequence.equals(EmojiLanguage.EMOJI_SEQUENCE) && super.nextSpecialCharacters(new EmojiLanguage())) + || (digitSequence.equals(Language.SPECIAL_CHARS_KEY) && super.nextSpecialCharacters()); } @Override 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 34e4ffaa..87fb52ff 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 @@ -3,6 +3,7 @@ package io.github.sspanak.tt9.ime.modes.helpers; import java.util.ArrayList; import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.languages.EmojiLanguage; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.preferences.SettingsStore; @@ -36,10 +37,6 @@ public class Predictions { return this; } - public String getDigitSequence() { - return digitSequence; - } - public Predictions setIsStemFuzzy(boolean yes) { this.isStemFuzzy = yes; return this; @@ -106,8 +103,10 @@ public class Predictions { return; } + boolean retryAllowed = !digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE); + WordStoreAsync.getWords( - (words) -> onDbWords(words, true), + (words) -> onDbWords(words, retryAllowed), language, digitSequence, stem, @@ -153,10 +152,14 @@ public class Predictions { } words.clear(); - suggestStem(); - suggestMissingWords(generatePossibleStemVariations(dbWords)); - suggestMissingWords(dbWords.isEmpty() ? generateWordVariations(inputWord) : dbWords); - words = insertPunctuationCompletions(words); + if (digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE)) { + words.addAll(dbWords); + } else { + suggestStem(); + suggestMissingWords(generatePossibleStemVariations(dbWords)); + suggestMissingWords(dbWords.isEmpty() ? generateWordVariations(inputWord) : dbWords); + words = insertPunctuationCompletions(words); + } onWordsChanged.run(); } 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 b9538ff2..60898b9e 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 @@ -28,7 +28,7 @@ public class Characters { )); final public static ArrayList Currency = new ArrayList<>(Arrays.asList( - "$", "€", "₹", "₿", "₩", "¢", "₺", "₱", "¥", "₽", "£" + "$", "€", "₹", "₿", "₩", "¢", "¤", "₺", "₱", "¥", "₽", "£" )); final public static ArrayList Special = new ArrayList<>(Arrays.asList( @@ -58,13 +58,24 @@ public class Characters { )) )); - public static boolean noEmojiSupported() { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M; + public static boolean isGraphic(String str) { + if (str == null) { + return false; + } + + for (int i = 0, end = str.length(); i < end; i++) { + char ch = str.charAt(i); + + if (ch < 256 || Character.isLetterOrDigit(ch) || Character.isAlphabetic(ch)) { + return false; + } + } + + return true; } - - public static int getEmojiLevels() { - return noEmojiSupported() ? 1 : Emoji.size(); + public static boolean noEmojiSupported() { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M; } @@ -73,7 +84,9 @@ public class Characters { return new ArrayList<>(TextEmoticons); } - level = (Emoji.size() > level) ? level : Emoji.size() - 1; + if (level < 0 || level >= Emoji.size()) { + return new ArrayList<>(); + } Paint paint = new Paint(); ArrayList availableEmoji = new ArrayList<>(); diff --git a/app/src/main/java/io/github/sspanak/tt9/languages/EmojiLanguage.java b/app/src/main/java/io/github/sspanak/tt9/languages/EmojiLanguage.java new file mode 100644 index 00000000..f3c7b93e --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/languages/EmojiLanguage.java @@ -0,0 +1,26 @@ +package io.github.sspanak.tt9.languages; + +import java.util.ArrayList; +import java.util.Locale; + +public class EmojiLanguage extends Language { + final public static String EMOJI_SEQUENCE = "11"; + final public static String CUSTOM_EMOJI_SEQUENCE = EMOJI_SEQUENCE + "1"; + + public EmojiLanguage() { + id = Integer.parseInt(EMOJI_SEQUENCE); + locale = Locale.ROOT; + abcString = "emoji"; + name = "Emoji"; + } + + @Override + public String getDigitSequenceForWord(String word) { + return Characters.isGraphic(word) ? CUSTOM_EMOJI_SEQUENCE : null; + } + + @Override + public ArrayList getKeyCharacters(int key, int characterGroup) { + return key == 1 && characterGroup >= 0 ? new ArrayList<>(Characters.getEmoji(characterGroup)) : super.getKeyCharacters(key, characterGroup); + } +} 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 7fdbafb8..def4fa69 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,10 +8,11 @@ import java.util.Locale; public class Language implements Comparable { - public static String SPECIAL_CHARS_KEY = "0"; - public static String PUNCTUATION_KEY = "1"; + final public static String SPECIAL_CHARS_KEY = "0"; + final public static String PUNCTUATION_KEY = "1"; + final public static String PREFERRED_CHAR_SEQUENCE = "00"; - private int id; + protected int id; protected String name; protected Locale locale; protected String dictionaryFile; @@ -193,7 +194,8 @@ public class Language implements Comparable { * -> 2 | 224 | 2048 | 229376 (shift each 5-bit number, not overlap with the previous ones) * -> 231650 * - * Maximum ID is: "zz-ZZ" -> 879450 + * Minimum ID: "aa" -> 33 + * Maximum ID: "zz-ZZ" -> 879450 */ private int generateId() { String idString = (locale.getLanguage() + locale.getCountry()).toUpperCase(); @@ -219,14 +221,12 @@ public class Language implements Comparable { return new ArrayList<>(); } - ArrayList chars = new ArrayList<>(layout.get(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 { + ArrayList chars = layout.get(key); + if (key == 0) { + if (characterGroup > 1) { chars = new ArrayList<>(); + } else if (characterGroup == 1) { + chars = new ArrayList<>(Characters.Currency); } } diff --git a/docs/user-manual.md b/docs/user-manual.md index 35158e98..4729fc31 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -77,15 +77,32 @@ _**Note2:** In messaging applications, you need to enable their "Send with ENTER - **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 + - **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. -- **In ABC and Predictive mode:** type a letter/punctuation character or hold to type the respective number. +#### 1-key: +- **In 123 mode:** + - **Press:** type "1". + - **Hold:** type sentence characters +- **In ABC mode:** + - **Press:** type sentence characters + - **Hold:** type "1". +- **In Predictive mode:** + - **Press:** type sentence characters + - **Double Press:** type emoji + - **Triple Press:** type custom added emoji (you must have added some using [the Add Word key](#add-word-key-default-press-)) + - **Hold:** type "1". + - **Double Press "1", then press "Next Mode" (Default: Press "1", "1", then keep pressing "#"):** select between the predefined emoji groups + + +#### 2- to 9-key: +- **In 123 mode:** type the respective number +- **In ABC and Predictive mode:** type a letter character or hold to type the respective number. #### Add Word Key (Default: Press ✱): Add a new word to the dictionary for the current language. +You can also add new emoji. Then access them by pressing 1-1-# (change "#" with the "Next Mode" key, if you have changed the default). Regardless of the selected language at the time of adding, all emoji will be available in all languages. + #### Backspace Key: Just deletes text.