mirror of https://github.com/termux/termux-app
Merge pull request #2237 from agnostic-apollo/extra-keys-conversion-to-agnosticism
Extra keys conversion to agnosticism and disabling hardware keyboard shortcuts and terminal margin customization support
This commit is contained in:
commit
e7d06aebb5
|
@ -29,6 +29,7 @@ import android.view.autofill.AutofillManager;
|
|||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ListView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
|
@ -44,11 +45,12 @@ import com.termux.app.terminal.TermuxSessionsListViewController;
|
|||
import com.termux.app.terminal.io.TerminalToolbarViewPager;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
import com.termux.app.utils.CrashUtils;
|
||||
|
@ -204,6 +206,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
return;
|
||||
}
|
||||
|
||||
setMargins();
|
||||
|
||||
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
|
||||
mTermuxActivityRootView.setActivity(this);
|
||||
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
|
||||
|
@ -416,6 +420,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
}
|
||||
}
|
||||
|
||||
private void setMargins() {
|
||||
RelativeLayout relativeLayout = findViewById(R.id.activity_termux_root_relative_layout);
|
||||
int marginHorizontal = mProperties.getTerminalMarginHorizontal();
|
||||
int marginVertical = mProperties.getTerminalMarginVertical();
|
||||
ViewUtils.setLayoutMarginsInDp(relativeLayout, marginHorizontal, marginVertical, marginHorizontal, marginVertical);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void addTermuxActivityRootViewGlobalLayoutListener() {
|
||||
|
@ -873,6 +884,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
}
|
||||
}
|
||||
|
||||
setMargins();
|
||||
setTerminalToolbarHeight();
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
|
|
|
@ -3,7 +3,9 @@ package com.termux.app.settings.properties;
|
|||
import android.content.Context;
|
||||
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysInfo;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
|
@ -50,13 +52,20 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties {
|
|||
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
||||
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
|
||||
|
||||
ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle);
|
||||
if (EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(extraKeysStyle)) {
|
||||
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + extraKeysStyle + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
|
||||
extraKeysStyle = TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE;
|
||||
}
|
||||
|
||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
|
||||
} catch (JSONException e) {
|
||||
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
||||
|
||||
try {
|
||||
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
|
||||
} catch (JSONException e2) {
|
||||
Logger.showToast(mContext, "Can't create default extra keys",true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||
|
|
|
@ -27,13 +27,13 @@ import com.termux.shared.file.FileUtils;
|
|||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.terminal.io.extrakeys.SpecialButton;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
@ -75,6 +75,10 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
}
|
||||
|
||||
public TermuxActivity getActivity() {
|
||||
return mActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onCreate() is called
|
||||
*/
|
||||
|
@ -212,7 +216,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
||||
return true;
|
||||
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
||||
} else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
|
||||
e.isCtrlPressed() && e.isAltPressed()) {
|
||||
// Get the unmodified code point:
|
||||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
|
@ -289,12 +294,32 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
@Override
|
||||
public boolean readControlKey() {
|
||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||
return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readAltKey() {
|
||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||
return readExtraKeysSpecialButton(SpecialButton.ALT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readShiftKey() {
|
||||
return readExtraKeysSpecialButton(SpecialButton.SHIFT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readFnKey() {
|
||||
return readExtraKeysSpecialButton(SpecialButton.FN);
|
||||
}
|
||||
|
||||
public boolean readExtraKeysSpecialButton(SpecialButton specialButton) {
|
||||
if (mActivity.getExtraKeysView() == null) return false;
|
||||
Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true);
|
||||
if (state == null) {
|
||||
Logger.logError(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys.");
|
||||
return false;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -11,7 +11,7 @@ import androidx.viewpager.widget.ViewPager;
|
|||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
public class TerminalToolbarViewPager {
|
||||
|
@ -44,8 +44,8 @@ public class TerminalToolbarViewPager {
|
|||
if (position == 0) {
|
||||
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
||||
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
|
||||
extraKeysView.setTermuxTerminalSessionClient(mActivity.getTermuxTerminalSessionClient());
|
||||
extraKeysView.setExtraKeysViewClient(new TermuxTerminalExtraKeys(mActivity.getTerminalView(),
|
||||
mActivity.getTermuxTerminalViewClient(), mActivity.getTermuxTerminalSessionClient()));
|
||||
mActivity.setExtraKeysView(extraKeysView);
|
||||
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.shared.terminal.io.TerminalExtraKeys;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
|
||||
|
||||
|
||||
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
|
||||
public TermuxTerminalExtraKeys(@NonNull TerminalView terminalView,
|
||||
TermuxTerminalViewClient termuxTerminalViewClient,
|
||||
TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
super(terminalView);
|
||||
mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
@Override
|
||||
public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
|
||||
if ("KEYBOARD".equals(key)) {
|
||||
if(mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
|
||||
} else if ("DRAWER".equals(key)) {
|
||||
DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer();
|
||||
if (drawerLayout.isDrawerOpen(Gravity.LEFT))
|
||||
drawerLayout.closeDrawer(Gravity.LEFT);
|
||||
else
|
||||
drawerLayout.openDrawer(Gravity.LEFT);
|
||||
} else if ("PASTE".equals(key)) {
|
||||
if(mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onPasteTextFromClipboard(null);
|
||||
} else {
|
||||
super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExtraKeyButton {
|
||||
|
||||
/**
|
||||
* The key that will be sent to the terminal, either a control character
|
||||
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||
* some text.
|
||||
*/
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||
*/
|
||||
private final boolean macro;
|
||||
|
||||
/**
|
||||
* The text that will be shown on the button.
|
||||
*/
|
||||
private final String display;
|
||||
|
||||
/**
|
||||
* The information of the popup (triggered by swipe up).
|
||||
*/
|
||||
@Nullable
|
||||
private ExtraKeyButton popup;
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||
this(charDisplayMap, config, null);
|
||||
}
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
|
||||
String keyFromConfig = config.optString("key", null);
|
||||
String macroFromConfig = config.optString("macro", null);
|
||||
String[] keys;
|
||||
if (keyFromConfig != null && macroFromConfig != null) {
|
||||
throw new JSONException("Both key and macro can't be set for the same key");
|
||||
} else if (keyFromConfig != null) {
|
||||
keys = new String[]{keyFromConfig};
|
||||
this.macro = false;
|
||||
} else if (macroFromConfig != null) {
|
||||
keys = macroFromConfig.split(" ");
|
||||
this.macro = true;
|
||||
} else {
|
||||
throw new JSONException("All keys have to specify either key or macro");
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
|
||||
}
|
||||
|
||||
this.key = TextUtils.join(" ", keys);
|
||||
|
||||
String displayFromConfig = config.optString("display", null);
|
||||
if (displayFromConfig != null) {
|
||||
this.display = displayFromConfig;
|
||||
} else {
|
||||
this.display = Arrays.stream(keys)
|
||||
.map(key -> charDisplayMap.get(key, key))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public boolean isMacro() {
|
||||
return macro;
|
||||
}
|
||||
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ExtraKeyButton getPopup() {
|
||||
return popup;
|
||||
}
|
||||
}
|
|
@ -1,260 +0,0 @@
|
|||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ExtraKeysInfo {
|
||||
|
||||
/**
|
||||
* Matrix of buttons displayed
|
||||
*/
|
||||
private final ExtraKeyButton[][] buttons;
|
||||
|
||||
/**
|
||||
* This corresponds to one of the CharMapDisplay below
|
||||
*/
|
||||
private String style;
|
||||
|
||||
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
|
||||
this.style = style;
|
||||
|
||||
// Convert String propertiesInfo to Array of Arrays
|
||||
JSONArray arr = new JSONArray(propertiesInfo);
|
||||
Object[][] matrix = new Object[arr.length()][];
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONArray line = arr.getJSONArray(i);
|
||||
matrix[i] = new Object[line.length()];
|
||||
for (int j = 0; j < line.length(); j++) {
|
||||
matrix[i][j] = line.get(j);
|
||||
}
|
||||
}
|
||||
|
||||
// convert matrix to buttons
|
||||
this.buttons = new ExtraKeyButton[matrix.length][];
|
||||
for (int i = 0; i < matrix.length; i++) {
|
||||
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
|
||||
for (int j = 0; j < matrix[i].length; j++) {
|
||||
Object key = matrix[i][j];
|
||||
|
||||
JSONObject jobject = normalizeKeyConfig(key);
|
||||
|
||||
ExtraKeyButton button;
|
||||
|
||||
if (! jobject.has("popup")) {
|
||||
// no popup
|
||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||
} else {
|
||||
// a popup
|
||||
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
|
||||
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
|
||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
|
||||
}
|
||||
|
||||
this.buttons[i][j] = button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "hello" -> {"key": "hello"}
|
||||
*/
|
||||
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||
JSONObject jobject;
|
||||
if (key instanceof String) {
|
||||
jobject = new JSONObject();
|
||||
jobject.put("key", key);
|
||||
} else if (key instanceof JSONObject) {
|
||||
jobject = (JSONObject) key;
|
||||
} else {
|
||||
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||
}
|
||||
return jobject;
|
||||
}
|
||||
|
||||
public ExtraKeyButton[][] getMatrix() {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* HashMap that implements Python dict.get(key, default) function.
|
||||
* Default java.util .get(key) is then the same as .get(key, null);
|
||||
*/
|
||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||
V get(K key, V defaultValue) {
|
||||
if (containsKey(key))
|
||||
return get(key);
|
||||
else
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
static class CharDisplayMap extends CleverMap<String, String> {}
|
||||
|
||||
/**
|
||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
||||
*/
|
||||
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
|
||||
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||
}};
|
||||
|
||||
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
|
||||
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
||||
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
||||
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
||||
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
||||
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
|
||||
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||
put("PASTE", "⎘"); // U+2398
|
||||
}};
|
||||
|
||||
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
|
||||
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
||||
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
||||
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
||||
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
||||
}};
|
||||
|
||||
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
|
||||
// alternative to classic arrow keys
|
||||
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||
}};
|
||||
|
||||
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
|
||||
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
||||
// put("FN", "FN"); // no ISO character exists
|
||||
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
||||
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
||||
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
||||
}};
|
||||
|
||||
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
|
||||
// nicer looking for most cases
|
||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||
}};
|
||||
|
||||
/*
|
||||
* Multiple maps are available to quickly change
|
||||
* the style of the keys.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some classic symbols everybody knows
|
||||
*/
|
||||
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(nicerLookingDisplay);
|
||||
// all other characters are displayed as themselves
|
||||
}};
|
||||
|
||||
/**
|
||||
* Classic symbols and less known symbols
|
||||
*/
|
||||
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(lessKnownCharactersDisplay); // NEW
|
||||
putAll(nicerLookingDisplay);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Only arrows
|
||||
*/
|
||||
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||
putAll(nicerLookingDisplay);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Full Iso
|
||||
*/
|
||||
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(lessKnownCharactersDisplay); // NEW
|
||||
putAll(nicerLookingDisplay);
|
||||
putAll(notKnownIsoCharacters); // NEW
|
||||
}};
|
||||
|
||||
/**
|
||||
* Some people might call our keys differently
|
||||
*/
|
||||
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
|
||||
put("ESCAPE", "ESC");
|
||||
put("CONTROL", "CTRL");
|
||||
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
||||
put("FUNCTION", "FN");
|
||||
// no alias for ALT
|
||||
|
||||
// Directions are sometimes written as first and last letter for brevety
|
||||
put("LT", "LEFT");
|
||||
put("RT", "RIGHT");
|
||||
put("DN", "DOWN");
|
||||
// put("UP", "UP"); well, "UP" is already two letters
|
||||
|
||||
put("PAGEUP", "PGUP");
|
||||
put("PAGE_UP", "PGUP");
|
||||
put("PAGE UP", "PGUP");
|
||||
put("PAGE-UP", "PGUP");
|
||||
|
||||
// no alias for HOME
|
||||
// no alias for END
|
||||
|
||||
put("PAGEDOWN", "PGDN");
|
||||
put("PAGE_DOWN", "PGDN");
|
||||
put("PAGE-DOWN", "PGDN");
|
||||
|
||||
put("DELETE", "DEL");
|
||||
put("BACKSPACE", "BKSP");
|
||||
|
||||
// easier for writing in termux.properties
|
||||
put("BACKSLASH", "\\");
|
||||
put("QUOTE", "\"");
|
||||
put("APOSTROPHE", "'");
|
||||
}};
|
||||
|
||||
CharDisplayMap getSelectedCharMap() {
|
||||
switch (style) {
|
||||
case "arrows-only":
|
||||
return arrowsOnlyCharDisplay;
|
||||
case "arrows-all":
|
||||
return lotsOfArrowsCharDisplay;
|
||||
case "all":
|
||||
return fullIsoCharDisplay;
|
||||
case "none":
|
||||
return new CharDisplayMap();
|
||||
default:
|
||||
if (!TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(style))
|
||||
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + style + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
|
||||
return defaultCharDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
|
||||
* Modifies the array, doesn't return a new one.
|
||||
*/
|
||||
public static String replaceAlias(String key) {
|
||||
return controlCharsAliases.get(key, key);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,401 +0,0 @@
|
|||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import android.view.Gravity;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
/**
|
||||
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||
* keyboard.
|
||||
*/
|
||||
public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int BUTTON_COLOR = 0x00000000;
|
||||
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
||||
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
|
||||
|
||||
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
|
||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
|
||||
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||
put("END", KeyEvent.KEYCODE_MOVE_END);
|
||||
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
||||
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
||||
put("INS", KeyEvent.KEYCODE_INSERT);
|
||||
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
||||
put("BKSP", KeyEvent.KEYCODE_DEL);
|
||||
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
||||
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||
put("F1", KeyEvent.KEYCODE_F1);
|
||||
put("F2", KeyEvent.KEYCODE_F2);
|
||||
put("F3", KeyEvent.KEYCODE_F3);
|
||||
put("F4", KeyEvent.KEYCODE_F4);
|
||||
put("F5", KeyEvent.KEYCODE_F5);
|
||||
put("F6", KeyEvent.KEYCODE_F6);
|
||||
put("F7", KeyEvent.KEYCODE_F7);
|
||||
put("F8", KeyEvent.KEYCODE_F8);
|
||||
put("F9", KeyEvent.KEYCODE_F9);
|
||||
put("F10", KeyEvent.KEYCODE_F10);
|
||||
put("F11", KeyEvent.KEYCODE_F11);
|
||||
put("F12", KeyEvent.KEYCODE_F12);
|
||||
}};
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||
if ("KEYBOARD".equals(keyName)) {
|
||||
if(mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
|
||||
} else if ("DRAWER".equals(keyName)) {
|
||||
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||
drawer.openDrawer(Gravity.LEFT);
|
||||
} else if ("PASTE".equals(keyName)) {
|
||||
if(mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onPasteTextFromClipboard(null);
|
||||
} else if (keyCodesForString.containsKey(keyName)) {
|
||||
Integer keyCode = keyCodesForString.get(keyName);
|
||||
if (keyCode == null) return;
|
||||
int metaState = 0;
|
||||
if (forceCtrlDown) {
|
||||
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||
}
|
||||
if (forceLeftAltDown) {
|
||||
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||
}
|
||||
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
|
||||
terminalView.onKeyDown(keyCode, keyEvent);
|
||||
} else {
|
||||
// not a control char
|
||||
keyName.codePoints().forEach(codePoint -> {
|
||||
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sendKey(View view, ExtraKeyButton buttonInfo) {
|
||||
if (buttonInfo.isMacro()) {
|
||||
String[] keys = buttonInfo.getKey().split(" ");
|
||||
boolean ctrlDown = false;
|
||||
boolean altDown = false;
|
||||
for (String key : keys) {
|
||||
if ("CTRL".equals(key)) {
|
||||
ctrlDown = true;
|
||||
} else if ("ALT".equals(key)) {
|
||||
altDown = true;
|
||||
} else {
|
||||
sendKey(view, key, ctrlDown, altDown);
|
||||
ctrlDown = false;
|
||||
altDown = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendKey(view, buttonInfo.getKey(), false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public enum SpecialButton {
|
||||
CTRL, ALT, FN
|
||||
}
|
||||
|
||||
private static class SpecialButtonState {
|
||||
boolean isOn = false;
|
||||
boolean isActive = false;
|
||||
List<Button> buttons = new ArrayList<>();
|
||||
|
||||
void setIsActive(boolean value) {
|
||||
isActive = value;
|
||||
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||
put(SpecialButton.CTRL, new SpecialButtonState());
|
||||
put(SpecialButton.ALT, new SpecialButtonState());
|
||||
put(SpecialButton.FN, new SpecialButtonState());
|
||||
}};
|
||||
|
||||
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
|
||||
|
||||
private boolean isSpecialButton(ExtraKeyButton button) {
|
||||
return specialButtonsKeys.contains(button.getKey());
|
||||
}
|
||||
|
||||
private ScheduledExecutorService scheduledExecutor;
|
||||
private PopupWindow popupWindow;
|
||||
private int longPressCount;
|
||||
|
||||
public boolean readSpecialButton(SpecialButton name) {
|
||||
SpecialButtonState state = specialButtons.get(name);
|
||||
if (state == null)
|
||||
throw new RuntimeException("Must be a valid special button (see source)");
|
||||
|
||||
if (!state.isOn || !state.isActive)
|
||||
return false;
|
||||
|
||||
state.setIsActive(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||
if (state == null) return null;
|
||||
state.isOn = true;
|
||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
|
||||
if (needUpdate) {
|
||||
state.buttons.add(button);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
void popup(View view, ExtraKeyButton extraButton) {
|
||||
int width = view.getMeasuredWidth();
|
||||
int height = view.getMeasuredHeight();
|
||||
Button button;
|
||||
if (isSpecialButton(extraButton)) {
|
||||
button = createSpecialButton(extraButton.getKey(), false);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
}
|
||||
button.setText(extraButton.getDisplay());
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
button.setMinHeight(0);
|
||||
button.setMinWidth(0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinimumHeight(0);
|
||||
button.setWidth(width);
|
||||
button.setHeight(height);
|
||||
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
popupWindow = new PopupWindow(this);
|
||||
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setContentView(button);
|
||||
popupWindow.setOutsideTouchable(true);
|
||||
popupWindow.setFocusable(false);
|
||||
popupWindow.showAsDropDown(view, 0, -2 * height);
|
||||
}
|
||||
|
||||
/**
|
||||
* General util function to compute the longest column length in a matrix.
|
||||
*/
|
||||
static int maximumLength(Object[][] matrix) {
|
||||
int m = 0;
|
||||
for (Object[] row : matrix)
|
||||
m = Math.max(m, row.length);
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the view given parameters in termux.properties
|
||||
*
|
||||
* @param infos matrix as defined in termux.properties extrakeys
|
||||
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
|
||||
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
|
||||
* Any string of length > 1 in total Uppercase will print a warning
|
||||
*
|
||||
* Examples:
|
||||
* "ENTER" will trigger the ENTER keycode
|
||||
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
|
||||
* "→" will input a "→" character
|
||||
* "−" will input a "−" character
|
||||
* "-_-" will input the string "-_-"
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void reload(ExtraKeysInfo infos) {
|
||||
if (infos == null)
|
||||
return;
|
||||
|
||||
for(SpecialButtonState state : specialButtons.values())
|
||||
state.buttons = new ArrayList<>();
|
||||
|
||||
removeAllViews();
|
||||
|
||||
ExtraKeyButton[][] buttons = infos.getMatrix();
|
||||
|
||||
setRowCount(buttons.length);
|
||||
setColumnCount(maximumLength(buttons));
|
||||
|
||||
for (int row = 0; row < buttons.length; row++) {
|
||||
for (int col = 0; col < buttons[row].length; col++) {
|
||||
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||
|
||||
Button button;
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
}
|
||||
|
||||
button.setText(buttonInfo.getDisplay());
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
|
||||
final Button finalButton = button;
|
||||
button.setOnClickListener(v -> {
|
||||
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else {
|
||||
// Perform haptic feedback only if no total silence mode enabled.
|
||||
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
|
||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
View root = getRootView();
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||
if (state == null) return;
|
||||
state.setIsActive(!state.isActive);
|
||||
} else {
|
||||
sendKey(root, buttonInfo);
|
||||
}
|
||||
});
|
||||
|
||||
button.setOnTouchListener((v, event) -> {
|
||||
final View root = getRootView();
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
longPressCount = 0;
|
||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
|
||||
// autorepeat
|
||||
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
scheduledExecutor.scheduleWithFixedDelay(() -> {
|
||||
longPressCount++;
|
||||
sendKey(root, buttonInfo);
|
||||
}, 400, 80, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
if (popupWindow == null && event.getY() < 0) {
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
popup(v, buttonInfo.getPopup());
|
||||
}
|
||||
if (popupWindow != null && event.getY() > 0) {
|
||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
popupWindow.dismiss();
|
||||
popupWindow = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
if (longPressCount == 0 || popupWindow != null) {
|
||||
if (popupWindow != null) {
|
||||
popupWindow.setContentView(null);
|
||||
popupWindow.dismiss();
|
||||
popupWindow = null;
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
if (isSpecialButton(buttonInfo.getPopup())) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
|
||||
if (state == null) return true;
|
||||
state.setIsActive(!state.isActive);
|
||||
} else {
|
||||
sendKey(root, buttonInfo.getPopup());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
v.performClick();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
LayoutParams param = new GridLayout.LayoutParams();
|
||||
param.width = 0;
|
||||
param.height = 0;
|
||||
param.setMargins(0, 0, 0, 0);
|
||||
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
|
||||
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
||||
button.setLayoutParams(param);
|
||||
|
||||
addView(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
|
||||
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||
}
|
||||
|
||||
public void setTermuxTerminalSessionClient(TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
}
|
||||
|
||||
}
|
|
@ -6,9 +6,12 @@
|
|||
android:fitsSystemWindows="true">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/activity_termux_root_relative_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_marginVertical="0dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
|
@ -22,8 +25,6 @@
|
|||
android:id="@+id/terminal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginRight="3dp"
|
||||
android:layout_marginLeft="3dp"
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||
android:scrollbars="vertical"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.termux.shared.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/terminal_toolbar_extra_keys"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -18,7 +18,7 @@ android.useAndroidX=true
|
|||
minSdkVersion=24
|
||||
targetSdkVersion=28
|
||||
ndkVersion=22.1.7171670
|
||||
compileSdkVersion=31
|
||||
compileSdkVersion=30
|
||||
|
||||
markwonVersion=4.6.2
|
||||
|
||||
|
|
|
@ -271,6 +271,10 @@ public final class TerminalView extends View {
|
|||
// Some keyboards seems do not reset the internal state on TYPE_NULL.
|
||||
// Affects mostly Samsung stock keyboards.
|
||||
// https://github.com/termux/termux-app/issues/686
|
||||
// However, this is not a valid value as per AOSP since `InputType.TYPE_CLASS_*` is
|
||||
// not set and it logs a warning:
|
||||
// W/InputAttributes: Unexpected input class: inputType=0x00080090 imeOptions=0x02000000
|
||||
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/InputAttributes.java;l=79
|
||||
outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
|
||||
} else {
|
||||
// Using InputType.NULL is the most correct input type and avoids issues with other hacks.
|
||||
|
@ -346,6 +350,10 @@ public final class TerminalView extends View {
|
|||
codePoint = firstChar;
|
||||
}
|
||||
|
||||
// Check onKeyDown() for details.
|
||||
if (mClient.readShiftKey())
|
||||
codePoint = Character.toUpperCase(codePoint);
|
||||
|
||||
boolean ctrlHeld = false;
|
||||
if (codePoint <= 31 && codePoint != 27) {
|
||||
if (codePoint == '\n') {
|
||||
|
@ -576,6 +584,102 @@ public final class TerminalView extends View {
|
|||
return super.onKeyPreIme(keyCode, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Key presses in software keyboards will generally NOT trigger this listener, although some
|
||||
* may elect to do so in some situations. Do not rely on this to catch software key presses.
|
||||
* Gboard calls this when shouldEnforceCharBasedInput() is disabled (InputType.TYPE_NULL) instead
|
||||
* of calling commitText(), with deviceId=-1. However, Hacker's Keyboard, OpenBoard, LG Keyboard
|
||||
* call commitText().
|
||||
*
|
||||
* This function may also be called directly without android calling it, like by
|
||||
* `TerminalExtraKeys` which generates a KeyEvent manually which uses {@link KeyCharacterMap#VIRTUAL_KEYBOARD}
|
||||
* as the device (deviceId=-1), as does Gboard. That would normally use mappings defined in
|
||||
* `/system/usr/keychars/Virtual.kcm`. You can run `dumpsys input` to find the `KeyCharacterMapFile`
|
||||
* used by virtual keyboard or hardware keyboard. Note that virtual keyboard device is not the
|
||||
* same as software keyboard, like Gboard, etc. Its a fake device used for generating events and
|
||||
* for testing.
|
||||
*
|
||||
* We handle shift key in `commitText()` to convert codepoint to uppercase case there with a
|
||||
* call to {@link Character#toUpperCase(int)}, but here we instead rely on getUnicodeChar() for
|
||||
* conversion of keyCode, for both hardware keyboard shift key (via effectiveMetaState) and
|
||||
* `mClient.readShiftKey()`, based on value in kcm files.
|
||||
* This may result in different behaviour depending on keyboard and android kcm files set for the
|
||||
* InputDevice for the event passed to this function. This will likely be an issue for non-english
|
||||
* languages since `Virtual.kcm` in english only by default or at least in AOSP. For both hardware
|
||||
* shift key (via effectiveMetaState) and `mClient.readShiftKey()`, `getUnicodeChar()` is used
|
||||
* for shift specific behaviour which usually is to uppercase.
|
||||
*
|
||||
* For fn key on hardware keyboard, android checks kcm files for hardware keyboards, which is
|
||||
* `Generic.kcm` by default, unless a vendor specific one is defined. The event passed will have
|
||||
* {@link KeyEvent#META_FUNCTION_ON} set. If the kcm file only defines a single character or unicode
|
||||
* code point `\\uxxxx`, then only one event is passed with that value. However, if kcm defines
|
||||
* a `fallback` key for fn or others, like `key DPAD_UP { ... fn: fallback PAGE_UP }`, then
|
||||
* android will first pass an event with original key `DPAD_UP` and {@link KeyEvent#META_FUNCTION_ON}
|
||||
* set. But this function will not consume it and android will pass another event with `PAGE_UP`
|
||||
* and {@link KeyEvent#META_FUNCTION_ON} not set, which will be consumed.
|
||||
*
|
||||
* Now there are some other issues as well, firstly ctrl and alt flags are not passed to
|
||||
* `getUnicodeChar()`, so modified key values in kcm are not used. Secondly, if the kcm file
|
||||
* for other modifiers like shift or fn define a non-alphabet, like { fn: '\u0015' } to act as
|
||||
* DPAD_LEFT, the `getUnicodeChar()` will correctly return `21` as the code point but action will
|
||||
* not happen because the `handleKeyCode()` function that transforms DPAD_LEFT to `\033[D`
|
||||
* escape sequence for the terminal to perform the left action would not be called since its
|
||||
* called before `getUnicodeChar()` and terminal will instead get `21 0x15 Negative Acknowledgement`.
|
||||
* The solution to such issues is calling `getUnicodeChar()` before the call to `handleKeyCode()`
|
||||
* if user has defined a custom kcm file, like done in POC mentioned in #2237. Note that
|
||||
* Hacker's Keyboard calls `commitText()` so don't test fn/shift with it for this function.
|
||||
* https://github.com/termux/termux-app/pull/2237
|
||||
* https://github.com/agnostic-apollo/termux-app/blob/terminal-code-point-custom-mapping/terminal-view/src/main/java/com/termux/view/TerminalView.java
|
||||
*
|
||||
* Key Character Map (kcm) and Key Layout (kl) files info:
|
||||
* https://source.android.com/devices/input/key-character-map-files
|
||||
* https://source.android.com/devices/input/key-layout-files
|
||||
* https://source.android.com/devices/input/keyboard-devices
|
||||
* AOSP kcm and kl files:
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/data/keyboards
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/packages/InputDevices/res/raw
|
||||
*
|
||||
* KeyCodes:
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/view/KeyEvent.java
|
||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/native/include/android/keycodes.h
|
||||
*
|
||||
* `dumpsys input`:
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/services/inputflinger/reader/EventHub.cpp;l=1917
|
||||
*
|
||||
* Loading of keymap:
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/services/inputflinger/reader/EventHub.cpp;l=1644
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/Keyboard.cpp;l=41
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/InputDevice.cpp
|
||||
* OVERLAY keymaps for hardware keyboards may be combined as well:
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=165
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=831
|
||||
*
|
||||
* Parse kcm file:
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=727
|
||||
* Parse key value:
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=981
|
||||
*
|
||||
* `KeyEvent.getUnicodeChar()`
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/view/KeyEvent.java;l=2716
|
||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/KeyCharacterMap.java;l=368
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/jni/android_view_KeyCharacterMap.cpp;l=117
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=231
|
||||
*
|
||||
* Keyboard layouts advertised by applications, like for hardware keyboards via #ACTION_QUERY_KEYBOARD_LAYOUTS
|
||||
* Config is stored in `/data/system/input-manager-state.xml`
|
||||
* https://github.com/ris58h/custom-keyboard-layout
|
||||
* Loading from apps:
|
||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=1221
|
||||
* Set:
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/hardware/input/InputManager.java;l=89
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/hardware/input/InputManager.java;l=543
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/apps/Settings/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java;l=167
|
||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=1385
|
||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/PersistentDataStore.java
|
||||
* Get overlay keyboard layout
|
||||
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=2158
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp;l=616
|
||||
*/
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
||||
|
@ -598,13 +702,15 @@ public final class TerminalView extends View {
|
|||
final int metaState = event.getMetaState();
|
||||
final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey();
|
||||
final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey();
|
||||
final boolean shiftDown = event.isShiftPressed() || mClient.readShiftKey();
|
||||
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
||||
|
||||
int keyMod = 0;
|
||||
if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL;
|
||||
if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT;
|
||||
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
||||
if (shiftDown) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
||||
if (event.isNumLockOn()) keyMod |= KeyHandler.KEYMOD_NUM_LOCK;
|
||||
// https://github.com/termux/termux-app/issues/731
|
||||
if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) {
|
||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "handleKeyCode() took key event");
|
||||
return true;
|
||||
|
@ -620,6 +726,9 @@ public final class TerminalView extends View {
|
|||
}
|
||||
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
|
||||
|
||||
if (shiftDown) effectiveMetaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
|
||||
if (mClient.readFnKey()) effectiveMetaState |= KeyEvent.META_FUNCTION_ON;
|
||||
|
||||
int result = event.getUnicodeChar(effectiveMetaState);
|
||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
||||
mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
|
||||
|
@ -655,6 +764,10 @@ public final class TerminalView extends View {
|
|||
|
||||
if (mTermSession == null) return;
|
||||
|
||||
// Ensure cursor is shown when a key is pressed down like long hold on (arrow) keys
|
||||
if (mEmulator != null)
|
||||
mEmulator.setCursorBlinkState(true);
|
||||
|
||||
final boolean controlDown = controlDownFromEvent || mClient.readControlKey();
|
||||
final boolean altDown = leftAltDownFromEvent || mClient.readAltKey();
|
||||
|
||||
|
|
|
@ -54,6 +54,11 @@ public interface TerminalViewClient {
|
|||
|
||||
boolean readAltKey();
|
||||
|
||||
boolean readShiftKey();
|
||||
|
||||
boolean readFnKey();
|
||||
|
||||
|
||||
|
||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||
|
||||
|
|
|
@ -553,6 +553,17 @@ public class SharedProperties {
|
|||
return (object == null) ? def : object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link String} object itself if it is not {@code null} or empty, otherwise default.
|
||||
*
|
||||
* @param object The {@link String} to check.
|
||||
* @param def The default {@link String}.
|
||||
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
|
||||
*/
|
||||
public static String getDefaultIfNullOrEmpty(@androidx.annotation.Nullable String object, @androidx.annotation.Nullable String def) {
|
||||
return (object == null || object.isEmpty()) ? def : object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Covert the {@link String} value to lowercase.
|
||||
*
|
||||
|
|
|
@ -12,7 +12,7 @@ import java.util.HashSet;
|
|||
import java.util.Set;
|
||||
|
||||
/*
|
||||
* Version: v0.12.0
|
||||
* Version: v0.13.0
|
||||
*
|
||||
* Changelog
|
||||
*
|
||||
|
@ -55,6 +55,9 @@ import java.util.Set;
|
|||
*
|
||||
* - 0.12.0 (2021-06-10)
|
||||
* - Add `*KEY_TERMINAL_CURSOR_STYLE*`.
|
||||
*
|
||||
* - 0.13.0 (2021-08-25)
|
||||
* - Add `*KEY_TERMINAL_MARGIN_HORIZONTAL*` and `*KEY_TERMINAL_MARGIN_VERTICAL*`.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -72,6 +75,10 @@ public final class TermuxPropertyConstants {
|
|||
|
||||
/* boolean */
|
||||
|
||||
/** Defines the key for whether hardware keyboard shortcuts are enabled. */
|
||||
public static final String KEY_DISABLE_HARDWARE_KEYBOARD_SHORTCUTS = "disable-hardware-keyboard-shortcuts"; // Default: "disable-hardware-keyboard-shortcuts"
|
||||
|
||||
|
||||
/** Defines the key for whether a toast will be shown when user changes the terminal session */
|
||||
public static final String KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST = "disable-terminal-session-change-toast"; // Default: "disable-terminal-session-change-toast"
|
||||
|
||||
|
@ -169,6 +176,20 @@ public final class TermuxPropertyConstants {
|
|||
|
||||
|
||||
|
||||
/** Defines the key for the terminal margin on left and right in dp units */
|
||||
public static final String KEY_TERMINAL_MARGIN_HORIZONTAL = "terminal-margin-horizontal"; // Default: "terminal-margin-horizontal"
|
||||
public static final int IVALUE_TERMINAL_MARGIN_HORIZONTAL_MIN = 0;
|
||||
public static final int IVALUE_TERMINAL_MARGIN_HORIZONTAL_MAX = 100;
|
||||
public static final int DEFAULT_IVALUE_TERMINAL_HORIZONTAL_MARGIN = 3;
|
||||
|
||||
/** Defines the key for the terminal margin on top and bottom in dp units */
|
||||
public static final String KEY_TERMINAL_MARGIN_VERTICAL = "terminal-margin-vertical"; // Default: "terminal-margin-vertical"
|
||||
public static final int IVALUE_TERMINAL_MARGIN_VERTICAL_MIN = 0;
|
||||
public static final int IVALUE_TERMINAL_MARGIN_VERTICAL_MAX = 100;
|
||||
public static final int DEFAULT_IVALUE_TERMINAL_VERTICAL_MARGIN = 0;
|
||||
|
||||
|
||||
|
||||
/** Defines the key for the terminal transcript rows */
|
||||
public static final String KEY_TERMINAL_TRANSCRIPT_ROWS = "terminal-transcript-rows"; // Default: "terminal-transcript-rows"
|
||||
public static final int IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN = TerminalEmulator.TERMINAL_TRANSCRIPT_ROWS_MIN;
|
||||
|
@ -295,6 +316,7 @@ public final class TermuxPropertyConstants {
|
|||
* */
|
||||
public static final Set<String> TERMUX_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
||||
/* boolean */
|
||||
KEY_DISABLE_HARDWARE_KEYBOARD_SHORTCUTS,
|
||||
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
|
||||
KEY_ENFORCE_CHAR_BASED_INPUT,
|
||||
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
||||
|
@ -309,6 +331,8 @@ public final class TermuxPropertyConstants {
|
|||
KEY_BELL_BEHAVIOUR,
|
||||
KEY_TERMINAL_CURSOR_BLINK_RATE,
|
||||
KEY_TERMINAL_CURSOR_STYLE,
|
||||
KEY_TERMINAL_MARGIN_HORIZONTAL,
|
||||
KEY_TERMINAL_MARGIN_VERTICAL,
|
||||
KEY_TERMINAL_TRANSCRIPT_ROWS,
|
||||
|
||||
/* float */
|
||||
|
@ -335,6 +359,7 @@ public final class TermuxPropertyConstants {
|
|||
* default: false
|
||||
* */
|
||||
public static final Set<String> TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
||||
KEY_DISABLE_HARDWARE_KEYBOARD_SHORTCUTS,
|
||||
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
|
||||
KEY_ENFORCE_CHAR_BASED_INPUT,
|
||||
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
||||
|
|
|
@ -219,6 +219,10 @@ public class TermuxSharedProperties {
|
|||
return (int) getTerminalCursorBlinkRateInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE:
|
||||
return (int) getTerminalCursorStyleInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_MARGIN_HORIZONTAL:
|
||||
return (int) getTerminalMarginHorizontalInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_MARGIN_VERTICAL:
|
||||
return (int) getTerminalMarginVerticalInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS:
|
||||
return (int) getTerminalTranscriptRowsInternalPropertyValueFromValue(value);
|
||||
|
||||
|
@ -318,6 +322,42 @@ public class TermuxSharedProperties {
|
|||
return (int) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE, TermuxPropertyConstants.MAP_TERMINAL_CURSOR_STYLE, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the int for the value if its not null and is between
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_MARGIN_HORIZONTAL_MIN} and
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_MARGIN_HORIZONTAL_MAX},
|
||||
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_HORIZONTAL_MARGIN}.
|
||||
*
|
||||
* @param value The {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static int getTerminalMarginHorizontalInternalPropertyValueFromValue(String value) {
|
||||
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_MARGIN_HORIZONTAL,
|
||||
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_HORIZONTAL_MARGIN),
|
||||
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_HORIZONTAL_MARGIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_MARGIN_HORIZONTAL_MIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_MARGIN_HORIZONTAL_MAX,
|
||||
true, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the int for the value if its not null and is between
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_MARGIN_VERTICAL_MIN} and
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_MARGIN_VERTICAL_MAX},
|
||||
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_VERTICAL_MARGIN}.
|
||||
*
|
||||
* @param value The {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static int getTerminalMarginVerticalInternalPropertyValueFromValue(String value) {
|
||||
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_MARGIN_VERTICAL,
|
||||
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_VERTICAL_MARGIN),
|
||||
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_VERTICAL_MARGIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_MARGIN_VERTICAL_MIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_MARGIN_VERTICAL_MAX,
|
||||
true, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the int for the value if its not null and is between
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN} and
|
||||
|
@ -423,7 +463,7 @@ public class TermuxSharedProperties {
|
|||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static String getExtraKeysInternalPropertyValueFromValue(String value) {
|
||||
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS);
|
||||
return SharedProperties.getDefaultIfNullOrEmpty(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -433,7 +473,7 @@ public class TermuxSharedProperties {
|
|||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static String getExtraKeysStyleInternalPropertyValueFromValue(String value) {
|
||||
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||
return SharedProperties.getDefaultIfNullOrEmpty(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -460,6 +500,10 @@ public class TermuxSharedProperties {
|
|||
|
||||
|
||||
|
||||
public boolean areHardwareKeyboardShortcutsDisabled() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST, true);
|
||||
}
|
||||
|
||||
public boolean areTerminalSessionChangeToastsDisabled() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST, true);
|
||||
}
|
||||
|
@ -504,6 +548,14 @@ public class TermuxSharedProperties {
|
|||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE, true);
|
||||
}
|
||||
|
||||
public int getTerminalMarginHorizontal() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_MARGIN_HORIZONTAL, true);
|
||||
}
|
||||
|
||||
public int getTerminalMarginVertical() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_MARGIN_VERTICAL, true);
|
||||
}
|
||||
|
||||
public int getTerminalTranscriptRows() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS, true);
|
||||
}
|
||||
|
|
|
@ -67,6 +67,18 @@ public class TermuxTerminalViewClientBase implements TerminalViewClient {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readShiftKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readFnKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package com.termux.shared.terminal.io;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeyButton;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.terminal.io.extrakeys.SpecialButton;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import static com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants.PRIMARY_KEY_CODES_FOR_STRINGS;
|
||||
|
||||
|
||||
public class TerminalExtraKeys implements ExtraKeysView.IExtraKeysView {
|
||||
|
||||
private final TerminalView mTerminalView;
|
||||
|
||||
public TerminalExtraKeys(@NonNull TerminalView terminalView) {
|
||||
mTerminalView = terminalView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
if (buttonInfo.isMacro()) {
|
||||
String[] keys = buttonInfo.getKey().split(" ");
|
||||
boolean ctrlDown = false;
|
||||
boolean altDown = false;
|
||||
boolean shiftDown = false;
|
||||
boolean fnDown = false;
|
||||
for (String key : keys) {
|
||||
if (SpecialButton.CTRL.getKey().equals(key)) {
|
||||
ctrlDown = true;
|
||||
} else if (SpecialButton.ALT.getKey().equals(key)) {
|
||||
altDown = true;
|
||||
} else if (SpecialButton.SHIFT.getKey().equals(key)) {
|
||||
shiftDown = true;
|
||||
} else if (SpecialButton.FN.getKey().equals(key)) {
|
||||
fnDown = true;
|
||||
} else {
|
||||
onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
|
||||
ctrlDown = false; altDown = false; shiftDown = false; fnDown = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onTerminalExtraKeyButtonClick(view, buttonInfo.getKey(), false, false, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
|
||||
if (PRIMARY_KEY_CODES_FOR_STRINGS.containsKey(key)) {
|
||||
Integer keyCode = PRIMARY_KEY_CODES_FOR_STRINGS.get(key);
|
||||
if (keyCode == null) return;
|
||||
int metaState = 0;
|
||||
if (ctrlDown) metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||
if (altDown) metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||
if (shiftDown) metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
|
||||
if (fnDown) metaState |= KeyEvent.META_FUNCTION_ON;
|
||||
|
||||
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
|
||||
mTerminalView.onKeyDown(keyCode, keyEvent);
|
||||
} else {
|
||||
// not a control char
|
||||
key.codePoints().forEach(codePoint -> {
|
||||
mTerminalView.inputCodePoint(codePoint, ctrlDown, altDown);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performExtraKeyButtonHapticFeedback(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExtraKeyButton {
|
||||
|
||||
/** The key name for the name of the extra key if using a dict to define the extra key. {key: name, ...} */
|
||||
public static final String KEY_KEY_NAME = "key";
|
||||
|
||||
/** The key name for the macro value of the extra key if using a dict to define the extra key. {macro: value, ...} */
|
||||
public static final String KEY_MACRO = "macro";
|
||||
|
||||
/** The key name for the alternate display name of the extra key if using a dict to define the extra key. {display: name, ...} */
|
||||
public static final String KEY_DISPLAY_NAME = "display";
|
||||
|
||||
/** The key name for the nested dict to define popup extra key info if using a dict to define the extra key. {popup: {key: name, ...}, ...} */
|
||||
public static final String KEY_POPUP = "popup";
|
||||
|
||||
|
||||
/**
|
||||
* The key that will be sent to the terminal, either a control character, like defined in
|
||||
* {@link ExtraKeysConstants#PRIMARY_KEY_CODES_FOR_STRINGS} (LEFT, RIGHT, PGUP...) or some text.
|
||||
*/
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||
*/
|
||||
private final boolean macro;
|
||||
|
||||
/**
|
||||
* The text that will be displayed on the button.
|
||||
*/
|
||||
private final String display;
|
||||
|
||||
/**
|
||||
* The {@link ExtraKeyButton} containing the information of the popup button (triggered by swipe up).
|
||||
*/
|
||||
@Nullable
|
||||
private final ExtraKeyButton popup;
|
||||
|
||||
|
||||
/**
|
||||
* Initialize a {@link ExtraKeyButton}.
|
||||
*
|
||||
* @param config The {@link JSONObject} containing the info to create the {@link ExtraKeyButton}.
|
||||
* @param extraKeyDisplayMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* display text mapping for the keys if a custom value is not defined
|
||||
* by {@link #KEY_DISPLAY_NAME}.
|
||||
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names.
|
||||
*/
|
||||
public ExtraKeyButton(@NonNull JSONObject config,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
this(config, null, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a {@link ExtraKeyButton}.
|
||||
*
|
||||
* @param config The {@link JSONObject} containing the info to create the {@link ExtraKeyButton}.
|
||||
* @param popup The {@link ExtraKeyButton} optional {@link #popup} button.
|
||||
* @param extraKeyDisplayMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* display text mapping for the keys if a custom value is not defined
|
||||
* by {@link #KEY_DISPLAY_NAME}.
|
||||
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names.
|
||||
*/
|
||||
public ExtraKeyButton(@NonNull JSONObject config, @Nullable ExtraKeyButton popup,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
String keyFromConfig = getStringFromJson(config, KEY_KEY_NAME);
|
||||
String macroFromConfig = getStringFromJson(config, KEY_MACRO);
|
||||
String[] keys;
|
||||
if (keyFromConfig != null && macroFromConfig != null) {
|
||||
throw new JSONException("Both key and macro can't be set for the same key. key: \"" + keyFromConfig + "\", macro: \"" + macroFromConfig + "\"");
|
||||
} else if (keyFromConfig != null) {
|
||||
keys = new String[]{keyFromConfig};
|
||||
this.macro = false;
|
||||
} else if (macroFromConfig != null) {
|
||||
keys = macroFromConfig.split(" ");
|
||||
this.macro = true;
|
||||
} else {
|
||||
throw new JSONException("All keys have to specify either key or macro");
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = replaceAlias(extraKeyAliasMap, keys[i]);
|
||||
}
|
||||
|
||||
this.key = TextUtils.join(" ", keys);
|
||||
|
||||
String displayFromConfig = getStringFromJson(config, KEY_DISPLAY_NAME);
|
||||
if (displayFromConfig != null) {
|
||||
this.display = displayFromConfig;
|
||||
} else {
|
||||
this.display = Arrays.stream(keys)
|
||||
.map(key -> extraKeyDisplayMap.get(key, key))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
public String getStringFromJson(@NonNull JSONObject config, @NonNull String key) {
|
||||
try {
|
||||
return config.getString(key);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get {@link #key}. */
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/** Check whether a {@link #macro} is defined or not. */
|
||||
public boolean isMacro() {
|
||||
return macro;
|
||||
}
|
||||
|
||||
/** Get {@link #display}. */
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
/** Get {@link #popup}. */
|
||||
@Nullable
|
||||
public ExtraKeyButton getPopup() {
|
||||
return popup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the alias with its actual key name if found in extraKeyAliasMap.
|
||||
*/
|
||||
public static String replaceAlias(@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap, String key) {
|
||||
return extraKeyAliasMap.get(key, key);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ExtraKeysConstants {
|
||||
|
||||
/** Defines the repetitive keys that can be passed to {@link ExtraKeysView#setRepetitiveKeys(List)}. */
|
||||
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL");
|
||||
|
||||
|
||||
|
||||
/** Defines the {@link KeyEvent} for common keys. */
|
||||
public static Map<String, Integer> PRIMARY_KEY_CODES_FOR_STRINGS = new HashMap<String, Integer>() {{
|
||||
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||
put("END", KeyEvent.KEYCODE_MOVE_END);
|
||||
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
||||
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
||||
put("INS", KeyEvent.KEYCODE_INSERT);
|
||||
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
||||
put("BKSP", KeyEvent.KEYCODE_DEL);
|
||||
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
||||
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||
put("F1", KeyEvent.KEYCODE_F1);
|
||||
put("F2", KeyEvent.KEYCODE_F2);
|
||||
put("F3", KeyEvent.KEYCODE_F3);
|
||||
put("F4", KeyEvent.KEYCODE_F4);
|
||||
put("F5", KeyEvent.KEYCODE_F5);
|
||||
put("F6", KeyEvent.KEYCODE_F6);
|
||||
put("F7", KeyEvent.KEYCODE_F7);
|
||||
put("F8", KeyEvent.KEYCODE_F8);
|
||||
put("F9", KeyEvent.KEYCODE_F9);
|
||||
put("F10", KeyEvent.KEYCODE_F10);
|
||||
put("F11", KeyEvent.KEYCODE_F11);
|
||||
put("F12", KeyEvent.KEYCODE_F12);
|
||||
}};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HashMap that implements Python dict.get(key, default) function.
|
||||
* Default java.util .get(key) is then the same as .get(key, null);
|
||||
*/
|
||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||
V get(K key, V defaultValue) {
|
||||
if (containsKey(key))
|
||||
return get(key);
|
||||
else
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExtraKeyDisplayMap extends CleverMap<String, String> {}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Multiple maps are available to quickly change
|
||||
* the style of the keys.
|
||||
*/
|
||||
|
||||
public static class EXTRA_KEY_DISPLAY_MAPS {
|
||||
/**
|
||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap CLASSIC_ARROWS_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap WELL_KNOWN_CHARACTERS_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
||||
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
||||
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
||||
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
||||
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
|
||||
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||
put("PASTE", "⎘"); // U+2398
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap LESS_KNOWN_CHARACTERS_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
||||
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
||||
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
||||
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap ARROW_TRIANGLE_VARIATION_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// alternative to classic arrow keys
|
||||
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap NOT_KNOWN_ISO_CHARACTERS = new ExtraKeyDisplayMap() {{
|
||||
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
||||
// put("FN", "FN"); // no ISO character exists
|
||||
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
||||
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
||||
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap NICER_LOOKING_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// nicer looking for most cases
|
||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||
}};
|
||||
|
||||
/**
|
||||
* Full Iso
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap FULL_ISO_CHAR_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(LESS_KNOWN_CHARACTERS_DISPLAY); // NEW
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
putAll(NOT_KNOWN_ISO_CHARACTERS); // NEW
|
||||
}};
|
||||
|
||||
/**
|
||||
* Only arrows
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap ARROWS_ONLY_CHAR_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Classic symbols and less known symbols
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap LOTS_OF_ARROWS_CHAR_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(LESS_KNOWN_CHARACTERS_DISPLAY); // NEW
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Some classic symbols everybody knows
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap DEFAULT_CHAR_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
// all other characters are displayed as themselves
|
||||
}};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Aliases for the keys
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap CONTROL_CHARS_ALIASES = new ExtraKeyDisplayMap() {{
|
||||
put("ESCAPE", "ESC");
|
||||
put("CONTROL", "CTRL");
|
||||
put("SHFT", "SHIFT");
|
||||
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
||||
put("FUNCTION", "FN");
|
||||
// no alias for ALT
|
||||
|
||||
// Directions are sometimes written as first and last letter for brevety
|
||||
put("LT", "LEFT");
|
||||
put("RT", "RIGHT");
|
||||
put("DN", "DOWN");
|
||||
// put("UP", "UP"); well, "UP" is already two letters
|
||||
|
||||
put("PAGEUP", "PGUP");
|
||||
put("PAGE_UP", "PGUP");
|
||||
put("PAGE UP", "PGUP");
|
||||
put("PAGE-UP", "PGUP");
|
||||
|
||||
// no alias for HOME
|
||||
// no alias for END
|
||||
|
||||
put("PAGEDOWN", "PGDN");
|
||||
put("PAGE_DOWN", "PGDN");
|
||||
put("PAGE-DOWN", "PGDN");
|
||||
|
||||
put("DELETE", "DEL");
|
||||
put("BACKSPACE", "BKSP");
|
||||
|
||||
// easier for writing in termux.properties
|
||||
put("BACKSLASH", "\\");
|
||||
put("QUOTE", "\"");
|
||||
put("APOSTROPHE", "'");
|
||||
}};
|
||||
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* A {@link Class} that defines the info needed by {@link ExtraKeysView} to display the extra key
|
||||
* views.
|
||||
*
|
||||
* The {@code propertiesInfo} passed to the constructors of this class must be json array of arrays.
|
||||
* Each array element of the json array will be considered a separate row of keys.
|
||||
* Each key can either be simple string that defines the name of the key or a json dict that defines
|
||||
* advance info for the key. The syntax can be `'KEY'` or `{key: 'KEY'}`.
|
||||
* For example `HOME` or `{key: 'HOME', ...}.
|
||||
*
|
||||
* In advance json dict mode, the key can also be a sequence of space separated keys instead of one
|
||||
* key. This can be done by replacing `key` key/value pair of the dict with a `macro` key/value pair.
|
||||
* The syntax is `{macro: 'KEY COMBINATION'}`. For example {macro: 'HOME RIGHT', ...}.
|
||||
*
|
||||
* In advance json dict mode, you can define a nested json dict with the `popup` key which will be
|
||||
* used as the popup key and will be triggered on swipe up. The syntax can be
|
||||
* `{key: 'KEY', popup: 'POPUP_KEY'}` or `{key: 'KEY', popup: {macro: 'KEY COMBINATION', display: 'Key combo'}}`.
|
||||
* For example `{key: 'HOME', popup: {KEY: 'END', ...}, ...}`.
|
||||
*
|
||||
* In advance json dict mode, the key can also have a custom display name that can be used as the
|
||||
* text to display on the button by defining the `display` key. The syntax is `{display: 'DISPLAY'}`.
|
||||
* For example {display: 'Custom name', ...}.
|
||||
*
|
||||
* Examples:
|
||||
* {@code
|
||||
* # Empty:
|
||||
* []
|
||||
*
|
||||
* # Single row:
|
||||
* [[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]
|
||||
*
|
||||
* # 2 row:
|
||||
* [['ESC','/',{key: '-', popup: '|'},'HOME','UP','END','PGUP'],
|
||||
* ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]
|
||||
*
|
||||
* # Advance:
|
||||
* [[
|
||||
* {key: ESC, popup: {macro: "CTRL f d", display: "tmux exit"}},
|
||||
* {key: CTRL, popup: {macro: "CTRL f BKSP", display: "tmux ←"}},
|
||||
* {key: ALT, popup: {macro: "CTRL f TAB", display: "tmux →"}},
|
||||
* {key: TAB, popup: {macro: "ALT a", display: A-a}},
|
||||
* {key: LEFT, popup: HOME},
|
||||
* {key: DOWN, popup: PGDN},
|
||||
* {key: UP, popup: PGUP},
|
||||
* {key: RIGHT, popup: END},
|
||||
* {macro: "ALT j", display: A-j, popup: {macro: "ALT g", display: A-g}},
|
||||
* {key: KEYBOARD, popup: {macro: "CTRL d", display: exit}}
|
||||
* ]]
|
||||
*
|
||||
* }
|
||||
*
|
||||
* Aliases are also allowed for the keys that you can pass as {@code extraKeyAliasMap}. Check
|
||||
* {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}.
|
||||
*
|
||||
* Its up to the {@link ExtraKeysView.IExtraKeysView} client on how to handle individual key values
|
||||
* of an {@link ExtraKeyButton}. They are sent as is via
|
||||
* {@link ExtraKeysView.IExtraKeysView#onExtraKeyButtonClick(View, ExtraKeyButton, Button)}. The
|
||||
* {@link com.termux.shared.terminal.io.TerminalExtraKeys} which is an implementation of the interface,
|
||||
* checks if the key is one of {@link ExtraKeysConstants#PRIMARY_KEY_CODES_FOR_STRINGS} and generates
|
||||
* a {@link android.view.KeyEvent} for it, and if its not, then converts the key to code points by
|
||||
* calling {@link CharSequence#codePoints()} and passes them to the terminal as literal strings.
|
||||
*
|
||||
* Examples:
|
||||
* {@code
|
||||
* "ENTER" will trigger the ENTER keycode
|
||||
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
|
||||
* "→" will input a "→" character
|
||||
* "−" will input a "−" character
|
||||
* "-_-" will input the string "-_-"
|
||||
* }
|
||||
*
|
||||
* For more info, check https://wiki.termux.com/wiki/Touch_Keyboard.
|
||||
*/
|
||||
public class ExtraKeysInfo {
|
||||
|
||||
/**
|
||||
* Matrix of buttons to be displayed in {@link ExtraKeysView}.
|
||||
*/
|
||||
private final ExtraKeyButton[][] mButtons;
|
||||
|
||||
/**
|
||||
* Initialize {@link ExtraKeysInfo}.
|
||||
*
|
||||
* @param propertiesInfo The {@link String} containing the info to create the {@link ExtraKeysInfo}.
|
||||
* Check the class javadoc for details.
|
||||
* @param style The style to pass to {@link #getCharDisplayMapForStyle(String)} to get the
|
||||
* {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the display text
|
||||
* mapping for the keys if a custom value is not defined by
|
||||
* {@link ExtraKeyButton#KEY_DISPLAY_NAME} for a key.
|
||||
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names. You can create your own or
|
||||
* optionally pass {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}.
|
||||
*/
|
||||
public ExtraKeysInfo(@NonNull String propertiesInfo, String style,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
mButtons = initExtraKeysInfo(propertiesInfo, getCharDisplayMapForStyle(style), extraKeyAliasMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize {@link ExtraKeysInfo}.
|
||||
*
|
||||
* @param propertiesInfo The {@link String} containing the info to create the {@link ExtraKeysInfo}.
|
||||
* Check the class javadoc for details.
|
||||
* @param extraKeyDisplayMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* display text mapping for the keys if a custom value is not defined
|
||||
* by {@link ExtraKeyButton#KEY_DISPLAY_NAME} for a key. You can create
|
||||
* your own or optionally pass one of the values defined in
|
||||
* {@link #getCharDisplayMapForStyle(String)}.
|
||||
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names. You can create your own or
|
||||
* optionally pass {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}.
|
||||
*/
|
||||
public ExtraKeysInfo(@NonNull String propertiesInfo,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
mButtons = initExtraKeysInfo(propertiesInfo, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
}
|
||||
|
||||
private ExtraKeyButton[][] initExtraKeysInfo(@NonNull String propertiesInfo,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
// Convert String propertiesInfo to Array of Arrays
|
||||
JSONArray arr = new JSONArray(propertiesInfo);
|
||||
Object[][] matrix = new Object[arr.length()][];
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONArray line = arr.getJSONArray(i);
|
||||
matrix[i] = new Object[line.length()];
|
||||
for (int j = 0; j < line.length(); j++) {
|
||||
matrix[i][j] = line.get(j);
|
||||
}
|
||||
}
|
||||
|
||||
// convert matrix to buttons
|
||||
ExtraKeyButton[][] buttons = new ExtraKeyButton[matrix.length][];
|
||||
for (int i = 0; i < matrix.length; i++) {
|
||||
buttons[i] = new ExtraKeyButton[matrix[i].length];
|
||||
for (int j = 0; j < matrix[i].length; j++) {
|
||||
Object key = matrix[i][j];
|
||||
|
||||
JSONObject jobject = normalizeKeyConfig(key);
|
||||
|
||||
ExtraKeyButton button;
|
||||
|
||||
if (!jobject.has(ExtraKeyButton.KEY_POPUP)) {
|
||||
// no popup
|
||||
button = new ExtraKeyButton(jobject, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
} else {
|
||||
// a popup
|
||||
JSONObject popupJobject = normalizeKeyConfig(jobject.get(ExtraKeyButton.KEY_POPUP));
|
||||
ExtraKeyButton popup = new ExtraKeyButton(popupJobject, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
button = new ExtraKeyButton(jobject, popup, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
}
|
||||
|
||||
buttons[i][j] = button;
|
||||
}
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert "value" -> {"key": "value"}. Required by
|
||||
* {@link ExtraKeyButton#ExtraKeyButton(JSONObject, ExtraKeyButton, ExtraKeysConstants.ExtraKeyDisplayMap, ExtraKeysConstants.ExtraKeyDisplayMap)}.
|
||||
*/
|
||||
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||
JSONObject jobject;
|
||||
if (key instanceof String) {
|
||||
jobject = new JSONObject();
|
||||
jobject.put(ExtraKeyButton.KEY_KEY_NAME, key);
|
||||
} else if (key instanceof JSONObject) {
|
||||
jobject = (JSONObject) key;
|
||||
} else {
|
||||
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||
}
|
||||
return jobject;
|
||||
}
|
||||
|
||||
public ExtraKeyButton[][] getMatrix() {
|
||||
return mButtons;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ExtraKeysConstants.ExtraKeyDisplayMap getCharDisplayMapForStyle(String style) {
|
||||
switch (style) {
|
||||
case "arrows-only":
|
||||
return EXTRA_KEY_DISPLAY_MAPS.ARROWS_ONLY_CHAR_DISPLAY;
|
||||
case "arrows-all":
|
||||
return EXTRA_KEY_DISPLAY_MAPS.LOTS_OF_ARROWS_CHAR_DISPLAY;
|
||||
case "all":
|
||||
return EXTRA_KEY_DISPLAY_MAPS.FULL_ISO_CHAR_DISPLAY;
|
||||
case "none":
|
||||
return new ExtraKeysConstants.ExtraKeyDisplayMap();
|
||||
default:
|
||||
return EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,646 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link View} showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||
* keyboards.
|
||||
*
|
||||
* To use it, add following to a layout file and import it in your activity layout file or inflate
|
||||
* it with a {@link androidx.viewpager.widget.ViewPager}.:
|
||||
* {@code
|
||||
* <?xml version="1.0" encoding="utf-8"?>
|
||||
* <com.termux.shared.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
* android:id="@+id/extra_keys"
|
||||
* style="?android:attr/buttonBarStyle"
|
||||
* android:layout_width="match_parent"
|
||||
* android:layout_height="match_parent"
|
||||
* android:layout_alignParentBottom="true"
|
||||
* android:orientation="horizontal" />
|
||||
* }
|
||||
*
|
||||
* Then in your activity, get its reference by a call to {@link android.app.Activity#findViewById(int)}
|
||||
* or {@link LayoutInflater#inflate(int, ViewGroup)} if using {@link androidx.viewpager.widget.ViewPager}.
|
||||
* Then call {@link #setExtraKeysViewClient(IExtraKeysView)} and pass it the implementation of
|
||||
* {@link IExtraKeysView} so that you can receive callbacks. You can also override other values set
|
||||
* in {@link ExtraKeysView#ExtraKeysView(Context, AttributeSet)} by calling the respective functions.
|
||||
* If you extend {@link ExtraKeysView}, you can also set them in the constructor, but do call super().
|
||||
*
|
||||
* After this you will have to make a call to {@link ExtraKeysView#reload(ExtraKeysInfo) and pass
|
||||
* it the {@link ExtraKeysInfo} to load and display the extra keys. Read its class javadocs for more
|
||||
* info on how to create it.
|
||||
*
|
||||
* Termux app defines the view in res/layout/view_terminal_toolbar_extra_keys and
|
||||
* inflates it in TerminalToolbarViewPager.instantiateItem() and sets the {@link ExtraKeysView} client
|
||||
* and calls {@link ExtraKeysView#reload(ExtraKeysInfo).
|
||||
* The {@link ExtraKeysInfo} is created by TermuxAppSharedProperties.setExtraKeys().
|
||||
* Then its got and the view height is adjusted in TermuxActivity.setTerminalToolbarHeight().
|
||||
* The client used is TermuxTerminalExtraKeys, which extends
|
||||
* {@link com.termux.shared.terminal.io.TerminalExtraKeys} to handle Termux app specific logic and
|
||||
* leave the rest to the super class.
|
||||
*/
|
||||
public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
/** The client for the {@link ExtraKeysView}. */
|
||||
public interface IExtraKeysView {
|
||||
|
||||
/**
|
||||
* This is called by {@link ExtraKeysView} when a button is clicked. This is also called
|
||||
* for {@link #mRepetitiveKeys} and {@link ExtraKeyButton} that have a popup set.
|
||||
* However, this is not called for {@link #mSpecialButtons}, whose state can instead be read
|
||||
* via a call to {@link #readSpecialButton(SpecialButton, boolean)}.
|
||||
*
|
||||
* @param view The view that was clicked.
|
||||
* @param buttonInfo The {@link ExtraKeyButton} for the button that was clicked.
|
||||
* The button may be a {@link ExtraKeyButton#KEY_MACRO} set which can be
|
||||
* checked with a call to {@link ExtraKeyButton#isMacro()}.
|
||||
* @param button The {@link Button} that was clicked.
|
||||
*/
|
||||
void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, Button button);
|
||||
|
||||
/**
|
||||
* This is called by {@link ExtraKeysView} when a button is clicked so that the client
|
||||
* can perform any hepatic feedback. This is only called in the {@link Button.OnClickListener}
|
||||
* and not for every repeat. Its also called for {@link #mSpecialButtons}.
|
||||
*
|
||||
* @param view The view that was clicked.
|
||||
* @param buttonInfo The {@link ExtraKeyButton} for the button that was clicked.
|
||||
* @param button The {@link Button} that was clicked.
|
||||
* @return Return {@code true} if the client handled the feedback, otherwise {@code false}
|
||||
* so that {@link ExtraKeysView#performExtraKeyButtonHapticFeedback(View, ExtraKeyButton, Button)}
|
||||
* can handle it depending on system settings.
|
||||
*/
|
||||
boolean performExtraKeyButtonHapticFeedback(View view, ExtraKeyButton buttonInfo, Button button);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** Defines the default value for {@link #mButtonTextColor}. */
|
||||
public static final int DEFAULT_BUTTON_TEXT_COLOR = 0xFFFFFFFF;
|
||||
/** Defines the default value for {@link #mButtonActiveTextColor}. */
|
||||
public static final int DEFAULT_BUTTON_ACTIVE_TEXT_COLOR = 0xFF80DEEA;
|
||||
/** Defines the default value for {@link #mButtonBackgroundColor}. */
|
||||
public static final int DEFAULT_BUTTON_BACKGROUND_COLOR = 0x00000000;
|
||||
/** Defines the default value for {@link #mButtonActiveBackgroundColor}. */
|
||||
public static final int DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR = 0xFF7F7F7F;
|
||||
|
||||
|
||||
/** Defines the minimum allowed duration in milliseconds for {@link #mLongPressTimeout}. */
|
||||
public static final int MIN_LONG_PRESS_DURATION = 200;
|
||||
/** Defines the maximum allowed duration in milliseconds for {@link #mLongPressTimeout}. */
|
||||
public static final int MAX_LONG_PRESS_DURATION = 3000;
|
||||
/** Defines the fallback duration in milliseconds for {@link #mLongPressTimeout}. */
|
||||
public static final int FALLBACK_LONG_PRESS_DURATION = 400;
|
||||
|
||||
/** Defines the minimum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. */
|
||||
public static final int MIN_LONG_PRESS__REPEAT_DELAY = 5;
|
||||
/** Defines the maximum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. */
|
||||
public static final int MAX_LONG_PRESS__REPEAT_DELAY = 2000;
|
||||
/** Defines the default duration in milliseconds for {@link #mLongPressRepeatDelay}. */
|
||||
public static final int DEFAULT_LONG_PRESS_REPEAT_DELAY = 80;
|
||||
|
||||
|
||||
|
||||
/** The implementation of the {@link IExtraKeysView} that acts as a client for the {@link ExtraKeysView}. */
|
||||
private IExtraKeysView mExtraKeysViewClient;
|
||||
|
||||
/** The map for the {@link SpecialButton} and their {@link SpecialButtonState}. Defaults to
|
||||
* the one returned by {@link #getDefaultSpecialButtons(ExtraKeysView)}. */
|
||||
private Map<SpecialButton, SpecialButtonState> mSpecialButtons;
|
||||
|
||||
/** The keys for the {@link SpecialButton} added to {@link #mSpecialButtons}. This is automatically
|
||||
* set when the call to {@link #setSpecialButtons(Map)} is made. */
|
||||
private Set<String> mSpecialButtonsKeys;
|
||||
|
||||
|
||||
/**
|
||||
* The list of keys for which auto repeat of key should be triggered if its extra keys button
|
||||
* is long pressed. This is done by calling {@link IExtraKeysView#onExtraKeyButtonClick(View, ExtraKeyButton, Button)}
|
||||
* every {@link #mLongPressRepeatDelay} seconds after {@link #mLongPressTimeout} has passed.
|
||||
* The default keys are defined by {@link ExtraKeysConstants#PRIMARY_REPETITIVE_KEYS}.
|
||||
*/
|
||||
private List<String> mRepetitiveKeys;
|
||||
|
||||
|
||||
/** The text color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_TEXT_COLOR}. */
|
||||
private int mButtonTextColor;
|
||||
/** The text color for the extra keys button when its active.
|
||||
* Defaults to {@link #DEFAULT_BUTTON_ACTIVE_TEXT_COLOR}. */
|
||||
private int mButtonActiveTextColor;
|
||||
/** The background color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_BACKGROUND_COLOR}. */
|
||||
private int mButtonBackgroundColor;
|
||||
/** The background color for the extra keys button when its active. Defaults to
|
||||
* {@link #DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR}. */
|
||||
private int mButtonActiveBackgroundColor;
|
||||
|
||||
|
||||
/**
|
||||
* Defines the duration in milliseconds before a press turns into a long press. The default
|
||||
* duration used is the one returned by a call to {@link ViewConfiguration#getLongPressTimeout()}
|
||||
* which will return the system defined duration which can be changed in accessibility settings.
|
||||
* The duration must be in between {@link #MIN_LONG_PRESS_DURATION} and {@link #MAX_LONG_PRESS_DURATION},
|
||||
* otherwise {@link #FALLBACK_LONG_PRESS_DURATION} is used.
|
||||
*/
|
||||
private int mLongPressTimeout;
|
||||
|
||||
/**
|
||||
* Defines the duration in milliseconds for the delay between trigger of each repeat of
|
||||
* {@link #mRepetitiveKeys}. The default value is defined by {@link #DEFAULT_LONG_PRESS_REPEAT_DELAY}.
|
||||
* The duration must be in between {@link #MIN_LONG_PRESS__REPEAT_DELAY} and
|
||||
* {@link #MAX_LONG_PRESS__REPEAT_DELAY}, otherwise {@link #DEFAULT_LONG_PRESS_REPEAT_DELAY} is used.
|
||||
*/
|
||||
private int mLongPressRepeatDelay;
|
||||
|
||||
|
||||
/** The popup window shown if {@link ExtraKeyButton#getPopup()} returns a {@code non-null} value
|
||||
* and a swipe up action is done on an extra key. */
|
||||
private PopupWindow mPopupWindow;
|
||||
|
||||
private ScheduledExecutorService mScheduledExecutor;
|
||||
private Handler mHandler;
|
||||
private SpecialButtonsLongHoldRunnable mSpecialButtonsLongHoldRunnable;
|
||||
private int mLongPressCount;
|
||||
|
||||
|
||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setRepetitiveKeys(ExtraKeysConstants.PRIMARY_REPETITIVE_KEYS);
|
||||
setSpecialButtons(getDefaultSpecialButtons(this));
|
||||
setButtonColors(DEFAULT_BUTTON_TEXT_COLOR, DEFAULT_BUTTON_ACTIVE_TEXT_COLOR,
|
||||
DEFAULT_BUTTON_BACKGROUND_COLOR, DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR);
|
||||
setLongPressTimeout(ViewConfiguration.getLongPressTimeout());
|
||||
setLongPressRepeatDelay(DEFAULT_LONG_PRESS_REPEAT_DELAY);
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mExtraKeysViewClient}. */
|
||||
public IExtraKeysView getExtraKeysViewClient() {
|
||||
return mExtraKeysViewClient;
|
||||
}
|
||||
|
||||
/** Set {@link #mExtraKeysViewClient}. */
|
||||
public void setExtraKeysViewClient(IExtraKeysView extraKeysViewClient) {
|
||||
mExtraKeysViewClient = extraKeysViewClient;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mRepetitiveKeys}. */
|
||||
public List<String> getRepetitiveKeys() {
|
||||
if (mRepetitiveKeys == null) return null;
|
||||
return mRepetitiveKeys.stream().map(String::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** Set {@link #mRepetitiveKeys}. Must not be {@code null}. */
|
||||
public void setRepetitiveKeys(@NonNull List<String> repetitiveKeys) {
|
||||
mRepetitiveKeys = repetitiveKeys;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mSpecialButtons}. */
|
||||
public Map<SpecialButton, SpecialButtonState> getSpecialButtons() {
|
||||
if (mSpecialButtons == null) return null;
|
||||
return mSpecialButtons.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/** Get {@link #mSpecialButtonsKeys}. */
|
||||
public Set<String> getSpecialButtonsKeys() {
|
||||
if (mSpecialButtonsKeys == null) return null;
|
||||
return mSpecialButtonsKeys.stream().map(String::new).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/** Set {@link #mSpecialButtonsKeys}. Must not be {@code null}. */
|
||||
public void setSpecialButtons(@NonNull Map<SpecialButton, SpecialButtonState> specialButtons) {
|
||||
mSpecialButtons = specialButtons;
|
||||
mSpecialButtonsKeys = this.mSpecialButtons.keySet().stream().map(SpecialButton::getKey).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the {@link ExtraKeysView} button colors.
|
||||
*
|
||||
* @param buttonTextColor The value for {@link #mButtonTextColor}.
|
||||
* @param buttonActiveTextColor The value for {@link #mButtonActiveTextColor}.
|
||||
* @param buttonBackgroundColor The value for {@link #mButtonBackgroundColor}.
|
||||
* @param buttonActiveBackgroundColor The value for {@link #mButtonActiveBackgroundColor}.
|
||||
*/
|
||||
public void setButtonColors(int buttonTextColor, int buttonActiveTextColor, int buttonBackgroundColor, int buttonActiveBackgroundColor) {
|
||||
mButtonTextColor = buttonTextColor;
|
||||
mButtonActiveTextColor = buttonActiveTextColor;
|
||||
mButtonBackgroundColor = buttonBackgroundColor;
|
||||
mButtonActiveBackgroundColor = buttonActiveBackgroundColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mButtonTextColor}. */
|
||||
public int getButtonTextColor() {
|
||||
return mButtonTextColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonTextColor}. */
|
||||
public void setButtonTextColor(int buttonTextColor) {
|
||||
mButtonTextColor = buttonTextColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mButtonActiveTextColor}. */
|
||||
public int getButtonActiveTextColor() {
|
||||
return mButtonActiveTextColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonActiveTextColor}. */
|
||||
public void setButtonActiveTextColor(int buttonActiveTextColor) {
|
||||
mButtonActiveTextColor = buttonActiveTextColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mButtonBackgroundColor}. */
|
||||
public int getButtonBackgroundColor() {
|
||||
return mButtonBackgroundColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonBackgroundColor}. */
|
||||
public void setButtonBackgroundColor(int buttonBackgroundColor) {
|
||||
mButtonBackgroundColor = buttonBackgroundColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mButtonActiveBackgroundColor}. */
|
||||
public int getButtonActiveBackgroundColor() {
|
||||
return mButtonActiveBackgroundColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonActiveBackgroundColor}. */
|
||||
public void setButtonActiveBackgroundColor(int buttonActiveBackgroundColor) {
|
||||
mButtonActiveBackgroundColor = buttonActiveBackgroundColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mLongPressTimeout}. */
|
||||
public int getLongPressTimeout() {
|
||||
return mLongPressTimeout;
|
||||
}
|
||||
|
||||
/** Set {@link #mLongPressTimeout}. */
|
||||
public void setLongPressTimeout(int longPressDuration) {
|
||||
if (longPressDuration >= MIN_LONG_PRESS_DURATION && longPressDuration <= MAX_LONG_PRESS_DURATION) {
|
||||
mLongPressTimeout = longPressDuration;
|
||||
} else {
|
||||
mLongPressTimeout = FALLBACK_LONG_PRESS_DURATION;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get {@link #mLongPressRepeatDelay}. */
|
||||
public int getLongPressRepeatDelay() {
|
||||
return mLongPressRepeatDelay;
|
||||
}
|
||||
|
||||
/** Set {@link #mLongPressRepeatDelay}. */
|
||||
public void setLongPressRepeatDelay(int longPressRepeatDelay) {
|
||||
if (mLongPressRepeatDelay >= MIN_LONG_PRESS__REPEAT_DELAY && mLongPressRepeatDelay <= MAX_LONG_PRESS__REPEAT_DELAY) {
|
||||
mLongPressRepeatDelay = longPressRepeatDelay;
|
||||
} else {
|
||||
mLongPressRepeatDelay = DEFAULT_LONG_PRESS_REPEAT_DELAY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Get the default map that can be used for {@link #mSpecialButtons}. */
|
||||
@NonNull
|
||||
public Map<SpecialButton, SpecialButtonState> getDefaultSpecialButtons(ExtraKeysView extraKeysView) {
|
||||
return new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||
put(SpecialButton.CTRL, new SpecialButtonState(extraKeysView));
|
||||
put(SpecialButton.ALT, new SpecialButtonState(extraKeysView));
|
||||
put(SpecialButton.SHIFT, new SpecialButtonState(extraKeysView));
|
||||
put(SpecialButton.FN, new SpecialButtonState(extraKeysView));
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reload this instance of {@link ExtraKeysView} with the info passed in {@code extraKeysInfo}.
|
||||
*
|
||||
* @param extraKeysInfo The {@link ExtraKeysInfo} that defines the necessary info for the extra keys.
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void reload(ExtraKeysInfo extraKeysInfo) {
|
||||
if (extraKeysInfo == null)
|
||||
return;
|
||||
|
||||
for(SpecialButtonState state : mSpecialButtons.values())
|
||||
state.buttons = new ArrayList<>();
|
||||
|
||||
removeAllViews();
|
||||
|
||||
ExtraKeyButton[][] buttons = extraKeysInfo.getMatrix();
|
||||
|
||||
setRowCount(buttons.length);
|
||||
setColumnCount(maximumLength(buttons));
|
||||
|
||||
for (int row = 0; row < buttons.length; row++) {
|
||||
for (int col = 0; col < buttons[row].length; col++) {
|
||||
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||
|
||||
Button button;
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
}
|
||||
|
||||
button.setText(buttonInfo.getDisplay());
|
||||
button.setTextColor(mButtonTextColor);
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
|
||||
button.setOnClickListener(view -> {
|
||||
performExtraKeyButtonHapticFeedback(view, buttonInfo, button);
|
||||
onAnyExtraKeyButtonClick(view, buttonInfo, button);
|
||||
});
|
||||
|
||||
button.setOnTouchListener((view, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
view.setBackgroundColor(mButtonActiveBackgroundColor);
|
||||
// Start long press scheduled executors which will be stopped in next MotionEvent
|
||||
startScheduledExecutors(view, buttonInfo, button);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
// Show popup on swipe up
|
||||
if (mPopupWindow == null && event.getY() < 0) {
|
||||
stopScheduledExecutors();
|
||||
view.setBackgroundColor(mButtonBackgroundColor);
|
||||
showPopup(view, buttonInfo.getPopup());
|
||||
}
|
||||
if (mPopupWindow != null && event.getY() > 0) {
|
||||
view.setBackgroundColor(mButtonActiveBackgroundColor);
|
||||
dismissPopup();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
view.setBackgroundColor(mButtonBackgroundColor);
|
||||
stopScheduledExecutors();
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
view.setBackgroundColor(mButtonBackgroundColor);
|
||||
stopScheduledExecutors();
|
||||
// If ACTION_UP up was not from a repetitive key or was with a key with a popup button
|
||||
if (mLongPressCount == 0 || mPopupWindow != null) {
|
||||
// Trigger popup button click if swipe up complete
|
||||
if (mPopupWindow != null) {
|
||||
dismissPopup();
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
onAnyExtraKeyButtonClick(view, buttonInfo.getPopup(), button);
|
||||
}
|
||||
} else {
|
||||
view.performClick();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
LayoutParams param = new GridLayout.LayoutParams();
|
||||
param.width = 0;
|
||||
param.height = 0;
|
||||
param.setMargins(0, 0, 0, 0);
|
||||
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
|
||||
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
||||
button.setLayoutParams(param);
|
||||
|
||||
addView(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
if (mExtraKeysViewClient != null)
|
||||
mExtraKeysViewClient.onExtraKeyButtonClick(view, buttonInfo, button);
|
||||
}
|
||||
|
||||
private void performExtraKeyButtonHapticFeedback(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
if (mExtraKeysViewClient != null) {
|
||||
// If client handled the feedback, then just return
|
||||
if (mExtraKeysViewClient.performExtraKeyButtonHapticFeedback(view, buttonInfo, button))
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
button.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else {
|
||||
// Perform haptic feedback only if no total silence mode enabled.
|
||||
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
|
||||
button.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void onAnyExtraKeyButtonClick(View view, @NonNull ExtraKeyButton buttonInfo, Button button) {
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
if (mLongPressCount > 0) return;
|
||||
SpecialButtonState state = mSpecialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||
if (state == null) return;
|
||||
|
||||
// Toggle active state and disable lock state if new state is not active
|
||||
state.setIsActive(!state.isActive);
|
||||
if (!state.isActive)
|
||||
state.setIsLocked(false);
|
||||
} else {
|
||||
onExtraKeyButtonClick(view, buttonInfo, button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void startScheduledExecutors(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
stopScheduledExecutors();
|
||||
mLongPressCount = 0;
|
||||
if (mRepetitiveKeys.contains(buttonInfo.getKey())) {
|
||||
// Auto repeat key if long pressed until ACTION_UP stops it by calling stopScheduledExecutors.
|
||||
// Currently, only one (last) repeat key can run at a time. Old ones are stopped.
|
||||
mScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
mScheduledExecutor.scheduleWithFixedDelay(() -> {
|
||||
mLongPressCount++;
|
||||
onExtraKeyButtonClick(view, buttonInfo, button);
|
||||
}, mLongPressTimeout, mLongPressRepeatDelay, TimeUnit.MILLISECONDS);
|
||||
} else if (isSpecialButton(buttonInfo)) {
|
||||
// Lock the key if long pressed by running mSpecialButtonsLongHoldRunnable after
|
||||
// waiting for mLongPressTimeout milliseconds. If user does not long press, then the
|
||||
// ACTION_UP triggered will cancel the runnable by calling stopScheduledExecutors before
|
||||
// it has a chance to run.
|
||||
SpecialButtonState state = mSpecialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||
if (state == null) return;
|
||||
if (mHandler == null)
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mSpecialButtonsLongHoldRunnable = new SpecialButtonsLongHoldRunnable(state);
|
||||
mHandler.postDelayed(mSpecialButtonsLongHoldRunnable, mLongPressTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopScheduledExecutors() {
|
||||
if (mScheduledExecutor != null) {
|
||||
mScheduledExecutor.shutdownNow();
|
||||
mScheduledExecutor = null;
|
||||
}
|
||||
|
||||
if (mSpecialButtonsLongHoldRunnable != null && mHandler != null) {
|
||||
mHandler.removeCallbacks(mSpecialButtonsLongHoldRunnable);
|
||||
mSpecialButtonsLongHoldRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class SpecialButtonsLongHoldRunnable implements Runnable {
|
||||
private final SpecialButtonState mState;
|
||||
|
||||
public SpecialButtonsLongHoldRunnable(SpecialButtonState state) {
|
||||
mState = state;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
// Toggle active and lock state
|
||||
mState.setIsLocked(!mState.isActive);
|
||||
mState.setIsActive(!mState.isActive);
|
||||
mLongPressCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void showPopup(View view, ExtraKeyButton extraButton) {
|
||||
int width = view.getMeasuredWidth();
|
||||
int height = view.getMeasuredHeight();
|
||||
Button button;
|
||||
if (isSpecialButton(extraButton)) {
|
||||
button = createSpecialButton(extraButton.getKey(), false);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(mButtonTextColor);
|
||||
}
|
||||
button.setText(extraButton.getDisplay());
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
button.setMinHeight(0);
|
||||
button.setMinWidth(0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinimumHeight(0);
|
||||
button.setWidth(width);
|
||||
button.setHeight(height);
|
||||
button.setBackgroundColor(mButtonActiveBackgroundColor);
|
||||
mPopupWindow = new PopupWindow(this);
|
||||
mPopupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
||||
mPopupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
||||
mPopupWindow.setContentView(button);
|
||||
mPopupWindow.setOutsideTouchable(true);
|
||||
mPopupWindow.setFocusable(false);
|
||||
mPopupWindow.showAsDropDown(view, 0, -2 * height);
|
||||
}
|
||||
|
||||
private void dismissPopup() {
|
||||
mPopupWindow.setContentView(null);
|
||||
mPopupWindow.dismiss();
|
||||
mPopupWindow = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Check whether a {@link ExtraKeyButton} is a {@link SpecialButton}. */
|
||||
public boolean isSpecialButton(ExtraKeyButton button) {
|
||||
return mSpecialButtonsKeys.contains(button.getKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read whether {@link SpecialButton} registered in {@link #mSpecialButtons} is active or not.
|
||||
*
|
||||
* @param specialButton The {@link SpecialButton} to read.
|
||||
* @param autoSetInActive Set to {@code true} if {@link SpecialButtonState#isActive} should be
|
||||
* set {@code false} if button is not locked.
|
||||
* @return Returns {@code null} if button does not exist in {@link #mSpecialButtons}. If button
|
||||
* exists, then returns {@code true} if the button is created in {@link ExtraKeysView}
|
||||
* and is active, otherwise {@code false}.
|
||||
*/
|
||||
@Nullable
|
||||
public Boolean readSpecialButton(SpecialButton specialButton, boolean autoSetInActive) {
|
||||
SpecialButtonState state = mSpecialButtons.get(specialButton);
|
||||
if (state == null) return null;
|
||||
|
||||
if (!state.isCreated || !state.isActive)
|
||||
return false;
|
||||
|
||||
// Disable active state only if not locked
|
||||
if (autoSetInActive && !state.isLocked)
|
||||
state.setIsActive(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||
SpecialButtonState state = mSpecialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||
if (state == null) return null;
|
||||
state.setIsCreated(true);
|
||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(state.isActive ? mButtonActiveTextColor : mButtonTextColor);
|
||||
if (needUpdate) {
|
||||
state.buttons.add(button);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* General util function to compute the longest column length in a matrix.
|
||||
*/
|
||||
static int maximumLength(Object[][] matrix) {
|
||||
int m = 0;
|
||||
for (Object[] row : matrix)
|
||||
m = Math.max(m, row.length);
|
||||
return m;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/** The {@link Class} that implements special buttons for {@link ExtraKeysView}. */
|
||||
public class SpecialButton {
|
||||
|
||||
private static final HashMap<String, SpecialButton> map = new HashMap<>();
|
||||
|
||||
public static final SpecialButton CTRL = new SpecialButton("CTRL");
|
||||
public static final SpecialButton ALT = new SpecialButton("ALT");
|
||||
public static final SpecialButton SHIFT = new SpecialButton("SHIFT");
|
||||
public static final SpecialButton FN = new SpecialButton("FN");
|
||||
|
||||
/** The special button key. */
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* Initialize a {@link SpecialButton}.
|
||||
*
|
||||
* @param key The unique key name for the special button. The key is registered in {@link #map}
|
||||
* with which the {@link SpecialButton} can be retrieved via a call to
|
||||
* {@link #valueOf(String)}.
|
||||
*/
|
||||
public SpecialButton(@NonNull final String key) {
|
||||
this.key = key;
|
||||
map.put(key, this);
|
||||
}
|
||||
|
||||
/** Get {@link #key} for this {@link SpecialButton}. */
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link SpecialButton} for {@code key}.
|
||||
*
|
||||
* @param key The unique key name for the special button.
|
||||
*/
|
||||
public static SpecialButton valueOf(String key) {
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return key;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.widget.Button;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** The {@link Class} that maintains a state of a {@link SpecialButton} */
|
||||
public class SpecialButtonState {
|
||||
|
||||
/** If special button has been created for the {@link ExtraKeysView}. */
|
||||
boolean isCreated = false;
|
||||
/** If special button is active. */
|
||||
boolean isActive = false;
|
||||
/** If special button is locked due to long hold on it and should not be deactivated if its
|
||||
* state is read. */
|
||||
boolean isLocked = false;
|
||||
|
||||
List<Button> buttons = new ArrayList<>();
|
||||
|
||||
ExtraKeysView mExtraKeysView;
|
||||
|
||||
/**
|
||||
* Initialize a {@link SpecialButtonState} to maintain state of a {@link SpecialButton}.
|
||||
*
|
||||
* @param extraKeysView The {@link ExtraKeysView} instance in which the {@link SpecialButton}
|
||||
* is to be registered.
|
||||
*/
|
||||
public SpecialButtonState(ExtraKeysView extraKeysView) {
|
||||
mExtraKeysView = extraKeysView;
|
||||
}
|
||||
|
||||
/** Set {@link #isCreated}. */
|
||||
public void setIsCreated(boolean value) {
|
||||
isCreated = value;
|
||||
}
|
||||
|
||||
/** Set {@link #isActive}. */
|
||||
public void setIsActive(boolean value) {
|
||||
isActive = value;
|
||||
buttons.forEach(button -> button.setTextColor(value ? mExtraKeysView.getButtonActiveTextColor() : mExtraKeysView.getButtonTextColor()));
|
||||
}
|
||||
|
||||
/** Set {@link #isLocked}. */
|
||||
public void setIsLocked(boolean value) {
|
||||
isLocked = value;
|
||||
}
|
||||
|
||||
}
|
|
@ -8,6 +8,7 @@ import android.graphics.Point;
|
|||
import android.graphics.Rect;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -218,4 +219,18 @@ public class ViewUtils {
|
|||
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
|
||||
public static void setLayoutMarginsInDp(@NonNull View view, int left, int top, int right, int bottom) {
|
||||
Context context = view.getContext();
|
||||
setLayoutMarginsInPixels(view, dpToPx(context, left), dpToPx(context, top), dpToPx(context, right), dpToPx(context, bottom));
|
||||
}
|
||||
|
||||
public static void setLayoutMarginsInPixels(@NonNull View view, int left, int top, int right, int bottom) {
|
||||
if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
|
||||
params.setMargins(left, top, right, bottom);
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue