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

@ -20,6 +20,11 @@ With the default settings, it is only possible to type in 123 and ABC modes. In
_If you don't do the above, there will be no suggestions when typing in Predictive mode._
#### Dictionary Tips
Once a dictionary is loaded, it will stay there until you use the Clear option. This means you can enable and disable languages without reloading their dictionaries every time. Just do it once, only the first time.
It also means that if you need to start using language X, you can safely disable all other languages, load only dictionary X (and save time!), then re-enable all languages you used before.
## Hotkeys
#### D-pad Up (↑):
@ -31,7 +36,10 @@ Select next word/letter suggestion.
#### D-pad Right (→):
_Predictive mode only._
Filter the suggestion list leaving out only words similar to the current suggestion. For example, "6463" results in: "nine", "mine", "mind", and so on. But selecting "mind" and pressing Right will leave out only the similar ones: "mind", "minds", "mindy", "minded"...
- **Single press**: Filter the suggestion list, leaving out only the ones that start with the current word. It doesn't matter if it is a complete word or not. For example, type "rewin" and press Right. It will leave out all words starting with "rewin": "rewin" itself, "rewind", "rewinds", "rewinded", "rewinding", and so on.
- **Double press**: Expand the filter to the full suggestion. For example, type "rewin" and press Right twice. It will first filter by "rewin", then expand the filter to "rewind". You can keep expanding the filter with Right, until you get to the longest suggestion in the list.
Filtering can also be used to type unknown words. Let's say you want to type "Anakin", which is not in the dictionary. Start with "A", then press Right to hide "B" and "C". Now press 6-key. Since the filter is on, in addition to the real dictionary words, it will provide all possible combinations for 6: "Am", "An", "Ao". Select "An" and press Right to confirm your selection. Now pressing 2-key, will provide "Ana", "Anb", "Anc". You can keep going, until you complete "Anakin".
#### D-pad Left (←):
_Predictive mode only._
@ -39,12 +47,12 @@ _Predictive mode only._
- Clear the suggestion filter, if applied.
- When no filter is applied, accept the current word as-is, even if it does not fully match a suggestion, then jump before it.
#### 0 key
#### 0-key
- **In 123 mode:** type "0" or hold it to type "+".
- **In ABC mode:** type secondary punctuation or hold to type "0".
- **In Predictive mode:** type space or hold to type "0".
#### 1 to 9 keys
#### 1- to 9-key
- **In 123 mode:** type the respective number.
- **In ABC and Predictive mode:** type a letter/punctuation character or hold to type the respective number.

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) {