mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-04-07 09:01:29 -06:00
Merge 0c6e7c6cc5 into 39363cd435
This commit is contained in:
commit
035f2f84b5
@ -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}\",")
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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.SettingsViewModel
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
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
|
||||
@ -52,14 +49,12 @@ 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
|
||||
import kotlin.math.abs
|
||||
|
||||
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 +83,32 @@ class EmulationActivity : AppCompatActivity() {
|
||||
RefreshRateUtil.enforceRefreshRate(this, sixtyHz = true)
|
||||
|
||||
ThemeUtil.setTheme(this)
|
||||
settingsViewModel.settings.loadSettings()
|
||||
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)
|
||||
}
|
||||
// load per-game settings
|
||||
SettingsFile.loadSettings(Settings.settings, String.format("%016X", game.titleId))
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
secondaryDisplay = SecondaryDisplay(this)
|
||||
|
||||
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 =
|
||||
@ -121,18 +134,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)
|
||||
}
|
||||
|
||||
@ -142,7 +143,7 @@ class EmulationActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
enableFullscreenImmersive()
|
||||
applyOrientationSettings() // Check for orientation settings changes on runtime
|
||||
applyOrientationSettings()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@ -179,6 +180,8 @@ class EmulationActivity : AppCompatActivity() {
|
||||
secondaryDisplay.releasePresentation()
|
||||
secondaryDisplay.releaseVD()
|
||||
|
||||
Settings.settings.removePerGameSettings()
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@ -229,11 +232,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 +253,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)
|
||||
}
|
||||
|
||||
@ -281,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -307,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)
|
||||
}
|
||||
|
||||
@ -315,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<Int, Pair<Float, Float>>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -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<MaterialButton>(R.id.application_settings).setOnClickListener {
|
||||
SettingsActivity.launch(
|
||||
context,
|
||||
SettingsFile.FILE_NAME_CONFIG,
|
||||
String.format("%016X", holder.game.titleId)
|
||||
)
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
val compressDecompressButton = bottomSheetView.findViewById<MaterialButton>(R.id.compress_decompress)
|
||||
if (game.isInstalled) {
|
||||
compressDecompressButton.setOnClickListener {
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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<DefaultButtonMapping>,
|
||||
axisMappings: List<DefaultAxisMapping>
|
||||
) {
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
}
|
||||
@ -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),
|
||||
@ -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,27 +16,30 @@ 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,
|
||||
private val context: Context
|
||||
private val context: Context,
|
||||
private val settings: Settings
|
||||
) {
|
||||
|
||||
private val hotkeyButtons = Hotkey.entries.map { it.button }
|
||||
private var hotkeyIsEnabled = false
|
||||
var hotkeyIsPressed = false
|
||||
private val currentlyPressedButtons = mutableSetOf<Int>()
|
||||
/** Store which axis directions are currently pressed as (axis, direction) pairs. */
|
||||
private val pressedAxisDirections = HashSet<Pair<Int, Int>>() // (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) {
|
||||
@ -57,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) }
|
||||
@ -95,7 +102,7 @@ class HotkeyUtility(
|
||||
)
|
||||
) {
|
||||
handled = NativeLibrary.onGamePadEvent(
|
||||
keyEvent.device.descriptor,
|
||||
descriptor,
|
||||
button,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
) || handled
|
||||
@ -103,6 +110,8 @@ class HotkeyUtility(
|
||||
}
|
||||
}
|
||||
}
|
||||
updateAxisStateForKey(axisSet,false)
|
||||
handled = sendAxisState(descriptor, axisSet) || handled
|
||||
return handled
|
||||
}
|
||||
|
||||
@ -112,7 +121,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(
|
||||
@ -141,4 +150,42 @@ class HotkeyUtility(
|
||||
hotkeyIsPressed = true
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateAxisStateForKey(axisSet: List<Pair<Int, Int>>, 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<Pair<Int, Int>>): Boolean {
|
||||
val stickAccumulator = HashMap<Int, Pair<Float, Float>>()
|
||||
// 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
|
||||
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
}
|
||||
@ -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 <axis, direction> 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<Int, MutableList<Int>>()
|
||||
private val keyToOutAxes = HashMap<Int, MutableList<Pair<Int, Int>>>()
|
||||
|
||||
private val axisToOutAxes = HashMap<Pair<Int, Int>, MutableList<Pair<Int, Int>>>()
|
||||
private val axisToOutButtons = HashMap<Pair<Int, Int>, MutableList<Int>>()
|
||||
|
||||
private val outAxisToMapping = HashMap<Pair<Int, Int>, Input>()
|
||||
private val buttonToMapping = HashMap<Int, Input>()
|
||||
|
||||
|
||||
/** 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<Int, Int>): List<Pair<Int, Int>> =
|
||||
axisToOutAxes[pair] ?: emptyList()
|
||||
|
||||
fun getOutButtonsForAxis(pair: Pair<Int, Int>): List<Int> =
|
||||
axisToOutButtons[pair] ?: emptyList()
|
||||
|
||||
fun getMappingForOutAxis(pair: Pair<Int, Int>): Input? =
|
||||
outAxisToMapping[pair]
|
||||
|
||||
fun getOutButtonsForKey(keyCode: Int): List<Int> =
|
||||
keyToOutButtons[keyCode] ?: emptyList()
|
||||
|
||||
fun getOutAxesForKey(keyCode: Int): List<Pair<Int, Int>> =
|
||||
keyToOutAxes[keyCode] ?: emptyList()
|
||||
|
||||
fun getMappingForButton(outKey: Int): Input? =
|
||||
buttonToMapping[outKey]
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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<E> : AbstractSetting {
|
||||
var list: List<E>
|
||||
}
|
||||
@ -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<T> {
|
||||
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?
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -10,7 +10,7 @@ enum class BooleanSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: Boolean
|
||||
) : AbstractBooleanSetting {
|
||||
) : AbstractSetting<Boolean> {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Float> {
|
||||
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<FloatSetting>()
|
||||
|
||||
fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
|
||||
|
||||
fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Input?> {
|
||||
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 }
|
||||
|
||||
}
|
||||
}
|
||||
@ -9,28 +9,17 @@ enum class IntListSetting(
|
||||
override val section: String,
|
||||
override val defaultValue: List<Int>,
|
||||
val canBeEmpty: Boolean = true
|
||||
) : AbstractListSetting<Int> {
|
||||
) : AbstractSetting<List<Int>> {
|
||||
|
||||
LAYOUTS_TO_CYCLE("layouts_to_cycle", Settings.SECTION_LAYOUT, listOf(0, 1, 2, 3, 4, 5), canBeEmpty = false);
|
||||
|
||||
private var backingList: List<Int> = defaultValue
|
||||
private var lastValidList : List<Int> = defaultValue
|
||||
|
||||
override var list: List<Int>
|
||||
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<Int>): String = value.joinToString()
|
||||
|
||||
override fun valueFromString(string: String): List<Int>? {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ enum class IntSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: Int
|
||||
) : AbstractIntSetting {
|
||||
) : AbstractSetting<Int> {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<ScaledFloatSetting>()
|
||||
|
||||
fun from(key: String): ScaledFloatSetting? =
|
||||
ScaledFloatSetting.values().firstOrNull { it.key == key }
|
||||
|
||||
fun clear() = ScaledFloatSetting.values().forEach { it.float = it.defaultValue * it.scale }
|
||||
}
|
||||
}
|
||||
@ -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<String, AbstractSetting>()
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,99 +4,111 @@
|
||||
|
||||
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.input.Input
|
||||
import org.citra.citra_emu.features.input.InputMappingManager
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import java.util.TreeMap
|
||||
|
||||
class Settings {
|
||||
private var gameId: String? = null
|
||||
private val globalValues = HashMap<String, Any>()
|
||||
private val perGameOverrides = HashMap<String, Any>()
|
||||
|
||||
var isLoaded = false
|
||||
val inputMappingManager = InputMappingManager()
|
||||
|
||||
var gameId: String? = null
|
||||
|
||||
fun isPerGame(): Boolean = gameId != null && gameId != ""
|
||||
|
||||
fun <T> get(setting: AbstractSetting<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return (perGameOverrides[setting.key]
|
||||
?: globalValues[setting.key]
|
||||
?: setting.defaultValue) as T
|
||||
}
|
||||
|
||||
fun <T> getGlobal(setting: AbstractSetting<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return (globalValues[setting.key] ?: setting.defaultValue) as T
|
||||
}
|
||||
|
||||
/** Sets the global value specifically */
|
||||
fun <T> setGlobal(setting: AbstractSetting<T>, 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 <T> setOverride(setting: AbstractSetting<T>, 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 by the Custom Settings Activity
|
||||
*/
|
||||
fun <T> set(setting: AbstractSetting<T>, value: T) {
|
||||
if (isPerGame()) setOverride(setting, value) else setGlobal(setting, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* A HashMap<String></String>, 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 this particular setting is *currently* global or local.
|
||||
* This should be used by the Quick Menu
|
||||
*/
|
||||
class SettingsSectionMap : HashMap<String, SettingSection?>() {
|
||||
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 <T> update(setting: AbstractSetting<T>, 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
|
||||
}
|
||||
|
||||
inputMappingManager.rebuild(this)
|
||||
}
|
||||
|
||||
fun <T> clearOverride(setting: AbstractSetting<T>) {
|
||||
perGameOverrides.remove(setting.key)
|
||||
if (setting is InputMappingSetting) {
|
||||
inputMappingManager.rebind(setting, getGlobal(setting))
|
||||
}
|
||||
}
|
||||
|
||||
var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
|
||||
|
||||
fun getSection(sectionName: String): SettingSection? {
|
||||
return sections[sectionName]
|
||||
fun hasOverride(setting: AbstractSetting<*>): Boolean {
|
||||
return perGameOverrides.containsKey(setting.key)
|
||||
}
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = sections.isEmpty()
|
||||
fun getAllOverrides(): Map<String, Any> = perGameOverrides.toMap()
|
||||
|
||||
fun loadSettings(view: SettingsActivityView? = null) {
|
||||
sections = SettingsSectionMap()
|
||||
loadCitraSettings(view)
|
||||
if (!TextUtils.isEmpty(gameId)) {
|
||||
loadCustomGameSettings(gameId!!, view)
|
||||
}
|
||||
isLoaded = true
|
||||
fun getAllGlobal(): Map<String, Any> = globalValues.toMap()
|
||||
|
||||
fun clearAll() {
|
||||
globalValues.clear()
|
||||
perGameOverrides.clear()
|
||||
inputMappingManager.clear()
|
||||
}
|
||||
|
||||
private fun loadCitraSettings(view: SettingsActivityView?) {
|
||||
for ((fileName) in configFileSectionsMap) {
|
||||
sections.putAll(SettingsFile.readFile(fileName, view))
|
||||
}
|
||||
fun clearOverrides() {
|
||||
perGameOverrides.clear()
|
||||
inputMappingManager.rebuild(this)
|
||||
}
|
||||
|
||||
private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
|
||||
// Custom game settings
|
||||
mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
|
||||
fun removePerGameSettings() {
|
||||
clearOverrides()
|
||||
gameId = null
|
||||
}
|
||||
|
||||
private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
|
||||
for ((key, updatedSection) in updatedSections) {
|
||||
if (sections.containsKey(key)) {
|
||||
val originalSection = sections[key]
|
||||
originalSection!!.mergeSection(updatedSection!!)
|
||||
} else {
|
||||
sections[key] = updatedSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<String, SettingSection?>()
|
||||
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"
|
||||
@ -115,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"
|
||||
@ -234,20 +137,7 @@ class Settings {
|
||||
|
||||
private val configFileSectionsMap: MutableMap<String, List<String>> = 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()
|
||||
}
|
||||
}
|
||||
@ -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 primarily manipulates its own copy of the settings object
|
||||
// syncing it with the active settings only when saving
|
||||
val settings = Settings()
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ enum class StringSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: String
|
||||
) : AbstractStringSetting {
|
||||
) : AbstractSetting<String>{
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String>?,
|
||||
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<String>)
|
||||
} 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<String>
|
||||
settings.set(stringSetting, datetime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.settings.model.AbstractStringSetting
|
||||
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.
|
||||
*/
|
||||
fun removeOldMapping() {
|
||||
// Try remove all possible keys we wrote for this setting
|
||||
val oldKey = preferences.getString(reverseKey, "")
|
||||
if (oldKey != "") {
|
||||
(setting as AbstractStringSetting).string = ""
|
||||
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<String>())!!.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<String> 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<DefaultButtonMapping>,
|
||||
axisMappings: List<DefaultAxisMapping>
|
||||
) {
|
||||
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<Int> {
|
||||
val key = getInputButtonKey(keyCode)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
var buttonCodes = try {
|
||||
preferences.getStringSet(key, mutableSetOf<String>())
|
||||
} catch (e: ClassCastException) {
|
||||
val prefInt = preferences.getInt(key, -1);
|
||||
val migratedSet = if (prefInt != -1) {
|
||||
mutableSetOf(prefInt.toString())
|
||||
} else {
|
||||
mutableSetOf<String>()
|
||||
}
|
||||
migratedSet
|
||||
}
|
||||
if (buttonCodes == null) buttonCodes = mutableSetOf<String>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<List<Int>>?,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val choicesId: Int,
|
||||
val valuesId: Int,
|
||||
val key: String? = null,
|
||||
val defaultValue: List<Int>? = null,
|
||||
override var isEnabled: Boolean = true
|
||||
override var isEnabled: Boolean = true,
|
||||
private val getValue: (()->List<Int>)? = null,
|
||||
private val setValue: ((List<Int>)-> Unit)? = null
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_MULTI_CHOICE
|
||||
|
||||
val selectedValues: List<Int>
|
||||
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<Int>): IntListSetting {
|
||||
val intSetting = setting as IntListSetting
|
||||
intSetting.list = selection
|
||||
return intSetting
|
||||
fun setSelectedValue(selection: List<Int>) {
|
||||
if (setValue != null) {
|
||||
setValue(selection)
|
||||
}else {
|
||||
val intSetting = setting as IntListSetting
|
||||
settings.set(intSetting, selection)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Int>) ?: 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<Int>
|
||||
settings.set(backSetting, selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Int>).toFloat()
|
||||
}
|
||||
|
||||
is Float -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
settings.get(s as AbstractSetting<Float>)
|
||||
}
|
||||
|
||||
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<Int>).toString()
|
||||
}
|
||||
|
||||
is Float -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
roundedFloat(settings.get(it as AbstractSetting<Float>)).toString()
|
||||
}
|
||||
|
||||
else -> ""
|
||||
}
|
||||
} ?: defaultValue?.toString() ?: ""
|
||||
|
||||
fun setSelectedValue(selection: Int): AbstractSetting<Int> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val intSetting = setting as AbstractSetting<Int>
|
||||
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<Float>
|
||||
settings.set(floatSetting, selection)
|
||||
}
|
||||
return floatSetting
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String>?,
|
||||
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<String>)
|
||||
}
|
||||
|
||||
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<String>
|
||||
settings.set(stringSetting, selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String>?,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val choices: Array<String>,
|
||||
val values: Array<String>?,
|
||||
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<String>)
|
||||
}
|
||||
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<String>
|
||||
settings.set(stringSetting, selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Boolean>?,
|
||||
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<Boolean>
|
||||
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<Boolean>
|
||||
settings.set(setting, checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -20,20 +19,14 @@ 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
|
||||
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 +35,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 +57,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)
|
||||
@ -204,20 +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()
|
||||
|
||||
// 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()) {
|
||||
|
||||
@ -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,16 +21,32 @@ 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
|
||||
private lateinit var gameId: String
|
||||
private var perGameInGlobalContext = false
|
||||
|
||||
fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
|
||||
this.menuTag = menuTag
|
||||
this.gameId = gameId
|
||||
|
||||
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)
|
||||
}
|
||||
@ -47,13 +65,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 +83,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 +92,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 +106,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() {
|
||||
|
||||
@ -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,11 +67,12 @@ import java.text.SimpleDateFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SettingsAdapter(
|
||||
private val fragmentView: SettingsFragmentView,
|
||||
public val context: Context
|
||||
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener,
|
||||
val fragmentView: SettingsFragmentView,
|
||||
val context: Context
|
||||
) : RecyclerView.Adapter<SettingViewHolder<SettingsItem>?>(), DialogInterface.OnClickListener,
|
||||
DialogInterface.OnMultiChoiceClickListener {
|
||||
private var settings: ArrayList<SettingsItem>? = null
|
||||
var isPerGame: Boolean = false
|
||||
private var clickedItem: SettingsItem? = null
|
||||
private var clickedPosition: Int
|
||||
private var dialog: AlertDialog? = null
|
||||
@ -94,7 +88,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 +138,7 @@ class SettingsAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: SettingViewHolder<SettingsItem>, position: Int) {
|
||||
getItem(position)?.let { holder.bind(it) }
|
||||
}
|
||||
|
||||
@ -226,14 +220,15 @@ 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
|
||||
if (fragmentView.activityView != null)
|
||||
// Reload the settings list to update the UI
|
||||
fragmentView.loadSettingsList()
|
||||
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
private fun onSingleChoiceClick(item: SingleChoiceSetting) {
|
||||
@ -253,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)
|
||||
@ -273,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)
|
||||
@ -338,8 +333,7 @@ class SettingsAdapter(
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
notifyItemChanged(clickedPosition)
|
||||
val setting = item.setSelectedValue(rtcString)
|
||||
fragmentView.putSetting(setting)
|
||||
item.setSelectedValue(rtcString)
|
||||
fragmentView.loadSettingsList()
|
||||
clickedItem = null
|
||||
}
|
||||
@ -359,7 +353,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())
|
||||
@ -376,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"
|
||||
@ -393,9 +387,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 +412,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 +468,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 +479,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 +490,11 @@ 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()
|
||||
when {
|
||||
it.setting?.defaultValue is Int -> it.setSelectedValue(sliderProgress.roundToInt())
|
||||
else -> it.setSelectedValue(sliderProgress)
|
||||
}
|
||||
fragmentView.loadSettingsList()
|
||||
closeDialog()
|
||||
@ -555,10 +505,9 @@ class SettingsAdapter(
|
||||
val inputSetting = clickedItem as? StringInputSetting
|
||||
inputSetting?.let {
|
||||
if (it.selectedValue != textInputValue) {
|
||||
fragmentView?.onSettingChanged()
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
val setting = it.setSelectedValue(textInputValue ?: "")
|
||||
fragmentView?.putSetting(setting)
|
||||
it.setSelectedValue(textInputValue)
|
||||
fragmentView.loadSettingsList()
|
||||
closeDialog()
|
||||
}
|
||||
@ -575,36 +524,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 +544,23 @@ class SettingsAdapter(
|
||||
return true
|
||||
}
|
||||
|
||||
fun <T> resetSettingToDefault(setting: AbstractSetting<T>, position: Int) {
|
||||
val settings = fragmentView.activityView?.settings ?: return
|
||||
settings.set(setting,setting.defaultValue)
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.loadSettingsList()
|
||||
}
|
||||
|
||||
fun <T> resetSettingToGlobal(setting: AbstractSetting<T>, 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)
|
||||
@ -652,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()
|
||||
}
|
||||
@ -732,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}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
*/
|
||||
|
||||
@ -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<DateTimeSetting>(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
|
||||
@ -56,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) {
|
||||
|
||||
@ -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<SettingsItem>(binding.root, adapter) {
|
||||
override var setting = null
|
||||
init {
|
||||
itemView.setOnClickListener(null)
|
||||
}
|
||||
|
||||
@ -13,14 +13,12 @@ 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<InputBindingSetting>(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) {
|
||||
|
||||
@ -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<SettingsItem>(binding.root, adapter) {
|
||||
override lateinit var setting: SettingsItem
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item
|
||||
binding.textSettingName.setText(item.nameId)
|
||||
@ -35,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 {
|
||||
|
||||
@ -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<RunnableSetting>(binding.root, adapter) {
|
||||
override lateinit var setting: RunnableSetting
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item as RunnableSetting
|
||||
|
||||
@ -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<out T: SettingsItem>(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.
|
||||
@ -34,4 +37,22 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<SettingsItem>(binding.root, adapter) {
|
||||
override lateinit var setting: SettingsItem
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item
|
||||
@ -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 {
|
||||
|
||||
@ -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<SliderSetting>(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
|
||||
@ -44,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) {
|
||||
|
||||
@ -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<StringInputSetting>(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
|
||||
@ -35,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) {
|
||||
@ -42,7 +43,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 {
|
||||
|
||||
@ -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<SubmenuSetting>(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 {
|
||||
|
||||
@ -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<SwitchSetting>(binding.root, adapter) {
|
||||
|
||||
private lateinit var setting: SwitchSetting
|
||||
override lateinit var setting: SwitchSetting
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item as SwitchSetting
|
||||
@ -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) {
|
||||
|
||||
@ -12,14 +12,12 @@ 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.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 +25,6 @@ import java.io.BufferedReader
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.util.TreeMap
|
||||
|
||||
|
||||
/**
|
||||
@ -36,40 +33,47 @@ import java.util.TreeMap
|
||||
object SettingsFile {
|
||||
const val FILE_NAME_CONFIG = "config"
|
||||
|
||||
private var sectionsMap = BiMap<String?, String?>()
|
||||
private val allSettings: List<AbstractSetting<*>> by lazy {
|
||||
BooleanSetting.values().toList() +
|
||||
IntSetting.values().toList() +
|
||||
FloatSetting.values().toList() +
|
||||
StringSetting.values().toList() +
|
||||
IntListSetting.values().toList() +
|
||||
InputMappingSetting.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<String, SettingSection?> {
|
||||
val sections: HashMap<String, SettingSection?> = 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 +91,154 @@ object SettingsFile {
|
||||
}
|
||||
}
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> {
|
||||
return readFile(getSettingsFile(fileName), false, view)
|
||||
}
|
||||
|
||||
fun readFile(fileName: String): HashMap<String, SettingSection?> = 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<String, SettingSection?> {
|
||||
return readFile(getCustomGameSettingsFile(gameId), true, view)
|
||||
fun loadSettings(settings: Settings, view: SettingsActivityView? = null) {
|
||||
readFile(getSettingsFile(FILE_NAME_CONFIG),settings,false,view)
|
||||
settings.inputMappingManager.rebuild(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
settings.inputMappingManager.rebuild(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the settings object to parse the raw string and store it in the correct map
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> loadSettingInto(
|
||||
settings: Settings,
|
||||
setting: AbstractSetting<T>,
|
||||
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<String, SettingSection?>,
|
||||
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<String> = 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 <T> saveSetting(setting: AbstractSetting<T>, 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 <T> writeSingleSettingToFile(ini: DocumentFile, setting: AbstractSetting<T>, 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 <T> writeSettingToWini(writer: Wini, descriptor: AbstractSetting<T>, 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<String, String>? {
|
||||
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 +246,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<String> = 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")!!
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -44,7 +44,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 +69,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 +76,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
|
||||
@ -93,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!!
|
||||
@ -104,8 +101,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() }
|
||||
@ -184,7 +179,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)
|
||||
}
|
||||
@ -211,7 +206,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
|
||||
@ -221,7 +216,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)
|
||||
@ -389,6 +384,27 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
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
|
||||
}
|
||||
|
||||
R.id.menu_exit -> {
|
||||
emulationState.pause()
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
@ -508,7 +524,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 ->
|
||||
@ -523,7 +539,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
}
|
||||
|
||||
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||
emulationState.run(emulationActivity.isActivityRecreated)
|
||||
emulationState.run(emulationActivity!!.isActivityRecreated)
|
||||
} else {
|
||||
setupCitraDirectoriesThenStartEmulation()
|
||||
}
|
||||
@ -538,6 +554,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
emulationActivity = null
|
||||
NativeLibrary.clearEmulationActivity()
|
||||
super.onDetach()
|
||||
}
|
||||
@ -557,7 +574,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
|
||||
) {
|
||||
@ -707,7 +724,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
|
||||
@ -724,8 +741,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
|
||||
}
|
||||
@ -875,7 +893,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
|
||||
}
|
||||
|
||||
@ -921,7 +939,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
|
||||
|
||||
@ -993,7 +1011,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 ->
|
||||
@ -1255,12 +1273,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
binding.surfaceInputOverlay.resetButtonPlacement()
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun updateShowPerformanceOverlay() {
|
||||
if (perfStatsUpdater != null) {
|
||||
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
|
||||
@ -1275,11 +1295,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(
|
||||
@ -1294,7 +1314,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(
|
||||
@ -1304,14 +1324,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 =
|
||||
@ -1324,14 +1344,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)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
@ -168,12 +173,12 @@ 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()
|
||||
}
|
||||
else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) {
|
||||
TurboHelper.toggleTurbo(true)
|
||||
TurboHelper.toggleTurbo(true, settings)
|
||||
}
|
||||
|
||||
NativeLibrary.onGamePadEvent(
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,14 @@ 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
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
|
||||
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 +26,12 @@ class EmulationViewModel : ViewModel() {
|
||||
val shaderMessage get() = _shaderMessage.asStateFlow()
|
||||
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
|
||||
}
|
||||
|
||||
@ -79,46 +79,79 @@ static const std::array<int, Settings::NativeAnalog::NumAnalogs> default_analogs
|
||||
}};
|
||||
|
||||
template <>
|
||||
void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
|
||||
std::string setting_value =
|
||||
android_config->Get(group, setting.GetLabel(), setting.GetDefault());
|
||||
std::string Config::GetSetting(const std::string& group, Settings::Setting<std::string>& 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<std::string>& setting) {
|
||||
setting = std::move(GetSetting(group, setting));
|
||||
}
|
||||
|
||||
template <>
|
||||
bool Config::GetSetting(const std::string& group, Settings::Setting<bool>& 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<bool>& setting) {
|
||||
setting = android_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault());
|
||||
setting = GetSetting(group, setting);
|
||||
}
|
||||
|
||||
//TODO: figure out why ranged isn't being used
|
||||
template <typename Type, bool ranged>
|
||||
Type Config::GetSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
|
||||
if constexpr (std::is_floating_point_v<Type>) {
|
||||
double value = static_cast<double>(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<Type>(value);
|
||||
} else {
|
||||
long value = static_cast<long>(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<Type>(value);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Type, bool ranged>
|
||||
void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
|
||||
if constexpr (std::is_floating_point_v<Type>) {
|
||||
setting = android_config->GetReal(group, setting.GetLabel(), setting.GetDefault());
|
||||
} else {
|
||||
setting = static_cast<Type>(android_config->GetInteger(
|
||||
group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
|
||||
}
|
||||
setting = GetSetting(group, setting);
|
||||
}
|
||||
|
||||
void Config::ReadValues() {
|
||||
// Controls
|
||||
// Controls - Always use the default_buttons and default_analogs on the native side, don't
|
||||
// actually read the INI file. It is only used by Kotlin, which will correctly dispatch
|
||||
// the codes as needed.
|
||||
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
|
||||
std::string default_param = InputManager::GenerateButtonParamPackage(default_buttons[i]);
|
||||
Settings::values.current_input_profile.buttons[i] = android_config->GetString(
|
||||
"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(
|
||||
@ -139,9 +172,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 +188,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 +222,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<int>(Settings::LayoutOption::LargeScreen));
|
||||
if (layoutInt < 0 || layoutInt > 5) {
|
||||
layoutInt = static_cast<int>(Settings::LayoutOption::LargeScreen);
|
||||
}
|
||||
Settings::values.layout_option = static_cast<Settings::LayoutOption>(layoutInt);
|
||||
Settings::values.screen_gap =
|
||||
static_cast<int>(android_config->GetReal("Layout", "screen_gap", 0));
|
||||
Settings::values.large_screen_proportion =
|
||||
static_cast<float>(android_config->GetReal("Layout", "large_screen_proportion", 2.25));
|
||||
Settings::values.small_screen_position = static_cast<Settings::SmallScreenPosition>(
|
||||
android_config->GetInteger("Layout", "small_screen_position",
|
||||
static_cast<int>(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 +240,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<Settings::PortraitLayoutOption>(android_config->GetInteger(
|
||||
"Layout", "portrait_layout_option",
|
||||
static_cast<int>(Settings::PortraitLayoutOption::PortraitTopFullWidth)));
|
||||
Settings::values.secondary_display_layout = static_cast<Settings::SecondaryDisplayLayout>(
|
||||
android_config->GetInteger("Layout", Settings::HKeys::secondary_display_layout.c_str(),
|
||||
static_cast<int>(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 +371,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<INIReader>(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();
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ class Config {
|
||||
private:
|
||||
std::unique_ptr<INIReader> android_config;
|
||||
std::string android_config_loc;
|
||||
std::unique_ptr<INIReader> 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 <typename Type, bool ranged>
|
||||
void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& 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 <typename Type, bool ranged>
|
||||
Type GetSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
|
||||
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Camera::StillImage::Factory>());
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -180,6 +180,15 @@
|
||||
android:contentDescription="@string/cheats"
|
||||
android:text="@string/cheats" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/application_settings"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/application_settings"
|
||||
android:text="@string/application_settings" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/insert_cartridge_button"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
|
||||
@ -62,6 +62,15 @@
|
||||
android:textSize="13sp"
|
||||
tools:text="1x" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_use_global"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/use_global"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -46,6 +46,15 @@
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="@string/frame_limit_enable_description" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_use_global"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/use_global"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@ -57,6 +57,11 @@
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/preferences_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_application_settings"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/application_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_exit"
|
||||
android:icon="@drawable/ic_exit"
|
||||
|
||||
@ -522,6 +522,8 @@
|
||||
<string name="menu_emulation_amiibo">Amiibo</string>
|
||||
<string name="menu_emulation_amiibo_load">Load</string>
|
||||
<string name="menu_emulation_amiibo_remove">Remove</string>
|
||||
<string name="application_settings">Custom Settings</string>
|
||||
<string name="use_global">Customized: Revert to Global</string>
|
||||
<string name="select_amiibo">Select Amiibo File</string>
|
||||
<string name="amiibo_load_error">Error Loading Amiibo</string>
|
||||
<string name="amiibo_load_error_message">While loading the specified Amiibo file, an error occurred. Please check that the file is correct.</string>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user