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 40c0af0b24..2dba024592 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 @@ -639,6 +639,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager companion object { const val EXTRA_SELECTED_GAME = "SelectedGame" + const val EXTRA_OVERLAY_GAMELESS_EDIT_MODE = "overlayGamelessEditMode" fun stopForegroundService(activity: Activity) { val startIntent = Intent(activity, ForegroundService::class.java) @@ -652,6 +653,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager activity.startActivity(launcher) } + fun launchForOverlayEdit(context: Context): Intent { + return Intent(context, EmulationActivity::class.java).apply { + putExtra(EXTRA_OVERLAY_GAMELESS_EDIT_MODE, true) + } + } + private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean { if (view == null) { return true 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 e173fbab8b..bd898251e6 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 @@ -40,6 +40,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { DPAD_SLIDE("dpad_slide"), HAPTIC_FEEDBACK("haptic_feedback"), SHOW_INPUT_OVERLAY("show_input_overlay"), + OVERLAY_SNAP_TO_GRID("overlay_snap_to_grid"), TOUCHSCREEN("touchscreen"), AIRPLANE_MODE("airplane_mode"), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index d74ca69804..8ec498ad22 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -64,6 +64,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { WIFI_WEB_AUTH_APPLET("wifi_web_auth_applet_mode"), MY_PAGE_APPLET("my_page_applet_mode"), INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide"), + OVERLAY_GRID_SIZE("overlay_grid_size"), DEBUG_KNOBS("debug_knobs") ; diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/LaunchableSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/LaunchableSetting.kt new file mode 100644 index 0000000000..eb25f0989f --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/LaunchableSetting.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import android.content.Intent +import androidx.annotation.StringRes + +/** + * A settings item that launches an intent when clicked. + */ +class LaunchableSetting( + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", + val launchIntent: (android.content.Context) -> Intent +) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { + override val type = SettingsItem.TYPE_LAUNCHABLE +} 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 73336abfb4..39fb435b4d 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 @@ -97,6 +97,7 @@ abstract class SettingsItem( const val TYPE_INPUT_PROFILE = 10 const val TYPE_STRING_INPUT = 11 const val TYPE_SPINBOX = 12 + const val TYPE_LAUNCHABLE = 13 const val FASTMEM_COMBINED = "fastmem_combined" @@ -364,6 +365,30 @@ abstract class SettingsItem( warningMessage = R.string.warning_resolution ) ) + put( + SwitchSetting( + BooleanSetting.SHOW_INPUT_OVERLAY, + titleId = R.string.show_input_overlay, + descriptionId = R.string.show_input_overlay_description + ) + ) + put( + SwitchSetting( + BooleanSetting.OVERLAY_SNAP_TO_GRID, + titleId = R.string.overlay_snap_to_grid, + descriptionId = R.string.overlay_snap_to_grid_description + ) + ) + put( + SliderSetting( + IntSetting.OVERLAY_GRID_SIZE, + titleId = R.string.overlay_grid_size, + descriptionId = R.string.overlay_grid_size_description, + min = 16, + max = 128, + units = "px" + ) + ) put( SwitchSetting( BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index 71a3e54cb3..576af8ece8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -93,6 +93,9 @@ class SettingsAdapter( StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this) } + SettingsItem.TYPE_LAUNCHABLE -> { + LaunchableViewHolder(ListItemSettingBinding.inflate(inflater), this) + } else -> { HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) } @@ -209,6 +212,11 @@ class SettingsAdapter( fragment.view?.findNavController()?.navigate(action) } + fun onLaunchableClick(item: LaunchableSetting) { + val intent = item.launchIntent(context) + fragment.requireActivity().startActivity(intent) + } + fun onInputProfileClick(item: InputProfileSetting, position: Int) { InputProfileDialogFragment.newInstance( settingsViewModel, 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 5f31dd7ed2..ca5df58fe8 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 @@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.input.model.AnalogDirection import org.yuzu.yuzu_emu.features.input.model.NativeAnalog @@ -294,6 +295,19 @@ class SettingsFragmentPresenter( private fun addInputOverlaySettings(sl: ArrayList) { sl.apply { + add(BooleanSetting.SHOW_INPUT_OVERLAY.key) + add(BooleanSetting.OVERLAY_SNAP_TO_GRID.key) + add(IntSetting.OVERLAY_GRID_SIZE.key) + add( + LaunchableSetting( + titleId = R.string.edit_overlay_layout, + descriptionId = R.string.edit_overlay_layout_description, + launchIntent = { context -> + EmulationActivity.launchForOverlayEdit(context) + } + ) + ) + add(HeaderSetting(R.string.input_overlay_behavior)) 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/features/settings/ui/viewholder/LaunchableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/LaunchableViewHolder.kt new file mode 100644 index 0000000000..8336517559 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/LaunchableViewHolder.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui.viewholder + +import android.view.View +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.features.settings.model.view.LaunchableSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible + +class LaunchableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: LaunchableSetting + + override fun bind(item: SettingsItem) { + setting = item as LaunchableSetting + + binding.textSettingName.text = setting.title + binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) + binding.textSettingDescription.text = setting.description + + binding.textSettingValue.setVisible(true) + binding.textSettingValue.text = "" + binding.textSettingValue.setCompoundDrawablesRelativeWithIntrinsicBounds( + 0, 0, R.drawable.ic_arrow_forward, 0 + ) + + binding.buttonClear.setVisible(false) + } + + override fun onClick(clicked: View) { + adapter.onLaunchableClick(setting) + } + + override fun onLongClick(clicked: View): Boolean { + // no-op + return true + } +} 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 3f5abc0858..05134cffb1 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 @@ -201,6 +201,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { super.onCreate(savedInstanceState) updateOrientation() + if (args.overlayGamelessEditMode) { + return + } + val intent = requireActivity().intent val intentUri: Uri? = intent.data intentGame = null @@ -558,6 +562,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { return } + if (args.overlayGamelessEditMode) { + setupOverlayGamelessEditMode() + return + } + if (game == null) { Log.warning( "[EmulationFragment] Game not yet initialized in onViewCreated - will be set up by async intent handler" @@ -568,6 +577,39 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { completeViewSetup() } + + private fun setupOverlayGamelessEditMode() { + binding.surfaceInputOverlay.post { + binding.surfaceInputOverlay.refreshControls(gameless = true) + } + + binding.doneControlConfig.setOnClickListener { + finishOverlayGamelessEditMode() + } + + binding.doneControlConfig.visibility = View.VISIBLE + binding.surfaceInputOverlay.setIsInEditMode(true) + binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + binding.surfaceInputOverlay.visibility = View.VISIBLE + binding.loadingIndicator.visibility = View.GONE + + // in gameless edit mode, back = done + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finishOverlayGamelessEditMode() + } + } + ) + } + + private fun finishOverlayGamelessEditMode() { + binding.surfaceInputOverlay.setIsInEditMode(false) + NativeConfig.saveGlobalConfig() + requireActivity().finish() + } + private fun completeViewSetup() { if (_binding == null || game == null) { return @@ -900,9 +942,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { b.surfaceInputOverlay.setVisible(visible = false, gone = false) } } else { - b.surfaceInputOverlay.setVisible( + val shouldShowOverlay = if (args.overlayGamelessEditMode) { + true + } else { showInputOverlay && emulationViewModel.emulationStarted.value - ) + } + b.surfaceInputOverlay.setVisible(shouldShowOverlay) if (!isInFoldableLayout) { if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { b.surfaceInputOverlay.layout = OverlayLayout.Portrait @@ -1531,6 +1576,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean() findItem(R.id.menu_show_overlay).isChecked = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() + findItem(R.id.menu_snap_to_grid).isChecked = + BooleanSetting.OVERLAY_SNAP_TO_GRID.getBoolean() findItem(R.id.menu_haptics).isChecked = BooleanSetting.HAPTIC_FEEDBACK.getBoolean() findItem(R.id.menu_touchscreen).isChecked = BooleanSetting.TOUCHSCREEN.getBoolean() } @@ -1559,6 +1606,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { true } + R.id.menu_snap_to_grid -> { + it.isChecked = !it.isChecked + BooleanSetting.OVERLAY_SNAP_TO_GRID.setBoolean(it.isChecked) + binding.surfaceInputOverlay.invalidate() + true + } + R.id.menu_adjust_overlay -> { adjustOverlay() true @@ -1942,6 +1996,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } fun handleScreenTap(isLongTap: Boolean) { + if (binding.surfaceInputOverlay.isGamelessMode()) { + return + } + val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt() val shouldProceed = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() && BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean() @@ -1963,6 +2021,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } private fun initializeOverlayAutoHide() { + if (binding.surfaceInputOverlay.isGamelessMode()) { + return + } + val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt() val autoHideEnabled = BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean() val showOverlay = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt index 256e5968d1..d1252fc3c4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt @@ -8,6 +8,8 @@ import android.content.Context import android.content.SharedPreferences import android.graphics.Bitmap import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint import android.graphics.Point import android.graphics.Rect import android.graphics.drawable.Drawable @@ -50,6 +52,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : private val overlayJoysticks: MutableSet = HashSet() private var inEditMode = false + private var gamelessMode = false private var buttonBeingConfigured: InputOverlayDrawableButton? = null private var dpadBeingConfigured: InputOverlayDrawableDpad? = null private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null @@ -60,6 +63,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : private var hasMoved = false private val moveThreshold = 20f + private val gridPaint = Paint().apply { + color = Color.argb(60, 255, 255, 255) + strokeWidth = 1f + style = Paint.Style.STROKE + } + private lateinit var windowInsets: WindowInsets var layout = OverlayLayout.Landscape @@ -91,6 +100,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : override fun draw(canvas: Canvas) { super.draw(canvas) + + // Draw grid when in edit mode and snap-to-grid is enabled + if (inEditMode && BooleanSetting.OVERLAY_SNAP_TO_GRID.getBoolean()) { + drawGrid(canvas) + } + for (button in overlayButtons) { button.draw(canvas) } @@ -102,6 +117,26 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : } } + private fun drawGrid(canvas: Canvas) { + val gridSize = IntSetting.OVERLAY_GRID_SIZE.getInt() + val width = canvas.width + val height = canvas.height + + // Draw vertical lines + var x = 0 + while (x <= width) { + canvas.drawLine(x.toFloat(), 0f, x.toFloat(), height.toFloat(), gridPaint) + x += gridSize + } + + // Draw horizontal lines + var y = 0 + while (y <= height) { + canvas.drawLine(0f, y.toFloat(), width.toFloat(), y.toFloat(), gridPaint) + y += gridSize + } + } + override fun onTouch(v: View, event: MotionEvent): Boolean { if (inEditMode) { return onTouchWhileEditing(event) @@ -668,14 +703,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : } } - fun refreshControls() { + fun refreshControls(gameless: Boolean = false) { + // Store gameless mode if set to true + if (gameless) { + gamelessMode = true + } + // Remove all the overlay buttons from the HashSet. overlayButtons.clear() overlayDpads.clear() overlayJoysticks.clear() // Add all the enabled overlay items back to the HashSet. - if (BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) { + if (gamelessMode || BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) { addOverlayControls(layout) } invalidate() @@ -712,9 +752,14 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : if (!editMode) { scaleDialog?.dismiss() scaleDialog = null + gamelessMode = false } + + invalidate() } + fun isGamelessMode(): Boolean = gamelessMode + private fun showScaleDialog( button: InputOverlayDrawableButton?, dpad: InputOverlayDrawableDpad?, @@ -867,6 +912,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : } companion object { + // Increase this number every time there is a breaking change to every overlay layout const val OVERLAY_VERSION = 1 diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt index fee3d04ee3..da85c875b5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -11,6 +14,8 @@ import android.graphics.drawable.BitmapDrawable import android.view.MotionEvent import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState import org.yuzu.yuzu_emu.features.input.model.NativeButton +import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting +import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.overlay.model.OverlayControlData /** @@ -121,11 +126,23 @@ class InputOverlayDrawableButton( MotionEvent.ACTION_MOVE -> { controlPositionX += fingerPositionX - previousTouchX controlPositionY += fingerPositionY - previousTouchY + + val finalX = if (BooleanSetting.OVERLAY_SNAP_TO_GRID.getBoolean()) { + snapToGrid(controlPositionX) + } else { + controlPositionX + } + val finalY = if (BooleanSetting.OVERLAY_SNAP_TO_GRID.getBoolean()) { + snapToGrid(controlPositionY) + } else { + controlPositionY + } + setBounds( - controlPositionX, - controlPositionY, - width + controlPositionX, - height + controlPositionY + finalX, + finalY, + width + finalX, + height + finalY ) previousTouchX = fingerPositionX previousTouchY = fingerPositionY @@ -134,6 +151,11 @@ class InputOverlayDrawableButton( return true } + private fun snapToGrid(value: Int): Int { + val gridSize = IntSetting.OVERLAY_GRID_SIZE.getInt() + return ((value + gridSize / 2) / gridSize) * gridSize + } + fun setBounds(left: Int, top: Int, right: Int, bottom: Int) { defaultStateBitmap.setBounds(left, top, right, bottom) pressedStateBitmap.setBounds(left, top, right, bottom) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt index 01f07e4f36..3a40bd7419 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt @@ -11,6 +11,8 @@ import android.graphics.drawable.BitmapDrawable import android.view.MotionEvent import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState import org.yuzu.yuzu_emu.features.input.model.NativeButton +import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting +import org.yuzu.yuzu_emu.features.settings.model.IntSetting /** * Custom [BitmapDrawable] that is capable @@ -229,11 +231,23 @@ class InputOverlayDrawableDpad( MotionEvent.ACTION_MOVE -> { controlPositionX += fingerPositionX - previousTouchX controlPositionY += fingerPositionY - previousTouchY + + val finalX = if (BooleanSetting.OVERLAY_SNAP_TO_GRID.getBoolean()) { + snapToGrid(controlPositionX) + } else { + controlPositionX + } + val finalY = if (BooleanSetting.OVERLAY_SNAP_TO_GRID.getBoolean()) { + snapToGrid(controlPositionY) + } else { + controlPositionY + } + setBounds( - controlPositionX, - controlPositionY, - width + controlPositionX, - height + controlPositionY + finalX, + finalY, + width + finalX, + height + finalY ) previousTouchX = fingerPositionX previousTouchY = fingerPositionY @@ -242,6 +256,11 @@ class InputOverlayDrawableDpad( return true } + private fun snapToGrid(value: Int): Int { + val gridSize = IntSetting.OVERLAY_GRID_SIZE.getInt() + return ((value + gridSize / 2) / gridSize) * gridSize + } + fun setPosition(x: Int, y: Int) { controlPositionX = x controlPositionY = y diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt index bc3ff15b21..9943daa069 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt @@ -17,6 +17,7 @@ import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState import org.yuzu.yuzu_emu.features.input.model.NativeAnalog import org.yuzu.yuzu_emu.features.input.model.NativeButton import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting +import org.yuzu.yuzu_emu.features.settings.model.IntSetting /** * Custom [BitmapDrawable] that is capable @@ -213,25 +214,37 @@ class InputOverlayDrawableJoystick( MotionEvent.ACTION_MOVE -> { controlPositionX += fingerPositionX - previousTouchX controlPositionY += fingerPositionY - previousTouchY + + val finalX = if (BooleanSetting.OVERLAY_SNAP_TO_GRID.getBoolean()) { + snapToGrid(controlPositionX) + } else { + controlPositionX + } + val finalY = if (BooleanSetting.OVERLAY_SNAP_TO_GRID.getBoolean()) { + snapToGrid(controlPositionY) + } else { + controlPositionY + } + bounds = Rect( - controlPositionX, - controlPositionY, - outerBitmap.intrinsicWidth + controlPositionX, - outerBitmap.intrinsicHeight + controlPositionY + finalX, + finalY, + outerBitmap.intrinsicWidth + finalX, + outerBitmap.intrinsicHeight + finalY ) virtBounds = Rect( - controlPositionX, - controlPositionY, - outerBitmap.intrinsicWidth + controlPositionX, - outerBitmap.intrinsicHeight + controlPositionY + finalX, + finalY, + outerBitmap.intrinsicWidth + finalX, + outerBitmap.intrinsicHeight + finalY ) setInnerBounds() bounds = Rect( Rect( - controlPositionX, - controlPositionY, - outerBitmap.intrinsicWidth + controlPositionX, - outerBitmap.intrinsicHeight + controlPositionY + finalX, + finalY, + outerBitmap.intrinsicWidth + finalX, + outerBitmap.intrinsicHeight + finalY ) ) previousTouchX = fingerPositionX @@ -242,6 +255,11 @@ class InputOverlayDrawableJoystick( return true } + private fun snapToGrid(value: Int): Int { + val gridSize = IntSetting.OVERLAY_GRID_SIZE.getInt() + return ((value + gridSize / 2) / gridSize) * gridSize + } + private fun setInnerBounds() { var x = virtBounds.centerX() + (xAxis * (virtBounds.width() / 2)).toInt() var y = virtBounds.centerY() + (yAxis * (virtBounds.height() / 2)).toInt() diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index e276f19284..eb8e7b77ea 100644 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h @@ -146,6 +146,10 @@ namespace AndroidSettings { Settings::Setting show_input_overlay{linkage, true, "show_input_overlay", Settings::Category::Overlay}; + Settings::Setting overlay_snap_to_grid{linkage, false, "overlay_snap_to_grid", + Settings::Category::Overlay}; + Settings::Setting overlay_grid_size{linkage, 32, "overlay_grid_size", + Settings::Category::Overlay}; Settings::Setting touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay}; Settings::Setting lock_drawer{linkage, false, "lock_drawer", 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 71202bfe3f..8ed9fb78be 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 @@ -15,6 +15,11 @@ android:id="@+id/menu_edit_overlay" android:title="@string/emulation_touch_overlay_edit" /> + + diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml index 2f8c3fa0dd..2adc60a47c 100644 --- a/src/android/app/src/main/res/navigation/emulation_navigation.xml +++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml @@ -19,6 +19,10 @@ android:name="custom" app:argType="boolean" android:defaultValue="false" /> + + Show Input Overlay + Display touch controls overlay during emulation + Snap to Grid + Snap overlay controls to a grid when editing + Grid Size + Size of the grid cells in pixels + Behavior Overlay Auto Hide Automatically hide the touch controls overlay after the specified time of inactivity. Enable Overlay Auto Hide @@ -31,6 +38,8 @@ Input Overlay Configure on-screen controls + Edit Overlay Layout + Adjust the position and scale of on-screen controls @@ -841,6 +850,7 @@ Opacity Reset overlay Edit overlay + Snap to grid Pause emulation Unpause emulation Overlay options