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.res.AssetManager;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
|
||||
|
|
@ -10,9 +11,6 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
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.WordFile;
|
||||
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.SQLiteOpener;
|
||||
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.Language;
|
||||
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
|
||||
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.ui.DictionaryLoadingBar;
|
||||
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 {
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import androidx.annotation.NonNull;
|
|||
|
||||
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.WordList;
|
||||
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.SQLiteOpener;
|
||||
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.Language;
|
||||
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.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 {
|
||||
|
|
@ -32,7 +31,7 @@ public class WordStore {
|
|||
private ReadOps readOps = null;
|
||||
|
||||
|
||||
public WordStore(@NonNull Context context) {
|
||||
private WordStore(@NonNull Context context) {
|
||||
try {
|
||||
sqlite = SQLiteOpener.getInstance(context);
|
||||
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) {
|
||||
context = context == null ? TraditionalT9.getMainContext() : 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;
|
||||
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
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.preferences.SettingsStore;
|
||||
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;
|
||||
|
||||
// debounce handling
|
||||
|
|
@ -66,7 +64,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
*/
|
||||
@Override
|
||||
public View onCreateInputView() {
|
||||
return createSoftKeyView();
|
||||
return createMainView();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -105,7 +103,6 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
@Override
|
||||
public void onFinishInput() {
|
||||
super.onFinishInput();
|
||||
// Logger.d("onFinishInput", "When is this called?");
|
||||
onStop();
|
||||
}
|
||||
|
||||
|
|
@ -256,43 +253,7 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
|
||||
|
||||
private boolean handleHotkey(int keyCode, boolean hold, boolean repeat, boolean validateOnly) {
|
||||
if (keyCode == settings.getKeyAddWord() * (hold ? -1 : 1)) {
|
||||
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;
|
||||
return onHotkey(keyCode * (hold ? -1 : 1), repeat, validateOnly);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -321,34 +282,4 @@ abstract class KeyPadHandler extends InputMethodService {
|
|||
|
||||
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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
|
@ -13,130 +11,21 @@ import android.view.inputmethod.InputMethodManager;
|
|||
|
||||
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.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.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.helpers.Hotkeys;
|
||||
import io.github.sspanak.tt9.ui.PopupDialogActivity;
|
||||
import io.github.sspanak.tt9.ui.UI;
|
||||
import io.github.sspanak.tt9.ui.main.MainView;
|
||||
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.Text;
|
||||
|
||||
public class TraditionalT9 extends KeyPadHandler {
|
||||
private InputConnection currentInputConnection = null;
|
||||
// internal settings/data
|
||||
@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
|
||||
public class TraditionalT9 extends HotkeyHandler {
|
||||
@NonNull
|
||||
private final Handler normalizationHandler = new Handler(Looper.getMainLooper());
|
||||
private MainView mainView = 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
|
||||
|
|
@ -147,7 +36,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
if (message != null) {
|
||||
forceShowWindowIfHidden();
|
||||
if (!message.isEmpty()) {
|
||||
UI.toastLong(self, message);
|
||||
UI.toastLong(this, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,8 +44,8 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onInit() {
|
||||
self = this;
|
||||
Logger.setLevel(settings.getLogLevel());
|
||||
|
||||
WordStoreAsync.init(this);
|
||||
|
|
@ -166,42 +55,21 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
initTray();
|
||||
}
|
||||
|
||||
validateFunctionKeys();
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
|
||||
private void initTray() {
|
||||
setInputView(mainView.getView());
|
||||
createSuggestionBar(mainView.getView());
|
||||
statusBar = new StatusBar(mainView.getView());
|
||||
suggestionBar = new SuggestionsBar(this, mainView.getView());
|
||||
}
|
||||
|
||||
|
||||
private void setDarkTheme() {
|
||||
mainView.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()) {
|
||||
initTray();
|
||||
}
|
||||
statusBar.setText(mInputMode.toString());
|
||||
setStatusText(mInputMode.toString());
|
||||
setDarkTheme();
|
||||
mainView.render();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onStart(InputConnection connection, EditorInfo field) {
|
||||
Logger.setLevel(settings.getLogLevel());
|
||||
|
||||
setInputField(connection, field);
|
||||
initTyping();
|
||||
super.onStart(connection, field);
|
||||
|
||||
if (mInputMode.isPassthrough()) {
|
||||
// When the input is invalid or simple, let Android handle it.
|
||||
|
|
@ -229,25 +97,25 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
}
|
||||
|
||||
normalizationHandler.removeCallbacksAndMessages(null);
|
||||
systemLanguage = LanguageCollection.getDefault(this);
|
||||
|
||||
initUi();
|
||||
updateInputViewShown();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onFinishTyping() {
|
||||
cancelAutoAccept();
|
||||
if (!(mInputMode instanceof ModePassthrough)) {
|
||||
DictionaryLoader.autoLoad(this, mLanguage);
|
||||
}
|
||||
mInputMode = InputMode.getInstance(null, null, null, InputMode.MODE_PASSTHROUGH);
|
||||
super.onFinishTyping();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
onFinishTyping();
|
||||
clearSuggestions();
|
||||
suggestionOps.clear();
|
||||
statusBar.setText("--");
|
||||
|
||||
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
|
||||
*
|
||||
* @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
|
||||
* createMainView
|
||||
* Generates the actual UI of TT9.
|
||||
*/
|
||||
protected View createSoftKeyView() {
|
||||
protected View createMainView() {
|
||||
mainView.forceCreateView();
|
||||
initTray();
|
||||
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
|
||||
* 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
|
||||
protected boolean shouldBeVisible() {
|
||||
return !getInputMode().isPassthrough();
|
||||
|
|
@ -854,4 +182,31 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
protected boolean shouldBeOff() {
|
||||
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
|
||||
* 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()) {
|
||||
textField.setComposingText(word);
|
||||
return true;
|
||||
} else {
|
||||
textField.setComposingTextWithHighlightedStem(word, inputMode);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -294,8 +294,8 @@ public class TextField {
|
|||
return word;
|
||||
}
|
||||
|
||||
// nothing to highlight in an empty word or if the target is beyond the last letter
|
||||
if (word == null || word.length() == 0 || word.length() <= start) {
|
||||
// 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 || !Character.isLetterOrDigit(word.charAt(0))) {
|
||||
return word;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,37 +3,37 @@ package io.github.sspanak.tt9.ui;
|
|||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.Looper;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.preferences.PreferencesActivity;
|
||||
|
||||
public class UI {
|
||||
private static Toast toastLang = null;
|
||||
|
||||
public static void showAddWordDialog(TraditionalT9 tt9, int language, String currentWord) {
|
||||
Intent intent = new Intent(tt9, PopupDialogActivity.class);
|
||||
public static void showAddWordDialog(InputMethodService ims, int language, String currentWord) {
|
||||
Intent intent = new Intent(ims, PopupDialogActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||
intent.putExtra("word", currentWord);
|
||||
intent.putExtra("lang", language);
|
||||
intent.putExtra("popup_type", PopupDialogActivity.DIALOG_ADD_WORD_INTENT);
|
||||
tt9.startActivity(intent);
|
||||
ims.startActivity(intent);
|
||||
}
|
||||
|
||||
|
||||
public static void showConfirmDictionaryUpdateDialog(TraditionalT9 tt9, int language) {
|
||||
Intent intent = new Intent(tt9, PopupDialogActivity.class);
|
||||
public static void showConfirmDictionaryUpdateDialog(InputMethodService ims, int language) {
|
||||
Intent intent = new Intent(ims, PopupDialogActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||
intent.putExtra("lang", language);
|
||||
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) {
|
||||
Intent prefIntent = new Intent(tt9, PreferencesActivity.class);
|
||||
public static void showSettingsScreen(InputMethodService ims) {
|
||||
Intent prefIntent = new Intent(ims, PreferencesActivity.class);
|
||||
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||
tt9.hideWindow();
|
||||
tt9.startActivity(prefIntent);
|
||||
ims.hideWindow();
|
||||
ims.startActivity(prefIntent);
|
||||
}
|
||||
|
||||
public static void alert(Context context, int titleResource, int messageResource) {
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ import android.view.View;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import io.github.sspanak.tt9.util.Logger;
|
||||
import io.github.sspanak.tt9.R;
|
||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||
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 {
|
||||
protected TraditionalT9 tt9;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class SoftPunctuationKey extends SoftKey {
|
|||
|
||||
@Override
|
||||
protected boolean handleRelease() {
|
||||
return tt9.onText(getKeyChar());
|
||||
return tt9.onText(getKeyChar(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
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;
|
||||
|
||||
public class SuggestionsBar {
|
||||
|
|
@ -27,14 +27,14 @@ public class SuggestionsBar {
|
|||
private boolean isDarkThemeEnabled = false;
|
||||
|
||||
private final RecyclerView mView;
|
||||
private final TraditionalT9 tt9;
|
||||
private final AbstractHandler tt9;
|
||||
private SuggestionsAdapter mSuggestionsAdapter;
|
||||
|
||||
private final Handler alternativeScrollingHandler = new Handler();
|
||||
private final int suggestionScrollingDelay;
|
||||
|
||||
|
||||
public SuggestionsBar(TraditionalT9 tt9, View mainView) {
|
||||
public SuggestionsBar(AbstractHandler tt9, View mainView) {
|
||||
super();
|
||||
|
||||
this.tt9 = tt9;
|
||||
|
|
@ -100,11 +100,6 @@ public class SuggestionsBar {
|
|||
}
|
||||
|
||||
|
||||
public String getCurrentSuggestion() {
|
||||
return getSuggestion(selectedIndex);
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
public String getSuggestion(int id) {
|
||||
if (id < 0 || id >= suggestions.size()) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue