Add option for pad position & removed side menu button

This commit is contained in:
SSimco
2025-08-30 19:33:40 +03:00
parent 9bf84560a7
commit 4c2e69445d
7 changed files with 129 additions and 43 deletions
@@ -9,20 +9,44 @@ class PreferenceDelegate<T>(
private val sharedPreferences: SharedPreferences,
private val defaultValue: T,
private val getter: SharedPreferences.(String, T) -> T,
private val setter: SharedPreferences.Editor.(String, T) -> SharedPreferences.Editor
private val setter: SharedPreferences.Editor.(String, T) -> Unit
) : ReadWriteProperty<Any, T> {
private var cachedValue: T? = null
private var isCached = false
override fun getValue(thisRef: Any, property: KProperty<*>): T {
val key = "${thisRef::class.simpleName}_${property.name}".uppercase()
return sharedPreferences.getter(key, defaultValue)
if (!isCached) {
cachedValue = sharedPreferences.getter(key, defaultValue)
isCached = true
}
return cachedValue ?: defaultValue
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
val key = "${thisRef::class.simpleName}_${property.name}".uppercase()
cachedValue = value
isCached = true
sharedPreferences.edit { setter(key, value) }
}
}
inline fun <reified T : Enum<T>> SharedPreferences.enumPref(default: T) =
PreferenceDelegate(
this,
default,
{ key, default ->
val enumOrdinal = getInt(key, default.ordinal)
enumValues<T>().firstOrNull { it.ordinal == enumOrdinal } ?: default
},
{ key, value ->
putInt(key, value.ordinal)
}
)
fun SharedPreferences.booleanPref(default: Boolean) =
PreferenceDelegate(
this,
@@ -5,8 +5,18 @@ import android.content.SharedPreferences
import info.cemu.cemu.common.ui.localization.DEFAULT_LANGUAGE
import kotlin.getValue
class EmulationScreenSettings(sharedPreferences: SharedPreferences) {
var isDrawerButtonVisible by sharedPreferences.booleanPref(false)
enum class GamePadPosition {
ABOVE,
BELOW,
LEFT,
RIGHT;
fun isVertical() = this == ABOVE || this == BELOW
fun appearsAfterTV() = this == BELOW || this == RIGHT
}
class EmulationSettings(sharedPreferences: SharedPreferences) {
var gamePadPosition by sharedPreferences.enumPref(GamePadPosition.RIGHT)
}
class GuiSettings(sharedPreferences: SharedPreferences) {
@@ -28,7 +38,7 @@ object SettingsManager {
private lateinit var sharedPreferences: SharedPreferences
val emulationScreenSettings by lazy { EmulationScreenSettings(sharedPreferences) }
val emulationSettings by lazy { EmulationSettings(sharedPreferences) }
val guiSettings by lazy { GuiSettings(sharedPreferences) }
@@ -29,13 +29,15 @@ import info.cemu.cemu.databinding.LayoutSideMenuEmulationBinding
import info.cemu.cemu.databinding.LayoutSideMenuTextItemBinding
import info.cemu.cemu.nativeinterface.NativeEmulation
import info.cemu.cemu.nativeinterface.NativeException
import info.cemu.cemu.common.settings.EmulationScreenSettings
import info.cemu.cemu.common.settings.EmulationSettings
import info.cemu.cemu.common.settings.GamePadPosition
import info.cemu.cemu.common.settings.InputOverlaySettings
import info.cemu.cemu.common.settings.SettingsManager
import info.cemu.cemu.emulation.inputoverlay.InputOverlaySurfaceView
import java.lang.ref.WeakReference
import kotlin.system.exitProcess
@SuppressLint("ClickableViewAccessibility")
class EmulationActivity : AppCompatActivity() {
private inner class CanvasSurfaceHolderCallback(val isMainCanvas: Boolean) :
SurfaceHolder.Callback {
@@ -68,21 +70,25 @@ class EmulationActivity : AppCompatActivity() {
}
private var emulationTextInputDialog: AlertDialog? = null
private var isGameRunning = false
private var padCanvas: SurfaceView? = null
private var toast: Toast? = null
private lateinit var binding: ActivityEmulationBinding
private var isMotionEnabled = false
private lateinit var inputOverlaySettings: InputOverlaySettings
private lateinit var emulationScreenSettings: EmulationScreenSettings
private lateinit var emulationSettings: EmulationSettings
private lateinit var inputOverlaySurfaceView: InputOverlaySurfaceView
private lateinit var sensorManager: SensorManager
private var toast: Toast? = null
private var isGameRunning = false
private var isMotionEnabled = false
private var isDrawerLocked = false
private var hasEmulationError = false
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
if (InputHandler.onMotionEvent(event)) {
return true
}
return super.onGenericMotionEvent(event)
}
@@ -90,6 +96,7 @@ class EmulationActivity : AppCompatActivity() {
if (InputHandler.onKeyEvent(event)) {
return true
}
return super.dispatchKeyEvent(event)
}
@@ -97,15 +104,19 @@ class EmulationActivity : AppCompatActivity() {
val extras = intent.extras
val data = intent.data
var launchPath: String? = null
if (extras != null) {
launchPath = extras.getString(EXTRA_LAUNCH_PATH)
}
if (launchPath == null && data != null) {
launchPath = data.toString()
}
if (launchPath == null) {
throw RuntimeException("launchPath is null")
}
return launchPath
}
@@ -117,20 +128,27 @@ class EmulationActivity : AppCompatActivity() {
emulationActivityInstance = WeakReference(this)
inputOverlaySettings = SettingsManager.inputOverlaySettings
emulationScreenSettings = SettingsManager.emulationScreenSettings
emulationSettings = SettingsManager.emulationSettings
sensorManager = SensorManager(this)
sensorManager.setDeviceRotationProvider(deviceRotationProvider = { display.rotation })
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
showExitConfirmationDialog()
}
override fun handleOnBackPressed() = toggleDrawer()
})
initializeView(getLaunchPath())
setContentView(binding.root)
}
private fun toggleDrawer() {
if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close()
} else {
binding.drawerLayout.open()
}
}
private fun destroyPadCanvas() {
if (padCanvas == null) {
return
@@ -190,18 +208,30 @@ class EmulationActivity : AppCompatActivity() {
private fun LayoutSideMenuEmulationBinding.configureSideMenu() {
val isInputOverlayEnabled = inputOverlaySettings.isOverlayEnabled
enableMotionCheckbox.configure(
label = tr("Enable motion"),
onCheckChanged = ::setMotionEnabled
)
lockDrawerCheckbox.configure(
tr("Lock drawer"),
onCheckChanged = {
isDrawerLocked = !isDrawerLocked
binding.drawerLayout.setLockedMode(isDrawerLocked)
}
)
replaceTvWithPadCheckbox.configure(
label = tr("Replace TV with PAD"),
onCheckChanged = NativeEmulation::setReplaceTVWithPadView
)
showPadCheckbox.configure(
label = tr(text = "Show PAD"),
onCheckChanged = ::setPadViewVisibility
)
showInputOverlayCheckbox.configure(
tr(text = "Show input overlay"),
initialCheckedStatus = isInputOverlayEnabled,
@@ -211,6 +241,7 @@ class EmulationActivity : AppCompatActivity() {
inputOverlaySurfaceView.setVisible(showInputOverlay)
}
)
editInputsMenuItem.configure(
label = tr("Edit inputs"),
isEnabled = isInputOverlayEnabled,
@@ -219,15 +250,16 @@ class EmulationActivity : AppCompatActivity() {
binding.finishEditInputsButton.visibility = View.VISIBLE
binding.moveInputsButton.performClick()
})
resetInputOverlayMenuItem.configure(
tr(text = "Reset input overlay"),
isEnabled = isInputOverlayEnabled,
onClick = inputOverlaySurfaceView::resetInputs
)
exitMenuItem.configure(tr("Exit"), onClick = onBackPressedDispatcher::onBackPressed)
exitMenuItem.configure(tr("Exit"), onClick = ::showExitConfirmationDialog)
}
@SuppressLint("ClickableViewAccessibility")
private fun initializeView(launchPath: String) {
setFullscreen()
@@ -263,14 +295,6 @@ class EmulationActivity : AppCompatActivity() {
toastMessage(tr("Exited input edit mode"))
}
if (emulationScreenSettings.isDrawerButtonVisible) {
binding.emulationSettingsButton.setOnClickListener { binding.drawerLayout.open() }
binding.drawerLayout.setLockedMode(true)
} else {
binding.emulationSettingsButton.visibility = View.GONE
}
val mainCanvas = binding.mainCanvas
try {
val testSurfaceTexture = SurfaceTexture(0)
val testSurface = Surface(testSurfaceTexture)
@@ -282,6 +306,7 @@ class EmulationActivity : AppCompatActivity() {
return
}
val mainCanvas = binding.mainCanvas
val mainCanvasHolder = mainCanvas.holder
mainCanvasHolder.addCallback(CanvasSurfaceHolderCallback(isMainCanvas = true))
mainCanvasHolder.addCallback(object : SurfaceChangedListener() {
@@ -312,9 +337,11 @@ class EmulationActivity : AppCompatActivity() {
private fun startGame(launchPath: String) {
val result = NativeEmulation.startGame(launchPath)
if (result == NativeEmulation.StartGameStatusCode.SUCCESSFUL) {
return
}
val errorMessage = when (result) {
NativeEmulation.StartGameStatusCode.ERROR_GAME_BASE_FILES_NOT_FOUND -> tr("Unable to launch game because the base files were not found.")
NativeEmulation.StartGameStatusCode.ERROR_NO_DISC_KEY -> tr("Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title.")
@@ -343,15 +370,36 @@ class EmulationActivity : AppCompatActivity() {
sensorManager.pauseListening()
}
@SuppressLint("ClickableViewAccessibility")
private fun createPadCanvas() {
if (padCanvas != null) {
return
}
val padCanvas = SurfaceView(this)
val padCanvasViewIndex: Int
val canvasLayoutParams: ViewGroup.LayoutParams
val orientation: Int
val position = emulationSettings.gamePadPosition
if (position.isVertical()) {
orientation = LinearLayout.VERTICAL
canvasLayoutParams =
LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f)
} else {
orientation = LinearLayout.HORIZONTAL
canvasLayoutParams =
LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f)
}
padCanvasViewIndex = if (position.appearsAfterTV()) 1 else 0
binding.mainCanvas.layoutParams = canvasLayoutParams
binding.canvasesLayout.orientation = orientation
binding.canvasesLayout.addView(
padCanvas,
LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f)
padCanvasViewIndex,
canvasLayoutParams
)
padCanvas.holder.addCallback(CanvasSurfaceHolderCallback(false))
padCanvas.setOnTouchListener(CanvasOnTouchListener(false))
@@ -4,10 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.lifecycle.viewmodel.compose.viewModel
import info.cemu.cemu.common.settings.GamePadPosition
import info.cemu.cemu.common.ui.components.Button
import info.cemu.cemu.common.ui.components.ScreenContent
import info.cemu.cemu.common.ui.components.SingleSelection
import info.cemu.cemu.common.ui.components.Toggle
import info.cemu.cemu.common.ui.localization.tr
import info.cemu.cemu.nativeinterface.NativeSettings
@@ -55,17 +55,24 @@ fun GeneralSettingsScreen(
NativeSettings.ConsoleLanguage.TAIWANESE,
),
)
Toggle(
label = tr("Side menu button"),
description = tr("Show the emulation side menu button"),
initialCheckedState = { generalSettingsViewModel.emulationScreenSettings.isDrawerButtonVisible },
onCheckedChanged = {
generalSettingsViewModel.emulationScreenSettings.isDrawerButtonVisible = it
}
SingleSelection(
label = tr("GamePad position"),
initialChoice = { generalSettingsViewModel.emulationSettings.gamePadPosition },
onChoiceChanged = { generalSettingsViewModel.emulationSettings.gamePadPosition = it },
choiceToString = { gamePadPositionToString(it) },
choices = GamePadPosition.entries,
)
}
}
private fun gamePadPositionToString(position: GamePadPosition) = when (position) {
GamePadPosition.ABOVE -> tr("Above")
GamePadPosition.BELOW -> tr("Below")
GamePadPosition.LEFT -> tr("Left")
GamePadPosition.RIGHT -> tr("Right")
}
private fun consoleLanguageToString(channels: Int): String = when (channels) {
NativeSettings.ConsoleLanguage.JAPANESE -> tr("Japanese")
NativeSettings.ConsoleLanguage.ENGLISH -> tr("English")
@@ -2,7 +2,7 @@ package info.cemu.cemu.settings.general
import android.content.Context
import androidx.lifecycle.ViewModel
import info.cemu.cemu.common.settings.EmulationScreenSettings
import info.cemu.cemu.common.settings.EmulationSettings
import info.cemu.cemu.common.settings.GuiSettings
import info.cemu.cemu.common.settings.SettingsManager
import info.cemu.cemu.common.ui.localization.getAvailableLanguages
@@ -10,7 +10,7 @@ import info.cemu.cemu.common.ui.localization.getAvailableLanguages
class GeneralSettingsViewModel : ViewModel() {
val languages: List<String>
val languageToDisplayNameMap: Map<String, String>
val emulationScreenSettings: EmulationScreenSettings = SettingsManager.emulationScreenSettings
val emulationSettings: EmulationSettings = SettingsManager.emulationSettings
val guiSettings: GuiSettings = SettingsManager.guiSettings
init {
@@ -61,13 +61,6 @@
android:layout_height="wrap_content"
app:icon="@drawable/ic_resize" />
</LinearLayout>
<Button
android:id="@+id/emulation_settings_button"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_settings" />
</RelativeLayout>
<include
@@ -25,6 +25,10 @@
android:id="@+id/enable_motion_checkbox"
layout="@layout/layout_side_menu_checkbox_item" />
<include
android:id="@+id/lock_drawer_checkbox"
layout="@layout/layout_side_menu_checkbox_item" />
<include
android:id="@+id/replace_tv_with_pad_checkbox"
layout="@layout/layout_side_menu_checkbox_item" />