1
0
Fork 0

New Settings screen

* Settings screen is now based on the Android SharedPreferences

* Added function key configuration on the Settings screen

* Added a setting for toggling the on-screen buttons

* Added a dark/light theme setting

* Improved translations

* Fixed a problem with launching the Settings screen directly from the Android settings

* Fixed ignoring keys not actually ignoring them properly
This commit is contained in:
sspanak 2022-11-08 15:13:28 +02:00 committed by Dimo Karaivanov
parent 4e59d3393c
commit b550d5d5dd
84 changed files with 1463 additions and 1205 deletions

View file

@ -17,11 +17,13 @@ import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException;
import io.github.sspanak.tt9.languages.InvalidLanguageException;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.T9Preferences;
import io.github.sspanak.tt9.preferences.SettingsStore;
public class DictionaryLoader {
private static DictionaryLoader self;
private final AssetManager assets;
private final T9Preferences prefs;
private final SettingsStore settings;
private final Pattern containsPunctuation = Pattern.compile("\\p{Punct}(?<!-)");
private Thread loadThread;
@ -30,9 +32,20 @@ public class DictionaryLoader {
private long lastProgressUpdate = 0;
public static DictionaryLoader getInstance(Context context) {
if (self == null) {
self = new DictionaryLoader(context);
}
return self;
}
public DictionaryLoader(Context context) {
assets = context.getAssets();
prefs = T9Preferences.getInstance();
settings = new SettingsStore(context);
}
@ -180,14 +193,14 @@ public class DictionaryLoader {
validateWord(language, word, line);
dbWords.add(stringToWord(language, word));
if (line % prefs.getDictionaryImportWordChunkSize() == 0) {
if (line % settings.getDictionaryImportWordChunkSize() == 0) {
DictionaryDb.insertWordsSync(dbWords);
dbWords.clear();
}
if (totalWords > 0) {
int progress = (int) Math.floor(100.0 * line / totalWords);
sendProgressMessage(handler, language, progress, prefs.getDictionaryImportProgressUpdateInterval());
sendProgressMessage(handler, language, progress, settings.getDictionaryImportProgressUpdateInterval());
}
}

View file

@ -71,7 +71,7 @@ class InputFieldHelper {
* determineInputModes
* Determine the typing mode based on the input field being edited. Returns an ArrayList of the allowed modes.
*
* @return ArrayList<T9Preferences.MODE_ABC | T9Preferences.MODE_123 | T9Preferences.MODE_PREDICTIVE>
* @return ArrayList<SettingsStore.MODE_ABC | SettingsStore.MODE_123 | SettingsStore.MODE_PREDICTIVE>
*/
public static ArrayList<Integer> determineInputModes(EditorInfo inputField) {
final int INPUT_TYPE_SHARP_007H_PHONE_BOOK = 65633;
@ -134,7 +134,7 @@ class InputFieldHelper {
*/
public static void determineTextCase(EditorInfo inputField) {
// Logger.d("updateShift", "CM start: " + mCapsMode);
// if (inputField != null && mCapsMode != T9Preferences.CASE_UPPER) {
// if (inputField != null && mCapsMode != SettingsStore.CASE_UPPER) {
// int caps = 0;
// if (inputField.inputType != InputType.TYPE_NULL) {
// caps = currentInputConnection.getCursorCapsMode(inputField.inputType);
@ -142,13 +142,13 @@ class InputFieldHelper {
// // mInputView.setShifted(mCapsLock || caps != 0);
// // Logger.d("updateShift", "caps: " + caps);
// if ((caps & TextUtils.CAP_MODE_CHARACTERS) == TextUtils.CAP_MODE_CHARACTERS) {
// mCapsMode = T9Preferences.CASE_UPPER;
// mCapsMode = SettingsStore.CASE_UPPER;
// } else if ((caps & TextUtils.CAP_MODE_SENTENCES) == TextUtils.CAP_MODE_SENTENCES) {
// mCapsMode = T9Preferences.CASE_CAPITALIZE;
// mCapsMode = SettingsStore.CASE_CAPITALIZE;
// } else if ((caps & TextUtils.CAP_MODE_WORDS) == TextUtils.CAP_MODE_WORDS) {
// mCapsMode = T9Preferences.CASE_CAPITALIZE;
// mCapsMode = SettingsStore.CASE_CAPITALIZE;
// } else {
// mCapsMode = T9Preferences.CASE_LOWER;
// mCapsMode = SettingsStore.CASE_LOWER;
// }
// updateStatusIcon();
// }

View file

@ -7,10 +7,10 @@ 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.languages.definitions.English;
import io.github.sspanak.tt9.preferences.T9Preferences;
import io.github.sspanak.tt9.preferences.SettingsStore;
public class InputModeValidator {
public static ArrayList<Integer> validateEnabledLanguages(T9Preferences prefs, ArrayList<Integer> enabledLanguageIds) {
public static ArrayList<Integer> validateEnabledLanguages(SettingsStore settings, ArrayList<Integer> enabledLanguageIds) {
ArrayList<Language> validLanguages = LanguageCollection.getAll(enabledLanguageIds);
ArrayList<Integer> validLanguageIds = new ArrayList<>();
for (Language lang : validLanguages) {
@ -21,12 +21,12 @@ public class InputModeValidator {
Logger.e("tt9/validateEnabledLanguages", "The language list seems to be corrupted. Resetting to first language only.");
}
prefs.saveEnabledLanguages(validLanguageIds);
settings.saveEnabledLanguageIds(validLanguageIds);
return validLanguageIds;
}
public static Language validateLanguage(T9Preferences prefs, Language language, ArrayList<Integer> validLanguageIds) {
public static Language validateLanguage(SettingsStore settings, Language language, ArrayList<Integer> validLanguageIds) {
if (language != null && validLanguageIds.contains(language.getId())) {
return language;
}
@ -36,20 +36,20 @@ public class InputModeValidator {
Language validLanguage = LanguageCollection.getLanguage(validLanguageIds.get(0));
validLanguage = validLanguage == null ? LanguageCollection.getLanguage(1) : validLanguage;
validLanguage = validLanguage == null ? new English() : validLanguage;
prefs.saveInputLanguage(validLanguage.getId());
settings.saveInputLanguage(validLanguage.getId());
Logger.w("tt9/validateSavedLanguage", error + " Enforcing language: " + validLanguage.getId());
return validLanguage;
}
public static InputMode validateMode(T9Preferences prefs, InputMode inputMode, ArrayList<Integer> allowedModes) {
public static InputMode validateMode(SettingsStore settings, InputMode inputMode, ArrayList<Integer> allowedModes) {
if (allowedModes.size() > 0 && allowedModes.contains(inputMode.getId())) {
return inputMode;
}
InputMode newMode = InputMode.getInstance(allowedModes.size() > 0 ? allowedModes.get(0) : InputMode.MODE_123);
prefs.saveInputMode(newMode);
settings.saveInputMode(newMode);
if (newMode.getId() != inputMode.getId()) {
Logger.w("tt9/validateMode", "Invalid input mode: " + inputMode.getId() + " Enforcing: " + newMode.getId());
@ -58,12 +58,12 @@ public class InputModeValidator {
return newMode;
}
public static void validateTextCase(T9Preferences prefs, InputMode inputMode, int newTextCase) {
public static void validateTextCase(SettingsStore settings, InputMode inputMode, int newTextCase) {
if (!inputMode.setTextCase(newTextCase)) {
inputMode.defaultTextCase();
Logger.w("tt9/validateTextCase", "Invalid text case: " + newTextCase + " Enforcing: " + inputMode.getTextCase());
}
prefs.saveTextCase(inputMode.getTextCase());
settings.saveTextCase(inputMode.getTextCase());
}
}

View file

@ -7,13 +7,13 @@ import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import io.github.sspanak.tt9.preferences.T9Preferences;
import io.github.sspanak.tt9.preferences.SettingsStore;
abstract class KeyPadHandler extends InputMethodService {
protected InputConnection currentInputConnection = null;
protected T9Preferences prefs;
protected SettingsStore settings;
// editing mode
protected static final int NON_EDIT = 0;
@ -44,7 +44,7 @@ abstract class KeyPadHandler extends InputMethodService {
@Override
public void onCreate() {
super.onCreate();
prefs = new T9Preferences(getApplicationContext());
settings = new SettingsStore(getApplicationContext());
onInit();
}
@ -130,7 +130,7 @@ abstract class KeyPadHandler extends InputMethodService {
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (isOff()) {
return super.onKeyDown(keyCode, event);
return false;
}
// Logger.d("onKeyDown", "Key: " + event + " repeat?: " + event.getRepeatCount() + " long-time: " + event.isLongPress());
@ -138,7 +138,7 @@ abstract class KeyPadHandler extends InputMethodService {
// "backspace" key must repeat its function, when held down, so we handle it in a special way
// Also dialer fields seem to handle backspace on their own and we must ignore it,
// otherwise, keyDown race condition occur for all keys.
if (mEditing != EDITING_DIALER && keyCode == prefs.getKeyBackspace()) {
if (mEditing != EDITING_DIALER && keyCode == settings.getKeyBackspace()) {
boolean isThereTextBefore = InputFieldHelper.isThereText(currentInputConnection);
boolean backspaceHandleStatus = handleBackspaceHold();
@ -170,8 +170,7 @@ abstract class KeyPadHandler extends InputMethodService {
}
if (
keyCode == prefs.getKeyOtherActions()
|| keyCode == prefs.getKeyInputMode()
isSpecialFunctionKey(keyCode)
|| keyCode == KeyEvent.KEYCODE_STAR
|| keyCode == KeyEvent.KEYCODE_POUND
|| (isNumber(keyCode) && shouldTrackNumPress())
@ -189,7 +188,7 @@ abstract class KeyPadHandler extends InputMethodService {
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (isOff()) {
return super.onKeyDown(keyCode, event);
return false;
}
// Logger.d("onLongPress", "LONG PRESS: " + keyCode);
@ -200,12 +199,8 @@ abstract class KeyPadHandler extends InputMethodService {
ignoreNextKeyUp = keyCode;
if (keyCode == prefs.getKeyOtherActions()) {
return onKeyOtherAction(true);
}
if (keyCode == prefs.getKeyInputMode()) {
return onKeyInputMode(true);
if (handleSpecialFunctionKey(keyCode, true)) {
return true;
}
switch (keyCode) {
@ -256,7 +251,7 @@ abstract class KeyPadHandler extends InputMethodService {
if (
mEditing != EDITING_DIALER // dialer fields seem to handle backspace on their own
&& keyCode == prefs.getKeyBackspace()
&& keyCode == settings.getKeyBackspace()
&& InputFieldHelper.isThereText(currentInputConnection)
) {
return true;
@ -277,12 +272,8 @@ abstract class KeyPadHandler extends InputMethodService {
return false;
}
if (keyCode == prefs.getKeyOtherActions()) {
return onKeyOtherAction(false);
}
if (keyCode == prefs.getKeyInputMode()) {
return onKeyInputMode(false);
if (handleSpecialFunctionKey(keyCode, false)) {
return true;
}
switch(keyCode) {
@ -321,6 +312,27 @@ abstract class KeyPadHandler extends InputMethodService {
}
private boolean handleSpecialFunctionKey(int keyCode, boolean hold) {
if (keyCode == settings.getKeyAddWord() * (hold ? -1 : 1)) {
return onKeyAddWord();
}
if (keyCode == settings.getKeyNextLanguage() * (hold ? -1 : 1)) {
return onKeyNextLanguage();
}
if (keyCode == settings.getKeyNextInputMode() * (hold ? -1 : 1)) {
return onKeyNextInputMode();
}
if (keyCode == settings.getKeyShowSettings() * (hold ? -1 : 1)) {
return onKeyShowSettings();
}
return false;
}
private boolean isOff() {
return currentInputConnection == null || mEditing == NON_EDIT;
}
@ -331,6 +343,15 @@ abstract class KeyPadHandler extends InputMethodService {
}
private boolean isSpecialFunctionKey(int keyCode) {
return keyCode == settings.getKeyAddWord()
|| keyCode == settings.getKeyBackspace()
|| keyCode == settings.getKeyNextLanguage()
|| keyCode == settings.getKeyNextInputMode()
|| keyCode == settings.getKeyShowSettings();
}
protected void resetKeyRepeat() {
isNumKeyRepeated = false;
lastNumKeyCode = 0;
@ -381,8 +402,10 @@ abstract class KeyPadHandler extends InputMethodService {
abstract protected boolean onPound();
// customized key handlers
abstract protected boolean onKeyInputMode(boolean hold);
abstract protected boolean onKeyOtherAction(boolean hold);
abstract protected boolean onKeyAddWord();
abstract protected boolean onKeyNextLanguage();
abstract protected boolean onKeyNextInputMode();
abstract protected boolean onKeyShowSettings();
// helpers
abstract protected void onInit();

View file

@ -1,8 +1,13 @@
package io.github.sspanak.tt9.ime;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
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;
@ -12,16 +17,16 @@ class SoftKeyHandler implements View.OnTouchListener {
private final TraditionalT9 tt9;
private View view = null;
public SoftKeyHandler(LayoutInflater layoutInflater, TraditionalT9 tt9) {
public SoftKeyHandler(TraditionalT9 tt9) {
this.tt9 = tt9;
createView(layoutInflater);
getView();
}
View createView(LayoutInflater layoutInflater) {
View getView() {
if (view == null) {
view = layoutInflater.inflate(R.layout.mainview, null);
view = LayoutInflater.from(tt9.getApplicationContext()).inflate(R.layout.mainview, null);
for (int buttonId : buttons) {
view.findViewById(buttonId).setOnTouchListener(this);
@ -31,10 +36,6 @@ class SoftKeyHandler implements View.OnTouchListener {
return view;
}
View getView() {
return view;
}
void show() {
if (view != null) {
@ -50,13 +51,63 @@ class SoftKeyHandler implements View.OnTouchListener {
}
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);
}
@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.showPreferencesScreen(tt9);
UI.showSettingsScreen(tt9);
return view.performClick();
}

View file

@ -8,7 +8,7 @@ import android.text.style.UnderlineSpan;
import io.github.sspanak.tt9.Logger;
public class Util {
public class TextHelper {
public static CharSequence highlightComposingText(CharSequence word, int start, int end) {
if (end < start || start < 0) {
Logger.w("tt9.util.highlightComposingText", "Cannot highlight invalid composing text range: [" + start + ", " + end + "]");

View file

@ -40,17 +40,24 @@ public class TraditionalT9 extends KeyPadHandler {
}
private void loadPreferences() {
mLanguage = LanguageCollection.getLanguage(prefs.getInputLanguage());
mEnabledLanguages = prefs.getEnabledLanguages();
mInputMode = InputMode.getInstance(prefs.getInputMode());
mInputMode.setTextCase(prefs.getTextCase());
private void loadSettings() {
mLanguage = LanguageCollection.getLanguage(settings.getInputLanguage());
mEnabledLanguages = settings.getEnabledLanguageIds();
mInputMode = InputMode.getInstance(settings.getInputMode());
mInputMode.setTextCase(settings.getTextCase());
}
private void validateLanguages() {
mEnabledLanguages = InputModeValidator.validateEnabledLanguages(prefs, mEnabledLanguages);
mLanguage = InputModeValidator.validateLanguage(prefs, mLanguage, mEnabledLanguages);
mEnabledLanguages = InputModeValidator.validateEnabledLanguages(settings, mEnabledLanguages);
mLanguage = InputModeValidator.validateLanguage(settings, mLanguage, mEnabledLanguages);
}
private void validateFunctionKeys() {
if (!settings.areFunctionKeysSet()) {
settings.setDefaultKeys();
}
}
@ -58,35 +65,42 @@ public class TraditionalT9 extends KeyPadHandler {
self = this;
if (softKeyHandler == null) {
softKeyHandler = new SoftKeyHandler(getLayoutInflater(), this);
softKeyHandler = new SoftKeyHandler(this);
}
if (mSuggestionView == null) {
mSuggestionView = new SuggestionsView(softKeyHandler.getView());
}
loadPreferences();
prefs.clearLastWord();
loadSettings();
validateFunctionKeys();
settings.clearLastWord();
}
protected void onRestart(EditorInfo inputField) {
// in case we are back from Preferences screen, update the language list
mEnabledLanguages = prefs.getEnabledLanguages();
// in case we are back from Settings screen, update the language list
mEnabledLanguages = settings.getEnabledLanguageIds();
validateLanguages();
// some input fields support only numbers or do not accept predictions
determineAllowedInputModes(inputField);
mInputMode = InputModeValidator.validateMode(prefs, mInputMode, allowedInputModes);
mInputMode = InputModeValidator.validateMode(settings, mInputMode, allowedInputModes);
// Some modes may want to change the default text case based on grammar rules.
determineNextTextCase();
InputModeValidator.validateTextCase(prefs, mInputMode, prefs.getTextCase());
InputModeValidator.validateTextCase(settings, mInputMode, settings.getTextCase());
// build the UI
clearSuggestions();
UI.updateStatusIcon(this, mLanguage, mInputMode);
clearSuggestions();
mSuggestionView.setDarkTheme(settings.getDarkTheme());
softKeyHandler.setDarkTheme(settings.getDarkTheme());
softKeyHandler.setSoftKeysVisibility(settings.getShowSoftKeys());
softKeyHandler.show();
if (!isInputViewShown()) {
showWindow(true);
}
@ -239,32 +253,42 @@ public class TraditionalT9 extends KeyPadHandler {
}
protected boolean onKeyInputMode(boolean hold) {
if (mEditing == EDITING_DIALER) {
return false;
}
if (hold) {
nextLang();
} else {
nextInputMode();
}
return true;
}
protected boolean onKeyOtherAction(boolean hold) {
protected boolean onKeyAddWord() {
if (mEditing == EDITING_NOSHOW || mEditing == EDITING_DIALER) {
return false;
}
if (hold) {
UI.showPreferencesScreen(this);
} else {
showAddWord();
showAddWord();
return true;
}
protected boolean onKeyNextLanguage() {
if (mEditing == EDITING_DIALER) {
return false;
}
nextLang();
return true;
}
protected boolean onKeyNextInputMode() {
if (mEditing == EDITING_DIALER) {
return false;
}
nextInputMode();
return true;
}
protected boolean onKeyShowSettings() {
if (mEditing == EDITING_NOSHOW || mEditing == EDITING_DIALER) {
return false;
}
UI.showSettingsScreen(this);
return true;
}
@ -407,7 +431,7 @@ public class TraditionalT9 extends KeyPadHandler {
private void setComposingTextWithWordStemIndication(CharSequence word) {
if (mInputMode.getWordStem().length() > 0) {
setComposingText(Util.highlightComposingText(word, 0, mInputMode.getWordStem().length()));
setComposingText(TextHelper.highlightComposingText(word, 0, mInputMode.getWordStem().length()));
} else {
setComposingText(word);
}
@ -441,8 +465,8 @@ public class TraditionalT9 extends KeyPadHandler {
}
// save the settings for the next time
prefs.saveInputMode(mInputMode);
prefs.saveTextCase(mInputMode.getTextCase());
settings.saveInputMode(mInputMode);
settings.saveTextCase(mInputMode.getTextCase());
UI.updateStatusIcon(this, mLanguage, mInputMode);
}
@ -463,7 +487,7 @@ public class TraditionalT9 extends KeyPadHandler {
validateLanguages();
// save it for the next time
prefs.saveInputLanguage(mLanguage.getId());
settings.saveInputLanguage(mLanguage.getId());
UI.updateStatusIcon(this, mLanguage, mInputMode);
}
@ -483,7 +507,7 @@ public class TraditionalT9 extends KeyPadHandler {
private void determineAllowedInputModes(EditorInfo inputField) {
allowedInputModes = InputFieldHelper.determineInputModes(inputField);
int lastInputModeId = prefs.getInputMode();
int lastInputModeId = settings.getInputMode();
if (allowedInputModes.contains(lastInputModeId)) {
mInputMode = InputMode.getInstance(lastInputModeId);
} else if (allowedInputModes.contains(InputMode.MODE_ABC)) {
@ -527,8 +551,8 @@ public class TraditionalT9 extends KeyPadHandler {
* If a new word was added to the dictionary, this function will append add it to the current input field.
*/
private void restoreAddedWordIfAny() {
String word = prefs.getLastWord();
prefs.clearLastWord();
String word = settings.getLastWord();
settings.clearLastWord();
if (word.length() == 0 || word.equals(InputFieldHelper.getSurroundingWord(currentInputConnection))) {
return;
@ -549,6 +573,6 @@ public class TraditionalT9 extends KeyPadHandler {
* Generates the actual UI of TT9.
*/
protected View createSoftKeyView() {
return softKeyHandler.createView(getLayoutInflater());
return softKeyHandler.getView();
}
}

View file

@ -11,7 +11,7 @@ import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.db.DictionaryDb;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.Punctuation;
import io.github.sspanak.tt9.preferences.T9Preferences;
import io.github.sspanak.tt9.preferences.SettingsStore;
public class ModePredictive extends InputMode {
public int getId() { return MODE_PREDICTIVE; }
@ -194,8 +194,8 @@ public class ModePredictive extends InputMode {
language,
digitSequence,
stem,
T9Preferences.getInstance().getSuggestionsMin(),
T9Preferences.getInstance().getSuggestionsMax()
SettingsStore.getInstance().getSuggestionsMin(),
SettingsStore.getInstance().getSuggestionsMax()
);
return true;

View file

@ -1,5 +1,7 @@
package io.github.sspanak.tt9.languages;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Locale;
@ -102,4 +104,10 @@ public class Language {
return sequence.toString();
}
@NonNull
@Override
public String toString() {
return name != null ? name : "";
}
}

View file

@ -1,7 +1,10 @@
package io.github.sspanak.tt9.languages;
import android.os.Build;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@ -57,7 +60,7 @@ public class LanguageCollection {
return null;
}
public static ArrayList<Language> getAll(ArrayList<Integer> languageIds) {
public static ArrayList<Language> getAll(ArrayList<Integer> languageIds, boolean sort) {
ArrayList<Language> langList = new ArrayList<>();
for (int languageId : languageIds) {
@ -67,6 +70,41 @@ public class LanguageCollection {
}
}
if (sort && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
langList.sort(Comparator.comparing(l -> l.getLocale().toString()));
}
return langList;
}
public static ArrayList<Language> getAll(ArrayList<Integer> languageIds) {
return getAll(languageIds, false);
}
public static ArrayList<Language> getAll(boolean sort) {
ArrayList<Language> langList = new ArrayList<>(getInstance().languages.values());
if (sort && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
langList.sort(Comparator.comparing(l -> l.getLocale().toString()));
}
return langList;
}
public static ArrayList<Language> getAll() {
return getAll(false);
}
public static String toString(ArrayList<Language> list) {
StringBuilder stringList = new StringBuilder();
int listSize = list.size();
for (int i = 0; i < listSize; i++) {
stringList.append(list.get(i));
stringList.append((i < listSize - 1) ? ", " : " ");
}
return stringList.toString();
}
}

View file

@ -11,7 +11,7 @@ import io.github.sspanak.tt9.languages.Punctuation;
public class Bulgarian extends Language {
public Bulgarian() {
id = 7;
name = "български";
name = "Български";
locale = new Locale("bg","BG");
dictionaryFile = "bg-utf8.txt";
icon = R.drawable.ime_lang_bg;

View file

@ -11,7 +11,7 @@ import io.github.sspanak.tt9.languages.Punctuation;
public class Russian extends Language {
public Russian() {
id = 2;
name = "русский";
name = "Русский";
locale = new Locale("ru","RU");
dictionaryFile = "ru-utf8.txt";
icon = R.drawable.ime_lang_ru;

View file

@ -11,7 +11,7 @@ import io.github.sspanak.tt9.languages.Punctuation;
public class Ukrainian extends Language {
public Ukrainian() {
id = 6;
name = "українська";
name = "Українська";
locale = new Locale("uk","UA");
dictionaryFile = "uk-utf8.txt";
icon = R.drawable.ime_lang_uk;

View file

@ -0,0 +1,17 @@
package io.github.sspanak.tt9.preferences;
import androidx.preference.Preference;
public abstract class ItemClickable {
protected final Preference item;
ItemClickable(Preference item) {
this.item = item;
}
public void enableClickHandler() {
item.setOnPreferenceClickListener(this::onClick);
}
abstract protected boolean onClick(Preference p);
}

View file

@ -0,0 +1,83 @@
package io.github.sspanak.tt9.preferences;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.preference.Preference;
import java.util.ArrayList;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DictionaryImportAlreadyRunningException;
import io.github.sspanak.tt9.db.DictionaryLoader;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.ui.DictionaryLoadingBar;
import io.github.sspanak.tt9.ui.UI;
public class ItemLoadDictionary extends ItemClickable {
public static final String NAME = "dictionary_load";
private final Context context;
private final DictionaryLoader loader;
private final DictionaryLoadingBar progressBar;
ItemLoadDictionary(Preference item, Context context, DictionaryLoader loader, DictionaryLoadingBar progressBar) {
super(item);
this.context = context;
this.loader = loader;
this.progressBar = progressBar;
if (!progressBar.isCompleted() && !progressBar.isFailed()) {
changeToCancelButton();
}
}
private final Handler onDictionaryLoading = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
progressBar.show(msg.getData());
if (progressBar.isCompleted()) {
changeToLoadButton();
UI.toast(context, R.string.dictionary_loaded);
} else if (progressBar.isFailed()) {
changeToLoadButton();
UI.toast(context, R.string.dictionary_load_failed);
}
}
};
@Override
protected boolean onClick(Preference p) {
ArrayList<Language> languages = LanguageCollection.getAll(SettingsStore.getInstance().getEnabledLanguageIds());
progressBar.setFileCount(languages.size());
try {
loader.load(onDictionaryLoading, languages);
changeToCancelButton();
} catch (DictionaryImportAlreadyRunningException e) {
loader.stop();
changeToLoadButton();
}
return false;
}
public void changeToCancelButton() {
item.setTitle(context.getString(R.string.dictionary_cancel_load));
}
public void changeToLoadButton() {
item.setTitle(context.getString(R.string.dictionary_load_title));
}
}

View file

@ -0,0 +1,33 @@
package io.github.sspanak.tt9.preferences;
import android.content.Context;
import androidx.preference.Preference;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ui.UI;
public class ItemResetKeys extends ItemClickable {
public static final String NAME = "reset_keys";
private final Context context;
private final SectionKeymap dropdowns;
private final SettingsStore settings;
ItemResetKeys(Preference item, Context context, SectionKeymap dropdowns, SettingsStore settings) {
super(item);
this.context = context;
this.dropdowns = dropdowns;
this.settings = settings;
}
@Override
protected boolean onClick(Preference p) {
settings.setDefaultKeys();
dropdowns.reloadSettings();
UI.toast(context, R.string.function_reset_keys_done);
return false;
}
}

View file

@ -0,0 +1,76 @@
package io.github.sspanak.tt9.preferences;
import androidx.preference.MultiSelectListPreference;
import java.util.ArrayList;
import java.util.HashSet;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
public class ItemSelectLanguage {
public static final String NAME = "pref_languages";
private final SettingsStore settings;
private final MultiSelectListPreference item;
ItemSelectLanguage(MultiSelectListPreference multiSelect, SettingsStore settings) {
this.item = multiSelect;
this.settings = settings;
}
public ItemSelectLanguage populate() {
if (item == null) {
return this;
}
ArrayList<Language> languages = LanguageCollection.getAll(true);
ArrayList<CharSequence> values = new ArrayList<>();
for (Language l : languages) {
values.add(String.valueOf(l.getId()));
}
ArrayList<String> keys = new ArrayList<>();
for (Language l : languages) {
keys.add(l.getName());
}
item.setEntries(keys.toArray(new CharSequence[0]));
item.setEntryValues(values.toArray(new CharSequence[0]));
item.setValues(settings.getEnabledLanguagesIdsAsStrings());
previewSelection();
return this;
}
public ItemSelectLanguage enableValidation() {
if (item == null) {
return this;
}
item.setOnPreferenceChangeListener((preference, newValue) -> {
HashSet<String> newLanguages = (HashSet<String>) newValue;
if (newLanguages.size() == 0) {
newLanguages.add("1");
}
settings.saveEnabledLanguageIds(newLanguages);
item.setValues(settings.getEnabledLanguagesIdsAsStrings());
previewSelection();
// we validate and save manually above, so "false" disables automatic save
return false;
});
return this;
}
private void previewSelection() {
item.setSummary(
LanguageCollection.toString(LanguageCollection.getAll(settings.getEnabledLanguageIds(), true))
);
}
}

View file

@ -0,0 +1,27 @@
package io.github.sspanak.tt9.preferences;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
public class ItemToggleDarkTheme {
public static final String NAME = "pref_dark_theme";
private final SwitchPreferenceCompat themeToggle;
public ItemToggleDarkTheme(SwitchPreferenceCompat item) {
themeToggle = item;
}
public void enableToggleHandler() {
themeToggle.setOnPreferenceChangeListener(this::onChange);
}
private boolean onChange(Preference p, Object newValue) {
AppCompatDelegate.setDefaultNightMode(
((boolean) newValue) ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO
);
return true;
}
}

View file

@ -0,0 +1,49 @@
package io.github.sspanak.tt9.preferences;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.preference.Preference;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DictionaryDb;
import io.github.sspanak.tt9.db.DictionaryLoader;
import io.github.sspanak.tt9.ui.UI;
public class ItemTruncateDictionary extends ItemClickable {
public static final String NAME = "dictionary_truncate";
private final Context context;
private final DictionaryLoader loader;
private final ItemLoadDictionary loadItem;
ItemTruncateDictionary(Preference item, ItemLoadDictionary loadItem, Context context, DictionaryLoader loader) {
super(item);
this.context = context;
this.loadItem = loadItem;
this.loader = loader;
}
private final Handler onDictionaryTruncated = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
UI.toast(context, R.string.dictionary_truncated);
}
};
@Override
protected boolean onClick(Preference p) {
if (loader != null && loader.isRunning()) {
loader.stop();
loadItem.changeToLoadButton();
}
DictionaryDb.truncateWords(onDictionaryTruncated);
return false;
}
}

View file

@ -0,0 +1,50 @@
package io.github.sspanak.tt9.preferences;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DictionaryLoader;
import io.github.sspanak.tt9.ui.DictionaryLoadingBar;
public class PreferencesActivity extends AppCompatActivity {
SettingsStore settings;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
settings = new SettingsStore(this);
applyTheme();
buildScreen();
}
private void applyTheme() {
AppCompatDelegate.setDefaultNightMode(
settings.getDarkTheme() ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO
);
}
private void buildScreen() {
setContentView(R.layout.preferences_container);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.preferences_container, new PreferencesFragment(this))
.commit();
}
DictionaryLoadingBar getDictionaryProgressBar() {
return DictionaryLoadingBar.getInstance(this);
}
DictionaryLoader getDictionaryLoader() {
return DictionaryLoader.getInstance(this);
}
}

View file

@ -0,0 +1,107 @@
package io.github.sspanak.tt9.preferences;
import android.os.Bundle;
import androidx.preference.DropDownPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import java.util.Arrays;
import io.github.sspanak.tt9.BuildConfig;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.R;
public class PreferencesFragment extends PreferenceFragmentCompat {
private PreferencesActivity activity;
public PreferencesFragment() {
super();
init();
}
public PreferencesFragment(PreferencesActivity activity) {
super();
this.activity = activity;
init();
}
private void init() {
if (activity == null) {
activity = (PreferencesActivity) getActivity();
}
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.prefs, rootKey);
if (activity == null) {
Logger.w(
"tt9/PreferencesFragment",
"Starting up without an Activity. Preference Items will not be fully initialized."
);
return;
}
createDictionarySection();
createAppearanceSection();
createKeymapSection();
createAboutSection();
}
private void createDictionarySection() {
ItemSelectLanguage multiSelect = new ItemSelectLanguage(
findPreference(ItemSelectLanguage.NAME),
activity.settings
);
multiSelect.populate().enableValidation();
ItemLoadDictionary loadItem = new ItemLoadDictionary(
findPreference(ItemLoadDictionary.NAME),
activity,
activity.getDictionaryLoader(),
activity.getDictionaryProgressBar()
);
loadItem.enableClickHandler();
ItemTruncateDictionary truncateItem = new ItemTruncateDictionary(
findPreference(ItemTruncateDictionary.NAME),
loadItem,
activity,
activity.getDictionaryLoader()
);
truncateItem.enableClickHandler();
}
private void createAppearanceSection() {
(new ItemToggleDarkTheme(findPreference(ItemToggleDarkTheme.NAME))).enableToggleHandler();
}
private void createKeymapSection() {
DropDownPreference[] dropDowns = {
findPreference(SectionKeymap.ITEM_ADD_WORD),
findPreference(SectionKeymap.ITEM_BACKSPACE),
findPreference(SectionKeymap.ITEM_NEXT_INPUT_MODE),
findPreference(SectionKeymap.ITEM_NEXT_LANGUAGE),
findPreference(SectionKeymap.ITEM_SHOW_SETTINGS),
};
SectionKeymap section = new SectionKeymap(Arrays.asList(dropDowns), activity, activity.settings);
section.populate().activate();
(new ItemResetKeys(findPreference(ItemResetKeys.NAME), activity, section, activity.settings))
.enableClickHandler();
}
private void createAboutSection() {
Preference vi = findPreference("version_info");
if (vi != null) {
vi.setSummary(BuildConfig.VERSION_NAME);
}
}
}

View file

@ -0,0 +1,219 @@
package io.github.sspanak.tt9.preferences;
import android.content.Context;
import android.content.res.Resources;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.ViewConfiguration;
import androidx.preference.DropDownPreference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Objects;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.R;
public class SectionKeymap {
public static final String ITEM_ADD_WORD = "key_add_word";
public static final String ITEM_BACKSPACE = "key_backspace";
public static final String ITEM_NEXT_INPUT_MODE = "key_next_input_mode";
public static final String ITEM_NEXT_LANGUAGE = "key_next_language";
public static final String ITEM_SHOW_SETTINGS = "key_show_settings";
private final LinkedHashMap<String, String> KEYS = new LinkedHashMap<>();
private final Collection<DropDownPreference> items;
private final SettingsStore settings;
public SectionKeymap(Collection<DropDownPreference> dropDowns, Context context, SettingsStore settings) {
items = dropDowns;
this.settings = settings;
Resources resources = context.getResources();
KEYS.put(String.valueOf(0), resources.getString(R.string.key_none));
if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK)) {
KEYS.put(String.valueOf(KeyEvent.KEYCODE_BACK), resources.getString(R.string.key_back));
KEYS.put(String.valueOf(KeyEvent.KEYCODE_CALL), resources.getString(R.string.key_call));
}
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_CALL),
resources.getString(R.string.key_call) + " " + resources.getString(R.string.key_hold_key)
);
if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_DEL)) {
KEYS.put(String.valueOf(KeyEvent.KEYCODE_DEL), resources.getString(R.string.key_delete));
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_DEL),
resources.getString(R.string.key_delete) + " " + resources.getString(R.string.key_hold_key)
);
}
if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_F1)) {
KEYS.put(String.valueOf(KeyEvent.KEYCODE_F1), resources.getString(R.string.key_f1));
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_F1),
resources.getString(R.string.key_f1) + " " + resources.getString(R.string.key_hold_key)
);
}
if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_F2)) {
KEYS.put(String.valueOf(KeyEvent.KEYCODE_F2), resources.getString(R.string.key_f2));
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_F2),
resources.getString(R.string.key_f2) + " " + resources.getString(R.string.key_hold_key)
);
}
if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_F3)) {
KEYS.put(String.valueOf(KeyEvent.KEYCODE_F3), resources.getString(R.string.key_f3));
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_F3),
resources.getString(R.string.key_f3) + " " + resources.getString(R.string.key_hold_key)
);
}
if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_F4)) {
KEYS.put(String.valueOf(KeyEvent.KEYCODE_F4), resources.getString(R.string.key_f4));
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_F4),
resources.getString(R.string.key_f4) + " " + resources.getString(R.string.key_hold_key)
);
}
if (ViewConfiguration.get(context).hasPermanentMenuKey()) {
KEYS.put(String.valueOf(KeyEvent.KEYCODE_MENU), resources.getString(R.string.key_menu));
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_MENU),
resources.getString(R.string.key_menu) + " " + resources.getString(R.string.key_hold_key)
);
}
KEYS.put(String.valueOf(KeyEvent.KEYCODE_POUND), resources.getString(R.string.key_pound));
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_POUND),
resources.getString(R.string.key_pound) + " " + resources.getString(R.string.key_hold_key)
);
KEYS.put(String.valueOf(KeyEvent.KEYCODE_STAR), resources.getString(R.string.key_star));
KEYS.put(
String.valueOf(-KeyEvent.KEYCODE_STAR),
resources.getString(R.string.key_star) + " " + resources.getString(R.string.key_hold_key)
);
}
public void reloadSettings() {
for (DropDownPreference dropDown : items) {
int keypadKey = settings.getFunctionKey(dropDown.getKey());
if (keypadKey != 0) {
dropDown.setValue(String.valueOf(keypadKey));
previewCurrentKey(dropDown);
}
}
}
public SectionKeymap populate() {
populateOtherItems(null);
return this;
}
public SectionKeymap activate() {
for (DropDownPreference item : items) {
onItemClick(item);
}
return this;
}
private void populateOtherItems(DropDownPreference itemToSkip) {
for (DropDownPreference item : items) {
if (itemToSkip != null && item != null && Objects.equals(itemToSkip.getKey(), item.getKey())) {
continue;
}
populateItem(item);
previewCurrentKey(item);
}
}
private void populateItem(DropDownPreference dropDown) {
if (dropDown == null) {
Logger.w("tt9/SectionKeymap.populateItem", "Cannot populate a NULL item. Ignoring.");
return;
}
ArrayList<String> keys = new ArrayList<>();
for (String key : KEYS.keySet()) {
if (
validateKey(dropDown, String.valueOf(key))
// backspace works both when pressed short and long,
// so separate "hold" and "not hold" options for it make no sense
&& !(dropDown.getKey().equals(ITEM_BACKSPACE) && Integer.parseInt(key) < 0)
// "show settings" must always be available for the users not to lose
// access to the Settings screen
&& !(dropDown.getKey().equals(ITEM_SHOW_SETTINGS) && key.equals("0"))
) {
keys.add(String.valueOf(key));
}
}
ArrayList<String> values = new ArrayList<>();
for (String key : keys) {
values.add(KEYS.get(key));
}
dropDown.setEntries(values.toArray(new CharSequence[0]));
dropDown.setEntryValues(keys.toArray(new CharSequence[0]));
}
private void onItemClick(DropDownPreference item) {
if (item == null) {
Logger.w("tt9/SectionKeymap.populateItem", "Cannot set a click listener a NULL item. Ignoring.");
return;
}
item.setOnPreferenceChangeListener((preference, newKey) -> {
if (!validateKey((DropDownPreference) preference, newKey.toString())) {
return false;
}
((DropDownPreference) preference).setValue(newKey.toString());
previewCurrentKey((DropDownPreference) preference, newKey.toString());
populateOtherItems((DropDownPreference) preference);
return true;
});
}
private void previewCurrentKey(DropDownPreference dropDown) {
previewCurrentKey(dropDown, dropDown.getValue());
}
private void previewCurrentKey(DropDownPreference dropDown, String key) {
if (dropDown == null) {
return;
}
dropDown.setSummary(KEYS.get(key));
}
private boolean validateKey(DropDownPreference dropDown, String key) {
if (dropDown == null || key == null) {
return false;
}
if (key.equals("0")) {
return true;
}
for (DropDownPreference item : items) {
if (item != null && !dropDown.getKey().equals(item.getKey()) && key.equals(item.getValue())) {
Logger.i("tt9/SectionKeymap.validateKey", "Key: '" + key + "' is already in use for function: " + item.getKey());
return false;
}
}
return true;
}
}

View file

@ -8,6 +8,9 @@ import androidx.preference.PreferenceManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.ime.TraditionalT9;
@ -15,24 +18,22 @@ import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.languages.LanguageCollection;
public class T9Preferences {
public static final int MAX_LANGUAGES = 32;
private static T9Preferences self;
public class SettingsStore {
private static SettingsStore self;
private final SharedPreferences prefs;
private final SharedPreferences.Editor prefsEditor;
public T9Preferences (Context context) {
public SettingsStore(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefsEditor = prefs.edit();
}
public static T9Preferences getInstance() {
public static SettingsStore getInstance() {
if (self == null) {
self = new T9Preferences(TraditionalT9.getMainContext());
self = new SettingsStore(TraditionalT9.getMainContext());
}
return self;
@ -45,21 +46,12 @@ public class T9Preferences {
return LanguageCollection.getLanguage(langId) != null;
}
private boolean isLanguageInRange(int langId) {
return langId > 0 && langId <= MAX_LANGUAGES;
}
private boolean validateSavedLanguage(int langId, String logTag) {
if (!doesLanguageExist(langId)) {
Logger.w(logTag, "Not saving invalid language with ID: " + langId);
return false;
}
if (!isLanguageInRange(langId)) {
Logger.w(logTag, "Valid language ID range is [0, 31]. Not saving out-of-range language: " + langId);
return false;
}
return true;
}
@ -75,32 +67,47 @@ public class T9Preferences {
/************* input settings *************/
public ArrayList<Integer> getEnabledLanguages() {
int languageMask = prefs.getInt("pref_enabled_languages", 1);
ArrayList<Integer>languageIds = new ArrayList<>();
public ArrayList<Integer> getEnabledLanguageIds() {
Set<String> languagesPref = getEnabledLanguagesIdsAsStrings();
for (int langId = 1; langId < MAX_LANGUAGES; langId++) {
int maskBit = 1 << (langId - 1);
if ((maskBit & languageMask) != 0) {
languageIds.add(langId);
}
ArrayList<Integer>languageIds = new ArrayList<>();
for (String languageId : languagesPref) {
languageIds.add(Integer.valueOf(languageId));
}
return languageIds;
}
public void saveEnabledLanguages(ArrayList<Integer> languageIds) {
int languageMask = 0;
public Set<String> getEnabledLanguagesIdsAsStrings() {
return prefs.getStringSet("pref_languages", new HashSet<>(Collections.singletonList("1")));
}
public void saveEnabledLanguageIds(ArrayList<Integer> languageIds) {
Set<String> idsAsStrings = new HashSet<>();
for (int langId : languageIds) {
if (!validateSavedLanguage(langId, "tt9/saveEnabledLanguages")){
idsAsStrings.add(String.valueOf(langId));
}
saveEnabledLanguageIds(idsAsStrings);
}
public void saveEnabledLanguageIds(Set<String> languageIds) {
Set<String> validLanguageIds = new HashSet<>();
for (String langId : languageIds) {
if (!validateSavedLanguage(Integer.parseInt(langId), "tt9/saveEnabledLanguageIds")){
continue;
}
int languageMaskBit = 1 << (langId - 1);
languageMask |= languageMaskBit;
validLanguageIds.add(langId);
}
prefsEditor.putInt("pref_enabled_languages", languageMask);
if (validLanguageIds.size() == 0) {
Logger.w("tt9/saveEnabledLanguageIds", "Refusing to save an empty language list");
return;
}
prefsEditor.putStringSet("pref_languages", validLanguageIds);
prefsEditor.apply();
}
@ -151,14 +158,55 @@ public class T9Preferences {
}
/************* hotkey settings *************/
/************* function key settings *************/
public boolean areFunctionKeysSet() {
return getKeyShowSettings() != 0;
}
public void setDefaultKeys() {
prefsEditor.putString(SectionKeymap.ITEM_ADD_WORD, String.valueOf(KeyEvent.KEYCODE_STAR));
prefsEditor.putString(SectionKeymap.ITEM_BACKSPACE, String.valueOf(KeyEvent.KEYCODE_BACK));
prefsEditor.putString(SectionKeymap.ITEM_NEXT_INPUT_MODE, String.valueOf(KeyEvent.KEYCODE_POUND));
prefsEditor.putString(SectionKeymap.ITEM_NEXT_LANGUAGE, String.valueOf(-KeyEvent.KEYCODE_POUND));
prefsEditor.putString(SectionKeymap.ITEM_SHOW_SETTINGS, String.valueOf(-KeyEvent.KEYCODE_STAR));
prefsEditor.apply();
}
public int getFunctionKey(String functionName) {
try {
return Integer.parseInt(prefs.getString(functionName, "0"));
} catch (NumberFormatException e) {
return 0;
}
}
public int getKeyAddWord() {
return getFunctionKey(SectionKeymap.ITEM_ADD_WORD);
}
public int getKeyBackspace() {
return prefs.getInt("pref_key_backspace", KeyEvent.KEYCODE_BACK);
return getFunctionKey(SectionKeymap.ITEM_BACKSPACE);
}
public int getKeyInputMode() { return prefs.getInt("pref_key_input_mode", KeyEvent.KEYCODE_POUND); }
public int getKeyOtherActions() { return prefs.getInt("pref_key_other_actions", KeyEvent.KEYCODE_STAR); }
public int getKeyNextInputMode() {
return getFunctionKey(SectionKeymap.ITEM_NEXT_INPUT_MODE);
}
public int getKeyNextLanguage() {
return getFunctionKey(SectionKeymap.ITEM_NEXT_LANGUAGE);
}
public int getKeyShowSettings() {
return getFunctionKey(SectionKeymap.ITEM_SHOW_SETTINGS);
}
/************* UI settings *************/
public boolean getDarkTheme() { return prefs.getBoolean("pref_dark_theme", true); }
public void setDarkTheme(boolean yes) { prefsEditor.putBoolean("pref_dark_theme", yes); }
public boolean getShowSoftKeys() { return prefs.getBoolean("pref_show_soft_keys", true); }
/************* internal settings *************/
@ -177,7 +225,7 @@ public class T9Preferences {
}
public void saveLastWord(String lastWord) {
// "last_word" was part of the original Preferences implementation.
// "last_word" was part of the original Settings implementation.
// It is weird, but it is simple and it works, so I decided to keep it.
prefsEditor.putString("last_word", lastWord);
prefsEditor.apply();

View file

@ -1,39 +0,0 @@
package io.github.sspanak.tt9.settings_legacy;
// http://stackoverflow.com/a/8488691
import android.content.Context;
import android.content.res.XmlResourceParser;
import android.util.AttributeSet;
import org.xmlpull.v1.XmlPullParser;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
public class CustomInflater {
public static ArrayList<Setting> inflate(Context context, int xmlFileResId, Object[] isettings)
throws Exception {
ArrayList<Setting> settings = new ArrayList<Setting>();
XmlResourceParser parser = context.getResources().getXml(xmlFileResId);
int token;
while ((token = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (token == XmlPullParser.START_TAG) {
if (!parser.getName().equals("Settings")) {
//prepend package
Class aClass = Class.forName("io.github.sspanak.tt9.settings_legacy."+parser.getName());
Class<?>[] params = new Class[]{Context.class, AttributeSet.class, isettings.getClass()};
Constructor<?> constructor = aClass.getConstructor(params);
try {
settings.add((Setting) constructor.newInstance(context, parser, isettings));
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
}
return settings;
}
}

View file

@ -1,42 +0,0 @@
package io.github.sspanak.tt9.settings_legacy;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import io.github.sspanak.tt9.R;
public class Setting {
String title;
String summary = null;
public String id;
public int widgetID = 0;
public int layout;
protected View view;
public Setting (Context context, AttributeSet attrs, Object[] isettings) {
// http://stackoverflow.com/a/8488691
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attr = attrs.getAttributeName(i);
if ("title".equals(attr)) {
// load string resource
title = context.getString(attrs.getAttributeResourceValue(i, 0));
} else if ("summary".equals(attr)) {
summary = context.getString(attrs.getAttributeResourceValue(i, 0));
} else if ("id".equals(attr)){
id = attrs.getAttributeValue(i);
}
}
if (summary == null)
layout = R.layout.setting;
else
layout = R.layout.setting_sum;
}
public void clicked(final Context context) {}
public void setView(View view) {
this.view = view;
}
public void init() {};
}

View file

@ -1,54 +0,0 @@
package io.github.sspanak.tt9.settings_legacy;
// https://github.com/codepath/android_guides/wiki/Using-an-ArrayAdapter-with-ListView
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import io.github.sspanak.tt9.R;
import java.util.ArrayList;
public class SettingAdapter extends ArrayAdapter<Setting> {
public SettingAdapter(Context context, ArrayList<Setting> settings) {
super(context, 0, settings);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Get the data item for this position
Setting setting = getItem(position);
final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
// Check if an existing view is being reused, otherwise inflate the view
if (convertView == null) {
convertView = layoutInflater.inflate(R.layout.setting_widget, parent, false);
}
setting.setView(convertView);
// Lookup view for data population
((TextView) convertView.findViewById(R.id.title)).setText(setting.title);
View sv = convertView.findViewById(R.id.summary);
if (setting.summary != null && sv != null) {
((TextView) sv).setText(setting.summary);
sv.setVisibility(View.VISIBLE);
}
else if (sv != null) { sv.setVisibility(View.GONE); }
final ViewGroup widgetFrame = (ViewGroup) convertView.findViewById(R.id.widget_frame);
if (setting.widgetID != 0) {
widgetFrame.removeAllViews();
layoutInflater.inflate(setting.widgetID, widgetFrame);
widgetFrame.setVisibility(View.VISIBLE);
}
else {
// hide the widget area
widgetFrame.setVisibility(View.GONE);
}
setting.init();
// Return the completed view to render on screen
return convertView;
}
}

View file

@ -1,56 +0,0 @@
package io.github.sspanak.tt9.settings_legacy;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.AttributeSet;
import io.github.sspanak.tt9.R;
public class SettingList extends Setting {
String[] entries;
int[] entryValues;
int defaultValue;
int value;
public SettingList (Context context, AttributeSet attrs, Object[] isettings) {
super(context, attrs, isettings);
// http://stackoverflow.com/a/8488691
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attr = attrs.getAttributeName(i);
if ("defaultValue".equals(attr)) {
defaultValue = attrs.getAttributeIntValue(i, -1);
} else if ("entryValues".equals(attr)) {
// load string resource
entryValues = context.getResources().getIntArray(attrs.getAttributeResourceValue(i, 0));
} else if ("entries".equals(attr)) {
entries = context.getResources().getStringArray(attrs.getAttributeResourceValue(i, 0));
}
}
widgetID = R.layout.preference_dialog;
layout = R.layout.setting_widget;
}
public void clicked(final Context context) {
AlertDialog.Builder builderSingle = new AlertDialog.Builder(context);
builderSingle.setTitle(title);
builderSingle.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builderSingle.setSingleChoiceItems(entries, value,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
value = entryValues[which];
dialog.dismiss();
}
});
builderSingle.show();
}
}

View file

@ -1,84 +0,0 @@
package io.github.sspanak.tt9.settings_legacy;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.AttributeSet;
import android.widget.TextView;
import java.util.ArrayList;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.preferences.T9Preferences;
public class SettingMultiList extends SettingList {
boolean[] selectedEntries;
public SettingMultiList (Context context, AttributeSet attrs, Object[] isettings) {
super(context, attrs, isettings);
selectedEntries = new boolean[entries.length];
for (int langId : T9Preferences.getInstance().getEnabledLanguages()) {
selectedEntries[langId - 1] = true; // languages are 1-based, unlike arrays
}
summary = buildItems();
}
public void clicked(final Context context) {
AlertDialog.Builder builderMulti = new AlertDialog.Builder(context);
builderMulti.setTitle(title);
builderMulti.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builderMulti.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (id.equals("pref_lang_support")) {
T9Preferences.getInstance().saveEnabledLanguages(buildSelection());
}
summary = buildItems();
dialog.dismiss();
((TextView)view.findViewById(R.id.summary)).setText(summary);
}
});
builderMulti.setMultiChoiceItems(entries, selectedEntries,
new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which, boolean opt) {
selectedEntries[which] = opt;
}
});
builderMulti.show();
}
private ArrayList<Integer> buildSelection(){
ArrayList<Integer> selection = new ArrayList<>();
for (int x=0;x<selectedEntries.length;x++) {
if (selectedEntries[x]) {
selection.add(entryValues[x]);
}
}
if (selection.size() < 1) {
selection.add(entryValues[0]);
}
return selection;
}
private String buildItems() {
StringBuilder sb = new StringBuilder();
for (int x=0;x<selectedEntries.length;x++) {
if (selectedEntries[x]) {
sb.append(entries[x]);
sb.append((", "));
}
}
if (sb.length() > 1)
sb.setLength(sb.length()-2);
return sb.toString();
}
}

View file

@ -1,6 +1,5 @@
package io.github.sspanak.tt9.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
@ -9,23 +8,31 @@ import android.os.Message;
import android.view.View;
import android.widget.EditText;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DictionaryDb;
import io.github.sspanak.tt9.db.InsertBlankWordException;
import io.github.sspanak.tt9.languages.InvalidLanguageException;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.preferences.T9Preferences;
import io.github.sspanak.tt9.preferences.SettingsStore;
public class AddWordAct extends Activity {
public class AddWordAct extends AppCompatActivity {
View main;
int lang;
String word;
private View main;
private int lang;
private String word;
@Override
protected void onCreate(Bundle savedData) {
AppCompatDelegate.setDefaultNightMode(
SettingsStore.getInstance().getDarkTheme() ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO
);
super.onCreate(savedData);
Intent i = getIntent();
word = i.getStringExtra("io.github.sspanak.tt9.word");
lang = i.getIntExtra("io.github.sspanak.tt9.lang", -1);
@ -46,7 +53,7 @@ public class AddWordAct extends Activity {
switch (msg.what) {
case 0:
Logger.d("onAddedWord", "Added word: '" + word + "'...");
T9Preferences.getInstance().saveLastWord(word);
SettingsStore.getInstance().saveLastWord(word);
break;
case 1:

View file

@ -21,6 +21,8 @@ import io.github.sspanak.tt9.languages.LanguageCollection;
public class DictionaryLoadingBar {
private static DictionaryLoadingBar self;
private static final int NOTIFICATION_ID = 1;
private static final String NOTIFICATION_CHANNEL_ID = "loading-notifications";
@ -33,7 +35,16 @@ public class DictionaryLoadingBar {
private boolean hasFailed = false;
DictionaryLoadingBar(Context context) {
public static DictionaryLoadingBar getInstance(Context context) {
if (self == null) {
self = new DictionaryLoadingBar(context);
}
return self;
}
public DictionaryLoadingBar(Context context) {
resources = context.getResources();
manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
@ -46,6 +57,7 @@ public class DictionaryLoadingBar {
));
notificationBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
} else {
//noinspection deprecation
notificationBuilder = new NotificationCompat.Builder(context);
}

View file

@ -13,17 +13,17 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionsAdapter.ViewHolder> {
private final int colorHighlight;
private final int layout;
private final int textViewResourceId;
private final LayoutInflater mInflater;
private final List<String> mSuggestions;
private int colorDefault;
private int colorHighlight;
private int selectedIndex = 0;
public SuggestionsAdapter(Context context, int layout, int textViewResourceId, int highLightColor, List<String> suggestions) {
this.colorHighlight = highLightColor;
public SuggestionsAdapter(Context context, int layout, int textViewResourceId, List<String> suggestions) {
this.layout = layout;
this.textViewResourceId = textViewResourceId;
this.mInflater = LayoutInflater.from(context);
@ -41,6 +41,7 @@ public class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionsAdapter.
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.suggestionItem.setText(mSuggestions.get(position));
holder.suggestionItem.setTextColor(colorDefault);
holder.suggestionItem.setBackgroundColor(selectedIndex == position ? colorHighlight : Color.TRANSPARENT);
}
@ -56,6 +57,16 @@ public class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionsAdapter.
}
public void setColorDefault(int colorDefault) {
this.colorDefault = colorDefault;
}
public void setColorHighlight(int colorHighlight) {
this.colorHighlight = colorHighlight;
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView suggestionItem;

View file

@ -15,7 +15,7 @@ import java.util.ArrayList;
import java.util.List;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.preferences.T9Preferences;
import io.github.sspanak.tt9.preferences.SettingsStore;
public class SuggestionsView {
private final List<String> suggestions = new ArrayList<>();
@ -40,8 +40,8 @@ public class SuggestionsView {
private void configureAnimation() {
DefaultItemAnimator animator = new DefaultItemAnimator();
int translateDuration = T9Preferences.getInstance().getSuggestionTranslateAnimationDuration();
int selectDuration = T9Preferences.getInstance().getSuggestionSelectAnimationDuration();
int translateDuration = SettingsStore.getInstance().getSuggestionTranslateAnimationDuration();
int selectDuration = SettingsStore.getInstance().getSuggestionSelectAnimationDuration();
animator.setMoveDuration(selectDuration);
animator.setChangeDuration(translateDuration);
@ -57,10 +57,11 @@ public class SuggestionsView {
context,
R.layout.suggestion_list_view,
R.id.suggestion_list_item,
ContextCompat.getColor(context, R.color.candidate_selected),
suggestions
);
mView.setAdapter(mSuggestionsAdapter);
setDarkTheme(true); // just use some default colors
}
@ -134,4 +135,28 @@ public class SuggestionsView {
mView.scrollToPosition(selectedIndex);
}
/**
* setDarkTheme
* Changes the suggestion 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
*/
public void setDarkTheme(boolean darkEnabled) {
Context context = mView.getContext();
int backgroundColor = darkEnabled ? R.color.dark_candidate_background : R.color.candidate_background;
int defaultColor = darkEnabled ? R.color.dark_candidate_color : R.color.candidate_color;
int highlightColor = darkEnabled ? R.color.dark_candidate_selected : R.color.candidate_selected;
mView.setBackgroundColor(ContextCompat.getColor(context, backgroundColor));
mSuggestionsAdapter.setColorDefault(ContextCompat.getColor(context, defaultColor));
mSuggestionsAdapter.setColorHighlight(ContextCompat.getColor(context, highlightColor));
}
}

View file

@ -1,134 +0,0 @@
package io.github.sspanak.tt9.ui;
import android.app.ListActivity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.widget.ListAdapter;
import android.widget.ListView;
import java.util.ArrayList;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DictionaryDb;
import io.github.sspanak.tt9.db.DictionaryImportAlreadyRunningException;
import io.github.sspanak.tt9.db.DictionaryLoader;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.preferences.T9Preferences;
import io.github.sspanak.tt9.settings_legacy.CustomInflater;
import io.github.sspanak.tt9.settings_legacy.Setting;
import io.github.sspanak.tt9.settings_legacy.SettingAdapter;
public class TraditionalT9Settings extends ListActivity implements DialogInterface.OnCancelListener {
private DictionaryLoader loader;
DictionaryLoadingBar progressBar;
Context mContext = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
progressBar = new DictionaryLoadingBar(this);
// get settings
T9Preferences prefs = new T9Preferences(getApplicationContext());
Object[] settings = {
prefs.getInputMode()
};
ListAdapter settingitems;
try {
settingitems = new SettingAdapter(this, CustomInflater.inflate(this, R.xml.prefs, settings));
} catch (Exception e) {
e.printStackTrace();
return;
}
setContentView(R.layout.preference_list_content);
setListAdapter(settingitems);
mContext = this;
}
@Override
public void onCancel(DialogInterface dint) {
if (loader != null) {
loader.stop();
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
Setting s = (Setting)getListView().getItemAtPosition(position);
switch (s.id) {
case "help":
openHelp();
break;
case "loaddict":
loadDictionaries();
break;
case "truncatedict":
truncateWords();
break;
default:
s.clicked(mContext);
break;
}
}
private void openHelp() {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(getString(R.string.help_url)));
startActivity(i);
}
private void truncateWords() {
if (loader != null && loader.isRunning()) {
loader.stop();
}
Handler afterTruncate = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
UI.toast(mContext, R.string.dictionary_truncated);
}
};
DictionaryDb.truncateWords(afterTruncate);
}
private void loadDictionaries() {
ArrayList<Language> languages = LanguageCollection.getAll(T9Preferences.getInstance().getEnabledLanguages());
progressBar.setFileCount(languages.size());
Handler loadHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
progressBar.show(msg.getData());
if (progressBar.isCompleted()) {
UI.toast(mContext, R.string.dictionary_loaded);
} else if (progressBar.isFailed()) {
UI.toast(mContext, R.string.dictionary_load_failed);
}
}
};
if (loader == null) {
loader = new DictionaryLoader(this);
}
try {
loader.load(loadHandler, languages);
} catch (DictionaryImportAlreadyRunningException e) {
loader.stop();
UI.toast(this, getString(R.string.dictionary_load_cancelled));
}
}
}

View file

@ -9,6 +9,7 @@ import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.TraditionalT9;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.PreferencesActivity;
public class UI {
public static void showAddWordDialog(TraditionalT9 tt9, int language, String currentWord) {
@ -21,8 +22,8 @@ public class UI {
}
public static void showPreferencesScreen(TraditionalT9 tt9) {
Intent prefIntent = new Intent(tt9, TraditionalT9Settings.class);
public static void showSettingsScreen(TraditionalT9 tt9) {
Intent prefIntent = new Intent(tt9, PreferencesActivity.class);
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
tt9.hideWindow();