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() {
|
||||
mLanguage = LanguageCollection.getLanguage(settings.getInputLanguage());
|
||||
mEnabledLanguages = settings.getEnabledLanguageIds();
|
||||
validateLanguages();
|
||||
|
||||
mInputMode = InputMode.getInstance(settings, mLanguage, settings.getInputMode());
|
||||
mInputMode.setTextCase(settings.getTextCase());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,12 +43,22 @@ public class TextField {
|
|||
|
||||
|
||||
/**
|
||||
* isThereSpaceAhead
|
||||
* Checks whether there is a space after the cursor.
|
||||
* getPreviousChar
|
||||
* Gets the character before the cursor.
|
||||
*/
|
||||
public boolean isThereSpaceAhead() {
|
||||
CharSequence after = connection != null ? connection.getTextAfterCursor(1, 0) : null;
|
||||
return after != null && after.equals(" ");
|
||||
public String getPreviousChars(int numberOfChars) {
|
||||
CharSequence character = connection != null ? connection.getTextBeforeCursor(numberOfChars, 0) : null;
|
||||
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; }
|
||||
abstract public boolean onNumber(int key, boolean hold, int repeat);
|
||||
|
||||
// Suggestions
|
||||
// Predictions
|
||||
public void onAcceptSuggestion(String suggestion) {}
|
||||
protected void onSuggestionsUpdated(Handler handler) { handler.sendEmptyMessage(0); }
|
||||
public boolean loadSuggestions(Handler handler, String currentWord) { return false; }
|
||||
|
|
|
|||
|
|
@ -4,21 +4,18 @@ 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.Logger;
|
||||
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.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.Language;
|
||||
import io.github.sspanak.tt9.languages.Characters;
|
||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||
|
||||
public class ModePredictive extends InputMode {
|
||||
private final EmptyDatabaseWarning emptyDbWarning;
|
||||
private final SettingsStore settings;
|
||||
|
||||
public int getId() { return MODE_PREDICTIVE; }
|
||||
|
|
@ -32,30 +29,22 @@ public class ModePredictive extends InputMode {
|
|||
private String stem = "";
|
||||
|
||||
// async suggestion handling
|
||||
private String currentInputFieldWord = "";
|
||||
private static Handler handleSuggestionsExternal;
|
||||
|
||||
// auto text case selection
|
||||
private final Pattern startOfSentenceRegex = Pattern.compile("(?<!\\.)(^|[.?!¿¡])\\s*$");
|
||||
|
||||
// punctuation/emoji
|
||||
private final Pattern containsOnly1Regex = Pattern.compile("^1+$");
|
||||
private final String maxEmojiSequence;
|
||||
// text analysis tools
|
||||
private final AutoSpace autoSpace;
|
||||
private final AutoTextCase autoTextCase;
|
||||
private final Predictions predictions;
|
||||
|
||||
|
||||
ModePredictive(SettingsStore settings, Language lang) {
|
||||
changeLanguage(lang);
|
||||
|
||||
emptyDbWarning = new EmptyDatabaseWarning(settings);
|
||||
this.settings = settings;
|
||||
autoSpace = new AutoSpace(settings);
|
||||
autoTextCase = new AutoTextCase(settings);
|
||||
predictions = new Predictions(settings);
|
||||
|
||||
// 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();
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
* Queries the dictionary database for a list of suggestions matching the current language and
|
||||
* sequence. Returns "false" when there is nothing to do.
|
||||
* Loads the possible list of suggestions for the current digitSequence.
|
||||
* Returns "false" on invalid sequence.
|
||||
*
|
||||
* "lastWord" is used for generating suggestions when there are no results.
|
||||
* See: generatePossibleCompletions()
|
||||
* "currentWord" is used for generating suggestions when there are no results.
|
||||
* See: Predictions.generatePossibleCompletions()
|
||||
*/
|
||||
@Override
|
||||
public boolean loadSuggestions(Handler handler, String currentWord) {
|
||||
if (loadStaticSuggestions()) {
|
||||
super.onSuggestionsUpdated(handler);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (digitSequence.length() == 0) {
|
||||
suggestions = new ArrayList<>();
|
||||
return false;
|
||||
}
|
||||
predictions
|
||||
.setDigitSequence(digitSequence)
|
||||
.setIsStemFuzzy(isStemFuzzy)
|
||||
.setStem(stem)
|
||||
.setLanguage(language)
|
||||
.setInputWord(currentWord)
|
||||
.setWordsChangedHandler(handleSuggestions);
|
||||
|
||||
handleSuggestionsExternal = handler;
|
||||
currentInputFieldWord = currentWord.toLowerCase(language.getLocale());
|
||||
super.reset();
|
||||
|
||||
DictionaryDb.getSuggestions(
|
||||
handleSuggestions,
|
||||
language,
|
||||
digitSequence,
|
||||
stem,
|
||||
settings.getSuggestionsMin(),
|
||||
settings.getSuggestionsMax()
|
||||
);
|
||||
|
||||
return true;
|
||||
return predictions.load();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -276,125 +228,15 @@ public class ModePredictive extends InputMode {
|
|||
*/
|
||||
private final Handler handleSuggestions = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
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);
|
||||
}
|
||||
|
||||
public void handleMessage(Message m) {
|
||||
suggestions.clear();
|
||||
suggestStem();
|
||||
suggestions.addAll(generatePossibleStemVariations(dbSuggestions));
|
||||
suggestMoreWords(dbSuggestions);
|
||||
suggestions.addAll(predictions.getList());
|
||||
|
||||
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
|
||||
* 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
|
||||
protected String adjustSuggestionTextCase(String word, int newTextCase) {
|
||||
switch (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;
|
||||
}
|
||||
return autoTextCase.adjustSuggestionTextCase(language, word, newTextCase);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
public void determineNextWordTextCase(SettingsStore settings, boolean isThereText, String textBeforeCursor) {
|
||||
if (!settings.getAutoTextCase()) {
|
||||
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;
|
||||
textCase = autoTextCase.determineNextWordTextCase(isThereText, textCase, textFieldTextCase, textBeforeCursor);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void nextTextCase() {
|
||||
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
|
||||
public boolean shouldAddAutoSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int incomingKey, boolean hold, boolean repeat) {
|
||||
return
|
||||
settings.getAutoSpace()
|
||||
&& !hold
|
||||
&& (
|
||||
shouldAddAutoSpaceAfterPunctuation(inputType, incomingKey, repeat)
|
||||
|| shouldAddAutoSpaceAfterWord(inputType, isWordAcceptedManually)
|
||||
)
|
||||
&& !textField.isThereSpaceAhead();
|
||||
return autoSpace
|
||||
.setLastWord(lastAcceptedWord)
|
||||
.setLastSequence(lastAcceptedSequence)
|
||||
.setInputType(inputType)
|
||||
.setTextField(textField)
|
||||
.shouldAddAutoSpace(isWordAcceptedManually, incomingKey, hold, repeat);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
public boolean shouldDeletePrecedingSpace(InputType inputType) {
|
||||
return
|
||||
settings.getAutoSpace()
|
||||
&& (
|
||||
lastAcceptedWord.equals(".")
|
||||
|| lastAcceptedWord.equals(",")
|
||||
|| lastAcceptedWord.equals(";")
|
||||
|| lastAcceptedWord.equals(":")
|
||||
|| lastAcceptedWord.equals("!")
|
||||
|| lastAcceptedWord.equals("?")
|
||||
|| lastAcceptedWord.equals(")")
|
||||
|| lastAcceptedWord.equals("]")
|
||||
|| lastAcceptedWord.equals("'")
|
||||
|| lastAcceptedWord.equals("@")
|
||||
)
|
||||
&& !inputType.isSpecialized();
|
||||
return autoSpace
|
||||
.setLastWord(lastAcceptedWord)
|
||||
.setLastSequence(lastAcceptedSequence)
|
||||
.setInputType(inputType)
|
||||
.setTextField(null)
|
||||
.shouldDeletePrecedingSpace();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
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