mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2025-12-16 12:08:49 +00:00
Added bottom hot corners
This commit is contained in:
parent
e0b8e8440a
commit
170457b070
@ -8,6 +8,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@ -49,6 +50,7 @@ import androidx.navigation.findNavController
|
|||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@ -74,6 +76,9 @@ import org.citra.citra_emu.model.Game
|
|||||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState
|
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState
|
||||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||||
|
import org.citra.citra_emu.overlay.HotCornerOverlay
|
||||||
|
import org.citra.citra_emu.utils.HotCornerSettings
|
||||||
|
import org.citra.citra_emu.utils.TurboHelper
|
||||||
import org.citra.citra_emu.utils.FileUtil
|
import org.citra.citra_emu.utils.FileUtil
|
||||||
import org.citra.citra_emu.utils.GameHelper
|
import org.citra.citra_emu.utils.GameHelper
|
||||||
import org.citra.citra_emu.utils.GameIconUtils
|
import org.citra.citra_emu.utils.GameIconUtils
|
||||||
@ -103,7 +108,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
private val settings get() = settingsViewModel.settings
|
private val settings get() = settingsViewModel.settings
|
||||||
|
|
||||||
private val onPause = Runnable{ togglePause() }
|
private val onPause = Runnable{ togglePauseAndSyncMenu() }
|
||||||
private val onShutdown = Runnable{ emulationState.stop() }
|
private val onShutdown = Runnable{ emulationState.stop() }
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
@ -183,6 +188,32 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.surfaceEmulation.holder.addCallback(this)
|
binding.surfaceEmulation.holder.addCallback(this)
|
||||||
|
// Setup hot corner overlay
|
||||||
|
binding.hotCornerOverlay.apply {
|
||||||
|
actionListener = object : HotCornerOverlay.OnActionListener {
|
||||||
|
override fun onHotCornerAction(action: HotCornerSettings.HotCornerAction) {
|
||||||
|
when (action) {
|
||||||
|
HotCornerSettings.HotCornerAction.NONE -> {}
|
||||||
|
HotCornerSettings.HotCornerAction.PAUSE_RESUME -> togglePauseAndSyncMenu()
|
||||||
|
HotCornerSettings.HotCornerAction.TOGGLE_TURBO -> TurboHelper.toggleTurbo(true)
|
||||||
|
HotCornerSettings.HotCornerAction.QUICK_SAVE -> {
|
||||||
|
NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
|
||||||
|
Toast.makeText(context, getString(R.string.saving), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
HotCornerSettings.HotCornerAction.QUICK_LOAD -> {
|
||||||
|
val loaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT)
|
||||||
|
val resId = if (loaded) R.string.loading else R.string.quickload_not_found
|
||||||
|
Toast.makeText(context, getString(resId), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
HotCornerSettings.HotCornerAction.OPEN_MENU -> {
|
||||||
|
if (!binding.drawerLayout.isOpen) binding.drawerLayout.open()
|
||||||
|
}
|
||||||
|
HotCornerSettings.HotCornerAction.SWAP_SCREENS -> screenAdjustmentUtil.swapScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
binding.doneControlConfig.setOnClickListener {
|
binding.doneControlConfig.setOnClickListener {
|
||||||
binding.doneControlConfig.visibility = View.GONE
|
binding.doneControlConfig.visibility = View.GONE
|
||||||
binding.surfaceInputOverlay.setIsInEditMode(false)
|
binding.surfaceInputOverlay.setIsInEditMode(false)
|
||||||
@ -471,9 +502,23 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun togglePauseAndSyncMenu() {
|
||||||
|
togglePause()
|
||||||
|
binding.inGameMenu.menu.findItem(R.id.menu_emulation_pause)?.let { menuItem ->
|
||||||
|
if (emulationState.isPaused) {
|
||||||
|
menuItem.title = resources.getString(R.string.resume_emulation)
|
||||||
|
menuItem.icon = ResourcesCompat.getDrawable(resources, R.drawable.ic_play, requireContext().theme)
|
||||||
|
} else {
|
||||||
|
menuItem.title = resources.getString(R.string.pause_emulation)
|
||||||
|
menuItem.icon = ResourcesCompat.getDrawable(resources, R.drawable.ic_pause, requireContext().theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
Choreographer.getInstance().postFrameCallback(this)
|
Choreographer.getInstance().postFrameCallback(this)
|
||||||
|
binding.hotCornerOverlay.refresh()
|
||||||
if (NativeLibrary.isRunning()) {
|
if (NativeLibrary.isRunning()) {
|
||||||
emulationState.pause()
|
emulationState.pause()
|
||||||
|
|
||||||
@ -823,6 +868,28 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.menu_hot_corner_radius -> {
|
||||||
|
runAfterDrawerClosed { showHotCornerRadiusDialog() }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.menu_hot_corner_portrait_bl -> {
|
||||||
|
showHotCornerSelectDialog(Configuration.ORIENTATION_PORTRAIT, HotCornerSettings.HotCornerPosition.BOTTOM_LEFT)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.menu_hot_corner_portrait_br -> {
|
||||||
|
showHotCornerSelectDialog(Configuration.ORIENTATION_PORTRAIT, HotCornerSettings.HotCornerPosition.BOTTOM_RIGHT)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.menu_hot_corner_landscape_bl -> {
|
||||||
|
showHotCornerSelectDialog(Configuration.ORIENTATION_LANDSCAPE, HotCornerSettings.HotCornerPosition.BOTTOM_LEFT)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.menu_hot_corner_landscape_br -> {
|
||||||
|
showHotCornerSelectDialog(Configuration.ORIENTATION_LANDSCAPE, HotCornerSettings.HotCornerPosition.BOTTOM_RIGHT)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -830,6 +897,110 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||||||
popupMenu.show()
|
popupMenu.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showHotCornerSelectDialog(orientation: Int, position: HotCornerSettings.HotCornerPosition) {
|
||||||
|
val actions = arrayOf(
|
||||||
|
getString(R.string.hot_corner_action_none),
|
||||||
|
getString(R.string.hot_corner_action_pause_resume),
|
||||||
|
getString(R.string.hot_corner_action_toggle_turbo),
|
||||||
|
getString(R.string.hot_corner_action_quick_save),
|
||||||
|
getString(R.string.hot_corner_action_quick_load),
|
||||||
|
getString(R.string.hot_corner_action_open_menu),
|
||||||
|
getString(R.string.hot_corner_action_swap_screens)
|
||||||
|
)
|
||||||
|
|
||||||
|
val values = HotCornerSettings.HotCornerAction.values()
|
||||||
|
val current = HotCornerSettings.getAction(orientation, position)
|
||||||
|
var selectedIndex = values.indexOf(current)
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.hot_corner_settings)
|
||||||
|
.setSingleChoiceItems(actions, selectedIndex) { _: DialogInterface?, which: Int ->
|
||||||
|
selectedIndex = which
|
||||||
|
}
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
|
HotCornerSettings.setAction(orientation, position, values[selectedIndex])
|
||||||
|
binding.hotCornerOverlay.refresh()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showHotCornerRadiusDialog() {
|
||||||
|
val sliderBinding = DialogSliderBinding.inflate(layoutInflater)
|
||||||
|
val max = 125
|
||||||
|
val min = 0
|
||||||
|
val previousDp = HotCornerSettings.getRadiusDp()
|
||||||
|
var currentDp = previousDp
|
||||||
|
|
||||||
|
sliderBinding.apply {
|
||||||
|
slider.valueFrom = min.toFloat()
|
||||||
|
slider.valueTo = max.toFloat()
|
||||||
|
slider.value = previousDp.toFloat()
|
||||||
|
textValue.setText(previousDp.toString())
|
||||||
|
textInput.suffixText = "dp"
|
||||||
|
slider.addOnChangeListener { _: Slider, value: Float, _: Boolean ->
|
||||||
|
currentDp = value.toInt()
|
||||||
|
if (textValue.text.toString() != currentDp.toString()) {
|
||||||
|
textValue.setText(currentDp.toString())
|
||||||
|
textValue.setSelection(textValue.length())
|
||||||
|
}
|
||||||
|
// Live preview
|
||||||
|
binding.hotCornerOverlay.showPreview(true)
|
||||||
|
binding.hotCornerOverlay.updatePreviewRadiusDp(currentDp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure preview is visible with current value before interaction
|
||||||
|
binding.hotCornerOverlay.showPreview(true)
|
||||||
|
binding.hotCornerOverlay.updatePreviewRadiusDp(previousDp)
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.hot_corner_radius)
|
||||||
|
.setView(sliderBinding.root)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
|
||||||
|
// Revert to previous value
|
||||||
|
HotCornerSettings.setRadiusDp(previousDp)
|
||||||
|
binding.hotCornerOverlay.showPreview(false)
|
||||||
|
binding.hotCornerOverlay.refresh()
|
||||||
|
}
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
|
// Persist current value
|
||||||
|
HotCornerSettings.setRadiusDp(currentDp)
|
||||||
|
binding.hotCornerOverlay.showPreview(false)
|
||||||
|
binding.hotCornerOverlay.refresh()
|
||||||
|
}
|
||||||
|
.setNeutralButton(R.string.slider_default, null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
val neutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
|
||||||
|
neutral.setOnClickListener {
|
||||||
|
currentDp = 72
|
||||||
|
sliderBinding.slider.value = 72f
|
||||||
|
binding.hotCornerOverlay.showPreview(true)
|
||||||
|
binding.hotCornerOverlay.updatePreviewRadiusDp(currentDp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runAfterDrawerClosed(action: () -> Unit) {
|
||||||
|
if (!binding.drawerLayout.isOpen) {
|
||||||
|
action()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.drawerLayout.addDrawerListener(object : DrawerListener {
|
||||||
|
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
|
||||||
|
override fun onDrawerOpened(drawerView: View) {}
|
||||||
|
override fun onDrawerStateChanged(newState: Int) {}
|
||||||
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
|
binding.drawerLayout.removeDrawerListener(this)
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
binding.drawerLayout.close()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showAmiiboMenu() {
|
private fun showAmiiboMenu() {
|
||||||
val popupMenu = PopupMenu(
|
val popupMenu = PopupMenu(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
|
|||||||
@ -0,0 +1,210 @@
|
|||||||
|
// 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.overlay
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.utils.HotCornerSettings
|
||||||
|
|
||||||
|
class HotCornerOverlay @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
interface OnActionListener {
|
||||||
|
fun onHotCornerAction(action: HotCornerSettings.HotCornerAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionListener: OnActionListener? = null
|
||||||
|
private var previewEnabled = false
|
||||||
|
private var previewRadiusPx: Int? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
isClickable = false
|
||||||
|
isFocusable = false
|
||||||
|
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
removeAllViews()
|
||||||
|
|
||||||
|
val orientation = resources.configuration.orientation
|
||||||
|
val radiusDp = HotCornerSettings.getRadiusDp()
|
||||||
|
val sizePx = previewRadiusPx ?: (radiusDp * resources.displayMetrics.density).toInt()
|
||||||
|
|
||||||
|
addCornerIfNeeded(
|
||||||
|
sizePx,
|
||||||
|
Gravity.BOTTOM or Gravity.START,
|
||||||
|
HotCornerSettings.getAction(orientation, HotCornerSettings.HotCornerPosition.BOTTOM_LEFT)
|
||||||
|
)
|
||||||
|
|
||||||
|
addCornerIfNeeded(
|
||||||
|
sizePx,
|
||||||
|
Gravity.BOTTOM or Gravity.END,
|
||||||
|
HotCornerSettings.getAction(orientation, HotCornerSettings.HotCornerPosition.BOTTOM_RIGHT)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (previewEnabled) addPreviewOverlay(sizePx)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCornerIfNeeded(sizePx: Int, gravity: Int, action: HotCornerSettings.HotCornerAction) {
|
||||||
|
if (action == HotCornerSettings.HotCornerAction.NONE) return
|
||||||
|
|
||||||
|
val position = if ((gravity and Gravity.START) == Gravity.START) {
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_LEFT
|
||||||
|
} else {
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
val v = QuarterCircleTouchView(context, position, sizePx).apply {
|
||||||
|
layoutParams = LayoutParams(sizePx, sizePx, gravity)
|
||||||
|
contentDescription = action.name
|
||||||
|
setOnClickListener { actionListener?.onHotCornerAction(action) }
|
||||||
|
}
|
||||||
|
addView(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showPreview(enable: Boolean) {
|
||||||
|
previewEnabled = enable
|
||||||
|
if (!enable) previewRadiusPx = null
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePreviewRadiusDp(dp: Int) {
|
||||||
|
previewRadiusPx = (dp * resources.displayMetrics.density).toInt()
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addPreviewOverlay(sizePx: Int) {
|
||||||
|
val overlayColor = 0x80FF0000.toInt() // 50% alpha red
|
||||||
|
|
||||||
|
val left = QuarterCirclePreviewView(
|
||||||
|
context,
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_LEFT,
|
||||||
|
sizePx,
|
||||||
|
overlayColor
|
||||||
|
).apply {
|
||||||
|
layoutParams = LayoutParams(sizePx, sizePx, Gravity.BOTTOM or Gravity.START)
|
||||||
|
}
|
||||||
|
|
||||||
|
val right = QuarterCirclePreviewView(
|
||||||
|
context,
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_RIGHT,
|
||||||
|
sizePx,
|
||||||
|
overlayColor
|
||||||
|
).apply {
|
||||||
|
layoutParams = LayoutParams(sizePx, sizePx, Gravity.BOTTOM or Gravity.END)
|
||||||
|
}
|
||||||
|
|
||||||
|
addView(left)
|
||||||
|
addView(right)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QuarterCircleTouchView(
|
||||||
|
context: Context,
|
||||||
|
private val position: HotCornerSettings.HotCornerPosition,
|
||||||
|
private val radiusPx: Int
|
||||||
|
) : View(context) {
|
||||||
|
private var downInside = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
isClickable = true
|
||||||
|
isFocusable = false
|
||||||
|
background = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
val inside = isInsideQuarterCircle(event.x, event.y)
|
||||||
|
return when (event.actionMasked) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
if (inside) {
|
||||||
|
downInside = true
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
downInside = false
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
val handled = downInside && inside
|
||||||
|
if (handled) performClick()
|
||||||
|
downInside = false
|
||||||
|
handled
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
|
downInside = false
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> downInside
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun performClick(): Boolean {
|
||||||
|
return super.performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isInsideQuarterCircle(x: Float, y: Float): Boolean {
|
||||||
|
val r = radiusPx.toFloat()
|
||||||
|
val (cx, cy) = when (position) {
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_LEFT -> 0f to r
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_RIGHT -> r to r
|
||||||
|
}
|
||||||
|
|
||||||
|
val dx = x - cx
|
||||||
|
val dy = y - cy
|
||||||
|
// Constrain to the visible quadrant only
|
||||||
|
val inQuadrant = when (position) {
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_LEFT -> (dx >= 0f && dy <= 0f)
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_RIGHT -> (dx <= 0f && dy <= 0f)
|
||||||
|
}
|
||||||
|
return inQuadrant && (dx * dx + dy * dy <= r * r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QuarterCirclePreviewView(
|
||||||
|
context: Context,
|
||||||
|
private val position: HotCornerSettings.HotCornerPosition,
|
||||||
|
private val radiusPx: Int,
|
||||||
|
color: Int
|
||||||
|
) : View(context) {
|
||||||
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
this.color = color
|
||||||
|
}
|
||||||
|
private val oval = RectF()
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
val r = radiusPx.toFloat()
|
||||||
|
when (position) {
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_LEFT -> {
|
||||||
|
// Center at (0, r), rect spans [-r, 0]..[r, 2r]
|
||||||
|
oval.set(-r, 0f, r, 2f * r)
|
||||||
|
canvas.drawArc(oval, 270f, 90f, true, paint)
|
||||||
|
}
|
||||||
|
HotCornerSettings.HotCornerPosition.BOTTOM_RIGHT -> {
|
||||||
|
// Center at (r, r), rect spans [0, 0]..[2r, 2r]
|
||||||
|
oval.set(0f, 0f, 2f * r, 2f * r)
|
||||||
|
canvas.drawArc(oval, 180f, 90f, true, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -598,6 +598,12 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
|||||||
|
|
||||||
fun setIsInEditMode(isInEditMode: Boolean) {
|
fun setIsInEditMode(isInEditMode: Boolean) {
|
||||||
this.isInEditMode = isInEditMode
|
this.isInEditMode = isInEditMode
|
||||||
|
// Hide hot corners while editing overlay to avoid drag conflicts
|
||||||
|
try {
|
||||||
|
val root = rootView
|
||||||
|
val hotCorner = root.findViewById<HotCornerOverlay>(R.id.hot_corner_overlay)
|
||||||
|
hotCorner?.visibility = if (isInEditMode) View.GONE else View.VISIBLE
|
||||||
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultOverlay() {
|
private fun defaultOverlay() {
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
// 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.utils
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores per-orientation hot corner actions.
|
||||||
|
* Defaults:
|
||||||
|
* - Portrait: BL=TOGGLE_TURBO, BR=PAUSE_RESUME
|
||||||
|
* - Landscape: BL=PAUSE_RESUME, BR=NONE
|
||||||
|
*/
|
||||||
|
object HotCornerSettings {
|
||||||
|
private val preferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
private const val KEY_RADIUS_DP = "HotCorner_radius_dp"
|
||||||
|
|
||||||
|
enum class HotCornerPosition { BOTTOM_LEFT, BOTTOM_RIGHT }
|
||||||
|
|
||||||
|
enum class HotCornerAction(val keySuffix: String) {
|
||||||
|
NONE("none"),
|
||||||
|
PAUSE_RESUME("pause_resume"),
|
||||||
|
TOGGLE_TURBO("toggle_turbo"),
|
||||||
|
QUICK_SAVE("quick_save"),
|
||||||
|
QUICK_LOAD("quick_load"),
|
||||||
|
OPEN_MENU("open_menu"),
|
||||||
|
SWAP_SCREENS("swap_screens")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun key(orientation: Int, position: HotCornerPosition): String {
|
||||||
|
val orient = if (orientation == Configuration.ORIENTATION_LANDSCAPE) "land" else "port"
|
||||||
|
val pos = if (position == HotCornerPosition.BOTTOM_RIGHT) "br" else "bl"
|
||||||
|
return "HotCorner_${orient}_${pos}_action"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAction(orientation: Int, position: HotCornerPosition): HotCornerAction {
|
||||||
|
val stored = preferences.getString(key(orientation, position), null)
|
||||||
|
val defaultAction = HotCornerAction.NONE
|
||||||
|
val value = stored ?: defaultAction.name
|
||||||
|
return runCatching { HotCornerAction.valueOf(value) }.getOrElse { defaultAction }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAction(orientation: Int, position: HotCornerPosition, action: HotCornerAction) {
|
||||||
|
preferences.edit().putString(key(orientation, position), action.name).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRadiusDp(): Int {
|
||||||
|
return preferences.getInt(KEY_RADIUS_DP, 72)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRadiusDp(value: Int) {
|
||||||
|
preferences.edit().putInt(KEY_RADIUS_DP, value).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,16 @@
|
|||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
android:visibility="invisible" />
|
android:visibility="invisible" />
|
||||||
|
|
||||||
|
<!-- Hot corner overlay: hosts invisible corner touch targets; placed above input overlay -->
|
||||||
|
<org.citra.citra_emu.overlay.HotCornerOverlay
|
||||||
|
android:id="@+id/hot_corner_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/done_control_config"
|
android:id="@+id/done_control_config"
|
||||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||||
|
|||||||
@ -24,6 +24,29 @@
|
|||||||
android:id="@+id/menu_emulation_toggle_controls"
|
android:id="@+id/menu_emulation_toggle_controls"
|
||||||
android:title="@string/emulation_toggle_controls" />
|
android:title="@string/emulation_toggle_controls" />
|
||||||
|
|
||||||
|
<!-- Hot corner settings moved up -->
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_hot_corner_settings"
|
||||||
|
android:title="@string/hot_corner_settings">
|
||||||
|
<menu>
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_hot_corner_portrait_bl"
|
||||||
|
android:title="@string/hot_corner_portrait_bl" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_hot_corner_portrait_br"
|
||||||
|
android:title="@string/hot_corner_portrait_br" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_hot_corner_landscape_bl"
|
||||||
|
android:title="@string/hot_corner_landscape_bl" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_hot_corner_landscape_br"
|
||||||
|
android:title="@string/hot_corner_landscape_br" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_hot_corner_radius"
|
||||||
|
android:title="@string/hot_corner_radius" />
|
||||||
|
</menu>
|
||||||
|
</item>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_emulation_adjust_scales"
|
android:id="@+id/menu_emulation_adjust_scales"
|
||||||
android:title="@string/emulation_control_scale">
|
android:title="@string/emulation_control_scale">
|
||||||
@ -104,5 +127,4 @@
|
|||||||
<item
|
<item
|
||||||
android:id="@+id/menu_emulation_reset_overlay"
|
android:id="@+id/menu_emulation_reset_overlay"
|
||||||
android:title="@string/emulation_touch_overlay_reset" />
|
android:title="@string/emulation_touch_overlay_reset" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@ -472,6 +472,21 @@
|
|||||||
<string name="lock_drawer">锁定侧滑菜单</string>
|
<string name="lock_drawer">锁定侧滑菜单</string>
|
||||||
<string name="unlock_drawer">解锁侧滑菜单</string>
|
<string name="unlock_drawer">解锁侧滑菜单</string>
|
||||||
|
|
||||||
|
<!-- Hot corner strings -->
|
||||||
|
<string name="hot_corner_settings">底部热区设置</string>
|
||||||
|
<string name="hot_corner_portrait_bl">竖屏 左下角</string>
|
||||||
|
<string name="hot_corner_portrait_br">竖屏 右下角</string>
|
||||||
|
<string name="hot_corner_landscape_bl">横屏 左下角</string>
|
||||||
|
<string name="hot_corner_landscape_br">横屏 右下角</string>
|
||||||
|
<string name="hot_corner_action_none">无</string>
|
||||||
|
<string name="hot_corner_action_pause_resume">暂停/继续</string>
|
||||||
|
<string name="hot_corner_action_toggle_turbo">加速</string>
|
||||||
|
<string name="hot_corner_action_quick_save">快速保存</string>
|
||||||
|
<string name="hot_corner_action_quick_load">快速加载</string>
|
||||||
|
<string name="hot_corner_action_open_menu">打开菜单</string>
|
||||||
|
<string name="hot_corner_action_swap_screens">交换上下屏</string>
|
||||||
|
<string name="hot_corner_radius">热区大小设置</string>
|
||||||
|
|
||||||
<string name="write_permission_needed">您需要允许 Lime3DS 对外部存储器进行读写访问,以便模拟器正常工作。</string>
|
<string name="write_permission_needed">您需要允许 Lime3DS 对外部存储器进行读写访问,以便模拟器正常工作。</string>
|
||||||
<string name="load_settings">正在加载设置…</string>
|
<string name="load_settings">正在加载设置…</string>
|
||||||
|
|
||||||
|
|||||||
@ -16,4 +16,7 @@
|
|||||||
<dimen name="spacing_fab">24dp</dimen>
|
<dimen name="spacing_fab">24dp</dimen>
|
||||||
|
|
||||||
<dimen name="dialog_margin">20dp</dimen>
|
<dimen name="dialog_margin">20dp</dimen>
|
||||||
|
|
||||||
|
<!-- Hot corner area size -->
|
||||||
|
<dimen name="hot_corner_size">50dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -502,6 +502,21 @@
|
|||||||
<string name="lock_drawer">Lock Drawer</string>
|
<string name="lock_drawer">Lock Drawer</string>
|
||||||
<string name="unlock_drawer">Unlock Drawer</string>
|
<string name="unlock_drawer">Unlock Drawer</string>
|
||||||
|
|
||||||
|
<!-- Hot corner strings -->
|
||||||
|
<string name="hot_corner_settings">Bottom Hot Corners</string>
|
||||||
|
<string name="hot_corner_portrait_bl">Portrait Bottom-Left</string>
|
||||||
|
<string name="hot_corner_portrait_br">Portrait Bottom-Right</string>
|
||||||
|
<string name="hot_corner_landscape_bl">Landscape Bottom-Left</string>
|
||||||
|
<string name="hot_corner_landscape_br">Landscape Bottom-Right</string>
|
||||||
|
<string name="hot_corner_action_none">None</string>
|
||||||
|
<string name="hot_corner_action_pause_resume">Pause/Resume</string>
|
||||||
|
<string name="hot_corner_action_toggle_turbo">Turbo Speed</string>
|
||||||
|
<string name="hot_corner_action_quick_save">Quicksave</string>
|
||||||
|
<string name="hot_corner_action_quick_load">Quickload</string>
|
||||||
|
<string name="hot_corner_action_open_menu">Open Menu</string>
|
||||||
|
<string name="hot_corner_action_swap_screens">Swap Screens</string>
|
||||||
|
<string name="hot_corner_radius">Hot Corner Size</string>
|
||||||
|
|
||||||
<string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
|
<string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
|
||||||
<string name="load_settings">Loading Settings…</string>
|
<string name="load_settings">Loading Settings…</string>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user