mirror of
https://github.com/izzy2lost/dolphin.git
synced 2026-06-19 01:16:48 -07:00
Merge pull request #10092 from JosJuice/android-cheats
Android: Add cheat GUI
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
android:theme="@style/DolphinSettingsBase"
|
||||
android:label="@string/settings"/>
|
||||
|
||||
<activity
|
||||
android:name=".features.cheats.ui.CheatsActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/DolphinSettingsBase"
|
||||
android:label="@string/cheats"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.EmulationActivity"
|
||||
android:exported="false"
|
||||
|
||||
+10
-3
@@ -12,6 +12,7 @@ import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.dolphinemu.dolphinemu.R;
|
||||
import org.dolphinemu.dolphinemu.activities.ConvertActivity;
|
||||
import org.dolphinemu.dolphinemu.features.cheats.ui.CheatsActivity;
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
|
||||
@@ -28,7 +29,8 @@ public class GamePropertiesDialog extends DialogFragment
|
||||
{
|
||||
public static final String TAG = "GamePropertiesDialog";
|
||||
private static final String ARG_PATH = "path";
|
||||
private static final String ARG_GAMEID = "game_id";
|
||||
private static final String ARG_GAME_ID = "game_id";
|
||||
private static final String ARG_GAMETDB_ID = "gametdb_id";
|
||||
public static final String ARG_REVISION = "revision";
|
||||
private static final String ARG_PLATFORM = "platform";
|
||||
private static final String ARG_SHOULD_ALLOW_CONVERSION = "should_allow_conversion";
|
||||
@@ -39,7 +41,8 @@ public class GamePropertiesDialog extends DialogFragment
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString(ARG_PATH, gameFile.getPath());
|
||||
arguments.putString(ARG_GAMEID, gameFile.getGameId());
|
||||
arguments.putString(ARG_GAME_ID, gameFile.getGameId());
|
||||
arguments.putString(ARG_GAMETDB_ID, gameFile.getGameTdbId());
|
||||
arguments.putInt(ARG_REVISION, gameFile.getRevision());
|
||||
arguments.putInt(ARG_PLATFORM, gameFile.getPlatform());
|
||||
arguments.putBoolean(ARG_SHOULD_ALLOW_CONVERSION, gameFile.shouldAllowConversion());
|
||||
@@ -53,7 +56,8 @@ public class GamePropertiesDialog extends DialogFragment
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState)
|
||||
{
|
||||
final String path = requireArguments().getString(ARG_PATH);
|
||||
final String gameId = requireArguments().getString(ARG_GAMEID);
|
||||
final String gameId = requireArguments().getString(ARG_GAME_ID);
|
||||
final String gameTdbId = requireArguments().getString(ARG_GAMETDB_ID);
|
||||
final int revision = requireArguments().getInt(ARG_REVISION);
|
||||
final int platform = requireArguments().getInt(ARG_PLATFORM);
|
||||
final boolean shouldAllowConversion =
|
||||
@@ -91,6 +95,9 @@ public class GamePropertiesDialog extends DialogFragment
|
||||
itemsBuilder.add(R.string.properties_edit_game_settings, (dialog, i) ->
|
||||
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));
|
||||
|
||||
|
||||
+60
@@ -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);
|
||||
}
|
||||
+62
@@ -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);
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
+297
@@ -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<Cheat> mSelectedCheat = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
|
||||
|
||||
private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Integer> mGeckoCheatsDownloadedEvent = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
|
||||
|
||||
private ArrayList<PatchCheat> mPatchCheats;
|
||||
private ArrayList<ARCheat> mARCheats;
|
||||
private ArrayList<GeckoCheat> 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<Cheat> getSelectedCheat()
|
||||
{
|
||||
return mSelectedCheat;
|
||||
}
|
||||
|
||||
public void setSelectedCheat(Cheat cheat, int position)
|
||||
{
|
||||
if (mIsEditing.getValue())
|
||||
setIsEditing(false);
|
||||
|
||||
mSelectedCheat.setValue(cheat);
|
||||
mSelectedCheatPosition = position;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> 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<Boolean> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Boolean> getOpenDetailsViewEvent()
|
||||
{
|
||||
return mOpenDetailsViewEvent;
|
||||
}
|
||||
|
||||
public void openDetailsView()
|
||||
{
|
||||
mOpenDetailsViewEvent.setValue(true);
|
||||
mOpenDetailsViewEvent.setValue(false);
|
||||
}
|
||||
|
||||
public ArrayList<PatchCheat> getPatchCheats()
|
||||
{
|
||||
return mPatchCheats;
|
||||
}
|
||||
|
||||
public ArrayList<ARCheat> getARCheats()
|
||||
{
|
||||
return mARCheats;
|
||||
}
|
||||
|
||||
public ArrayList<GeckoCheat> getGeckoCheats()
|
||||
{
|
||||
return mGeckoCheats;
|
||||
}
|
||||
}
|
||||
+78
@@ -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);
|
||||
}
|
||||
+60
@@ -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);
|
||||
}
|
||||
+67
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+198
@@ -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);
|
||||
}
|
||||
}
|
||||
+49
@@ -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;
|
||||
}
|
||||
}
|
||||
+20
@@ -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);
|
||||
}
|
||||
+43
@@ -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));
|
||||
}
|
||||
}
|
||||
+64
@@ -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);
|
||||
}
|
||||
}
|
||||
+63
@@ -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);
|
||||
}
|
||||
}
|
||||
+253
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+162
@@ -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<CheatItemViewHolder>
|
||||
{
|
||||
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<PatchCheat> 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<ARCheat> 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<GeckoCheat> 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();
|
||||
}
|
||||
}
|
||||
+28
@@ -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());
|
||||
}
|
||||
}
|
||||
+2
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user