diff --git a/Source/Android/app/build.gradle b/Source/Android/app/build.gradle index 369cc46e79..57715c02dc 100644 --- a/Source/Android/app/build.gradle +++ b/Source/Android/app/build.gradle @@ -90,6 +90,8 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.lifecycle:lifecycle-viewmodel:2.3.1' + implementation 'androidx.fragment:fragment:1.3.6' + implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-alpha03" implementation 'com.google.android.material:material:1.4.0' // Android TV UI libraries. diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 6c49619945..9074dfb3da 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -76,6 +76,12 @@ android:theme="@style/DolphinSettingsBase" android:label="@string/settings"/> + + SettingsActivity.launch(getContext(), MenuTag.SETTINGS, gameId, revision, isWii)); + itemsBuilder.add(R.string.properties_edit_cheats, (dialog, i) -> + CheatsActivity.launch(getContext(), gameId, gameTdbId, revision, isWii)); + itemsBuilder.add(R.string.properties_clear_game_settings, (dialog, i) -> clearGameSettings(gameId)); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/ARCheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/ARCheat.java new file mode 100644 index 0000000000..8a168e8bc5 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/ARCheat.java @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.cheats.model; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; + +public class ARCheat extends AbstractCheat +{ + @Keep + private final long mPointer; + + public ARCheat() + { + mPointer = createNew(); + } + + @Keep + private ARCheat(long pointer) + { + mPointer = pointer; + } + + @Override + public native void finalize(); + + private native long createNew(); + + public boolean supportsCreator() + { + return false; + } + + public boolean supportsNotes() + { + return false; + } + + @NonNull + public native String getName(); + + @NonNull + public native String getCode(); + + public native boolean getUserDefined(); + + public native boolean getEnabled(); + + @Override + protected native int trySetImpl(@NonNull String name, @NonNull String creator, + @NonNull String notes, @NonNull String code); + + @Override + protected native void setEnabledImpl(boolean enabled); + + @NonNull + public static native ARCheat[] loadCodes(String gameId, int revision); + + public static native void saveCodes(String gameId, int revision, ARCheat[] codes); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/AbstractCheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/AbstractCheat.java new file mode 100644 index 0000000000..9a86d58533 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/AbstractCheat.java @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.cheats.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public abstract class AbstractCheat implements Cheat +{ + private Runnable mChangedCallback = null; + + public int trySet(@NonNull String name, @NonNull String creator, @NonNull String notes, + @NonNull String code) + { + if (!code.isEmpty() && code.charAt(0) == '$') + { + int firstLineEnd = code.indexOf('\n'); + if (firstLineEnd == -1) + { + name = code.substring(1); + code = ""; + } + else + { + name = code.substring(1, firstLineEnd); + code = code.substring(firstLineEnd + 1); + } + } + + if (name.isEmpty()) + return TRY_SET_FAIL_NO_NAME; + + int result = trySetImpl(name, creator, notes, code); + + if (result == TRY_SET_SUCCESS) + onChanged(); + + return result; + } + + public void setEnabled(boolean enabled) + { + setEnabledImpl(enabled); + onChanged(); + } + + public void setChangedCallback(@Nullable Runnable callback) + { + mChangedCallback = callback; + } + + protected void onChanged() + { + if (mChangedCallback != null) + mChangedCallback.run(); + } + + protected abstract int trySetImpl(@NonNull String name, @NonNull String creator, + @NonNull String notes, @NonNull String code); + + protected abstract void setEnabledImpl(boolean enabled); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/Cheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/Cheat.java new file mode 100644 index 0000000000..75a3befc6f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/Cheat.java @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.cheats.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface Cheat +{ + int TRY_SET_FAIL_CODE_MIXED_ENCRYPTION = -3; + int TRY_SET_FAIL_NO_CODE_LINES = -2; + int TRY_SET_FAIL_NO_NAME = -1; + int TRY_SET_SUCCESS = 0; + // Result codes greater than 0 represent an error on the corresponding code line (one-indexed) + + boolean supportsCreator(); + + boolean supportsNotes(); + + @NonNull + String getName(); + + @NonNull + default String getCreator() + { + return ""; + } + + @NonNull + default String getNotes() + { + return ""; + } + + @NonNull + String getCode(); + + int trySet(@NonNull String name, @NonNull String creator, @NonNull String notes, + @NonNull String code); + + boolean getUserDefined(); + + boolean getEnabled(); + + void setEnabled(boolean enabled); + + void setChangedCallback(@Nullable Runnable callback); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/CheatsViewModel.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/CheatsViewModel.java new file mode 100644 index 0000000000..0238628319 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/CheatsViewModel.java @@ -0,0 +1,297 @@ +// 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 java.util.ArrayList; +import java.util.Collections; + +public class CheatsViewModel extends ViewModel +{ + private boolean mLoaded = false; + + private int mSelectedCheatPosition = -1; + private final MutableLiveData mSelectedCheat = new MutableLiveData<>(null); + private final MutableLiveData mIsAdding = new MutableLiveData<>(false); + private final MutableLiveData mIsEditing = new MutableLiveData<>(false); + + private final MutableLiveData mCheatAddedEvent = new MutableLiveData<>(null); + private final MutableLiveData mCheatChangedEvent = new MutableLiveData<>(null); + private final MutableLiveData mCheatDeletedEvent = new MutableLiveData<>(null); + private final MutableLiveData mGeckoCheatsDownloadedEvent = new MutableLiveData<>(null); + private final MutableLiveData mOpenDetailsViewEvent = new MutableLiveData<>(false); + + private ArrayList mPatchCheats; + private ArrayList mARCheats; + private ArrayList mGeckoCheats; + + private boolean mPatchCheatsNeedSaving = false; + private boolean mARCheatsNeedSaving = false; + private boolean mGeckoCheatsNeedSaving = false; + + public void load(String gameID, int revision) + { + if (mLoaded) + return; + + mPatchCheats = new ArrayList<>(); + Collections.addAll(mPatchCheats, PatchCheat.loadCodes(gameID, revision)); + mARCheats = new ArrayList<>(); + Collections.addAll(mARCheats, ARCheat.loadCodes(gameID, revision)); + mGeckoCheats = new ArrayList<>(); + Collections.addAll(mGeckoCheats, GeckoCheat.loadCodes(gameID, revision)); + + for (PatchCheat cheat : mPatchCheats) + { + cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true); + } + for (ARCheat cheat : mARCheats) + { + cheat.setChangedCallback(() -> mARCheatsNeedSaving = true); + } + for (GeckoCheat cheat : mGeckoCheats) + { + cheat.setChangedCallback(() -> mGeckoCheatsNeedSaving = true); + } + + mLoaded = true; + } + + public void saveIfNeeded(String gameID, int revision) + { + if (mPatchCheatsNeedSaving) + { + PatchCheat.saveCodes(gameID, revision, mPatchCheats.toArray(new PatchCheat[0])); + mPatchCheatsNeedSaving = false; + } + + if (mARCheatsNeedSaving) + { + ARCheat.saveCodes(gameID, revision, mARCheats.toArray(new ARCheat[0])); + mARCheatsNeedSaving = false; + } + + if (mGeckoCheatsNeedSaving) + { + GeckoCheat.saveCodes(gameID, revision, mGeckoCheats.toArray(new GeckoCheat[0])); + mGeckoCheatsNeedSaving = false; + } + } + + public LiveData getSelectedCheat() + { + return mSelectedCheat; + } + + public void setSelectedCheat(Cheat cheat, int position) + { + if (mIsEditing.getValue()) + setIsEditing(false); + + mSelectedCheat.setValue(cheat); + mSelectedCheatPosition = position; + } + + public LiveData getIsAdding() + { + return mIsAdding; + } + + public void startAddingCheat(Cheat cheat, int position) + { + mSelectedCheat.setValue(cheat); + mSelectedCheatPosition = position; + + mIsAdding.setValue(true); + mIsEditing.setValue(true); + } + + public void finishAddingCheat() + { + if (!mIsAdding.getValue()) + throw new IllegalStateException(); + + mIsAdding.setValue(false); + mIsEditing.setValue(false); + + Cheat cheat = mSelectedCheat.getValue(); + + if (cheat instanceof PatchCheat) + { + mPatchCheats.add((PatchCheat) mSelectedCheat.getValue()); + cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true); + mPatchCheatsNeedSaving = true; + } + else if (cheat instanceof ARCheat) + { + mARCheats.add((ARCheat) mSelectedCheat.getValue()); + cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true); + mARCheatsNeedSaving = true; + } + else if (cheat instanceof GeckoCheat) + { + mGeckoCheats.add((GeckoCheat) mSelectedCheat.getValue()); + cheat.setChangedCallback(() -> mGeckoCheatsNeedSaving = true); + mGeckoCheatsNeedSaving = true; + } + else + { + throw new UnsupportedOperationException(); + } + + notifyCheatAdded(); + } + + public LiveData getIsEditing() + { + return mIsEditing; + } + + public void setIsEditing(boolean isEditing) + { + mIsEditing.setValue(isEditing); + + if (mIsAdding.getValue() && !isEditing) + { + mIsAdding.setValue(false); + setSelectedCheat(null, -1); + } + } + + /** + * When a cheat is added, the integer stored in the returned LiveData + * changes to the position of that cheat, then changes back to null. + */ + public LiveData getCheatAddedEvent() + { + return mCheatAddedEvent; + } + + private void notifyCheatAdded() + { + mCheatAddedEvent.setValue(mSelectedCheatPosition); + mCheatAddedEvent.setValue(null); + } + + /** + * When a cheat is edited, the integer stored in the returned LiveData + * changes to the position of that cheat, then changes back to null. + */ + public LiveData getCheatChangedEvent() + { + return mCheatChangedEvent; + } + + /** + * Notifies that an edit has been made to the contents of the currently selected cheat. + */ + public void notifySelectedCheatChanged() + { + notifyCheatChanged(mSelectedCheatPosition); + } + + /** + * Notifies that an edit has been made to the contents of the cheat at the given position. + */ + public void notifyCheatChanged(int position) + { + mCheatChangedEvent.setValue(position); + mCheatChangedEvent.setValue(null); + } + + /** + * When a cheat is deleted, the integer stored in the returned LiveData + * changes to the position of that cheat, then changes back to null. + */ + public LiveData getCheatDeletedEvent() + { + return mCheatDeletedEvent; + } + + public void deleteSelectedCheat() + { + Cheat cheat = mSelectedCheat.getValue(); + int position = mSelectedCheatPosition; + + setSelectedCheat(null, -1); + + if (mPatchCheats.remove(cheat)) + mPatchCheatsNeedSaving = true; + if (mARCheats.remove(cheat)) + mARCheatsNeedSaving = true; + if (mGeckoCheats.remove(cheat)) + mGeckoCheatsNeedSaving = true; + + notifyCheatDeleted(position); + } + + /** + * Notifies that the cheat at the given position has been deleted. + */ + private void notifyCheatDeleted(int position) + { + mCheatDeletedEvent.setValue(position); + mCheatDeletedEvent.setValue(null); + } + + /** + * When Gecko cheats are downloaded, the integer stored in the returned LiveData + * changes to the number of cheats added, then changes back to null. + */ + public LiveData getGeckoCheatsDownloadedEvent() + { + return mGeckoCheatsDownloadedEvent; + } + + public int addDownloadedGeckoCodes(GeckoCheat[] cheats) + { + int cheatsAdded = 0; + + for (GeckoCheat cheat : cheats) + { + if (!mGeckoCheats.contains(cheat)) + { + mGeckoCheats.add(cheat); + cheatsAdded++; + } + } + + if (cheatsAdded != 0) + { + mGeckoCheatsNeedSaving = true; + mGeckoCheatsDownloadedEvent.setValue(cheatsAdded); + mGeckoCheatsDownloadedEvent.setValue(null); + } + + return cheatsAdded; + } + + public LiveData getOpenDetailsViewEvent() + { + return mOpenDetailsViewEvent; + } + + public void openDetailsView() + { + mOpenDetailsViewEvent.setValue(true); + mOpenDetailsViewEvent.setValue(false); + } + + public ArrayList getPatchCheats() + { + return mPatchCheats; + } + + public ArrayList getARCheats() + { + return mARCheats; + } + + public ArrayList getGeckoCheats() + { + return mGeckoCheats; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/GeckoCheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/GeckoCheat.java new file mode 100644 index 0000000000..4397a95d81 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/GeckoCheat.java @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.cheats.model; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class GeckoCheat extends AbstractCheat +{ + @Keep + private final long mPointer; + + public GeckoCheat() + { + mPointer = createNew(); + } + + @Keep + private GeckoCheat(long pointer) + { + mPointer = pointer; + } + + @Override + public native void finalize(); + + private native long createNew(); + + @Override + public boolean equals(@Nullable Object obj) + { + return obj != null && getClass() == obj.getClass() && equalsImpl((GeckoCheat) obj); + } + + public boolean supportsCreator() + { + return true; + } + + public boolean supportsNotes() + { + return true; + } + + @NonNull + public native String getName(); + + @NonNull + public native String getCreator(); + + @NonNull + public native String getNotes(); + + @NonNull + public native String getCode(); + + public native boolean getUserDefined(); + + public native boolean getEnabled(); + + public native boolean equalsImpl(@NonNull GeckoCheat other); + + @Override + protected native int trySetImpl(@NonNull String name, @NonNull String creator, + @NonNull String notes, @NonNull String code); + + @Override + protected native void setEnabledImpl(boolean enabled); + + @NonNull + public static native GeckoCheat[] loadCodes(String gameId, int revision); + + public static native void saveCodes(String gameId, int revision, GeckoCheat[] codes); + + @Nullable + public static native GeckoCheat[] downloadCodes(String gameTdbId); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/PatchCheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/PatchCheat.java new file mode 100644 index 0000000000..9dcb9f8111 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/PatchCheat.java @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.cheats.model; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; + +public class PatchCheat extends AbstractCheat +{ + @Keep + private final long mPointer; + + public PatchCheat() + { + mPointer = createNew(); + } + + @Keep + private PatchCheat(long pointer) + { + mPointer = pointer; + } + + @Override + public native void finalize(); + + private native long createNew(); + + public boolean supportsCreator() + { + return false; + } + + public boolean supportsNotes() + { + return false; + } + + @NonNull + public native String getName(); + + @NonNull + public native String getCode(); + + public native boolean getUserDefined(); + + public native boolean getEnabled(); + + @Override + protected native int trySetImpl(@NonNull String name, @NonNull String creator, + @NonNull String notes, @NonNull String code); + + @Override + protected native void setEnabledImpl(boolean enabled); + + @NonNull + public static native PatchCheat[] loadCodes(String gameId, int revision); + + public static native void saveCodes(String gameId, int revision, PatchCheat[] codes); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/ActionViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/ActionViewHolder.java new file mode 100644 index 0000000000..3e99384fdd --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/ActionViewHolder.java @@ -0,0 +1,67 @@ +// 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.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; + +import org.dolphinemu.dolphinemu.R; +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; + +public class ActionViewHolder extends CheatItemViewHolder implements View.OnClickListener +{ + private final TextView mName; + + private CheatsActivity mActivity; + private CheatsViewModel mViewModel; + private int mString; + private int mPosition; + + public ActionViewHolder(@NonNull View itemView) + { + super(itemView); + + mName = itemView.findViewById(R.id.text_setting_name); + + itemView.setOnClickListener(this); + } + + public void bind(CheatsActivity activity, CheatItem item, int position) + { + mActivity = activity; + mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); + mString = item.getString(); + mPosition = position; + + mName.setText(mString); + } + + public void onClick(View root) + { + if (mString == R.string.cheats_add_ar) + { + mViewModel.startAddingCheat(new ARCheat(), mPosition); + mViewModel.openDetailsView(); + } + else if (mString == R.string.cheats_add_gecko) + { + mViewModel.startAddingCheat(new GeckoCheat(), mPosition); + mViewModel.openDetailsView(); + } + else if (mString == R.string.cheats_add_patch) + { + mViewModel.startAddingCheat(new PatchCheat(), mPosition); + mViewModel.openDetailsView(); + } + else if (mString == R.string.cheats_download_gecko) + { + mActivity.downloadGeckoCodes(); + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java new file mode 100644 index 0000000000..2ab6bb31b0 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java @@ -0,0 +1,198 @@ +// 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 android.widget.Button; +import android.widget.EditText; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.cheats.model.Cheat; +import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel; + +public class CheatDetailsFragment extends Fragment +{ + private View mRoot; + private ScrollView mScrollView; + private TextView mLabelName; + private EditText mEditName; + private TextView mLabelCreator; + private EditText mEditCreator; + private TextView mLabelNotes; + private EditText mEditNotes; + private EditText mEditCode; + private Button mButtonDelete; + private Button mButtonEdit; + private Button mButtonCancel; + private Button mButtonOk; + + private CheatsViewModel mViewModel; + private Cheat mCheat; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) + { + return inflater.inflate(R.layout.fragment_cheat_details, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + mRoot = view.findViewById(R.id.root); + mScrollView = view.findViewById(R.id.scroll_view); + mLabelName = view.findViewById(R.id.label_name); + mEditName = view.findViewById(R.id.edit_name); + mLabelCreator = view.findViewById(R.id.label_creator); + mEditCreator = view.findViewById(R.id.edit_creator); + mLabelNotes = view.findViewById(R.id.label_notes); + mEditNotes = view.findViewById(R.id.edit_notes); + mEditCode = view.findViewById(R.id.edit_code); + mButtonDelete = view.findViewById(R.id.button_delete); + mButtonEdit = view.findViewById(R.id.button_edit); + mButtonCancel = view.findViewById(R.id.button_cancel); + mButtonOk = view.findViewById(R.id.button_ok); + + CheatsActivity activity = (CheatsActivity) requireActivity(); + mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); + + mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(), this::onSelectedCheatUpdated); + mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated); + + mButtonDelete.setOnClickListener(this::onDeleteClicked); + mButtonEdit.setOnClickListener(this::onEditClicked); + mButtonCancel.setOnClickListener(this::onCancelClicked); + mButtonOk.setOnClickListener(this::onOkClicked); + + CheatsActivity.setOnFocusChangeListenerRecursively(view, + (v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus)); + } + + private void clearEditErrors() + { + mEditName.setError(null); + mEditCode.setError(null); + } + + private void onDeleteClicked(View view) + { + AlertDialog.Builder builder = + new AlertDialog.Builder(requireContext(), R.style.DolphinDialogBase); + builder.setMessage(getString(R.string.cheats_delete_confirmation, mCheat.getName())); + builder.setPositiveButton(R.string.yes, (dialog, i) -> mViewModel.deleteSelectedCheat()); + builder.setNegativeButton(R.string.no, null); + builder.show(); + } + + private void onEditClicked(View view) + { + mViewModel.setIsEditing(true); + mButtonOk.requestFocus(); + } + + private void onCancelClicked(View view) + { + mViewModel.setIsEditing(false); + onSelectedCheatUpdated(mCheat); + mButtonDelete.requestFocus(); + } + + private void onOkClicked(View view) + { + clearEditErrors(); + + int result = mCheat.trySet(mEditName.getText().toString(), mEditCreator.getText().toString(), + mEditNotes.getText().toString(), mEditCode.getText().toString()); + + switch (result) + { + case Cheat.TRY_SET_SUCCESS: + if (mViewModel.getIsAdding().getValue()) + { + mViewModel.finishAddingCheat(); + onSelectedCheatUpdated(mCheat); + } + else + { + mViewModel.notifySelectedCheatChanged(); + mViewModel.setIsEditing(false); + } + mButtonEdit.requestFocus(); + break; + case Cheat.TRY_SET_FAIL_NO_NAME: + mEditName.setError(getString(R.string.cheats_error_no_name)); + mScrollView.smoothScrollTo(0, mLabelName.getTop()); + break; + case Cheat.TRY_SET_FAIL_NO_CODE_LINES: + mEditCode.setError(getString(R.string.cheats_error_no_code_lines)); + mScrollView.smoothScrollTo(0, mEditCode.getBottom()); + break; + case Cheat.TRY_SET_FAIL_CODE_MIXED_ENCRYPTION: + mEditCode.setError(getString(R.string.cheats_error_mixed_encryption)); + mScrollView.smoothScrollTo(0, mEditCode.getBottom()); + break; + default: + mEditCode.setError(getString(R.string.cheats_error_on_line, result)); + mScrollView.smoothScrollTo(0, mEditCode.getBottom()); + break; + } + } + + private void onSelectedCheatUpdated(@Nullable Cheat cheat) + { + clearEditErrors(); + + mRoot.setVisibility(cheat == null ? View.GONE : View.VISIBLE); + + int creatorVisibility = cheat != null && cheat.supportsCreator() ? View.VISIBLE : View.GONE; + int notesVisibility = cheat != null && cheat.supportsNotes() ? View.VISIBLE : View.GONE; + mLabelCreator.setVisibility(creatorVisibility); + mEditCreator.setVisibility(creatorVisibility); + mLabelNotes.setVisibility(notesVisibility); + mEditNotes.setVisibility(notesVisibility); + + boolean userDefined = cheat != null && cheat.getUserDefined(); + mButtonDelete.setEnabled(userDefined); + mButtonEdit.setEnabled(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 + boolean isEditing = mViewModel.getIsEditing().getValue(); + + if (!isEditing && cheat != null) + { + mEditName.setText(cheat.getName()); + mEditCreator.setText(cheat.getCreator()); + mEditNotes.setText(cheat.getNotes()); + mEditCode.setText(cheat.getCode()); + } + + mCheat = cheat; + } + + private void onIsEditingUpdated(boolean isEditing) + { + mEditName.setEnabled(isEditing); + mEditCreator.setEnabled(isEditing); + mEditNotes.setEnabled(isEditing); + mEditCode.setEnabled(isEditing); + + mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE); + mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE); + mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE); + mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItem.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItem.java new file mode 100644 index 0000000000..b293cd84d1 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItem.java @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.cheats.ui; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.dolphinemu.dolphinemu.features.cheats.model.Cheat; + +public class CheatItem +{ + public static final int TYPE_CHEAT = 0; + public static final int TYPE_HEADER = 1; + public static final int TYPE_ACTION = 2; + + private final @Nullable Cheat mCheat; + private final int mString; + private final int mType; + + public CheatItem(@NonNull Cheat cheat) + { + mCheat = cheat; + mString = 0; + mType = TYPE_CHEAT; + } + + public CheatItem(int type, int string) + { + mCheat = null; + mString = string; + mType = type; + } + + @Nullable + public Cheat getCheat() + { + return mCheat; + } + + public int getString() + { + return mString; + } + + public int getType() + { + return mType; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItemViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItemViewHolder.java new file mode 100644 index 0000000000..b4e38f4c1b --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItemViewHolder.java @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.cheats.ui; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel; + +public abstract class CheatItemViewHolder extends RecyclerView.ViewHolder +{ + public CheatItemViewHolder(@NonNull View itemView) + { + super(itemView); + } + + public abstract void bind(CheatsActivity activity, CheatItem item, int position); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java new file mode 100644 index 0000000000..efc446d50e --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java @@ -0,0 +1,43 @@ +// 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.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel; +import org.dolphinemu.dolphinemu.ui.DividerItemDecoration; + +public class CheatListFragment extends Fragment +{ + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) + { + return inflater.inflate(R.layout.fragment_cheat_list, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + RecyclerView recyclerView = view.findViewById(R.id.cheat_list); + + CheatsActivity activity = (CheatsActivity) requireActivity(); + CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); + + recyclerView.setAdapter(new CheatsAdapter(activity, viewModel)); + recyclerView.setLayoutManager(new LinearLayoutManager(activity)); + recyclerView.addItemDecoration(new DividerItemDecoration(activity, null)); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatViewHolder.java new file mode 100644 index 0000000000..58e391fb01 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatViewHolder.java @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.cheats.ui; + +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.cheats.model.Cheat; +import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel; + +public class CheatViewHolder extends CheatItemViewHolder + implements View.OnClickListener, CompoundButton.OnCheckedChangeListener +{ + private final View mRoot; + private final TextView mName; + private final CheckBox mCheckbox; + + private CheatsViewModel mViewModel; + private Cheat mCheat; + private int mPosition; + + public CheatViewHolder(@NonNull View itemView) + { + super(itemView); + + mRoot = itemView.findViewById(R.id.root); + mName = itemView.findViewById(R.id.text_name); + mCheckbox = itemView.findViewById(R.id.checkbox); + } + + public void bind(CheatsActivity activity, CheatItem item, int position) + { + mCheckbox.setOnCheckedChangeListener(null); + + mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); + mCheat = item.getCheat(); + mPosition = position; + + mName.setText(mCheat.getName()); + mCheckbox.setChecked(mCheat.getEnabled()); + + mRoot.setOnClickListener(this); + mCheckbox.setOnCheckedChangeListener(this); + } + + public void onClick(View root) + { + mViewModel.setSelectedCheat(mCheat, mPosition); + mViewModel.openDetailsView(); + } + + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) + { + mCheat.setEnabled(isChecked); + mViewModel.notifyCheatChanged(mPosition); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java new file mode 100644 index 0000000000..df6d9d94b0 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java @@ -0,0 +1,63 @@ +// 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 android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; +import org.dolphinemu.dolphinemu.features.settings.model.Settings; +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; +import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity; + +public class CheatWarningFragment extends Fragment implements View.OnClickListener +{ + private View mView; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) + { + return inflater.inflate(R.layout.fragment_cheat_warning, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + mView = view; + + Button settingsButton = view.findViewById(R.id.button_settings); + settingsButton.setOnClickListener(this); + + CheatsActivity activity = (CheatsActivity) requireActivity(); + CheatsActivity.setOnFocusChangeListenerRecursively(view, + (v, hasFocus) -> activity.onListViewFocusChange(hasFocus)); + } + + @Override + public void onResume() + { + super.onResume(); + + CheatsActivity activity = (CheatsActivity) requireActivity(); + try (Settings settings = activity.loadGameSpecificSettings()) + { + boolean cheatsEnabled = BooleanSetting.MAIN_ENABLE_CHEATS.getBoolean(settings); + mView.setVisibility(cheatsEnabled ? View.GONE : View.VISIBLE); + } + } + + public void onClick(View view) + { + SettingsActivity.launch(requireContext(), MenuTag.CONFIG_GENERAL); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java new file mode 100644 index 0000000000..9eff4d1c9e --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java @@ -0,0 +1,253 @@ +// 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.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.ViewCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.slidingpanelayout.widget.SlidingPaneLayout; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.cheats.model.Cheat; +import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel; +import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat; +import org.dolphinemu.dolphinemu.features.settings.model.Settings; +import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback; +import org.dolphinemu.dolphinemu.ui.main.MainPresenter; + +public class CheatsActivity extends AppCompatActivity + implements SlidingPaneLayout.PanelSlideListener +{ + private static final String ARG_GAME_ID = "game_id"; + private static final String ARG_GAMETDB_ID = "gametdb_id"; + private static final String ARG_REVISION = "revision"; + private static final String ARG_IS_WII = "is_wii"; + + private String mGameId; + private String mGameTdbId; + private int mRevision; + private boolean mIsWii; + private CheatsViewModel mViewModel; + + private SlidingPaneLayout mSlidingPaneLayout; + private View mCheatList; + private View mCheatDetails; + + private View mCheatListLastFocus; + private View mCheatDetailsLastFocus; + + public static void launch(Context context, String gameId, String gameTdbId, int revision, + boolean isWii) + { + Intent intent = new Intent(context, CheatsActivity.class); + 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); + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + MainPresenter.skipRescanningLibrary(); + + Intent intent = getIntent(); + mGameId = intent.getStringExtra(ARG_GAME_ID); + mGameTdbId = intent.getStringExtra(ARG_GAMETDB_ID); + mRevision = intent.getIntExtra(ARG_REVISION, 0); + mIsWii = intent.getBooleanExtra(ARG_IS_WII, true); + + setTitle(getString(R.string.cheats_with_game_id, mGameId)); + + mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class); + mViewModel.load(mGameId, mRevision); + + setContentView(R.layout.activity_cheats); + + mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout); + mCheatList = findViewById(R.id.cheat_list); + mCheatDetails = findViewById(R.id.cheat_details); + + mCheatListLastFocus = mCheatList; + mCheatDetailsLastFocus = mCheatDetails; + + mSlidingPaneLayout.addPanelSlideListener(this); + + getOnBackPressedDispatcher().addCallback(this, + new TwoPaneOnBackPressedCallback(mSlidingPaneLayout)); + + mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged); + onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue()); + + mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_settings, menu); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == R.id.menu_save_exit) + { + finish(); + return true; + } + + return false; + } + + @Override + protected void onStop() + { + super.onStop(); + + mViewModel.saveIfNeeded(mGameId, mRevision); + } + + @Override + public void onPanelSlide(@NonNull View panel, float slideOffset) + { + } + + @Override + public void onPanelOpened(@NonNull View panel) + { + boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL; + mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT); + } + + @Override + public void onPanelClosed(@NonNull View panel) + { + boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL; + mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT); + } + + private void onSelectedCheatChanged(Cheat selectedCheat) + { + boolean cheatSelected = selectedCheat != null; + + if (!cheatSelected && mSlidingPaneLayout.isOpen()) + mSlidingPaneLayout.close(); + + mSlidingPaneLayout.setLockMode(cheatSelected ? + SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED); + } + + public void onListViewFocusChange(boolean hasFocus) + { + if (hasFocus) + { + mCheatListLastFocus = mCheatList.findFocus(); + if (mCheatListLastFocus == null) + throw new NullPointerException(); + + mSlidingPaneLayout.close(); + } + } + + public void onDetailsViewFocusChange(boolean hasFocus) + { + if (hasFocus) + { + mCheatDetailsLastFocus = mCheatDetails.findFocus(); + if (mCheatDetailsLastFocus == null) + throw new NullPointerException(); + + mSlidingPaneLayout.open(); + } + } + + private void openDetailsView(boolean open) + { + if (open) + mSlidingPaneLayout.open(); + } + + public Settings loadGameSpecificSettings() + { + Settings settings = new Settings(); + settings.loadSettings(null, mGameId, mRevision, mIsWii); + return settings; + } + + public void downloadGeckoCodes() + { + AlertDialog progressDialog = new AlertDialog.Builder(this, R.style.DolphinDialogBase).create(); + progressDialog.setTitle(R.string.cheats_downloading); + progressDialog.setCancelable(false); + progressDialog.show(); + + new Thread(() -> + { + GeckoCheat[] codes = GeckoCheat.downloadCodes(mGameTdbId); + + runOnUiThread(() -> + { + progressDialog.dismiss(); + + if (codes == null) + { + new AlertDialog.Builder(this, R.style.DolphinDialogBase) + .setMessage(getString(R.string.cheats_download_failed)) + .setPositiveButton(R.string.ok, null) + .show(); + } + else if (codes.length == 0) + { + new AlertDialog.Builder(this, R.style.DolphinDialogBase) + .setMessage(getString(R.string.cheats_download_empty)) + .setPositiveButton(R.string.ok, null) + .show(); + } + else + { + int cheatsAdded = mViewModel.addDownloadedGeckoCodes(codes); + String message = getString(R.string.cheats_download_succeeded, codes.length, cheatsAdded); + + new AlertDialog.Builder(this, R.style.DolphinDialogBase) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show(); + } + }); + }).start(); + } + + public static void setOnFocusChangeListenerRecursively(@NonNull View view, + View.OnFocusChangeListener listener) + { + view.setOnFocusChangeListener(listener); + + if (view instanceof ViewGroup) + { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) + { + View child = viewGroup.getChildAt(i); + setOnFocusChangeListenerRecursively(child, listener); + } + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java new file mode 100644 index 0000000000..5994feab2a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java @@ -0,0 +1,162 @@ +// 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.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; + +import org.dolphinemu.dolphinemu.R; +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; + +import java.util.ArrayList; + +public class CheatsAdapter extends RecyclerView.Adapter +{ + private final CheatsActivity mActivity; + private final CheatsViewModel mViewModel; + + public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) + { + mActivity = activity; + mViewModel = viewModel; + + mViewModel.getCheatAddedEvent().observe(activity, (position) -> + { + if (position != null) + notifyItemInserted(position); + }); + + mViewModel.getCheatChangedEvent().observe(activity, (position) -> + { + if (position != null) + notifyItemChanged(position); + }); + + mViewModel.getCheatDeletedEvent().observe(activity, (position) -> + { + if (position != null) + notifyItemRemoved(position); + }); + + mViewModel.getGeckoCheatsDownloadedEvent().observe(activity, (cheatsAdded) -> + { + if (cheatsAdded != null) + { + int positionEnd = getItemCount() - 2; // Skip "Add Gecko Code" and "Download Gecko Codes" + int positionStart = positionEnd - cheatsAdded; + notifyItemRangeInserted(positionStart, cheatsAdded); + } + }); + } + + @NonNull + @Override + public CheatItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) + { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + + switch (viewType) + { + case CheatItem.TYPE_CHEAT: + View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false); + addViewListeners(cheatView); + return new CheatViewHolder(cheatView); + case CheatItem.TYPE_HEADER: + View headerView = inflater.inflate(R.layout.list_item_header, parent, false); + addViewListeners(headerView); + return new HeaderViewHolder(headerView); + case CheatItem.TYPE_ACTION: + View actionView = inflater.inflate(R.layout.list_item_submenu, parent, false); + addViewListeners(actionView); + return new ActionViewHolder(actionView); + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public void onBindViewHolder(@NonNull CheatItemViewHolder holder, int position) + { + holder.bind(mActivity, getItemAt(position), position); + } + + @Override + public int getItemCount() + { + return mViewModel.getARCheats().size() + mViewModel.getGeckoCheats().size() + + mViewModel.getPatchCheats().size() + 7; + } + + @Override + public int getItemViewType(int position) + { + return getItemAt(position).getType(); + } + + private void addViewListeners(View view) + { + CheatsActivity.setOnFocusChangeListenerRecursively(view, + (v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus)); + } + + private CheatItem getItemAt(int position) + { + // Patches + + if (position == 0) + return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_patch); + position -= 1; + + ArrayList patchCheats = mViewModel.getPatchCheats(); + if (position < patchCheats.size()) + return new CheatItem(patchCheats.get(position)); + position -= patchCheats.size(); + + if (position == 0) + return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_patch); + position -= 1; + + // AR codes + + if (position == 0) + return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_ar); + position -= 1; + + ArrayList arCheats = mViewModel.getARCheats(); + if (position < arCheats.size()) + return new CheatItem(arCheats.get(position)); + position -= arCheats.size(); + + if (position == 0) + return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_ar); + position -= 1; + + // Gecko codes + + if (position == 0) + return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_gecko); + position -= 1; + + ArrayList geckoCheats = mViewModel.getGeckoCheats(); + if (position < geckoCheats.size()) + return new CheatItem(geckoCheats.get(position)); + position -= geckoCheats.size(); + + if (position == 0) + return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_gecko); + position -= 1; + + if (position == 0) + return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_download_gecko); + + throw new IndexOutOfBoundsException(); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/HeaderViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/HeaderViewHolder.java new file mode 100644 index 0000000000..57c0edebb0 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/HeaderViewHolder.java @@ -0,0 +1,28 @@ +// 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.annotation.NonNull; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel; + +public class HeaderViewHolder extends CheatItemViewHolder +{ + private TextView mHeaderName; + + public HeaderViewHolder(@NonNull View itemView) + { + super(itemView); + + mHeaderName = itemView.findViewById(R.id.text_header_name); + } + + public void bind(CheatsActivity activity, CheatItem item, int position) + { + mHeaderName.setText(item.getString()); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java index bd71683c1f..480f69937f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java @@ -14,6 +14,7 @@ public enum BooleanSetting implements AbstractBooleanSetting MAIN_FASTMEM(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "Fastmem", true), MAIN_CPU_THREAD(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "CPUThread", true), MAIN_SYNC_ON_SKIP_IDLE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "SyncOnSkipIdle", true), + MAIN_ENABLE_CHEATS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "EnableCheats", false), MAIN_OVERRIDE_REGION_SETTINGS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "OverrideRegionSettings", false), MAIN_AUDIO_STRETCH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "AudioStretch", false), @@ -198,6 +199,7 @@ public enum BooleanSetting implements AbstractBooleanSetting private static final BooleanSetting[] NOT_RUNTIME_EDITABLE_ARRAY = new BooleanSetting[]{ MAIN_DSP_HLE, MAIN_CPU_THREAD, + MAIN_ENABLE_CHEATS, MAIN_OVERRIDE_REGION_SETTINGS, MAIN_WII_SD_CARD, // Can actually be changed, but specific code is required MAIN_DSP_JIT diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java index 85e26ef6ca..753dddf522 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java @@ -18,9 +18,7 @@ import androidx.recyclerview.widget.RecyclerView; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.dialogs.MotionAlertDialog; -import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting; import org.dolphinemu.dolphinemu.features.settings.model.Settings; -import org.dolphinemu.dolphinemu.features.settings.model.StringSetting; import org.dolphinemu.dolphinemu.features.settings.model.view.CheckBoxSetting; import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker; import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting; @@ -43,7 +41,6 @@ import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHold import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder; import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder; import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHolder; -import org.dolphinemu.dolphinemu.ui.main.MainPresenter; import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.Log; @@ -51,10 +48,7 @@ import org.dolphinemu.dolphinemu.utils.Log; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; -import java.security.InvalidParameterException; import java.util.ArrayList; -import java.util.HashSet; -import java.util.Map; public final class SettingsAdapter extends RecyclerView.Adapter implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener @@ -87,7 +81,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter + + + + + + + diff --git a/Source/Android/app/src/main/res/layout/activity_cheats.xml b/Source/Android/app/src/main/res/layout/activity_cheats.xml new file mode 100644 index 0000000000..0f89b488e1 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/activity_cheats.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/Source/Android/app/src/main/res/layout/fragment_cheat_details.xml b/Source/Android/app/src/main/res/layout/fragment_cheat_details.xml new file mode 100644 index 0000000000..c758b919ba --- /dev/null +++ b/Source/Android/app/src/main/res/layout/fragment_cheat_details.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +