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 + ")");