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\" ...