Terminal is now fully implemented

This commit is contained in:
Akash Yadav
2021-09-13 19:47:24 +05:30
parent 25965b8eb0
commit 29de8503a0
27 changed files with 1595 additions and 247 deletions
+1
View File
@@ -115,6 +115,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
implementation 'androidx.drawerlayout:drawerlayout:1.1.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
@@ -173,10 +173,6 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
private static final int ACTIONID_OTHERS = 101;
private static final int ACTIONID_ALL = 102;
private static final int STD_TYPE_NORMAL = 0;
private static final int STD_TYPE_ERROR = 1;
private static final int STD_TYPE_SUCCESS = 2;
public static final String EXTRA_PROJECT = "project";
private LogReceiver mLogReceiver = new LogReceiver().setLogListener(line -> appendApkLog(line));
@@ -3,17 +3,23 @@ package com.itsaky.androidide;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import com.blankj.utilcode.util.ClipboardUtils;
import com.blankj.utilcode.util.KeyboardUtils;
import com.blankj.utilcode.util.SizeUtils;
import com.blankj.utilcode.util.ThrowableUtils;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.databinding.ActivityTerminalBinding;
import com.itsaky.androidide.managers.ToolsManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.androidide.utils.TypefaceUtils;
import com.itsaky.androidide.views.virtualkeys.SpecialButton;
import com.itsaky.androidide.views.virtualkeys.VirtualKeyButton;
import com.itsaky.androidide.views.virtualkeys.VirtualKeysConstants;
import com.itsaky.androidide.views.virtualkeys.VirtualKeysInfo;
import com.itsaky.androidide.views.virtualkeys.VirtualKeysView;
import com.itsaky.terminal.TerminalEmulator;
import com.itsaky.terminal.TerminalSession;
import com.itsaky.terminal.TerminalSessionClient;
@@ -21,6 +27,7 @@ import com.itsaky.terminal.TextStyle;
import com.itsaky.terminal.view.TerminalView;
import com.itsaky.terminal.view.TerminalViewClient;
import java.util.Map;
import org.json.JSONException;
public class TerminalActivity extends StudioActivity {
@@ -28,8 +35,10 @@ public class TerminalActivity extends StudioActivity {
private TerminalView terminal;
private TerminalSession session;
private boolean isVisible = false;
private int currentTextSize = 0;
private KeyListener listener;
private final Client client = new Client();
private static final Logger LOG = Logger.instance("TerminalActivity");
@@ -38,28 +47,52 @@ public class TerminalActivity extends StudioActivity {
binding = ActivityTerminalBinding.inflate(getLayoutInflater());
terminal = new TerminalView(this, null);
terminal.setTerminalViewClient(client);
terminal.setTextSize(currentTextSize = SizeUtils.dp2px(10));
terminal.setTypeface(TypefaceUtils.jetbrainsMono());
terminal.attachSession(createSession());
terminal.setKeepScreenOn(true);
terminal.setTextSize(currentTextSize = SizeUtils.dp2px(10));
terminal.setTypeface(TypefaceUtils.jetbrainsMono());
binding.getRoot().addView(terminal, new ViewGroup.LayoutParams(-1, -1));
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(-1, 0);
params.weight = 1f;
binding.getRoot().addView(terminal, 0, params);
try {
binding.virtualKeyTable.setVirtualKeysViewClient(getKeyListener());
binding.virtualKeyTable.reload(new VirtualKeysInfo(ConstantsBridge.VIRTUAL_KEYS, "", VirtualKeysConstants.CONTROL_CHARS_ALIASES));
} catch (JSONException e) {
// Won't happen
}
return binding.getRoot();
}
private KeyListener getKeyListener() {
return listener == null ? listener = new KeyListener(terminal) : listener;
}
@Override
protected void onResume() {
super.onResume();
setTerminalCursorBlinkingState(true);
}
@Override
protected void onStart() {
super.onStart();
isVisible = true;
}
@Override
protected void onStop() {
super.onStop();
isVisible = false;
setTerminalCursorBlinkingState(false);
}
public boolean isVisible() {
return isVisible;
}
private void setTerminalCursorBlinkingState(boolean start ) {
if(terminal != null && terminal.mEmulator != null) {
terminal.setTerminalCursorBlinkerState(start, true);
@@ -76,7 +109,7 @@ public class TerminalActivity extends StudioActivity {
}
session = new TerminalSession(
Environment.BASH.getAbsolutePath(), // Shell command
Environment.SHELL.getAbsolutePath(), // Shell command
Environment.HOME.getAbsolutePath(), // Working directory
new String[]{}, // Arguments
env, // Environment variables
@@ -93,6 +126,70 @@ public class TerminalActivity extends StudioActivity {
private final class KeyListener implements VirtualKeysView.IVirtualKeysView {
private final TerminalView terminal;
public KeyListener(TerminalView terminal) {
this.terminal = terminal;
}
@Override
public void onVirtualKeyButtonClick(View view, VirtualKeyButton buttonInfo, Button button) {
if(terminal == null) return;
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 (VirtualKeysConstants.PRIMARY_KEY_CODES_FOR_STRINGS.containsKey(key)) {
Integer keyCode = VirtualKeysConstants.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);
terminal.onKeyDown(keyCode, keyEvent);
} else {
// not a control char
key.codePoints().forEach(codePoint -> {
terminal.inputCodePoint(codePoint, ctrlDown, altDown);
});
}
}
@Override
public boolean performVirtualKeyButtonHapticFeedback(View view, VirtualKeyButton buttonInfo, Button button) {
// No need to handle this
// VirtualKeysView will take care of performing haptic feedback
return false;
}
}
private final class Client implements TerminalViewClient, TerminalSessionClient {
@Override
@@ -1,15 +1,13 @@
package com.itsaky.androidide.adapters;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.core.widget.TextViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.itsaky.androidide.databinding.LayoutSymbolItemBinding;
import com.itsaky.androidide.utils.Symbols;
import com.itsaky.androidide.views.SymbolInputView.Symbol;
import io.github.rosemoe.editor.widget.CodeEditor;
import io.github.rosemoe.editor.widget.SymbolChannel;
import android.util.TypedValue;
public class SymbolInputAdapter extends RecyclerView.Adapter<SymbolInputAdapter.VH> {
@@ -47,8 +45,7 @@ public class SymbolInputAdapter extends RecyclerView.Adapter<SymbolInputAdapter.
if(symbols == null || symbols[position] == null) return;
final Symbol symbol = symbols[position];
holder.binding.symbol.setText(symbol.label);
// TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(holder.binding.symbol, 16, 27, 1, TypedValue.COMPLEX_UNIT_SP);
holder.binding.symbol.setOnClickListener(__ -> channel.insertSymbol(symbol.commit, symbol.offset));
holder.binding.symbol.setOnClickListener(__ -> channel.insertSymbol(symbol.commit, symbol.offset));
}
@Override
@@ -7,6 +7,7 @@ import android.view.ViewGroup;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.adapters.CompletionListAdapter;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.FragmentEditorBinding;
import com.itsaky.androidide.fragments.preferences.EditorPreferences;
import com.itsaky.androidide.interfaces.JLSRequestor;
@@ -18,6 +19,7 @@ import com.itsaky.androidide.language.xml.lexer.XMLLexer;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.AndroidProject;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.services.IDEService;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.ReadFileTask;
@@ -35,7 +35,6 @@ public class ToolsManager {
extractLogsenderIfNeeded();
writeInitScript();
writeBashrc();
rewriteProjectData();
@@ -57,13 +56,6 @@ public class ToolsManager {
}
}
private static void writeBashrc() {
File rc = Environment.BASHRC;
if(!rc.exists()) {
FileIOUtils.writeFileFromString(rc, BASHRC);
}
}
private static void writeInitScript() {
FileIOUtils.writeFileFromString(Environment.INIT_SCRIPT, INIT_SCRIPT);
}
@@ -80,7 +72,7 @@ public class ToolsManager {
final boolean exists = gson && jls && proto;
final boolean isOld = JLS_VERSION > prefs.getInt(KEY_JLS_VERSION, 0);
if(true/*!exists || isOld*/) {
if(!exists || isOld) {
try {
extractJls();
} catch (Throwable e) {}
@@ -112,10 +104,6 @@ public class ToolsManager {
} catch (IOException e) {}
}
private static final String BASHRC =
"PROMPT_DIRTRIM=2" +
"\nPS1=\'\\[\\e[0;34m\\]\\w\\[\\e[0m\\] \\[\\e[0;97m\\]$\\[\\e[0m\\] \'";
private static final String INIT_SCRIPT =
( "import org.gradle.api.Plugin"
+ "\nimport org.gradle.api.Project"
@@ -183,6 +171,7 @@ public class ToolsManager {
+ "\n ideProject.displayName = root.getDisplayName()"
+ "\n ideProject.description = root.getDescription()"
+ "\n ideProject.path = root.getPath()"
+ "\n ideProject.projectDir = root.getProjectDir().getAbsolutePath()"
+ "\n "
+ "\n def tasks = root.tasks"
+ "\n if(tasks != null && tasks.size() > 0) {"
@@ -194,6 +183,9 @@ public class ToolsManager {
+ "\n }"
+ "\n "
+ "\n File out = new File(\"@@PROJECT_DATA_FILE@@\")"
+ "\n if(out.exists()) {"
+ "\n out.delete()"
+ "\n }"
+ "\n out << new JsonBuilder( ideProject ).toPrettyString()"
+ "\n "
+ "\n println \">>> PROJECT INITIALIZED <<<\""
@@ -236,6 +228,7 @@ public class ToolsManager {
+ "\n module.targetSdk = config.targetSdkVersion"
+ "\n module.versionCode = config.versionCode"
+ "\n module.versionName = config.versionName"
+ "\n module.projectDir = sub.getProjectDir().getAbsolutePath()"
+ "\n "
+ "\n if(isApp) {"
+ "\n module.applicationId = config.applicationId"
@@ -301,6 +294,7 @@ public class ToolsManager {
+ "\n def displayName = \"Not defined\""
+ "\n def description = \"Not defined\""
+ "\n def path = \"Not defined\""
+ "\n def projectDir = \"Not defined\""
+ "\n List<IDEProject> modules = new ArrayList<IDEProject>()"
+ "\n List<IDETask> tasks = new ArrayList<IDETask>()"
+ "\n List<String> dependencies = new ArrayList<String>()"
@@ -21,4 +21,33 @@ public class ConstantsBridge {
// Password for GradleApi zip
// Real password is gradle-api@admin@androidide
public static final String GRADLE_API_ZIP_PASSWORD_HASH = "b50b462f8e8c5789f32428d7e1967da2";
public static final String VIRTUAL_KEYS =
"["
+ "\n ["
+ "\n \"ESC\","
+ "\n {"
+ "\n \"key\": \"/\","
+ "\n \"popup\": \"\\\\\""
+ "\n },"
+ "\n {"
+ "\n \"key\": \"-\","
+ "\n \"popup\": \"|\""
+ "\n },"
+ "\n \"HOME\","
+ "\n \"UP\","
+ "\n \"END\","
+ "\n \"PGUP\""
+ "\n ],"
+ "\n ["
+ "\n \"TAB\","
+ "\n \"CTRL\","
+ "\n \"ALT\","
+ "\n \"LEFT\","
+ "\n \"DOWN\","
+ "\n \"RIGHT\","
+ "\n \"PGDN\""
+ "\n ]"
+ "\n]";
}
@@ -35,6 +35,12 @@ public class IDEProject {
@SerializedName("path")
public String path = "";
/**
* Path of the directory containing this project
*/
@SerializedName("projectDir")
public String projectDir;
/**
* File path of the application's icon
* We get this manually by parsing AndroidManifest.xml
@@ -25,6 +25,7 @@ import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import com.itsaky.androidide.tasks.gradle.resources.ProcessDebugResources;
public class IDEService implements ShellServer.Callback {
@@ -105,6 +106,13 @@ public class IDEService implements ShellServer.Callback {
if(currentTask != null && currentTask.getTaskID() != TASK_SHOW_DEPENDENCIES)
listener.onBuildSuccessful(currentTask, charSequence.toString().trim());
appendOutputSeparator();
if(currentTask != null && currentTask instanceof ProcessDebugResources) {
/**
* An XML file was changed and saved
* Generaring R.jar
*/
}
} else
if(charSequence.toString().contains(BUILD_FAILED)) {
isBuilding = false;
@@ -201,7 +209,11 @@ public class IDEService implements ShellServer.Callback {
return TextUtils.join(" ", args);
}
public IDEProject getIDEProject () {
return mIDEProject;
}
public void execTask(GradleTask task) {
execTask(task, false);
}
@@ -244,19 +256,7 @@ public class IDEService implements ShellServer.Callback {
public void bundle() {
execTask(BaseGradleTasks.BUNDLE);
}
public void mergeLibAndProjectDex() {
execTask(BaseGradleTasks.MERGE_LIB_AND_PROJECT_DEX);
}
public void mergeExtDex() {
execTask(BaseGradleTasks.MERGE_EXT_DEX);
}
public void mergeDex() {
execTask(BaseGradleTasks.MERGE_DEX);
}
public void clean() {
execTask(BaseGradleTasks.CLEAN);
}
@@ -267,6 +267,11 @@ public class IDEService implements ShellServer.Callback {
public void stopAllDaemons() {
app.newShell(null).bgAppend("gradle --stop");
if(isBuilding()) {
if(listener != null)
listener.onBuildFailed(currentTask, getString(R.string.msg_daemons_stopped));
isBuilding = false;
}
}
public void lint() {
@@ -281,6 +286,10 @@ public class IDEService implements ShellServer.Callback {
execTask(BaseGradleTasks.LINT_RELEASE);
}
public void processDebugResources() {
execTask(BaseGradleTasks.PROCESS_DEBUG_RESOURCES);
}
public String typeString(int type) {
switch (type) {
case TASK_SHOW_DEPENDENCIES:
@@ -38,4 +38,5 @@ public interface GradleTask {
public static final int TASK_LINT_RELEASE = 11;
public static final int TASK_TASKS = 12;
public static final int TASK_INIT_PROJECT = 13;
public static final int TASK_PROCESS_DEBUG_RESOURCES = 14;
}
@@ -8,17 +8,15 @@ import com.itsaky.androidide.tasks.gradle.build.Bundle;
import com.itsaky.androidide.tasks.gradle.build.Clean;
import com.itsaky.androidide.tasks.gradle.build.CleanBuild;
import com.itsaky.androidide.tasks.gradle.core.Tasks;
import com.itsaky.androidide.tasks.gradle.dex.MergeDex;
import com.itsaky.androidide.tasks.gradle.dex.MergeExtDex;
import com.itsaky.androidide.tasks.gradle.dex.MergeLibAndProjectDex;
import com.itsaky.androidide.tasks.gradle.lint.Lint;
import com.itsaky.androidide.tasks.gradle.lint.LintDebug;
import com.itsaky.androidide.tasks.gradle.lint.LintRelease;
import com.itsaky.androidide.tasks.gradle.plugin.InitializeIDEProject;
import com.itsaky.androidide.tasks.gradle.resources.ProcessDebugResources;
public class BaseGradleTasks {
public static BaseGradleTask
public static final BaseGradleTask
TASKS = new Tasks(),
@@ -28,13 +26,12 @@ public class BaseGradleTasks {
BUNDLE = new Bundle(),
CLEAN = new Clean(),
CLEAN_BUILD = new CleanBuild(),
LINT = new Lint(),
LINT_DEBUG = new LintDebug(),
LINT_RELEASE = new LintRelease(),
MERGE_DEX = new MergeDex(),
MERGE_EXT_DEX = new MergeExtDex(),
MERGE_LIB_AND_PROJECT_DEX = new MergeLibAndProjectDex(),
PROCESS_DEBUG_RESOURCES = new ProcessDebugResources(),
INITIALIZE_IDE_PROJECT = new InitializeIDEProject();
}
@@ -1,52 +0,0 @@
package com.itsaky.androidide.tasks.gradle.dex;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.io.File;
import java.util.List;
import java.util.Arrays;
public class MergeDex extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.task_merge_dex);
}
@Override
public String getCommands() {
return "mergeDexDebug";
}
@Override
public List<String> getTasks() {
return Arrays.asList("mergeDexDebug");
}
@Override
public int getTaskID() {
return IDEService.TASK_DEX;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public boolean buildsApk() {
return false;
}
@Override
public File getApk(String buildFolder, String moduleName) {
return null;
}
@Override
public Type getType() {
return Type.OTHER;
}
}
@@ -1,52 +0,0 @@
package com.itsaky.androidide.tasks.gradle.dex;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.io.File;
import java.util.Arrays;
import java.util.List;
public class MergeExtDex extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.task_merge_dex);
}
@Override
public String getCommands() {
return "mergeExtDexDebug";
}
@Override
public List<String> getTasks() {
return Arrays.asList("mergeExtDexDebug");
}
@Override
public int getTaskID() {
return IDEService.TASK_DEX;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public boolean buildsApk() {
return false;
}
@Override
public File getApk(String buildFolder, String moduleName) {
return null;
}
@Override
public Type getType() {
return Type.OTHER;
}
}
@@ -1,52 +0,0 @@
package com.itsaky.androidide.tasks.gradle.dex;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.io.File;
import java.util.Arrays;
import java.util.List;
public class MergeLibAndProjectDex extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.task_merge_dex);
}
@Override
public String getCommands() {
return "mergeLibDexDebug mergeProjectDexDebug";
}
@Override
public List<String> getTasks() {
return Arrays.asList("mergeLibDexDebug", "mergeProjectDexDebug");
}
@Override
public int getTaskID() {
return IDEService.TASK_DEX;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public boolean buildsApk() {
return false;
}
@Override
public File getApk(String buildFolder, String moduleName) {
return null;
}
@Override
public Type getType() {
return Type.OTHER;
}
}
@@ -0,0 +1,50 @@
package com.itsaky.androidide.tasks.gradle.resources;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.io.File;
import java.util.List;
import java.util.Arrays;
public class ProcessDebugResources extends BaseGradleTask {
@Override
public String getName() {
return "processDebugResources";
}
@Override
public String getCommands() {
return "processDebugResources";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return TASK_PROCESS_DEBUG_RESOURCES;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public boolean buildsApk() {
return false;
}
@Override
public File getApk(String buildFolder, String moduleName) {
return null;
}
@Override
public GradleTask.Type getType() {
return Type.BUILD;
}
}
@@ -38,8 +38,7 @@ public final class Environment {
public static final File GRADLE;
public static final File JAVA;
public static final File BUSYBOX;
public static final File BASH;
public static final File BASHRC;
public static final File SHELL;
public static File BOOTCLASSPATH;
@@ -77,12 +76,11 @@ public final class Environment {
ANDROID_HOME = new File(props.getOrDefault("ANDROID_HOME", DEFAULT_ANDROID_HOME));
JAVA_HOME = new File(props.getOrDefault("JAVA_HOME", DEFAULT_JAVA_HOME));
GRADLE_HOME = new File(props.getOrDefault("GRADLE_HOME", DEFAULT_GRADLE_HOME));
BASH = new File(BINDIR, "bash");
GRADLE = new File(GRADLE_HOME, "bin/gradle");
JAVA = new File(JAVA_HOME, "bin/java");
BUSYBOX = new File(BINDIR, "busybox");
BASHRC = new File(HOME, ".bashrc");
SHELL = new File(BINDIR, "sh.sh");
}
private static Map<String, String> readProperties() {
@@ -128,7 +126,7 @@ public final class Environment {
map.put("PREFIX", SYSROOT.getAbsolutePath());
map.put("BUSYBOX", BUSYBOX.getAbsolutePath());
map.put("SHELL", BASH.getAbsolutePath());
map.put("SHELL", SHELL.getAbsolutePath());
map.put("TERM", "xterm-256color");
map.put("COLORTERM", "truecolor");
@@ -1,41 +1,36 @@
package com.itsaky.androidide.utils;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.fragments.EditorFragment;
import com.itsaky.androidide.views.SymbolInputView.Symbol;
import java.io.File;
import com.itsaky.androidide.app.StudioApp;
public class Symbols {
public static Symbol[] forFile(File file) {
if(file.isFile()) {
if(file.getName().endsWith(EditorFragment.EXT_JAVA)
|| file.getName().endsWith(EditorFragment.EXT_GRADLE))
if (file.isFile()) {
if (file.getName().endsWith(EditorFragment.EXT_JAVA)
|| file.getName().endsWith(EditorFragment.EXT_GRADLE))
return javaSymbols();
if(file.getName().endsWith(EditorFragment.EXT_XML))
if (file.getName().endsWith(EditorFragment.EXT_XML))
return xmlSymbols();
}
return new Symbol[0];
}
private static Symbol tab() {
String tab = createTabSpaces();
return new Symbol("↹", tab, tab.length());
}
public static String createTabSpaces() {
int size = StudioApp.getInstance().getPrefManager().getEditorTabSize();
String tab = "";
for(int i=1;i<=size;i++) {
tab += " ";
StringBuilder tab = new StringBuilder();
for (int i=1;i <= size;i++) {
tab.append(" ");
}
return tab;
return tab.toString();
}
public static Symbol[] javaSymbols() {
return new Symbol[]{tab(),
return new Symbol[]{new Symbol("↹", "\t"),
new Symbol("{", "{}"),
new Symbol("}"),
new Symbol("(", "()"),
@@ -58,9 +53,9 @@ public class Symbols {
new Symbol(":"),
new Symbol("_")};
}
public static Symbol[] xmlSymbols() {
return new Symbol[]{tab(),
return new Symbol[]{new Symbol("↹", "\t"),
new Symbol("<", "<>"),
new Symbol(">"),
new Symbol("/"),
@@ -85,5 +80,5 @@ public class Symbols {
new Symbol("_"),
new Symbol("-")};
}
}
@@ -0,0 +1,52 @@
package com.itsaky.androidide.views.virtualkeys;
import androidx.annotation.NonNull;
import java.util.HashMap;
/** The {@link Class} that implements special buttons for {@link VirtualKeysView}. */
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.itsaky.androidide.views.virtualkeys;
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 VirtualKeysView}. */
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<>();
VirtualKeysView mVirtualKeysView;
/**
* Initialize a {@link SpecialButtonState} to maintain state of a {@link SpecialButton}.
*
* @param extraKeysView The {@link VirtualKeysView} instance in which the {@link SpecialButton}
* is to be registered.
*/
public SpecialButtonState(VirtualKeysView extraKeysView) {
mVirtualKeysView = 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 ? mVirtualKeysView.getButtonActiveTextColor() : mVirtualKeysView.getButtonTextColor()));
}
/** Set {@link #isLocked}. */
public void setIsLocked(boolean value) {
isLocked = value;
}
}
@@ -0,0 +1,151 @@
package com.itsaky.androidide.views.virtualkeys;
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 VirtualKeyButton {
/** 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 VirtualKeysConstants#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 VirtualKeyButton} containing the information of the popup button (triggered by swipe up).
*/
@Nullable
private final VirtualKeyButton popup;
/**
* Initialize a {@link VirtualKeyButton}.
*
* @param config The {@link JSONObject} containing the info to create the {@link VirtualKeyButton}.
* @param extraKeyDisplayMap The {@link VirtualKeysConstants.VirtualKeyDisplayMap} 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 VirtualKeysConstants.VirtualKeyDisplayMap} that defines the
* aliases for the actual key names.
*/
public VirtualKeyButton(@NonNull JSONObject config,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyDisplayMap,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyAliasMap) throws JSONException {
this(config, null, extraKeyDisplayMap, extraKeyAliasMap);
}
/**
* Initialize a {@link VirtualKeyButton}.
*
* @param config The {@link JSONObject} containing the info to create the {@link VirtualKeyButton}.
* @param popup The {@link VirtualKeyButton} optional {@link #popup} button.
* @param extraKeyDisplayMap The {@link VirtualKeysConstants.VirtualKeyDisplayMap} 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 VirtualKeysConstants.VirtualKeyDisplayMap} that defines the
* aliases for the actual key names.
*/
public VirtualKeyButton(@NonNull JSONObject config, @Nullable VirtualKeyButton popup,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyDisplayMap,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap 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 VirtualKeyButton getPopup() {
return popup;
}
/**
* Replace the alias with its actual key name if found in extraKeyAliasMap.
*/
public static String replaceAlias(@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyAliasMap, String key) {
return extraKeyAliasMap.get(key, key);
}
}
@@ -0,0 +1,208 @@
package com.itsaky.androidide.views.virtualkeys;
import android.view.KeyEvent;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class VirtualKeysConstants {
/** Defines the repetitive keys that can be passed to {@link VirtualKeysView#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 VirtualKeyDisplayMap 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 VirtualKeyDisplayMap CLASSIC_ARROWS_DISPLAY = new VirtualKeyDisplayMap() {{
// 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 VirtualKeyDisplayMap WELL_KNOWN_CHARACTERS_DISPLAY = new VirtualKeyDisplayMap() {{
// 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 VirtualKeyDisplayMap LESS_KNOWN_CHARACTERS_DISPLAY = new VirtualKeyDisplayMap() {{
// 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 VirtualKeyDisplayMap ARROW_TRIANGLE_VARIATION_DISPLAY = new VirtualKeyDisplayMap() {{
// 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 VirtualKeyDisplayMap NOT_KNOWN_ISO_CHARACTERS = new VirtualKeyDisplayMap() {{
// 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 VirtualKeyDisplayMap NICER_LOOKING_DISPLAY = new VirtualKeyDisplayMap() {{
// nicer looking for most cases
put("-", "―"); // U+2015 ― HORIZONTAL BAR
}};
/**
* Full Iso
*/
public static final VirtualKeyDisplayMap FULL_ISO_CHAR_DISPLAY = new VirtualKeyDisplayMap() {{
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 VirtualKeyDisplayMap ARROWS_ONLY_CHAR_DISPLAY = new VirtualKeyDisplayMap() {{
putAll(CLASSIC_ARROWS_DISPLAY);
// putAll(wellKnownCharactersDisplay); // REMOVED
// putAll(lessKnownCharactersDisplay); // REMOVED
putAll(NICER_LOOKING_DISPLAY);
}};
/**
* Classic symbols and less known symbols
*/
public static final VirtualKeyDisplayMap LOTS_OF_ARROWS_CHAR_DISPLAY = new VirtualKeyDisplayMap() {{
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 VirtualKeyDisplayMap DEFAULT_CHAR_DISPLAY = new VirtualKeyDisplayMap() {{
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 VirtualKeyDisplayMap CONTROL_CHARS_ALIASES = new VirtualKeyDisplayMap() {{
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.itsaky.androidide.views.virtualkeys;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import com.itsaky.androidide.views.virtualkeys.VirtualKeysConstants.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 VirtualKeysView} 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 VirtualKeysConstants#CONTROL_CHARS_ALIASES}.
*
* Its up to the {@link VirtualKeysView.IVirtualKeysView} client on how to handle individual key values
* of an {@link VirtualKeyButton}. They are sent as is via
* {@link VirtualKeysView.IVirtualKeysView#onVirtualKeyButtonClick(View, VirtualKeyButton, Button)}. The
* {@link com.termux.shared.terminal.io.TerminalVirtualKeys} which is an implementation of the interface,
* checks if the key is one of {@link VirtualKeysConstants#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 VirtualKeysInfo {
/**
* Matrix of buttons to be displayed in {@link VirtualKeysView}.
*/
private final VirtualKeyButton[][] mButtons;
/**
* Initialize {@link VirtualKeysInfo}.
*
* @param propertiesInfo The {@link String} containing the info to create the {@link VirtualKeysInfo}.
* Check the class javadoc for details.
* @param style The style to pass to {@link #getCharDisplayMapForStyle(String)} to get the
* {@link VirtualKeysConstants.VirtualKeyDisplayMap} that defines the display text
* mapping for the keys if a custom value is not defined by
* {@link VirtualKeyButton#KEY_DISPLAY_NAME} for a key.
* @param extraKeyAliasMap The {@link VirtualKeysConstants.VirtualKeyDisplayMap} that defines the
* aliases for the actual key names. You can create your own or
* optionally pass {@link VirtualKeysConstants#CONTROL_CHARS_ALIASES}.
*/
public VirtualKeysInfo(@NonNull String propertiesInfo, String style,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyAliasMap) throws JSONException {
mButtons = initVirtualKeysInfo(propertiesInfo, getCharDisplayMapForStyle(style), extraKeyAliasMap);
}
/**
* Initialize {@link VirtualKeysInfo}.
*
* @param propertiesInfo The {@link String} containing the info to create the {@link VirtualKeysInfo}.
* Check the class javadoc for details.
* @param extraKeyDisplayMap The {@link VirtualKeysConstants.VirtualKeyDisplayMap} that defines the
* display text mapping for the keys if a custom value is not defined
* by {@link VirtualKeyButton#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 VirtualKeysConstants.VirtualKeyDisplayMap} that defines the
* aliases for the actual key names. You can create your own or
* optionally pass {@link VirtualKeysConstants#CONTROL_CHARS_ALIASES}.
*/
public VirtualKeysInfo(@NonNull String propertiesInfo,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyDisplayMap,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyAliasMap) throws JSONException {
mButtons = initVirtualKeysInfo(propertiesInfo, extraKeyDisplayMap, extraKeyAliasMap);
}
private VirtualKeyButton[][] initVirtualKeysInfo(@NonNull String propertiesInfo,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyDisplayMap,
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap 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
VirtualKeyButton[][] buttons = new VirtualKeyButton[matrix.length][];
for (int i = 0; i < matrix.length; i++) {
buttons[i] = new VirtualKeyButton[matrix[i].length];
for (int j = 0; j < matrix[i].length; j++) {
Object key = matrix[i][j];
JSONObject jobject = normalizeKeyConfig(key);
VirtualKeyButton button;
if (!jobject.has(VirtualKeyButton.KEY_POPUP)) {
// no popup
button = new VirtualKeyButton(jobject, extraKeyDisplayMap, extraKeyAliasMap);
} else {
// a popup
JSONObject popupJobject = normalizeKeyConfig(jobject.get(VirtualKeyButton.KEY_POPUP));
VirtualKeyButton popup = new VirtualKeyButton(popupJobject, extraKeyDisplayMap, extraKeyAliasMap);
button = new VirtualKeyButton(jobject, popup, extraKeyDisplayMap, extraKeyAliasMap);
}
buttons[i][j] = button;
}
}
return buttons;
}
/**
* Convert "value" -> {"key": "value"}. Required by
* {@link VirtualKeyButton#VirtualKeyButton(JSONObject, VirtualKeyButton, VirtualKeysConstants.VirtualKeyDisplayMap, VirtualKeysConstants.VirtualKeyDisplayMap)}.
*/
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
JSONObject jobject;
if (key instanceof String) {
jobject = new JSONObject();
jobject.put(VirtualKeyButton.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 VirtualKeyButton[][] getMatrix() {
return mButtons;
}
@NonNull
public static VirtualKeysConstants.VirtualKeyDisplayMap 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 VirtualKeysConstants.VirtualKeyDisplayMap();
default:
return EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY;
}
}
}
@@ -0,0 +1,656 @@
package com.itsaky.androidide.views.virtualkeys;
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.VirtualKeysView 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 #setVirtualKeysViewClient(IVirtualKeysView)} and pass it the implementation of
* {@link IVirtualKeysView} so that you can receive callbacks. You can also override other values set
* in {@link VirtualKeysView#VirtualKeysView(Context, AttributeSet)} by calling the respective functions.
* If you extend {@link VirtualKeysView}, you can also set them in the constructor, but do call super().
*
* After this you will have to make a call to {@link VirtualKeysView#reload(VirtualKeysInfo) and pass
* it the {@link VirtualKeysInfo} 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 VirtualKeysView} client
* and calls {@link VirtualKeysView#reload(VirtualKeysInfo).
* The {@link VirtualKeysInfo} is created by TermuxAppSharedProperties.setVirtualKeys().
* Then its got and the view height is adjusted in TermuxActivity.setTerminalToolbarHeight().
* The client used is TermuxTerminalVirtualKeys, which extends
* {@link com.termux.shared.terminal.io.TerminalVirtualKeys} to handle Termux app specific logic and
* leave the rest to the super class.
*/
public final class VirtualKeysView extends GridLayout {
/** The client for the {@link VirtualKeysView}. */
public interface IVirtualKeysView {
/**
* This is called by {@link VirtualKeysView} when a button is clicked. This is also called
* for {@link #mRepetitiveKeys} and {@link VirtualKeyButton} 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 VirtualKeyButton} for the button that was clicked.
* The button may be a {@link VirtualKeyButton#KEY_MACRO} set which can be
* checked with a call to {@link VirtualKeyButton#isMacro()}.
* @param button The {@link Button} that was clicked.
*/
void onVirtualKeyButtonClick(View view, VirtualKeyButton buttonInfo, Button button);
/**
* This is called by {@link VirtualKeysView} 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 VirtualKeyButton} 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 VirtualKeysView#performVirtualKeyButtonHapticFeedback(View, VirtualKeyButton, Button)}
* can handle it depending on system settings.
*/
boolean performVirtualKeyButtonHapticFeedback(View view, VirtualKeyButton 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 = 0xFFf44336;
/** 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 IVirtualKeysView} that acts as a client for the {@link VirtualKeysView}. */
private IVirtualKeysView mVirtualKeysViewClient;
/** The map for the {@link SpecialButton} and their {@link SpecialButtonState}. Defaults to
* the one returned by {@link #getDefaultSpecialButtons(VirtualKeysView)}. */
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 IVirtualKeysView#onVirtualKeyButtonClick(View, VirtualKeyButton, Button)}
* every {@link #mLongPressRepeatDelay} seconds after {@link #mLongPressTimeout} has passed.
* The default keys are defined by {@link VirtualKeysConstants#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 whether text for the extra keys button should be all capitalized automatically. */
private boolean mButtonTextAllCaps = true;
/**
* 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 VirtualKeyButton#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 VirtualKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
setRepetitiveKeys(VirtualKeysConstants.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 #mVirtualKeysViewClient}. */
public IVirtualKeysView getVirtualKeysViewClient() {
return mVirtualKeysViewClient;
}
/** Set {@link #mVirtualKeysViewClient}. */
public void setVirtualKeysViewClient(IVirtualKeysView extraKeysViewClient) {
mVirtualKeysViewClient = 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 VirtualKeysView} 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;
}
/** Set {@link #mButtonTextAllCaps}. */
public void setButtonTextAllCaps(boolean buttonTextAllCaps) {
mButtonTextAllCaps = buttonTextAllCaps;
}
/** 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(VirtualKeysView 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 VirtualKeysView} with the info passed in {@code extraKeysInfo}.
*
* @param extraKeysInfo The {@link VirtualKeysInfo} that defines the necessary info for the extra keys.
*/
@SuppressLint("ClickableViewAccessibility")
public void reload(VirtualKeysInfo extraKeysInfo) {
if (extraKeysInfo == null)
return;
for(SpecialButtonState state : mSpecialButtons.values())
state.buttons = new ArrayList<>();
removeAllViews();
VirtualKeyButton[][] 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 VirtualKeyButton 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.setAllCaps(mButtonTextAllCaps);
button.setPadding(0, 0, 0, 0);
button.setOnClickListener(view -> {
performVirtualKeyButtonHapticFeedback(view, buttonInfo, button);
onAnyVirtualKeyButtonClick(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) {
onAnyVirtualKeyButtonClick(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 onVirtualKeyButtonClick(View view, VirtualKeyButton buttonInfo, Button button) {
if (mVirtualKeysViewClient != null)
mVirtualKeysViewClient.onVirtualKeyButtonClick(view, buttonInfo, button);
}
private void performVirtualKeyButtonHapticFeedback(View view, VirtualKeyButton buttonInfo, Button button) {
if (mVirtualKeysViewClient != null) {
// If client handled the feedback, then just return
if (mVirtualKeysViewClient.performVirtualKeyButtonHapticFeedback(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 onAnyVirtualKeyButtonClick(View view, @NonNull VirtualKeyButton 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 {
onVirtualKeyButtonClick(view, buttonInfo, button);
}
}
private void startScheduledExecutors(View view, VirtualKeyButton 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++;
onVirtualKeyButtonClick(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, VirtualKeyButton 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.setAllCaps(mButtonTextAllCaps);
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 VirtualKeyButton} is a {@link SpecialButton}. */
public boolean isSpecialButton(VirtualKeyButton 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 VirtualKeysView}
* 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;
}
}
@@ -15,13 +15,13 @@
*/
package io.github.rosemoe.editor.text;
import com.itsaky.androidide.utils.Symbols;
import io.github.rosemoe.editor.annotations.Experimental;
import io.github.rosemoe.editor.widget.CodeEditor;
import io.github.rosemoe.struct.BlockLinkedList;
import java.util.ArrayList;
import java.util.List;
import io.github.rosemoe.editor.annotations.Experimental;
import io.github.rosemoe.struct.BlockLinkedList;
import io.github.rosemoe.editor.widget.CodeEditor;
/**
* This class saves the text content for editor and maintains line widths
*
@@ -275,10 +275,10 @@ public class Content implements CharSequence {
public void insert(int line, int column, CharSequence text) {
if(!checkLineAndColumn(line, column, true)) return;
if (text == null) {
throw new IllegalArgumentException("text can not be null");
return;
}
text = text.toString().replace("\t", " ");
text = text.toString().replace("\t", Symbols.createTabSpaces());
//-----Notify------
if (mCursor != null)
mCursor.beforeInsert(line, column);
@@ -2385,7 +2385,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
/**
* Commit a tab to cursor
*/
private void commitTab() {
public void commitTab() {
if (mConnection != null && isEditable()) {
mConnection.commitTextInternal(Symbols.createTabSpaces(), true);
}
@@ -3735,7 +3735,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
// There should not be any tabs in the text
// This is needed for the JavaLanguageServer
String t = text.toString();
t = t.replace("\t", " ");
t = t.replace("\t", Symbols.createTabSpaces());
mText = new Content(this, t);
mCursor = mText.getCursor();
@@ -15,6 +15,7 @@
*/
package io.github.rosemoe.editor.widget;
import com.itsaky.androidide.utils.Symbols;
import io.github.rosemoe.editor.text.Cursor;
import java.util.ArrayList;
import java.util.List;
@@ -39,21 +40,20 @@ public class SymbolChannel {
pairs.add('\'');
pairs.add('>');
}
/**
* Inserts the given text in the editor.
* <p>
* This method allows you to insert texts externally to the content of editor.
* The content of {@param symbolText} is not checked to be exactly characters of symbols.
*
* @throws IllegalArgumentException If the {@param selectionRegion} is invalid
* @param symbolText Text to insert, usually a text of symbols
* @param selectionOffset New selection position relative to the start of text to insert.
* Ranging from 0 to symbolText.length()
*/
public void insertSymbol(String text, int selectionOffset) {
if (selectionOffset < 0 || selectionOffset > text.length()) {
throw new IllegalArgumentException("selectionOffset is invalid");
return;
}
Cursor cur = mEditor.getText().getCursor();
if (cur.isSelected()) {
@@ -1,10 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.itsaky.androidide.views.virtualkeys.VirtualKeysView
android:id="@+id/virtual_key_table"
android:layout_width="match_parent"
android:layout_height="75dp"
android:background="@android:color/black"/>
</LinearLayout>