diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 990073c2..fed9250e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,6 +105,16 @@ To translate Traditional T9 menus and messages in your language, add: `res/value Alternatively, if you don't have Android Studio, you could just use `res/values/strings.xml` as a reference and translate all strings in your file, skipping the ones that have the `translatable="false"` attribute. +## Adding Support for Keys +TT9 allows assigning hotkeys for performing different functions. If your phone has a special key that does not appear on the Hotkey configuration screen, you can easily add support for it. + +- Find [preferences/helpers/Hotkeys.java](io/github/sspanak/tt9/preferences/helpers/Hotkeys.java). +- In the file, find the `generateList()` function. +- Add the new key there. The order of adding is used when displaying the dropdown options. +- Optionally, you can translate the name of the key in different languages in the `res/values-XX/strings.xml` files. + + _You can find the key codes [in the Android docs](https://developer.android.com/reference/android/view/KeyEvent)._ + ## Contribution Process ### Before you start diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index 21152d10..9c9aecfc 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -50,4 +50,6 @@ Нов ред Интервал Напишете дума… + Клавиши в обратен ред + Включете настройката, ако на първият ред са 7–8–9, вместо 1–2–3. diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 71811193..a4d0a3a0 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -31,4 +31,6 @@ Carácter cuando se presiona \"0\" varias veces Teclado en pantalla Diccionario cargado con éxito. + Orden de teclas inverso + Habilite la configuración si hay 7–8–9 en la primera fila, en lugar de 1–2–3. diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 1ee02445..9ac2f3c7 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -41,4 +41,6 @@ Nouvelle ligne Caractère lorsque «0» est appuyé plusieurs fois Tapez un mot… + Inverser l\'ordre des clés + Activez le paramètre s\'il y a 7–8–9 sur le premier rang, au lieu de 1–2–3. diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index e259f3be..db8b574d 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -27,5 +27,7 @@ Tastiera Spazio Nuova riga + Invertire l\'ordine delle chiavi + Abilita l\'impostazione se ci sono 7–8–9 sulla prima riga, invece di 1–2–3. diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 144967b2..c2fb58fb 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -50,4 +50,6 @@ Назад Позвонить Введите слово… + Перевернутая клавиатура + Используйте настройку, если в первом ряду 7–8–9 вместо 1–2–3. diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 6c57552b..3b754668 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -50,4 +50,6 @@ Немає словника для мови «%1$s». Перейдіть до Налаштувань, щоб завантажити його. Помилка завантаження. Недійсне слово «%1$s» в рядку %2$d мови «%3$s». Введіть слово… + Зворотна клавіатура + Використовуйте налаштування, якщо 7–8–9 у першому рядку замість 1–2–3. diff --git a/res/values/strings.xml b/res/values/strings.xml index bb0fcb02..c0e71431 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -29,6 +29,8 @@ Character for Multiple 0-key Press Show On-Screen Keys Help + Reverse Key Order + Use this if you have 7–8–9 on the first row, instead of 1–2–3. Cancel loading Loading failed. Invalid word \"%1$s\" on line %2$d of language \"%3$s\". @@ -56,13 +58,7 @@ Back Call Delete - F1 - F2 - F3 - F4 Menu - # - . New Line diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index 9ddaa431..3ceb77c0 100644 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -88,11 +88,20 @@ + + + + 0); - case KeyEvent.KEYCODE_1: - case KeyEvent.KEYCODE_2: - case KeyEvent.KEYCODE_3: - case KeyEvent.KEYCODE_4: - case KeyEvent.KEYCODE_5: - case KeyEvent.KEYCODE_6: - case KeyEvent.KEYCODE_7: - case KeyEvent.KEYCODE_8: - case KeyEvent.KEYCODE_9: - return onNumber(Key.codeToNumber(keyCode), false, numKeyRepeatCounter); case KeyEvent.KEYCODE_STAR: return onStar(); case KeyEvent.KEYCODE_POUND: return onPound(); } diff --git a/src/io/github/sspanak/tt9/ime/helpers/Key.java b/src/io/github/sspanak/tt9/ime/helpers/Key.java index cfe515ad..ac5963de 100644 --- a/src/io/github/sspanak/tt9/ime/helpers/Key.java +++ b/src/io/github/sspanak/tt9/ime/helpers/Key.java @@ -6,7 +6,9 @@ import io.github.sspanak.tt9.preferences.SettingsStore; public class Key { public static boolean isNumber(int keyCode) { - return keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9; + return + (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) + || (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_9); } @@ -27,28 +29,38 @@ public class Key { } - public static int codeToNumber(int keyCode) { + public static int codeToNumber(SettingsStore settings, int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_0: + case KeyEvent.KEYCODE_NUMPAD_0: return 0; case KeyEvent.KEYCODE_1: - return 1; + case KeyEvent.KEYCODE_NUMPAD_1: + return settings.getUpsideDownKeys() ? 7 : 1; case KeyEvent.KEYCODE_2: - return 2; + case KeyEvent.KEYCODE_NUMPAD_2: + return settings.getUpsideDownKeys() ? 8 : 2; case KeyEvent.KEYCODE_3: - return 3; + case KeyEvent.KEYCODE_NUMPAD_3: + return settings.getUpsideDownKeys() ? 9 : 3; case KeyEvent.KEYCODE_4: + case KeyEvent.KEYCODE_NUMPAD_4: return 4; case KeyEvent.KEYCODE_5: + case KeyEvent.KEYCODE_NUMPAD_5: return 5; case KeyEvent.KEYCODE_6: + case KeyEvent.KEYCODE_NUMPAD_6: return 6; case KeyEvent.KEYCODE_7: - return 7; + case KeyEvent.KEYCODE_NUMPAD_7: + return settings.getUpsideDownKeys() ? 1 : 7; case KeyEvent.KEYCODE_8: - return 8; + case KeyEvent.KEYCODE_NUMPAD_8: + return settings.getUpsideDownKeys() ? 2 : 8; case KeyEvent.KEYCODE_9: - return 9; + case KeyEvent.KEYCODE_NUMPAD_9: + return settings.getUpsideDownKeys() ? 3 : 9; default: return -1; } diff --git a/src/io/github/sspanak/tt9/preferences/SettingsStore.java b/src/io/github/sspanak/tt9/preferences/SettingsStore.java index 246bda4c..c878eb84 100644 --- a/src/io/github/sspanak/tt9/preferences/SettingsStore.java +++ b/src/io/github/sspanak/tt9/preferences/SettingsStore.java @@ -45,6 +45,7 @@ public class SettingsStore { return true; } + @SuppressWarnings("SameParameterValue") private boolean isIntInList(int number, ArrayList list, String logTag, String logMsg) { if (!list.contains(number)) { Logger.w(logTag, logMsg); @@ -209,6 +210,7 @@ public class SettingsStore { public boolean getAutoSpace() { return prefs.getBoolean("auto_space", false); } public boolean getAutoTextCase() { return prefs.getBoolean("auto_text_case", true); } public String getDoubleZeroChar() { return prefs.getString("pref_double_zero_char", " "); } + public boolean getUpsideDownKeys() { return prefs.getBoolean("pref_upside_down_keys", false); } /************* internal settings *************/ diff --git a/src/io/github/sspanak/tt9/preferences/helpers/Hotkeys.java b/src/io/github/sspanak/tt9/preferences/helpers/Hotkeys.java new file mode 100644 index 00000000..31322be3 --- /dev/null +++ b/src/io/github/sspanak/tt9/preferences/helpers/Hotkeys.java @@ -0,0 +1,135 @@ +package io.github.sspanak.tt9.preferences.helpers; + +import android.content.Context; +import android.content.res.Resources; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.ViewConfiguration; + +import java.util.LinkedHashMap; +import java.util.Set; + +import io.github.sspanak.tt9.R; + +public class Hotkeys { + private final Context context; + private final Resources resources; + private final String holdKeyTranslation; + + private final LinkedHashMap KEYS = new LinkedHashMap<>(); + + + public Hotkeys(Context context) { + this.context = context; + resources = context.getResources(); + holdKeyTranslation = resources.getString(R.string.key_hold_key); + + addNoKey(); + generateList(); + } + + + public String get(String key) { + return KEYS.get(key); + } + + + public Set toSet() { + return KEYS.keySet(); + } + + + /** + * addIfDeviceHasKey + * Add the key only if Android says the device has such keypad button or a permanent touch key. + */ + private void addIfDeviceHasKey(int code, String name, boolean allowHold) { + if ( + (code == KeyEvent.KEYCODE_MENU && ViewConfiguration.get(context).hasPermanentMenuKey()) + || KeyCharacterMap.deviceHasKey(code) + ) { + add(code, name, allowHold); + } + } + + + /** + * addIfDeviceHasKey + * Same as addIfDeviceHasKey, but accepts a Resource String as a key name. + * + */ + @SuppressWarnings("SameParameterValue") + private void addIfDeviceHasKey(int code, int nameResource, boolean allowHold) { + addIfDeviceHasKey(code, resources.getString(nameResource), allowHold); + } + + + /** + * add + * These key will be added as a selectable option, regardless if it exists or or not. + * No validation will be performed. + */ + private void add(int code, String name, boolean allowHold) { + KEYS.put(String.valueOf(code), name); + + if (allowHold) { + KEYS.put(String.valueOf(-code), name + " " + holdKeyTranslation); + } + } + + /** + * add + * Same as add(), but accepts a Resource String as a key name. + */ + @SuppressWarnings("SameParameterValue") + private void add(int code, int nameResource, boolean allowHold) { + add(code, resources.getString(nameResource), allowHold); + } + + + /** + * addNoKey + * This is the "--" option. The key code matches no key on the keypad. + */ + private void addNoKey() { + add(0, R.string.key_none, false); + } + + + /** + * generateList + * These keys will appears as options only if Android says the device has them. + * + * NOTE: Some TT9 functions do not support all keys. Here you just list all possible options. + * Actual validation and assigning happens in SectionKeymap.populate(). + * + * NOTE 2: Holding is deliberately skipped for most of the keys. + * It's because handling holding requires short press event to be consumed in + * KeyPadHandler, as well. + * + * From user perspective, when holding is assigned to a function, + * short press will also stop performing its default system action, which may be confusing. + * And in order to avoid lengthy explanations in the documentation (that no one reads), + * the problem is avoided by simply not causing it. + */ + private void generateList() { + add(KeyEvent.KEYCODE_CALL, R.string.key_call, false); + + addIfDeviceHasKey(KeyEvent.KEYCODE_BACK, R.string.key_back, false); + addIfDeviceHasKey(KeyEvent.KEYCODE_DEL, R.string.key_delete, false); + addIfDeviceHasKey(KeyEvent.KEYCODE_F1, "F1", true); + addIfDeviceHasKey(KeyEvent.KEYCODE_F2, "F2", true); + addIfDeviceHasKey(KeyEvent.KEYCODE_F3, "F3", true); + addIfDeviceHasKey(KeyEvent.KEYCODE_F4, "F4", true); + addIfDeviceHasKey(KeyEvent.KEYCODE_MENU, R.string.key_menu, false); + + add(KeyEvent.KEYCODE_POUND, "#", true); + add(KeyEvent.KEYCODE_STAR, "✱", true); + + addIfDeviceHasKey(KeyEvent.KEYCODE_NUMPAD_ADD, "Num +", true); + addIfDeviceHasKey(KeyEvent.KEYCODE_NUMPAD_SUBTRACT, "Num -", true); + addIfDeviceHasKey(KeyEvent.KEYCODE_NUMPAD_MULTIPLY, "Num *", true); + addIfDeviceHasKey(KeyEvent.KEYCODE_NUMPAD_DIVIDE, "Num /", true); + addIfDeviceHasKey(KeyEvent.KEYCODE_NUMPAD_DOT, "Num .", true); + } +} diff --git a/src/io/github/sspanak/tt9/preferences/items/SectionKeymap.java b/src/io/github/sspanak/tt9/preferences/items/SectionKeymap.java index d97091e2..52790069 100644 --- a/src/io/github/sspanak/tt9/preferences/items/SectionKeymap.java +++ b/src/io/github/sspanak/tt9/preferences/items/SectionKeymap.java @@ -1,21 +1,16 @@ package io.github.sspanak.tt9.preferences.items; 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; import io.github.sspanak.tt9.preferences.SettingsStore; +import io.github.sspanak.tt9.preferences.helpers.Hotkeys; public class SectionKeymap { public static final String ITEM_ADD_WORD = "key_add_word"; @@ -24,15 +19,15 @@ public class SectionKeymap { public static final String ITEM_NEXT_LANGUAGE = "key_next_language"; public static final String ITEM_SHOW_SETTINGS = "key_show_settings"; - private final LinkedHashMap KEYS = new LinkedHashMap<>(); + private final Hotkeys hotkeys; private final Collection items; private final SettingsStore settings; public SectionKeymap(Collection dropDowns, Context context, SettingsStore settings) { items = dropDowns; + hotkeys = new Hotkeys(context); this.settings = settings; - generateKeyList(context); } @@ -60,95 +55,7 @@ public class SectionKeymap { } - /** - * generateKeyList - * Generates the key list to be used in each dropdown. - * - * NOTE: Some dropdowns do not support some keys,but filtering is performed - * in populate(), not here. - * - * NOTE 2: Holding is deliberately skipped for most of the keys. It's because - * when a function is assigned only to the "hold" state, the "short press" state - * also gets consumed in KeyPadHandler, effectively disabling the default function of the key. - * However, this may be confusing from user perspective, so in order to avoid lengthy - * explanations in the documentation (that no one reads), the problem is avoided by - * simply not causing it. - * - * So, if adding support for new keys, think twice whether to add the "hold" variant as well. - */ - private void generateKeyList(Context context) { - Resources resources = context.getResources(); - KEYS.put(String.valueOf(0), resources.getString(R.string.key_none)); - - // BACK - if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK)) { - KEYS.put(String.valueOf(KeyEvent.KEYCODE_BACK), resources.getString(R.string.key_back)); - } - - // CALL - KEYS.put(String.valueOf(KeyEvent.KEYCODE_CALL), resources.getString(R.string.key_call)); - - // DELETE / BACKSPACE - if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_DEL)) { - KEYS.put(String.valueOf(KeyEvent.KEYCODE_DEL), resources.getString(R.string.key_delete)); - } - - // F1 - 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) - ); - } - - // F2 - 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) - ); - } - - // F3 - 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) - ); - } - - // F4 - 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) - ); - } - - // MENU - if (ViewConfiguration.get(context).hasPermanentMenuKey()) { - KEYS.put(String.valueOf(KeyEvent.KEYCODE_MENU), resources.getString(R.string.key_menu)); - } - - // # - 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) - ); - } private void populateOtherItems(DropDownPreference itemToSkip) { @@ -169,7 +76,7 @@ public class SectionKeymap { } ArrayList keys = new ArrayList<>(); - for (String key : KEYS.keySet()) { + for (String key : hotkeys.toSet()) { if ( validateKey(dropDown, String.valueOf(key)) // backspace works both when pressed short and long, @@ -185,7 +92,7 @@ public class SectionKeymap { ArrayList values = new ArrayList<>(); for (String key : keys) { - values.add(KEYS.get(key)); + values.add(hotkeys.get(key)); } dropDown.setEntries(values.toArray(new CharSequence[0])); @@ -222,7 +129,7 @@ public class SectionKeymap { return; } - dropDown.setSummary(KEYS.get(key)); + dropDown.setSummary(hotkeys.get(key)); }