From 526d9d4cea8da1e1d9e704d2c0bfcc74acb70431 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 27 Feb 2026 12:57:41 -0600 Subject: [PATCH] android: Add auto-map controller button with long-press to clear all bindings (#1769) --- .../model/view/InputBindingSetting.kt | 256 +++++++++++++++++- .../settings/model/view/RunnableSetting.kt | 9 +- .../settings/model/view/SettingsItem.kt | 2 +- .../features/settings/ui/SettingsAdapter.kt | 47 +++- .../settings/ui/SettingsFragmentPresenter.kt | 10 + .../ui/viewholder/RunnableViewHolder.kt | 7 +- .../fragments/AutoMapDialogFragment.kt | 152 +++++++++++ .../drawable-xxxhdpi/automap_face_buttons.png | Bin 0 -> 33023 bytes .../src/main/res/layout/dialog_auto_map.xml | 55 ++++ .../app/src/main/res/values/strings.xml | 6 + 10 files changed, 509 insertions(+), 35 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt create mode 100644 src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png create mode 100644 src/android/app/src/main/res/layout/dialog_auto_map.xml 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 d78f5c3a3..6ec851db1 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 @@ -9,6 +9,7 @@ import android.content.SharedPreferences import android.view.InputDevice import android.view.InputDevice.MotionRange import android.view.KeyEvent +import android.view.MotionEvent import android.widget.Toast import androidx.preference.PreferenceManager import org.citra.citra_emu.CitraApplication @@ -235,8 +236,7 @@ class InputBindingSetting( val code = translateEventToKeyId(keyEvent) writeButtonMapping(keyEvent) - val uiString = "${keyEvent.device.name}: Button $code" - value = uiString + value = "${keyEvent.device.name}: ${getButtonName(code)}" } /** @@ -263,8 +263,7 @@ class InputBindingSetting( // use UP (-) to map vertical, but use RIGHT (+) to map horizontal val inverted = if (isHorizontalOrientation()) axisDir == '-' else axisDir == '+' writeAxisMapping(motionRange.axis, button, inverted) - val uiString = "${device.name}: Axis ${motionRange.axis}" + axisDir - value = uiString + value = "Axis ${motionRange.axis}$axisDir" } override val type = TYPE_INPUT_BINDING @@ -272,6 +271,241 @@ class InputBindingSetting( companion object { private const val INPUT_MAPPING_PREFIX = "InputMapping" + private fun toTitleCase(raw: String): String = + raw.replace("_", " ").lowercase() + .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + + private const val BUTTON_NAME_L3 = "Button L3" + private const val BUTTON_NAME_R3 = "Button R3" + + private val buttonNameOverrides = mapOf( + KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3, + KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3, + LINUX_BTN_DPAD_UP to "Dpad Up", + LINUX_BTN_DPAD_DOWN to "Dpad Down", + LINUX_BTN_DPAD_LEFT to "Dpad Left", + LINUX_BTN_DPAD_RIGHT to "Dpad Right" + ) + + fun getButtonName(keyCode: Int): String = + buttonNameOverrides[keyCode] + ?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_")) + + private data class DefaultButtonMapping( + val settingKey: String, + val hostKeyCode: Int, + val guestButtonCode: Int + ) + // Auto-map always sets inverted = false. Users needing inverted axes should remap manually. + private data class DefaultAxisMapping( + val settingKey: String, + val hostAxis: Int, + val guestButton: Int, + val orientation: Int, + val inverted: Boolean + ) + + private val xboxFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_Y) + ) + + private val nintendoFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) + ) + + private val commonButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1, NativeLibrary.ButtonType.TRIGGER_L), + DefaultButtonMapping(Settings.KEY_BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1, NativeLibrary.ButtonType.TRIGGER_R), + DefaultButtonMapping(Settings.KEY_BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2, NativeLibrary.ButtonType.BUTTON_ZL), + DefaultButtonMapping(Settings.KEY_BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2, NativeLibrary.ButtonType.BUTTON_ZR), + DefaultButtonMapping(Settings.KEY_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT, NativeLibrary.ButtonType.BUTTON_SELECT), + DefaultButtonMapping(Settings.KEY_BUTTON_START, KeyEvent.KEYCODE_BUTTON_START, NativeLibrary.ButtonType.BUTTON_START) + ) + + private val dpadButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_UP, KeyEvent.KEYCODE_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), + DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), + DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, KeyEvent.KEYCODE_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), + DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) + ) + + private val stickAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_Z, NativeLibrary.ButtonType.STICK_C, 0, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RZ, NativeLibrary.ButtonType.STICK_C, 1, false) + ) + + private val dpadAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_DPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_HAT_X, NativeLibrary.ButtonType.DPAD, 0, false), + DefaultAxisMapping(Settings.KEY_DPAD_AXIS_VERTICAL, MotionEvent.AXIS_HAT_Y, NativeLibrary.ButtonType.DPAD, 1, false) + ) + + // Nintendo Switch Joy-Con specific mappings. + // Joy-Cons connected via Bluetooth on Android have several quirks: + // - They register as two separate InputDevices (left and right) + // - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A) + // but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y) + // - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes + // - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ + private const val NINTENDO_VENDOR_ID = 0x057e + + // Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as + // KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't + // translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to + // the scan code in that case. + private const val LINUX_BTN_DPAD_UP = 0x220 // 544 + private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545 + private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546 + private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547 + + // Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not. + // This is different from both the standard Xbox table (full swap) and the + // Nintendo table (no swap). + private val joyconFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) + ) + + // Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN + private val joyconDpadButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_UP, LINUX_BTN_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), + DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, LINUX_BTN_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), + DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, LINUX_BTN_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), + DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, LINUX_BTN_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) + ) + + // Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY + // (not Z/RZ like most controllers). The horizontal axis is inverted relative to + // the standard orientation - verified empirically on paired Joy-Cons via Bluetooth. + private val joyconStickAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_RX, NativeLibrary.ButtonType.STICK_C, 0, true), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RY, NativeLibrary.ButtonType.STICK_C, 1, false) + ) + + /** + * Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a + * Pro Controller or other Nintendo device) by checking vendor ID + device + * capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the + * right stick, while the Pro Controller has standard HAT axes and Z/RZ. + */ + fun isJoyCon(device: InputDevice?): Boolean { + if (device == null) return false + if (device.vendorId != NINTENDO_VENDOR_ID) return false + + // Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick). + // Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ. + var hasHatAxes = false + var hasStandardRightStick = false + for (range in device.motionRanges) { + when (range.axis) { + MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true + MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true + } + } + return !hasHatAxes && !hasStandardRightStick + } + + private val allBindingKeys: Set by lazy { + (Settings.buttonKeys + Settings.triggerKeys + + Settings.circlePadKeys + Settings.cStickKeys + Settings.dPadAxisKeys + + Settings.dPadButtonKeys).toSet() + } + + fun clearAllBindings() { + val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + val editor = prefs.edit() + val allKeys = prefs.all.keys.toList() + for (key in allKeys) { + if (key.startsWith(INPUT_MAPPING_PREFIX) || key in allBindingKeys) { + editor.remove(key) + } + } + editor.apply() + } + + private fun applyBindings( + buttonMappings: List, + axisMappings: List + ) { + val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + val editor = prefs.edit() + buttonMappings.forEach { applyDefaultButtonMapping(editor, it) } + axisMappings.forEach { applyDefaultAxisMapping(editor, it) } + editor.apply() + } + + /** + * Applies Joy-Con specific bindings: scan code D-pad, partial face button + * swap, and AXIS_RX/RY right stick. + */ + fun applyJoyConBindings() { + applyBindings( + joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings, + joyconStickAxisMappings + ) + } + + /** + * Applies auto-mapped bindings based on detected controller layout and d-pad type. + * + * @param isNintendoLayout true if the controller uses Nintendo face button layout + * (A=east, B=south), false for Xbox layout (A=south, B=east) + * @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y), + * false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT) + */ + fun applyAutoMapBindings(isNintendoLayout: Boolean, useAxisDpad: Boolean) { + val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings + val buttonMappings = if (useAxisDpad) { + faceButtons + commonButtonMappings + } else { + faceButtons + commonButtonMappings + dpadButtonMappings + } + val axisMappings = if (useAxisDpad) { + stickAxisMappings + dpadAxisMappings + } else { + stickAxisMappings + } + applyBindings(buttonMappings, axisMappings) + } + + private fun applyDefaultButtonMapping( + editor: SharedPreferences.Editor, + mapping: DefaultButtonMapping + ) { + val prefKey = getInputButtonKey(mapping.hostKeyCode) + editor.putInt(prefKey, mapping.guestButtonCode) + editor.putString(mapping.settingKey, getButtonName(mapping.hostKeyCode)) + editor.putString( + "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}", + prefKey + ) + } + + private fun applyDefaultAxisMapping( + editor: SharedPreferences.Editor, + mapping: DefaultAxisMapping + ) { + val axisKey = getInputAxisKey(mapping.hostAxis) + editor.putInt(getInputAxisOrientationKey(mapping.hostAxis), mapping.orientation) + editor.putInt(getInputAxisButtonKey(mapping.hostAxis), mapping.guestButton) + editor.putBoolean(getInputAxisInvertedKey(mapping.hostAxis), mapping.inverted) + val dir = if (mapping.orientation == 0) '+' else '-' + editor.putString(mapping.settingKey, "Axis ${mapping.hostAxis}$dir") + val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}_${mapping.orientation}" + editor.putString(reverseKey, axisKey) + } + /** * Returns the settings key for the specified Citra button code. */ @@ -315,18 +549,10 @@ class InputBindingSetting( return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet() } - /** - * Helper function to get the settings key for an gamepad button. - * - */ - @Deprecated("Use the new getInputButtonKey(keyEvent) method to handle unknown keys") - fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}" + private fun getInputButtonKey(keyId: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyId}" - /** - * Helper function to get the settings key for an gamepad button. - * - */ - fun getInputButtonKey(event: KeyEvent): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${translateEventToKeyId(event)}" + /** Falls back to the scan code when keyCode is KEYCODE_UNKNOWN. */ + fun getInputButtonKey(event: KeyEvent): String = getInputButtonKey(translateEventToKeyId(event)) /** * Helper function to get the settings key for an gamepad axis. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt index 99039556b..54e8fd09b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt @@ -1,10 +1,11 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.model.view import androidx.annotation.DrawableRes +import org.citra.citra_emu.activities.EmulationActivity class RunnableSetting( titleId: Int, @@ -12,7 +13,11 @@ class RunnableSetting( val isRuntimeRunnable: Boolean, @DrawableRes val iconId: Int = 0, val runnable: () -> Unit, - val value: (() -> String)? = null + val value: (() -> String)? = null, + val onLongClick: (() -> Boolean)? = null ) : SettingsItem(null, titleId, descriptionId) { override val type = TYPE_RUNNABLE + + override val isEditable: Boolean + get() = if (EmulationActivity.isRunning()) isRuntimeRunnable else true } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt index 68aa2226c..066912dd9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt @@ -22,7 +22,7 @@ abstract class SettingsItem( ) { abstract val type: Int - val isEditable: Boolean + open val isEditable: Boolean get() { if (!EmulationActivity.isRunning()) return true return setting?.isRuntimeEditable ?: false diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index 054ff8d63..43a1dcbbd 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -65,6 +65,7 @@ import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.StringInputViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder +import org.citra.citra_emu.fragments.AutoMapDialogFragment import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment import org.citra.citra_emu.utils.SystemSaveGame @@ -642,26 +643,42 @@ class SettingsAdapter( ).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG) } + fun onClickAutoMap() { + val activity = fragmentView.activityView as FragmentActivity + AutoMapDialogFragment.newInstance { + fragmentView.loadSettingsList() + fragmentView.onSettingChanged() + }.show(activity.supportFragmentManager, AutoMapDialogFragment.TAG) + } + + fun onLongClickAutoMap(): Boolean { + showConfirmationDialog(R.string.controller_clear_all, R.string.controller_clear_all_confirm) { + InputBindingSetting.clearAllBindings() + fragmentView.loadSettingsList() + fragmentView.onSettingChanged() + } + return true + } + fun onClickRegenerateConsoleId() { - MaterialAlertDialogBuilder(context) - .setTitle(R.string.regenerate_console_id) - .setMessage(R.string.regenerate_console_id_description) - .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - SystemSaveGame.regenerateConsoleId() - notifyDataSetChanged() - } - .setNegativeButton(android.R.string.cancel, null) - .show() + showConfirmationDialog(R.string.regenerate_console_id, R.string.regenerate_console_id_description) { + SystemSaveGame.regenerateConsoleId() + notifyDataSetChanged() + } } fun onClickRegenerateMAC() { + showConfirmationDialog(R.string.regenerate_mac_address, R.string.regenerate_mac_address_description) { + SystemSaveGame.regenerateMac() + notifyDataSetChanged() + } + } + + private fun showConfirmationDialog(titleId: Int, messageId: Int, onConfirm: () -> Unit) { MaterialAlertDialogBuilder(context) - .setTitle(R.string.regenerate_mac_address) - .setMessage(R.string.regenerate_mac_address_description) - .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - SystemSaveGame.regenerateMac() - notifyDataSetChanged() - } + .setTitle(titleId) + .setMessage(messageId) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> onConfirm() } .setNegativeButton(android.R.string.cancel, null) .show() } 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 be70309ac..1b7812342 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 @@ -779,6 +779,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addControlsSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls)) sl.apply { + add( + RunnableSetting( + R.string.controller_auto_map, + R.string.controller_auto_map_description, + true, + R.drawable.ic_controller, + { settingsAdapter.onClickAutoMap() }, + onLongClick = { settingsAdapter.onLongClickAutoMap() } + ) + ) add(HeaderSetting(R.string.generic_buttons)) Settings.buttonKeys.forEachIndexed { i: Int, key: String -> val button = getInputObject(key) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt index e3119e60d..d75368598 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt @@ -67,7 +67,10 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA } override fun onLongClick(clicked: View): Boolean { - // no-op - return true + if (!setting.isEditable) { + adapter.onClickDisabledSetting(true) + return true + } + return setting.onLongClick?.invoke() ?: true } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt new file mode 100644 index 000000000..569a0caca --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt @@ -0,0 +1,152 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.fragments + +import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.citra.citra_emu.R +import org.citra.citra_emu.databinding.DialogAutoMapBinding +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.utils.Log + +/** + * Captures a single button press to detect controller layout (Xbox vs Nintendo) + * and d-pad type (axis vs button), then applies the appropriate bindings. + */ +class AutoMapDialogFragment : BottomSheetDialogFragment() { + private var _binding: DialogAutoMapBinding? = null + private val binding get() = _binding!! + + private var onComplete: (() -> Unit)? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogAutoMapBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + BottomSheetBehavior.from(view.parent as View).state = + BottomSheetBehavior.STATE_EXPANDED + + isCancelable = false + view.requestFocus() + view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } + + binding.textTitle.setText(R.string.controller_auto_map) + binding.textMessage.setText(R.string.auto_map_prompt) + + binding.imageFaceButtons.setImageResource(R.drawable.automap_face_buttons) + + dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } + + binding.buttonCancel.setOnClickListener { + dismiss() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun onKeyEvent(event: KeyEvent): Boolean { + if (event.action != KeyEvent.ACTION_UP) return false + + val keyCode = event.keyCode + val device = event.device + + // Check if this is a Nintendo Switch Joy-Con (not Pro Controller). + // Joy-Cons have unique quirks: split devices, non-standard D-pad scan codes, + // partial A/B swap but no X/Y swap from Android's evdev layer. + val isJoyCon = InputBindingSetting.isJoyCon(device) + + if (isJoyCon) { + Log.info("[AutoMap] Detected Joy-Con - using Joy-Con mappings") + InputBindingSetting.clearAllBindings() + InputBindingSetting.applyJoyConBindings() + onComplete?.invoke() + dismiss() + return true + } + + // For non-Joy-Con controllers, determine layout from which keycode arrives + // for the east/right position. + // The user is pressing the button in the "A" (east/right) position on the 3DS diamond. + // Xbox layout: east position sends KEYCODE_BUTTON_B (97) + // Nintendo layout: east position sends KEYCODE_BUTTON_A (96) + val isNintendoLayout = when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A -> true + KeyEvent.KEYCODE_BUTTON_B -> false + else -> { + // Unrecognized button - ignore and wait for a valid press + Log.warning("[AutoMap] Ignoring unrecognized keycode $keyCode, waiting for A or B") + return true + } + } + + val layoutName = if (isNintendoLayout) "Nintendo" else "Xbox" + Log.info("[AutoMap] Detected $layoutName layout (keyCode=$keyCode)") + + val useAxisDpad = detectDpadType(device) + + val dpadName = if (useAxisDpad) "axis" else "button" + Log.info("[AutoMap] Detected $dpadName d-pad (device=${device?.name})") + + InputBindingSetting.clearAllBindings() + InputBindingSetting.applyAutoMapBindings(isNintendoLayout, useAxisDpad) + + onComplete?.invoke() + dismiss() + return true + } + + companion object { + const val TAG = "AutoMapDialogFragment" + + fun newInstance( + onComplete: () -> Unit + ): AutoMapDialogFragment { + val dialog = AutoMapDialogFragment() + dialog.onComplete = onComplete + return dialog + } + + /** + * Returns true for axis d-pad (HAT_X/HAT_Y), false for button d-pad (DPAD_UP/DOWN/LEFT/RIGHT). + * Prefers axis when both are present. Defaults to axis if detection fails. + */ + private fun detectDpadType(device: InputDevice?): Boolean { + if (device == null) return true + + val hasAxisDpad = device.motionRanges.any { + it.axis == MotionEvent.AXIS_HAT_X || it.axis == MotionEvent.AXIS_HAT_Y + } + + if (hasAxisDpad) return true + + val dpadKeyCodes = intArrayOf( + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT + ) + val hasButtonDpad = device.hasKeys(*dpadKeyCodes).any { it } + + return !hasButtonDpad + } + } +} diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png b/src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..60f7c2badd1f74dfac20360111ccb8616d0fd3ec GIT binary patch literal 33023 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 literal 0 HcmV?d00001 diff --git a/src/android/app/src/main/res/layout/dialog_auto_map.xml b/src/android/app/src/main/res/layout/dialog_auto_map.xml new file mode 100644 index 000000000..95cb8d138 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_auto_map.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + +