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 ae170c1c7..fbf3e5525 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 @@ -796,6 +796,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram true } + R.id.menu_emulation_button_sliding -> { + showButtonSlidingMenu() + true + } + R.id.menu_emulation_dpad_slide_enable -> { EmulationMenuSettings.dpadSlide = !EmulationMenuSettings.dpadSlide true @@ -840,6 +845,28 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.show() } + private fun showButtonSlidingMenu() { + val editor = preferences.edit() + + val buttonSlidingModes = mutableListOf() + buttonSlidingModes.add(getString(R.string.emulation_button_sliding_disabled)) + buttonSlidingModes.add(getString(R.string.emulation_button_sliding_enabled)) + buttonSlidingModes.add(getString(R.string.emulation_button_sliding_alternative)) + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.emulation_button_sliding) + .setSingleChoiceItems( + buttonSlidingModes.toTypedArray(), + EmulationMenuSettings.buttonSlide + ) { _: DialogInterface?, which: Int -> + EmulationMenuSettings.buttonSlide = which + } + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + editor.apply() + } + .show() + } + private fun showLandscapeScreenLayoutMenu() { val popupMenu = PopupMenu( requireContext(), 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 7540fe529..23dcddc78 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 @@ -96,57 +96,115 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex if (isInEditMode) { return onTouchWhileEditing(event) } - var shouldUpdateView = false + + var hasActiveButtons = false + val pointerIndex = event.actionIndex + val pointerId = event.getPointerId(pointerIndex) for (button in overlayButtons) { - if (!button.updateStatus(event, this)) { - continue + if (button.trackId == pointerId) { + hasActiveButtons = true + break } - - if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) { - swapScreen() - } - - if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) { - TurboHelper.toggleTurbo(true) - } - - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.id, button.status) - shouldUpdateView = true } - for (dpad in overlayDpads) { - if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide, this)) { - continue + var hasActiveDpad = false + if (!hasActiveButtons) { + for (dpad in overlayDpads) { + if (dpad.trackId == pointerId) { + hasActiveDpad = true + break + } } - NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.upId, dpad.upStatus) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - dpad.downId, - dpad.downStatus - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - dpad.leftId, - dpad.leftStatus - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - dpad.rightId, - dpad.rightStatus - ) - shouldUpdateView = true } - for (joystick in overlayJoysticks) { - if (!joystick.updateStatus(event, this)) { - continue + + var hasActiveJoystick = false + if(!hasActiveButtons && !hasActiveDpad){ + for (joystick in overlayJoysticks) { + if (joystick.trackId == pointerId) { + hasActiveJoystick = true + break + } + } + } + + var shouldUpdateView = false + if(!hasActiveDpad && !hasActiveJoystick) { + for (button in overlayButtons) { + val stateChanged = button.updateStatus(event, hasActiveButtons, this) + if (!stateChanged) { + continue + } + + if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) { + swapScreen() + } + else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) { + TurboHelper.toggleTurbo(true) + } + + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + button.id, + button.status + ) + + shouldUpdateView = true + } + } + + if(!hasActiveButtons && !hasActiveJoystick) { + for (dpad in overlayDpads) { + val stateChanged = dpad.updateStatus( + event, + hasActiveDpad, + EmulationMenuSettings.dpadSlide, + this + ) + if (!stateChanged) { + continue + } + + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + dpad.upId, + dpad.upStatus + ) + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + dpad.downId, + dpad.downStatus + ) + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + dpad.leftId, + dpad.leftStatus + ) + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + dpad.rightId, + dpad.rightStatus + ) + + shouldUpdateView = true + } + } + + if(!hasActiveDpad && !hasActiveButtons) { + for (joystick in overlayJoysticks) { + val stateChanged = joystick.updateStatus(event, hasActiveJoystick, this) + if (!stateChanged) { + continue + } + + val axisID = joystick.joystickId + NativeLibrary.onGamePadMoveEvent( + NativeLibrary.TouchScreenDevice, + axisID, + joystick.xAxis, + joystick.yAxis + ) + + shouldUpdateView = true } - val axisID = joystick.joystickId - NativeLibrary.onGamePadMoveEvent( - NativeLibrary.TouchScreenDevice, - axisID, - joystick.xAxis, - joystick.yAxis - ) - shouldUpdateView = true } if (shouldUpdateView) { @@ -157,10 +215,8 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex return true } - val pointerIndex = event.actionIndex val xPosition = event.getX(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt() - val pointerId = event.getPointerId(pointerIndex) val motionEvent = event.action and MotionEvent.ACTION_MASK val isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt index 541d3270f..0e9b0185d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt @@ -1,4 +1,4 @@ -// 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. @@ -12,6 +12,20 @@ import android.graphics.drawable.BitmapDrawable import android.view.HapticFeedbackConstants import android.view.MotionEvent import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.utils.EmulationMenuSettings + +enum class ButtonSlidingMode(val int: Int) { + // Disabled, buttons can only be triggered by pressing them directly. + Disabled(0), + + // Additionally to pressing buttons directly, they can be activated and released by sliding into + // and out of their area. + Enabled(1), + + // The first button is kept activated until released, further buttons use the simple button + // sliding method. + Alternative(2) +} /** * Custom [BitmapDrawable] that is capable @@ -30,6 +44,9 @@ class InputOverlayDrawableButton( val opacity: Int ) { var trackId: Int + + private var isMotionFirstButton = false // mark the first activated button with the current motion + private var previousTouchX = 0 private var previousTouchY = 0 private var controlPositionX = 0 @@ -53,7 +70,8 @@ class InputOverlayDrawableButton( * * @return true if value was changed */ - fun updateStatus(event: MotionEvent, overlay:InputOverlay): Boolean { + fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean { + val buttonSliding = EmulationMenuSettings.buttonSlide val pointerIndex = event.actionIndex val xPosition = event.getX(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt() @@ -67,23 +85,60 @@ class InputOverlayDrawableButton( if (!bounds.contains(xPosition, yPosition)) { return false } - pressedState = true - trackId = pointerId - overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + buttonDown(true, pointerId, overlay) return true } if (isActionUp) { if (trackId != pointerId) { return false } - pressedState = false - trackId = -1 - overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE) + buttonUp(overlay) return true } + + val isActionMoving = motionEvent == MotionEvent.ACTION_MOVE + if (buttonSliding != ButtonSlidingMode.Disabled.int && isActionMoving) { + val inside = bounds.contains(xPosition, yPosition) + if (pressedState) { + // button is already pressed + // check whether we moved out of the button area to update the state + if (inside || trackId != pointerId) { + return false + } + // prevent the first (directly pressed) button to deactivate when sliding off + if (buttonSliding == ButtonSlidingMode.Alternative.int && isMotionFirstButton) { + return false + } + buttonUp(overlay) + return true + } else { + // button was not yet pressed + // check whether we moved into the button area to update the state + if (!inside) { + return false + } + buttonDown(!hasActiveButtons, pointerId, overlay) + return true + } + } + return false } + private fun buttonDown(firstBtn: Boolean, pointerId: Int, overlay: InputOverlay) { + pressedState = true + isMotionFirstButton = firstBtn + trackId = pointerId + overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + } + + private fun buttonUp(overlay: InputOverlay) { + pressedState = false + isMotionFirstButton = false + trackId = -1 + overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE) + } + fun onConfigureTouch(event: MotionEvent): Boolean { val pointerIndex = event.actionIndex val fingerPositionX = event.getX(pointerIndex).toInt() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt index bc23201f1..c032ee3bc 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt @@ -1,4 +1,4 @@ -// 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. @@ -12,6 +12,7 @@ import android.graphics.drawable.BitmapDrawable import android.view.HapticFeedbackConstants import android.view.MotionEvent import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.utils.EmulationMenuSettings /** * Custom [BitmapDrawable] that is capable @@ -62,15 +63,19 @@ class InputOverlayDrawableDpad( trackId = -1 } - fun updateStatus(event: MotionEvent, dpadSlide: Boolean, overlay:InputOverlay): Boolean { + fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, dpadSlide: Boolean, overlay: InputOverlay): Boolean { var isDown = false val pointerIndex = event.actionIndex val xPosition = event.getX(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt() val pointerId = event.getPointerId(pointerIndex) val motionEvent = event.action and MotionEvent.ACTION_MASK - val isActionDown = + var isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN + if (!isActionDown && EmulationMenuSettings.buttonSlide != ButtonSlidingMode.Disabled.int) { + isActionDown = motionEvent == MotionEvent.ACTION_MOVE && !hasActiveButtons + } + val isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP if (isActionDown) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt index 3d90bad23..b66118c90 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt @@ -1,4 +1,4 @@ -// 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. @@ -93,14 +93,18 @@ class InputOverlayDrawableJoystick( currentStateBitmapDrawable.draw(canvas) } - fun updateStatus(event: MotionEvent, overlay:InputOverlay): Boolean { + fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean { val pointerIndex = event.actionIndex val xPosition = event.getX(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt() val pointerId = event.getPointerId(pointerIndex) val motionEvent = event.action and MotionEvent.ACTION_MASK - val isActionDown = + var isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN + if (!isActionDown && EmulationMenuSettings.buttonSlide != ButtonSlidingMode.Disabled.int) { + isActionDown = motionEvent == MotionEvent.ACTION_MOVE && !hasActiveButtons + } + val isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP if (isActionDown) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.kt index 6d633360b..184964549 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.kt @@ -1,4 +1,4 @@ -// 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. @@ -7,6 +7,7 @@ package org.citra.citra_emu.utils import androidx.drawerlayout.widget.DrawerLayout import androidx.preference.PreferenceManager import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.overlay.ButtonSlidingMode object EmulationMenuSettings { private val preferences = @@ -26,6 +27,13 @@ object EmulationMenuSettings { .putBoolean("EmulationMenuSettings_DpadSlideEnable", value) .apply() } + var buttonSlide: Int + get() = preferences.getInt("EmulationMenuSettings_ButtonSlideMode", ButtonSlidingMode.Disabled.int) + set(value) { + preferences.edit() + .putInt("EmulationMenuSettings_ButtonSlideMode", value) + .apply() + } var showPerformanceOverlay: Boolean get() = preferences.getBoolean("EmulationMenuSettings_showPerformanceOverlay", false) diff --git a/src/android/app/src/main/res/menu/menu_overlay_options.xml b/src/android/app/src/main/res/menu/menu_overlay_options.xml index ca6ae47fe..8bb19ee26 100644 --- a/src/android/app/src/main/res/menu/menu_overlay_options.xml +++ b/src/android/app/src/main/res/menu/menu_overlay_options.xml @@ -86,6 +86,10 @@ android:id="@+id/menu_emulation_adjust_opacity" android:title="@string/emulation_control_opacity" /> + + Configure Controls Edit Layout Done + Button Sliding + Hold originally pressed button + Hold currently pressed button + Hold original and currently pressed button Toggle Controls Adjust Scale Global Scale