From b5718010eec86c04863cc09f5557287c3deff53f Mon Sep 17 00:00:00 2001 From: Gabriel Machado <97042217+GabrielBRDeveloper@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:43:30 -0400 Subject: [PATCH] Add scripting support for Android (#369) * Lua Patcher and code editor: initial commit * Code Editor + Lua Patchs + Fixes * some fixes * bonk --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- src/jni_driver.cpp | 6 + src/pandroid/app/src/main/AndroidManifest.xml | 5 + .../app/src/main/assets/fonts/comic_mono.ttf | Bin 0 -> 18724 bytes .../com/panda3ds/pandroid/AlberDriver.java | 2 +- .../panda3ds/pandroid/app/BaseActivity.java | 14 +- .../panda3ds/pandroid/app/GameActivity.java | 21 +- .../panda3ds/pandroid/app/MainActivity.java | 10 +- .../pandroid/app/PandroidApplication.java | 31 ++ .../app/base/BottomDialogFragment.java | 28 ++ .../app/editor/CodeEditorActivity.java | 196 +++++++++++ .../pandroid/app/game/DrawerFragment.java | 10 +- .../pandroid/app/game/LuaDialogFragment.java | 185 ++++++++++ .../panda3ds/pandroid/utils/FileUtils.java | 47 +++ .../pandroid/view/code/BaseEditor.java | 322 ++++++++++++++++++ .../pandroid/view/code/BasicTextEditor.java | 154 +++++++++ .../pandroid/view/code/CodeEditor.java | 51 +++ .../pandroid/view/code/EditorColors.java | 63 ++++ .../pandroid/view/code/syntax/CodeSyntax.java | 21 ++ .../pandroid/view/code/syntax/LuaSyntax.java | 58 ++++ .../view/code/syntax/PatternUtils.java | 18 + .../view/gamesgrid/GamesGridView.java | 27 +- .../view/recycler/AutoFitGridLayout.java | 33 ++ .../view/recycler/SimpleListAdapter.java | 76 +++++ .../app/src/main/res/drawable/ic_code.xml | 5 + .../app/src/main/res/drawable/ic_edit.xml | 5 + .../main/res/drawable/ic_keyboard_hide.xml | 5 + .../app/src/main/res/drawable/ic_play.xml | 5 + .../app/src/main/res/drawable/ic_tab.xml | 5 + .../main/res/layout/activity_code_editor.xml | 133 ++++++++ .../main/res/layout/dialog_lua_scripts.xml | 65 ++++ .../main/res/layout/fragment_game_drawer.xml | 13 + .../src/main/res/layout/holder_lua_script.xml | 52 +++ .../src/main/res/menu/game_drawer_others.xml | 7 + .../src/main/res/values-pt-rBR/strings.xml | 10 + .../app/src/main/res/values/strings.xml | 10 + 35 files changed, 1639 insertions(+), 54 deletions(-) create mode 100644 src/pandroid/app/src/main/assets/fonts/comic_mono.ttf create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomDialogFragment.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/editor/CodeEditorActivity.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BaseEditor.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/CodeEditor.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/EditorColors.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/CodeSyntax.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/LuaSyntax.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/AutoFitGridLayout.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/SimpleListAdapter.java create mode 100644 src/pandroid/app/src/main/res/drawable/ic_code.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_edit.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_keyboard_hide.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_play.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_tab.xml create mode 100644 src/pandroid/app/src/main/res/layout/activity_code_editor.xml create mode 100644 src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml create mode 100644 src/pandroid/app/src/main/res/layout/holder_lua_script.xml create mode 100644 src/pandroid/app/src/main/res/menu/game_drawer_others.xml diff --git a/src/jni_driver.cpp b/src/jni_driver.cpp index 0bdaa3459..6eeb727aa 100644 --- a/src/jni_driver.cpp +++ b/src/jni_driver.cpp @@ -80,6 +80,12 @@ AlberFunction(void, LoadRom)(JNIEnv* env, jobject obj, jstring path) { env->ReleaseStringUTFChars(path, pathStr); } +AlberFunction(void, LoadLuaScript)(JNIEnv* env, jobject obj, jstring script) { + const char* scriptStr = env->GetStringUTFChars(script, nullptr); + emulator->getLua().loadString(scriptStr); + env->ReleaseStringUTFChars(script, scriptStr); +} + AlberFunction(void, TouchScreenDown)(JNIEnv* env, jobject obj, jint x, jint y) { hidService->setTouchScreenPress((u16)x, (u16)y); } AlberFunction(void, TouchScreenUp)(JNIEnv* env, jobject obj) { hidService->releaseTouchScreen(); } AlberFunction(void, KeyUp)(JNIEnv* env, jobject obj, jint keyCode) { hidService->releaseKey((u32)keyCode); } diff --git a/src/pandroid/app/src/main/AndroidManifest.xml b/src/pandroid/app/src/main/AndroidManifest.xml index 8caf9bb06..9f7676543 100644 --- a/src/pandroid/app/src/main/AndroidManifest.xml +++ b/src/pandroid/app/src/main/AndroidManifest.xml @@ -35,6 +35,11 @@ android:name=".app.GameActivity" android:configChanges="screenSize|screenLayout|orientation|density|uiMode"> + + diff --git a/src/pandroid/app/src/main/assets/fonts/comic_mono.ttf b/src/pandroid/app/src/main/assets/fonts/comic_mono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9bc7354e3ad1702a85a08dea067bb2871bce689a GIT binary patch literal 18724 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 literal 0 HcmV?d00001 diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java index 46ab2577d..5cff703c7 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java @@ -19,7 +19,7 @@ public class AlberDriver { public static native void TouchScreenDown(int x, int y); public static native void Pause(); public static native void Resume(); - + public static native void LoadLuaScript(String script); public static native byte[] GetSmdh(); static { System.loadLibrary("Alber"); } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java index 527d49668..56c82d969 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java @@ -8,7 +8,7 @@ public class BaseActivity extends AppCompatActivity { - private int currentTheme = GlobalConfig.get(GlobalConfig.KEY_APP_THEME); + private int currentTheme = PandroidApplication.getThemeId(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -20,19 +20,13 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onResume() { super.onResume(); - if (GlobalConfig.get(GlobalConfig.KEY_APP_THEME) != currentTheme) { + if (PandroidApplication.getThemeId() != currentTheme) { recreate(); } } private void applyTheme() { - switch (GlobalConfig.get(GlobalConfig.KEY_APP_THEME)) { - case GlobalConfig.THEME_ANDROID: setTheme(R.style.Theme_Pandroid); break; - case GlobalConfig.THEME_LIGHT: setTheme(R.style.Theme_Pandroid_Light); break; - case GlobalConfig.THEME_DARK: setTheme(R.style.Theme_Pandroid_Dark); break; - case GlobalConfig.THEME_BLACK: setTheme(R.style.Theme_Pandroid_Black); break; - } - - currentTheme = GlobalConfig.get(GlobalConfig.KEY_APP_THEME); + currentTheme = PandroidApplication.getThemeId(); + setTheme(currentTheme); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java index ade3e2ac9..aced6faad 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java @@ -24,13 +24,7 @@ public class GameActivity extends BaseActivity { private final DrawerFragment drawerFragment = new DrawerFragment(); - private final AlberInputListener inputListener = new AlberInputListener(() -> { - if (drawerFragment.isOpened()) { - drawerFragment.close(); - } else { - drawerFragment.open(); - } - }); + private final AlberInputListener inputListener = new AlberInputListener(this::onBackPressed); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -84,16 +78,25 @@ protected void onPause() { @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (InputHandler.processKeyEvent(event)) { + if ((!drawerFragment.isOpened()) && InputHandler.processKeyEvent(event)) { return true; } return super.dispatchKeyEvent(event); } + @Override + public void onBackPressed() { + if (drawerFragment.isOpened()) { + drawerFragment.close(); + } else { + drawerFragment.open(); + } + } + @Override public boolean dispatchGenericMotionEvent(MotionEvent ev) { - if (InputHandler.processMotionEvent(ev)) { + if ((!drawerFragment.isOpened()) && InputHandler.processMotionEvent(ev)) { return true; } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java index ee7acf883..18914a804 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java @@ -15,10 +15,13 @@ import androidx.fragment.app.FragmentManager; import com.google.android.material.navigation.NavigationBarView; import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.editor.CodeEditorActivity; import com.panda3ds.pandroid.app.main.GamesFragment; import com.panda3ds.pandroid.app.main.SearchFragment; import com.panda3ds.pandroid.app.main.SettingsFragment; +import java.io.File; + public class MainActivity extends BaseActivity implements NavigationBarView.OnItemSelectedListener { private static final int PICK_ROM = 2; @@ -28,13 +31,6 @@ public class MainActivity extends BaseActivity implements NavigationBarView.OnIt private final SearchFragment searchFragment = new SearchFragment(); private final SettingsFragment settingsFragment = new SettingsFragment(); - private void openFile() { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, PICK_ROM); - } - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java index 0ae779dd6..02fbbbcc3 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java @@ -2,7 +2,11 @@ import android.app.Application; import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; + import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; import com.panda3ds.pandroid.data.config.GlobalConfig; import com.panda3ds.pandroid.input.InputMap; import com.panda3ds.pandroid.utils.GameUtils; @@ -22,5 +26,32 @@ public void onCreate() { AlberDriver.Setup(); } + public static int getThemeId() { + switch (GlobalConfig.get(GlobalConfig.KEY_APP_THEME)) { + case GlobalConfig.THEME_LIGHT: + return R.style.Theme_Pandroid_Light; + case GlobalConfig.THEME_DARK: + return R.style.Theme_Pandroid_Dark; + case GlobalConfig.THEME_BLACK: + return R.style.Theme_Pandroid_Black; + } + + return R.style.Theme_Pandroid; + } + + public static boolean isDarkMode() { + switch (GlobalConfig.get(GlobalConfig.KEY_APP_THEME)) { + case GlobalConfig.THEME_DARK: + case GlobalConfig.THEME_BLACK: + return true; + case GlobalConfig.THEME_LIGHT: + return false; + } + + Resources res = Resources.getSystem(); + int nightFlags = res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + return nightFlags == Configuration.UI_MODE_NIGHT_YES; + } + public static Context getAppContext() { return appContext; } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomDialogFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomDialogFragment.java new file mode 100644 index 000000000..4e54dc615 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomDialogFragment.java @@ -0,0 +1,28 @@ +package com.panda3ds.pandroid.app.base; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.Gravity; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import com.panda3ds.pandroid.R; + +public class BottomDialogFragment extends DialogFragment { + @Override + public int getTheme() { + return R.style.AlertDialog; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.getWindow().setGravity(Gravity.CENTER | Gravity.BOTTOM); + dialog.getWindow().getAttributes().y = Math.round(getContext().getResources().getDisplayMetrics().density * 15); + + return dialog; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/editor/CodeEditorActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/editor/CodeEditorActivity.java new file mode 100644 index 000000000..e5ced4b4c --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/editor/CodeEditorActivity.java @@ -0,0 +1,196 @@ +package com.panda3ds.pandroid.app.editor; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import androidx.activity.result.contract.ActivityResultContract; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; +import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.FileUtils; +import com.panda3ds.pandroid.view.code.CodeEditor; +import com.panda3ds.pandroid.view.code.syntax.CodeSyntax; + +import java.io.Serializable; + +public class CodeEditorActivity extends BaseActivity { + private static final String TAB = " "; + private String path; + private String fileName; + private CodeEditor editor; + private AppCompatTextView title; + private View saveButton; + private boolean changed = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_code_editor); + Arguments args = (Arguments) getIntent().getSerializableExtra("args"); + + editor = findViewById(R.id.editor); + getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(this::onGlobalLayoutChanged); + + path = args.path; + fileName = args.fileName; + title = findViewById(R.id.title); + title.setText(fileName); + + saveButton = findViewById(R.id.save); + + saveButton.setVisibility(View.GONE); + saveButton.setOnClickListener(v -> save()); + + new Task(() -> { + String content = FileUtils.readTextFile(path + "/" + fileName); + + editor.post(() -> { + editor.setText(content); + editor.setSyntax(CodeSyntax.getFromFilename(fileName)); + editor.setOnContentChangedListener(this::onDocumentContentChanged); + }); + }).start(); + + switch (args.type) { + case LUA_SCRIPT_EDITOR: + setupLuaPatchEditor(); + break; + case READ_ONLY_EDITOR: + setupReadOnlyEditor(); + break; + } + + onGlobalLayoutChanged(); + + findViewById(R.id.key_hide).setOnClickListener(v -> { + ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(v.getWindowToken(), 0); + }); + findViewById(R.id.key_tab).setOnClickListener(v -> { + editor.insert(TAB); + }); + } + + // Detect virtual keyboard is visible + private void onGlobalLayoutChanged() { + View view = getWindow().getDecorView(); + Rect rect = new Rect(); + view.getWindowVisibleDisplayFrame(rect); + int currentHeight = rect.height(); + int height = view.getHeight(); + + if (currentHeight < height * 0.8) { + findViewById(R.id.keybar).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.keybar).setVisibility(View.GONE); + } + } + + private void setupReadOnlyEditor() { + editor.setEnabled(false); + editor.setFocusable(false); + } + + private void setupLuaPatchEditor() { + findViewById(R.id.lua_toolbar).setVisibility(View.VISIBLE); + findViewById(R.id.lua_play).setOnClickListener(v -> { + if (changed) { + save(); + } + setResult(Activity.RESULT_OK, new Intent(Result.ACTION_PLAY.name())); + finish(); + }); + } + + @SuppressLint("SetTextI18n") + private void onDocumentContentChanged() { + changed = true; + + title.setText("*" + fileName); + saveButton.setVisibility(View.VISIBLE); + } + + public void save() { + title.setText(fileName); + saveButton.setVisibility(View.GONE); + + changed = false; + new Task(() -> FileUtils.writeTextFile(path, fileName, String.valueOf(editor.getText()))).runSync(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_TAB) { + if (event.getAction() == KeyEvent.ACTION_UP) { + editor.insert(TAB); + } + + return true; + } + + return super.dispatchKeyEvent(event); + } + + @Override + public void onBackPressed() { + if (changed) { + new BottomAlertDialog(this) + .setNeutralButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()) + .setPositiveButton(R.string.save_and_exit, (dialog, which) -> { + save(); + finish(); + }) + .setNegativeButton(R.string.exit_without_saving, (dialog, which) -> finish()) + .setTitle(String.format(getString(R.string.exit_without_saving_title_ff), fileName)).show(); + } else { + super.onBackPressed(); + } + } + + public static final class Arguments implements Serializable { + private final String path; + private final String fileName; + private final EditorType type; + + public Arguments(String path, String fileName, EditorType type) { + this.path = path; + this.fileName = fileName; + this.type = type; + } + } + + public enum Result { + ACTION_PLAY, + NULL + } + + public enum EditorType { + LUA_SCRIPT_EDITOR, + READ_ONLY_EDITOR, + TEXT_EDITOR + } + + public static final class Contract extends ActivityResultContract { + @NonNull + @Override + public Intent createIntent(@NonNull Context context, Arguments args) { + return new Intent(context, CodeEditorActivity.class).putExtra("args", args); + } + + @Override + public Result parseResult(int i, @Nullable Intent intent) { + return i == RESULT_OK && intent != null ? Result.valueOf(intent.getAction()) : Result.NULL; + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java index d18fac2b8..bd402b525 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java @@ -47,11 +47,15 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat ((AppCompatTextView)view.findViewById(R.id.game_publisher)).setText(game.getPublisher()); ((NavigationView)view.findViewById(R.id.action_navigation)).setNavigationItemSelectedListener(this); + ((NavigationView)view.findViewById(R.id.others_navigation)).setNavigationItemSelectedListener(this); } @Override public void onDetach() { - drawerContainer.removeDrawerListener(this); + if (drawerContainer != null) { + drawerContainer.removeDrawerListener(this); + } + super.onDetach(); } @@ -99,7 +103,9 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { if (id == R.id.resume) { close(); } else if (id == R.id.exit) { - requireActivity().onBackPressed(); + requireActivity().finish(); + } else if (id == R.id.lua_script){ + new LuaDialogFragment().show(getParentFragmentManager(), null); } return false; diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java new file mode 100644 index 000000000..1db9f9c76 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java @@ -0,0 +1,185 @@ +package com.panda3ds.pandroid.app.game; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; +import com.panda3ds.pandroid.app.base.BottomDialogFragment; +import com.panda3ds.pandroid.app.editor.CodeEditorActivity; +import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.FileUtils; +import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout; +import com.panda3ds.pandroid.view.recycler.SimpleListAdapter; + +import java.util.ArrayList; +import java.util.UUID; + +public class LuaDialogFragment extends BottomDialogFragment { + private final SimpleListAdapter adapter = new SimpleListAdapter<>(R.layout.holder_lua_script, this::onCreateListItem); + private ActivityResultLauncher codeEditorLauncher; + private LuaFile currentEditorFile; + + private ActivityResultLauncher openDocumentLauncher; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.dialog_lua_scripts, container, false); + } + + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + openDocumentLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { + if (result != null) { + String fileName = FileUtils.getName(result.toString()); + + if (fileName.toLowerCase().endsWith(".lua")) { + new Task(() -> { + String content = FileUtils.readTextFile(result.toString()); + createFile(FileUtils.getName(result.toString()), content); + }).start(); + } else { + Toast.makeText(getContext(), R.string.file_not_supported, Toast.LENGTH_SHORT).show(); + } + } + }); + + codeEditorLauncher = registerForActivityResult(new CodeEditorActivity.Contract(), result -> { + if (result != null) { + switch (result) { + case ACTION_PLAY: + loadScript(currentEditorFile); + break; + } + } + + orderByModified(); + }); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + view.findViewById(R.id.open_file).setOnClickListener(v -> { + openDocumentLauncher.launch(new String[]{"*/*"}); + }); + view.findViewById(R.id.create).setOnClickListener(v -> { + new BottomAlertDialog(requireContext()) + .setTextInput(getString(R.string.name), arg -> { + String name = arg.trim(); + if (name.length() > 1) { + new Task(() -> { + LuaFile file = createFile(name, ""); + currentEditorFile = file; + codeEditorLauncher.launch(new CodeEditorActivity.Arguments(file.path, file.name, CodeEditorActivity.EditorType.LUA_SCRIPT_EDITOR)); + }).start(); + } + }).setTitle(R.string.create_new) + .show(); + }); + + ((RecyclerView) view.findViewById(R.id.recycler)).setAdapter(adapter); + ((RecyclerView) view.findViewById(R.id.recycler)).setLayoutManager(new AutoFitGridLayout(getContext(), 140)); + FileUtils.createDir(FileUtils.getResourcesPath(), "Lua Scripts"); + ArrayList files = new ArrayList<>(); + String path = FileUtils.getResourcesPath() + "/Lua Scripts/"; + for (String file : FileUtils.listFiles(path)) { + files.add(new LuaFile(file)); + } + + adapter.addAll(files); + orderByModified(); + } + + private LuaFile createFile(String name, String content) { + if (name.toLowerCase().endsWith(".lua")) { + name = name.substring(0, name.length() - 4); + } + + name = name.replaceAll("[^[a-zA-Z0-9-_ ]]", "-"); + + String fileName = name + "." + UUID.randomUUID().toString().substring(0, 4) + ".lua"; + LuaFile file = new LuaFile(fileName); + FileUtils.writeTextFile(file.path, fileName, content); + getView().post(() -> { + adapter.addAll(file); + orderByModified(); + }); + + return file; + } + + private void orderByModified() { + adapter.sort((o1, o2) -> Long.compare(o2.lastModified(), o1.lastModified())); + } + + private void onCreateListItem(int position, LuaFile file, View view) { + ((TextView) view.findViewById(R.id.title)) + .setText(file.name.split("\\.")[0]); + + view.setOnClickListener(v -> loadScript(file)); + view.findViewById(R.id.edit).setOnClickListener(v -> { + currentEditorFile = file; + codeEditorLauncher.launch(new CodeEditorActivity.Arguments(file.path, file.name, CodeEditorActivity.EditorType.LUA_SCRIPT_EDITOR)); + }); + } + + private void loadScript(LuaFile file) { + dismiss(); + + Toast.makeText(getContext(), String.format(getString(R.string.running_ff), file.name), Toast.LENGTH_SHORT).show(); + new Task(() -> { + String script = FileUtils.readTextFile(file.absolutePath()); + file.update(); + AlberDriver.LoadLuaScript(script); + }).start(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + openDocumentLauncher.unregister(); + codeEditorLauncher.unregister(); + } + + private static class LuaFile { + private final String name; + private final String path; + + private LuaFile(String path, String name) { + this.name = name; + this.path = path; + } + + private LuaFile(String name) { + this(FileUtils.getResourcesPath() + "/Lua Scripts/", name); + } + + private String absolutePath() { + return path + "/" + name; + } + + private void update() { + FileUtils.updateFile(absolutePath()); + } + + private long lastModified() { + return FileUtils.getLastModified(absolutePath()); + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java index a065cd0ca..45faf5a47 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java @@ -39,6 +39,15 @@ public static String getName(String path) { return parseFile(path).getName(); } + public static String getResourcesPath(){ + File file = new File(getPrivatePath(), "config/resources"); + if (!file.exists()) { + file.mkdirs(); + } + + return file.getAbsolutePath(); + } + public static String getPrivatePath() { File file = getContext().getFilesDir(); if (!file.exists()) { @@ -177,4 +186,42 @@ public static String obtainRealPath(String uri) { return null; } } + + public static void updateFile(String path){ + DocumentFile file = parseFile(path); + Uri uri = file.getUri(); + + switch (uri.getScheme()) { + case "file": { + new File(uri.getPath()).setLastModified(System.currentTimeMillis()); + break; + } + + case "content": { + getContext().getContentResolver().update(uri, null, null, null); + break; + } + + default: { + Log.w(Constants.LOG_TAG, "Cannot update file from scheme: " + uri.getScheme()); + break; + } + } + } + + public static long getLastModified(String path) { + return parseFile(path).lastModified(); + } + + public static String[] listFiles(String path){ + DocumentFile folder = parseFile(path); + DocumentFile[] files = folder.listFiles(); + + String[] result = new String[files.length]; + for (int i = 0; i < result.length; i++){ + result[i] = files[i].getName(); + } + + return result; + } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BaseEditor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BaseEditor.java new file mode 100644 index 000000000..4dba9f7cd --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BaseEditor.java @@ -0,0 +1,322 @@ +package com.panda3ds.pandroid.view.code; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.text.Editable; +import android.text.Layout; +import android.util.AttributeSet; +import android.view.ViewTreeObserver; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; + +public class BaseEditor extends BasicTextEditor { + private static final String HELLO_WORLD = "Hello World"; + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); + private final Rect rect = new Rect(); + private int currentLine; + private float spaceWidth; + private int lineHeight; + private int textOffset; + private int beginLine; + private int beginIndex; + private int endLine; + private int endIndex; + private int visibleHeight; + private int contentWidth; + private Layout textLayout; + private int currentWidth = -1; + private int currentHeight = -1; + + private final char[] textBuffer = new char[1]; + protected final int[] colors = new int[256]; + + // Allocate 512KB for the buffer + protected final byte[] syntaxBuffer = new byte[512 * 1024]; + private boolean requireUpdate = true; + + public BaseEditor(@NonNull Context context) { + super(context); + } + + public BaseEditor(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public BaseEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + { + EditorColors.obtainColorScheme(colors, getContext()); + } + + @Override + protected void initialize() { + super.initialize(); + getViewTreeObserver().addOnGlobalLayoutListener(() -> { + adjustScroll(); + requireUpdate = true; + }); + } + + @SuppressLint("MissingSuperCall") + @Override + public void draw(Canvas canvas) { + //super.draw(canvas); + canvas.drawColor(colors[EditorColors.COLOR_BACKGROUND]); + textLayout = getLayout(); + if (textLayout == null) { + postDelayed(this::invalidate, 25); + return; + } + + try { + prepareDraw(); + if (requireUpdate) { + onVisibleContentChanged(beginIndex, endIndex - beginIndex); + } + + if (getSelectionStart() == getSelectionEnd()) { + drawCaret(canvas); + drawCurrentLine(canvas); + } else { + drawSelection(canvas); + } + + drawText(canvas); + drawLineCount(canvas); + } catch (Throwable e) { + drawError(canvas, e); + } + } + + private void drawError(Canvas canvas, Throwable e) { + canvas.drawColor(Color.RED); + paint.setTextSize(getTextSize()); + paint.setColor(Color.WHITE); + canvas.drawText("Editor draw error:", getPaddingLeft(), getLineHeight(), paint); + canvas.drawText(String.valueOf(e), getPaddingLeft(), getLineHeight() * 2, paint); + + int index = 2; + for (StackTraceElement trace : e.getStackTrace()) { + index++; + if (index > 5) break; + canvas.drawText(trace.getClassName() + ":" + trace.getMethodName() + ":" + trace.getLineNumber(), getPaddingLeft(), getLineHeight() * index, paint); + } + } + + private void prepareDraw() { + paint.setTypeface(getTypeface()); + paint.setTextSize(getTextSize()); + + Paint.FontMetrics fontMetrics = paint.getFontMetrics(); + spaceWidth = paint.measureText(" "); + lineHeight = getLineHeight(); + + //Align text to center of line + { + int ascent = (int) Math.abs(fontMetrics.ascent); + paint.getTextBounds(HELLO_WORLD, 0, HELLO_WORLD.length(), rect); + textOffset = Math.max(((lineHeight - rect.height()) / 2), 0) + ascent; + } + + int lineCount = textLayout.getLineCount(); + currentLine = textLayout.getLineForOffset(getSelectionStart()); + + int oldBeginLine = beginLine; + int oldEndLine = endLine; + + beginLine = Math.max(0, Math.min((getScrollY() / lineHeight) - 1, lineCount)); + beginIndex = textLayout.getLineStart(beginLine); + + if (oldEndLine != endLine || beginLine != oldBeginLine) { + requireUpdate = true; + } + + getGlobalVisibleRect(rect); + visibleHeight = rect.height(); + + endLine = Math.round(((float) visibleHeight / lineHeight) + 2) + beginLine; + endIndex = getLayout().getLineStart(Math.min(lineCount, endLine)); + + int padding = (int) (paint.measureText(String.valueOf(lineCount)) + (spaceWidth * 4)); + if (getPaddingLeft() != padding) { + setPadding(padding, 0, 0, 0); + } + + contentWidth = getWidth() + getScrollX(); + } + + private void drawLineCount(Canvas canvas) { + int colorEnable = colors[EditorColors.COLOR_TEXT]; + int colorDisable = applyAlphaToColor(colors[EditorColors.COLOR_TEXT], 100); + + paint.setColor(colors[EditorColors.COLOR_BACKGROUND_SECONDARY]); + int scrollY = getScrollY(); + float x = getScrollX(); + + canvas.translate(x, 0); + canvas.drawRect(0, scrollY, getPaddingLeft() - spaceWidth, visibleHeight + scrollY, paint); + paint.setColor(colors[EditorColors.COLOR_CURRENT_LINE]); + canvas.drawRect(0, currentLine * lineHeight, getPaddingLeft() - spaceWidth, (currentLine * lineHeight) + lineHeight, paint); + + for (int i = beginLine; i < Math.min(getLineCount(), endLine); i++) { + String text = String.valueOf(i + 1); + if (i == currentLine) { + paint.setColor(colorEnable); + } else { + paint.setColor(colorDisable); + } + + float width = paint.measureText(text); + canvas.drawText(text, getPaddingLeft() - width - (spaceWidth * 2.5f), (i * lineHeight) + textOffset, paint); + } + + paint.setColor(applyAlphaToColor(colorEnable, 10)); + canvas.drawRect(getPaddingLeft() - spaceWidth - (spaceWidth / 4), scrollY, getPaddingLeft() - spaceWidth, visibleHeight + scrollY, paint); + + canvas.translate(-x, 0); + } + + private void drawCurrentLine(Canvas canvas) { + float y = currentLine * lineHeight; + paint.setColor(colors[EditorColors.COLOR_CURRENT_LINE]); + canvas.drawRect(0, y, contentWidth, y + lineHeight, paint); + } + + private void drawText(Canvas canvas) { + Editable edit = getText(); + float x = 0; + float y = textOffset; + int line = 0; + + canvas.translate(getPaddingLeft(), beginLine * lineHeight); + + paint.setColor(colors[EditorColors.COLOR_TEXT]); + for (int i = beginIndex; i < endIndex; i++) { + textBuffer[0] = edit.charAt(i); + switch (textBuffer[0]) { + case '\n': + line++; + x = 0; + y = (line * lineHeight) + textOffset; + break; + + case ' ': + x += spaceWidth; + break; + + default: + paint.setColor(colors[syntaxBuffer[i - beginIndex]]); + canvas.drawText(textBuffer, 0, 1, x, y, paint); + x += paint.measureText(textBuffer, 0, 1); + break; + } + } + + canvas.translate(-getPaddingLeft(), -(beginLine * lineHeight)); + } + + private void drawCaret(Canvas canvas) { + int start = textLayout.getLineStart(currentLine); + int end = textLayout.getLineEnd(currentLine); + int position = getSelectionStart(); + float x = getPaddingLeft(); + float y = (currentLine * lineHeight); + Editable text = getText(); + for (int i = start; i < end; i++) { + if (i == position) { + break; + } + + textBuffer[0] = text.charAt(i); + x += paint.measureText(textBuffer, 0, 1); + } + + paint.setColor(colors[EditorColors.COLOR_CARET]); + float caretWidth = spaceWidth / 2; + canvas.drawRect(x - (caretWidth / 2), y, x + (caretWidth / 2), y + lineHeight, paint); + } + + private void drawSelection(Canvas canvas) { + int start = getSelectionStart(); + int end = getSelectionEnd(); + int endLine = textLayout.getLineForOffset(end); + canvas.translate(getPaddingLeft(), 0); + + paint.setColor(colors[EditorColors.COLOR_SELECTION]); + + Editable text = getText(); + + for (int line = currentLine; line <= endLine; line++) { + + if (line < beginLine) continue; + if (line > this.endLine) break; + + if (line == endLine || line == currentLine) { + int lineStart = textLayout.getLineStart(line); + float x = 0; + + if (lineStart <= start) { + x = paint.measureText(text, lineStart, start); + lineStart = start; + } + float width; + if (line < endLine) { + width = contentWidth; + } else { + width = paint.measureText(text, lineStart, end); + } + + canvas.drawRect(x, lineHeight * line, x + width, (lineHeight * line) + lineHeight, paint); + } else { + canvas.drawRect(0, lineHeight * line, contentWidth, (lineHeight * line) + lineHeight, paint); + } + } + canvas.translate(-getPaddingLeft(), 0); + } + + public int applyAlphaToColor(int color, int alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + protected void onVisibleContentChanged(int index, int length) { + requireUpdate = false; + + Arrays.fill(syntaxBuffer, (byte) 0); + if (length > 0) { + onRefreshColorScheme(syntaxBuffer, index, length); + } + } + + protected void onRefreshColorScheme(byte[] buffer, int index, int length) { + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (currentWidth != getMeasuredWidth() || currentHeight != getMeasuredHeight()) { + currentWidth = getMeasuredWidth(); + currentHeight = getMeasuredHeight(); + invalidateAll(); + } + } + + protected void invalidateAll() { + requireUpdate = true; + invalidate(); + } + + @Override + protected void onTextChanged() { + requireUpdate = true; + super.onTextChanged(); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java new file mode 100644 index 000000000..1d4976567 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java @@ -0,0 +1,154 @@ +package com.panda3ds.pandroid.view.code; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.EditorInfo; +import android.widget.Scroller; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; + +import com.panda3ds.pandroid.view.SimpleTextWatcher; + +public class BasicTextEditor extends AppCompatEditText { + private GestureDetector gestureDetector; + private final Rect visibleRect = new Rect(); + + public BasicTextEditor(@NonNull Context context) { + super(context); + initialize(); + } + + public BasicTextEditor(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + public BasicTextEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(); + } + + protected void initialize() { + setTypeface(Typeface.MONOSPACE); + gestureDetector = new GestureDetector(getContext(), new ScrollGesture()); + + setTypeface(Typeface.createFromAsset(getContext().getAssets(), "fonts/comic_mono.ttf")); + setGravity(Gravity.START | Gravity.TOP); + setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + setLineSpacing(0, 1.3f); + setScroller(new Scroller(getContext())); + + setInputType(InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | + InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE | + InputType.TYPE_TEXT_FLAG_MULTI_LINE | + InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + + setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + setBackgroundColor(Color.BLACK); + setTextColor(Color.WHITE); + + setFocusableInTouchMode(true); + setHorizontallyScrolling(true); + setHorizontalScrollBarEnabled(true); + + addTextChangedListener((SimpleTextWatcher) value -> BasicTextEditor.this.onTextChanged()); + } + + // Disable default Android scroll + @Override + public void scrollBy(int x, int y) {} + + @Override + public void scrollTo(int x, int y) {} + + public void setScroll(int x, int y) { + x = Math.max(0, x); + y = Math.max(0, y); + + int maxHeight = Math.round(getLineCount() * getLineHeight()); + getGlobalVisibleRect(visibleRect); + maxHeight = Math.max(0, maxHeight - visibleRect.height()); + + int maxWidth = (int) getPaint().measureText(getText(), 0, length()); + maxWidth += getPaddingLeft() + getPaddingRight(); + + int scrollX = x - Math.max(Math.min(maxWidth - visibleRect.width(), x), 0); + int scrollY = Math.min(maxHeight, y); + + super.scrollTo(scrollX, scrollY); + } + + public void adjustScroll(){ + setScroll(getScrollX(), getScrollY()); + } + + protected void onTextChanged() {} + + private boolean onSuperTouchListener(MotionEvent event) { + return super.onTouchEvent(event); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + + private class ScrollGesture implements GestureDetector.OnGestureListener { + @Override + public boolean onDown(@NonNull MotionEvent e) { + return true; + } + + @Override + public void onShowPress(@NonNull MotionEvent e) { + onSuperTouchListener(e); + } + + @Override + public boolean onSingleTapUp(@NonNull MotionEvent e) { + return onSuperTouchListener(e); + } + + @Override + public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { + int scrollX = (int) Math.max(0, getScrollX() + distanceX); + int scrollY = (int) Math.max(0, getScrollY() + distanceY); + setScroll(scrollX, scrollY); + return true; + } + + @Override + public void onLongPress(@NonNull MotionEvent e) { + onSuperTouchListener(e); + } + + @Override + public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { + return false; + } + } + + public void insert(CharSequence text) { + if (getSelectionStart() == getSelectionEnd()) { + getText().insert(getSelectionStart(), text); + } else { + getText().replace(getSelectionStart(), getSelectionEnd(), text); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/CodeEditor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/CodeEditor.java new file mode 100644 index 000000000..96a8637b2 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/CodeEditor.java @@ -0,0 +1,51 @@ +package com.panda3ds.pandroid.view.code; + +import android.content.Context; +import android.util.AttributeSet; + +import com.panda3ds.pandroid.view.code.syntax.CodeSyntax; + +public class CodeEditor extends BaseEditor { + private CodeSyntax syntax; + private Runnable contentChangeListener; + + public CodeEditor(Context context) { + super(context); + } + + public CodeEditor(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CodeEditor(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setSyntax(CodeSyntax syntax) { + this.syntax = syntax; + invalidateAll(); + } + + public void setOnContentChangedListener(Runnable contentChangeListener) { + this.contentChangeListener = contentChangeListener; + } + + @Override + protected void onTextChanged() { + super.onTextChanged(); + if (contentChangeListener != null) { + contentChangeListener.run(); + } + } + + @Override + protected void onRefreshColorScheme(byte[] buffer, int index, int length) { + super.onRefreshColorScheme(buffer, index, length); + + if (syntax != null) { + final CharSequence text = getText().subSequence(index, index + length); + syntax.apply(syntaxBuffer, text); + System.gc(); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/EditorColors.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/EditorColors.java new file mode 100644 index 000000000..3b12ddf9e --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/EditorColors.java @@ -0,0 +1,63 @@ +package com.panda3ds.pandroid.view.code; + +import android.content.Context; + +import com.panda3ds.pandroid.app.PandroidApplication; + +public class EditorColors { + public static final byte COLOR_TEXT = 0x0; + public static final byte COLOR_KEYWORDS = 0x1; + public static final byte COLOR_NUMBERS = 0x2; + public static final byte COLOR_STRING = 0x3; + public static final byte COLOR_METADATA = 0x4; + public static final byte COLOR_COMMENT = 0x5; + public static final byte COLOR_SYMBOLS = 0x6; + public static final byte COLOR_FIELDS = 0x7; + public static final byte COLOR_BACKGROUND = 0x1D; + public static final byte COLOR_BACKGROUND_SECONDARY = 0x2D; + public static final byte COLOR_SELECTION = 0x3D; + public static final byte COLOR_CARET = 0x4D; + public static final byte COLOR_CURRENT_LINE = 0x5D; + + public static void obtainColorScheme(int[] colors, Context context) { + if (PandroidApplication.isDarkMode()) { + applyDarkTheme(colors); + } else { + applyLightTheme(colors); + } + } + + private static void applyLightTheme(int[] colors) { + colors[EditorColors.COLOR_TEXT] = 0xFF000000; + colors[EditorColors.COLOR_KEYWORDS] = 0xFF3AE666; + colors[EditorColors.COLOR_NUMBERS] = 0xFF3A9EE6; + colors[EditorColors.COLOR_METADATA] = 0xFF806AE6; + colors[EditorColors.COLOR_SYMBOLS] = 0xFF202020; + colors[EditorColors.COLOR_STRING] = 0xFF2EB541; + colors[EditorColors.COLOR_FIELDS] = 0xFF9876AA; + colors[EditorColors.COLOR_COMMENT] = 0xFF808080; + + colors[EditorColors.COLOR_BACKGROUND] = 0xFFFFFFFF; + colors[EditorColors.COLOR_BACKGROUND_SECONDARY] = 0xFFF0F0F0; + colors[EditorColors.COLOR_SELECTION] = 0x701F9EDE; + colors[EditorColors.COLOR_CARET] = 0xFF000000; + colors[EditorColors.COLOR_CURRENT_LINE] = 0x05000050; + } + + private static void applyDarkTheme(int[] colors) { + colors[EditorColors.COLOR_TEXT] = 0xFFFFFFFF; + colors[EditorColors.COLOR_KEYWORDS] = 0xFFE37F3E; + colors[EditorColors.COLOR_NUMBERS] = 0xFF3A9EE6; + colors[EditorColors.COLOR_METADATA] = 0xFFC5CA1D; + colors[EditorColors.COLOR_SYMBOLS] = 0xFFC0C0C0; + colors[EditorColors.COLOR_STRING] = 0xFF2EB541; + colors[EditorColors.COLOR_FIELDS] = 0xFF9876AA; + colors[EditorColors.COLOR_COMMENT] = 0xFFBBBBBB; + + colors[EditorColors.COLOR_BACKGROUND] = 0xFF2B2B2B; + colors[EditorColors.COLOR_BACKGROUND_SECONDARY] = 0xFF313335; + colors[EditorColors.COLOR_SELECTION] = 0x701F9EDE; + colors[EditorColors.COLOR_CARET] = 0x60FFFFFF; + colors[EditorColors.COLOR_CURRENT_LINE] = 0x10FFFFFF; + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/CodeSyntax.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/CodeSyntax.java new file mode 100644 index 000000000..6c50865f4 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/CodeSyntax.java @@ -0,0 +1,21 @@ +package com.panda3ds.pandroid.view.code.syntax; + +public abstract class CodeSyntax { + public abstract void apply(byte[] syntaxBuffer, final CharSequence text); + + // Get syntax highlighting data for a file based on its filename, by looking at the extension + public static CodeSyntax getFromFilename(String name) { + name = name.trim().toLowerCase(); + String[] parts = name.split("\\."); + if (parts.length == 0) + return null; + + // Get syntax based on file extension + switch (parts[parts.length - 1]) { + case "lua": + return new LuaSyntax(); + default: + return null; + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/LuaSyntax.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/LuaSyntax.java new file mode 100644 index 000000000..d53fb1d79 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/LuaSyntax.java @@ -0,0 +1,58 @@ +package com.panda3ds.pandroid.view.code.syntax; + +import com.panda3ds.pandroid.view.code.EditorColors; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class LuaSyntax extends CodeSyntax { + public static final Pattern comment = Pattern.compile("(\\-\\-.*)"); + + public static final Pattern keywords = PatternUtils.buildGenericKeywords( + "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", + "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"); + + public static final Pattern identifiers = PatternUtils.buildGenericKeywords( + "assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "loadfile", "load", "loadstring", "next", "pairs", "pcall", "print", "rawequal", "rawlen", "rawget", "rawset", + "select", "setmetatable", "tonumber", "tostring", "type", "xpcall", "_G", "_VERSION", "arshift", "band", "bnot", "bor", "bxor", "btest", "extract", "lrotate", "lshift", "replace", + "rrotate", "rshift", "create", "resume", "running", "status", "wrap", "yield", "isyieldable", "debug", "getuservalue", "gethook", "getinfo", "getlocal", "getregistry", "getmetatable", + "getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen", + "read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger", + "floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh", + "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", + "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", + "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", + "coroutine", "table", "io", "os", "string", "uint8_t", "bit32", "math", "debug", "package"); + + public static final Pattern string = Pattern.compile("((\")(.*?)([^\\\\]\"))|((\")(.+))|((')(.?)('))"); + public static final Pattern symbols = Pattern.compile("([.!&?:;*+/{}()\\]\\[,=-])"); + public static final Pattern numbers = Pattern.compile("\\b((\\d*[.]?\\d+([Ee][+-]?[\\d]+)?[LlfFdD]?)|(0[xX][0-9a-zA-Z]+)|(0[bB][0-1]+)|(0[0-7]+))\\b"); + + @Override + public void apply(byte[] syntaxBuffer, CharSequence text) { + for (Matcher matcher = keywords.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_KEYWORDS); + } + + for (Matcher matcher = identifiers.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_FIELDS); + } + + for (Matcher matcher = symbols.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_SYMBOLS); + } + + for (Matcher matcher = numbers.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_NUMBERS); + } + + for (Matcher matcher = string.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_STRING); + } + + for (Matcher matcher = comment.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_COMMENT); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java new file mode 100644 index 000000000..e3e5128ae --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java @@ -0,0 +1,18 @@ +package com.panda3ds.pandroid.view.code.syntax; + +import java.util.regex.Pattern; + +class PatternUtils { + public static Pattern buildGenericKeywords(String... keywords){ + StringBuilder builder = new StringBuilder(); + builder.append("\\b("); + for (int i = 0; i < keywords.length; i++){ + builder.append(keywords[i]); + if (i+1 != keywords.length){ + builder.append("|"); + } + } + builder.append(")\\b"); + return Pattern.compile(builder.toString()); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java index d218d4677..24e65e2ff 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java @@ -5,15 +5,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout; import java.util.List; public class GamesGridView extends RecyclerView { - private int iconSize = 170; private final GameAdapter adapter; public GamesGridView(@NonNull Context context) { @@ -26,33 +25,11 @@ public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs) { public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - setLayoutManager(new AutoFitLayout()); + setLayoutManager(new AutoFitGridLayout(getContext(), 170)); setAdapter(adapter = new GameAdapter()); } public void setGameList(List games) { adapter.replace(games); } - - public void setIconSize(int iconSize) { - this.iconSize = iconSize; - requestLayout(); - measure(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY); - } - - private final class AutoFitLayout extends GridLayoutManager { - public AutoFitLayout() { - super(GamesGridView.this.getContext(), 1); - } - - @Override - public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec, int heightSpec) { - super.onMeasure(recycler, state, widthSpec, heightSpec); - int width = getMeasuredWidth(); - int iconSize = (int) (GamesGridView.this.iconSize * getResources().getDisplayMetrics().density); - int iconCount = Math.max(1, width / iconSize); - if (getSpanCount() != iconCount) - setSpanCount(iconCount); - } - } } \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/AutoFitGridLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/AutoFitGridLayout.java new file mode 100644 index 000000000..26f80adb8 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/AutoFitGridLayout.java @@ -0,0 +1,33 @@ +package com.panda3ds.pandroid.view.recycler; + +import android.content.Context; +import android.util.TypedValue; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public final class AutoFitGridLayout extends GridLayoutManager { + private final int iconSize; + private final Context context; + + public AutoFitGridLayout(Context context, int iconSize) { + super(context, 1); + + this.iconSize = iconSize; + this.context = context; + } + + @Override + public void onMeasure(@NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state, int widthSpec, int heightSpec) { + super.onMeasure(recycler, state, widthSpec, heightSpec); + int width = View.MeasureSpec.getSize(widthSpec); + int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.iconSize, context.getResources().getDisplayMetrics()); + int iconCount = Math.max(1, width / iconSize); + + if (getSpanCount() != iconCount) { + setSpanCount(iconCount); + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/SimpleListAdapter.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/SimpleListAdapter.java new file mode 100644 index 000000000..7d4fa7c37 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/SimpleListAdapter.java @@ -0,0 +1,76 @@ +package com.panda3ds.pandroid.view.recycler; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class SimpleListAdapter extends RecyclerView.Adapter { + private final ArrayList list = new ArrayList<>(); + private final Binder binder; + private final int layoutId; + + public SimpleListAdapter(@LayoutRes int layoutId, Binder binder) { + this.layoutId = layoutId; + this.binder = binder; + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new Holder(LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + binder.bind(position, list.get(position), holder.getView()); + } + + public void addAll(T... items) { + addAll(Arrays.asList(items)); + } + + public void addAll(List items) { + int index = list.size(); + this.list.addAll(items); + notifyItemRangeInserted(index, getItemCount() - index); + } + + public void clear() { + int count = getItemCount(); + list.clear(); + notifyItemRangeRemoved(0, count); + } + + public void sort(Comparator comparator) { + list.sort(comparator); + notifyItemRangeChanged(0, getItemCount()); + } + + @Override + public int getItemCount() { + return list.size(); + } + + public interface Binder { + void bind(int position, I item, View view); + } + + public static class Holder extends RecyclerView.ViewHolder { + public Holder(@NonNull View itemView) { + super(itemView); + } + + public View getView() { + return itemView; + } + } +} diff --git a/src/pandroid/app/src/main/res/drawable/ic_code.xml b/src/pandroid/app/src/main/res/drawable/ic_code.xml new file mode 100644 index 000000000..8ef40bd2d --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_code.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_edit.xml b/src/pandroid/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..1c9bd3e6b --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_keyboard_hide.xml b/src/pandroid/app/src/main/res/drawable/ic_keyboard_hide.xml new file mode 100644 index 000000000..d4e7929b2 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_keyboard_hide.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_play.xml b/src/pandroid/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 000000000..d75d7da3e --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_tab.xml b/src/pandroid/app/src/main/res/drawable/ic_tab.xml new file mode 100644 index 000000000..3f7efd95c --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_tab.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/layout/activity_code_editor.xml b/src/pandroid/app/src/main/res/layout/activity_code_editor.xml new file mode 100644 index 000000000..5cef8609d --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/activity_code_editor.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml b/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml new file mode 100644 index 000000000..69a9d0a46 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml b/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml index 2da6b9c8e..fa81a503f 100644 --- a/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml +++ b/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml @@ -85,6 +85,19 @@ app:menu="@menu/game_drawer_actions" android:background="?colorSurface"/> + + + + diff --git a/src/pandroid/app/src/main/res/layout/holder_lua_script.xml b/src/pandroid/app/src/main/res/layout/holder_lua_script.xml new file mode 100644 index 000000000..a1865c3f4 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/holder_lua_script.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/menu/game_drawer_others.xml b/src/pandroid/app/src/main/res/menu/game_drawer_others.xml new file mode 100644 index 000000000..b6dd48976 --- /dev/null +++ b/src/pandroid/app/src/main/res/menu/game_drawer_others.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml index 18edc50f0..065b9e4f0 100644 --- a/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml +++ b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml @@ -35,4 +35,14 @@ Disposições de controle Disposição de controle padrão Nome Invalido + Trapaças + Script Lua + Scripts + Esse arquivo não é suportado + Salvar e sair + Sair sem salvar + Salvar \"%s\" antes de sair? + Abrir arquivo + Criar novo + Executando \"%s\" ... \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values/strings.xml b/src/pandroid/app/src/main/res/values/strings.xml index f8e2098a4..e0de62e18 100644 --- a/src/pandroid/app/src/main/res/values/strings.xml +++ b/src/pandroid/app/src/main/res/values/strings.xml @@ -36,4 +36,14 @@ Screen gamepad layouts Default screen gamepad layout Invalid name + Hacks + Lua script + Scripts + File type isn\'t supported + Save and exit + Exit without saving + Exit without saving \"%s\"? + Open file + Create new + Running \"%s\" ...