| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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>? | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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>) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } |