1
0
Fork 0

More Predictive mode improvements (#82)

* Predictive mode now allows typing new/unknown words entirely, instead of allowing only existing word variations

* suggestions can now be filtered using the DPAD Right key
This commit is contained in:
Dimo Karaivanov 2022-10-24 13:40:08 +03:00 committed by GitHub
parent 8b67929a07
commit 0ac7ec1790
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 76 deletions

View file

@ -27,7 +27,11 @@ abstract class KeyPadHandler extends InputMethodService {
// temporal key handling
private int ignoreNextKeyUp = 0;
private int lastKeyCode = 0;
private boolean isKeyRepeated = false;
private int lastNumKeyCode = 0;
private boolean isNumKeyRepeated = false;
// throttling
@ -255,9 +259,12 @@ abstract class KeyPadHandler extends InputMethodService {
return true;
}
isKeyRepeated = (lastKeyCode == keyCode);
lastKeyCode = keyCode;
if (isNumber(keyCode)) {
isNumKeyRepeated = (lastKeyCode == keyCode);
lastKeyCode = keyCode;
isNumKeyRepeated = (lastNumKeyCode == keyCode);
lastNumKeyCode = keyCode;
}
// Logger.d("onKeyUp", "Key: " + keyCode + " repeat?: " + event.getRepeatCount());
@ -298,7 +305,7 @@ abstract class KeyPadHandler extends InputMethodService {
case KeyEvent.KEYCODE_DPAD_UP: return onUp();
case KeyEvent.KEYCODE_DPAD_DOWN: return onDown();
case KeyEvent.KEYCODE_DPAD_LEFT: return onLeft();
case KeyEvent.KEYCODE_DPAD_RIGHT: return onRight();
case KeyEvent.KEYCODE_DPAD_RIGHT: return onRight(isKeyRepeated);
case KeyEvent.KEYCODE_1:
case KeyEvent.KEYCODE_2:
case KeyEvent.KEYCODE_3:
@ -341,7 +348,7 @@ abstract class KeyPadHandler extends InputMethodService {
protected void resetKeyRepeat() {
isNumKeyRepeated = false;
lastKeyCode = 0;
lastNumKeyCode = 0;
}
@ -383,7 +390,7 @@ abstract class KeyPadHandler extends InputMethodService {
abstract protected boolean onUp();
abstract protected boolean onDown();
abstract protected boolean onLeft();
abstract protected boolean onRight();
abstract protected boolean onRight(boolean repeat);
abstract protected boolean onNumber(int key, boolean hold, boolean repeat);
abstract protected boolean onStar();
abstract protected boolean onPound();

View file

@ -145,15 +145,24 @@ public class TraditionalT9 extends KeyPadHandler {
protected boolean onUp() {
return previousSuggestion();
if (previousSuggestion()) {
mInputMode.setWordStem(mLanguage, mSuggestionView.getCurrentSuggestion(), true);
return true;
}
return false;
}
protected boolean onDown() {
return nextSuggestion();
if (nextSuggestion()) {
mInputMode.setWordStem(mLanguage, mSuggestionView.getCurrentSuggestion(), true);
return true;
}
return false;
}
protected boolean onLeft() {
if (mInputMode.isStemFilterOn()) {
mInputMode.clearStemFilter();
if (mInputMode.clearWordStem()) {
mInputMode.getSuggestionsAsync(handleSuggestionsAsync, mLanguage, getComposingText());
} else {
jumpBeforeComposingText();
@ -162,10 +171,10 @@ public class TraditionalT9 extends KeyPadHandler {
return true;
}
protected boolean onRight() {
String filter = mSuggestionView.getCurrentSuggestion();
protected boolean onRight(boolean repeat) {
String filter = repeat ? mSuggestionView.getSuggestion(1) : getComposingText();
if (mInputMode.setStemFilter(mLanguage, filter)) {
if (mInputMode.setWordStem(mLanguage, filter, repeat)) {
mInputMode.getSuggestionsAsync(handleSuggestionsAsync, mLanguage, filter);
} else if (filter.length() == 0) {
mInputMode.reset();
@ -483,7 +492,7 @@ public class TraditionalT9 extends KeyPadHandler {
private void determineAllowedTextCases() {
allowedTextCases = mInputMode.getAllowedTextCases();
// @todo: determine the text case of the input and validate using the allowed ones
// @todo: determine the text case of the input and validate using the allowed ones [ https://github.com/sspanak/tt9/issues/48 ]
}

View file

@ -76,9 +76,8 @@ abstract public class InputMode {
// Stem filtering.
// Where applicable, return "true" if the mode supports it and the operation was possible.
public boolean isStemFilterOn() { return false; }
public void clearStemFilter() {}
public boolean setStemFilter(Language language, String stem) { return false; }
public boolean clearWordStem() { return false; }
public boolean setWordStem(Language language, String stem, boolean exact) { return false; }
public boolean shouldTrackNumPress() { return true; }
public boolean shouldTrackUpDown() { return false; }

View file

@ -21,8 +21,8 @@ public class ModePredictive extends InputMode {
private String digitSequence = "";
// stem filter
private String stemFilter = "";
private final int STEM_FILTER_MIN_LENGTH = 2;
private boolean isStemFuzzy = false;
private String stem = "";
// async suggestion handling
private Language currentLanguage = null;
@ -38,18 +38,16 @@ public class ModePredictive extends InputMode {
public boolean onBackspace() {
if (stemFilter.length() < STEM_FILTER_MIN_LENGTH) {
stemFilter = "";
}
if (digitSequence.length() < 1) {
stemFilter = "";
clearWordStem();
return false;
}
digitSequence = digitSequence.substring(0, digitSequence.length() - 1);
if (stemFilter.length() > digitSequence.length()) {
stemFilter = stemFilter.substring(0, digitSequence.length() - 1);
if (digitSequence.length() == 0) {
clearWordStem();
} else if (stem.length() > digitSequence.length()) {
stem = stem.substring(0, digitSequence.length() - 1);
}
return true;
@ -86,7 +84,7 @@ public class ModePredictive extends InputMode {
public void reset() {
super.reset();
digitSequence = "";
stemFilter = "";
stem = "";
}
@ -119,41 +117,49 @@ public class ModePredictive extends InputMode {
/**
* isStemFilterOn
* Returns "true" if a filter was applied using "setStem()".
* clearWordStem
* Do not filter the suggestions by the word set using "setWordStem()", use only the digit sequence.
*/
public boolean isStemFilterOn() {
return stemFilter.length() > 0;
public boolean clearWordStem() {
stem = "";
Logger.d("tt9/setWordStem", "Stem filter cleared");
return true;
}
/**
* clearStemFilter
* Do not filter the suggestions by the word set using "setStem()", use only the digit sequence.
*/
public void clearStemFilter() {
stemFilter = "";
}
/**
* setStemFilter
* Filter the possible suggestions by the given stem. The stem must have
* a minimum length of STEM_FILTER_MIN_LENGTH.
* setWordStem
* Filter the possible suggestions by the given stem.
*
* If exact is "true", the database will be filtered by "stem" and if the stem word is missing,
* it will be added to the suggestions list.
* For example: "exac_" -> "exac", {database suggestions...}
*
* If "exact" is false, in addition to the above, all possible next combinations will be
* added to the suggestions list, even if they make no sense.
* For example: "exac_" -> "exac", "exact", "exacu", "exacv", {database suggestions...}
*
*
* Note that you need to manually get the suggestions again to obtain a filtered list.
*/
public boolean setStemFilter(Language language, String stem) {
if (language == null || stem == null || stem.length() < STEM_FILTER_MIN_LENGTH) {
public boolean setWordStem(Language language, String wordStem, boolean exact) {
if (language == null || wordStem == null || wordStem.length() < 1) {
return false;
}
try {
digitSequence = language.getDigitSequenceForWord(stem);
stemFilter = stem;
digitSequence = language.getDigitSequenceForWord(wordStem);
isStemFuzzy = !exact;
stem = wordStem.toLowerCase(language.getLocale());
Logger.d("tt9/setWordStem", "Stem is now: " + wordStem);
return true;
} catch (Exception e) {
Logger.w("tt9/setStemFilter", "Ignoring invalid stem filter: " + stem + ". " + e.getMessage());
isStemFuzzy = false;
stem = "";
Logger.w("tt9/setWordStem", "Ignoring invalid stem: " + wordStem + ". " + e.getMessage());
return false;
}
}
@ -165,7 +171,7 @@ public class ModePredictive extends InputMode {
* sequence. Returns "false" when there is nothing to do.
*
* "lastWord" is used for generating suggestions when there are no results.
* See: generateSuggestionWhenNone()
* See: generatePossibleCompletions()
*/
public boolean getSuggestionsAsync(Handler handler, Language language, String lastWord) {
if (isEmoji) {
@ -178,7 +184,7 @@ public class ModePredictive extends InputMode {
}
handleSuggestionsExternal = handler;
lastInputFieldWord = lastWord;
lastInputFieldWord = lastWord.toLowerCase(language.getLocale());
currentLanguage = language;
super.reset();
@ -186,7 +192,7 @@ public class ModePredictive extends InputMode {
handleSuggestions,
language,
digitSequence,
stemFilter,
stem,
T9Preferences.getInstance().getSuggestionsMin(),
T9Preferences.getInstance().getSuggestionsMax()
);
@ -204,44 +210,87 @@ public class ModePredictive extends InputMode {
@Override
public void handleMessage(Message msg) {
ArrayList<String> suggestions = msg.getData().getStringArrayList("suggestions");
suggestions = generateSuggestionWhenNone(suggestions, currentLanguage, lastInputFieldWord);
suggestions = suggestions == null ? new ArrayList<>() : suggestions;
ModePredictive.super.sendSuggestions(handleSuggestionsExternal, suggestions);
if (suggestions.size() == 0 && digitSequence.length() > 0) {
suggestions = generatePossibleCompletions(currentLanguage, lastInputFieldWord);
}
ArrayList<String> stemVariations = generatePossibleStemVariations(currentLanguage, suggestions);
stemVariations.addAll(suggestions);
ModePredictive.super.sendSuggestions(handleSuggestionsExternal, stemVariations);
}
};
/**
* generateSuggestionWhenNone
* generatePossibleCompletions
* When there are no matching suggestions after the last key press, generate a list of possible
* ones, so that the user can complete the missing word.
* 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> generateSuggestionWhenNone(ArrayList<String> suggestions, Language language, String word) {
if (
(word == null || word.length() == 0) ||
(suggestions != null && suggestions.size() > 0) ||
digitSequence.length() == 0 ||
digitSequence.charAt(0) == '1'
) {
return suggestions;
}
private ArrayList<String> generatePossibleCompletions(Language language, String baseWord) {
ArrayList<String> generatedWords = new ArrayList<>();
// append all letters for the last key
word = word.substring(0, Math.min(digitSequence.length() - 1, word.length()));
ArrayList<String> generatedSuggestions = 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)) {
if (keyLetter.charAt(0) - '0' > '9') { // append only letters, not numbers
generatedSuggestions.add(word + keyLetter);
// 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 (generatedSuggestions.size() == 0) {
generatedSuggestions.add(word + digitSequence.charAt(digitSequence.length() - 1));
if (generatedWords.size() == 0) {
generatedWords.add(baseWord + digitSequence.charAt(digitSequence.length() - 1));
}
return generatedSuggestions;
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, similar to the ones
* in the dictionary.
*/
private ArrayList<String> generatePossibleStemVariations(Language language, ArrayList<String> currentSuggestions) {
ArrayList<String> variations = new ArrayList<>();
if (stem.length() == 0) {
return variations;
}
if (stem.length() == digitSequence.length() && !currentSuggestions.contains(stem)) {
variations.add(stem);
}
if (isStemFuzzy && stem.length() == digitSequence.length() - 1) {
for (String word : generatePossibleCompletions(language, stem)) {
if (!currentSuggestions.contains(word)) {
variations.add(word);
}
}
}
return variations;
}
@ -265,6 +314,7 @@ public class ModePredictive extends InputMode {
}
}
/**
* getNextWordTextCase
* Dynamically determine text case of words as the user types to reduce key presses.

View file

@ -178,8 +178,16 @@ public class CandidateView extends View {
invalidate();
}
public int getCurrentIndex() {
return mSelectedIndex;
}
public String getCurrentSuggestion() {
return mSuggestions != null && mSelectedIndex >= 0 && mSelectedIndex < mSuggestions.size() ? mSuggestions.get(mSelectedIndex) : "";
return getSuggestion(mSelectedIndex);
}
public String getSuggestion(int id) {
return mSuggestions != null && id >= 0 && id < mSuggestions.size() ? mSuggestions.get(id) : "";
}
public void setSuggestions(List<String> suggestions, int initialSel) {