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:
parent
178e602589
commit
60a280af24
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Reference in New Issue