19 Commits
Author SHA1 Message Date
emil f5043031e7 Update README 2025-01-16 13:53:07 +01:00
emil c710fd1e6a Add screenshot 2025-01-16 13:42:02 +01:00
emil bddfc5c725 Manual updates 2025-01-16 13:19:47 +01:00
emil cdb0e55e67 Bugfixes, refactoring, add Czech and Hebrew 2025-01-16 13:02:43 +01:00
emil 0816c1a1d9 Refactoring 2024-11-28 22:44:25 +01:00
emil 3ab7bf1eba More refractoring, add symbol meta key definition in XML 2024-11-28 20:37:30 +01:00
emil c03c3f01c5 Some slight refactoring 2024-11-28 14:47:20 +01:00
emil 7b5da54aea Keymap fixes 2024-11-27 00:09:11 +01:00
emil 3a40699cc9 TODO 2024-11-26 02:17:09 +01:00
emil 6076c69f3f More keymap refactoring 2024-11-26 01:44:15 +01:00
emil e47227ce6d Some major changes, keymap restructuring with device and language keymaps 2024-11-25 21:14:45 +01:00
emil ec931a53a6 Basic tweak functionality 2024-11-22 22:52:17 +01:00
emil 7cb31ff224 Add preference categories, update the manual 2024-11-22 18:50:28 +01:00
emil 7f60898dd8 Add a user manual screen 2024-11-22 17:44:08 +01:00
emil 6abc48c121 Refactor the InputView 2024-11-22 12:55:42 +01:00
emil a7de68929a Shift+Enter always sends newline, misc. fixes 2024-11-21 23:54:45 +01:00
emil 68f48dce09 Add some TODOs 2024-11-21 22:51:04 +01:00
emil 32ae47e349 Fix alt+backspace deleting lines of text, add Greek question mark under Alt+X in Greek layout, move Arabic diacritics to shifted symbols, fix button being displayed in all caps 2024-11-13 17:01:31 +01:00
emil 185d518faa Bring back automatic cycling 2024-11-09 00:03:41 +01:00
40 changed files with 4166 additions and 2515 deletions
+13 -7
View File
@@ -1,5 +1,7 @@
# Titanboard
![Screenshot of the program at work](screenshot.png "Titanboard")
Titanboard is an input method for physical keyboard phones, supporting as low as Android 1.5
Cupcake.
@@ -9,17 +11,15 @@ Cupcake.
- Nimble and small; I set out to have the APK be at most 100 kB
- Each physical key can have several additional symbols, which the user can cycle by long pressing
a given key. This also applies for alternate (alt) symbols
- 9 supported languages, with more planned (which are trivial to add)
- 11 supported languages, which are trivial to add yourself (consult res/xml/language_keymaps.xml)
- No hard to memorize key combinations; reasonably uses what the physical keyboard has to offer
- Clipboard management
- Fixes for PKB quirks
- Reasonably customizable
## Planned features
- Autocorrect
- Emojis
- Support for content other than text in clipboard
- Floating interface, which is placed over the current text in certain contexts, to save screen
space
- For now, Titanboard is an abandoned, but stable project.
## Supported languages
@@ -32,9 +32,15 @@ Cupcake.
- Russian (Phonetic)
- Greek (polytonic diacritics under Q)
- Arabic (Phonetic)
- Hebrew
- Czech
## Supported devices
Thus far, the supported devices are:
- Unihertz Titan (additional diacritics under 1 key for all languages)
- Unihertz Titan
- Unihertz Titan Slim
- Unihertz Titan Pocket
It's very easy to add new devices as well, just look into res/xml/device_keymaps.xml.
+2 -2
View File
@@ -10,8 +10,8 @@ android {
applicationId 'eur.cfpifpsi.titanboard'
minSdk 3
targetSdk 35
versionCode 30
versionName '0.3'
versionCode 40
versionName '0.4'
}
buildTypes {
+11 -4
View File
@@ -29,11 +29,8 @@
android:roundIcon="@drawable/ic_app_round"
android:supportsRtl="true"
tools:targetApi="31">
<profileable
android:shell="true"
tools:targetApi="q" />
<service
android:name=".InputMethod"
android:name=".TitanboardIME"
android:directBootAware="true"
android:exported="false"
android:label="@string/app_name"
@@ -54,6 +51,16 @@
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity
android:name=".ManualActivity"
android:exported="true"
android:label="@string/manual_name">
<intent-filter>
<action android:name="eur.cfpifpsi.titanboard.intent.SHOW_MANUAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -26,18 +26,21 @@ import android.os.Build;
import java.util.ArrayList;
import eur.cfpifpsi.titanboard.views.InputView;
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class ClipManager implements ClipboardManager.OnPrimaryClipChangedListener {
private final InputMethod mInputMethod;
private final PreferenceHolder mPreferenceHolder;
private final InputView mInputView;
private final ClipboardManager mClipboardManager;
private final ArrayList<ClipData.Item> mClips;
private long mLastCallbackTimestamp;
public ClipManager(Context context) {
mInputMethod = (InputMethod) context;
mClips = new ArrayList<>(mInputMethod.mPreferenceHolder.getClipboardSize());
mClipboardManager = (ClipboardManager)
mInputMethod.getSystemService(Context.CLIPBOARD_SERVICE);
public ClipManager(Context context, PreferenceHolder preferenceHolder, InputView inputView) {
mPreferenceHolder = preferenceHolder;
mInputView = inputView;
mClips = new ArrayList<>(mPreferenceHolder.getClipboardSize());
mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
mClipboardManager.addPrimaryClipChangedListener(this);
}
@@ -58,12 +61,12 @@ public class ClipManager implements ClipboardManager.OnPrimaryClipChangedListene
mClips.add(0, item);
}
if (mClips.size() > mInputMethod.mPreferenceHolder.getClipboardSize()) {
if (mClips.size() > mPreferenceHolder.getClipboardSize()) {
mClips.remove(mClips.size() - 1);
}
}
mInputMethod.mInputView.setClips(mClips);
mInputView.setClips(mClips);
}
}
}
@@ -1,262 +0,0 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard;
import android.annotation.SuppressLint;
import android.inputmethodservice.InputMethodService;
import android.os.Handler;
import android.os.Looper;
import android.os.Vibrator;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import java.util.ArrayList;
// TODO: Implement device recognition and a keymap picking based on it.
public class InputMethod extends InputMethodService {
public MetaKeyManager mMetaKeyManager;
public KeymapManager mKeymapManager;
public PreferenceHolder mPreferenceHolder;
public ClipManager mClipManager;
public InputView mInputView;
public Vibrator mVibrator;
int mCurrentCursorPosition;
Handler mHandler;
private SymbolAutoCycler mSymbolAutoCycler;
private SymbolManualCycler mSymbolManualCycler;
private FinishCyclingRunnable mFinishCyclingRunnable;
private int mLastSymbolKeyCode;
@Override
public void onCreate() {
super.onCreate();
mPreferenceHolder = new PreferenceHolder(this);
mVibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
mMetaKeyManager = new MetaKeyManager(mVibrator);
mKeymapManager = new KeymapManager(this);
mClipManager = new ClipManager(this);
mHandler = new Handler(Looper.getMainLooper());
mSymbolAutoCycler = new SymbolAutoCycler();
mSymbolManualCycler = new SymbolManualCycler();
mFinishCyclingRunnable = new FinishCyclingRunnable();
mKeymapManager.nextKeymap(mPreferenceHolder.getSelectedKeymaps());
}
@SuppressLint("InflateParams")
@Override
public void onInitializeInterface() {
mInputView = (InputView) getLayoutInflater().inflate(R.layout.input, null);
mInputView.updateStateText();
mInputView.setSymbolsVisible(false);
}
@Override
public View onCreateInputView() {
return mInputView;
}
@Override
public boolean onEvaluateFullscreenMode() {
// This is to override the default behaviour, which seems to do more than detect landscape
// mode.
return false;
}
/**
* @noinspection FeatureEnvy
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
mMetaKeyManager.updateMetaStateDown(keyCode);
mInputView.updateStateText();
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (isInputViewShown()) {
requestHideSelf(0);
return true;
}
return false;
}
if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_ENTER) {
mMetaKeyManager.adjustMetaStateAfterKeypress();
mInputView.updateStateText();
return false;
}
ArrayList<String> symbols = mKeymapManager.getSymbols(keyCode, mMetaKeyManager);
InputConnection conn = getCurrentInputConnection();
EditorInfo info = getCurrentInputEditorInfo();
if (symbols == null || conn == null || info == null) {
return false;
}
if (info.inputType == InputType.TYPE_NULL) {
return false;
}
if (event.getRepeatCount() == 0 && !symbols.isEmpty()) {
mMetaKeyManager.adjustMetaStateAfterKeypress();
mInputView.updateStateText();
if (mPreferenceHolder.isCycleManualOn()) {
boolean isCycling = mHandler.hasMessages(0);
if (isCycling && keyCode == mLastSymbolKeyCode && symbols.size() > 1) {
mSymbolManualCycler.run();
} else {
mFinishCyclingRunnable.run();
conn.commitText(symbols.get(0), 1);
mSymbolManualCycler.prepare(symbols);
}
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(mFinishCyclingRunnable, mPreferenceHolder.getCycleSpeed());
mLastSymbolKeyCode = keyCode;
} else {
conn.commitText(symbols.get(0), 1);
if (symbols.size() > 1) {
mSymbolAutoCycler.prepare(symbols);
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(mSymbolAutoCycler, mPreferenceHolder.getCycleSpeed());
}
}
}
return true;
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
// If manual cycling is on, all this is done using a Runnable after some timeout.
if (!mPreferenceHolder.isCycleManualOn()) {
mFinishCyclingRunnable.run();
}
mMetaKeyManager.updateMetaStateUp(keyCode, event);
mInputView.updateStateText();
return false;
}
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
int candidatesStart, int candidatesEnd) {
mCurrentCursorPosition = newSelStart;
}
@Override
public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
// getCharacterBounds() --- API 21 (Lollipop)
// getInsertionMarkerTop() -- API 21 (Lollipop)
}
@Override
public void onStartInput(EditorInfo attribute, boolean restarting) {
super.onStartInput(attribute, restarting);
mKeymapManager.adjustCurrentKeymap(mPreferenceHolder.getSelectedKeymaps());
mInputView.updateStateText();
// Soon™
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// getCurrentInputConnection().requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR);
// }
}
@Override
public void onFinishInput() {
mMetaKeyManager.resetMetaKeyState();
mInputView.mClipboardPopup.dismiss();
}
// Cyclers & runnables
class FinishCyclingRunnable implements Runnable {
@Override
public void run() {
InputConnection conn = getCurrentInputConnection();
if (conn != null) {
conn.finishComposingText();
}
mHandler.removeCallbacksAndMessages(null);
mInputView.setSymbolsVisible(false);
}
}
class SymbolManualCycler extends Cycler {
@Override
public void onCycle() {
mVibrator.vibrate(10);
mInputView.setSelectedSymbol(getIndex());
InputConnection conn = getCurrentInputConnection();
if (conn != null) {
conn.setComposingText(getCurrentElement().toString(), 1);
}
}
@SuppressWarnings("unchecked") //NON-NLS
@Override
public void onFirstCycle() {
mInputView.setSymbolsVisible(true);
mInputView.setSymbols((ArrayList<String>) getList());
InputConnection conn = getCurrentInputConnection();
if (conn != null) {
conn.setSelection(mCurrentCursorPosition - getElementAt(0).toString().length(),
mCurrentCursorPosition);
}
}
}
class SymbolAutoCycler extends Cycler {
@Override
public void onCycle() {
mVibrator.vibrate(10);
mInputView.setSelectedSymbol(getIndex());
InputConnection conn = getCurrentInputConnection();
if (conn != null) {
conn.setComposingText(getCurrentElement().toString(), 1);
}
}
@SuppressWarnings("unchecked") //NON-NLS
@Override
public void onFirstCycle() {
mInputView.setSymbolsVisible(true);
mInputView.setSymbols((ArrayList<String>) getList());
InputConnection conn = getCurrentInputConnection();
if (conn != null) {
conn.setSelection(mCurrentCursorPosition - getElementAt(0).toString().length(),
mCurrentCursorPosition);
}
}
}
}
@@ -1,231 +0,0 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard;
import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.os.Build;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import java.util.ArrayList;
// TODO: programmatically scroll if current selected symbol is outside the visible space of the
// scrollview
public final class InputView extends RelativeLayout {
private final int[] buttonStateToggled = {R.attr.state_toggled};
private final InputMethod mInputMethod;
private final LinearLayout.LayoutParams mSymbolLayout;
private final LinearLayout.LayoutParams mClipLayout;
public PopupWindow mClipboardPopup;
private LinearLayout mSymbolList;
private LinearLayout mClipList;
private Button mStateButton;
private ImageButton mClipboardButton;
public InputView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
mInputMethod = (InputMethod) context;
mSymbolLayout = new LinearLayout.LayoutParams(
getResources().getDimensionPixelSize(R.dimen.symbol_width),
ViewGroup.LayoutParams.MATCH_PARENT, 0);
mClipLayout = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mSymbolList = findViewById(R.id.symbol_list);
mStateButton = findViewById(R.id.state_button);
mClipboardButton = findViewById(R.id.clipboard_button);
ScrollView mClipboardView = (ScrollView) inflate(getContext(), R.layout.clipboard, null);
mClipList = mClipboardView.findViewById(R.id.clip_list);
mClipboardPopup = new PopupWindow(mClipboardView,
getResources().getDimensionPixelSize(R.dimen.clipboard_popup_width),
getResources().getDimensionPixelSize(R.dimen.clipboard_popup_height), true);
mClipboardPopup.setOnDismissListener(() -> mClipboardButton.setImageState(null, true));
mStateButton.setOnClickListener(button -> {
MetaKeyManager metaManager = mInputMethod.mMetaKeyManager;
if (metaManager.getAlt().isActive()) {
metaManager.getAlt().resetState();
} else {
mInputMethod.mKeymapManager
.nextKeymap(mInputMethod.mPreferenceHolder.getSelectedKeymaps());
}
updateStateText();
});
mStateButton.setOnLongClickListener(button -> {
Intent intent = new Intent(getContext(), SettingsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
// For some reason, this vibrates by default, so we don't need to call the vibrator.
return true;
});
mClipboardButton.setOnClickListener(button -> {
mClipboardPopup.showAsDropDown(mClipboardButton,
-(mClipboardPopup.getWidth() - mClipboardButton.getWidth()) / 2, 0);
mClipboardButton.setImageState(buttonStateToggled, true);
});
mClipboardButton.setOnLongClickListener(button -> {
// On a long click, we just send the latest clip (if any).
if (!mInputMethod.mClipManager.isEmpty()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mInputMethod.getCurrentInputConnection()
.commitText(mInputMethod.mClipManager.getItemAt(0).getText(), 1);
}
return true;
});
}
/**
* @noinspection FeatureEnvy
*/
public void updateStateText() {
MetaKeyManager metaManager = mInputMethod.mMetaKeyManager;
KeymapManager keymapManager = mInputMethod.mKeymapManager;
mStateButton.setPaintFlags(mStateButton.getPaintFlags() & ~(Paint.UNDERLINE_TEXT_FLAG));
if (metaManager.getAlt().isLocked() || metaManager.getShift().isLocked()) {
mStateButton.setPaintFlags(mStateButton.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
}
if (metaManager.getAlt().isPressed() || metaManager.getAlt().isLocked()) {
mStateButton.setText(R.string.state_button_alt_locked);
return;
}
if (metaManager.getAlt().isLatched()) {
mStateButton.setText(R.string.state_button_alt_latched);
return;
}
if (metaManager.getShift().isPressed() || metaManager.getShift().isLocked()) {
mStateButton.setText(keymapManager.getCurrentLockedLabel());
return;
}
if (metaManager.getShift().isLatched()) {
mStateButton.setText(keymapManager.getCurrentLatchedLabel());
return;
}
mStateButton.setText(keymapManager.getCurrentLabel());
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void setClips(ArrayList<? extends ClipData.Item> items) {
int size = items.size();
for (int i = 0; i < size; ++i) {
ClipData.Item item = items.get(i);
LinearLayout clipView = (LinearLayout) mClipList.getChildAt(i);
if (clipView == null) {
clipView = (LinearLayout) inflate(getContext(), R.layout.clip, null);
clipView.setTag(i);
clipView.setLayoutParams(mClipLayout);
mClipList.addView(clipView);
}
Button pasteButton = clipView.findViewById(R.id.paste_button);
pasteButton.setText(item.getText());
pasteButton.setOnClickListener(button -> mInputMethod.getCurrentInputConnection()
.commitText(item.getText(), 1));
LinearLayout finalClipView = clipView;
clipView.findViewById(R.id.button_delete).setOnClickListener(button -> {
mClipList.removeView(finalClipView);
mInputMethod.mClipManager.removeItem(item);
});
}
}
public void setSymbolsVisible(boolean state) {
mSymbolList.setVisibility(state ? VISIBLE : GONE);
}
/**
* @noinspection MethodWithMultipleLoops
*/
public void setSymbols(ArrayList<String> strings) {
int size = strings.size();
for (int i = 0; i < size; ++i) {
TextView tv = (TextView) mSymbolList.getChildAt(i);
if (tv == null) {
tv = (TextView) inflate(getContext(), R.layout.symbol, null);
tv.setTag(i);
tv.setLayoutParams(mSymbolLayout);
mSymbolList.addView(tv);
}
tv.setText(strings.get(i));
tv.setVisibility(VISIBLE);
}
int childCount = mSymbolList.getChildCount();
for (int i = size; i < childCount; ++i) {
mSymbolList.getChildAt(i).setVisibility(GONE);
}
}
public void setSelectedSymbol(int index) {
int childCount = mSymbolList.getChildCount();
for (int i = 0; i < childCount; ++i) {
TextView tv = (TextView) mSymbolList.getChildAt(i);
if (i == index) {
HorizontalScrollView hsw = findViewById(R.id.symbol_list_container);
tv.setPressed(true);
tv.setTypeface(null, Typeface.BOLD);
// This, if the symbol list is larger than the scroll view, scrolls it just to show
// the selected symbol at the very end. It isn't really necessary, as it's a rarity
// that a key has more than five symbols, but perhaps someone will want to add a
// keymap that has a lot of symbols under one key, and also this is a safeguard for
// smaller screen sizes.
hsw.smoothScrollTo(tv.getRight()
+ (getResources().getDimensionPixelSize(R.dimen.default_space) << 1)
- hsw.getRight(), 0);
} else {
tv.setPressed(false);
tv.setTypeface(null, Typeface.NORMAL);
}
}
}
}
@@ -1,240 +0,0 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard;
import android.content.Context;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class KeymapManager {
private static final String TAG_KEY = "key"; //NON-NLS
private static final String TAG_KEYMAP = "keymap"; //NON-NLS
private static final String TAG_BASE = "base"; //NON-NLS
private static final String TAG_ALT = "alt"; //NON-NLS
private static final String TAG_SHIFT = "shift"; //NON-NLS
private static final String ATTR_CODE = "code"; //NON-NLS
private static final String ATTR_VALUE = "value"; //NON-NLS
private static final String ATTR_LABEL = "label"; //NON-NLS
private static final String ATTR_LATCHED_LABEL = "latched-label"; //NON-NLS
private static final String ATTR_LOCKED_LABEL = "locked-label"; //NON-NLS
private final Context mContext;
private final HashMap<String, Keymap> mKeymaps;
private Keymap mCurrentKeymap;
public KeymapManager(Context context) {
mContext = context;
mKeymaps = new HashMap<>(context.getResources()
.getStringArray(R.array.preferences_keymaps).length);
loadDeviceKeymaps(context.getResources().getXml(R.xml.keymaps_titan));
}
/**
* @noinspection BreakStatement
*/
private void loadDeviceKeymaps(XmlPullParser parser) {
Keymap keymap = null;
int code = 0;
String label = null, latchedLabel = null, lockedLabel = null;
Key key = null;
while (true) {
int eventType;
try {
eventType = parser.getEventType();
} catch (XmlPullParserException e) {
break;
}
if (eventType == XmlPullParser.END_DOCUMENT) break;
// We encounter a new keymap.
if (eventType == XmlPullParser.START_TAG && parser.getName().equals(TAG_KEYMAP)) {
keymap = new Keymap();
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; ++i) {
switch (parser.getAttributeName(i)) {
case ATTR_LABEL:
label = parser.getAttributeValue(i);
break;
case ATTR_LATCHED_LABEL:
latchedLabel = parser.getAttributeValue(i);
break;
case ATTR_LOCKED_LABEL:
lockedLabel = parser.getAttributeValue(i);
break;
}
}
} else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG_KEYMAP)
&& keymap != null) {
keymap.code = label;
keymap.latchedCode = latchedLabel;
keymap.lockedCode = lockedLabel;
mKeymaps.put(label, keymap);
}
// We got a new key.
if (eventType == XmlPullParser.START_TAG && parser.getName().equals(TAG_KEY)) {
key = new Key();
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; ++i) {
if (parser.getAttributeName(i).equals(ATTR_CODE)) {
code = Integer.parseInt(parser.getAttributeValue(i));
}
}
} else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG_KEY)
&& keymap != null) {
keymap.put(code, key);
}
// We encountered a base value.
if (eventType == XmlPullParser.START_TAG && parser.getName().equals(TAG_BASE)) {
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; ++i) {
if (parser.getAttributeName(i).startsWith(ATTR_VALUE) && key != null) {
key.symbolsBase.add(parser.getAttributeValue(i));
}
}
}
// We encountered a shift value.
if (eventType == XmlPullParser.START_TAG && parser.getName().equals(TAG_SHIFT)) {
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; ++i) {
if (parser.getAttributeName(i).startsWith(ATTR_VALUE) && key != null) {
key.symbolsShift.add(parser.getAttributeValue(i));
}
}
}
// We encountered an alt value.
if (eventType == XmlPullParser.START_TAG && parser.getName().equals(TAG_ALT)) {
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; ++i) {
if (parser.getAttributeName(i).startsWith(ATTR_VALUE) && key != null) {
key.symbolsAlt.add(parser.getAttributeValue(i));
}
}
}
try {
parser.next();
} catch (XmlPullParserException | IOException e) {
break;
}
}
}
public ArrayList<String> getSymbols(int keyCode, MetaKeyManager metaKeyManager) {
ArrayList<String> alt = mCurrentKeymap.getSymbolsAlt(keyCode);
ArrayList<String> shift = mCurrentKeymap.getSymbolsShift(keyCode);
if (metaKeyManager.getAlt().isActive() && alt != null && !alt.isEmpty()) {
return alt;
}
if (metaKeyManager.getShift().isActive() && shift != null && !shift.isEmpty()) {
return shift;
}
return mCurrentKeymap.getSymbolsBase(keyCode);
}
public void adjustCurrentKeymap(List<String> selectedKeymaps) {
if (!selectedKeymaps.contains(mCurrentKeymap.code)) {
nextKeymap(selectedKeymaps);
}
}
public void nextKeymap(List<String> selectedKeymaps) {
if (!selectedKeymaps.isEmpty() && mCurrentKeymap != null) {
int index = selectedKeymaps.indexOf(mCurrentKeymap.code);
index++;
if (index == selectedKeymaps.size()) {
index = 0;
}
mCurrentKeymap = mKeymaps.get(selectedKeymaps.get(index));
} else {
mCurrentKeymap =
mKeymaps.get(mContext.getResources().getString(R.string.keymap_default));
}
}
public String getCurrentLabel() {
return mCurrentKeymap.code;
}
public String getCurrentLatchedLabel() {
return mCurrentKeymap.latchedCode;
}
public String getCurrentLockedLabel() {
return mCurrentKeymap.lockedCode;
}
static class Keymap {
private final static int INITIAL_KEYMAP_SIZE = 64; // Enough for just about any QWERTY.
private final HashMap<Integer, Key> mMap = new HashMap<>(INITIAL_KEYMAP_SIZE);
String code;
String latchedCode;
String lockedCode;
/**
* @noinspection ReturnOfNull
*/
public ArrayList<String> getSymbolsBase(int keyCode) {
Key key = mMap.get(keyCode);
return key == null ? null : key.symbolsBase;
}
/**
* @noinspection ReturnOfNull
*/
public ArrayList<String> getSymbolsAlt(int keyCode) {
Key key = mMap.get(keyCode);
return key == null ? null : key.symbolsAlt;
}
/**
* @noinspection ReturnOfNull
*/
public ArrayList<String> getSymbolsShift(int keyCode) {
Key key = mMap.get(keyCode);
return key == null ? null : key.symbolsShift;
}
public void put(Integer key, Key value) {
mMap.put(key, value);
}
}
static class Key {
private final static int INITIAL_SIZE = 6;
final ArrayList<String> symbolsBase = new ArrayList<>(INITIAL_SIZE);
final ArrayList<String> symbolsShift = new ArrayList<>(INITIAL_SIZE);
final ArrayList<String> symbolsAlt = new ArrayList<>(INITIAL_SIZE);
}
}
@@ -0,0 +1,35 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard;
import android.app.Activity;
import android.os.Bundle;
import android.text.Html;
import android.widget.TextView;
public class ManualActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_manual);
// Convert the HTML manual into a TextView format
TextView tv = findViewById(R.id.manual_text);
tv.setText(Html.fromHtml(getString(R.string.user_manual_html)));
}
}
@@ -1,3 +1,21 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard;
import android.view.KeyEvent;
@@ -8,7 +26,7 @@ public class MetaKey {
private boolean mLatched;
private boolean mLocked;
private boolean mUsed;
private long mKeyUpTime;
private long mLastKeyUpTime;
public void onDown() {
mUsed = false;
@@ -19,12 +37,12 @@ public class MetaKey {
mLocked = false;
}
public void onUp(KeyEvent event) {
if (event.getEventTime() - mKeyUpTime < TOGGLE_DURATION) {
public void onUp(long eventTime) {
if (eventTime - mLastKeyUpTime < TOGGLE_DURATION) {
mLocked = true;
}
mKeyUpTime = event.getEventTime();
mLastKeyUpTime = eventTime;
mPressed = false;
mLatched = !mLatched && !mLocked && !mUsed;
}
@@ -21,71 +21,66 @@ package eur.cfpifpsi.titanboard;
import android.os.Vibrator;
import android.view.KeyEvent;
import java.util.ArrayList;
public class MetaKeyManager {
private static final int TOGGLE_VIBRATION_TIME = 30;
public final MetaKey mShift = new MetaKey();
public final MetaKey mAlt = new MetaKey();
public final MetaKey mSymbol = new MetaKey();
private final Vibrator mVibrator;
public MetaKeyManager(Vibrator vibrator) {
mVibrator = vibrator;
}
public void updateMetaStateDown(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_ALT_LEFT:
case KeyEvent.KEYCODE_ALT_RIGHT:
mAlt.onDown();
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
case KeyEvent.KEYCODE_SHIFT_RIGHT:
mShift.onDown();
break;
default:
break;
public boolean updateMetaStateDown(int keyCode, ArrayList<Integer> symbolMeta) {
if (symbolMeta.contains(keyCode)) {
mSymbol.onDown();
return true;
} else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
mShift.onDown();
return true;
}
return false;
}
/**
* @noinspection FeatureEnvy
*/
public void updateMetaStateUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ALT_LEFT:
case KeyEvent.KEYCODE_ALT_RIGHT:
mAlt.onUp(event);
if (mAlt.isLocked()) {
mVibrator.vibrate(TOGGLE_VIBRATION_TIME);
}
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
case KeyEvent.KEYCODE_SHIFT_RIGHT:
mShift.onUp(event);
if (mShift.isLocked()) {
mVibrator.vibrate(TOGGLE_VIBRATION_TIME);
}
break;
default:
break;
public boolean updateMetaStateUp(int keyCode, long eventTime, ArrayList<Integer> symbolMeta) {
if (symbolMeta.contains(keyCode)) {
mSymbol.onUp(eventTime);
if (mSymbol.isLocked()) {
mVibrator.vibrate(TOGGLE_VIBRATION_TIME);
}
return true;
} else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
mShift.onUp(eventTime);
if (mShift.isLocked()) {
mVibrator.vibrate(TOGGLE_VIBRATION_TIME);
}
return true;
}
return false;
}
public void adjustMetaStateAfterKeypress() {
mShift.afterKeypress();
mAlt.afterKeypress();
mSymbol.afterKeypress();
}
public void resetMetaKeyState() {
mShift.resetState();
mAlt.resetState();
mSymbol.resetState();
}
public MetaKey getShift() {
return mShift;
}
public MetaKey getAlt() {
return mAlt;
public MetaKey getSymbol() {
return mSymbol;
}
}
@@ -32,6 +32,7 @@ import java.util.Set;
public class PreferenceHolder implements SharedPreferences.OnSharedPreferenceChangeListener {
private final Map<String, Object> mPreferenceValues;
private final Context mContext;
private final SharedPreferences.Editor mEditor;
@SuppressWarnings("unchecked") //NON-NLS
public PreferenceHolder(Context context) {
@@ -40,6 +41,7 @@ public class PreferenceHolder implements SharedPreferences.OnSharedPreferenceCha
PreferenceManager.setDefaultValues(mContext, R.xml.preferences, false);
mPreferences.registerOnSharedPreferenceChangeListener(this);
mPreferenceValues = (Map<String, Object>) mPreferences.getAll();
mEditor = mPreferences.edit();
}
@Override
@@ -55,6 +57,11 @@ public class PreferenceHolder implements SharedPreferences.OnSharedPreferenceCha
return value == null ? defaultValue : value;
}
public String getDevice() {
return (String) getOrDefault(mContext.getString(R.string.preferences_key_device),
mContext.getString(R.string.device_default));
}
public boolean isCycleManualOn() {
return (boolean) getOrDefault(mContext.getString(R.string.preferences_key_cycle_manual),
mContext.getResources().getBoolean(R.bool.cycle_manual_default));
@@ -75,9 +82,32 @@ public class PreferenceHolder implements SharedPreferences.OnSharedPreferenceCha
@SuppressWarnings("unchecked") //NON-NLS
public ArrayList<String> getSelectedKeymaps() {
Set<String> selectedKeymaps = (Set<String>)
getOrDefault(mContext.getString(R.string.preferences_key_keymaps),
getOrDefault(mContext.getString(R.string.preferences_key_languages),
mContext.getResources()
.getStringArray(R.array.preferences_keymap_defaults));
.getStringArray(R.array.preferences_language_defaults));
return new ArrayList<>(selectedKeymaps);
}
public boolean isAlternateComposingOn(String appName) {
return (boolean) getOrDefault(
mContext.getString(R.string.tweaks_key_alternate_composing) + appName, false);
}
public boolean isEnterAlwaysSendsOn(String appName) {
return (boolean) getOrDefault(
mContext.getString(R.string.tweaks_key_enter_always_sends) + appName, false);
}
public void setAlternateComposing(String appName, boolean value) {
mEditor.putBoolean(
mContext.getString(R.string.tweaks_key_alternate_composing) + appName, value);
mEditor.commit();
}
public void setEnterAlwaysSends(String appName, boolean value) {
mEditor.putBoolean(
mContext.getString(R.string.tweaks_key_enter_always_sends) + appName, value);
mEditor.commit();
}
}
@@ -0,0 +1,216 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard;
import android.annotation.SuppressLint;
import android.inputmethodservice.InputMethodService;
import android.os.Vibrator;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import java.util.ArrayList;
import eur.cfpifpsi.titanboard.input.SymbolInputHandler;
import eur.cfpifpsi.titanboard.keymapping.KeymapManager;
import eur.cfpifpsi.titanboard.views.InputView;
// TODO: Add auto-capitalization
// TODO: Add auto-periods
// TODO: Add auto-correct
// TODO: Emojis with popup window
// TODO: Composing with diacritics, polytonic Greek as well
// TODO: Fix up the tweaks popup so it matches in style other UI elements
// TODO: Call answering/rejecting like in Pocketboard
// TODO: Floating input view
// TODO: Make the IME work after reboot, e.g. to type in passwords
public class TitanboardIME extends InputMethodService {
private MetaKeyManager mMetaKeyManager;
private KeymapManager mKeymapManager;
private PreferenceHolder mPreferenceHolder;
private ClipManager mClipManager;
private InputView mInputView;
private Vibrator mVibrator;
private SymbolInputHandler mSymbolInputHandler;
@Override
public void onCreate() {
super.onCreate();
mPreferenceHolder = new PreferenceHolder(this);
mVibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
mMetaKeyManager = new MetaKeyManager(mVibrator);
mKeymapManager = new KeymapManager(this);
mKeymapManager.nextKeymap(mPreferenceHolder.getSelectedKeymaps());
}
@SuppressLint("InflateParams")
@Override
public void onInitializeInterface() {
mInputView = (InputView) getLayoutInflater().inflate(R.layout.input, null);
// These things shouldn't depend on one another like that, this is bound to break
// something badly.
mClipManager = new ClipManager(this, mPreferenceHolder, mInputView);
mInputView.setDependencies(mKeymapManager, mMetaKeyManager, mPreferenceHolder, mClipManager);
mSymbolInputHandler = new SymbolInputHandler(mInputView, mVibrator);
mInputView.updateStateText();
mInputView.setSymbolsVisible(false);
}
@Override
public View onCreateInputView() {
return mInputView;
}
@Override
public boolean onEvaluateFullscreenMode() {
// This is to override the default behaviour, which seems to do more than detect landscape
// mode.
return false;
}
/**
* @noinspection FeatureEnvy
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO: make sure to return false if it's a key we don't really know, so that media keys
// etc. on some keyboards are handled correctly
if (mMetaKeyManager.updateMetaStateDown(keyCode,
mKeymapManager.getDeviceMetaSymbolKeyCodes(mPreferenceHolder.getDevice()))) {
mInputView.updateStateText();
return true;
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (isInputViewShown()) {
requestHideSelf(0);
return true;
}
return false;
}
ArrayList<String> symbols = mKeymapManager.getCurrentKeySymbolsMeta(keyCode,
mPreferenceHolder.getDevice(), mMetaKeyManager);
InputConnection conn = getCurrentInputConnection();
EditorInfo info = getCurrentInputEditorInfo();
if (conn == null || info == null || info.inputType == InputType.TYPE_NULL) {
return false;
}
if (keyCode == KeyEvent.KEYCODE_DEL) {
return handleDelete();
}
if (keyCode == KeyEvent.KEYCODE_ENTER) {
return handleEnter(conn, mPreferenceHolder.isEnterAlwaysSendsOn(info.packageName));
}
// TODO: It's kind of a mess, isn't it? I have to rethink this.
if (event.getRepeatCount() == 0) {
mMetaKeyManager.adjustMetaStateAfterKeypress();
mInputView.updateStateText();
mSymbolInputHandler.handleSymbolInput(conn, symbols, keyCode,
mPreferenceHolder.isAlternateComposingOn(info.packageName),
mPreferenceHolder.isCycleManualOn(), mPreferenceHolder.getCycleSpeed());
}
return true;
}
public boolean handleDelete() {
if (mMetaKeyManager.getSymbol().isPressed()) {
return true;
}
mMetaKeyManager.adjustMetaStateAfterKeypress();
mInputView.updateStateText();
return false;
}
public boolean handleEnter(InputConnection conn, boolean enterAlwaysSends) {
if (mMetaKeyManager.getShift().isPressed()) {
conn.commitText("\n", 1);
mMetaKeyManager.adjustMetaStateAfterKeypress();
mInputView.updateStateText();
return true;
}
if (enterAlwaysSends) {
conn.performEditorAction(EditorInfo.IME_ACTION_SEND);
return true;
}
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
InputConnection conn = getCurrentInputConnection();
// If manual cycling is on, all this is done using a Runnable after some timeout.
if (!mPreferenceHolder.isCycleManualOn() && conn != null) {
mSymbolInputHandler.finishCycling(conn);
}
if (mMetaKeyManager.updateMetaStateUp(keyCode, event.getEventTime(),
mKeymapManager.getDeviceMetaSymbolKeyCodes(mPreferenceHolder.getDevice()))) {
mInputView.updateStateText();
return true;
}
return false;
}
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
int candidatesStart, int candidatesEnd) {
mSymbolInputHandler.setCursorPos(newSelStart);
}
@Override
public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
// getCharacterBounds() --- API 21 (Lollipop)
// getInsertionMarkerTop() -- API 21 (Lollipop)
}
@Override
public void onStartInput(EditorInfo attribute, boolean restarting) {
super.onStartInput(attribute, restarting);
mKeymapManager.adjustCurrentKeymap(mPreferenceHolder.getSelectedKeymaps());
mInputView.updateStateText();
mInputView.updateTweaks();
// Soon™
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// getCurrentInputConnection().requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR);
// }
}
@Override
public void onFinishInput() {
mMetaKeyManager.resetMetaKeyState();
mInputView.dismissPopup();
}
}
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard;
package eur.cfpifpsi.titanboard.input;
import java.util.ArrayList;
@@ -44,8 +44,8 @@ public abstract class Cycler implements Runnable {
public void prepare(ArrayList<?> list) {
mList = list;
mHasCycled = false;
mIndex = 0;
mHasCycled = false;
}
@Override
@@ -55,11 +55,7 @@ public abstract class Cycler implements Runnable {
mHasCycled = true;
}
++mIndex;
if (mIndex >= mList.size()) {
mIndex = 0;
}
mIndex = (mIndex + 1) % mList.size();
onCycle();
}
@@ -0,0 +1,85 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.input;
import android.os.Handler;
import android.os.Vibrator;
import android.view.inputmethod.InputConnection;
import java.util.ArrayList;
import eur.cfpifpsi.titanboard.views.InputView;
public class SymbolCycler extends Cycler {
private static final int CYCLE_VIBRATION_TIME = 10;
private final InputView mInputView;
private final Vibrator mVibrator;
private final Handler mHandler;
private InputConnection mInputConnection;
private boolean mAlternateComposing;
private boolean mManual;
private int mCycleSpeed;
private int mLastCursorPosition;
public SymbolCycler(InputView inputView, Vibrator vibrator, Handler handler) {
mInputView = inputView;
mVibrator = vibrator;
mHandler = handler;
}
public void setInputParameters(InputConnection conn, boolean alternateComposing,
boolean automatic, int cycleSpeed) {
mInputConnection = conn;
mAlternateComposing = alternateComposing;
mManual = automatic;
mCycleSpeed = cycleSpeed;
}
public void setCursorPos(int cursorPos) {
mLastCursorPosition = cursorPos;
}
@Override
public void onFirstCycle() {
mInputView.setSymbolsVisible(true);
mInputView.setSymbols((ArrayList<String>) getList());
if (!mAlternateComposing) {
mInputConnection.setSelection(mLastCursorPosition - getElementAt(0).toString().length(),
mLastCursorPosition);
}
}
@Override
public void onCycle() {
mVibrator.vibrate(CYCLE_VIBRATION_TIME);
mInputView.setSelectedSymbol(getIndex());
if (mAlternateComposing) {
mInputConnection.deleteSurroundingText(getElementAt(0).toString().length(), 0);
mInputConnection.commitText(getCurrentElement().toString(), 1);
} else {
mInputConnection.setComposingText(getCurrentElement().toString(), 1);
}
if (!mManual) {
mHandler.postDelayed(this, mCycleSpeed);
}
}
}
@@ -0,0 +1,98 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.input;
import android.os.Handler;
import android.os.Looper;
import android.os.Vibrator;
import android.view.inputmethod.InputConnection;
import java.util.ArrayList;
import eur.cfpifpsi.titanboard.views.InputView;
// TODO: This looks kind of weird. Think about refactoring it.
// TODO: Cycling doesn't stop when pressing a button that isn't defined or something. Strange...
// Only happens in manual mode.
public class SymbolInputHandler {
private final InputView mInputView;
private final SymbolCycler mSymbolCycler;
private final Handler mHandler;
private int mLastSymbolKeyCode;
public SymbolInputHandler(InputView inputView, Vibrator vibrator) {
mHandler = new Handler(Looper.getMainLooper());
mInputView = inputView;
mSymbolCycler = new SymbolCycler(inputView, vibrator, mHandler);
}
public void setCursorPos(int value) {
mSymbolCycler.setCursorPos(value);
}
public void handleSymbolInput(InputConnection inputConnection, ArrayList<String> symbols,
int keyCode, boolean isAlternateComposingOn, boolean isManual,
int cycleSpeed) {
mSymbolCycler.setInputParameters(inputConnection, isAlternateComposingOn, isManual,
cycleSpeed);
if (isManual) {
cycleManual(inputConnection, symbols, keyCode, cycleSpeed);
} else {
cycleAutomatic(inputConnection, symbols, cycleSpeed);
}
}
public void cycleManual(InputConnection inputConnection, ArrayList<String> symbols,
int keyCode, int cycleSpeed) {
boolean isCycling = mHandler.hasMessages(0);
if (isCycling && keyCode == mLastSymbolKeyCode
&& mSymbolCycler.getList().size() > 1) {
mSymbolCycler.run();
} else if (symbols != null && !symbols.isEmpty()) {
finishCycling(inputConnection);
inputConnection.commitText(symbols.get(0), 1);
mSymbolCycler.prepare(symbols);
}
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(() -> finishCycling(inputConnection), cycleSpeed);
mLastSymbolKeyCode = keyCode;
}
public void cycleAutomatic(InputConnection inputConnection, ArrayList<String> symbols,
int cycleSpeed) {
if (symbols != null && !symbols.isEmpty()) {
inputConnection.commitText(symbols.get(0), 1);
if (symbols.size() > 1) {
mSymbolCycler.prepare(symbols);
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(mSymbolCycler, cycleSpeed);
}
}
}
public void finishCycling(InputConnection inputConnection) {
inputConnection.finishComposingText();
mHandler.removeCallbacksAndMessages(null);
mInputView.setSymbolsVisible(false);
}
}
@@ -0,0 +1,33 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.keymapping;
import java.util.ArrayList;
public class DeviceKeymap extends Keymap {
private final ArrayList<Integer> mMetaSymbolKeyCodes = new ArrayList<>(2);
public ArrayList<Integer> getMetaSymbolKeyCodes() {
return mMetaSymbolKeyCodes;
}
public void addMetaSymbolKeyCode(Integer keyCode) {
mMetaSymbolKeyCodes.add(keyCode);
}
}
@@ -0,0 +1,52 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.keymapping;
import java.util.ArrayList;
public final class KeySymbols {
private final static int INITIAL_SIZE = 6;
private final ArrayList<String> mBase = new ArrayList<>(INITIAL_SIZE);
private final ArrayList<String> mShift = new ArrayList<>(INITIAL_SIZE);
private final ArrayList<String> mAlt = new ArrayList<>(INITIAL_SIZE);
public ArrayList<String> getBase() {
return mBase;
}
public ArrayList<String> getShift() {
return mShift;
}
public ArrayList<String> getAlt() {
return mAlt;
}
public void addBase(String symbol) {
mBase.add(symbol);
}
public void addShift(String symbol) {
mShift.add(symbol);
}
public void addAlt(String symbol) {
mAlt.add(symbol);
}
}
@@ -0,0 +1,58 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.keymapping;
import java.util.ArrayList;
import java.util.HashMap;
import eur.cfpifpsi.titanboard.MetaKeyManager;
public class Keymap {
private final static int INITIAL_KEYMAP_SIZE = 64; // Enough for just about any QWERTY.
private final HashMap<Integer, KeySymbols> mMap = new HashMap<>(INITIAL_KEYMAP_SIZE);
public ArrayList<String> getSymbolsMeta(int keyCode, MetaKeyManager metaKeyManager) {
if (metaKeyManager.getSymbol().isActive()) {
return getSymbolsAlt(keyCode);
} else if (metaKeyManager.getShift().isActive()) {
return getSymbolsShift(keyCode);
} else {
return getSymbolsBase(keyCode);
}
}
public ArrayList<String> getSymbolsBase(int keyCode) {
KeySymbols key = mMap.get(keyCode);
return key == null ? null : key.getBase();
}
public ArrayList<String> getSymbolsAlt(int keyCode) {
KeySymbols key = mMap.get(keyCode);
return key == null ? null : key.getAlt();
}
public ArrayList<String> getSymbolsShift(int keyCode) {
KeySymbols key = mMap.get(keyCode);
return key == null ? null : key.getShift();
}
public void putKeySymbols(Integer key, KeySymbols value) {
mMap.put(key, value);
}
}
@@ -0,0 +1,219 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.keymapping;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
public enum KeymapLoader {
;
private static final String TAG_SYMBOLS = "symbols"; //NON-NLS
private static final String TAG_ATTACHED_SYMBOLS = "attached-symbols"; //NON-NLS
private static final String TAG_LANGUAGE_KEYMAP = "language-keymap"; //NON-NLS
private static final String TAG_DEVICE_KEYMAP = "device-keymap"; //NON-NLS
private static final String TAG_SYMBOL = "symbol"; //NON-NLS
private static final String TAG_BASE = "base"; //NON-NLS
private static final String TAG_ALT = "alt"; //NON-NLS
private static final String TAG_SHIFT = "shift"; //NON-NLS
private static final String TAG_SPACE = "space"; //NON-NLS
private static final String TAG_FOR_CODE = "for-code"; //NON-NLS
private static final String TAG_FOR_SYMBOL = "for-symbol"; //NON-NLS
private static final String TAG_CODE = "code"; //NON-NLS
private static final String TAG_LATCHED_CODE = "latched-code"; //NON-NLS
private static final String TAG_LOCKED_CODE = "locked-code"; //NON-NLS
private static final String TAG_ID = "id"; //NON-NLS
private static final String TAG_SYMBOLS_META_KEY = "symbol-meta-key-code"; //NON-NLS
public static void loadLanguageKeymaps(HashMap<String, LanguageKeymap> keymaps,
XmlPullParser parser) {
LanguageKeymap keymap = null;
int code = 0;
KeySymbols keySymbols = null;
String forSymbol = null;
ArrayList<String> attachedSymbols = null;
int eventType;
while (true) {
try {
eventType = parser.getEventType();
if (eventType == XmlPullParser.END_DOCUMENT) break;
if (eventType == XmlPullParser.START_TAG) {
switch (parser.getName()) {
case TAG_LANGUAGE_KEYMAP:
keymap = new LanguageKeymap();
break;
case TAG_CODE:
assert keymap != null : "<code> not preceded by <language-keymap>";
keymap.setCode(nextTextLowerApi(parser));
break;
case TAG_LATCHED_CODE:
assert keymap != null : "<latched-code> not preceded by <language-keymap>";
keymap.setLatchedCode(nextTextLowerApi(parser));
break;
case TAG_LOCKED_CODE:
assert keymap != null : "<locked-code> not preceded by <language-keymap>";
keymap.setLockedCode(nextTextLowerApi(parser));
break;
case TAG_SYMBOLS:
keySymbols = new KeySymbols();
break;
case TAG_FOR_CODE:
code = Integer.parseInt(nextTextLowerApi(parser));
break;
case TAG_BASE:
assert keySymbols != null : "<base> tag isn't preceded by <symbols>";
keySymbols.addBase(nextTextLowerApi(parser));
break;
case TAG_SHIFT:
assert keySymbols != null : "<shift> tag isn't preceded by <symbols>";
keySymbols.addShift(nextTextLowerApi(parser));
break;
case TAG_ATTACHED_SYMBOLS:
attachedSymbols = new ArrayList<>();
break;
case TAG_FOR_SYMBOL:
forSymbol = nextTextLowerApi(parser);
break;
case TAG_SYMBOL:
assert attachedSymbols != null : "<symbol> tag isn't preceded by <attached-symbols>";
attachedSymbols.add(nextTextLowerApi(parser));
break;
}
}
if (eventType == XmlPullParser.END_TAG) {
switch (parser.getName()) {
case TAG_LANGUAGE_KEYMAP:
assert keymap != null : "<language-keymap> tag not defined";
keymaps.put(keymap.getCode(), keymap);
break;
case TAG_SYMBOLS:
assert keymap != null : "<language-keymap> tag not defined";
keymap.putKeySymbols(code, keySymbols);
keySymbols = new KeySymbols();
break;
case TAG_ATTACHED_SYMBOLS:
assert keymap != null : "<language-keymap> tag not defined";
keymap.putAttachedSymbols(forSymbol, attachedSymbols);
break;
}
}
parser.next();
} catch (XmlPullParserException | IOException e) {
// TODO: Real error handling?
break;
}
}
}
public static void loadDeviceKeymaps(HashMap<String, DeviceKeymap> keymaps,
XmlPullParser parser) {
DeviceKeymap keymap = null;
int code = 0;
String id = null;
KeySymbols keySymbols = null;
int eventType;
while (true) {
try {
eventType = parser.getEventType();
if (eventType == XmlPullParser.END_DOCUMENT) break;
if (eventType == XmlPullParser.START_TAG) {
switch (parser.getName()) {
case TAG_DEVICE_KEYMAP:
keymap = new DeviceKeymap();
break;
case TAG_ID:
id = nextTextLowerApi(parser);
break;
case TAG_SYMBOLS_META_KEY:
assert keymap != null : "<device-keymap> tag not defined";
keymap.addMetaSymbolKeyCode(Integer.parseInt(nextTextLowerApi(parser)));
break;
case TAG_SYMBOLS:
keySymbols = new KeySymbols();
break;
case TAG_FOR_CODE:
code = Integer.parseInt(nextTextLowerApi(parser));
break;
case TAG_BASE:
assert keySymbols != null : "<base> tag isn't preceded by <symbols>";
keySymbols.addBase(nextTextLowerApi(parser));
break;
case TAG_SHIFT:
assert keySymbols != null : "<shift> tag isn't preceded by <symbols>";
keySymbols.addShift(nextTextLowerApi(parser));
break;
case TAG_ALT:
assert keySymbols != null : "<alt> tag isn't preceded by <symbols>";
keySymbols.addAlt(nextTextLowerApi(parser));
break;
case TAG_SPACE:
assert keySymbols != null : "</space> tag isn't preceded by <symbols>";
keySymbols.addBase(" ");
keySymbols.addShift(" ");
keySymbols.addAlt(" ");
break;
}
}
if (eventType == XmlPullParser.END_TAG) {
switch (parser.getName()) {
case TAG_DEVICE_KEYMAP:
keymaps.put(id, keymap);
break;
case TAG_SYMBOLS:
assert keymap != null : "<device-keymap> tag not defined";
keymap.putKeySymbols(code, keySymbols);
keySymbols = new KeySymbols();
break;
}
}
parser.next();
} catch (XmlPullParserException | IOException e) {
// TODO: Real error handling?
break;
}
}
}
// Taken from the javadoc of XmlPullParser.nextText
public static String nextTextLowerApi(XmlPullParser parser)
throws XmlPullParserException, IOException {
String text = parser.nextText();
if (parser.getEventType() != XmlPullParser.END_TAG) {
parser.next();
}
return text;
}
}
@@ -0,0 +1,114 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.keymapping;
import android.content.Context;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import eur.cfpifpsi.titanboard.R;
import eur.cfpifpsi.titanboard.MetaKeyManager;
public class KeymapManager {
private final Context mContext;
private final HashMap<String, DeviceKeymap> mDeviceKeymaps;
private final HashMap<String, LanguageKeymap> mLanguageKeymaps;
private LanguageKeymap mCurrentLanguageKeymap;
public KeymapManager(Context context) {
mContext = context;
mDeviceKeymaps = new HashMap<>(2);
mLanguageKeymaps = new HashMap<>(context.getResources()
.getStringArray(R.array.preferences_languages).length);
KeymapLoader.loadDeviceKeymaps(mDeviceKeymaps,
context.getResources().getXml(R.xml.device_keymaps));
KeymapLoader.loadLanguageKeymaps(mLanguageKeymaps,
context.getResources().getXml(R.xml.language_keymaps));
}
public ArrayList<String> getCurrentKeySymbolsMeta(int keyCode,
String device,
MetaKeyManager metaKeyManager) {
Keymap deviceKeymap = mDeviceKeymaps.get(device);
ArrayList<String> deviceSymbols = deviceKeymap != null
? deviceKeymap.getSymbolsMeta(keyCode, metaKeyManager) : new ArrayList<>();
ArrayList<String> languageSymbols = mCurrentLanguageKeymap.getSymbolsMeta(keyCode, metaKeyManager);
ArrayList<String> combinedSymbols = new ArrayList<>();
if (deviceSymbols != null) {
combinedSymbols.addAll(deviceSymbols);
}
if (languageSymbols != null) {
combinedSymbols.addAll(languageSymbols);
}
// We NEED to do this as not to iterate and modify combinedSymbols at the same time.
ArrayList<String> finalSymbols = new ArrayList<>(combinedSymbols);
for (String symbol : combinedSymbols) {
ArrayList<String> attachedSymbols = mCurrentLanguageKeymap.getAttachedSymbols(symbol);
if (attachedSymbols != null) {
finalSymbols.addAll(attachedSymbols);
}
}
return finalSymbols;
}
public void adjustCurrentKeymap(List<String> selectedKeymaps) {
if (!selectedKeymaps.contains(mCurrentLanguageKeymap.getCode())) {
nextKeymap(selectedKeymaps);
}
}
public void nextKeymap(List<String> selectedKeymaps) {
if (!selectedKeymaps.isEmpty() && mCurrentLanguageKeymap != null) {
int index = selectedKeymaps.indexOf(mCurrentLanguageKeymap.getCode());
index++;
if (index == selectedKeymaps.size()) {
index = 0;
}
mCurrentLanguageKeymap = mLanguageKeymaps.get(selectedKeymaps.get(index));
} else {
mCurrentLanguageKeymap =
mLanguageKeymaps.get(mContext.getResources().getString(R.string.language_default));
}
}
public ArrayList<Integer> getDeviceMetaSymbolKeyCodes(String device) {
return mDeviceKeymaps.get(device).getMetaSymbolKeyCodes();
}
public String getCurrentCode() {
return mCurrentLanguageKeymap.getCode();
}
public String getCurrentLatchedCode() {
return mCurrentLanguageKeymap.getLatchedCode();
}
public String getCurrentLockedCode() {
return mCurrentLanguageKeymap.getLockedCode();
}
}
@@ -0,0 +1,61 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.keymapping;
import java.util.ArrayList;
import java.util.HashMap;
public final class LanguageKeymap extends Keymap {
private final HashMap<String, ArrayList<String>> mAttachedSymbols = new HashMap<>();
private String mCode;
private String mLatchedCode;
private String mLockedCode;
public String getCode() {
return mCode;
}
public void setCode(String value) {
mCode = value;
}
public String getLatchedCode() {
return mLatchedCode;
}
public void setLatchedCode(String value) {
mLatchedCode = value;
}
public String getLockedCode() {
return mLockedCode;
}
public void setLockedCode(String value) {
mLockedCode = value;
}
public ArrayList<String> getAttachedSymbols(String key) {
return mAttachedSymbols.get(key);
}
public void putAttachedSymbols(String key, ArrayList<String> value) {
mAttachedSymbols.put(key, value);
}
}
@@ -0,0 +1,94 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.views;
import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import java.util.ArrayList;
import eur.cfpifpsi.titanboard.ClipManager;
import eur.cfpifpsi.titanboard.R;
import eur.cfpifpsi.titanboard.TitanboardIME;
public class ClipboardView extends ScrollView {
private final TitanboardIME mTitanboardIME;
private final LinearLayout.LayoutParams mClipLayout;
private ClipManager mClipManager;
private LinearLayout mClipList;
public ClipboardView(Context context, AttributeSet attrs) {
super(context, attrs);
mTitanboardIME = (TitanboardIME) context;
mClipLayout = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}
public void setDependencies(ClipManager clipManager) {
mClipManager = clipManager;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mClipList = findViewById(R.id.clip_list);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void setClips(ArrayList<? extends ClipData.Item> items) {
int size = items.size();
for (int i = 0; i < size; ++i) {
ClipData.Item item = items.get(i);
LinearLayout clipView = (LinearLayout) mClipList.getChildAt(i);
if (clipView == null) {
clipView = (LinearLayout) inflate(getContext(), R.layout.clip, null);
clipView.setTag(i);
clipView.setLayoutParams(mClipLayout);
mClipList.addView(clipView);
}
Button pasteButton = clipView.findViewById(R.id.paste_button);
pasteButton.setText(item.getText());
pasteButton.setOnClickListener(button -> pasteClip(item));
LinearLayout finalClipView = clipView;
clipView.findViewById(R.id.button_delete)
.setOnClickListener(button -> deleteClip(finalClipView, item));
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void pasteClip(ClipData.Item item) {
mTitanboardIME.getCurrentInputConnection().commitText(item.getText(), 1);
}
private void deleteClip(LinearLayout clipView, ClipData.Item item) {
mClipList.removeView(clipView);
mClipManager.removeItem(item);
}
}
@@ -0,0 +1,166 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.views;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageButton;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import java.util.ArrayList;
import eur.cfpifpsi.titanboard.ClipManager;
import eur.cfpifpsi.titanboard.PreferenceHolder;
import eur.cfpifpsi.titanboard.R;
import eur.cfpifpsi.titanboard.SettingsActivity;
import eur.cfpifpsi.titanboard.TitanboardIME;
import eur.cfpifpsi.titanboard.MetaKeyManager;
import eur.cfpifpsi.titanboard.keymapping.KeymapManager;
public final class InputView extends RelativeLayout {
private final int[] buttonStateToggled = {R.attr.state_toggled};
private final TitanboardIME mTitanboardIME;
private KeymapManager mKeymapManager;
private PreferenceHolder mPreferenceHolder;
private MetaKeyManager mMetaKeyManager;
private ClipManager mClipManager;
private PopupWindow mPopup;
private StateButton mStateButton;
private ImageButton mClipboardButton;
private ImageButton mTweaksButton;
private SymbolsView mSymbolsView;
private ClipboardView mClipboardView;
private TweaksView mTweaksView;
public InputView(Context context, AttributeSet attrs) {
super(context, attrs);
mTitanboardIME = (TitanboardIME) context;
}
public void setDependencies(KeymapManager keymapManager, MetaKeyManager metaKeyManager,
PreferenceHolder preferenceHolder, ClipManager clipManager) {
mKeymapManager = keymapManager;
mMetaKeyManager = metaKeyManager;
mPreferenceHolder = preferenceHolder;
mClipManager = clipManager;
mClipboardView.setDependencies(clipManager);
mTweaksView.setDependencies(preferenceHolder);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mStateButton = findViewById(R.id.state_button);
mClipboardButton = findViewById(R.id.clipboard_button);
mTweaksButton = findViewById(R.id.tweaks_button);
mSymbolsView = findViewById(R.id.symbols_view);
mClipboardView = (ClipboardView) inflate(getContext(), R.layout.clipboard, null);
mTweaksView = (TweaksView) inflate(getContext(), R.layout.tweaks, null);
mPopup = new PopupWindow(mClipboardView,
getResources().getDimensionPixelSize(R.dimen.popup_width),
getResources().getDimensionPixelSize(R.dimen.clipboard_popup_height), true);
mPopup.setOnDismissListener(() -> {
mClipboardButton.setImageState(null, true);
mTweaksButton.setImageState(null, true);
});
mStateButton.setOnClickListener(button -> {
if (mMetaKeyManager.getSymbol().isActive()) {
mMetaKeyManager.getSymbol().resetState();
} else {
mKeymapManager.nextKeymap(mPreferenceHolder.getSelectedKeymaps());
}
updateStateText();
});
mStateButton.setOnLongClickListener(button -> {
Intent intent = new Intent(getContext(), SettingsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
// For some reason, this vibrates by default, so we don't need to call the vibrator.
return true;
});
mClipboardButton.setOnClickListener(button -> showPopup(mClipboardButton, mClipboardView,
R.dimen.clipboard_popup_height));
mClipboardButton.setOnLongClickListener(button -> {
// On a long click, we just send the latest clip (if any).
if (!mClipManager.isEmpty()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mTitanboardIME.getCurrentInputConnection()
.commitText(mClipManager.getItemAt(0).getText(), 1);
}
return true;
});
mTweaksButton.setOnClickListener(button -> showPopup(mTweaksButton, mTweaksView,
R.dimen.tweaks_popup_height));
}
public void dismissPopup() {
mPopup.dismiss();
}
private void showPopup(ImageButton button, View contentView, int popupHeight) {
mPopup.setContentView(contentView);
mPopup.setHeight(getResources().getDimensionPixelSize(popupHeight));
mPopup.showAsDropDown(button,
-(mPopup.getWidth() - button.getWidth()) / 2, 0);
button.setImageState(buttonStateToggled, true);
}
public void updateStateText() {
mStateButton.updateStateText(mMetaKeyManager.getShift(), mMetaKeyManager.getSymbol(),
mKeymapManager.getCurrentCode(), mKeymapManager.getCurrentLatchedCode(),
mKeymapManager.getCurrentLockedCode());
}
public void updateTweaks() {
mTweaksView.updateTweaks();
}
public void setClips(ArrayList<? extends ClipData.Item> items) {
mClipboardView.setClips(items);
}
public void setSymbolsVisible(boolean state) {
mSymbolsView.setSymbolsVisible(state);
}
public void setSymbols(ArrayList<String> strings) {
mSymbolsView.setSymbols(strings);
}
public void setSelectedSymbol(int index) {
mSymbolsView.setSelectedSymbol(index);
}
}
@@ -0,0 +1,54 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.views;
import android.content.Context;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.Button;
import eur.cfpifpsi.titanboard.R;
import eur.cfpifpsi.titanboard.MetaKey;
public class StateButton extends Button {
public StateButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void updateStateText(MetaKey shift, MetaKey alt, String code, String latchedCode,
String lockedCode) {
setPaintFlags(getPaintFlags() & ~(Paint.UNDERLINE_TEXT_FLAG));
if (alt.isLocked() || shift.isLocked()) {
setPaintFlags(getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
}
if (alt.isPressed() || alt.isLocked()) {
setText(R.string.state_button_alt_locked);
} else if (alt.isLatched()) {
setText(R.string.state_button_alt_latched);
} else if (shift.isPressed() || shift.isLocked()) {
setText(lockedCode);
} else if (shift.isLatched()) {
setText(latchedCode);
} else {
setText(code);
}
}
}
@@ -0,0 +1,106 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.views;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
import eur.cfpifpsi.titanboard.R;
public class SymbolsView extends HorizontalScrollView {
private final LinearLayout.LayoutParams mSymbolLayout;
private LinearLayout mSymbolList;
public SymbolsView(Context context, AttributeSet attrs) {
super(context, attrs);
mSymbolLayout = new LinearLayout.LayoutParams(
getResources().getDimensionPixelSize(R.dimen.symbol_width),
ViewGroup.LayoutParams.MATCH_PARENT, 0);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mSymbolList = findViewById(R.id.symbol_list);
}
public void setSymbolsVisible(boolean state) {
mSymbolList.setVisibility(state ? VISIBLE : GONE);
}
/**
* @noinspection MethodWithMultipleLoops
*/
public void setSymbols(ArrayList<String> strings) {
// Once we set the symbols, we also scroll back to the very front. This is in case we have
// previously used a symbol that's far away, so that the user doesn't get a visual shock
// since setSelectedSymbol would smoothScroll to the beginning.
scrollTo(0, 0);
int size = strings.size();
for (int i = 0; i < size; ++i) {
TextView tv = (TextView) mSymbolList.getChildAt(i);
if (tv == null) {
tv = (TextView) inflate(getContext(), R.layout.symbol, null);
tv.setTag(i);
tv.setLayoutParams(mSymbolLayout);
mSymbolList.addView(tv);
}
tv.setText(strings.get(i));
tv.setVisibility(VISIBLE);
}
int childCount = mSymbolList.getChildCount();
for (int i = size; i < childCount; ++i) {
mSymbolList.getChildAt(i).setVisibility(GONE);
}
}
public void setSelectedSymbol(int index) {
int childCount = mSymbolList.getChildCount();
for (int i = 0; i < childCount; ++i) {
TextView tv = (TextView) mSymbolList.getChildAt(i);
if (i == index) {
tv.setPressed(true);
tv.setTypeface(null, Typeface.BOLD);
// This, if the symbol list is larger than the scroll view, scrolls it just to show
// the selected symbol at the very end. It isn't really necessary, as it's a rarity
// that a key has more than five symbols, but perhaps someone will want to add a
// keymap that has a lot of symbols under one key, and also this is a safeguard for
// smaller screen sizes.
smoothScrollTo(tv.getRight()
+ (getResources().getDimensionPixelSize(R.dimen.default_space) << 1)
- getRight(), 0);
} else {
tv.setPressed(false);
tv.setTypeface(null, Typeface.NORMAL);
}
}
}
}
@@ -0,0 +1,77 @@
/*
* An input method for physical keyboard phones
* Copyright (C) 2024 Emil Kosz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package eur.cfpifpsi.titanboard.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import eur.cfpifpsi.titanboard.PreferenceHolder;
import eur.cfpifpsi.titanboard.R;
import eur.cfpifpsi.titanboard.TitanboardIME;
public class TweaksView extends LinearLayout {
private final TitanboardIME mTitanboardIME;
private PreferenceHolder mPreferenceHolder;
private CheckBox mComposingCheckbox;
private CheckBox mEnterCheckbox;
public TweaksView(Context context, AttributeSet attrs) {
super(context, attrs);
mTitanboardIME = (TitanboardIME) context;
}
public void setDependencies(PreferenceHolder preferenceHolder) {
mPreferenceHolder = preferenceHolder;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mComposingCheckbox = findViewById(R.id.composing_checkbox);
mEnterCheckbox = findViewById(R.id.enter_checkbox);
mComposingCheckbox.setOnCheckedChangeListener((button, state) -> {
EditorInfo editorInfo = mTitanboardIME.getCurrentInputEditorInfo();
if (editorInfo != null) {
mPreferenceHolder.setAlternateComposing(editorInfo.packageName, state);
}
});
mEnterCheckbox.setOnCheckedChangeListener((button, state) -> {
EditorInfo editorInfo = mTitanboardIME.getCurrentInputEditorInfo();
if (editorInfo != null) {
mPreferenceHolder.setEnterAlwaysSends(editorInfo.packageName, state);
}
});
}
public void updateTweaks() {
EditorInfo editorInfo = mTitanboardIME.getCurrentInputEditorInfo();
if (editorInfo != null) {
mComposingCheckbox.setChecked(mPreferenceHolder.isAlternateComposingOn(editorInfo.packageName));
mEnterCheckbox.setChecked(mPreferenceHolder.isEnterAlwaysSendsOn(editorInfo.packageName));
}
}
}
+1 -2
View File
@@ -16,8 +16,7 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item app:state_toggled="true">
<shape android:shape="rectangle">
<solid android:color="@color/button_toggled" />
+88
View File
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ An input method for physical keyboard phones
~ Copyright (C) 2024 Emil Kosz
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- <item android:top="11dp"-->
<!-- android:bottom="11dp"-->
<!-- android:right="13dp"-->
<!-- android:left="13dp">-->
<!-- <shape android:shape="rectangle">-->
<!-- <solid android:color="@android:color/darker_gray" />-->
<!-- </shape>-->
<!-- </item>-->
<item
android:bottom="11dp"
android:left="15dp"
android:right="31dp"
android:top="11dp">
<shape android:shape="rectangle">
<solid android:color="@color/text_color" />
<corners android:radius="2dp" />
</shape>
</item>
<item
android:bottom="21dp"
android:left="13dp"
android:right="29dp"
android:top="21dp">
<shape android:shape="oval">
<solid android:color="@color/text_color" />
</shape>
</item>
<item
android:bottom="11dp"
android:left="31dp"
android:right="15dp"
android:top="11dp">
<shape android:shape="rectangle">
<solid android:color="@color/text_color" />
<corners android:radius="2dp" />
</shape>
</item>
<item
android:bottom="15dp"
android:left="29dp"
android:right="13dp"
android:top="27dp">
<shape android:shape="oval">
<solid android:color="@color/text_color" />
</shape>
</item>
<item
android:bottom="11dp"
android:left="23dp"
android:right="23dp"
android:top="11dp">
<shape android:shape="rectangle">
<solid android:color="@color/text_color" />
<corners android:radius="2dp" />
</shape>
</item>
<item
android:bottom="27dp"
android:left="21dp"
android:right="21dp"
android:top="15dp">
<shape android:shape="oval">
<solid android:color="@color/text_color" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ An input method for physical keyboard phones
~ Copyright (C) 2024 Emil Kosz
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/manual_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/default_space"
android:text="@string/app_name"
android:textColor="@android:color/primary_text_light"
android:textSize="@dimen/default_text_size" />
</ScrollView>
+2 -2
View File
@@ -16,7 +16,7 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<eur.cfpifpsi.titanboard.views.ClipboardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -29,4 +29,4 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
</eur.cfpifpsi.titanboard.views.ClipboardView>
+20 -8
View File
@@ -16,7 +16,7 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<eur.cfpifpsi.titanboard.InputView xmlns:android="http://schemas.android.com/apk/res/android"
<eur.cfpifpsi.titanboard.views.InputView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -24,14 +24,15 @@
android:background="@color/background"
tools:ignore="Overdraw">
<HorizontalScrollView
android:id="@+id/symbol_list_container"
<eur.cfpifpsi.titanboard.views.SymbolsView
android:id="@+id/symbols_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignTop="@id/clipboard_button"
android:layout_alignBottom="@id/clipboard_button"
android:layout_toStartOf="@+id/clipboard_button"
android:layout_toLeftOf="@id/clipboard_button"
android:layout_toStartOf="@id/tweaks_button"
android:layout_toLeftOf="@id/tweaks_button"
android:contentDescription="@string/symbols_view_description"
android:scrollbarThumbHorizontal="@color/text_color">
<LinearLayout
@@ -39,7 +40,7 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal" />
</HorizontalScrollView>
</eur.cfpifpsi.titanboard.views.SymbolsView>
<ImageButton
android:id="@+id/clipboard_button"
@@ -52,7 +53,17 @@
android:src="@drawable/ic_clipboard"
tools:ignore="UnusedAttribute" />
<Button
<ImageButton
android:id="@+id/tweaks_button"
android:layout_width="@dimen/button_size"
android:layout_height="@dimen/button_size"
android:layout_toStartOf="@id/clipboard_button"
android:layout_toLeftOf="@id/clipboard_button"
android:background="@drawable/button"
android:contentDescription="@string/tweaks_button_description"
android:src="@drawable/ic_faders" />
<eur.cfpifpsi.titanboard.views.StateButton
android:id="@+id/state_button"
android:layout_width="@dimen/button_size"
android:layout_height="@dimen/button_size"
@@ -60,7 +71,8 @@
android:layout_alignParentRight="true"
android:background="@drawable/button_state"
android:text="@string/state_button_default"
android:textAllCaps="false"
android:textColor="@color/background"
android:textSize="@dimen/default_text_size" />
</eur.cfpifpsi.titanboard.InputView>
</eur.cfpifpsi.titanboard.views.InputView>
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ An input method for physical keyboard phones
~ Copyright (C) 2024 Emil Kosz
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<eur.cfpifpsi.titanboard.views.TweaksView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/button_toggled"
android:orientation="vertical">
<CheckBox
android:id="@+id/composing_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/button_size"
android:padding="@dimen/default_space"
android:text="@string/tweaks_label_alternate_composing"
android:textColor="@color/text_color"
android:textSize="@dimen/default_text_size" />
<CheckBox
android:id="@+id/enter_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/button_size"
android:padding="@dimen/default_space"
android:text="@string/tweaks_label_enter_always_sends"
android:textColor="@color/text_color"
android:textSize="@dimen/default_text_size" />
</eur.cfpifpsi.titanboard.views.TweaksView>
+17 -3
View File
@@ -17,7 +17,7 @@
-->
<resources>
<string-array name="preferences_keymap_labels">
<string-array name="preferences_language_labels">
<item>English</item>
<item>German</item>
<item>French</item>
@@ -27,9 +27,11 @@
<item>Russian (Phonetic)</item>
<item>Greek (diacritics under Q)</item>
<item>Arabic (Phonetic)</item>
<item>Hebrew</item>
<item>Czech</item>
</string-array>
<string-array name="preferences_keymaps">
<string-array name="preferences_languages">
<item>en</item>
<item>de</item>
<item>fr</item>
@@ -39,13 +41,25 @@
<item>ru</item>
<item>el</item>
<item>ar</item>
<item>he</item>
<item>cz</item>
</string-array>
<string-array name="preferences_keymap_defaults">
<string-array name="preferences_language_defaults">
<item>en</item>
<item>ru</item>
</string-array>
<string-array name="preferences_device_labels">
<item>Unihertz Titan</item>
<item>Unihertz Titan Pocket/Slim</item>
</string-array>
<string-array name="preferences_devices">
<item>titan</item>
<item>titan_pocket_slim</item>
</string-array>
<string-array name="preferences_cycle_speed_labels">
<item>200 ms</item>
<item>300 ms</item>
+2 -1
View File
@@ -21,8 +21,9 @@
<dimen name="button_size">48dp</dimen>
<dimen name="symbol_width">40dp</dimen>
<dimen name="clipboard_popup_width">256dp</dimen>
<dimen name="popup_width">256dp</dimen>
<dimen name="clipboard_popup_height">256dp</dimen>
<dimen name="tweaks_popup_height">96dp</dimen>
<dimen name="default_text_size">16sp</dimen>
<dimen name="symbol_text_size">18sp</dimen>
+145 -6
View File
@@ -19,8 +19,10 @@
<resources>
<string name="app_name">Titanboard</string>
<string name="preferences_name">Titanboard preferences</string>
<string name="manual_name">Titanboard manual</string>
<string name="keymap_default">en</string>
<string name="device_default">titan</string>
<string name="language_default">en</string>
<string name="cycle_speed_default">500</string>
<string name="clipboard_size_default">5</string>
@@ -32,24 +34,161 @@
<string name="clipboard_button_description">
Open clipboard or paste first item on long press
</string>
<string name="tweaks_button_description">
Open per-app tweaks window
</string>
<string name="symbols_view_description">
List visible when cycling through symbols
</string>
<string name="clip_default_text">Clip</string>
<string name="preferences_category_keyboard">Keyboard</string>
<string name="preferences_category_functionality">Functionality</string>
<string name="preferences_category_other">Other</string>
<string name="preferences_label_cycle_manual">
Cycle symbols by consecutively pressing a key
</string>
<string name="preferences_label_cycle_speed">Symbol cycling speed/timeout</string>
<string name="preferences_label_clipboard_size">Clipboard size</string>
<string name="preferences_label_keymaps">Languages</string>
<string name="preferences_label_copyright_info">
Copyleft 🄯 Emil Kosz, 2024. License: GPLv3
</string>
<string name="preferences_label_device">Device</string>
<string name="preferences_label_languages">Languages</string>
<string name="preferences_label_user_manual">User manual</string>
<string name="preferences_summary_cycle_manual">
Overrides the default behaviour of long pressing to cycle
</string>
<string name="preferences_summary_copyright_info">
Copyleft 🄯 Emil Kosz, 2024. License: GPLv3
</string>
<!-- Even though some may not make sense, leave them be as not to break users' configurations
when updating -->
<string name="preferences_key_cycle_manual">cycle_manual</string>
<string name="preferences_key_keymaps">selected_keymaps</string>
<string name="preferences_key_languages">selected_keymaps</string>
<string name="preferences_key_device">device</string>
<string name="preferences_key_cycle_speed">cycle_speed</string>
<string name="preferences_key_clipboard_size">clipboard_size</string>
<string name="tweaks_label_alternate_composing">Alternate composing method</string>
<string name="tweaks_label_enter_always_sends">Enter always sends</string>
<string name="tweaks_key_alternate_composing">alternate_composing_for_</string>
<string name="tweaks_key_enter_always_sends">enter_always_sends_for_</string>
<string name="user_manual_html">
<![CDATA[
<h1>Introduction</h1>
<p>
Thank you for trying out Titanboard! This keyboard program is designed to make your
experience with physical keyboards as pleasant and powerful as possible. It was designed
to be intuitive, and work exactly as you would expect it to. This means that pressing
any key, with or without modifiers like Alt or Shift, will show the corresponding
character on the screen. For example, on the Unihertz Titan, pressing R will input
&quot;r&quot;, pressing Alt+R will input &quot;-&quot;, Shift+R will input
&quot;R&quot;. Whatever you see on your physical keyboard will show up on the screen.
</p>
<p>
However, on most physical keyboard devices, this makes it very hard to input characters
which aren&apos;t on it. For example, on the Unihertz Titan, it isn&apos;t possible to
input the &quot;&amp;&quot; symbol. The stock keyboard application solves this by using
an additional visual keyboard interface, but I think that misses the point. Instead,
Titanboard allows every symbol on the physical keyboard, even those with modifiers, to
have extra symbols. These are accessed by <i>cycling</i>.
</p>
<p>
<i>Cycling</i> is basically a method for accessing the extra symbols under a given key. By
default, this method is long pressing a key. For example, on the Unihertz Titan, the
) key (Alt+Y) has four symbols: &quot;)&quot;, &quot;&gt;&quot;, &quot;]&quot;,
&quot;}&quot;. The first is inputted by simply pressing Alt, then Y (or Alt and Y). The
rest can be accessed by pressing Alt (or holding), then pressing and holding Y. This
will start switching through the aforementioned characters at the rate specified in the
preferences (by default 500 ms).
</p>
<p>
There is an alternate cycling method which can be enabled under the &quot;Cycle symbols
by consecutively pressing a key&quot; setting in the preferences. This is
self-explanatory. Instead of long pressing like in the aforementioned example, you
instead quickly press the key several times. So, if I want to input &quot;]&quot;, I
press Alt, then I press Y three times. This is very good for fast typing.
</p>
<p>
If you tried the above, you might have noticed that the Alt or Shift keys can be
&quot;locked&quot; (or latched). This is the same behaviour like in regular virtual
keyboards Android. Simply put, pressing a modifier key once puts it into a latched
state, which means the next key pressed will be uppercased or be the alternative symbol.
You can also lock a modifier key by pressing it twice in a quick fashion. Otherwise,
modifier keys work as expected.
</p>
<h1>User Interface</h1>
<p>
The user interface contains four elements: the symbol list, the tweaks button (three
lines with circles mimicking fader controls), the clipboard button (a clipboard icon),
and the state/language button (the white square with letters). The first, if not
cycling, contains nothing and looks like a blank background. If you start typing in
extra symbols, then the available symbols and the currently selected one are shown.
</p>
<p>
The tweaks button, when pressed, will show a window with app-specific tweaks. These fix
often encountered problems that happen in interactions between apps and physical
keyboards. These settings can be set and are saved for every app. The &quot;Alternate
composing method&quot; tweak is useful for applications like a terminal, which do not
support composing characters. You&apos;ll know an application doesn&apos;t support it
when cycling symbols feels off, and extra characters are inputted other than the one you
desire. The &quot;Enter always sends&quot; tweak makes it so pressing the Enter key
always tries to send a message instead of inserting a new line.
</p>
<p>
The clipboard button, when pressed, shows the clipboard with all the recent copied
stuff. Each clip has a red trashcan next to it which deletes it from the list. Clicking
on the clip itself sends it to the application. Long pressing the clipboard button sends
the most recent clip to the application.
</p>
<p>
The language button shows the current language being used, in a two letter code
(English is en/En/EN, Arabic is ar/Ar/AR, etc.). It also shows the state of the modifier
keys: if there are no modifier keys latched, locked, or pressed, the language code is in
all lowercase — &quot;en&quot;. If Shift is latched, the first letter is uppercased —
&quot;En&quot;. If it is pressed, then the code is all uppercase — &quot;EN&quot;. If it
is locked, then it is all uppercase and underlined. The case is similar for the Alt key;
if Alt is latched, the button displays &quot;#?&quot;, if it is pressed, then
&quot;#!&quot;, and if locked, then the same as pressed but underlined.
</p>
<p>
Pressing the language button switches to the next language. Long pressing the language
button opens the keyboard settings.
</p>
<h1>Keymap quirks</h1>
<p>
Due to the limited nature of QWERTY phones, which rarely boast a full QWERTY (and for
that kind of phone this program is designed for), regular language keymaps cannot be
applied. However, Titanboard tries to be as close to them as possible anyway, unless
it is a naive feat. Here is a list of quirks in language keymaps, so that you can get
used to them:
</p>
<ul>
<li>All keymaps: language-specific currencies and punctuation marks are added onto
preexisting characters on the keyboard; for example, German quotes are under the
&quot; key.</li>
<li>English: no quirks at all</li>
<li>German: QWERTY, umlaut characters are under their corresponding characters
(ä under a, ß under s, etc.)</li>
<li>French: same as above</li>
<li>Italian: same as above</li>
<li>Spanish: same as above, quotation and exclamation mark keys have additional Spanish
opening types</li>
<li>Polish: same as above, except for the X key having Ź for easier typing</li>
<li>Russian: phonetic variant based on the Russian Phonetic Keyboard 2.0</li>
<li>Greek: same as a regular QWERTY layout, except the Q key has additional diacritics,
and all common diacriticized letters appear under their corresponding letters</li>
<li>Arabic: handmade layout, sort-of phonetic, keys with Shift are diacritics</li>
<li>Hebrew: almost like the QWERTY, except Alt-Gr symbols are moved to Shift</li>
<li>Czech: like all the others above</li>
<ul>
<h1>Miscellaneous</h1>
<p>
Regardless of the Enter key tweak, Shift+Enter will always input a new line.
</p>
]]>
</string>
</resources>
+300
View File
@@ -0,0 +1,300 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ An input method for physical keyboard phones
~ Copyright (C) 2024 Emil Kosz
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<device-keymap-list>
<!-- This is the exemplary device keymap. -->
<device-keymap>
<!-- This is used to differentiate keymaps. It can be any string, really, but keep it
short and descriptive. -->
<id>titan</id>
<!-- These are the keycodes for the key that's responsible for accessing alternate symbols.
On BlackBerries, this is the sym key, here on the Titan, it's the alt key. -->
<symbol-meta-key-code>57</symbol-meta-key-code> <!-- Left alt -->
<symbol-meta-key-code>58</symbol-meta-key-code> <!-- Right alt -->
<!-- Symbols, apart from the keycode and alt values, can also contain <base> and <shift>
values, which can be used for phones that have extra keys, such as a dot key. It's
useful that, like in this example, you add more than the default single alternate key,
so the user can type in more symbols that don't exist on the physical keyboard. Make
sure to group them in a sensible way, for example, the : key having the ; symbol also, etc.
-->
<symbols>
<for-code>45</for-code> <!-- Q -->
<alt>:</alt>
<alt>;</alt>
</symbols>
<symbols>
<for-code>51</for-code> <!-- W -->
<alt>/</alt>
<alt>\</alt>
<alt>|</alt>
</symbols>
<symbols>
<for-code>33</for-code> <!-- E -->
<alt>_</alt>
<alt>~</alt>
</symbols>
<symbols>
<for-code>46</for-code> <!-- R -->
<alt>-</alt>
<alt>–</alt>
<alt>—</alt>
</symbols>
<symbols>
<for-code>48</for-code> <!-- T -->
<alt>(</alt>
<alt>&lt;</alt>
<alt>[</alt>
<alt>{</alt>
</symbols>
<symbols>
<for-code>53</for-code> <!-- Y -->
<alt>)</alt>
<alt>&gt;</alt>
<alt>]</alt>
<alt>}</alt>
</symbols>
<symbols>
<for-code>49</for-code> <!-- U -->
<alt>1</alt>
</symbols>
<symbols>
<for-code>37</for-code> <!-- I -->
<alt>2</alt>
</symbols>
<symbols>
<for-code>43</for-code> <!-- O -->
<alt>3</alt>
</symbols>
<symbols>
<for-code>44</for-code> <!-- P -->
<alt>0</alt>
</symbols>
<symbols>
<for-code>29</for-code> <!-- A -->
<alt>@</alt>
</symbols>
<symbols>
<for-code>47</for-code> <!-- S -->
<alt>'</alt>
</symbols>
<symbols>
<for-code>32</for-code> <!-- D -->
<alt>&quot;</alt>
</symbols>
<symbols>
<for-code>34</for-code> <!-- F -->
<alt>+</alt>
<alt>=</alt>
<alt>%</alt>
</symbols>
<symbols>
<for-code>35</for-code> <!-- G -->
<alt>*</alt>
<alt>^</alt>
<alt>`</alt>
</symbols>
<symbols>
<for-code>36</for-code> <!-- H -->
<alt>#</alt>
<alt>$</alt>
<alt>€</alt>
</symbols>
<symbols>
<for-code>38</for-code> <!-- J -->
<alt>4</alt>
</symbols>
<symbols>
<for-code>39</for-code> <!-- K -->
<alt>5</alt>
</symbols>
<symbols>
<for-code>40</for-code> <!-- L -->
<alt>6</alt>
</symbols>
<symbols>
<for-code>54</for-code> <!-- Z -->
<alt>!</alt>
</symbols>
<symbols>
<for-code>52</for-code> <!-- X -->
<alt>?</alt>
</symbols>
<symbols>
<for-code>31</for-code> <!-- C -->
<alt>,</alt>
<alt>&amp;</alt>
</symbols>
<symbols>
<for-code>50</for-code> <!-- V -->
<alt>.</alt>
<alt>…</alt>
</symbols>
<symbols>
<for-code>30</for-code> <!-- B -->
<alt>7</alt>
</symbols>
<symbols>
<for-code>42</for-code> <!-- N -->
<alt>8</alt>
</symbols>
<symbols>
<for-code>41</for-code> <!-- M -->
<alt>9</alt>
</symbols>
<symbols>
<for-code>62</for-code> <!-- Space -->
<!-- Yeah, a special tag, whatever. It still makes the code simpler... -->
<space />
</symbols>
</device-keymap>
<device-keymap>
<id>titan_pocket_slim</id>
<symbol-meta-key-code>57</symbol-meta-key-code> <!-- Left alt -->
<symbol-meta-key-code>58</symbol-meta-key-code> <!-- Right alt -->
<symbols>
<for-code>45</for-code> <!-- Q -->
<alt>0</alt>
</symbols>
<symbols>
<for-code>51</for-code> <!-- W -->
<alt>1</alt>
</symbols>
<symbols>
<for-code>33</for-code> <!-- E -->
<alt>2</alt>
</symbols>
<symbols>
<for-code>46</for-code> <!-- R -->
<alt>3</alt>
</symbols>
<symbols>
<for-code>48</for-code> <!-- T -->
<alt>(</alt>
<alt>&lt;</alt>
<alt>[</alt>
<alt>{</alt>
</symbols>
<symbols>
<for-code>53</for-code> <!-- Y -->
<alt>)</alt>
<alt>&gt;</alt>
<alt>]</alt>
<alt>}</alt>
</symbols>
<symbols>
<for-code>49</for-code> <!-- U -->
<alt>-</alt>
<alt>–</alt>
<alt>—</alt>
</symbols>
<symbols>
<for-code>37</for-code> <!-- I -->
<alt>_</alt>
<alt>~</alt>
</symbols>
<symbols>
<for-code>43</for-code> <!-- O -->
<alt>/</alt>
<alt>\</alt>
<alt>|</alt>
</symbols>
<symbols>
<for-code>44</for-code> <!-- P -->
<alt>:</alt>
<alt>;</alt>
</symbols>
<symbols>
<for-code>29</for-code> <!-- A -->
<alt>@</alt>
</symbols>
<symbols>
<for-code>47</for-code> <!-- S -->
<alt>4</alt>
</symbols>
<symbols>
<for-code>32</for-code> <!-- D -->
<alt>5</alt>
</symbols>
<symbols>
<for-code>34</for-code> <!-- F -->
<alt>6</alt>
</symbols>
<symbols>
<for-code>35</for-code> <!-- G -->
<alt>*</alt>
<alt>^</alt>
<alt>`</alt>
</symbols>
<symbols>
<for-code>36</for-code> <!-- H -->
<alt>#</alt>
<alt>$</alt>
<alt>€</alt>
</symbols>
<symbols>
<for-code>38</for-code> <!-- J -->
<alt>+</alt>
<alt>=</alt>
<alt>%</alt>
</symbols>
<symbols>
<for-code>39</for-code> <!-- K -->
<alt>"</alt>
</symbols>
<symbols>
<for-code>40</for-code> <!-- L -->
<alt>'</alt>
</symbols>
<symbols>
<for-code>54</for-code> <!-- Z -->
<alt>!</alt>
</symbols>
<symbols>
<for-code>52</for-code> <!-- X -->
<alt>7</alt>
</symbols>
<symbols>
<for-code>31</for-code> <!-- C -->
<alt>8</alt>
</symbols>
<symbols>
<for-code>50</for-code> <!-- V -->
<alt>9</alt>
</symbols>
<symbols>
<for-code>30</for-code> <!-- B -->
<alt>.</alt>
<alt>…</alt>
</symbols>
<symbols>
<for-code>42</for-code> <!-- N -->
<alt>,</alt>
<alt>&amp;</alt>
</symbols>
<symbols>
<for-code>41</for-code> <!-- M -->
<alt>?</alt>
</symbols>
<symbols>
<for-code>62</for-code> <!-- Space -->
<space />
</symbols>
</device-keymap>
</device-keymap-list>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+46 -27
View File
@@ -19,31 +19,50 @@
<!--suppress DeprecatedClassUsageInspection -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="@bool/cycle_manual_default"
android:key="@string/preferences_key_cycle_manual"
android:summary="@string/preferences_summary_cycle_manual"
android:title="@string/preferences_label_cycle_manual" />
<ListPreference
android:defaultValue="@string/cycle_speed_default"
android:disableDependentsState="true"
android:entries="@array/preferences_cycle_speed_labels"
android:entryValues="@array/preferences_cycle_speeds"
android:key="@string/preferences_key_cycle_speed"
android:title="@string/preferences_label_cycle_speed" />
<ListPreference
android:defaultValue="@string/clipboard_size_default"
android:entries="@array/preferences_clipboard_size_labels"
android:entryValues="@array/preferences_clipboard_sizes"
android:key="@string/preferences_key_clipboard_size"
android:title="@string/preferences_label_clipboard_size" />
<MultiSelectListPreference
android:defaultValue="@array/preferences_keymap_defaults"
android:entries="@array/preferences_keymap_labels"
android:entryValues="@array/preferences_keymaps"
android:key="@string/preferences_key_keymaps"
android:title="@string/preferences_label_keymaps" />
<Preference
android:selectable="false"
android:summary="@string/preferences_label_copyright_info" />
<PreferenceCategory android:title="@string/preferences_category_keyboard">
<ListPreference
android:defaultValue="@string/device_default"
android:entries="@array/preferences_device_labels"
android:entryValues="@array/preferences_devices"
android:key="@string/preferences_key_device"
android:title="@string/preferences_label_device" />
<MultiSelectListPreference
android:defaultValue="@array/preferences_language_defaults"
android:entries="@array/preferences_language_labels"
android:entryValues="@array/preferences_languages"
android:key="@string/preferences_key_languages"
android:title="@string/preferences_label_languages" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/preferences_category_functionality">
<SwitchPreference
android:defaultValue="@bool/cycle_manual_default"
android:key="@string/preferences_key_cycle_manual"
android:summary="@string/preferences_summary_cycle_manual"
android:title="@string/preferences_label_cycle_manual" />
<ListPreference
android:defaultValue="@string/cycle_speed_default"
android:disableDependentsState="true"
android:entries="@array/preferences_cycle_speed_labels"
android:entryValues="@array/preferences_cycle_speeds"
android:key="@string/preferences_key_cycle_speed"
android:title="@string/preferences_label_cycle_speed" />
<ListPreference
android:defaultValue="@string/clipboard_size_default"
android:entries="@array/preferences_clipboard_size_labels"
android:entryValues="@array/preferences_clipboard_sizes"
android:key="@string/preferences_key_clipboard_size"
android:title="@string/preferences_label_clipboard_size" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/preferences_category_other">
<Preference android:title="@string/preferences_label_user_manual">
<intent
android:action="eur.cfpifpsi.titanboard.intent.SHOW_MANUAL"
android:targetClass="eur.cfpifpsi.titanboard.ManualActivity" />
</Preference>
<Preference
android:selectable="false"
android:summary="@string/preferences_summary_copyright_info" />
</PreferenceCategory>
</PreferenceScreen>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB