feat(android-hotkeys): Introduce hotkey support for Android app and add missing hybrid layout (#7241)

* feat(android-hotkeys): Introduce hotkey support for Android app

* android: Fix settings not saving for layout options - screen swap + layout.

* android: Fix `from` method to default to "DEFAULT" if passed an invalid method (and also not be based on ordering)

* android: PR response - name to togglePause
This commit is contained in:
James Forward 2023-12-23 03:52:12 +00:00 committed by GitHub
parent 178e602589
commit 60a280af24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 259 additions and 50 deletions

View file

@ -20,7 +20,6 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
@ -32,13 +31,15 @@ import org.citra.citra_emu.R
import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
import org.citra.citra_emu.contracts.OpenFileResultContract import org.citra.citra_emu.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityEmulationBinding import org.citra.citra_emu.databinding.ActivityEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.utils.ControllerMappingHelper import org.citra.citra_emu.utils.ControllerMappingHelper
import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.FileBrowserHelper import org.citra.citra_emu.utils.FileBrowserHelper
import org.citra.citra_emu.utils.ForegroundService import org.citra.citra_emu.utils.ForegroundService
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.ThemeUtil import org.citra.citra_emu.utils.ThemeUtil
import org.citra.citra_emu.viewmodel.EmulationViewModel import org.citra.citra_emu.viewmodel.EmulationViewModel
@ -52,6 +53,8 @@ class EmulationActivity : AppCompatActivity() {
private val emulationViewModel: EmulationViewModel by viewModels() private val emulationViewModel: EmulationViewModel by viewModels()
private lateinit var binding: ActivityEmulationBinding private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.setTheme(this) ThemeUtil.setTheme(this)
@ -61,6 +64,8 @@ class EmulationActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings)
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil)
setContentView(binding.root) setContentView(binding.root)
val navHostFragment = val navHostFragment =
@ -73,15 +78,11 @@ class EmulationActivity : AppCompatActivity() {
// Set these options now so that the SurfaceView the game renders into is the right size. // Set these options now so that the SurfaceView the game renders into is the right size.
enableFullscreenImmersive() enableFullscreenImmersive()
// Override Citra core INI with the one set by our in game menu
NativeLibrary.swapScreens(
EmulationMenuSettings.swapScreens,
windowManager.defaultDisplay.rotation
)
// Start a foreground service to prevent the app from getting killed in the background // Start a foreground service to prevent the app from getting killed in the background
foregroundService = Intent(this, ForegroundService::class.java) foregroundService = Intent(this, ForegroundService::class.java)
startForegroundService(foregroundService) startForegroundService(foregroundService)
EmulationLifecycleUtil.addShutdownHook(hook = { this.finish() })
} }
// On some devices, the system bars will not disappear on first boot or after some // On some devices, the system bars will not disappear on first boot or after some
@ -103,6 +104,7 @@ class EmulationActivity : AppCompatActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
EmulationLifecycleUtil.clear()
stopForegroundService(this) stopForegroundService(this)
super.onDestroy() super.onDestroy()
} }
@ -188,6 +190,8 @@ class EmulationActivity : AppCompatActivity() {
onBackPressed() onBackPressed()
} }
hotkeyUtility.handleHotkey(button)
// Normal key events. // Normal key events.
NativeLibrary.ButtonState.PRESSED NativeLibrary.ButtonState.PRESSED
} }

View file

@ -0,0 +1,42 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.display
import android.view.WindowManager
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.utils.EmulationMenuSettings
class ScreenAdjustmentUtil(private val windowManager: WindowManager,
private val settings: Settings) {
fun swapScreen() {
val isEnabled = !EmulationMenuSettings.swapScreens
EmulationMenuSettings.swapScreens = isEnabled
NativeLibrary.swapScreens(
isEnabled,
windowManager.defaultDisplay.rotation
)
BooleanSetting.SWAP_SCREEN.boolean = isEnabled
settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG)
}
fun cycleLayouts() {
val nextLayout = (EmulationMenuSettings.landscapeScreenLayout + 1) % ScreenLayout.entries.size
changeScreenOrientation(ScreenLayout.from(nextLayout))
}
fun changeScreenOrientation(layoutOption: ScreenLayout) {
EmulationMenuSettings.landscapeScreenLayout = layoutOption.int
NativeLibrary.notifyOrientationChange(
EmulationMenuSettings.landscapeScreenLayout,
windowManager.defaultDisplay.rotation
)
IntSetting.SCREEN_LAYOUT.int = layoutOption.int
settings.saveSetting(IntSetting.SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG)
}
}

View file

@ -0,0 +1,22 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.display
enum class ScreenLayout(val int: Int) {
// These must match what is defined in src/common/settings.h
DEFAULT(0),
SINGLE_SCREEN(1),
LARGE_SCREEN(2),
SIDE_SCREEN(3),
HYBRID_SCREEN(4),
MOBILE_PORTRAIT(5),
MOBILE_LANDSCAPE(6);
companion object {
fun from(int: Int): ScreenLayout {
return entries.firstOrNull { it.int == int } ?: DEFAULT
}
}
}

View file

@ -0,0 +1,12 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.hotkeys
enum class Hotkey(val button: Int) {
SWAP_SCREEN(10001),
CYCLE_LAYOUT(10002),
CLOSE_GAME(10003),
PAUSE_OR_RESUME(10004);
}

View file

@ -0,0 +1,27 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.hotkeys
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.display.ScreenAdjustmentUtil
class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
val hotkeyButtons = Hotkey.entries.map { it.button }
fun handleHotkey(bindedButton: Int): Boolean {
if(hotkeyButtons.contains(bindedButton)) {
when (bindedButton) {
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
else -> {}
}
return true
}
return false
}
}

View file

@ -12,7 +12,8 @@ enum class BooleanSetting(
SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true), SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false), ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false), PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true); ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true),
SWAP_SCREEN("swap_screen", Settings.SECTION_LAYOUT, false);
override var boolean: Boolean = defaultValue override var boolean: Boolean = defaultValue

View file

@ -22,6 +22,7 @@ enum class IntSetting(
CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85), CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85),
CARDBOARD_X_SHIFT("cardboard_x_shift", Settings.SECTION_LAYOUT, 0), CARDBOARD_X_SHIFT("cardboard_x_shift", Settings.SECTION_LAYOUT, 0),
CARDBOARD_Y_SHIFT("cardboard_y_shift", Settings.SECTION_LAYOUT, 0), CARDBOARD_Y_SHIFT("cardboard_y_shift", Settings.SECTION_LAYOUT, 0),
SCREEN_LAYOUT("layout_option", Settings.SECTION_LAYOUT, 0),
AUDIO_INPUT_TYPE("output_type", Settings.SECTION_AUDIO, 0), AUDIO_INPUT_TYPE("output_type", Settings.SECTION_AUDIO, 0),
NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, 1), NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, 1),
CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100), CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100),

View file

@ -94,6 +94,10 @@ class Settings {
} }
} }
fun saveSetting(setting: AbstractSetting, filename: String) {
SettingsFile.saveFile(filename, setting)
}
companion object { companion object {
const val SECTION_CORE = "Core" const val SECTION_CORE = "Core"
const val SECTION_SYSTEM = "System" const val SECTION_SYSTEM = "System"
@ -128,6 +132,11 @@ class Settings {
const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical" const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"
const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal" const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"
const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap"
const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout"
const val HOTKEY_CLOSE_GAME = "hotkey_close_game"
const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game"
val buttonKeys = listOf( val buttonKeys = listOf(
KEY_BUTTON_A, KEY_BUTTON_A,
KEY_BUTTON_B, KEY_BUTTON_B,
@ -174,6 +183,18 @@ class Settings {
R.string.button_zl, R.string.button_zl,
R.string.button_zr R.string.button_zr
) )
val hotKeys = listOf(
HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME,
HOTKEY_PAUSE_OR_RESUME
)
val hotkeyTitles = listOf(
R.string.emulation_swap_screens,
R.string.emulation_cycle_landscape_layouts,
R.string.emulation_close_game,
R.string.emulation_toggle_pause
)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MATERIAL_YOU = "MaterialYouTheme" const val PREF_MATERIAL_YOU = "MaterialYouTheme"

View file

@ -6,14 +6,15 @@ package org.citra.citra_emu.features.settings.model.view
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import android.view.InputDevice import android.view.InputDevice
import android.view.InputDevice.MotionRange import android.view.InputDevice.MotionRange
import android.view.KeyEvent import android.view.KeyEvent
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R import org.citra.citra_emu.R
import org.citra.citra_emu.features.hotkeys.Hotkey
import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.Settings
@ -127,6 +128,11 @@ class InputBindingSetting(
Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
Settings.HOTKEY_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button
Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button
Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button
Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button
else -> -1 else -> -1
} }

View file

@ -38,8 +38,8 @@ import org.citra.citra_emu.features.settings.model.view.SwitchSetting
import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.fragments.ResetSettingsDialogFragment import org.citra.citra_emu.fragments.ResetSettingsDialogFragment
import org.citra.citra_emu.utils.BirthdayMonth import org.citra.citra_emu.utils.BirthdayMonth
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.ThemeUtil import org.citra.citra_emu.utils.ThemeUtil
class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
@ -620,6 +620,12 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
val button = getInputObject(key) val button = getInputObject(key)
add(InputBindingSetting(button, Settings.triggerTitles[i])) add(InputBindingSetting(button, Settings.triggerTitles[i]))
} }
add(HeaderSetting(R.string.controller_hotkeys))
Settings.hotKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key)
add(InputBindingSetting(button, Settings.hotkeyTitles[i]))
}
} }
} }

View file

@ -8,7 +8,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R import org.citra.citra_emu.R
import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.BooleanSetting
@ -23,9 +22,11 @@ import org.citra.citra_emu.utils.BiMap
import org.citra.citra_emu.utils.DirectoryInitialization.userDirectory import org.citra.citra_emu.utils.DirectoryInitialization.userDirectory
import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.Log
import org.ini4j.Wini import org.ini4j.Wini
import java.io.* import java.io.BufferedReader
import java.lang.NumberFormatException import java.io.FileNotFoundException
import java.util.* import java.io.IOException
import java.io.InputStreamReader
import java.util.TreeMap
/** /**
@ -146,6 +147,26 @@ object SettingsFile {
} }
} }
fun saveFile(
fileName: String,
setting: AbstractSetting
) {
val ini = getSettingsFile(fileName)
try {
val context: Context = CitraApplication.appContext
val inputStream = context.contentResolver.openInputStream(ini.uri)
val writer = Wini(inputStream)
writer.put(setting.section, setting.key, setting.valueAsString)
inputStream!!.close()
val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt")
writer.store(outputStream)
outputStream!!.flush()
outputStream.close()
} catch (e: Exception) {
Log.error("[SettingsFile] File not found: $fileName.ini: ${e.message}")
}
}
private fun mapSectionNameFromIni(generalSectionName: String): String? { private fun mapSectionNameFromIni(generalSectionName: String): String? {
return if (sectionsMap.getForward(generalSectionName) != null) { return if (sectionsMap.getForward(generalSectionName) != null) {
sectionsMap.getForward(generalSectionName) sectionsMap.getForward(generalSectionName)

View file

@ -15,7 +15,6 @@ import android.os.Looper
import android.os.SystemClock import android.os.SystemClock
import android.view.Choreographer import android.view.Choreographer
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent import android.view.MotionEvent
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
@ -33,6 +32,7 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
@ -51,6 +51,9 @@ import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.databinding.DialogCheckboxBinding import org.citra.citra_emu.databinding.DialogCheckboxBinding
import org.citra.citra_emu.databinding.DialogSliderBinding import org.citra.citra_emu.databinding.DialogSliderBinding
import org.citra.citra_emu.databinding.FragmentEmulationBinding import org.citra.citra_emu.databinding.FragmentEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.display.ScreenLayout
import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.model.Game import org.citra.citra_emu.model.Game
@ -60,10 +63,10 @@ import org.citra.citra_emu.utils.EmulationMenuSettings
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
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.ViewUtils import org.citra.citra_emu.utils.ViewUtils
import org.citra.citra_emu.viewmodel.EmulationViewModel import org.citra.citra_emu.viewmodel.EmulationViewModel
import java.lang.NullPointerException
class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.FrameCallback { class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.FrameCallback {
private val preferences: SharedPreferences private val preferences: SharedPreferences
@ -80,8 +83,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private val args by navArgs<EmulationFragmentArgs>() private val args by navArgs<EmulationFragmentArgs>()
private lateinit var game: Game private lateinit var game: Game
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private val emulationViewModel: EmulationViewModel by activityViewModels() private val emulationViewModel: EmulationViewModel by activityViewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@ -137,6 +142,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
retainInstance = true retainInstance = true
emulationState = EmulationState(game.path) emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity emulationActivity = requireActivity() as EmulationActivity
screenAdjustmentUtil = ScreenAdjustmentUtil(emulationActivity.windowManager, settingsViewModel.settings)
EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() })
EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() })
} }
override fun onCreateView( override fun onCreateView(
@ -258,12 +266,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
} }
R.id.menu_swap_screens -> { R.id.menu_swap_screens -> {
val isEnabled = !EmulationMenuSettings.swapScreens screenAdjustmentUtil.swapScreen()
EmulationMenuSettings.swapScreens = isEnabled
NativeLibrary.swapScreens(
isEnabled,
requireActivity().windowManager.defaultDisplay.rotation
)
true true
} }
@ -315,8 +318,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
.setTitle(R.string.emulation_close_game) .setTitle(R.string.emulation_close_game)
.setMessage(R.string.emulation_close_game_message) .setMessage(R.string.emulation_close_game_message)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
emulationState.stop() EmulationLifecycleUtil.closeGame()
requireActivity().finish()
} }
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> .setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
NativeLibrary.unPauseEmulation() NativeLibrary.unPauseEmulation()
@ -410,6 +412,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
setInsets() setInsets()
} }
private fun togglePause() {
if(emulationState.isPaused) {
emulationState.unpause()
} else {
emulationState.pause()
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
Choreographer.getInstance().postFrameCallback(this) Choreographer.getInstance().postFrameCallback(this)
@ -666,15 +676,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menuInflater.inflate(R.menu.menu_landscape_screen_layout, popupMenu.menu) popupMenu.menuInflater.inflate(R.menu.menu_landscape_screen_layout, popupMenu.menu)
val layoutOptionMenuItem = when (EmulationMenuSettings.landscapeScreenLayout) { val layoutOptionMenuItem = when (EmulationMenuSettings.landscapeScreenLayout) {
EmulationMenuSettings.LayoutOption_SingleScreen -> ScreenLayout.SINGLE_SCREEN.int ->
R.id.menu_screen_layout_single R.id.menu_screen_layout_single
EmulationMenuSettings.LayoutOption_SideScreen -> ScreenLayout.SIDE_SCREEN.int ->
R.id.menu_screen_layout_sidebyside R.id.menu_screen_layout_sidebyside
EmulationMenuSettings.LayoutOption_MobilePortrait -> ScreenLayout.MOBILE_PORTRAIT.int ->
R.id.menu_screen_layout_portrait R.id.menu_screen_layout_portrait
ScreenLayout.HYBRID_SCREEN.int ->
R.id.menu_screen_layout_hybrid
else -> R.id.menu_screen_layout_landscape else -> R.id.menu_screen_layout_landscape
} }
popupMenu.menu.findItem(layoutOptionMenuItem).setChecked(true) popupMenu.menu.findItem(layoutOptionMenuItem).setChecked(true)
@ -682,22 +695,27 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.setOnMenuItemClickListener { popupMenu.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_screen_layout_landscape -> { R.id.menu_screen_layout_landscape -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, it) screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.MOBILE_LANDSCAPE)
true true
} }
R.id.menu_screen_layout_portrait -> { R.id.menu_screen_layout_portrait -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, it) screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.MOBILE_PORTRAIT)
true true
} }
R.id.menu_screen_layout_single -> { R.id.menu_screen_layout_single -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, it) screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.SINGLE_SCREEN)
true true
} }
R.id.menu_screen_layout_sidebyside -> { R.id.menu_screen_layout_sidebyside -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, it) screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.SIDE_SCREEN)
true
}
R.id.menu_screen_layout_hybrid -> {
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.HYBRID_SCREEN)
true true
} }
@ -708,15 +726,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show() popupMenu.show()
} }
private fun changeScreenOrientation(layoutOption: Int, item: MenuItem) {
item.setChecked(true)
NativeLibrary.notifyOrientationChange(
layoutOption,
requireActivity().windowManager.defaultDisplay.rotation
)
EmulationMenuSettings.landscapeScreenLayout = layoutOption
}
private fun editControlsPlacement() { private fun editControlsPlacement() {
if (binding.surfaceInputOverlay.isInEditMode) { if (binding.surfaceInputOverlay.isInEditMode) {
binding.doneControlConfig.visibility = View.GONE binding.doneControlConfig.visibility = View.GONE

View file

@ -0,0 +1,32 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.utils
object EmulationLifecycleUtil {
private var shutdownHooks: MutableList<Runnable> = ArrayList()
private var pauseResumeHooks: MutableList<Runnable> = ArrayList()
fun closeGame() {
shutdownHooks.forEach(Runnable::run)
}
fun pauseOrResume() {
pauseResumeHooks.forEach(Runnable::run)
}
fun addShutdownHook(hook: Runnable) {
shutdownHooks.add(hook)
}
fun addPauseResumeHook(hook: Runnable) {
pauseResumeHooks.add(hook)
}
fun clear() {
pauseResumeHooks.clear()
shutdownHooks.clear()
}
}

View file

@ -7,19 +7,12 @@ package org.citra.citra_emu.utils
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.display.ScreenLayout
object EmulationMenuSettings { object EmulationMenuSettings {
private val preferences = private val preferences =
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
// These must match what is defined in src/common/settings.h
const val LayoutOption_Default = 0
const val LayoutOption_SingleScreen = 1
const val LayoutOption_LargeScreen = 2
const val LayoutOption_SideScreen = 3
const val LayoutOption_MobilePortrait = 5
const val LayoutOption_MobileLandscape = 6
var joystickRelCenter: Boolean var joystickRelCenter: Boolean
get() = preferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true) get() = preferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true)
set(value) { set(value) {
@ -37,7 +30,7 @@ object EmulationMenuSettings {
var landscapeScreenLayout: Int var landscapeScreenLayout: Int
get() = preferences.getInt( get() = preferences.getInt(
"EmulationMenuSettings_LandscapeScreenLayout", "EmulationMenuSettings_LandscapeScreenLayout",
LayoutOption_MobileLandscape ScreenLayout.MOBILE_LANDSCAPE.int
) )
set(value) { set(value) {
preferences.edit() preferences.edit()

View file

@ -83,6 +83,10 @@
<item <item
android:id="@+id/menu_screen_layout_sidebyside" android:id="@+id/menu_screen_layout_sidebyside"
android:title="@string/emulation_screen_layout_sidebyside" /> android:title="@string/emulation_screen_layout_sidebyside" />
<item
android:id="@+id/menu_screen_layout_hybrid"
android:title="@string/emulation_screen_layout_hybrid" />
</group> </group>
</menu> </menu>
</item> </item>

View file

@ -19,6 +19,10 @@
android:id="@+id/menu_screen_layout_sidebyside" android:id="@+id/menu_screen_layout_sidebyside"
android:title="@string/emulation_screen_layout_sidebyside" /> android:title="@string/emulation_screen_layout_sidebyside" />
<item
android:id="@+id/menu_screen_layout_hybrid"
android:title="@string/emulation_screen_layout_hybrid" />
</group> </group>
</menu> </menu>

View file

@ -104,6 +104,7 @@
<!-- Input related strings --> <!-- Input related strings -->
<string name="controller_circlepad">Circle Pad</string> <string name="controller_circlepad">Circle Pad</string>
<string name="controller_c">C-Stick</string> <string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Hotkeys</string>
<string name="controller_triggers">Triggers</string> <string name="controller_triggers">Triggers</string>
<string name="controller_trigger">Trigger</string> <string name="controller_trigger">Trigger</string>
<string name="controller_dpad">D-Pad</string> <string name="controller_dpad">D-Pad</string>
@ -336,10 +337,13 @@
<string name="emulation_screen_layout_portrait">Portrait</string> <string name="emulation_screen_layout_portrait">Portrait</string>
<string name="emulation_screen_layout_single">Single Screen</string> <string name="emulation_screen_layout_single">Single Screen</string>
<string name="emulation_screen_layout_sidebyside">Side by Side Screens</string> <string name="emulation_screen_layout_sidebyside">Side by Side Screens</string>
<string name="emulation_screen_layout_hybrid">Hybrid Screens</string>
<string name="emulation_cycle_landscape_layouts">Cycle Landscape Layouts</string>
<string name="emulation_swap_screens">Swap Screens</string> <string name="emulation_swap_screens">Swap Screens</string>
<string name="emulation_touch_overlay_reset">Reset Overlay</string> <string name="emulation_touch_overlay_reset">Reset Overlay</string>
<string name="emulation_show_overlay">Show Overlay</string> <string name="emulation_show_overlay">Show Overlay</string>
<string name="emulation_close_game">Close Game</string> <string name="emulation_close_game">Close Game</string>
<string name="emulation_toggle_pause">Toggle Pause</string>
<string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string> <string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string>
<string name="menu_emulation_amiibo">Amiibo</string> <string name="menu_emulation_amiibo">Amiibo</string>
<string name="menu_emulation_amiibo_load">Load</string> <string name="menu_emulation_amiibo_load">Load</string>