Compare commits
7 Commits
android-14
...
android-14
Author | SHA1 | Date | |
---|---|---|---|
21e859ff2b | |||
b1b0ce1a59 | |||
2ef3268944 | |||
c7649a0cdb | |||
5a96c525e3 | |||
281eb020ea | |||
f61cf14646 |
@ -260,6 +260,11 @@ if (UNIX)
|
|||||||
add_definitions(-DYUZU_UNIX=1)
|
add_definitions(-DYUZU_UNIX=1)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (ARCHITECTURE_arm64 AND (ANDROID OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux"))
|
||||||
|
set(HAS_NCE 1)
|
||||||
|
add_definitions(-DHAS_NCE=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Configure C++ standard
|
# Configure C++ standard
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
| Pull Request | Commit | Title | Author | Merged? |
|
| Pull Request | Commit | Title | Author | Merged? |
|
||||||
|----|----|----|----|----|
|
|----|----|----|----|----|
|
||||||
| [12074](https://github.com/yuzu-emu/yuzu//pull/12074) | [`15f35b865`](https://github.com/yuzu-emu/yuzu//pull/12074/files) | Implement Native Code Execution (NCE) | [GPUCode](https://github.com/GPUCode/) | Yes |
|
| [12074](https://github.com/yuzu-emu/yuzu//pull/12074) | [`a76a8fb5f`](https://github.com/yuzu-emu/yuzu//pull/12074/files) | Implement Native Code Execution (NCE) | [GPUCode](https://github.com/GPUCode/) | Yes |
|
||||||
|
| [12204](https://github.com/yuzu-emu/yuzu//pull/12204) | [`caa83609f`](https://github.com/yuzu-emu/yuzu//pull/12204/files) | android: Multi directory UI | [t895](https://github.com/t895/) | Yes |
|
||||||
|
|
||||||
|
|
||||||
End of merge log. You can find the original README.md below the break.
|
End of merge log. You can find the original README.md below the break.
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
||||||
|
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
|
||||||
|
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
|
||||||
|
ListAdapter<GameDir, FolderAdapter.FolderViewHolder>(
|
||||||
|
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||||
|
) {
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): FolderAdapter.FolderViewHolder {
|
||||||
|
CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
.also { return FolderViewHolder(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
|
||||||
|
holder.bind(currentList[position])
|
||||||
|
|
||||||
|
inner class FolderViewHolder(val binding: CardFolderBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
private lateinit var gameDir: GameDir
|
||||||
|
|
||||||
|
fun bind(gameDir: GameDir) {
|
||||||
|
this.gameDir = gameDir
|
||||||
|
|
||||||
|
binding.apply {
|
||||||
|
path.text = Uri.parse(gameDir.uriString).path
|
||||||
|
path.postDelayed(
|
||||||
|
{
|
||||||
|
path.isSelected = true
|
||||||
|
path.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
|
buttonEdit.setOnClickListener {
|
||||||
|
GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir)
|
||||||
|
.show(
|
||||||
|
activity.supportFragmentManager,
|
||||||
|
GameFolderPropertiesDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonDelete.setOnClickListener {
|
||||||
|
gamesViewModel.removeFolder(this@FolderViewHolder.gameDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
|
||||||
|
override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,33 +3,9 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.widget.Toast
|
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
object Settings {
|
object Settings {
|
||||||
private val context get() = YuzuApplication.appContext
|
|
||||||
|
|
||||||
fun saveSettings(gameId: String = "") {
|
|
||||||
if (TextUtils.isEmpty(gameId)) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.ini_saved),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
NativeConfig.saveSettings()
|
|
||||||
} else {
|
|
||||||
// TODO: Save custom game settings
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.gameid_saved, gameId),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Category {
|
enum class Category {
|
||||||
Android,
|
Android,
|
||||||
Audio,
|
Audio,
|
||||||
|
@ -19,12 +19,13 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
|
||||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||||
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
|
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
|
||||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
|
import org.yuzu.yuzu_emu.model.SettingsViewModel
|
||||||
@ -53,10 +54,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
||||||
InsetsHelper.GESTURE_NAVIGATION
|
InsetsHelper.GESTURE_NAVIGATION
|
||||||
) {
|
) {
|
||||||
@ -127,12 +124,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
// Critical: If super method is not called, rotations will be busted.
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
// TODO: Load custom settings contextually
|
// TODO: Load custom settings contextually
|
||||||
@ -141,16 +132,10 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is called, the user has left the settings screen (potentially through the
|
|
||||||
* home button) and will expect their changes to be persisted. So we kick off an
|
|
||||||
* IntentService which will do so on a background thread.
|
|
||||||
*/
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
if (isFinishing && settingsViewModel.shouldSave) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
NativeConfig.saveSettings()
|
||||||
Settings.saveSettings()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,9 +145,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onSettingsReset() {
|
fun onSettingsReset() {
|
||||||
// Prevents saving to a non-existent settings file
|
|
||||||
settingsViewModel.shouldSave = false
|
|
||||||
|
|
||||||
// Delete settings file because the user may have changed values that do not exist in the UI
|
// Delete settings file because the user may have changed values that do not exist in the UI
|
||||||
NativeConfig.unloadConfig()
|
NativeConfig.unloadConfig()
|
||||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
||||||
@ -194,8 +176,4 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
windowInsets
|
windowInsets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val KEY_SHOULD_SAVE = "should_save"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,6 @@ class SettingsAdapter(
|
|||||||
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
||||||
item.checked = checked
|
item.checked = checked
|
||||||
settingsViewModel.setShouldReloadSettingsList(true)
|
settingsViewModel.setShouldReloadSettingsList(true)
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
||||||
@ -161,7 +160,6 @@ class SettingsAdapter(
|
|||||||
epochTime += timePicker.hour.toLong() * 60 * 60
|
epochTime += timePicker.hour.toLong() * 60 * 60
|
||||||
epochTime += timePicker.minute.toLong() * 60
|
epochTime += timePicker.minute.toLong() * 60
|
||||||
if (item.value != epochTime) {
|
if (item.value != epochTime) {
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
item.value = epochTime
|
item.value = epochTime
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
|
||||||
|
class AddGameFolderDialogFragment : DialogFragment() {
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val binding = DialogAddFolderBinding.inflate(layoutInflater)
|
||||||
|
val folderUriString = requireArguments().getString(FOLDER_URI_STRING)
|
||||||
|
if (folderUriString == null) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
binding.path.text = Uri.parse(folderUriString).path
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.add_game_folder)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
|
||||||
|
gamesViewModel.addFolder(newGameDir)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setView(binding.root)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "AddGameFolderDialogFragment"
|
||||||
|
|
||||||
|
private const val FOLDER_URI_STRING = "FolderUriString"
|
||||||
|
|
||||||
|
fun newInstance(folderUriString: String): AddGameFolderDialogFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(FOLDER_URI_STRING, folderUriString)
|
||||||
|
val fragment = AddGameFolderDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
|
||||||
|
|
||||||
|
class GameFolderPropertiesDialogFragment : DialogFragment() {
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var deepScan = false
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
||||||
|
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
||||||
|
|
||||||
|
// Restore checkbox state
|
||||||
|
binding.deepScanSwitch.isChecked =
|
||||||
|
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
||||||
|
|
||||||
|
// Ensure that we can get the checkbox state even if the view is destroyed
|
||||||
|
deepScan = binding.deepScanSwitch.isChecked
|
||||||
|
binding.deepScanSwitch.setOnClickListener {
|
||||||
|
deepScan = binding.deepScanSwitch.isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(R.string.game_folder_properties)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
||||||
|
if (folderIndex != -1) {
|
||||||
|
gamesViewModel.folders.value[folderIndex].deepScan =
|
||||||
|
binding.deepScanSwitch.isChecked
|
||||||
|
gamesViewModel.updateGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putBoolean(DEEP_SCAN, deepScan)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "GameFolderPropertiesDialogFragment"
|
||||||
|
|
||||||
|
private const val GAME_DIR = "GameDir"
|
||||||
|
|
||||||
|
private const val DEEP_SCAN = "DeepScan"
|
||||||
|
|
||||||
|
fun newInstance(gameDir: GameDir): GameFolderPropertiesDialogFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putParcelable(GAME_DIR, gameDir)
|
||||||
|
val fragment = GameFolderPropertiesDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.adapters.FolderAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
|
|
||||||
|
class GameFoldersFragment : Fragment() {
|
||||||
|
private var _binding: FragmentFoldersBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
|
||||||
|
gamesViewModel.onOpenGameFoldersFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentFoldersBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
binding.toolbarFolders.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.listFolders.apply {
|
||||||
|
layoutManager = GridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
resources.getInteger(R.integer.grid_columns)
|
||||||
|
)
|
||||||
|
adapter = FolderAdapter(requireActivity(), gamesViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
gamesViewModel.folders.collect {
|
||||||
|
(binding.listFolders.adapter as FolderAdapter).submitList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mainActivity = requireActivity() as MainActivity
|
||||||
|
binding.buttonAdd.setOnClickListener {
|
||||||
|
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
gamesViewModel.onCloseGameFoldersFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpToolbar = binding.toolbarFolders.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpToolbar.leftMargin = leftInsets
|
||||||
|
mlpToolbar.rightMargin = rightInsets
|
||||||
|
binding.toolbarFolders.layoutParams = mlpToolbar
|
||||||
|
|
||||||
|
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||||
|
val mlpFab =
|
||||||
|
binding.buttonAdd.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpFab.leftMargin = leftInsets + fabSpacing
|
||||||
|
mlpFab.rightMargin = rightInsets + fabSpacing
|
||||||
|
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
|
||||||
|
binding.buttonAdd.layoutParams = mlpFab
|
||||||
|
|
||||||
|
val mlpListFolders = binding.listFolders.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpListFolders.leftMargin = leftInsets
|
||||||
|
mlpListFolders.rightMargin = rightInsets
|
||||||
|
binding.listFolders.layoutParams = mlpListFolders
|
||||||
|
|
||||||
|
binding.listFolders.updatePadding(
|
||||||
|
bottom = barInsets.bottom +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||||
|
)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
@ -127,18 +127,13 @@ class HomeSettingsFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.select_games_folder,
|
R.string.manage_game_folders,
|
||||||
R.string.select_games_folder_description,
|
R.string.select_games_folder_description,
|
||||||
R.drawable.ic_add,
|
R.drawable.ic_add,
|
||||||
{
|
{
|
||||||
mainActivity.getGamesDirectory.launch(
|
binding.root.findNavController()
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
.navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
|
||||||
)
|
}
|
||||||
},
|
|
||||||
{ true },
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
homeViewModel.gamesDir
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
|
@ -52,7 +52,6 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
|||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
settingsViewModel.clickedItem!!.setting.reset()
|
settingsViewModel.clickedItem!!.setting.reset()
|
||||||
settingsViewModel.setAdapterItemChanged(position)
|
settingsViewModel.setAdapterItemChanged(position)
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.create()
|
.create()
|
||||||
@ -137,24 +136,17 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
|||||||
is SingleChoiceSetting -> {
|
is SingleChoiceSetting -> {
|
||||||
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
||||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||||
if (scSetting.selectedValue != value) {
|
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
|
||||||
scSetting.selectedValue = value
|
scSetting.selectedValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
is StringSingleChoiceSetting -> {
|
is StringSingleChoiceSetting -> {
|
||||||
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
||||||
val value = scSetting.getValueAt(which)
|
val value = scSetting.getValueAt(which)
|
||||||
if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true
|
|
||||||
scSetting.selectedValue = value
|
scSetting.selectedValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
is SliderSetting -> {
|
is SliderSetting -> {
|
||||||
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
||||||
if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) {
|
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
|
||||||
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
|
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage
|
|||||||
import org.yuzu.yuzu_emu.model.StepState
|
import org.yuzu.yuzu_emu.model.StepState
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||||
|
|
||||||
class SetupFragment : Fragment() {
|
class SetupFragment : Fragment() {
|
||||||
@ -184,11 +184,7 @@ class SetupFragment : Fragment() {
|
|||||||
R.string.add_games_warning_description,
|
R.string.add_games_warning_description,
|
||||||
R.string.add_games_warning_help,
|
R.string.add_games_warning_help,
|
||||||
{
|
{
|
||||||
val preferences =
|
if (NativeConfig.getGameDirs().isNotEmpty()) {
|
||||||
PreferenceManager.getDefaultSharedPreferences(
|
|
||||||
YuzuApplication.appContext
|
|
||||||
)
|
|
||||||
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
|
|
||||||
StepState.COMPLETE
|
StepState.COMPLETE
|
||||||
} else {
|
} else {
|
||||||
StepState.INCOMPLETE
|
StepState.INCOMPLETE
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class GameDir(
|
||||||
|
val uriString: String,
|
||||||
|
var deepScan: Boolean
|
||||||
|
) : Parcelable
|
@ -12,6 +12,7 @@ import java.util.Locale
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
@ -20,6 +21,7 @@ import org.yuzu.yuzu_emu.NativeLibrary
|
|||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||||
import org.yuzu.yuzu_emu.utils.GameMetadata
|
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
class GamesViewModel : ViewModel() {
|
class GamesViewModel : ViewModel() {
|
||||||
val games: StateFlow<List<Game>> get() = _games
|
val games: StateFlow<List<Game>> get() = _games
|
||||||
@ -40,6 +42,9 @@ class GamesViewModel : ViewModel() {
|
|||||||
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
||||||
private val _searchFocused = MutableStateFlow(false)
|
private val _searchFocused = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private val _folders = MutableStateFlow(mutableListOf<GameDir>())
|
||||||
|
val folders = _folders.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
@ -50,6 +55,7 @@ class GamesViewModel : ViewModel() {
|
|||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
getGameDirs()
|
||||||
if (storedGames!!.isNotEmpty()) {
|
if (storedGames!!.isNotEmpty()) {
|
||||||
val deserializedGames = mutableSetOf<Game>()
|
val deserializedGames = mutableSetOf<Game>()
|
||||||
storedGames.forEach {
|
storedGames.forEach {
|
||||||
@ -104,7 +110,7 @@ class GamesViewModel : ViewModel() {
|
|||||||
_searchFocused.value = searchFocused
|
_searchFocused.value = searchFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadGames(directoryChanged: Boolean) {
|
fun reloadGames(directoriesChanged: Boolean) {
|
||||||
if (isReloading.value) {
|
if (isReloading.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -116,10 +122,61 @@ class GamesViewModel : ViewModel() {
|
|||||||
setGames(GameHelper.getGames())
|
setGames(GameHelper.getGames())
|
||||||
_isReloading.value = false
|
_isReloading.value = false
|
||||||
|
|
||||||
if (directoryChanged) {
|
if (directoriesChanged) {
|
||||||
setShouldSwapData(true)
|
setShouldSwapData(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addFolder(gameDir: GameDir) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
NativeConfig.addGameDir(gameDir)
|
||||||
|
getGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFolder(gameDir: GameDir) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val gameDirs = _folders.value.toMutableList()
|
||||||
|
val removedDirIndex = gameDirs.indexOf(gameDir)
|
||||||
|
if (removedDirIndex != -1) {
|
||||||
|
gameDirs.removeAt(removedDirIndex)
|
||||||
|
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||||
|
getGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateGameDirs() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
NativeConfig.setGameDirs(_folders.value.toTypedArray())
|
||||||
|
getGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onOpenGameFoldersFragment() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
getGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCloseGameFoldersFragment() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
getGameDirs(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGameDirs(reloadList: Boolean = false) {
|
||||||
|
val gameDirs = NativeConfig.getGameDirs()
|
||||||
|
_folders.value = gameDirs.toMutableList()
|
||||||
|
if (reloadList) {
|
||||||
|
reloadGames(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,9 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.model
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
||||||
@ -23,14 +17,6 @@ class HomeViewModel : ViewModel() {
|
|||||||
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
||||||
private val _shouldPageForward = MutableStateFlow(false)
|
private val _shouldPageForward = MutableStateFlow(false)
|
||||||
|
|
||||||
val gamesDir: StateFlow<String> get() = _gamesDir
|
|
||||||
private val _gamesDir = MutableStateFlow(
|
|
||||||
Uri.parse(
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
||||||
.getString(GameHelper.KEY_GAME_PATH, "")
|
|
||||||
).path ?: ""
|
|
||||||
)
|
|
||||||
|
|
||||||
var navigatedToSetup = false
|
var navigatedToSetup = false
|
||||||
|
|
||||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||||
@ -50,9 +36,4 @@ class HomeViewModel : ViewModel() {
|
|||||||
fun setShouldPageForward(pageForward: Boolean) {
|
fun setShouldPageForward(pageForward: Boolean) {
|
||||||
_shouldPageForward.value = pageForward
|
_shouldPageForward.value = pageForward
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGamesDir(activity: FragmentActivity, dir: String) {
|
|
||||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
|
||||||
_gamesDir.value = dir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
|||||||
class SettingsViewModel : ViewModel() {
|
class SettingsViewModel : ViewModel() {
|
||||||
var game: Game? = null
|
var game: Game? = null
|
||||||
|
|
||||||
var shouldSave = false
|
|
||||||
|
|
||||||
var clickedItem: SettingsItem? = null
|
var clickedItem: SettingsItem? = null
|
||||||
|
|
||||||
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
|
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
|
||||||
@ -73,6 +71,5 @@ class SettingsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
game = null
|
game = null
|
||||||
shouldSave = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ import org.yuzu.yuzu_emu.R
|
|||||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||||
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
|
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||||
import org.yuzu.yuzu_emu.getPublicFilesDir
|
import org.yuzu.yuzu_emu.getPublicFilesDir
|
||||||
@ -252,6 +253,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
NativeConfig.saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
EmulationActivity.stopForegroundService(this)
|
EmulationActivity.stopForegroundService(this)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
@ -293,20 +301,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
)
|
)
|
||||||
|
|
||||||
// When a new directory is picked, we currently will reset the existing games
|
val uriString = result.toString()
|
||||||
// database. This effectively means that only one game directory is supported.
|
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
||||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
|
if (folder != null) {
|
||||||
.putString(GameHelper.KEY_GAME_PATH, result.toString())
|
Toast.makeText(
|
||||||
.apply()
|
applicationContext,
|
||||||
|
R.string.folder_already_added,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Toast.makeText(
|
AddGameFolderDialogFragment.newInstance(uriString)
|
||||||
applicationContext,
|
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
||||||
R.string.games_dir_selected,
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
|
|
||||||
gamesViewModel.reloadGames(true)
|
|
||||||
homeViewModel.setGamesDir(this, result.path!!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val getProdKey =
|
val getProdKey =
|
||||||
|
@ -364,6 +364,27 @@ object FileUtil {
|
|||||||
.lowercase()
|
.lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isTreeUriValid(uri: Uri): Boolean {
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val columns = arrayOf(
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||||
|
)
|
||||||
|
return try {
|
||||||
|
val docId: String = if (isRootTreeUri(uri)) {
|
||||||
|
DocumentsContract.getTreeDocumentId(uri)
|
||||||
|
} else {
|
||||||
|
DocumentsContract.getDocumentId(uri)
|
||||||
|
}
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
|
||||||
|
resolver.query(childrenUri, columns, null, null, null)
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getStringFromFile(file: File): String =
|
fun getStringFromFile(file: File): String =
|
||||||
String(file.readBytes(), StandardCharsets.UTF_8)
|
String(file.readBytes(), StandardCharsets.UTF_8)
|
||||||
|
@ -11,10 +11,11 @@ import kotlinx.serialization.json.Json
|
|||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||||
|
|
||||||
object GameHelper {
|
object GameHelper {
|
||||||
const val KEY_GAME_PATH = "game_path"
|
private const val KEY_OLD_GAME_PATH = "game_path"
|
||||||
const val KEY_GAMES = "Games"
|
const val KEY_GAMES = "Games"
|
||||||
|
|
||||||
private lateinit var preferences: SharedPreferences
|
private lateinit var preferences: SharedPreferences
|
||||||
@ -22,15 +23,43 @@ object GameHelper {
|
|||||||
fun getGames(): List<Game> {
|
fun getGames(): List<Game> {
|
||||||
val games = mutableListOf<Game>()
|
val games = mutableListOf<Game>()
|
||||||
val context = YuzuApplication.appContext
|
val context = YuzuApplication.appContext
|
||||||
val gamesDir =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
|
|
||||||
val gamesUri = Uri.parse(gamesDir)
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val gameDirs = mutableListOf<GameDir>()
|
||||||
|
val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: ""
|
||||||
|
if (oldGamesDir.isNotEmpty()) {
|
||||||
|
gameDirs.add(GameDir(oldGamesDir, true))
|
||||||
|
preferences.edit().remove(KEY_OLD_GAME_PATH).apply()
|
||||||
|
}
|
||||||
|
gameDirs.addAll(NativeConfig.getGameDirs())
|
||||||
|
|
||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
|
|
||||||
addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
|
val badDirs = mutableListOf<Int>()
|
||||||
|
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||||
|
val gameDirUri = Uri.parse(gameDir.uriString)
|
||||||
|
val isValid = FileUtil.isTreeUriValid(gameDirUri)
|
||||||
|
if (isValid) {
|
||||||
|
addGamesRecursive(
|
||||||
|
games,
|
||||||
|
FileUtil.listFiles(gameDirUri),
|
||||||
|
if (gameDir.deepScan) 3 else 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
badDirs.add(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all game dirs with insufficient permissions from config
|
||||||
|
if (badDirs.isNotEmpty()) {
|
||||||
|
var offset = 0
|
||||||
|
badDirs.forEach {
|
||||||
|
gameDirs.removeAt(it - offset)
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
// Cache list of games found on disk
|
// Cache list of games found on disk
|
||||||
val serializedGames = mutableSetOf<String>()
|
val serializedGames = mutableSetOf<String>()
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
|
||||||
object NativeConfig {
|
object NativeConfig {
|
||||||
/**
|
/**
|
||||||
* Creates a Config object and opens the emulation config.
|
* Creates a Config object and opens the emulation config.
|
||||||
@ -54,4 +56,22 @@ object NativeConfig {
|
|||||||
external fun getConfigHeader(category: Int): String
|
external fun getConfigHeader(category: Int): String
|
||||||
|
|
||||||
external fun getPairedSettingKey(key: String): String
|
external fun getPairedSettingKey(key: String): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets every [GameDir] in AndroidSettings::values.game_dirs
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun getGameDirs(): Array<GameDir>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the AndroidSettings::values.game_dirs array and replaces them with the provided array
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun setGameDirs(dirs: Array<GameDir>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a single [GameDir] to the AndroidSettings::values.game_dirs array
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun addGameDir(dir: GameDir)
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ void AndroidConfig::SaveAllValues() {
|
|||||||
void AndroidConfig::ReadAndroidValues() {
|
void AndroidConfig::ReadAndroidValues() {
|
||||||
if (global) {
|
if (global) {
|
||||||
ReadAndroidUIValues();
|
ReadAndroidUIValues();
|
||||||
|
ReadUIValues();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,9 +46,35 @@ void AndroidConfig::ReadAndroidUIValues() {
|
|||||||
EndGroup();
|
EndGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AndroidConfig::ReadUIValues() {
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
|
||||||
|
|
||||||
|
ReadPathValues();
|
||||||
|
|
||||||
|
EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidConfig::ReadPathValues() {
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
||||||
|
|
||||||
|
const int gamedirs_size = BeginArray(std::string("gamedirs"));
|
||||||
|
for (int i = 0; i < gamedirs_size; ++i) {
|
||||||
|
SetArrayIndex(i);
|
||||||
|
AndroidSettings::GameDir game_dir;
|
||||||
|
game_dir.path = ReadStringSetting(std::string("path"));
|
||||||
|
game_dir.deep_scan =
|
||||||
|
ReadBooleanSetting(std::string("deep_scan"), std::make_optional(false));
|
||||||
|
AndroidSettings::values.game_dirs.push_back(game_dir);
|
||||||
|
}
|
||||||
|
EndArray();
|
||||||
|
|
||||||
|
EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
void AndroidConfig::SaveAndroidValues() {
|
void AndroidConfig::SaveAndroidValues() {
|
||||||
if (global) {
|
if (global) {
|
||||||
SaveAndroidUIValues();
|
SaveAndroidUIValues();
|
||||||
|
SaveUIValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteToIni();
|
WriteToIni();
|
||||||
@ -61,6 +88,29 @@ void AndroidConfig::SaveAndroidUIValues() {
|
|||||||
EndGroup();
|
EndGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AndroidConfig::SaveUIValues() {
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
|
||||||
|
|
||||||
|
SavePathValues();
|
||||||
|
|
||||||
|
EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidConfig::SavePathValues() {
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
||||||
|
|
||||||
|
BeginArray(std::string("gamedirs"));
|
||||||
|
for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
|
||||||
|
SetArrayIndex(i);
|
||||||
|
const auto& game_dir = AndroidSettings::values.game_dirs[i];
|
||||||
|
WriteSetting(std::string("path"), game_dir.path);
|
||||||
|
WriteSetting(std::string("deep_scan"), game_dir.deep_scan, std::make_optional(false));
|
||||||
|
}
|
||||||
|
EndArray();
|
||||||
|
|
||||||
|
EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
|
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
|
||||||
auto& map = Settings::values.linkage.by_category;
|
auto& map = Settings::values.linkage.by_category;
|
||||||
if (map.contains(category)) {
|
if (map.contains(category)) {
|
||||||
|
@ -19,9 +19,9 @@ protected:
|
|||||||
void ReadAndroidUIValues();
|
void ReadAndroidUIValues();
|
||||||
void ReadHidbusValues() override {}
|
void ReadHidbusValues() override {}
|
||||||
void ReadDebugControlValues() override {}
|
void ReadDebugControlValues() override {}
|
||||||
void ReadPathValues() override {}
|
void ReadPathValues() override;
|
||||||
void ReadShortcutValues() override {}
|
void ReadShortcutValues() override {}
|
||||||
void ReadUIValues() override {}
|
void ReadUIValues() override;
|
||||||
void ReadUIGamelistValues() override {}
|
void ReadUIGamelistValues() override {}
|
||||||
void ReadUILayoutValues() override {}
|
void ReadUILayoutValues() override {}
|
||||||
void ReadMultiplayerValues() override {}
|
void ReadMultiplayerValues() override {}
|
||||||
@ -30,9 +30,9 @@ protected:
|
|||||||
void SaveAndroidUIValues();
|
void SaveAndroidUIValues();
|
||||||
void SaveHidbusValues() override {}
|
void SaveHidbusValues() override {}
|
||||||
void SaveDebugControlValues() override {}
|
void SaveDebugControlValues() override {}
|
||||||
void SavePathValues() override {}
|
void SavePathValues() override;
|
||||||
void SaveShortcutValues() override {}
|
void SaveShortcutValues() override {}
|
||||||
void SaveUIValues() override {}
|
void SaveUIValues() override;
|
||||||
void SaveUIGamelistValues() override {}
|
void SaveUIGamelistValues() override {}
|
||||||
void SaveUILayoutValues() override {}
|
void SaveUILayoutValues() override {}
|
||||||
void SaveMultiplayerValues() override {}
|
void SaveMultiplayerValues() override {}
|
||||||
|
@ -9,9 +9,17 @@
|
|||||||
|
|
||||||
namespace AndroidSettings {
|
namespace AndroidSettings {
|
||||||
|
|
||||||
|
struct GameDir {
|
||||||
|
std::string path;
|
||||||
|
bool deep_scan = false;
|
||||||
|
};
|
||||||
|
|
||||||
struct Values {
|
struct Values {
|
||||||
Settings::Linkage linkage;
|
Settings::Linkage linkage;
|
||||||
|
|
||||||
|
// Path settings
|
||||||
|
std::vector<GameDir> game_dirs;
|
||||||
|
|
||||||
// Android
|
// Android
|
||||||
Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",
|
Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",
|
||||||
Settings::Category::Android};
|
Settings::Category::Android};
|
||||||
|
@ -13,6 +13,8 @@ static JavaVM* s_java_vm;
|
|||||||
static jclass s_native_library_class;
|
static jclass s_native_library_class;
|
||||||
static jclass s_disk_cache_progress_class;
|
static jclass s_disk_cache_progress_class;
|
||||||
static jclass s_load_callback_stage_class;
|
static jclass s_load_callback_stage_class;
|
||||||
|
static jclass s_game_dir_class;
|
||||||
|
static jmethodID s_game_dir_constructor;
|
||||||
static jmethodID s_exit_emulation_activity;
|
static jmethodID s_exit_emulation_activity;
|
||||||
static jmethodID s_disk_cache_load_progress;
|
static jmethodID s_disk_cache_load_progress;
|
||||||
static jmethodID s_on_emulation_started;
|
static jmethodID s_on_emulation_started;
|
||||||
@ -53,6 +55,14 @@ jclass GetDiskCacheLoadCallbackStageClass() {
|
|||||||
return s_load_callback_stage_class;
|
return s_load_callback_stage_class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetGameDirClass() {
|
||||||
|
return s_game_dir_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetGameDirConstructor() {
|
||||||
|
return s_game_dir_constructor;
|
||||||
|
}
|
||||||
|
|
||||||
jmethodID GetExitEmulationActivity() {
|
jmethodID GetExitEmulationActivity() {
|
||||||
return s_exit_emulation_activity;
|
return s_exit_emulation_activity;
|
||||||
}
|
}
|
||||||
@ -90,6 +100,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|||||||
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
||||||
"org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
"org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
||||||
|
|
||||||
|
const jclass game_dir_class = env->FindClass("org/yuzu/yuzu_emu/model/GameDir");
|
||||||
|
s_game_dir_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_dir_class));
|
||||||
|
s_game_dir_constructor = env->GetMethodID(game_dir_class, "<init>", "(Ljava/lang/String;Z)V");
|
||||||
|
env->DeleteLocalRef(game_dir_class);
|
||||||
|
|
||||||
// Initialize methods
|
// Initialize methods
|
||||||
s_exit_emulation_activity =
|
s_exit_emulation_activity =
|
||||||
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
||||||
@ -120,6 +135,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
|||||||
env->DeleteGlobalRef(s_native_library_class);
|
env->DeleteGlobalRef(s_native_library_class);
|
||||||
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
||||||
env->DeleteGlobalRef(s_load_callback_stage_class);
|
env->DeleteGlobalRef(s_load_callback_stage_class);
|
||||||
|
env->DeleteGlobalRef(s_game_dir_class);
|
||||||
|
|
||||||
// UnInitialize applets
|
// UnInitialize applets
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
|
@ -13,6 +13,8 @@ JNIEnv* GetEnvForThread();
|
|||||||
jclass GetNativeLibraryClass();
|
jclass GetNativeLibraryClass();
|
||||||
jclass GetDiskCacheProgressClass();
|
jclass GetDiskCacheProgressClass();
|
||||||
jclass GetDiskCacheLoadCallbackStageClass();
|
jclass GetDiskCacheLoadCallbackStageClass();
|
||||||
|
jclass GetGameDirClass();
|
||||||
|
jmethodID GetGameDirConstructor();
|
||||||
jmethodID GetExitEmulationActivity();
|
jmethodID GetExitEmulationActivity();
|
||||||
jmethodID GetDiskCacheLoadProgress();
|
jmethodID GetDiskCacheLoadProgress();
|
||||||
jmethodID GetOnEmulationStarted();
|
jmethodID GetOnEmulationStarted();
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "frontend_common/config.h"
|
#include "frontend_common/config.h"
|
||||||
#include "jni/android_common/android_common.h"
|
#include "jni/android_common/android_common.h"
|
||||||
|
#include "jni/id_cache.h"
|
||||||
|
|
||||||
std::unique_ptr<AndroidConfig> config;
|
std::unique_ptr<AndroidConfig> config;
|
||||||
|
|
||||||
@ -253,4 +254,55 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e
|
|||||||
return ToJString(env, setting->PairedSetting()->GetLabel());
|
return ToJString(env, setting->PairedSetting()->GetLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) {
|
||||||
|
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||||
|
jmethodID gameDirConstructor = IDCache::GetGameDirConstructor();
|
||||||
|
jobjectArray jgameDirArray =
|
||||||
|
env->NewObjectArray(AndroidSettings::values.game_dirs.size(), gameDirClass, nullptr);
|
||||||
|
for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
|
||||||
|
jobject jgameDir =
|
||||||
|
env->NewObject(gameDirClass, gameDirConstructor,
|
||||||
|
ToJString(env, AndroidSettings::values.game_dirs[i].path),
|
||||||
|
static_cast<jboolean>(AndroidSettings::values.game_dirs[i].deep_scan));
|
||||||
|
env->SetObjectArrayElement(jgameDirArray, i, jgameDir);
|
||||||
|
}
|
||||||
|
return jgameDirArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGameDirs(JNIEnv* env, jobject obj,
|
||||||
|
jobjectArray gameDirs) {
|
||||||
|
AndroidSettings::values.game_dirs.clear();
|
||||||
|
int size = env->GetArrayLength(gameDirs);
|
||||||
|
|
||||||
|
if (size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobject dir = env->GetObjectArrayElement(gameDirs, 0);
|
||||||
|
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||||
|
jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;");
|
||||||
|
jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z");
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
dir = env->GetObjectArrayElement(gameDirs, i);
|
||||||
|
jstring juriString = static_cast<jstring>(env->GetObjectField(dir, uriStringField));
|
||||||
|
jboolean jdeepScanBoolean = env->GetBooleanField(dir, deepScanBooleanField);
|
||||||
|
std::string uriString = GetJString(env, juriString);
|
||||||
|
AndroidSettings::values.game_dirs.push_back(
|
||||||
|
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject obj,
|
||||||
|
jobject gameDir) {
|
||||||
|
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||||
|
jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;");
|
||||||
|
jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z");
|
||||||
|
|
||||||
|
jstring juriString = static_cast<jstring>(env->GetObjectField(gameDir, uriStringField));
|
||||||
|
jboolean jdeepScanBoolean = env->GetBooleanField(gameDir, deepScanBooleanField);
|
||||||
|
std::string uriString = GetJString(env, juriString);
|
||||||
|
AndroidSettings::values.game_dirs.push_back(
|
||||||
|
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
70
src/android/app/src/main/res/layout/card_folder.xml
Normal file
70
src/android/app/src/main/res/layout/card_folder.xml
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView 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"
|
||||||
|
style="?attr/materialCardViewOutlinedStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/path"
|
||||||
|
style="@style/TextAppearance.Material3.BodyLarge"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@string/select_gpu_driver_default" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_edit"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/delete"
|
||||||
|
android:tooltipText="@string/edit"
|
||||||
|
app:icon="@drawable/ic_edit"
|
||||||
|
app:iconTint="?attr/colorControlNormal" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_delete"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/delete"
|
||||||
|
android:tooltipText="@string/delete"
|
||||||
|
app:icon="@drawable/ic_delete"
|
||||||
|
app:iconTint="?attr/colorControlNormal" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
45
src/android/app/src/main/res/layout/dialog_add_folder.xml
Normal file
45
src/android/app/src/main/res/layout/dialog_add_folder.xml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/path"
|
||||||
|
style="@style/TextAppearance.Material3.BodyLarge"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
tools:text="folder/folder/folder/folder" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/deep_scan"
|
||||||
|
android:textAlignment="viewStart" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/deep_scan_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/deep_scan_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/deep_scan"
|
||||||
|
android:textAlignment="viewStart" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/deep_scan_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
48
src/android/app/src/main/res/layout/fragment_folders.xml
Normal file
48
src/android/app/src/main/res/layout/fragment_folders.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/coordinator_folders"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar_folders"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:liftOnScrollTargetViewId="@id/list_folders">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_folders"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="@string/game_folders" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list_folders"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/button_add"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:contentDescription="@string/add_games"
|
||||||
|
app:srcCompat="@drawable/ic_add"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -28,6 +28,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
|
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
|
||||||
app:destination="@id/appletLauncherFragment" />
|
app:destination="@id/appletLauncherFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
|
||||||
|
app:destination="@id/gameFoldersFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
@ -117,5 +120,9 @@
|
|||||||
android:id="@+id/cabinetLauncherDialogFragment"
|
android:id="@+id/cabinetLauncherDialogFragment"
|
||||||
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
|
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
|
||||||
android:label="CabinetLauncherDialogFragment" />
|
android:label="CabinetLauncherDialogFragment" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/gameFoldersFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
|
||||||
|
android:label="GameFoldersFragment" />
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<dimen name="menu_width">256dp</dimen>
|
<dimen name="menu_width">256dp</dimen>
|
||||||
<dimen name="card_width">165dp</dimen>
|
<dimen name="card_width">165dp</dimen>
|
||||||
<dimen name="icon_inset">24dp</dimen>
|
<dimen name="icon_inset">24dp</dimen>
|
||||||
<dimen name="spacing_bottom_list_fab">72dp</dimen>
|
<dimen name="spacing_bottom_list_fab">76dp</dimen>
|
||||||
<dimen name="spacing_fab">24dp</dimen>
|
<dimen name="spacing_fab">24dp</dimen>
|
||||||
|
|
||||||
<dimen name="dialog_margin">20dp</dimen>
|
<dimen name="dialog_margin">20dp</dimen>
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
||||||
<string name="search_and_filter_games">Search and filter games</string>
|
<string name="search_and_filter_games">Search and filter games</string>
|
||||||
<string name="select_games_folder">Select games folder</string>
|
<string name="select_games_folder">Select games folder</string>
|
||||||
|
<string name="manage_game_folders">Manage game folders</string>
|
||||||
<string name="select_games_folder_description">Allows yuzu to populate the games list</string>
|
<string name="select_games_folder_description">Allows yuzu to populate the games list</string>
|
||||||
<string name="add_games_warning">Skip selecting games folder?</string>
|
<string name="add_games_warning">Skip selecting games folder?</string>
|
||||||
<string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
|
<string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
|
||||||
@ -124,6 +125,11 @@
|
|||||||
<string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
|
<string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
|
||||||
<string name="share_save_file">Share save file</string>
|
<string name="share_save_file">Share save file</string>
|
||||||
<string name="export_save_failed">Failed to export save</string>
|
<string name="export_save_failed">Failed to export save</string>
|
||||||
|
<string name="game_folders">Game folders</string>
|
||||||
|
<string name="deep_scan">Deep scan</string>
|
||||||
|
<string name="add_game_folder">Add game folder</string>
|
||||||
|
<string name="folder_already_added">This folder was already added!</string>
|
||||||
|
<string name="game_folder_properties">Game folder properties</string>
|
||||||
|
|
||||||
<!-- Applet launcher strings -->
|
<!-- Applet launcher strings -->
|
||||||
<string name="applets">Applet launcher</string>
|
<string name="applets">Applet launcher</string>
|
||||||
@ -258,6 +264,7 @@
|
|||||||
<string name="cancelling">Cancelling</string>
|
<string name="cancelling">Cancelling</string>
|
||||||
<string name="install">Install</string>
|
<string name="install">Install</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
|
<string name="edit">Edit</string>
|
||||||
<string name="export_success">Exported successfully</string>
|
<string name="export_success">Exported successfully</string>
|
||||||
|
|
||||||
<!-- GPU driver installation -->
|
<!-- GPU driver installation -->
|
||||||
|
@ -199,7 +199,7 @@ if(ARCHITECTURE_x86_64)
|
|||||||
target_link_libraries(common PRIVATE xbyak::xbyak)
|
target_link_libraries(common PRIVATE xbyak::xbyak)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (ARCHITECTURE_arm64 AND (ANDROID OR LINUX))
|
if (HAS_NCE)
|
||||||
target_sources(common
|
target_sources(common
|
||||||
PRIVATE
|
PRIVATE
|
||||||
arm64/native_clock.cpp
|
arm64/native_clock.cpp
|
||||||
|
@ -182,7 +182,7 @@ struct Values {
|
|||||||
// Cpu
|
// Cpu
|
||||||
SwitchableSetting<CpuBackend, true> cpu_backend{
|
SwitchableSetting<CpuBackend, true> cpu_backend{
|
||||||
linkage, CpuBackend::Dynarmic, CpuBackend::Dynarmic,
|
linkage, CpuBackend::Dynarmic, CpuBackend::Dynarmic,
|
||||||
#ifdef ARCHITECTURE_arm64
|
#ifdef HAS_NCE
|
||||||
CpuBackend::Nce,
|
CpuBackend::Nce,
|
||||||
#else
|
#else
|
||||||
CpuBackend::Dynarmic,
|
CpuBackend::Dynarmic,
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
#include "common/x64/rdtsc.h"
|
#include "common/x64/rdtsc.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(ARCHITECTURE_arm64) && defined(__linux__)
|
#ifdef HAS_NCE
|
||||||
#include "common/arm64/native_clock.h"
|
#include "common/arm64/native_clock.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ std::unique_ptr<WallClock> CreateOptimalClock() {
|
|||||||
// - Is not more precise than 1 GHz (1ns resolution)
|
// - Is not more precise than 1 GHz (1ns resolution)
|
||||||
return std::make_unique<StandardWallClock>();
|
return std::make_unique<StandardWallClock>();
|
||||||
}
|
}
|
||||||
#elif defined(ARCHITECTURE_arm64) && defined(__linux__)
|
#elif defined(HAS_NCE)
|
||||||
return std::make_unique<Arm64::NativeClock>();
|
return std::make_unique<Arm64::NativeClock>();
|
||||||
#else
|
#else
|
||||||
return std::make_unique<StandardWallClock>();
|
return std::make_unique<StandardWallClock>();
|
||||||
|
@ -926,8 +926,7 @@ if (ENABLE_WEB_SERVICE)
|
|||||||
target_link_libraries(core PRIVATE web_service)
|
target_link_libraries(core PRIVATE web_service)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (ARCHITECTURE_arm64 AND (ANDROID OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux"))
|
if (HAS_NCE)
|
||||||
target_compile_definitions(core PRIVATE -DHAS_NCE)
|
|
||||||
enable_language(C ASM)
|
enable_language(C ASM)
|
||||||
set(CMAKE_ASM_FLAGS "${CFLAGS} -x assembler-with-cpp")
|
set(CMAKE_ASM_FLAGS "${CFLAGS} -x assembler-with-cpp")
|
||||||
|
|
||||||
|
@ -167,6 +167,11 @@ protected:
|
|||||||
*/
|
*/
|
||||||
std::pair<f32, f32> MapToTouchScreen(u32 framebuffer_x, u32 framebuffer_y) const;
|
std::pair<f32, f32> MapToTouchScreen(u32 framebuffer_x, u32 framebuffer_y) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clip the provided coordinates to be inside the touchscreen area.
|
||||||
|
*/
|
||||||
|
std::pair<u32, u32> ClipToTouchScreen(u32 new_x, u32 new_y) const;
|
||||||
|
|
||||||
WindowSystemInfo window_info;
|
WindowSystemInfo window_info;
|
||||||
|
|
||||||
bool strict_context_required = false;
|
bool strict_context_required = false;
|
||||||
@ -181,11 +186,6 @@ private:
|
|||||||
// By default, ignore this request and do nothing.
|
// By default, ignore this request and do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clip the provided coordinates to be inside the touchscreen area.
|
|
||||||
*/
|
|
||||||
std::pair<u32, u32> ClipToTouchScreen(u32 new_x, u32 new_y) const;
|
|
||||||
|
|
||||||
Layout::FramebufferLayout framebuffer_layout; ///< Current framebuffer layout
|
Layout::FramebufferLayout framebuffer_layout; ///< Current framebuffer layout
|
||||||
|
|
||||||
u32 client_area_width; ///< Current client width, should be set by window impl.
|
u32 client_area_width; ///< Current client width, should be set by window impl.
|
||||||
|
@ -131,7 +131,7 @@ void Cabinet::DisplayCompleted(bool apply_changes, std::string_view amiibo_name)
|
|||||||
nfp_device->DeleteApplicationArea();
|
nfp_device->DeleteApplicationArea();
|
||||||
break;
|
break;
|
||||||
case Service::NFP::CabinetMode::StartRestorer:
|
case Service::NFP::CabinetMode::StartRestorer:
|
||||||
nfp_device->RestoreAmiibo();
|
nfp_device->Restore();
|
||||||
break;
|
break;
|
||||||
case Service::NFP::CabinetMode::StartFormatter:
|
case Service::NFP::CabinetMode::StartFormatter:
|
||||||
nfp_device->Format();
|
nfp_device->Format();
|
||||||
|
@ -98,7 +98,7 @@ void Ver3StoreData::BuildToStoreData(StoreData& out_store_data) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Ver3StoreData::BuildFromStoreData(const StoreData& store_data) {
|
void Ver3StoreData::BuildFromStoreData(const StoreData& store_data) {
|
||||||
version = 1;
|
version = 3;
|
||||||
mii_information.gender.Assign(static_cast<u8>(store_data.GetGender()));
|
mii_information.gender.Assign(static_cast<u8>(store_data.GetGender()));
|
||||||
mii_information.favorite_color.Assign(static_cast<u8>(store_data.GetFavoriteColor()));
|
mii_information.favorite_color.Assign(static_cast<u8>(store_data.GetFavoriteColor()));
|
||||||
height = store_data.GetHeight();
|
height = store_data.GetHeight();
|
||||||
|
@ -401,6 +401,12 @@ Result NfcDevice::SendCommandByPassThrough(const Time::Clock::TimeSpanType& time
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target_) {
|
Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target_) {
|
||||||
|
bool is_corrupted = false;
|
||||||
|
|
||||||
|
if (model_type != NFP::ModelType::Amiibo) {
|
||||||
|
return ResultInvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
if (device_state != DeviceState::TagFound) {
|
if (device_state != DeviceState::TagFound) {
|
||||||
LOG_ERROR(Service_NFP, "Wrong device state {}", device_state);
|
LOG_ERROR(Service_NFP, "Wrong device state {}", device_state);
|
||||||
return ResultWrongDeviceState;
|
return ResultWrongDeviceState;
|
||||||
@ -420,26 +426,32 @@ Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target
|
|||||||
if (is_plain_amiibo) {
|
if (is_plain_amiibo) {
|
||||||
std::vector<u8> data(sizeof(NFP::NTAG215File));
|
std::vector<u8> data(sizeof(NFP::NTAG215File));
|
||||||
memcpy(data.data(), &tag_data, sizeof(tag_data));
|
memcpy(data.data(), &tag_data, sizeof(tag_data));
|
||||||
WriteBackupData(tag_data.uid, data);
|
|
||||||
|
|
||||||
device_state = DeviceState::TagMounted;
|
|
||||||
mount_target = mount_target_;
|
|
||||||
return ResultSuccess;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!NFP::AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data)) {
|
if (!is_plain_amiibo && !NFP::AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data)) {
|
||||||
bool has_backup = HasBackup(encrypted_tag_data.uuid).IsSuccess();
|
LOG_ERROR(Service_NFP, "Can't decode amiibo");
|
||||||
LOG_ERROR(Service_NFP, "Can't decode amiibo, has_backup= {}", has_backup);
|
is_corrupted = true;
|
||||||
return has_backup ? ResultCorruptedDataWithBackup : ResultCorruptedData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<u8> data(sizeof(NFP::EncryptedNTAG215File));
|
if (tag_data.settings.settings.amiibo_initialized && !tag_data.owner_mii.IsValid()) {
|
||||||
memcpy(data.data(), &encrypted_tag_data, sizeof(encrypted_tag_data));
|
LOG_ERROR(Service_NFP, "Invalid mii data");
|
||||||
WriteBackupData(encrypted_tag_data.uuid, data);
|
is_corrupted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_corrupted) {
|
||||||
|
std::vector<u8> data(sizeof(NFP::EncryptedNTAG215File));
|
||||||
|
memcpy(data.data(), &encrypted_tag_data, sizeof(encrypted_tag_data));
|
||||||
|
WriteBackupData(encrypted_tag_data.uuid, data);
|
||||||
|
}
|
||||||
|
|
||||||
device_state = DeviceState::TagMounted;
|
device_state = DeviceState::TagMounted;
|
||||||
mount_target = mount_target_;
|
mount_target = mount_target_;
|
||||||
|
|
||||||
|
if (is_corrupted) {
|
||||||
|
bool has_backup = HasBackup(encrypted_tag_data.uuid).IsSuccess();
|
||||||
|
return has_backup ? ResultCorruptedDataWithBackup : ResultCorruptedData;
|
||||||
|
}
|
||||||
|
|
||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -606,6 +618,17 @@ Result NfcDevice::Restore() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore mii data in case is corrupted by previous instances of yuzu
|
||||||
|
if (tag_data.settings.settings.amiibo_initialized && !tag_data.owner_mii.IsValid()) {
|
||||||
|
LOG_ERROR(Service_NFP, "Regenerating mii data");
|
||||||
|
Mii::StoreData new_mii{};
|
||||||
|
new_mii.BuildRandom(Mii::Age::All, Mii::Gender::All, Mii::Race::All);
|
||||||
|
new_mii.SetNickname({u'y', u'u', u'z', u'u', u'\0'});
|
||||||
|
|
||||||
|
tag_data.owner_mii.BuildFromStoreData(new_mii);
|
||||||
|
tag_data.mii_extension.SetFromStoreData(new_mii);
|
||||||
|
}
|
||||||
|
|
||||||
// Overwrite tag contents with backup and mount the tag
|
// Overwrite tag contents with backup and mount the tag
|
||||||
tag_data = temporary_tag_data;
|
tag_data = temporary_tag_data;
|
||||||
encrypted_tag_data = temporary_encrypted_tag_data;
|
encrypted_tag_data = temporary_encrypted_tag_data;
|
||||||
@ -851,25 +874,6 @@ Result NfcDevice::SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& registe
|
|||||||
return Flush();
|
return Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result NfcDevice::RestoreAmiibo() {
|
|
||||||
if (device_state != DeviceState::TagMounted) {
|
|
||||||
LOG_ERROR(Service_NFP, "Wrong device state {}", device_state);
|
|
||||||
if (device_state == DeviceState::TagRemoved) {
|
|
||||||
return ResultTagRemoved;
|
|
||||||
}
|
|
||||||
return ResultWrongDeviceState;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mount_target == NFP::MountTarget::None || mount_target == NFP::MountTarget::Rom) {
|
|
||||||
LOG_ERROR(Service_NFP, "Amiibo is read only", device_state);
|
|
||||||
return ResultWrongDeviceState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Load amiibo from backup on system
|
|
||||||
LOG_ERROR(Service_NFP, "Not Implemented");
|
|
||||||
return ResultSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result NfcDevice::Format() {
|
Result NfcDevice::Format() {
|
||||||
Result result = ResultSuccess;
|
Result result = ResultSuccess;
|
||||||
|
|
||||||
@ -877,7 +881,9 @@ Result NfcDevice::Format() {
|
|||||||
result = Mount(NFP::ModelType::Amiibo, NFP::MountTarget::All);
|
result = Mount(NFP::ModelType::Amiibo, NFP::MountTarget::All);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.IsError()) {
|
// We are formatting all data. Corruption is not an issue.
|
||||||
|
if (result.IsError() &&
|
||||||
|
(result != ResultCorruptedData && result != ResultCorruptedDataWithBackup)) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,6 @@ public:
|
|||||||
|
|
||||||
Result DeleteRegisterInfo();
|
Result DeleteRegisterInfo();
|
||||||
Result SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& register_info);
|
Result SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& register_info);
|
||||||
Result RestoreAmiibo();
|
|
||||||
Result Format();
|
Result Format();
|
||||||
|
|
||||||
Result OpenApplicationArea(u32 access_id);
|
Result OpenApplicationArea(u32 access_id);
|
||||||
|
@ -924,12 +924,14 @@ std::string Config::AdjustOutputString(const std::string& string) {
|
|||||||
|
|
||||||
// Windows requires that two forward slashes are used at the start of a path for unmapped
|
// Windows requires that two forward slashes are used at the start of a path for unmapped
|
||||||
// network drives so we have to watch for that here
|
// network drives so we have to watch for that here
|
||||||
|
#ifndef ANDROID
|
||||||
if (string.substr(0, 2) == "//") {
|
if (string.substr(0, 2) == "//") {
|
||||||
boost::replace_all(adjusted_string, "//", "/");
|
boost::replace_all(adjusted_string, "//", "/");
|
||||||
adjusted_string.insert(0, "/");
|
adjusted_string.insert(0, "/");
|
||||||
} else {
|
} else {
|
||||||
boost::replace_all(adjusted_string, "//", "/");
|
boost::replace_all(adjusted_string, "//", "/");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Needed for backwards compatibility with QSettings deserialization
|
// Needed for backwards compatibility with QSettings deserialization
|
||||||
for (const auto& special_character : special_characters) {
|
for (const auto& special_character : special_characters) {
|
||||||
|
@ -30,7 +30,6 @@
|
|||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
#include <QSurfaceFormat>
|
#include <QSurfaceFormat>
|
||||||
#include <QTimer>
|
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
#include <QtCore/qobjectdefs.h>
|
#include <QtCore/qobjectdefs.h>
|
||||||
|
|
||||||
@ -66,6 +65,8 @@ class QObject;
|
|||||||
class QPaintEngine;
|
class QPaintEngine;
|
||||||
class QSurface;
|
class QSurface;
|
||||||
|
|
||||||
|
constexpr int default_mouse_constrain_timeout = 10;
|
||||||
|
|
||||||
EmuThread::EmuThread(Core::System& system) : m_system{system} {}
|
EmuThread::EmuThread(Core::System& system) : m_system{system} {}
|
||||||
|
|
||||||
EmuThread::~EmuThread() = default;
|
EmuThread::~EmuThread() = default;
|
||||||
@ -304,6 +305,9 @@ GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_,
|
|||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
connect(this, &GRenderWindow::ExitSignal, parent, &GMainWindow::OnExit, Qt::QueuedConnection);
|
connect(this, &GRenderWindow::ExitSignal, parent, &GMainWindow::OnExit, Qt::QueuedConnection);
|
||||||
connect(this, &GRenderWindow::TasPlaybackStateChanged, parent, &GMainWindow::OnTasStateChanged);
|
connect(this, &GRenderWindow::TasPlaybackStateChanged, parent, &GMainWindow::OnTasStateChanged);
|
||||||
|
|
||||||
|
mouse_constrain_timer.setInterval(default_mouse_constrain_timeout);
|
||||||
|
connect(&mouse_constrain_timer, &QTimer::timeout, this, &GRenderWindow::ConstrainMouse);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GRenderWindow::ExecuteProgram(std::size_t program_index) {
|
void GRenderWindow::ExecuteProgram(std::size_t program_index) {
|
||||||
@ -393,6 +397,22 @@ void GRenderWindow::closeEvent(QCloseEvent* event) {
|
|||||||
QWidget::closeEvent(event);
|
QWidget::closeEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GRenderWindow::leaveEvent(QEvent* event) {
|
||||||
|
if (Settings::values.mouse_panning) {
|
||||||
|
const QRect& rect = QWidget::geometry();
|
||||||
|
QPoint position = QCursor::pos();
|
||||||
|
|
||||||
|
qint32 x = qBound(rect.left(), position.x(), rect.right());
|
||||||
|
qint32 y = qBound(rect.top(), position.y(), rect.bottom());
|
||||||
|
// Only start the timer if the mouse has left the window bound.
|
||||||
|
// The leave event is also triggered when the window looses focus.
|
||||||
|
if (x != position.x() || y != position.y()) {
|
||||||
|
mouse_constrain_timer.start();
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int GRenderWindow::QtKeyToSwitchKey(Qt::Key qt_key) {
|
int GRenderWindow::QtKeyToSwitchKey(Qt::Key qt_key) {
|
||||||
static constexpr std::array<std::pair<Qt::Key, Settings::NativeKeyboard::Keys>, 106> key_map = {
|
static constexpr std::array<std::pair<Qt::Key, Settings::NativeKeyboard::Keys>, 106> key_map = {
|
||||||
std::pair<Qt::Key, Settings::NativeKeyboard::Keys>{Qt::Key_A, Settings::NativeKeyboard::A},
|
std::pair<Qt::Key, Settings::NativeKeyboard::Keys>{Qt::Key_A, Settings::NativeKeyboard::A},
|
||||||
@ -658,10 +678,19 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
|
|||||||
input_subsystem->GetMouse()->TouchMove(touch_x, touch_y);
|
input_subsystem->GetMouse()->TouchMove(touch_x, touch_y);
|
||||||
input_subsystem->GetMouse()->Move(pos.x(), pos.y(), center_x, center_y);
|
input_subsystem->GetMouse()->Move(pos.x(), pos.y(), center_x, center_y);
|
||||||
|
|
||||||
|
// Center mouse for mouse panning
|
||||||
if (Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
|
if (Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
|
||||||
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constrain mouse for mouse emulation with mouse panning
|
||||||
|
if (Settings::values.mouse_panning && Settings::values.mouse_enabled) {
|
||||||
|
const auto [clamped_mouse_x, clamped_mouse_y] = ClipToTouchScreen(x, y);
|
||||||
|
QCursor::setPos(mapToGlobal(
|
||||||
|
QPoint{static_cast<int>(clamped_mouse_x), static_cast<int>(clamped_mouse_y)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse_constrain_timer.stop();
|
||||||
emit MouseActivity();
|
emit MouseActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -675,6 +704,31 @@ void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
|
|||||||
input_subsystem->GetMouse()->ReleaseButton(button);
|
input_subsystem->GetMouse()->ReleaseButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GRenderWindow::ConstrainMouse() {
|
||||||
|
if (emu_thread == nullptr || !Settings::values.mouse_panning) {
|
||||||
|
mouse_constrain_timer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this->isActiveWindow()) {
|
||||||
|
mouse_constrain_timer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings::values.mouse_enabled) {
|
||||||
|
const auto pos = mapFromGlobal(QCursor::pos());
|
||||||
|
const int new_pos_x = std::clamp(pos.x(), 0, width());
|
||||||
|
const int new_pos_y = std::clamp(pos.y(), 0, height());
|
||||||
|
|
||||||
|
QCursor::setPos(mapToGlobal(QPoint{new_pos_x, new_pos_y}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int center_x = width() / 2;
|
||||||
|
const int center_y = height() / 2;
|
||||||
|
|
||||||
|
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
||||||
|
}
|
||||||
|
|
||||||
void GRenderWindow::wheelEvent(QWheelEvent* event) {
|
void GRenderWindow::wheelEvent(QWheelEvent* event) {
|
||||||
const int x = event->angleDelta().x();
|
const int x = event->angleDelta().x();
|
||||||
const int y = event->angleDelta().y();
|
const int y = event->angleDelta().y();
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <qglobal.h>
|
#include <qglobal.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
@ -38,7 +39,6 @@ class QMouseEvent;
|
|||||||
class QObject;
|
class QObject;
|
||||||
class QResizeEvent;
|
class QResizeEvent;
|
||||||
class QShowEvent;
|
class QShowEvent;
|
||||||
class QTimer;
|
|
||||||
class QTouchEvent;
|
class QTouchEvent;
|
||||||
class QWheelEvent;
|
class QWheelEvent;
|
||||||
|
|
||||||
@ -166,6 +166,7 @@ public:
|
|||||||
std::pair<u32, u32> ScaleTouch(const QPointF& pos) const;
|
std::pair<u32, u32> ScaleTouch(const QPointF& pos) const;
|
||||||
|
|
||||||
void closeEvent(QCloseEvent* event) override;
|
void closeEvent(QCloseEvent* event) override;
|
||||||
|
void leaveEvent(QEvent* event) override;
|
||||||
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
|
||||||
@ -229,6 +230,7 @@ private:
|
|||||||
void TouchBeginEvent(const QTouchEvent* event);
|
void TouchBeginEvent(const QTouchEvent* event);
|
||||||
void TouchUpdateEvent(const QTouchEvent* event);
|
void TouchUpdateEvent(const QTouchEvent* event);
|
||||||
void TouchEndEvent();
|
void TouchEndEvent();
|
||||||
|
void ConstrainMouse();
|
||||||
|
|
||||||
void RequestCameraCapture();
|
void RequestCameraCapture();
|
||||||
void OnCameraCapture(int requestId, const QImage& img);
|
void OnCameraCapture(int requestId, const QImage& img);
|
||||||
@ -268,6 +270,8 @@ private:
|
|||||||
std::unique_ptr<QTimer> camera_timer;
|
std::unique_ptr<QTimer> camera_timer;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
QTimer mouse_constrain_timer;
|
||||||
|
|
||||||
Core::System& system;
|
Core::System& system;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -27,6 +27,13 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
|
|||||||
|
|
||||||
connect(accuracy_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
connect(accuracy_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||||
&ConfigureCpu::UpdateGroup);
|
&ConfigureCpu::UpdateGroup);
|
||||||
|
|
||||||
|
connect(backend_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||||
|
&ConfigureCpu::UpdateGroup);
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
ui->backend_group->setVisible(true);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigureCpu::~ConfigureCpu() = default;
|
ConfigureCpu::~ConfigureCpu() = default;
|
||||||
@ -34,6 +41,7 @@ ConfigureCpu::~ConfigureCpu() = default;
|
|||||||
void ConfigureCpu::SetConfiguration() {}
|
void ConfigureCpu::SetConfiguration() {}
|
||||||
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||||
auto* accuracy_layout = ui->widget_accuracy->layout();
|
auto* accuracy_layout = ui->widget_accuracy->layout();
|
||||||
|
auto* backend_layout = ui->widget_backend->layout();
|
||||||
auto* unsafe_layout = ui->unsafe_widget->layout();
|
auto* unsafe_layout = ui->unsafe_widget->layout();
|
||||||
std::map<u32, QWidget*> unsafe_hold{};
|
std::map<u32, QWidget*> unsafe_hold{};
|
||||||
|
|
||||||
@ -62,6 +70,9 @@ void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
|||||||
// Keep track of cpu_accuracy combobox to display/hide the unsafe settings
|
// Keep track of cpu_accuracy combobox to display/hide the unsafe settings
|
||||||
accuracy_layout->addWidget(widget);
|
accuracy_layout->addWidget(widget);
|
||||||
accuracy_combobox = widget->combobox;
|
accuracy_combobox = widget->combobox;
|
||||||
|
} else if (setting->Id() == Settings::values.cpu_backend.Id()) {
|
||||||
|
backend_layout->addWidget(widget);
|
||||||
|
backend_combobox = widget->combobox;
|
||||||
} else {
|
} else {
|
||||||
// Presently, all other settings here are unsafe checkboxes
|
// Presently, all other settings here are unsafe checkboxes
|
||||||
unsafe_hold.insert({setting->Id(), widget});
|
unsafe_hold.insert({setting->Id(), widget});
|
||||||
@ -73,6 +84,7 @@ void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpdateGroup(accuracy_combobox->currentIndex());
|
UpdateGroup(accuracy_combobox->currentIndex());
|
||||||
|
UpdateGroup(backend_combobox->currentIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureCpu::UpdateGroup(int index) {
|
void ConfigureCpu::UpdateGroup(int index) {
|
||||||
|
@ -49,4 +49,5 @@ private:
|
|||||||
std::vector<std::function<void(bool)>> apply_funcs{};
|
std::vector<std::function<void(bool)>> apply_funcs{};
|
||||||
|
|
||||||
QComboBox* accuracy_combobox;
|
QComboBox* accuracy_combobox;
|
||||||
|
QComboBox* backend_combobox;
|
||||||
};
|
};
|
||||||
|
@ -59,6 +59,36 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="backend_group">
|
||||||
|
<property name="title">
|
||||||
|
<string>CPU Backend</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="widget_backend" native="true">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout1">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="unsafe_group">
|
<widget class="QGroupBox" name="unsafe_group">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
|
@ -44,6 +44,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
|
|||||||
|
|
||||||
// Cpu
|
// Cpu
|
||||||
INSERT(Settings, cpu_accuracy, tr("Accuracy:"), QStringLiteral());
|
INSERT(Settings, cpu_accuracy, tr("Accuracy:"), QStringLiteral());
|
||||||
|
INSERT(Settings, cpu_backend, tr("Backend:"), QStringLiteral());
|
||||||
|
|
||||||
// Cpu Debug
|
// Cpu Debug
|
||||||
|
|
||||||
@ -240,6 +241,11 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QWidget* parent) {
|
|||||||
PAIR(CpuAccuracy, Unsafe, tr("Unsafe")),
|
PAIR(CpuAccuracy, Unsafe, tr("Unsafe")),
|
||||||
PAIR(CpuAccuracy, Paranoid, tr("Paranoid (disables most optimizations)")),
|
PAIR(CpuAccuracy, Paranoid, tr("Paranoid (disables most optimizations)")),
|
||||||
}});
|
}});
|
||||||
|
translations->insert({Settings::EnumMetadata<Settings::CpuBackend>::Index(),
|
||||||
|
{
|
||||||
|
PAIR(CpuBackend, Dynarmic, tr("Dynarmic")),
|
||||||
|
PAIR(CpuBackend, Nce, tr("NCE")),
|
||||||
|
}});
|
||||||
translations->insert({Settings::EnumMetadata<Settings::FullscreenMode>::Index(),
|
translations->insert({Settings::EnumMetadata<Settings::FullscreenMode>::Index(),
|
||||||
{
|
{
|
||||||
PAIR(FullscreenMode, Borderless, tr("Borderless Windowed")),
|
PAIR(FullscreenMode, Borderless, tr("Borderless Windowed")),
|
||||||
|
@ -187,7 +187,6 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
constexpr int default_mouse_hide_timeout = 2500;
|
constexpr int default_mouse_hide_timeout = 2500;
|
||||||
constexpr int default_mouse_center_timeout = 10;
|
|
||||||
constexpr int default_input_update_timeout = 1;
|
constexpr int default_input_update_timeout = 1;
|
||||||
|
|
||||||
constexpr size_t CopyBufferSize = 1_MiB;
|
constexpr size_t CopyBufferSize = 1_MiB;
|
||||||
@ -437,9 +436,6 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
|
|||||||
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
|
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
|
||||||
connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor);
|
connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor);
|
||||||
|
|
||||||
mouse_center_timer.setInterval(default_mouse_center_timeout);
|
|
||||||
connect(&mouse_center_timer, &QTimer::timeout, this, &GMainWindow::CenterMouseCursor);
|
|
||||||
|
|
||||||
update_input_timer.setInterval(default_input_update_timeout);
|
update_input_timer.setInterval(default_input_update_timeout);
|
||||||
connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers);
|
connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers);
|
||||||
update_input_timer.start();
|
update_input_timer.start();
|
||||||
@ -1376,14 +1372,6 @@ void GMainWindow::InitializeHotkeys() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] {
|
connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] {
|
||||||
if (Settings::values.mouse_enabled) {
|
|
||||||
Settings::values.mouse_panning = false;
|
|
||||||
QMessageBox::warning(
|
|
||||||
this, tr("Emulated mouse is enabled"),
|
|
||||||
tr("Real mouse input and mouse panning are incompatible. Please disable the "
|
|
||||||
"emulated mouse in input advanced settings to allow mouse panning."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Settings::values.mouse_panning = !Settings::values.mouse_panning;
|
Settings::values.mouse_panning = !Settings::values.mouse_panning;
|
||||||
if (Settings::values.mouse_panning) {
|
if (Settings::values.mouse_panning) {
|
||||||
render_window->installEventFilter(render_window);
|
render_window->installEventFilter(render_window);
|
||||||
@ -4711,26 +4699,10 @@ void GMainWindow::ShowMouseCursor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::CenterMouseCursor() {
|
|
||||||
if (emu_thread == nullptr || !Settings::values.mouse_panning) {
|
|
||||||
mouse_center_timer.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this->isActiveWindow()) {
|
|
||||||
mouse_center_timer.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const int center_x = render_window->width() / 2;
|
|
||||||
const int center_y = render_window->height() / 2;
|
|
||||||
|
|
||||||
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
|
||||||
}
|
|
||||||
|
|
||||||
void GMainWindow::OnMouseActivity() {
|
void GMainWindow::OnMouseActivity() {
|
||||||
if (!Settings::values.mouse_panning) {
|
if (!Settings::values.mouse_panning) {
|
||||||
ShowMouseCursor();
|
ShowMouseCursor();
|
||||||
}
|
}
|
||||||
mouse_center_timer.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
|
void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
|
||||||
@ -5031,22 +5003,6 @@ void GMainWindow::dragMoveEvent(QDragMoveEvent* event) {
|
|||||||
AcceptDropEvent(event);
|
AcceptDropEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::leaveEvent(QEvent* event) {
|
|
||||||
if (Settings::values.mouse_panning) {
|
|
||||||
const QRect& rect = geometry();
|
|
||||||
QPoint position = QCursor::pos();
|
|
||||||
|
|
||||||
qint32 x = qBound(rect.left(), position.x(), rect.right());
|
|
||||||
qint32 y = qBound(rect.top(), position.y(), rect.bottom());
|
|
||||||
// Only start the timer if the mouse has left the window bound.
|
|
||||||
// The leave event is also triggered when the window looses focus.
|
|
||||||
if (x != position.x() || y != position.y()) {
|
|
||||||
mouse_center_timer.start();
|
|
||||||
}
|
|
||||||
event->accept();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool GMainWindow::ConfirmChangeGame() {
|
bool GMainWindow::ConfirmChangeGame() {
|
||||||
if (emu_thread == nullptr)
|
if (emu_thread == nullptr)
|
||||||
return true;
|
return true;
|
||||||
|
@ -451,7 +451,6 @@ private:
|
|||||||
void UpdateInputDrivers();
|
void UpdateInputDrivers();
|
||||||
void HideMouseCursor();
|
void HideMouseCursor();
|
||||||
void ShowMouseCursor();
|
void ShowMouseCursor();
|
||||||
void CenterMouseCursor();
|
|
||||||
void OpenURL(const QUrl& url);
|
void OpenURL(const QUrl& url);
|
||||||
void LoadTranslation();
|
void LoadTranslation();
|
||||||
void OpenPerGameConfiguration(u64 title_id, const std::string& file_name);
|
void OpenPerGameConfiguration(u64 title_id, const std::string& file_name);
|
||||||
@ -535,7 +534,6 @@ private:
|
|||||||
bool auto_paused = false;
|
bool auto_paused = false;
|
||||||
bool auto_muted = false;
|
bool auto_muted = false;
|
||||||
QTimer mouse_hide_timer;
|
QTimer mouse_hide_timer;
|
||||||
QTimer mouse_center_timer;
|
|
||||||
QTimer update_input_timer;
|
QTimer update_input_timer;
|
||||||
|
|
||||||
QString startup_icon_theme;
|
QString startup_icon_theme;
|
||||||
@ -592,5 +590,4 @@ protected:
|
|||||||
void dropEvent(QDropEvent* event) override;
|
void dropEvent(QDropEvent* event) override;
|
||||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||||
void dragMoveEvent(QDragMoveEvent* event) override;
|
void dragMoveEvent(QDragMoveEvent* event) override;
|
||||||
void leaveEvent(QEvent* event) override;
|
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user