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
-
-
-
+
+