From 67baf49f5112e227594ea7a30630ad7b7dd786d2 Mon Sep 17 00:00:00 2001 From: whyydk <254117058+whyydk@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:21:35 +0200 Subject: [PATCH 1/2] android: Add the ability to edit touch controls overlay without opening a game --- .../citra_emu/activities/EmulationActivity.kt | 43 ++++++++----- .../features/settings/model/Settings.kt | 2 + .../settings/ui/SettingsFragmentPresenter.kt | 63 +++++++++++++++++++ .../citra_emu/fragments/EmulationFragment.kt | 49 ++++++++++++++- .../citra/citra_emu/overlay/InputOverlay.kt | 4 +- .../res/navigation/emulation_navigation.xml | 4 ++ .../app/src/main/res/values/strings.xml | 7 +++ 7 files changed, 154 insertions(+), 18 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index 5f5215876..2f1994e10 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -6,6 +6,7 @@ package org.citra.citra_emu.activities import android.Manifest.permission import android.annotation.SuppressLint +import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager @@ -116,24 +117,29 @@ class EmulationActivity : AppCompatActivity() { EmulationLifecycleUtil.addShutdownHook(onShutdown) - isEmulationRunning = true - instance = this + if (!intent.getBooleanExtra(NO_GAME_EDIT_MODE, false)) { + isEmulationRunning = true + instance = this + } 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") + if (!intent.getBooleanExtra(NO_GAME_EDIT_MODE, false)) { + 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 } - } catch (e: Exception) { - Log.error("[EmulationActivity] Failed to retrieve game data: ${e.message}") - return - } - NativeLibrary.playTimeManagerStart(game.titleId) + + NativeLibrary.playTimeManagerStart(game.titleId) + } } // On some devices, the system bars will not disappear on first boot or after some @@ -174,8 +180,10 @@ class EmulationActivity : AppCompatActivity() { override fun onDestroy() { EmulationLifecycleUtil.removeHook(onShutdown) NativeLibrary.playTimeManagerStop() - isEmulationRunning = false - instance = null + if (!intent.getBooleanExtra(NO_GAME_EDIT_MODE, false)) { + isEmulationRunning = false + instance = null + } secondaryDisplay.releasePresentation() secondaryDisplay.releaseVD() @@ -544,6 +552,13 @@ class EmulationActivity : AppCompatActivity() { companion object { private var instance: EmulationActivity? = null + const val NO_GAME_EDIT_MODE = "noGameEditMode" + + fun launchForOverlayEdit(context: Context): Intent { + return Intent(context, EmulationActivity::class.java).apply { + putExtra(NO_GAME_EDIT_MODE, true) + } + } fun isRunning(): Boolean { return instance?.isEmulationRunning ?: false diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index 547a53594..14bed3081 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -104,6 +104,7 @@ class Settings { const val SECTION_CAMERA = "Camera" const val SECTION_CONTROLS = "Controls" const val SECTION_RENDERER = "Renderer" + const val SECTION_INPUT_OVERLAY = "Input Overlay" const val SECTION_LAYOUT = "Layout" const val SECTION_UTILITY = "Utility" const val SECTION_AUDIO = "Audio" @@ -242,6 +243,7 @@ class Settings { SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, + SECTION_INPUT_OVERLAY, SECTION_LAYOUT, SECTION_STORAGE, SECTION_UTILITY, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 25e56517f..ac332c0f7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -5,6 +5,7 @@ package org.citra.citra_emu.features.settings.ui import android.content.Context +import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources import android.hardware.camera2.CameraAccessException @@ -17,6 +18,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.serialization.builtins.IntArraySerializer import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R +import org.citra.citra_emu.activities.EmulationActivity import org.citra.citra_emu.display.ScreenLayout import org.citra.citra_emu.display.StereoMode import org.citra.citra_emu.display.StereoWhichDisplay @@ -99,6 +101,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) Settings.SECTION_CAMERA -> addCameraSettings(sl) Settings.SECTION_CONTROLS -> addControlsSettings(sl) Settings.SECTION_RENDERER -> addGraphicsSettings(sl) + Settings.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl) Settings.SECTION_LAYOUT -> addLayoutSettings(sl) Settings.SECTION_AUDIO -> addAudioSettings(sl) Settings.SECTION_DEBUG -> addDebugSettings(sl) @@ -179,6 +182,14 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) Settings.SECTION_RENDERER ) ) + add( + SubmenuSetting( + R.string.preferences_input_overlay, + 0, + R.drawable.dpad, + Settings.SECTION_INPUT_OVERLAY + ) + ) add( SubmenuSetting( R.string.preferences_layout, @@ -1127,6 +1138,58 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } } + private fun addInputOverlaySettings(sl: ArrayList) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_input_overlay)) + sl.apply { + val inputOverlayOpacitySetting = object : AbstractBooleanSetting { + private val preferences = + PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + + override var boolean: Boolean + get() = preferences.getBoolean("EmulationMenuSettings_ShowOverlay", true) + set(value) { + preferences.edit() + .putBoolean("EmulationMenuSettings_ShowOverlay", value) + .apply() + } + + override val key: String? = null + override val section: String? = null + override val isRuntimeEditable: Boolean = true + override val valueAsString: String + get() = preferences.getBoolean("EmulationMenuSettings_ShowOverlay", true) + .toString() + override val defaultValue = true + } + + add( + SwitchSetting( + inputOverlayOpacitySetting, + R.string.enable_input_overlay, + 0 + ) + ) + add( + HeaderSetting( + R.string.realtime_edit, + ) + ) + add( + RunnableSetting( + R.string.edit_overlay_layout, + R.string.edit_overlay_layout_description, + false, + R.drawable.dpad, + runnable = { + val intent = EmulationActivity.launchForOverlayEdit(CitraApplication.appContext) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + CitraApplication.appContext.startActivity(intent) + }, + ) + ) + } + } + private fun addLayoutSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_layout)) sl.apply { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 785438526..1f4b44822 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -129,6 +129,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (args.noGameEditMode) { + return + } + val intent = requireActivity().intent var intentUri: Uri? = intent.data val oldIntentInfo = Pair( @@ -212,6 +216,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram return } + if (args.noGameEditMode) { + setupNoGameEditMode() + return + } + binding.surfaceEmulation.holder.addCallback(this) binding.doneControlConfig.setOnClickListener { binding.doneControlConfig.visibility = View.GONE @@ -503,6 +512,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram override fun onResume() { super.onResume() + + if (args.noGameEditMode) { + return + } + Choreographer.getInstance().postFrameCallback(this) if (NativeLibrary.isRunning()) { emulationState.unpause() @@ -530,7 +544,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } override fun onPause() { - if (NativeLibrary.isRunning()) { + if (!args.noGameEditMode && NativeLibrary.isRunning()) { emulationState.pause() } Choreographer.getInstance().removeFrameCallback(this) @@ -574,6 +588,37 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } } + private fun setupNoGameEditMode() { + binding.surfaceInputOverlay.post { + binding.surfaceInputOverlay.refreshControls(true) + } + + binding.doneControlConfig.setOnClickListener { + finishNoGameEditMode() + } + + binding.doneControlConfig.visibility = View.VISIBLE + binding.surfaceInputOverlay.setIsInEditMode(true) + binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + binding.surfaceInputOverlay.visibility = View.VISIBLE + binding.loadingIndicator.visibility = View.GONE + + // in no game edit mode, back = done + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finishNoGameEditMode() + } + } + ) + } + + private fun finishNoGameEditMode() { + binding.surfaceInputOverlay.setIsInEditMode(false) + emulationActivity.finish() + } + private fun showSavestateMenu() { val popupMenu = PopupMenu( requireContext(), @@ -1236,7 +1281,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram .show() } - private fun resetInputOverlay() { + fun resetInputOverlay() { resetAllScales() preferences.edit() .putInt("controlOpacity", 50) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt index f7519bb81..0fca53a5d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt @@ -570,7 +570,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex } } - fun refreshControls() { + fun refreshControls(noGameEdit: Boolean = false) { // Remove all the overlay buttons from the HashSet. overlayButtons.clear() overlayDpads.clear() @@ -583,7 +583,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex } // Add all the enabled overlay items back to the HashSet. - if (EmulationMenuSettings.showOverlay) { + if (noGameEdit || EmulationMenuSettings.showOverlay) { addOverlayControls(orientation) } invalidate() diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml index c8e628cda..f0a94ad16 100644 --- a/src/android/app/src/main/res/navigation/emulation_navigation.xml +++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml @@ -15,6 +15,10 @@ app:argType="org.citra.citra_emu.model.Game" app:nullable="true" android:defaultValue="@null" /> + Decompression failed. Already installed applications cannot be compressed or decompressed. + + Input Overlay + Enable Touch Input Overlay + Edit Touch Input Overlay + Edit in realtime + Edit the touch overlay without having to open a game. + From e210aa8a790d2c65c5d0e0896189005cbf81982d Mon Sep 17 00:00:00 2001 From: ptyfyre <254117058+ptyfyre@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:29:42 +0200 Subject: [PATCH 2/2] no back button press i guess Due to the way the emulationActivity is launched from settingsActivity, the callbacks doesn't register unless the emulationActivity is firstly launched from MainActivity with fragment navigation, I'm removing it for now till i find a proper way to do it. --- .../org/citra/citra_emu/fragments/EmulationFragment.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 1f4b44822..95eaa41f1 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -602,16 +602,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) binding.surfaceInputOverlay.visibility = View.VISIBLE binding.loadingIndicator.visibility = View.GONE - - // in no game edit mode, back = done - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finishNoGameEditMode() - } - } - ) } private fun finishNoGameEditMode() {