From e98639dc9de032849c5eb8f794c6941a163e36f0 Mon Sep 17 00:00:00 2001 From: Emil Kosz Date: Sat, 19 Oct 2024 16:20:48 +0200 Subject: [PATCH] Different way of inputting alternate characters (long press), miscellaneous fixes. --- USER_EXPERIENCE.md | 53 +- app/src/main/AndroidManifest.xml | 2 +- .../titanboard/{IME.java => InputMethod.java} | 123 +- .../eu/titanboard/KeymapManager.java | 132 +- .../eu/titanboard/MetaKeyManager.java | 26 - .../eu/titanboard/PreferencesActivity.java | 11 + app/src/main/res/drawable/key.xml | 13 - app/src/main/res/drawable/suggestion.xml | 13 - app/src/main/res/layout/input.xml | 134 +- app/src/main/res/layout/preferences.xml | 18 +- app/src/main/res/values/styles.xml | 30 +- .../res/values/{strings.xml => values.xml} | 2 + app/src/main/res/xml/keymaps_titan.xml | 1208 +++++------------ 13 files changed, 512 insertions(+), 1253 deletions(-) rename app/src/main/java/cfpi_fpsi/eu/titanboard/{IME.java => InputMethod.java} (60%) delete mode 100644 app/src/main/res/drawable/key.xml delete mode 100644 app/src/main/res/drawable/suggestion.xml rename app/src/main/res/values/{strings.xml => values.xml} (71%) diff --git a/USER_EXPERIENCE.md b/USER_EXPERIENCE.md index 5e29686..13035d8 100644 --- a/USER_EXPERIENCE.md +++ b/USER_EXPERIENCE.md @@ -38,31 +38,20 @@ and so on. Here are my solutions for the above listed problems: 1. This is solvable with shift/alt latching. This makes it so when you press these keys once, the - next key pressed will be either uppercased or the alternate key visible on the physical button. - Additionally, the shift and alt key can be toggled by double-clicking in a fast succession, - similar - to regular visual keyboards on Android. +next key pressed will be either uppercased or the alternate key visible on the physical button. +Additionally, the shift and alt key can be toggled by double-clicking in a fast succession, similar +to regular visual keyboards on Android. 2. This is unfortunately unsolvable with software. 3. See 1., latching the keys solves this issue. -4. This requires a visual keyboard, the UI of which is discussed later. Why does this require a - visual keyboard, and not some alternate "symbol" mode, perhaps accessible with some keyboard - shortcuts? That would make the learning curve of the keyboard higher, and it would be less - intuitive. One should make sure not to complicate the physical keyboard any more than it already - is, - and use all of its idiosyncrasies but add no more. +4. This is solved by assigning alternate symbols when alt is pressed/latched/locked and the given +key is long pressed. A menu pops up, and as you hold the key the symbols are cycled through. 5. Similar to regular visual keyboards on Android, a diacritic/alternate key is assigned to each key - according to the language. Long pressing a key will print it. Ideally, there should also be a - popup - menu allowing for several characters under one key. Cycling through the keys would use the - shift/alt - keys (so that no matter what key you use, the other hand can cycle through) and selecting it - would - happen on release of the key. +according to the language. Long pressing a key will allow you to cycle through available +alternatives. 6. I have no plans on adding a control key, as it is largely unnecessary on phones. 7. I do not really use emojis, but I am aware that they are used by the majority of people. Most - IM apps provide their own emoji list, so this isn't a priority. Designing a good UX for it is - also - hard on the Titan. +IM apps provide their own emoji list, so this isn't a priority. Designing a good UX for it is also +hard on the Titan. Taking into account typing ergonomics and pre-existing UX choices of other keyboard designers, this input method should make typing on the Unihertz Titan a pleasant experience. Other than fixing the @@ -73,9 +62,8 @@ fundamental issues, the keyboard should also: 2. Autocorrect, language switching, auto caps and periods. 3. Some minimal customization, but just enough not to overwhelm the user. -For the Unihertz Titan Pocket/Slim, the sym key will be used as the alt+gr key, providing diacritics -and such. This is because in the official documentation and implementation the alt key is designed -for the alternate keys printed on the buttons. +For the Unihertz Titan Pocket/Slim, the sym key is unused. I have no ideas what it could be useful +for. It can be set in Unihertz's software to do operating system actions, which I think is good. # User interface @@ -85,14 +73,15 @@ be a square, and so will the remaining view. On the Titan, the screen is already remaining space will be proportionally much smaller. Therefore, the keyboard should occupy very minimal screen estate, as to allow the user to see the application they are interacting with. -For this reason, I make sure the keyboard occupies 20–25 % screen estate. This is enough for a -symbol bar and a suggestions bar, as well as some additional buttons such as language change or -settings. +For this reason, I make sure the keyboard occupies 20–25 % screen estate. This is enough for simple +status bar, which shows the current shift/alt state, the current language, and a symbol bar which +appears when long pressing keys. -Both the symbol bar and suggestions bar are scrollable. The suggestions being scrollable is a -feature on some visual keyboards already, however, I have come to the decision to make this for the -symbol bar as well. An expandable menu would make the keyboard occupy half, or more of the screen. -A scrollable bar is more intuitive and can hold an indefinite amount of characters, possibly also -user-defined. +# Keymaps +Latin keymaps use the regular QWERTY layout of the Titan. There is one, large, general European key +layout, which contains all characters used in Europe. -A simple divider between the suggestion and symbol bar is added to avoid accidental clicks. \ No newline at end of file +## European +German has the most speakers, therefore it's diacritics are the first in the list. Then French and +Italian, which have similar diacritics. These come next. Afterwards, Spanish, then Polish, and the +rest. \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2758b9f..661f2bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ android:supportsRtl="true" tools:targetApi="31"> mCurrentValues; private KeymapManager mKeymapManager; private Deque mLanguageQueue = new ArrayDeque<>(); private String mCurrentLanguage; private LinearLayout mInputView; private Vibrator mVibrator; + SharedPreferences mPreferences; + private int mKeyCounter = 0; + private boolean mRegularKeyPressed = false; + @Override public void onCreate() { @@ -59,6 +68,7 @@ public class IME extends InputMethodService { mLanguageQueue.addLast(mCurrentLanguage); mVibrator = (Vibrator) getBaseContext().getSystemService(VIBRATOR_SERVICE); mMetaKeyManager = new MetaKeyManager(mVibrator); + mPreferences = getSharedPreferences(getString(R.string.preferences_key), Context.MODE_PRIVATE); } @SuppressLint("InflateParams") @@ -98,28 +108,68 @@ public class IME extends InputMethodService { mMetaKeyManager.adjustMetaStateAfterKeypress(); return false; case KeyEvent.KEYCODE_SPACE: - conn.commitText(" ", 1); - mMetaKeyManager.adjustMetaStateAfterKeypress(); + handleSpacebar(conn); return true; default: - String key = getKey(keyCode); + mCurrentValues = getKeyValues(keyCode); - if (key == null) { + if (mCurrentValues == null) { return false; } - Log.d("STATE", "Sending key: " + key + " whose keyCode is: " + keyCode); + mRegularKeyPressed = true; - conn.commitText(key, 1); - mMetaKeyManager.adjustMetaStateAfterKeypress(); + if (event.getRepeatCount() == 0) { + conn.commitText(mCurrentValues.get(0), 1); + } else if (event.getRepeatCount() % 7 == 0) { + mKeyCounter++; + if (mKeyCounter >= mCurrentValues.size()) { + mKeyCounter = 0; + } + + updateSymbolsView(mCurrentValues, mKeyCounter); + + ExtractedText text = conn.getExtractedText(new ExtractedTextRequest(), 0); + + conn.setComposingRegion(text.selectionStart - 1, text.selectionStart); + conn.setComposingText(mCurrentValues.get(mKeyCounter), 1); + } return true; } } + public void handleSpacebar(InputConnection conn) { + // If we press alt + space, we switch the language. + if (mMetaKeyManager.getAlt().isActive()) { + mCurrentLanguage = mLanguageQueue.pop(); + mLanguageQueue.addLast(mCurrentLanguage); + updateMetaStateTextView(); + } else { + conn.commitText(" ", 1); + } + + mMetaKeyManager.adjustMetaStateAfterKeypress(); + } + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mRegularKeyPressed) { + InputConnection conn = getCurrentInputConnection(); + + if (conn == null) { + return false; + } + + conn.finishComposingText(); + mMetaKeyManager.adjustMetaStateAfterKeypress(); + mKeyCounter = 0; + mRegularKeyPressed = false; + updateSymbolsView(null, 0); + } + mMetaKeyManager.updateMetaStateUp(keyCode, event); updateMetaStateTextView(); + return false; } @@ -140,41 +190,29 @@ public class IME extends InputMethodService { updateMetaStateTextView(); } - private String getKey(int keyCode) { + private ArrayList getKeyValues(int keyCode) { if (mMetaKeyManager.getAlt().isActive()) { return mKeymapManager.getKeyAlt(mCurrentLanguage, keyCode); } - if (mMetaKeyManager.getSym().isActive() && mMetaKeyManager.getShift().isActive()) { - return mKeymapManager.getKeyShiftSym(mCurrentLanguage, keyCode); - } - - if (mMetaKeyManager.getSym().isActive()) { - return mKeymapManager.getKeySym(mCurrentLanguage, keyCode); - } - if (mMetaKeyManager.getShift().isActive()) { return mKeymapManager.getKeyShift(mCurrentLanguage, keyCode); } - // Nothing active. - return mKeymapManager.getKeyNone(mCurrentLanguage, keyCode); + return mKeymapManager.getKeyBase(mCurrentLanguage, keyCode); } @SuppressLint("SetTextI18n") private void updateMetaStateTextView() { TextView shiftStateView = mInputView.findViewById(R.id.state_shift); TextView altStateView = mInputView.findViewById(R.id.state_alt); - TextView symStateView = mInputView.findViewById(R.id.state_sym); - Button languageButton = mInputView.findViewById(R.id.language_button); + TextView languageStateView = mInputView.findViewById(R.id.state_language); shiftStateView.setText(getStringForMetaKey( mMetaKeyManager.getShift(), "aa", "Aa", "AA")); altStateView.setText(getStringForMetaKey( mMetaKeyManager.getAlt(), "..", "#?", "#!")); - symStateView.setText(getStringForMetaKey( - mMetaKeyManager.getSym(), "..", "þπ", "ÞΩ")); - languageButton.setText(mKeymapManager.getShortLabel(mCurrentLanguage)); + languageStateView.setText(mKeymapManager.getShortLabel(mCurrentLanguage)); } private String getStringForMetaKey(MetaKeyManager.MetaKey key, String normal, String latched, String pressed) { @@ -189,20 +227,25 @@ public class IME extends InputMethodService { return normal; } - public void onInputViewLanguageClick(View v) { - mCurrentLanguage = mLanguageQueue.pop(); - mLanguageQueue.addLast(mCurrentLanguage); - updateMetaStateTextView(); - } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void updateSymbolsView(ArrayList symbols, int index) { + LinearLayout symbolsView = mInputView.findViewById(R.id.symbols); + symbolsView.removeAllViews(); + if (symbols != null) { + for (int i = 0; i < symbols.size(); ++i) { + Button symbolButton; - /** - * @noinspection unused - */ - public void onInputViewKeyClick(View v) { - Button key = (Button) v; - InputConnection conn = getCurrentInputConnection(); - if (conn != null) { - conn.commitText(key.getText(), 1); + if (i == index) { + symbolButton = new Button(getBaseContext(), null, 0, + R.style.KeyboardSymbolSelected); + } else { + symbolButton = new Button(getBaseContext(), null, 0, + R.style.KeyboardSymbol); + } + + symbolButton.setText(symbols.get(i)); + symbolsView.addView(symbolButton); + } } } } diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/KeymapManager.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/KeymapManager.java index 80e9bd5..94a7791 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/KeymapManager.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/KeymapManager.java @@ -25,6 +25,8 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.HashMap; public class KeymapManager { @@ -33,14 +35,15 @@ public class KeymapManager { public KeymapManager(XmlPullParser parser) { Keymap keymap = null; - String lang = null, shortLabel = null, label = null; int code = 0; - String none = null, shift = null, alt = null, sym = null, shiftSym = null; + String lang = null, shortLabel = null, label = null; + Key key = null; try { while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { int type = parser.getEventType(); + // We encounter a new keymap. if (type == XmlPullParser.START_TAG && parser.getName().equals("keymap")) { keymap = new Keymap(); lang = null; @@ -59,46 +62,49 @@ public class KeymapManager { break; } } - Log.d("STATE", "BEGINNING OF KEYMAP: " + label); } else if (type == XmlPullParser.END_TAG && parser.getName().equals("keymap")) { - Log.d("STATE", "END OF KEYMAP: " + label); keymap.mShortLabel = shortLabel; keymap.mLabel = label; mKeymaps.put(lang, keymap); } + // We got a new key. if (type == XmlPullParser.START_TAG && parser.getName().equals("key")) { - none = null; - shift = null; - alt = null; - sym = null; - shiftSym = null; + key = new Key(); for (int i = 0; i < parser.getAttributeCount(); ++i) { - String attr = parser.getAttributeName(i); - switch (attr) { - case "code": - code = Integer.parseInt(parser.getAttributeValue(i)); - break; - case "none": - none = parser.getAttributeValue(i); - break; - case "shift": - shift = parser.getAttributeValue(i); - break; - case "alt": - alt = parser.getAttributeValue(i); - break; - case "sym": - sym = parser.getAttributeValue(i); - break; - case "shiftSym": - shiftSym = parser.getAttributeValue(i); - break; + if (parser.getAttributeName(i).equals("code")) { + code = Integer.parseInt(parser.getAttributeValue(i)); } } - Log.d("STATE", "Key code: " + code + ". Value sym: " + sym); } else if (type == XmlPullParser.END_TAG && parser.getName().equals("key")) { - keymap.put(code, new Key(none, shift, alt, sym, shiftSym)); + Log.d("STATE", "+++ Keycode: " + code); + Log.d("STATE", key.toString()); + keymap.put(code, key); + } + + // We encountered a base value. + if (type == XmlPullParser.START_TAG && parser.getName().equals("base")) { + for (int i = 0; i < parser.getAttributeCount(); ++i) { + if (parser.getAttributeName(i).startsWith("value")) { + key.mBase.add(parser.getAttributeValue(i)); + } + } + } + // We encountered a shift value. + if (type == XmlPullParser.START_TAG && parser.getName().equals("shift")) { + for (int i = 0; i < parser.getAttributeCount(); ++i) { + if (parser.getAttributeName(i).startsWith("value")) { + key.mShift.add(parser.getAttributeValue(i)); + } + } + } + // We encountered an alt value. + if (type == XmlPullParser.START_TAG && parser.getName().equals("alt")) { + for (int i = 0; i < parser.getAttributeCount(); ++i) { + if (parser.getAttributeName(i).startsWith("value")) { + key.mAlt.add(parser.getAttributeValue(i)); + } + } } parser.next(); @@ -121,16 +127,16 @@ public class KeymapManager { } } - public String getKeyNone(String lang, int keyCode) { + public ArrayList getKeyBase(String lang, int keyCode) { Key key = getKey(lang, keyCode); if (key != null) { - return key.mNone; + return key.mBase; } else { return null; } } - public String getKeyShift(String lang, int keyCode) { + public ArrayList getKeyShift(String lang, int keyCode) { Key key = getKey(lang, keyCode); if (key != null) { return key.mShift; @@ -139,7 +145,7 @@ public class KeymapManager { } } - public String getKeyAlt(String lang, int keyCode) { + public ArrayList getKeyAlt(String lang, int keyCode) { Key key = getKey(lang, keyCode); if (key != null) { return key.mAlt; @@ -148,26 +154,8 @@ public class KeymapManager { } } - public String getKeySym(String lang, int keyCode) { - Key key = getKey(lang, keyCode); - if (key != null) { - return key.mSym; - } else { - return null; - } - } - - public String getKeyShiftSym(String lang, int keyCode) { - Key key = getKey(lang, keyCode); - if (key != null) { - return key.mShiftSym; - } else { - return null; - } - } - public Key getKey(String lang, int keyCode) { - HashMap keymap = mKeymaps.get(lang); + Keymap keymap = mKeymaps.get(lang); if (keymap != null) { Key key = keymap.get(keyCode); if (key != null) { @@ -186,18 +174,32 @@ public class KeymapManager { } public static class Key { - public String mNone; - public String mShift; - public String mAlt; - public String mSym; - public String mShiftSym; + public ArrayList mBase = new ArrayList<>(); + public ArrayList mShift = new ArrayList<>(); + public ArrayList mAlt = new ArrayList<>(); - public Key(String none, String shift, String alt, String sym, String shiftSym) { - mNone = none; - mShift = shift; - mAlt = alt; - mSym = sym; - mShiftSym = shiftSym; + public String toString() { + StringBuilder output = new StringBuilder(); + + output.append("\n=== Base: "); + for (String value : mBase) { + output.append(value); + output.append(","); + } + + output.append("\n=== Shift: "); + for (String value : mShift) { + output.append(value); + output.append(","); + } + + output.append("\n=== Alt: "); + for (String value : mAlt) { + output.append(value); + output.append(","); + } + + return output.toString(); } } } diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/MetaKeyManager.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/MetaKeyManager.java index 118e684..41cce55 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/MetaKeyManager.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/MetaKeyManager.java @@ -25,7 +25,6 @@ import android.view.KeyEvent; public class MetaKeyManager { public MetaKey mShift = new MetaKey(); public MetaKey mAlt = new MetaKey(); - public MetaKey mSym = new MetaKey(); private Vibrator mVibrator; @@ -43,9 +42,6 @@ public class MetaKeyManager { case KeyEvent.KEYCODE_SHIFT_RIGHT: updateMetaKeyDown(mShift, event); break; - case KeyEvent.KEYCODE_SYM: - updateMetaKeyDown(mSym, event); - break; default: break; } @@ -57,13 +53,6 @@ public class MetaKeyManager { key.setPressed(true); key.setLatched(key.isLocked() ? key.isLocked() : key.isLatched()); key.setLocked(false); - - if (mShift.isPressed() && mAlt.isPressed()) { - mSym.setLatched(!mSym.isLatched()); - mShift.setUsed(true); - mAlt.setUsed(true); - mVibrator.vibrate(20); - } } public void updateMetaStateUp(int keyCode, KeyEvent event) { @@ -76,9 +65,6 @@ public class MetaKeyManager { case KeyEvent.KEYCODE_SHIFT_RIGHT: updateMetaKeyUp(mShift, event); break; - case KeyEvent.KEYCODE_SYM: - updateMetaKeyUp(mSym, event); - break; default: break; } @@ -99,7 +85,6 @@ public class MetaKeyManager { public void adjustMetaStateAfterKeypress() { adjustMetaStateHelper(mShift); adjustMetaStateHelper(mAlt); - adjustMetaStateHelper(mSym); } public void adjustMetaStateHelper(MetaKey key) { @@ -116,7 +101,6 @@ public class MetaKeyManager { public void resetMetaKeyState() { mShift.resetState(); mAlt.resetState(); - mSym.resetState(); } public MetaKey getShift() { @@ -127,10 +111,6 @@ public class MetaKeyManager { return mAlt; } - public MetaKey getSym() { - return mSym; - } - public static class MetaKey { private boolean mPressed = false; private boolean mLatched = false; @@ -188,11 +168,5 @@ public class MetaKeyManager { public void setKeyUpTime(long time) { mKeyUpTime = time; } - - public enum MetaKeyType { - ALT, - SHIFT, - SYM - } } } diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/PreferencesActivity.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/PreferencesActivity.java index cbd8e38..b13c864 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/PreferencesActivity.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/PreferencesActivity.java @@ -20,12 +20,23 @@ package cfpi_fpsi.eu.titanboard; import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; +import android.widget.RadioGroup; +import android.widget.SeekBar; public class PreferencesActivity extends Activity { + SharedPreferences mPreferences; + SharedPreferences.Editor mPrefEditor; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.preferences); + + mPreferences = getBaseContext().getSharedPreferences(getString(R.string.preferences_key), + Context.MODE_PRIVATE); + mPrefEditor = mPreferences.edit(); } } diff --git a/app/src/main/res/drawable/key.xml b/app/src/main/res/drawable/key.xml deleted file mode 100644 index 3e0a863..0000000 --- a/app/src/main/res/drawable/key.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/suggestion.xml b/app/src/main/res/drawable/suggestion.xml deleted file mode 100644 index 6defad3..0000000 --- a/app/src/main/res/drawable/suggestion.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/input.xml b/app/src/main/res/layout/input.xml index 37516ed..ebe9705 100644 --- a/app/src/main/res/layout/input.xml +++ b/app/src/main/res/layout/input.xml @@ -10,7 +10,7 @@ @@ -28,128 +28,24 @@ android:text=".." tools:ignore="HardcodedText" /> + + + + + - -