1
0
Fork 0

Touchscreen support + small improvements

* Soft-Keyboard numpad

* no more SoftKeyHandler, the main view is in its own package

* settings are no longer passed unnecessarilly here and there

* fixed numeric mode not working in some cases

* simplified suggestion handling

* fixed crashing when changing the phone orientation
This commit is contained in:
nebkrid 2023-02-17 22:09:31 +01:00 committed by Dimo Karaivanov
parent 7f6cd6110d
commit 4e5416f6b4
36 changed files with 1142 additions and 368 deletions

View file

@ -143,36 +143,17 @@ abstract class KeyPadHandler extends InputMethodService {
isBackspaceHandled = false;
}
if (Key.isOK(keyCode)) {
return true;
}
// In numeric fields, we do not want to handle anything, but "backspace"
if (mEditing == EDITING_STRICT_NUMERIC) {
return false;
}
// holding "0" is important in all cases
if (keyCode == KeyEvent.KEYCODE_0) {
event.startTracking();
return true;
}
// In dialer fields we just want passthrough, but we do handle holding "0",
// to convert it to "+".
if (mEditing == EDITING_DIALER) {
return false;
}
// start tracking key hold
if (shouldTrackNumPress() || Key.isHotkey(settings, -keyCode)) {
if (Key.isNumber(keyCode) || Key.isHotkey(settings, -keyCode)) {
event.startTracking();
}
return Key.isHotkey(settings, keyCode) || Key.isHotkey(settings, -keyCode)
return
Key.isNumber(keyCode)
|| Key.isOK(keyCode)
|| Key.isHotkey(settings, keyCode) || Key.isHotkey(settings, -keyCode)
|| keyCode == KeyEvent.KEYCODE_STAR
|| keyCode == KeyEvent.KEYCODE_POUND
|| (Key.isNumber(keyCode) && shouldTrackNumPress())
|| ((keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && shouldTrackUpDown())
|| ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) && shouldTrackLeftRight());
}
@ -217,12 +198,15 @@ abstract class KeyPadHandler extends InputMethodService {
return false;
}
// Logger.d("onKeyUp", "Key: " + keyCode + " repeat?: " + event.getRepeatCount());
if (keyCode == ignoreNextKeyUp) {
// Logger.d("onKeyUp", "Ignored: " + keyCode);
ignoreNextKeyUp = 0;
return true;
}
// repeat handling
keyRepeatCounter = (lastKeyCode == keyCode) ? keyRepeatCounter + 1 : 0;
lastKeyCode = keyCode;
@ -231,46 +215,31 @@ abstract class KeyPadHandler extends InputMethodService {
lastNumKeyCode = keyCode;
}
// Logger.d("onKeyUp", "Key: " + keyCode + " repeat?: " + event.getRepeatCount());
// backspace is handled in onKeyDown only, so we ignore it here
if (isBackspaceHandled) {
return true;
}
if (Key.isOK(keyCode)) {
return onOK();
}
// in numeric fields, we just handle backspace and let the rest go as-is.
if (mEditing == EDITING_STRICT_NUMERIC) {
return false;
}
if (keyCode == KeyEvent.KEYCODE_0) {
return onNumber(Key.codeToNumber(settings, keyCode), false, numKeyRepeatCounter);
}
// dialer fields are similar to pure numeric fields, but for user convenience, holding "0"
// is converted to "+"
if (mEditing == EDITING_DIALER) {
return false;
}
if (handleHotkey(keyCode, false)) {
return true;
}
if (Key.isNumber(keyCode)) {
return onNumber(Key.codeToNumber(settings, keyCode), false, numKeyRepeatCounter);
}
if (Key.isOK(keyCode)) {
return onOK();
}
if (handleHotkey(keyCode, false)) {
return true;
}
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP: return onUp();
case KeyEvent.KEYCODE_DPAD_DOWN: return onDown();
case KeyEvent.KEYCODE_DPAD_LEFT: return onLeft();
case KeyEvent.KEYCODE_DPAD_RIGHT: return onRight(keyRepeatCounter > 0);
case KeyEvent.KEYCODE_STAR: return onStar();
case KeyEvent.KEYCODE_POUND: return onPound();
case KeyEvent.KEYCODE_STAR:
case KeyEvent.KEYCODE_POUND:
return onOtherKey(keyCode);
}
return false;
@ -324,8 +293,7 @@ abstract class KeyPadHandler extends InputMethodService {
abstract protected boolean onLeft();
abstract protected boolean onRight(boolean repeat);
abstract protected boolean onNumber(int key, boolean hold, int repeat);
abstract protected boolean onStar();
abstract protected boolean onPound();
abstract protected boolean onOtherKey(int keyCode);
// customized key handlers
abstract protected boolean onKeyAddWord();

View file

@ -1,150 +0,0 @@
package io.github.sspanak.tt9.ime;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.core.content.ContextCompat;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ui.UI;
class SoftKeyHandler implements View.OnTouchListener {
private static final int[] buttons = { R.id.main_left, R.id.main_mid, R.id.main_right };
private final TraditionalT9 tt9;
private View view = null;
private long lastBackspaceCall = 0;
public SoftKeyHandler(TraditionalT9 tt9) {
this.tt9 = tt9;
getView();
}
View getView() {
if (view == null) {
view = View.inflate(tt9.getApplicationContext(), R.layout.mainview, null);
for (int buttonId : buttons) {
view.findViewById(buttonId).setOnTouchListener(this);
}
}
return view;
}
void show() {
if (view != null) {
view.setVisibility(View.VISIBLE);
}
}
void hide() {
if (view != null) {
view.setVisibility(View.GONE);
}
}
void setSoftKeysVisibility(boolean visible) {
if (view != null) {
view.findViewById(R.id.main_soft_keys).setVisibility(visible ? LinearLayout.VISIBLE : LinearLayout.GONE);
}
}
/** setDarkTheme
* Changes the main view colors according to the theme.
*
* We need to do this manually, instead of relying on the Context to resolve the appropriate colors,
* because this View is part of the main service View. And service Views are always locked to the
* system context and theme.
*
* More info:
* https://stackoverflow.com/questions/72382886/system-applies-night-mode-to-views-added-in-service-type-application-overlay
*/
void setDarkTheme(boolean darkEnabled) {
if (view == null) {
return;
}
// background
view.findViewById(R.id.main_soft_keys).setBackground(ContextCompat.getDrawable(
view.getContext(),
darkEnabled ? R.drawable.button_background_dark : R.drawable.button_background
));
// text
int textColor = ContextCompat.getColor(
view.getContext(),
darkEnabled ? R.color.dark_button_text : R.color.button_text
);
for (int buttonId : buttons) {
Button button = view.findViewById(buttonId);
button.setTextColor(textColor);
}
// separators
Drawable separatorColor = ContextCompat.getDrawable(
view.getContext(),
darkEnabled ? R.drawable.button_separator_dark : R.drawable.button_separator
);
view.findViewById(R.id.main_separator_left).setBackground(separatorColor);
view.findViewById(R.id.main_separator_right).setBackground(separatorColor);
}
private boolean handleBackspaceHold() {
if (System.currentTimeMillis() - lastBackspaceCall < tt9.settings.getSoftKeyRepeatDelay()) {
return true;
}
boolean handled = tt9.onBackspace();
long now = System.currentTimeMillis();
lastBackspaceCall = lastBackspaceCall == 0 ? tt9.settings.getSoftKeyInitialDelay() + now : now;
return handled;
}
private boolean handleBackspaceUp() {
lastBackspaceCall = 0;
return true;
}
@Override
public boolean onTouch(View view, MotionEvent event) {
int action = event.getAction();
int buttonId = view.getId();
if (buttonId == R.id.main_left && action == MotionEvent.ACTION_UP) {
UI.showSettingsScreen(tt9);
return view.performClick();
}
if (buttonId == R.id.main_mid && action == MotionEvent.ACTION_UP) {
tt9.onOK();
return view.performClick();
}
if (buttonId == R.id.main_right) {
if (action == MotionEvent.AXIS_PRESSURE) {
return handleBackspaceHold();
} else if (action == MotionEvent.ACTION_UP) {
return handleBackspaceUp();
}
}
return false;
}
}

View file

@ -17,13 +17,16 @@ import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.db.DictionaryDb;
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
import io.github.sspanak.tt9.ime.helpers.InputType;
import io.github.sspanak.tt9.ime.helpers.Key;
import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.ui.bottom.StatusBar;
import io.github.sspanak.tt9.ui.bottom.SuggestionsBar;
import io.github.sspanak.tt9.preferences.SettingsStore;
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;
public class TraditionalT9 extends KeyPadHandler {
// internal settings/data
@ -40,7 +43,7 @@ public class TraditionalT9 extends KeyPadHandler {
protected Language mLanguage;
// soft key view
private SoftKeyHandler softKeyHandler = null;
private MainView mainView = null;
private StatusBar statusBar = null;
private SuggestionsBar suggestionBar = null;
@ -50,6 +53,10 @@ public class TraditionalT9 extends KeyPadHandler {
return self.getApplicationContext();
}
public SettingsStore getSettings() {
return settings;
}
private void loadSettings() {
mLanguage = LanguageCollection.getLanguage(settings.getInputLanguage());
@ -80,16 +87,9 @@ public class TraditionalT9 extends KeyPadHandler {
DictionaryDb.init(this);
DictionaryDb.normalizeWordFrequencies(settings);
if (softKeyHandler == null) {
softKeyHandler = new SoftKeyHandler(this);
}
if (statusBar == null) {
statusBar = new StatusBar(softKeyHandler.getView());
}
if (suggestionBar == null) {
suggestionBar = new SuggestionsBar(settings, softKeyHandler.getView());
if (mainView == null) {
mainView = new MainView(this);
initTray();
}
loadSettings();
@ -115,17 +115,28 @@ public class TraditionalT9 extends KeyPadHandler {
}
private void initUi() {
statusBar
.setText(mInputMode != null ? mInputMode.toString() : "")
.setDarkTheme(settings.getDarkTheme());
private void initTray() {
setInputView(mainView.getView());
statusBar = new StatusBar(mainView.getView());
suggestionBar = new SuggestionsBar(this, mainView.getView());
}
clearSuggestions();
private void setDarkTheme() {
mainView.setDarkTheme(settings.getDarkTheme());
statusBar.setDarkTheme(settings.getDarkTheme());
suggestionBar.setDarkTheme(settings.getDarkTheme());
}
softKeyHandler.setDarkTheme(settings.getDarkTheme());
softKeyHandler.setSoftKeysVisibility(settings.getShowSoftKeys());
softKeyHandler.show();
private void initUi() {
if (mainView.createView()) {
initTray();
}
clearSuggestions();
statusBar.setText(mInputMode != null ? mInputMode.toString() : "");
setDarkTheme();
mainView.render();
}
@ -163,8 +174,6 @@ public class TraditionalT9 extends KeyPadHandler {
protected void onStop() {
onFinishTyping();
clearSuggestions();
softKeyHandler.hide();
}
@ -193,10 +202,10 @@ public class TraditionalT9 extends KeyPadHandler {
public boolean onOK() {
if (!textField.isThereText()) {
if (!isInputViewShown() && !textField.isThereText()) {
forceShowWindowIfHidden();
return true;
} else if (isSuggestionViewHidden() && currentInputConnection != null) {
} else if (isSuggestionViewHidden()) {
return performOKAction();
}
@ -275,12 +284,10 @@ public class TraditionalT9 extends KeyPadHandler {
String currentWord = getComposingText();
// Automatically accept the current word, when the next one is a space or whatnot,
// Automatically accept the current word, when the next one is a space or punctuation,
// instead of requiring "OK" before that.
if (mInputMode.shouldAcceptCurrentSuggestion(key, hold, repeat > 0)) {
mInputMode.onAcceptSuggestion(currentWord);
commitCurrentSuggestion(false);
autoCorrectSpace(currentWord, false, key, hold, repeat > 0);
autoCorrectSpace(acceptIncompleteSuggestion(), false, key, hold, repeat > 0);
currentWord = "";
}
@ -296,17 +303,6 @@ public class TraditionalT9 extends KeyPadHandler {
if (mInputMode.shouldSelectNextSuggestion() && !isSuggestionViewHidden()) {
nextSuggestion();
return true;
}
if (mInputMode.getWord() != null) {
currentWord = mInputMode.getWord();
mInputMode.onAcceptSuggestion(currentWord);
textField.setText(currentWord);
clearSuggestions();
autoCorrectSpace(currentWord, true, key, hold, repeat > 0);
resetKeyRepeat();
} else {
getSuggestions();
}
@ -315,19 +311,33 @@ public class TraditionalT9 extends KeyPadHandler {
}
protected boolean onPound() {
textField.setText("#");
public boolean onOtherKey(int keyCode) {
if (
keyCode <= 0 ||
(mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER) && !Key.isNumber(keyCode)
) {
return false;
}
autoCorrectSpace(acceptIncompleteSuggestion(), false, -1, false, false);
sendDownUpKeyEvents(keyCode);
return true;
}
protected boolean onStar() {
textField.setText("*");
public boolean onText(String text) {
if (mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER || text.length() == 0) {
return false;
}
autoCorrectSpace(acceptIncompleteSuggestion(), false, -1, false, false);
textField.setText(text);
autoCorrectSpace(text, false, -1, false, false);
return true;
}
protected boolean onKeyAddWord() {
public boolean onKeyAddWord() {
if (mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER) {
return false;
}
@ -337,7 +347,7 @@ public class TraditionalT9 extends KeyPadHandler {
}
protected boolean onKeyNextLanguage() {
public boolean onKeyNextLanguage() {
if (nextLang()) {
commitCurrentSuggestion(false);
mInputMode.changeLanguage(mLanguage);
@ -345,6 +355,7 @@ public class TraditionalT9 extends KeyPadHandler {
resetKeyRepeat();
clearSuggestions();
statusBar.setText(mInputMode.toString());
mainView.render();
forceShowWindowIfHidden();
return true;
@ -354,14 +365,15 @@ public class TraditionalT9 extends KeyPadHandler {
}
protected boolean onKeyNextInputMode() {
public boolean onKeyNextInputMode() {
nextInputMode();
mainView.render();
forceShowWindowIfHidden();
return (mEditing != EDITING_STRICT_NUMERIC && mEditing != EDITING_DIALER);
}
protected boolean onKeyShowSettings() {
public boolean onKeyShowSettings() {
if (mEditing == EDITING_DIALER) {
return false;
}
@ -414,6 +426,15 @@ public class TraditionalT9 extends KeyPadHandler {
}
private String acceptIncompleteSuggestion() {
String currentWord = getComposingText();
mInputMode.onAcceptSuggestion(currentWord);
commitCurrentSuggestion(false);
return currentWord;
}
private void commitCurrentSuggestion() {
commitCurrentSuggestion(true);
}
@ -448,9 +469,22 @@ public class TraditionalT9 extends KeyPadHandler {
private void handleSuggestions() {
// key code "suggestions" take priority over words
if (mInputMode.getKeyCode() > 0) {
sendDownUpKeyEvents(mInputMode.getKeyCode());
mInputMode.onAcceptSuggestion(null);
}
// display the list of suggestions
setSuggestions(mInputMode.getSuggestions());
// Put the first suggestion in the text field,
// flush the first suggestion immediately, if the InputMode has requested it
if (mInputMode.getAutoAcceptTimeout() == 0) {
onOK();
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();
@ -615,6 +649,10 @@ public class TraditionalT9 extends KeyPadHandler {
private boolean performOKAction() {
if (currentInputConnection == null) {
return false;
}
int action = textField.getAction();
switch (action) {
case EditorInfo.IME_ACTION_NONE:
@ -684,7 +722,10 @@ public class TraditionalT9 extends KeyPadHandler {
* Generates the actual UI of TT9.
*/
protected View createSoftKeyView() {
return softKeyHandler.getView();
mainView.forceCreateView();
initTray();
setDarkTheme();
return mainView.getView();
}

View file

@ -65,4 +65,12 @@ public class Key {
return -1;
}
}
public static int numberToCode(int number) {
if (number >= 0 && number <= 9) {
return KeyEvent.KEYCODE_0 + number;
} else {
return -1;
}
}
}

View file

@ -27,9 +27,10 @@ abstract public class InputMode {
protected int textFieldTextCase = CASE_UNDEFINED;
// data
protected int autoAcceptTimeout = -1;
protected Language language;
protected ArrayList<String> suggestions = new ArrayList<>();
protected String word = null;
protected int keyCode = 0;
public static InputMode getInstance(SettingsStore settings, Language language, int mode) {
@ -47,7 +48,7 @@ abstract public class InputMode {
// Key handlers. Return "true" when handling the key or "false", when is nothing to do.
public boolean onBackspace() { return false; }
abstract public boolean onNumber(int key, boolean hold, int repeat);
abstract public boolean onNumber(int number, boolean hold, int repeat);
// Predictions
public void onAcceptSuggestion(String suggestion) {}
@ -63,9 +64,6 @@ abstract public class InputMode {
return newSuggestions;
}
// Word
public String getWord() { return word; }
// Mode identifiers
public boolean isPredictive() { return false; }
public boolean isABC() { return false; }
@ -74,6 +72,10 @@ abstract public class InputMode {
// Utility
abstract public int getId();
abstract public int getSequenceLength(); // The number of key presses for the current word.
public int getAutoAcceptTimeout() {
return autoAcceptTimeout;
}
public int getKeyCode() { return keyCode; }
public void changeLanguage(Language newLanguage) {
if (newLanguage != null) {
language = newLanguage;
@ -90,8 +92,9 @@ abstract public class InputMode {
public boolean shouldTrackLeftRight() { return false; }
public void reset() {
suggestions = new ArrayList<>();
word = null;
autoAcceptTimeout = -1;
keyCode = 0;
suggestions.clear();
}
// Text case

View file

@ -2,7 +2,7 @@ package io.github.sspanak.tt9.ime.modes;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import io.github.sspanak.tt9.ime.helpers.Key;
public class Mode123 extends InputMode {
public int getId() { return MODE_123; }
@ -11,21 +11,24 @@ public class Mode123 extends InputMode {
allowedTextCases.add(CASE_LOWER);
}
@Override
public boolean onNumber(int number, boolean hold, int repeat) {
reset();
public boolean onNumber(int key, boolean hold, int repeat) {
if (key != 0) {
return false;
if (number == 0 && hold) {
autoAcceptTimeout = 0;
suggestions.add("+");
} else {
keyCode = Key.numberToCode(number);
}
suggestions = new ArrayList<>();
word = hold ? "+" : "0";
return true;
}
final public boolean is123() { return true; }
public int getSequenceLength() { return 0; }
public boolean shouldTrackNumPress() { return false; }
@Override final public boolean is123() { return true; }
@Override public int getSequenceLength() { return 0; }
@Override public boolean shouldTrackNumPress() { return false; }
@NonNull
@Override

View file

@ -2,8 +2,6 @@ package io.github.sspanak.tt9.ime.modes;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import io.github.sspanak.tt9.languages.Language;
public class ModeABC extends InputMode {
@ -17,16 +15,16 @@ public class ModeABC extends InputMode {
@Override
public boolean onNumber(int key, boolean hold, int repeat) {
shouldSelectNextLetter = false;
suggestions = language.getKeyCharacters(key);
word = null;
public boolean onNumber(int number, boolean hold, int repeat) {
if (hold) {
suggestions = new ArrayList<>();
word = String.valueOf(key);
reset();
suggestions.add(String.valueOf(number));
autoAcceptTimeout = 0;
} else if (repeat > 0) {
shouldSelectNextLetter = true;
} else {
reset();
suggestions.addAll(language.getKeyCharacters(number));
}
return true;
@ -59,6 +57,12 @@ public class ModeABC extends InputMode {
return shouldSelectNextLetter;
}
@Override
public void reset() {
super.reset();
shouldSelectNextLetter = false;
}
@NonNull
@Override
public String toString() {

View file

@ -13,7 +13,6 @@ import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace;
import io.github.sspanak.tt9.ime.modes.helpers.AutoTextCase;
import io.github.sspanak.tt9.ime.modes.helpers.Predictions;
import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.SettingsStore;
@ -69,40 +68,25 @@ public class ModePredictive extends InputMode {
@Override
public boolean onNumber(int key, boolean hold, int repeat) {
public boolean onNumber(int number, boolean hold, int repeat) {
if (hold) {
// hold to type any digit
reset();
word = String.valueOf(key);
} else if (key == 0 && repeat > 0) {
onDouble0();
autoAcceptTimeout = 0;
suggestions.add(String.valueOf(number));
} else {
// words
super.reset();
digitSequence += key;
digitSequence += number;
if (number == 0 && repeat > 0) {
autoAcceptTimeout = 0;
}
}
return true;
}
/**
* onDouble0
* Double "0" is a shortcut for the preferred character.
*/
private void onDouble0() {
try {
reset();
word = settings.getDoubleZeroChar();
digitSequence = language.getDigitSequenceForWord(word);
} catch (InvalidLanguageCharactersException e) {
Logger.w("tt9/onDouble0", "Failed getting the sequence for word: '" + word + "'. Performing standard 0-key action.");
reset();
digitSequence = "0";
}
}
@Override
public void changeLanguage(Language language) {
super.changeLanguage(language);
@ -217,7 +201,6 @@ public class ModePredictive extends InputMode {
.setWordsChangedHandler(handleSuggestions);
handleSuggestionsExternal = handler;
super.reset();
return predictions.load();
}

View file

@ -27,7 +27,7 @@ public class Predictions {
private Handler wordsChangedHandler;
// data
private ArrayList<String> words = new ArrayList<>();
private final ArrayList<String> words = new ArrayList<>();
// punctuation/emoji
private final Pattern containsOnly1Regex = Pattern.compile("^1+$");
@ -127,7 +127,7 @@ public class Predictions {
*/
public boolean load() {
if (digitSequence == null || digitSequence.length() == 0) {
words = new ArrayList<>();
words.clear();
onWordsChanged();
return false;
}
@ -155,16 +155,27 @@ public class Predictions {
* Returns "false", when there are no static options for the current digitSequence.
*/
private boolean loadStatic() {
// whitespace/special/math characters
if (digitSequence.equals("0")) {
words.clear();
stem = "";
words = language.getKeyCharacters(0, false);
} else if (containsOnly1Regex.matcher(digitSequence).matches()) {
words.addAll(language.getKeyCharacters(0, false));
}
// "00" is a shortcut for the preferred character
else if (digitSequence.equals("00")) {
words.clear();
stem = "";
words.add(settings.getDoubleZeroChar());
}
// emoji
else if (containsOnly1Regex.matcher(digitSequence).matches()) {
words.clear();
stem = "";
if (digitSequence.length() == 1) {
words = language.getKeyCharacters(1, false);
words.addAll(language.getKeyCharacters(1, false));
} else {
digitSequence = digitSequence.length() <= maxEmojiSequence.length() ? digitSequence : maxEmojiSequence;
words = Characters.getEmoji(digitSequence.length() - 2);
words.addAll(Characters.getEmoji(digitSequence.length() - 2));
}
} else {
return false;

View file

@ -205,10 +205,9 @@ public class SettingsStore {
public boolean getDarkTheme() { return prefs.getBoolean("pref_dark_theme", true); }
public boolean getShowSoftKeys() { return prefs.getBoolean("pref_show_soft_keys", true); }
public boolean getShowSoftNumpad() { return getShowSoftKeys() && prefs.getBoolean("pref_show_soft_numpad", false); }
/************* typing settings *************/

View file

@ -0,0 +1,92 @@
package io.github.sspanak.tt9.ui.main;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import io.github.sspanak.tt9.ime.TraditionalT9;
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
abstract class BaseMainLayout {
protected TraditionalT9 tt9;
private final int xml;
protected View view = null;
protected ArrayList<SoftKey> keys = new ArrayList<>();
public BaseMainLayout(TraditionalT9 tt9, int xml) {
this.tt9 = tt9;
this.xml = xml;
}
/** setDarkTheme
* Changes the main view colors according to the theme.
*
* We need to do this manually, instead of relying on the Context to resolve the appropriate colors,
* because this View is part of the main service View. And service Views are always locked to the
* system context and theme.
*
* More info:
* <a href="https://stackoverflow.com/questions/72382886/system-applies-night-mode-to-views-added-in-service-type-application-overlay">...</a>
*/
abstract public void setDarkTheme(boolean yes);
/**
* render
* Do all the necessary stuff to display the View.
*/
abstract public void render();
/**
* getKeys
* Returns a list of all the usable Soft Keys.
*/
abstract protected ArrayList<SoftKey> getKeys();
public View getView() {
if (view == null) {
view = View.inflate(tt9.getApplicationContext(), xml, null);
}
return view;
}
public void show() {
if (view != null) {
view.setVisibility(View.VISIBLE);
}
}
public void hide() {
if (view != null) {
view.setVisibility(View.GONE);
}
}
public void enableClickHandlers() {
for (SoftKey key : getKeys()) {
key.setTT9(tt9);
}
}
protected ArrayList<SoftKey> getKeysFromContainer(ViewGroup container) {
ArrayList<SoftKey> keyList = new ArrayList<>();
final int childrenCount = container != null ? container.getChildCount() : 0;
for (int i = 0; i < childrenCount; i++) {
View child = container.getChildAt(i);
if (child instanceof SoftKey) {
keyList.add((SoftKey) child);
}
}
return keyList;
}
}

View file

@ -0,0 +1,64 @@
package io.github.sspanak.tt9.ui.main;
import android.view.View;
import android.view.ViewGroup;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.TraditionalT9;
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
class MainLayoutNumpad extends BaseMainLayout {
public MainLayoutNumpad(TraditionalT9 tt9) {
super(tt9, R.layout.main_numpad);
}
@Override
public void setDarkTheme(boolean darkEnabled) {
if (view == null) {
return;
}
// background
view.setBackground(ContextCompat.getDrawable(
view.getContext(),
darkEnabled ? R.color.dark_numpad_background : R.color.numpad_background
));
// text
for (SoftKey key : getKeys()) {
key.setDarkTheme(darkEnabled);
}
}
@Override
public void render() {
getView();
enableClickHandlers();
for (SoftKey key : getKeys()) {
key.render();
}
}
@Override
protected ArrayList<SoftKey> getKeys() {
if (keys != null && keys.size() > 0) {
return keys;
}
ViewGroup table = view.findViewById(R.id.main_soft_keys);
int tableRowsCount = table.getChildCount();
for (int rowId = 0; rowId < tableRowsCount; rowId++) {
View row = table.getChildAt(rowId);
if (row instanceof ViewGroup) {
keys.addAll(getKeysFromContainer((ViewGroup) row));
}
}
return keys;
}
}

View file

@ -0,0 +1,67 @@
package io.github.sspanak.tt9.ui.main;
import android.graphics.drawable.Drawable;
import android.widget.LinearLayout;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.TraditionalT9;
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
class MainLayoutSmall extends BaseMainLayout {
public MainLayoutSmall(TraditionalT9 tt9) {
super(tt9, R.layout.main_small);
}
private void setSoftKeysVisibility() {
if (view != null) {
view.findViewById(R.id.main_soft_keys).setVisibility(tt9.getSettings().getShowSoftKeys() ? LinearLayout.VISIBLE : LinearLayout.GONE);
}
}
@Override
public void render() {
getView();
enableClickHandlers();
setSoftKeysVisibility();
}
@Override
final public void setDarkTheme(boolean darkEnabled) {
if (view == null) {
return;
}
// background
view.findViewById(R.id.main_soft_keys).setBackground(ContextCompat.getDrawable(
view.getContext(),
darkEnabled ? R.drawable.button_background_dark : R.drawable.button_background
));
// text
for (SoftKey key : getKeys()) {
key.setDarkTheme(darkEnabled);
}
// separators
Drawable separatorColor = ContextCompat.getDrawable(
view.getContext(),
darkEnabled ? R.drawable.button_separator_dark : R.drawable.button_separator
);
view.findViewById(R.id.main_separator_left).setBackground(separatorColor);
view.findViewById(R.id.main_separator_right).setBackground(separatorColor);
}
@Override
protected ArrayList<SoftKey> getKeys() {
if (view != null && (keys == null || keys.size() == 0)) {
keys = getKeysFromContainer(view.findViewById(R.id.main_soft_keys));
}
return keys;
}
}

View file

@ -0,0 +1,47 @@
package io.github.sspanak.tt9.ui.main;
import android.view.View;
import io.github.sspanak.tt9.ime.TraditionalT9;
public class MainView {
private final TraditionalT9 tt9;
private BaseMainLayout main;
public MainView(TraditionalT9 tt9) {
this.tt9 = tt9;
forceCreateView();
}
public boolean createView() {
if (tt9.getSettings().getShowSoftNumpad() && !(main instanceof MainLayoutNumpad)) {
main = new MainLayoutNumpad(tt9);
main.render();
return true;
} else if (!tt9.getSettings().getShowSoftNumpad() && !(main instanceof MainLayoutSmall)) {
main = new MainLayoutSmall(tt9);
main.render();
return true;
}
return false;
}
public void forceCreateView() {
main = null;
createView();
}
public View getView() {
return main.getView();
}
public void render() {
main.render();
}
public void setDarkTheme(boolean darkEnabled) {
main.setDarkTheme(darkEnabled);
}
}

View file

@ -0,0 +1,75 @@
package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.preferences.SettingsStore;
public class SoftBackspaceKey extends SoftKey {
private SettingsStore settings;
long lastBackspaceCall = 0;
public SoftBackspaceKey(Context context) {
super(context);
}
public SoftBackspaceKey(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SoftBackspaceKey(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private SettingsStore getSettings() {
if (settings == null) {
settings = new SettingsStore(getContext());
}
return settings;
}
@Override
final public boolean onTouch(View view, MotionEvent event) {
if (tt9 == null) {
Logger.w(getClass().getCanonicalName(), "Traditional T9 handler is not set. Ignoring key press.");
return false;
}
int action = event.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.AXIS_PRESSURE) {
handleHold();
} else if (action == MotionEvent.ACTION_UP) {
handleUp();
} else if (action == MotionEvent.ACTION_DOWN) {
// Fallback for phones that do not report AXIS_PRESSURE, when a key is being held
handlePress(-1);
}
return true;
}
private void handleHold() {
if (System.currentTimeMillis() - lastBackspaceCall < getSettings().getSoftKeyRepeatDelay()) {
return;
}
handlePress(-1);
long now = System.currentTimeMillis();
lastBackspaceCall = lastBackspaceCall == 0 ? getSettings().getSoftKeyInitialDelay() + now : now;
}
private void handleUp() {
lastBackspaceCall = 0;
}
@Override
final protected boolean handlePress(int b) {
return tt9.onBackspace();
}
}

View file

@ -0,0 +1,126 @@
package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.core.content.ContextCompat;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.TraditionalT9;
public class SoftKey extends androidx.appcompat.widget.AppCompatButton implements View.OnTouchListener {
protected TraditionalT9 tt9;
public SoftKey(Context context) {
super(context);
}
public SoftKey(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SoftKey(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setTT9(TraditionalT9 tt9) {
this.tt9 = tt9;
}
public void setDarkTheme(boolean darkEnabled) {
int textColor = ContextCompat.getColor(
getContext(),
darkEnabled ? R.color.dark_button_text : R.color.button_text
);
setTextColor(textColor);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
getRootView().setOnTouchListener(this);
}
@Override
public boolean onTouch(View view, MotionEvent event) {
super.onTouchEvent(event);
if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
return handlePress(view.getId());
}
return false;
}
protected boolean handlePress(int keyId) {
if (tt9 == null) {
Logger.w(getClass().getCanonicalName(), "Traditional T9 handler is not set. Ignoring key press.");
return false;
}
if (keyId == R.id.soft_key_add_word) return tt9.onKeyAddWord();
if (keyId == R.id.soft_key_input_mode) return tt9.onKeyNextInputMode();
if (keyId == R.id.soft_key_language) return tt9.onKeyNextLanguage();
if (keyId == R.id.soft_key_ok) return tt9.onOK();
if (keyId == R.id.soft_key_settings) return tt9.onKeyShowSettings();
return false;
}
/**
* Generates the name of the key, for example: "OK", "Backspace", "1", etc...
*/
protected String getKeyNameLabel() {
return null;
}
/**
* Generates a String describing what the key does.
* For example: "ABC" for 2-key; "" for Backspace key, "" for Settings key, and so on.
*
* The function label is optional.
*/
protected String getKeyFunctionLabel() {
return null;
}
/**
* render
* Sets the key label using "getKeyNameLabel()" and "getKeyFunctionLabel()" or if they both
* return NULL, the XML "text" attribute will be preserved.
*
* If there is only name label, it will be centered and at normal font size.
* If there is also a function label, it will be displayed below the name label and both will
* have their font size adjusted to fit inside the key.
*/
public void render() {
String name = getKeyNameLabel();
String func = getKeyFunctionLabel();
if (name == null) {
return;
} else if (func == null) {
setText(name);
return;
}
SpannableStringBuilder sb = new SpannableStringBuilder(name);
sb.append('\n');
sb.append(func);
sb.setSpan(new RelativeSizeSpan(0.55f), 0, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
sb.setSpan(new StyleSpan(Typeface.ITALIC), 0, 2, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
sb.setSpan(new RelativeSizeSpan(0.75f), 1, sb.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
setText(sb);
}
}

View file

@ -0,0 +1,99 @@
package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import java.util.ArrayList;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.helpers.Key;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
public class SoftNumberKey extends SoftKey {
public SoftNumberKey(Context context) {
super(context);
}
public SoftNumberKey(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SoftNumberKey(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
protected boolean handlePress(int keyId) {
if (tt9 == null) {
Logger.w(getClass().getCanonicalName(), "Traditional T9 handler is not set. Ignoring key press.");
return false;
}
int keyCode = Key.numberToCode(getNumber(keyId));
if (keyCode < 0) {
return false;
}
tt9.onKeyDown(keyCode, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
tt9.onKeyUp(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode));
return true;
}
private int getNumber(int keyId) {
if (keyId == R.id.soft_key_0) return 0;
if (keyId == R.id.soft_key_1) return 1;
if (keyId == R.id.soft_key_2) return 2;
if (keyId == R.id.soft_key_3) return 3;
if (keyId == R.id.soft_key_4) return 4;
if (keyId == R.id.soft_key_5) return 5;
if (keyId == R.id.soft_key_6) return 6;
if (keyId == R.id.soft_key_7) return 7;
if (keyId == R.id.soft_key_8) return 8;
if (keyId == R.id.soft_key_9) return 9;
return -1;
}
@Override
protected String getKeyNameLabel() {
return String.valueOf(getNumber(getId()));
}
@Override
protected String getKeyFunctionLabel() {
if (tt9 == null || tt9.getSettings().getInputMode() == InputMode.MODE_123) {
return null;
}
int number = getNumber(getId());
int textCase = tt9.getSettings().getTextCase();
Language language = LanguageCollection.getLanguage(tt9.getSettings().getInputLanguage());
if (language == null) {
Logger.d("SoftNumberKey.getLabel", "Cannot generate a label when the language is NULL.");
return "";
}
if (number == 0) {
return "";
}
if (number == 1) {
return ",:-)";
}
StringBuilder sb = new StringBuilder();
ArrayList<String> chars = language.getKeyCharacters(number, false);
for (int i = 0; i < 5 && i < chars.size(); i++) {
sb.append(
textCase == InputMode.CASE_UPPER ? chars.get(i).toUpperCase(language.getLocale()) : chars.get(i)
);
}
return sb.toString();
}
}

View file

@ -0,0 +1,55 @@
package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.modes.InputMode;
public class SoftPunctuationKey extends SoftKey {
public SoftPunctuationKey(Context context) {
super(context);
}
public SoftPunctuationKey(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SoftPunctuationKey(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
protected boolean handlePress(int keyId) {
if (tt9 == null) {
Logger.w(getClass().getCanonicalName(), "Traditional T9 handler is not set. Ignoring key press.");
return false;
}
if (tt9.getSettings().getInputMode() == InputMode.MODE_123) {
if (keyId == R.id.soft_key_punctuation_1) return tt9.onOtherKey(KeyEvent.KEYCODE_STAR);
if (keyId == R.id.soft_key_punctuation_2) return tt9.onOtherKey(KeyEvent.KEYCODE_POUND);
} else {
if (keyId == R.id.soft_key_punctuation_1) return tt9.onText("!");
if (keyId == R.id.soft_key_punctuation_2) return tt9.onText("?");
}
return true;
}
@Override
protected String getKeyNameLabel() {
int keyId = getId();
if (tt9.getSettings().getInputMode() == InputMode.MODE_123) {
if (keyId == R.id.soft_key_punctuation_1) return "";
if (keyId == R.id.soft_key_punctuation_2) return "#";
} else {
if (keyId == R.id.soft_key_punctuation_1) return "!";
if (keyId == R.id.soft_key_punctuation_2) return "?";
}
return "PUNC";
}
}

View file

@ -1,4 +1,4 @@
package io.github.sspanak.tt9.ui.bottom;
package io.github.sspanak.tt9.ui.tray;
import android.content.Context;
import android.view.View;

View file

@ -1,4 +1,4 @@
package io.github.sspanak.tt9.ui.bottom;
package io.github.sspanak.tt9.ui.tray;
import android.content.Context;
import android.graphics.Color;
@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionsAdapter.ViewHolder> {
private final SuggestionsBar suggestionsBar;
private final int layout;
private final int textViewResourceId;
private final LayoutInflater mInflater;
@ -23,7 +24,8 @@ public class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionsAdapter.
private int selectedIndex = 0;
public SuggestionsAdapter(Context context, int layout, int textViewResourceId, List<String> suggestions) {
public SuggestionsAdapter(Context context, SuggestionsBar suggestionBar, int layout, int textViewResourceId, List<String> suggestions) {
this.suggestionsBar = suggestionBar;
this.layout = layout;
this.textViewResourceId = textViewResourceId;
this.mInflater = LayoutInflater.from(context);
@ -43,6 +45,7 @@ public class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionsAdapter.
holder.suggestionItem.setText(mSuggestions.get(position));
holder.suggestionItem.setTextColor(colorDefault);
holder.suggestionItem.setBackgroundColor(selectedIndex == position ? colorHighlight : Color.TRANSPARENT);
holder.suggestionItem.setOnClickListener(v -> suggestionsBar.onItemClick(holder.getAdapterPosition()));
}

View file

@ -1,4 +1,4 @@
package io.github.sspanak.tt9.ui.bottom;
package io.github.sspanak.tt9.ui.tray;
import android.annotation.SuppressLint;
import android.content.Context;
@ -16,7 +16,7 @@ import java.util.ArrayList;
import java.util.List;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.preferences.SettingsStore;
import io.github.sspanak.tt9.ime.TraditionalT9;
public class SuggestionsBar {
private final List<String> suggestions = new ArrayList<>();
@ -24,14 +24,14 @@ public class SuggestionsBar {
private boolean isDarkThemeEnabled = false;
private final RecyclerView mView;
private final SettingsStore settings;
private final TraditionalT9 tt9;
private SuggestionsAdapter mSuggestionsAdapter;
public SuggestionsBar(SettingsStore settings, View mainView) {
public SuggestionsBar(TraditionalT9 tt9, View mainView) {
super();
this.settings = settings;
this.tt9 = tt9;
mView = mainView.findViewById(R.id.suggestions_bar);
mView.setLayoutManager(new LinearLayoutManager(mainView.getContext(), RecyclerView.HORIZONTAL,false));
@ -45,8 +45,8 @@ public class SuggestionsBar {
private void configureAnimation() {
DefaultItemAnimator animator = new DefaultItemAnimator();
int translateDuration = settings.getSuggestionTranslateAnimationDuration();
int selectDuration = settings.getSuggestionSelectAnimationDuration();
int translateDuration = tt9.getSettings().getSuggestionTranslateAnimationDuration();
int selectDuration = tt9.getSettings().getSuggestionSelectAnimationDuration();
animator.setMoveDuration(selectDuration);
animator.setChangeDuration(translateDuration);
@ -60,13 +60,14 @@ public class SuggestionsBar {
private void initDataAdapter(Context context) {
mSuggestionsAdapter = new SuggestionsAdapter(
context,
R.layout.suggestion_list_view,
this,
tt9.getSettings().getShowSoftNumpad() ? R.layout.suggestion_list_numpad : R.layout.suggestion_list,
R.id.suggestion_list_item,
suggestions
);
mView.setAdapter(mSuggestionsAdapter);
setDarkTheme(settings.getDarkTheme());
setDarkTheme(tt9.getSettings().getDarkTheme());
}
@ -213,4 +214,14 @@ public class SuggestionsBar {
setBackground(newSuggestions);
}
/**
* onItemClick
* Passes through suggestion selected using the touchscreen.
*/
public void onItemClick(int position) {
selectedIndex = position;
tt9.onOK();
}
}