Android: Hotkey Enable Button (#1464)

This commit is contained in:
David Griswold 2026-02-26 09:40:42 -08:00 committed by GitHub
parent 5ac0ef8fde
commit 6b2ac400eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 177 additions and 64 deletions

View File

@ -267,36 +267,28 @@ class EmulationActivity : AppCompatActivity() {
return super.dispatchKeyEvent(event)
}
val button =
preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode)
val action: Int = when (event.action) {
when (event.action) {
KeyEvent.ACTION_DOWN -> {
hotkeyUtility.handleHotkey(button)
// On some devices, the back gesture / button press is not intercepted by androidx
// and fails to open the emulation menu. So we're stuck running deprecated code to
// cover for either a fault on androidx's side or in OEM skins (MIUI at least)
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
// If the hotkey is pressed, we don't want to open the drawer
if (!hotkeyUtility.HotkeyIsPressed) {
if (!hotkeyUtility.hotkeyIsPressed) {
onBackPressed()
return true
}
}
// Normal key events.
NativeLibrary.ButtonState.PRESSED
return hotkeyUtility.handleKeyPress(event)
}
KeyEvent.ACTION_UP -> {
hotkeyUtility.HotkeyIsPressed = false
NativeLibrary.ButtonState.RELEASED
return hotkeyUtility.handleKeyRelease(event)
}
else -> {
return false;
}
else -> return false
}
val input = event.device
?: // Controller was disconnected
return false
return NativeLibrary.onGamePadEvent(input.descriptor, button, action)
}
private fun onAmiiboSelected(selectedFile: String) {

View File

@ -11,5 +11,6 @@ enum class Hotkey(val button: Int) {
PAUSE_OR_RESUME(10004),
QUICKSAVE(10005),
QUICKLOAD(10006),
TURBO_LIMIT(10007);
TURBO_LIMIT(10007),
ENABLE(10008);
}

View File

@ -5,50 +5,140 @@
package org.citra.citra_emu.features.hotkeys
import android.content.Context
import android.view.KeyEvent
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.utils.EmulationLifecycleUtil
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
class HotkeyUtility(
private val screenAdjustmentUtil: ScreenAdjustmentUtil,
private val context: Context) {
private val context: Context
) {
private val hotkeyButtons = Hotkey.entries.map { it.button }
var HotkeyIsPressed = false
private var hotkeyIsEnabled = false
var hotkeyIsPressed = false
private val currentlyPressedButtons = mutableSetOf<Int>()
fun handleKeyPress(keyEvent: KeyEvent): Boolean {
var handled = false
val buttonSet = InputBindingSetting.getButtonSet(keyEvent)
val enableButton =
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
.getString(Settings.HOTKEY_ENABLE, "")
val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button)
val thisKeyIsHotkey =
!thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) }
hotkeyIsEnabled = hotkeyIsEnabled || enableButton == "" || thisKeyIsEnableButton
// Now process all internal buttons associated with this keypress
for (button in buttonSet) {
currentlyPressedButtons.add(button)
//option 1 - this is the enable command, which was already handled
if (button == Hotkey.ENABLE.button) {
handled = true
}
// option 2 - this is a different hotkey command
else if (hotkeyButtons.contains(button)) {
if (hotkeyIsEnabled) {
handled = handleHotkey(button) || handled
}
}
// option 3 - this is a normal key
else {
// if this key press is ALSO associated with a hotkey that will process, skip
// the normal key event.
if (!thisKeyIsHotkey || !hotkeyIsEnabled) {
handled = NativeLibrary.onGamePadEvent(
keyEvent.device.descriptor,
button,
NativeLibrary.ButtonState.PRESSED
) || handled
}
}
}
return handled
}
fun handleKeyRelease(keyEvent: KeyEvent): Boolean {
var handled = false
val buttonSet = InputBindingSetting.getButtonSet(keyEvent)
val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button)
val thisKeyIsHotkey =
!thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) }
if (thisKeyIsEnableButton) {
handled = true; hotkeyIsEnabled = false
}
for (button in buttonSet) {
// this is a hotkey button
if (hotkeyButtons.contains(button)) {
currentlyPressedButtons.remove(button)
if (!currentlyPressedButtons.any { hotkeyButtons.contains(it) }) {
// all hotkeys are no longer pressed
hotkeyIsPressed = false
}
} else {
// if this key ALSO sends a hotkey command that we already/will handle,
// or if we did not register the press of this button, e.g. if this key
// was also a hotkey pressed after enable, but released after enable button release, then
// skip the normal key event
if ((!thisKeyIsHotkey || !hotkeyIsEnabled) && currentlyPressedButtons.contains(
button
)
) {
handled = NativeLibrary.onGamePadEvent(
keyEvent.device.descriptor,
button,
NativeLibrary.ButtonState.RELEASED
) || handled
currentlyPressedButtons.remove(button)
}
}
}
return handled
}
fun handleHotkey(bindedButton: Int): Boolean {
if(hotkeyButtons.contains(bindedButton)) {
when (bindedButton) {
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
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.QUICKSAVE.button -> {
NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
Toast.makeText(context,
context.getString(R.string.saving),
Toast.LENGTH_SHORT).show()
}
Hotkey.QUICKLOAD.button -> {
val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT)
val stringRes = if(wasLoaded) {
R.string.loading
} else {
R.string.quickload_not_found
}
Toast.makeText(context,
context.getString(stringRes),
Toast.LENGTH_SHORT).show()
}
else -> {}
when (bindedButton) {
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
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.QUICKSAVE.button -> {
NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
Toast.makeText(
context,
context.getString(R.string.saving),
Toast.LENGTH_SHORT
).show()
}
HotkeyIsPressed = true
return true
Hotkey.QUICKLOAD.button -> {
val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT)
val stringRes = if (wasLoaded) {
R.string.loading
} else {
R.string.quickload_not_found
}
Toast.makeText(
context,
context.getString(stringRes),
Toast.LENGTH_SHORT
).show()
}
else -> {}
}
return false
hotkeyIsPressed = true
return true
}
}

View File

@ -135,6 +135,7 @@ class Settings {
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"
@ -202,6 +203,7 @@ class Settings {
R.string.button_zr
)
val hotKeys = listOf(
HOTKEY_ENABLE,
HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME,
@ -211,6 +213,7 @@ class Settings {
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,

View File

@ -128,6 +128,7 @@ class InputBindingSetting(
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
@ -162,36 +163,40 @@ class InputBindingSetting(
fun removeOldMapping() {
// Try remove all possible keys we wrote for this setting
val oldKey = preferences.getString(reverseKey, "")
(setting as AbstractStringSetting).string = ""
if (oldKey != "") {
(setting as AbstractStringSetting).string = ""
preferences.edit()
.remove(abstractSetting.key) // Used for ui text
.remove(oldKey) // Used for button mapping
.remove(oldKey + "_GuestOrientation") // Used for axis orientation
.remove(oldKey + "_GuestButton") // Used for axis button
.remove(oldKey + "_Inverted") // used for axis inversion
.apply()
.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()
}
}
/**
* Helper function to write a gamepad button mapping for the setting.
*/
private fun writeButtonMapping(key: String) {
private fun writeButtonMapping(keyEvent: KeyEvent) {
val editor = preferences.edit()
// Remove mapping for another setting using this input
val oldButtonCode = preferences.getInt(key, -1)
if (oldButtonCode != -1) {
val oldKey = getButtonKey(oldButtonCode)
editor.remove(oldKey) // Only need to remove UI text setting, others will be overwritten
}
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()
// Write new mapping
editor.putInt(key, buttonCode)
editor.putStringSet(key, buttonCodes.mapTo(mutableSetOf()) {it.toString()})
// Write next reverse mapping for future cleanup
editor.putString(reverseKey, key)
@ -229,7 +234,7 @@ class InputBindingSetting(
}
val code = translateEventToKeyId(keyEvent)
writeButtonMapping(getInputButtonKey(code))
writeButtonMapping(keyEvent)
val uiString = "${keyEvent.device.name}: Button $code"
value = uiString
}
@ -289,6 +294,26 @@ class InputBindingSetting(
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()
}
/**
* Helper function to get the settings key for an gamepad button.

View File

@ -811,7 +811,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
add(InputBindingSetting(button, Settings.triggerTitles[i]))
}
add(HeaderSetting(R.string.controller_hotkeys))
add(HeaderSetting(R.string.controller_hotkeys,R.string.controller_hotkeys_description))
Settings.hotKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key)
add(InputBindingSetting(button, Settings.hotkeyTitles[i]))

View File

@ -122,6 +122,8 @@
<string name="controller_circlepad">Circle Pad</string>
<string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Hotkeys</string>
<string name="controller_hotkeys_description">If the "Hotkey Enable" key is mapped, that key must be pressed in addition to the mapped hotkey</string>
<string name="controller_hotkey_enable_button">Hotkey Enable</string>
<string name="controller_triggers">Triggers</string>
<string name="controller_trigger">Trigger</string>
<string name="controller_dpad">D-Pad</string>