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 000000000..9bc7354e3
Binary files /dev/null and b/src/pandroid/app/src/main/assets/fonts/comic_mono.ttf differ
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\" ...