From c9b1e22cb1b3a825102f5763e184461f8d94e84c Mon Sep 17 00:00:00 2001 From: David Griswold Date: Tue, 10 Mar 2026 15:21:07 +0300 Subject: [PATCH 1/3] refactor android settings - generic abstract type, setting enums are no longer mutable, actual values live in a hash map in the Settings class accessed through settings.get(key) and settings.set(key, value) and are no longer static; instead, the EmulationActivity gets a separate Settings instance from the SettingsActivity, to avoid settings crashing into each other during edit. SettingSections no longer exist internally, only in the file. Settings contain both a globalSettings hashmap and a perGameOverrides hashmap, used when appropriate. --- .../citra_emu/activities/EmulationActivity.kt | 35 +- .../citra_emu/display/ScreenAdjustmentUtil.kt | 28 +- .../citra_emu/display/SecondaryDisplay.kt | 6 +- .../features/hotkeys/HotkeyUtility.kt | 5 +- .../settings/model/AbstractBooleanSetting.kt | 9 - .../settings/model/AbstractFloatSetting.kt | 9 - .../settings/model/AbstractIntSetting.kt | 9 - .../settings/model/AbstractListSetting.kt | 9 - .../settings/model/AbstractSetting.kt | 15 +- .../settings/model/AbstractShortSetting.kt | 9 - .../settings/model/AbstractStringSetting.kt | 9 - .../features/settings/model/BooleanSetting.kt | 17 +- .../features/settings/model/FloatSetting.kt | 25 +- .../features/settings/model/IntListSetting.kt | 27 +- .../features/settings/model/IntSetting.kt | 17 +- .../settings/model/ScaledFloatSetting.kt | 43 -- .../features/settings/model/SettingSection.kt | 38 -- .../features/settings/model/Settings.kt | 156 +++-- .../settings/model/SettingsViewModel.kt | 4 +- .../features/settings/model/StringSetting.kt | 8 +- .../settings/model/view/DateTimeSetting.kt | 36 +- .../settings/model/view/HeaderSetting.kt | 2 +- .../model/view/InputBindingSetting.kt | 6 +- .../settings/model/view/MultiChoiceSetting.kt | 28 +- .../settings/model/view/SettingsItem.kt | 4 +- .../model/view/SingleChoiceSetting.kt | 56 +- .../settings/model/view/SliderSetting.kt | 83 ++- .../settings/model/view/StringInputSetting.kt | 30 +- .../model/view/StringSingleChoiceSetting.kt | 47 +- .../settings/model/view/SwitchSetting.kt | 29 +- .../features/settings/ui/SettingsActivity.kt | 24 +- .../settings/ui/SettingsActivityPresenter.kt | 33 +- .../features/settings/ui/SettingsAdapter.kt | 141 ++--- .../features/settings/ui/SettingsFragment.kt | 15 +- .../settings/ui/SettingsFragmentPresenter.kt | 542 ++++++++---------- .../settings/ui/SettingsFragmentView.kt | 10 +- .../ui/viewholder/DateTimeViewHolder.kt | 7 +- .../ui/viewholder/HeaderViewHolder.kt | 6 +- .../InputBindingSettingViewHolder.kt | 4 +- .../ui/viewholder/MultiChoiceViewHolder.kt | 5 +- .../ui/viewholder/RunnableViewHolder.kt | 4 +- .../ui/viewholder/SettingViewHolder.kt | 7 +- .../ui/viewholder/SingleChoiceViewHolder.kt | 4 +- .../ui/viewholder/SliderViewHolder.kt | 14 +- .../ui/viewholder/StringInputViewHolder.kt | 11 +- .../ui/viewholder/SubmenuViewHolder.kt | 8 +- .../ui/viewholder/SwitchSettingViewHolder.kt | 4 +- .../features/settings/utils/SettingsFile.kt | 302 +++++----- .../citra_emu/fragments/EmulationFragment.kt | 45 +- .../citra/citra_emu/overlay/InputOverlay.kt | 9 +- .../citra/citra_emu/ui/main/MainActivity.kt | 6 +- .../org/citra/citra_emu/utils/TurboHelper.kt | 13 +- .../citra_emu/viewmodel/EmulationViewModel.kt | 7 +- 53 files changed, 885 insertions(+), 1135 deletions(-) delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractShortSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index 5f5215876..c9d6c9e3f 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -38,7 +38,7 @@ import org.citra.citra_emu.display.SecondaryDisplay import org.citra.citra_emu.features.hotkeys.HotkeyUtility import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.IntSetting -import org.citra.citra_emu.features.settings.model.SettingsViewModel +import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.fragments.EmulationFragment import org.citra.citra_emu.fragments.MessageDialogFragment @@ -52,14 +52,13 @@ import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.RefreshRateUtil import org.citra.citra_emu.utils.ThemeUtil import org.citra.citra_emu.viewmodel.EmulationViewModel +import org.citra.citra_emu.features.settings.utils.SettingsFile class EmulationActivity : AppCompatActivity() { private val preferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) var isActivityRecreated = false - private val emulationViewModel: EmulationViewModel by viewModels() - val settingsViewModel: SettingsViewModel by viewModels() - + val emulationViewModel: EmulationViewModel by viewModels() private lateinit var binding: ActivityEmulationBinding private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil private lateinit var hotkeyUtility: HotkeyUtility @@ -88,14 +87,22 @@ class EmulationActivity : AppCompatActivity() { RefreshRateUtil.enforceRefreshRate(this, sixtyHz = true) ThemeUtil.setTheme(this) - settingsViewModel.settings.loadSettings() + + super.onCreate(savedInstanceState) - secondaryDisplay = SecondaryDisplay(this) + + // load global settings if for some reason they aren't (should be loaded in MainActivity) + if (Settings.settings.getAllGlobal().isEmpty()) { + SettingsFile.loadSettings(Settings.settings) + } + // once per-game settings are added, load them here! + + secondaryDisplay = SecondaryDisplay(this, Settings.settings) secondaryDisplay.updateDisplay() binding = ActivityEmulationBinding.inflate(layoutInflater) - screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings) - hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this) + screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, Settings.settings) + hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this, Settings.settings) setContentView(binding.root) val navHostFragment = @@ -142,7 +149,7 @@ class EmulationActivity : AppCompatActivity() { override fun onResume() { super.onResume() enableFullscreenImmersive() - applyOrientationSettings() // Check for orientation settings changes on runtime + applyOrientationSettings() } override fun onStop() { @@ -179,6 +186,8 @@ class EmulationActivity : AppCompatActivity() { secondaryDisplay.releasePresentation() secondaryDisplay.releaseVD() + Settings.settings.removePerGameSettings() + super.onDestroy() } @@ -229,11 +238,11 @@ class EmulationActivity : AppCompatActivity() { ).show() } - private fun enableFullscreenImmersive() { + fun enableFullscreenImmersive() { val attributes = window.attributes attributes.layoutInDisplayCutoutMode = - if (BooleanSetting.EXPAND_TO_CUTOUT_AREA.boolean) { + if (Settings.settings.get(BooleanSetting.EXPAND_TO_CUTOUT_AREA)) { WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } else { WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER @@ -250,8 +259,8 @@ class EmulationActivity : AppCompatActivity() { } } - private fun applyOrientationSettings() { - val orientationOption = IntSetting.ORIENTATION_OPTION.int + fun applyOrientationSettings() { + val orientationOption = Settings.settings.get(IntSetting.ORIENTATION_OPTION) screenAdjustmentUtil.changeActivityOrientation(orientationOption) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt index e63960fa8..d6b477e4b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt @@ -29,13 +29,13 @@ class ScreenAdjustmentUtil( isEnabled, windowManager.defaultDisplay.rotation ) - BooleanSetting.SWAP_SCREEN.boolean = isEnabled - settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG) + settings.update(BooleanSetting.SWAP_SCREEN, isEnabled) + SettingsFile.saveSetting(BooleanSetting.SWAP_SCREEN, settings) } fun cycleLayouts() { - val landscapeLayoutsToCycle = IntListSetting.LAYOUTS_TO_CYCLE.list; + val landscapeLayoutsToCycle = settings.get(IntListSetting.LAYOUTS_TO_CYCLE) val landscapeValues = if (landscapeLayoutsToCycle.isNotEmpty()) landscapeLayoutsToCycle.toIntArray() @@ -45,12 +45,12 @@ class ScreenAdjustmentUtil( val portraitValues = context.resources.getIntArray(R.array.portraitValues) if (NativeLibrary.isPortraitMode) { - val currentLayout = IntSetting.PORTRAIT_SCREEN_LAYOUT.int + val currentLayout = settings.get(IntSetting.PORTRAIT_SCREEN_LAYOUT) val pos = portraitValues.indexOf(currentLayout) val layoutOption = portraitValues[(pos + 1) % portraitValues.size] changePortraitOrientation(layoutOption) } else { - val currentLayout = IntSetting.SCREEN_LAYOUT.int + val currentLayout = settings.get(IntSetting.SCREEN_LAYOUT) val pos = landscapeValues.indexOf(currentLayout) val layoutOption = landscapeValues[(pos + 1) % landscapeValues.size] changeScreenOrientation(layoutOption) @@ -58,30 +58,30 @@ class ScreenAdjustmentUtil( } fun changePortraitOrientation(layoutOption: Int) { - IntSetting.PORTRAIT_SCREEN_LAYOUT.int = layoutOption - settings.saveSetting(IntSetting.PORTRAIT_SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG) + settings.update(IntSetting.PORTRAIT_SCREEN_LAYOUT, layoutOption) + SettingsFile.saveSetting(IntSetting.PORTRAIT_SCREEN_LAYOUT, settings) NativeLibrary.reloadSettings() NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode) } fun changeScreenOrientation(layoutOption: Int) { - IntSetting.SCREEN_LAYOUT.int = layoutOption - settings.saveSetting(IntSetting.SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG) + settings.update(IntSetting.SCREEN_LAYOUT, layoutOption) + SettingsFile.saveSetting(IntSetting.SCREEN_LAYOUT, settings) NativeLibrary.reloadSettings() NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode) } fun changeActivityOrientation(orientationOption: Int) { val activity = context as? Activity ?: return - IntSetting.ORIENTATION_OPTION.int = orientationOption - settings.saveSetting(IntSetting.ORIENTATION_OPTION, SettingsFile.FILE_NAME_CONFIG) + settings.update(IntSetting.ORIENTATION_OPTION, orientationOption) + SettingsFile.saveSetting(IntSetting.ORIENTATION_OPTION, settings) activity.requestedOrientation = orientationOption } fun toggleScreenUpright() { - val uprightBoolean = BooleanSetting.UPRIGHT_SCREEN.boolean - BooleanSetting.UPRIGHT_SCREEN.boolean = !uprightBoolean - settings.saveSetting(BooleanSetting.UPRIGHT_SCREEN, SettingsFile.FILE_NAME_CONFIG) + val uprightBoolean = settings.get(BooleanSetting.UPRIGHT_SCREEN) + settings.update(BooleanSetting.UPRIGHT_SCREEN, !uprightBoolean) + SettingsFile.saveSetting(BooleanSetting.UPRIGHT_SCREEN, settings) NativeLibrary.reloadSettings() NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index d09daab41..2fec60d2a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt @@ -16,8 +16,9 @@ import android.view.SurfaceView import android.view.WindowManager import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.features.settings.model.Settings -class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { +class SecondaryDisplay(val context: Context, private val settings: Settings) : DisplayManager.DisplayListener { private var pres: SecondaryDisplayPresentation? = null private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val vd: VirtualDisplay @@ -70,8 +71,7 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { // decide if we are going to the external display or the internal one var display = getExternalDisplay(context) - if (display == null || - IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) { + if (display == null || settings.get(IntSetting.SECONDARY_DISPLAY_LAYOUT) == SecondaryDisplayLayout.NONE.int) { display = vd.display } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt index d01d5f769..d57ff5e8b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt @@ -19,7 +19,8 @@ import org.citra.citra_emu.features.settings.model.Settings class HotkeyUtility( private val screenAdjustmentUtil: ScreenAdjustmentUtil, - private val context: Context + private val context: Context, + private val settings: Settings ) { private val hotkeyButtons = Hotkey.entries.map { it.button } @@ -112,7 +113,7 @@ class HotkeyUtility( Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts() Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame() Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume() - Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true) + Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true, settings) Hotkey.QUICKSAVE.button -> { NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT) Toast.makeText( diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt deleted file mode 100644 index e60b1ca36..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.features.settings.model - -interface AbstractBooleanSetting : AbstractSetting { - var boolean: Boolean -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt deleted file mode 100644 index c3b2c8e2e..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.features.settings.model - -interface AbstractFloatSetting : AbstractSetting { - var float: Float -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt deleted file mode 100644 index 7c3660854..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.features.settings.model - -interface AbstractIntSetting : AbstractSetting { - var int: Int -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt deleted file mode 100644 index d89db48af..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.features.settings.model - -interface AbstractListSetting : AbstractSetting { - var list: List -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt index 54af79efb..1d9e2f456 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt @@ -1,13 +1,16 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.model -interface AbstractSetting { - val key: String? - val section: String? +interface AbstractSetting { + val key: String + val section: String + val defaultValue: T val isRuntimeEditable: Boolean - val valueAsString: String - val defaultValue: Any + + fun valueToString(value: T): String = value.toString() + + fun valueFromString(string: String): T? } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractShortSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractShortSetting.kt deleted file mode 100644 index 9fafc5410..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractShortSetting.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.features.settings.model - -interface AbstractShortSetting : AbstractSetting { - var short: Short -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt deleted file mode 100644 index 41ecc5038..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.features.settings.model - -interface AbstractStringSetting : AbstractSetting { - var string: String -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt index e12d87544..5c075bc3f 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt @@ -10,7 +10,7 @@ enum class BooleanSetting( override val key: String, override val section: String, override val defaultValue: Boolean -) : AbstractBooleanSetting { +) : AbstractSetting { EXPAND_TO_CUTOUT_AREA(SettingKeys.expand_to_cutout_area(), Settings.SECTION_LAYOUT, false), SPIRV_SHADER_GEN(SettingKeys.spirv_shader_gen(), Settings.SECTION_RENDERER, true), ASYNC_SHADERS(SettingKeys.async_shader_compilation(), Settings.SECTION_RENDERER, false), @@ -58,10 +58,13 @@ enum class BooleanSetting( APPLY_REGION_FREE_PATCH(SettingKeys.apply_region_free_patch(), Settings.SECTION_SYSTEM, true), USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false); - override var boolean: Boolean = defaultValue - - override val valueAsString: String - get() = boolean.toString() + override fun valueFromString(string: String): Boolean? { + return when (string.trim().lowercase()) { + "1", "true" -> true + "0", "false" -> false + else -> null + } + } override val isRuntimeEditable: Boolean get() { @@ -98,7 +101,5 @@ enum class BooleanSetting( fun from(key: String): BooleanSetting? = BooleanSetting.values().firstOrNull { it.key == key } - - fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue } - } + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt index c192677bd..c221a28c4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt @@ -9,18 +9,25 @@ import org.citra.citra_emu.features.settings.SettingKeys enum class FloatSetting( override val key: String, override val section: String, - override val defaultValue: Float -) : AbstractFloatSetting { + override val defaultValue: Float, + val scale:Int = 1 +) : AbstractSetting { LARGE_SCREEN_PROPORTION(SettingKeys.large_screen_proportion(),Settings.SECTION_LAYOUT,2.25f), SECOND_SCREEN_OPACITY(SettingKeys.custom_second_layer_opacity(), Settings.SECTION_RENDERER, 100f), - BACKGROUND_RED(SettingKeys.bg_red(), Settings.SECTION_RENDERER, 0f), - BACKGROUND_BLUE(SettingKeys.bg_blue(), Settings.SECTION_RENDERER, 0f), - BACKGROUND_GREEN(SettingKeys.bg_green(), Settings.SECTION_RENDERER, 0f); + BACKGROUND_RED(SettingKeys.bg_red(), Settings.SECTION_RENDERER, 0f, 255), + BACKGROUND_BLUE(SettingKeys.bg_blue(), Settings.SECTION_RENDERER, 0f, 255), + BACKGROUND_GREEN(SettingKeys.bg_green(), Settings.SECTION_RENDERER, 0f, 255), + AUDIO_VOLUME(SettingKeys.volume(), Settings.SECTION_AUDIO, 100f, 100); - override var float: Float = defaultValue + // valueFromString reads raw setting from file, scales up for UI + override fun valueFromString(string: String): Float? { + return string.toFloatOrNull()?.times(scale) + } - override val valueAsString: String - get() = float.toString() + // valueToString scales back down to raw for file + override fun valueToString(value: Float): String { + return (value / scale).toString() + } override val isRuntimeEditable: Boolean get() { @@ -36,7 +43,5 @@ enum class FloatSetting( private val NOT_RUNTIME_EDITABLE = emptyList() fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key } - - fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt index 0de51acce..fbffc735c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt @@ -9,28 +9,17 @@ enum class IntListSetting( override val section: String, override val defaultValue: List, val canBeEmpty: Boolean = true -) : AbstractListSetting { +) : AbstractSetting> { LAYOUTS_TO_CYCLE("layouts_to_cycle", Settings.SECTION_LAYOUT, listOf(0, 1, 2, 3, 4, 5), canBeEmpty = false); - private var backingList: List = defaultValue - private var lastValidList : List = defaultValue - - override var list: List - get() = backingList - set(value) { - if (!canBeEmpty && value.isEmpty()) { - backingList = lastValidList - } else { - backingList = value - lastValidList = value - } - } - - override val valueAsString: String - get() = list.joinToString() - + override fun valueToString(value: List): String = value.joinToString() + override fun valueFromString(string: String): List? { + return string.split(",") + .mapNotNull { it.trim().toIntOrNull() } + .takeIf { canBeEmpty || it.isNotEmpty() } + } override val isRuntimeEditable: Boolean get() { for (setting in NOT_RUNTIME_EDITABLE) { @@ -46,7 +35,5 @@ enum class IntListSetting( fun from(key: String): IntListSetting? = values().firstOrNull { it.key == key } - - fun clear() = values().forEach { it.list = it.defaultValue } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt index 2c8cbf2a1..364a3d1db 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -10,7 +10,7 @@ enum class IntSetting( override val key: String, override val section: String, override val defaultValue: Int -) : AbstractIntSetting { +) : AbstractSetting { FRAME_LIMIT(SettingKeys.frame_limit(), Settings.SECTION_RENDERER, 100), EMULATED_REGION(SettingKeys.region_value(), Settings.SECTION_SYSTEM, -1), INIT_CLOCK(SettingKeys.init_clock(), Settings.SECTION_SYSTEM, 0), @@ -50,7 +50,6 @@ enum class IntSetting( CPU_CLOCK_SPEED(SettingKeys.cpu_clock_percentage(), Settings.SECTION_CORE, 100), TEXTURE_FILTER(SettingKeys.texture_filter(), Settings.SECTION_RENDERER, 0), TEXTURE_SAMPLING(SettingKeys.texture_sampling(), Settings.SECTION_RENDERER, 0), - USE_FRAME_LIMIT(SettingKeys.use_frame_limit(), Settings.SECTION_RENDERER, 1), DELAY_RENDER_THREAD_US(SettingKeys.delay_game_render_thread_us(), Settings.SECTION_RENDERER, 0), ORIENTATION_OPTION(SettingKeys.screen_orientation(), Settings.SECTION_LAYOUT, 2), TURBO_LIMIT(SettingKeys.turbo_limit(), Settings.SECTION_CORE, 200), @@ -58,10 +57,15 @@ enum class IntSetting( RENDER_3D_WHICH_DISPLAY(SettingKeys.render_3d_which_display(),Settings.SECTION_RENDERER,0), ASPECT_RATIO(SettingKeys.aspect_ratio(), Settings.SECTION_LAYOUT, 0); - override var int: Int = defaultValue - override val valueAsString: String - get() = int.toString() + override fun valueFromString(string: String): Int? { + return string.toIntOrNull() ?: when (string.trim().lowercase()) { + "true" -> 1 + "false" -> 0 + else -> null + } + } + override val isRuntimeEditable: Boolean get() { @@ -83,6 +87,5 @@ enum class IntSetting( fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key } - fun clear() = IntSetting.values().forEach { it.int = it.defaultValue } } -} +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt deleted file mode 100644 index a37452f58..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.features.settings.model - -import org.citra.citra_emu.features.settings.SettingKeys - -enum class ScaledFloatSetting( - override val key: String, - override val section: String, - override val defaultValue: Float, - val scale: Int -) : AbstractFloatSetting { - AUDIO_VOLUME(SettingKeys.volume(), Settings.SECTION_AUDIO, 1.0f, 100); - - override var float: Float = defaultValue - get() = field * scale - set(value) { - field = value / scale - } - - override val valueAsString: String get() = (float / scale).toString() - - override val isRuntimeEditable: Boolean - get() { - for (setting in NOT_RUNTIME_EDITABLE) { - if (setting == this) { - return false - } - } - return true - } - - companion object { - private val NOT_RUNTIME_EDITABLE = emptyList() - - fun from(key: String): ScaledFloatSetting? = - ScaledFloatSetting.values().firstOrNull { it.key == key } - - fun clear() = ScaledFloatSetting.values().forEach { it.float = it.defaultValue * it.scale } - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt deleted file mode 100644 index 02c5fa2d5..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.features.settings.model - -/** - * A semantically-related group of Settings objects. These Settings are - * internally stored as a HashMap. - */ -class SettingSection(val name: String) { - val settings = HashMap() - - /** - * Convenience method; inserts a value directly into the backing HashMap. - * - * @param setting The Setting to be inserted. - */ - fun putSetting(setting: AbstractSetting) { - settings[setting.key!!] = setting - } - - /** - * Convenience method; gets a value directly from the backing HashMap. - * - * @param key Used to retrieve the Setting. - * @return A Setting object (you should probably cast this before using) - */ - fun getSetting(key: String): AbstractSetting? { - return settings[key] - } - - fun mergeSection(settingSection: SettingSection) { - for (setting in settingSection.settings.values) { - putSetting(setting) - } - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index 547a53594..5ef881bfd 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -4,101 +4,94 @@ package org.citra.citra_emu.features.settings.model -import android.text.TextUtils -import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R -import org.citra.citra_emu.features.settings.ui.SettingsActivityView -import org.citra.citra_emu.features.settings.utils.SettingsFile -import java.util.TreeMap class Settings { - private var gameId: String? = null + private val globalValues = HashMap() + private val perGameOverrides = HashMap() - var isLoaded = false + var gameId: String? = null + + fun isPerGame(): Boolean = gameId != null && gameId != "" + + fun get(setting: AbstractSetting): T { + @Suppress("UNCHECKED_CAST") + return (perGameOverrides[setting.key] + ?: globalValues[setting.key] + ?: setting.defaultValue) as T + } + + fun getGlobal(setting: AbstractSetting): T { + @Suppress("UNCHECKED_CAST") + return (globalValues[setting.key] ?: setting.defaultValue) as T + } + + fun setGlobal(setting: AbstractSetting, value: T) { + globalValues[setting.key] = value as Any + } + + fun setOverride(setting: AbstractSetting, value: T) { + perGameOverrides[setting.key] = value as Any + } + + /** Sets the per-game or global setting based on whether this file has ANY per-game setting. + * This should be used, for example, by the Settings Activity + */ + fun set(setting: AbstractSetting, value: T) { + if (isPerGame()) setOverride(setting, value) else setGlobal(setting, value) + } /** - * A HashMap, SettingSection> that constructs a new SettingSection instead of returning null - * when getting a key not already in the map + * Updates an existing setting honoring whether it is *currently* global or local. This will + * be used by the Quick Menu */ - class SettingsSectionMap : HashMap() { - override operator fun get(key: String): SettingSection? { - if (!super.containsKey(key)) { - val section = SettingSection(key) - super.put(key, section) - return section - } - return super.get(key) + fun update(setting: AbstractSetting, value: T) { + if (hasOverride(setting)) setOverride(setting, value) else setGlobal(setting, value) + } + + /** Merge the globals from other into the current settings. Merge per-game if game id is the same. */ + fun mergeSettings(other: Settings) { + other.globalValues.forEach{ (key, value) -> + globalValues[key] = value + } + + if (gameId != other.gameId) return + + perGameOverrides.clear() + other.perGameOverrides.forEach{ (key, value) -> + perGameOverrides[key] = value } } - var sections: HashMap = SettingsSectionMap() - - fun getSection(sectionName: String): SettingSection? { - return sections[sectionName] + fun clearOverride(setting: AbstractSetting) { + perGameOverrides.remove(setting.key) } - val isEmpty: Boolean - get() = sections.isEmpty() - - fun loadSettings(view: SettingsActivityView? = null) { - sections = SettingsSectionMap() - loadCitraSettings(view) - if (!TextUtils.isEmpty(gameId)) { - loadCustomGameSettings(gameId!!, view) - } - isLoaded = true + fun hasOverride(setting: AbstractSetting<*>): Boolean { + return perGameOverrides.containsKey(setting.key) } - private fun loadCitraSettings(view: SettingsActivityView?) { - for ((fileName) in configFileSectionsMap) { - sections.putAll(SettingsFile.readFile(fileName, view)) - } + fun getAllOverrides(): Map = perGameOverrides.toMap() + + fun getAllGlobal(): Map = globalValues.toMap() + + fun clearAll() { + globalValues.clear() + perGameOverrides.clear() } - private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) { - // Custom game settings - mergeSections(SettingsFile.readCustomGameSettings(gameId, view)) + fun clearOverrides() { + perGameOverrides.clear() } - private fun mergeSections(updatedSections: HashMap) { - for ((key, updatedSection) in updatedSections) { - if (sections.containsKey(key)) { - val originalSection = sections[key] - originalSection!!.mergeSection(updatedSection!!) - } else { - sections[key] = updatedSection - } - } + fun removePerGameSettings() { + clearOverrides() + gameId = null } - fun loadSettings(gameId: String, view: SettingsActivityView) { - this.gameId = gameId - loadSettings(view) - } - - fun saveSettings(view: SettingsActivityView) { - if (TextUtils.isEmpty(gameId)) { - view.showToastMessage( - CitraApplication.appContext.getString(R.string.ini_saved), - false - ) - for ((fileName, sectionNames) in configFileSectionsMap.entries) { - val iniSections = TreeMap() - for (section in sectionNames) { - iniSections[section] = sections[section] - } - SettingsFile.saveFile(fileName, iniSections, view) - } - } else { - // TODO: Implement per game settings - } - } - - fun saveSetting(setting: AbstractSetting, filename: String) { - SettingsFile.saveFile(filename, setting) - } companion object { + const val SECTION_CORE = "Core" const val SECTION_SYSTEM = "System" const val SECTION_CAMERA = "Camera" @@ -234,20 +227,7 @@ class Settings { private val configFileSectionsMap: MutableMap> = HashMap() - init { - configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = - listOf( - SECTION_CORE, - SECTION_SYSTEM, - SECTION_CAMERA, - SECTION_CONTROLS, - SECTION_RENDERER, - SECTION_LAYOUT, - SECTION_STORAGE, - SECTION_UTILITY, - SECTION_AUDIO, - SECTION_DEBUG - ) - } + /** Stores the settings as a singleton available everywhere.*/ + val settings = Settings() } } \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt index 3f9b4ad1f..166c74d16 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -7,5 +7,7 @@ package org.citra.citra_emu.features.settings.model import androidx.lifecycle.ViewModel class SettingsViewModel : ViewModel() { + // the Settings Activity will always work with a local copy of settings while + // editing, to avoid issues with conflicting active/edited settings val settings = Settings() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt index fe476a1fa..100cbd7ae 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt @@ -10,7 +10,7 @@ enum class StringSetting( override val key: String, override val section: String, override val defaultValue: String -) : AbstractStringSetting { +) : AbstractSetting{ INIT_TIME(SettingKeys.init_time(), Settings.SECTION_SYSTEM, "946731601"), CAMERA_INNER_NAME(SettingKeys.camera_inner_name(), Settings.SECTION_CAMERA, "ndk"), CAMERA_INNER_CONFIG(SettingKeys.camera_inner_config(), Settings.SECTION_CAMERA, "_front"), @@ -19,10 +19,7 @@ enum class StringSetting( CAMERA_OUTER_RIGHT_NAME(SettingKeys.camera_outer_right_name(), Settings.SECTION_CAMERA, "ndk"), CAMERA_OUTER_RIGHT_CONFIG(SettingKeys.camera_outer_right_config(), Settings.SECTION_CAMERA, "_back"); - override var string: String = defaultValue - - override val valueAsString: String - get() = string + override fun valueFromString(string: String) = string override val isRuntimeEditable: Boolean get() { @@ -47,6 +44,5 @@ enum class StringSetting( fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key } - fun clear() = StringSetting.values().forEach { it.string = it.defaultValue } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt index f1752614e..90a9af41f 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt @@ -1,33 +1,41 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.model.view import org.citra.citra_emu.features.settings.model.AbstractSetting -import org.citra.citra_emu.features.settings.model.AbstractStringSetting +import org.citra.citra_emu.features.settings.model.Settings class DateTimeSetting( - setting: AbstractSetting?, + private val settings: Settings, + setting: AbstractSetting?, titleId: Int, descriptionId: Int, val key: String? = null, private val defaultValue: String? = null, - override var isEnabled: Boolean = true + override var isEnabled: Boolean = true, + private val getValue: (()->String)? = null, + private val setValue: ((String)-> Unit)? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_DATETIME_SETTING + @Suppress("UNCHECKED_CAST") val value: String - get() = if (setting != null) { - val setting = setting as AbstractStringSetting - setting.string - } else { - defaultValue!! - } + get() = getValue?.invoke() + ?: if (setting != null) { + settings.get(setting as AbstractSetting) + } else { + defaultValue!! + } - fun setSelectedValue(datetime: String): AbstractStringSetting { - val stringSetting = setting as AbstractStringSetting - stringSetting.string = datetime - return stringSetting + @Suppress("UNCHECKED_CAST") + fun setSelectedValue(datetime: String) { + if (setValue != null) { + setValue(datetime) + }else { + val stringSetting = setting as AbstractSetting + settings.set(stringSetting, datetime) + } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt index d2a50f648..a08f26d99 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index 6ec851db1..aa67dd15b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -17,11 +17,10 @@ import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.features.hotkeys.Hotkey import org.citra.citra_emu.features.settings.model.AbstractSetting -import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.Settings class InputBindingSetting( - val abstractSetting: AbstractSetting, + val abstractSetting: AbstractSetting<*>, titleId: Int ) : SettingsItem(abstractSetting, titleId, 0) { private val context: Context get() = CitraApplication.appContext @@ -161,11 +160,12 @@ class InputBindingSetting( /** * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. */ + @Suppress("UNCHECKED_CAST") fun removeOldMapping() { // Try remove all possible keys we wrote for this setting val oldKey = preferences.getString(reverseKey, "") if (oldKey != "") { - (setting as AbstractStringSetting).string = "" + //settings.set(setting as AbstractSetting,"") preferences.edit() .remove(abstractSetting.key) // Used for ui text .remove(oldKey + "_GuestOrientation") // Used for axis orientation diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt index d097696e0..1a4b95c93 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt @@ -5,26 +5,34 @@ package org.citra.citra_emu.features.settings.model.view import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.IntListSetting +import org.citra.citra_emu.features.settings.model.Settings + class MultiChoiceSetting( - setting: AbstractSetting?, + val settings: Settings, + setting: AbstractSetting>?, titleId: Int, descriptionId: Int, val choicesId: Int, val valuesId: Int, val key: String? = null, val defaultValue: List? = null, - override var isEnabled: Boolean = true + override var isEnabled: Boolean = true, + private val getValue: (()->List)? = null, + private val setValue: ((List)-> Unit)? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_MULTI_CHOICE val selectedValues: List get() { + if (getValue != null) { + @Suppress("UNCHECKED_CAST") + return getValue.invoke() + } if (setting == null) { return defaultValue!! } try { - val setting = setting as IntListSetting - return setting.list + return settings.get(setting as IntListSetting) }catch (_: ClassCastException) { } return defaultValue!! @@ -35,12 +43,14 @@ class MultiChoiceSetting( * initializes a new one and returns it, so it can be added to the Hashmap. * * @param selection New value of the int. - * @return the existing setting with the new value applied. */ - fun setSelectedValue(selection: List): IntListSetting { - val intSetting = setting as IntListSetting - intSetting.list = selection - return intSetting + fun setSelectedValue(selection: List) { + if (setValue != null) { + setValue(selection) + }else { + val intSetting = setting as IntListSetting + settings.set(intSetting, selection) + } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt index 066912dd9..6028a67fe 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt @@ -16,7 +16,7 @@ import org.citra.citra_emu.features.settings.model.AbstractSetting * file.) */ abstract class SettingsItem( - var setting: AbstractSetting?, + var setting: AbstractSetting<*>?, val nameId: Int, val descriptionId: Int ) { @@ -35,6 +35,8 @@ abstract class SettingsItem( return this.isEditable && this.isEnabled } + + companion object { const val TYPE_HEADER = 0 const val TYPE_SWITCH = 1 diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt index 9ba2fe8c5..708b4e1e0 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt @@ -1,62 +1,48 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.model.view -import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting -import org.citra.citra_emu.features.settings.model.AbstractShortSetting +import org.citra.citra_emu.features.settings.model.Settings class SingleChoiceSetting( - setting: AbstractSetting?, + val settings: Settings, + setting: AbstractSetting<*>?, titleId: Int, descriptionId: Int, val choicesId: Int, val valuesId: Int, val key: String? = null, val defaultValue: Int? = null, - override var isEnabled: Boolean = true + override var isEnabled: Boolean = true, + private val getValue: (()->Int)? = null, + private val setValue: ((Int)-> Unit)? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SINGLE_CHOICE val selectedValue: Int get() { - if (setting == null) { - return defaultValue!! + if (getValue != null) { + return getValue.invoke() } - - try { - val setting = setting as AbstractIntSetting - return setting.int - } catch (_: ClassCastException) { - } - - try { - val setting = setting as AbstractShortSetting - return setting.short.toInt() - } catch (_: ClassCastException) { - } - - return defaultValue!! + @Suppress("UNCHECKED_CAST") + val s = (setting as? AbstractSetting) ?: return defaultValue!! + return settings.get(s) } /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * + * Write a value to the backing int . * @param selection New value of the int. - * @return the existing setting with the new value applied. */ - fun setSelectedValue(selection: Int): AbstractIntSetting { - val intSetting = setting as AbstractIntSetting - intSetting.int = selection - return intSetting - } - - fun setSelectedValue(selection: Short): AbstractShortSetting { - val shortSetting = setting as AbstractShortSetting - shortSetting.short = selection - return shortSetting + fun setSelectedValue(selection: Int) { + if (setValue != null) { + setValue(selection) + }else { + @Suppress("UNCHECKED_CAST") + val backSetting = setting as AbstractSetting + settings.set(backSetting, selection) + } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt index 46ed42905..cbb653823 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt @@ -4,15 +4,16 @@ package org.citra.citra_emu.features.settings.model.view -import org.citra.citra_emu.features.settings.model.AbstractFloatSetting -import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.FloatSetting -import org.citra.citra_emu.features.settings.model.ScaledFloatSetting +import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.utils.Log +import kotlin.math.pow +import kotlin.math.roundToInt class SliderSetting( - setting: AbstractSetting?, + val settings: Settings, + setting: AbstractSetting<*>?, titleId: Int, descriptionId: Int, val min: Int, @@ -20,17 +21,28 @@ class SliderSetting( val units: String, val key: String? = null, val defaultValue: Float? = null, - override var isEnabled: Boolean = true + val rounding: Int = 2, + override var isEnabled: Boolean = true, + private val getValue: (()->Float)? = null, + private val setValue: ((Float)-> Unit)? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SLIDER val selectedFloat: Float get() { - val setting = setting ?: return defaultValue!!.toFloat() + if (getValue != null) return getValue.invoke() + val s = setting ?: return defaultValue!! + + val ret = when (s.defaultValue) { + is Int -> { + @Suppress("UNCHECKED_CAST") + settings.get(s as AbstractSetting).toFloat() + } + + is Float -> { + @Suppress("UNCHECKED_CAST") + settings.get(s as AbstractSetting) + } - val ret = when (setting) { - is AbstractIntSetting -> setting.int.toFloat() - is FloatSetting -> setting.float - is ScaledFloatSetting -> setting.float else -> { Log.error("[SliderSetting] Error casting setting type.") -1f @@ -38,16 +50,32 @@ class SliderSetting( } return ret.coerceIn(min.toFloat(), max.toFloat()) } - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return the existing setting with the new value applied. - */ - fun setSelectedValue(selection: Int): AbstractIntSetting { - val intSetting = setting as AbstractIntSetting - intSetting.int = selection + fun roundedFloat(value: Float): Float { + val factor = 10f.pow(rounding) + return (value * factor).roundToInt() / factor + } + + val valueAsString: String + get() = setting?.let { + when (it.defaultValue) { + is Int -> { + @Suppress("UNCHECKED_CAST") + settings.get(it as AbstractSetting).toString() + } + + is Float -> { + @Suppress("UNCHECKED_CAST") + roundedFloat(settings.get(it as AbstractSetting)).toString() + } + + else -> "" + } + } ?: defaultValue?.toString() ?: "" + + fun setSelectedValue(selection: Int): AbstractSetting { + @Suppress("UNCHECKED_CAST") + val intSetting = setting as AbstractSetting + settings.set(intSetting, selection) return intSetting } @@ -56,15 +84,14 @@ class SliderSetting( * initializes a new one and returns it, so it can be added to the Hashmap. * * @param selection New value of the float. - * @return the existing setting with the new value applied. */ - fun setSelectedValue(selection: Float): AbstractFloatSetting { - val floatSetting = setting as AbstractFloatSetting - if (floatSetting is ScaledFloatSetting) { - floatSetting.float = selection - } else { - floatSetting.float = selection + fun setSelectedValue(selection: Float) { + if (setValue != null) { + setValue(selection) + }else { + @Suppress("UNCHECKED_CAST") + val floatSetting = setting as AbstractSetting + settings.set(floatSetting, selection) } - return floatSetting } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt index f32c078a6..611851967 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt @@ -1,28 +1,40 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.model.view import org.citra.citra_emu.features.settings.model.AbstractSetting -import org.citra.citra_emu.features.settings.model.AbstractStringSetting +import org.citra.citra_emu.features.settings.model.Settings class StringInputSetting( - setting: AbstractSetting?, + val settings: Settings, + setting: AbstractSetting?, titleId: Int, descriptionId: Int, val defaultValue: String, val characterLimit: Int = 0, - override var isEnabled: Boolean = true + override var isEnabled: Boolean = true, + private val getValue: (()->String)? = null, + private val setValue: ((String)-> Unit)? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_STRING_INPUT val selectedValue: String - get() = setting?.valueAsString ?: defaultValue + @Suppress("UNCHECKED_CAST") + get() { + if (getValue != null) return getValue.invoke() + setting ?: return defaultValue + return settings.get(setting as AbstractSetting) + } - fun setSelectedValue(selection: String): AbstractStringSetting { - val stringSetting = setting as AbstractStringSetting - stringSetting.string = selection - return stringSetting + fun setSelectedValue(selection: String) { + if (setValue != null) { + setValue.invoke(selection) + }else { + @Suppress("UNCHECKED_CAST") + val stringSetting = setting as AbstractSetting + settings.set(stringSetting, selection) + } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt index 037a26ffc..f74140fd4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt @@ -1,22 +1,24 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.model.view import org.citra.citra_emu.features.settings.model.AbstractSetting -import org.citra.citra_emu.features.settings.model.AbstractShortSetting -import org.citra.citra_emu.features.settings.model.AbstractStringSetting +import org.citra.citra_emu.features.settings.model.Settings class StringSingleChoiceSetting( - setting: AbstractSetting?, + val settings: Settings, + setting: AbstractSetting?, titleId: Int, descriptionId: Int, val choices: Array, val values: Array?, val key: String? = null, private val defaultValue: String? = null, - override var isEnabled: Boolean = true + override var isEnabled: Boolean = true, + private val getValue: (()->String)? = null, + private val setValue: ((String)-> Unit)? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_STRING_SINGLE_CHOICE @@ -31,22 +33,12 @@ class StringSingleChoiceSetting( val selectedValue: String get() { + if (getValue != null) return getValue.invoke() if (setting == null) { return defaultValue!! } - - try { - val setting = setting as AbstractStringSetting - return setting.string - } catch (_: ClassCastException) { - } - - try { - val setting = setting as AbstractShortSetting - return setting.short.toString() - } catch (_: ClassCastException) { - } - return defaultValue!! + @Suppress("UNCHECKED_CAST") + return settings.get(setting as AbstractSetting) } val selectValueIndex: Int get() { @@ -64,17 +56,14 @@ class StringSingleChoiceSetting( * initializes a new one and returns it, so it can be added to the Hashmap. * * @param selection New value of the int. - * @return the existing setting with the new value applied. */ - fun setSelectedValue(selection: String): AbstractStringSetting { - val stringSetting = setting as AbstractStringSetting - stringSetting.string = selection - return stringSetting - } - - fun setSelectedValue(selection: Short): AbstractShortSetting { - val shortSetting = setting as AbstractShortSetting - shortSetting.short = selection - return shortSetting + fun setSelectedValue(selection: String) { + if (setValue != null) { + setValue(selection) + }else { + @Suppress("UNCHECKED_CAST") + val stringSetting = setting as AbstractSetting + settings.set(stringSetting, selection) + } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt index b8badfd06..1e63f0a97 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt @@ -4,38 +4,45 @@ package org.citra.citra_emu.features.settings.model.view -import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting -import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.Settings class SwitchSetting( - setting: AbstractBooleanSetting, + val settings: Settings, + setting: AbstractSetting?, titleId: Int, descriptionId: Int, val key: String? = null, val defaultValue: Boolean = false, - override var isEnabled: Boolean = true + override var isEnabled: Boolean = true, + private val getValue: (() -> Boolean)? = null, + private val setValue: ((Boolean) -> Unit)? = null ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SWITCH val isChecked: Boolean get() { + if (getValue != null) return getValue.invoke() if (setting == null) { return defaultValue } - val setting = setting as AbstractBooleanSetting - return setting.boolean + @Suppress("UNCHECKED_CAST") + val setting = setting as AbstractSetting + return settings.get(setting) } /** * Write a value to the backing boolean. * * @param checked Pretty self explanatory. - * @return the existing setting with the new value applied. */ - fun setChecked(checked: Boolean): AbstractBooleanSetting { - val setting = setting as AbstractBooleanSetting - setting.boolean = checked - return setting + fun setChecked(checked: Boolean) { + if (setValue != null) { + setValue(checked) + }else { + @Suppress("UNCHECKED_CAST") + val setting = setting as AbstractSetting + settings.set(setting, checked) + } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt index 064fa700e..7366406df 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt @@ -7,7 +7,6 @@ package org.citra.citra_emu.features.settings.ui import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.View import android.view.ViewGroup.MarginLayoutParams @@ -27,13 +26,8 @@ import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.databinding.ActivitySettingsBinding import java.io.IOException -import org.citra.citra_emu.features.settings.model.BooleanSetting -import org.citra.citra_emu.features.settings.model.FloatSetting -import org.citra.citra_emu.features.settings.model.IntSetting -import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.SettingsViewModel -import org.citra.citra_emu.features.settings.model.StringSetting import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.DirectoryInitialization @@ -42,12 +36,13 @@ import org.citra.citra_emu.utils.RefreshRateUtil import org.citra.citra_emu.utils.ThemeUtil class SettingsActivity : AppCompatActivity(), SettingsActivityView { - private val presenter = SettingsActivityPresenter(this) private lateinit var binding: ActivitySettingsBinding + val settingsViewModel: SettingsViewModel by viewModels() - private val settingsViewModel: SettingsViewModel by viewModels() + private val presenter by lazy { SettingsActivityPresenter(this, settingsViewModel) } + // the activity will work with the fresh Settings() object created and stored in the viewmodel override val settings: Settings get() = settingsViewModel.settings override fun onCreate(savedInstanceState: Bundle?) { @@ -63,9 +58,9 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { WindowCompat.setDecorFitsSystemWindows(window, false) val launcher = intent - val gameID = launcher.getStringExtra(ARG_GAME_ID) - val menuTag = launcher.getStringExtra(ARG_MENU_TAG) - presenter.onCreate(savedInstanceState, menuTag!!, gameID!!) + val gameID = launcher.getStringExtra(ARG_GAME_ID) ?: "" + val menuTag = launcher.getStringExtra(ARG_MENU_TAG) ?: "" + presenter.onCreate(savedInstanceState, menuTag, gameID) // Show "Back" button in the action bar for navigation setSupportActionBar(binding.toolbarSettings) @@ -211,13 +206,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { controllerKeys.forEach { editor.remove(it) } editor.apply() - // Reset the static memory representation of each setting - BooleanSetting.clear() - FloatSetting.clear() - ScaledFloatSetting.clear() - IntSetting.clear() - StringSetting.clear() - // Delete settings file because the user may have changed values that do not exist in the UI val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) if (!settingsFile.delete()) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt index 33aea46f9..1c2cbf144 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt @@ -12,6 +12,8 @@ import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.features.settings.model.SettingsViewModel +import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.DirectoryInitialization import org.citra.citra_emu.utils.FileUtil @@ -19,8 +21,8 @@ import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.PermissionsHandler import org.citra.citra_emu.utils.TurboHelper -class SettingsActivityPresenter(private val activityView: SettingsActivityView) { - val settings: Settings get() = activityView.settings +class SettingsActivityPresenter(private val activityView: SettingsActivityView, private val viewModel: SettingsViewModel) { + val settings: Settings get() = viewModel.settings private var shouldSave = false private lateinit var menuTag: String @@ -29,6 +31,9 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView) fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) { this.menuTag = menuTag this.gameId = gameId + // merge the active settings into the local settings activity instance + settings.mergeSettings(Settings.settings) + if (savedInstanceState != null) { shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE) } @@ -47,13 +52,6 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView) } private fun loadSettingsUI() { - if (!settings.isLoaded) { - if (!TextUtils.isEmpty(gameId)) { - settings.loadSettings(gameId, activityView) - } else { - settings.loadSettings(activityView) - } - } activityView.showSettingsFragment(menuTag, false, gameId) activityView.onSettingsFileLoaded() } @@ -72,7 +70,8 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView) val nomediaFileExists: Boolean try { dataDirTreeUri = PermissionsHandler.citraDirectory - dataDirDocument = DocumentFile.fromTreeUri(CitraApplication.appContext, dataDirTreeUri)!! + dataDirDocument = + DocumentFile.fromTreeUri(CitraApplication.appContext, dataDirTreeUri)!! nomediaFileDocument = dataDirDocument.findFile(".nomedia") nomediaFileExists = (nomediaFileDocument != null) } catch (e: Exception) { @@ -80,7 +79,7 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView) return } - if (BooleanSetting.ANDROID_HIDE_IMAGES.boolean) { + if (settings.get(BooleanSetting.ANDROID_HIDE_IMAGES)) { if (!nomediaFileExists) { Log.info("[SettingsActivity]: Attempting to create .nomedia in user data directory") FileUtil.createFile(dataDirTreeUri.toString(), ".nomedia") @@ -94,14 +93,18 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView) fun onStop(finishing: Boolean) { if (finishing && shouldSave) { Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...") - settings.saveSettings(activityView) - //added to ensure that layout changes take effect as soon as settings window closes + if (settings.isPerGame()) { + SettingsFile.saveCustomFile(settings,activityView) + }else{ + SettingsFile.saveGlobalFile(settings,activityView) + } + // merge the edited settings back into the active settings + Settings.settings.mergeSettings(settings) NativeLibrary.reloadSettings() NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode) updateAndroidImageVisibility() - TurboHelper.reloadTurbo(false) // TODO: Can this go somewhere else? -OS + TurboHelper.reloadTurbo(false, settings) // TODO: Can this go somewhere else? -OS } - NativeLibrary.reloadSettings() } fun onSettingChanged() { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index 43a1dcbbd..c124cf383 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -35,15 +35,8 @@ import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding import org.citra.citra_emu.databinding.ListItemSettingBinding import org.citra.citra_emu.databinding.ListItemSettingSwitchBinding import org.citra.citra_emu.databinding.ListItemSettingsHeaderBinding -import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting -import org.citra.citra_emu.features.settings.model.AbstractFloatSetting -import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting -import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.FloatSetting -import org.citra.citra_emu.features.settings.model.IntListSetting -import org.citra.citra_emu.features.settings.model.ScaledFloatSetting -import org.citra.citra_emu.features.settings.model.AbstractShortSetting import org.citra.citra_emu.features.settings.model.view.DateTimeSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem @@ -74,9 +67,9 @@ import java.text.SimpleDateFormat import kotlin.math.roundToInt class SettingsAdapter( - private val fragmentView: SettingsFragmentView, - public val context: Context -) : RecyclerView.Adapter(), DialogInterface.OnClickListener, + val fragmentView: SettingsFragmentView, + val context: Context +) : RecyclerView.Adapter?>(), DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener { private var settings: ArrayList? = null private var clickedItem: SettingsItem? = null @@ -94,7 +87,7 @@ class SettingsAdapter( clickedPosition = -1 } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder<*> { val inflater = LayoutInflater.from(parent.context) return when (viewType) { SettingsItem.TYPE_HEADER -> { @@ -144,7 +137,7 @@ class SettingsAdapter( } } - override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { + override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { getItem(position)?.let { holder.bind(it) } } @@ -226,8 +219,7 @@ class SettingsAdapter( } fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) { - val setting = item.setChecked(checked) - fragmentView.putSetting(setting) + item.setChecked(checked) fragmentView.onSettingChanged() // If statement is required otherwise the app will crash on activity recreate ex. theme settings @@ -338,8 +330,7 @@ class SettingsAdapter( fragmentView.onSettingChanged() } notifyItemChanged(clickedPosition) - val setting = item.setSelectedValue(rtcString) - fragmentView.putSetting(setting) + item.setSelectedValue(rtcString) fragmentView.loadSettingsList() clickedItem = null } @@ -359,7 +350,7 @@ class SettingsAdapter( val sliderBinding = DialogSliderBinding.inflate(inflater) textInputLayout = sliderBinding.textInput textSliderValue = sliderBinding.textValue - if (item.setting is FloatSetting) { + if (item.setting?.defaultValue is Float) { textSliderValue?.let { it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL it.setText(sliderProgress.toString()) @@ -393,9 +384,9 @@ class SettingsAdapter( }) addOnChangeListener { _: Slider, value: Float, _: Boolean -> - sliderProgress = (value * 100).roundToInt().toFloat() / 100f + sliderProgress = item.roundedFloat(value) var sliderString = sliderProgress.toString() - if (item.setting !is FloatSetting) { + if (item.setting?.defaultValue !is Float) { sliderString = sliderProgress.roundToInt().toString() if (textSliderValue?.text.toString() != sliderString) { textSliderValue?.setText(sliderString) @@ -418,16 +409,12 @@ class SettingsAdapter( .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, defaultCancelListener) .setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int -> - sliderBinding.slider?.value = when (item.setting) { - is ScaledFloatSetting -> { - val scaledSetting = item.setting as ScaledFloatSetting - scaledSetting.defaultValue * scaledSetting.scale - } - - is FloatSetting -> (item.setting as FloatSetting).defaultValue - else -> item.defaultValue ?: 0f + sliderBinding.slider.value = when (item.setting?.defaultValue) { + is Float -> item.setting!!.defaultValue as Float + is Int -> (item.setting!!.defaultValue as Int).toFloat() + else -> 0f } - onClick(dialog, which) + onClick(dialog, which) } .show() } @@ -478,26 +465,9 @@ class SettingsAdapter( is SingleChoiceSetting -> { val scSetting = clickedItem as? SingleChoiceSetting scSetting?.let { - val setting = when (it.setting) { - is AbstractIntSetting -> { - val value = getValueForSingleChoiceSelection(it, which) - if (it.selectedValue != value) { - fragmentView?.onSettingChanged() - } - it.setSelectedValue(value) - } - - is AbstractShortSetting -> { - val value = getValueForSingleChoiceSelection(it, which).toShort() - if (it.selectedValue.toShort() != value) { - fragmentView?.onSettingChanged() - } - it.setSelectedValue(value) - } - - else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!") - } - fragmentView?.putSetting(setting) + val value = getValueForSingleChoiceSelection(it, which) + if (it.selectedValue != value) fragmentView?.onSettingChanged() + it.setSelectedValue(value) fragmentView.loadSettingsList() closeDialog() } @@ -506,22 +476,9 @@ class SettingsAdapter( is StringSingleChoiceSetting -> { val scSetting = clickedItem as? StringSingleChoiceSetting scSetting?.let { - val setting = when (it.setting) { - is AbstractStringSetting -> { - val value = it.getValueAt(which) - if (it.selectedValue != value) fragmentView?.onSettingChanged() - it.setSelectedValue(value ?: "") - } - - is AbstractShortSetting -> { - if (it.selectValueIndex != which) fragmentView?.onSettingChanged() - it.setSelectedValue(it.getValueAt(which)?.toShort() ?: 1) - } - - else -> throw IllegalStateException("Unrecognized type used for StringSingleChoiceSetting!") - } - - fragmentView?.putSetting(setting) + val value = it.getValueAt(which) ?: "" + if (it.selectedValue != value) fragmentView?.onSettingChanged() + it.setSelectedValue(value) fragmentView.loadSettingsList() closeDialog() } @@ -530,21 +487,12 @@ class SettingsAdapter( is SliderSetting -> { val sliderSetting = clickedItem as? SliderSetting sliderSetting?.let { - val sliderval = (it.selectedFloat * 100).roundToInt().toFloat() / 100 - if (sliderval != sliderProgress) { - fragmentView?.onSettingChanged() - } - when (it.setting) { - is AbstractIntSetting -> { - val value = sliderProgress.roundToInt() - val setting = it.setSelectedValue(value) - fragmentView?.putSetting(setting) - } - - else -> { - val setting = it.setSelectedValue(sliderProgress) - fragmentView?.putSetting(setting) - } + val sliderval = it.roundedFloat(sliderProgress) + if (sliderval != it.selectedFloat) fragmentView.onSettingChanged() + val s = it.setting + when { + it.setting?.defaultValue is Int -> it.setSelectedValue(sliderProgress.roundToInt()) + else -> it.setSelectedValue(sliderProgress) } fragmentView.loadSettingsList() closeDialog() @@ -557,8 +505,7 @@ class SettingsAdapter( if (it.selectedValue != textInputValue) { fragmentView?.onSettingChanged() } - val setting = it.setSelectedValue(textInputValue ?: "") - fragmentView?.putSetting(setting) + it.setSelectedValue(textInputValue) fragmentView.loadSettingsList() closeDialog() } @@ -575,36 +522,19 @@ class SettingsAdapter( mcsetting?.let { val value = getValueForMultiChoiceSelection(it, which) if (it.selectedValues.contains(value) != isChecked) { - val setting = it.setSelectedValue((if (isChecked) it.selectedValues + value else it.selectedValues - value).sorted()) - fragmentView?.putSetting(setting) - fragmentView?.onSettingChanged() + it.setSelectedValue((if (isChecked) it.selectedValues + value else it.selectedValues - value).sorted()) + fragmentView.onSettingChanged() } fragmentView.loadSettingsList() } } - fun onLongClick(setting: AbstractSetting, position: Int): Boolean { + fun onLongClick(setting: AbstractSetting<*>, position: Int): Boolean { MaterialAlertDialogBuilder(context) .setMessage(R.string.reset_setting_confirmation) .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - when (setting) { - is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean - is AbstractFloatSetting -> { - if (setting is ScaledFloatSetting) { - setting.float = setting.defaultValue * setting.scale - } else { - setting.float = setting.defaultValue as Float - } - } - - is AbstractIntSetting -> setting.int = setting.defaultValue as Int - is AbstractStringSetting -> setting.string = setting.defaultValue as String - is AbstractShortSetting -> setting.short = setting.defaultValue as Short - } - notifyItemChanged(position) - fragmentView.onSettingChanged() - fragmentView.loadSettingsList() + resetSettingToDefault(setting, position) } .setNegativeButton(android.R.string.cancel, null) .show() @@ -612,6 +542,13 @@ class SettingsAdapter( return true } + fun resetSettingToDefault(setting: AbstractSetting, position: Int) { + fragmentView.activityView?.settings?.set(setting,setting.defaultValue) + notifyItemChanged(position) + fragmentView.onSettingChanged() + fragmentView.loadSettingsList() + } + fun onInputBindingLongClick(setting: InputBindingSetting, position: Int): Boolean { MaterialAlertDialogBuilder(context) .setMessage(R.string.reset_setting_confirmation) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt index 96568a76a..4521bae95 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt @@ -13,16 +13,19 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.divider.MaterialDividerItemDecoration import org.citra.citra_emu.databinding.FragmentSettingsBinding -import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.model.view.SettingsItem +import kotlin.getValue class SettingsFragment : Fragment(), SettingsFragmentView { override var activityView: SettingsActivityView? = null - private val fragmentPresenter = SettingsFragmentPresenter(this) + private val settingsViewModel: SettingsViewModel by activityViewModels() + + private val fragmentPresenter by lazy { SettingsFragmentPresenter(this) } private var settingsAdapter: SettingsAdapter? = null private var _binding: FragmentSettingsBinding? = null @@ -37,7 +40,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView { super.onCreate(savedInstanceState) val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG) val gameId = requireArguments().getString(ARGUMENT_GAME_ID) - fragmentPresenter.onCreate(menuTag!!, gameId!!) + fragmentPresenter.onCreate(menuTag!!, gameId!!, settingsViewModel.settings) } override fun onCreateView( @@ -88,10 +91,6 @@ class SettingsFragment : Fragment(), SettingsFragmentView { activityView!!.showToastMessage(message!!, is_long) } - override fun putSetting(setting: AbstractSetting) { - fragmentPresenter.putSetting(setting) - } - override fun onSettingChanged() { activityView!!.onSettingChanged() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 0143ac804..aaa067af2 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -14,22 +14,16 @@ import android.os.Build import android.text.TextUtils import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.serialization.builtins.IntArraySerializer import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R import org.citra.citra_emu.display.ScreenLayout import org.citra.citra_emu.display.StereoMode import org.citra.citra_emu.display.StereoWhichDisplay -import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting -import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting -import org.citra.citra_emu.features.settings.model.AbstractShortSetting -import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.IntListSetting -import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.StringSetting import org.citra.citra_emu.features.settings.model.view.DateTimeSetting @@ -50,6 +44,7 @@ import org.citra.citra_emu.utils.BirthdayMonth import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.ThemeUtil +import kotlin.math.roundToInt class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { private var menuTag: String? = null @@ -57,14 +52,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private var settingsList: ArrayList? = null private val settingsActivity get() = fragmentView.activityView as SettingsActivity - private val settings get() = fragmentView.activityView!!.settings + private lateinit var settings: Settings private lateinit var settingsAdapter: SettingsAdapter private lateinit var preferences: SharedPreferences - fun onCreate(menuTag: String, gameId: String) { + fun onCreate(menuTag: String, gameId: String, settings: Settings) { this.gameId = gameId this.menuTag = menuTag + this.settings = settings } fun onViewCreated(settingsAdapter: SettingsAdapter) { @@ -73,16 +69,6 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) loadSettingsList() } - fun putSetting(setting: AbstractSetting) { - if (setting.section == null || setting.key == null) { - return - } - - val section = settings.getSection(setting.section!!)!! - if (section.getSetting(setting.key!!) == null) { - section.putSetting(setting) - } - } fun loadSettingsList() { if (!TextUtils.isEmpty(gameId)) { @@ -226,6 +212,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) sl.apply { add( SwitchSetting( + settings, BooleanSetting.USE_FRAME_LIMIT, R.string.frame_limit_enable, R.string.frame_limit_enable_description, @@ -235,6 +222,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.FRAME_LIMIT, R.string.frame_limit_slider, R.string.frame_limit_slider_description, @@ -247,6 +235,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.TURBO_LIMIT, R.string.turbo_limit, R.string.turbo_limit_description, @@ -259,6 +248,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.ANDROID_HIDE_IMAGES, R.string.android_hide_images, R.string.android_hide_images_description, @@ -274,7 +264,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun checkCountryCompatibility() { if (countryCompatibilityChanged) { countryCompatibilityChanged = false - val compatFlags = SystemSaveGame.getCountryCompatibility(IntSetting.EMULATED_REGION.int) + val compatFlags = SystemSaveGame.getCountryCompatibility(settings.get(IntSetting.EMULATED_REGION)) if (compatFlags != 0) { var message = "" if (compatFlags and 1 != 0) { @@ -297,19 +287,10 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addSystemSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system)) sl.apply { - val usernameSetting = object : AbstractStringSetting { - override var string: String - get() = SystemSaveGame.getUsername() - set(value) = SystemSaveGame.setUsername(value) - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString get() = string - override val defaultValue = "AZAHAR" - } add(HeaderSetting(R.string.emulation_settings)) add( SwitchSetting( + settings, BooleanSetting.NEW_3DS, R.string.new_3ds, 0, @@ -319,6 +300,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.LLE_APPLETS, R.string.lle_applets, 0, @@ -328,6 +310,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.REQUIRED_ONLINE_LLE_MODULES, R.string.enable_required_online_lle_modules, R.string.enable_required_online_lle_modules_desc, @@ -336,35 +319,29 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) ) add(HeaderSetting(R.string.profile_settings)) - val regionSetting = object : AbstractIntSetting { - override var int: Int - get() { - val ret = IntSetting.EMULATED_REGION.int - checkCountryCompatibility() - return ret - } - set(value) { - IntSetting.EMULATED_REGION.int = value - countryCompatibilityChanged = true - checkCountryCompatibility() - } - override val key = IntSetting.EMULATED_REGION.key - override val section = null - override val isRuntimeEditable = false - override val valueAsString get() = int.toString() - override val defaultValue = IntSetting.EMULATED_REGION.defaultValue - } add( SingleChoiceSetting( - regionSetting, + settings, + null, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, + getValue = { + val ret = settings.get(IntSetting.EMULATED_REGION) + checkCountryCompatibility() + ret + }, + setValue = { + settings.set(IntSetting.EMULATED_REGION, it) + countryCompatibilityChanged = true + checkCountryCompatibility() + } ) ) add( SwitchSetting( + settings, BooleanSetting.APPLY_REGION_FREE_PATCH, R.string.apply_region_free_patch, R.string.apply_region_free_patch_desc, @@ -372,24 +349,6 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) BooleanSetting.APPLY_REGION_FREE_PATCH.defaultValue ) ) - val systemCountrySetting = object : AbstractShortSetting { - override var short: Short - get() { - val ret = SystemSaveGame.getCountryCode() - checkCountryCompatibility() - return ret; - } - set(value) { - SystemSaveGame.setCountryCode(value) - countryCompatibilityChanged = true - checkCountryCompatibility() - } - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString = short.toString() - override val defaultValue: Short = 49 - } var index = -1 val countries = settingsActivity.resources.getStringArray(R.array.countries) .mapNotNull { @@ -398,63 +357,64 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } add( StringSingleChoiceSetting( - systemCountrySetting, + settings, + null, R.string.country, 0, countries.map { it.first }.toTypedArray(), - countries.map { it.second }.toTypedArray() + countries.map { it.second }.toTypedArray(), + getValue = { + val ret = SystemSaveGame.getCountryCode() + checkCountryCompatibility() + ret.toString() + }, + setValue = { + SystemSaveGame.setCountryCode(it.toShort()) + countryCompatibilityChanged = true + checkCountryCompatibility() + } ) ) - val systemLanguageSetting = object : AbstractIntSetting { - override var int: Int - get() = SystemSaveGame.getSystemLanguage() - set(value) = SystemSaveGame.setSystemLanguage(value) - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString get() = int.toString() - override val defaultValue = 1 - } add( SingleChoiceSetting( - systemLanguageSetting, + settings, + null, R.string.emulated_language, 0, R.array.languageNames, - R.array.languageValues + R.array.languageValues, + getValue = { SystemSaveGame.getSystemLanguage() }, + setValue = { SystemSaveGame.setSystemLanguage(it) } ) ) add( StringInputSetting( - usernameSetting, + settings, + null, R.string.username, 0, "AZAHAR", - 10 + 10, + getValue = { SystemSaveGame.getUsername() }, + setValue = { SystemSaveGame.setUsername(it) } ) ) - val playCoinSettings = object : AbstractIntSetting { - override var int: Int - get() = SystemSaveGame.getPlayCoins() - set(value) = SystemSaveGame.setPlayCoins(value) - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString = int.toString() - override val defaultValue = 42 - } add( SliderSetting( - playCoinSettings, + settings, + null, R.string.play_coins, 0, 0, 300, - "" + "", + getValue = { SystemSaveGame.getPlayCoins().toFloat() }, + setValue = { SystemSaveGame.setPlayCoins(it.roundToInt()) } ) ) add( SliderSetting( + settings, IntSetting.STEPS_PER_HOUR, R.string.steps_per_hour, R.string.steps_per_hour_description, @@ -487,10 +447,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add(HeaderSetting(R.string.birthday)) - val systemBirthdayMonthSetting = object : AbstractShortSetting { - override var short: Short - get() = SystemSaveGame.getBirthday()[0] - set(value) { + add( + SingleChoiceSetting( + settings, + null, + R.string.birthday_month, + 0, + R.array.months, + R.array.monthValues, + getValue = { SystemSaveGame.getBirthday()[0].toInt() }, + setValue = { + val value = it.toShort() val birthdayDay = SystemSaveGame.getBirthday()[1] val daysInNewMonth = BirthdayMonth.getMonthFromCode(value)?.days ?: 31 if (daysInNewMonth < birthdayDay) { @@ -500,26 +467,23 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) SystemSaveGame.setBirthday(value, birthdayDay) } } - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString get() = short.toString() - override val defaultValue: Short = 11 - } - add( - SingleChoiceSetting( - systemBirthdayMonthSetting, - R.string.birthday_month, - 0, - R.array.months, - R.array.monthValues ) ) - val systemBirthdayDaySetting = object : AbstractShortSetting { - override var short: Short - get() = SystemSaveGame.getBirthday()[1] - set(value) { + val birthdayMonth = SystemSaveGame.getBirthday()[0] + val daysInMonth = BirthdayMonth.getMonthFromCode(birthdayMonth)?.days ?: 31 + val dayArray = Array(daysInMonth) { "${it + 1}" } + add( + StringSingleChoiceSetting( + settings, + null, + R.string.birthday_day, + 0, + dayArray, + dayArray, + getValue = { SystemSaveGame.getBirthday()[1].toString() }, + setValue = { + val value = it.toShort() val birthdayMonth = SystemSaveGame.getBirthday()[0] val daysInNewMonth = BirthdayMonth.getMonthFromCode(birthdayMonth)?.days ?: 31 @@ -529,28 +493,14 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) SystemSaveGame.setBirthday(birthdayMonth, value) } } - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString get() = short.toString() - override val defaultValue: Short = 7 - } - val birthdayMonth = SystemSaveGame.getBirthday()[0] - val daysInMonth = BirthdayMonth.getMonthFromCode(birthdayMonth)?.days ?: 31 - val dayArray = Array(daysInMonth) { "${it + 1}" } - add( - StringSingleChoiceSetting( - systemBirthdayDaySetting, - R.string.birthday_day, - 0, - dayArray, - dayArray + ) ) add(HeaderSetting(R.string.clock)) add( SingleChoiceSetting( + settings, IntSetting.INIT_CLOCK, R.string.init_clock, R.string.init_clock_description, @@ -562,6 +512,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( DateTimeSetting( + settings, StringSetting.INIT_TIME, R.string.simulated_clock, R.string.simulated_clock_description, @@ -573,6 +524,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.plugin_loader)) add( SwitchSetting( + settings, BooleanSetting.PLUGIN_LOADER, R.string.plugin_loader, R.string.plugin_loader_description, @@ -582,6 +534,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.ALLOW_PLUGIN_LOADER, R.string.allow_plugin_loader, R.string.allow_plugin_loader_description, @@ -592,6 +545,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.storage)) add( SwitchSetting( + settings, BooleanSetting.COMPRESS_INSTALLED_CIA_CONTENT, R.string.compress_cia_installs, R.string.compress_cia_installs_description, @@ -666,6 +620,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.inner_camera)) add( StringSingleChoiceSetting( + settings, StringSetting.CAMERA_INNER_NAME, R.string.image_source, R.string.image_source_description, @@ -678,6 +633,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) if (haveCameraDevices) { add( StringSingleChoiceSetting( + settings, StringSetting.CAMERA_INNER_CONFIG, R.string.camera_device, R.string.camera_device_description, @@ -690,6 +646,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } add( SingleChoiceSetting( + settings, IntSetting.CAMERA_INNER_FLIP, R.string.image_flip, 0, @@ -703,6 +660,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.outer_left_camera)) add( StringSingleChoiceSetting( + settings, StringSetting.CAMERA_OUTER_LEFT_NAME, R.string.image_source, R.string.image_source_description, @@ -715,6 +673,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) if (haveCameraDevices) { add( StringSingleChoiceSetting( + settings, StringSetting.CAMERA_OUTER_LEFT_CONFIG, R.string.camera_device, R.string.camera_device_description, @@ -727,6 +686,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } add( SingleChoiceSetting( + settings, IntSetting.CAMERA_OUTER_LEFT_FLIP, R.string.image_flip, 0, @@ -740,6 +700,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.outer_right_camera)) add( StringSingleChoiceSetting( + settings, StringSetting.CAMERA_OUTER_RIGHT_NAME, R.string.image_source, R.string.image_source_description, @@ -752,6 +713,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) if (haveCameraDevices) { add( StringSingleChoiceSetting( + settings, StringSetting.CAMERA_OUTER_RIGHT_CONFIG, R.string.camera_device, R.string.camera_device_description, @@ -764,6 +726,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } add( SingleChoiceSetting( + settings, IntSetting.CAMERA_OUTER_RIGHT_FLIP, R.string.image_flip, 0, @@ -832,6 +795,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.miscellaneous)) add( SwitchSetting( + settings, BooleanSetting.USE_ARTIC_BASE_CONTROLLER, R.string.use_artic_base_controller, R.string.use_artic_base_controller_description, @@ -842,20 +806,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } } - private fun getInputObject(key: String): AbstractStringSetting { - return object : AbstractStringSetting { - override var string: String - get() = preferences.getString(key, "")!! - set(value) { - preferences.edit() - .putString(key, value) - .apply() - } + private fun getInputObject(key: String): AbstractSetting { + return object : AbstractSetting { override val key = key override val section = Settings.SECTION_CONTROLS override val isRuntimeEditable = true - override val valueAsString = preferences.getString(key, "")!! override val defaultValue = "" + override fun valueFromString(string: String): String = string + override fun valueToString(value: String): String = value + // TODO: make input mappings also work per-game, which will be easy if we move + // them to config files } } @@ -865,6 +825,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.renderer)) add( SingleChoiceSetting( + settings, IntSetting.GRAPHICS_API, R.string.graphics_api, 0, @@ -876,6 +837,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.SPIRV_SHADER_GEN, R.string.spirv_shader_gen, R.string.spirv_shader_gen_description, @@ -885,6 +847,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.DISABLE_SPIRV_OPTIMIZER, R.string.disable_spirv_optimizer, R.string.disable_spirv_optimizer_description, @@ -894,6 +857,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.ASYNC_SHADERS, R.string.async_shaders, R.string.async_shaders_description, @@ -903,6 +867,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + settings, IntSetting.RESOLUTION_FACTOR, R.string.internal_resolution, R.string.internal_resolution_description, @@ -914,6 +879,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.USE_INTEGER_SCALING, R.string.use_integer_scaling, R.string.use_integer_scaling_description, @@ -923,6 +889,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.LINEAR_FILTERING, R.string.linear_filtering, R.string.linear_filtering_description, @@ -932,6 +899,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.SHADERS_ACCURATE_MUL, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, @@ -941,6 +909,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.DISK_SHADER_CACHE, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, @@ -950,6 +919,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + settings, IntSetting.TEXTURE_FILTER, R.string.texture_filter_name, R.string.texture_filter_description, @@ -961,6 +931,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.DELAY_RENDER_THREAD_US, R.string.delay_render_thread, R.string.delay_render_thread_description, @@ -975,6 +946,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.stereoscopy)) add( SingleChoiceSetting( + settings, IntSetting.RENDER_3D_WHICH_DISPLAY, R.string.render_3d_which_display, R.string.render_3d_which_display_description, @@ -986,6 +958,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + settings, IntSetting.STEREOSCOPIC_3D_MODE, R.string.render3d, R.string.render3d_description, @@ -993,12 +966,13 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.array.render3dValues, IntSetting.STEREOSCOPIC_3D_MODE.key, IntSetting.STEREOSCOPIC_3D_MODE.defaultValue, - isEnabled = IntSetting.RENDER_3D_WHICH_DISPLAY.int != StereoWhichDisplay.NONE.int + isEnabled = settings.get(IntSetting.RENDER_3D_WHICH_DISPLAY) != StereoWhichDisplay.NONE.int ) ) add( SliderSetting( + settings, IntSetting.STEREOSCOPIC_3D_DEPTH, R.string.factor3d, R.string.factor3d_description, @@ -1011,6 +985,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.DISABLE_RIGHT_EYE_RENDER, R.string.disable_right_eye_render, R.string.disable_right_eye_render_description, @@ -1021,18 +996,20 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.SWAP_EYES_3D, R.string.swap_eyes_3d, R.string.swap_eyes_3d_description, BooleanSetting.SWAP_EYES_3D.key, BooleanSetting.SWAP_EYES_3D.defaultValue, - isEnabled = IntSetting.RENDER_3D_WHICH_DISPLAY.int != StereoWhichDisplay.NONE.int + isEnabled = settings.get(IntSetting.RENDER_3D_WHICH_DISPLAY) != StereoWhichDisplay.NONE.int ) ) add(HeaderSetting(R.string.cardboard_vr)) add( SliderSetting( + settings, IntSetting.CARDBOARD_SCREEN_SIZE, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, @@ -1041,11 +1018,12 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) "%", IntSetting.CARDBOARD_SCREEN_SIZE.key, IntSetting.CARDBOARD_SCREEN_SIZE.defaultValue.toFloat(), - isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int + isEnabled = settings.get(IntSetting.STEREOSCOPIC_3D_MODE) == StereoMode.CARDBOARD_VR.int ) ) add( SliderSetting( + settings, IntSetting.CARDBOARD_X_SHIFT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, @@ -1054,11 +1032,12 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) "%", IntSetting.CARDBOARD_X_SHIFT.key, IntSetting.CARDBOARD_X_SHIFT.defaultValue.toFloat(), - isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int + isEnabled = settings.get(IntSetting.STEREOSCOPIC_3D_MODE) == StereoMode.CARDBOARD_VR.int ) ) add( SliderSetting( + settings, IntSetting.CARDBOARD_Y_SHIFT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, @@ -1067,13 +1046,14 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) "%", IntSetting.CARDBOARD_Y_SHIFT.key, IntSetting.CARDBOARD_Y_SHIFT.defaultValue.toFloat(), - isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int + isEnabled = settings.get(IntSetting.STEREOSCOPIC_3D_MODE) == StereoMode.CARDBOARD_VR.int ) ) add(HeaderSetting(R.string.utility)) add( SwitchSetting( + settings, BooleanSetting.DUMP_TEXTURES, R.string.dump_textures, R.string.dump_textures_description, @@ -1083,6 +1063,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.CUSTOM_TEXTURES, R.string.custom_textures, R.string.custom_textures_description, @@ -1092,6 +1073,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.ASYNC_CUSTOM_LOADING, R.string.async_custom_loading, R.string.async_custom_loading_description, @@ -1103,6 +1085,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.advanced)) add( SingleChoiceSetting( + settings, IntSetting.TEXTURE_SAMPLING, R.string.texture_sampling_name, R.string.texture_sampling_description, @@ -1132,6 +1115,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) sl.apply { add( SingleChoiceSetting( + settings, IntSetting.ORIENTATION_OPTION, R.string.layout_screen_orientation, 0, @@ -1143,6 +1127,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.EXPAND_TO_CUTOUT_AREA, R.string.expand_to_cutout_area, R.string.expand_to_cutout_area_description, @@ -1152,6 +1137,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + settings, IntSetting.SCREEN_LAYOUT, R.string.emulation_switch_screen_layout, 0, @@ -1163,6 +1149,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.UPRIGHT_SCREEN, R.string.emulation_rotate_upright, 0, @@ -1172,6 +1159,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( MultiChoiceSetting( + settings, IntListSetting.LAYOUTS_TO_CYCLE, R.string.layouts_to_cycle, R.string.layouts_to_cycle_description, @@ -1183,6 +1171,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + settings, IntSetting.PORTRAIT_SCREEN_LAYOUT, R.string.emulation_switch_portrait_layout, 0, @@ -1194,6 +1183,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + settings, IntSetting.SECONDARY_DISPLAY_LAYOUT, R.string.emulation_switch_secondary_layout, R.string.emulation_switch_secondary_layout_description, @@ -1205,6 +1195,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + settings, IntSetting.ASPECT_RATIO, R.string.emulation_aspect_ratio, 0, @@ -1212,11 +1203,12 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.array.aspectRatioValues, IntSetting.ASPECT_RATIO.key, IntSetting.ASPECT_RATIO.defaultValue, - isEnabled = IntSetting.SCREEN_LAYOUT.int == ScreenLayout.SINGLE_SCREEN.int, + isEnabled = settings.get(IntSetting.SCREEN_LAYOUT) == ScreenLayout.SINGLE_SCREEN.int, ) ) add( SingleChoiceSetting( + settings, IntSetting.SMALL_SCREEN_POSITION, R.string.emulation_small_screen_position, R.string.small_screen_position_description, @@ -1228,6 +1220,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.SCREEN_GAP, R.string.screen_gap, R.string.screen_gap_description, @@ -1240,6 +1233,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, FloatSetting.LARGE_SCREEN_PROPORTION, R.string.large_screen_proportion, R.string.large_screen_proportion_description, @@ -1252,6 +1246,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, FloatSetting.SECOND_SCREEN_OPACITY, R.string.second_screen_opacity, R.string.second_screen_opacity_description, @@ -1260,77 +1255,47 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) "%", FloatSetting.SECOND_SCREEN_OPACITY.key, FloatSetting.SECOND_SCREEN_OPACITY.defaultValue, - isEnabled = IntSetting.SCREEN_LAYOUT.int == ScreenLayout.CUSTOM_LAYOUT.int + 0, + isEnabled = settings.get(IntSetting.SCREEN_LAYOUT) == ScreenLayout.CUSTOM_LAYOUT.int ) ) add(HeaderSetting(R.string.bg_color, R.string.bg_color_description)) - val bgRedSetting = object : AbstractIntSetting { - override var int: Int - get() = (FloatSetting.BACKGROUND_RED.float * 255).toInt() - set(value) { - FloatSetting.BACKGROUND_RED.float = value.toFloat() / 255 - settings.saveSetting(FloatSetting.BACKGROUND_RED, SettingsFile.FILE_NAME_CONFIG) - } - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString = int.toString() - override val defaultValue = FloatSetting.BACKGROUND_RED.defaultValue - } add( SliderSetting( - bgRedSetting, + settings, + FloatSetting.BACKGROUND_RED, R.string.bg_red, 0, 0, 255, - "" + "", + rounding = 0 ) ) - val bgGreenSetting = object : AbstractIntSetting { - override var int: Int - get() = (FloatSetting.BACKGROUND_GREEN.float * 255).toInt() - set(value) { - FloatSetting.BACKGROUND_GREEN.float = value.toFloat() / 255 - settings.saveSetting(FloatSetting.BACKGROUND_GREEN, SettingsFile.FILE_NAME_CONFIG) - } - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString = int.toString() - override val defaultValue = FloatSetting.BACKGROUND_GREEN.defaultValue - } + add( SliderSetting( - bgGreenSetting, + settings, + FloatSetting.BACKGROUND_GREEN, R.string.bg_green, 0, 0, 255, - "" + "", + rounding = 0 ) ) - val bgBlueSetting = object : AbstractIntSetting { - override var int: Int - get() = (FloatSetting.BACKGROUND_BLUE.float * 255).toInt() - set(value) { - FloatSetting.BACKGROUND_BLUE.float = value.toFloat() / 255 - settings.saveSetting(FloatSetting.BACKGROUND_BLUE, SettingsFile.FILE_NAME_CONFIG) - } - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString = int.toString() - override val defaultValue = FloatSetting.BACKGROUND_BLUE.defaultValue - } + add( SliderSetting( - bgBlueSetting, + settings, + FloatSetting.BACKGROUND_BLUE, R.string.bg_blue, 0, 0, 255, - "" + "", + rounding = 0 ) ) add( @@ -1368,6 +1333,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.PERF_OVERLAY_ENABLE, R.string.performance_overlay_enable, 0, @@ -1378,6 +1344,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.PERF_OVERLAY_BACKGROUND, R.string.performance_overlay_background, R.string.performance_overlay_background_description, @@ -1388,6 +1355,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SingleChoiceSetting( + settings, IntSetting.PERFORMANCE_OVERLAY_POSITION, R.string.performance_overlay_position, R.string.performance_overlay_position_description, @@ -1401,6 +1369,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.PERF_OVERLAY_SHOW_FPS, R.string.performance_overlay_show_fps, R.string.performance_overlay_show_fps_description, @@ -1411,6 +1380,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME, R.string.performance_overlay_show_frametime, R.string.performance_overlay_show_frametime_description, @@ -1421,6 +1391,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.PERF_OVERLAY_SHOW_SPEED, R.string.performance_overlay_show_speed, R.string.performance_overlay_show_speed_description, @@ -1431,6 +1402,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE, R.string.performance_overlay_show_app_ram_usage, R.string.performance_overlay_show_app_ram_usage_description, @@ -1441,6 +1413,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM, R.string.performance_overlay_show_available_ram, R.string.performance_overlay_show_available_ram_description, @@ -1451,6 +1424,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( SwitchSetting( + settings, BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP, R.string.performance_overlay_show_battery_temp, R.string.performance_overlay_show_battery_temp_description, @@ -1467,6 +1441,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.emulation_top_screen)) add( SliderSetting( + settings, IntSetting.LANDSCAPE_TOP_X, R.string.emulation_custom_layout_x, 0, @@ -1479,6 +1454,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.LANDSCAPE_TOP_Y, R.string.emulation_custom_layout_y, 0, @@ -1491,6 +1467,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.LANDSCAPE_TOP_WIDTH, R.string.emulation_custom_layout_width, 0, @@ -1503,6 +1480,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.LANDSCAPE_TOP_HEIGHT, R.string.emulation_custom_layout_height, 0, @@ -1516,6 +1494,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.emulation_bottom_screen)) add( SliderSetting( + settings, IntSetting.LANDSCAPE_BOTTOM_X, R.string.emulation_custom_layout_x, 0, @@ -1528,6 +1507,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.LANDSCAPE_BOTTOM_Y, R.string.emulation_custom_layout_y, 0, @@ -1540,6 +1520,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.LANDSCAPE_BOTTOM_WIDTH, R.string.emulation_custom_layout_width, 0, @@ -1552,6 +1533,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.LANDSCAPE_BOTTOM_HEIGHT, R.string.emulation_custom_layout_height, 0, @@ -1572,6 +1554,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.emulation_top_screen)) add( SliderSetting( + settings, IntSetting.PORTRAIT_TOP_X, R.string.emulation_custom_layout_x, 0, @@ -1584,6 +1567,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.PORTRAIT_TOP_Y, R.string.emulation_custom_layout_y, 0, @@ -1596,6 +1580,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.PORTRAIT_TOP_WIDTH, R.string.emulation_custom_layout_width, 0, @@ -1608,6 +1593,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.PORTRAIT_TOP_HEIGHT, R.string.emulation_custom_layout_height, 0, @@ -1621,6 +1607,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.emulation_bottom_screen)) add( SliderSetting( + settings, IntSetting.PORTRAIT_BOTTOM_X, R.string.emulation_custom_layout_x, 0, @@ -1633,6 +1620,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.PORTRAIT_BOTTOM_Y, R.string.emulation_custom_layout_y, 0, @@ -1645,6 +1633,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.PORTRAIT_BOTTOM_WIDTH, R.string.emulation_custom_layout_width, 0, @@ -1657,6 +1646,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SliderSetting( + settings, IntSetting.PORTRAIT_BOTTOM_HEIGHT, R.string.emulation_custom_layout_height, 0, @@ -1676,18 +1666,21 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) sl.apply { add( SliderSetting( - ScaledFloatSetting.AUDIO_VOLUME, + settings, + FloatSetting.AUDIO_VOLUME, R.string.audio_volume, 0, 0, 100, "%", - ScaledFloatSetting.AUDIO_VOLUME.key, - ScaledFloatSetting.AUDIO_VOLUME.defaultValue + FloatSetting.AUDIO_VOLUME.key, + FloatSetting.AUDIO_VOLUME.defaultValue, + rounding = 0 ) ) add( SwitchSetting( + settings, BooleanSetting.ENABLE_AUDIO_STRETCHING, R.string.audio_stretch, R.string.audio_stretch_description, @@ -1697,6 +1690,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.ENABLE_REALTIME_AUDIO, R.string.realtime_audio, R.string.realtime_audio_description, @@ -1706,6 +1700,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SingleChoiceSetting( + settings, IntSetting.AUDIO_INPUT_TYPE, R.string.audio_input_type, 0, @@ -1715,24 +1710,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.AUDIO_INPUT_TYPE.defaultValue ) ) - - val soundOutputModeSetting = object : AbstractIntSetting { - override var int: Int - get() = SystemSaveGame.getSoundOutputMode() - set(value) = SystemSaveGame.setSoundOutputMode(value) - override val key = null - override val section = null - override val isRuntimeEditable = false - override val valueAsString = int.toString() - override val defaultValue = 1 - } add( SingleChoiceSetting( - soundOutputModeSetting, + settings, + null, R.string.sound_output_mode, 0, R.array.soundOutputModes, - R.array.soundOutputModeValues + R.array.soundOutputModeValues, + getValue = { SystemSaveGame.getSoundOutputMode() }, + setValue = { SystemSaveGame.setSoundOutputMode(it) } ) ) } @@ -1744,6 +1731,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(HeaderSetting(R.string.debug_warning)) add( SliderSetting( + settings, IntSetting.CPU_CLOCK_SPEED, R.string.cpu_clock_speed, 0, @@ -1756,6 +1744,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.CPU_JIT, R.string.cpu_jit, R.string.cpu_jit_description, @@ -1765,6 +1754,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.HW_SHADER, R.string.hw_shaders, R.string.hw_shaders_description, @@ -1774,6 +1764,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.SHADER_JIT, R.string.shader_jit, R.string.shader_jit_description, @@ -1783,6 +1774,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.VSYNC, R.string.vsync, R.string.vsync_description, @@ -1792,6 +1784,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.DEBUG_RENDERER, R.string.renderer_debug, R.string.renderer_debug_description, @@ -1801,6 +1794,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.INSTANT_DEBUG_LOG, R.string.instant_debug_log, R.string.instant_debug_log_description, @@ -1810,6 +1804,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.ENABLE_RPC_SERVER, R.string.enable_rpc_server, R.string.enable_rpc_server_desc, @@ -1819,6 +1814,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.TOGGLE_UNIQUE_DATA_CONSOLE_TYPE, R.string.toggle_unique_data_console_type, R.string.toggle_unique_data_console_type_desc, @@ -1828,6 +1824,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.DELAY_START_LLE_MODULES, R.string.delay_start_lle_modules, R.string.delay_start_lle_modules_description, @@ -1837,6 +1834,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add( SwitchSetting( + settings, BooleanSetting.DETERMINISTIC_ASYNC_OPERATIONS, R.string.deterministic_async_operations, R.string.deterministic_async_operations_description, @@ -1851,111 +1849,77 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addThemeSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme)) sl.apply { - val theme: AbstractBooleanSetting = object : AbstractBooleanSetting { - override var boolean: Boolean - get() = preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false) - set(value) { - preferences.edit() - .putBoolean(Settings.PREF_MATERIAL_YOU, value) - .apply() - settingsActivity.recreate() - } - override val key: String? = null - override val section: String? = null - override val isRuntimeEditable: Boolean = false - override val valueAsString: String - get() = preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false).toString() - override val defaultValue = false - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { add( SwitchSetting( - theme, + settings, + null, R.string.material_you, - R.string.material_you_description + R.string.material_you_description, + getValue = { + preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false) + }, + setValue = { + preferences.edit() + .putBoolean(Settings.PREF_MATERIAL_YOU, it) + .apply() + settingsActivity.recreate() + } ) ) } - - val staticThemeColor: AbstractIntSetting = object : AbstractIntSetting { - override var int: Int - get() = preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) - set(value) { - preferences.edit() - .putInt(Settings.PREF_STATIC_THEME_COLOR, value) - .apply() - settingsActivity.recreate() - } - override val key: String? = null - override val section: String? = null - override val isRuntimeEditable: Boolean = false - override val valueAsString: String - get() = preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0).toString() - override val defaultValue: Any = 0 - } - add( SingleChoiceSetting( - staticThemeColor, + settings, + null, R.string.static_theme_color, R.string.static_theme_color_description, R.array.staticThemeNames, - R.array.staticThemeValues + R.array.staticThemeValues, + getValue = { + preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) + }, + setValue = { + preferences.edit() + .putInt(Settings.PREF_STATIC_THEME_COLOR, it) + .apply() + settingsActivity.recreate() + } ) ) - - val themeMode: AbstractIntSetting = object : AbstractIntSetting { - override var int: Int - get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) - set(value) { + add( + SingleChoiceSetting( + settings, + null, + R.string.change_theme_mode, + 0, + R.array.themeModeEntries, + R.array.themeModeValues, + getValue = { + preferences.getInt(Settings.PREF_THEME_MODE, -1) + }, + setValue = { preferences.edit() - .putInt(Settings.PREF_THEME_MODE, value) + .putInt(Settings.PREF_THEME_MODE, it) .apply() ThemeUtil.setThemeMode(settingsActivity) settingsActivity.recreate() } - override val key: String? = null - override val section: String? = null - override val isRuntimeEditable: Boolean = false - override val valueAsString: String - get() = preferences.getInt(Settings.PREF_THEME_MODE, -1).toString() - override val defaultValue: Any = -1 - } - - add( - SingleChoiceSetting( - themeMode, - R.string.change_theme_mode, - 0, - R.array.themeModeEntries, - R.array.themeModeValues ) ) - - val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { - override var boolean: Boolean - get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) - set(value) { + add( + SwitchSetting( + settings, + null, + R.string.use_black_backgrounds, + R.string.use_black_backgrounds_description, + getValue = { preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) }, + setValue = { preferences.edit() - .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value) + .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, it) .apply() settingsActivity.recreate() } - override val key: String? = null - override val section: String? = null - override val isRuntimeEditable: Boolean = false - override val valueAsString: String - get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) - .toString() - override val defaultValue: Any = false - } - - add( - SwitchSetting( - blackBackgrounds, - R.string.use_black_backgrounds, - R.string.use_black_backgrounds_description ) ) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt index e1bb25230..95b8efe12 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt @@ -1,10 +1,9 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.ui -import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem /** @@ -45,13 +44,6 @@ interface SettingsFragmentView { */ fun showToastMessage(message: String?, is_long: Boolean) - /** - * Have the fragment add a setting to the HashMap. - * - * @param setting The (possibly previously missing) new setting. - */ - fun putSetting(setting: AbstractSetting) - /** * Have the fragment tell the containing Activity that a setting was modified. */ diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 7eb2dae97..69dd3fc0b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt @@ -18,12 +18,11 @@ import org.citra.citra_emu.features.settings.ui.SettingsAdapter import java.text.SimpleDateFormat class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - private lateinit var setting: DateTimeSetting - + SettingViewHolder(binding.root, adapter) { + override lateinit var setting: DateTimeSetting @SuppressLint("SimpleDateFormat") override fun bind(item: SettingsItem) { - setting = item as DateTimeSetting + setting = item as? DateTimeSetting ?: return binding.textSettingName.setText(item.nameId) if (item.descriptionId != 0) { binding.textSettingDescription.visibility = View.VISIBLE diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt index ed794fcfb..6b34bf5e3 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -10,8 +10,8 @@ import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.ui.SettingsAdapter class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - + SettingViewHolder(binding.root, adapter) { + override var setting = null init { itemView.setOnClickListener(null) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt index 5d2a812e0..44e26758e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt @@ -13,8 +13,8 @@ import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.ui.SettingsAdapter class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - private lateinit var setting: InputBindingSetting + SettingViewHolder(binding.root, adapter) { + override lateinit var setting: InputBindingSetting override fun bind(item: SettingsItem) { val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt index 8493115a4..e94932b42 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt @@ -11,9 +11,8 @@ import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting import org.citra.citra_emu.features.settings.ui.SettingsAdapter class MultiChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - private lateinit var setting: SettingsItem - + SettingViewHolder(binding.root, adapter) { + override lateinit var setting: SettingsItem override fun bind(item: SettingsItem) { setting = item binding.textSettingName.setText(item.nameId) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt index d75368598..c836bdfc7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt @@ -14,8 +14,8 @@ import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.ui.SettingsAdapter class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - private lateinit var setting: RunnableSetting + SettingViewHolder(binding.root, adapter) { + override lateinit var setting: RunnableSetting override fun bind(item: SettingsItem) { setting = item as RunnableSetting diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt index 5b4d39cf4..2b408abe0 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt @@ -9,14 +9,17 @@ import androidx.recyclerview.widget.RecyclerView import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.ui.SettingsAdapter -abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) : +abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener { - init { itemView.setOnClickListener(this) itemView.setOnLongClickListener(this) } + /** + * The SettingsItem we are holding + */ + abstract val setting: T? /** * Called by the adapter to set this ViewHolder's child views to display the list item * it must now represent. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index 6e899d946..8c764b617 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -12,8 +12,8 @@ import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSettin import org.citra.citra_emu.features.settings.ui.SettingsAdapter class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - private lateinit var setting: SettingsItem + SettingViewHolder(binding.root, adapter) { + override lateinit var setting: SettingsItem override fun bind(item: SettingsItem) { setting = item diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt index 726d8c96f..1d651046e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt @@ -6,17 +6,14 @@ package org.citra.citra_emu.features.settings.ui.viewholder import android.view.View import org.citra.citra_emu.databinding.ListItemSettingBinding -import org.citra.citra_emu.features.settings.model.AbstractFloatSetting -import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.FloatSetting -import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.model.view.SliderSetting import org.citra.citra_emu.features.settings.ui.SettingsAdapter class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - private lateinit var setting: SliderSetting + SettingViewHolder(binding.root, adapter) { + override lateinit var setting: SliderSetting override fun bind(item: SettingsItem) { setting = item as SliderSetting @@ -28,12 +25,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda binding.textSettingDescription.visibility = View.GONE } binding.textSettingValue.visibility = View.VISIBLE - binding.textSettingValue.text = when (setting.setting) { - is ScaledFloatSetting -> - "${(setting.setting as ScaledFloatSetting).float.toInt()}${setting.units}" - is FloatSetting -> "${(setting.setting as AbstractFloatSetting).float}${setting.units}" - else -> "${(setting.setting as AbstractIntSetting).int}${setting.units}" - } + binding.textSettingValue.text = "${setting.valueAsString}${setting.units}" if (setting.isActive) { binding.textSettingName.alpha = 1f diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt index 09bc206d3..75981f538 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt @@ -11,11 +11,10 @@ import org.citra.citra_emu.features.settings.model.view.StringInputSetting import org.citra.citra_emu.features.settings.ui.SettingsAdapter class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - private lateinit var setting: SettingsItem - + SettingViewHolder(binding.root, adapter) { + override lateinit var setting: StringInputSetting override fun bind(item: SettingsItem) { - setting = item + setting = item as StringInputSetting binding.textSettingName.setText(item.nameId) if (item.descriptionId != 0) { binding.textSettingDescription.visibility = View.VISIBLE @@ -24,7 +23,7 @@ class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: Settin binding.textSettingDescription.visibility = View.GONE } binding.textSettingValue.visibility = View.VISIBLE - binding.textSettingValue.text = setting.setting?.valueAsString + binding.textSettingValue.text = setting.selectedValue if (setting.isActive) { binding.textSettingName.alpha = 1f @@ -42,7 +41,7 @@ class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: Settin adapter.onClickDisabledSetting(!setting.isEditable) return } - adapter.onStringInputClick((setting as StringInputSetting), bindingAdapterPosition) + adapter.onStringInputClick(setting, bindingAdapterPosition) } override fun onLongClick(clicked: View): Boolean { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt index 461221763..8e2cc27e9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt @@ -12,11 +12,11 @@ import org.citra.citra_emu.features.settings.model.view.SubmenuSetting import org.citra.citra_emu.features.settings.ui.SettingsAdapter class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { - private lateinit var item: SubmenuSetting + SettingViewHolder(binding.root, adapter) { + override lateinit var setting: SubmenuSetting override fun bind(item: SettingsItem) { - this.item = item as SubmenuSetting + setting = item as SubmenuSetting if (item.iconId == 0) { binding.icon.visibility = View.GONE } else { @@ -40,7 +40,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd } override fun onClick(clicked: View) { - adapter.onSubmenuClick(item) + adapter.onSubmenuClick(setting) } override fun onLongClick(clicked: View): Boolean { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index 72ebd8d0b..c92b7c6b7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -12,9 +12,9 @@ import org.citra.citra_emu.features.settings.model.view.SwitchSetting import org.citra.citra_emu.features.settings.ui.SettingsAdapter class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : - SettingViewHolder(binding.root, adapter) { + SettingViewHolder(binding.root, adapter) { - private lateinit var setting: SwitchSetting + override lateinit var setting: SwitchSetting override fun bind(item: SettingsItem) { setting = item as SwitchSetting diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt index a9e1d4743..2e03fc722 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt @@ -14,12 +14,9 @@ import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.IntSetting -import org.citra.citra_emu.features.settings.model.ScaledFloatSetting -import org.citra.citra_emu.features.settings.model.SettingSection -import org.citra.citra_emu.features.settings.model.Settings.SettingsSectionMap +import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.StringSetting import org.citra.citra_emu.features.settings.ui.SettingsActivityView -import org.citra.citra_emu.utils.BiMap import org.citra.citra_emu.utils.DirectoryInitialization.userDirectory import org.citra.citra_emu.utils.Log import org.ini4j.Wini @@ -27,7 +24,6 @@ import java.io.BufferedReader import java.io.FileNotFoundException import java.io.IOException import java.io.InputStreamReader -import java.util.TreeMap /** @@ -36,40 +32,46 @@ import java.util.TreeMap object SettingsFile { const val FILE_NAME_CONFIG = "config" - private var sectionsMap = BiMap() + private val allSettings: List> by lazy { + BooleanSetting.values().toList() + + IntSetting.values().toList() + + FloatSetting.values().toList() + + StringSetting.values().toList() + + IntListSetting.values().toList() + } + private fun findSettingByKey(key: String): AbstractSetting<*>? = + allSettings.firstOrNull { it.key == key } /** - * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves - * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it - * failed. + * Reads a given .ini file from disk and updates a instance of the Settings class appropriately * * @param ini The ini file to load the settings from + * @param settings The Settings instance to edit * @param isCustomGame * @param view The current view. * @return An Observable that emits a HashMap of the file's contents, then completes. */ fun readFile( ini: DocumentFile, + settings: Settings, isCustomGame: Boolean, view: SettingsActivityView? - ): HashMap { - val sections: HashMap = SettingsSectionMap() + ) { var reader: BufferedReader? = null try { val context: Context = CitraApplication.appContext val inputStream = context.contentResolver.openInputStream(ini.uri) reader = BufferedReader(InputStreamReader(inputStream)) - var current: SettingSection? = null + var currentSection: String? = null var line: String? while (reader.readLine().also { line = it } != null) { - if (line!!.startsWith("[") && line!!.endsWith("]")) { - current = sectionFromLine(line!!, isCustomGame) - sections[current.name] = current - } else if (current != null) { - val setting = settingFromLine(line!!) - if (setting != null) { - current.putSetting(setting) - } + if (line!!.startsWith("[") && line.endsWith("]")) { + currentSection = line.substring(1, line.length-1) + } else if (currentSection != null) { + val pair = parseLineToKeyValuePair(line) ?: continue + val (key, rawValue) = pair + val descriptor = findSettingByKey(key) ?: continue + loadSettingInto(settings, descriptor, rawValue, isCustomGame) } } } catch (e: FileNotFoundException) { @@ -87,102 +89,152 @@ object SettingsFile { } } } - return sections } - fun readFile(fileName: String, view: SettingsActivityView?): HashMap { - return readFile(getSettingsFile(fileName), false, view) - } - - fun readFile(fileName: String): HashMap = readFile(fileName, null) - /** - * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves - * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it - * failed. - * - * @param gameId the id of the game to load it's settings. - * @param view The current view. + * Load global settings from the config file into the settings instance */ - fun readCustomGameSettings( - gameId: String, - view: SettingsActivityView? - ): HashMap { - return readFile(getCustomGameSettingsFile(gameId), true, view) + fun loadSettings(settings: Settings, view: SettingsActivityView? = null) { + readFile(getSettingsFile(FILE_NAME_CONFIG),settings,false,view) } /** - * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error + * Load global settings AND custom settings into the settings instance, sets gameId + */ + fun loadSettings(settings: Settings, gameId: String, view: SettingsActivityView? = null) { + settings.gameId = gameId + loadSettings(settings, view) + val file = findCustomGameSettingsFile(gameId) ?: return + readFile(file, settings, true, view) + } + + /** + * Uses the settings object to parse the raw string and store it in the correct map + */ + @Suppress("UNCHECKED_CAST") + private fun loadSettingInto( + settings: Settings, + setting: AbstractSetting, + rawValue: String, + isCustomGame: Boolean + ) { + val value = setting.valueFromString(rawValue) ?: return + if (isCustomGame) { + settings.setOverride(setting, value) + } else { + settings.setGlobal(setting, value) + } + } + + /** + * Saves a the global settings from a Settings instance + * to the global .ini file on disk. If unsuccessful, outputs an error * telling why it failed. * - * @param fileName The target filename without a path or extension. - * @param sections The HashMap containing the Settings we want to serialize. + * @param settings The Settings instance we are saving * @param view The current view. */ - fun saveFile( - fileName: String, - sections: TreeMap, - view: SettingsActivityView + fun saveGlobalFile( + settings: Settings, + view: SettingsActivityView? = null ) { - val ini = getSettingsFile(fileName) + val ini = getSettingsFile(FILE_NAME_CONFIG) try { val context: Context = CitraApplication.appContext val inputStream = context.contentResolver.openInputStream(ini.uri) val writer = Wini(inputStream) - val keySet: Set = sections.keys - for (key in keySet) { - val section = sections[key] - writeSection(writer, section!!) - } inputStream!!.close() + + for (setting in allSettings) { + val value = settings.getGlobal(setting) ?: continue + writeSettingToWini(writer, setting, value) + } + val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt") writer.store(outputStream) outputStream!!.flush() outputStream.close() } catch (e: Exception) { - Log.error("[SettingsFile] File not found: $fileName.ini: ${e.message}") - view.showToastMessage( + Log.error("[SettingsFile] File not found: $FILE_NAME_CONFIG.ini: ${e.message}") + view?.showToastMessage( CitraApplication.appContext - .getString(R.string.error_saving, fileName, e.message), false + .getString(R.string.error_saving, FILE_NAME_CONFIG, e.message), false ) } } - fun saveFile( - fileName: String, - setting: AbstractSetting + /** + * Save the per-game overrides to a per-game config file + */ + + fun saveCustomFile( + settings: Settings, + view: SettingsActivityView? = null ) { - val ini = getSettingsFile(fileName) + if (!settings.isPerGame()) return + val ini = getOrCreateCustomGameSettingsFile(settings.gameId!!) try { val context: Context = CitraApplication.appContext + val writer = Wini() + + val overrides = settings.getAllOverrides() + for (descriptor in allSettings) { + val value = overrides[descriptor.key] ?: continue + writeSettingToWini(writer, descriptor, value) + } + + val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt") + writer.store(outputStream) + outputStream?.flush() + outputStream?.close() + } catch (e: Exception) { + Log.error("[SettingsFile] Error saving custom file for ${settings.gameId}: ${e.message}") + view?.onSettingsFileNotFound() + } + } + + fun saveSetting(setting: AbstractSetting, settings: Settings) { + if (settings.hasOverride(setting)) { + // Currently a per-game setting, keep it that way + val ini = getOrCreateCustomGameSettingsFile(settings.gameId!!) + writeSingleSettingToFile(ini, setting, settings.get(setting)) + } else { + // Currently global, save to global file + val ini = getSettingsFile(FILE_NAME_CONFIG) + writeSingleSettingToFile(ini, setting, settings.getGlobal(setting)) + } + } + + private fun writeSingleSettingToFile(ini: DocumentFile, setting: AbstractSetting, value: T) { + try { + val context = CitraApplication.appContext val inputStream = context.contentResolver.openInputStream(ini.uri) - val writer = Wini(inputStream) - writer.put(setting.section, setting.key, setting.valueAsString) - inputStream!!.close() + val writer = if (inputStream != null) Wini(inputStream) else Wini() + inputStream?.close() + writeSettingToWini(writer, setting, value as Any) val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt") writer.store(outputStream) outputStream!!.flush() outputStream.close() } catch (e: Exception) { - Log.error("[SettingsFile] File not found: $fileName.ini: ${e.message}") + Log.error("[SettingsFile] Error saving setting ${setting.key}: ${e.message}") } } + @Suppress("UNCHECKED_CAST") + private fun writeSettingToWini(writer: Wini, descriptor: AbstractSetting, value: Any) { + val typedValue = value as T + writer.put(descriptor.section, descriptor.key, descriptor.valueToString(typedValue)) + } - private fun mapSectionNameFromIni(generalSectionName: String): String? { - return if (sectionsMap.getForward(generalSectionName) != null) { - sectionsMap.getForward(generalSectionName) - } else { - generalSectionName - } + private fun parseLineToKeyValuePair(line: String): Pair? { + val splitLine = line.split("=".toRegex(), limit = 2) + if (splitLine.size != 2) return null + val key = splitLine[0].trim() + val value = splitLine[1].trim() + if (value.isEmpty()) return null + return Pair(key, value) } - private fun mapSectionNameToIni(generalSectionName: String): String { - return if (sectionsMap.getBackward(generalSectionName) != null) { - sectionsMap.getBackward(generalSectionName).toString() - } else { - generalSectionName - } - } fun getSettingsFile(fileName: String): DocumentFile { val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory)) @@ -190,96 +242,20 @@ object SettingsFile { return configDirectory!!.findFile("$fileName.ini")!! } - private fun getCustomGameSettingsFile(gameId: String): DocumentFile { + fun customExists(gameId: String): Boolean = findCustomGameSettingsFile(gameId) != null + + private fun findCustomGameSettingsFile(gameId: String): DocumentFile? { val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory)) - val configDirectory = root!!.findFile("GameSettings") - return configDirectory!!.findFile("$gameId.ini")!! + val configDir = root?.findFile("config") ?: return null + val customDir = configDir.findFile("custom") ?: return null + return customDir.findFile("$gameId.ini") } - private fun sectionFromLine(line: String, isCustomGame: Boolean): SettingSection { - var sectionName: String = line.substring(1, line.length - 1) - if (isCustomGame) { - sectionName = mapSectionNameToIni(sectionName) - } - return SettingSection(sectionName) - } - - /** - * For a line of text, determines what type of data is being represented, and returns - * a Setting object containing this data. - * - * @param line The line of text being parsed. - * @return A typed Setting containing the key/value contained in the line. - */ - private fun settingFromLine(line: String): AbstractSetting? { - val splitLine = line.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - if (splitLine.size != 2) { - return null - } - val key = splitLine[0].trim { it <= ' ' } - val value = splitLine[1].trim { it <= ' ' } - if (value.isEmpty()) { - return null - } - - val booleanSetting = BooleanSetting.from(key) - if (booleanSetting != null) { - booleanSetting.boolean = value.toBoolean() - return booleanSetting - } - - val intSetting = IntSetting.from(key) - if (intSetting != null) { - try { - intSetting.int = value.toInt() - } catch (e: NumberFormatException) { - intSetting.int = if (value.toBoolean()) 1 else 0 - } - return intSetting - } - - val scaledFloatSetting = ScaledFloatSetting.from(key) - if (scaledFloatSetting != null) { - scaledFloatSetting.float = value.toFloat() * scaledFloatSetting.scale - return scaledFloatSetting - } - - val floatSetting = FloatSetting.from(key) - if (floatSetting != null) { - floatSetting.float = value.toFloat() - return floatSetting - } - - val stringSetting = StringSetting.from(key) - if (stringSetting != null) { - stringSetting.string = value - return stringSetting - } - - val intListSetting = IntListSetting.from(key) - if (intListSetting != null) { - intListSetting.list = value.split(", ").map { it.toInt() } - } - - return null - } - - /** - * Writes the contents of a Section HashMap to disk. - * - * @param parser A Wini pointed at a file on disk. - * @param section A section containing settings to be written to the file. - */ - private fun writeSection(parser: Wini, section: SettingSection) { - // Write the section header. - val header = section.name - - // Write this section's values. - val settings = section.settings - val keySet: Set = settings.keys - for (key in keySet) { - val setting = settings[key] - parser.put(header, setting!!.key, setting.valueAsString) - } + private fun getOrCreateCustomGameSettingsFile(gameId: String): DocumentFile { + val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory))!! + val configDir = root.findFile("config") ?: root.createDirectory("config")!! + val customDir = configDir.findFile("custom") ?: configDir.createDirectory("custom")!! + return customDir.findFile("$gameId.ini") + ?: customDir.createFile("*/*", "$gameId.ini")!! } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 35899ce7f..6cf8083b8 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -35,6 +35,7 @@ import android.widget.PopupMenu import android.widget.TextView import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.Insets @@ -44,7 +45,6 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout.DrawerListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -70,7 +70,6 @@ import org.citra.citra_emu.display.ScreenLayout import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.Settings -import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.model.Game @@ -78,7 +77,6 @@ import org.citra.citra_emu.utils.BuildUtil import org.citra.citra_emu.utils.DirectoryInitialization import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState import org.citra.citra_emu.utils.EmulationMenuSettings -import org.citra.citra_emu.utils.FileUtil import org.citra.citra_emu.utils.GameHelper import org.citra.citra_emu.utils.GameIconUtils import org.citra.citra_emu.utils.EmulationLifecycleUtil @@ -104,8 +102,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil private val emulationViewModel: EmulationViewModel by activityViewModels() - private val settingsViewModel: SettingsViewModel by viewModels() - private val settings get() = settingsViewModel.settings private val onPause = Runnable{ togglePause() } private val onShutdown = Runnable{ emulationState.stop() } @@ -181,7 +177,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram retainInstance = true emulationState = EmulationState(game.path) emulationActivity = requireActivity() as EmulationActivity - screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings) + screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, Settings.settings) EmulationLifecycleUtil.addPauseResumeHook(onPause) EmulationLifecycleUtil.addShutdownHook(onShutdown) } @@ -208,7 +204,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram if (requireActivity().isFinishing) { return } - + binding.surfaceInputOverlay.initializeSettings(Settings.settings) binding.surfaceEmulation.holder.addCallback(this) binding.doneControlConfig.setOnClickListener { binding.doneControlConfig.visibility = View.GONE @@ -218,7 +214,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram // Show/hide the "Stats" overlay updateShowPerformanceOverlay() - val position = IntSetting.PERFORMANCE_OVERLAY_POSITION.int + val position = Settings.settings.get(IntSetting.PERFORMANCE_OVERLAY_POSITION) updateStatsPosition(position) binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) @@ -379,10 +375,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram R.id.menu_settings -> { SettingsActivity.launch( requireContext(), - SettingsFile.FILE_NAME_CONFIG, - "" + SettingsFile.FILE_NAME_CONFIG, null ) - true } @@ -505,7 +499,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram emulationState.unpause() // If the overlay is enabled, we need to update the position if changed - val position = IntSetting.PERFORMANCE_OVERLAY_POSITION.int + val position = Settings.settings.get(IntSetting.PERFORMANCE_OVERLAY_POSITION) updateStatsPosition(position) binding.inGameMenu.menu.findItem(R.id.menu_emulation_pause)?.let { menuItem -> @@ -704,7 +698,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.menu.apply { findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay findItem(R.id.menu_performance_overlay_show).isChecked = - BooleanSetting.PERF_OVERLAY_ENABLE.boolean + Settings.settings.get(BooleanSetting.PERF_OVERLAY_ENABLE) findItem(R.id.menu_haptic_feedback).isChecked = EmulationMenuSettings.hapticFeedback findItem(R.id.menu_emulation_joystick_rel_center).isChecked = EmulationMenuSettings.joystickRelCenter @@ -721,8 +715,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } R.id.menu_performance_overlay_show -> { - BooleanSetting.PERF_OVERLAY_ENABLE.boolean = !BooleanSetting.PERF_OVERLAY_ENABLE.boolean - settings.saveSetting(BooleanSetting.PERF_OVERLAY_ENABLE, SettingsFile.FILE_NAME_CONFIG) + Settings.settings.update(BooleanSetting.PERF_OVERLAY_ENABLE, + Settings.settings.get(BooleanSetting.PERF_OVERLAY_ENABLE)) + SettingsFile.saveSetting(BooleanSetting.PERF_OVERLAY_ENABLE, Settings.settings) updateShowPerformanceOverlay() true } @@ -918,7 +913,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.menuInflater.inflate(R.menu.menu_landscape_screen_layout, popupMenu.menu) - val layoutOptionMenuItem = when (IntSetting.SCREEN_LAYOUT.int) { + val layoutOptionMenuItem = when (Settings.settings.get(IntSetting.SCREEN_LAYOUT)) { ScreenLayout.ORIGINAL.int -> R.id.menu_screen_layout_original @@ -990,7 +985,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.menuInflater.inflate(R.menu.menu_portrait_screen_layout, popupMenu.menu) - val layoutOptionMenuItem = when (IntSetting.PORTRAIT_SCREEN_LAYOUT.int) { + val layoutOptionMenuItem = when (Settings.settings.get(IntSetting.PORTRAIT_SCREEN_LAYOUT)) { PortraitScreenLayout.TOP_FULL_WIDTH.int -> R.id.menu_portrait_layout_top_full PortraitScreenLayout.ORIGINAL.int -> @@ -1257,7 +1252,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) } - if (BooleanSetting.PERF_OVERLAY_ENABLE.boolean) { + if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_ENABLE)) { val SYSTEM_FPS = 0 val FPS = 1 val SPEED = 2 @@ -1272,11 +1267,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram val perfStats = NativeLibrary.getPerfStats() val dividerString = "\u00A0\u2502 " if (perfStats[FPS] > 0) { - if (BooleanSetting.PERF_OVERLAY_SHOW_FPS.boolean) { + if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_FPS)) { sb.append(String.format("FPS:\u00A0%d", (perfStats[FPS] + 0.5).toInt())) } - if (BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME.boolean) { + if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME)) { if (sb.isNotEmpty()) sb.append(dividerString) sb.append( String.format( @@ -1291,7 +1286,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram ) } - if (BooleanSetting.PERF_OVERLAY_SHOW_SPEED.boolean) { + if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_SPEED)) { if (sb.isNotEmpty()) sb.append(dividerString) sb.append( String.format( @@ -1301,14 +1296,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram ) } - if (BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE.boolean) { + if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE)) { if (sb.isNotEmpty()) sb.append(dividerString) val appRamUsage = File("/proc/self/statm").readLines()[0].split(' ')[1].toLong() * 4096 / 1000000 sb.append("Process\u00A0RAM:\u00A0$appRamUsage\u00A0MB") } - if (BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM.boolean) { + if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM)) { if (sb.isNotEmpty()) sb.append(dividerString) context?.let { ctx -> val activityManager = @@ -1321,14 +1316,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } } - if (BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP.boolean) { + if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP)) { if (sb.isNotEmpty()) sb.append(dividerString) val batteryTemp = getBatteryTemperature() val tempF = celsiusToFahrenheit(batteryTemp) sb.append(String.format("%.1f°C/%.1f°F", batteryTemp, tempF)) } - if (BooleanSetting.PERF_OVERLAY_BACKGROUND.boolean) { + if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_BACKGROUND)) { binding.performanceOverlayShowText.setBackgroundResource(R.color.citra_transparent_black) } else { binding.performanceOverlayShowText.setBackgroundResource(0) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt index f7519bb81..c50b7d8f7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt @@ -24,6 +24,7 @@ import androidx.preference.PreferenceManager import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R +import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.utils.EmulationMenuSettings import org.citra.citra_emu.utils.TurboHelper import java.lang.NullPointerException @@ -45,7 +46,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex private var buttonBeingConfigured: InputOverlayDrawableButton? = null private var dpadBeingConfigured: InputOverlayDrawableDpad? = null private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null - private val settingsViewModel = NativeLibrary.sEmulationActivity.get()!!.settingsViewModel + private lateinit var settings: Settings // Stores the ID of the pointer that interacted with the 3DS touchscreen. private var touchscreenPointerId = -1 @@ -71,6 +72,10 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex requestFocus() } + fun initializeSettings(settings: Settings) { + this.settings = settings + } + override fun draw(canvas: Canvas) { super.draw(canvas) overlayButtons.forEach { it.draw(canvas) } @@ -173,7 +178,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex swapScreen() } else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) { - TurboHelper.toggleTurbo(true) + TurboHelper.toggleTurbo(true, settings) } NativeLibrary.onGamePadEvent( diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt index 91df60632..316510b32 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt @@ -48,7 +48,6 @@ import org.citra.citra_emu.R import org.citra.citra_emu.contracts.OpenFileResultContract import org.citra.citra_emu.databinding.ActivityMainBinding import org.citra.citra_emu.features.settings.model.Settings -import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.fragments.GrantMissingFilesystemPermissionFragment @@ -72,7 +71,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private val homeViewModel: HomeViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels() - private val settingsViewModel: SettingsViewModel by viewModels() override var themeId: Int = 0 @@ -95,13 +93,15 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (PermissionsHandler.hasWriteAccess(applicationContext) && DirectoryInitialization.areCitraDirectoriesReady() && !CitraDirectoryUtils.needToUpdateManually()) { - settingsViewModel.settings.loadSettings() } ThemeUtil.ThemeChangeListener(this) ThemeUtil.setTheme(this) super.onCreate(savedInstanceState) + // load the global settings from the config file at program launch + SettingsFile.loadSettings(Settings.settings) + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/TurboHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/TurboHelper.kt index 26ac69a75..4ffbcc72b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/TurboHelper.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/TurboHelper.kt @@ -9,6 +9,7 @@ import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.Settings object TurboHelper { private var turboSpeedEnabled = false @@ -17,12 +18,12 @@ object TurboHelper { return turboSpeedEnabled } - fun reloadTurbo(showToast: Boolean) { + fun reloadTurbo(showToast: Boolean, settings: Settings) { val context = CitraApplication.appContext val toastMessage: String if (turboSpeedEnabled) { - NativeLibrary.setTemporaryFrameLimit(IntSetting.TURBO_LIMIT.int.toDouble()) + NativeLibrary.setTemporaryFrameLimit(settings.get(IntSetting.TURBO_LIMIT).toDouble()) toastMessage = context.getString(R.string.turbo_enabled_toast) } else { NativeLibrary.disableTemporaryFrameLimit() @@ -34,12 +35,12 @@ object TurboHelper { } } - fun setTurboEnabled(state: Boolean, showToast: Boolean) { + fun setTurboEnabled(state: Boolean, showToast: Boolean, settings: Settings) { turboSpeedEnabled = state - reloadTurbo(showToast) + reloadTurbo(showToast, settings) } - fun toggleTurbo(showToast: Boolean) { - setTurboEnabled(!TurboHelper.isTurboSpeedEnabled(), showToast) + fun toggleTurbo(showToast: Boolean, settings: Settings) { + setTurboEnabled(!TurboHelper.isTurboSpeedEnabled(), showToast, settings) } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt index 3a5571e9b..68458254e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -7,9 +7,13 @@ package org.citra.citra_emu.viewmodel import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import org.citra.citra_emu.features.settings.model.Settings class EmulationViewModel : ViewModel() { val emulationStarted get() = _emulationStarted.asStateFlow() + // convenience shortcut for + val settings = Settings.settings + private val _emulationStarted = MutableStateFlow(false) val shaderProgress get() = _shaderProgress.asStateFlow() @@ -21,6 +25,7 @@ class EmulationViewModel : ViewModel() { val shaderMessage get() = _shaderMessage.asStateFlow() private val _shaderMessage = MutableStateFlow("") + fun setShaderProgress(progress: Int) { _shaderProgress.value = progress } From 7f94e80e8dd16aa9c33bd64c810bcca884057171 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Sun, 15 Mar 2026 15:56:26 +0300 Subject: [PATCH 2/3] implement custom settings fully # Conflicts: # src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt # src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt --- .../citra_emu/activities/EmulationActivity.kt | 32 ++--- .../citra/citra_emu/adapters/GameAdapter.kt | 11 ++ .../features/settings/model/Settings.kt | 2 +- .../settings/model/SettingsViewModel.kt | 4 +- .../settings/ui/SettingsActivityPresenter.kt | 15 +- .../features/settings/ui/SettingsAdapter.kt | 15 +- .../features/settings/ui/SettingsFragment.kt | 2 +- .../settings/ui/SettingsFragmentPresenter.kt | 1 + .../ui/viewholder/DateTimeViewHolder.kt | 3 + .../ui/viewholder/MultiChoiceViewHolder.kt | 2 + .../ui/viewholder/SettingViewHolder.kt | 18 +++ .../ui/viewholder/SingleChoiceViewHolder.kt | 2 + .../ui/viewholder/SliderViewHolder.kt | 2 + .../ui/viewholder/StringInputViewHolder.kt | 2 + .../ui/viewholder/SwitchSettingViewHolder.kt | 2 + .../citra_emu/fragments/EmulationFragment.kt | 35 ++++- .../citra_emu/viewmodel/EmulationViewModel.kt | 6 + src/android/app/src/main/jni/config.cpp | 136 +++++++++++++----- src/android/app/src/main/jni/config.h | 17 ++- src/android/app/src/main/jni/native.cpp | 44 ++++-- .../src/main/res/layout/dialog_about_game.xml | 9 ++ .../src/main/res/layout/list_item_setting.xml | 9 ++ .../res/layout/list_item_setting_switch.xml | 9 ++ .../app/src/main/res/menu/menu_in_game.xml | 5 + .../app/src/main/res/values/strings.xml | 2 + src/common/file_util.cpp | 4 + src/common/file_util.h | 1 + 27 files changed, 308 insertions(+), 82 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index c9d6c9e3f..c764054e6 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -87,15 +87,25 @@ class EmulationActivity : AppCompatActivity() { RefreshRateUtil.enforceRefreshRate(this, sixtyHz = true) ThemeUtil.setTheme(this) - - - super.onCreate(savedInstanceState) - + val game = try { + intent.extras?.let { extras -> + BundleCompat.getParcelable(extras, "game", Game::class.java) + } ?: run { + Log.error("[EmulationActivity] Missing game data in intent extras") + return + } + } catch (e: Exception) { + Log.error("[EmulationActivity] Failed to retrieve game data: ${e.message}") + return + } // load global settings if for some reason they aren't (should be loaded in MainActivity) if (Settings.settings.getAllGlobal().isEmpty()) { SettingsFile.loadSettings(Settings.settings) } - // once per-game settings are added, load them here! + // load per-game settings + SettingsFile.loadSettings(Settings.settings, String.format("%016X", game.titleId)) + + super.onCreate(savedInstanceState) secondaryDisplay = SecondaryDisplay(this, Settings.settings) secondaryDisplay.updateDisplay() @@ -128,18 +138,6 @@ class EmulationActivity : AppCompatActivity() { applyOrientationSettings() // Check for orientation settings at startup - val game = try { - intent.extras?.let { extras -> - BundleCompat.getParcelable(extras, "game", Game::class.java) - } ?: run { - Log.error("[EmulationActivity] Missing game data in intent extras") - return - } - } catch (e: Exception) { - Log.error("[EmulationActivity] Failed to retrieve game data: ${e.message}") - return - } - NativeLibrary.playTimeManagerStart(game.titleId) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt index d43ea5a60..d3319d3c9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt @@ -59,6 +59,8 @@ import org.citra.citra_emu.utils.FileUtil import org.citra.citra_emu.utils.GameIconUtils import org.citra.citra_emu.utils.Log import org.citra.citra_emu.viewmodel.GamesViewModel +import org.citra.citra_emu.features.settings.ui.SettingsActivity +import org.citra.citra_emu.features.settings.utils.SettingsFile class GameAdapter( private val activity: AppCompatActivity, @@ -485,6 +487,15 @@ class GameAdapter( bottomSheetDialog.dismiss() } + bottomSheetView.findViewById(R.id.application_settings).setOnClickListener { + SettingsActivity.launch( + context, + SettingsFile.FILE_NAME_CONFIG, + String.format("%016X", holder.game.titleId) + ) + bottomSheetDialog.dismiss() + } + val compressDecompressButton = bottomSheetView.findViewById(R.id.compress_decompress) if (game.isInstalled) { compressDecompressButton.setOnClickListener { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index 5ef881bfd..c2b54d12e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -175,7 +175,7 @@ class Settings { KEY_BUTTON_RIGHT ) val axisTitles = listOf( - R.string.controller_axis_vertical, + R.string.controller_axis_vertical, R.string.controller_axis_horizontal ) val dPadTitles = listOf( diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt index 166c74d16..e0f4f6e48 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt @@ -7,7 +7,7 @@ package org.citra.citra_emu.features.settings.model import androidx.lifecycle.ViewModel class SettingsViewModel : ViewModel() { - // the Settings Activity will always work with a local copy of settings while - // editing, to avoid issues with conflicting active/edited settings + // the settings activity primarily manipulates its own copy of the settings object + // syncing it with the active settings only when saving val settings = Settings() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt index 1c2cbf144..095241097 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt @@ -27,13 +27,26 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView, private var shouldSave = false private lateinit var menuTag: String private lateinit var gameId: String + private var perGameInGlobalContext = false fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) { this.menuTag = menuTag this.gameId = gameId - // merge the active settings into the local settings activity instance + + perGameInGlobalContext = gameId != "" && !Settings.settings.isPerGame() + + // sync the active settings into my local settings appropriately + // if we are editing global settings rom a game, this should just work + // to sync only the global settings into the local version + settings.gameId = gameId settings.mergeSettings(Settings.settings) + // if we are editing per-game settings when the game is not loaded, + // we need to load the per-game settings now from the ini file + if (perGameInGlobalContext) { + SettingsFile.loadSettings(settings, gameId) + } + if (savedInstanceState != null) { shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index c124cf383..f08ebc07c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -72,6 +72,7 @@ class SettingsAdapter( ) : RecyclerView.Adapter?>(), DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener { private var settings: ArrayList? = null + var isPerGame: Boolean = false private var clickedItem: SettingsItem? = null private var clickedPosition: Int private var dialog: AlertDialog? = null @@ -226,6 +227,8 @@ class SettingsAdapter( if (fragmentView.activityView != null) // Reload the settings list to update the UI fragmentView.loadSettingsList() + + notifyItemChanged(position) } private fun onSingleChoiceClick(item: SingleChoiceSetting) { @@ -543,12 +546,22 @@ class SettingsAdapter( } fun resetSettingToDefault(setting: AbstractSetting, position: Int) { - fragmentView.activityView?.settings?.set(setting,setting.defaultValue) + val settings = fragmentView.activityView?.settings ?: return + settings.set(setting,setting.defaultValue) notifyItemChanged(position) fragmentView.onSettingChanged() fragmentView.loadSettingsList() } + fun resetSettingToGlobal(setting: AbstractSetting, position: Int) { + val settings = fragmentView.activityView?.settings ?: return + settings.clearOverride(setting) + notifyItemChanged(position) + fragmentView.onSettingChanged() + fragmentView.loadSettingsList() + + } + fun onInputBindingLongClick(setting: InputBindingSetting, position: Int): Boolean { MaterialAlertDialogBuilder(context) .setMessage(R.string.reset_setting_confirmation) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt index 4521bae95..02827d659 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index aaa067af2..e9506b44b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -66,6 +66,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) fun onViewCreated(settingsAdapter: SettingsAdapter) { this.settingsAdapter = settingsAdapter preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + settingsAdapter.isPerGame = !TextUtils.isEmpty(gameId) loadSettingsList() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 69dd3fc0b..3d3102b95 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt @@ -55,6 +55,9 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA binding.textSettingDescription.alpha = 0.5f binding.textSettingValue.alpha = 0.5f } + + showGlobalButtonIfNeeded(binding.buttonUseGlobal, position) + } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt index e94932b42..d9f08896d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt @@ -34,6 +34,8 @@ class MultiChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Settin binding.textSettingDescription.alpha = 0.5f binding.textSettingValue.alpha = 0.5f } + + showGlobalButtonIfNeeded(binding.buttonUseGlobal, position) } private fun getTextSetting(): String { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt index 2b408abe0..02a4df748 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt @@ -37,4 +37,22 @@ abstract class SettingViewHolder(itemView: View, protected abstract override fun onClick(clicked: View) abstract override fun onLongClick(clicked: View): Boolean + + fun showGlobalButtonIfNeeded(buttonUseGlobal: View, position: Int) { + setting ?: return + // Show "Revert to global" button in Custom Settings if applicable. + val settings = adapter.fragmentView.activityView?.settings + val showGlobal = settings?.isPerGame() == true + && setting?.setting != null + && settings.hasOverride(setting!!.setting!!) + + buttonUseGlobal.visibility = if (showGlobal) View.VISIBLE else View.GONE + if (showGlobal) { + buttonUseGlobal.setOnClickListener { + setting?.setting?.let { descriptor -> + adapter.resetSettingToGlobal(descriptor, bindingAdapterPosition) + } + } + } + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index 8c764b617..ed16d8952 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -36,6 +36,8 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti binding.textSettingDescription.alpha = 0.5f binding.textSettingValue.alpha = 0.5f } + + showGlobalButtonIfNeeded(binding.buttonUseGlobal, position) } private fun getTextSetting(): String { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt index 1d651046e..7050df9cf 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt @@ -36,6 +36,8 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda binding.textSettingDescription.alpha = 0.5f binding.textSettingValue.alpha = 0.5f } + + showGlobalButtonIfNeeded(binding.buttonUseGlobal, position) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt index 75981f538..de5296f34 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt @@ -34,6 +34,8 @@ class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: Settin binding.textSettingDescription.alpha = 0.5f binding.textSettingValue.alpha = 0.5f } + + showGlobalButtonIfNeeded(binding.buttonUseGlobal, position) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index c92b7c6b7..8346f0fdb 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -38,6 +38,8 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter val textAlpha = if (setting.isActive) 1f else 0.5f binding.textSettingName.alpha = textAlpha binding.textSettingDescription.alpha = textAlpha + + showGlobalButtonIfNeeded(binding.buttonUseGlobal, position) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 6cf8083b8..f82a36724 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -35,7 +35,6 @@ import android.widget.PopupMenu import android.widget.TextView import android.widget.Toast import androidx.activity.OnBackPressedCallback -import androidx.activity.result.contract.ActivityResultContracts import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.Insets @@ -91,7 +90,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram private lateinit var emulationState: EmulationState private var perfStatsUpdater: Runnable? = null - private lateinit var emulationActivity: EmulationActivity + private var emulationActivity: EmulationActivity? = null private var _binding: FragmentEmulationBinding? = null private val binding get() = _binding!! @@ -375,8 +374,31 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram R.id.menu_settings -> { SettingsActivity.launch( requireContext(), - SettingsFile.FILE_NAME_CONFIG, null + SettingsFile.FILE_NAME_CONFIG, + "" ) + + true + } + + R.id.menu_application_settings -> { + val titleId = NativeLibrary.getRunningTitleId() + if (titleId != 0L) { + val gameId = java.lang.String.format("%016X", titleId) + SettingsActivity.launch( + requireContext(), + SettingsFile.FILE_NAME_CONFIG, + gameId + ) + } else { + // Fallback: open global settings if title id unknown + SettingsActivity.launch( + requireContext(), + SettingsFile.FILE_NAME_CONFIG, + "" + ) + } + true } @@ -514,7 +536,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } if (DirectoryInitialization.areCitraDirectoriesReady()) { - emulationState.run(emulationActivity.isActivityRecreated) + emulationState.run(emulationActivity!!.isActivityRecreated) } else { setupCitraDirectoriesThenStartEmulation() } @@ -529,6 +551,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } override fun onDetach() { + emulationActivity = null NativeLibrary.clearEmulationActivity() super.onDetach() } @@ -548,7 +571,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram if (directoryInitializationState === DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED ) { - emulationState.run(emulationActivity.isActivityRecreated) + emulationState.run(emulationActivity!!.isActivityRecreated) } else if (directoryInitializationState === DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED ) { @@ -867,7 +890,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.setOnMenuItemClickListener { when (it.itemId) { R.id.menu_emulation_amiibo_load -> { - emulationActivity.openAmiiboFileLauncher.launch(false) + emulationActivity!!.openAmiiboFileLauncher.launch(false) true } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt index 68458254e..3fc912671 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.features.settings.utils.SettingsFile class EmulationViewModel : ViewModel() { val emulationStarted get() = _emulationStarted.asStateFlow() @@ -26,6 +27,11 @@ class EmulationViewModel : ViewModel() { private val _shaderMessage = MutableStateFlow("") + /** Used for the initial load of settings. Call rebuild for later creations. */ + fun loadSettings(titleId: Long) { + if (settings.getAllGlobal().isNotEmpty()) return //already loaded + SettingsFile.loadSettings(settings, String.format("%016X", titleId)) + } fun setShaderProgress(progress: Int) { _shaderProgress.value = progress } diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index e0fa260c5..654d94b3e 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -79,28 +79,65 @@ static const std::array default_analogs }}; template <> -void Config::ReadSetting(const std::string& group, Settings::Setting& setting) { - std::string setting_value = - android_config->Get(group, setting.GetLabel(), setting.GetDefault()); +std::string Config::GetSetting(const std::string& group, Settings::Setting& setting) { + std::string setting_value = setting.GetDefault(); + if (per_game_config && per_game_config->HasValue(group, setting.GetLabel())) { + setting_value = per_game_config->Get(group, setting.GetLabel(), setting_value); + } else if (android_config) { + setting_value = android_config->Get(group, setting.GetLabel(), setting_value); + } if (setting_value.empty()) { setting_value = setting.GetDefault(); } - setting = std::move(setting_value); + return setting_value; +} + +template <> +void Config::ReadSetting(const std::string& group, Settings::Setting& setting) { + setting = std::move(GetSetting(group, setting)); +} + +template <> +bool Config::GetSetting(const std::string& group, Settings::Setting& setting) { + bool value = setting.GetDefault(); + if (per_game_config && per_game_config->HasValue(group, setting.GetLabel())) { + value = per_game_config->GetBoolean(group, setting.GetLabel(), value); + } else if (android_config) { + value = android_config->GetBoolean(group, setting.GetLabel(), value); + } + return value; } template <> void Config::ReadSetting(const std::string& group, Settings::Setting& setting) { - setting = android_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault()); + setting = GetSetting(group, setting); +} + +//TODO: figure out why ranged isn't being used +template +Type Config::GetSetting(const std::string& group, Settings::Setting& setting) { + if constexpr (std::is_floating_point_v) { + double value = static_cast(setting.GetDefault()); + if (per_game_config && per_game_config->HasValue(group, setting.GetLabel())) { + value = per_game_config->GetReal(group, setting.GetLabel(), value); + } else if (android_config) { + value = android_config->GetReal(group, setting.GetLabel(), value); + } + return static_cast(value); + } else { + long value = static_cast(setting.GetDefault()); + if (per_game_config && per_game_config->HasValue(group, setting.GetLabel())) { + value = per_game_config->GetInteger(group, setting.GetLabel(), value); + } else if (android_config) { + value = android_config->GetInteger(group, setting.GetLabel(), value); + } + return static_cast(value); + } } template void Config::ReadSetting(const std::string& group, Settings::Setting& setting) { - if constexpr (std::is_floating_point_v) { - setting = android_config->GetReal(group, setting.GetLabel(), setting.GetDefault()); - } else { - setting = static_cast(android_config->GetInteger( - group, setting.GetLabel(), static_cast(setting.GetDefault()))); - } + setting = GetSetting(group, setting); } void Config::ReadValues() { @@ -139,9 +176,8 @@ void Config::ReadValues() { ReadSetting("Core", Settings::values.cpu_clock_percentage); // Renderer - Settings::values.use_gles = android_config->GetBoolean("Renderer", "use_gles", true); - Settings::values.shaders_accurate_mul = - android_config->GetBoolean("Renderer", "shaders_accurate_mul", false); + ReadSetting("Renderer",Settings::values.use_gles); + ReadSetting("Renderer",Settings::values.shaders_accurate_mul); ReadSetting("Renderer", Settings::values.graphics_api); ReadSetting("Renderer", Settings::values.async_presentation); ReadSetting("Renderer", Settings::values.async_shader_compilation); @@ -156,7 +192,14 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.texture_sampling); ReadSetting("Renderer", Settings::values.turbo_limit); // Workaround to map Android setting for enabling the frame limiter to the format Citra expects - if (android_config->GetBoolean("Renderer", "use_frame_limit", true)) { + // TODO: test this! + bool use_frame_limit = false; + if (per_game_config && per_game_config->HasValue("Renderer", "use_frame_limit")) { + use_frame_limit = per_game_config->GetBoolean("Renderer", "use_frame_limit", true); + } else if (android_config) { + use_frame_limit = android_config->GetBoolean("Renderer", "use_frame_limit", true); + } + if (use_frame_limit) { ReadSetting("Renderer", Settings::values.frame_limit); } else { Settings::values.frame_limit = 0; @@ -183,21 +226,10 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.swap_eyes_3d); ReadSetting("Renderer", Settings::values.render_3d_which_display); // Layout - // Somewhat inelegant solution to ensure layout value is between 0 and 5 on read - // since older config files may have other values - int layoutInt = (int)android_config->GetInteger( - "Layout", "layout_option", static_cast(Settings::LayoutOption::LargeScreen)); - if (layoutInt < 0 || layoutInt > 5) { - layoutInt = static_cast(Settings::LayoutOption::LargeScreen); - } - Settings::values.layout_option = static_cast(layoutInt); - Settings::values.screen_gap = - static_cast(android_config->GetReal("Layout", "screen_gap", 0)); - Settings::values.large_screen_proportion = - static_cast(android_config->GetReal("Layout", "large_screen_proportion", 2.25)); - Settings::values.small_screen_position = static_cast( - android_config->GetInteger("Layout", "small_screen_position", - static_cast(Settings::SmallScreenPosition::TopRight))); + + ReadSetting("Layout",Settings::values.layout_option); + ReadSetting("Layout",Settings::values.screen_gap); + ReadSetting("Layout", Settings::values.small_screen_position); ReadSetting("Layout", Settings::values.screen_gap); ReadSetting("Layout", Settings::values.custom_top_x); ReadSetting("Layout", Settings::values.custom_top_y); @@ -212,14 +244,8 @@ void Config::ReadValues() { ReadSetting("Layout", Settings::values.cardboard_x_shift); ReadSetting("Layout", Settings::values.cardboard_y_shift); ReadSetting("Layout", Settings::values.upright_screen); - - Settings::values.portrait_layout_option = - static_cast(android_config->GetInteger( - "Layout", "portrait_layout_option", - static_cast(Settings::PortraitLayoutOption::PortraitTopFullWidth))); - Settings::values.secondary_display_layout = static_cast( - android_config->GetInteger("Layout", Settings::HKeys::secondary_display_layout.c_str(), - static_cast(Settings::SecondaryDisplayLayout::None))); + ReadSetting("Layout", Settings::values.portrait_layout_option); + ReadSetting("Layout", Settings::values.secondary_display_layout); ReadSetting("Layout", Settings::values.custom_portrait_top_x); ReadSetting("Layout", Settings::values.custom_portrait_top_y); ReadSetting("Layout", Settings::values.custom_portrait_top_width); @@ -349,3 +375,37 @@ void Config::Reload() { LoadINI(DefaultINI::android_config_default_file_content); ReadValues(); } + +void Config::LoadPerGameConfig(u64 title_id, const std::string& fallback_name) { + // Determine file name + std::string name; + if (title_id != 0) { + std::ostringstream ss; + ss << std::uppercase << std::hex << std::setw(16) << std::setfill('0') << title_id; + name = ss.str(); + } else { + name = fallback_name; + } + if (name.empty()) { + per_game_config.reset(); + per_game_config_loc.clear(); + return; + } + + const auto base = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir); + per_game_config_loc = base + "custom/" + name + ".ini"; + + std::string ini_buffer; + FileUtil::ReadFileToString(true, per_game_config_loc, ini_buffer); + if (!ini_buffer.empty()) { + per_game_config = std::make_unique(ini_buffer.c_str(), ini_buffer.size()); + if (per_game_config->ParseError() < 0) { + per_game_config.reset(); + } + } else { + per_game_config.reset(); + } + + // Re-apply values so that per-game overrides (if any) take effect immediately. + ReadValues(); +} diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h index 9d40295c4..b2989bd56 100644 --- a/src/android/app/src/main/jni/config.h +++ b/src/android/app/src/main/jni/config.h @@ -14,6 +14,8 @@ class Config { private: std::unique_ptr android_config; std::string android_config_loc; + std::unique_ptr per_game_config; + std::string per_game_config_loc; bool LoadINI(const std::string& default_contents = "", bool retry = true); void ReadValues(); @@ -23,14 +25,27 @@ public: ~Config(); void Reload(); + // Load a per-game config overlay by title id or fallback name. Does not create files. + void LoadPerGameConfig(u64 title_id, const std::string& fallback_name = ""); private: /** * Applies a value read from the android_config to a Setting. * * @param group The name of the INI group - * @param setting The yuzu setting to modify + * @param setting The setting to modify */ template void ReadSetting(const std::string& group, Settings::Setting& setting); + + /** + * Reads a value honoring per_game config, and returns it. + * Does not modify the setting. + * + * @param group The name of the INI group + * @param setting The setting to modify + */ + template + Type GetSetting(const std::string& group, Settings::Setting& setting); + }; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index f4a30610c..e3412891c 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -191,6 +191,28 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { const auto graphics_api = Settings::values.graphics_api.GetValue(); EGLContext* shared_context; + + + // Load game-specific settings overlay if available + u64 program_id{}; + FileUtil::SetCurrentRomPath(filepath); + auto app_loader = Loader::GetLoader(filepath); + if (app_loader) { + app_loader->ReadProgramId(program_id); + system.RegisterAppLoaderEarly(app_loader); + } + + // Forces a config reload on game boot, if the user changed settings in the UI + Config global_config{}; + + // Use filename as fallback if title id is zero (e.g., homebrew) + const std::string fallback_name = + program_id == 0 ? std::string(FileUtil::GetFilename(filepath)) : std::string{}; + global_config.LoadPerGameConfig(program_id, fallback_name); + system.ApplySettings(); + Settings::LogSettings(); + + switch (graphics_api) { #ifdef ENABLE_OPENGL case Settings::GraphicsAPI::OpenGL: @@ -228,18 +250,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { break; } - // Forces a config reload on game boot, if the user changed settings in the UI - Config{}; - // Replace with game-specific settings - u64 program_id{}; - FileUtil::SetCurrentRomPath(filepath); - auto app_loader = Loader::GetLoader(filepath); - if (app_loader) { - app_loader->ReadProgramId(program_id); - system.RegisterAppLoaderEarly(app_loader); - } - system.ApplySettings(); - Settings::LogSettings(); + Camera::RegisterFactory("image", std::make_unique()); @@ -913,13 +924,18 @@ void Java_org_citra_citra_1emu_NativeLibrary_logUserDirectory(JNIEnv* env, void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { - Config{}; + Config cfg{}; Core::System& system{Core::System::GetInstance()}; - // Replace with game-specific settings + // Load game-specific settings overlay (if a game is running) if (system.IsPoweredOn()) { u64 program_id{}; system.GetAppLoader().ReadProgramId(program_id); + // Use the registered ROM path (if any) to derive a fallback name + const std::string current_rom_path = FileUtil::GetCurrentRomPath(); + const std::string fallback_name = + program_id == 0 ? std::string(FileUtil::GetFilename(current_rom_path)) : std::string{}; + cfg.LoadPerGameConfig(program_id, fallback_name); } system.ApplySettings(); diff --git a/src/android/app/src/main/res/layout/dialog_about_game.xml b/src/android/app/src/main/res/layout/dialog_about_game.xml index cb7eea8de..2cac7adc7 100644 --- a/src/android/app/src/main/res/layout/dialog_about_game.xml +++ b/src/android/app/src/main/res/layout/dialog_about_game.xml @@ -180,6 +180,15 @@ android:contentDescription="@string/cheats" android:text="@string/cheats" /> + + + + diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml index dc1e82258..d81585119 100644 --- a/src/android/app/src/main/res/layout/list_item_setting_switch.xml +++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml @@ -46,6 +46,15 @@ android:textAlignment="viewStart" tools:text="@string/frame_limit_enable_description" /> + + diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index 950ab6fc8..8ca92310f 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml @@ -57,6 +57,11 @@ android:icon="@drawable/ic_settings" android:title="@string/preferences_settings" /> + + Amiibo Load Remove + Custom Settings + Customized: Revert to Global Select Amiibo File Error Loading Amiibo While loading the specified Amiibo file, an error occurred. Please check that the file is correct. diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 52f406226..e43b58551 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -1037,6 +1037,10 @@ void SetCurrentRomPath(const std::string& path) { g_currentRomPath = path; } +std::string GetCurrentRomPath() { + return g_currentRomPath; +} + bool StringReplace(std::string& haystack, const std::string& a, const std::string& b, bool swap) { const auto& needle = swap ? b : a; const auto& replacement = swap ? a : b; diff --git a/src/common/file_util.h b/src/common/file_util.h index 56dbeea93..cf7b29531 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -213,6 +213,7 @@ bool SetCurrentDir(const std::string& directory); void SetUserPath(const std::string& path = ""); void SetCurrentRomPath(const std::string& path); +[[nodiscard]] std::string GetCurrentRomPath(); // Returns a pointer to a string with a Citra data dir in the user's home // directory. To be used in "multi-user" mode (that is, installed). From 0c6e7c6cc5a009de71ff2d176201b37e0b4ffacd Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 18 Mar 2026 12:19:25 +0300 Subject: [PATCH 3/3] refactor android input settings completely to enable per-game settings and saving in config.ini, as well as more efficient storage of setting information --- CMakeModules/GenerateSettingKeys.cmake | 31 + .../java/org/citra/citra_emu/NativeLibrary.kt | 1 + .../citra_emu/activities/EmulationActivity.kt | 267 ++------ .../citra_emu/features/input/GamepadHelper.kt | 374 +++++++++++ .../features/{hotkeys => input}/Hotkey.kt | 2 +- .../{hotkeys => input}/HotkeyUtility.kt | 68 +- .../citra/citra_emu/features/input/Input.kt | 16 + .../features/input/InputMappingManager.kt | 149 +++++ .../features/settings/SettingKeys.kt | 34 + .../settings/model/InputMappingSetting.kt | 221 +++++++ .../features/settings/model/Settings.kt | 138 +---- .../model/view/InputBindingSetting.kt | 583 +----------------- .../features/settings/ui/SettingsActivity.kt | 8 - .../settings/ui/SettingsActivityPresenter.kt | 2 +- .../features/settings/ui/SettingsAdapter.kt | 30 +- .../settings/ui/SettingsFragmentPresenter.kt | 55 +- .../InputBindingSettingViewHolder.kt | 6 +- .../features/settings/utils/SettingsFile.kt | 6 +- .../fragments/AutoMapDialogFragment.kt | 14 +- .../citra_emu/fragments/EmulationFragment.kt | 2 + .../MotionBottomSheetDialogFragment.kt | 38 +- .../citra/citra_emu/overlay/InputOverlay.kt | 2 +- .../utils/ControllerMappingHelper.kt | 69 --- src/android/app/src/main/jni/config.cpp | 18 +- src/android/app/src/main/jni/default_ini.h | 34 + 25 files changed, 1101 insertions(+), 1067 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/input/GamepadHelper.kt rename src/android/app/src/main/java/org/citra/citra_emu/features/{hotkeys => input}/Hotkey.kt (89%) rename src/android/app/src/main/java/org/citra/citra_emu/features/{hotkeys => input}/HotkeyUtility.kt (63%) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/input/Input.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/input/InputMappingManager.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/InputMappingSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt diff --git a/CMakeModules/GenerateSettingKeys.cmake b/CMakeModules/GenerateSettingKeys.cmake index 4fe58113c..28d08ceb8 100644 --- a/CMakeModules/GenerateSettingKeys.cmake +++ b/CMakeModules/GenerateSettingKeys.cmake @@ -234,6 +234,37 @@ if (ANDROID) "android_hide_images" "screen_orientation" "performance_overlay_position" + "button_a" + "button_b" + "button_x" + "button_y" + "button_home" + "circlepad_up" + "circlepad_down" + "circlepad_left" + "circlepad_right" + "button_r" + "button_l" + "button_zr" + "button_zl" + "button_start" + "button_select" + "dpad_up" + "dpad_down" + "dpad_left" + "dpad_right" + "cstick_up" + "cstick_down" + "cstick_left" + "cstick_right" + "hotkey_cycle_layout" + "hotkey_close" + "hotkey_swap" + "hotkey_pause_resume" + "hotkey_quicksave" + "hotkey_turbo_limit" + "hotkey_quickload" + "hotkey_enable" ) string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY}) set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",") diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 9d2015baa..c917011d1 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -959,6 +959,7 @@ object NativeLibrary { const val DPAD = 780 const val BUTTON_DEBUG = 781 const val BUTTON_GPIO14 = 782 + // TODO: replace these with Hotkey buttons - they aren't native! const val BUTTON_SWAP = 800 const val BUTTON_TURBO = 801 } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index c764054e6..df233afe5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -7,7 +7,6 @@ package org.citra.citra_emu.activities import android.Manifest.permission import android.annotation.SuppressLint import android.content.Intent -import android.content.SharedPreferences import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle @@ -26,25 +25,23 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.navigation.fragment.NavHostFragment -import androidx.preference.PreferenceManager -import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.NativeLibrary.ButtonState import org.citra.citra_emu.R import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult import org.citra.citra_emu.contracts.OpenFileResultContract import org.citra.citra_emu.databinding.ActivityEmulationBinding import org.citra.citra_emu.display.ScreenAdjustmentUtil import org.citra.citra_emu.display.SecondaryDisplay -import org.citra.citra_emu.features.hotkeys.HotkeyUtility +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.input.HotkeyUtility import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.Settings -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.fragments.EmulationFragment import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.model.Game import org.citra.citra_emu.utils.BuildUtil -import org.citra.citra_emu.utils.ControllerMappingHelper import org.citra.citra_emu.utils.FileBrowserHelper import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.utils.EmulationMenuSettings @@ -53,10 +50,9 @@ import org.citra.citra_emu.utils.RefreshRateUtil import org.citra.citra_emu.utils.ThemeUtil import org.citra.citra_emu.viewmodel.EmulationViewModel import org.citra.citra_emu.features.settings.utils.SettingsFile +import kotlin.math.abs class EmulationActivity : AppCompatActivity() { - private val preferences: SharedPreferences - get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) var isActivityRecreated = false val emulationViewModel: EmulationViewModel by viewModels() private lateinit var binding: ActivityEmulationBinding @@ -288,13 +284,13 @@ class EmulationActivity : AppCompatActivity() { return true } } - return hotkeyUtility.handleKeyPress(event) + return hotkeyUtility.handleKeyPress(if (event.keyCode == 0) event.scanCode else event.keyCode, event.device.descriptor) } KeyEvent.ACTION_UP -> { - return hotkeyUtility.handleKeyRelease(event) + return hotkeyUtility.handleKeyRelease(if (event.keyCode == 0) event.scanCode else event.keyCode, event.device.descriptor) } else -> { - return false; + return false } } } @@ -314,7 +310,8 @@ class EmulationActivity : AppCompatActivity() { // TODO: Move this check into native code - prevents crash if input pressed before starting emulation if (!NativeLibrary.isRunning() || (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) || - emulationFragment.isDrawerOpen()) { + emulationFragment.isDrawerOpen() + ) { return super.dispatchGenericMotionEvent(event) } @@ -322,206 +319,76 @@ class EmulationActivity : AppCompatActivity() { if (event.actionMasked == MotionEvent.ACTION_CANCEL) { return true } - val input = event.device - val motions = input.motionRanges - val axisValuesCirclePad = floatArrayOf(0.0f, 0.0f) - val axisValuesCStick = floatArrayOf(0.0f, 0.0f) - val axisValuesDPad = floatArrayOf(0.0f, 0.0f) - var isTriggerPressedLMapped = false - var isTriggerPressedRMapped = false - var isTriggerPressedZLMapped = false - var isTriggerPressedZRMapped = false - var isTriggerPressedL = false - var isTriggerPressedR = false - var isTriggerPressedZL = false - var isTriggerPressedZR = false - for (range in motions) { + val device = event.device + val manager = emulationViewModel.settings.inputMappingManager + + val stickAccumulator = HashMap>() + + for (range in device.motionRanges) { val axis = range.axis val origValue = event.getAxisValue(axis) - var value = ControllerMappingHelper.scaleAxis(input, axis, origValue) - val nextMapping = - preferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1) - val guestOrientation = - preferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1) - val inverted = preferences.getBoolean(InputBindingSetting.getInputAxisInvertedKey(axis),false); - if (nextMapping == -1 || guestOrientation == -1) { - // Axis is unmapped + var value = GamepadHelper.scaleAxis(device, axis, origValue) + if (value > -0.1f && value < 0.1f) value = 0f + + val axisPair = Pair(axis, if (value >= 0f) 1 else -1) + + // special case where the axis value is 0 - we need to send releases to both directions + if (value == 0f) { + listOf(Pair(axis, 1), Pair(axis, -1)).forEach { zeroPair -> + manager.getOutAxesForAxis(zeroPair).forEach { (outAxis, outDir) -> + val component = + GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach + val current = + stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f)) + stickAccumulator[component.joystickType] = if (component.isVertical) { + Pair(current.first, 0f) + } else { + Pair(0f, current.second) + } + } + manager.getOutButtonsForAxis(zeroPair).forEach { outButton -> + NativeLibrary.onGamePadEvent( + device.descriptor, + outButton, + ButtonState.RELEASED + ) + } + } continue } - if (value > 0f && value < 0.1f || value < 0f && value > -0.1f) { - // Skip joystick wobble - value = 0f + // Axis to Axis mappings + manager.getOutAxesForAxis(axisPair).forEach { (outAxis, outDir) -> + val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach + val current = + stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f)) + val contribution = abs(value) * outDir + stickAccumulator[component.joystickType] = if (component.isVertical) { + Pair(current.first, contribution) + } else { + Pair(contribution, current.second) + } } - if (inverted) value = -value; - when (nextMapping) { - NativeLibrary.ButtonType.STICK_LEFT -> { - axisValuesCirclePad[guestOrientation] = value - } - - NativeLibrary.ButtonType.STICK_C -> { - axisValuesCStick[guestOrientation] = value - } - - NativeLibrary.ButtonType.DPAD -> { - axisValuesDPad[guestOrientation] = value - } - - NativeLibrary.ButtonType.TRIGGER_L -> { - isTriggerPressedLMapped = true - isTriggerPressedL = value != 0f - } - - NativeLibrary.ButtonType.TRIGGER_R -> { - isTriggerPressedRMapped = true - isTriggerPressedR = value != 0f - } - - NativeLibrary.ButtonType.BUTTON_ZL -> { - isTriggerPressedZLMapped = true - isTriggerPressedZL = value != 0f - } - - NativeLibrary.ButtonType.BUTTON_ZR -> { - isTriggerPressedZRMapped = true - isTriggerPressedZR = value != 0f + // Axis to Button mappings + manager.getOutButtonsForAxis(axisPair).forEach { button -> + val mapping = manager.getMappingForButton(button) + if (abs(value) > (mapping?.threshold ?: 0.5f)) { + hotkeyUtility.handleKeyPress(button, device.descriptor) + NativeLibrary.onGamePadEvent(device.descriptor, button, ButtonState.PRESSED) + } else { + NativeLibrary.onGamePadEvent( + device.descriptor, + button, + ButtonState.RELEASED + ) } } } - // Circle-Pad and C-Stick status - NativeLibrary.onGamePadMoveEvent( - input.descriptor, - NativeLibrary.ButtonType.STICK_LEFT, - axisValuesCirclePad[0], - axisValuesCirclePad[1] - ) - NativeLibrary.onGamePadMoveEvent( - input.descriptor, - NativeLibrary.ButtonType.STICK_C, - axisValuesCStick[0], - axisValuesCStick[1] - ) - - // Triggers L/R and ZL/ZR - if (isTriggerPressedLMapped) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.TRIGGER_L, - if (isTriggerPressedL) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - ) - } - if (isTriggerPressedRMapped) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.TRIGGER_R, - if (isTriggerPressedR) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - ) - } - if (isTriggerPressedZLMapped) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.BUTTON_ZL, - if (isTriggerPressedZL) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - ) - } - if (isTriggerPressedZRMapped) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.BUTTON_ZR, - if (isTriggerPressedZR) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - ) + stickAccumulator.forEach { (outAxis, value) -> + NativeLibrary.onGamePadMoveEvent(device.descriptor, outAxis, value.first, value.second) } - // Work-around to allow D-pad axis to be bound to emulated buttons - if (axisValuesDPad[0] == 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_LEFT, - NativeLibrary.ButtonState.RELEASED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_RIGHT, - NativeLibrary.ButtonState.RELEASED - ) - } - if (axisValuesDPad[0] < 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_LEFT, - NativeLibrary.ButtonState.PRESSED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_RIGHT, - NativeLibrary.ButtonState.RELEASED - ) - } - if (axisValuesDPad[0] > 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_LEFT, - NativeLibrary.ButtonState.RELEASED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_RIGHT, - NativeLibrary.ButtonState.PRESSED - ) - } - if (axisValuesDPad[1] == 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_UP, - NativeLibrary.ButtonState.RELEASED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_DOWN, - NativeLibrary.ButtonState.RELEASED - ) - } - if (axisValuesDPad[1] < 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_UP, - NativeLibrary.ButtonState.PRESSED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_DOWN, - NativeLibrary.ButtonState.RELEASED - ) - } - if (axisValuesDPad[1] > 0f) { - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_UP, - NativeLibrary.ButtonState.RELEASED - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - NativeLibrary.ButtonType.DPAD_DOWN, - NativeLibrary.ButtonState.PRESSED - ) - } return true } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/input/GamepadHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/GamepadHelper.kt new file mode 100644 index 000000000..ed32d9988 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/GamepadHelper.kt @@ -0,0 +1,374 @@ +package org.citra.citra_emu.features.input + +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.R +import org.citra.citra_emu.features.settings.model.InputMappingSetting +import org.citra.citra_emu.features.settings.model.Settings + +object GamepadHelper { + private const val BUTTON_NAME_L3 = "Button L3" + private const val BUTTON_NAME_R3 = "Button R3" + private const val NINTENDO_VENDOR_ID = 0x057e + + // Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as + // KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't + // translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to + // the scan code in that case. + private const val LINUX_BTN_DPAD_UP = 0x220 // 544 + private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545 + private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546 + private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547 + + + private val buttonNameOverrides = mapOf( + KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3, + KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3, + LINUX_BTN_DPAD_UP to "Dpad Up", + LINUX_BTN_DPAD_DOWN to "Dpad Down", + LINUX_BTN_DPAD_LEFT to "Dpad Left", + LINUX_BTN_DPAD_RIGHT to "Dpad Right" + ) + + fun getButtonName(keyCode: Int): String = + buttonNameOverrides[keyCode] + ?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_")) + + fun getAxisName(axis: Int, direction: Int?): String = "Axis " + axis + direction?.let { if(it > 0) "+" else "-" } + + private fun toTitleCase(raw: String): String = + raw.replace("_", " ").lowercase() + .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + + private data class DefaultButtonMapping( + val setting: InputMappingSetting, + val hostKeyCode: Int + ) + // Auto-map always sets inverted = false. Users needing inverted axes should remap manually. + private data class DefaultAxisMapping( + val setting: InputMappingSetting, + val hostAxis: Int, + val hostDirection: Int + ) + + private val xboxFaceButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_B), + DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_A), + DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y), + DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X) + ) + + private val nintendoFaceButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_A), + DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_B), + DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_X), + DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y) + ) + + private val commonButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1), + DefaultButtonMapping(InputMappingSetting.BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1), + DefaultButtonMapping(InputMappingSetting.BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2), + DefaultButtonMapping(InputMappingSetting.BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2), + DefaultButtonMapping(InputMappingSetting.BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT), + DefaultButtonMapping(InputMappingSetting.BUTTON_START, KeyEvent.KEYCODE_BUTTON_START) + ) + + private val dpadButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.DPAD_UP, KeyEvent.KEYCODE_DPAD_UP), + DefaultButtonMapping(InputMappingSetting.DPAD_DOWN, KeyEvent.KEYCODE_DPAD_DOWN), + DefaultButtonMapping(InputMappingSetting.DPAD_LEFT, KeyEvent.KEYCODE_DPAD_LEFT), + DefaultButtonMapping(InputMappingSetting.DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT) + ) + + private val stickAxisMappings = listOf( + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_LEFT, MotionEvent.AXIS_X, -1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_RIGHT, MotionEvent.AXIS_X, 1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_UP, MotionEvent.AXIS_Y, -1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_DOWN, MotionEvent.AXIS_Y, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_LEFT, MotionEvent.AXIS_Z, -1), + DefaultAxisMapping(InputMappingSetting.CSTICK_RIGHT, MotionEvent.AXIS_Z, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_UP, MotionEvent.AXIS_RZ, -1), + DefaultAxisMapping(InputMappingSetting.CSTICK_DOWN, MotionEvent.AXIS_RZ, 1) + ) + + private val dpadAxisMappings = listOf( + DefaultAxisMapping(InputMappingSetting.DPAD_UP, MotionEvent.AXIS_HAT_Y, -1), + DefaultAxisMapping(InputMappingSetting.DPAD_DOWN, MotionEvent.AXIS_HAT_Y, 1), + DefaultAxisMapping(InputMappingSetting.DPAD_LEFT, MotionEvent.AXIS_HAT_X, -1), + DefaultAxisMapping(InputMappingSetting.DPAD_RIGHT, MotionEvent.AXIS_HAT_X, 1) + ) + + // Nintendo Switch Joy-Con specific mappings. + // Joy-Cons connected via Bluetooth on Android have several quirks: + // - They register as two separate InputDevices (left and right) + // - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A) + // but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y) + // - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes + // - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ + + // Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not. + // This is different from both the standard Xbox table (full swap) and the + // Nintendo table (no swap). + private val joyconFaceButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_B), + DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_A), + DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_X), + DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y) + ) + + // Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN + private val joyconDpadButtonMappings = listOf( + DefaultButtonMapping(InputMappingSetting.DPAD_UP, LINUX_BTN_DPAD_UP), + DefaultButtonMapping(InputMappingSetting.DPAD_DOWN, LINUX_BTN_DPAD_DOWN), + DefaultButtonMapping(InputMappingSetting.DPAD_LEFT, LINUX_BTN_DPAD_LEFT), + DefaultButtonMapping(InputMappingSetting.DPAD_RIGHT, LINUX_BTN_DPAD_RIGHT) + ) + + // Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY + // (not Z/RZ like most controllers). The horizontal axis is inverted relative to + // the standard orientation - verified empirically on paired Joy-Cons via Bluetooth. + private val joyconStickAxisMappings = listOf( + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_UP, MotionEvent.AXIS_Y, -1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_DOWN, MotionEvent.AXIS_Y, 1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_LEFT, MotionEvent.AXIS_X, -1), + DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_RIGHT, MotionEvent.AXIS_X, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_UP, MotionEvent.AXIS_RY, -1), + DefaultAxisMapping(InputMappingSetting.CSTICK_DOWN, MotionEvent.AXIS_RY, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_LEFT, MotionEvent.AXIS_RX, 1), + DefaultAxisMapping(InputMappingSetting.CSTICK_RIGHT, MotionEvent.AXIS_RX, -1) + ) + + /** + * Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a + * Pro Controller or other Nintendo device) by checking vendor ID + device + * capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the + * right stick, while the Pro Controller has standard HAT axes and Z/RZ. + */ + fun isJoyCon(device: InputDevice?): Boolean { + if (device == null) return false + if (device.vendorId != NINTENDO_VENDOR_ID) return false + + // Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick). + // Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ. + var hasHatAxes = false + var hasStandardRightStick = false + for (range in device.motionRanges) { + when (range.axis) { + MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true + MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true + } + } + return !hasHatAxes && !hasStandardRightStick + } + + fun clearAllBindings(settings: Settings) { + InputMappingSetting.values().forEach { settings.set(it, Input()) } + } + + private fun applyBindings( + settings: Settings, + buttonMappings: List, + axisMappings: List + ) { + + buttonMappings.forEach { mapping -> + settings.set(mapping.setting, Input(key = mapping.hostKeyCode)) + } + axisMappings.forEach { mapping -> + settings.set( + mapping.setting, + Input( + axis = mapping.hostAxis, + direction = mapping.hostDirection + ) + ) + } + } + + /** + * Applies Joy-Con specific bindings: scan code D-pad, partial face button + * swap, and AXIS_RX/RY right stick. + */ + fun applyJoyConBindings(settings: Settings) { + applyBindings( + settings, + joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings, + joyconStickAxisMappings + ) + } + + /** + * Applies auto-mapped bindings based on detected controller layout and d-pad type. + * + * @param isNintendoLayout true if the controller uses Nintendo face button layout + * (A=east, B=south), false for Xbox layout (A=south, B=east) + * @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y), + * false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT) + */ + fun applyAutoMapBindings(settings: Settings, isNintendoLayout: Boolean, useAxisDpad: Boolean) { + val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings + val buttonMappings = if (useAxisDpad) { + faceButtons + commonButtonMappings + } else { + faceButtons + commonButtonMappings + dpadButtonMappings + } + val axisMappings = if (useAxisDpad) { + stickAxisMappings + dpadAxisMappings + } else { + stickAxisMappings + } + applyBindings(settings,buttonMappings, axisMappings) + } + + /** + * Some controllers report extra button presses that can be ignored. + */ + fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean { + return if (isDualShock4(inputDevice)) { + // The two analog triggers generate analog motion events as well as a keycode. + // We always prefer to use the analog values, so throw away the button press + keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2 + } else false + } + + /** + * Scale an axis to be zero-centered with a proper range. + */ + fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float { + if (isDualShock4(inputDevice)) { + // Android doesn't have correct mappings for this controller's triggers. It reports them + // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] + // Scale them to properly zero-centered with a range of [0.0, 1.0]. + if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { + return (value + 1) / 2.0f + } + } else if (isXboxOneWireless(inputDevice)) { + // Same as the DualShock 4, the mappings are missing. + if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { + return (value + 1) / 2.0f + } + if (axis == MotionEvent.AXIS_GENERIC_1) { + // This axis is stuck at ~.5. Ignore it. + return 0.0f + } + } else if (isMogaPro2Hid(inputDevice)) { + // This controller has a broken axis that reports a constant value. Ignore it. + if (axis == MotionEvent.AXIS_GENERIC_1) { + return 0.0f + } + } + return value + } + + private fun isDualShock4(inputDevice: InputDevice): Boolean { + // Sony DualShock 4 controller + return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc + } + + private fun isXboxOneWireless(inputDevice: InputDevice): Boolean { + // Microsoft Xbox One controller + return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0 + } + + private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean { + // Moga Pro 2 HID + return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271 + } + + data class JoystickComponent(val joystickType: Int, val isVertical: Boolean) + + fun getJoystickComponent(buttonType: Int): JoystickComponent? = when (buttonType) { + NativeLibrary.ButtonType.STICK_LEFT_UP, + NativeLibrary.ButtonType.STICK_LEFT_DOWN -> JoystickComponent(NativeLibrary.ButtonType.STICK_LEFT, true) + NativeLibrary.ButtonType.STICK_LEFT_LEFT, + NativeLibrary.ButtonType.STICK_LEFT_RIGHT -> JoystickComponent(NativeLibrary.ButtonType.STICK_LEFT, false) + NativeLibrary.ButtonType.STICK_C_UP, + NativeLibrary.ButtonType.STICK_C_DOWN -> JoystickComponent(NativeLibrary.ButtonType.STICK_C, true) + NativeLibrary.ButtonType.STICK_C_LEFT, + NativeLibrary.ButtonType.STICK_C_RIGHT -> JoystickComponent(NativeLibrary.ButtonType.STICK_C, false) + else -> null + } + + val buttonKeys = listOf( + InputMappingSetting.BUTTON_A, + InputMappingSetting.BUTTON_B, + InputMappingSetting.BUTTON_X, + InputMappingSetting.BUTTON_Y, + InputMappingSetting.BUTTON_SELECT, + InputMappingSetting.BUTTON_START, + InputMappingSetting.BUTTON_HOME + ) + val buttonTitles = listOf( + R.string.button_a, + R.string.button_b, + R.string.button_x, + R.string.button_y, + R.string.button_select, + R.string.button_start, + R.string.button_home + ) + val circlePadKeys = listOf( + InputMappingSetting.CIRCLEPAD_UP, + InputMappingSetting.CIRCLEPAD_DOWN, + InputMappingSetting.CIRCLEPAD_LEFT, + InputMappingSetting.CIRCLEPAD_RIGHT + ) + val cStickKeys = listOf( + InputMappingSetting.CSTICK_UP, + InputMappingSetting.CSTICK_DOWN, + InputMappingSetting.CSTICK_LEFT, + InputMappingSetting.CSTICK_RIGHT + ) + + val dPadButtonKeys = listOf( + InputMappingSetting.DPAD_UP, + InputMappingSetting.DPAD_DOWN, + InputMappingSetting.DPAD_LEFT, + InputMappingSetting.DPAD_RIGHT + ) + + val dPadTitles = listOf( + R.string.direction_up, + R.string.direction_down, + R.string.direction_left, + R.string.direction_right + ) + val axisTitles = dPadTitles + + val triggerKeys = listOf( + InputMappingSetting.BUTTON_L, + InputMappingSetting.BUTTON_R, + InputMappingSetting.BUTTON_ZL, + InputMappingSetting.BUTTON_ZR + ) + val triggerTitles = listOf( + R.string.button_l, + R.string.button_r, + R.string.button_zl, + R.string.button_zr + ) + val hotKeys = listOf( + InputMappingSetting.HOTKEY_ENABLE, + InputMappingSetting.HOTKEY_SWAP, + InputMappingSetting.HOTKEY_CYCLE_LAYOUT, + InputMappingSetting.HOTKEY_CLOSE_GAME, + InputMappingSetting.HOTKEY_PAUSE_OR_RESUME, + InputMappingSetting.HOTKEY_QUICKSAVE, + InputMappingSetting.HOTKEY_QUICKLOAD, + InputMappingSetting.HOTKEY_TURBO_LIMIT + ) + val hotkeyTitles = listOf( + R.string.controller_hotkey_enable_button, + R.string.emulation_swap_screens, + R.string.emulation_cycle_landscape_layouts, + R.string.emulation_close_game, + R.string.emulation_toggle_pause, + R.string.emulation_quicksave, + R.string.emulation_quickload, + R.string.turbo_limit_hotkey + ) + +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/Hotkey.kt similarity index 89% rename from src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt rename to src/android/app/src/main/java/org/citra/citra_emu/features/input/Hotkey.kt index e2319a7e4..c347659f0 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/Hotkey.kt @@ -2,7 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -package org.citra.citra_emu.features.hotkeys +package org.citra.citra_emu.features.input enum class Hotkey(val button: Int) { SWAP_SCREEN(10001), diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/HotkeyUtility.kt similarity index 63% rename from src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt rename to src/android/app/src/main/java/org/citra/citra_emu/features/input/HotkeyUtility.kt index d57ff5e8b..72e0ffd49 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/HotkeyUtility.kt @@ -2,7 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -package org.citra.citra_emu.features.hotkeys +package org.citra.citra_emu.features.input import android.content.Context import android.view.KeyEvent @@ -16,6 +16,7 @@ import org.citra.citra_emu.utils.TurboHelper import org.citra.citra_emu.display.ScreenAdjustmentUtil import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.features.settings.model.Settings +import kotlin.math.abs class HotkeyUtility( private val screenAdjustmentUtil: ScreenAdjustmentUtil, @@ -27,17 +28,18 @@ class HotkeyUtility( private var hotkeyIsEnabled = false var hotkeyIsPressed = false private val currentlyPressedButtons = mutableSetOf() + /** Store which axis directions are currently pressed as (axis, direction) pairs. */ + private val pressedAxisDirections = HashSet>() // (outAxis, outDir) - fun handleKeyPress(keyEvent: KeyEvent): Boolean { + fun handleKeyPress(key: Int, descriptor: String): Boolean { var handled = false - val buttonSet = InputBindingSetting.getButtonSet(keyEvent) - val enableButton = - PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - .getString(Settings.HOTKEY_ENABLE, "") + val buttonSet = settings.inputMappingManager.getOutButtonsForKey(key) + val axisSet = settings.inputMappingManager.getOutAxesForKey(key) + val enableButtonMapped = settings.inputMappingManager.getMappingForButton(Hotkey.ENABLE.button) != null val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button) val thisKeyIsHotkey = !thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) } - hotkeyIsEnabled = hotkeyIsEnabled || enableButton == "" || thisKeyIsEnableButton + hotkeyIsEnabled = hotkeyIsEnabled || !enableButtonMapped || thisKeyIsEnableButton // Now process all internal buttons associated with this keypress for (button in buttonSet) { @@ -58,19 +60,23 @@ class HotkeyUtility( // the normal key event. if (!thisKeyIsHotkey || !hotkeyIsEnabled) { handled = NativeLibrary.onGamePadEvent( - keyEvent.device.descriptor, + descriptor, button, NativeLibrary.ButtonState.PRESSED ) || handled } } } + // Handle axes in helper functions + updateAxisStateForKey(axisSet,true) + handled = sendAxisState(descriptor, axisSet) || handled return handled } - fun handleKeyRelease(keyEvent: KeyEvent): Boolean { + fun handleKeyRelease(key: Int, descriptor: String): Boolean { var handled = false - val buttonSet = InputBindingSetting.getButtonSet(keyEvent) + val buttonSet = settings.inputMappingManager.getOutButtonsForKey(key) + val axisSet = settings.inputMappingManager.getOutAxesForKey(key) val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button) val thisKeyIsHotkey = !thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) } @@ -96,7 +102,7 @@ class HotkeyUtility( ) ) { handled = NativeLibrary.onGamePadEvent( - keyEvent.device.descriptor, + descriptor, button, NativeLibrary.ButtonState.RELEASED ) || handled @@ -104,6 +110,8 @@ class HotkeyUtility( } } } + updateAxisStateForKey(axisSet,false) + handled = sendAxisState(descriptor, axisSet) || handled return handled } @@ -142,4 +150,42 @@ class HotkeyUtility( hotkeyIsPressed = true return true } + + private fun updateAxisStateForKey(axisSet: List>, pressed: Boolean) { + axisSet.forEach { (outAxis, outDir) -> + if (pressed) pressedAxisDirections.add(Pair(outAxis, outDir)) + else pressedAxisDirections.remove(Pair(outAxis, outDir)) + } + } + + /** + * Update axis state based on currently pressed buttons + */ + private fun sendAxisState(descriptor: String, affectedAxes: List>): Boolean { + val stickAccumulator = HashMap>() + // to make sure that when both directions are released, we still get a return to 0, but only for + // axes that have been touched by buttons + affectedAxes.forEach { (outAxis, outDir) -> + val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach + stickAccumulator.putIfAbsent(component.joystickType, Pair(0f,0f)) + } + + pressedAxisDirections.forEach{ (outAxis, outDir) -> + val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach + val current = + stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f)) + // if opposite directions of the same stick are held, this will let them cancel each other out + stickAccumulator[component.joystickType] = if (component.isVertical) { + Pair(current.first, current.second + outDir) + } else { + Pair(current.first + outDir, current.second) + } + } + var handled = false + stickAccumulator.forEach { (joystickType, value) -> + handled = NativeLibrary.onGamePadMoveEvent(descriptor, joystickType, value.first, value.second) || handled + } + return handled + + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/input/Input.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/Input.kt new file mode 100644 index 000000000..f2b4b03be --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/Input.kt @@ -0,0 +1,16 @@ +package org.citra.citra_emu.features.input + +/** + * Stores information about a particular input + */ +data class Input( + val key: Int? = null, + val axis: Int? = null, + // +1 or -1 + val direction: Int? = null, + val threshold: Float? = null +) { + val empty: Boolean + get() = key == null && axis == null + +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/input/InputMappingManager.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/input/InputMappingManager.kt new file mode 100644 index 000000000..db676dd6f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/input/InputMappingManager.kt @@ -0,0 +1,149 @@ +package org.citra.citra_emu.features.input + +import org.citra.citra_emu.features.settings.model.InputMappingSetting +import org.citra.citra_emu.features.settings.model.Settings + +class InputMappingManager() { + + /** + * input keys are represented by a single int, and can be mapped to 3ds buttons or axis directions + * input axes are represented by an pair, where direction is either 1 or -1 + * and can be mapped to output axes or an output button + * output keys are a single int + * output axes are also a pair + * + * For now, we are only allowing one output *axis* per input, but the code is designed to change + * that if necessary. There can be more than on output *button* per input because hotkeys are treated + * as buttons and it is possible to map an input to both a 3ds button and a hotkey. + */ + + private val keyToOutButtons = HashMap>() + private val keyToOutAxes = HashMap>>() + + private val axisToOutAxes = HashMap, MutableList>>() + private val axisToOutButtons = HashMap, MutableList>() + + private val outAxisToMapping = HashMap, Input>() + private val buttonToMapping = HashMap() + + + /** Rebuilds the input maps from the given settings instance */ + fun rebuild(settings: Settings) { + clear() + InputMappingSetting.values().forEach { setting -> + val mapping = settings.get(setting) ?: return@forEach + register(setting, mapping) + } + } + + fun clear() { + axisToOutButtons.clear() + buttonToMapping.clear() + axisToOutAxes.clear() + outAxisToMapping.clear() + keyToOutButtons.clear() + keyToOutAxes.clear() + } + + /** Rebind a particular setting */ + fun rebind(setting: InputMappingSetting, newMapping: Input?) { + clearMapping(setting) + if (newMapping != null) register(setting, newMapping) + } + + /** Clear a mapping from all hashmaps */ + fun clearMapping(setting: InputMappingSetting) { + val outPair = if (setting.outAxis != null && setting.outDirection != null) Pair( + setting.outAxis, + setting.outDirection + ) else null + + val oldMapping = if (setting.outKey != null) { + buttonToMapping.get(setting.outKey) + } else if (setting.outAxis != null && setting.outDirection != null) { + outAxisToMapping.get(Pair(setting.outAxis, setting.outDirection)) + } else { + null + } + + val oldPair = if (oldMapping?.axis != null && oldMapping?.direction != null) Pair( + oldMapping.axis, + oldMapping.direction + ) else null + + // if our old mapping was a key, remove its binds + if (oldMapping?.key != null) { + keyToOutButtons[oldMapping.key]?.remove(setting.outKey) + if (outPair != null) { + keyToOutAxes[oldMapping.key]?.remove(outPair) + } + } + // if our old mapping was an axis, remove its binds + if (oldPair != null) { + if (setting.outAxis != null && setting.outDirection != null) + axisToOutAxes[oldPair]?.remove(outPair) + if (setting.outKey != null) + axisToOutButtons[oldPair]?.remove(setting.outKey) + } + + // remove the reverse binds + if (setting.outKey != null) { + buttonToMapping.remove(setting.outKey) + } + if (outPair != null) { + outAxisToMapping.remove(outPair) + } + } + + /** + * Add a single item to the maps based on the value of the InputMapping and the InputMappingSetting + */ + private fun register(setting: InputMappingSetting, mapping: Input) { + val outPair = if (setting.outAxis != null && setting.outDirection != null) Pair( + setting.outAxis, + setting.outDirection + ) else null + + val inPair = if (mapping.axis != null && mapping.direction != null) Pair( + mapping.axis, + mapping.direction + ) else null + + if (setting.outKey != null) { + if (mapping.key != null) + keyToOutButtons.getOrPut(mapping.key, { mutableListOf() }).add(setting.outKey) + if (inPair != null) { + if (setting.outKey != null) + axisToOutButtons.getOrPut(inPair, { mutableListOf() }).add(setting.outKey) + } + buttonToMapping[setting.outKey] = mapping + } + if (outPair != null) { + if (mapping.key != null) + keyToOutAxes.getOrPut(mapping.key, { mutableListOf() }) + .add(outPair) + if (inPair != null) + axisToOutAxes.getOrPut(inPair, { mutableListOf() }) + .add(outPair) + outAxisToMapping[outPair] = mapping + } + } + + fun getOutAxesForAxis(pair: Pair): List> = + axisToOutAxes[pair] ?: emptyList() + + fun getOutButtonsForAxis(pair: Pair): List = + axisToOutButtons[pair] ?: emptyList() + + fun getMappingForOutAxis(pair: Pair): Input? = + outAxisToMapping[pair] + + fun getOutButtonsForKey(keyCode: Int): List = + keyToOutButtons[keyCode] ?: emptyList() + + fun getOutAxesForKey(keyCode: Int): List> = + keyToOutAxes[keyCode] ?: emptyList() + + fun getMappingForButton(outKey: Int): Input? = + buttonToMapping[outKey] +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt index 1d6e0dcee..6280455a9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt @@ -138,4 +138,38 @@ object SettingKeys { external fun android_hide_images(): String external fun screen_orientation(): String external fun performance_overlay_position(): String + + external fun button_a(): String + external fun button_b(): String + external fun button_x(): String + external fun button_y(): String + external fun button_home(): String + external fun circlepad_up(): String + external fun circlepad_down(): String + external fun circlepad_left(): String + external fun circlepad_right(): String + external fun button_r(): String + external fun button_l(): String + external fun button_zr(): String + external fun button_zl(): String + external fun button_start(): String + external fun button_select(): String + external fun dpad_up(): String + external fun dpad_down(): String + external fun dpad_left(): String + external fun dpad_right(): String + external fun cstick_up(): String + external fun cstick_down(): String + external fun cstick_left(): String + external fun cstick_right(): String + external fun hotkey_cycle_layout(): String + external fun hotkey_close(): String + external fun hotkey_swap(): String + external fun hotkey_pause_resume(): String + external fun hotkey_quicksave(): String + external fun hotkey_turbo_limit(): String + external fun hotkey_quickload(): String + external fun hotkey_enable(): String + + } \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/InputMappingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/InputMappingSetting.kt new file mode 100644 index 000000000..24dd03073 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/InputMappingSetting.kt @@ -0,0 +1,221 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.features.input.Input +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.input.Hotkey +import org.citra.citra_emu.features.settings.SettingKeys + +enum class InputMappingSetting( + override val key: String, + override val section: String, + override val defaultValue: Input = Input(), + val outKey: Int? = null, + val outAxis: Int? = null, + val outDirection: Int? = null +) : AbstractSetting { + BUTTON_A( + SettingKeys.button_a(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_A + ), + BUTTON_B( + SettingKeys.button_b(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_B + ), + BUTTON_X( + SettingKeys.button_x(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_X + ), + BUTTON_Y( + SettingKeys.button_y(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_Y + ), + BUTTON_HOME( + SettingKeys.button_home(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_HOME + ), + BUTTON_L( + SettingKeys.button_l(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.TRIGGER_L + ), + BUTTON_R( + SettingKeys.button_r(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.TRIGGER_R + ), + BUTTON_SELECT( + SettingKeys.button_select(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_SELECT + ), + BUTTON_START( + SettingKeys.button_start(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_START + ), + BUTTON_ZL( + SettingKeys.button_zl(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_ZL + ), + BUTTON_ZR( + SettingKeys.button_zr(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.BUTTON_ZR + ), + DPAD_UP( + SettingKeys.dpad_up(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.DPAD_UP + ), + DPAD_DOWN( + SettingKeys.dpad_down(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.DPAD_DOWN + ), + DPAD_LEFT( + SettingKeys.dpad_left(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.DPAD_LEFT + ), + DPAD_RIGHT( + SettingKeys.dpad_right(), + Settings.SECTION_CONTROLS, + outKey = NativeLibrary.ButtonType.DPAD_RIGHT + ), + + CIRCLEPAD_UP( + SettingKeys.circlepad_up(), + Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_LEFT_UP, + outDirection = -1 + ), + CIRCLEPAD_DOWN( + SettingKeys.circlepad_down(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_LEFT_DOWN, outDirection = 1 + ), + CIRCLEPAD_LEFT( + SettingKeys.circlepad_left(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_LEFT_LEFT, outDirection = -1 + ), + CIRCLEPAD_RIGHT( + SettingKeys.circlepad_right(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_LEFT_RIGHT, outDirection = 1 + ), + CSTICK_UP( + SettingKeys.cstick_up(), + Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_C_UP, + outDirection = -1 + ), + CSTICK_DOWN( + SettingKeys.cstick_down(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_C_DOWN, outDirection = 1 + ), + CSTICK_LEFT( + SettingKeys.cstick_left(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_C_LEFT, outDirection = -1 + ), + CSTICK_RIGHT( + SettingKeys.cstick_right(), Settings.SECTION_CONTROLS, + outAxis = NativeLibrary.ButtonType.STICK_C_RIGHT, outDirection = 1 + ), + HOTKEY_CYCLE_LAYOUT( + SettingKeys.hotkey_cycle_layout(), Settings.SECTION_CONTROLS, + outKey = Hotkey.CYCLE_LAYOUT.button + ), + HOTKEY_CLOSE_GAME( + SettingKeys.hotkey_close(), Settings.SECTION_CONTROLS, + outKey = Hotkey.CLOSE_GAME.button + ), + HOTKEY_SWAP( + SettingKeys.hotkey_swap(), Settings.SECTION_CONTROLS, + outKey = Hotkey.SWAP_SCREEN.button + ), + HOTKEY_PAUSE_OR_RESUME( + SettingKeys.hotkey_pause_resume(), Settings.SECTION_CONTROLS, + outKey = Hotkey.PAUSE_OR_RESUME.button + ), + HOTKEY_QUICKSAVE( + SettingKeys.hotkey_quicksave(), Settings.SECTION_CONTROLS, + outKey = Hotkey.QUICKSAVE.button + ), + HOTKEY_TURBO_LIMIT( + SettingKeys.hotkey_turbo_limit(), Settings.SECTION_CONTROLS, + outKey = Hotkey.TURBO_LIMIT.button + ), + HOTKEY_QUICKLOAD( + SettingKeys.hotkey_quickload(), Settings.SECTION_CONTROLS, + outKey = Hotkey.QUICKLOAD.button + ), + HOTKEY_ENABLE( + SettingKeys.hotkey_enable(), Settings.SECTION_CONTROLS, + outKey = Hotkey.ENABLE.button + ); + + + /** Parse a configuration string into an input binding */ + override fun valueFromString(string: String): Input { + if (string.isBlank()) return defaultValue + + val params = string.split(",") + .mapNotNull { part -> + val split = part.split(":", limit = 2) + if (split.size < 2) null else split[0] to split[1] + }.toMap() + if (params["engine"] != "gamepad") return defaultValue + return Input( + key = params["code"]?.toIntOrNull(), + axis = params["axis"]?.toIntOrNull(), + direction = params["direction"]?.toIntOrNull(), + threshold = params["threshold"]?.toFloatOrNull(), + ).takeIf { it.key != null || it.axis != null } ?: defaultValue + } + + + /** Create a configuration string from an input binding */ + override fun valueToString(binding: Input?): String { + binding ?: return "" + if (binding.empty) return "" + val ret = "engine:gamepad" + return ret + when { + binding.key != null -> ",code:${binding.key}" + binding.axis != null -> buildString { + append(",axis:${binding.axis}") + binding.threshold?.let { append(",threshold:$it") } + binding.direction?.let { append(",direction:$it") } + } + + else -> "" + } + } + + /** What will display is different from what is saved in this case */ + fun displayValue(binding: Input?): String { + if (binding?.key != null) { + return GamepadHelper.getButtonName(binding.key) + } else if (binding?.axis != null) { + return GamepadHelper.getAxisName(binding.axis, binding.direction) + } else { + return "" + } + } + + override val isRuntimeEditable: Boolean = true + + companion object { + + fun from(key: String): InputMappingSetting? = values().firstOrNull { it.key == key } + + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index c2b54d12e..7c0647760 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -4,12 +4,16 @@ package org.citra.citra_emu.features.settings.model -import org.citra.citra_emu.R +import org.citra.citra_emu.features.input.Input +import org.citra.citra_emu.features.input.InputMappingManager +import org.citra.citra_emu.features.settings.utils.SettingsFile class Settings { private val globalValues = HashMap() private val perGameOverrides = HashMap() + val inputMappingManager = InputMappingManager() + var gameId: String? = null fun isPerGame(): Boolean = gameId != null && gameId != "" @@ -26,24 +30,33 @@ class Settings { return (globalValues[setting.key] ?: setting.defaultValue) as T } + /** Sets the global value specifically */ fun setGlobal(setting: AbstractSetting, value: T) { globalValues[setting.key] = value as Any + // only update the InputMapping if this global setting actually is in effect now + if (setting is InputMappingSetting && !hasOverride(setting)) { + inputMappingManager.rebind(setting, value as? Input) + } } + /** Sets the override specifically */ fun setOverride(setting: AbstractSetting, value: T) { perGameOverrides[setting.key] = value as Any + if (setting is InputMappingSetting) { + inputMappingManager.rebind(setting, value as? Input) + } } /** Sets the per-game or global setting based on whether this file has ANY per-game setting. - * This should be used, for example, by the Settings Activity + * This should be used by the Custom Settings Activity */ fun set(setting: AbstractSetting, value: T) { if (isPerGame()) setOverride(setting, value) else setGlobal(setting, value) } /** - * Updates an existing setting honoring whether it is *currently* global or local. This will - * be used by the Quick Menu + * Updates an existing setting honoring whether this particular setting is *currently* global or local. + * This should be used by the Quick Menu */ fun update(setting: AbstractSetting, value: T) { if (hasOverride(setting)) setOverride(setting, value) else setGlobal(setting, value) @@ -61,10 +74,15 @@ class Settings { other.perGameOverrides.forEach{ (key, value) -> perGameOverrides[key] = value } + + inputMappingManager.rebuild(this) } fun clearOverride(setting: AbstractSetting) { perGameOverrides.remove(setting.key) + if (setting is InputMappingSetting) { + inputMappingManager.rebind(setting, getGlobal(setting)) + } } fun hasOverride(setting: AbstractSetting<*>): Boolean { @@ -78,10 +96,12 @@ class Settings { fun clearAll() { globalValues.clear() perGameOverrides.clear() + inputMappingManager.clear() } fun clearOverrides() { perGameOverrides.clear() + inputMappingManager.rebuild(this) } fun removePerGameSettings() { @@ -91,7 +111,6 @@ class Settings { companion object { - const val SECTION_CORE = "Core" const val SECTION_SYSTEM = "System" const val SECTION_CAMERA = "Camera" @@ -108,115 +127,6 @@ class Settings { const val SECTION_STORAGE = "Storage" const val SECTION_MISC = "Miscellaneous" - const val KEY_BUTTON_A = "button_a" - const val KEY_BUTTON_B = "button_b" - const val KEY_BUTTON_X = "button_x" - const val KEY_BUTTON_Y = "button_y" - const val KEY_BUTTON_SELECT = "button_select" - const val KEY_BUTTON_START = "button_start" - const val KEY_BUTTON_HOME = "button_home" - const val KEY_BUTTON_UP = "button_up" - const val KEY_BUTTON_DOWN = "button_down" - const val KEY_BUTTON_LEFT = "button_left" - const val KEY_BUTTON_RIGHT = "button_right" - const val KEY_BUTTON_L = "button_l" - const val KEY_BUTTON_R = "button_r" - const val KEY_BUTTON_ZL = "button_zl" - const val KEY_BUTTON_ZR = "button_zr" - const val KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical" - const val KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal" - const val KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical" - const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal" - const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical" - const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal" - const val HOTKEY_ENABLE = "hotkey_enable" - const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap" - const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout" - const val HOTKEY_CLOSE_GAME = "hotkey_close_game" - const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game" - const val HOTKEY_QUICKSAVE = "hotkey_quickload" - const val HOTKEY_QUICKlOAD = "hotkey_quickpause" - const val HOTKEY_TURBO_LIMIT = "hotkey_turbo_limit" - - val buttonKeys = listOf( - KEY_BUTTON_A, - KEY_BUTTON_B, - KEY_BUTTON_X, - KEY_BUTTON_Y, - KEY_BUTTON_SELECT, - KEY_BUTTON_START, - KEY_BUTTON_HOME - ) - val buttonTitles = listOf( - R.string.button_a, - R.string.button_b, - R.string.button_x, - R.string.button_y, - R.string.button_select, - R.string.button_start, - R.string.button_home - ) - val circlePadKeys = listOf( - KEY_CIRCLEPAD_AXIS_VERTICAL, - KEY_CIRCLEPAD_AXIS_HORIZONTAL - ) - val cStickKeys = listOf( - KEY_CSTICK_AXIS_VERTICAL, - KEY_CSTICK_AXIS_HORIZONTAL - ) - val dPadAxisKeys = listOf( - KEY_DPAD_AXIS_VERTICAL, - KEY_DPAD_AXIS_HORIZONTAL - ) - val dPadButtonKeys = listOf( - KEY_BUTTON_UP, - KEY_BUTTON_DOWN, - KEY_BUTTON_LEFT, - KEY_BUTTON_RIGHT - ) - val axisTitles = listOf( - R.string.controller_axis_vertical, - R.string.controller_axis_horizontal - ) - val dPadTitles = listOf( - R.string.direction_up, - R.string.direction_down, - R.string.direction_left, - R.string.direction_right - ) - val triggerKeys = listOf( - KEY_BUTTON_L, - KEY_BUTTON_R, - KEY_BUTTON_ZL, - KEY_BUTTON_ZR - ) - val triggerTitles = listOf( - R.string.button_l, - R.string.button_r, - R.string.button_zl, - R.string.button_zr - ) - val hotKeys = listOf( - HOTKEY_ENABLE, - HOTKEY_SCREEN_SWAP, - HOTKEY_CYCLE_LAYOUT, - HOTKEY_CLOSE_GAME, - HOTKEY_PAUSE_OR_RESUME, - HOTKEY_QUICKSAVE, - HOTKEY_QUICKlOAD, - HOTKEY_TURBO_LIMIT - ) - val hotkeyTitles = listOf( - R.string.controller_hotkey_enable_button, - R.string.emulation_swap_screens, - R.string.emulation_cycle_landscape_layouts, - R.string.emulation_close_game, - R.string.emulation_toggle_pause, - R.string.emulation_quicksave, - R.string.emulation_quickload, - R.string.turbo_limit_hotkey - ) - // TODO: Move these in with the other setting keys in GenerateSettingKeys.cmake const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" const val PREF_MATERIAL_YOU = "MaterialYouTheme" diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index aa67dd15b..532cfa2e9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -4,593 +4,58 @@ package org.citra.citra_emu.features.settings.model.view -import android.content.Context -import android.content.SharedPreferences -import android.view.InputDevice import android.view.InputDevice.MotionRange import android.view.KeyEvent -import android.view.MotionEvent -import android.widget.Toast -import androidx.preference.PreferenceManager -import org.citra.citra_emu.CitraApplication -import org.citra.citra_emu.NativeLibrary -import org.citra.citra_emu.R -import org.citra.citra_emu.features.hotkeys.Hotkey -import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.input.Input +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.settings.model.InputMappingSetting import org.citra.citra_emu.features.settings.model.Settings class InputBindingSetting( - val abstractSetting: AbstractSetting<*>, + val inputSetting: InputMappingSetting, + val settings: Settings, titleId: Int -) : SettingsItem(abstractSetting, titleId, 0) { - private val context: Context get() = CitraApplication.appContext - private val preferences: SharedPreferences - get() = PreferenceManager.getDefaultSharedPreferences(context) - - var value: String - get() = preferences.getString(abstractSetting.key, "")!! - set(string) { - preferences.edit() - .putString(abstractSetting.key, string) - .apply() - } - - /** - * Returns true if this key is for the 3DS Circle Pad - */ - fun isCirclePad(): Boolean = - when (abstractSetting.key) { - Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, - Settings.KEY_CIRCLEPAD_AXIS_VERTICAL -> true - - else -> false - } - - /** - * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad - */ - fun isHorizontalOrientation(): Boolean = - when (abstractSetting.key) { - Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, - Settings.KEY_CSTICK_AXIS_HORIZONTAL, - Settings.KEY_DPAD_AXIS_HORIZONTAL -> true - - else -> false - } - - /** - * Returns true if this key is for the 3DS C-Stick - */ - fun isCStick(): Boolean = - when (abstractSetting.key) { - Settings.KEY_CSTICK_AXIS_HORIZONTAL, - Settings.KEY_CSTICK_AXIS_VERTICAL -> true - - else -> false - } - - /** - * Returns true if this key is for the 3DS D-Pad - */ - fun isDPad(): Boolean = - when (abstractSetting.key) { - Settings.KEY_DPAD_AXIS_HORIZONTAL, - Settings.KEY_DPAD_AXIS_VERTICAL -> true - - else -> false - } - /** - * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real - * triggers on the 3DS, but we support them as such on a physical gamepad. - */ - fun isTrigger(): Boolean = - when (abstractSetting.key) { - Settings.KEY_BUTTON_L, - Settings.KEY_BUTTON_R, - Settings.KEY_BUTTON_ZL, - Settings.KEY_BUTTON_ZR -> true - - else -> false - } - - /** - * Returns true if a gamepad axis can be used to map this key. - */ - fun isAxisMappingSupported(): Boolean { - return isCirclePad() || isCStick() || isDPad() || isTrigger() - } - - /** - * Returns true if a gamepad button can be used to map this key. - */ - fun isButtonMappingSupported(): Boolean { - return !isAxisMappingSupported() || isTrigger() - } - - /** - * Returns the Citra button code for the settings key. - */ - private val buttonCode: Int - get() = - when (abstractSetting.key) { - Settings.KEY_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A - Settings.KEY_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B - Settings.KEY_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X - Settings.KEY_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y - Settings.KEY_BUTTON_L -> NativeLibrary.ButtonType.TRIGGER_L - Settings.KEY_BUTTON_R -> NativeLibrary.ButtonType.TRIGGER_R - Settings.KEY_BUTTON_ZL -> NativeLibrary.ButtonType.BUTTON_ZL - Settings.KEY_BUTTON_ZR -> NativeLibrary.ButtonType.BUTTON_ZR - Settings.KEY_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_SELECT - Settings.KEY_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_START - Settings.KEY_BUTTON_HOME -> NativeLibrary.ButtonType.BUTTON_HOME - Settings.KEY_BUTTON_UP -> NativeLibrary.ButtonType.DPAD_UP - Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN - Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT - Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT - Settings.HOTKEY_ENABLE -> Hotkey.ENABLE.button - Settings.HOTKEY_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button - Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button - Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button - Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button - Settings.HOTKEY_QUICKSAVE -> Hotkey.QUICKSAVE.button - Settings.HOTKEY_QUICKlOAD -> Hotkey.QUICKLOAD.button - Settings.HOTKEY_TURBO_LIMIT -> Hotkey.TURBO_LIMIT.button - else -> -1 - } - - /** - * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old - * settings on re-mapping or clearing of a setting. - */ - private val reverseKey: String +) : SettingsItem(inputSetting, titleId, 0) { + val value: String get() { - var reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${abstractSetting.key}" - if (isAxisMappingSupported() && !isTrigger()) { - // Triggers are the only axis-supported mappings without orientation - reverseKey += "_" + if (isHorizontalOrientation()) { - 0 - } else { - 1 - } - } - return reverseKey + val mapping = settings.get(inputSetting) ?: return "" + return inputSetting.displayValue(mapping) } - /** - * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. - */ - @Suppress("UNCHECKED_CAST") fun removeOldMapping() { - // Try remove all possible keys we wrote for this setting - val oldKey = preferences.getString(reverseKey, "") - if (oldKey != "") { - //settings.set(setting as AbstractSetting,"") - preferences.edit() - .remove(abstractSetting.key) // Used for ui text - .remove(oldKey + "_GuestOrientation") // Used for axis orientation - .remove(oldKey + "_GuestButton") // Used for axis button - .remove(oldKey + "_Inverted") // used for axis inversion - .remove(reverseKey) - val buttonCodes = try { - preferences.getStringSet(oldKey, mutableSetOf())!!.toMutableSet() - } catch (e: ClassCastException) { - // if this is an int pref, either old button or an axis, so just remove it - preferences.edit().remove(oldKey).apply() - return; - } - buttonCodes.remove(buttonCode.toString()); - preferences.edit().putStringSet(oldKey,buttonCodes).apply() - } + /** Default value is cleared */ + settings.set(inputSetting, inputSetting.defaultValue) } /** - * Helper function to write a gamepad button mapping for the setting. - */ - private fun writeButtonMapping(keyEvent: KeyEvent) { - val editor = preferences.edit() - val key = getInputButtonKey(keyEvent) - // Pull in all codes associated with this key - // Migrate from the old int preference if need be - val buttonCodes = InputBindingSetting.getButtonSet(keyEvent) - buttonCodes.add(buttonCode) - // Cleanup old mapping for this setting - removeOldMapping() - - editor.putStringSet(key, buttonCodes.mapTo(mutableSetOf()) {it.toString()}) - - // Write next reverse mapping for future cleanup - editor.putString(reverseKey, key) - - // Apply changes - editor.apply() - } - - /** - * Helper function to write a gamepad axis mapping for the setting. - */ - private fun writeAxisMapping(axis: Int, value: Int, inverted: Boolean) { - // Cleanup old mapping - removeOldMapping() - - // Write new mapping - preferences.edit() - .putInt(getInputAxisOrientationKey(axis), if (isHorizontalOrientation()) 0 else 1) - .putInt(getInputAxisButtonKey(axis), value) - .putBoolean(getInputAxisInvertedKey(axis),inverted) - // Write next reverse mapping for future cleanup - .putString(reverseKey, getInputAxisKey(axis)) - .apply() - } - - /** - * Saves the provided key input setting as an Android preference. + * Saves the provided key input to the setting * * @param keyEvent KeyEvent of this key press. */ fun onKeyInput(keyEvent: KeyEvent) { - if (!isButtonMappingSupported()) { - Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show() - return - } - val code = translateEventToKeyId(keyEvent) - writeButtonMapping(keyEvent) - value = "${keyEvent.device.name}: ${getButtonName(code)}" + val mapping = Input(key = code) + settings.set(inputSetting, mapping) } /** * Saves the provided motion input setting as an Android preference. * - * @param device InputDevice from which the input event originated. * @param motionRange MotionRange of the movement - * @param axisDir Either '-' or '+' + * @param axisDir Either -1 or 1 */ - fun onMotionInput(device: InputDevice, motionRange: MotionRange, axisDir: Char) { - if (!isAxisMappingSupported()) { - Toast.makeText(context, R.string.input_message_button_only, Toast.LENGTH_LONG).show() - return - } - val button = if (isCirclePad()) { - NativeLibrary.ButtonType.STICK_LEFT - } else if (isCStick()) { - NativeLibrary.ButtonType.STICK_C - } else if (isDPad()) { - NativeLibrary.ButtonType.DPAD + fun onMotionInput(motionRange: MotionRange, axisDir: Int) { + val mapping = Input(axis = motionRange.axis, direction = axisDir, threshold = 0.5f) + settings.set(inputSetting, mapping) + } + + private fun translateEventToKeyId(event: KeyEvent): Int { + return if (event.keyCode == 0) { + event.scanCode } else { - buttonCode + event.keyCode } - // use UP (-) to map vertical, but use RIGHT (+) to map horizontal - val inverted = if (isHorizontalOrientation()) axisDir == '-' else axisDir == '+' - writeAxisMapping(motionRange.axis, button, inverted) - value = "Axis ${motionRange.axis}$axisDir" } override val type = TYPE_INPUT_BINDING - - companion object { - private const val INPUT_MAPPING_PREFIX = "InputMapping" - - private fun toTitleCase(raw: String): String = - raw.replace("_", " ").lowercase() - .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } - - private const val BUTTON_NAME_L3 = "Button L3" - private const val BUTTON_NAME_R3 = "Button R3" - - private val buttonNameOverrides = mapOf( - KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3, - KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3, - LINUX_BTN_DPAD_UP to "Dpad Up", - LINUX_BTN_DPAD_DOWN to "Dpad Down", - LINUX_BTN_DPAD_LEFT to "Dpad Left", - LINUX_BTN_DPAD_RIGHT to "Dpad Right" - ) - - fun getButtonName(keyCode: Int): String = - buttonNameOverrides[keyCode] - ?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_")) - - private data class DefaultButtonMapping( - val settingKey: String, - val hostKeyCode: Int, - val guestButtonCode: Int - ) - // Auto-map always sets inverted = false. Users needing inverted axes should remap manually. - private data class DefaultAxisMapping( - val settingKey: String, - val hostAxis: Int, - val guestButton: Int, - val orientation: Int, - val inverted: Boolean - ) - - private val xboxFaceButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), - DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), - DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_X), - DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_Y) - ) - - private val nintendoFaceButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_A), - DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_B), - DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), - DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) - ) - - private val commonButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1, NativeLibrary.ButtonType.TRIGGER_L), - DefaultButtonMapping(Settings.KEY_BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1, NativeLibrary.ButtonType.TRIGGER_R), - DefaultButtonMapping(Settings.KEY_BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2, NativeLibrary.ButtonType.BUTTON_ZL), - DefaultButtonMapping(Settings.KEY_BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2, NativeLibrary.ButtonType.BUTTON_ZR), - DefaultButtonMapping(Settings.KEY_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT, NativeLibrary.ButtonType.BUTTON_SELECT), - DefaultButtonMapping(Settings.KEY_BUTTON_START, KeyEvent.KEYCODE_BUTTON_START, NativeLibrary.ButtonType.BUTTON_START) - ) - - private val dpadButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_UP, KeyEvent.KEYCODE_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), - DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), - DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, KeyEvent.KEYCODE_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), - DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) - ) - - private val stickAxisMappings = listOf( - DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), - DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), - DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_Z, NativeLibrary.ButtonType.STICK_C, 0, false), - DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RZ, NativeLibrary.ButtonType.STICK_C, 1, false) - ) - - private val dpadAxisMappings = listOf( - DefaultAxisMapping(Settings.KEY_DPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_HAT_X, NativeLibrary.ButtonType.DPAD, 0, false), - DefaultAxisMapping(Settings.KEY_DPAD_AXIS_VERTICAL, MotionEvent.AXIS_HAT_Y, NativeLibrary.ButtonType.DPAD, 1, false) - ) - - // Nintendo Switch Joy-Con specific mappings. - // Joy-Cons connected via Bluetooth on Android have several quirks: - // - They register as two separate InputDevices (left and right) - // - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A) - // but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y) - // - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes - // - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ - private const val NINTENDO_VENDOR_ID = 0x057e - - // Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as - // KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't - // translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to - // the scan code in that case. - private const val LINUX_BTN_DPAD_UP = 0x220 // 544 - private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545 - private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546 - private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547 - - // Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not. - // This is different from both the standard Xbox table (full swap) and the - // Nintendo table (no swap). - private val joyconFaceButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), - DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), - DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), - DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) - ) - - // Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN - private val joyconDpadButtonMappings = listOf( - DefaultButtonMapping(Settings.KEY_BUTTON_UP, LINUX_BTN_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), - DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, LINUX_BTN_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), - DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, LINUX_BTN_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), - DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, LINUX_BTN_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) - ) - - // Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY - // (not Z/RZ like most controllers). The horizontal axis is inverted relative to - // the standard orientation - verified empirically on paired Joy-Cons via Bluetooth. - private val joyconStickAxisMappings = listOf( - DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), - DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), - DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_RX, NativeLibrary.ButtonType.STICK_C, 0, true), - DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RY, NativeLibrary.ButtonType.STICK_C, 1, false) - ) - - /** - * Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a - * Pro Controller or other Nintendo device) by checking vendor ID + device - * capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the - * right stick, while the Pro Controller has standard HAT axes and Z/RZ. - */ - fun isJoyCon(device: InputDevice?): Boolean { - if (device == null) return false - if (device.vendorId != NINTENDO_VENDOR_ID) return false - - // Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick). - // Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ. - var hasHatAxes = false - var hasStandardRightStick = false - for (range in device.motionRanges) { - when (range.axis) { - MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true - MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true - } - } - return !hasHatAxes && !hasStandardRightStick - } - - private val allBindingKeys: Set by lazy { - (Settings.buttonKeys + Settings.triggerKeys + - Settings.circlePadKeys + Settings.cStickKeys + Settings.dPadAxisKeys + - Settings.dPadButtonKeys).toSet() - } - - fun clearAllBindings() { - val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - val editor = prefs.edit() - val allKeys = prefs.all.keys.toList() - for (key in allKeys) { - if (key.startsWith(INPUT_MAPPING_PREFIX) || key in allBindingKeys) { - editor.remove(key) - } - } - editor.apply() - } - - private fun applyBindings( - buttonMappings: List, - axisMappings: List - ) { - val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - val editor = prefs.edit() - buttonMappings.forEach { applyDefaultButtonMapping(editor, it) } - axisMappings.forEach { applyDefaultAxisMapping(editor, it) } - editor.apply() - } - - /** - * Applies Joy-Con specific bindings: scan code D-pad, partial face button - * swap, and AXIS_RX/RY right stick. - */ - fun applyJoyConBindings() { - applyBindings( - joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings, - joyconStickAxisMappings - ) - } - - /** - * Applies auto-mapped bindings based on detected controller layout and d-pad type. - * - * @param isNintendoLayout true if the controller uses Nintendo face button layout - * (A=east, B=south), false for Xbox layout (A=south, B=east) - * @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y), - * false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT) - */ - fun applyAutoMapBindings(isNintendoLayout: Boolean, useAxisDpad: Boolean) { - val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings - val buttonMappings = if (useAxisDpad) { - faceButtons + commonButtonMappings - } else { - faceButtons + commonButtonMappings + dpadButtonMappings - } - val axisMappings = if (useAxisDpad) { - stickAxisMappings + dpadAxisMappings - } else { - stickAxisMappings - } - applyBindings(buttonMappings, axisMappings) - } - - private fun applyDefaultButtonMapping( - editor: SharedPreferences.Editor, - mapping: DefaultButtonMapping - ) { - val prefKey = getInputButtonKey(mapping.hostKeyCode) - editor.putInt(prefKey, mapping.guestButtonCode) - editor.putString(mapping.settingKey, getButtonName(mapping.hostKeyCode)) - editor.putString( - "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}", - prefKey - ) - } - - private fun applyDefaultAxisMapping( - editor: SharedPreferences.Editor, - mapping: DefaultAxisMapping - ) { - val axisKey = getInputAxisKey(mapping.hostAxis) - editor.putInt(getInputAxisOrientationKey(mapping.hostAxis), mapping.orientation) - editor.putInt(getInputAxisButtonKey(mapping.hostAxis), mapping.guestButton) - editor.putBoolean(getInputAxisInvertedKey(mapping.hostAxis), mapping.inverted) - val dir = if (mapping.orientation == 0) '+' else '-' - editor.putString(mapping.settingKey, "Axis ${mapping.hostAxis}$dir") - val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}_${mapping.orientation}" - editor.putString(reverseKey, axisKey) - } - - /** - * Returns the settings key for the specified Citra button code. - */ - private fun getButtonKey(buttonCode: Int): String = - when (buttonCode) { - NativeLibrary.ButtonType.BUTTON_A -> Settings.KEY_BUTTON_A - NativeLibrary.ButtonType.BUTTON_B -> Settings.KEY_BUTTON_B - NativeLibrary.ButtonType.BUTTON_X -> Settings.KEY_BUTTON_X - NativeLibrary.ButtonType.BUTTON_Y -> Settings.KEY_BUTTON_Y - NativeLibrary.ButtonType.TRIGGER_L -> Settings.KEY_BUTTON_L - NativeLibrary.ButtonType.TRIGGER_R -> Settings.KEY_BUTTON_R - NativeLibrary.ButtonType.BUTTON_ZL -> Settings.KEY_BUTTON_ZL - NativeLibrary.ButtonType.BUTTON_ZR -> Settings.KEY_BUTTON_ZR - NativeLibrary.ButtonType.BUTTON_SELECT -> Settings.KEY_BUTTON_SELECT - NativeLibrary.ButtonType.BUTTON_START -> Settings.KEY_BUTTON_START - NativeLibrary.ButtonType.BUTTON_HOME -> Settings.KEY_BUTTON_HOME - NativeLibrary.ButtonType.DPAD_UP -> Settings.KEY_BUTTON_UP - NativeLibrary.ButtonType.DPAD_DOWN -> Settings.KEY_BUTTON_DOWN - NativeLibrary.ButtonType.DPAD_LEFT -> Settings.KEY_BUTTON_LEFT - NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT - else -> "" - } - /** - * Get the mutable set of int button values this key should map to given an event - */ - fun getButtonSet(keyCode: KeyEvent):MutableSet { - val key = getInputButtonKey(keyCode) - val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - var buttonCodes = try { - preferences.getStringSet(key, mutableSetOf()) - } catch (e: ClassCastException) { - val prefInt = preferences.getInt(key, -1); - val migratedSet = if (prefInt != -1) { - mutableSetOf(prefInt.toString()) - } else { - mutableSetOf() - } - migratedSet - } - if (buttonCodes == null) buttonCodes = mutableSetOf() - return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet() - } - - private fun getInputButtonKey(keyId: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyId}" - - /** Falls back to the scan code when keyCode is KEYCODE_UNKNOWN. */ - fun getInputButtonKey(event: KeyEvent): String = getInputButtonKey(translateEventToKeyId(event)) - - /** - * Helper function to get the settings key for an gamepad axis. - */ - fun getInputAxisKey(axis: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${axis}" - - /** - * Helper function to get the settings key for an gamepad axis button (stick or trigger). - */ - fun getInputAxisButtonKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestButton" - - /** - * Helper function to get the settings key for an whether a gamepad axis is inverted. - */ - fun getInputAxisInvertedKey(axis: Int): String = "${getInputAxisKey(axis)}_Inverted" - - /** - * Helper function to get the settings key for an gamepad axis orientation. - */ - fun getInputAxisOrientationKey(axis: Int): String = - "${getInputAxisKey(axis)}_GuestOrientation" - - - /** - * This function translates a keyEvent into an "keyid" - * This key id is either the keyCode from the event, or - * the raw scanCode. - * Only when the keyCode itself is 0, (so it is an unknown key) - * we fall back to the raw scan code. - * This handles keys like the media-keys on google statia-controllers - * that don't have a conventional "mapping" and report as "unknown" - */ - fun translateEventToKeyId(event: KeyEvent): Int { - return if (event.keyCode == 0) { - event.scanCode - } else { - event.keyCode - } - } - } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt index 7366406df..15b190fa0 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt @@ -19,7 +19,6 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding -import androidx.preference.PreferenceManager import com.google.android.material.color.MaterialColors import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary @@ -199,13 +198,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { // Prevents saving to a non-existent settings file presenter.onSettingsReset() - val controllerKeys = Settings.buttonKeys + Settings.circlePadKeys + Settings.cStickKeys + - Settings.dPadAxisKeys + Settings.dPadButtonKeys + Settings.triggerKeys - val editor = - PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext).edit() - controllerKeys.forEach { editor.remove(it) } - editor.apply() - // Delete settings file because the user may have changed values that do not exist in the UI val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) if (!settingsFile.delete()) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt index 095241097..c8bec4aad 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt @@ -33,7 +33,7 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView, this.menuTag = menuTag this.gameId = gameId - perGameInGlobalContext = gameId != "" && !Settings.settings.isPerGame() + perGameInGlobalContext = gameId != "" && ! Settings.settings.isPerGame() // sync the active settings into my local settings appropriately // if we are editing global settings rom a game, this should just work diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index f08ebc07c..f6087ab98 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -248,7 +248,7 @@ class SettingsAdapter( private fun onMultiChoiceClick(item: MultiChoiceSetting) { clickedItem = item - val value: BooleanArray = getSelectionForMultiChoiceValue(item); + val value: BooleanArray = getSelectionForMultiChoiceValue(item) dialog = MaterialAlertDialogBuilder(context) .setTitle(item.nameId) .setMultiChoiceItems(item.choicesId, value, this) @@ -268,7 +268,7 @@ class SettingsAdapter( private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { clickedItem = item - dialog = context?.let { + dialog = context.let { MaterialAlertDialogBuilder(it) .setTitle(item.nameId) .setSingleChoiceItems(item.choices, item.selectValueIndex, this) @@ -370,9 +370,9 @@ class SettingsAdapter( value = sliderProgress textSliderValue?.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable) { - var textValue = s.toString().toFloatOrNull(); + var textValue = s.toString().toFloatOrNull() if (item.setting !is FloatSetting) { - textValue = textValue?.roundToInt()?.toFloat(); + textValue = textValue?.roundToInt()?.toFloat() } if (textValue == null || textValue < valueFrom || textValue > valueTo) { textInputLayout?.error = "Inappropriate value" @@ -469,7 +469,7 @@ class SettingsAdapter( val scSetting = clickedItem as? SingleChoiceSetting scSetting?.let { val value = getValueForSingleChoiceSelection(it, which) - if (it.selectedValue != value) fragmentView?.onSettingChanged() + if (it.selectedValue != value) fragmentView.onSettingChanged() it.setSelectedValue(value) fragmentView.loadSettingsList() closeDialog() @@ -480,7 +480,7 @@ class SettingsAdapter( val scSetting = clickedItem as? StringSingleChoiceSetting scSetting?.let { val value = it.getValueAt(which) ?: "" - if (it.selectedValue != value) fragmentView?.onSettingChanged() + if (it.selectedValue != value) fragmentView.onSettingChanged() it.setSelectedValue(value) fragmentView.loadSettingsList() closeDialog() @@ -492,7 +492,6 @@ class SettingsAdapter( sliderSetting?.let { val sliderval = it.roundedFloat(sliderProgress) if (sliderval != it.selectedFloat) fragmentView.onSettingChanged() - val s = it.setting when { it.setting?.defaultValue is Int -> it.setSelectedValue(sliderProgress.roundToInt()) else -> it.setSelectedValue(sliderProgress) @@ -506,7 +505,7 @@ class SettingsAdapter( val inputSetting = clickedItem as? StringInputSetting inputSetting?.let { if (it.selectedValue != textInputValue) { - fragmentView?.onSettingChanged() + fragmentView.onSettingChanged() } it.setSelectedValue(textInputValue) fragmentView.loadSettingsList() @@ -602,8 +601,9 @@ class SettingsAdapter( } fun onLongClickAutoMap(): Boolean { + val settings = fragmentView.activityView?.settings ?: return false showConfirmationDialog(R.string.controller_clear_all, R.string.controller_clear_all_confirm) { - InputBindingSetting.clearAllBindings() + settings.inputMappingManager.clear() fragmentView.loadSettingsList() fragmentView.onSettingChanged() } @@ -682,18 +682,18 @@ class SettingsAdapter( } private fun getSelectionForMultiChoiceValue(item: MultiChoiceSetting): BooleanArray { - val value = item.selectedValues; - val valuesId = item.valuesId; + val value = item.selectedValues + val valuesId = item.valuesId if (valuesId > 0) { - val valuesArray = context.resources.getIntArray(valuesId); + val valuesArray = context.resources.getIntArray(valuesId) val res = BooleanArray(valuesArray.size){false} for (index in valuesArray.indices) { if (value.contains(valuesArray[index])) { - res[index] = true; + res[index] = true } } - return res; + return res } - return BooleanArray(1){false}; + return BooleanArray(1){false} } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index e9506b44b..e01c16e80 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -19,7 +19,6 @@ import org.citra.citra_emu.R import org.citra.citra_emu.display.ScreenLayout import org.citra.citra_emu.display.StereoMode import org.citra.citra_emu.display.StereoWhichDisplay -import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting import org.citra.citra_emu.features.settings.model.IntSetting @@ -45,6 +44,8 @@ import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.ThemeUtil import kotlin.math.roundToInt +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.settings.model.InputMappingSetting class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { private var menuTag: String? = null @@ -754,45 +755,36 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) ) add(HeaderSetting(R.string.generic_buttons)) - Settings.buttonKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.buttonTitles[i])) + GamepadHelper.buttonKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.buttonTitles[i])) } add(HeaderSetting(R.string.controller_circlepad)) - Settings.circlePadKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.axisTitles[i])) + GamepadHelper.circlePadKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings,GamepadHelper.axisTitles[i])) } add(HeaderSetting(R.string.controller_c)) - Settings.cStickKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.axisTitles[i])) + GamepadHelper.cStickKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.axisTitles[i])) } - add(HeaderSetting(R.string.controller_dpad_axis,R.string.controller_dpad_axis_description)) - Settings.dPadAxisKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.axisTitles[i])) - } - add(HeaderSetting(R.string.controller_dpad_button,R.string.controller_dpad_button_description)) - Settings.dPadButtonKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.dPadTitles[i])) + add(HeaderSetting(R.string.controller_dpad,R.string.controller_dpad_axis_description)) + + GamepadHelper.dPadButtonKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.axisTitles[i])) } add(HeaderSetting(R.string.controller_triggers)) - Settings.triggerKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.triggerTitles[i])) + GamepadHelper.triggerKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.triggerTitles[i])) } add(HeaderSetting(R.string.controller_hotkeys,R.string.controller_hotkeys_description)) - Settings.hotKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) - add(InputBindingSetting(button, Settings.hotkeyTitles[i])) + GamepadHelper.hotKeys.forEachIndexed { i: Int, setting: InputMappingSetting -> + add(InputBindingSetting(setting, settings, GamepadHelper.hotkeyTitles[i])) } + add(HeaderSetting(R.string.miscellaneous)) add( SwitchSetting( @@ -807,19 +799,6 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } } - private fun getInputObject(key: String): AbstractSetting { - return object : AbstractSetting { - override val key = key - override val section = Settings.SECTION_CONTROLS - override val isRuntimeEditable = true - override val defaultValue = "" - override fun valueFromString(string: String): String = string - override fun valueToString(value: String): String = value - // TODO: make input mappings also work per-game, which will be easy if we move - // them to config files - } - } - private fun addGraphicsSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) sl.apply { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt index 44e26758e..9f9965940 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt @@ -15,12 +15,10 @@ import org.citra.citra_emu.features.settings.ui.SettingsAdapter class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { override lateinit var setting: InputBindingSetting - override fun bind(item: SettingsItem) { - val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) setting = item as InputBindingSetting binding.textSettingName.setText(item.nameId) - val uiString = preferences.getString(setting.abstractSetting.key, "")!! + val uiString = setting.value if (uiString.isNotEmpty()) { binding.textSettingDescription.visibility = View.GONE binding.textSettingValue.visibility = View.VISIBLE @@ -39,6 +37,8 @@ class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter binding.textSettingDescription.alpha = 0.5f binding.textSettingValue.alpha = 0.5f } + + showGlobalButtonIfNeeded(binding.buttonUseGlobal, position) } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt index 2e03fc722..888d723d6 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt @@ -12,6 +12,7 @@ import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.InputMappingSetting import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.Settings @@ -37,7 +38,8 @@ object SettingsFile { IntSetting.values().toList() + FloatSetting.values().toList() + StringSetting.values().toList() + - IntListSetting.values().toList() + IntListSetting.values().toList() + + InputMappingSetting.values().toList() } private fun findSettingByKey(key: String): AbstractSetting<*>? = @@ -96,6 +98,7 @@ object SettingsFile { */ fun loadSettings(settings: Settings, view: SettingsActivityView? = null) { readFile(getSettingsFile(FILE_NAME_CONFIG),settings,false,view) + settings.inputMappingManager.rebuild(settings) } /** @@ -106,6 +109,7 @@ object SettingsFile { loadSettings(settings, view) val file = findCustomGameSettingsFile(gameId) ?: return readFile(file, settings, true, view) + settings.inputMappingManager.rebuild(settings) } /** diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt index 569a0caca..8b3d01e65 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt @@ -11,12 +11,14 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import androidx.fragment.app.activityViewModels import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.citra.citra_emu.R import org.citra.citra_emu.databinding.DialogAutoMapBinding -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.utils.Log +import org.citra.citra_emu.features.input.GamepadHelper +import org.citra.citra_emu.features.settings.model.SettingsViewModel /** * Captures a single button press to detect controller layout (Xbox vs Nintendo) @@ -25,7 +27,7 @@ import org.citra.citra_emu.utils.Log class AutoMapDialogFragment : BottomSheetDialogFragment() { private var _binding: DialogAutoMapBinding? = null private val binding get() = _binding!! - + private val settingsViewModel: SettingsViewModel by activityViewModels() private var onComplete: (() -> Unit)? = null override fun onCreateView( @@ -72,12 +74,11 @@ class AutoMapDialogFragment : BottomSheetDialogFragment() { // Check if this is a Nintendo Switch Joy-Con (not Pro Controller). // Joy-Cons have unique quirks: split devices, non-standard D-pad scan codes, // partial A/B swap but no X/Y swap from Android's evdev layer. - val isJoyCon = InputBindingSetting.isJoyCon(device) + val isJoyCon = GamepadHelper.isJoyCon(device) if (isJoyCon) { Log.info("[AutoMap] Detected Joy-Con - using Joy-Con mappings") - InputBindingSetting.clearAllBindings() - InputBindingSetting.applyJoyConBindings() + GamepadHelper.applyJoyConBindings(settingsViewModel.settings) onComplete?.invoke() dismiss() return true @@ -106,8 +107,7 @@ class AutoMapDialogFragment : BottomSheetDialogFragment() { val dpadName = if (useAxisDpad) "axis" else "button" Log.info("[AutoMap] Detected $dpadName d-pad (device=${device?.name})") - InputBindingSetting.clearAllBindings() - InputBindingSetting.applyAutoMapBindings(isNintendoLayout, useAxisDpad) + GamepadHelper.applyAutoMapBindings(settingsViewModel.settings, isNintendoLayout, useAxisDpad) onComplete?.invoke() dismiss() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index f82a36724..d59088439 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -1270,6 +1270,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram binding.surfaceInputOverlay.resetButtonPlacement() } + + fun updateShowPerformanceOverlay() { if (perfStatsUpdater != null) { perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt index cf42bed12..b0d3e0732 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt @@ -56,36 +56,18 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { isCancelable = false view.requestFocus() view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } - if (setting!!.isButtonMappingSupported()) { - dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } - } - if (setting!!.isAxisMappingSupported()) { - binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } - } + dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } + binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } + - val inputTypeId = when { - setting!!.isCirclePad() -> R.string.controller_circlepad - setting!!.isCStick() -> R.string.controller_c - setting!!.isDPad() -> R.string.controller_dpad - setting!!.isTrigger() -> R.string.controller_trigger - else -> R.string.button - } binding.textTitle.text = String.format( getString(R.string.input_dialog_title), - getString(inputTypeId), - getString(setting!!.nameId) + setting!!.value,"" ) - var messageResId: Int = R.string.input_dialog_description - if (setting!!.isAxisMappingSupported() && !setting!!.isTrigger()) { - // Use specialized message for axis left/right or up/down - messageResId = if (setting!!.isHorizontalOrientation()) { - R.string.input_binding_description_horizontal_axis - } else { - R.string.input_binding_description_vertical_axis - } - } + val messageResId: Int = R.string.input_dialog_description + binding.textMessage.text = getString(messageResId) binding.buttonClear.setOnClickListener { @@ -140,7 +122,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { var numMovedAxis = 0 var axisMoveValue = 0.0f var lastMovedRange: InputDevice.MotionRange? = null - var lastMovedDir = '?' + var lastMovedDir = 0 if (waitingForEvent) { for (i in motionRanges.indices) { val range = motionRanges[i] @@ -164,7 +146,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { axisMoveValue = origValue numMovedAxis++ lastMovedRange = range - lastMovedDir = if (origValue < 0.0f) '-' else '+' + lastMovedDir = if (origValue < 0.0f) -1 else 1 } } else if (abs(origValue) < 0.25f && abs(previousValue) > 0.75f) { // Special case for d-pads (axis value jumps between 0 and 1 without any values @@ -172,7 +154,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { // due to the first press being caught by the "if (firstEvent)" case further up. numMovedAxis++ lastMovedRange = range - lastMovedDir = if (previousValue < 0.0f) '-' else '+' + lastMovedDir = if (previousValue < 0.0f) -1 else 1 } } previousValues[i] = origValue @@ -181,7 +163,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { // If only one axis moved, that's the winner. if (numMovedAxis == 1) { waitingForEvent = false - setting?.onMotionInput(input, lastMovedRange!!, lastMovedDir) + setting?.onMotionInput(lastMovedRange!!, lastMovedDir) dismiss() } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt index c50b7d8f7..15d95878d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt @@ -173,7 +173,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex continue } anyOverlayStateChanged = true - + // TODO - switch these to using standard hotkey buttons instead of nativelibrary buttons if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) { swapScreen() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt deleted file mode 100644 index 7bba904b5..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.kt +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.utils - -import android.view.InputDevice -import android.view.KeyEvent -import android.view.MotionEvent - -/** - * Some controllers have incorrect mappings. This class has special-case fixes for them. - */ -object ControllerMappingHelper { - /** - * Some controllers report extra button presses that can be ignored. - */ - fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean { - return if (isDualShock4(inputDevice)) { - // The two analog triggers generate analog motion events as well as a keycode. - // We always prefer to use the analog values, so throw away the button press - keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2 - } else false - } - - /** - * Scale an axis to be zero-centered with a proper range. - */ - fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float { - if (isDualShock4(inputDevice)) { - // Android doesn't have correct mappings for this controller's triggers. It reports them - // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] - // Scale them to properly zero-centered with a range of [0.0, 1.0]. - if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { - return (value + 1) / 2.0f - } - } else if (isXboxOneWireless(inputDevice)) { - // Same as the DualShock 4, the mappings are missing. - if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { - return (value + 1) / 2.0f - } - if (axis == MotionEvent.AXIS_GENERIC_1) { - // This axis is stuck at ~.5. Ignore it. - return 0.0f - } - } else if (isMogaPro2Hid(inputDevice)) { - // This controller has a broken axis that reports a constant value. Ignore it. - if (axis == MotionEvent.AXIS_GENERIC_1) { - return 0.0f - } - } - return value - } - - private fun isDualShock4(inputDevice: InputDevice): Boolean { - // Sony DualShock 4 controller - return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc - } - - private fun isXboxOneWireless(inputDevice: InputDevice): Boolean { - // Microsoft Xbox One controller - return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0 - } - - private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean { - // Moga Pro 2 HID - return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271 - } -} diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 654d94b3e..4a8035139 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -141,21 +141,17 @@ void Config::ReadSetting(const std::string& group, Settings::SettingGetString( - "Controls", Settings::NativeButton::mapping[i], default_param); - if (Settings::values.current_input_profile.buttons[i].empty()) - Settings::values.current_input_profile.buttons[i] = default_param; + Settings::values.current_input_profile.buttons[i] = + InputManager::GenerateButtonParamPackage(default_buttons[i]); } for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { - std::string default_param = InputManager::GenerateAnalogParamPackage(default_analogs[i]); - Settings::values.current_input_profile.analogs[i] = android_config->GetString( - "Controls", Settings::NativeAnalog::mapping[i], default_param); - if (Settings::values.current_input_profile.analogs[i].empty()) - Settings::values.current_input_profile.analogs[i] = default_param; + Settings::values.current_input_profile.analogs[i] = + InputManager::GenerateAnalogParamPackage(default_analogs[i]); } Settings::values.current_input_profile.motion_device = android_config->GetString( diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 5e2eab1d1..abcdfac7b 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -77,6 +77,39 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"( # Use Artic Controller when connected to Artic Base Server. (Default 0) )") DECLARE_KEY(use_artic_base_controller) BOOST_HANA_STRING(R"( +# Configuration strings for 3ds buttons and axes. On Android, all such strings use "engine:gamepad" once bound. Default unbound. +)") DECLARE_KEY(button_a) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_b) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_x) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_y) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_l) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_r) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_zl) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_zr) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_start) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_select) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(button_home) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(dpad_up) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(dpad_down) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(dpad_left) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(dpad_right) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(circlepad_up) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(circlepad_down) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(circlepad_left) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(circlepad_right) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(cstick_up) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(cstick_down) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(cstick_left) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(cstick_right) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_close) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_cycle_layout) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_enable) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_pause_resume) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_quickload) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_quicksave) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_swap) BOOST_HANA_STRING(R"( +)") DECLARE_KEY(hotkey_turbo_limit) BOOST_HANA_STRING(R"( + [Core] # Whether to use the Just-In-Time (JIT) compiler for CPU emulation # 0: Interpreter (slow), 1 (default): JIT (fast) @@ -88,6 +121,7 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"( # Range is any positive integer (but we suspect 25 - 400 is a good idea) Default is 100 )") DECLARE_KEY(cpu_clock_percentage) BOOST_HANA_STRING(R"( + [Renderer] # Whether to render using OpenGL # 1: OpenGL ES (default), 2: Vulkan