From 0af823607ac5d6ff282c4eee175e4a533176196c Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Tue, 9 Jun 2020 11:17:07 +0200 Subject: [PATCH] Improvements to extra keys (#1479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make popup keys for extra keys row configurable This makes the keys you get when swiping up on a key configurable. You can configure such a key by using an array of strings instead of a single string in the row. The first entry will be the normal key and the second will be the extra key. This is a slightly breaking change, as people that have configured custom extra keys with "-" or "/" will have to change the config to keep the popup keys. The default config will remain the same in terms of functionality, i.e. it includes the same popup key for "-". * Make popup keys interact well with long press keys This stops the repeat action when the popup is shown, and makes sure the popup is closed when you release even if there has been some repeat actions. * Support configuring the style of the extra keys This adds a setting for choosing between the different ways to render key names that were already present in ExtraKeysView. The available setting values are "arrows-only", "arrows-all", "all", "none" and "default". Other values will fallback to "default". Can be used as a workaround for #1410 * Support using modifier keys with letter keys in extra keys This allows you to use the modifier keys on the extra keys rows, e.g. ctrl, together with another button on the extra keys rows, as long as that button is a normal letter and not a special key. Support for special keys will come in the next commit. * Support using modifier keys with special keys in extra keys This allows you to use the modifier keys on the extra keys rows together with a special key on the extra keys rows, e.g. CTRL+LEFT. Fixes #745, fixes most of #895 and possibly #154 * Support mapping extra keys to other actions This adds a setting called extra-keys-map which allows you to map a key on the extra keys rows to another action. The value is a json object where the key is the button text as configured in extra-keys and the value is the action. Multiple actions can be used, but if they are special characters (like ESC or LEFT) they have to be separated from the other characters with a space on each side. If you want an actual space character, use SPACE. For example if you want to add a key to go to the next active channel in weechat, you can use this: extra-keys-map = {"weechat next": "ESC a"} And then add "weechat next" to extra-keys. The name can of course be whatever you want. Or if you want the button for the UP arrow to show ⇧ instead of ↑, you can use this: extra-keys-map = {"⇧": "UP"} And put "⇧" in extra-keys instead of "UP". Modifier keys (ctrl, alt and shift) can't be used in this map yet. Support for ctrl and alt will come in the next commit. I think this fixes #1186 * Support CTRL and ALT in extra keys map This allows you to use CTRL and ALT in extra-keys-map. For example if you want a button to exit the terminal, you can use this: extra-keys-map = {"exit": "CTRL d"} And add "exit" to extra-keys. * Support a KEYBOARD button in extra keys This toggles showing the keyboard input method. * Support specifying macro keys in the extra-keys option Instead of specifying macros in the separate extra-keys-map option by matching the key name in the two options, you can now use "macro" instead of "key" in extra-keys, and it will be a macro, i.e. a sequence of multiple keys separated by space. * Remove option extra-keys-map Now that you can specify macro in extra-keys, there is no point in having this separate option. Instead of specifying the value to display as key, and the macro to perform in extra-keys-map, you would now specify the value to display in the display property and the macro to perform in the macro property. * Lookup display text when creating ExtraKeyButton This will make it easier to support key aliases for macros in the next commit. * Add support for a key to open the drawer Fixes (I think) #1325 --- .../java/com/termux/app/ExtraKeysInfos.java | 338 ++++++++++++++++++ .../java/com/termux/app/ExtraKeysView.java | 283 +++++---------- .../java/com/termux/app/TermuxActivity.java | 6 +- .../com/termux/app/TermuxPreferences.java | 32 +- .../java/com/termux/view/TerminalView.java | 14 +- 5 files changed, 460 insertions(+), 213 deletions(-) create mode 100644 app/src/main/java/com/termux/app/ExtraKeysInfos.java diff --git a/app/src/main/java/com/termux/app/ExtraKeysInfos.java b/app/src/main/java/com/termux/app/ExtraKeysInfos.java new file mode 100644 index 00000000..86d97449 --- /dev/null +++ b/app/src/main/java/com/termux/app/ExtraKeysInfos.java @@ -0,0 +1,338 @@ +package com.termux.app; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class ExtraKeysInfos { + + /** + * Matrix of buttons displayed + */ + private ExtraKeyButton[][] buttons; + + /** + * This corresponds to one of the CharMapDisplay below + */ + private String style = "default"; + + public ExtraKeysInfos(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 extends HashMap { + V get(K key, V defaultValue) { + if(containsKey(key)) + return get(key); + else + return defaultValue; + } + } + + static class CharDisplayMap extends CleverMap {} + + /** + * 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 + }}; + + 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: + 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); + } +} + +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 String key; + + /** + * If the key is a macro, i.e. a sequence of keys separated by space. + */ + private boolean macro; + + /** + * The text that will be shown on the button. + */ + private String display; + + /** + * The information of the popup (triggered by swipe up). + */ + @Nullable + private ExtraKeyButton popup = null; + + public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException { + this(charDisplayMap, config, null); + } + + public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config, 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] = ExtraKeysInfos.replaceAlias(keys[i]); + } + + this.key = String.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; + } +} diff --git a/app/src/main/java/com/termux/app/ExtraKeysView.java b/app/src/main/java/com/termux/app/ExtraKeysView.java index e2f76dc3..018c33c6 100644 --- a/app/src/main/java/com/termux/app/ExtraKeysView.java +++ b/app/src/main/java/com/termux/app/ExtraKeysView.java @@ -1,5 +1,6 @@ package com.termux.app; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; import android.provider.Settings; @@ -13,19 +14,22 @@ import java.util.Map; import java.util.HashMap; import java.util.Arrays; +import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.GridLayout; import android.widget.PopupWindow; import android.widget.ToggleButton; import com.termux.R; -import com.termux.terminal.TerminalSession; 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. @@ -40,26 +44,9 @@ public final class ExtraKeysView extends GridLayout { public ExtraKeysView(Context context, AttributeSet attrs) { super(context, attrs); } - - /** - * 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 extends HashMap { - V get(K key, V defaultValue) { - if(containsKey(key)) - return get(key); - else - return defaultValue; - } - } - - static class CharDisplayMap extends CleverMap {} - - /** - * Keys are displayed in a natural looking way, like "→" for "RIGHT" - */ + static final Map keyCodesForString = new HashMap() {{ + put("SPACE", KeyEvent.KEYCODE_SPACE); put("ESC", KeyEvent.KEYCODE_ESCAPE); put("TAB", KeyEvent.KEYCODE_TAB); put("HOME", KeyEvent.KEYCODE_MOVE_HOME); @@ -87,45 +74,79 @@ public final class ExtraKeysView extends GridLayout { put("F11", KeyEvent.KEYCODE_F11); put("F12", KeyEvent.KEYCODE_F12); }}; - - static void sendKey(View view, String keyName) { + + private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) { TerminalView terminalView = view.findViewById(R.id.terminal_view); - if (keyCodesForString.containsKey(keyName)) { + if ("KEYBOARD".equals(keyName)) { + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.toggleSoftInput(0, 0); + } else if ("DRAWER".equals(keyName)) { + DrawerLayout drawer = view.findViewById(R.id.drawer_layout); + drawer.openDrawer(Gravity.LEFT); + } else if (keyCodesForString.containsKey(keyName)) { int keyCode = keyCodesForString.get(keyName); - terminalView.onKeyDown(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode)); - // view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); + 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 - TerminalSession session = terminalView.getCurrentSession(); - if (session != null && keyName.length() > 0) - session.write(keyName); + 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; ToggleButton button = null; } - + private Map specialButtons = new HashMap() {{ put(SpecialButton.CTRL, new SpecialButtonState()); put(SpecialButton.ALT, new SpecialButtonState()); put(SpecialButton.FN, new SpecialButtonState()); }}; - + 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) return false; @@ -166,145 +187,21 @@ public final class ExtraKeysView extends GridLayout { popupWindow.setFocusable(false); popupWindow.showAsDropDown(view, 0, -2 * height); } - - 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 - }}; - - 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 - }}; - - /** - * Keys are displayed in a natural looking way, like "→" for "RIGHT" or "↲" for ENTER - */ - public static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{ - putAll(classicArrowsDisplay); - putAll(wellKnownCharactersDisplay); - putAll(nicerLookingDisplay); - // all other characters are displayed as themselves - }}; - - public static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{ - putAll(classicArrowsDisplay); - putAll(wellKnownCharactersDisplay); - putAll(lessKnownCharactersDisplay); // NEW - putAll(nicerLookingDisplay); - }}; - - public static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{ - putAll(classicArrowsDisplay); - // putAll(wellKnownCharactersDisplay); // REMOVED - // putAll(lessKnownCharactersDisplay); // REMOVED - putAll(nicerLookingDisplay); - }}; - - public 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 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", "'"); - }}; - - /** - * Applies the 'controlCharsAliases' mapping to all the strings in *buttons* - * Modifies the array, doesn't return a new one. - */ - void replaceAliases(String[][] buttons) { - for(int i = 0; i < buttons.length; i++) - for(int j = 0; j < buttons[i].length; j++) - buttons[i][j] = controlCharsAliases.get(buttons[i][j], buttons[i][j]); - } - /** * General util function to compute the longest column length in a matrix. */ - static int maximumLength(String[][] matrix) { + static int maximumLength(Object[][] matrix) { int m = 0; - for (String[] aMatrix : matrix) m = Math.max(m, aMatrix.length); + for (Object[] row : matrix) + m = Math.max(m, row.length); return m; } - + /** * Reload the view given parameters in termux.properties * - * @param buttons matrix of String as defined in termux.properties extrakeys + * @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 @@ -316,36 +213,36 @@ public final class ExtraKeysView extends GridLayout { * "−" will input a "−" character * "-_-" will input the string "-_-" */ - void reload(String[][] buttons, CharDisplayMap charDisplayMap) { + @SuppressLint("ClickableViewAccessibility") + void reload(ExtraKeysInfos infos) { + if(infos == null) + return; + for(SpecialButtonState state : specialButtons.values()) state.button = null; - + removeAllViews(); - - replaceAliases(buttons); // modifies the array - final int rows = buttons.length; - final int cols = maximumLength(buttons); + ExtraKeyButton[][] buttons = infos.getMatrix(); - setRowCount(rows); - setColumnCount(cols); + setRowCount(buttons.length); + setColumnCount(maximumLength(buttons)); - for (int row = 0; row < rows; row++) { + for (int row = 0; row < buttons.length; row++) { for (int col = 0; col < buttons[row].length; col++) { - final String buttonText = buttons[row][col]; - + final ExtraKeyButton buttonInfo = buttons[row][col]; + Button button; - if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) { - SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonText)); // for valueOf: https://stackoverflow.com/a/604426/1980630 + if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) { + SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey())); // for valueOf: https://stackoverflow.com/a/604426/1980630 state.isOn = true; button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle); button.setClickable(true); } else { button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle); } - - final String displayedText = charDisplayMap.get(buttonText, buttonText); - button.setText(displayedText); + + button.setText(buttonInfo.getDisplay()); button.setTextColor(TEXT_COLOR); button.setPadding(0, 0, 0, 0); @@ -365,12 +262,12 @@ public final class ExtraKeysView extends GridLayout { } View root = getRootView(); - if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) { + if (Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) { ToggleButton self = (ToggleButton) finalButton; self.setChecked(self.isChecked()); self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR); } else { - sendKey(root, buttonText); + sendKey(root, buttonInfo); } }); @@ -380,22 +277,26 @@ public final class ExtraKeysView extends GridLayout { case MotionEvent.ACTION_DOWN: longPressCount = 0; v.setBackgroundColor(BUTTON_PRESSED_COLOR); - if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT").contains(buttonText)) { + if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) { + // autorepeat scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); scheduledExecutor.scheduleWithFixedDelay(() -> { longPressCount++; - sendKey(root, buttonText); + sendKey(root, buttonInfo); }, 400, 80, TimeUnit.MILLISECONDS); } return true; case MotionEvent.ACTION_MOVE: - // These two keys have a Move-Up button appearing - if (Arrays.asList("/", "-").contains(buttonText)) { + if (buttonInfo.getPopup() != null) { if (popupWindow == null && event.getY() < 0) { + if (scheduledExecutor != null) { + scheduledExecutor.shutdownNow(); + scheduledExecutor = null; + } v.setBackgroundColor(BUTTON_COLOR); - String text = "-".equals(buttonText) ? "|" : "\\"; - popup(v, text); + String extraButtonDisplayedText = buttonInfo.getPopup().getDisplay(); + popup(v, extraButtonDisplayedText); } if (popupWindow != null && event.getY() > 0) { v.setBackgroundColor(BUTTON_PRESSED_COLOR); @@ -418,12 +319,14 @@ public final class ExtraKeysView extends GridLayout { scheduledExecutor.shutdownNow(); scheduledExecutor = null; } - if (longPressCount == 0) { - if (popupWindow != null && Arrays.asList("/", "-").contains(buttonText)) { + if (longPressCount == 0 || popupWindow != null) { + if (popupWindow != null) { popupWindow.setContentView(null); popupWindow.dismiss(); popupWindow = null; - sendKey(root, "-".equals(buttonText) ? "|" : "\\"); + if (buttonInfo.getPopup() != null) { + sendKey(root, buttonInfo.getPopup()); + } } else { v.performClick(); } diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index ae61b302..946d693a 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -148,7 +148,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection mSettings.reloadFromProperties(TermuxActivity.this); if (mExtraKeysView != null) { - mExtraKeysView.reload(mSettings.mExtraKeys, ExtraKeysView.defaultCharDisplay); + mExtraKeysView.reload(mSettings.mExtraKeys); } } } @@ -229,7 +229,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams(); - layoutParams.height = layoutParams.height * mSettings.mExtraKeys.length; + layoutParams.height = layoutParams.height * (mSettings.mExtraKeys == null ? 0 : mSettings.mExtraKeys.getMatrix().length); viewPager.setLayoutParams(layoutParams); viewPager.setAdapter(new PagerAdapter() { @@ -250,7 +250,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection View layout; if (position == 0) { layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false); - mExtraKeysView.reload(mSettings.mExtraKeys, ExtraKeysView.defaultCharDisplay); + mExtraKeysView.reload(mSettings.mExtraKeys); } else { layout = inflater.inflate(R.layout.extra_keys_right, collection, false); final EditText editText = layout.findViewById(R.id.text_input); diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java index a2392453..3703cd09 100644 --- a/app/src/main/java/com/termux/app/TermuxPreferences.java +++ b/app/src/main/java/com/termux/app/TermuxPreferences.java @@ -10,6 +10,7 @@ import android.widget.Toast; import com.termux.terminal.TerminalSession; import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; @@ -19,7 +20,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Properties; import androidx.annotation.IntDef; @@ -70,7 +74,7 @@ final class TermuxPreferences { boolean mDisableVolumeVirtualKeys; boolean mShowExtraKeys; - String[][] mExtraKeys; + ExtraKeysInfos mExtraKeys; final List shortcuts = new ArrayList<>(); @@ -150,7 +154,7 @@ final class TermuxPreferences { } return null; } - + void reloadFromProperties(Context context) { File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties"); if (!propsFile.exists()) @@ -192,21 +196,23 @@ final class TermuxPreferences { mUseDarkUI = nightMode == Configuration.UI_MODE_NIGHT_YES; } - try { - JSONArray arr = new JSONArray(props.getProperty("extra-keys", "[['ESC', 'TAB', 'CTRL', 'ALT', '-', 'DOWN', 'UP']]")); + String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; - mExtraKeys = new String[arr.length()][]; - for (int i = 0; i < arr.length(); i++) { - JSONArray line = arr.getJSONArray(i); - mExtraKeys[i] = new String[line.length()]; - for (int j = 0; j < line.length(); j++) { - mExtraKeys[i][j] = line.getString(j); - } - } + try { + String extrakeyProp = props.getProperty("extra-keys", defaultExtraKeys); + String extraKeysStyle = props.getProperty("extra-keys-style", "default"); + mExtraKeys = new ExtraKeysInfos(extrakeyProp, extraKeysStyle); } catch (JSONException e) { Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show(); Log.e("termux", "Error loading props", e); - mExtraKeys = new String[0][]; + + try { + mExtraKeys = new ExtraKeysInfos(defaultExtraKeys, "default"); + } catch (JSONException e2) { + e2.printStackTrace(); + Toast.makeText(context, "Can't create default extra keys", Toast.LENGTH_LONG).show(); + mExtraKeys = null; + } } mBackIsEscape = "escape".equals(props.getProperty("back-key", "back")); diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 702bd2a4..76f6380e 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -558,13 +558,13 @@ public final class TerminalView extends View { } final int metaState = event.getMetaState(); - final boolean controlDownFromEvent = event.isCtrlPressed(); - final boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0; + final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey(); + final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey(); final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0; int keyMod = 0; - if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL; - if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT; + if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL; + if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT; if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT; if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) { if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event"); @@ -592,7 +592,7 @@ public final class TerminalView extends View { if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) { // If entered combining accent previously, write it out: if (mCombiningAccent != 0) - inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent); + inputCodePoint(mCombiningAccent, controlDown, leftAltDown); mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK; } else { if (mCombiningAccent != 0) { @@ -600,7 +600,7 @@ public final class TerminalView extends View { if (combinedChar > 0) result = combinedChar; mCombiningAccent = 0; } - inputCodePoint(result, controlDownFromEvent, leftAltDownFromEvent); + inputCodePoint(result, controlDown, leftAltDown); } if (mCombiningAccent != oldCombiningAccent) invalidate(); @@ -608,7 +608,7 @@ public final class TerminalView extends View { return true; } - void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) { + public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) { if (LOG_KEY_EVENTS) { Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent=" + leftAltDownFromEvent + ")");