This file was deleted.

@@ -0,0 +1,207 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.model

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat.Companion.loadCodes
import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat.Companion.saveCodes
import kotlin.collections.ArrayList

class CheatsViewModel : ViewModel() {
private var loaded = false

private var selectedCheatPosition = -1
private val _selectedCheat = MutableLiveData<Cheat?>(null)
val selectedCheat: LiveData<Cheat?> get() = _selectedCheat
private val _isAdding = MutableLiveData(false)
val isAdding: LiveData<Boolean> get() = _isAdding
private val _isEditing = MutableLiveData(false)
val isEditing: LiveData<Boolean> get() = _isEditing

private val _cheatAddedEvent = MutableLiveData<Int?>(null)
val cheatAddedEvent: LiveData<Int?> get() = _cheatAddedEvent
private val _cheatChangedEvent = MutableLiveData<Int?>(null)
val cheatChangedEvent: LiveData<Int?> get() = _cheatChangedEvent
private val _cheatDeletedEvent = MutableLiveData<Int?>(null)
val cheatDeletedEvent: LiveData<Int?> get() = _cheatDeletedEvent
private val _geckoCheatsDownloadedEvent = MutableLiveData<Int?>(null)
val geckoCheatsDownloadedEvent: LiveData<Int?> get() = _geckoCheatsDownloadedEvent
private val _openDetailsViewEvent = MutableLiveData(false)
val openDetailsViewEvent: LiveData<Boolean> get() = _openDetailsViewEvent

private var graphicsModGroup: GraphicsModGroup? = null
var graphicsMods: ArrayList<GraphicsMod> = ArrayList()
var patchCheats: ArrayList<PatchCheat> = ArrayList()
var aRCheats: ArrayList<ARCheat> = ArrayList()
var geckoCheats: ArrayList<GeckoCheat> = ArrayList()

private var graphicsModsNeedSaving = false
private var patchCheatsNeedSaving = false
private var aRCheatsNeedSaving = false
private var geckoCheatsNeedSaving = false

fun load(gameID: String, revision: Int) {
if (loaded) return

graphicsModGroup = GraphicsModGroup.load(gameID)
graphicsMods.addAll(graphicsModGroup!!.mods)
patchCheats.addAll(PatchCheat.loadCodes(gameID, revision))
aRCheats.addAll(loadCodes(gameID, revision))
geckoCheats.addAll(GeckoCheat.loadCodes(gameID, revision))

for (mod in graphicsMods) {
mod.setChangedCallback { graphicsModsNeedSaving = true }
}
for (cheat in patchCheats) {
cheat.setChangedCallback { patchCheatsNeedSaving = true }
}
for (cheat in aRCheats) {
cheat.setChangedCallback { aRCheatsNeedSaving = true }
}
for (cheat in geckoCheats) {
cheat.setChangedCallback { geckoCheatsNeedSaving = true }
}

loaded = true
}

fun saveIfNeeded(gameID: String, revision: Int) {
if (graphicsModsNeedSaving) {
graphicsModGroup!!.save()
graphicsModsNeedSaving = false
}

if (patchCheatsNeedSaving) {
PatchCheat.saveCodes(gameID, revision, patchCheats.toTypedArray())
patchCheatsNeedSaving = false
}

if (aRCheatsNeedSaving) {
saveCodes(gameID, revision, aRCheats.toTypedArray())
aRCheatsNeedSaving = false
}

if (geckoCheatsNeedSaving) {
GeckoCheat.saveCodes(gameID, revision, geckoCheats.toTypedArray())
geckoCheatsNeedSaving = false
}
}

fun setSelectedCheat(cheat: Cheat?, position: Int) {
if (isEditing.value!!) setIsEditing(false)

_selectedCheat.value = cheat
selectedCheatPosition = position
}

fun startAddingCheat(cheat: Cheat?, position: Int) {
_selectedCheat.value = cheat
selectedCheatPosition = position

_isAdding.value = true
_isEditing.value = true
}

fun finishAddingCheat() {
check(isAdding.value!!)

_isAdding.value = false
_isEditing.value = false

when (val cheat = selectedCheat.value) {
is PatchCheat -> {
patchCheats.add(cheat)
cheat.setChangedCallback(Runnable { patchCheatsNeedSaving = true })
patchCheatsNeedSaving = true
}
is ARCheat -> {
aRCheats.add(cheat)
cheat.setChangedCallback(Runnable { patchCheatsNeedSaving = true })
aRCheatsNeedSaving = true
}
is GeckoCheat -> {
geckoCheats.add(cheat)
cheat.setChangedCallback(Runnable { geckoCheatsNeedSaving = true })
geckoCheatsNeedSaving = true
}
else -> throw UnsupportedOperationException()
}

notifyCheatAdded()
}

fun setIsEditing(isEditing: Boolean) {
_isEditing.value = isEditing
if (isAdding.value!! && !isEditing) {
_isAdding.value = false
setSelectedCheat(null, -1)
}
}

private fun notifyCheatAdded() {
_cheatAddedEvent.value = selectedCheatPosition
_cheatAddedEvent.value = null
}

/**
* Notifies that an edit has been made to the contents of the currently selected cheat.
*/
fun notifySelectedCheatChanged() {
notifyCheatChanged(selectedCheatPosition)
}

/**
* Notifies that an edit has been made to the contents of the cheat at the given position.
*/
private fun notifyCheatChanged(position: Int) {
_cheatChangedEvent.value = position
_cheatChangedEvent.value = null
}

fun deleteSelectedCheat() {
val cheat = selectedCheat.value
val position = selectedCheatPosition

setSelectedCheat(null, -1)

if (patchCheats.remove(cheat)) patchCheatsNeedSaving = true
if (aRCheats.remove(cheat)) aRCheatsNeedSaving = true
if (geckoCheats.remove(cheat)) geckoCheatsNeedSaving = true

notifyCheatDeleted(position)
}

/**
* Notifies that the cheat at the given position has been deleted.
*/
private fun notifyCheatDeleted(position: Int) {
_cheatDeletedEvent.value = position
_cheatDeletedEvent.value = null
}

fun addDownloadedGeckoCodes(cheats: Array<GeckoCheat>): Int {
var cheatsAdded = 0

for (cheat in cheats) {
if (!geckoCheats.contains(cheat)) {
geckoCheats.add(cheat)
cheatsAdded++
}
}

if (cheatsAdded != 0) {
geckoCheatsNeedSaving = true
_geckoCheatsDownloadedEvent.value = cheatsAdded
_geckoCheatsDownloadedEvent.value = null
}

return cheatsAdded
}

fun openDetailsView() {
_openDetailsViewEvent.value = true
_openDetailsViewEvent.value = false
}
}

This file was deleted.

@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.model

import androidx.annotation.Keep

class GeckoCheat : AbstractCheat {
@Keep
private val mPointer: Long

constructor() {
mPointer = createNew()
}

@Keep
private constructor(pointer: Long) {
mPointer = pointer
}

external fun finalize()

private external fun createNew(): Long

override fun equals(other: Any?): Boolean {
return other != null && javaClass == other.javaClass && equalsImpl(other as GeckoCheat)
}

override fun hashCode(): Int {
return mPointer.hashCode()
}

override fun supportsCreator(): Boolean {
return true
}

override fun supportsNotes(): Boolean {
return true
}

external override fun getName(): String

external override fun getCreator(): String

external override fun getNotes(): String

external override fun getCode(): String

external override fun getUserDefined(): Boolean

external override fun getEnabled(): Boolean

private external fun equalsImpl(other: GeckoCheat): Boolean

external override fun setCheatImpl(
name: String,
creator: String,
notes: String,
code: String
): Int

external override fun setEnabledImpl(enabled: Boolean)

companion object {
@JvmStatic
external fun loadCodes(gameId: String, revision: Int): Array<GeckoCheat>

@JvmStatic
external fun saveCodes(gameId: String, revision: Int, codes: Array<GeckoCheat>)

@JvmStatic
external fun downloadCodes(gameTdbId: String): Array<GeckoCheat>?
}
}

This file was deleted.

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.model

import androidx.annotation.Keep

class GraphicsMod @Keep private constructor(
@Keep private val pointer: Long,
// When a C++ GraphicsModGroup object is destroyed, it also destroys the GraphicsMods it owns.
// To avoid getting dangling pointers, we keep a reference to the GraphicsModGroup here.
@Keep private val parent: GraphicsModGroup
) : ReadOnlyCheat() {
override fun supportsCreator(): Boolean = true

override fun supportsNotes(): Boolean = true

override fun supportsCode(): Boolean = false

external override fun getName(): String

external override fun getCreator(): String

external override fun getNotes(): String

// Technically graphics mods can be user defined, but we don't support editing graphics mods
// in the GUI, and editability is what this really controls
override fun getUserDefined(): Boolean = false

external override fun getEnabled(): Boolean

external override fun setEnabledImpl(enabled: Boolean)
}

This file was deleted.

@@ -0,0 +1,17 @@
package org.dolphinemu.dolphinemu.features.cheats.model

import androidx.annotation.Keep

class GraphicsModGroup @Keep private constructor(@field:Keep private val pointer: Long) {
external fun finalize()

val mods: Array<GraphicsMod>
external get

external fun save()

companion object {
@JvmStatic
external fun load(gameId: String): GraphicsModGroup
}
}

This file was deleted.

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.model

import androidx.annotation.Keep

class PatchCheat : AbstractCheat {
@Keep
private val pointer: Long

constructor() {
pointer = createNew()
}

@Keep
private constructor(pointer: Long) {
this.pointer = pointer
}

external fun finalize()

private external fun createNew(): Long

override fun supportsCreator(): Boolean {
return false
}

override fun supportsNotes(): Boolean {
return false
}

external override fun getName(): String

external override fun getCode(): String

external override fun getUserDefined(): Boolean

external override fun getEnabled(): Boolean

external override fun setCheatImpl(
name: String,
creator: String,
notes: String,
code: String
): Int

external override fun setEnabledImpl(enabled: Boolean)

companion object {
@JvmStatic
external fun loadCodes(gameId: String, revision: Int): Array<PatchCheat>

@JvmStatic
external fun saveCodes(gameId: String, revision: Int, codes: Array<PatchCheat>)
}
}

This file was deleted.

@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.model

abstract class ReadOnlyCheat : Cheat {
private var onChangedCallback: Runnable? = null

override fun setCheat(
name: String,
creator: String,
notes: String,
code: String
): Int {
throw UnsupportedOperationException()
}

override fun setEnabled(isChecked: Boolean) {
setEnabledImpl(isChecked)
onChanged()
}

override fun setChangedCallback(callback: Runnable?) {
onChangedCallback = callback
}

protected fun onChanged() {
if (onChangedCallback != null) onChangedCallback!!.run()
}

protected abstract fun setEnabledImpl(enabled: Boolean)
}

This file was deleted.

@@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import android.view.View
import android.widget.TextView
import androidx.lifecycle.ViewModelProvider
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ListItemSubmenuBinding
import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel
import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat
import org.dolphinemu.dolphinemu.features.cheats.model.PatchCheat

class ActionViewHolder(binding: ListItemSubmenuBinding) : CheatItemViewHolder(binding.root),
View.OnClickListener {
private val mName: TextView

private lateinit var activity: CheatsActivity
private lateinit var viewModel: CheatsViewModel
private var string = 0
private var position = 0

init {
mName = binding.textSettingName
binding.root.setOnClickListener(this)
}

override fun bind(activity: CheatsActivity, item: CheatItem, position: Int) {
this.activity = activity
viewModel = ViewModelProvider(this.activity)[CheatsViewModel::class.java]
string = item.string
this.position = position
mName.setText(string)
}

override fun onClick(root: View) {
when(string) {
R.string.cheats_add_ar -> {
viewModel.startAddingCheat(ARCheat(), position)
viewModel.openDetailsView()
}
R.string.cheats_add_gecko -> {
viewModel.startAddingCheat(GeckoCheat(), position)
viewModel.openDetailsView()
}
R.string.cheats_add_patch -> {
viewModel.startAddingCheat(PatchCheat(), position)
viewModel.openDetailsView()
}
R.string.cheats_download_gecko -> activity.downloadGeckoCodes()
}
}
}

This file was deleted.

@@ -0,0 +1,170 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.FragmentCheatDetailsBinding
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel

class CheatDetailsFragment : Fragment() {
private lateinit var viewModel: CheatsViewModel
private var cheat: Cheat? = null

private var _binding: FragmentCheatDetailsBinding? = null
private val binding get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCheatDetailsBinding.inflate(inflater, container, false)
return binding.getRoot()
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val activity = requireActivity() as CheatsActivity
viewModel = ViewModelProvider(activity)[CheatsViewModel::class.java]

viewModel.selectedCheat.observe(viewLifecycleOwner) { cheat: Cheat? ->
onSelectedCheatUpdated(
cheat
)
}
viewModel.isEditing.observe(viewLifecycleOwner) { isEditing: Boolean ->
onIsEditingUpdated(
isEditing
)
}

binding.buttonDelete.setOnClickListener { onDeleteClicked() }
binding.buttonEdit.setOnClickListener { onEditClicked() }
binding.buttonCancel.setOnClickListener { onCancelClicked() }
binding.buttonOk.setOnClickListener { onOkClicked() }

CheatsActivity.setOnFocusChangeListenerRecursively(
view
) { _: View?, hasFocus: Boolean -> activity.onDetailsViewFocusChange(hasFocus) }
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

private fun clearEditErrors() {
binding.editName.error = null
binding.editCode.error = null
}

private fun onDeleteClicked() {
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.cheats_delete_confirmation, cheat!!.getName()))
.setPositiveButton(R.string.yes) { _: DialogInterface?, i: Int -> viewModel.deleteSelectedCheat() }
.setNegativeButton(R.string.no, null)
.show()
}

private fun onEditClicked() {
viewModel.setIsEditing(true)
binding.buttonOk.requestFocus()
}

private fun onCancelClicked() {
viewModel.setIsEditing(false)
onSelectedCheatUpdated(cheat)
binding.buttonDelete.requestFocus()
}

private fun onOkClicked() {
clearEditErrors()

val result = cheat!!.setCheat(
binding.editNameInput.text.toString(),
binding.editCreatorInput.text.toString(),
binding.editNotesInput.text.toString(),
binding.editCodeInput.text.toString()
)

when (result) {
Cheat.TRY_SET_SUCCESS -> {
if (viewModel.isAdding.value!!) {
viewModel.finishAddingCheat()
onSelectedCheatUpdated(cheat)
} else {
viewModel.notifySelectedCheatChanged()
viewModel.setIsEditing(false)
}
binding.buttonEdit.requestFocus()
}
Cheat.TRY_SET_FAIL_NO_NAME -> {
binding.editName.error = getString(R.string.cheats_error_no_name)
binding.scrollView.smoothScrollTo(0, binding.editNameInput.top)
}
Cheat.TRY_SET_FAIL_NO_CODE_LINES -> {
binding.editCode.error = getString(R.string.cheats_error_no_code_lines)
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
}
Cheat.TRY_SET_FAIL_CODE_MIXED_ENCRYPTION -> {
binding.editCode.error = getString(R.string.cheats_error_mixed_encryption)
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
}
else -> {
binding.editCode.error = getString(R.string.cheats_error_on_line, result)
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
}
}
}

private fun onSelectedCheatUpdated(cheat: Cheat?) {
clearEditErrors()

binding.root.visibility = if (cheat == null) View.GONE else View.VISIBLE

val creatorVisibility =
if (cheat != null && cheat.supportsCreator()) View.VISIBLE else View.GONE
val notesVisibility =
if (cheat != null && cheat.supportsNotes()) View.VISIBLE else View.GONE
val codeVisibility = if (cheat != null && cheat.supportsCode()) View.VISIBLE else View.GONE
binding.editCreator.visibility = creatorVisibility
binding.editNotes.visibility = notesVisibility
binding.editCode.visibility = codeVisibility

val userDefined = cheat != null && cheat.getUserDefined()
binding.buttonDelete.isEnabled = userDefined
binding.buttonEdit.isEnabled = userDefined

// If the fragment was recreated while editing a cheat, it's vital that we
// don't repopulate the fields, otherwise the user's changes will be lost
val isEditing = viewModel.isEditing.value!!

if (!isEditing && cheat != null) {
binding.editNameInput.setText(cheat.getName())
binding.editCreatorInput.setText(cheat.getCreator())
binding.editNotesInput.setText(cheat.getNotes())
binding.editCodeInput.setText(cheat.getCode())
}
this.cheat = cheat
}

private fun onIsEditingUpdated(isEditing: Boolean) {
binding.editNameInput.isEnabled = isEditing
binding.editCreatorInput.isEnabled = isEditing
binding.editNotesInput.isEnabled = isEditing
binding.editCodeInput.isEnabled = isEditing

binding.buttonDelete.visibility = if (isEditing) View.GONE else View.VISIBLE
binding.buttonEdit.visibility = if (isEditing) View.GONE else View.VISIBLE
binding.buttonCancel.visibility = if (isEditing) View.VISIBLE else View.GONE
binding.buttonOk.visibility = if (isEditing) View.VISIBLE else View.GONE
}
}

This file was deleted.

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import org.dolphinemu.dolphinemu.features.cheats.model.Cheat

class CheatItem {
val cheat: Cheat?
val string: Int
val type: Int

constructor(cheat: Cheat) {
this.cheat = cheat
string = 0
type = TYPE_CHEAT
}

constructor(type: Int, string: Int) {
cheat = null
this.string = string
this.type = type
}

companion object {
const val TYPE_CHEAT = 0
const val TYPE_HEADER = 1
const val TYPE_ACTION = 2
}
}

This file was deleted.

@@ -0,0 +1,10 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import android.view.View
import androidx.recyclerview.widget.RecyclerView

abstract class CheatItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(activity: CheatsActivity, item: CheatItem, position: Int)
}

This file was deleted.

@@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.ColorInt
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.elevation.ElevationOverlayProvider
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.FragmentCheatListBinding
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel

class CheatListFragment : Fragment() {
private var _binding: FragmentCheatListBinding? = null
private val binding get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCheatListBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val activity = requireActivity() as CheatsActivity
val viewModel = ViewModelProvider(activity)[CheatsViewModel::class.java]

binding.cheatList.adapter = CheatsAdapter(activity, viewModel)
binding.cheatList.layoutManager = LinearLayoutManager(activity)

val divider = MaterialDividerItemDecoration(requireActivity(), LinearLayoutManager.VERTICAL)
divider.isLastItemDecorated = false
binding.cheatList.addItemDecoration(divider)

@ColorInt val color =
ElevationOverlayProvider(binding.cheatsWarning.context).compositeOverlay(
MaterialColors.getColor(binding.cheatsWarning, R.attr.colorSurface),
resources.getDimensionPixelSize(R.dimen.elevated_app_bar).toFloat()
)
binding.cheatsWarning.setBackgroundColor(color)
binding.gfxModsWarning.setBackgroundColor(color)

setInsets()
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.cheatList) { v: View, windowInsets: WindowInsetsCompat ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(
0,
0,
0,
insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_xtralarge)
)
windowInsets
}
}
}

This file was deleted.

@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import android.view.View
import android.widget.CompoundButton
import androidx.lifecycle.ViewModelProvider
import org.dolphinemu.dolphinemu.databinding.ListItemCheatBinding
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel

class CheatViewHolder(private val binding: ListItemCheatBinding) :
CheatItemViewHolder(binding.getRoot()),
View.OnClickListener,
CompoundButton.OnCheckedChangeListener {
private lateinit var viewModel: CheatsViewModel
private lateinit var cheat: Cheat
private var position = 0

override fun bind(activity: CheatsActivity, item: CheatItem, position: Int) {
binding.cheatSwitch.setOnCheckedChangeListener(null)
viewModel = ViewModelProvider(activity)[CheatsViewModel::class.java]
cheat = item.cheat!!
this.position = position
binding.textName.text = cheat.getName()
binding.cheatSwitch.isChecked = cheat.getEnabled()
binding.root.setOnClickListener(this)
binding.cheatSwitch.setOnCheckedChangeListener(this)
}

override fun onClick(root: View) {
viewModel.setSelectedCheat(cheat, position)
viewModel.openDetailsView()
}

override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
cheat.setEnabled(isChecked)
}
}

This file was deleted.

@@ -0,0 +1,293 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.slidingpanelayout.widget.SlidingPaneLayout
import androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.ElevationOverlayProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivityCheatsBinding
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel
import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat.Companion.downloadCodes
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.InsetsHelper
import org.dolphinemu.dolphinemu.utils.ThemeHelper

class CheatsActivity : AppCompatActivity(), PanelSlideListener {
private var gameId: String? = null
private var gameTdbId: String? = null
private var revision = 0
private var isWii = false
private lateinit var viewModel: CheatsViewModel

private var cheatListLastFocus: View? = null
private var cheatDetailsLastFocus: View? = null

private lateinit var binding: ActivityCheatsBinding

override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this)

super.onCreate(savedInstanceState)

MainPresenter.skipRescanningLibrary()

gameId = intent.getStringExtra(ARG_GAME_ID)
gameTdbId = intent.getStringExtra(ARG_GAMETDB_ID)
revision = intent.getIntExtra(ARG_REVISION, 0)
isWii = intent.getBooleanExtra(ARG_IS_WII, true)

title = getString(R.string.cheats_with_game_id, gameId)

viewModel = ViewModelProvider(this)[CheatsViewModel::class.java]
viewModel.load(gameId!!, revision)

binding = ActivityCheatsBinding.inflate(layoutInflater)
setContentView(binding.root)

WindowCompat.setDecorFitsSystemWindows(window, false)

cheatListLastFocus = binding.cheatList
cheatDetailsLastFocus = binding.cheatDetails

binding.slidingPaneLayout.addPanelSlideListener(this)

onBackPressedDispatcher.addCallback(
this,
TwoPaneOnBackPressedCallback(binding.slidingPaneLayout)
)

viewModel.selectedCheat.observe(this) { selectedCheat: Cheat? ->
onSelectedCheatChanged(
selectedCheat
)
}
onSelectedCheatChanged(viewModel.selectedCheat.value)

viewModel.openDetailsViewEvent.observe(this) { open: Boolean -> openDetailsView(open) }

setSupportActionBar(binding.toolbarCheats)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)

setInsets()

@ColorInt val color =
ElevationOverlayProvider(binding.toolbarCheats.context).compositeOverlay(
MaterialColors.getColor(binding.toolbarCheats, R.attr.colorSurface),
resources.getDimensionPixelSize(R.dimen.elevated_app_bar).toFloat()
)
binding.toolbarCheats.setBackgroundColor(color)
ThemeHelper.setStatusBarColor(this, color)
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_settings, menu)
return true
}

override fun onStop() {
super.onStop()
viewModel.saveIfNeeded(gameId!!, revision)
}

override fun onPanelSlide(panel: View, slideOffset: Float) {}
override fun onPanelOpened(panel: View) {
val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
cheatDetailsLastFocus!!.requestFocus(if (rtl) View.FOCUS_LEFT else View.FOCUS_RIGHT)
}

override fun onPanelClosed(panel: View) {
val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
cheatListLastFocus!!.requestFocus(if (rtl) View.FOCUS_RIGHT else View.FOCUS_LEFT)
}

private fun onSelectedCheatChanged(selectedCheat: Cheat?) {
val cheatSelected = selectedCheat != null
if (!cheatSelected && binding.slidingPaneLayout.isOpen) binding.slidingPaneLayout.close()

binding.slidingPaneLayout.lockMode =
if (cheatSelected) SlidingPaneLayout.LOCK_MODE_UNLOCKED else SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED
}

fun onListViewFocusChange(hasFocus: Boolean) {
if (hasFocus) {
cheatListLastFocus = binding.cheatList.findFocus()
if (cheatListLastFocus == null) throw NullPointerException()
binding.slidingPaneLayout.close()
}
}

fun onDetailsViewFocusChange(hasFocus: Boolean) {
if (hasFocus) {
cheatDetailsLastFocus = binding.cheatDetails.findFocus()
if (cheatDetailsLastFocus == null) throw NullPointerException()
binding.slidingPaneLayout.open()
}
}

override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}

private fun openDetailsView(open: Boolean) {
if (open) binding.slidingPaneLayout.open()
}

fun loadGameSpecificSettings(): Settings {
val settings = Settings()
settings.loadSettings(null, gameId, revision, isWii)
return settings
}

fun downloadGeckoCodes() {
val progressDialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.cheats_downloading)
.setView(R.layout.dialog_indeterminate_progress)
.setCancelable(false)
.show()

lifecycleScope.launchWhenResumed {
withContext(Dispatchers.IO) {
val codes = downloadCodes(gameTdbId!!)
withContext(Dispatchers.Main) {
progressDialog.dismiss()
if (codes == null) {
MaterialAlertDialogBuilder(binding.root.context)
.setMessage(getString(R.string.cheats_download_failed))
.setPositiveButton(R.string.ok, null)
.show()
} else if (codes.isEmpty()) {
MaterialAlertDialogBuilder(binding.root.context)
.setMessage(getString(R.string.cheats_download_empty))
.setPositiveButton(R.string.ok, null)
.show()
} else {
val cheatsAdded = viewModel.addDownloadedGeckoCodes(codes)
val message =
getString(R.string.cheats_download_succeeded, codes.size, cheatsAdded)
MaterialAlertDialogBuilder(binding.root.context)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
}
}

private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.appbarCheats) { _: View?, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())

InsetsHelper.insetAppBar(barInsets, binding.appbarCheats)
binding.slidingPaneLayout.setPadding(barInsets.left, 0, barInsets.right, 0)

// Set keyboard insets if the system supports smooth keyboard animations
val mlpDetails = binding.cheatDetails.layoutParams as MarginLayoutParams
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (keyboardInsets.bottom > 0) {
mlpDetails.bottomMargin = keyboardInsets.bottom
} else {
mlpDetails.bottomMargin = barInsets.bottom
}
} else {
if (mlpDetails.bottomMargin == 0) {
mlpDetails.bottomMargin = barInsets.bottom
}
}
binding.cheatDetails.layoutParams = mlpDetails

InsetsHelper.applyNavbarWorkaround(barInsets.bottom, binding.workaroundView)
ThemeHelper.setNavigationBarColor(
this,
MaterialColors.getColor(binding.appbarCheats, R.attr.colorSurface)
)

windowInsets
}

// Update the layout for every frame that the keyboard animates in
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ViewCompat.setWindowInsetsAnimationCallback(
binding.cheatDetails,
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
var keyboardInsets = 0
var barInsets = 0
override fun onProgress(
insets: WindowInsetsCompat,
runningAnimations: List<WindowInsetsAnimationCompat>
): WindowInsetsCompat {
val mlpDetails = binding.cheatDetails.layoutParams as MarginLayoutParams
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
mlpDetails.bottomMargin = keyboardInsets.coerceAtLeast(barInsets)
binding.cheatDetails.layoutParams = mlpDetails
return insets
}
})
}
}

companion object {
private const val ARG_GAME_ID = "game_id"
private const val ARG_GAMETDB_ID = "gametdb_id"
private const val ARG_REVISION = "revision"
private const val ARG_IS_WII = "is_wii"

@JvmStatic
fun launch(
context: Context,
gameId: String,
gameTdbId: String,
revision: Int,
isWii: Boolean
) {
val intent = Intent(context, CheatsActivity::class.java)
intent.putExtra(ARG_GAME_ID, gameId)
intent.putExtra(ARG_GAMETDB_ID, gameTdbId)
intent.putExtra(ARG_REVISION, revision)
intent.putExtra(ARG_IS_WII, isWii)
context.startActivity(intent)
}

@JvmStatic
fun setOnFocusChangeListenerRecursively(
view: View,
listener: View.OnFocusChangeListener?
) {
view.onFocusChangeListener = listener
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
setOnFocusChangeListenerRecursively(child, listener)
}
}
}
}
}

This file was deleted.

@@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ListItemCheatBinding
import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding
import org.dolphinemu.dolphinemu.databinding.ListItemSubmenuBinding
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel
import org.dolphinemu.dolphinemu.features.cheats.ui.CheatsActivity.Companion.setOnFocusChangeListenerRecursively

class CheatsAdapter(
private val activity: CheatsActivity,
private val viewModel: CheatsViewModel
) : RecyclerView.Adapter<CheatItemViewHolder>() {
init {
viewModel.cheatAddedEvent.observe(activity) { position: Int? ->
position?.let { notifyItemInserted(it) }
}

viewModel.cheatChangedEvent.observe(activity) { position: Int? ->
position?.let { notifyItemChanged(it) }
}

viewModel.cheatDeletedEvent.observe(activity) { position: Int? ->
position?.let { notifyItemRemoved(it) }
}

viewModel.geckoCheatsDownloadedEvent.observe(activity) { cheatsAdded: Int? ->
cheatsAdded?.let {
val positionEnd = itemCount - 2 // Skip "Add Gecko Code" and "Download Gecko Codes"
val positionStart = positionEnd - it
notifyItemRangeInserted(positionStart, it)
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheatItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
CheatItem.TYPE_CHEAT -> {
val listItemCheatBinding = ListItemCheatBinding.inflate(inflater)
addViewListeners(listItemCheatBinding.getRoot())
CheatViewHolder(listItemCheatBinding)
}
CheatItem.TYPE_HEADER -> {
val listItemHeaderBinding = ListItemHeaderBinding.inflate(inflater)
addViewListeners(listItemHeaderBinding.root)
HeaderViewHolder(listItemHeaderBinding)
}
CheatItem.TYPE_ACTION -> {
val listItemSubmenuBinding = ListItemSubmenuBinding.inflate(inflater)
addViewListeners(listItemSubmenuBinding.root)
ActionViewHolder(listItemSubmenuBinding)
}
else -> throw UnsupportedOperationException()
}
}

override fun onBindViewHolder(holder: CheatItemViewHolder, position: Int) {
holder.bind(activity, getItemAt(position), position)
}

override fun getItemCount(): Int {
return viewModel.graphicsMods.size + viewModel.patchCheats.size + viewModel.aRCheats.size +
viewModel.geckoCheats.size + 8
}

override fun getItemViewType(position: Int): Int {
return getItemAt(position).type
}

private fun addViewListeners(view: View) {
setOnFocusChangeListenerRecursively(view) { _: View?, hasFocus: Boolean ->
activity.onListViewFocusChange(
hasFocus
)
}
}

private fun getItemAt(position: Int): CheatItem {
// Graphics mods
var itemPosition = position
if (itemPosition == 0) return CheatItem(
CheatItem.TYPE_HEADER,
R.string.cheats_header_graphics_mod
)
itemPosition -= 1

val graphicsMods = viewModel.graphicsMods
if (itemPosition < graphicsMods.size) return CheatItem(graphicsMods[itemPosition])
itemPosition -= graphicsMods.size

// Patches
if (itemPosition == 0) return CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_patch)
itemPosition -= 1

val patchCheats = viewModel.patchCheats
if (itemPosition < patchCheats.size) return CheatItem(patchCheats[itemPosition])
itemPosition -= patchCheats.size

if (itemPosition == 0) return CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_patch)
itemPosition -= 1

// AR codes
if (itemPosition == 0) return CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_ar)
itemPosition -= 1

val arCheats = viewModel.aRCheats
if (itemPosition < arCheats.size) return CheatItem(arCheats[itemPosition])
itemPosition -= arCheats.size

if (itemPosition == 0) return CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_ar)
itemPosition -= 1

// Gecko codes
if (itemPosition == 0) return CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_gecko)
itemPosition -= 1

val geckoCheats = viewModel.geckoCheats
if (itemPosition < geckoCheats.size) return CheatItem(geckoCheats[itemPosition])
itemPosition -= geckoCheats.size

if (itemPosition == 0) return CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_gecko)
itemPosition -= 1

if (itemPosition == 0) return CheatItem(
CheatItem.TYPE_ACTION,
R.string.cheats_download_gecko
)

throw IndexOutOfBoundsException()
}
}

This file was deleted.

@@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag

class CheatsDisabledWarningFragment : SettingDisabledWarningFragment(
BooleanSetting.MAIN_ENABLE_CHEATS,
MenuTag.CONFIG_GENERAL,
R.string.cheats_disabled_warning
)

This file was deleted.

@@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag

class GraphicsModsDisabledWarningFragment : SettingDisabledWarningFragment(
BooleanSetting.GFX_MODS_ENABLE,
MenuTag.ADVANCED_GRAPHICS,
R.string.gfx_mods_disabled_warning
)

This file was deleted.

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.cheats.ui

import android.widget.TextView
import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding

class HeaderViewHolder(binding: ListItemHeaderBinding) : CheatItemViewHolder(binding.root) {
private val headerName: TextView

init {
headerName = binding.textHeaderName
}

override fun bind(activity: CheatsActivity, item: CheatItem, position: Int) {
headerName.setText(item.string)
}
}

This file was deleted.