Android: Add graphical save/load state selector
This commit is contained in:
@ -136,6 +136,8 @@ public class AndroidHostInterface {
|
||||
|
||||
public native boolean setMediaFilename(String filename);
|
||||
|
||||
public native SaveStateInfo[] getSaveStateInfo(boolean includeEmpty);
|
||||
|
||||
static {
|
||||
System.loadLibrary("duckstation-native");
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@ -41,7 +42,6 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||
private boolean mApplySettingsOnSurfaceRestored = false;
|
||||
private String mGameTitle = null;
|
||||
private EmulationSurfaceView mContentView;
|
||||
private int mSaveStateSlot = 0;
|
||||
|
||||
private boolean getBooleanSetting(String key, boolean defaultValue) {
|
||||
return mPreferences.getBoolean(key, defaultValue);
|
||||
@ -398,42 +398,36 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
if (mGameTitle != null && !mGameTitle.isEmpty())
|
||||
builder.setTitle(mGameTitle);
|
||||
builder.setItems(R.array.emulation_menu, (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0: // Quick Load
|
||||
case 0: // Load State
|
||||
{
|
||||
AndroidHostInterface.getInstance().loadState(false, mSaveStateSlot);
|
||||
onMenuClosed();
|
||||
showSaveStateMenu(false);
|
||||
return;
|
||||
}
|
||||
|
||||
case 1: // Quick Save
|
||||
case 1: // Save State
|
||||
{
|
||||
AndroidHostInterface.getInstance().saveState(false, mSaveStateSlot);
|
||||
onMenuClosed();
|
||||
showSaveStateMenu(true);
|
||||
return;
|
||||
}
|
||||
|
||||
case 2: // Save State Slot
|
||||
{
|
||||
showSaveStateSlotMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
case 3: // Toggle Fast Forward
|
||||
case 2: // Toggle Fast Forward
|
||||
{
|
||||
AndroidHostInterface.getInstance().setFastForwardEnabled(!AndroidHostInterface.getInstance().isFastForwardEnabled());
|
||||
onMenuClosed();
|
||||
return;
|
||||
}
|
||||
|
||||
case 4: // More Options
|
||||
case 3: // More Options
|
||||
{
|
||||
showMoreMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
case 5: // Quit
|
||||
case 4: // Quit
|
||||
{
|
||||
mStopRequested = true;
|
||||
finish();
|
||||
@ -445,15 +439,34 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void showSaveStateSlotMenu() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setSingleChoiceItems(R.array.emulation_save_state_slot_menu, mSaveStateSlot, (dialogInterface, i) -> {
|
||||
mSaveStateSlot = i;
|
||||
dialogInterface.dismiss();
|
||||
private void showSaveStateMenu(boolean saving) {
|
||||
final SaveStateInfo[] infos = AndroidHostInterface.getInstance().getSaveStateInfo(true);
|
||||
if (infos == null) {
|
||||
onMenuClosed();
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
final ListView listView = new ListView(this);
|
||||
listView.setAdapter(new SaveStateInfo.ListAdapter(this, infos));
|
||||
builder.setView(listView);
|
||||
builder.setOnDismissListener((dialog) -> {
|
||||
onMenuClosed();
|
||||
});
|
||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
|
||||
final AlertDialog dialog = builder.create();
|
||||
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
SaveStateInfo info = infos[position];
|
||||
if (saving) {
|
||||
AndroidHostInterface.getInstance().saveState(info.isGlobal(), info.getSlot());
|
||||
} else {
|
||||
AndroidHostInterface.getInstance().loadState(info.isGlobal(), info.getSlot());
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void showMoreMenu() {
|
||||
|
||||
@ -298,7 +298,7 @@ public class GameDirectoriesActivity extends AppCompatActivity {
|
||||
.setTitle(R.string.edit_game_directories_add_path)
|
||||
.setMessage(R.string.edit_game_directories_add_path_summary)
|
||||
.setView(text)
|
||||
.setPositiveButton("Add", (dialog, which) -> {
|
||||
.setPositiveButton("Add", (dialog, which) -> {
|
||||
final String path = text.getText().toString();
|
||||
if (!path.isEmpty()) {
|
||||
addSearchDirectory(GameDirectoriesActivity.this, path, true);
|
||||
|
||||
@ -0,0 +1,151 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class SaveStateInfo {
|
||||
private String mPath;
|
||||
private String mGameTitle;
|
||||
private String mGameCode;
|
||||
private String mMediaPath;
|
||||
private String mTimestamp;
|
||||
private int mSlot;
|
||||
private boolean mGlobal;
|
||||
private Bitmap mScreenshot;
|
||||
|
||||
public SaveStateInfo(String path, String gameTitle, String gameCode, String mediaPath, String timestamp, int slot, boolean global,
|
||||
int screenshotWidth, int screenshotHeight, byte[] screenshotData) {
|
||||
mPath = path;
|
||||
mGameTitle = gameTitle;
|
||||
mGameCode = gameCode;
|
||||
mMediaPath = mediaPath;
|
||||
mTimestamp = timestamp;
|
||||
mSlot = slot;
|
||||
mGlobal = global;
|
||||
|
||||
if (screenshotData != null) {
|
||||
try {
|
||||
mScreenshot = Bitmap.createBitmap(screenshotWidth, screenshotHeight, Bitmap.Config.ARGB_8888);
|
||||
mScreenshot.copyPixelsFromBuffer(ByteBuffer.wrap(screenshotData));
|
||||
} catch (Exception e) {
|
||||
mScreenshot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return mPath != null;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public String getGameTitle() {
|
||||
return mGameTitle;
|
||||
}
|
||||
|
||||
public String getGameCode() {
|
||||
return mGameCode;
|
||||
}
|
||||
|
||||
public String getMediaPath() {
|
||||
return mMediaPath;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
public int getSlot() {
|
||||
return mSlot;
|
||||
}
|
||||
|
||||
public boolean isGlobal() {
|
||||
return mGlobal;
|
||||
}
|
||||
|
||||
public Bitmap getScreenshot() {
|
||||
return mScreenshot;
|
||||
}
|
||||
|
||||
private void fillView(Context context, View view) {
|
||||
ImageView imageView = (ImageView) view.findViewById(R.id.image);
|
||||
TextView summaryView = (TextView) view.findViewById(R.id.summary);
|
||||
TextView gameView = (TextView) view.findViewById(R.id.game);
|
||||
TextView pathView = (TextView) view.findViewById(R.id.path);
|
||||
TextView timestampView = (TextView) view.findViewById(R.id.timestamp);
|
||||
|
||||
if (mScreenshot != null)
|
||||
imageView.setImageBitmap(mScreenshot);
|
||||
else
|
||||
imageView.setImageDrawable(context.getDrawable(R.drawable.ic_baseline_not_interested_60));
|
||||
|
||||
String summaryText;
|
||||
if (mGlobal)
|
||||
summaryView.setText(String.format(context.getString(R.string.save_state_info_global_save_n), mSlot));
|
||||
else if (mSlot == 0)
|
||||
summaryView.setText(R.string.save_state_info_quick_save);
|
||||
else
|
||||
summaryView.setText(String.format(context.getString(R.string.save_state_info_game_save_n), mSlot));
|
||||
|
||||
if (exists()) {
|
||||
gameView.setText(String.format("%s - %s", mGameCode, mGameTitle));
|
||||
|
||||
int lastSlashPosition = mMediaPath.lastIndexOf('/');
|
||||
if (lastSlashPosition >= 0)
|
||||
pathView.setText(mMediaPath.substring(lastSlashPosition + 1));
|
||||
else
|
||||
pathView.setText(mMediaPath);
|
||||
|
||||
timestampView.setText(mTimestamp);
|
||||
} else {
|
||||
gameView.setText(R.string.save_state_info_slot_is_empty);
|
||||
pathView.setText("");
|
||||
timestampView.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ListAdapter extends BaseAdapter {
|
||||
private final Context mContext;
|
||||
private final SaveStateInfo[] mInfos;
|
||||
|
||||
public ListAdapter(Context context, SaveStateInfo[] infos) {
|
||||
mContext = context;
|
||||
mInfos = infos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mInfos.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mInfos[position];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(mContext).inflate(R.layout.save_state_view_entry, parent, false);
|
||||
}
|
||||
|
||||
mInfos[position].fillView(mContext, convertView);
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user