Android: Add per-game settings and properties UI
This commit is contained in:
parent
7f008ea5c7
commit
4eee5ebdb7
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
|
@ -46,6 +46,8 @@ static jmethodID s_EmulationActivity_method_onGameTitleChanged;
|
|||
static jmethodID s_EmulationActivity_method_setVibration;
|
||||
static jclass s_PatchCode_class;
|
||||
static jmethodID s_PatchCode_constructor;
|
||||
static jclass s_GameListEntry_class;
|
||||
static jmethodID s_GameListEntry_constructor;
|
||||
|
||||
namespace AndroidHelpers {
|
||||
// helper for retrieving the current per-thread jni environment
|
||||
|
@ -524,7 +526,6 @@ void AndroidHostInterface::UpdateControllerInterface()
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void AndroidHostInterface::OnSystemPaused(bool paused)
|
||||
{
|
||||
CommonHostInterface::OnSystemPaused(paused);
|
||||
|
@ -664,12 +665,11 @@ void AndroidHostInterface::HandleControllerButtonEvent(u32 controller_index, u32
|
|||
if (!IsEmulationThreadRunning())
|
||||
return;
|
||||
|
||||
RunOnEmulationThread(
|
||||
[this, controller_index, button_index, pressed]() {
|
||||
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
|
||||
if (ci)
|
||||
ci->HandleButtonEvent(controller_index, button_index, pressed);
|
||||
});
|
||||
RunOnEmulationThread([this, controller_index, button_index, pressed]() {
|
||||
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
|
||||
if (ci)
|
||||
ci->HandleButtonEvent(controller_index, button_index, pressed);
|
||||
});
|
||||
}
|
||||
|
||||
void AndroidHostInterface::HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value)
|
||||
|
@ -677,12 +677,11 @@ void AndroidHostInterface::HandleControllerAxisEvent(u32 controller_index, u32 a
|
|||
if (!IsEmulationThreadRunning())
|
||||
return;
|
||||
|
||||
RunOnEmulationThread(
|
||||
[this, controller_index, axis_index, value]() {
|
||||
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
|
||||
if (ci)
|
||||
ci->HandleAxisEvent(controller_index, axis_index, value);
|
||||
});
|
||||
RunOnEmulationThread([this, controller_index, axis_index, value]() {
|
||||
AndroidControllerInterface* ci = static_cast<AndroidControllerInterface*>(m_controller_interface.get());
|
||||
if (ci)
|
||||
ci->HandleAxisEvent(controller_index, axis_index, value);
|
||||
});
|
||||
}
|
||||
|
||||
void AndroidHostInterface::SetFastForwardEnabled(bool enabled)
|
||||
|
@ -811,7 +810,7 @@ jobjectArray AndroidHostInterface::GetInputProfileNames(JNIEnv* env) const
|
|||
return name_array;
|
||||
}
|
||||
|
||||
bool AndroidHostInterface::ApplyInputProfile(const char *profile_name)
|
||||
bool AndroidHostInterface::ApplyInputProfile(const char* profile_name)
|
||||
{
|
||||
const std::string path(GetInputProfilePath(profile_name));
|
||||
if (path.empty())
|
||||
|
@ -822,7 +821,7 @@ bool AndroidHostInterface::ApplyInputProfile(const char *profile_name)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool AndroidHostInterface::SaveInputProfile(const char *profile_name)
|
||||
bool AndroidHostInterface::SaveInputProfile(const char* profile_name)
|
||||
{
|
||||
const std::string path(GetSavePathForInputProfile(profile_name));
|
||||
if (path.empty())
|
||||
|
@ -838,13 +837,15 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
|||
|
||||
// Create global reference so it doesn't get cleaned up.
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
jclass string_class, host_interface_class, patch_code_class;
|
||||
jclass string_class, host_interface_class, patch_code_class, game_list_entry_class;
|
||||
if ((string_class = env->FindClass("java/lang/String")) == nullptr ||
|
||||
(s_String_class = static_cast<jclass>(env->NewGlobalRef(string_class))) == nullptr ||
|
||||
(host_interface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr ||
|
||||
(s_AndroidHostInterface_class = static_cast<jclass>(env->NewGlobalRef(host_interface_class))) == nullptr ||
|
||||
(patch_code_class = env->FindClass("com/github/stenzek/duckstation/PatchCode")) == nullptr ||
|
||||
(s_PatchCode_class = static_cast<jclass>(env->NewGlobalRef(patch_code_class))) == nullptr)
|
||||
(s_PatchCode_class = static_cast<jclass>(env->NewGlobalRef(patch_code_class))) == nullptr ||
|
||||
(game_list_entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry")) == nullptr ||
|
||||
(s_GameListEntry_class = static_cast<jclass>(env->NewGlobalRef(game_list_entry_class))) == nullptr)
|
||||
{
|
||||
Log_ErrorPrint("AndroidHostInterface class lookup failed");
|
||||
return -1;
|
||||
|
@ -853,6 +854,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
|||
env->DeleteLocalRef(string_class);
|
||||
env->DeleteLocalRef(host_interface_class);
|
||||
env->DeleteLocalRef(patch_code_class);
|
||||
env->DeleteLocalRef(game_list_entry_class);
|
||||
|
||||
jclass emulation_activity_class;
|
||||
if ((s_AndroidHostInterface_constructor =
|
||||
|
@ -879,7 +881,11 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
|||
env->GetMethodID(s_EmulationActivity_class, "onGameTitleChanged", "(Ljava/lang/String;)V")) == nullptr ||
|
||||
(s_EmulationActivity_method_setVibration = env->GetMethodID(emulation_activity_class, "setVibration", "(Z)V")) ==
|
||||
nullptr ||
|
||||
(s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "<init>", "(ILjava/lang/String;Z)V")) == nullptr)
|
||||
(s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "<init>", "(ILjava/lang/String;Z)V")) == nullptr ||
|
||||
(s_GameListEntry_constructor = env->GetMethodID(
|
||||
s_GameListEntry_class, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/"
|
||||
"String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V")) == nullptr)
|
||||
{
|
||||
Log_ErrorPrint("AndroidHostInterface lookups failed");
|
||||
return -1;
|
||||
|
@ -1026,10 +1032,11 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerAxisCode, jobject
|
|||
return code.value_or(-1);
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerButtonNames, jobject unused, jstring controller_type)
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerButtonNames, jobject unused,
|
||||
jstring controller_type)
|
||||
{
|
||||
std::optional<ControllerType> type =
|
||||
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
|
||||
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
|
||||
if (!type)
|
||||
return nullptr;
|
||||
|
||||
|
@ -1050,10 +1057,11 @@ DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerButtonNam
|
|||
return name_array;
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames, jobject unused, jstring controller_type)
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames, jobject unused,
|
||||
jstring controller_type)
|
||||
{
|
||||
std::optional<ControllerType> type =
|
||||
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
|
||||
Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str());
|
||||
if (!type)
|
||||
return nullptr;
|
||||
|
||||
|
@ -1074,12 +1082,14 @@ DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getControllerAxisNames
|
|||
return name_array;
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index, jint button_index, jboolean pressed)
|
||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerButtonEvent, jobject obj, jint controller_index,
|
||||
jint button_index, jboolean pressed)
|
||||
{
|
||||
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerButtonEvent(controller_index, button_index, pressed);
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index, jint axis_index, jfloat value)
|
||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_handleControllerAxisEvent, jobject obj, jint controller_index,
|
||||
jint axis_index, jfloat value)
|
||||
{
|
||||
AndroidHelpers::GetNativeClass(env, obj)->HandleControllerAxisEvent(controller_index, axis_index, value);
|
||||
}
|
||||
|
@ -1115,69 +1125,116 @@ static const char* DiscRegionToString(DiscRegion region)
|
|||
return names[static_cast<int>(region)];
|
||||
}
|
||||
|
||||
static jobject CreateGameListEntry(JNIEnv* env, AndroidHostInterface* hi, const GameListEntry& entry)
|
||||
{
|
||||
const Timestamp modified_ts(
|
||||
Timestamp::FromUnixTimestamp(static_cast<Timestamp::UnixTimestampValue>(entry.last_modified_time)));
|
||||
const std::string file_title_str(System::GetTitleForPath(entry.path.c_str()));
|
||||
const std::string cover_path_str(hi->GetGameList()->GetCoverImagePathForEntry(&entry));
|
||||
|
||||
jstring path = env->NewStringUTF(entry.path.c_str());
|
||||
jstring code = env->NewStringUTF(entry.code.c_str());
|
||||
jstring title = env->NewStringUTF(entry.title.c_str());
|
||||
jstring file_title = env->NewStringUTF(file_title_str.c_str());
|
||||
jstring region = env->NewStringUTF(DiscRegionToString(entry.region));
|
||||
jstring type = env->NewStringUTF(GameList::EntryTypeToString(entry.type));
|
||||
jstring compatibility_rating =
|
||||
env->NewStringUTF(GameList::EntryCompatibilityRatingToString(entry.compatibility_rating));
|
||||
jstring cover_path = (cover_path_str.empty()) ? nullptr : env->NewStringUTF(cover_path_str.c_str());
|
||||
jstring modified_time = env->NewStringUTF(modified_ts.ToString("%Y/%m/%d, %H:%M:%S"));
|
||||
jlong size = entry.total_size;
|
||||
|
||||
jobject entry_jobject =
|
||||
env->NewObject(s_GameListEntry_class, s_GameListEntry_constructor, path, code, title, file_title, size,
|
||||
modified_time, region, type, compatibility_rating, cover_path);
|
||||
|
||||
env->DeleteLocalRef(modified_time);
|
||||
if (cover_path)
|
||||
env->DeleteLocalRef(cover_path);
|
||||
env->DeleteLocalRef(compatibility_rating);
|
||||
env->DeleteLocalRef(type);
|
||||
env->DeleteLocalRef(region);
|
||||
env->DeleteLocalRef(file_title);
|
||||
env->DeleteLocalRef(title);
|
||||
env->DeleteLocalRef(code);
|
||||
env->DeleteLocalRef(path);
|
||||
|
||||
return entry_jobject;
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jarray, AndroidHostInterface_getGameListEntries, jobject obj)
|
||||
{
|
||||
jclass entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry");
|
||||
Assert(entry_class != nullptr);
|
||||
|
||||
jmethodID entry_constructor =
|
||||
env->GetMethodID(entry_class, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/"
|
||||
"String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
|
||||
Assert(entry_constructor != nullptr);
|
||||
|
||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||
jobjectArray entry_array = env->NewObjectArray(hi->GetGameList()->GetEntryCount(), entry_class, nullptr);
|
||||
jobjectArray entry_array = env->NewObjectArray(hi->GetGameList()->GetEntryCount(), s_GameListEntry_class, nullptr);
|
||||
Assert(entry_array != nullptr);
|
||||
|
||||
u32 counter = 0;
|
||||
for (const GameListEntry& entry : hi->GetGameList()->GetEntries())
|
||||
{
|
||||
const Timestamp modified_ts(
|
||||
Timestamp::FromUnixTimestamp(static_cast<Timestamp::UnixTimestampValue>(entry.last_modified_time)));
|
||||
const std::string file_title_str(System::GetTitleForPath(entry.path.c_str()));
|
||||
const std::string cover_path_str(hi->GetGameList()->GetCoverImagePathForEntry(&entry));
|
||||
|
||||
jstring path = env->NewStringUTF(entry.path.c_str());
|
||||
jstring code = env->NewStringUTF(entry.code.c_str());
|
||||
jstring title = env->NewStringUTF(entry.title.c_str());
|
||||
jstring file_title = env->NewStringUTF(file_title_str.c_str());
|
||||
jstring region = env->NewStringUTF(DiscRegionToString(entry.region));
|
||||
jstring type = env->NewStringUTF(GameList::EntryTypeToString(entry.type));
|
||||
jstring compatibility_rating =
|
||||
env->NewStringUTF(GameList::EntryCompatibilityRatingToString(entry.compatibility_rating));
|
||||
jstring cover_path = (cover_path_str.empty()) ? nullptr : env->NewStringUTF(cover_path_str.c_str());
|
||||
jstring modified_time = env->NewStringUTF(modified_ts.ToString("%Y/%m/%d, %H:%M:%S"));
|
||||
jlong size = entry.total_size;
|
||||
|
||||
jobject entry_jobject = env->NewObject(entry_class, entry_constructor, path, code, title, file_title, size,
|
||||
modified_time, region, type, compatibility_rating, cover_path);
|
||||
|
||||
jobject entry_jobject = CreateGameListEntry(env, hi, entry);
|
||||
env->SetObjectArrayElement(entry_array, counter++, entry_jobject);
|
||||
env->DeleteLocalRef(entry_jobject);
|
||||
env->DeleteLocalRef(modified_time);
|
||||
if (cover_path)
|
||||
env->DeleteLocalRef(cover_path);
|
||||
env->DeleteLocalRef(compatibility_rating);
|
||||
env->DeleteLocalRef(type);
|
||||
env->DeleteLocalRef(region);
|
||||
env->DeleteLocalRef(file_title);
|
||||
env->DeleteLocalRef(title);
|
||||
env->DeleteLocalRef(code);
|
||||
env->DeleteLocalRef(path);
|
||||
}
|
||||
|
||||
return entry_array;
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray , AndroidHostInterface_getHotkeyInfoList, jobject obj)
|
||||
DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_getGameListEntry, jobject obj, jstring path)
|
||||
{
|
||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||
const std::string path_str(AndroidHelpers::JStringToString(env, path));
|
||||
const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str());
|
||||
if (!entry)
|
||||
return nullptr;
|
||||
|
||||
return CreateGameListEntry(env, hi, *entry);
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getGameSettingValue, jobject obj, jstring path, jstring key)
|
||||
{
|
||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||
const std::string path_str(AndroidHelpers::JStringToString(env, path));
|
||||
const std::string key_str(AndroidHelpers::JStringToString(env, key));
|
||||
|
||||
const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str());
|
||||
if (!entry)
|
||||
return nullptr;
|
||||
|
||||
std::optional<std::string> value = entry->settings.GetValueForKey(key_str);
|
||||
if (!value.has_value())
|
||||
return nullptr;
|
||||
else
|
||||
return env->NewStringUTF(value->c_str());
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setGameSettingValue, jobject obj, jstring path, jstring key,
|
||||
jstring value)
|
||||
{
|
||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||
const std::string path_str(AndroidHelpers::JStringToString(env, path));
|
||||
const std::string key_str(AndroidHelpers::JStringToString(env, key));
|
||||
|
||||
const GameListEntry* entry = hi->GetGameList()->GetEntryForPath(path_str.c_str());
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
GameSettings::Entry new_entry(entry->settings);
|
||||
|
||||
std::optional<std::string> value_str;
|
||||
if (value)
|
||||
value_str = AndroidHelpers::JStringToString(env, value);
|
||||
|
||||
new_entry.SetValueForKey(key_str, value_str);
|
||||
hi->GetGameList()->UpdateGameSettings(path_str, entry->code, entry->title, new_entry, true);
|
||||
}
|
||||
|
||||
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getHotkeyInfoList, jobject obj)
|
||||
{
|
||||
jclass entry_class = env->FindClass("com/github/stenzek/duckstation/HotkeyInfo");
|
||||
Assert(entry_class != nullptr);
|
||||
|
||||
jmethodID entry_constructor =
|
||||
env->GetMethodID(entry_class, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
|
||||
env->GetMethodID(entry_class, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
|
||||
Assert(entry_constructor != nullptr);
|
||||
|
||||
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||
|
|
|
@ -43,6 +43,15 @@
|
|||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".GamePropertiesActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/activity_game_properties"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
|
|
|
@ -87,6 +87,11 @@ public class AndroidHostInterface {
|
|||
|
||||
public native GameListEntry[] getGameListEntries();
|
||||
|
||||
public native GameListEntry getGameListEntry(String path);
|
||||
|
||||
public native String getGameSettingValue(String path, String key);
|
||||
public native void setGameSettingValue(String path, String key, String value);
|
||||
|
||||
public native void resetSystem();
|
||||
|
||||
public native void loadState(boolean global, int slot);
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.util.Property;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GamePropertiesActivity extends AppCompatActivity {
|
||||
PropertyListAdapter mPropertiesListAdapter;
|
||||
GameListEntry mGameListEntry;
|
||||
|
||||
public ListAdapter getPropertyListAdapter() {
|
||||
if (mPropertiesListAdapter != null)
|
||||
return mPropertiesListAdapter;
|
||||
|
||||
mPropertiesListAdapter = new PropertyListAdapter(this);
|
||||
mPropertiesListAdapter.addItem("title", "Title", mGameListEntry.getTitle());
|
||||
mPropertiesListAdapter.addItem("filetitle", "File Title", mGameListEntry.getFileTitle());
|
||||
mPropertiesListAdapter.addItem("serial", "Serial", mGameListEntry.getCode());
|
||||
mPropertiesListAdapter.addItem("type", "Type", mGameListEntry.getType().toString());
|
||||
mPropertiesListAdapter.addItem("path", "Path", mGameListEntry.getPath());
|
||||
mPropertiesListAdapter.addItem("region", "Region", mGameListEntry.getRegion().toString());
|
||||
mPropertiesListAdapter.addItem("compatibility", "Compatibility Rating", mGameListEntry.getCompatibilityRating().toString());
|
||||
return mPropertiesListAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String path = getIntent().getStringExtra("path");
|
||||
if (path == null || path.isEmpty()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
mGameListEntry = AndroidHostInterface.getInstance().getGameListEntry(path);
|
||||
if (mGameListEntry == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.settings_activity);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new SettingsCollectionFragment(this))
|
||||
.commit();
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
setTitle(mGameListEntry.getTitle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void displayError(String text) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(text)
|
||||
.setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void createBooleanGameSetting(PreferenceScreen ps, String key, int titleId) {
|
||||
GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
private void createListGameSetting(PreferenceScreen ps, String key, int titleId, int entryId, int entryValuesId) {
|
||||
GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId, entryId, entryValuesId);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
public static class GameSettingsFragment extends PreferenceFragmentCompat {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public GameSettingsFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
activity.createListGameSetting(ps, "CPUOverclock", R.string.settings_cpu_overclocking, R.array.settings_advanced_cpu_overclock_entries, R.array.settings_advanced_cpu_overclock_values);
|
||||
activity.createListGameSetting(ps,"CDROMReadSpeedup", R.string.settings_cdrom_read_speedup, R.array.settings_cdrom_read_speedup_entries, R.array.settings_cdrom_read_speedup_values);
|
||||
|
||||
activity.createListGameSetting(ps, "DisplayAspectRatio", R.string.settings_aspect_ratio, R.array.settings_display_aspect_ratio_names, R.array.settings_display_aspect_ratio_values);
|
||||
activity.createListGameSetting(ps, "DisplayCropMode", R.string.settings_crop_mode,R.array.settings_display_crop_mode_entries,R.array.settings_display_crop_mode_values);
|
||||
activity.createListGameSetting(ps,"GPUDownsampleMode", R.string.settings_downsample_mode, R.array.settings_downsample_mode_entries, R.array.settings_downsample_mode_values);
|
||||
activity.createBooleanGameSetting(ps, "DisplayLinearUpscaling", R.string.settings_linear_upscaling);
|
||||
activity.createBooleanGameSetting(ps,"DisplayIntegerUpscaling",R.string.settings_integer_upscaling);
|
||||
activity.createBooleanGameSetting(ps,"DisplayForce4_3For24Bit",R.string.settings_force_4_3_for_24bit);
|
||||
|
||||
activity.createListGameSetting(ps, "GPUResolutionScale", R.string.settings_gpu_resolution_scale, R.array.settings_gpu_resolution_scale_entries, R.array.settings_gpu_resolution_scale_values);
|
||||
activity.createListGameSetting(ps, "GPUMSAA", R.string.settings_msaa, R.array.settings_gpu_msaa_entries, R.array.settings_gpu_msaa_values);
|
||||
activity.createBooleanGameSetting(ps, "GPUTrueColor", R.string.settings_true_color);
|
||||
activity.createBooleanGameSetting(ps,"GPUScaledDithering",R.string.settings_scaled_dithering);
|
||||
activity.createListGameSetting(ps, "GPUTextureFilter", R.string.settings_texture_filtering, R.array.settings_gpu_texture_filter_names, R.array.settings_gpu_texture_filter_values);
|
||||
activity.createBooleanGameSetting(ps,"GPUForceNTSCTimings",R.string.settings_force_ntsc_timings);
|
||||
activity.createBooleanGameSetting(ps, "GPUWidescreenHack", R.string.settings_widescreen_hack);
|
||||
activity.createBooleanGameSetting(ps, "GPUPGXP", R.string.settings_pgxp_geometry_correction);
|
||||
activity.createBooleanGameSetting(ps, "GPUPGXPDepthBuffer", R.string.settings_pgxp_depth_buffer);
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ControllerSettingsFragment extends PreferenceFragmentCompat {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public ControllerSettingsFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
private void createInputProfileSetting(PreferenceScreen ps) {
|
||||
final GameSettingPreference pref = new GameSettingPreference(ps.getContext(), activity.mGameListEntry.getPath(), "InputProfileName", R.string.settings_input_profile);
|
||||
|
||||
final String[] inputProfileNames = AndroidHostInterface.getInstance().getInputProfileNames();
|
||||
pref.setEntries(inputProfileNames);
|
||||
pref.setEntryValues(inputProfileNames);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
|
||||
activity.createListGameSetting(ps, "Controller1Type", R.string.settings_controller_type, R.array.settings_controller_type_entries, R.array.settings_controller_type_values);
|
||||
activity.createListGameSetting(ps, "MemoryCard1Type", R.string.settings_memory_card_1_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values);
|
||||
activity.createListGameSetting(ps, "MemoryCard2Type", R.string.settings_memory_card_2_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values);
|
||||
createInputProfileSetting(ps);
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionFragment extends Fragment {
|
||||
private GamePropertiesActivity activity;
|
||||
private SettingsCollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
public SettingsCollectionFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_controller_mapping, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
adapter = new SettingsCollectionAdapter(activity, this);
|
||||
viewPager = view.findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||
switch (position)
|
||||
{
|
||||
case 0: tab.setText("Summary"); break;
|
||||
case 1: tab.setText("Game Settings"); break;
|
||||
case 2: tab.setText("Controller Settings"); break;
|
||||
}
|
||||
}).attach();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public SettingsCollectionAdapter(@NonNull GamePropertiesActivity activity, @NonNull Fragment fragment) {
|
||||
super(fragment);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position)
|
||||
{
|
||||
case 0: { // Summary
|
||||
ListFragment lf = new ListFragment();
|
||||
lf.setListAdapter(activity.getPropertyListAdapter());
|
||||
return lf;
|
||||
}
|
||||
|
||||
case 1: { // Game Settings
|
||||
return new GameSettingsFragment(activity);
|
||||
}
|
||||
|
||||
case 2: { // Controller Settings
|
||||
return new ControllerSettingsFragment(activity);
|
||||
}
|
||||
|
||||
// TODO: Memory Card Editor
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
|
||||
public class GameSettingPreference extends ListPreference {
|
||||
private String mGamePath;
|
||||
|
||||
/**
|
||||
* Creates a boolean game property preference.
|
||||
*/
|
||||
public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId) {
|
||||
super(context);
|
||||
mGamePath = gamePath;
|
||||
setPersistent(false);
|
||||
setTitle(titleId);
|
||||
setKey(settingKey);
|
||||
setIconSpaceReserved(false);
|
||||
setSummaryProvider(SimpleSummaryProvider.getInstance());
|
||||
|
||||
setEntries(R.array.settings_boolean_entries);
|
||||
setEntryValues(R.array.settings_boolean_values);
|
||||
|
||||
updateValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list game property preference.
|
||||
*/
|
||||
public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId, int entryArray, int entryValuesArray) {
|
||||
super(context);
|
||||
mGamePath = gamePath;
|
||||
setPersistent(false);
|
||||
setTitle(titleId);
|
||||
setKey(settingKey);
|
||||
setIconSpaceReserved(false);
|
||||
setSummaryProvider(SimpleSummaryProvider.getInstance());
|
||||
|
||||
setEntries(entryArray);
|
||||
setEntryValues(entryValuesArray);
|
||||
|
||||
updateValue();
|
||||
}
|
||||
|
||||
private void updateValue() {
|
||||
final String value = AndroidHostInterface.getInstance().getGameSettingValue(mGamePath, getKey());
|
||||
if (value == null)
|
||||
super.setValue("null");
|
||||
else
|
||||
super.setValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(String value) {
|
||||
super.setValue(value);
|
||||
if (value.equals("null"))
|
||||
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), null);
|
||||
else
|
||||
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntries(CharSequence[] entries) {
|
||||
final int length = (entries != null) ? entries.length : 0;
|
||||
CharSequence[] newEntries = new CharSequence[length + 1];
|
||||
newEntries[0] = getContext().getString(R.string.game_properties_preference_use_global_setting);
|
||||
if (entries != null)
|
||||
System.arraycopy(entries, 0, newEntries, 1, entries.length);
|
||||
super.setEntries(newEntries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntryValues(CharSequence[] entryValues) {
|
||||
final int length = (entryValues != null) ? entryValues.length : 0;
|
||||
CharSequence[] newEntryValues = new CharSequence[length + 1];
|
||||
newEntryValues[0] = "null";
|
||||
if (entryValues != null)
|
||||
System.arraycopy(entryValues, 0, newEntryValues, 1, length);
|
||||
super.setEntryValues(newEntryValues);
|
||||
}
|
||||
}
|
|
@ -151,6 +151,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
} else if (id == R.id.game_list_entry_menu_resume_game) {
|
||||
startEmulation(mGameList.getEntry(position).getPath(), true);
|
||||
return true;
|
||||
} else if (id == R.id.game_list_entry_menu_properties) {
|
||||
openGameProperties(mGameList.getEntry(position).getPath());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -356,6 +359,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean openGameProperties(String path) {
|
||||
Intent intent = new Intent(this, GamePropertiesActivity.class);
|
||||
intent.putExtra("path", path);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean startEmulation(String bootPath, boolean resumeState) {
|
||||
if (!doBIOSCheck())
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class PropertyListAdapter extends BaseAdapter {
|
||||
private class Item {
|
||||
public String key;
|
||||
public String title;
|
||||
public String value;
|
||||
|
||||
Item(String key, String title, String value) {
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private ArrayList<Item> mItems = new ArrayList<>();
|
||||
|
||||
public PropertyListAdapter(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public Item getItemByKey(String key) {
|
||||
for (Item it : mItems) {
|
||||
if (it.key.equals(key))
|
||||
return it;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int addItem(String key, String title, String value) {
|
||||
if (getItemByKey(key) != null)
|
||||
return -1;
|
||||
|
||||
Item it = new Item(key, title, value);
|
||||
int position = mItems.size();
|
||||
mItems.add(it);
|
||||
return position;
|
||||
}
|
||||
|
||||
public boolean removeItem(Item item) {
|
||||
return mItems.remove(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mItems.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mItems.get(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.layout_game_property_entry, parent, false);
|
||||
}
|
||||
|
||||
TextView titleView = (TextView)convertView.findViewById(R.id.property_title);
|
||||
TextView valueView = (TextView)convertView.findViewById(R.id.property_value);
|
||||
Item prop = mItems.get(position);
|
||||
titleView.setText(prop.title);
|
||||
valueView.setText(prop.value);
|
||||
return convertView;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/property_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="TextView"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/property_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_below="@id/property_title"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:text="TextView" />
|
||||
</RelativeLayout>
|
|
@ -7,4 +7,7 @@
|
|||
<item
|
||||
android:id="@+id/game_list_entry_menu_resume_game"
|
||||
android:title="@string/menu_game_list_entry_resume_game" />
|
||||
<item
|
||||
android:id="@+id/game_list_entry_menu_properties"
|
||||
android:title="Game Properties" />
|
||||
</menu>
|
|
@ -460,4 +460,12 @@
|
|||
<item>Box</item>
|
||||
<item>Adaptive</item>
|
||||
</string-array>
|
||||
<string-array name="settings_boolean_entries">
|
||||
<item>Disabled</item>
|
||||
<item>Enabled</item>
|
||||
</string-array>
|
||||
<string-array name="settings_boolean_values">
|
||||
<item>false</item>
|
||||
<item>true</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -183,4 +183,7 @@
|
|||
<string name="settings_disable_all_enhancements">Disable All Enhancements</string>
|
||||
<string name="settings_summary_disable_all_enhancements">Temporarily disables all enhancements, which can be useful when debugging issues.</string>
|
||||
<string name="settings_downsample_mode">Downsampling</string>
|
||||
<string name="activity_game_properties">Game Properties</string>
|
||||
<string name="game_properties_preference_use_global_setting">Use Global Setting</string>
|
||||
<string name="settings_input_profile">Input Profile</string>
|
||||
</resources>
|
||||
|
|
|
@ -86,7 +86,7 @@ public:
|
|||
virtual void DestroySystem() override;
|
||||
|
||||
/// Returns the game list.
|
||||
ALWAYS_INLINE const GameList* GetGameList() const { return m_game_list.get(); }
|
||||
ALWAYS_INLINE GameList* GetGameList() const { return m_game_list.get(); }
|
||||
|
||||
/// Returns a list of all available hotkeys.
|
||||
ALWAYS_INLINE const HotkeyInfoList& GetHotkeyInfoList() const { return m_hotkeys; }
|
||||
|
|
Loading…
Reference in New Issue