Punctuation Improvements (#211)
improved auto space adjustment around '-' and '/' improved auto space adjustment when the next character is a punctuation mark code cleanup: moved auto text case code, auto space code and suggestion loading code to their own classes
This commit is contained in:
parent
ca15ff230b
commit
a41dd9edd5
7 changed files with 546 additions and 323 deletions
|
|
@ -51,6 +51,8 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
private void loadSettings() {
|
private void loadSettings() {
|
||||||
mLanguage = LanguageCollection.getLanguage(settings.getInputLanguage());
|
mLanguage = LanguageCollection.getLanguage(settings.getInputLanguage());
|
||||||
mEnabledLanguages = settings.getEnabledLanguageIds();
|
mEnabledLanguages = settings.getEnabledLanguageIds();
|
||||||
|
validateLanguages();
|
||||||
|
|
||||||
mInputMode = InputMode.getInstance(settings, mLanguage, settings.getInputMode());
|
mInputMode = InputMode.getInstance(settings, mLanguage, settings.getInputMode());
|
||||||
mInputMode.setTextCase(settings.getTextCase());
|
mInputMode.setTextCase(settings.getTextCase());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,22 @@ public class TextField {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isThereSpaceAhead
|
* getPreviousChar
|
||||||
* Checks whether there is a space after the cursor.
|
* Gets the character before the cursor.
|
||||||
*/
|
*/
|
||||||
public boolean isThereSpaceAhead() {
|
public String getPreviousChars(int numberOfChars) {
|
||||||
CharSequence after = connection != null ? connection.getTextAfterCursor(1, 0) : null;
|
CharSequence character = connection != null ? connection.getTextBeforeCursor(numberOfChars, 0) : null;
|
||||||
return after != null && after.equals(" ");
|
return character != null ? character.toString() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getNextChar
|
||||||
|
* Gets the character after the cursor.
|
||||||
|
*/
|
||||||
|
public String getNextChars(int numberOfChars) {
|
||||||
|
CharSequence character = connection != null ? connection.getTextAfterCursor(numberOfChars, 0) : null;
|
||||||
|
return character != null ? character.toString() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ abstract public class InputMode {
|
||||||
public boolean onBackspace() { return false; }
|
public boolean onBackspace() { return false; }
|
||||||
abstract public boolean onNumber(int key, boolean hold, int repeat);
|
abstract public boolean onNumber(int key, boolean hold, int repeat);
|
||||||
|
|
||||||
// Suggestions
|
// Predictions
|
||||||
public void onAcceptSuggestion(String suggestion) {}
|
public void onAcceptSuggestion(String suggestion) {}
|
||||||
protected void onSuggestionsUpdated(Handler handler) { handler.sendEmptyMessage(0); }
|
protected void onSuggestionsUpdated(Handler handler) { handler.sendEmptyMessage(0); }
|
||||||
public boolean loadSuggestions(Handler handler, String currentWord) { return false; }
|
public boolean loadSuggestions(Handler handler, String currentWord) { return false; }
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,18 @@ import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import io.github.sspanak.tt9.Logger;
|
import io.github.sspanak.tt9.Logger;
|
||||||
import io.github.sspanak.tt9.db.DictionaryDb;
|
import io.github.sspanak.tt9.db.DictionaryDb;
|
||||||
import io.github.sspanak.tt9.ime.EmptyDatabaseWarning;
|
|
||||||
import io.github.sspanak.tt9.ime.helpers.InputType;
|
import io.github.sspanak.tt9.ime.helpers.InputType;
|
||||||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
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.InvalidLanguageCharactersException;
|
import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException;
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
import io.github.sspanak.tt9.languages.Characters;
|
|
||||||
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 EmptyDatabaseWarning emptyDbWarning;
|
|
||||||
private final SettingsStore settings;
|
private final SettingsStore settings;
|
||||||
|
|
||||||
public int getId() { return MODE_PREDICTIVE; }
|
public int getId() { return MODE_PREDICTIVE; }
|
||||||
|
|
@ -32,30 +29,22 @@ public class ModePredictive extends InputMode {
|
||||||
private String stem = "";
|
private String stem = "";
|
||||||
|
|
||||||
// async suggestion handling
|
// async suggestion handling
|
||||||
private String currentInputFieldWord = "";
|
|
||||||
private static Handler handleSuggestionsExternal;
|
private static Handler handleSuggestionsExternal;
|
||||||
|
|
||||||
// auto text case selection
|
// text analysis tools
|
||||||
private final Pattern startOfSentenceRegex = Pattern.compile("(?<!\\.)(^|[.?!¿¡])\\s*$");
|
private final AutoSpace autoSpace;
|
||||||
|
private final AutoTextCase autoTextCase;
|
||||||
// punctuation/emoji
|
private final Predictions predictions;
|
||||||
private final Pattern containsOnly1Regex = Pattern.compile("^1+$");
|
|
||||||
private final String maxEmojiSequence;
|
|
||||||
|
|
||||||
|
|
||||||
ModePredictive(SettingsStore settings, Language lang) {
|
ModePredictive(SettingsStore settings, Language lang) {
|
||||||
changeLanguage(lang);
|
changeLanguage(lang);
|
||||||
|
|
||||||
emptyDbWarning = new EmptyDatabaseWarning(settings);
|
autoSpace = new AutoSpace(settings);
|
||||||
this.settings = settings;
|
autoTextCase = new AutoTextCase(settings);
|
||||||
|
predictions = new Predictions(settings);
|
||||||
|
|
||||||
// digitSequence limiter when selecting emoji
|
this.settings = settings;
|
||||||
// "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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -207,65 +196,28 @@ public class ModePredictive extends InputMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loadStaticSuggestions
|
|
||||||
* Similar to "loadSuggestions()", but loads suggestions that are not in the database.
|
|
||||||
* Returns "false", when there are no static suggestions for the current digitSequence.
|
|
||||||
*/
|
|
||||||
private boolean loadStaticSuggestions() {
|
|
||||||
if (digitSequence.equals("0")) {
|
|
||||||
stem = "";
|
|
||||||
suggestions = language.getKeyCharacters(0, false);
|
|
||||||
} else if (containsOnly1Regex.matcher(digitSequence).matches()) {
|
|
||||||
stem = "";
|
|
||||||
if (digitSequence.length() == 1) {
|
|
||||||
suggestions = language.getKeyCharacters(1, false);
|
|
||||||
} else {
|
|
||||||
digitSequence = digitSequence.length() <= maxEmojiSequence.length() ? digitSequence : maxEmojiSequence;
|
|
||||||
suggestions = Characters.getEmoji(digitSequence.length() - 2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* loadSuggestions
|
* loadSuggestions
|
||||||
* Queries the dictionary database for a list of suggestions matching the current language and
|
* Loads the possible list of suggestions for the current digitSequence.
|
||||||
* sequence. Returns "false" when there is nothing to do.
|
* Returns "false" on invalid sequence.
|
||||||
*
|
*
|
||||||
* "lastWord" is used for generating suggestions when there are no results.
|
* "currentWord" is used for generating suggestions when there are no results.
|
||||||
* See: generatePossibleCompletions()
|
* See: Predictions.generatePossibleCompletions()
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean loadSuggestions(Handler handler, String currentWord) {
|
public boolean loadSuggestions(Handler handler, String currentWord) {
|
||||||
if (loadStaticSuggestions()) {
|
predictions
|
||||||
super.onSuggestionsUpdated(handler);
|
.setDigitSequence(digitSequence)
|
||||||
return true;
|
.setIsStemFuzzy(isStemFuzzy)
|
||||||
}
|
.setStem(stem)
|
||||||
|
.setLanguage(language)
|
||||||
if (digitSequence.length() == 0) {
|
.setInputWord(currentWord)
|
||||||
suggestions = new ArrayList<>();
|
.setWordsChangedHandler(handleSuggestions);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSuggestionsExternal = handler;
|
handleSuggestionsExternal = handler;
|
||||||
currentInputFieldWord = currentWord.toLowerCase(language.getLocale());
|
|
||||||
super.reset();
|
super.reset();
|
||||||
|
|
||||||
DictionaryDb.getSuggestions(
|
return predictions.load();
|
||||||
handleSuggestions,
|
|
||||||
language,
|
|
||||||
digitSequence,
|
|
||||||
stem,
|
|
||||||
settings.getSuggestionsMin(),
|
|
||||||
settings.getSuggestionsMax()
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -276,125 +228,15 @@ public class ModePredictive extends InputMode {
|
||||||
*/
|
*/
|
||||||
private final Handler handleSuggestions = new Handler(Looper.getMainLooper()) {
|
private final Handler handleSuggestions = new Handler(Looper.getMainLooper()) {
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message msg) {
|
public void handleMessage(Message m) {
|
||||||
ArrayList<String> dbSuggestions = msg.getData().getStringArrayList("suggestions");
|
|
||||||
dbSuggestions = dbSuggestions == null ? new ArrayList<>() : dbSuggestions;
|
|
||||||
|
|
||||||
if (dbSuggestions.size() == 0 && digitSequence.length() > 0) {
|
|
||||||
emptyDbWarning.emitOnce(language);
|
|
||||||
dbSuggestions = generatePossibleCompletions(currentInputFieldWord);
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestions.clear();
|
suggestions.clear();
|
||||||
suggestStem();
|
suggestions.addAll(predictions.getList());
|
||||||
suggestions.addAll(generatePossibleStemVariations(dbSuggestions));
|
|
||||||
suggestMoreWords(dbSuggestions);
|
|
||||||
|
|
||||||
ModePredictive.super.onSuggestionsUpdated(handleSuggestionsExternal);
|
onSuggestionsUpdated(handleSuggestionsExternal);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* generatePossibleCompletions
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* For example, if the word is "missin_" and the last pressed key is "4", the results would be:
|
|
||||||
* | missing | missinh | missini |
|
|
||||||
*/
|
|
||||||
private ArrayList<String> generatePossibleCompletions(String baseWord) {
|
|
||||||
ArrayList<String> generatedWords = new ArrayList<>();
|
|
||||||
|
|
||||||
// Make sure the displayed word and the digit sequence, we will be generating suggestions from,
|
|
||||||
// have the same length, to prevent visual discrepancies.
|
|
||||||
baseWord = (baseWord != null && baseWord.length() > 0) ? baseWord.substring(0, Math.min(digitSequence.length() - 1, baseWord.length())) : "";
|
|
||||||
|
|
||||||
// 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)) {
|
|
||||||
// let's skip numbers, because it's weird, for example:
|
|
||||||
// | weird | weire | weirf | weir2 |
|
|
||||||
if (keyLetter.charAt(0) < '0' || keyLetter.charAt(0) > '9') {
|
|
||||||
generatedWords.add(baseWord + keyLetter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there are no letters for this key, just append the number
|
|
||||||
if (generatedWords.size() == 0) {
|
|
||||||
generatedWords.add(baseWord + digitSequence.charAt(digitSequence.length() - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return generatedWords;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* generatePossibleStemVariations
|
|
||||||
* Similar to generatePossibleCompletions(), but uses the current filter as a base word. This is
|
|
||||||
* used to complement the database results with all possible variations for the next key, when
|
|
||||||
* the stem filter is on.
|
|
||||||
*
|
|
||||||
* It will not generate anything if more than one key was pressed after filtering though.
|
|
||||||
*
|
|
||||||
* For example, if the filter is "extr", the current word is "extr_" and the user has pressed "1",
|
|
||||||
* the database would have returned only "extra", but this function would also
|
|
||||||
* generate: "extrb" and "extrc". This is useful for typing an unknown word, that is similar to
|
|
||||||
* the ones in the dictionary.
|
|
||||||
*/
|
|
||||||
private ArrayList<String> generatePossibleStemVariations(ArrayList<String> dbSuggestions) {
|
|
||||||
ArrayList<String> variations = new ArrayList<>();
|
|
||||||
if (stem.length() == 0) {
|
|
||||||
return variations;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStemFuzzy && stem.length() == digitSequence.length() - 1) {
|
|
||||||
ArrayList<String> allPossibleVariations = generatePossibleCompletions(stem);
|
|
||||||
|
|
||||||
// first add the known words, because it makes more sense to see them first
|
|
||||||
for (String word : allPossibleVariations) {
|
|
||||||
if (dbSuggestions.contains(word)) {
|
|
||||||
variations.add(word);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// then add the unknown ones, so they can be used as possible beginnings of new words.
|
|
||||||
for (String word : allPossibleVariations) {
|
|
||||||
if (!dbSuggestions.contains(word)) {
|
|
||||||
variations.add(word);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return variations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* suggestStem
|
|
||||||
* Add the current stem filter to the suggestion list, when it has length of X and
|
|
||||||
* the user has pressed X keys.
|
|
||||||
*/
|
|
||||||
private void suggestStem() {
|
|
||||||
if (stem.length() > 0 && stem.length() == digitSequence.length()) {
|
|
||||||
suggestions.add(stem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* suggestMoreWords
|
|
||||||
* Takes a list of words and appends them to the suggestion list, if they are missing.
|
|
||||||
*/
|
|
||||||
private void suggestMoreWords(ArrayList<String> newSuggestions) {
|
|
||||||
for (String word : newSuggestions) {
|
|
||||||
if (!suggestions.contains(word)) {
|
|
||||||
suggestions.add(word);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onAcceptSuggestion
|
* onAcceptSuggestion
|
||||||
* Bring this word up in the suggestions list next time.
|
* Bring this word up in the suggestions list next time.
|
||||||
|
|
@ -424,70 +266,16 @@ public class ModePredictive extends InputMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* adjustSuggestionTextCase
|
|
||||||
* In addition to uppercase/lowercase, here we use the result from determineNextWordTextCase(),
|
|
||||||
* to conveniently start sentences with capitals or whatnot.
|
|
||||||
*
|
|
||||||
* Also, by default we preserve any mixed case words in the dictionary,
|
|
||||||
* for example: "dB", "Mb", proper names, German nouns, that always start with a capital,
|
|
||||||
* or Dutch words such as: "'s-Hertogenbosch".
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected String adjustSuggestionTextCase(String word, int newTextCase) {
|
protected String adjustSuggestionTextCase(String word, int newTextCase) {
|
||||||
switch (newTextCase) {
|
return autoTextCase.adjustSuggestionTextCase(language, word, newTextCase);
|
||||||
case CASE_UPPER:
|
|
||||||
return word.toUpperCase(language.getLocale());
|
|
||||||
case CASE_LOWER:
|
|
||||||
return word.toLowerCase(language.getLocale());
|
|
||||||
case CASE_CAPITALIZE:
|
|
||||||
return language.isMixedCaseWord(word) ? word : language.capitalize(word);
|
|
||||||
case CASE_DICTIONARY:
|
|
||||||
return language.isMixedCaseWord(word) ? word : word.toLowerCase(language.getLocale());
|
|
||||||
default:
|
|
||||||
return word;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* determineNextWordTextCase
|
|
||||||
* Dynamically determine text case of words as the user types, to reduce key presses.
|
|
||||||
* For example, this function will return CASE_LOWER by default, but CASE_UPPER at the beginning
|
|
||||||
* of a sentence.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void determineNextWordTextCase(SettingsStore settings, boolean isThereText, String textBeforeCursor) {
|
public void determineNextWordTextCase(SettingsStore settings, boolean isThereText, String textBeforeCursor) {
|
||||||
if (!settings.getAutoTextCase()) {
|
textCase = autoTextCase.determineNextWordTextCase(isThereText, textCase, textFieldTextCase, textBeforeCursor);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user wants to type in uppercase, this must be for a reason, so we better not override it.
|
|
||||||
if (textCase == CASE_UPPER) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textFieldTextCase != CASE_UNDEFINED) {
|
|
||||||
textCase = textFieldTextCase;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// start of text
|
|
||||||
if (!isThereText) {
|
|
||||||
textCase = CASE_CAPITALIZE;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// start of sentence, excluding after "..."
|
|
||||||
if (startOfSentenceRegex.matcher(textBeforeCursor).find()) {
|
|
||||||
textCase = CASE_CAPITALIZE;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
textCase = CASE_DICTIONARY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void nextTextCase() {
|
public void nextTextCase() {
|
||||||
textFieldTextCase = CASE_UNDEFINED; // since it's a user's choice, the default matters no more
|
textFieldTextCase = CASE_UNDEFINED; // since it's a user's choice, the default matters no more
|
||||||
|
|
@ -515,90 +303,26 @@ public class ModePredictive extends InputMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shouldAddAutoSpace
|
|
||||||
* When the "auto-space" settings is enabled, this determines whether to automatically add a space
|
|
||||||
* at the end of a sentence or after accepting a suggestion. This allows faster typing, without
|
|
||||||
* pressing space.
|
|
||||||
*
|
|
||||||
* See the helper functions for the list of rules.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldAddAutoSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) {
|
public boolean shouldAddAutoSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) {
|
||||||
return
|
return autoSpace
|
||||||
settings.getAutoSpace()
|
.setLastWord(lastAcceptedWord)
|
||||||
&& !hold
|
.setLastSequence(lastAcceptedSequence)
|
||||||
&& (
|
.setInputType(inputType)
|
||||||
shouldAddAutoSpaceAfterPunctuation(inputType, incomingKey, repeat)
|
.setTextField(textField)
|
||||||
|| shouldAddAutoSpaceAfterWord(inputType, isWordAcceptedManually)
|
.shouldAddAutoSpace(isWordAcceptedManually, incomingKey, hold, repeat);
|
||||||
)
|
|
||||||
&& !textField.isThereSpaceAhead();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shouldAddAutoSpaceAfterPunctuation
|
|
||||||
* Determines whether to automatically adding a space after certain punctuation signs makes sense.
|
|
||||||
* The rules are similar to the ones in the standard Android keyboard (with some exceptions,
|
|
||||||
* because we are not using a QWERTY keyboard here).
|
|
||||||
*/
|
|
||||||
private boolean shouldAddAutoSpaceAfterPunctuation(InputType inputType, int incomingKey, boolean repeat) {
|
|
||||||
return
|
|
||||||
(incomingKey != 0 || repeat)
|
|
||||||
&& (
|
|
||||||
lastAcceptedWord.endsWith(".")
|
|
||||||
|| lastAcceptedWord.endsWith(",")
|
|
||||||
|| lastAcceptedWord.endsWith(";")
|
|
||||||
|| lastAcceptedWord.endsWith(":")
|
|
||||||
|| lastAcceptedWord.endsWith("!")
|
|
||||||
|| lastAcceptedWord.endsWith("?")
|
|
||||||
|| lastAcceptedWord.endsWith(")")
|
|
||||||
|| lastAcceptedWord.endsWith("]")
|
|
||||||
|| lastAcceptedWord.endsWith("%")
|
|
||||||
)
|
|
||||||
&& !inputType.isSpecialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shouldAddAutoSpaceAfterPunctuation
|
|
||||||
* Similar to "shouldAddAutoSpaceAfterPunctuation()", but determines whether to add a space after
|
|
||||||
* words.
|
|
||||||
*/
|
|
||||||
private boolean shouldAddAutoSpaceAfterWord(InputType inputType, boolean isWordAcceptedManually) {
|
|
||||||
return
|
|
||||||
// Do not add space when auto-accepting words, because it feels very confusing when typing.
|
|
||||||
isWordAcceptedManually
|
|
||||||
// Secondary punctuation
|
|
||||||
&& !lastAcceptedSequence.equals("0")
|
|
||||||
// Emoji
|
|
||||||
&& !lastAcceptedSequence.startsWith("1")
|
|
||||||
&& !inputType.isSpecialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shouldDeletePrecedingSpace
|
|
||||||
* When the "auto-space" settings is enabled, determine whether to delete spaces before punctuation.
|
|
||||||
* This allows automatic conversion from: "words ." to: "words."
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldDeletePrecedingSpace(InputType inputType) {
|
public boolean shouldDeletePrecedingSpace(InputType inputType) {
|
||||||
return
|
return autoSpace
|
||||||
settings.getAutoSpace()
|
.setLastWord(lastAcceptedWord)
|
||||||
&& (
|
.setLastSequence(lastAcceptedSequence)
|
||||||
lastAcceptedWord.equals(".")
|
.setInputType(inputType)
|
||||||
|| lastAcceptedWord.equals(",")
|
.setTextField(null)
|
||||||
|| lastAcceptedWord.equals(";")
|
.shouldDeletePrecedingSpace();
|
||||||
|| lastAcceptedWord.equals(":")
|
|
||||||
|| lastAcceptedWord.equals("!")
|
|
||||||
|| lastAcceptedWord.equals("?")
|
|
||||||
|| lastAcceptedWord.equals(")")
|
|
||||||
|| lastAcceptedWord.equals("]")
|
|
||||||
|| lastAcceptedWord.equals("'")
|
|
||||||
|| lastAcceptedWord.equals("@")
|
|
||||||
)
|
|
||||||
&& !inputType.isSpecialized();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
133
src/io/github/sspanak/tt9/ime/modes/helpers/AutoSpace.java
Normal file
133
src/io/github/sspanak/tt9/ime/modes/helpers/AutoSpace.java
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
package io.github.sspanak.tt9.ime.modes.helpers;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.Logger;
|
||||||
|
import io.github.sspanak.tt9.ime.helpers.InputType;
|
||||||
|
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
|
public class AutoSpace {
|
||||||
|
private final Pattern nextIsPunctuation = Pattern.compile("\\p{Punct}");
|
||||||
|
private final SettingsStore settings;
|
||||||
|
|
||||||
|
private InputType inputType;
|
||||||
|
private TextField textField;
|
||||||
|
private String lastWord;
|
||||||
|
private String lastSequence;
|
||||||
|
|
||||||
|
public AutoSpace(SettingsStore settingsStore) {
|
||||||
|
settings = settingsStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AutoSpace setInputType(InputType inputType) {
|
||||||
|
this.inputType = inputType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AutoSpace setTextField(TextField textField) {
|
||||||
|
this.textField = textField;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AutoSpace setLastWord(String lastWord) {
|
||||||
|
this.lastWord = lastWord;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AutoSpace setLastSequence(String lastSequence) {
|
||||||
|
this.lastSequence = lastSequence;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shouldAddAutoSpace
|
||||||
|
* When the "auto-space" settings is enabled, this determines whether to automatically add a space
|
||||||
|
* at the end of a sentence or after accepting a suggestion. This allows faster typing, without
|
||||||
|
* pressing space.
|
||||||
|
*
|
||||||
|
* See the helper functions for the list of rules.
|
||||||
|
*/
|
||||||
|
public boolean shouldAddAutoSpace(boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) {
|
||||||
|
String previousChars = textField.getPreviousChars(2);
|
||||||
|
String nextChars = textField.getNextChars(2);
|
||||||
|
Logger.d("shouldAddAutoSpace", "next chars: '" + nextChars + "'");
|
||||||
|
|
||||||
|
return
|
||||||
|
settings.getAutoSpace()
|
||||||
|
&& !hold
|
||||||
|
&& (
|
||||||
|
shouldAddAutoSpaceAfterPunctuation(previousChars, incomingKey, repeat)
|
||||||
|
|| shouldAddAutoSpaceAfterWord(isWordAcceptedManually)
|
||||||
|
)
|
||||||
|
&& !nextChars.startsWith(" ")
|
||||||
|
&& !nextIsPunctuation.matcher(nextChars).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shouldAddAutoSpaceAfterPunctuation
|
||||||
|
* Determines whether to automatically adding a space after certain punctuation signs makes sense.
|
||||||
|
* The rules are similar to the ones in the standard Android keyboard (with some exceptions,
|
||||||
|
* because we are not using a QWERTY keyboard here).
|
||||||
|
*/
|
||||||
|
private boolean shouldAddAutoSpaceAfterPunctuation(String previousChars, int incomingKey, boolean repeat) {
|
||||||
|
return
|
||||||
|
(incomingKey != 0 || repeat)
|
||||||
|
&& !inputType.isSpecialized()
|
||||||
|
&& (
|
||||||
|
previousChars.endsWith(".")
|
||||||
|
|| previousChars.endsWith(",")
|
||||||
|
|| previousChars.endsWith(";")
|
||||||
|
|| previousChars.endsWith(":")
|
||||||
|
|| previousChars.endsWith("!")
|
||||||
|
|| previousChars.endsWith("?")
|
||||||
|
|| previousChars.endsWith(")")
|
||||||
|
|| previousChars.endsWith("]")
|
||||||
|
|| previousChars.endsWith("%")
|
||||||
|
|| previousChars.endsWith(" -")
|
||||||
|
|| previousChars.endsWith(" /")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shouldAddAutoSpaceAfterPunctuation
|
||||||
|
* Similar to "shouldAddAutoSpaceAfterPunctuation()", but determines whether to add a space after
|
||||||
|
* words.
|
||||||
|
*/
|
||||||
|
private boolean shouldAddAutoSpaceAfterWord(boolean isWordAcceptedManually) {
|
||||||
|
return
|
||||||
|
// Do not add space when auto-accepting words, because it feels very confusing when typing.
|
||||||
|
isWordAcceptedManually
|
||||||
|
// Secondary punctuation
|
||||||
|
&& !lastSequence.equals("0")
|
||||||
|
// Emoji
|
||||||
|
&& !lastSequence.startsWith("1")
|
||||||
|
&& !inputType.isSpecialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shouldDeletePrecedingSpace
|
||||||
|
* When the "auto-space" settings is enabled, determine whether to delete spaces before punctuation.
|
||||||
|
* This allows automatic conversion from: "words ." to: "words."
|
||||||
|
*/
|
||||||
|
public boolean shouldDeletePrecedingSpace() {
|
||||||
|
return
|
||||||
|
settings.getAutoSpace()
|
||||||
|
&& (
|
||||||
|
lastWord.equals(".")
|
||||||
|
|| lastWord.equals(",")
|
||||||
|
|| lastWord.equals(";")
|
||||||
|
|| lastWord.equals(":")
|
||||||
|
|| lastWord.equals("!")
|
||||||
|
|| lastWord.equals("?")
|
||||||
|
|| lastWord.equals(")")
|
||||||
|
|| lastWord.equals("]")
|
||||||
|
|| lastWord.equals("'")
|
||||||
|
|| lastWord.equals("@")
|
||||||
|
)
|
||||||
|
&& !inputType.isSpecialized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package io.github.sspanak.tt9.ime.modes.helpers;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
|
public class AutoTextCase {
|
||||||
|
private final Pattern startOfSentenceRegex = Pattern.compile("(?<!\\.)(^|[.?!¿¡])\\s*$");
|
||||||
|
private final SettingsStore settings;
|
||||||
|
|
||||||
|
|
||||||
|
public AutoTextCase(SettingsStore settingsStore) {
|
||||||
|
settings = settingsStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adjustSuggestionTextCase
|
||||||
|
* In addition to uppercase/lowercase, here we use the result from determineNextWordTextCase(),
|
||||||
|
* to conveniently start sentences with capitals or whatnot.
|
||||||
|
*
|
||||||
|
* Also, by default we preserve any mixed case words in the dictionary,
|
||||||
|
* for example: "dB", "Mb", proper names, German nouns, that always start with a capital,
|
||||||
|
* or Dutch words such as: "'s-Hertogenbosch".
|
||||||
|
*/
|
||||||
|
public String adjustSuggestionTextCase(Language language, String word, int newTextCase) {
|
||||||
|
switch (newTextCase) {
|
||||||
|
case InputMode.CASE_UPPER:
|
||||||
|
return word.toUpperCase(language.getLocale());
|
||||||
|
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());
|
||||||
|
default:
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determineNextWordTextCase
|
||||||
|
* Dynamically determine text case of words as the user types, to reduce key presses.
|
||||||
|
* For example, this function will return CASE_LOWER by default, but CASE_UPPER at the beginning
|
||||||
|
* of a sentence.
|
||||||
|
*/
|
||||||
|
public int determineNextWordTextCase(boolean isThereText, int currentTextCase, int textFieldTextCase, String textBeforeCursor) {
|
||||||
|
if (
|
||||||
|
// When the setting is off, don't do any changes.
|
||||||
|
!settings.getAutoTextCase()
|
||||||
|
// If the user wants to type in uppercase, this must be for a reason, so we better not override it.
|
||||||
|
|| currentTextCase == InputMode.CASE_UPPER
|
||||||
|
) {
|
||||||
|
return currentTextCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (textFieldTextCase != InputMode.CASE_UNDEFINED) {
|
||||||
|
return textFieldTextCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start of text
|
||||||
|
if (!isThereText) {
|
||||||
|
return InputMode.CASE_CAPITALIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start of sentence, excluding after "..."
|
||||||
|
if (startOfSentenceRegex.matcher(textBeforeCursor).find()) {
|
||||||
|
return InputMode.CASE_CAPITALIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InputMode.CASE_DICTIONARY;
|
||||||
|
}
|
||||||
|
}
|
||||||
278
src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java
Normal file
278
src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
package io.github.sspanak.tt9.ime.modes.helpers;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.db.DictionaryDb;
|
||||||
|
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;
|
||||||
|
private boolean isStemFuzzy;
|
||||||
|
private String stem;
|
||||||
|
private String inputWord;
|
||||||
|
|
||||||
|
// async operations
|
||||||
|
private Handler wordsChangedHandler;
|
||||||
|
|
||||||
|
// data
|
||||||
|
private ArrayList<String> words = new ArrayList<>();
|
||||||
|
|
||||||
|
// punctuation/emoji
|
||||||
|
private final Pattern containsOnly1Regex = Pattern.compile("^1+$");
|
||||||
|
private final String maxEmojiSequence;
|
||||||
|
|
||||||
|
|
||||||
|
public Predictions(SettingsStore settingsStore) {
|
||||||
|
emptyDbWarning = new EmptyDatabaseWarning(settingsStore);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Predictions setLanguage(Language language) {
|
||||||
|
this.language = language;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predictions setDigitSequence(String digitSequence) {
|
||||||
|
this.digitSequence = digitSequence;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predictions setIsStemFuzzy(boolean yes) {
|
||||||
|
this.isStemFuzzy = yes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predictions setStem(String stem) {
|
||||||
|
this.stem = stem;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predictions setInputWord(String inputWord) {
|
||||||
|
this.inputWord = inputWord.toLowerCase(language.getLocale());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predictions setWordsChangedHandler(Handler handler) {
|
||||||
|
wordsChangedHandler = handler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<String> getList() {
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* suggestStem
|
||||||
|
* Add the current stem filter to the predictions list, when it has length of X and
|
||||||
|
* the user has pressed X keys (otherwise, it makes no sense to add it).
|
||||||
|
*/
|
||||||
|
private void suggestStem() {
|
||||||
|
if (stem.length() > 0 && stem.length() == digitSequence.length()) {
|
||||||
|
words.add(stem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* suggestMissingWords
|
||||||
|
* Takes a list of words and appends them to the words list, if they are missing.
|
||||||
|
*/
|
||||||
|
private void suggestMissingWords(ArrayList<String> newWords) {
|
||||||
|
for (String newWord : newWords) {
|
||||||
|
if (!words.contains(newWord) && !words.contains(newWord.toLowerCase(language.getLocale()))) {
|
||||||
|
words.add(newWord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onWordsChanged
|
||||||
|
* Notify the external handler the word list has changed, so they can get the new ones using getList().
|
||||||
|
*/
|
||||||
|
private void onWordsChanged() {
|
||||||
|
wordsChangedHandler.sendEmptyMessage(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load
|
||||||
|
* Queries the dictionary database for a list of words matching the current language and
|
||||||
|
* sequence or loads the static ones.
|
||||||
|
*
|
||||||
|
* Returns "false" on invalid digitSequence.
|
||||||
|
*/
|
||||||
|
public boolean load() {
|
||||||
|
if (digitSequence == null || digitSequence.length() == 0) {
|
||||||
|
words = new ArrayList<>();
|
||||||
|
onWordsChanged();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadStatic()) {
|
||||||
|
onWordsChanged();
|
||||||
|
} else {
|
||||||
|
DictionaryDb.getSuggestions(
|
||||||
|
dbWordsHandler,
|
||||||
|
language,
|
||||||
|
digitSequence,
|
||||||
|
stem,
|
||||||
|
settings.getSuggestionsMin(),
|
||||||
|
settings.getSuggestionsMax()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
if (digitSequence.equals("0")) {
|
||||||
|
stem = "";
|
||||||
|
words = language.getKeyCharacters(0, false);
|
||||||
|
} else if (containsOnly1Regex.matcher(digitSequence).matches()) {
|
||||||
|
stem = "";
|
||||||
|
if (digitSequence.length() == 1) {
|
||||||
|
words = language.getKeyCharacters(1, false);
|
||||||
|
} else {
|
||||||
|
digitSequence = digitSequence.length() <= maxEmojiSequence.length() ? digitSequence : maxEmojiSequence;
|
||||||
|
words = Characters.getEmoji(digitSequence.length() - 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dbWordsHandler
|
||||||
|
* Extracts the words from the Message object, generates extra words, if necessary, then
|
||||||
|
* notifies the external handler it is now possible to use "getList()".
|
||||||
|
* If there were no matches in the database, they will be generated based on the "inputWord".
|
||||||
|
*/
|
||||||
|
private final Handler dbWordsHandler = new Handler(Looper.getMainLooper()) {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
ArrayList<String> dbWords = msg.getData().getStringArrayList("suggestions");
|
||||||
|
dbWords = dbWords != null ? dbWords : new ArrayList<>();
|
||||||
|
|
||||||
|
if (dbWords.size() == 0 && digitSequence.length() > 0) {
|
||||||
|
emptyDbWarning.emitOnce(language);
|
||||||
|
dbWords = generatePossibleCompletions(inputWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
words.clear();
|
||||||
|
suggestStem();
|
||||||
|
suggestMissingWords(generatePossibleStemVariations(dbWords));
|
||||||
|
suggestMissingWords(dbWords);
|
||||||
|
|
||||||
|
onWordsChanged();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generatePossibleCompletions
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* For example, if the word is "missin_" and the last pressed key is "4", the results would be:
|
||||||
|
* | missing | missinh | missini |
|
||||||
|
*/
|
||||||
|
private ArrayList<String> generatePossibleCompletions(String baseWord) {
|
||||||
|
ArrayList<String> generatedWords = new ArrayList<>();
|
||||||
|
|
||||||
|
// Make sure the displayed word and the digit sequence, we will be generating suggestions from,
|
||||||
|
// have the same length, to prevent visual discrepancies.
|
||||||
|
baseWord = (baseWord != null && baseWord.length() > 0) ? baseWord.substring(0, Math.min(digitSequence.length() - 1, baseWord.length())) : "";
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
|
// let's skip numbers, because it's weird, for example:
|
||||||
|
// | weird | weire | weirf | weir2 |
|
||||||
|
if (keyLetter.charAt(0) < '0' || keyLetter.charAt(0) > '9') {
|
||||||
|
generatedWords.add(baseWord + keyLetter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are no letters for this key, just append the number
|
||||||
|
if (generatedWords.size() == 0) {
|
||||||
|
generatedWords.add(baseWord + digitSequence.charAt(digitSequence.length() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return generatedWords;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generatePossibleStemVariations
|
||||||
|
* Similar to generatePossibleCompletions(), but uses the current filter as a base word. This is
|
||||||
|
* used to complement the database results with all possible variations for the next key, when
|
||||||
|
* the stem filter is on.
|
||||||
|
*
|
||||||
|
* It will not generate anything if more than one key was pressed after filtering though.
|
||||||
|
*
|
||||||
|
* For example, if the filter is "extr", the current word is "extr_" and the user has pressed "1",
|
||||||
|
* the database would have returned only "extra", but this function would also
|
||||||
|
* generate: "extrb" and "extrc". This is useful for typing an unknown word, that is similar to
|
||||||
|
* the ones in the dictionary.
|
||||||
|
*/
|
||||||
|
private ArrayList<String> generatePossibleStemVariations(ArrayList<String> dbWords) {
|
||||||
|
ArrayList<String> variations = new ArrayList<>();
|
||||||
|
if (stem.length() == 0) {
|
||||||
|
return variations;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStemFuzzy && stem.length() == digitSequence.length() - 1) {
|
||||||
|
ArrayList<String> allPossibleVariations = generatePossibleCompletions(stem);
|
||||||
|
|
||||||
|
// first add the known words, because it makes more sense to see them first
|
||||||
|
for (String variation : allPossibleVariations) {
|
||||||
|
if (dbWords.contains(variation)) {
|
||||||
|
variations.add(variation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// then add the unknown ones, so they can be used as possible beginnings of new words.
|
||||||
|
for (String word : allPossibleVariations) {
|
||||||
|
if (!dbWords.contains(word)) {
|
||||||
|
variations.add(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variations;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue