Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5043031e7 | ||
|
|
c710fd1e6a | ||
|
|
bddfc5c725 | ||
|
|
cdb0e55e67 | ||
|
|
0816c1a1d9 | ||
|
|
3ab7bf1eba | ||
|
|
c03c3f01c5 | ||
|
|
7b5da54aea | ||
|
|
3a40699cc9 | ||
|
|
6076c69f3f | ||
|
|
e47227ce6d | ||
|
|
ec931a53a6 | ||
|
|
7cb31ff224 | ||
|
|
7f60898dd8 | ||
|
|
6abc48c121 | ||
|
|
a7de68929a | ||
|
|
68f48dce09 | ||
|
|
32ae47e349 | ||
|
|
185d518faa |
@@ -1,5 +1,7 @@
|
||||
# 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
@@ -10,8 +10,8 @@ android {
|
||||
applicationId 'eur.cfpifpsi.titanboard'
|
||||
minSdk 3
|
||||
targetSdk 35
|
||||
versionCode 30
|
||||
versionName '0.3'
|
||||
versionCode 40
|
||||
versionName '0.4'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+3
-7
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
"r", pressing Alt+R will input "-", Shift+R will input
|
||||
"R". 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't on it. For example, on the Unihertz Titan, it isn't possible to
|
||||
input the "&" 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: ")", ">", "]",
|
||||
"}". 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 "Cycle symbols
|
||||
by consecutively pressing a key" 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 "]", 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
|
||||
"locked" (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 "Alternate
|
||||
composing method" tweak is useful for applications like a terminal, which do not
|
||||
support composing characters. You'll know an application doesn't support it
|
||||
when cycling symbols feels off, and extra characters are inputted other than the one you
|
||||
desire. The "Enter always sends" 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 — "en". If Shift is latched, the first letter is uppercased —
|
||||
"En". If it is pressed, then the code is all uppercase — "EN". 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 "#?", if it is pressed, then
|
||||
"#!", 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
|
||||
" 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>
|
||||
@@ -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><</alt>
|
||||
<alt>[</alt>
|
||||
<alt>{</alt>
|
||||
</symbols>
|
||||
<symbols>
|
||||
<for-code>53</for-code> <!-- Y -->
|
||||
<alt>)</alt>
|
||||
<alt>></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>"</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>&</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><</alt>
|
||||
<alt>[</alt>
|
||||
<alt>{</alt>
|
||||
</symbols>
|
||||
<symbols>
|
||||
<for-code>53</for-code> <!-- Y -->
|
||||
<alt>)</alt>
|
||||
<alt>></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>&</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
@@ -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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user