From 6cc2e7402b0db87de2cecfa6c0dbc42b92cd33e8 Mon Sep 17 00:00:00 2001 From: Dimo Karaivanov Date: Mon, 8 Jan 2024 15:48:47 +0200 Subject: [PATCH] small fixes 27 (#404) * fix deleting words with apostrophes and filtering in general * fixed typing non-existing Ukrainian and Hebrew words not working after filtering is applied * fixed the suggestions sometimes appearing in the wrong order, when some of them are generated * fixed lowercase being incorrectly forced sometimes * fixed attempting to add a word while still typing it, and the suggestions are visible, causing the word to be erased --- .../github/sspanak/tt9/ime/TraditionalT9.java | 8 +- .../sspanak/tt9/ime/helpers/TextField.java | 4 +- .../sspanak/tt9/ime/modes/InputMode.java | 2 +- .../sspanak/tt9/ime/modes/ModePredictive.java | 81 ++++++++++--------- .../tt9/ime/modes/helpers/AutoTextCase.java | 4 +- .../tt9/ime/modes/helpers/Predictions.java | 25 +++--- .../sspanak/tt9/languages/Language.java | 32 ++++---- 7 files changed, 77 insertions(+), 79 deletions(-) diff --git a/src/io/github/sspanak/tt9/ime/TraditionalT9.java b/src/io/github/sspanak/tt9/ime/TraditionalT9.java index 116e5009..e4dad23e 100644 --- a/src/io/github/sspanak/tt9/ime/TraditionalT9.java +++ b/src/io/github/sspanak/tt9/ime/TraditionalT9.java @@ -357,7 +357,7 @@ public class TraditionalT9 extends KeyPadHandler { } cancelAutoAccept(); - clearSuggestions(); + acceptIncompleteSuggestion(); String word = textField.getSurroundingWord(mLanguage); if (word.isEmpty()) { @@ -421,10 +421,10 @@ public class TraditionalT9 extends KeyPadHandler { filter = getComposingText(); } - if (mInputMode.setWordStem(filter, repeat)) { - mInputMode.loadSuggestions(this::getSuggestions, filter); - } else if (filter.length() == 0) { + if (filter.isEmpty()) { mInputMode.reset(); + } else if (mInputMode.setWordStem(filter, repeat)) { + mInputMode.loadSuggestions(this::getSuggestions, filter); } return true; diff --git a/src/io/github/sspanak/tt9/ime/helpers/TextField.java b/src/io/github/sspanak/tt9/ime/helpers/TextField.java index 6ea89a7f..f24ad48a 100644 --- a/src/io/github/sspanak/tt9/ime/helpers/TextField.java +++ b/src/io/github/sspanak/tt9/ime/helpers/TextField.java @@ -185,8 +185,8 @@ public class TextField { Matcher before; Matcher after; - if (language != null && language.isUkrainian()) { - // Ukrainian uses apostrophes as letters + if (language != null && (language.isHebrew() || language.isUkrainian())) { + // Hebrew and Ukrainian use apostrophes as letters before = beforeCursorUkrainianRegex.matcher(getTextBeforeCursor()); after = afterCursorUkrainianRegex.matcher(getTextAfterCursor()); } else { diff --git a/src/io/github/sspanak/tt9/ime/modes/InputMode.java b/src/io/github/sspanak/tt9/ime/modes/InputMode.java index fa631ca4..62fd18ec 100644 --- a/src/io/github/sspanak/tt9/ime/modes/InputMode.java +++ b/src/io/github/sspanak/tt9/ime/modes/InputMode.java @@ -138,7 +138,7 @@ abstract public class InputMode { // Stem filtering. // Where applicable, return "true" if the mode supports it and the operation was possible. - public boolean clearWordStem() { return false; } + public boolean clearWordStem() { return setWordStem("", true); } public boolean isStemFilterFuzzy() { return false; } public String getWordStem() { return ""; } public boolean setWordStem(String stem, boolean exact) { 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 3e59c015..1aa108db 100644 --- a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java +++ b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java @@ -16,6 +16,8 @@ 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 SettingsStore settings; public int getId() { return MODE_PREDICTIVE; } @@ -35,6 +37,7 @@ public class ModePredictive extends InputMode { private final AutoSpace autoSpace; private final AutoTextCase autoTextCase; private final Predictions predictions; + private boolean isCursorDirectionForward = false; ModePredictive(SettingsStore settings, Language lang) { @@ -51,6 +54,8 @@ public class ModePredictive extends InputMode { @Override public boolean onBackspace() { + isCursorDirectionForward = false; + if (digitSequence.length() < 1) { clearWordStem(); return false; @@ -60,7 +65,7 @@ public class ModePredictive extends InputMode { if (digitSequence.length() == 0) { clearWordStem(); } else if (stem.length() > digitSequence.length()) { - stem = stem.substring(0, digitSequence.length() - 1); + stem = stem.substring(0, digitSequence.length()); } return true; @@ -69,6 +74,8 @@ public class ModePredictive extends InputMode { @Override public boolean onNumber(int number, boolean hold, int repeat) { + isCursorDirectionForward = true; + if (hold) { // hold to type any digit reset(); @@ -136,23 +143,6 @@ public class ModePredictive extends InputMode { } - /** - * clearWordStem - * Do not filter the suggestions by the word set using "setWordStem()", use only the digit sequence. - */ - @Override - public boolean clearWordStem() { - if (stem.length() == 0) { - return false; - } - - stem = ""; - Logger.d("setWordStem", "Stem filter cleared"); - - return true; - } - - /** * setWordStem * Filter the possible suggestions by the given stem. @@ -165,30 +155,30 @@ public class ModePredictive extends InputMode { * added to the suggestions list, even if they make no sense. * For example: "exac_" -> "exac", "exact", "exacu", "exacv", {database suggestions...} * - * * Note that you need to manually get the suggestions again to obtain a filtered list. */ @Override public boolean setWordStem(String newStem, boolean exact) { - String sanitizedStem = TextTools.removeNonLetters(newStem); - if (language == null || sanitizedStem == null || sanitizedStem.length() < 1) { - return false; + if (newStem == null || newStem.isEmpty()) { + isStemFuzzy = false; + stem = ""; + + Logger.d(LOG_TAG, "Stem filter cleared"); + return true; } try { - // digitSequence = "the raw input", so that everything the user typed is preserved visually - // stem = "the sanitized input", because filtering by anything that is not a letter makes no sense digitSequence = language.getDigitSequenceForWord(newStem); - stem = sanitizedStem.toLowerCase(language.getLocale()); isStemFuzzy = !exact; + stem = newStem.toLowerCase(language.getLocale()); - Logger.d("setWordStem", "Stem is now: " + stem + (isStemFuzzy ? " (fuzzy)" : "")); + Logger.d(LOG_TAG, "Stem is now: " + stem + (isStemFuzzy ? " (fuzzy)" : "")); return true; } catch (Exception e) { isStemFuzzy = false; stem = ""; - Logger.w("setWordStem", "Ignoring invalid stem: " + newStem + ". " + e.getMessage()); + Logger.w("setWordStem", "Ignoring invalid stem: " + newStem + " in language: " + language + ". " + e.getMessage()); return false; } } @@ -270,7 +260,7 @@ public class ModePredictive extends InputMode { stem = ""; if (currentWord.isEmpty()) { - Logger.i("acceptCurrentSuggestion", "Current word is empty. Nothing to accept."); + Logger.i(LOG_TAG, "Current word is empty. Nothing to accept."); return; } @@ -284,7 +274,7 @@ public class ModePredictive extends InputMode { DictionaryDb.incrementWordFrequency(language, currentWord, sequence); } } catch (Exception e) { - Logger.e("ModePredictive", "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage()); + Logger.e(LOG_TAG, "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage()); } } @@ -314,8 +304,8 @@ public class ModePredictive extends InputMode { /** * shouldAcceptPreviousSuggestion - * In this mode, In addition to confirming the suggestion in the input field, - * we also increase its' priority. This function determines whether we want to do all this or not. + * Automatic space assistance. Spaces (and special chars) cause suggestions to be accepted + * automatically. This is used for analysis before processing the incoming pressed key. */ @Override public boolean shouldAcceptPreviousSuggestion(int nextKey) { @@ -333,14 +323,29 @@ public class ModePredictive extends InputMode { */ @Override public boolean shouldAcceptPreviousSuggestion() { + // backspace never breaks words + if (!isCursorDirectionForward) { + return false; + } + + // special characters always break words + if (autoAcceptTimeout == 0 && !digitSequence.startsWith("0")) { + return true; + } + + // allow apostrophes in the middle or at the end of Hebrew and Ukrainian words + if (language.isHebrew() || language.isUkrainian()) { + return + predictions.noDbWords() + && digitSequence.equals("1"); + } + + // punctuation breaks words, unless there are database matches ('s, qu', по-, etc...) return - (autoAcceptTimeout == 0 && !digitSequence.startsWith("0")) - || ( - !digitSequence.isEmpty() - && !predictions.areThereDbWords() - && digitSequence.contains("1") - && TextTools.containsOtherThan1(digitSequence) - ); + !digitSequence.isEmpty() + && predictions.noDbWords() + && digitSequence.contains("1") + && TextTools.containsOtherThan1(digitSequence); } diff --git a/src/io/github/sspanak/tt9/ime/modes/helpers/AutoTextCase.java b/src/io/github/sspanak/tt9/ime/modes/helpers/AutoTextCase.java index 80ef3128..60bfffbe 100644 --- a/src/io/github/sspanak/tt9/ime/modes/helpers/AutoTextCase.java +++ b/src/io/github/sspanak/tt9/ime/modes/helpers/AutoTextCase.java @@ -29,9 +29,7 @@ public class AutoTextCase { case InputMode.CASE_LOWER: return word.toLowerCase(language.getLocale()); case InputMode.CASE_CAPITALIZE: - return language.isMixedCaseWord(word) ? word : language.capitalize(word); - case InputMode.CASE_DICTIONARY: - return language.isMixedCaseWord(word) ? word : word.toLowerCase(language.getLocale()); + return language.isMixedCaseWord(word) || language.isUpperCaseWord(word) ? word : language.capitalize(word); default: return word; } diff --git a/src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java b/src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java index 97a920af..f268d322 100644 --- a/src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java +++ b/src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java @@ -24,7 +24,7 @@ public class Predictions { // data private boolean areThereDbWords = false; - private final ArrayList words = new ArrayList<>(); + private ArrayList words = new ArrayList<>(); // punctuation/emoji private final Pattern containsOnly1Regex = Pattern.compile("^1+$"); @@ -83,8 +83,8 @@ public class Predictions { return words; } - public boolean areThereDbWords() { - return areThereDbWords; + public boolean noDbWords() { + return !areThereDbWords; } @@ -212,20 +212,20 @@ public class Predictions { if (dbWords.isEmpty() && !digitSequence.isEmpty()) { emptyDbWarning.emitOnce(language); - dbWords = generatePossibleCompletions(inputWord); } words.clear(); suggestStem(); suggestMissingWords(generatePossibleStemVariations(dbWords)); - suggestMissingWords(insertPunctuationCompletions(dbWords)); + suggestMissingWords(dbWords.isEmpty() ? generateWordVariations(inputWord) : dbWords); + words = insertPunctuationCompletions(words); onWordsChanged.run(); } /** - * generatePossibleCompletions + * generateWordVariations * When there are no matching suggestions after the last key press, generate a list of possible * ones, so that the user can complete a missing word that is completely different from the ones * in the dictionary. @@ -233,7 +233,7 @@ public class Predictions { * For example, if the word is "missin_" and the last pressed key is "4", the results would be: * | missing | missinh | missini | */ - private ArrayList generatePossibleCompletions(String baseWord) { + private ArrayList generateWordVariations(String baseWord) { ArrayList generatedWords = new ArrayList<>(); // Make sure the displayed word and the digit sequence, we will be generating suggestions from, @@ -257,7 +257,7 @@ public class Predictions { /** * insertPunctuationCompletions - * When given: "you'", for example, this also generates all other 1-key alternatives, like: + * When given: "you'", for example, this inserts all other 1-key alternatives, like: * "you.", "you?", "you!" and so on. The generated words will be inserted after the direct * database matches and before the fuzzy matches, as if they were direct matches with low frequency. * This is to preserve the sorting by length and frequency. @@ -278,7 +278,7 @@ public class Predictions { } // generated "exact matches" - for (String w : generatePossibleCompletions(dbWords.get(0))) { + for (String w : generateWordVariations(dbWords.get(0))) { if (!dbWords.contains(w) && !dbWords.contains(w.toLowerCase(language.getLocale()))) { complementedWords.add(w); } @@ -310,12 +310,9 @@ public class Predictions { */ private ArrayList generatePossibleStemVariations(ArrayList dbWords) { ArrayList variations = new ArrayList<>(); - if (stem.isEmpty()) { - return variations; - } - if (isStemFuzzy && stem.length() == digitSequence.length() - 1) { - ArrayList allPossibleVariations = generatePossibleCompletions(stem); + if (isStemFuzzy && !stem.isEmpty() && stem.length() == digitSequence.length() - 1) { + ArrayList allPossibleVariations = generateWordVariations(stem); // first add the known words, because it makes more sense to see them first for (String variation : allPossibleVariations) { diff --git a/src/io/github/sspanak/tt9/languages/Language.java b/src/io/github/sspanak/tt9/languages/Language.java index 3736b6ab..3ed1a18d 100644 --- a/src/io/github/sspanak/tt9/languages/Language.java +++ b/src/io/github/sspanak/tt9/languages/Language.java @@ -150,26 +150,21 @@ public class Language { /** * isLatinBased * Returns "true" when the language is based on the Latin alphabet or "false" otherwise. - * WARNING: This performs somewhat resource-intensive operations every time, so consider - * caching the result. */ public boolean isLatinBased() { - ArrayList letters = getKeyCharacters(2, false); - return letters.contains("a"); + return getKeyCharacters(2, false).contains("a"); } - /** - * isGreek - * Similar to "isLatinBased()", this returns "true" when the language is based on the Greek alphabet. - */ public boolean isGreek() { - ArrayList letters = getKeyCharacters(2, false); - return letters.contains("α"); + return getKeyCharacters(2, false).contains("α"); } public boolean isUkrainian() { - ArrayList letters = getKeyCharacters(4, false); - return letters.contains("ї"); + return getKeyCharacters(3, false).contains("є"); + } + + public boolean isHebrew() { + return getKeyCharacters(3, false).contains("א"); } /* ************ utility ************ */ @@ -230,11 +225,14 @@ public class Language { } public boolean isMixedCaseWord(String word) { - return word != null - && ( - (word.length() == 1 && word.toUpperCase(locale).equals(word)) - || (!word.toLowerCase(locale).equals(word) && !word.toUpperCase(locale).equals(word)) - ); + return + word != null + && !word.toLowerCase(locale).equals(word) + && !word.toUpperCase(locale).equals(word); + } + + public boolean isUpperCaseWord(String word) { + return word != null && word.toUpperCase(locale).equals(word); } public ArrayList getKeyCharacters(int key, boolean includeDigit) {