mirror of
https://github.com/izzy2lost/WeeU.git
synced 2026-07-06 00:19:59 -07:00
refactor emulation activity
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package info.cemu.cemu.emulation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import info.cemu.cemu.nativeinterface.NativeInput
|
||||
|
||||
class CanvasOnTouchListener(val isTV: Boolean) : View.OnTouchListener {
|
||||
private var currentPointerId: Int = -1
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
if (currentPointerId != -1 && pointerId != currentPointerId) {
|
||||
return false
|
||||
}
|
||||
val x = event.getX(pointerIndex).toInt()
|
||||
val y = event.getY(pointerIndex).toInt()
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
NativeInput.onTouchDown(x, y, isTV)
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
|
||||
currentPointerId = -1
|
||||
NativeInput.onTouchUp(x, y, isTV)
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
NativeInput.onTouchMove(x, y, isTV)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
package info.cemu.cemu.emulation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter.LengthFilter
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
@@ -17,14 +27,62 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
import info.cemu.cemu.BuildConfig
|
||||
import info.cemu.cemu.R
|
||||
import info.cemu.cemu.databinding.ActivityEmulationBinding
|
||||
import info.cemu.cemu.emulation.EmulationFragment.OnEmulationErrorCallback
|
||||
import info.cemu.cemu.databinding.LayoutSideMenuCheckboxItemBinding
|
||||
import info.cemu.cemu.databinding.LayoutSideMenuEmulationBinding
|
||||
import info.cemu.cemu.databinding.LayoutSideMenuTextItemBinding
|
||||
import info.cemu.cemu.input.InputManager
|
||||
import info.cemu.cemu.input.SensorManager
|
||||
import info.cemu.cemu.inputoverlay.InputOverlaySettingsManager
|
||||
import info.cemu.cemu.inputoverlay.InputOverlaySurfaceView
|
||||
import info.cemu.cemu.inputoverlay.OverlaySettings
|
||||
import info.cemu.cemu.nativeinterface.NativeEmulation
|
||||
import info.cemu.cemu.nativeinterface.NativeException
|
||||
import info.cemu.cemu.nativeinterface.NativeSwkbd.setCurrentInputText
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class EmulationActivity : AppCompatActivity() {
|
||||
private inner class CanvasSurfaceHolderCallback(val isMainCanvas: Boolean) :
|
||||
SurfaceHolder.Callback {
|
||||
var surfaceSet: Boolean = false
|
||||
|
||||
override fun surfaceCreated(surfaceHolder: SurfaceHolder) {}
|
||||
|
||||
override fun surfaceChanged(
|
||||
surfaceHolder: SurfaceHolder,
|
||||
format: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
try {
|
||||
NativeEmulation.setSurfaceSize(width, height, isMainCanvas)
|
||||
if (surfaceSet) {
|
||||
return
|
||||
}
|
||||
NativeEmulation.setSurface(surfaceHolder.surface, isMainCanvas)
|
||||
surfaceSet = true
|
||||
} catch (exception: NativeException) {
|
||||
onEmulationError(getString(R.string.failed_create_surface_error, exception.message))
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) {
|
||||
NativeEmulation.clearSurface(isMainCanvas)
|
||||
surfaceSet = false
|
||||
}
|
||||
}
|
||||
|
||||
private val inputManager = InputManager()
|
||||
private var emulationTextInputDialog: AlertDialog? = null
|
||||
private var isGameRunning = false
|
||||
private var padCanvas: SurfaceView? = null
|
||||
private var toast: Toast? = null
|
||||
private lateinit var binding: ActivityEmulationBinding
|
||||
private var isMotionEnabled = false
|
||||
private lateinit var overlaySettings: OverlaySettings
|
||||
private lateinit var inputOverlaySurfaceView: InputOverlaySurfaceView
|
||||
private lateinit var sensorManager: SensorManager
|
||||
private var hasEmulationError = false
|
||||
|
||||
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
|
||||
if (inputManager.onMotionEvent(event)) {
|
||||
@@ -40,16 +98,7 @@ class EmulationActivity : AppCompatActivity() {
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
emulationActivityInstance = this
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
showExitConfirmationDialog()
|
||||
}
|
||||
})
|
||||
|
||||
val intent = intent
|
||||
private fun getLaunchPath(): String {
|
||||
val extras = intent.extras
|
||||
val data = intent.data
|
||||
var launchPath: String? = null
|
||||
@@ -62,25 +111,231 @@ class EmulationActivity : AppCompatActivity() {
|
||||
if (launchPath == null) {
|
||||
throw RuntimeException("launchPath is null")
|
||||
}
|
||||
setFullscreen()
|
||||
val binding = ActivityEmulationBinding.inflate(
|
||||
layoutInflater
|
||||
)
|
||||
return launchPath
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
emulationActivityInstance = WeakReference(this)
|
||||
|
||||
val inputOverlaySettingsManager = InputOverlaySettingsManager(this)
|
||||
overlaySettings = inputOverlaySettingsManager.overlaySettings
|
||||
sensorManager = SensorManager(this)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
showExitConfirmationDialog()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
initializeView(getLaunchPath())
|
||||
setContentView(binding.root)
|
||||
var emulationFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.emulation_frame) as EmulationFragment?
|
||||
if (emulationFragment == null) {
|
||||
emulationFragment = EmulationFragment(launchPath)
|
||||
emulationFragment.setOnEmulationErrorCallback(OnEmulationErrorCallback { emulationError: String? ->
|
||||
this.onEmulationError(
|
||||
emulationError
|
||||
)
|
||||
})
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.add(R.id.emulation_frame, emulationFragment)
|
||||
.commit()
|
||||
}
|
||||
|
||||
private fun destroyPadCanvas() {
|
||||
if (padCanvas == null) {
|
||||
return
|
||||
}
|
||||
binding.canvasesLayout.removeView(padCanvas)
|
||||
padCanvas = null
|
||||
}
|
||||
|
||||
private fun setPadViewVisibility(visible: Boolean) {
|
||||
if (visible) {
|
||||
createPadCanvas()
|
||||
} else {
|
||||
destroyPadCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMotionEnabled(enabled: Boolean) {
|
||||
isMotionEnabled = enabled
|
||||
if (isMotionEnabled) {
|
||||
sensorManager.startListening()
|
||||
} else {
|
||||
sensorManager.pauseListening()
|
||||
}
|
||||
}
|
||||
|
||||
private fun LayoutSideMenuTextItemBinding.setEnabled(isEnabled: Boolean) {
|
||||
textItem.isEnabled = isEnabled
|
||||
textItem.alpha = if (isEnabled) 1f else 0.7f
|
||||
}
|
||||
|
||||
private fun LayoutSideMenuTextItemBinding.configure(
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
setEnabled(isEnabled)
|
||||
textItem.setOnClickListener {
|
||||
onClick()
|
||||
binding.drawerLayout.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun LayoutSideMenuCheckboxItemBinding.configure(
|
||||
initialCheckedStatus: Boolean = false,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
checkbox.isChecked = initialCheckedStatus
|
||||
checkboxItem.setOnClickListener {
|
||||
checkbox.isChecked = !checkbox.isChecked
|
||||
onCheckChanged(checkbox.isChecked)
|
||||
binding.drawerLayout.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun LayoutSideMenuEmulationBinding.configureSideMenu(isInputOverlayEnabled: Boolean) {
|
||||
enableMotionCheckbox.configure(onCheckChanged = ::setMotionEnabled)
|
||||
replaceTvWithPadCheckbox.configure(onCheckChanged = NativeEmulation::setReplaceTVWithPadView)
|
||||
showPadCheckbox.configure(onCheckChanged = ::setPadViewVisibility)
|
||||
showInputOverlayCheckbox.configure(initialCheckedStatus = isInputOverlayEnabled) { showInputOverlay ->
|
||||
editInputsMenuItem.setEnabled(showInputOverlay)
|
||||
resetInputOverlayMenuItem.setEnabled(showInputOverlay)
|
||||
inputOverlaySurfaceView.setVisible(showInputOverlay)
|
||||
}
|
||||
editInputsMenuItem.configure(isEnabled = isInputOverlayEnabled) {
|
||||
binding.editInputsLayout.visibility = View.VISIBLE
|
||||
binding.finishEditInputsButton.visibility = View.VISIBLE
|
||||
binding.moveInputsButton.performClick()
|
||||
}
|
||||
resetInputOverlayMenuItem.configure(
|
||||
isEnabled = isInputOverlayEnabled,
|
||||
onClick = inputOverlaySurfaceView::resetInputs
|
||||
)
|
||||
exitMenuItem.configure(onClick = onBackPressedDispatcher::onBackPressed)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun initializeView(launchPath: String) {
|
||||
setFullscreen()
|
||||
|
||||
binding = ActivityEmulationBinding.inflate(layoutInflater)
|
||||
inputOverlaySurfaceView = binding.inputOverlay
|
||||
|
||||
binding.sideMenu.configureSideMenu(overlaySettings.isOverlayEnabled)
|
||||
|
||||
binding.moveInputsButton.setOnClickListener { _ ->
|
||||
if (inputOverlaySurfaceView.getInputMode() == InputOverlaySurfaceView.InputMode.EDIT_POSITION) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.resizeInputsButton.alpha = 0.5f
|
||||
binding.moveInputsButton.alpha = 1.0f
|
||||
toastMessage(R.string.input_mode_edit_position)
|
||||
inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.EDIT_POSITION)
|
||||
}
|
||||
binding.resizeInputsButton.setOnClickListener { _ ->
|
||||
if (inputOverlaySurfaceView.getInputMode() == InputOverlaySurfaceView.InputMode.EDIT_SIZE) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.moveInputsButton.alpha = 0.5f
|
||||
binding.resizeInputsButton.alpha = 1.0f
|
||||
toastMessage(R.string.input_mode_edit_size)
|
||||
inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.EDIT_SIZE)
|
||||
}
|
||||
binding.finishEditInputsButton.setOnClickListener { _ ->
|
||||
inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.DEFAULT)
|
||||
binding.finishEditInputsButton.visibility = View.GONE
|
||||
binding.editInputsLayout.visibility = View.GONE
|
||||
toastMessage(R.string.input_mode_default)
|
||||
}
|
||||
binding.emulationSettingsButton.setOnClickListener { binding.drawerLayout.open() }
|
||||
val mainCanvas = binding.mainCanvas
|
||||
try {
|
||||
val testSurfaceTexture = SurfaceTexture(0)
|
||||
val testSurface = Surface(testSurfaceTexture)
|
||||
NativeEmulation.initializeRenderer(testSurface)
|
||||
testSurface.release()
|
||||
testSurfaceTexture.release()
|
||||
} catch (exception: NativeException) {
|
||||
onEmulationError(
|
||||
getString(
|
||||
R.string.failed_initialize_renderer_error,
|
||||
exception.message
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val mainCanvasHolder = mainCanvas.holder
|
||||
mainCanvasHolder.addCallback(CanvasSurfaceHolderCallback(isMainCanvas = true))
|
||||
mainCanvasHolder.addCallback(object : SurfaceHolder.Callback {
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
}
|
||||
|
||||
override fun surfaceChanged(
|
||||
holder: SurfaceHolder,
|
||||
format: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
if (hasEmulationError) {
|
||||
return
|
||||
}
|
||||
if (!isGameRunning) {
|
||||
isGameRunning = true
|
||||
startGame(launchPath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
}
|
||||
})
|
||||
mainCanvas.setOnTouchListener(CanvasOnTouchListener(isTV = true))
|
||||
}
|
||||
|
||||
private fun toastMessage(@StringRes toastTextResId: Int) {
|
||||
toast?.cancel()
|
||||
toast = Toast.makeText(this, toastTextResId, Toast.LENGTH_SHORT)
|
||||
.also { it.show() }
|
||||
}
|
||||
|
||||
private fun startGame(launchPath: String) {
|
||||
val result = NativeEmulation.startGame(launchPath)
|
||||
if (result == NativeEmulation.START_GAME_SUCCESSFUL) {
|
||||
return
|
||||
}
|
||||
val errorMessage = when (result) {
|
||||
NativeEmulation.START_GAME_ERROR_GAME_BASE_FILES_NOT_FOUND -> getString(R.string.game_not_found)
|
||||
NativeEmulation.START_GAME_ERROR_NO_DISC_KEY -> getString(R.string.no_disk_key)
|
||||
NativeEmulation.START_GAME_ERROR_NO_TITLE_TIK -> getString(R.string.no_title_tik)
|
||||
else -> getString(R.string.game_files_unknown_error, launchPath)
|
||||
}
|
||||
onEmulationError(errorMessage)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
sensorManager.pauseListening()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (isMotionEnabled) {
|
||||
sensorManager.startListening()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
sensorManager.pauseListening()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun createPadCanvas() {
|
||||
if (padCanvas != null) {
|
||||
return
|
||||
}
|
||||
val padCanvas = SurfaceView(this)
|
||||
binding.canvasesLayout.addView(
|
||||
padCanvas,
|
||||
LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f)
|
||||
)
|
||||
padCanvas.holder.addCallback(CanvasSurfaceHolderCallback(false))
|
||||
padCanvas.setOnTouchListener(CanvasOnTouchListener(false))
|
||||
this.padCanvas = padCanvas
|
||||
}
|
||||
|
||||
private fun showExitConfirmationDialog() {
|
||||
@@ -116,7 +371,8 @@ class EmulationActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_LAUNCH_PATH: String = BuildConfig.APPLICATION_ID + ".LaunchPath"
|
||||
private var emulationActivityInstance: EmulationActivity? = null
|
||||
private var emulationActivityInstance: WeakReference<EmulationActivity?> =
|
||||
WeakReference(null)
|
||||
|
||||
/**
|
||||
* This method is called by swkbd using JNI.
|
||||
@@ -124,20 +380,21 @@ class EmulationActivity : AppCompatActivity() {
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun showEmulationTextInput(initialText: String?, maxLength: Int) {
|
||||
if (emulationActivityInstance == null || emulationActivityInstance!!.emulationTextInputDialog != null) {
|
||||
val emulationActivity = emulationActivityInstance.get() ?: return
|
||||
if (emulationActivity.emulationTextInputDialog != null) {
|
||||
return
|
||||
}
|
||||
setCurrentInputText(initialText)
|
||||
emulationActivityInstance!!.runOnUiThread {
|
||||
emulationActivity.runOnUiThread {
|
||||
val inputEditTextLayout =
|
||||
emulationActivityInstance!!.layoutInflater.inflate(
|
||||
emulationActivity.layoutInflater.inflate(
|
||||
R.layout.layout_emulation_input,
|
||||
null
|
||||
)
|
||||
val inputEditText =
|
||||
inputEditTextLayout.requireViewById<EmulationTextInputEditText>(R.id.emulation_input_text)
|
||||
inputEditText.updateText(initialText)
|
||||
val dialog = MaterialAlertDialogBuilder(emulationActivityInstance!!)
|
||||
val dialog = MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setView(inputEditTextLayout)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.done) { _, _ -> }.show()
|
||||
@@ -156,7 +413,7 @@ class EmulationActivity : AppCompatActivity() {
|
||||
} else {
|
||||
parentTextInputLayout.isCounterEnabled = false
|
||||
}
|
||||
emulationActivityInstance!!.emulationTextInputDialog = dialog
|
||||
emulationActivity.emulationTextInputDialog = dialog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,12 +423,10 @@ class EmulationActivity : AppCompatActivity() {
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun hideEmulationTextInput() {
|
||||
if (emulationActivityInstance?.emulationTextInputDialog == null) {
|
||||
return
|
||||
}
|
||||
val textInputDialog = emulationActivityInstance!!.emulationTextInputDialog!!
|
||||
emulationActivityInstance!!.emulationTextInputDialog = null
|
||||
emulationActivityInstance!!.runOnUiThread { textInputDialog.dismiss() }
|
||||
val emulationActivity = emulationActivityInstance.get() ?: return
|
||||
val textInputDialog = emulationActivity.emulationTextInputDialog ?: return
|
||||
emulationActivity.emulationTextInputDialog = null
|
||||
emulationActivity.runOnUiThread { textInputDialog.dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
package info.cemu.cemu.emulation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import info.cemu.cemu.R
|
||||
import info.cemu.cemu.databinding.FragmentEmulationBinding
|
||||
import info.cemu.cemu.databinding.LayoutSideMenuCheckboxItemBinding
|
||||
import info.cemu.cemu.databinding.LayoutSideMenuEmulationBinding
|
||||
import info.cemu.cemu.databinding.LayoutSideMenuTextItemBinding
|
||||
import info.cemu.cemu.input.SensorManager
|
||||
import info.cemu.cemu.inputoverlay.InputOverlaySettingsManager
|
||||
import info.cemu.cemu.inputoverlay.InputOverlaySurfaceView
|
||||
import info.cemu.cemu.inputoverlay.OverlaySettings
|
||||
import info.cemu.cemu.nativeinterface.NativeEmulation
|
||||
import info.cemu.cemu.nativeinterface.NativeException
|
||||
import info.cemu.cemu.nativeinterface.NativeInput
|
||||
|
||||
private class CanvasOnTouchListener(val isTV: Boolean) : OnTouchListener {
|
||||
var currentPointerId: Int = -1
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
if (currentPointerId != -1 && pointerId != currentPointerId) {
|
||||
return false
|
||||
}
|
||||
val x = event.getX(pointerIndex).toInt()
|
||||
val y = event.getY(pointerIndex).toInt()
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
NativeInput.onTouchDown(x, y, isTV)
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
|
||||
currentPointerId = -1
|
||||
NativeInput.onTouchUp(x, y, isTV)
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
NativeInput.onTouchMove(x, y, isTV)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class EmulationFragment(private val launchPath: String) : Fragment() {
|
||||
private inner class CanvasSurfaceHolderCallback(val isMainCanvas: Boolean) :
|
||||
SurfaceHolder.Callback {
|
||||
var surfaceSet: Boolean = false
|
||||
|
||||
override fun surfaceCreated(surfaceHolder: SurfaceHolder) {}
|
||||
|
||||
override fun surfaceChanged(
|
||||
surfaceHolder: SurfaceHolder,
|
||||
format: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
try {
|
||||
NativeEmulation.setSurfaceSize(width, height, isMainCanvas)
|
||||
if (surfaceSet) {
|
||||
return
|
||||
}
|
||||
NativeEmulation.setSurface(surfaceHolder.surface, isMainCanvas)
|
||||
surfaceSet = true
|
||||
} catch (exception: NativeException) {
|
||||
onEmulationError(getString(R.string.failed_create_surface_error, exception.message))
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) {
|
||||
NativeEmulation.clearSurface(isMainCanvas)
|
||||
surfaceSet = false
|
||||
}
|
||||
}
|
||||
|
||||
fun interface OnEmulationErrorCallback {
|
||||
fun onEmulationError(errorMessage: String)
|
||||
}
|
||||
|
||||
private var isGameRunning = false
|
||||
private var padCanvas: SurfaceView? = null
|
||||
private var toast: Toast? = null
|
||||
private lateinit var binding: FragmentEmulationBinding
|
||||
private var isMotionEnabled = false
|
||||
private lateinit var overlaySettings: OverlaySettings
|
||||
private lateinit var inputOverlaySurfaceView: InputOverlaySurfaceView
|
||||
private lateinit var sensorManager: SensorManager
|
||||
private var onEmulationErrorCallback: OnEmulationErrorCallback? = null
|
||||
private var hasEmulationError = false
|
||||
|
||||
fun setOnEmulationErrorCallback(onEmulationErrorCallback: OnEmulationErrorCallback) {
|
||||
this.onEmulationErrorCallback = onEmulationErrorCallback
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val inputOverlaySettingsManager = InputOverlaySettingsManager(requireContext())
|
||||
overlaySettings = inputOverlaySettingsManager.overlaySettings
|
||||
sensorManager = SensorManager(requireContext())
|
||||
sensorManager.setIsLandscape(resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
sensorManager.setIsLandscape(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
sensorManager.pauseListening()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (isMotionEnabled) {
|
||||
sensorManager.startListening()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
sensorManager.pauseListening()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun createPadCanvas() {
|
||||
if (padCanvas != null) {
|
||||
return
|
||||
}
|
||||
padCanvas = SurfaceView(requireContext())
|
||||
binding.canvasesLayout.addView(
|
||||
padCanvas,
|
||||
LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f)
|
||||
)
|
||||
padCanvas!!.holder.addCallback(CanvasSurfaceHolderCallback(false))
|
||||
padCanvas!!.setOnTouchListener(CanvasOnTouchListener(false))
|
||||
}
|
||||
|
||||
private fun toastMessage(@StringRes toastTextResId: Int) {
|
||||
toast?.cancel()
|
||||
toast = Toast.makeText(requireContext(), toastTextResId, Toast.LENGTH_SHORT)
|
||||
.also { it.show() }
|
||||
}
|
||||
|
||||
private fun destroyPadCanvas() {
|
||||
if (padCanvas == null) {
|
||||
return
|
||||
}
|
||||
binding.canvasesLayout.removeView(padCanvas)
|
||||
padCanvas = null
|
||||
}
|
||||
|
||||
private fun setPadViewVisibility(visible: Boolean) {
|
||||
if (visible) {
|
||||
createPadCanvas()
|
||||
} else {
|
||||
destroyPadCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMotionEnabled(enabled: Boolean) {
|
||||
isMotionEnabled = enabled
|
||||
if (isMotionEnabled) {
|
||||
sensorManager.startListening()
|
||||
} else {
|
||||
sensorManager.pauseListening()
|
||||
}
|
||||
}
|
||||
|
||||
private fun LayoutSideMenuTextItemBinding.setEnabled(isEnabled: Boolean) {
|
||||
textItem.isEnabled = isEnabled
|
||||
textItem.alpha = if (isEnabled) 1f else 0.7f
|
||||
}
|
||||
|
||||
private fun LayoutSideMenuTextItemBinding.configure(
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
setEnabled(isEnabled)
|
||||
textItem.setOnClickListener {
|
||||
onClick()
|
||||
binding.drawerLayout.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun LayoutSideMenuCheckboxItemBinding.configure(
|
||||
initialCheckedStatus: Boolean = false,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
checkbox.isChecked = initialCheckedStatus
|
||||
checkboxItem.setOnClickListener {
|
||||
checkbox.isChecked = !checkbox.isChecked
|
||||
onCheckChanged(checkbox.isChecked)
|
||||
binding.drawerLayout.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun LayoutSideMenuEmulationBinding.configureSideMenu(isInputOverlayEnabled: Boolean) {
|
||||
enableMotionCheckbox.configure(onCheckChanged = ::setMotionEnabled)
|
||||
replaceTvWithPadCheckbox.configure(onCheckChanged = NativeEmulation::setReplaceTVWithPadView)
|
||||
showPadCheckbox.configure(onCheckChanged = ::setPadViewVisibility)
|
||||
showInputOverlayCheckbox.configure(initialCheckedStatus = isInputOverlayEnabled) { showInputOverlay ->
|
||||
editInputsMenuItem.setEnabled(showInputOverlay)
|
||||
resetInputOverlayMenuItem.setEnabled(showInputOverlay)
|
||||
inputOverlaySurfaceView.setVisible(showInputOverlay)
|
||||
this@EmulationFragment.view?.invalidate()
|
||||
}
|
||||
editInputsMenuItem.configure(isEnabled = isInputOverlayEnabled) {
|
||||
binding.editInputsLayout.visibility = View.VISIBLE
|
||||
binding.finishEditInputsButton.visibility = View.VISIBLE
|
||||
binding.moveInputsButton.performClick()
|
||||
}
|
||||
resetInputOverlayMenuItem.configure(
|
||||
isEnabled = isInputOverlayEnabled,
|
||||
onClick = inputOverlaySurfaceView::resetInputs
|
||||
)
|
||||
exitMenuItem.configure { requireActivity().onBackPressedDispatcher.onBackPressed() }
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
binding = FragmentEmulationBinding.inflate(inflater, container, false)
|
||||
inputOverlaySurfaceView = binding.inputOverlay
|
||||
binding.sideMenu.configureSideMenu(isInputOverlayEnabled = overlaySettings.isOverlayEnabled)
|
||||
inputOverlaySurfaceView.setVisible(overlaySettings.isOverlayEnabled)
|
||||
binding.moveInputsButton.setOnClickListener { _ ->
|
||||
if (inputOverlaySurfaceView.getInputMode() == InputOverlaySurfaceView.InputMode.EDIT_POSITION) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.resizeInputsButton.alpha = 0.5f
|
||||
binding.moveInputsButton.alpha = 1.0f
|
||||
toastMessage(R.string.input_mode_edit_position)
|
||||
inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.EDIT_POSITION)
|
||||
}
|
||||
binding.resizeInputsButton.setOnClickListener { _ ->
|
||||
if (inputOverlaySurfaceView.getInputMode() == InputOverlaySurfaceView.InputMode.EDIT_SIZE) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.moveInputsButton.alpha = 0.5f
|
||||
binding.resizeInputsButton.alpha = 1.0f
|
||||
toastMessage(R.string.input_mode_edit_size)
|
||||
inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.EDIT_SIZE)
|
||||
}
|
||||
binding.finishEditInputsButton.setOnClickListener { _ ->
|
||||
inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.DEFAULT)
|
||||
binding.finishEditInputsButton.visibility = View.GONE
|
||||
binding.editInputsLayout.visibility = View.GONE
|
||||
toastMessage(R.string.input_mode_default)
|
||||
}
|
||||
binding.emulationSettingsButton.setOnClickListener { binding.drawerLayout.open() }
|
||||
val mainCanvas = binding.mainCanvas
|
||||
try {
|
||||
val testSurfaceTexture = SurfaceTexture(0)
|
||||
val testSurface = Surface(testSurfaceTexture)
|
||||
NativeEmulation.initializeRenderer(testSurface)
|
||||
testSurface.release()
|
||||
testSurfaceTexture.release()
|
||||
} catch (exception: NativeException) {
|
||||
onEmulationError(
|
||||
getString(
|
||||
R.string.failed_initialize_renderer_error,
|
||||
exception.message
|
||||
)
|
||||
)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
val mainCanvasHolder = mainCanvas.holder
|
||||
mainCanvasHolder.addCallback(CanvasSurfaceHolderCallback(isMainCanvas = true))
|
||||
mainCanvasHolder.addCallback(object : SurfaceHolder.Callback {
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
}
|
||||
|
||||
override fun surfaceChanged(
|
||||
holder: SurfaceHolder,
|
||||
format: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
if (hasEmulationError) {
|
||||
return
|
||||
}
|
||||
if (!isGameRunning) {
|
||||
isGameRunning = true
|
||||
startGame()
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
}
|
||||
})
|
||||
mainCanvas.setOnTouchListener(CanvasOnTouchListener(isTV = true))
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun startGame() {
|
||||
val result = NativeEmulation.startGame(launchPath)
|
||||
if (result == NativeEmulation.START_GAME_SUCCESSFUL) {
|
||||
return
|
||||
}
|
||||
val errorMessage = when (result) {
|
||||
NativeEmulation.START_GAME_ERROR_GAME_BASE_FILES_NOT_FOUND -> getString(R.string.game_not_found)
|
||||
NativeEmulation.START_GAME_ERROR_NO_DISC_KEY -> getString(R.string.no_disk_key)
|
||||
NativeEmulation.START_GAME_ERROR_NO_TITLE_TIK -> getString(R.string.no_title_tik)
|
||||
else -> getString(R.string.game_files_unknown_error, launchPath)
|
||||
}
|
||||
onEmulationError(errorMessage)
|
||||
}
|
||||
|
||||
private fun onEmulationError(errorMessage: String) {
|
||||
hasEmulationError = true
|
||||
onEmulationErrorCallback?.onEmulationError(errorMessage)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<info.cemu.cemu.guicore.components.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
tools:context=".emulation.EmulationActivity">
|
||||
android:touchscreenBlocksFocus="true"
|
||||
tools:openDrawer="start">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/emulation_frame"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</FrameLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/canvases_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<SurfaceView
|
||||
android:id="@+id/main_canvas"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<info.cemu.cemu.inputoverlay.InputOverlaySurfaceView
|
||||
android:id="@+id/input_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/finish_edit_inputs_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="@string/input_finish_edit_label"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/edit_inputs_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<Button
|
||||
android:id="@+id/move_inputs_button"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/move_inputs_description"
|
||||
app:icon="@drawable/ic_move" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/resize_inputs_button"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/resize_inputs_description"
|
||||
app:icon="@drawable/ic_resize" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/emulation_settings_button"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/input_mode_button_description"
|
||||
app:icon="@drawable/ic_settings" />
|
||||
</RelativeLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/side_menu"
|
||||
layout="@layout/layout_side_menu_emulation" />
|
||||
|
||||
</info.cemu.cemu.guicore.components.DrawerLayout>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<info.cemu.cemu.guicore.components.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:touchscreenBlocksFocus="true"
|
||||
tools:context=".emulation.EmulationFragment"
|
||||
tools:openDrawer="start">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/canvases_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<SurfaceView
|
||||
android:id="@+id/main_canvas"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<info.cemu.cemu.inputoverlay.InputOverlaySurfaceView
|
||||
android:id="@+id/input_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/finish_edit_inputs_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="@string/input_finish_edit_label"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/edit_inputs_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<Button
|
||||
android:id="@+id/move_inputs_button"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/move_inputs_description"
|
||||
app:icon="@drawable/ic_move" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/resize_inputs_button"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/resize_inputs_description"
|
||||
app:icon="@drawable/ic_resize" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/emulation_settings_button"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/input_mode_button_description"
|
||||
app:icon="@drawable/ic_settings" />
|
||||
</RelativeLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/side_menu"
|
||||
layout="@layout/layout_side_menu_emulation" />
|
||||
|
||||
</info.cemu.cemu.guicore.components.DrawerLayout>
|
||||
Reference in New Issue
Block a user