From 02496b57eb13fd809080088af30f182b4d6c3fd7 Mon Sep 17 00:00:00 2001 From: Gabriel Machado <97042217+GabrielBRDeveloper@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:06:23 -0400 Subject: [PATCH] Pandroid: Screen layout editor (#425) * Add Dynamic colors option for Android 12 or above (#8) * Dynamic color for android 12 or above @Ishan09811 * Revert "Add Dynamic colors option for Android 12 or above (#8)" (#9) This reverts commit 6ee1a39fb4e5913e4f7f75004b51ff06c25c1fd0. * Material Switch (#10) | ISHAN | REVIEW * Review Material Switch * basic implement, ds editor, and remake theme selector * add swap screen in drawer * basic file provider for access private folder from file explorer * Pandroid: Game about dialog (#7) * Alert dialog idea * Reimplement ishan game dialog. * Add Picture In Picture support with a option (#11) - Ishan picture in picture pr * some fixes * Add game to launcher and some clear code. --------- Co-authored-by: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Co-authored-by: gabriel --- src/pandroid/app/src/main/AndroidManifest.xml | 25 +- .../panda3ds/pandroid/app/BaseActivity.java | 7 +- .../panda3ds/pandroid/app/GameActivity.java | 70 +++- .../panda3ds/pandroid/app/MainActivity.java | 17 +- .../app/base/BasePreferenceFragment.java | 8 +- .../pandroid/app/base/BaseSheetDialog.java | 46 +++ .../pandroid/app/base/GameAboutDialog.java | 81 ++++ .../pandroid/app/game/AlberInputListener.java | 11 +- .../pandroid/app/game/DrawerFragment.java | 33 +- .../pandroid/app/game/EmulatorCallback.java | 11 + .../pandroid/app/game/GameLauncher.java | 37 ++ .../pandroid/app/main/GamesFragment.java | 8 + .../pandroid/app/main/SearchFragment.java | 6 + .../pandroid/app/main/SettingsFragment.java | 16 +- .../app/preferences/AdvancedPreferences.java | 15 +- .../preferences/AppearancePreferences.java | 27 -- .../preferences/GamesFoldersPreferences.java | 100 +++++ .../app/preferences/GeneralPreferences.java | 35 ++ .../app/preferences/ThemeSelectorDialog.java | 80 ++++ .../screen_editor/ScreenEditorPreference.java | 45 +++ .../ScreenLayoutsPreference.java | 42 +++ .../app/provider/AppDataDocumentProvider.java | 162 ++++++++ .../pandroid/data/GsonConfigParser.java | 7 +- .../java/com/panda3ds/pandroid/data/SMDH.java | 2 +- .../pandroid/data/config/GlobalConfig.java | 22 ++ .../pandroid/data/game/GameMetadata.java | 21 +- .../pandroid/data/game/GameRegion.java | 22 +- .../pandroid/data/game/GamesFolder.java | 61 +++ .../panda3ds/pandroid/input/InputHandler.java | 15 +- .../com/panda3ds/pandroid/input/KeyName.java | 3 +- .../panda3ds/pandroid/utils/CompatUtils.java | 39 ++ .../panda3ds/pandroid/utils/FileUtils.java | 62 +-- .../panda3ds/pandroid/utils/GameUtils.java | 70 +++- .../pandroid/view/PandaGlRenderer.java | 44 +-- .../com/panda3ds/pandroid/view/ds/Bounds.java | 46 +++ .../pandroid/view/ds/DsEditorView.java | 353 ++++++++++++++++++ .../panda3ds/pandroid/view/ds/DsLayout.java | 179 +++++++++ .../pandroid/view/ds/DsLayoutManager.java | 52 +++ .../com/panda3ds/pandroid/view/ds/Mode.java | 7 + .../com/panda3ds/pandroid/view/ds/Model.java | 30 ++ .../pandroid/view/gamesgrid/GameAdapter.java | 13 + .../view/gamesgrid/GamesGridView.java | 19 +- .../pandroid/view/gamesgrid/ItemHolder.java | 8 +- .../SingleSelectionPreferences.java | 86 ----- ...nLayout.java => RelativeScreenLayout.java} | 81 +++- .../renderer/layout/SingleScreenLayout.java | 63 ++++ .../res/drawable/alert_dialog_background.xml | 1 + .../drawable/ds_editor_popup_background.xml | 5 + .../src/main/res/drawable/ic_align_center.xml | 9 + .../src/main/res/drawable/ic_arrow_down.xml | 10 + .../app/src/main/res/drawable/ic_arrow_up.xml | 10 + .../app/src/main/res/drawable/ic_code.xml | 2 +- .../main/res/drawable/ic_compare_arrow.xml | 10 + .../app/src/main/res/drawable/ic_done.xml | 2 +- .../app/src/main/res/drawable/ic_edit.xml | 2 +- .../app/src/main/res/drawable/ic_exit.xml | 2 +- .../app/src/main/res/drawable/ic_folder.xml | 10 + .../app/src/main/res/drawable/ic_shortcut.xml | 2 +- .../main/res/drawable/ic_switch_screen.xml | 9 + .../src/main/res/drawable/switch_thumb.xml | 37 -- .../src/main/res/drawable/switch_track.xml | 19 - .../main/res/layout/dialog_bottom_sheet.xml | 39 ++ .../src/main/res/layout/dialog_game_about.xml | 129 +++++++ .../main/res/layout/dialog_games_folder.xml | 99 +++++ .../main/res/layout/dialog_select_theme.xml | 15 + .../res/layout/ds_editor_gravity_anchor.xml | 61 +++ .../main/res/layout/ds_editor_lock_aspect.xml | 33 ++ .../src/main/res/layout/ds_editor_spinner.xml | 30 ++ .../res/layout/ds_editor_spinner_label.xml | 12 + .../main/res/layout/fragment_game_drawer.xml | 35 +- .../main/res/layout/hold_theme_preview.xml | 34 ++ .../res/layout/hold_theme_preview_base.xml | 39 ++ .../app/src/main/res/layout/holder_game.xml | 1 + .../res/layout/material_switch_widget.xml | 11 + .../app/src/main/res/menu/game_drawer.xml | 4 + .../src/main/res/values-pt-rBR/strings.xml | 34 ++ .../app/src/main/res/values/strings.xml | 42 ++- .../app/src/main/res/values/themes.xml | 19 +- .../src/main/res/xml/advanced_preferences.xml | 28 +- .../main/res/xml/appearance_preference.xml | 14 - .../src/main/res/xml/empty_preferences.xml | 4 + .../src/main/res/xml/general_preference.xml | 36 ++ .../main/res/xml/input_map_preferences.xml | 6 + .../src/main/res/xml/start_preferences.xml | 9 +- 84 files changed, 2677 insertions(+), 374 deletions(-) create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BaseSheetDialog.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/GameAboutDialog.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/EmulatorCallback.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/GameLauncher.java delete mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/GamesFoldersPreferences.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/GeneralPreferences.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ThemeSelectorDialog.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/screen_editor/ScreenEditorPreference.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/screen_editor/ScreenLayoutsPreference.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/provider/AppDataDocumentProvider.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GamesFolder.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/CompatUtils.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Bounds.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsEditorView.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsLayout.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsLayoutManager.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Mode.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Model.java delete mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/preferences/SingleSelectionPreferences.java rename src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/{DefaultScreenLayout.java => RelativeScreenLayout.java} (50%) create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/SingleScreenLayout.java create mode 100644 src/pandroid/app/src/main/res/drawable/ds_editor_popup_background.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_align_center.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_arrow_down.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_arrow_up.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_compare_arrow.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_folder.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_switch_screen.xml delete mode 100644 src/pandroid/app/src/main/res/drawable/switch_thumb.xml delete mode 100644 src/pandroid/app/src/main/res/drawable/switch_track.xml create mode 100644 src/pandroid/app/src/main/res/layout/dialog_bottom_sheet.xml create mode 100644 src/pandroid/app/src/main/res/layout/dialog_game_about.xml create mode 100644 src/pandroid/app/src/main/res/layout/dialog_games_folder.xml create mode 100644 src/pandroid/app/src/main/res/layout/dialog_select_theme.xml create mode 100644 src/pandroid/app/src/main/res/layout/ds_editor_gravity_anchor.xml create mode 100644 src/pandroid/app/src/main/res/layout/ds_editor_lock_aspect.xml create mode 100644 src/pandroid/app/src/main/res/layout/ds_editor_spinner.xml create mode 100644 src/pandroid/app/src/main/res/layout/ds_editor_spinner_label.xml create mode 100644 src/pandroid/app/src/main/res/layout/hold_theme_preview.xml create mode 100644 src/pandroid/app/src/main/res/layout/hold_theme_preview_base.xml create mode 100644 src/pandroid/app/src/main/res/layout/material_switch_widget.xml delete mode 100644 src/pandroid/app/src/main/res/xml/appearance_preference.xml create mode 100644 src/pandroid/app/src/main/res/xml/empty_preferences.xml create mode 100644 src/pandroid/app/src/main/res/xml/general_preference.xml diff --git a/src/pandroid/app/src/main/AndroidManifest.xml b/src/pandroid/app/src/main/AndroidManifest.xml index 1deae1a4..d47ae2bb 100644 --- a/src/pandroid/app/src/main/AndroidManifest.xml +++ b/src/pandroid/app/src/main/AndroidManifest.xml @@ -35,7 +35,10 @@ + android:supportsPictureInPicture="true" + android:taskAffinity="emulator.GameActivity" + android:launchMode="singleTop" + android:configChanges="screenSize|screenLayout|smallestScreenSize|orientation|density|uiMode"> + + + + + + + + + + + + + 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 56c82d96..67a606f6 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 @@ -1,9 +1,11 @@ package com.panda3ds.pandroid.app; import android.os.Bundle; + import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import com.panda3ds.pandroid.R; + +import com.google.android.material.color.DynamicColors; import com.panda3ds.pandroid.data.config.GlobalConfig; @@ -28,5 +30,8 @@ public class BaseActivity extends AppCompatActivity { private void applyTheme() { currentTheme = PandroidApplication.getThemeId(); setTheme(currentTheme); + if (GlobalConfig.get(GlobalConfig.KEY_APP_THEME) == GlobalConfig.THEME_ANDROID){ + DynamicColors.applyToActivityIfAvailable(this); + } } } 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 946ef883..700b00f0 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 @@ -1,8 +1,11 @@ package com.panda3ds.pandroid.app; +import android.app.ActivityManager; +import android.app.PictureInPictureParams; import android.content.Intent; import android.os.Build; import android.os.Bundle; +import android.util.Rational; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -11,22 +14,29 @@ import android.view.WindowManager; import android.widget.CheckBox; import android.widget.FrameLayout; import android.widget.Toast; + import androidx.annotation.Nullable; + import com.panda3ds.pandroid.AlberDriver; import com.panda3ds.pandroid.R; import com.panda3ds.pandroid.app.game.AlberInputListener; import com.panda3ds.pandroid.app.game.DrawerFragment; +import com.panda3ds.pandroid.app.game.EmulatorCallback; import com.panda3ds.pandroid.data.config.GlobalConfig; import com.panda3ds.pandroid.input.InputHandler; import com.panda3ds.pandroid.input.InputMap; import com.panda3ds.pandroid.utils.Constants; import com.panda3ds.pandroid.view.PandaGlSurfaceView; import com.panda3ds.pandroid.view.PandaLayoutController; +import com.panda3ds.pandroid.view.ds.DsLayoutManager; +import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; import com.panda3ds.pandroid.view.utils.PerformanceView; -public class GameActivity extends BaseActivity { +public class GameActivity extends BaseActivity implements EmulatorCallback { private final DrawerFragment drawerFragment = new DrawerFragment(); - private final AlberInputListener inputListener = new AlberInputListener(this::onBackPressed); + private final AlberInputListener inputListener = new AlberInputListener(this); + private ConsoleRenderer renderer; + private int currentDsLayout; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -43,16 +53,16 @@ public class GameActivity extends BaseActivity { PandaGlSurfaceView pandaSurface = new PandaGlSurfaceView(this, intent.getStringExtra(Constants.ACTIVITY_PARAMETER_PATH)); setContentView(R.layout.game_activity); + renderer = pandaSurface.getRenderer(); + ((FrameLayout) findViewById(R.id.panda_gl_frame)) - .addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + .addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); PandaLayoutController controllerLayout = findViewById(R.id.controller_layout); controllerLayout.initialize(); ((CheckBox) findViewById(R.id.hide_screen_controller)).setOnCheckedChangeListener((buttonView, checked) -> { - findViewById(R.id.overlay_controller).setVisibility(checked ? View.VISIBLE : View.GONE); - findViewById(R.id.overlay_controller).invalidate(); - findViewById(R.id.overlay_controller).requestLayout(); + changeOverlayVisibility(checked); GlobalConfig.set(GlobalConfig.KEY_SCREEN_GAMEPAD_VISIBLE, checked); }); ((CheckBox) findViewById(R.id.hide_screen_controller)).setChecked(GlobalConfig.get(GlobalConfig.KEY_SCREEN_GAMEPAD_VISIBLE)); @@ -63,6 +73,13 @@ public class GameActivity extends BaseActivity { PerformanceView view = new PerformanceView(this); ((FrameLayout) findViewById(R.id.panda_gl_frame)).addView(view, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } + swapScreens(GlobalConfig.get(GlobalConfig.KEY_CURRENT_DS_LAYOUT)); + } + + private void changeOverlayVisibility(boolean visible){ + findViewById(R.id.overlay_controller).setVisibility(visible ? View.VISIBLE : View.GONE); + findViewById(R.id.overlay_controller).invalidate(); + findViewById(R.id.overlay_controller).requestLayout(); } @Override @@ -78,12 +95,30 @@ public class GameActivity extends BaseActivity { } } + private void goToPictureInPicture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(true); + builder.setSeamlessResizeEnabled(true); + } + builder.setAspectRatio(new Rational(10, 14)); + enterPictureInPictureMode(builder.build()); + } + } + @Override protected void onPause() { super.onPause(); InputHandler.reset(); - drawerFragment.open(); + if (GlobalConfig.get(GlobalConfig.KEY_PICTURE_IN_PICTURE)) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { + goToPictureInPicture(); + } + } else { + drawerFragment.open(); + } } @Override @@ -104,6 +139,13 @@ public class GameActivity extends BaseActivity { } } + @Override + public void swapScreens(int index) { + currentDsLayout = index; + GlobalConfig.set(GlobalConfig.KEY_CURRENT_DS_LAYOUT,index); + renderer.setLayout(DsLayoutManager.createLayout(currentDsLayout)); + } + @Override public boolean dispatchGenericMotionEvent(MotionEvent ev) { if ((!drawerFragment.isOpened()) && InputHandler.processMotionEvent(ev)) { @@ -113,12 +155,24 @@ public class GameActivity extends BaseActivity { return super.dispatchGenericMotionEvent(ev); } + @Override + public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode); + changeOverlayVisibility(!isInPictureInPictureMode && GlobalConfig.get(GlobalConfig.KEY_SCREEN_GAMEPAD_VISIBLE)); + findViewById(R.id.hide_screen_controller).setVisibility(isInPictureInPictureMode ? View.INVISIBLE : View.VISIBLE); + if (isInPictureInPictureMode){ + getWindow().getDecorView().postDelayed(drawerFragment::close, 250); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S){ + ActivityManager manager = ((ActivityManager) getSystemService(ACTIVITY_SERVICE)); + manager.getAppTasks().forEach(ActivityManager.AppTask::moveToFront); + } + } + @Override protected void onDestroy() { if (AlberDriver.HasRomLoaded()) { AlberDriver.Finalize(); } - super.onDestroy(); } } 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 5e03e516..8f677c3e 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 @@ -2,6 +2,7 @@ package com.panda3ds.pandroid.app; import android.os.Bundle; import android.view.MenuItem; + import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -16,15 +17,25 @@ public class MainActivity extends BaseActivity implements NavigationBarView.OnIt private final GamesFragment gamesFragment = new GamesFragment(); private final SearchFragment searchFragment = new SearchFragment(); private final SettingsFragment settingsFragment = new SettingsFragment(); + private NavigationBarView navigationBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - NavigationBarView bar = findViewById(R.id.navigation); - bar.setOnItemSelectedListener(this); - bar.postDelayed(() -> bar.setSelectedItemId(bar.getSelectedItemId()), 5); + navigationBar = findViewById(R.id.navigation); + navigationBar.setOnItemSelectedListener(this); + navigationBar.postDelayed(() -> navigationBar.setSelectedItemId(navigationBar.getSelectedItemId()), 5); + } + + @Override + public void onBackPressed() { + if (navigationBar.getSelectedItemId() != R.id.games){ + navigationBar.setSelectedItemId(R.id.games); + return; + } + super.onBackPressed(); } @Override diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java index 4f5c5761..3f6c72f1 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java @@ -7,7 +7,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.SwitchPreference; +import androidx.preference.SwitchPreferenceCompat; import com.panda3ds.pandroid.lang.Function; @@ -22,10 +22,14 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { }); } + protected void setSwitchValue(String id, boolean value){ + ((SwitchPreferenceCompat)findPreference(id)).setChecked(value); + } + protected void setActivityTitle(@StringRes int titleId) { ActionBar header = ((AppCompatActivity) requireActivity()).getSupportActionBar(); if (header != null) { header.setTitle(titleId); } } -} +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BaseSheetDialog.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BaseSheetDialog.java new file mode 100644 index 00000000..d905b844 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BaseSheetDialog.java @@ -0,0 +1,46 @@ +package com.panda3ds.pandroid.app.base; + +import android.content.Context; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; + +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.utils.CompatUtils; + +import org.jetbrains.annotations.NotNull; + +public class BaseSheetDialog extends BottomSheetDialog { + private final LinearLayout contentView; + public BaseSheetDialog(@NonNull Context context) { + super(CompatUtils.findActivity(context)); + int width = CompatUtils.findActivity(context).getWindow().getDecorView().getMeasuredWidth(); + int height = CompatUtils.findActivity(context).getWindow().getDecorView().getMeasuredHeight(); + getBehavior().setPeekHeight((int) (height*0.87)); + getBehavior().setMaxWidth(width); + getBehavior().setMaxHeight((int) (height*0.87)); + super.setContentView(R.layout.dialog_bottom_sheet); + contentView = super.findViewById(R.id.content); + } + + @Override + public void setContentView(View view) { + contentView.removeAllViews(); + contentView.addView(view); + } + + @Override + public void setContentView(int layoutResId) { + setContentView(LayoutInflater.from(getContext()).inflate(layoutResId, null, false)); + } + + @NotNull + @Override + public T findViewById(int id) { + return contentView.findViewById(id); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/GameAboutDialog.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/GameAboutDialog.java new file mode 100644 index 00000000..e08bc07d --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/GameAboutDialog.java @@ -0,0 +1,81 @@ +package com.panda3ds.pandroid.app.base; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.drawable.IconCompat; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PandroidApplication; +import com.panda3ds.pandroid.app.game.GameLauncher; +import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.utils.CompatUtils; +import com.panda3ds.pandroid.utils.FileUtils; +import com.panda3ds.pandroid.utils.GameUtils; +import com.panda3ds.pandroid.view.gamesgrid.GameIconView; + +public class GameAboutDialog extends BaseSheetDialog { + private final GameMetadata game; + public GameAboutDialog(@NonNull Context context, GameMetadata game) { + super(context); + this.game = game; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.dialog_game_about); + + ((GameIconView) findViewById(R.id.game_icon)).setImageBitmap(game.getIcon()); + ((TextView) findViewById(R.id.game_title)).setText(game.getTitle()); + ((TextView) findViewById(R.id.game_publisher)).setText(game.getPublisher()); + ((TextView) findViewById(R.id.region)).setText(game.getRegions()[0].localizedName()); + ((TextView) findViewById(R.id.directory)).setText(FileUtils.obtainUri(game.getRealPath()).getPath()); + findViewById(R.id.play).setOnClickListener(v -> { + dismiss(); + GameUtils.launch(getContext(), game); + }); + findViewById(R.id.shortcut).setOnClickListener(v -> { + dismiss(); + makeShortcut(); + }); + + if (game.getRomPath().startsWith("folder:")) { + findViewById(R.id.remove).setVisibility(View.GONE); + } else { + findViewById(R.id.remove).setOnClickListener(v -> { + dismiss(); + if (game.getRomPath().startsWith("elf:")) { + FileUtils.delete(game.getRealPath()); + } + GameUtils.removeGame(game); + }); + } + } + + private void makeShortcut() { + Context context = CompatUtils.findActivity(getContext()); + ShortcutInfoCompat.Builder shortcut = new ShortcutInfoCompat.Builder(context, game.getId()); + if (game.getIcon() != null){ + shortcut.setIcon(IconCompat.createWithAdaptiveBitmap(game.getIcon())); + } else { + shortcut.setIcon(IconCompat.createWithResource(getContext(), R.mipmap.ic_launcher)); + } + shortcut.setActivity(new ComponentName(context, GameLauncher.class)); + shortcut.setLongLabel(game.getTitle()); + shortcut.setShortLabel(game.getTitle()); + Intent intent = new Intent(PandroidApplication.getAppContext(), GameLauncher.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(new Uri.Builder().scheme("pandroid-game").authority(game.getId()).build()); + shortcut.setIntent(intent); + ShortcutManagerCompat.requestPinShortcut(context,shortcut.build(),null); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java index ec15a9bb..4777989d 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java @@ -11,8 +11,8 @@ import java.util.Objects; public class AlberInputListener implements Function { - private final Runnable backListener; - public AlberInputListener(Runnable backListener) { this.backListener = backListener; } + private final EmulatorCallback emulator; + public AlberInputListener(EmulatorCallback emulator) { this.emulator = emulator; } private final Vector2 axis = new Vector2(0.0f, 0.0f); @@ -21,7 +21,7 @@ public class AlberInputListener implements Function { KeyName key = InputMap.relative(event.getName()); if (Objects.equals(event.getName(), "KEYCODE_BACK")) { - backListener.run(); + emulator.onBackPressed(); return; } @@ -48,6 +48,11 @@ public class AlberInputListener implements Function { axis.x = event.getValue(); axisChanged = true; break; + case CHANGE_DS_LAYOUT: + if (!event.isDown()){ + emulator.swapScreens(); + } + break; default: if (event.isDown()) { AlberDriver.KeyDown(key.getKeyId()); 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 a1fa9eec..79369093 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 @@ -1,6 +1,8 @@ package com.panda3ds.pandroid.app.game; import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.MenuItem; @@ -13,7 +15,6 @@ import androidx.appcompat.widget.AppCompatTextView; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import android.content.pm.ActivityInfo; -import android.content.res.Configuration; import com.google.android.material.navigation.NavigationView; import com.panda3ds.pandroid.AlberDriver; @@ -24,10 +25,14 @@ import com.panda3ds.pandroid.view.gamesgrid.GameIconView; public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListener, NavigationView.OnNavigationItemSelectedListener { private DrawerLayout drawerContainer; + private View drawerLayout; + private EmulatorCallback emulator; + private GameMetadata game; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + emulator = ((EmulatorCallback) requireActivity()); drawerContainer = requireActivity().findViewById(R.id.drawer_container); drawerContainer.removeDrawerListener(this); drawerContainer.addDrawerListener(this); @@ -41,14 +46,22 @@ public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListe public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); drawerContainer.setVisibility(View.GONE); - - GameMetadata game = GameUtils.getCurrentGame(); - - ((GameIconView)view.findViewById(R.id.game_icon)).setImageBitmap(game.getIcon()); - ((AppCompatTextView)view.findViewById(R.id.game_title)).setText(game.getTitle()); - ((AppCompatTextView)view.findViewById(R.id.game_publisher)).setText(game.getPublisher()); + drawerLayout = view.findViewById(R.id.drawer_layout); ((NavigationView)view.findViewById(R.id.menu)).setNavigationItemSelectedListener(this); + refresh(); + } + + private void refresh(){ + game = GameUtils.getCurrentGame(); + if (game.getIcon() != null && !game.getIcon().isRecycled()) { + ((GameIconView) drawerLayout.findViewById(R.id.game_icon)).setImageBitmap(game.getIcon()); + } else { + ((GameIconView) drawerLayout.findViewById(R.id.game_icon)).setImageDrawable(new ColorDrawable(Color.TRANSPARENT)); + } + ((AppCompatTextView)drawerLayout.findViewById(R.id.game_title)).setText(game.getTitle()); + ((AppCompatTextView)drawerLayout.findViewById(R.id.game_publisher)).setText(game.getPublisher()); + } @Override @@ -72,6 +85,7 @@ public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListe drawerContainer.setVisibility(View.VISIBLE); drawerContainer.open(); drawerContainer.postDelayed(this::refreshLayout, 20); + refresh(); } } @@ -103,8 +117,11 @@ public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListe int id = item.getItemId(); if (id == R.id.resume) { close(); + } else if (id == R.id.ds_switch) { + emulator.swapScreens(); + close(); } else if (id == R.id.exit) { - requireActivity().finish(); + requireActivity().finishAndRemoveTask(); } else if (id == R.id.lua_script) { new LuaDialogFragment().show(getParentFragmentManager(), null); } else if (id == R.id.change_orientation) { diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/EmulatorCallback.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/EmulatorCallback.java new file mode 100644 index 00000000..399b3145 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/EmulatorCallback.java @@ -0,0 +1,11 @@ +package com.panda3ds.pandroid.app.game; + +import com.panda3ds.pandroid.data.config.GlobalConfig; + +public interface EmulatorCallback { + void onBackPressed(); + void swapScreens(int index); + default void swapScreens() { + swapScreens(GlobalConfig.get(GlobalConfig.KEY_CURRENT_DS_LAYOUT)+1); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/GameLauncher.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/GameLauncher.java new file mode 100644 index 00000000..6ad820b0 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/GameLauncher.java @@ -0,0 +1,37 @@ +package com.panda3ds.pandroid.app.game; + +import android.net.Uri; +import android.os.Bundle; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.core.content.pm.ShortcutManagerCompat; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.utils.GameUtils; + +import java.util.Arrays; + +public class GameLauncher extends BaseActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(new TextView(this)); + Uri uri = getIntent().getData(); + if(uri != null && uri.getScheme().equals("pandroid-game")){ + String gameId = uri.getAuthority(); + GameMetadata game = GameUtils.findGameById(gameId); + if (game != null){ + GameUtils.launch(this, game); + } else { + Toast.makeText(this, R.string.invalid_game,Toast.LENGTH_LONG).show(); + ShortcutManagerCompat.removeDynamicShortcuts(this, Arrays.asList(gameId)); + ShortcutManagerCompat.removeLongLivedShortcuts(this, Arrays.asList(gameId)); + } + } + finish(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java index a5c673f5..337780b0 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java @@ -14,6 +14,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.base.GameAboutDialog; import com.panda3ds.pandroid.app.base.LoadingAlertDialog; import com.panda3ds.pandroid.data.game.GameMetadata; import com.panda3ds.pandroid.lang.Task; @@ -38,6 +39,13 @@ public class GamesFragment extends Fragment implements ActivityResultCallback{ + GameAboutDialog dialog = new GameAboutDialog(requireActivity(), game); + dialog.setOnDismissListener((x)-> { + gameListView.setGameList(GameUtils.getGames()); + }); + dialog.show(); + }); view.findViewById(R.id.add_rom).setOnClickListener((v) -> pickFileRequest.launch(new String[] {"*/*"})); } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java index e9db7f80..6c2ee024 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; import androidx.fragment.app.Fragment; import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.base.GameAboutDialog; import com.panda3ds.pandroid.data.game.GameMetadata; import com.panda3ds.pandroid.utils.GameUtils; import com.panda3ds.pandroid.utils.SearchAgent; @@ -33,6 +34,11 @@ public class SearchFragment extends Fragment { super.onViewCreated(view, savedInstanceState); gamesListView = view.findViewById(R.id.games); + gamesListView.setItemLongClick((game)->{ + GameAboutDialog dialog = new GameAboutDialog(requireActivity(), game); + dialog.setOnDismissListener((x)-> search(((AppCompatEditText) view.findViewById(R.id.search_bar)).getText().toString())); + dialog.show(); + }); ((AppCompatEditText) view.findViewById(R.id.search_bar)).addTextChangedListener((SimpleTextWatcher) this::search); } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java index 4ac73661..e49eb2b4 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java @@ -1,13 +1,15 @@ package com.panda3ds.pandroid.app.main; +import android.content.Context; import android.os.Bundle; import androidx.annotation.Nullable; import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PandroidApplication; import com.panda3ds.pandroid.app.PreferenceActivity; import com.panda3ds.pandroid.app.base.BasePreferenceFragment; -import com.panda3ds.pandroid.app.preferences.AppearancePreferences; +import com.panda3ds.pandroid.app.preferences.GeneralPreferences; import com.panda3ds.pandroid.app.preferences.AdvancedPreferences; import com.panda3ds.pandroid.app.preferences.InputPreferences; @@ -15,8 +17,18 @@ public class SettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.start_preferences, rootKey); + findPreference("application").setSummary(getVersionName()); setItemClick("input", (item) -> PreferenceActivity.launch(requireContext(), InputPreferences.class)); - setItemClick("appearance", (item)-> PreferenceActivity.launch(requireContext(), AppearancePreferences.class)); + setItemClick("general", (item)-> PreferenceActivity.launch(requireContext(), GeneralPreferences.class)); setItemClick("advanced", (item)-> PreferenceActivity.launch(requireContext(), AdvancedPreferences.class)); } + + private String getVersionName() { + try { + Context context = PandroidApplication.getAppContext(); + return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + } catch (Exception e){ + return "???"; + } + } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AdvancedPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AdvancedPreferences.java index fea8aef0..176bab14 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AdvancedPreferences.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AdvancedPreferences.java @@ -3,9 +3,10 @@ package com.panda3ds.pandroid.app.preferences; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.Build; import androidx.annotation.Nullable; -import androidx.preference.SwitchPreference; +import androidx.preference.SwitchPreferenceCompat; import com.panda3ds.pandroid.R; import com.panda3ds.pandroid.app.PandroidApplication; @@ -19,10 +20,10 @@ public class AdvancedPreferences extends BasePreferenceFragment { setPreferencesFromResource(R.xml.advanced_preferences, rootKey); setActivityTitle(R.string.advanced_options); - setItemClick("performanceMonitor", pref -> GlobalConfig.set(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY, ((SwitchPreference) pref).isChecked())); - setItemClick("shaderJit", pref -> GlobalConfig.set(GlobalConfig.KEY_SHADER_JIT, ((SwitchPreference) pref).isChecked())); + setItemClick("performanceMonitor", pref -> GlobalConfig.set(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY, ((SwitchPreferenceCompat) pref).isChecked())); + setItemClick("shaderJit", pref -> GlobalConfig.set(GlobalConfig.KEY_SHADER_JIT, ((SwitchPreferenceCompat) pref).isChecked())); setItemClick("loggerService", pref -> { - boolean checked = ((SwitchPreference) pref).isChecked(); + boolean checked = ((SwitchPreferenceCompat) pref).isChecked(); Context ctx = PandroidApplication.getAppContext(); if (checked) { ctx.startService(new Intent(ctx, LoggerService.class)); @@ -42,8 +43,8 @@ public class AdvancedPreferences extends BasePreferenceFragment { } private void refresh() { - ((SwitchPreference) findPreference("performanceMonitor")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)); - ((SwitchPreference) findPreference("loggerService")).setChecked(GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE)); - ((SwitchPreference) findPreference("shaderJit")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT)); + ((SwitchPreferenceCompat) findPreference("performanceMonitor")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)); + ((SwitchPreferenceCompat) findPreference("loggerService")).setChecked(GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE)); + ((SwitchPreferenceCompat) findPreference("shaderJit")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT)); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java deleted file mode 100644 index 04c89d9a..00000000 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.panda3ds.pandroid.app.preferences; - -import android.os.Bundle; - -import androidx.annotation.Nullable; - -import com.panda3ds.pandroid.R; -import com.panda3ds.pandroid.app.BaseActivity; -import com.panda3ds.pandroid.app.base.BasePreferenceFragment; -import com.panda3ds.pandroid.data.config.GlobalConfig; -import com.panda3ds.pandroid.view.preferences.SingleSelectionPreferences; - -public class AppearancePreferences extends BasePreferenceFragment { - @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { - setPreferencesFromResource(R.xml.appearance_preference, rootKey); - - setActivityTitle(R.string.appearance); - - SingleSelectionPreferences themePreference = findPreference("theme"); - themePreference.setSelectedItem(GlobalConfig.get(GlobalConfig.KEY_APP_THEME)); - themePreference.setOnPreferenceChangeListener((preference, value) -> { - GlobalConfig.set(GlobalConfig.KEY_APP_THEME, (int) value); - return false; - }); - } -} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/GamesFoldersPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/GamesFoldersPreferences.java new file mode 100644 index 00000000..c7b32338 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/GamesFoldersPreferences.java @@ -0,0 +1,100 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.annotation.SuppressLint; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.app.base.BaseSheetDialog; +import com.panda3ds.pandroid.data.game.GamesFolder; +import com.panda3ds.pandroid.utils.FileUtils; +import com.panda3ds.pandroid.utils.GameUtils; + +public class GamesFoldersPreferences extends BasePreferenceFragment implements ActivityResultCallback { + private final ActivityResultContracts.OpenDocumentTree openFolderContract = new ActivityResultContracts.OpenDocumentTree(); + private ActivityResultLauncher pickFolderRequest; + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.empty_preferences, rootKey); + setActivityTitle(R.string.pref_game_folders); + refreshList(); + pickFolderRequest = registerForActivityResult(openFolderContract, this); + } + + @SuppressLint("RestrictedApi") + private void refreshList() { + GamesFolder[] folders = GameUtils.getFolders(); + PreferenceScreen screen = getPreferenceScreen(); + screen.removeAll(); + for (GamesFolder folder : folders) { + Preference preference = new Preference(screen.getContext()); + preference.setOnPreferenceClickListener((item) -> { + showFolderInfo(folder); + screen.performClick(); + return false; + }); + preference.setTitle(FileUtils.getName(folder.getPath())); + preference.setSummary(String.format(getString(R.string.games_count_f), folder.getGames().size())); + preference.setIcon(R.drawable.ic_folder); + screen.addPreference(preference); + } + + Preference add = new Preference(screen.getContext()); + add.setTitle(R.string.import_folder); + add.setIcon(R.drawable.ic_add); + add.setOnPreferenceClickListener(preference -> { + pickFolderRequest.launch(null); + return false; + }); + screen.addPreference(add); + } + + private void showFolderInfo(GamesFolder folder) { + BaseSheetDialog dialog = new BaseSheetDialog(requireActivity()); + View layout = LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_games_folder, null, false); + dialog.setContentView(layout); + + ((TextView) layout.findViewById(R.id.name)).setText(FileUtils.getName(folder.getPath())); + ((TextView) layout.findViewById(R.id.directory)).setText(FileUtils.obtainUri(folder.getPath()).getPath()); + ((TextView) layout.findViewById(R.id.games)).setText(String.valueOf(folder.getGames().size())); + + layout.findViewById(R.id.ok).setOnClickListener(v -> dialog.dismiss()); + layout.findViewById(R.id.remove).setOnClickListener(v -> { + dialog.dismiss(); + GameUtils.removeFolder(folder); + refreshList(); + }); + + dialog.show(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (pickFolderRequest != null) { + pickFolderRequest.unregister(); + pickFolderRequest = null; + } + } + + @Override + public void onActivityResult(Uri result) { + if (result != null) { + FileUtils.makeUriPermanent(result.toString(), "r"); + GameUtils.registerFolder(result.toString()); + refreshList(); + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/GeneralPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/GeneralPreferences.java new file mode 100644 index 00000000..8a49964a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/GeneralPreferences.java @@ -0,0 +1,35 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.preference.SwitchPreferenceCompat; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PreferenceActivity; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.app.preferences.screen_editor.ScreenLayoutsPreference; +import com.panda3ds.pandroid.data.config.GlobalConfig; + +public class GeneralPreferences extends BasePreferenceFragment { + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.general_preference, rootKey); + setItemClick("appearance.theme", (pref) -> new ThemeSelectorDialog(requireActivity()).show()); + setItemClick("appearance.ds", (pref) -> PreferenceActivity.launch(requireActivity(), ScreenLayoutsPreference.class)); + setItemClick("games.folders", (pref) -> PreferenceActivity.launch(requireActivity(), GamesFoldersPreferences.class)); + setItemClick("behavior.pictureInPicture", (pref)-> GlobalConfig.set(GlobalConfig.KEY_PICTURE_IN_PICTURE, ((SwitchPreferenceCompat)pref).isChecked())); + setActivityTitle(R.string.general); + refresh(); + } + + @Override + public void onResume() { + super.onResume(); + refresh(); + } + + private void refresh(){ + setSwitchValue("behavior.pictureInPicture", GlobalConfig.get(GlobalConfig.KEY_PICTURE_IN_PICTURE)); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ThemeSelectorDialog.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ThemeSelectorDialog.java new file mode 100644 index 00000000..fa22d884 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ThemeSelectorDialog.java @@ -0,0 +1,80 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.content.Context; +import android.os.Bundle; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatRadioButton; +import androidx.recyclerview.widget.RecyclerView; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.base.BaseSheetDialog; +import com.panda3ds.pandroid.utils.CompatUtils; +import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout; +import com.panda3ds.pandroid.view.recycler.SimpleListAdapter; + +import java.util.ArrayList; +import java.util.Arrays; + +public class ThemeSelectorDialog extends BaseSheetDialog { + + private final SimpleListAdapter adapter = new SimpleListAdapter<>(R.layout.hold_theme_preview_base, this::bindItemView); + private final int currentTheme = GlobalConfig.get(GlobalConfig.KEY_APP_THEME); + private static final ArrayList themes = new ArrayList<>(Arrays.asList( + new Theme(R.style.Theme_Pandroid, R.string.theme_device, GlobalConfig.THEME_ANDROID), + new Theme(R.style.Theme_Pandroid_Light, R.string.light, GlobalConfig.THEME_LIGHT), + new Theme(R.style.Theme_Pandroid_Dark, R.string.dark, GlobalConfig.THEME_DARK), + new Theme(R.style.Theme_Pandroid_Black, R.string.black, GlobalConfig.THEME_BLACK) + )); + + + public ThemeSelectorDialog(@NonNull Context context) { + super(context); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.dialog_select_theme); + adapter.clear(); + themes.sort((o1, o2) -> o1.value == currentTheme ? -1 : 0); + adapter.addAll(themes); + + RecyclerView recycler = findViewById(R.id.recycler); + recycler.setAdapter(adapter); + recycler.setLayoutManager(new AutoFitGridLayout(getContext(), 150)); + } + + private void bindItemView(int i, Theme theme, View view) { + ViewGroup container = view.findViewById(R.id.preview); + container.removeAllViews(); + container.addView(LayoutInflater.from(new ContextThemeWrapper(getContext(), theme.style)).inflate(R.layout.hold_theme_preview, null, false)); + ((TextView) view.findViewById(R.id.title)).setText(theme.name); + ((AppCompatRadioButton) view.findViewById(R.id.checkbox)).setChecked(GlobalConfig.get(GlobalConfig.KEY_APP_THEME) == theme.value); + view.setOnClickListener(v -> { + dismiss(); + if (theme.value != GlobalConfig.get(GlobalConfig.KEY_APP_THEME)) { + GlobalConfig.set(GlobalConfig.KEY_APP_THEME, theme.value); + CompatUtils.findActivity(getContext()).recreate(); + } + }); + } + + private static final class Theme { + private final int style; + private final int name; + private final int value; + + private Theme(int style, int name, int value) { + this.style = style; + this.name = name; + this.value = value; + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/screen_editor/ScreenEditorPreference.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/screen_editor/ScreenEditorPreference.java new file mode 100644 index 00000000..4bc6e299 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/screen_editor/ScreenEditorPreference.java @@ -0,0 +1,45 @@ +package com.panda3ds.pandroid.app.preferences.screen_editor; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.view.ds.DsEditorView; +import com.panda3ds.pandroid.view.ds.DsLayoutManager; + +public class ScreenEditorPreference extends Fragment { + private LinearLayout layout; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + layout = new LinearLayout(container.getContext()); + layout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION|View.SYSTEM_UI_FLAG_FULLSCREEN|View.SYSTEM_UI_FLAG_IMMERSIVE); + return layout; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + int index = getArguments().getInt("index"); + layout.removeAllViews(); + layout.addView(new DsEditorView(view.getContext(), index), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + ((BaseActivity)requireActivity()).getSupportActionBar().hide(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + DsLayoutManager.save(); + Toast.makeText(requireActivity(), R.string.saved, Toast.LENGTH_LONG).show(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/screen_editor/ScreenLayoutsPreference.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/screen_editor/ScreenLayoutsPreference.java new file mode 100644 index 00000000..7074b47b --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/screen_editor/ScreenLayoutsPreference.java @@ -0,0 +1,42 @@ +package com.panda3ds.pandroid.app.preferences.screen_editor; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PreferenceActivity; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.view.ds.DsLayoutManager; + +public class ScreenLayoutsPreference extends BasePreferenceFragment { + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.empty_preferences, rootKey); + setActivityTitle(R.string.dual_screen_layouts); + refresh(); + } + + public void refresh(){ + PreferenceScreen screen = getPreferenceScreen(); + screen.removeAll(); + for (int i = 0; i < DsLayoutManager.getLayoutCount(); i++){ + Preference pref = new Preference(getPreferenceScreen().getContext()); + pref.setIconSpaceReserved(false); + pref.setTitle("Layout "+(i+1)); + pref.setSummary(R.string.click_to_change); + pref.setIcon(R.drawable.ic_edit); + pref.setKey(String.valueOf(i)); + + final int index = i; + pref.setOnPreferenceClickListener(preference -> { + PreferenceActivity.launch(requireContext(), ScreenEditorPreference.class, new Intent().putExtra("index", index)); + return false; + }); + screen.addPreference(pref); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/provider/AppDataDocumentProvider.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/provider/AppDataDocumentProvider.java new file mode 100644 index 00000000..4c11485b --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/provider/AppDataDocumentProvider.java @@ -0,0 +1,162 @@ +package com.panda3ds.pandroid.app.provider; + +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; +import android.provider.DocumentsProvider; + +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PandroidApplication; +import com.panda3ds.pandroid.utils.FileUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Objects; + +public class AppDataDocumentProvider extends DocumentsProvider { + private static final String ROOT_ID = "root"; + private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{ + Root.COLUMN_ROOT_ID, + Root.COLUMN_MIME_TYPES, + Root.COLUMN_FLAGS, + Root.COLUMN_ICON, + Root.COLUMN_TITLE, + Root.COLUMN_SUMMARY, + Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_AVAILABLE_BYTES + }; + + private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{ + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_SIZE + }; + + private String obtainDocumentId(File file) { + String basePath = baseDirectory().getAbsolutePath(); + String fullPath = file.getAbsolutePath(); + return (ROOT_ID + "/" + fullPath.substring(basePath.length())).replaceAll("//", "/"); + } + + private File obtainFile(String documentId) { + if (documentId.startsWith(ROOT_ID)) { + return new File(baseDirectory(), documentId.substring(ROOT_ID.length())); + } + throw new IllegalArgumentException("Invalid document id: " + documentId); + } + + private Context context() { + return PandroidApplication.getAppContext(); + } + + private File baseDirectory() { + return context().getFilesDir(); + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor queryRoots(String[] projection) throws FileNotFoundException { + MatrixCursor cursor = new MatrixCursor(projection == null ? DEFAULT_ROOT_PROJECTION : projection); + cursor.newRow() + .add(Root.COLUMN_ROOT_ID, ROOT_ID) + .add(Root.COLUMN_SUMMARY, null) + .add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE) + .add(Root.COLUMN_DOCUMENT_ID, ROOT_ID + "/") + .add(Root.COLUMN_AVAILABLE_BYTES, baseDirectory().getFreeSpace()) + .add(Root.COLUMN_TITLE, context().getString(R.string.app_name)) + .add(Root.COLUMN_MIME_TYPES, "*/*") + .add(Root.COLUMN_ICON, R.mipmap.ic_launcher); + return cursor; + } + + @Override + public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { + File file = obtainFile(documentId); + MatrixCursor cursor = new MatrixCursor(projection == null ? DEFAULT_DOCUMENT_PROJECTION : projection); + includeFile(cursor, file); + return cursor; + } + + private void includeFile(MatrixCursor cursor, File file) { + int flags = 0; + if (file.isDirectory()) { + flags = Document.FLAG_DIR_SUPPORTS_CREATE; + } else { + flags = Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_REMOVE | Document.FLAG_SUPPORTS_DELETE; + } + cursor.newRow() + .add(Document.COLUMN_DOCUMENT_ID, obtainDocumentId(file)) + .add(Document.COLUMN_MIME_TYPE, file.isDirectory() ? Document.MIME_TYPE_DIR : "application/octect-stream") + .add(Document.COLUMN_FLAGS, flags) + .add(Document.COLUMN_LAST_MODIFIED, file.lastModified()) + .add(Document.COLUMN_DISPLAY_NAME, file.getName()) + .add(Document.COLUMN_SIZE, file.length()); + + } + + @Override + public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { + File file = obtainFile(parentDocumentId); + MatrixCursor cursor = new MatrixCursor(projection == null ? DEFAULT_DOCUMENT_PROJECTION : projection); + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + includeFile(cursor, child); + } + } + + return cursor; + } + + @Override + public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { + File parent = obtainFile(parentDocumentId); + File file = new File(parent, displayName); + if (!parent.exists()){ + throw new FileNotFoundException("Parent don't exists"); + } + + if (Objects.equals(mimeType, Document.MIME_TYPE_DIR)){ + if (!file.mkdirs()){ + throw new FileNotFoundException("Error on create directory"); + } + } else { + try { + if (!file.createNewFile()){ + throw new Exception("Error on create file"); + } + } catch (Exception e){ + throw new FileNotFoundException(e.getMessage()); + } + } + return obtainDocumentId(file); + } + + @Override + public void deleteDocument(String documentId) throws FileNotFoundException { + File file = obtainFile(documentId); + if (file.exists()){ + FileUtils.delete(file.getAbsolutePath()); + } else { + throw new FileNotFoundException("File not exists"); + } + } + + @Override + public ParcelFileDescriptor openDocument(String documentId, String mode, @Nullable CancellationSignal signal) throws FileNotFoundException { + return ParcelFileDescriptor.open(obtainFile(documentId), ParcelFileDescriptor.parseMode(mode)); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java index 0fde3d2f..eef15d3a 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java @@ -1,5 +1,7 @@ package com.panda3ds.pandroid.data; +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.panda3ds.pandroid.lang.Task; @@ -30,7 +32,10 @@ public class GsonConfigParser { String[] content = new String[] {"{}"}; new Task(()->{ if (FileUtils.exists(getPath())) { - content[0] = FileUtils.readTextFile(getPath()); + String src = FileUtils.readTextFile(getPath()); + if(src != null && src.length() > 2){ + content[0] = src; + } } }).runSync(); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java index e1230f24..b886ad3f 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java @@ -93,7 +93,7 @@ public class SMDH { smdh.position(META_OFFSET + (512 * i) + 0x80); data = new byte[0x100]; smdh.get(data); - title[i] = convertString(data).replaceAll("\n", " "); + title[i] = convertString(data); smdh.position(META_OFFSET + (512 * i) + 0x180); data = new byte[0x80]; diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java index bff1f9e0..3540bbd5 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java @@ -1,5 +1,7 @@ package com.panda3ds.pandroid.data.config; +import com.google.gson.Gson; +import com.google.gson.JsonObject; import com.google.gson.internal.LinkedTreeMap; import com.panda3ds.pandroid.data.GsonConfigParser; import com.panda3ds.pandroid.utils.Constants; @@ -10,6 +12,7 @@ import java.util.Map; public class GlobalConfig { private static final GsonConfigParser parser = new GsonConfigParser(Constants.PREF_GLOBAL_CONFIG); + private static final Gson gson = new Gson(); public static final int THEME_ANDROID = 0; public static final int THEME_LIGHT = 1; @@ -19,10 +22,13 @@ public class GlobalConfig { public static DataModel data; public static final Key KEY_SHADER_JIT = new Key<>("emu.shader_jit", false); + public static final Key KEY_PICTURE_IN_PICTURE = new Key<>("app.behavior.pictureInPicture", false); public static final Key KEY_SHOW_PERFORMANCE_OVERLAY = new Key<>("dev.performanceOverlay", false); public static final Key KEY_LOGGER_SERVICE = new Key<>("dev.loggerService", false); public static final Key KEY_APP_THEME = new Key<>("app.theme", THEME_ANDROID); public static final Key KEY_SCREEN_GAMEPAD_VISIBLE = new Key<>("app.screen_gamepad.visible", true); + public static final Key KEY_CURRENT_DS_LAYOUT = new Key<>("app.ds.current_layout",0); + public static final Key KEY_DS_LAYOUTS = new Key<>("app.ds.layouts", ""); public static void initialize() { data = parser.load(DataModel.class); @@ -54,6 +60,21 @@ public class GlobalConfig { writeChanges(); } + public static T getExtra(Key key, Class dataClass){ + if (data.extras.has(key.name)){ + return gson.fromJson(data.extras.getAsJsonObject(key.name), dataClass); + } + return gson.fromJson("{}", dataClass); + } + + public static synchronized void putExtra(Key key, Object value){ + if (data.extras.has(key.name)){ + data.extras.remove(key.name); + } + data.extras.add(key.name, gson.toJsonTree(value)); + writeChanges(); + } + private static void writeChanges() { parser.save(data); } @@ -70,6 +91,7 @@ public class GlobalConfig { private static class DataModel { private final Map configs = new LinkedTreeMap<>(); + private final JsonObject extras = new JsonObject(); public Object get(String key) { return configs.get(key); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java index 5acf2593..2c033171 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java @@ -15,9 +15,9 @@ import java.util.UUID; public class GameMetadata { private final String id; private final String romPath; - private final String title; - private final String publisher; - private final GameRegion[] regions; + private String title; + private String publisher; + private GameRegion[] regions; private transient Bitmap icon; private GameMetadata(String id, String romPath, String title, String publisher, Bitmap icon, GameRegion[] regions) { @@ -60,7 +60,7 @@ public class GameMetadata { } public Bitmap getIcon() { - if (icon == null) { + if (icon == null || icon.isRecycled()) { icon = GameUtils.loadGameIcon(id); } return icon; @@ -78,10 +78,15 @@ public class GameMetadata { return false; } - public static GameMetadata applySMDH(GameMetadata meta, SMDH smdh) { + public void applySMDH(SMDH smdh) { Bitmap icon = smdh.getBitmapIcon(); - GameMetadata newMeta = new GameMetadata(meta.getId(), meta.getRomPath(), smdh.getTitle(), smdh.getPublisher(), icon, new GameRegion[]{smdh.getRegion()}); - icon.recycle(); - return newMeta; + this.title = smdh.getTitle(); + this.publisher = smdh.getPublisher(); + this.icon = icon; + if (icon != null){ + GameUtils.setGameIcon(id, icon); + } + this.regions = new GameRegion[]{smdh.getRegion()}; + GameUtils.writeChanges(); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java index 9b99b095..9e025163 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java @@ -1,5 +1,7 @@ package com.panda3ds.pandroid.data.game; +import com.panda3ds.pandroid.R; + public enum GameRegion { NorthAmerican, Japan, @@ -8,5 +10,23 @@ public enum GameRegion { China, Korean, Taiwan, - None + None; + + public int localizedName(){ + switch (this){ + case NorthAmerican: + return R.string.region_north_armerican; + case Japan: + return R.string.region_japan; + case Europe: + return R.string.region_europe; + case Australia: + return R.string.region_australia; + case Korean: + return R.string.region_korean; + case Taiwan: + return R.string.region_taiwan; + } + return R.string.unknown; + } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GamesFolder.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GamesFolder.java new file mode 100644 index 00000000..78d454a1 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GamesFolder.java @@ -0,0 +1,61 @@ +package com.panda3ds.pandroid.data.game; + +import android.net.Uri; +import android.util.Log; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PandroidApplication; +import com.panda3ds.pandroid.utils.FileUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.UUID; + +public class GamesFolder { + + private final String id = UUID.randomUUID().toString(); + private final String path; + private final HashMap games = new HashMap<>(); + + public GamesFolder(String path) { + this.path = path; + } + + public boolean isValid(){ + return FileUtils.exists(path); + } + + public String getId() { + return id; + } + + public String getPath() { + return path; + } + + public Collection getGames() { + return games.values(); + } + + public void refresh() { + String[] gamesId = games.keySet().toArray(new String[0]); + for (String file: gamesId){ + if (!FileUtils.exists(path+"/"+file)){ + games.remove(file); + } + } + String unknown = PandroidApplication.getAppContext().getString(R.string.unknown); + + for (String file: FileUtils.listFiles(path)){ + String path = FileUtils.getChild(this.path, file); + if (FileUtils.isDirectory(path) || games.containsKey(file)){ + continue; + } + String ext = FileUtils.extension(path); + if (ext.equals("3ds") || ext.equals("3dsx")){ + String name = FileUtils.getName(path).trim().split("\\.")[0]; + games.put(file, new GameMetadata(new Uri.Builder().path(file).authority(id).scheme("folder").build().toString(),name, unknown)); + } + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java index 390708b9..28edd426 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java @@ -25,6 +25,7 @@ public class InputHandler { }; private static final HashMap motionDownEvents = new HashMap<>(); + private static final HashMap keyDownEvents = new HashMap<>(); private static boolean containsSource(int[] sources, int sourceMask) { for (int source : sources) { @@ -108,8 +109,17 @@ public class InputHandler { return true; } } - - handleEvent(new InputEvent(KeyEvent.keyCodeToString(event.getKeyCode()), event.getAction() == KeyEvent.ACTION_UP ? 0.0f : 1.0f)); + String code = KeyEvent.keyCodeToString(event.getKeyCode()); + if (event.getAction() == KeyEvent.ACTION_UP){ + keyDownEvents.remove(code); + handleEvent(new InputEvent(code, 0.0f)); + } else if (!keyDownEvents.containsKey(code)){ + keyDownEvents.put(code, new InputEvent(code, 1.0f)); + } + for (InputEvent env: keyDownEvents.values()){ + handleEvent(env); + } + return true; } @@ -117,5 +127,6 @@ public class InputHandler { eventListener = null; motionDeadZone = 0.0f; motionDownEvents.clear(); + keyDownEvents.clear(); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java index 1253529f..24c420d3 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java @@ -19,7 +19,8 @@ public enum KeyName { SELECT(Constants.INPUT_KEY_SELECT), L(Constants.INPUT_KEY_L), R(Constants.INPUT_KEY_R), - NULL; + NULL, + CHANGE_DS_LAYOUT; private final int keyId; diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/CompatUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/CompatUtils.java new file mode 100644 index 00000000..8b1682f7 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/CompatUtils.java @@ -0,0 +1,39 @@ +package com.panda3ds.pandroid.utils; + +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.util.TypedValue; + +import androidx.annotation.AttrRes; + +import com.panda3ds.pandroid.app.PandroidApplication; + +public class CompatUtils { + public static Activity findActivity(Context context) { + if (context instanceof Activity) { + return (Activity) context; + } else if ((context instanceof ContextWrapper)) { + return findActivity(((ContextWrapper) context).getBaseContext()); + } + return ((Activity) context); + } + + public static int resolveColor(Context context, @AttrRes int id){ + try { + TypedArray values = context.obtainStyledAttributes(new int[]{id}); + int color = values.getColor(0, Color.RED); + values.recycle(); + return color; + } catch (Exception e){ + return Color.rgb(255,0,255); + } + } + + public static float applyDimen(int unit, int size) { + return TypedValue.applyDimension(unit, size, PandroidApplication.getAppContext().getResources().getDisplayMetrics()); + } +} 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 2920c7c6..7455613a 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 @@ -2,8 +2,10 @@ package com.panda3ds.pandroid.utils; import android.content.Context; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; import android.system.Os; import android.util.Log; @@ -21,14 +23,21 @@ import java.util.Objects; public class FileUtils { public static final String MODE_READ = "r"; - public static final int CANONICAL_SEARCH_DEEP = 8; + private static final String TREE_URI = "tree"; private static DocumentFile parseFile(String path) { if (path.startsWith("/")) { return DocumentFile.fromFile(new File(path)); } Uri uri = Uri.parse(path); - return DocumentFile.fromSingleUri(getContext(), uri); + DocumentFile singleFile = DocumentFile.fromSingleUri(getContext(), uri); + if (singleFile.length() > 0 && singleFile.length() != 4096){ + return singleFile; + } + if (uri.getScheme().equals("content") && uri.getPath().startsWith("/"+TREE_URI)){ + return DocumentFile.fromTreeUri(getContext(), uri); + } + return singleFile; } private static Context getContext() { @@ -194,47 +203,6 @@ public class FileUtils { getContext().getContentResolver().takePersistableUriPermission(Uri.parse(uri), flags); } - /** - * When call ContentProvider.openFileDescriptor() android opens a file descriptor - * on app process in /proc/self/fd/[file descriptor id] this is a link to real file path - * can use File.getCanonicalPath() for get a link origin, but in some android version - * need use Os.readlink(path) to get a real path. - */ - public static String obtainRealPath(String uri) { - try { - ParcelFileDescriptor parcelDescriptor = getContext().getContentResolver().openFileDescriptor(Uri.parse(uri), "r"); - int fd = parcelDescriptor.getFd(); - File file = new File("/proc/self/fd/" + fd).getAbsoluteFile(); - - for (int i = 0; i < CANONICAL_SEARCH_DEEP; i++) { - try { - String canonical = file.getCanonicalPath(); - if (!Objects.equals(canonical, file.getAbsolutePath())) { - file = new File(canonical).getAbsoluteFile(); - } - } catch (Exception x) { - break; - } - } - - if (!file.getAbsolutePath().startsWith("/proc/self/")) { - parcelDescriptor.close(); - return file.getAbsolutePath(); - } - - String path = Os.readlink(file.getAbsolutePath()); - parcelDescriptor.close(); - - if (new File(path).exists()) { - return path; - } - - return null; - } catch (Exception e) { - return null; - } - } - public static void updateFile(String path) { DocumentFile file = parseFile(path); Uri uri = file.getUri(); @@ -310,4 +278,12 @@ public class FileUtils { } return true; } + + public static String getChild(String path, String name){ + return parseFile(path).findFile(name).getUri().toString(); + } + + public static boolean isDirectory(String path) { + return parseFile(path).isDirectory(); + } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java index f050af0a..b3a40423 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java @@ -10,16 +10,18 @@ import android.util.Log; import com.panda3ds.pandroid.app.GameActivity; import com.panda3ds.pandroid.data.GsonConfigParser; import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.data.game.GamesFolder; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Objects; public class GameUtils { private static final Bitmap DEFAULT_ICON = Bitmap.createBitmap(48, 48, Bitmap.Config.ARGB_8888); - public static GsonConfigParser parser = new GsonConfigParser(Constants.PREF_GAME_UTILS); + private final static GsonConfigParser parser = new GsonConfigParser(Constants.PREF_GAME_UTILS); private static DataModel data; @@ -27,10 +29,12 @@ public class GameUtils { public static void initialize() { data = parser.load(DataModel.class); + refreshFolders(); } public static GameMetadata findByRomPath(String romPath) { - for (GameMetadata game : data.games) { + ArrayList games = getGames(); + for (GameMetadata game : games) { if (Objects.equals(romPath, game.getRealPath())) { return game; } @@ -42,9 +46,7 @@ public class GameUtils { currentGame = game; String path = game.getRealPath(); if (path.contains("://")) { - String[] parts = Uri.decode(game.getRomPath()).split("/"); - String name = parts[parts.length - 1]; - path = "game://internal/" + name; + path = "game://internal/" + FileUtils.getName(game.getRealPath()); } context.startActivity(new Intent(context, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path)); @@ -72,19 +74,39 @@ public class GameUtils { Uri uri = Uri.parse(path); switch (uri.getScheme().toLowerCase()) { + case "folder": { + return FileUtils.getChild(data.folders.get(uri.getAuthority()).getPath(), uri.getPathSegments().get(0)); + } case "elf": { - return FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_ELF)+"/"+uri.getAuthority(); + return FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_ELF) + "/" + uri.getAuthority(); } } - return path; } - public static ArrayList getGames() { - return new ArrayList<>(data.games); + public static void refreshFolders() { + String[] keys = data.folders.keySet().toArray(new String[0]); + for (String key : keys) { + GamesFolder folder = data.folders.get(key); + if (!folder.isValid()){ + data.folders.remove(key); + } else { + folder.refresh(); + } + } + writeChanges(); } - private static void writeChanges() { + public static ArrayList getGames() { + ArrayList games = new ArrayList<>(); + games.addAll(data.games); + for (GamesFolder folder: data.folders.values()){ + games.addAll(folder.getGames()); + } + return games; + } + + public static void writeChanges() { parser.save(data); } @@ -117,7 +139,35 @@ public class GameUtils { return DEFAULT_ICON; } + public static GamesFolder[] getFolders() { + return data.folders.values().toArray(new GamesFolder[0]); + } + + public static void registerFolder(String path) { + if (!data.folders.containsKey(path)){ + GamesFolder folder = new GamesFolder(path); + data.folders.put(folder.getId(),folder); + folder.refresh(); + writeChanges(); + } + } + + public static void removeFolder(GamesFolder folder) { + data.folders.remove(folder.getId()); + writeChanges(); + } + + public static GameMetadata findGameById(String id) { + for (GameMetadata game: getGames()){ + if (game.getId().equals(id)){ + return game; + } + } + return null; + } + private static class DataModel { public final List games = new ArrayList<>(); + public final HashMap folders = new HashMap<>(); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java index 76dc5e7d..c57421ab 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java @@ -2,24 +2,27 @@ package com.panda3ds.pandroid.view; import static android.opengl.GLES32.*; +import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.opengl.GLSurfaceView; import android.os.Handler; import android.util.Log; -import android.widget.Toast; -import androidx.appcompat.app.AlertDialog; + import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; import com.panda3ds.pandroid.data.SMDH; import com.panda3ds.pandroid.data.config.GlobalConfig; import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.utils.CompatUtils; import com.panda3ds.pandroid.utils.Constants; import com.panda3ds.pandroid.utils.GameUtils; import com.panda3ds.pandroid.utils.PerformanceMonitor; +import com.panda3ds.pandroid.view.ds.DsLayoutManager; import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout; -import com.panda3ds.pandroid.view.renderer.layout.DefaultScreenLayout; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -38,7 +41,7 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; - setLayout(new DefaultScreenLayout()); + setLayout(DsLayoutManager.createLayout(0)); } @Override @@ -74,9 +77,9 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer glGenTextures(1, generateBuffer, 0); screenTexture = generateBuffer[0]; glBindTexture(GL_TEXTURE_2D, screenTexture); - glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, screenWidth, screenHeight); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, Constants.N3DS_WIDTH, Constants.N3DS_FULL_HEIGHT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_LINEAR); glBindTexture(GL_TEXTURE_2D, 0); glGenFramebuffers(1, generateBuffer, 0); @@ -95,19 +98,17 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer if (!AlberDriver.LoadRom(romPath)) { // Get a handler that can be used to post to the main thread Handler mainHandler = new Handler(context.getMainLooper()); - - Runnable runnable = new Runnable() { - @Override - public void run() { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle("Failed to load ROM") - .setMessage("Make sure it's a valid 3DS ROM and that storage permissions are configured properly.") - .setPositiveButton("OK", null) + mainHandler.post(()-> { + new BottomAlertDialog(context) + .setTitle(R.string.failed_load_rom) + .setMessage(R.string.dialog_message_invalid_rom) + .setPositiveButton(android.R.string.ok, (dialog, witch) -> { + dialog.dismiss(); + CompatUtils.findActivity(context).finishAndRemoveTask(); + }) .setCancelable(false) .show(); - } - }; - mainHandler.post(runnable); + }); GameMetadata game = GameUtils.getCurrentGame(); GameUtils.removeGame(game); @@ -122,15 +123,16 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer SMDH smdh = new SMDH(smdhData); Log.i(Constants.LOG_TAG, "Loaded rom SDMH"); Log.i(Constants.LOG_TAG, String.format("You are playing '%s' published by '%s'", smdh.getTitle(), smdh.getPublisher())); - GameMetadata game = GameUtils.getCurrentGame(); - GameUtils.removeGame(game); - GameUtils.addGame(GameMetadata.applySMDH(game, smdh)); + GameUtils.getCurrentGame().applySMDH(smdh); } PerformanceMonitor.initialize(getBackendName()); } public void onDrawFrame(GL10 unused) { + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + if (AlberDriver.HasRomLoaded()) { AlberDriver.RunFrame(screenFbo); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Bounds.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Bounds.java new file mode 100644 index 00000000..51f4d51f --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Bounds.java @@ -0,0 +1,46 @@ +package com.panda3ds.pandroid.view.ds; + +import android.graphics.Rect; + +class Bounds { + public int left = 0; + public int right = 0; + public int top = 0; + public int bottom = 0; + + public void normalize(){ + left = Math.abs(left); + right = Math.abs(right); + top = Math.abs(top); + bottom = Math.abs(bottom); + } + + public void applyWithAspect(Rect rect, int width, double aspectRatio){ + normalize(); + rect.set(left, top, width-right, (int) Math.round((width-right-left)*aspectRatio)+top); + } + + public void apply(Rect rect, int width, int height){ + normalize(); + rect.set(left, top, width-right, height-bottom); + } + + public void move(int x, int y){ + left += x; + right -= x; + + top += y; + bottom -= y; + normalize(); + } + + public void fixOverlay(int width, int height, int size) { + if (left > (width-right) - size){ + right = (width-left) - size; + } + if (top > (height - bottom) - size){ + bottom = (height - top) - size; + } + normalize(); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsEditorView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsEditorView.java new file mode 100644 index 00000000..7401a0a4 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsEditorView.java @@ -0,0 +1,353 @@ +package com.panda3ds.pandroid.view.ds; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.appcompat.widget.AppCompatSpinner; +import androidx.appcompat.widget.AppCompatTextView; + +import com.google.android.material.checkbox.MaterialCheckBox; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.utils.CompatUtils; +import com.panda3ds.pandroid.utils.Constants; + +@SuppressLint("ViewConstructor") +public class DsEditorView extends FrameLayout { + private final float SIZE_DP; + + private final Paint selectionPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final DsLayout layout; + private int width = 1, height = 1; + private final LinearLayout gravityAnchor; + private final LinearLayout aspectRatioFixLayout; + private final LinearLayout modeSelectorLayout; + private final AppCompatSpinner modeSelector; + private final PointView spacePoint; + private final PointView topDisplay; + private final PointView bottomDisplay; + private final PointView topDisplayResizer; + private final PointView bottomDisplayResizer; + + @SuppressLint("ClickableViewAccessibility") + public DsEditorView(Context context, int index) { + super(context); + layout = (DsLayout) DsLayoutManager.createLayout(index); + SIZE_DP = CompatUtils.applyDimen(TypedValue.COMPLEX_UNIT_DIP, 1); + int colorBottomSelection = CompatUtils.resolveColor(context, androidx.appcompat.R.attr.colorPrimary); + int colorTopSelection = CompatUtils.resolveColor(context, com.google.android.material.R.attr.colorAccent); + + selectionPaint.setColor(colorTopSelection); + selectionPaint.setStrokeWidth(SIZE_DP * 2); + selectionPaint.setPathEffect(new DashPathEffect(new float[]{SIZE_DP * 10, SIZE_DP * 10}, 0.0f)); + selectionPaint.setStyle(Paint.Style.STROKE); + + layout.setTopDisplaySourceSize(Constants.N3DS_WIDTH, Constants.N3DS_HALF_HEIGHT); + layout.setBottomDisplaySourceSize(Constants.N3DS_WIDTH - 40 - 40, Constants.N3DS_HALF_HEIGHT); + setBackgroundColor(Color.argb(2, 0, 0, 0)); + + LayoutInflater inflater = LayoutInflater.from(context); + + gravityAnchor = (LinearLayout) inflater.inflate(R.layout.ds_editor_gravity_anchor, this, false); + gravityAnchor.findViewById(R.id.up).setOnClickListener(v -> { + layout.getCurrentModel().gravity = Gravity.TOP; + refreshLayout(); + }); + gravityAnchor.findViewById(R.id.center).setOnClickListener(v -> { + layout.getCurrentModel().gravity = Gravity.CENTER; + refreshLayout(); + }); + gravityAnchor.findViewById(R.id.down).setOnClickListener(v -> { + layout.getCurrentModel().gravity = Gravity.BOTTOM; + refreshLayout(); + }); + gravityAnchor.findViewById(R.id.revert).setOnClickListener(v -> { + layout.getCurrentModel().reverse = !layout.getCurrentModel().reverse; + refreshLayout(); + }); + + { + modeSelectorLayout = (LinearLayout) inflater.inflate(R.layout.ds_editor_spinner, this, false); + ArrayAdapter spinnerAdapter = new ArrayAdapter<>(getContext(), R.layout.ds_editor_spinner_label); + spinnerAdapter.addAll("SINGLE", "RELATIVE", "ABSOLUTE"); + modeSelector = modeSelectorLayout.findViewById(R.id.spinner); + modeSelector.setAdapter(spinnerAdapter); + modeSelector.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + layout.getCurrentModel().mode = Mode.values()[position]; + refreshLayout(); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + } + + aspectRatioFixLayout = (LinearLayout) inflater.inflate(R.layout.ds_editor_lock_aspect, this, false); + ((MaterialCheckBox) aspectRatioFixLayout.findViewById(R.id.checkbox)).setOnCheckedChangeListener((buttonView, checked) -> { + layout.getCurrentModel().lockAspect = checked; + refreshPoints(); + }); + + spacePoint = new PointView(); + spacePoint.setColor(CompatUtils.resolveColor(context, com.google.android.material.R.attr.colorOnPrimary), colorTopSelection); + spacePoint.setOnTouchListener((view, motion) -> { + layout.getCurrentModel().space = (motion.getX() + spacePoint.x()) / (float) width; + refreshPoints(); + return true; + }); + + spacePoint.setLayoutGravity(Gravity.START | Gravity.CENTER); + + setOnClickListener(v -> { + if (layout.getCurrentModel().mode == Mode.SINGLE) { + layout.getCurrentModel().onlyTop = !layout.getCurrentModel().onlyTop; + refreshPoints(); + } + }); + + topDisplay = new PointView(); + topDisplay.setText(R.string.top_display); + topDisplay.setOnTouchListener(new DisplayTouchEvent(true)); + topDisplay.setTextColor(colorTopSelection); + topDisplay.setBackground(new SelectionDrawable(colorTopSelection)); + + bottomDisplay = new PointView(); + bottomDisplay.setText(R.string.bottom_display); + bottomDisplay.setOnTouchListener(new DisplayTouchEvent(false)); + bottomDisplay.setTextColor(colorBottomSelection); + bottomDisplay.setBackground(new SelectionDrawable(colorBottomSelection)); + + topDisplayResizer = new PointView(); + topDisplayResizer.setColor(0, colorTopSelection); + topDisplayResizer.setOnTouchListener(new DisplayResizeTouchEvent(true)); + + bottomDisplayResizer = new PointView(); + bottomDisplayResizer.setColor(0, colorBottomSelection); + bottomDisplayResizer.setOnTouchListener(new DisplayResizeTouchEvent(false)); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (this.width != getWidth() || this.height != getHeight()) { + this.width = getWidth(); + this.height = getHeight(); + refreshLayout(); + } + } + + private void refreshPoints() { + Model data = layout.getCurrentModel(); + data.preferredTop.fixOverlay(width, height, (int) (SIZE_DP * 5)); + data.preferredBottom.fixOverlay(width, height, (int) (SIZE_DP * 30)); + layout.update(width, height); + Rect bottomDisplay = layout.getBottomDisplayBounds(); + Rect topDisplay = layout.getTopDisplayBounds(); + + switch (data.mode) { + case RELATIVE: { + if (width > height) { + Rect primaryDisplay = data.reverse ? bottomDisplay : topDisplay; + data.space = primaryDisplay.width() / (float) width; + spacePoint.setCenterPosition(primaryDisplay.width(), (int) (SIZE_DP * 15)); + spacePoint.setText(String.valueOf((int) (data.space * 100))); + } + } + break; + case SINGLE: + case ABSOLUTE: { + } + break; + } + + this.topDisplay.setSize(topDisplay.width(), topDisplay.height()); + this.topDisplay.setPosition(topDisplay.left, topDisplay.top); + + this.bottomDisplay.setSize(bottomDisplay.width(), bottomDisplay.height()); + this.bottomDisplay.setPosition(bottomDisplay.left, bottomDisplay.top); + + if (data.lockAspect) { + topDisplayResizer.setCenterPosition(topDisplay.right, topDisplay.top + (topDisplay.height() / 2)); + bottomDisplayResizer.setCenterPosition(bottomDisplay.right, bottomDisplay.top + (bottomDisplay.height() / 2)); + } else { + topDisplayResizer.setCenterPosition(topDisplay.right, topDisplay.bottom); + bottomDisplayResizer.setCenterPosition(bottomDisplay.right, bottomDisplay.bottom); + } + + invalidate(); + } + + private void refreshLayout() { + removeAllViews(); + layout.update(width, height); + boolean landscape = width > height; + addView(topDisplay); + addView(bottomDisplay); + + gravityAnchor.setOrientation(LinearLayout.HORIZONTAL); + addView(modeSelectorLayout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.BOTTOM | Gravity.CENTER)); + switch (layout.getCurrentModel().mode) { + case RELATIVE: { + addView(gravityAnchor, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.CENTER | Gravity.TOP)); + if (landscape) { + addView(spacePoint); + } + } + break; + case ABSOLUTE: { + addView(aspectRatioFixLayout, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.CENTER | Gravity.TOP)); + addView(topDisplayResizer); + addView(bottomDisplayResizer); + } + break; + case SINGLE: { + addView(aspectRatioFixLayout, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.CENTER | Gravity.TOP)); + } + break; + } + ((MaterialCheckBox) aspectRatioFixLayout.findViewById(R.id.checkbox)).setChecked(layout.getCurrentModel().lockAspect); + + modeSelector.setSelection(layout.getCurrentModel().mode.ordinal()); + gravityAnchor.findViewById(R.id.revert).setRotation(landscape ? 0 : 90); + refreshPoints(); + } + + private class PointView extends AppCompatTextView { + + public PointView() { + super(DsEditorView.this.getContext()); + setLayoutParams(new FrameLayout.LayoutParams((int) (SIZE_DP * 30), (int) (SIZE_DP * 30))); + setBackgroundResource(R.drawable.medium_card_background); + setGravity(Gravity.CENTER); + this.setFocusable(true); + this.setClickable(true); + } + + public int x() { + return ((LayoutParams) getLayoutParams()).leftMargin; + } + + public int y() { + return ((LayoutParams) getLayoutParams()).topMargin; + } + + public int width() { + return ((LayoutParams) getLayoutParams()).width; + } + + public void setColor(int text, int background) { + setTextColor(text); + setBackgroundTintList(ColorStateList.valueOf(background)); + } + + public void setSize(int width, int height) { + LayoutParams params = (LayoutParams) getLayoutParams(); + params.width = Math.max(0, width); + params.height = Math.max(0, height); + setLayoutParams(params); + } + + public void setPosition(int x, int y) { + LayoutParams params = (LayoutParams) getLayoutParams(); + params.leftMargin = x; + params.topMargin = y; + setLayoutParams(params); + } + + public void setCenterPosition(int x, int y) { + int middle = this.width() / 2; + setPosition(Math.max(-middle, Math.min(x - middle, width - middle)), Math.max(-middle, Math.min(y - middle, height - middle))); + } + + public void setLayoutGravity(int gravity) { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + params.gravity = gravity; + setLayoutParams(params); + } + } + + private class DisplayTouchEvent implements OnTouchListener { + private final boolean topScreen; + private Vector2 downEvent = null; + + private DisplayTouchEvent(boolean topScreen) { + this.topScreen = topScreen; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + Bounds preferred = topScreen ? layout.getCurrentModel().preferredTop : layout.getCurrentModel().preferredBottom; + if (layout.getCurrentModel().mode == Mode.ABSOLUTE && event.getAction() != MotionEvent.ACTION_UP) { + if (downEvent == null) { + downEvent = new Vector2(event.getRawX(), event.getRawY()); + return true; + } + preferred.move((int) (event.getRawX() - downEvent.x), (int) (event.getRawY() - downEvent.y)); + downEvent.set(event.getRawX(), event.getRawY()); + refreshPoints(); + return true; + } else if (layout.getCurrentModel().mode == Mode.SINGLE && event.getAction() == MotionEvent.ACTION_UP) { + callOnClick(); + } + downEvent = null; + return false; + } + } + + private class DisplayResizeTouchEvent implements OnTouchListener { + private final boolean topScreen; + + private DisplayResizeTouchEvent(boolean topScreen) { + this.topScreen = topScreen; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + Bounds preferred = topScreen ? layout.getCurrentModel().preferredTop : layout.getCurrentModel().preferredBottom; + if (event.getAction() != MotionEvent.ACTION_UP) { + preferred.right = (int) (width - (((PointView) v).x() + event.getX())); + preferred.bottom = (int) (height - (((PointView) v).y() + event.getY())); + refreshPoints(); + return true; + } + return false; + } + } + + private class SelectionDrawable extends ColorDrawable { + private final Paint solidPaint = new Paint(); + + public SelectionDrawable(int color) { + super(color); + } + + @Override + public void draw(Canvas canvas) { + int color = this.getColor(); + selectionPaint.setColor(color); + solidPaint.setColor(Color.argb(65, Color.red(color), Color.green(color), Color.blue(color))); + canvas.drawRect(this.getBounds(), solidPaint); + canvas.drawRect(this.getBounds(), selectionPaint); + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsLayout.java new file mode 100644 index 00000000..8e892315 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsLayout.java @@ -0,0 +1,179 @@ +package com.panda3ds.pandroid.view.ds; + +import android.graphics.Rect; +import android.view.Gravity; + +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout; + +class DsLayout implements ConsoleLayout { + private final Rect topDisplay = new Rect(); + private final Rect bottomDisplay = new Rect(); + + private final Vector2 screenSize = new Vector2(0, 0); + private final Vector2 sourceTop = new Vector2(0, 0); + private final Vector2 sourceBottom = new Vector2(0, 0); + private final Model[] modes = new Model[2]; + + public DsLayout(Model landscape, Model portrait) { + modes[0] = landscape; + modes[1] = portrait; + } + + public DsLayout() { + this(new Model(), new Model()); + } + + @Override + public void update(int screenWidth, int screenHeight) { + screenSize.set(screenWidth, screenHeight); + update(); + } + + @Override + public void setBottomDisplaySourceSize(int width, int height) { + sourceBottom.set(width, height); + update(); + } + + @Override + public void setTopDisplaySourceSize(int width, int height) { + sourceTop.set(width, height); + update(); + } + + @Override + public Rect getBottomDisplayBounds() { + return bottomDisplay; + } + + @Override + public Rect getTopDisplayBounds() { + return topDisplay; + } + + public void update() { + Model data = getCurrentModel(); + Mode mode = data.mode; + switch (mode) { + case RELATIVE: + relative(data); + break; + case SINGLE: + single(data); + break; + case ABSOLUTE: + absolute(data); + break; + } + } + + private void absolute(Model data) { + if (data.lockAspect) { + data.preferredTop.applyWithAspect(topDisplay, (int) screenSize.x, (double) sourceTop.y / sourceTop.x); + data.preferredBottom.applyWithAspect(bottomDisplay, (int) screenSize.x, (double) sourceBottom.y / sourceBottom.x); + } else { + data.preferredTop.apply(topDisplay, (int) screenSize.x, (int) screenSize.y); + data.preferredBottom.apply(bottomDisplay, (int) screenSize.x, (int) screenSize.y); + } + } + + private void single(Model data) { + Vector2 source = data.onlyTop ? sourceTop : sourceBottom; + Rect dest = data.onlyTop ? topDisplay : bottomDisplay; + + if (data.lockAspect) { + int x = 0, y = 0; + int width = (int) ((screenSize.y / source.y) * source.x); + int height; + + if (width > screenSize.x) { + height = (int) ((screenSize.x / source.x) * source.y); + width = (int) screenSize.x; + y = (int) ((screenSize.y - height) / 2); + } else { + height = (int) screenSize.y; + x = (int) ((screenSize.x - width) / 2); + } + dest.set(x, y, x + width, y + height); + } else { + dest.set(0, 0, (int) screenSize.x, (int) screenSize.y); + } + (data.onlyTop ? bottomDisplay : topDisplay).set(0, 0, 0, 0); + } + + + /*** + * RELATIVE LAYOUT: + * ORGANIZE SCREEN IN POSITION BASED IN GRAVITY + * AND SPACE, THE SPACE DETERMINE LANDSCAPE TOP SCREEN SIZE + */ + private void relative(Model data) { + int screenWidth = (int) screenSize.x; + int screenHeight = (int) screenSize.y; + + Vector2 topSourceSize = this.sourceTop; + Vector2 bottomSourceSize = this.sourceBottom; + + Rect topDisplay = this.topDisplay; + Rect bottomDisplay = this.bottomDisplay; + + if (data.reverse) { + topSourceSize = this.sourceBottom; + bottomSourceSize = this.sourceTop; + + topDisplay = this.bottomDisplay; + bottomDisplay = this.topDisplay; + } + + if (screenWidth > screenHeight) { + int topDisplayWidth = (int) ((screenHeight / topSourceSize.y) * topSourceSize.x); + int topDisplayHeight = screenHeight; + + if (topDisplayWidth > (screenWidth * data.space)) { + topDisplayWidth = (int) (screenWidth * data.space); + topDisplayHeight = (int) ((topDisplayWidth / topSourceSize.x) * topSourceSize.y); + } + + int bottomDisplayHeight = (int) (((screenWidth - topDisplayWidth) / bottomSourceSize.x) * bottomSourceSize.y); + + topDisplay.set(0, 0, topDisplayWidth, topDisplayHeight); + bottomDisplay.set(topDisplayWidth, 0, topDisplayWidth + (screenWidth - topDisplayWidth), bottomDisplayHeight); + + switch (data.gravity) { + case Gravity.CENTER: { + bottomDisplay.offset(0, (screenHeight - bottomDisplay.height()) / 2); + topDisplay.offset(0, (screenHeight - topDisplay.height()) / 2); + } + break; + case Gravity.BOTTOM: { + bottomDisplay.offset(0, (screenHeight - bottomDisplay.height())); + topDisplay.offset(0, (screenHeight - topDisplay.height())); + } + break; + } + + } else { + int topScreenHeight = (int) ((screenWidth / topSourceSize.x) * topSourceSize.y); + topDisplay.set(0, 0, screenWidth, topScreenHeight); + + int bottomDisplayHeight = (int) ((screenWidth / bottomSourceSize.x) * bottomSourceSize.y); + int bottomDisplayWidth = screenWidth; + int bottomDisplayX = 0; + + if (topScreenHeight + bottomDisplayHeight > screenHeight) { + bottomDisplayHeight = (screenHeight - topScreenHeight); + bottomDisplayWidth = (int) ((bottomDisplayHeight / bottomSourceSize.y) * bottomSourceSize.x); + bottomDisplayX = (screenWidth - bottomDisplayX) / 2; + } + + topDisplay.set(0, 0, screenWidth, topScreenHeight); + bottomDisplay.set(bottomDisplayX, topScreenHeight, bottomDisplayX + bottomDisplayWidth, topScreenHeight + bottomDisplayHeight); + } + } + + public Model getCurrentModel() { + return screenSize.x > screenSize.y ? modes[0] : modes[1]; + } + +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsLayoutManager.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsLayoutManager.java new file mode 100644 index 00000000..6f41359e --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/DsLayoutManager.java @@ -0,0 +1,52 @@ +package com.panda3ds.pandroid.view.ds; + +import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout; + +import java.util.ArrayList; + +public class DsLayoutManager { + private static final DataModel data; + + static { + data = GlobalConfig.getExtra(GlobalConfig.KEY_DS_LAYOUTS, DataModel.class); + if (data.models.size() == 0){ + setupBasicModels(); + } + } + + private static void setupBasicModels() { + Model model1 = new Model(); + + Model model2 = new Model(); + model2.mode = Mode.SINGLE; + model2.onlyTop = false; + + Model model3 = new Model(); + model3.mode = Mode.SINGLE; + model3.onlyTop = true; + + data.models.add(new Model[]{model1, model1.clone()}); + data.models.add(new Model[]{model2, model2.clone()}); + data.models.add(new Model[]{model3, model3.clone()}); + + save(); + } + + public static synchronized void save(){ + GlobalConfig.putExtra(GlobalConfig.KEY_DS_LAYOUTS, data); + } + + public static int getLayoutCount(){ + return data.models.size(); + } + + public static ConsoleLayout createLayout(int index){ + index = Math.min(getLayoutCount()-1, index); + return new DsLayout(data.models.get(index)[0],data.models.get(index)[1]); + } + + private static class DataModel { + private final ArrayList models = new ArrayList<>(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Mode.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Mode.java new file mode 100644 index 00000000..0a31f62d --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Mode.java @@ -0,0 +1,7 @@ +package com.panda3ds.pandroid.view.ds; + +enum Mode { + SINGLE, + RELATIVE, + ABSOLUTE +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Model.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Model.java new file mode 100644 index 00000000..aff03c21 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/ds/Model.java @@ -0,0 +1,30 @@ +package com.panda3ds.pandroid.view.ds; + +import android.util.Log; +import android.view.Gravity; + +import androidx.annotation.NonNull; + +import com.panda3ds.pandroid.utils.Constants; + +class Model implements Cloneable { + public Mode mode = Mode.RELATIVE; + public final Bounds preferredTop = new Bounds(); + public final Bounds preferredBottom = new Bounds(); + public boolean reverse = false; + public boolean onlyTop = true; + public float space = 0.6f; + public int gravity = Gravity.CENTER; + public boolean lockAspect = true; + + @NonNull + @Override + public Model clone() { + try { + return (Model) super.clone(); + } catch (Exception e){ + Log.e(Constants.LOG_TAG, "Error on clone DsModel!", e); + return new Model(); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java index 1a3febd4..3a07fbf7 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java @@ -8,12 +8,20 @@ import androidx.recyclerview.widget.RecyclerView; import com.panda3ds.pandroid.R; import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.lang.Function; import java.util.ArrayList; import java.util.List; class GameAdapter extends RecyclerView.Adapter { private final ArrayList games = new ArrayList<>(); + private final Function clickListener; + private final Function longClickListener; + + GameAdapter(Function clickListener, Function longClickListener) { + this.clickListener = clickListener; + this.longClickListener = longClickListener; + } @NonNull @Override @@ -23,6 +31,11 @@ class GameAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull ItemHolder holder, int position) { + holder.itemView.setOnClickListener(v -> clickListener.run(games.get(position))); + holder.itemView.setOnLongClickListener(v -> { + longClickListener.run(games.get(position)); + return false; + }); holder.apply(games.get(position)); } 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 24e65e2f..2e09d966 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 @@ -8,12 +8,15 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.lang.Function; +import com.panda3ds.pandroid.utils.GameUtils; import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout; import java.util.List; public class GamesGridView extends RecyclerView { private final GameAdapter adapter; + private Function longClickListener = null; public GamesGridView(@NonNull Context context) { this(context, null); @@ -26,7 +29,21 @@ public class GamesGridView extends RecyclerView { public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setLayoutManager(new AutoFitGridLayout(getContext(), 170)); - setAdapter(adapter = new GameAdapter()); + setAdapter(adapter = new GameAdapter(this::onClickGame, this::onLongClickGame)); + } + + public void setItemLongClick(Function longClickListener) { + this.longClickListener = longClickListener; + } + + private void onClickGame(GameMetadata game) { + GameUtils.launch(getContext(), game); + } + + private void onLongClickGame(GameMetadata game) { + if (longClickListener != null){ + longClickListener.run(game); + } } public void setGameList(List games) { diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java index 54f86dae..2d55964f 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java @@ -5,10 +5,8 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatTextView; import androidx.recyclerview.widget.RecyclerView; - import com.panda3ds.pandroid.R; import com.panda3ds.pandroid.data.game.GameMetadata; -import com.panda3ds.pandroid.utils.GameUtils; class ItemHolder extends RecyclerView.ViewHolder { public ItemHolder(@NonNull View itemView) { @@ -22,9 +20,5 @@ class ItemHolder extends RecyclerView.ViewHolder { .setImageBitmap(game.getIcon()); ((AppCompatTextView) itemView.findViewById(R.id.description)) .setText(game.getPublisher()); - - itemView.setOnClickListener((v) -> { - GameUtils.launch(v.getContext(), game); - }); } -} \ No newline at end of file +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/preferences/SingleSelectionPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/preferences/SingleSelectionPreferences.java deleted file mode 100644 index 49fabd6a..00000000 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/preferences/SingleSelectionPreferences.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.panda3ds.pandroid.view.preferences; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.util.AttributeSet; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; - -import com.panda3ds.pandroid.R; -import com.panda3ds.pandroid.utils.Constants; - -public class SingleSelectionPreferences extends PreferenceCategory implements Preference.OnPreferenceClickListener { - private final Drawable transparent = new ColorDrawable(Color.TRANSPARENT); - private final Drawable doneDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_done); - - public SingleSelectionPreferences(@NonNull Context context) { - super(context); - } - - public SingleSelectionPreferences(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public SingleSelectionPreferences(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public SingleSelectionPreferences(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - { - try { - TypedArray color = getContext().obtainStyledAttributes(new int[]{ - android.R.attr.textColorSecondary - }); - doneDrawable.setTint(color.getColor(0, Color.RED)); - color.recycle(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - color.close(); - } - } catch (Exception e) { - Log.e(Constants.LOG_TAG, "Error on obtain text color secondary: ", e); - } - } - - @Override - public void onAttached() { - super.onAttached(); - - for (int i = 0; i < getPreferenceCount();i++) { - getPreference(i).setOnPreferenceClickListener(this); - } - } - - public void setSelectedItem(int index) { - onPreferenceClick(getPreference(index)); - } - - @Override - public boolean onPreferenceClick(@NonNull Preference preference) { - int index = 0; - - for (int i = 0; i < getPreferenceCount(); i++) { - Preference item = getPreference(i); - if (item == preference) { - index = i; - item.setIcon(R.drawable.ic_done); - } else { - item.setIcon(transparent); - } - } - - callChangeListener(index); - return false; - } -} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/RelativeScreenLayout.java similarity index 50% rename from src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java rename to src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/RelativeScreenLayout.java index a726b2e6..29b826e6 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/RelativeScreenLayout.java @@ -1,10 +1,11 @@ package com.panda3ds.pandroid.view.renderer.layout; import android.graphics.Rect; +import android.view.Gravity; import com.panda3ds.pandroid.math.Vector2; -public class DefaultScreenLayout implements ConsoleLayout { +public class RelativeScreenLayout implements ConsoleLayout { private final Rect topDisplay = new Rect(); private final Rect bottomDisplay = new Rect(); @@ -12,6 +13,12 @@ public class DefaultScreenLayout implements ConsoleLayout { private final Vector2 topSourceSize = new Vector2(1.0f, 1.0f); private final Vector2 bottomSourceSize = new Vector2(1.0f, 1.0f); + private boolean landscapeReverse = false; + private boolean portraitReverse = false; + private float landscapeSpace = 0.6f; + private int landscapeGravity = Gravity.CENTER; + private int portraitGravity = Gravity.TOP; + @Override public void update(int screenWidth, int screenHeight) { screenSize.set(screenWidth, screenHeight); @@ -29,16 +36,50 @@ public class DefaultScreenLayout implements ConsoleLayout { updateBounds(); } + public void setPortraitGravity(int portraitGravity) { + this.portraitGravity = portraitGravity; + } + + public void setLandscapeGravity(int landscapeGravity) { + this.landscapeGravity = landscapeGravity; + } + + public void setLandscapeSpace(float landscapeSpace) { + this.landscapeSpace = landscapeSpace; + } + + public void setLandscapeReverse(boolean landscapeReverse) { + this.landscapeReverse = landscapeReverse; + } + + public void setPortraitReverse(boolean portraitReverse) { + this.portraitReverse = portraitReverse; + } + private void updateBounds() { int screenWidth = (int) screenSize.x; int screenHeight = (int) screenSize.y; + Vector2 topSourceSize = this.topSourceSize; + Vector2 bottomSourceSize = this.bottomSourceSize; + + Rect topDisplay = this.topDisplay; + Rect bottomDisplay = this.bottomDisplay; + + if ((landscapeReverse && screenWidth > screenHeight) || (portraitReverse && screenWidth < screenHeight)){ + topSourceSize = this.bottomSourceSize; + bottomSourceSize = this.topSourceSize; + + topDisplay = this.bottomDisplay; + bottomDisplay = this.topDisplay; + } + if (screenWidth > screenHeight) { int topDisplayWidth = (int) ((screenHeight / topSourceSize.y) * topSourceSize.x); int topDisplayHeight = screenHeight; - if (topDisplayWidth > (screenWidth * 0.7)) { - topDisplayWidth = (int) (screenWidth * 0.7); + if (topDisplayWidth > (screenWidth * landscapeSpace)) { + topDisplayWidth = (int) (screenWidth * landscapeSpace); topDisplayHeight = (int) ((topDisplayWidth / topSourceSize.x) * topSourceSize.y); } @@ -46,6 +87,7 @@ public class DefaultScreenLayout implements ConsoleLayout { topDisplay.set(0, 0, topDisplayWidth, topDisplayHeight); bottomDisplay.set(topDisplayWidth, 0, topDisplayWidth + (screenWidth - topDisplayWidth), bottomDisplayHeight); + adjustHorizontalGravity(); } else { int topScreenHeight = (int) ((screenWidth / topSourceSize.x) * topSourceSize.y); topDisplay.set(0, 0, screenWidth, topScreenHeight); @@ -62,9 +104,42 @@ public class DefaultScreenLayout implements ConsoleLayout { topDisplay.set(0, 0, screenWidth, topScreenHeight); bottomDisplay.set(bottomDisplayX, topScreenHeight, bottomDisplayX + bottomDisplayWidth, topScreenHeight + bottomDisplayHeight); + adjustVerticalGravity(); } } + private void adjustHorizontalGravity(){ + int topOffset = 0; + int bottomOffset = 0; + switch (landscapeGravity){ + case Gravity.CENTER:{ + topOffset = (int) (screenSize.y - topDisplay.height())/2; + bottomOffset = (int) (screenSize.y - bottomDisplay.height())/2; + }break; + case Gravity.BOTTOM:{ + topOffset = (int) (screenSize.y - topDisplay.height()); + bottomOffset = (int) (screenSize.y - bottomDisplay.height()); + }break; + } + topDisplay.offset(0, topOffset); + bottomDisplay.offset(0, bottomOffset); + } + + private void adjustVerticalGravity(){ + int height = (topDisplay.height() + bottomDisplay.height()); + int space = 0; + switch (portraitGravity){ + case Gravity.CENTER:{ + space = (int) (screenSize.y - height)/2; + }break; + case Gravity.BOTTOM:{ + space = (int) (screenSize.y - height); + }break; + } + topDisplay.offset(0, space); + bottomDisplay.offset(0,space); + } + @Override public Rect getBottomDisplayBounds() { return bottomDisplay; diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/SingleScreenLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/SingleScreenLayout.java new file mode 100644 index 00000000..96be64bb --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/SingleScreenLayout.java @@ -0,0 +1,63 @@ +package com.panda3ds.pandroid.view.renderer.layout; + +import android.graphics.Rect; +import android.view.Gravity; + +import com.panda3ds.pandroid.math.Vector2; + +public class SingleScreenLayout implements ConsoleLayout { + private final Rect topDisplay = new Rect(); + private final Rect bottomDisplay = new Rect(); + private final Vector2 screenSize = new Vector2(1.0f, 1.0f); + private final Vector2 topSourceSize = new Vector2(1.0f, 1.0f); + private final Vector2 bottomSourceSize = new Vector2(1.0f, 1.0f); + private boolean top = true; + + @Override + public void update(int screenWidth, int screenHeight) { + screenSize.set(screenWidth, screenHeight); + updateBounds(); + } + + @Override + public void setBottomDisplaySourceSize(int width, int height) { + bottomSourceSize.set(width, height); + updateBounds(); + } + @Override + public void setTopDisplaySourceSize(int width, int height) { + topSourceSize.set(width, height); + updateBounds(); + } + + private void updateBounds() { + int screenWidth = (int) screenSize.x; + int screenHeight = (int) screenSize.y; + Vector2 source = top ? topSourceSize : bottomSourceSize; + Rect dest = top ? topDisplay : bottomDisplay; + + int width = Math.round((screenHeight / source.y) * source.x); + int height = screenHeight; + int y = 0; + int x = (screenWidth - width) / 2; + if (width > screenWidth){ + width = screenWidth; + height = Math.round((screenWidth / source.x) * source.y); + x = 0; + y = (screenHeight - height)/2; + } + dest.set(x, y, x + width, y+height); + + (top ? bottomDisplay : topDisplay).set(0,0,0,0); + } + + @Override + public Rect getBottomDisplayBounds() { + return bottomDisplay; + } + + @Override + public Rect getTopDisplayBounds() { + return topDisplay; + } +} diff --git a/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml b/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml index 20a71e46..729e056b 100644 --- a/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml +++ b/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml @@ -3,6 +3,7 @@ + diff --git a/src/pandroid/app/src/main/res/drawable/ds_editor_popup_background.xml b/src/pandroid/app/src/main/res/drawable/ds_editor_popup_background.xml new file mode 100644 index 00000000..80fee9a8 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ds_editor_popup_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/ic_align_center.xml b/src/pandroid/app/src/main/res/drawable/ic_align_center.xml new file mode 100644 index 00000000..1e2e0c74 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_align_center.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_arrow_down.xml b/src/pandroid/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 00000000..5eff5d5d --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_arrow_up.xml b/src/pandroid/app/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 00000000..f5859d56 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_code.xml b/src/pandroid/app/src/main/res/drawable/ic_code.xml index 8ef40bd2..167935e2 100644 --- a/src/pandroid/app/src/main/res/drawable/ic_code.xml +++ b/src/pandroid/app/src/main/res/drawable/ic_code.xml @@ -1,4 +1,4 @@ - diff --git a/src/pandroid/app/src/main/res/drawable/ic_compare_arrow.xml b/src/pandroid/app/src/main/res/drawable/ic_compare_arrow.xml new file mode 100644 index 00000000..937d53a8 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_compare_arrow.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_done.xml b/src/pandroid/app/src/main/res/drawable/ic_done.xml index 25e52514..e007f1d1 100644 --- a/src/pandroid/app/src/main/res/drawable/ic_done.xml +++ b/src/pandroid/app/src/main/res/drawable/ic_done.xml @@ -1,4 +1,4 @@ - diff --git a/src/pandroid/app/src/main/res/drawable/ic_edit.xml b/src/pandroid/app/src/main/res/drawable/ic_edit.xml index 1c9bd3e6..6ddb5cb1 100644 --- a/src/pandroid/app/src/main/res/drawable/ic_edit.xml +++ b/src/pandroid/app/src/main/res/drawable/ic_edit.xml @@ -1,4 +1,4 @@ - diff --git a/src/pandroid/app/src/main/res/drawable/ic_exit.xml b/src/pandroid/app/src/main/res/drawable/ic_exit.xml index a17ca78b..e052e1cb 100644 --- a/src/pandroid/app/src/main/res/drawable/ic_exit.xml +++ b/src/pandroid/app/src/main/res/drawable/ic_exit.xml @@ -1,5 +1,5 @@ diff --git a/src/pandroid/app/src/main/res/drawable/ic_folder.xml b/src/pandroid/app/src/main/res/drawable/ic_folder.xml new file mode 100644 index 00000000..ce1051f4 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_folder.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_shortcut.xml b/src/pandroid/app/src/main/res/drawable/ic_shortcut.xml index 13be1ace..8bb2a987 100644 --- a/src/pandroid/app/src/main/res/drawable/ic_shortcut.xml +++ b/src/pandroid/app/src/main/res/drawable/ic_shortcut.xml @@ -1,5 +1,5 @@ diff --git a/src/pandroid/app/src/main/res/drawable/ic_switch_screen.xml b/src/pandroid/app/src/main/res/drawable/ic_switch_screen.xml new file mode 100644 index 00000000..f577b24f --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_switch_screen.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/switch_thumb.xml b/src/pandroid/app/src/main/res/drawable/switch_thumb.xml deleted file mode 100644 index 02f1ab02..00000000 --- a/src/pandroid/app/src/main/res/drawable/switch_thumb.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/switch_track.xml b/src/pandroid/app/src/main/res/drawable/switch_track.xml deleted file mode 100644 index b665789c..00000000 --- a/src/pandroid/app/src/main/res/drawable/switch_track.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/dialog_bottom_sheet.xml b/src/pandroid/app/src/main/res/layout/dialog_bottom_sheet.xml new file mode 100644 index 00000000..88124bf1 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/dialog_bottom_sheet.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/dialog_game_about.xml b/src/pandroid/app/src/main/res/layout/dialog_game_about.xml new file mode 100644 index 00000000..3a91ae2c --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/dialog_game_about.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/dialog_games_folder.xml b/src/pandroid/app/src/main/res/layout/dialog_games_folder.xml new file mode 100644 index 00000000..c0b41d8f --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/dialog_games_folder.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/dialog_select_theme.xml b/src/pandroid/app/src/main/res/layout/dialog_select_theme.xml new file mode 100644 index 00000000..b3f9d0cf --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/dialog_select_theme.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/ds_editor_gravity_anchor.xml b/src/pandroid/app/src/main/res/layout/ds_editor_gravity_anchor.xml new file mode 100644 index 00000000..3945b27a --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/ds_editor_gravity_anchor.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/ds_editor_lock_aspect.xml b/src/pandroid/app/src/main/res/layout/ds_editor_lock_aspect.xml new file mode 100644 index 00000000..238b86c5 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/ds_editor_lock_aspect.xml @@ -0,0 +1,33 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/ds_editor_spinner.xml b/src/pandroid/app/src/main/res/layout/ds_editor_spinner.xml new file mode 100644 index 00000000..515f917f --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/ds_editor_spinner.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/ds_editor_spinner_label.xml b/src/pandroid/app/src/main/res/layout/ds_editor_spinner_label.xml new file mode 100644 index 00000000..125072e0 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/ds_editor_spinner_label.xml @@ -0,0 +1,12 @@ + + + \ 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 8019c3c3..30b08386 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 @@ -69,26 +69,25 @@ - - - - - - - + + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="start"> + + + + + diff --git a/src/pandroid/app/src/main/res/layout/hold_theme_preview.xml b/src/pandroid/app/src/main/res/layout/hold_theme_preview.xml new file mode 100644 index 00000000..9bdab2f1 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/hold_theme_preview.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/hold_theme_preview_base.xml b/src/pandroid/app/src/main/res/layout/hold_theme_preview_base.xml new file mode 100644 index 00000000..9e7a6e6f --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/hold_theme_preview_base.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/holder_game.xml b/src/pandroid/app/src/main/res/layout/holder_game.xml index f7e077c2..275926c3 100644 --- a/src/pandroid/app/src/main/res/layout/holder_game.xml +++ b/src/pandroid/app/src/main/res/layout/holder_game.xml @@ -31,6 +31,7 @@ android:textColor="?colorOnSurface" android:text="@string/app_name" android:textStyle="bold" + android:textSize="14sp" android:gravity="center" android:layout_marginTop="10dp"/> diff --git a/src/pandroid/app/src/main/res/layout/material_switch_widget.xml b/src/pandroid/app/src/main/res/layout/material_switch_widget.xml new file mode 100644 index 00000000..3cffb5c1 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/material_switch_widget.xml @@ -0,0 +1,11 @@ + + diff --git a/src/pandroid/app/src/main/res/menu/game_drawer.xml b/src/pandroid/app/src/main/res/menu/game_drawer.xml index 72574d7b..2ac82b68 100644 --- a/src/pandroid/app/src/main/res/menu/game_drawer.xml +++ b/src/pandroid/app/src/main/res/menu/game_drawer.xml @@ -10,6 +10,10 @@ android:id="@+id/resume" android:icon="@drawable/ic_shortcut" android:title="@string/resume" /> + Grave os registros para um arquivo. Shader Jit Usar recompilador de shaders. + Picture In Picture + Entrar em modo picture in picture quando a janela sai de foco Gráficos Carregando Rotacionar + Aplicar + Definir o tema do aplicativo + Tema do aplicativo + Falha ao carregar a ROM. + Falha ao carregar o arquivo ROM, por favor verifique se o arquivo não esta corrompido ou se o emulator tem permição para acessa-lo. + Sistema + Geral + Configurações gerais do emulador. + Tela inferior + Tela superior + Manter porporção + Disposição das telas + Altere as disposições disponiveis para as telas do console + Clique para mudar + Mudar telas + Pastas usadas para importar os jogos + Pastas de jogos + Adicionar pasta + %d Jogos + Diretorio + Remover + Jogar + Região + Estados Unidos + Japão + Europa + Australia + Coréia + Taiwan + Comportamento + Jogo invalido + Ferramentas diff --git a/src/pandroid/app/src/main/res/values/strings.xml b/src/pandroid/app/src/main/res/values/strings.xml index 20e6c5c8..25569528 100644 --- a/src/pandroid/app/src/main/res/values/strings.xml +++ b/src/pandroid/app/src/main/res/values/strings.xml @@ -54,10 +54,46 @@ Logger, performance statistics, etc. Performance monitor Show overlay with fps, memory, etc. - Logger - Store application logs to file. + Picture In Picture + Minimize window when placed in the background. + Graphics Shader JIT Use shader recompiler. - Graphics + Tools + Logger + Store application logs to file. Loading + Apply + Set application theme + Application theme + Failed to load ROM + Make sure it\'s a valid 3DS ROM and that storage permissions are configured properly. + System + General + General application configuration. + Screen layouts + Change layout of console screens. + Click to change + Swap screen + Folders for importing games + Game folders + Import folder + %d Games + Directory + Remove + Play + + Maintain aspect ratio + Bottom Display + Top Display + + Region + North American + Japan + Europe + Australia + Korean + Taiwan + Behavior + Invalid game diff --git a/src/pandroid/app/src/main/res/values/themes.xml b/src/pandroid/app/src/main/res/values/themes.xml index e292b5e3..5deec652 100644 --- a/src/pandroid/app/src/main/res/values/themes.xml +++ b/src/pandroid/app/src/main/res/values/themes.xml @@ -7,30 +7,21 @@ @style/PreferenceTheme ?colorSurface ?isLightTheme - - @style/SwitchStyle - @style/SwitchStyle - @style/SwitchStyle - - - + +