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 ca92b308d..1e372720e 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 @@ -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) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt index 4b22164bb..e2319a7e4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt @@ -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); } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt index 0a4a1ffa3..d01d5f769 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt @@ -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() + + 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 } } 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 02d10cfe9..96100349a 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 @@ -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, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index 482bc0b08..d78f5c3a3 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -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())!!.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 { + val key = getInputButtonKey(keyCode) + val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + var buttonCodes = try { + preferences.getStringSet(key, mutableSetOf()) + } catch (e: ClassCastException) { + val prefInt = preferences.getInt(key, -1); + val migratedSet = if (prefInt != -1) { + mutableSetOf(prefInt.toString()) + } else { + mutableSetOf() + } + migratedSet + } + if (buttonCodes == null) buttonCodes = mutableSetOf() + return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet() + } /** * Helper function to get the settings key for an gamepad button. 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 13c1d2d96..9a40ba90b 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 @@ -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])) diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index fa542ea74..de2a78ee9 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -122,6 +122,8 @@ Circle Pad C-Stick Hotkeys + If the "Hotkey Enable" key is mapped, that key must be pressed in addition to the mapped hotkey + Hotkey Enable Triggers Trigger D-Pad