1
0
Fork 0

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
This commit is contained in:
Dimo Karaivanov 2024-01-08 15:48:47 +02:00 committed by GitHub
parent 26afb4d460
commit 6cc2e7402b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 79 deletions

View file

@ -357,7 +357,7 @@ public class TraditionalT9 extends KeyPadHandler {
} }
cancelAutoAccept(); cancelAutoAccept();
clearSuggestions(); acceptIncompleteSuggestion();
String word = textField.getSurroundingWord(mLanguage); String word = textField.getSurroundingWord(mLanguage);
if (word.isEmpty()) { if (word.isEmpty()) {
@ -421,10 +421,10 @@ public class TraditionalT9 extends KeyPadHandler {
filter = getComposingText(); filter = getComposingText();
} }
if (mInputMode.setWordStem(filter, repeat)) { if (filter.isEmpty()) {
mInputMode.loadSuggestions(this::getSuggestions, filter);
} else if (filter.length() == 0) {
mInputMode.reset(); mInputMode.reset();
} else if (mInputMode.setWordStem(filter, repeat)) {
mInputMode.loadSuggestions(this::getSuggestions, filter);
} }
return true; return true;

View file

@ -185,8 +185,8 @@ public class TextField {
Matcher before; Matcher before;
Matcher after; Matcher after;
if (language != null && language.isUkrainian()) { if (language != null && (language.isHebrew() || language.isUkrainian())) {
// Ukrainian uses apostrophes as letters // Hebrew and Ukrainian use apostrophes as letters
before = beforeCursorUkrainianRegex.matcher(getTextBeforeCursor()); before = beforeCursorUkrainianRegex.matcher(getTextBeforeCursor());
after = afterCursorUkrainianRegex.matcher(getTextAfterCursor()); after = afterCursorUkrainianRegex.matcher(getTextAfterCursor());
} else { } else {

View file

@ -138,7 +138,7 @@ abstract public class InputMode {
// Stem filtering. // Stem filtering.
// Where applicable, return "true" if the mode supports it and the operation was possible. // 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 boolean isStemFilterFuzzy() { return false; }
public String getWordStem() { return ""; } public String getWordStem() { return ""; }
public boolean setWordStem(String stem, boolean exact) { return false; } public boolean setWordStem(String stem, boolean exact) { return false; }

View file

@ -16,6 +16,8 @@ import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.SettingsStore; import io.github.sspanak.tt9.preferences.SettingsStore;
public class ModePredictive extends InputMode { public class ModePredictive extends InputMode {
private final String LOG_TAG = getClass().getSimpleName();
private final SettingsStore settings; private final SettingsStore settings;
public int getId() { return MODE_PREDICTIVE; } public int getId() { return MODE_PREDICTIVE; }
@ -35,6 +37,7 @@ public class ModePredictive extends InputMode {
private final AutoSpace autoSpace; private final AutoSpace autoSpace;
private final AutoTextCase autoTextCase; private final AutoTextCase autoTextCase;
private final Predictions predictions; private final Predictions predictions;
private boolean isCursorDirectionForward = false;
ModePredictive(SettingsStore settings, Language lang) { ModePredictive(SettingsStore settings, Language lang) {
@ -51,6 +54,8 @@ public class ModePredictive extends InputMode {
@Override @Override
public boolean onBackspace() { public boolean onBackspace() {
isCursorDirectionForward = false;
if (digitSequence.length() < 1) { if (digitSequence.length() < 1) {
clearWordStem(); clearWordStem();
return false; return false;
@ -60,7 +65,7 @@ public class ModePredictive extends InputMode {
if (digitSequence.length() == 0) { if (digitSequence.length() == 0) {
clearWordStem(); clearWordStem();
} else if (stem.length() > digitSequence.length()) { } else if (stem.length() > digitSequence.length()) {
stem = stem.substring(0, digitSequence.length() - 1); stem = stem.substring(0, digitSequence.length());
} }
return true; return true;
@ -69,6 +74,8 @@ public class ModePredictive extends InputMode {
@Override @Override
public boolean onNumber(int number, boolean hold, int repeat) { public boolean onNumber(int number, boolean hold, int repeat) {
isCursorDirectionForward = true;
if (hold) { if (hold) {
// hold to type any digit // hold to type any digit
reset(); 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 * setWordStem
* Filter the possible suggestions by the given stem. * 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. * added to the suggestions list, even if they make no sense.
* For example: "exac_" -> "exac", "exact", "exacu", "exacv", {database suggestions...} * For example: "exac_" -> "exac", "exact", "exacu", "exacv", {database suggestions...}
* *
*
* Note that you need to manually get the suggestions again to obtain a filtered list. * Note that you need to manually get the suggestions again to obtain a filtered list.
*/ */
@Override @Override
public boolean setWordStem(String newStem, boolean exact) { public boolean setWordStem(String newStem, boolean exact) {
String sanitizedStem = TextTools.removeNonLetters(newStem); if (newStem == null || newStem.isEmpty()) {
if (language == null || sanitizedStem == null || sanitizedStem.length() < 1) { isStemFuzzy = false;
return false; stem = "";
Logger.d(LOG_TAG, "Stem filter cleared");
return true;
} }
try { 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); digitSequence = language.getDigitSequenceForWord(newStem);
stem = sanitizedStem.toLowerCase(language.getLocale());
isStemFuzzy = !exact; 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; return true;
} catch (Exception e) { } catch (Exception e) {
isStemFuzzy = false; isStemFuzzy = false;
stem = ""; stem = "";
Logger.w("setWordStem", "Ignoring invalid stem: " + newStem + ". " + e.getMessage()); Logger.w("setWordStem", "Ignoring invalid stem: " + newStem + " in language: " + language + ". " + e.getMessage());
return false; return false;
} }
} }
@ -270,7 +260,7 @@ public class ModePredictive extends InputMode {
stem = ""; stem = "";
if (currentWord.isEmpty()) { 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; return;
} }
@ -284,7 +274,7 @@ public class ModePredictive extends InputMode {
DictionaryDb.incrementWordFrequency(language, currentWord, sequence); DictionaryDb.incrementWordFrequency(language, currentWord, sequence);
} }
} catch (Exception e) { } 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 * shouldAcceptPreviousSuggestion
* In this mode, In addition to confirming the suggestion in the input field, * Automatic space assistance. Spaces (and special chars) cause suggestions to be accepted
* we also increase its' priority. This function determines whether we want to do all this or not. * automatically. This is used for analysis before processing the incoming pressed key.
*/ */
@Override @Override
public boolean shouldAcceptPreviousSuggestion(int nextKey) { public boolean shouldAcceptPreviousSuggestion(int nextKey) {
@ -333,14 +323,29 @@ public class ModePredictive extends InputMode {
*/ */
@Override @Override
public boolean shouldAcceptPreviousSuggestion() { 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 return
(autoAcceptTimeout == 0 && !digitSequence.startsWith("0")) !digitSequence.isEmpty()
|| ( && predictions.noDbWords()
!digitSequence.isEmpty() && digitSequence.contains("1")
&& !predictions.areThereDbWords() && TextTools.containsOtherThan1(digitSequence);
&& digitSequence.contains("1")
&& TextTools.containsOtherThan1(digitSequence)
);
} }

View file

@ -29,9 +29,7 @@ public class AutoTextCase {
case InputMode.CASE_LOWER: case InputMode.CASE_LOWER:
return word.toLowerCase(language.getLocale()); return word.toLowerCase(language.getLocale());
case InputMode.CASE_CAPITALIZE: case InputMode.CASE_CAPITALIZE:
return language.isMixedCaseWord(word) ? word : language.capitalize(word); return language.isMixedCaseWord(word) || language.isUpperCaseWord(word) ? word : language.capitalize(word);
case InputMode.CASE_DICTIONARY:
return language.isMixedCaseWord(word) ? word : word.toLowerCase(language.getLocale());
default: default:
return word; return word;
} }

View file

@ -24,7 +24,7 @@ public class Predictions {
// data // data
private boolean areThereDbWords = false; private boolean areThereDbWords = false;
private final ArrayList<String> words = new ArrayList<>(); private ArrayList<String> words = new ArrayList<>();
// punctuation/emoji // punctuation/emoji
private final Pattern containsOnly1Regex = Pattern.compile("^1+$"); private final Pattern containsOnly1Regex = Pattern.compile("^1+$");
@ -83,8 +83,8 @@ public class Predictions {
return words; return words;
} }
public boolean areThereDbWords() { public boolean noDbWords() {
return areThereDbWords; return !areThereDbWords;
} }
@ -212,20 +212,20 @@ public class Predictions {
if (dbWords.isEmpty() && !digitSequence.isEmpty()) { if (dbWords.isEmpty() && !digitSequence.isEmpty()) {
emptyDbWarning.emitOnce(language); emptyDbWarning.emitOnce(language);
dbWords = generatePossibleCompletions(inputWord);
} }
words.clear(); words.clear();
suggestStem(); suggestStem();
suggestMissingWords(generatePossibleStemVariations(dbWords)); suggestMissingWords(generatePossibleStemVariations(dbWords));
suggestMissingWords(insertPunctuationCompletions(dbWords)); suggestMissingWords(dbWords.isEmpty() ? generateWordVariations(inputWord) : dbWords);
words = insertPunctuationCompletions(words);
onWordsChanged.run(); onWordsChanged.run();
} }
/** /**
* generatePossibleCompletions * generateWordVariations
* When there are no matching suggestions after the last key press, generate a list of possible * 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 * ones, so that the user can complete a missing word that is completely different from the ones
* in the dictionary. * 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: * For example, if the word is "missin_" and the last pressed key is "4", the results would be:
* | missing | missinh | missini | * | missing | missinh | missini |
*/ */
private ArrayList<String> generatePossibleCompletions(String baseWord) { private ArrayList<String> generateWordVariations(String baseWord) {
ArrayList<String> generatedWords = new ArrayList<>(); ArrayList<String> generatedWords = new ArrayList<>();
// Make sure the displayed word and the digit sequence, we will be generating suggestions from, // Make sure the displayed word and the digit sequence, we will be generating suggestions from,
@ -257,7 +257,7 @@ public class Predictions {
/** /**
* insertPunctuationCompletions * 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 * "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. * 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. * This is to preserve the sorting by length and frequency.
@ -278,7 +278,7 @@ public class Predictions {
} }
// generated "exact matches" // 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()))) { if (!dbWords.contains(w) && !dbWords.contains(w.toLowerCase(language.getLocale()))) {
complementedWords.add(w); complementedWords.add(w);
} }
@ -310,12 +310,9 @@ public class Predictions {
*/ */
private ArrayList<String> generatePossibleStemVariations(ArrayList<String> dbWords) { private ArrayList<String> generatePossibleStemVariations(ArrayList<String> dbWords) {
ArrayList<String> variations = new ArrayList<>(); ArrayList<String> variations = new ArrayList<>();
if (stem.isEmpty()) {
return variations;
}
if (isStemFuzzy && stem.length() == digitSequence.length() - 1) { if (isStemFuzzy && !stem.isEmpty() && stem.length() == digitSequence.length() - 1) {
ArrayList<String> allPossibleVariations = generatePossibleCompletions(stem); ArrayList<String> allPossibleVariations = generateWordVariations(stem);
// first add the known words, because it makes more sense to see them first // first add the known words, because it makes more sense to see them first
for (String variation : allPossibleVariations) { for (String variation : allPossibleVariations) {

View file

@ -150,26 +150,21 @@ public class Language {
/** /**
* isLatinBased * isLatinBased
* Returns "true" when the language is based on the Latin alphabet or "false" otherwise. * 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() { public boolean isLatinBased() {
ArrayList<String> letters = getKeyCharacters(2, false); return getKeyCharacters(2, false).contains("a");
return letters.contains("a");
} }
/**
* isGreek
* Similar to "isLatinBased()", this returns "true" when the language is based on the Greek alphabet.
*/
public boolean isGreek() { public boolean isGreek() {
ArrayList<String> letters = getKeyCharacters(2, false); return getKeyCharacters(2, false).contains("α");
return letters.contains("α");
} }
public boolean isUkrainian() { public boolean isUkrainian() {
ArrayList<String> letters = getKeyCharacters(4, false); return getKeyCharacters(3, false).contains("є");
return letters.contains("ї"); }
public boolean isHebrew() {
return getKeyCharacters(3, false).contains("א");
} }
/* ************ utility ************ */ /* ************ utility ************ */
@ -230,11 +225,14 @@ public class Language {
} }
public boolean isMixedCaseWord(String word) { public boolean isMixedCaseWord(String word) {
return word != null return
&& ( word != null
(word.length() == 1 && word.toUpperCase(locale).equals(word)) && !word.toLowerCase(locale).equals(word)
|| (!word.toLowerCase(locale).equals(word) && !word.toUpperCase(locale).equals(word)) && !word.toUpperCase(locale).equals(word);
); }
public boolean isUpperCaseWord(String word) {
return word != null && word.toUpperCase(locale).equals(word);
} }
public ArrayList<String> getKeyCharacters(int key, boolean includeDigit) { public ArrayList<String> getKeyCharacters(int key, boolean includeDigit) {