1
0
Fork 0

* fixed not updating the priority of uppercase words (#76)

* OK button now always accepts the entire current suggestion (as it should)

* LEFT button accepts the current word as-is

* in Predictive mode, when there are no more dictionary matches after the last key pressed, suggest all words ending with the letters for that key, instead of only the first one

* OK button now performs the default action when supported by the application (e.g. submit a message, go to a web page, etc...)

* smarter automatic text case selection in Predictive mode

* suggestion stem filter in Predictive mode

* all emoji are graphical

* updated the docs
This commit is contained in:
Dimo Karaivanov 2022-10-17 08:50:46 +03:00 committed by GitHub
parent 575293edb9
commit acb48b7999
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 399 additions and 157 deletions

View file

@ -3,6 +3,9 @@ TT9 is an IME (Input Method Editor) for Android devices with hardware keypad. It
This is an updated version of the [original project](https://github.com/Clam-/TraditionalT9) by Clam-.
## Using Traditional T9
If you just wish to install and use TT9, see the [user manual](docs/user-manual.md). You don't need to read anything below this line.
## Building
The recommended way of building is using Android Studio. As the of time of writing this, the current version is: Android Studio Dolphin | 2021.3.1.
@ -53,9 +56,6 @@ To support a new language one needs to:
- Add new entries in `res/values/arrays.xml`.
- Add translations in `res/values/strings-your-lang`. The Android Studio translation editor is very handy.
## Using the app
See the [user manual](docs/user-manual.md).
## Word Lists
Here is detailed information and licenses about the word lists used:
- [Bulgarian word list](docs/bgWordlistReadme.txt)

View file

@ -14,7 +14,7 @@ _The actual menu names may vary depending on your Android version and phone._
### Enabling Predictive Mode
With the default settings, it is only possible to type in 123 and ABC modes. In order to enable the Predictive mode, there are additional steps:
- Open the [Configuration screen](#configuration-options).
- Open the [Settings screen](#settings-screen).
- Select the desired languages.
- Load the dictionaries.
@ -23,19 +23,30 @@ _If you don't do the above, there will be no suggestions when typing in Predicti
## Hotkeys
#### D-pad Up (↑):
Select previous word suggestion.
Select previous word/letter suggestion.
#### D-pad Down (↓):
Select next word suggestion.
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"...
#### D-pad Left (←):
_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
- 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".
- **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
- In 123 mode: type the respective number.
- In ABC and Predictive mode: type a letter/punctuation character or hold to type the respective number.
- **In 123 mode:** type the respective number.
- **In ABC and Predictive mode:** type a letter/punctuation character or hold to type the respective number.
#### Text Mode Key (Hash/Pound/#):
- **Short press:** Cycle input modes (abc → ABC → Predictive → 123)
@ -59,12 +70,12 @@ Select next word suggestion.
All functionality is available using the keypad, but for convenience, on touchscreen phones or the ones with customizable function keys, you could also use the on-screen soft keys.
#### Left Soft Key:
Open the [Configuration screen](#configuration-options).
Open the [Settings screen](#settings-screen).
#### Right Soft Key:
Backspace.
## Configuration Options
## Settings Screen
On the Configuration screen, you can choose your preferred languages, load a dictionary for Predictive mode or view this manual.
To access it:

View file

@ -72,15 +72,6 @@ public class DictionaryDb {
}
private static void sendSuggestions(Handler handler, ArrayList<String> data) {
Bundle bundle = new Bundle();
bundle.putStringArrayList("suggestions", data);
Message msg = new Message();
msg.setData(bundle);
handler.sendMessage(msg);
}
public static void truncateWords(Handler handler) {
new Thread() {
@Override
@ -134,6 +125,8 @@ public class DictionaryDb {
public static void incrementWordFrequency(Language language, String word, String sequence) throws Exception {
Logger.d("incrementWordFrequency", "Incrementing priority of Word: " + word +" | Sequence: " + sequence);
if (language == null) {
throw new InvalidLanguageException();
}
@ -146,18 +139,19 @@ public class DictionaryDb {
// If one of them is empty, then this is an invalid operation,
// because a digit sequence exist for every word.
if (word == null || word.length() == 0 || sequence == null || sequence.length() == 0) {
throw new Exception("Cannot increment word frequency. Word: '" + word + "', Sequence: '" + sequence + "'");
throw new Exception("Cannot increment word frequency. Word: " + word + " | Sequence: " + sequence);
}
new Thread() {
@Override
public void run() {
try {
getInstance().wordsDao().incrementFrequency(language.getId(), word, sequence);
int affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), word.toLowerCase(language.getLocale()), sequence);
Logger.d("incrementWordFrequency", "Affected rows: " + affectedRows);
} catch (Exception e) {
Logger.e(
DictionaryDb.class.getName(),
"Failed incrementing word frequency. Word: '" + word + "', Sequence: '" + sequence + "'. " + e.getMessage()
"Failed incrementing word frequency. Word: " + word + " | Sequence: " + sequence + ". " + e.getMessage()
);
}
}
@ -165,46 +159,93 @@ public class DictionaryDb {
}
public static void getSuggestions(Handler handler, Language language, String sequence, int minimumWords, int maximumWords) {
private static ArrayList<String> getSuggestionsExact(Language language, String sequence, String word, int maximumWords) {
long start = System.currentTimeMillis();
List<Word> exactMatches = getInstance().wordsDao().getMany(
language.getId(),
maximumWords,
sequence,
word == null || word.equals("") ? null : word
);
Logger.d(
"db.getSuggestionsExact",
"Exact matches: " + exactMatches.size() + ". Time: " + (System.currentTimeMillis() - start) + " ms"
);
ArrayList<String> suggestions = new ArrayList<>();
for (Word w : exactMatches) {
Logger.d("db.getSuggestions", "exact match: " + w.word + " | priority: " + w.frequency);
suggestions.add(w.word);
}
return suggestions;
}
private static ArrayList<String> getSuggestionsFuzzy(Language language, String sequence, String word, int maximumWords) {
long start = System.currentTimeMillis();
List<Word> extraWords = getInstance().wordsDao().getFuzzy(
language.getId(),
maximumWords,
sequence,
word == null || word.equals("") ? null : word
);
Logger.d(
"db.getSuggestionsFuzzy",
"Fuzzy matches: " + extraWords.size() + ". Time: " + (System.currentTimeMillis() - start) + " ms"
);
ArrayList<String> suggestions = new ArrayList<>();
for (Word w : extraWords) {
Logger.d(
"db.getSuggestions",
"fuzzy match: " + w.word + " | sequence: " + w.sequence + " | priority: " + w.frequency
);
suggestions.add(w.word);
}
return suggestions;
}
private static void sendSuggestions(Handler handler, ArrayList<String> data) {
Bundle bundle = new Bundle();
bundle.putStringArrayList("suggestions", data);
Message msg = new Message();
msg.setData(bundle);
handler.sendMessage(msg);
}
public static void getSuggestions(Handler handler, Language language, String sequence, String word, int minimumWords, int maximumWords) {
final int minWords = Math.max(minimumWords, 0);
final int maxWords = Math.max(maximumWords, minimumWords);
if (sequence == null || sequence.length() == 0) {
Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for an empty sequence.");
sendSuggestions(handler, new ArrayList<>());
return;
}
if (language == null) {
Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for NULL language.");
sendSuggestions(handler, new ArrayList<>());
return;
}
new Thread() {
@Override
public void run() {
if (sequence == null || sequence.length() == 0) {
Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for an empty sequence.");
sendSuggestions(handler, new ArrayList<>());
return;
}
if (language == null) {
Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for NULL language.");
sendSuggestions(handler, new ArrayList<>());
return;
}
// get exact sequence matches, for example: "9422" -> "what"
List<Word> exactMatches = getInstance().wordsDao().getMany(language.getId(), sequence, maxWords);
Logger.d("db.getSuggestions", "Exact matches: " + exactMatches.size());
ArrayList<String> suggestions = getSuggestionsExact(language, sequence, word, maxWords);
ArrayList<String> suggestions = new ArrayList<>();
for (Word word : exactMatches) {
Logger.d("db.getSuggestions", "exact match: " + word.word + ", priority: " + word.frequency);
suggestions.add(word.word);
}
// if the exact matches are too few, add some more words that start with the same characters,
// for example: "rol" => "roll", "roller", "rolling", ...
if (exactMatches.size() < minWords && sequence.length() >= 2) {
int extraWordsNeeded = minWords - exactMatches.size();
List<Word> extraWords = getInstance().wordsDao().getFuzzy(language.getId(), sequence, extraWordsNeeded);
Logger.d("db.getSuggestions", "Fuzzy matches: " + extraWords.size());
for (Word word : extraWords) {
Logger.d("db.getSuggestions", "fuzzy match: " + word.word + ", sequence: " + word.sequence);
suggestions.add(word.word);
}
// for example: "rol" -> "roll", "roller", "rolling", ...
if (suggestions.size() < minWords && sequence.length() >= 2) {
suggestions.addAll(
getSuggestionsFuzzy(language, sequence, word, minWords - suggestions.size())
);
}
if (suggestions.size() == 0) {
@ -215,6 +256,5 @@ public class DictionaryDb {
sendSuggestions(handler, suggestions);
}
}.start();
}
}

View file

@ -12,20 +12,26 @@ interface WordsDao {
@Query(
"SELECT * " +
"FROM words " +
"WHERE lang = :langId AND seq = :sequence " +
"WHERE " +
"lang = :langId " +
"AND seq = :sequence " +
"AND (:word IS NULL OR word LIKE :word || '%') " +
"ORDER BY freq DESC " +
"LIMIT :limit"
)
List<Word> getMany(int langId, String sequence, int limit);
List<Word> getMany(int langId, int limit, String sequence, String word);
@Query(
"SELECT * " +
"FROM words " +
"WHERE lang = :langId AND seq > :sequence AND seq <= :sequence || '99' " +
"ORDER BY freq DESC, seq ASC " +
"WHERE " +
"lang = :langId " +
"AND seq > :sequence AND seq <= :sequence || '99' " +
"AND (:word IS NULL OR word LIKE :word || '%') " +
"ORDER BY freq DESC, LENGTH(seq) ASC, seq ASC " +
"LIMIT :limit"
)
List<Word> getFuzzy(int langId, String sequence, int limit);
List<Word> getFuzzy(int langId, int limit, String sequence, String word);
@Insert
void insert(Word word);
@ -36,7 +42,7 @@ interface WordsDao {
@Query(
"UPDATE words " +
"SET freq = (SELECT IFNULL(MAX(freq), 0) FROM words WHERE lang = :langId AND seq = :sequence AND word <> :word) + 1 " +
"WHERE lang = :langId AND word = :word "
"WHERE lang = :langId AND word = :word AND seq = :sequence"
)
void incrementFrequency(int langId, String word, String sequence);
int incrementFrequency(int langId, String word, String sequence);
}

View file

@ -186,7 +186,8 @@ abstract class KeyPadHandler extends InputMethodService {
|| keyCode == KeyEvent.KEYCODE_STAR
|| keyCode == KeyEvent.KEYCODE_POUND
|| (isNumber(keyCode) && shouldTrackNumPress())
|| ((keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && shouldTrackArrows())
|| ((keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && shouldTrackUpDown())
|| ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) && shouldTrackLeftRight())
|| (mEditing != EDITING_NOSHOW && keyCode == KeyEvent.KEYCODE_DPAD_CENTER)
) {
return true;
@ -296,6 +297,8 @@ abstract class KeyPadHandler extends InputMethodService {
case KeyEvent.KEYCODE_DPAD_CENTER: return onOK();
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_1:
case KeyEvent.KEYCODE_2:
case KeyEvent.KEYCODE_3:
@ -370,7 +373,8 @@ abstract class KeyPadHandler extends InputMethodService {
}
// toggle handlers
abstract protected boolean shouldTrackArrows();
abstract protected boolean shouldTrackUpDown();
abstract protected boolean shouldTrackLeftRight();
abstract protected boolean shouldTrackNumPress();
// default hardware key handlers
@ -378,6 +382,8 @@ abstract class KeyPadHandler extends InputMethodService {
abstract public boolean onOK();
abstract protected boolean onUp();
abstract protected boolean onDown();
abstract protected boolean onLeft();
abstract protected boolean onRight();
abstract protected boolean onNumber(int key, boolean hold, boolean repeat);
abstract protected boolean onStar();
abstract protected boolean onPound();

View file

@ -37,7 +37,6 @@ public class TraditionalT9 extends KeyPadHandler {
private SoftKeyHandler softKeyHandler = null;
public static Context getMainContext() {
return self.getApplicationContext();
}
@ -75,6 +74,8 @@ public class TraditionalT9 extends KeyPadHandler {
protected void onRestart(EditorInfo inputField) {
determineNextTextCase();
// determine the valid state for the current input field and preferences
determineAllowedInputModes(inputField);
determineAllowedTextCases();
@ -120,7 +121,7 @@ public class TraditionalT9 extends KeyPadHandler {
if (mInputMode.onBackspace()) {
getSuggestions();
} else {
commitCurrentSuggestion();
commitCurrentSuggestion(false);
super.sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
}
@ -130,25 +131,49 @@ public class TraditionalT9 extends KeyPadHandler {
public boolean onOK() {
Logger.d("onOK", "enter handler");
if (isSuggestionViewHidden() && currentInputConnection != null) {
return sendDefaultEditorAction(false);
}
acceptCurrentSuggestion();
mInputMode.onAcceptSuggestion(mLanguage, mSuggestionView.getCurrentSuggestion());
commitCurrentSuggestion();
determineNextTextCase();
resetKeyRepeat();
mInputMode.reset();
return !isSuggestionViewHidden();
return true;
}
protected boolean onUp() {
return previousSuggestion();
}
protected boolean onDown() {
return nextSuggestion();
}
protected boolean onLeft() {
if (mInputMode.isStemFilterOn()) {
mInputMode.clearStemFilter();
mInputMode.getSuggestionsAsync(handleSuggestionsAsync, mLanguage, getComposingText());
} else {
jumpBeforeComposingText();
}
return true;
}
protected boolean onRight() {
String filter = mSuggestionView.getCurrentSuggestion();
if (mInputMode.setStemFilter(mLanguage, filter)) {
mInputMode.getSuggestionsAsync(handleSuggestionsAsync, mLanguage, filter);
} else if (filter.length() == 0) {
mInputMode.reset();
}
return true;
}
/**
* onNumber
*
@ -159,7 +184,13 @@ public class TraditionalT9 extends KeyPadHandler {
*/
protected boolean onNumber(int key, boolean hold, boolean repeat) {
if (mInputMode.shouldAcceptCurrentSuggestion(key, hold, repeat)) {
acceptCurrentSuggestion();
mInputMode.onAcceptSuggestion(mLanguage, getComposingText());
commitCurrentSuggestion(false);
determineNextTextCase();
} else if (!InputFieldHelper.isThereText(currentInputConnection)) {
// it would have been nice to determine the text case on every key press,
// but it is somewhat resource-intensive
determineNextTextCase();
}
if (!mInputMode.onNumber(mLanguage, key, hold, repeat)) {
@ -172,7 +203,7 @@ public class TraditionalT9 extends KeyPadHandler {
}
if (mInputMode.getWord() != null) {
setText(mInputMode.getWord());
commitText(mInputMode.getWord());
} else {
getSuggestions();
}
@ -182,13 +213,13 @@ public class TraditionalT9 extends KeyPadHandler {
protected boolean onPound() {
setText("#");
commitText("#");
return true;
}
protected boolean onStar() {
setText("*");
commitText("*");
return true;
}
@ -228,8 +259,12 @@ public class TraditionalT9 extends KeyPadHandler {
}
protected boolean shouldTrackArrows() {
return mEditing != EDITING_NOSHOW && !isSuggestionViewHidden();
protected boolean shouldTrackUpDown() {
return mEditing != EDITING_NOSHOW && !isSuggestionViewHidden() && mInputMode.shouldTrackUpDown();
}
protected boolean shouldTrackLeftRight() {
return mEditing != EDITING_NOSHOW && !isSuggestionViewHidden() && mInputMode.shouldTrackLeftRight();
}
@ -261,44 +296,17 @@ public class TraditionalT9 extends KeyPadHandler {
return true;
}
private void handleSuggestions(ArrayList<String> suggestions, int maxWordLength) {
setSuggestions(suggestions);
// Put the first suggestion in the text field,
// but cut it off to the length of the sequence (how many keys were pressed),
// for a more intuitive experience.
String word = mSuggestionView.getCurrentSuggestion();
word = word.substring(0, Math.min(maxWordLength, word.length()));
setComposingText(word);
}
private final Handler handleSuggestionsAsync = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
handleSuggestions(
msg.getData().getStringArrayList("suggestions"),
msg.getData().getInt("maxWordLength", 1000)
);
}
};
private void acceptCurrentSuggestion() {
mInputMode.onAcceptSuggestion(mLanguage, mSuggestionView.getCurrentSuggestion());
commitCurrentSuggestion();
}
private void commitCurrentSuggestion() {
// commit the current suggestion to the input field
if (!isSuggestionViewHidden()) {
if (mSuggestionView.getCurrentSuggestion().equals(" ")) {
// finishComposingText() seems to ignore a single space,
// so we have to force commit it.
setText(" ");
} else {
currentInputConnection.finishComposingText();
commitCurrentSuggestion(true);
}
private void commitCurrentSuggestion(boolean entireSuggestion) {
if (!isSuggestionViewHidden() && currentInputConnection != null) {
if (entireSuggestion) {
setComposingTextFromCurrentSuggestion();
}
currentInputConnection.finishComposingText();
}
setSuggestions(null);
@ -309,17 +317,39 @@ public class TraditionalT9 extends KeyPadHandler {
setSuggestions(null);
if (currentInputConnection != null) {
setComposingTextFromCurrentSuggestion();
setComposingText("");
currentInputConnection.finishComposingText();
}
}
private void getSuggestions() {
if (!mInputMode.getSuggestionsAsync(handleSuggestionsAsync, mLanguage, mSuggestionView.getCurrentSuggestion())) {
handleSuggestions(mInputMode.getSuggestions(), 1);
handleSuggestions(mInputMode.getSuggestions());
}
}
private void handleSuggestions(ArrayList<String> suggestions) {
setSuggestions(suggestions);
// Put the first suggestion in the text field,
// but cut it off to the length of the sequence (how many keys were pressed),
// for a more intuitive experience.
String word = mSuggestionView.getCurrentSuggestion();
word = word.substring(0, Math.min(mInputMode.getSequenceLength(), word.length()));
setComposingText(word);
}
private final Handler handleSuggestionsAsync = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
handleSuggestions(msg.getData().getStringArrayList("suggestions"));
}
};
private void setSuggestions(List<String> suggestions) {
if (mSuggestionView == null) {
return;
@ -332,14 +362,35 @@ public class TraditionalT9 extends KeyPadHandler {
setCandidatesViewShown(show);
}
private void setText(String text) {
if (text != null) {
currentInputConnection.commitText(text, text.length());
private void commitText(String text) {
if (text != null && currentInputConnection != null) {
currentInputConnection.commitText(text, 1);
}
}
private String getComposingText() {
String text = mSuggestionView.getCurrentSuggestion();
if (text.length() > 0 && text.length() > mInputMode.getSequenceLength()) {
text = text.substring(0, mInputMode.getSequenceLength());
}
return text;
}
private void setComposingText(String text) {
currentInputConnection.setComposingText(text, 1);
if (text != null && currentInputConnection != null) {
currentInputConnection.setComposingText(text, 1);
}
}
private void setComposingTextFromCurrentSuggestion() {
if (!isSuggestionViewHidden()) {
setComposingText(mSuggestionView.getCurrentSuggestion());
}
}
@ -356,7 +407,7 @@ public class TraditionalT9 extends KeyPadHandler {
mTextCase = allowedTextCases.get(modeIndex);
mSuggestionView.changeCase(mTextCase, mLanguage.getLocale());
setComposingTextFromCurrentSuggestion();
setComposingText(getComposingText()); // no mistake, this forces the new text case
}
// make "abc" and "ABC" separate modes from user perspective
else if (mInputMode.isABC() && mTextCase == InputMode.CASE_LOWER) {
@ -375,12 +426,6 @@ public class TraditionalT9 extends KeyPadHandler {
UI.updateStatusIcon(this, mLanguage, mInputMode, mTextCase);
}
private void setComposingTextFromCurrentSuggestion() {
if (!isSuggestionViewHidden()) {
setComposingText(mSuggestionView.getCurrentSuggestion());
}
}
private void nextLang() {
if (mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER) {
@ -403,6 +448,17 @@ public class TraditionalT9 extends KeyPadHandler {
}
private void jumpBeforeComposingText() {
if (currentInputConnection != null) {
currentInputConnection.setComposingText(getComposingText(), 0);
currentInputConnection.finishComposingText();
}
setSuggestions(null);
mInputMode.reset();
}
private void determineAllowedInputModes(EditorInfo inputField) {
allowedInputModes = InputFieldHelper.determineInputModes(inputField);
@ -431,8 +487,19 @@ public class TraditionalT9 extends KeyPadHandler {
}
private void determineNextTextCase() {
int nextTextCase = mInputMode.getNextWordTextCase(
mTextCase,
InputFieldHelper.isThereText(currentInputConnection),
(String) currentInputConnection.getTextBeforeCursor(50, 0)
);
mTextCase = nextTextCase != -1 ? nextTextCase : mTextCase;
}
private void showAddWord() {
acceptCurrentSuggestion();
currentInputConnection.finishComposingText();
clearSuggestions();
UI.showAddWordDialog(this, mLanguage.getId(), InputFieldHelper.getSurroundingWord(currentInputConnection));
@ -453,7 +520,7 @@ public class TraditionalT9 extends KeyPadHandler {
try {
Logger.d("restoreAddedWordIfAny", "Restoring word: '" + word + "'...");
setText(word);
commitText(word);
mInputMode.reset();
} catch (Exception e) {
Logger.w("tt9/restoreLastWord", "Could not restore the last added word. " + e.getMessage());

View file

@ -16,9 +16,9 @@ abstract public class InputMode {
public static final int MODE_123 = 2;
// text case
public static final int CASE_LOWER = 0;
public static final int CASE_UPPER = 0;
public static final int CASE_CAPITALIZE = 1;
public static final int CASE_UPPER = 2;
public static final int CASE_LOWER = 2;
protected ArrayList<Integer> allowedTextCases = new ArrayList<>();
// data
@ -47,10 +47,9 @@ abstract public class InputMode {
public void onAcceptSuggestion(Language language, String suggestion) {}
public ArrayList<String> getSuggestions() { return suggestions; }
public boolean getSuggestionsAsync(Handler handler, Language language, String lastWord) { return false; }
protected void sendSuggestions(Handler handler, ArrayList<String> suggestions, int maxWordLength) {
protected void sendSuggestions(Handler handler, ArrayList<String> suggestions) {
Bundle bundle = new Bundle();
bundle.putStringArrayList("suggestions", suggestions);
bundle.putInt("maxWordLength", maxWordLength);
Message msg = new Message();
msg.setData(bundle);
handler.sendMessage(msg);
@ -67,11 +66,23 @@ abstract public class InputMode {
// Utility
abstract public int getId();
public ArrayList<Integer> getAllowedTextCases() { return allowedTextCases; }
// Perform any special logic to determine the text case of the next word, or return "-1" if there is no need to change it.
public int getNextWordTextCase(int currentTextCase, boolean isThereText, String textBeforeCursor) { return -1; }
abstract public int getSequenceLength(); // The number of key presses for the current word.
public void reset() {
suggestions = new ArrayList<>();
word = null;
}
// 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 shouldTrackNumPress() { return true; }
public boolean shouldTrackUpDown() { return false; }
public boolean shouldTrackLeftRight() { return false; }
public boolean shouldAcceptCurrentSuggestion(int key, boolean hold, boolean repeat) { return false; }
public boolean shouldSelectNextSuggestion() { return false; }
}

View file

@ -24,5 +24,6 @@ public class Mode123 extends InputMode {
final public boolean is123() { return true; }
public int getSequenceLength() { return 0; }
public boolean shouldTrackNumPress() { return false; }
}

View file

@ -31,7 +31,11 @@ public class ModeABC extends InputMode {
final public boolean isABC() { return true; }
public int getSequenceLength() { return 1; }
public boolean shouldAcceptCurrentSuggestion(int key, boolean hold, boolean repeat) { return hold || !repeat; }
public boolean shouldTrackUpDown() { return true; }
public boolean shouldTrackLeftRight() { return true; }
public boolean shouldSelectNextSuggestion() {
return shouldSelectNextLetter;
}

View file

@ -5,7 +5,8 @@ import android.os.Looper;
import android.os.Message;
import java.util.ArrayList;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.db.DictionaryDb;
@ -16,33 +17,47 @@ import io.github.sspanak.tt9.preferences.T9Preferences;
public class ModePredictive extends InputMode {
public int getId() { return MODE_PREDICTIVE; }
private Language currentLanguage = null;
private boolean isEmoji = false;
private String digitSequence = "";
private boolean isEmoticon = false;
// stem filter
private String stemFilter = "";
private final int STEM_FILTER_MIN_LENGTH = 2;
// async suggestion handling
private Language currentLanguage = null;
private String lastInputFieldWord = "";
private static Handler handleSuggestionsExternal;
ModePredictive() {
allowedTextCases.add(CASE_UPPER);
allowedTextCases.add(CASE_CAPITALIZE);
allowedTextCases.add(CASE_LOWER);
allowedTextCases.add(CASE_UPPER);
}
public boolean onBackspace() {
if (stemFilter.length() < STEM_FILTER_MIN_LENGTH) {
stemFilter = "";
}
if (digitSequence.length() < 1) {
stemFilter = "";
return false;
}
digitSequence = digitSequence.substring(0, digitSequence.length() - 1);
if (stemFilter.length() > digitSequence.length()) {
stemFilter = stemFilter.substring(0, digitSequence.length() - 1);
}
return true;
}
public boolean onNumber(Language l, int key, boolean hold, boolean repeat) {
isEmoticon = false;
isEmoji = false;
if (hold) {
// hold to type any digit
@ -55,8 +70,8 @@ public class ModePredictive extends InputMode {
} else if (key == 1 && repeat) {
// emoticons
reset();
isEmoticon = true;
suggestions = Punctuation.Emoticons;
isEmoji = true;
suggestions = Punctuation.Emoji;
}
else {
// words
@ -71,6 +86,7 @@ public class ModePredictive extends InputMode {
public void reset() {
super.reset();
digitSequence = "";
stemFilter = "";
}
@ -78,6 +94,11 @@ public class ModePredictive extends InputMode {
return true;
}
public int getSequenceLength() { return isEmoji ? 2 : digitSequence.length(); }
public boolean shouldTrackUpDown() { return true; }
public boolean shouldTrackLeftRight() { return true; }
/**
* shouldAcceptCurrentSuggestion
@ -97,6 +118,47 @@ public class ModePredictive extends InputMode {
}
/**
* isStemFilterOn
* Returns "true" if a filter was applied using "setStem()".
*/
public boolean isStemFilterOn() {
return stemFilter.length() > 0;
}
/**
* 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.
*
* 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) {
return false;
}
try {
digitSequence = language.getDigitSequenceForWord(stem);
stemFilter = stem;
return true;
} catch (Exception e) {
Logger.w("tt9/setStemFilter", "Ignoring invalid stem filter: " + stem + ". " + e.getMessage());
return false;
}
}
/**
* getSuggestionsAsync
* Queries the dictionary database for a list of suggestions matching the current language and
@ -106,8 +168,8 @@ public class ModePredictive extends InputMode {
* See: generateSuggestionWhenNone()
*/
public boolean getSuggestionsAsync(Handler handler, Language language, String lastWord) {
if (isEmoticon) {
super.sendSuggestions(handler, suggestions, 2);
if (isEmoji) {
super.sendSuggestions(handler, suggestions);
return true;
}
@ -124,6 +186,7 @@ public class ModePredictive extends InputMode {
handleSuggestions,
language,
digitSequence,
stemFilter,
T9Preferences.getInstance().getSuggestionsMin(),
T9Preferences.getInstance().getSuggestionsMax()
);
@ -143,7 +206,7 @@ public class ModePredictive extends InputMode {
ArrayList<String> suggestions = msg.getData().getStringArrayList("suggestions");
suggestions = generateSuggestionWhenNone(suggestions, currentLanguage, lastInputFieldWord);
ModePredictive.super.sendSuggestions(handleSuggestionsExternal, suggestions, digitSequence.length());
ModePredictive.super.sendSuggestions(handleSuggestionsExternal, suggestions);
}
};
@ -152,9 +215,9 @@ public class ModePredictive extends InputMode {
* 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.
*/
private ArrayList<String> generateSuggestionWhenNone(ArrayList<String> suggestions, Language language, String lastWord) {
private ArrayList<String> generateSuggestionWhenNone(ArrayList<String> suggestions, Language language, String word) {
if (
(lastWord == null || lastWord.length() == 0) ||
(word == null || word.length() == 0) ||
(suggestions != null && suggestions.size() > 0) ||
digitSequence.length() == 0 ||
digitSequence.charAt(0) == '1'
@ -162,15 +225,23 @@ public class ModePredictive extends InputMode {
return suggestions;
}
lastWord = lastWord.substring(0, Math.min(digitSequence.length() - 1, lastWord.length()));
try {
int lastDigit = digitSequence.charAt(digitSequence.length() - 1) - '0';
lastWord += language.getKeyCharacters(lastDigit).get(0);
} catch (Exception e) {
lastWord += digitSequence.charAt(digitSequence.length() - 1);
// append all letters for the last key
word = word.substring(0, Math.min(digitSequence.length() - 1, word.length()));
ArrayList<String> generatedSuggestions = new ArrayList<>();
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);
}
}
return new ArrayList<>(Collections.singletonList(lastWord));
// if there are no letters for this key, just append the number
if (generatedSuggestions.size() == 0) {
generatedSuggestions.add(word + digitSequence.charAt(digitSequence.length() - 1));
}
return generatedSuggestions;
}
@ -179,7 +250,7 @@ public class ModePredictive extends InputMode {
* Bring this word up in the suggestions list next time.
*/
public void onAcceptSuggestion(Language language, String currentWord) {
digitSequence = "";
reset();
if (currentWord.length() == 0) {
Logger.i("acceptCurrentSuggestion", "Current word is empty. Nothing to accept.");
@ -193,4 +264,30 @@ public class ModePredictive extends InputMode {
Logger.e("tt9/ModePredictive", "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage());
}
}
/**
* getNextWordTextCase
* 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 getNextWordTextCase(int currentTextCase, boolean isThereText, String textBeforeCursor) {
// If the user wants to type in uppercase, this must be for a reason, so we better not override it.
if (currentTextCase == CASE_UPPER) {
return -1;
}
// start of text
if (!isThereText) {
return CASE_CAPITALIZE;
}
// start of sentence, excluding after "..."
Matcher endOfSentenceMatch = Pattern.compile("(?<!\\.)[.?!]\\s*$").matcher(textBeforeCursor);
if (endOfSentenceMatch.find()) {
return CASE_CAPITALIZE;
}
return CASE_LOWER;
}
}

View file

@ -12,8 +12,7 @@ public class Punctuation {
" ", "+", "\n"
));
final public static ArrayList<String> Emoticons = new ArrayList<>(Arrays.asList(
"👍", ":)", ":D", ";)", ":(", ":P"
final public static ArrayList<String> Emoji = new ArrayList<>(Arrays.asList(
"👍", "🙂", "😀", "😉", "🙁", "😢", "😛", "😬"
));
}