separated TraditionalT9 into smaller focused handlers, as much as the IME architecture permits
This commit is contained in:
parent
58f5123bdb
commit
50fedcad13
14 changed files with 929 additions and 818 deletions
|
|
@ -2,6 +2,7 @@ package io.github.sspanak.tt9.db;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
||||||
|
|
@ -10,9 +11,6 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.util.ConsumerCompat;
|
|
||||||
import io.github.sspanak.tt9.util.Logger;
|
|
||||||
import io.github.sspanak.tt9.util.Timer;
|
|
||||||
import io.github.sspanak.tt9.db.entities.WordBatch;
|
import io.github.sspanak.tt9.db.entities.WordBatch;
|
||||||
import io.github.sspanak.tt9.db.entities.WordFile;
|
import io.github.sspanak.tt9.db.entities.WordFile;
|
||||||
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAbortedException;
|
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAbortedException;
|
||||||
|
|
@ -21,14 +19,16 @@ import io.github.sspanak.tt9.db.sqlite.DeleteOps;
|
||||||
import io.github.sspanak.tt9.db.sqlite.InsertOps;
|
import io.github.sspanak.tt9.db.sqlite.InsertOps;
|
||||||
import io.github.sspanak.tt9.db.sqlite.SQLiteOpener;
|
import io.github.sspanak.tt9.db.sqlite.SQLiteOpener;
|
||||||
import io.github.sspanak.tt9.db.sqlite.Tables;
|
import io.github.sspanak.tt9.db.sqlite.Tables;
|
||||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
|
||||||
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
|
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
|
||||||
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageException;
|
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageException;
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
import io.github.sspanak.tt9.ui.DictionaryLoadingBar;
|
import io.github.sspanak.tt9.ui.DictionaryLoadingBar;
|
||||||
import io.github.sspanak.tt9.ui.UI;
|
import io.github.sspanak.tt9.ui.UI;
|
||||||
|
import io.github.sspanak.tt9.util.ConsumerCompat;
|
||||||
|
import io.github.sspanak.tt9.util.Logger;
|
||||||
|
import io.github.sspanak.tt9.util.Timer;
|
||||||
|
|
||||||
public class DictionaryLoader {
|
public class DictionaryLoader {
|
||||||
private static final String LOG_TAG = "DictionaryLoader";
|
private static final String LOG_TAG = "DictionaryLoader";
|
||||||
|
|
@ -109,7 +109,7 @@ public class DictionaryLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void autoLoad(TraditionalT9 context, Language language) {
|
public static void autoLoad(InputMethodService context, Language language) {
|
||||||
if (getInstance(context).isRunning()) {
|
if (getInstance(context).isRunning()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.util.Logger;
|
|
||||||
import io.github.sspanak.tt9.util.Timer;
|
|
||||||
import io.github.sspanak.tt9.db.entities.Word;
|
import io.github.sspanak.tt9.db.entities.Word;
|
||||||
import io.github.sspanak.tt9.db.entities.WordList;
|
import io.github.sspanak.tt9.db.entities.WordList;
|
||||||
import io.github.sspanak.tt9.db.sqlite.DeleteOps;
|
import io.github.sspanak.tt9.db.sqlite.DeleteOps;
|
||||||
|
|
@ -15,13 +13,14 @@ import io.github.sspanak.tt9.db.sqlite.InsertOps;
|
||||||
import io.github.sspanak.tt9.db.sqlite.ReadOps;
|
import io.github.sspanak.tt9.db.sqlite.ReadOps;
|
||||||
import io.github.sspanak.tt9.db.sqlite.SQLiteOpener;
|
import io.github.sspanak.tt9.db.sqlite.SQLiteOpener;
|
||||||
import io.github.sspanak.tt9.db.sqlite.UpdateOps;
|
import io.github.sspanak.tt9.db.sqlite.UpdateOps;
|
||||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
|
||||||
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
import io.github.sspanak.tt9.languages.NullLanguage;
|
import io.github.sspanak.tt9.languages.NullLanguage;
|
||||||
import io.github.sspanak.tt9.util.Text;
|
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
import io.github.sspanak.tt9.ui.dialogs.AddWordDialog;
|
import io.github.sspanak.tt9.ui.dialogs.AddWordDialog;
|
||||||
|
import io.github.sspanak.tt9.util.Logger;
|
||||||
|
import io.github.sspanak.tt9.util.Text;
|
||||||
|
import io.github.sspanak.tt9.util.Timer;
|
||||||
|
|
||||||
|
|
||||||
public class WordStore {
|
public class WordStore {
|
||||||
|
|
@ -32,7 +31,7 @@ public class WordStore {
|
||||||
private ReadOps readOps = null;
|
private ReadOps readOps = null;
|
||||||
|
|
||||||
|
|
||||||
public WordStore(@NonNull Context context) {
|
private WordStore(@NonNull Context context) {
|
||||||
try {
|
try {
|
||||||
sqlite = SQLiteOpener.getInstance(context);
|
sqlite = SQLiteOpener.getInstance(context);
|
||||||
sqlite.getDb();
|
sqlite.getDb();
|
||||||
|
|
@ -44,9 +43,8 @@ public class WordStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static synchronized WordStore getInstance(Context context) {
|
public static synchronized WordStore getInstance(@NonNull Context context) {
|
||||||
if (self == null) {
|
if (self == null) {
|
||||||
context = context == null ? TraditionalT9.getMainContext() : context;
|
|
||||||
self = new WordStore(context);
|
self = new WordStore(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package io.github.sspanak.tt9.ime;
|
||||||
|
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
|
abstract public class AbstractHandler extends InputMethodService {
|
||||||
|
// hardware key handlers
|
||||||
|
abstract protected boolean onBack();
|
||||||
|
abstract public boolean onBackspace();
|
||||||
|
abstract public boolean onHotkey(int keyCode, boolean repeat, boolean validateOnly);
|
||||||
|
abstract protected boolean onNumber(int key, boolean hold, int repeat);
|
||||||
|
abstract public boolean onOK();
|
||||||
|
abstract public boolean onText(String text, boolean validateOnly); // used for "#", "*" and whatnot
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
abstract public SettingsStore getSettings();
|
||||||
|
abstract protected void onInit();
|
||||||
|
abstract protected void onStart(InputConnection inputConnection, EditorInfo inputField);
|
||||||
|
abstract protected void onFinishTyping();
|
||||||
|
abstract protected void onStop();
|
||||||
|
abstract protected void setInputField(InputConnection inputConnection, EditorInfo inputField);
|
||||||
|
|
||||||
|
// UI
|
||||||
|
abstract protected View createMainView();
|
||||||
|
abstract protected void createSuggestionBar(View mainView);
|
||||||
|
abstract protected void forceShowWindowIfHidden();
|
||||||
|
abstract protected void renderMainView();
|
||||||
|
abstract protected void setStatusText(String status);
|
||||||
|
abstract protected boolean shouldBeVisible();
|
||||||
|
abstract protected boolean shouldBeOff();
|
||||||
|
}
|
||||||
338
app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java
Normal file
338
app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
package io.github.sspanak.tt9.ime;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.R;
|
||||||
|
import io.github.sspanak.tt9.db.DictionaryLoader;
|
||||||
|
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||||
|
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||||
|
import io.github.sspanak.tt9.ime.modes.ModeABC;
|
||||||
|
import io.github.sspanak.tt9.ime.modes.ModePredictive;
|
||||||
|
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||||
|
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||||
|
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
|
||||||
|
import io.github.sspanak.tt9.ui.UI;
|
||||||
|
|
||||||
|
public abstract class HotkeyHandler extends TypingHandler {
|
||||||
|
private boolean isSystemRTL = false;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onInit() {
|
||||||
|
if (settings.areHotkeysInitialized()) {
|
||||||
|
Hotkeys.setDefault(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart(InputConnection connection, EditorInfo field) {
|
||||||
|
super.onStart(connection, field);
|
||||||
|
isSystemRTL = LanguageKind.isRTL(LanguageCollection.getDefault(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override public boolean onBack() {
|
||||||
|
return settings.getShowSoftNumpad();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override public boolean onOK() {
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
|
||||||
|
if (suggestionOps.isEmpty()) {
|
||||||
|
int action = textField.getAction();
|
||||||
|
return action == TextField.IME_ACTION_ENTER ? appHacks.onEnter() : textField.performAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAcceptSuggestionManually(suggestionOps.acceptCurrent(), KeyEvent.KEYCODE_ENTER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onHotkey(int keyCode, boolean repeat, boolean validateOnly) {
|
||||||
|
if (keyCode == settings.getKeyAddWord()) {
|
||||||
|
return onKeyAddWord(validateOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == settings.getKeyChangeKeyboard()) {
|
||||||
|
return onKeyChangeKeyboard(validateOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == settings.getKeyFilterClear()) {
|
||||||
|
return onKeyFilterClear(validateOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == settings.getKeyFilterSuggestions()) {
|
||||||
|
return onKeyFilterSuggestions(validateOnly, repeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == settings.getKeyNextLanguage()) {
|
||||||
|
return onKeyNextLanguage(validateOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == settings.getKeyNextInputMode()) {
|
||||||
|
return onKeyNextInputMode(validateOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == settings.getKeyPreviousSuggestion()) {
|
||||||
|
return onKeyScrollSuggestion(validateOnly, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == settings.getKeyNextSuggestion()) {
|
||||||
|
return onKeyScrollSuggestion(validateOnly, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == settings.getKeyShowSettings()) {
|
||||||
|
return onKeyShowSettings(validateOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onKeyAddWord(boolean validateOnly) {
|
||||||
|
if (!isInputViewShown() || mInputMode.isNumeric()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DictionaryLoader.getInstance(this).isRunning()) {
|
||||||
|
UI.toast(this, R.string.dictionary_loading_please_wait);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
mInputMode.onAcceptSuggestion(suggestionOps.acceptIncomplete());
|
||||||
|
|
||||||
|
String word = textField.getSurroundingWord(mLanguage);
|
||||||
|
if (word.isEmpty()) {
|
||||||
|
UI.toastLong(this, R.string.add_word_no_selection);
|
||||||
|
} else {
|
||||||
|
UI.showAddWordDialog(this, mLanguage.getId(), word);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onKeyChangeKeyboard(boolean validateOnly) {
|
||||||
|
if (!isInputViewShown()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateOnly) {
|
||||||
|
UI.showChangeKeyboardDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onKeyFilterClear(boolean validateOnly) {
|
||||||
|
if (suggestionOps.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
|
||||||
|
if (mInputMode.clearWordStem()) {
|
||||||
|
mInputMode.loadSuggestions(this::getSuggestions, suggestionOps.getCurrent(mInputMode.getSequenceLength()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mInputMode.onAcceptSuggestion(suggestionOps.acceptIncomplete());
|
||||||
|
resetKeyRepeat();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onKeyFilterSuggestions(boolean validateOnly, boolean repeat) {
|
||||||
|
if (suggestionOps.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
|
||||||
|
String filter;
|
||||||
|
if (repeat && !suggestionOps.get(1).isEmpty()) {
|
||||||
|
filter = suggestionOps.get(1);
|
||||||
|
} else {
|
||||||
|
filter = suggestionOps.getCurrent(mInputMode.getSequenceLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.isEmpty()) {
|
||||||
|
mInputMode.reset();
|
||||||
|
} else if (mInputMode.setWordStem(filter, repeat)) {
|
||||||
|
mInputMode.loadSuggestions(super::getSuggestions, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onKeyScrollSuggestion(boolean validateOnly, boolean backward) {
|
||||||
|
if (suggestionOps.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
backward = isSystemRTL != backward;
|
||||||
|
suggestionOps.scrollTo(backward ? -1 : 1);
|
||||||
|
mInputMode.setWordStem(suggestionOps.getCurrent(), true);
|
||||||
|
appHacks.setComposingTextWithHighlightedStem(suggestionOps.getCurrent(), mInputMode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onKeyNextLanguage(boolean validateOnly) {
|
||||||
|
if (mInputMode.isNumeric() || mEnabledLanguages.size() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
nextLang();
|
||||||
|
mInputMode.changeLanguage(mLanguage);
|
||||||
|
mInputMode.clearWordStem();
|
||||||
|
getSuggestions();
|
||||||
|
|
||||||
|
setStatusText(mInputMode.toString());
|
||||||
|
renderMainView();
|
||||||
|
forceShowWindowIfHidden();
|
||||||
|
if (!suggestionOps.isEmpty()) {
|
||||||
|
UI.toastLanguage(this, mLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mInputMode instanceof ModePredictive) {
|
||||||
|
DictionaryLoader.autoLoad(this, mLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onKeyNextInputMode(boolean validateOnly) {
|
||||||
|
if (allowedInputModes.size() == 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.scheduleDelayedAccept(mInputMode.getAutoAcceptTimeout()); // restart the timer
|
||||||
|
nextInputMode();
|
||||||
|
renderMainView();
|
||||||
|
forceShowWindowIfHidden();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onKeyShowSettings(boolean validateOnly) {
|
||||||
|
if (!isInputViewShown()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateOnly) {
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
UI.showSettingsScreen(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void nextInputMode() {
|
||||||
|
if (mInputMode.isPassthrough()) {
|
||||||
|
return;
|
||||||
|
} else if (allowedInputModes.size() == 1 && allowedInputModes.contains(InputMode.MODE_123)) {
|
||||||
|
mInputMode = !mInputMode.is123() ? InputMode.getInstance(settings, mLanguage, inputType, InputMode.MODE_123) : mInputMode;
|
||||||
|
}
|
||||||
|
// when typing a word or viewing scrolling the suggestions, only change the case
|
||||||
|
else if (!suggestionOps.isEmpty()) {
|
||||||
|
nextTextCase();
|
||||||
|
}
|
||||||
|
// make "abc" and "ABC" separate modes from user perspective
|
||||||
|
else if (mInputMode instanceof ModeABC && mLanguage.hasUpperCase() && mInputMode.getTextCase() == InputMode.CASE_LOWER) {
|
||||||
|
mInputMode.nextTextCase();
|
||||||
|
} else {
|
||||||
|
int nextModeIndex = (allowedInputModes.indexOf(mInputMode.getId()) + 1) % allowedInputModes.size();
|
||||||
|
mInputMode = InputMode.getInstance(settings, mLanguage, inputType, allowedInputModes.get(nextModeIndex));
|
||||||
|
mInputMode.setTextFieldCase(textField.determineTextCase(inputType));
|
||||||
|
mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor());
|
||||||
|
|
||||||
|
resetKeyRepeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the settings for the next time
|
||||||
|
settings.saveInputMode(mInputMode.getId());
|
||||||
|
settings.saveTextCase(mInputMode.getTextCase());
|
||||||
|
|
||||||
|
setStatusText(mInputMode.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void nextLang() {
|
||||||
|
// select the next language
|
||||||
|
int previous = mEnabledLanguages.indexOf(mLanguage.getId());
|
||||||
|
int next = (previous + 1) % mEnabledLanguages.size();
|
||||||
|
mLanguage = LanguageCollection.getLanguage(getApplicationContext(), mEnabledLanguages.get(next));
|
||||||
|
|
||||||
|
validateLanguages();
|
||||||
|
|
||||||
|
// save it for the next time
|
||||||
|
settings.saveInputLanguage(mLanguage.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void nextTextCase() {
|
||||||
|
String currentSuggestionBefore = suggestionOps.getCurrent();
|
||||||
|
int currentSuggestionIndex = suggestionOps.getCurrentIndex();
|
||||||
|
|
||||||
|
// When we are in AUTO mode and the dictionary word is in uppercase,
|
||||||
|
// the mode would switch to UPPERCASE, but visually, the word would not change.
|
||||||
|
// This is why we retry, until there is a visual change.
|
||||||
|
for (int retries = 0; retries < 2 && mInputMode.nextTextCase(); retries++) {
|
||||||
|
String currentSuggestionAfter = mInputMode.getSuggestions().size() >= suggestionOps.getCurrentIndex() ? mInputMode.getSuggestions().get(suggestionOps.getCurrentIndex()) : "";
|
||||||
|
// If the suggestions are special characters, changing the text case means selecting the
|
||||||
|
// next character group. Hence, "before" and "after" are different. Also, if the new suggestion
|
||||||
|
// list is shorter, the "before" index may be invalid, so "after" would be empty.
|
||||||
|
// In these cases, we scroll to the first one, for consistency.
|
||||||
|
if (currentSuggestionAfter.isEmpty() || !currentSuggestionBefore.equalsIgnoreCase(currentSuggestionAfter)) {
|
||||||
|
currentSuggestionIndex = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the suggestion list is the same and the text case is different, so let's use it
|
||||||
|
if (!currentSuggestionBefore.equals(currentSuggestionAfter)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.set(mInputMode.getSuggestions(), currentSuggestionIndex);
|
||||||
|
textField.setComposingText(suggestionOps.getCurrent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
package io.github.sspanak.tt9.ime;
|
package io.github.sspanak.tt9.ime;
|
||||||
|
|
||||||
import android.inputmethodservice.InputMethodService;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputConnection;
|
|
||||||
|
|
||||||
import io.github.sspanak.tt9.util.Logger;
|
|
||||||
import io.github.sspanak.tt9.util.Timer;
|
|
||||||
import io.github.sspanak.tt9.ime.helpers.Key;
|
import io.github.sspanak.tt9.ime.helpers.Key;
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
import io.github.sspanak.tt9.preferences.screens.debug.ItemInputHandlingMode;
|
import io.github.sspanak.tt9.preferences.screens.debug.ItemInputHandlingMode;
|
||||||
|
import io.github.sspanak.tt9.util.Logger;
|
||||||
|
import io.github.sspanak.tt9.util.Timer;
|
||||||
|
|
||||||
|
|
||||||
abstract class KeyPadHandler extends InputMethodService {
|
abstract class KeyPadHandler extends AbstractHandler {
|
||||||
protected SettingsStore settings;
|
protected SettingsStore settings;
|
||||||
|
|
||||||
// debounce handling
|
// debounce handling
|
||||||
|
|
@ -66,7 +64,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public View onCreateInputView() {
|
public View onCreateInputView() {
|
||||||
return createSoftKeyView();
|
return createMainView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -105,7 +103,6 @@ abstract class KeyPadHandler extends InputMethodService {
|
||||||
@Override
|
@Override
|
||||||
public void onFinishInput() {
|
public void onFinishInput() {
|
||||||
super.onFinishInput();
|
super.onFinishInput();
|
||||||
// Logger.d("onFinishInput", "When is this called?");
|
|
||||||
onStop();
|
onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,43 +253,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
||||||
|
|
||||||
|
|
||||||
private boolean handleHotkey(int keyCode, boolean hold, boolean repeat, boolean validateOnly) {
|
private boolean handleHotkey(int keyCode, boolean hold, boolean repeat, boolean validateOnly) {
|
||||||
if (keyCode == settings.getKeyAddWord() * (hold ? -1 : 1)) {
|
return onHotkey(keyCode * (hold ? -1 : 1), repeat, validateOnly);
|
||||||
return onKeyAddWord(validateOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == settings.getKeyChangeKeyboard() * (hold ? -1 : 1)) {
|
|
||||||
return onKeyChangeKeyboard(validateOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == settings.getKeyFilterClear() * (hold ? -1 : 1)) {
|
|
||||||
return onKeyFilterClear(validateOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == settings.getKeyFilterSuggestions() * (hold ? -1 : 1)) {
|
|
||||||
return onKeyFilterSuggestions(validateOnly, repeat);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == settings.getKeyNextLanguage() * (hold ? -1 : 1)) {
|
|
||||||
return onKeyNextLanguage(validateOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == settings.getKeyNextInputMode() * (hold ? -1 : 1)) {
|
|
||||||
return onKeyNextInputMode(validateOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == settings.getKeyPreviousSuggestion() * (hold ? -1 : 1)) {
|
|
||||||
return onKeyScrollSuggestion(validateOnly, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == settings.getKeyNextSuggestion() * (hold ? -1 : 1)) {
|
|
||||||
return onKeyScrollSuggestion(validateOnly, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == settings.getKeyShowSettings() * (hold ? -1 : 1)) {
|
|
||||||
return onKeyShowSettings(validateOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -321,34 +282,4 @@ abstract class KeyPadHandler extends InputMethodService {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// hardware key handlers
|
|
||||||
abstract protected boolean onBack();
|
|
||||||
abstract public boolean onBackspace();
|
|
||||||
abstract protected boolean onNumber(int key, boolean hold, int repeat);
|
|
||||||
abstract public boolean onOK();
|
|
||||||
abstract public boolean onText(String text, boolean validateOnly); // used for "#", "*" and whatnot
|
|
||||||
|
|
||||||
// hotkey handlers
|
|
||||||
abstract protected boolean onKeyAddWord(boolean validateOnly);
|
|
||||||
abstract protected boolean onKeyChangeKeyboard(boolean validateOnly);
|
|
||||||
abstract protected boolean onKeyFilterClear(boolean validateOnly);
|
|
||||||
abstract protected boolean onKeyFilterSuggestions(boolean validateOnly, boolean repeat);
|
|
||||||
abstract protected boolean onKeyNextLanguage(boolean validateOnly);
|
|
||||||
abstract protected boolean onKeyNextInputMode(boolean validateOnly);
|
|
||||||
abstract protected boolean onKeyScrollSuggestion(boolean validateOnly, boolean backward);
|
|
||||||
abstract protected boolean onKeyShowSettings(boolean validateOnly);
|
|
||||||
|
|
||||||
// helpers
|
|
||||||
abstract protected void onInit();
|
|
||||||
abstract protected void onStart(InputConnection inputConnection, EditorInfo inputField);
|
|
||||||
abstract protected void onFinishTyping();
|
|
||||||
abstract protected void onStop();
|
|
||||||
abstract protected void setInputField(InputConnection inputConnection, EditorInfo inputField);
|
|
||||||
|
|
||||||
// UI
|
|
||||||
abstract protected View createSoftKeyView();
|
|
||||||
abstract protected boolean shouldBeVisible();
|
|
||||||
abstract protected boolean shouldBeOff();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
155
app/src/main/java/io/github/sspanak/tt9/ime/SuggestionOps.java
Normal file
155
app/src/main/java/io/github/sspanak/tt9/ime/SuggestionOps.java
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
package io.github.sspanak.tt9.ime;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||||
|
import io.github.sspanak.tt9.ui.tray.SuggestionsBar;
|
||||||
|
import io.github.sspanak.tt9.util.ConsumerCompat;
|
||||||
|
|
||||||
|
public class SuggestionOps {
|
||||||
|
@NonNull private final Handler delayedAcceptHandler;
|
||||||
|
@NonNull private final ConsumerCompat<String> onDelayedAccept;
|
||||||
|
@NonNull protected final SuggestionsBar suggestionBar;
|
||||||
|
@NonNull private TextField textField;
|
||||||
|
|
||||||
|
|
||||||
|
SuggestionOps(@NonNull TypingHandler tt9, View mainView, @NonNull ConsumerCompat<String> onDelayedAccept) {
|
||||||
|
delayedAcceptHandler = new Handler(Looper.getMainLooper());
|
||||||
|
this.onDelayedAccept = onDelayedAccept;
|
||||||
|
|
||||||
|
suggestionBar = new SuggestionsBar(tt9, mainView);
|
||||||
|
textField = new TextField(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void setTextField(@NonNull TextField textField) {
|
||||||
|
this.textField = textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return suggestionBar.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String get(int index) {
|
||||||
|
return suggestionBar.getSuggestion(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
set(null);
|
||||||
|
textField.setComposingText("");
|
||||||
|
textField.finishComposingText();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void set(ArrayList<String> suggestions) {
|
||||||
|
suggestionBar.setSuggestions(suggestions, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void set(ArrayList<String> suggestions, int selectIndex) {
|
||||||
|
suggestionBar.setSuggestions(suggestions, selectIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void scrollTo(int index) {
|
||||||
|
suggestionBar.scrollToSuggestion(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String acceptCurrent() {
|
||||||
|
String word = getCurrent();
|
||||||
|
if (!word.isEmpty()) {
|
||||||
|
commitCurrent(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String acceptIncomplete() {
|
||||||
|
String currentWord = this.getCurrent();
|
||||||
|
commitCurrent(false);
|
||||||
|
|
||||||
|
return currentWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String acceptPrevious(int sequenceLength) {
|
||||||
|
String lastComposingText = getCurrent(sequenceLength - 1);
|
||||||
|
commitCurrent(false);
|
||||||
|
return lastComposingText;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void commitCurrent(boolean entireSuggestion) {
|
||||||
|
if (!suggestionBar.isEmpty()) {
|
||||||
|
if (entireSuggestion) {
|
||||||
|
textField.setComposingText(getCurrent());
|
||||||
|
}
|
||||||
|
textField.finishComposingText();
|
||||||
|
}
|
||||||
|
|
||||||
|
set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int getCurrentIndex() {
|
||||||
|
return suggestionBar.getCurrentIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String getCurrent() {
|
||||||
|
return get(suggestionBar.getCurrentIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected String getCurrent(int maxLength) {
|
||||||
|
if (maxLength == 0 || suggestionBar.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = getCurrent();
|
||||||
|
if (maxLength > 0 && !text.isEmpty() && text.length() > maxLength) {
|
||||||
|
text = text.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
boolean scheduleDelayedAccept(int delay) {
|
||||||
|
cancelDelayedAccept();
|
||||||
|
|
||||||
|
if (suggestionBar.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay == 0) {
|
||||||
|
onDelayedAccept.accept(acceptCurrent());
|
||||||
|
return true;
|
||||||
|
} else if (delay > 0) {
|
||||||
|
delayedAcceptHandler.postDelayed(() -> onDelayedAccept.accept(acceptCurrent()), delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void cancelDelayedAccept() {
|
||||||
|
delayedAcceptHandler.removeCallbacksAndMessages(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void setDarkTheme(boolean yes) {
|
||||||
|
suggestionBar.setDarkTheme(yes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
package io.github.sspanak.tt9.ime;
|
package io.github.sspanak.tt9.ime;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputConnection;
|
import android.view.inputmethod.InputConnection;
|
||||||
|
|
@ -13,130 +11,21 @@ import android.view.inputmethod.InputMethodManager;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.github.sspanak.tt9.R;
|
|
||||||
import io.github.sspanak.tt9.db.DictionaryLoader;
|
import io.github.sspanak.tt9.db.DictionaryLoader;
|
||||||
import io.github.sspanak.tt9.db.WordStoreAsync;
|
import io.github.sspanak.tt9.db.WordStoreAsync;
|
||||||
import io.github.sspanak.tt9.ime.helpers.AppHacks;
|
|
||||||
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
|
|
||||||
import io.github.sspanak.tt9.ime.helpers.InputType;
|
|
||||||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
|
||||||
import io.github.sspanak.tt9.ime.modes.InputMode;
|
|
||||||
import io.github.sspanak.tt9.ime.modes.ModeABC;
|
|
||||||
import io.github.sspanak.tt9.ime.modes.ModePassthrough;
|
import io.github.sspanak.tt9.ime.modes.ModePassthrough;
|
||||||
import io.github.sspanak.tt9.ime.modes.ModePredictive;
|
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
|
||||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
|
||||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
|
|
||||||
import io.github.sspanak.tt9.ui.PopupDialogActivity;
|
import io.github.sspanak.tt9.ui.PopupDialogActivity;
|
||||||
import io.github.sspanak.tt9.ui.UI;
|
import io.github.sspanak.tt9.ui.UI;
|
||||||
import io.github.sspanak.tt9.ui.main.MainView;
|
import io.github.sspanak.tt9.ui.main.MainView;
|
||||||
import io.github.sspanak.tt9.ui.tray.StatusBar;
|
import io.github.sspanak.tt9.ui.tray.StatusBar;
|
||||||
import io.github.sspanak.tt9.ui.tray.SuggestionsBar;
|
|
||||||
import io.github.sspanak.tt9.util.Logger;
|
import io.github.sspanak.tt9.util.Logger;
|
||||||
import io.github.sspanak.tt9.util.Text;
|
|
||||||
|
|
||||||
public class TraditionalT9 extends KeyPadHandler {
|
public class TraditionalT9 extends HotkeyHandler {
|
||||||
private InputConnection currentInputConnection = null;
|
@NonNull
|
||||||
// internal settings/data
|
private final Handler normalizationHandler = new Handler(Looper.getMainLooper());
|
||||||
@NonNull private AppHacks appHacks = new AppHacks(null,null, null, null);
|
|
||||||
@NonNull private TextField textField = new TextField(null, null);
|
|
||||||
@NonNull private InputType inputType = new InputType(null, null);
|
|
||||||
@NonNull private final Handler autoAcceptHandler = new Handler(Looper.getMainLooper());
|
|
||||||
@NonNull private final Handler normalizationHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
// input mode
|
|
||||||
private ArrayList<Integer> allowedInputModes = new ArrayList<>();
|
|
||||||
@NonNull private InputMode mInputMode = InputMode.getInstance(null, null, null, InputMode.MODE_PASSTHROUGH);
|
|
||||||
|
|
||||||
// language
|
|
||||||
protected ArrayList<Integer> mEnabledLanguages;
|
|
||||||
protected Language mLanguage;
|
|
||||||
protected Language systemLanguage;
|
|
||||||
|
|
||||||
// soft key view
|
|
||||||
private MainView mainView = null;
|
private MainView mainView = null;
|
||||||
private StatusBar statusBar = null;
|
private StatusBar statusBar = null;
|
||||||
private SuggestionsBar suggestionBar = null;
|
|
||||||
|
|
||||||
private static TraditionalT9 self;
|
|
||||||
public static Context getMainContext() {
|
|
||||||
return self.getApplicationContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SettingsStore getSettings() {
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isInputModeNumeric() {
|
|
||||||
return mInputMode.is123();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isNumericModeStrict() {
|
|
||||||
return mInputMode.is123() && inputType.isNumeric() && !inputType.isPhoneNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isNumericModeSigned() {
|
|
||||||
return mInputMode.is123() && inputType.isSignedNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isInputModePhone() {
|
|
||||||
return mInputMode.is123() && inputType.isPhoneNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTextCase() {
|
|
||||||
return mInputMode.getTextCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void validateLanguages() {
|
|
||||||
mEnabledLanguages = InputModeValidator.validateEnabledLanguages(getMainContext(), mEnabledLanguages);
|
|
||||||
mLanguage = InputModeValidator.validateLanguage(getMainContext(), mLanguage, mEnabledLanguages);
|
|
||||||
|
|
||||||
settings.saveEnabledLanguageIds(mEnabledLanguages);
|
|
||||||
settings.saveInputLanguage(mLanguage.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void validateFunctionKeys() {
|
|
||||||
if (settings.areHotkeysInitialized()) {
|
|
||||||
Hotkeys.setDefault(settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getInputMode
|
|
||||||
* Load the last input mode or choose a more appropriate one.
|
|
||||||
* Some input fields support only numbers or are not suited for predictions (e.g. password fields)
|
|
||||||
*/
|
|
||||||
private InputMode getInputMode() {
|
|
||||||
if (!inputType.isValid() || (inputType.isLimited() && !appHacks.isTermux())) {
|
|
||||||
return InputMode.getInstance(settings, mLanguage, inputType, InputMode.MODE_PASSTHROUGH);
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedInputModes = textField.determineInputModes(inputType);
|
|
||||||
int validModeId = InputModeValidator.validateMode(settings.getInputMode(), allowedInputModes);
|
|
||||||
return InputMode.getInstance(settings, mLanguage, inputType, validModeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* determineTextCase
|
|
||||||
* Restore the last text case or auto-select a new one. If the InputMode supports it, it can change
|
|
||||||
* the text case based on grammar rules, otherwise we fallback to the input field properties or the
|
|
||||||
* last saved mode.
|
|
||||||
*/
|
|
||||||
private void determineTextCase() {
|
|
||||||
mInputMode.defaultTextCase();
|
|
||||||
mInputMode.setTextFieldCase(textField.determineTextCase(inputType));
|
|
||||||
mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor());
|
|
||||||
InputModeValidator.validateTextCase(mInputMode, settings.getTextCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -147,7 +36,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
forceShowWindowIfHidden();
|
forceShowWindowIfHidden();
|
||||||
if (!message.isEmpty()) {
|
if (!message.isEmpty()) {
|
||||||
UI.toastLong(self, message);
|
UI.toastLong(this, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,8 +44,8 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void onInit() {
|
protected void onInit() {
|
||||||
self = this;
|
|
||||||
Logger.setLevel(settings.getLogLevel());
|
Logger.setLevel(settings.getLogLevel());
|
||||||
|
|
||||||
WordStoreAsync.init(this);
|
WordStoreAsync.init(this);
|
||||||
|
|
@ -166,42 +55,21 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
initTray();
|
initTray();
|
||||||
}
|
}
|
||||||
|
|
||||||
validateFunctionKeys();
|
super.onInit();
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected void setInputField(InputConnection connection, EditorInfo field) {
|
|
||||||
currentInputConnection = connection;
|
|
||||||
inputType = new InputType(currentInputConnection, field);
|
|
||||||
textField = new TextField(currentInputConnection, field);
|
|
||||||
appHacks = new AppHacks(settings, connection, field, textField);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void initTyping() {
|
|
||||||
// in case we are back from Settings screen, update the language list
|
|
||||||
mEnabledLanguages = settings.getEnabledLanguageIds();
|
|
||||||
mLanguage = LanguageCollection.getLanguage(getMainContext(), settings.getInputLanguage());
|
|
||||||
validateLanguages();
|
|
||||||
|
|
||||||
resetKeyRepeat();
|
|
||||||
setSuggestions(null);
|
|
||||||
mInputMode = getInputMode();
|
|
||||||
determineTextCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void initTray() {
|
private void initTray() {
|
||||||
setInputView(mainView.getView());
|
setInputView(mainView.getView());
|
||||||
|
createSuggestionBar(mainView.getView());
|
||||||
statusBar = new StatusBar(mainView.getView());
|
statusBar = new StatusBar(mainView.getView());
|
||||||
suggestionBar = new SuggestionsBar(this, mainView.getView());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void setDarkTheme() {
|
private void setDarkTheme() {
|
||||||
mainView.setDarkTheme(settings.getDarkTheme());
|
mainView.setDarkTheme(settings.getDarkTheme());
|
||||||
statusBar.setDarkTheme(settings.getDarkTheme());
|
statusBar.setDarkTheme(settings.getDarkTheme());
|
||||||
suggestionBar.setDarkTheme(settings.getDarkTheme());
|
suggestionOps.setDarkTheme(settings.getDarkTheme());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -209,17 +77,17 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
if (mainView.createView()) {
|
if (mainView.createView()) {
|
||||||
initTray();
|
initTray();
|
||||||
}
|
}
|
||||||
statusBar.setText(mInputMode.toString());
|
setStatusText(mInputMode.toString());
|
||||||
setDarkTheme();
|
setDarkTheme();
|
||||||
mainView.render();
|
mainView.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void onStart(InputConnection connection, EditorInfo field) {
|
protected void onStart(InputConnection connection, EditorInfo field) {
|
||||||
Logger.setLevel(settings.getLogLevel());
|
Logger.setLevel(settings.getLogLevel());
|
||||||
|
|
||||||
setInputField(connection, field);
|
super.onStart(connection, field);
|
||||||
initTyping();
|
|
||||||
|
|
||||||
if (mInputMode.isPassthrough()) {
|
if (mInputMode.isPassthrough()) {
|
||||||
// When the input is invalid or simple, let Android handle it.
|
// When the input is invalid or simple, let Android handle it.
|
||||||
|
|
@ -229,25 +97,25 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizationHandler.removeCallbacksAndMessages(null);
|
normalizationHandler.removeCallbacksAndMessages(null);
|
||||||
systemLanguage = LanguageCollection.getDefault(this);
|
|
||||||
|
|
||||||
initUi();
|
initUi();
|
||||||
updateInputViewShown();
|
updateInputViewShown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void onFinishTyping() {
|
protected void onFinishTyping() {
|
||||||
cancelAutoAccept();
|
|
||||||
if (!(mInputMode instanceof ModePassthrough)) {
|
if (!(mInputMode instanceof ModePassthrough)) {
|
||||||
DictionaryLoader.autoLoad(this, mLanguage);
|
DictionaryLoader.autoLoad(this, mLanguage);
|
||||||
}
|
}
|
||||||
mInputMode = InputMode.getInstance(null, null, null, InputMode.MODE_PASSTHROUGH);
|
super.onFinishTyping();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
onFinishTyping();
|
onFinishTyping();
|
||||||
clearSuggestions();
|
suggestionOps.clear();
|
||||||
statusBar.setText("--");
|
statusBar.setText("--");
|
||||||
|
|
||||||
normalizationHandler.removeCallbacksAndMessages(null);
|
normalizationHandler.removeCallbacksAndMessages(null);
|
||||||
|
|
@ -258,566 +126,11 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean onBack() {
|
|
||||||
return settings.getShowSoftNumpad();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onBackspace() {
|
|
||||||
// 1. Dialer fields seem to handle backspace on their own and we must ignore it,
|
|
||||||
// otherwise, keyDown race condition occur for all keys.
|
|
||||||
// 2. Allow the assigned key to function normally, when there is no text (e.g. "Back" navigates back)
|
|
||||||
// 3. Some app may need special treatment, so let it be.
|
|
||||||
if (mInputMode.isPassthrough() || !(textField.isThereText() || appHacks.onBackspace(mInputMode))) {
|
|
||||||
Logger.d("onBackspace", "backspace ignored");
|
|
||||||
mInputMode.reset();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAutoAccept();
|
|
||||||
resetKeyRepeat();
|
|
||||||
|
|
||||||
if (mInputMode.onBackspace()) {
|
|
||||||
getSuggestions();
|
|
||||||
} else {
|
|
||||||
commitCurrentSuggestion(false);
|
|
||||||
super.sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.d("onBackspace", "backspace handled");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onNumber
|
* createMainView
|
||||||
*
|
|
||||||
* @param key Must be a number from 1 to 9, not a "KeyEvent.KEYCODE_X"
|
|
||||||
* @param hold If "true" we are calling the handler, because the key is being held.
|
|
||||||
* @param repeat If "true" we are calling the handler, because the key was pressed more than once
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
protected boolean onNumber(int key, boolean hold, int repeat) {
|
|
||||||
cancelAutoAccept();
|
|
||||||
forceShowWindowIfHidden();
|
|
||||||
|
|
||||||
// Automatically accept the previous word, when the next one is a space or punctuation,
|
|
||||||
// instead of requiring "OK" before that.
|
|
||||||
// First pass, analyze the incoming key press and decide whether it could be the start of
|
|
||||||
// a new word.
|
|
||||||
if (mInputMode.shouldAcceptPreviousSuggestion(key)) {
|
|
||||||
autoCorrectSpace(acceptIncompleteSuggestion(), false, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-adjust the text case before each word, if the InputMode supports it.
|
|
||||||
if (getComposingText().isEmpty()) {
|
|
||||||
mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mInputMode.onNumber(key, hold, repeat)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mInputMode.shouldSelectNextSuggestion() && !isSuggestionViewHidden()) {
|
|
||||||
onKeyScrollSuggestion(false, false);
|
|
||||||
scheduleAutoAccept(mInputMode.getAutoAcceptTimeout());
|
|
||||||
} else {
|
|
||||||
getSuggestions();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onOK() {
|
|
||||||
cancelAutoAccept();
|
|
||||||
|
|
||||||
if (isSuggestionViewHidden()) {
|
|
||||||
int action = textField.getAction();
|
|
||||||
return action == TextField.IME_ACTION_ENTER ? appHacks.onEnter() : textField.performAction(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptCurrentSuggestion(KeyEvent.KEYCODE_ENTER);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onText(String text) { return onText(text, false); }
|
|
||||||
|
|
||||||
public boolean onText(String text, boolean validateOnly) {
|
|
||||||
if (mInputMode.shouldIgnoreText(text)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAutoAccept();
|
|
||||||
forceShowWindowIfHidden();
|
|
||||||
|
|
||||||
// accept the previously typed word (if any)
|
|
||||||
autoCorrectSpace(acceptIncompleteSuggestion(), false, -1);
|
|
||||||
|
|
||||||
// "type" and accept the new word
|
|
||||||
mInputMode.onAcceptSuggestion(text);
|
|
||||||
textField.setText(text);
|
|
||||||
autoCorrectSpace(text, true, -1);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyAddWord(boolean validateOnly) {
|
|
||||||
if (!isInputViewShown() || mInputMode.isNumeric()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DictionaryLoader.getInstance(this).isRunning()) {
|
|
||||||
UI.toast(this, R.string.dictionary_loading_please_wait);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAutoAccept();
|
|
||||||
acceptIncompleteSuggestion();
|
|
||||||
|
|
||||||
String word = textField.getSurroundingWord(mLanguage);
|
|
||||||
if (word.isEmpty()) {
|
|
||||||
UI.toastLong(this, R.string.add_word_no_selection);
|
|
||||||
} else {
|
|
||||||
UI.showAddWordDialog(this, mLanguage.getId(), word);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyChangeKeyboard(boolean validateOnly) {
|
|
||||||
if (!isInputViewShown()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateOnly) {
|
|
||||||
UI.showChangeKeyboardDialog(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyFilterClear(boolean validateOnly) {
|
|
||||||
if (isSuggestionViewHidden()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAutoAccept();
|
|
||||||
|
|
||||||
if (mInputMode.clearWordStem()) {
|
|
||||||
mInputMode.loadSuggestions(this::getSuggestions, getComposingText());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptIncompleteSuggestion();
|
|
||||||
resetKeyRepeat();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyFilterSuggestions(boolean validateOnly, boolean repeat) {
|
|
||||||
if (isSuggestionViewHidden()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAutoAccept();
|
|
||||||
|
|
||||||
String filter;
|
|
||||||
if (repeat && !suggestionBar.getSuggestion(1).isEmpty()) {
|
|
||||||
filter = suggestionBar.getSuggestion(1);
|
|
||||||
} else {
|
|
||||||
filter = getComposingText();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.isEmpty()) {
|
|
||||||
mInputMode.reset();
|
|
||||||
} else if (mInputMode.setWordStem(filter, repeat)) {
|
|
||||||
mInputMode.loadSuggestions(this::getSuggestions, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyScrollSuggestion(boolean validateOnly, boolean backward) {
|
|
||||||
if (isSuggestionViewHidden()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAutoAccept();
|
|
||||||
backward = LanguageKind.isRTL(systemLanguage) != backward;
|
|
||||||
suggestionBar.scrollToSuggestion(backward ? -1 : 1);
|
|
||||||
mInputMode.setWordStem(suggestionBar.getCurrentSuggestion(), true);
|
|
||||||
setComposingTextWithHighlightedStem(suggestionBar.getCurrentSuggestion());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyNextLanguage(boolean validateOnly) {
|
|
||||||
if (mInputMode.isNumeric() || mEnabledLanguages.size() < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAutoAccept();
|
|
||||||
nextLang();
|
|
||||||
mInputMode.changeLanguage(mLanguage);
|
|
||||||
mInputMode.clearWordStem();
|
|
||||||
getSuggestions();
|
|
||||||
|
|
||||||
statusBar.setText(mInputMode.toString());
|
|
||||||
mainView.render();
|
|
||||||
forceShowWindowIfHidden();
|
|
||||||
if (!suggestionBar.isEmpty()) {
|
|
||||||
UI.toastLanguage(this, mLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mInputMode instanceof ModePredictive) {
|
|
||||||
DictionaryLoader.autoLoad(this, mLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyNextInputMode(boolean validateOnly) {
|
|
||||||
if (allowedInputModes.size() == 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleAutoAccept(mInputMode.getAutoAcceptTimeout()); // restart the timer
|
|
||||||
nextInputMode();
|
|
||||||
mainView.render();
|
|
||||||
forceShowWindowIfHidden();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyShowSettings(boolean validateOnly) {
|
|
||||||
if (!isInputViewShown()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAutoAccept();
|
|
||||||
UI.showSettingsScreen(this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) {
|
|
||||||
// Logger.d("onUpdateSelection", "oldSelStart: " + oldSelStart + " oldSelEnd: " + oldSelEnd + " newSelStart: " + newSelStart + " oldSelEnd: " + oldSelEnd + " candidatesStart: " + candidatesStart + " candidatesEnd: " + candidatesEnd);
|
|
||||||
|
|
||||||
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd);
|
|
||||||
|
|
||||||
// If the cursor moves while composing a word (usually, because the user has touched the screen outside the word), we must
|
|
||||||
// end typing end accept the word. Otherwise, the cursor would jump back at the end of the word, after the next key press.
|
|
||||||
// This is confusing from user perspective, so we want to avoid it.
|
|
||||||
if (
|
|
||||||
candidatesStart != -1 && candidatesEnd != -1
|
|
||||||
&& (newSelStart != candidatesEnd || newSelEnd != candidatesEnd)
|
|
||||||
&& !suggestionBar.isEmpty()
|
|
||||||
) {
|
|
||||||
acceptIncompleteSuggestion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isSuggestionViewHidden() {
|
|
||||||
return suggestionBar == null || suggestionBar.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean scheduleAutoAccept(int delay) {
|
|
||||||
cancelAutoAccept();
|
|
||||||
|
|
||||||
if (suggestionBar.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delay == 0) {
|
|
||||||
this.acceptCurrentSuggestion();
|
|
||||||
return true;
|
|
||||||
} else if (delay > 0) {
|
|
||||||
autoAcceptHandler.postDelayed(this::acceptCurrentSuggestion, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void cancelAutoAccept() {
|
|
||||||
autoAcceptHandler.removeCallbacksAndMessages(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void acceptCurrentSuggestion(int fromKey) {
|
|
||||||
String word = suggestionBar.getCurrentSuggestion();
|
|
||||||
if (word.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mInputMode.onAcceptSuggestion(word);
|
|
||||||
commitCurrentSuggestion();
|
|
||||||
autoCorrectSpace(word, true, fromKey);
|
|
||||||
resetKeyRepeat();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void acceptCurrentSuggestion() {
|
|
||||||
acceptCurrentSuggestion(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private String acceptIncompleteSuggestion() {
|
|
||||||
String currentWord = getComposingText();
|
|
||||||
mInputMode.onAcceptSuggestion(currentWord);
|
|
||||||
commitCurrentSuggestion(false);
|
|
||||||
|
|
||||||
return currentWord;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void commitCurrentSuggestion() {
|
|
||||||
commitCurrentSuggestion(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void commitCurrentSuggestion(boolean entireSuggestion) {
|
|
||||||
if (!isSuggestionViewHidden()) {
|
|
||||||
if (entireSuggestion) {
|
|
||||||
textField.setComposingText(suggestionBar.getCurrentSuggestion());
|
|
||||||
}
|
|
||||||
textField.finishComposingText();
|
|
||||||
}
|
|
||||||
|
|
||||||
setSuggestions(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void clearSuggestions() {
|
|
||||||
setSuggestions(null);
|
|
||||||
textField.setComposingText("");
|
|
||||||
textField.finishComposingText();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void getSuggestions() {
|
|
||||||
if (mInputMode instanceof ModePredictive && DictionaryLoader.getInstance(this).isRunning()) {
|
|
||||||
UI.toast(this, R.string.dictionary_loading_please_wait);
|
|
||||||
} else {
|
|
||||||
mInputMode.loadSuggestions(this::handleSuggestions, suggestionBar.getCurrentSuggestion());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void handleSuggestions() {
|
|
||||||
// Automatically accept the previous word, without requiring OK. This is similar to what
|
|
||||||
// Second pass, analyze the available suggestions and decide if combining them with the
|
|
||||||
// last key press makes up a compound word like: (it)'s, (I)'ve, l'(oiseau), or it is
|
|
||||||
// just the end of a sentence, like: "word." or "another?"
|
|
||||||
if (mInputMode.shouldAcceptPreviousSuggestion()) {
|
|
||||||
String lastComposingText = getComposingText(mInputMode.getSequenceLength() - 1);
|
|
||||||
commitCurrentSuggestion(false);
|
|
||||||
mInputMode.onAcceptSuggestion(lastComposingText, true);
|
|
||||||
autoCorrectSpace(lastComposingText, false, -1);
|
|
||||||
mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor());
|
|
||||||
}
|
|
||||||
|
|
||||||
// display the word suggestions
|
|
||||||
setSuggestions(mInputMode.getSuggestions());
|
|
||||||
|
|
||||||
// In case we are here, because the language was changed, and there were words for the old language,
|
|
||||||
// but there are no words for the new language, we'll get only generated suggestions, consisting
|
|
||||||
// of the last word of the previous language + endings from the new language. These words are invalid,
|
|
||||||
// so we discard them.
|
|
||||||
if (mInputMode instanceof ModePredictive && !mLanguage.isValidWord(suggestionBar.getCurrentSuggestion()) && !Text.isGraphic(suggestionBar.getCurrentSuggestion())) {
|
|
||||||
mInputMode.reset();
|
|
||||||
setSuggestions(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// flush the first suggestion, if the InputMode has requested it
|
|
||||||
if (scheduleAutoAccept(mInputMode.getAutoAcceptTimeout())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, 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 = suggestionBar.getCurrentSuggestion();
|
|
||||||
word = word.substring(0, Math.min(mInputMode.getSequenceLength(), word.length()));
|
|
||||||
setComposingTextWithHighlightedStem(word);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void setSuggestions(List<String> suggestions) {
|
|
||||||
setSuggestions(suggestions, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setSuggestions(List<String> suggestions, int selectedIndex) {
|
|
||||||
if (suggestionBar != null) {
|
|
||||||
suggestionBar.setSuggestions(suggestions, selectedIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private String getComposingText(int maxLength) {
|
|
||||||
if (maxLength == 0 || suggestionBar.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
maxLength = maxLength > 0 ? Math.min(maxLength, mInputMode.getSequenceLength()) : mInputMode.getSequenceLength();
|
|
||||||
|
|
||||||
String text = suggestionBar.getCurrentSuggestion();
|
|
||||||
if (!text.isEmpty() && text.length() > maxLength) {
|
|
||||||
text = text.substring(0, maxLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private String getComposingText() {
|
|
||||||
return getComposingText(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void setComposingTextWithHighlightedStem(@NonNull String word) {
|
|
||||||
if (appHacks.setComposingTextWithHighlightedStem(word)) {
|
|
||||||
Logger.w("highlightComposingText", "Defective text field detected! Text highlighting disabled.");
|
|
||||||
} else if (word.isEmpty() || !Character.isLetterOrDigit(word.charAt(0))) {
|
|
||||||
// Leave emoji and special characters alone. Adding bold or italic breaks them.
|
|
||||||
textField.setComposingText(word);
|
|
||||||
} else {
|
|
||||||
textField.setComposingTextWithHighlightedStem(word, mInputMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void nextInputMode() {
|
|
||||||
if (mInputMode.isPassthrough()) {
|
|
||||||
return;
|
|
||||||
} else if (allowedInputModes.size() == 1 && allowedInputModes.contains(InputMode.MODE_123)) {
|
|
||||||
mInputMode = !mInputMode.is123() ? InputMode.getInstance(settings, mLanguage, inputType, InputMode.MODE_123) : mInputMode;
|
|
||||||
}
|
|
||||||
// when typing a word or viewing scrolling the suggestions, only change the case
|
|
||||||
else if (!isSuggestionViewHidden()) {
|
|
||||||
nextTextCase();
|
|
||||||
}
|
|
||||||
// make "abc" and "ABC" separate modes from user perspective
|
|
||||||
else if (mInputMode instanceof ModeABC && mLanguage.hasUpperCase() && mInputMode.getTextCase() == InputMode.CASE_LOWER) {
|
|
||||||
mInputMode.nextTextCase();
|
|
||||||
} else {
|
|
||||||
int nextModeIndex = (allowedInputModes.indexOf(mInputMode.getId()) + 1) % allowedInputModes.size();
|
|
||||||
mInputMode = InputMode.getInstance(settings, mLanguage, inputType, allowedInputModes.get(nextModeIndex));
|
|
||||||
mInputMode.setTextFieldCase(textField.determineTextCase(inputType));
|
|
||||||
mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor());
|
|
||||||
|
|
||||||
resetKeyRepeat();
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the settings for the next time
|
|
||||||
settings.saveInputMode(mInputMode.getId());
|
|
||||||
settings.saveTextCase(mInputMode.getTextCase());
|
|
||||||
|
|
||||||
statusBar.setText(mInputMode.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void nextLang() {
|
|
||||||
// select the next language
|
|
||||||
int previous = mEnabledLanguages.indexOf(mLanguage.getId());
|
|
||||||
int next = (previous + 1) % mEnabledLanguages.size();
|
|
||||||
mLanguage = LanguageCollection.getLanguage(getMainContext(), mEnabledLanguages.get(next));
|
|
||||||
|
|
||||||
validateLanguages();
|
|
||||||
|
|
||||||
// save it for the next time
|
|
||||||
settings.saveInputLanguage(mLanguage.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void nextTextCase() {
|
|
||||||
String currentSuggestionBefore = getComposingText();
|
|
||||||
int currentSuggestionIndex = suggestionBar.getCurrentIndex();
|
|
||||||
|
|
||||||
// When we are in AUTO mode and the dictionary word is in uppercase,
|
|
||||||
// the mode would switch to UPPERCASE, but visually, the word would not change.
|
|
||||||
// This is why we retry, until there is a visual change.
|
|
||||||
for (int retries = 0; retries < 2 && mInputMode.nextTextCase(); retries++) {
|
|
||||||
String currentSuggestionAfter = mInputMode.getSuggestions().size() >= suggestionBar.getCurrentIndex() ? mInputMode.getSuggestions().get(suggestionBar.getCurrentIndex()) : "";
|
|
||||||
// If the suggestions are special characters, changing the text case means selecting the
|
|
||||||
// next character group. Hence, "before" and "after" are different. Also, if the new suggestion
|
|
||||||
// list is shorter, the "before" index may be invalid, so "after" would be empty.
|
|
||||||
// In these cases, we scroll to the first one, for consistency.
|
|
||||||
if (currentSuggestionAfter.isEmpty() || !currentSuggestionBefore.equalsIgnoreCase(currentSuggestionAfter)) {
|
|
||||||
currentSuggestionIndex = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the suggestion list is the same and the text case is different, so let's use it
|
|
||||||
if (!currentSuggestionBefore.equals(currentSuggestionAfter)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSuggestions(mInputMode.getSuggestions(), currentSuggestionIndex);
|
|
||||||
textField.setComposingText(suggestionBar.getCurrentSuggestion());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void autoCorrectSpace(String currentWord, boolean isWordAcceptedManually, int nextKey) {
|
|
||||||
if (mInputMode.shouldDeletePrecedingSpace(inputType)) {
|
|
||||||
textField.deletePrecedingSpace(currentWord);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mInputMode.shouldAddAutoSpace(inputType, textField, isWordAcceptedManually, nextKey)) {
|
|
||||||
textField.setText(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* createSoftKeyView
|
|
||||||
* Generates the actual UI of TT9.
|
* Generates the actual UI of TT9.
|
||||||
*/
|
*/
|
||||||
protected View createSoftKeyView() {
|
protected View createMainView() {
|
||||||
mainView.forceCreateView();
|
mainView.forceCreateView();
|
||||||
initTray();
|
initTray();
|
||||||
setDarkTheme();
|
setDarkTheme();
|
||||||
|
|
@ -825,6 +138,15 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the UI elements with strings and icons
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void renderMainView() {
|
||||||
|
mainView.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* forceShowWindowIfHidden
|
* forceShowWindowIfHidden
|
||||||
* Some applications may hide our window and it remains invisible until the screen is touched or OK is pressed.
|
* Some applications may hide our window and it remains invisible until the screen is touched or OK is pressed.
|
||||||
|
|
@ -844,6 +166,12 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setStatusText(String status) {
|
||||||
|
statusBar.setText(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean shouldBeVisible() {
|
protected boolean shouldBeVisible() {
|
||||||
return !getInputMode().isPassthrough();
|
return !getInputMode().isPassthrough();
|
||||||
|
|
@ -854,4 +182,31 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
protected boolean shouldBeOff() {
|
protected boolean shouldBeOff() {
|
||||||
return currentInputConnection == null || mInputMode.isPassthrough();
|
return currentInputConnection == null || mInputMode.isPassthrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**** Informational methods for the on-screen keyboard ****/
|
||||||
|
|
||||||
|
public int getTextCase() {
|
||||||
|
return mInputMode.getTextCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInputModeNumeric() {
|
||||||
|
return mInputMode.is123();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNumericModeStrict() {
|
||||||
|
return mInputMode.is123() && inputType.isNumeric() && !inputType.isPhoneNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNumericModeSigned() {
|
||||||
|
return mInputMode.is123() && inputType.isSignedNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInputModePhone() {
|
||||||
|
return mInputMode.is123() && inputType.isPhoneNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SettingsStore getSettings() {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
305
app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java
Normal file
305
app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
package io.github.sspanak.tt9.ime;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.R;
|
||||||
|
import io.github.sspanak.tt9.db.DictionaryLoader;
|
||||||
|
import io.github.sspanak.tt9.ime.helpers.AppHacks;
|
||||||
|
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
|
||||||
|
import io.github.sspanak.tt9.ime.helpers.InputType;
|
||||||
|
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||||
|
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||||
|
import io.github.sspanak.tt9.ime.modes.ModePredictive;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||||
|
import io.github.sspanak.tt9.ui.UI;
|
||||||
|
import io.github.sspanak.tt9.util.Logger;
|
||||||
|
import io.github.sspanak.tt9.util.Text;
|
||||||
|
|
||||||
|
public abstract class TypingHandler extends KeyPadHandler {
|
||||||
|
// internal settings/data
|
||||||
|
@NonNull protected AppHacks appHacks = new AppHacks(null,null, null, null);
|
||||||
|
protected InputConnection currentInputConnection = null;
|
||||||
|
@NonNull protected InputType inputType = new InputType(null, null);
|
||||||
|
@NonNull protected TextField textField = new TextField(null, null);
|
||||||
|
protected SuggestionOps suggestionOps;
|
||||||
|
|
||||||
|
// input
|
||||||
|
protected ArrayList<Integer> allowedInputModes = new ArrayList<>();
|
||||||
|
@NonNull
|
||||||
|
protected InputMode mInputMode = InputMode.getInstance(null, null, null, InputMode.MODE_PASSTHROUGH);
|
||||||
|
|
||||||
|
// language
|
||||||
|
protected ArrayList<Integer> mEnabledLanguages;
|
||||||
|
protected Language mLanguage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createSuggestionBar(View mainView) {
|
||||||
|
suggestionOps = new SuggestionOps(this, mainView, this::onAcceptSuggestionsDelayed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart(InputConnection connection, EditorInfo field) {
|
||||||
|
setInputField(connection, field);
|
||||||
|
|
||||||
|
// in case we are back from Settings screen, update the language list
|
||||||
|
mEnabledLanguages = settings.getEnabledLanguageIds();
|
||||||
|
mLanguage = LanguageCollection.getLanguage(getApplicationContext(), settings.getInputLanguage());
|
||||||
|
validateLanguages();
|
||||||
|
|
||||||
|
resetKeyRepeat();
|
||||||
|
mInputMode = getInputMode();
|
||||||
|
determineTextCase();
|
||||||
|
|
||||||
|
suggestionOps.setTextField(textField);
|
||||||
|
suggestionOps.set(null);
|
||||||
|
|
||||||
|
appHacks = new AppHacks(settings, connection, field, textField);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void setInputField(InputConnection connection, EditorInfo field) {
|
||||||
|
currentInputConnection = connection;
|
||||||
|
inputType = new InputType(currentInputConnection, field);
|
||||||
|
textField = new TextField(currentInputConnection, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void validateLanguages() {
|
||||||
|
mEnabledLanguages = InputModeValidator.validateEnabledLanguages(getApplicationContext(), mEnabledLanguages);
|
||||||
|
mLanguage = InputModeValidator.validateLanguage(getApplicationContext(), mLanguage, mEnabledLanguages);
|
||||||
|
|
||||||
|
settings.saveEnabledLanguageIds(mEnabledLanguages);
|
||||||
|
settings.saveInputLanguage(mLanguage.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void onFinishTyping() {
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
mInputMode = InputMode.getInstance(null, null, null, InputMode.MODE_PASSTHROUGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onBackspace() {
|
||||||
|
// 1. Dialer fields seem to handle backspace on their own and we must ignore it,
|
||||||
|
// otherwise, keyDown race condition occur for all keys.
|
||||||
|
// 2. Allow the assigned key to function normally, when there is no text (e.g. "Back" navigates back)
|
||||||
|
// 3. Some app may need special treatment, so let it be.
|
||||||
|
if (mInputMode.isPassthrough() || !(textField.isThereText() || appHacks.onBackspace(mInputMode))) {
|
||||||
|
Logger.d("onBackspace", "backspace ignored");
|
||||||
|
mInputMode.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
resetKeyRepeat();
|
||||||
|
|
||||||
|
if (mInputMode.onBackspace()) {
|
||||||
|
getSuggestions();
|
||||||
|
} else {
|
||||||
|
suggestionOps.commitCurrent(false);
|
||||||
|
super.sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.d("onBackspace", "backspace handled");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onNumber
|
||||||
|
*
|
||||||
|
* @param key Must be a number from 1 to 9, not a "KeyEvent.KEYCODE_X"
|
||||||
|
* @param hold If "true" we are calling the handler, because the key is being held.
|
||||||
|
* @param repeat If "true" we are calling the handler, because the key was pressed more than once
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
protected boolean onNumber(int key, boolean hold, int repeat) {
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
forceShowWindowIfHidden();
|
||||||
|
|
||||||
|
// Automatically accept the previous word, when the next one is a space or punctuation,
|
||||||
|
// instead of requiring "OK" before that.
|
||||||
|
// First pass, analyze the incoming key press and decide whether it could be the start of
|
||||||
|
// a new word.
|
||||||
|
if (mInputMode.shouldAcceptPreviousSuggestion(key)) {
|
||||||
|
String lastWord = suggestionOps.acceptIncomplete();
|
||||||
|
mInputMode.onAcceptSuggestion(lastWord);
|
||||||
|
autoCorrectSpace(lastWord, false, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-adjust the text case before each word, if the InputMode supports it.
|
||||||
|
if (suggestionOps.getCurrent().isEmpty()) {
|
||||||
|
mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mInputMode.onNumber(key, hold, repeat)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mInputMode.shouldSelectNextSuggestion() && !suggestionOps.isEmpty()) {
|
||||||
|
onHotkey(settings.getKeyNextSuggestion(), false, false);
|
||||||
|
suggestionOps.scheduleDelayedAccept(mInputMode.getAutoAcceptTimeout());
|
||||||
|
} else {
|
||||||
|
getSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean onText(String text, boolean validateOnly) {
|
||||||
|
if (mInputMode.shouldIgnoreText(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestionOps.cancelDelayedAccept();
|
||||||
|
forceShowWindowIfHidden();
|
||||||
|
|
||||||
|
// accept the previously typed word (if any)
|
||||||
|
String lastWord = suggestionOps.acceptIncomplete();
|
||||||
|
mInputMode.onAcceptSuggestion(lastWord);
|
||||||
|
autoCorrectSpace(lastWord, false, -1);
|
||||||
|
|
||||||
|
// "type" and accept the new word
|
||||||
|
mInputMode.onAcceptSuggestion(text);
|
||||||
|
textField.setText(text);
|
||||||
|
autoCorrectSpace(text, true, -1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void autoCorrectSpace(String currentWord, boolean isWordAcceptedManually, int nextKey) {
|
||||||
|
if (mInputMode.shouldDeletePrecedingSpace(inputType)) {
|
||||||
|
textField.deletePrecedingSpace(currentWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mInputMode.shouldAddAutoSpace(inputType, textField, isWordAcceptedManually, nextKey)) {
|
||||||
|
textField.setText(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determineTextCase
|
||||||
|
* Restore the last text case or auto-select a new one. If the InputMode supports it, it can change
|
||||||
|
* the text case based on grammar rules, otherwise we fallback to the input field properties or the
|
||||||
|
* last saved mode.
|
||||||
|
*/
|
||||||
|
private void determineTextCase() {
|
||||||
|
mInputMode.defaultTextCase();
|
||||||
|
mInputMode.setTextFieldCase(textField.determineTextCase(inputType));
|
||||||
|
mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor());
|
||||||
|
InputModeValidator.validateTextCase(mInputMode, settings.getTextCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getInputMode
|
||||||
|
* Load the last input mode or choose a more appropriate one.
|
||||||
|
* Some input fields support only numbers or are not suited for predictions (e.g. password fields)
|
||||||
|
*/
|
||||||
|
protected InputMode getInputMode() {
|
||||||
|
if (!inputType.isValid() || (inputType.isLimited() && !appHacks.isTermux())) {
|
||||||
|
return InputMode.getInstance(settings, mLanguage, inputType, InputMode.MODE_PASSTHROUGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedInputModes = textField.determineInputModes(inputType);
|
||||||
|
int validModeId = InputModeValidator.validateMode(settings.getInputMode(), allowedInputModes);
|
||||||
|
return InputMode.getInstance(settings, mLanguage, inputType, validModeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) {
|
||||||
|
// Logger.d("onUpdateSelection", "oldSelStart: " + oldSelStart + " oldSelEnd: " + oldSelEnd + " newSelStart: " + newSelStart + " oldSelEnd: " + oldSelEnd + " candidatesStart: " + candidatesStart + " candidatesEnd: " + candidatesEnd);
|
||||||
|
|
||||||
|
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd);
|
||||||
|
|
||||||
|
// If the cursor moves while composing a word (usually, because the user has touched the screen outside the word), we must
|
||||||
|
// end typing end accept the word. Otherwise, the cursor would jump back at the end of the word, after the next key press.
|
||||||
|
// This is confusing from user perspective, so we want to avoid it.
|
||||||
|
if (
|
||||||
|
candidatesStart != -1 && candidatesEnd != -1
|
||||||
|
&& (newSelStart != candidatesEnd || newSelEnd != candidatesEnd)
|
||||||
|
&& !suggestionOps.isEmpty()
|
||||||
|
) {
|
||||||
|
mInputMode.onAcceptSuggestion(suggestionOps.acceptIncomplete());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void onAcceptSuggestionAutomatically(String word) {
|
||||||
|
mInputMode.onAcceptSuggestion(word, true);
|
||||||
|
autoCorrectSpace(word, false, -1);
|
||||||
|
mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onAcceptSuggestionsDelayed(String word) {
|
||||||
|
onAcceptSuggestionManually(word, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onAcceptSuggestionManually(String word, int fromKey) {
|
||||||
|
mInputMode.onAcceptSuggestion(word);
|
||||||
|
if (!word.isEmpty()) {
|
||||||
|
autoCorrectSpace(word, true, fromKey);
|
||||||
|
resetKeyRepeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected void getSuggestions() {
|
||||||
|
if (mInputMode instanceof ModePredictive && DictionaryLoader.getInstance(this).isRunning()) {
|
||||||
|
UI.toast(this, R.string.dictionary_loading_please_wait);
|
||||||
|
} else {
|
||||||
|
mInputMode.loadSuggestions(this::handleSuggestions, suggestionOps.getCurrent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void handleSuggestions() {
|
||||||
|
// Second pass, analyze the available suggestions and decide if combining them with the
|
||||||
|
// last key press makes up a compound word like: (it)'s, (I)'ve, l'(oiseau), or it is
|
||||||
|
// just the end of a sentence, like: "word." or "another?"
|
||||||
|
if (mInputMode.shouldAcceptPreviousSuggestion()) {
|
||||||
|
String lastWord = suggestionOps.acceptPrevious(mInputMode.getSequenceLength());
|
||||||
|
onAcceptSuggestionAutomatically(lastWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
// display the word suggestions
|
||||||
|
suggestionOps.set(mInputMode.getSuggestions());
|
||||||
|
|
||||||
|
// In case we are here, because the language was changed, and there were words for the old language,
|
||||||
|
// but there are no words for the new language, we'll get only generated suggestions, consisting
|
||||||
|
// of the last word of the previous language + endings from the new language. These words are invalid,
|
||||||
|
// so we discard them.
|
||||||
|
if (mInputMode instanceof ModePredictive && !mLanguage.isValidWord(suggestionOps.getCurrent()) && !Text.isGraphic(suggestionOps.getCurrent())) {
|
||||||
|
mInputMode.reset();
|
||||||
|
suggestionOps.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush the first suggestion, if the InputMode has requested it
|
||||||
|
if (suggestionOps.scheduleDelayedAccept(mInputMode.getAutoAcceptTimeout())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, 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 trimmedWord = suggestionOps.getCurrent(mInputMode.getSequenceLength());
|
||||||
|
appHacks.setComposingTextWithHighlightedStem(trimmedWord, mInputMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -83,13 +83,12 @@ public class AppHacks {
|
||||||
* setComposingTextWithHighlightedStem
|
* setComposingTextWithHighlightedStem
|
||||||
* A compatibility function for text fields that do not support SpannableString. Effectively disables highlighting.
|
* A compatibility function for text fields that do not support SpannableString. Effectively disables highlighting.
|
||||||
*/
|
*/
|
||||||
public boolean setComposingTextWithHighlightedStem(@NonNull String word) {
|
public void setComposingTextWithHighlightedStem(@NonNull String word, InputMode inputMode) {
|
||||||
if (isKindleInvertedTextField()) {
|
if (isKindleInvertedTextField()) {
|
||||||
textField.setComposingText(word);
|
textField.setComposingText(word);
|
||||||
return true;
|
} else {
|
||||||
|
textField.setComposingTextWithHighlightedStem(word, inputMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,8 +294,8 @@ public class TextField {
|
||||||
return word;
|
return word;
|
||||||
}
|
}
|
||||||
|
|
||||||
// nothing to highlight in an empty word or if the target is beyond the last letter
|
// nothing to highlight in: an empty string; after the last letter; in special characters or emoji, because it breaks them
|
||||||
if (word == null || word.length() == 0 || word.length() <= start) {
|
if (word == null || word.length() == 0 || word.length() <= start || !Character.isLetterOrDigit(word.charAt(0))) {
|
||||||
return word;
|
return word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,37 +3,37 @@ package io.github.sspanak.tt9.ui;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
import io.github.sspanak.tt9.preferences.PreferencesActivity;
|
import io.github.sspanak.tt9.preferences.PreferencesActivity;
|
||||||
|
|
||||||
public class UI {
|
public class UI {
|
||||||
private static Toast toastLang = null;
|
private static Toast toastLang = null;
|
||||||
|
|
||||||
public static void showAddWordDialog(TraditionalT9 tt9, int language, String currentWord) {
|
public static void showAddWordDialog(InputMethodService ims, int language, String currentWord) {
|
||||||
Intent intent = new Intent(tt9, PopupDialogActivity.class);
|
Intent intent = new Intent(ims, PopupDialogActivity.class);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||||
intent.putExtra("word", currentWord);
|
intent.putExtra("word", currentWord);
|
||||||
intent.putExtra("lang", language);
|
intent.putExtra("lang", language);
|
||||||
intent.putExtra("popup_type", PopupDialogActivity.DIALOG_ADD_WORD_INTENT);
|
intent.putExtra("popup_type", PopupDialogActivity.DIALOG_ADD_WORD_INTENT);
|
||||||
tt9.startActivity(intent);
|
ims.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void showConfirmDictionaryUpdateDialog(TraditionalT9 tt9, int language) {
|
public static void showConfirmDictionaryUpdateDialog(InputMethodService ims, int language) {
|
||||||
Intent intent = new Intent(tt9, PopupDialogActivity.class);
|
Intent intent = new Intent(ims, PopupDialogActivity.class);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||||
intent.putExtra("lang", language);
|
intent.putExtra("lang", language);
|
||||||
intent.putExtra("popup_type", PopupDialogActivity.DIALOG_CONFIRM_WORDS_UPDATE_INTENT);
|
intent.putExtra("popup_type", PopupDialogActivity.DIALOG_CONFIRM_WORDS_UPDATE_INTENT);
|
||||||
tt9.startActivity(intent);
|
ims.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,12 +42,12 @@ public class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void showSettingsScreen(TraditionalT9 tt9) {
|
public static void showSettingsScreen(InputMethodService ims) {
|
||||||
Intent prefIntent = new Intent(tt9, PreferencesActivity.class);
|
Intent prefIntent = new Intent(ims, PreferencesActivity.class);
|
||||||
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||||
tt9.hideWindow();
|
ims.hideWindow();
|
||||||
tt9.startActivity(prefIntent);
|
ims.startActivity(prefIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void alert(Context context, int titleResource, int messageResource) {
|
public static void alert(Context context, int titleResource, int messageResource) {
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ import android.view.View;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.util.Logger;
|
|
||||||
import io.github.sspanak.tt9.R;
|
import io.github.sspanak.tt9.R;
|
||||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
import io.github.sspanak.tt9.util.Logger;
|
||||||
|
|
||||||
public class SoftKey extends androidx.appcompat.widget.AppCompatButton implements View.OnTouchListener, View.OnLongClickListener {
|
public class SoftKey extends androidx.appcompat.widget.AppCompatButton implements View.OnTouchListener, View.OnLongClickListener {
|
||||||
protected TraditionalT9 tt9;
|
protected TraditionalT9 tt9;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public class SoftPunctuationKey extends SoftKey {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean handleRelease() {
|
protected boolean handleRelease() {
|
||||||
return tt9.onText(getKeyChar());
|
return tt9.onText(getKeyChar(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.R;
|
import io.github.sspanak.tt9.R;
|
||||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
import io.github.sspanak.tt9.ime.AbstractHandler;
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
public class SuggestionsBar {
|
public class SuggestionsBar {
|
||||||
|
|
@ -27,14 +27,14 @@ public class SuggestionsBar {
|
||||||
private boolean isDarkThemeEnabled = false;
|
private boolean isDarkThemeEnabled = false;
|
||||||
|
|
||||||
private final RecyclerView mView;
|
private final RecyclerView mView;
|
||||||
private final TraditionalT9 tt9;
|
private final AbstractHandler tt9;
|
||||||
private SuggestionsAdapter mSuggestionsAdapter;
|
private SuggestionsAdapter mSuggestionsAdapter;
|
||||||
|
|
||||||
private final Handler alternativeScrollingHandler = new Handler();
|
private final Handler alternativeScrollingHandler = new Handler();
|
||||||
private final int suggestionScrollingDelay;
|
private final int suggestionScrollingDelay;
|
||||||
|
|
||||||
|
|
||||||
public SuggestionsBar(TraditionalT9 tt9, View mainView) {
|
public SuggestionsBar(AbstractHandler tt9, View mainView) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.tt9 = tt9;
|
this.tt9 = tt9;
|
||||||
|
|
@ -100,11 +100,6 @@ public class SuggestionsBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getCurrentSuggestion() {
|
|
||||||
return getSuggestion(selectedIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public String getSuggestion(int id) {
|
public String getSuggestion(int id) {
|
||||||
if (id < 0 || id >= suggestions.size()) {
|
if (id < 0 || id >= suggestions.size()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue