mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-10 05:28:56 +02:00
[android, inputOverlay] Add snap to grid option and allow editing the overlay without opening a game (#3234)
The new changes are in the input overlay section Known issues: - Auto hide, also hides the overlay in gameless edit mode - Same goes for the controller auto hide option Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3234 Reviewed-by: DraVee <dravee@eden-emu.dev> Reviewed-by: Maufeat <sahyno1996@gmail.com> Co-authored-by: kleidis <kleidis1@protonmail.com> Co-committed-by: kleidis <kleidis1@protonmail.com>
This commit is contained in:
parent
a79eab9564
commit
76be55bc2f
17 changed files with 332 additions and 24 deletions
|
|
@ -639,6 +639,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_SELECTED_GAME = "SelectedGame"
|
const val EXTRA_SELECTED_GAME = "SelectedGame"
|
||||||
|
const val EXTRA_OVERLAY_GAMELESS_EDIT_MODE = "overlayGamelessEditMode"
|
||||||
|
|
||||||
fun stopForegroundService(activity: Activity) {
|
fun stopForegroundService(activity: Activity) {
|
||||||
val startIntent = Intent(activity, ForegroundService::class.java)
|
val startIntent = Intent(activity, ForegroundService::class.java)
|
||||||
|
|
@ -652,6 +653,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
|
||||||
activity.startActivity(launcher)
|
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 {
|
private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean {
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||||
DPAD_SLIDE("dpad_slide"),
|
DPAD_SLIDE("dpad_slide"),
|
||||||
HAPTIC_FEEDBACK("haptic_feedback"),
|
HAPTIC_FEEDBACK("haptic_feedback"),
|
||||||
SHOW_INPUT_OVERLAY("show_input_overlay"),
|
SHOW_INPUT_OVERLAY("show_input_overlay"),
|
||||||
|
OVERLAY_SNAP_TO_GRID("overlay_snap_to_grid"),
|
||||||
TOUCHSCREEN("touchscreen"),
|
TOUCHSCREEN("touchscreen"),
|
||||||
AIRPLANE_MODE("airplane_mode"),
|
AIRPLANE_MODE("airplane_mode"),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
|
||||||
WIFI_WEB_AUTH_APPLET("wifi_web_auth_applet_mode"),
|
WIFI_WEB_AUTH_APPLET("wifi_web_auth_applet_mode"),
|
||||||
MY_PAGE_APPLET("my_page_applet_mode"),
|
MY_PAGE_APPLET("my_page_applet_mode"),
|
||||||
INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide"),
|
INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide"),
|
||||||
|
OVERLAY_GRID_SIZE("overlay_grid_size"),
|
||||||
DEBUG_KNOBS("debug_knobs")
|
DEBUG_KNOBS("debug_knobs")
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -97,6 +97,7 @@ abstract class SettingsItem(
|
||||||
const val TYPE_INPUT_PROFILE = 10
|
const val TYPE_INPUT_PROFILE = 10
|
||||||
const val TYPE_STRING_INPUT = 11
|
const val TYPE_STRING_INPUT = 11
|
||||||
const val TYPE_SPINBOX = 12
|
const val TYPE_SPINBOX = 12
|
||||||
|
const val TYPE_LAUNCHABLE = 13
|
||||||
|
|
||||||
const val FASTMEM_COMBINED = "fastmem_combined"
|
const val FASTMEM_COMBINED = "fastmem_combined"
|
||||||
|
|
||||||
|
|
@ -364,6 +365,30 @@ abstract class SettingsItem(
|
||||||
warningMessage = R.string.warning_resolution
|
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(
|
put(
|
||||||
SwitchSetting(
|
SwitchSetting(
|
||||||
BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE,
|
BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE,
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,9 @@ class SettingsAdapter(
|
||||||
StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsItem.TYPE_LAUNCHABLE -> {
|
||||||
|
LaunchableViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
|
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +212,11 @@ class SettingsAdapter(
|
||||||
fragment.view?.findNavController()?.navigate(action)
|
fragment.view?.findNavController()?.navigate(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onLaunchableClick(item: LaunchableSetting) {
|
||||||
|
val intent = item.launchIntent(context)
|
||||||
|
fragment.requireActivity().startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
fun onInputProfileClick(item: InputProfileSetting, position: Int) {
|
fun onInputProfileClick(item: InputProfileSetting, position: Int) {
|
||||||
InputProfileDialogFragment.newInstance(
|
InputProfileDialogFragment.newInstance(
|
||||||
settingsViewModel,
|
settingsViewModel,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
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.NativeInput
|
||||||
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
|
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
|
||||||
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
|
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
|
||||||
|
|
@ -294,6 +295,19 @@ class SettingsFragmentPresenter(
|
||||||
|
|
||||||
private fun addInputOverlaySettings(sl: ArrayList<SettingsItem>) {
|
private fun addInputOverlaySettings(sl: ArrayList<SettingsItem>) {
|
||||||
sl.apply {
|
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(BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.key)
|
||||||
add(IntSetting.INPUT_OVERLAY_AUTO_HIDE.key)
|
add(IntSetting.INPUT_OVERLAY_AUTO_HIDE.key)
|
||||||
add(BooleanSetting.HIDE_OVERLAY_ON_CONTROLLER_INPUT.key)
|
add(BooleanSetting.HIDE_OVERLAY_ON_CONTROLLER_INPUT.key)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -201,6 +201,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
updateOrientation()
|
updateOrientation()
|
||||||
|
|
||||||
|
if (args.overlayGamelessEditMode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val intent = requireActivity().intent
|
val intent = requireActivity().intent
|
||||||
val intentUri: Uri? = intent.data
|
val intentUri: Uri? = intent.data
|
||||||
intentGame = null
|
intentGame = null
|
||||||
|
|
@ -558,6 +562,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.overlayGamelessEditMode) {
|
||||||
|
setupOverlayGamelessEditMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (game == null) {
|
if (game == null) {
|
||||||
Log.warning(
|
Log.warning(
|
||||||
"[EmulationFragment] Game not yet initialized in onViewCreated - will be set up by async intent handler"
|
"[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()
|
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() {
|
private fun completeViewSetup() {
|
||||||
if (_binding == null || game == null) {
|
if (_binding == null || game == null) {
|
||||||
return
|
return
|
||||||
|
|
@ -900,9 +942,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
b.surfaceInputOverlay.setVisible(visible = false, gone = false)
|
b.surfaceInputOverlay.setVisible(visible = false, gone = false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.surfaceInputOverlay.setVisible(
|
val shouldShowOverlay = if (args.overlayGamelessEditMode) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
showInputOverlay && emulationViewModel.emulationStarted.value
|
showInputOverlay && emulationViewModel.emulationStarted.value
|
||||||
)
|
}
|
||||||
|
b.surfaceInputOverlay.setVisible(shouldShowOverlay)
|
||||||
if (!isInFoldableLayout) {
|
if (!isInFoldableLayout) {
|
||||||
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
b.surfaceInputOverlay.layout = OverlayLayout.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_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean()
|
||||||
findItem(R.id.menu_show_overlay).isChecked =
|
findItem(R.id.menu_show_overlay).isChecked =
|
||||||
BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()
|
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_haptics).isChecked = BooleanSetting.HAPTIC_FEEDBACK.getBoolean()
|
||||||
findItem(R.id.menu_touchscreen).isChecked = BooleanSetting.TOUCHSCREEN.getBoolean()
|
findItem(R.id.menu_touchscreen).isChecked = BooleanSetting.TOUCHSCREEN.getBoolean()
|
||||||
}
|
}
|
||||||
|
|
@ -1559,6 +1606,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
true
|
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 -> {
|
R.id.menu_adjust_overlay -> {
|
||||||
adjustOverlay()
|
adjustOverlay()
|
||||||
true
|
true
|
||||||
|
|
@ -1942,6 +1996,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleScreenTap(isLongTap: Boolean) {
|
fun handleScreenTap(isLongTap: Boolean) {
|
||||||
|
if (binding.surfaceInputOverlay.isGamelessMode()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt()
|
val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt()
|
||||||
val shouldProceed = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() && BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean()
|
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() {
|
private fun initializeOverlayAutoHide() {
|
||||||
|
if (binding.surfaceInputOverlay.isGamelessMode()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt()
|
val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt()
|
||||||
val autoHideEnabled = BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean()
|
val autoHideEnabled = BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean()
|
||||||
val showOverlay = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()
|
val showOverlay = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
import android.graphics.Point
|
import android.graphics.Point
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
|
@ -50,6 +52,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
|
||||||
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
|
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
|
||||||
|
|
||||||
private var inEditMode = false
|
private var inEditMode = false
|
||||||
|
private var gamelessMode = false
|
||||||
private var buttonBeingConfigured: InputOverlayDrawableButton? = null
|
private var buttonBeingConfigured: InputOverlayDrawableButton? = null
|
||||||
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
|
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
|
||||||
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
|
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
|
||||||
|
|
@ -60,6 +63,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
|
||||||
private var hasMoved = false
|
private var hasMoved = false
|
||||||
private val moveThreshold = 20f
|
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
|
private lateinit var windowInsets: WindowInsets
|
||||||
|
|
||||||
var layout = OverlayLayout.Landscape
|
var layout = OverlayLayout.Landscape
|
||||||
|
|
@ -91,6 +100,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
|
||||||
|
|
||||||
override fun draw(canvas: Canvas) {
|
override fun draw(canvas: Canvas) {
|
||||||
super.draw(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) {
|
for (button in overlayButtons) {
|
||||||
button.draw(canvas)
|
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 {
|
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||||
if (inEditMode) {
|
if (inEditMode) {
|
||||||
return onTouchWhileEditing(event)
|
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.
|
// Remove all the overlay buttons from the HashSet.
|
||||||
overlayButtons.clear()
|
overlayButtons.clear()
|
||||||
overlayDpads.clear()
|
overlayDpads.clear()
|
||||||
overlayJoysticks.clear()
|
overlayJoysticks.clear()
|
||||||
|
|
||||||
// Add all the enabled overlay items back to the HashSet.
|
// 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)
|
addOverlayControls(layout)
|
||||||
}
|
}
|
||||||
invalidate()
|
invalidate()
|
||||||
|
|
@ -712,9 +752,14 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
|
||||||
if (!editMode) {
|
if (!editMode) {
|
||||||
scaleDialog?.dismiss()
|
scaleDialog?.dismiss()
|
||||||
scaleDialog = null
|
scaleDialog = null
|
||||||
|
gamelessMode = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isGamelessMode(): Boolean = gamelessMode
|
||||||
|
|
||||||
private fun showScaleDialog(
|
private fun showScaleDialog(
|
||||||
button: InputOverlayDrawableButton?,
|
button: InputOverlayDrawableButton?,
|
||||||
dpad: InputOverlayDrawableDpad?,
|
dpad: InputOverlayDrawableDpad?,
|
||||||
|
|
@ -867,6 +912,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
// Increase this number every time there is a breaking change to every overlay layout
|
// Increase this number every time there is a breaking change to every overlay layout
|
||||||
const val OVERLAY_VERSION = 1
|
const val OVERLAY_VERSION = 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
|
@ -11,6 +14,8 @@ import android.graphics.drawable.BitmapDrawable
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
|
import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
|
||||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
|
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
|
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -121,11 +126,23 @@ class InputOverlayDrawableButton(
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
controlPositionX += fingerPositionX - previousTouchX
|
controlPositionX += fingerPositionX - previousTouchX
|
||||||
controlPositionY += fingerPositionY - previousTouchY
|
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(
|
setBounds(
|
||||||
controlPositionX,
|
finalX,
|
||||||
controlPositionY,
|
finalY,
|
||||||
width + controlPositionX,
|
width + finalX,
|
||||||
height + controlPositionY
|
height + finalY
|
||||||
)
|
)
|
||||||
previousTouchX = fingerPositionX
|
previousTouchX = fingerPositionX
|
||||||
previousTouchY = fingerPositionY
|
previousTouchY = fingerPositionY
|
||||||
|
|
@ -134,6 +151,11 @@ class InputOverlayDrawableButton(
|
||||||
return true
|
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) {
|
fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
defaultStateBitmap.setBounds(left, top, right, bottom)
|
defaultStateBitmap.setBounds(left, top, right, bottom)
|
||||||
pressedStateBitmap.setBounds(left, top, right, bottom)
|
pressedStateBitmap.setBounds(left, top, right, bottom)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import android.graphics.drawable.BitmapDrawable
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
|
import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
|
||||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
|
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
|
* Custom [BitmapDrawable] that is capable
|
||||||
|
|
@ -229,11 +231,23 @@ class InputOverlayDrawableDpad(
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
controlPositionX += fingerPositionX - previousTouchX
|
controlPositionX += fingerPositionX - previousTouchX
|
||||||
controlPositionY += fingerPositionY - previousTouchY
|
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(
|
setBounds(
|
||||||
controlPositionX,
|
finalX,
|
||||||
controlPositionY,
|
finalY,
|
||||||
width + controlPositionX,
|
width + finalX,
|
||||||
height + controlPositionY
|
height + finalY
|
||||||
)
|
)
|
||||||
previousTouchX = fingerPositionX
|
previousTouchX = fingerPositionX
|
||||||
previousTouchY = fingerPositionY
|
previousTouchY = fingerPositionY
|
||||||
|
|
@ -242,6 +256,11 @@ class InputOverlayDrawableDpad(
|
||||||
return true
|
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) {
|
fun setPosition(x: Int, y: Int) {
|
||||||
controlPositionX = x
|
controlPositionX = x
|
||||||
controlPositionY = y
|
controlPositionY = y
|
||||||
|
|
|
||||||
|
|
@ -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.NativeAnalog
|
||||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
|
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.BooleanSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom [BitmapDrawable] that is capable
|
* Custom [BitmapDrawable] that is capable
|
||||||
|
|
@ -213,25 +214,37 @@ class InputOverlayDrawableJoystick(
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
controlPositionX += fingerPositionX - previousTouchX
|
controlPositionX += fingerPositionX - previousTouchX
|
||||||
controlPositionY += fingerPositionY - previousTouchY
|
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(
|
bounds = Rect(
|
||||||
controlPositionX,
|
finalX,
|
||||||
controlPositionY,
|
finalY,
|
||||||
outerBitmap.intrinsicWidth + controlPositionX,
|
outerBitmap.intrinsicWidth + finalX,
|
||||||
outerBitmap.intrinsicHeight + controlPositionY
|
outerBitmap.intrinsicHeight + finalY
|
||||||
)
|
)
|
||||||
virtBounds = Rect(
|
virtBounds = Rect(
|
||||||
controlPositionX,
|
finalX,
|
||||||
controlPositionY,
|
finalY,
|
||||||
outerBitmap.intrinsicWidth + controlPositionX,
|
outerBitmap.intrinsicWidth + finalX,
|
||||||
outerBitmap.intrinsicHeight + controlPositionY
|
outerBitmap.intrinsicHeight + finalY
|
||||||
)
|
)
|
||||||
setInnerBounds()
|
setInnerBounds()
|
||||||
bounds = Rect(
|
bounds = Rect(
|
||||||
Rect(
|
Rect(
|
||||||
controlPositionX,
|
finalX,
|
||||||
controlPositionY,
|
finalY,
|
||||||
outerBitmap.intrinsicWidth + controlPositionX,
|
outerBitmap.intrinsicWidth + finalX,
|
||||||
outerBitmap.intrinsicHeight + controlPositionY
|
outerBitmap.intrinsicHeight + finalY
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
previousTouchX = fingerPositionX
|
previousTouchX = fingerPositionX
|
||||||
|
|
@ -242,6 +255,11 @@ class InputOverlayDrawableJoystick(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun snapToGrid(value: Int): Int {
|
||||||
|
val gridSize = IntSetting.OVERLAY_GRID_SIZE.getInt()
|
||||||
|
return ((value + gridSize / 2) / gridSize) * gridSize
|
||||||
|
}
|
||||||
|
|
||||||
private fun setInnerBounds() {
|
private fun setInnerBounds() {
|
||||||
var x = virtBounds.centerX() + (xAxis * (virtBounds.width() / 2)).toInt()
|
var x = virtBounds.centerX() + (xAxis * (virtBounds.width() / 2)).toInt()
|
||||||
var y = virtBounds.centerY() + (yAxis * (virtBounds.height() / 2)).toInt()
|
var y = virtBounds.centerY() + (yAxis * (virtBounds.height() / 2)).toInt()
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,10 @@ namespace AndroidSettings {
|
||||||
|
|
||||||
Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay",
|
Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay",
|
||||||
Settings::Category::Overlay};
|
Settings::Category::Overlay};
|
||||||
|
Settings::Setting<bool> overlay_snap_to_grid{linkage, false, "overlay_snap_to_grid",
|
||||||
|
Settings::Category::Overlay};
|
||||||
|
Settings::Setting<s32> overlay_grid_size{linkage, 32, "overlay_grid_size",
|
||||||
|
Settings::Category::Overlay};
|
||||||
Settings::Setting<bool> touchscreen{linkage, true, "touchscreen",
|
Settings::Setting<bool> touchscreen{linkage, true, "touchscreen",
|
||||||
Settings::Category::Overlay};
|
Settings::Category::Overlay};
|
||||||
Settings::Setting<s32> lock_drawer{linkage, false, "lock_drawer",
|
Settings::Setting<s32> lock_drawer{linkage, false, "lock_drawer",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@
|
||||||
android:id="@+id/menu_edit_overlay"
|
android:id="@+id/menu_edit_overlay"
|
||||||
android:title="@string/emulation_touch_overlay_edit" />
|
android:title="@string/emulation_touch_overlay_edit" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_snap_to_grid"
|
||||||
|
android:title="@string/emulation_snap_to_grid"
|
||||||
|
android:checkable="true" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_adjust_overlay"
|
android:id="@+id/menu_adjust_overlay"
|
||||||
android:title="@string/emulation_control_adjust" />
|
android:title="@string/emulation_control_adjust" />
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@
|
||||||
android:name="custom"
|
android:name="custom"
|
||||||
app:argType="boolean"
|
app:argType="boolean"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
|
<argument
|
||||||
|
android:name="overlayGamelessEditMode"
|
||||||
|
app:argType="boolean"
|
||||||
|
android:defaultValue="false" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,13 @@
|
||||||
|
|
||||||
|
|
||||||
<!-- Input Overlay -->
|
<!-- Input Overlay -->
|
||||||
|
<string name="show_input_overlay">Show Input Overlay</string>
|
||||||
|
<string name="show_input_overlay_description">Display touch controls overlay during emulation</string>
|
||||||
|
<string name="overlay_snap_to_grid">Snap to Grid</string>
|
||||||
|
<string name="overlay_snap_to_grid_description">Snap overlay controls to a grid when editing</string>
|
||||||
|
<string name="overlay_grid_size">Grid Size</string>
|
||||||
|
<string name="overlay_grid_size_description">Size of the grid cells in pixels</string>
|
||||||
|
<string name="input_overlay_behavior">Behavior</string>
|
||||||
<string name="overlay_auto_hide">Overlay Auto Hide</string>
|
<string name="overlay_auto_hide">Overlay Auto Hide</string>
|
||||||
<string name="overlay_auto_hide_description">Automatically hide the touch controls overlay after the specified time of inactivity.</string>
|
<string name="overlay_auto_hide_description">Automatically hide the touch controls overlay after the specified time of inactivity.</string>
|
||||||
<string name="enable_input_overlay_auto_hide">Enable Overlay Auto Hide</string>
|
<string name="enable_input_overlay_auto_hide">Enable Overlay Auto Hide</string>
|
||||||
|
|
@ -31,6 +38,8 @@
|
||||||
|
|
||||||
<string name="input_overlay_options">Input Overlay</string>
|
<string name="input_overlay_options">Input Overlay</string>
|
||||||
<string name="input_overlay_options_description">Configure on-screen controls</string>
|
<string name="input_overlay_options_description">Configure on-screen controls</string>
|
||||||
|
<string name="edit_overlay_layout">Edit Overlay Layout</string>
|
||||||
|
<string name="edit_overlay_layout_description">Adjust the position and scale of on-screen controls</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Stats Overlay settings -->
|
<!-- Stats Overlay settings -->
|
||||||
|
|
@ -841,6 +850,7 @@
|
||||||
<string name="emulation_control_opacity">Opacity</string>
|
<string name="emulation_control_opacity">Opacity</string>
|
||||||
<string name="emulation_touch_overlay_reset">Reset overlay</string>
|
<string name="emulation_touch_overlay_reset">Reset overlay</string>
|
||||||
<string name="emulation_touch_overlay_edit">Edit overlay</string>
|
<string name="emulation_touch_overlay_edit">Edit overlay</string>
|
||||||
|
<string name="emulation_snap_to_grid">Snap to grid</string>
|
||||||
<string name="emulation_pause">Pause emulation</string>
|
<string name="emulation_pause">Pause emulation</string>
|
||||||
<string name="emulation_unpause">Unpause emulation</string>
|
<string name="emulation_unpause">Unpause emulation</string>
|
||||||
<string name="emulation_input_overlay">Overlay options</string>
|
<string name="emulation_input_overlay">Overlay options</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue