From e4dccd5a5cca46eb98d2474060758bab58126b07 Mon Sep 17 00:00:00 2001 From: Producdevity Date: Wed, 17 Dec 2025 03:59:46 +0100 Subject: [PATCH] [android] setting to auto hide overlay on controller input (#3127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting `HIDE_OVERLAY_ON_CONTROLLER_INPUT` in *Advanced settings → Input Overlay* **Behavior:** - First controller input -> hides overlay - Controller disconnect → shows overlay again - Subsequent controller inputs → ignored (already hidden, so no retrigger needed) - Touch screen → does **not** show overlay (so you can use a controller and touchscreen to interact with games) - Sidebar "Show/Hide controller" button → still works as master toggle **State reset: The "first input" detection resets when:** 1. Controller disconnects 2. Overlay is shown via sidebar button 3. Controller reconnects **Interaction with other settings:** - Requires `SHOW_INPUT_OVERLAY` to be enabled (basicaly a master switch) - Independent from `ENABLE_INPUT_OVERLAY_AUTO_HIDE` (timer-based hide, was already implemented) - When both are enabled, touch-to-show is disabled (controller-hide takes precedence) Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3127 Reviewed-by: Caio Oliveira Reviewed-by: Maufeat Co-authored-by: Producdevity Co-committed-by: Producdevity --- .../yuzu_emu/activities/EmulationActivity.kt | 76 +++++++++++++++++-- .../features/settings/model/BooleanSetting.kt | 1 + .../settings/model/view/SettingsItem.kt | 7 ++ .../settings/ui/SettingsFragmentPresenter.kt | 1 + .../yuzu_emu/fragments/EmulationFragment.kt | 35 ++++++++- .../app/src/main/jni/android_settings.h | 5 ++ .../app/src/main/res/values/strings.xml | 2 + 7 files changed, 115 insertions(+), 12 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index f4ddde2e25..40c0af0b24 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -18,6 +18,7 @@ import android.content.IntentFilter import android.content.res.Configuration import android.graphics.Rect import android.graphics.drawable.Icon +import android.hardware.input.InputManager import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener @@ -63,11 +64,12 @@ import kotlin.math.roundToInt import org.yuzu.yuzu_emu.utils.ForegroundService import androidx.core.os.BundleCompat -class EmulationActivity : AppCompatActivity(), SensorEventListener { +class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager.InputDeviceListener { private lateinit var binding: ActivityEmulationBinding var isActivityRecreated = false private lateinit var nfcReader: NfcReader + private lateinit var inputManager: InputManager private var touchDownTime: Long = 0 private val maxTapDuration = 500L @@ -140,6 +142,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { nfcReader = NfcReader(this) nfcReader.initialize() + inputManager = getSystemService(INPUT_SERVICE) as InputManager + inputManager.registerInputDeviceListener(this, null) + foregroundService = Intent(this, ForegroundService::class.java) startForegroundService(foregroundService) @@ -206,9 +211,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { override fun onDestroy() { super.onDestroy() + inputManager.unregisterInputDeviceListener(this) stopForegroundService(this) NativeLibrary.playTimeManagerStop() - } override fun onUserLeaveHint() { @@ -244,8 +249,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { val isPhysicalKeyboard = event.source and InputDevice.SOURCE_KEYBOARD == InputDevice.SOURCE_KEYBOARD && event.device?.isVirtual == false - if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && - event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD && + val isControllerInput = event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK || + event.source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD + + if (!isControllerInput && event.source and InputDevice.SOURCE_MOUSE != InputDevice.SOURCE_MOUSE && !isPhysicalKeyboard ) { @@ -256,12 +263,18 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { return super.dispatchKeyEvent(event) } + if (isControllerInput && event.action == KeyEvent.ACTION_DOWN) { + notifyControllerInput() + } + return InputHandler.dispatchKeyEvent(event) } override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { - if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && - event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD && + val isControllerInput = event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK || + event.source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD + + if (!isControllerInput && event.source and InputDevice.SOURCE_KEYBOARD != InputDevice.SOURCE_KEYBOARD && event.source and InputDevice.SOURCE_MOUSE != InputDevice.SOURCE_MOUSE ) { @@ -277,9 +290,54 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { return true } + if (isControllerInput) { + notifyControllerInput() + } + return InputHandler.dispatchGenericMotionEvent(event) } + private fun notifyControllerInput() { + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as? NavHostFragment + val emulationFragment = + navHostFragment?.childFragmentManager?.fragments?.firstOrNull() as? org.yuzu.yuzu_emu.fragments.EmulationFragment + emulationFragment?.onControllerInputDetected() + } + + private fun isGameController(deviceId: Int): Boolean { + val device = InputDevice.getDevice(deviceId) ?: return false + val sources = device.sources + return sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || + sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK + } + + override fun onInputDeviceAdded(deviceId: Int) { + if (isGameController(deviceId)) { + InputHandler.updateControllerData() + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as? NavHostFragment + val emulationFragment = + navHostFragment?.childFragmentManager?.fragments?.firstOrNull() as? org.yuzu.yuzu_emu.fragments.EmulationFragment + emulationFragment?.onControllerConnected() + } + } + + override fun onInputDeviceRemoved(deviceId: Int) { + InputHandler.updateControllerData() + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as? NavHostFragment + val emulationFragment = + navHostFragment?.childFragmentManager?.fragments?.firstOrNull() as? org.yuzu.yuzu_emu.fragments.EmulationFragment + emulationFragment?.onControllerDisconnected() + } + + override fun onInputDeviceChanged(deviceId: Int) { + if (isGameController(deviceId)) { + InputHandler.updateControllerData() + } + } + override fun onSensorChanged(event: SensorEvent) { val rotation = this.display?.rotation if (rotation == Surface.ROTATION_90) { @@ -519,8 +577,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { when (event.action) { MotionEvent.ACTION_DOWN -> { touchDownTime = System.currentTimeMillis() - // show overlay immediately on touch and cancel timer - if (!emulationViewModel.drawerOpen.value) { + // show overlay immediately on touch and cancel timer when only auto-hide is enabled + if (!emulationViewModel.drawerOpen.value && + BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean() && + !BooleanSetting.HIDE_OVERLAY_ON_CONTROLLER_INPUT.getBoolean()) { fragment.handler.removeCallbacksAndMessages(null) fragment.showOverlay() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index 2e72e15846..475d9192c6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -52,6 +52,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { SOC_OVERLAY_BACKGROUND("soc_overlay_background"), ENABLE_INPUT_OVERLAY_AUTO_HIDE("enable_input_overlay_auto_hide"), + HIDE_OVERLAY_ON_CONTROLLER_INPUT("hide_overlay_on_controller_input"), PERF_OVERLAY_BACKGROUND("perf_overlay_background"), SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index b1fe56a866..62929bf371 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -387,6 +387,13 @@ abstract class SettingsItem( valueHint = R.string.seconds ) ) + put( + SwitchSetting( + BooleanSetting.HIDE_OVERLAY_ON_CONTROLLER_INPUT, + titleId = R.string.hide_overlay_on_controller_input, + descriptionId = R.string.hide_overlay_on_controller_input_description + ) + ) put( SwitchSetting( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 80b6ddb7b2..8d05abf703 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -274,6 +274,7 @@ class SettingsFragmentPresenter( sl.apply { add(BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.key) add(IntSetting.INPUT_OVERLAY_AUTO_HIDE.key) + add(BooleanSetting.HIDE_OVERLAY_ON_CONTROLLER_INPUT.key) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 37e187380e..3f5abc0858 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -93,7 +93,6 @@ import org.yuzu.yuzu_emu.utils.collect import org.yuzu.yuzu_emu.utils.CustomSettingsHandler import java.io.ByteArrayOutputStream import java.io.File -import kotlin.coroutines.coroutineContext import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -106,6 +105,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val handler = Handler(Looper.getMainLooper()) private var isOverlayVisible = true + private var controllerInputReceived = false private var _binding: FragmentEmulationBinding? = null @@ -656,6 +656,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { BooleanSetting.SHOW_INPUT_OVERLAY.setBoolean(newState) updateQuickOverlayMenuEntry(newState) binding.surfaceInputOverlay.refreshControls() + // Sync view visibility with the setting + if (newState) { + showOverlay() + } else { + hideOverlay() + } NativeConfig.saveGlobalConfig() true } @@ -1901,7 +1907,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { companion object { fun fromValue(value: Int): AmiiboState = - values().firstOrNull { it.value == value } ?: Disabled + entries.firstOrNull { it.value == value } ?: Disabled } } @@ -1914,7 +1920,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { companion object { fun fromValue(value: Int): AmiiboLoadResult = - values().firstOrNull { it.value == value } ?: Unknown + entries.firstOrNull { it.value == value } ?: Unknown } } @@ -1971,6 +1977,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { fun showOverlay() { if (!isOverlayVisible) { isOverlayVisible = true + // Reset controller input flag so controller can hide overlay again + controllerInputReceived = false ViewUtils.showView(binding.surfaceInputOverlay, 500) } } @@ -1978,7 +1986,26 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private fun hideOverlay() { if (isOverlayVisible) { isOverlayVisible = false - ViewUtils.hideView(binding.surfaceInputOverlay, 500) + ViewUtils.hideView(binding.surfaceInputOverlay) } } + + fun onControllerInputDetected() { + if (!BooleanSetting.HIDE_OVERLAY_ON_CONTROLLER_INPUT.getBoolean()) return + if (!BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) return + if (controllerInputReceived) return + controllerInputReceived = true + hideOverlay() + } + + fun onControllerConnected() { + controllerInputReceived = false + } + + fun onControllerDisconnected() { + if (!BooleanSetting.HIDE_OVERLAY_ON_CONTROLLER_INPUT.getBoolean()) return + if (!BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) return + controllerInputReceived = false + showOverlay() + } } diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index 19ac95652b..e276f19284 100644 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h @@ -92,6 +92,11 @@ namespace AndroidSettings { Settings::Setting input_overlay_auto_hide{linkage, 5, "input_overlay_auto_hide", Settings::Category::Overlay, Settings::Specialization::Default, true, true, &enable_input_overlay_auto_hide}; + Settings::Setting hide_overlay_on_controller_input{linkage, false, + "hide_overlay_on_controller_input", + Settings::Category::Overlay, + Settings::Specialization::Default, true, + true}; Settings::Setting perf_overlay_background{linkage, false, "perf_overlay_background", Settings::Category::Overlay, Settings::Specialization::Default, true, diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index ef8082a849..5c4854454a 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -26,6 +26,8 @@ Overlay Auto Hide Automatically hide the touch controls overlay after the specified time of inactivity. Enable Overlay Auto Hide + Hide Overlay on Controller Input + Automatically hide the touch controls overlay when a physical controller is used. Overlay reappears when controller is disconnected. Input Overlay Configure on-screen controls