diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml index 7bfef59df..d5d35ec44 100644 --- a/android/.idea/misc.xml +++ b/android/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 1a0b3a70c..21c1a92cd 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -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(m_controller_interface.get()); - if (ci) - ci->HandleButtonEvent(controller_index, button_index, pressed); - }); + RunOnEmulationThread([this, controller_index, button_index, pressed]() { + AndroidControllerInterface* ci = static_cast(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(m_controller_interface.get()); - if (ci) - ci->HandleAxisEvent(controller_index, axis_index, value); - }); + RunOnEmulationThread([this, controller_index, axis_index, value]() { + AndroidControllerInterface* ci = static_cast(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(env->NewGlobalRef(string_class))) == nullptr || (host_interface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr || (s_AndroidHostInterface_class = static_cast(env->NewGlobalRef(host_interface_class))) == nullptr || (patch_code_class = env->FindClass("com/github/stenzek/duckstation/PatchCode")) == nullptr || - (s_PatchCode_class = static_cast(env->NewGlobalRef(patch_code_class))) == nullptr) + (s_PatchCode_class = static_cast(env->NewGlobalRef(patch_code_class))) == nullptr || + (game_list_entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry")) == nullptr || + (s_GameListEntry_class = static_cast(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, "", "(ILjava/lang/String;Z)V")) == nullptr) + (s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "", "(ILjava/lang/String;Z)V")) == nullptr || + (s_GameListEntry_constructor = env->GetMethodID( + s_GameListEntry_class, "", + "(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 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 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(region)]; } +static jobject CreateGameListEntry(JNIEnv* env, AndroidHostInterface* hi, const GameListEntry& entry) +{ + const Timestamp modified_ts( + Timestamp::FromUnixTimestamp(static_cast(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, "", - "(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(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 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 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, "", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + env->GetMethodID(entry_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); Assert(entry_constructor != nullptr); AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0aea123cf..6239b5e30 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,6 +43,15 @@ android:name="android.support.PARENT_ACTIVITY" android:value="com.github.stenzek.duckstation.MainActivity" /> + + + 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; + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameSettingPreference.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameSettingPreference.java new file mode 100644 index 000000000..d86e8af41 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameSettingPreference.java @@ -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); + } +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java index 943f593b7..335045e12 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java @@ -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; diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/PropertyListAdapter.java b/android/app/src/main/java/com/github/stenzek/duckstation/PropertyListAdapter.java new file mode 100644 index 000000000..9d2a73678 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/PropertyListAdapter.java @@ -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 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; + } +} diff --git a/android/app/src/main/res/layout/layout_game_property_entry.xml b/android/app/src/main/res/layout/layout_game_property_entry.xml new file mode 100644 index 000000000..7a40786e4 --- /dev/null +++ b/android/app/src/main/res/layout/layout_game_property_entry.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/menu/menu_game_list_entry.xml b/android/app/src/main/res/menu/menu_game_list_entry.xml index a71da250a..317c66cf5 100644 --- a/android/app/src/main/res/menu/menu_game_list_entry.xml +++ b/android/app/src/main/res/menu/menu_game_list_entry.xml @@ -7,4 +7,7 @@ + \ No newline at end of file diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index 6fdd9cb48..4da2b1ae1 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -460,4 +460,12 @@ Box Adaptive + + Disabled + Enabled + + + false + true + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index d303900c1..8bf98b20b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -183,4 +183,7 @@ Disable All Enhancements Temporarily disables all enhancements, which can be useful when debugging issues. Downsampling + Game Properties + Use Global Setting + Input Profile diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 031085761..89be710a6 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -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; }