android: Add cheat GUI
Based on https://github.com/dolphin-emu/dolphin/pull/10092, with adaptations made for differences in how Citra handles cheats. You can access the cheat GUI while a game is running.
This commit is contained in:
parent
a51b1cd3cd
commit
5180122506
26 changed files with 1328 additions and 12 deletions
|
@ -10,7 +10,7 @@ def buildType
|
||||||
def abiFilter = "arm64-v8a" //, "x86"
|
def abiFilter = "arm64-v8a" //, "x86"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 31
|
||||||
ndkVersion "23.1.7779620"
|
ndkVersion "23.1.7779620"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
@ -109,6 +109,10 @@ dependencies {
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
|
||||||
|
implementation 'androidx.fragment:fragment:1.5.1'
|
||||||
|
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||||
implementation 'com.google.android.material:material:1.1.0'
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
|
|
||||||
// For loading huge screenshots from the disk.
|
// For loading huge screenshots from the disk.
|
||||||
|
|
|
@ -68,6 +68,12 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/CitraSettingsBase"
|
||||||
|
android:label="@string/cheats"/>
|
||||||
|
|
||||||
<service android:name="org.citra.citra_emu.utils.DirectoryInitialization"/>
|
<service android:name="org.citra.citra_emu.utils.DirectoryInitialization"/>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
@ -30,6 +30,7 @@ import androidx.fragment.app.FragmentActivity;
|
||||||
import org.citra.citra_emu.CitraApplication;
|
import org.citra.citra_emu.CitraApplication;
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
||||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||||
|
@ -72,6 +73,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||||
public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
|
public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
|
||||||
public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
|
public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
|
||||||
public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
|
public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
|
||||||
|
public static final int MENU_ACTION_OPEN_CHEATS = 17;
|
||||||
|
|
||||||
public static final int REQUEST_SELECT_AMIIBO = 2;
|
public static final int REQUEST_SELECT_AMIIBO = 2;
|
||||||
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
||||||
|
@ -110,6 +112,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||||
EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER);
|
EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER);
|
||||||
buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable,
|
buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable,
|
||||||
EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
|
EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
|
||||||
|
buttonsActionsMap
|
||||||
|
.append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private View mDecorView;
|
private View mDecorView;
|
||||||
|
@ -466,11 +470,16 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||||
EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled);
|
EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled);
|
||||||
item.setChecked(isJoystickRelCenterEnabled);
|
item.setChecked(isJoystickRelCenterEnabled);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MENU_ACTION_DPAD_SLIDE_ENABLE:
|
case MENU_ACTION_DPAD_SLIDE_ENABLE:
|
||||||
final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable();
|
final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable();
|
||||||
EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled);
|
EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled);
|
||||||
item.setChecked(isDpadSlideEnabled);
|
item.setChecked(isDpadSlideEnabled);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MENU_ACTION_OPEN_CHEATS:
|
||||||
|
CheatsActivity.launch(this);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.model;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public class Cheat {
|
||||||
|
@Keep
|
||||||
|
private final long mPointer;
|
||||||
|
|
||||||
|
private Runnable mEnabledChangedCallback = null;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
private Cheat(long pointer) {
|
||||||
|
mPointer = pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected native void finalize();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public native String getName();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public native String getNotes();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public native String getCode();
|
||||||
|
|
||||||
|
public native boolean getEnabled();
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
setEnabledImpl(enabled);
|
||||||
|
onEnabledChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private native void setEnabledImpl(boolean enabled);
|
||||||
|
|
||||||
|
public void setEnabledChangedCallback(@Nullable Runnable callback) {
|
||||||
|
mEnabledChangedCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onEnabledChanged() {
|
||||||
|
if (mEnabledChangedCallback != null) {
|
||||||
|
mEnabledChangedCallback.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the code is valid, returns 0. Otherwise, returns the 1-based index
|
||||||
|
* for the line containing the error.
|
||||||
|
*/
|
||||||
|
public static native int isValidGatewayCode(@NonNull String code);
|
||||||
|
|
||||||
|
public static native Cheat createGatewayCode(@NonNull String name, @NonNull String notes,
|
||||||
|
@NonNull String code);
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.model;
|
||||||
|
|
||||||
|
public class CheatEngine {
|
||||||
|
public static native Cheat[] getCheats();
|
||||||
|
|
||||||
|
public static native void addCheat(Cheat cheat);
|
||||||
|
|
||||||
|
public static native void removeCheat(int index);
|
||||||
|
|
||||||
|
public static native void updateCheat(int index, Cheat newCheat);
|
||||||
|
|
||||||
|
public static native void saveCheatFile();
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.model;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public class CheatsViewModel extends ViewModel {
|
||||||
|
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<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private Cheat[] mCheats;
|
||||||
|
private boolean mCheatsNeedSaving = false;
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
mCheats = CheatEngine.getCheats();
|
||||||
|
|
||||||
|
for (int i = 0; i < mCheats.length; i++) {
|
||||||
|
int position = i;
|
||||||
|
mCheats[i].setEnabledChangedCallback(() -> {
|
||||||
|
mCheatsNeedSaving = true;
|
||||||
|
notifyCheatUpdated(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveIfNeeded() {
|
||||||
|
if (mCheatsNeedSaving) {
|
||||||
|
CheatEngine.saveCheatFile();
|
||||||
|
mCheatsNeedSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cheat[] getCheats() {
|
||||||
|
return mCheats;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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(int position) {
|
||||||
|
mCheatAddedEvent.setValue(position);
|
||||||
|
mCheatAddedEvent.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startAddingCheat() {
|
||||||
|
mSelectedCheat.setValue(null);
|
||||||
|
mSelectedCheatPosition = -1;
|
||||||
|
|
||||||
|
mIsAdding.setValue(true);
|
||||||
|
mIsEditing.setValue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void finishAddingCheat(Cheat cheat) {
|
||||||
|
if (!mIsAdding.getValue()) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
mIsAdding.setValue(false);
|
||||||
|
mIsEditing.setValue(false);
|
||||||
|
|
||||||
|
int position = mCheats.length;
|
||||||
|
|
||||||
|
CheatEngine.addCheat(cheat);
|
||||||
|
|
||||||
|
mCheatsNeedSaving = true;
|
||||||
|
load();
|
||||||
|
|
||||||
|
notifyCheatAdded(position);
|
||||||
|
setSelectedCheat(mCheats[position], position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> getCheatUpdatedEvent() {
|
||||||
|
return mCheatChangedEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that an edit has been made to the contents of the cheat at the given position.
|
||||||
|
*/
|
||||||
|
private void notifyCheatUpdated(int position) {
|
||||||
|
mCheatChangedEvent.setValue(position);
|
||||||
|
mCheatChangedEvent.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSelectedCheat(Cheat newCheat) {
|
||||||
|
CheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
|
||||||
|
|
||||||
|
mCheatsNeedSaving = true;
|
||||||
|
load();
|
||||||
|
|
||||||
|
notifyCheatUpdated(mSelectedCheatPosition);
|
||||||
|
setSelectedCheat(mCheats[mSelectedCheatPosition], mSelectedCheatPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that the cheat at the given position has been deleted.
|
||||||
|
*/
|
||||||
|
private void notifyCheatDeleted(int position) {
|
||||||
|
mCheatDeletedEvent.setValue(position);
|
||||||
|
mCheatDeletedEvent.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteSelectedCheat() {
|
||||||
|
int position = mSelectedCheatPosition;
|
||||||
|
|
||||||
|
setSelectedCheat(null, -1);
|
||||||
|
|
||||||
|
CheatEngine.removeCheat(position);
|
||||||
|
|
||||||
|
mCheatsNeedSaving = true;
|
||||||
|
load();
|
||||||
|
|
||||||
|
notifyCheatDeleted(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> getOpenDetailsViewEvent() {
|
||||||
|
return mOpenDetailsViewEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openDetailsView() {
|
||||||
|
mOpenDetailsViewEvent.setValue(true);
|
||||||
|
mOpenDetailsViewEvent.setValue(false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package org.citra.citra_emu.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.ViewModelProvider;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
|
||||||
|
public class CheatDetailsFragment extends Fragment {
|
||||||
|
private View mRoot;
|
||||||
|
private ScrollView mScrollView;
|
||||||
|
private TextView mLabelName;
|
||||||
|
private EditText mEditName;
|
||||||
|
private EditText mEditNotes;
|
||||||
|
private EditText mEditCode;
|
||||||
|
private Button mButtonDelete;
|
||||||
|
private Button mButtonEdit;
|
||||||
|
private Button mButtonCancel;
|
||||||
|
private Button mButtonOk;
|
||||||
|
|
||||||
|
private CheatsViewModel mViewModel;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
||||||
|
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
||||||
|
// in the currently hidden pane, we need to manually show that pane.
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
||||||
|
(v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearEditErrors() {
|
||||||
|
mEditName.setError(null);
|
||||||
|
mEditCode.setError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDeleteClicked(View view) {
|
||||||
|
String name = mEditName.getText().toString();
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||||
|
builder.setMessage(getString(R.string.cheats_delete_confirmation, name));
|
||||||
|
builder.setPositiveButton(android.R.string.yes,
|
||||||
|
(dialog, i) -> mViewModel.deleteSelectedCheat());
|
||||||
|
builder.setNegativeButton(android.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(mViewModel.getSelectedCheat().getValue());
|
||||||
|
mButtonDelete.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onOkClicked(View view) {
|
||||||
|
clearEditErrors();
|
||||||
|
|
||||||
|
String name = mEditName.getText().toString();
|
||||||
|
String notes = mEditNotes.getText().toString();
|
||||||
|
String code = mEditCode.getText().toString();
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
mEditName.setError(getString(R.string.cheats_error_no_name));
|
||||||
|
mScrollView.smoothScrollTo(0, mLabelName.getTop());
|
||||||
|
return;
|
||||||
|
} else if (code.isEmpty()) {
|
||||||
|
mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
|
||||||
|
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int validityResult = Cheat.isValidGatewayCode(code);
|
||||||
|
|
||||||
|
if (validityResult != 0) {
|
||||||
|
mEditCode.setError(getString(R.string.cheats_error_on_line, validityResult));
|
||||||
|
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cheat newCheat = Cheat.createGatewayCode(name, notes, code);
|
||||||
|
|
||||||
|
if (mViewModel.getIsAdding().getValue()) {
|
||||||
|
mViewModel.finishAddingCheat(newCheat);
|
||||||
|
} else {
|
||||||
|
mViewModel.updateSelectedCheat(newCheat);
|
||||||
|
}
|
||||||
|
|
||||||
|
mButtonEdit.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSelectedCheatUpdated(@Nullable Cheat cheat) {
|
||||||
|
clearEditErrors();
|
||||||
|
|
||||||
|
boolean isEditing = mViewModel.getIsEditing().getValue();
|
||||||
|
|
||||||
|
mRoot.setVisibility(isEditing || cheat != null ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (!isEditing) {
|
||||||
|
if (cheat == null) {
|
||||||
|
mEditName.setText("");
|
||||||
|
mEditNotes.setText("");
|
||||||
|
mEditCode.setText("");
|
||||||
|
} else {
|
||||||
|
mEditName.setText(cheat.getName());
|
||||||
|
mEditNotes.setText(cheat.getNotes());
|
||||||
|
mEditCode.setText(cheat.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onIsEditingUpdated(boolean isEditing) {
|
||||||
|
if (isEditing) {
|
||||||
|
mRoot.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
mEditName.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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package org.citra.citra_emu.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 com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
import org.citra.citra_emu.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);
|
||||||
|
FloatingActionButton fab = view.findViewById(R.id.fab);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
fab.setOnClickListener(v -> {
|
||||||
|
viewModel.startAddingCheat();
|
||||||
|
viewModel.openDetailsView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package org.citra.citra_emu.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;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
|
||||||
|
public class CheatViewHolder extends RecyclerView.ViewHolder
|
||||||
|
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, Cheat cheat, int position) {
|
||||||
|
mCheckbox.setOnCheckedChangeListener(null);
|
||||||
|
|
||||||
|
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
||||||
|
mCheat = cheat;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package org.citra.citra_emu.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.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
|
||||||
|
|
||||||
|
public class CheatsActivity extends AppCompatActivity
|
||||||
|
implements SlidingPaneLayout.PanelSlideListener {
|
||||||
|
private CheatsViewModel mViewModel;
|
||||||
|
|
||||||
|
private SlidingPaneLayout mSlidingPaneLayout;
|
||||||
|
private View mCheatList;
|
||||||
|
private View mCheatDetails;
|
||||||
|
|
||||||
|
private View mCheatListLastFocus;
|
||||||
|
private View mCheatDetailsLastFocus;
|
||||||
|
|
||||||
|
public static void launch(Context context) {
|
||||||
|
Intent intent = new Intent(context, CheatsActivity.class);
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
|
||||||
|
mViewModel.load();
|
||||||
|
|
||||||
|
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);
|
||||||
|
mViewModel.getIsEditing().observe(this, this::onIsEditingChanged);
|
||||||
|
onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
|
||||||
|
|
||||||
|
mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
|
||||||
|
|
||||||
|
// Show "Up" button in the action bar for navigation
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
MenuInflater inflater = getMenuInflater();
|
||||||
|
inflater.inflate(R.menu.menu_settings, menu);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
mViewModel.saveIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 onIsEditingChanged(boolean isEditing) {
|
||||||
|
if (isEditing) {
|
||||||
|
mSlidingPaneLayout.setLockMode(SlidingPaneLayout.LOCK_MODE_UNLOCKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSelectedCheatChanged(Cheat selectedCheat) {
|
||||||
|
boolean cheatSelected = selectedCheat != null || mViewModel.getIsEditing().getValue();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSupportNavigateUp() {
|
||||||
|
onBackPressed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openDetailsView(boolean open) {
|
||||||
|
if (open) {
|
||||||
|
mSlidingPaneLayout.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.ui;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
|
||||||
|
public class CheatsAdapter extends RecyclerView.Adapter<CheatViewHolder> {
|
||||||
|
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.getCheatUpdatedEvent().observe(activity, (position) -> {
|
||||||
|
if (position != null) {
|
||||||
|
notifyItemChanged(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mViewModel.getCheatDeletedEvent().observe(activity, (position) -> {
|
||||||
|
if (position != null) {
|
||||||
|
notifyItemRemoved(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public CheatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||||
|
|
||||||
|
View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
|
||||||
|
addViewListeners(cheatView);
|
||||||
|
return new CheatViewHolder(cheatView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull CheatViewHolder holder, int position) {
|
||||||
|
holder.bind(mActivity, getItemAt(position), position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return mViewModel.getCheats().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addViewListeners(View view) {
|
||||||
|
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
||||||
|
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
||||||
|
// in the currently hidden pane, we need to manually show that pane.
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
||||||
|
(v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cheat getItemAt(int position) {
|
||||||
|
return mViewModel.getCheats()[position];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package org.citra.citra_emu.ui;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.activity.OnBackPressedCallback;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
|
||||||
|
|
||||||
|
public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
|
||||||
|
implements SlidingPaneLayout.PanelSlideListener {
|
||||||
|
private final SlidingPaneLayout mSlidingPaneLayout;
|
||||||
|
|
||||||
|
public TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) {
|
||||||
|
super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen());
|
||||||
|
mSlidingPaneLayout = slidingPaneLayout;
|
||||||
|
slidingPaneLayout.addPanelSlideListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleOnBackPressed() {
|
||||||
|
mSlidingPaneLayout.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelSlide(@NonNull View panel, float slideOffset) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelOpened(@NonNull View panel) {
|
||||||
|
setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelClosed(@NonNull View panel) {
|
||||||
|
setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,9 @@ add_library(citra-android SHARED
|
||||||
camera/ndk_camera.h
|
camera/ndk_camera.h
|
||||||
camera/still_image_camera.cpp
|
camera/still_image_camera.cpp
|
||||||
camera/still_image_camera.h
|
camera/still_image_camera.h
|
||||||
|
cheats/cheat.cpp
|
||||||
|
cheats/cheat.h
|
||||||
|
cheats/cheat_engine.cpp
|
||||||
config.cpp
|
config.cpp
|
||||||
config.h
|
config.h
|
||||||
default_ini.h
|
default_ini.h
|
||||||
|
|
84
src/android/app/src/main/jni/cheats/cheat.cpp
Normal file
84
src/android/app/src/main/jni/cheats/cheat.cpp
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2022 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "jni/cheats/cheat.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "core/cheats/cheat_base.h"
|
||||||
|
#include "core/cheats/gateway_cheat.h"
|
||||||
|
#include "jni/android_common/android_common.h"
|
||||||
|
#include "jni/id_cache.h"
|
||||||
|
|
||||||
|
std::shared_ptr<Cheats::CheatBase>* CheatFromJava(JNIEnv* env, jobject cheat) {
|
||||||
|
return reinterpret_cast<std::shared_ptr<Cheats::CheatBase>*>(
|
||||||
|
env->GetLongField(cheat, IDCache::GetCheatPointer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
jobject CheatToJava(JNIEnv* env, std::shared_ptr<Cheats::CheatBase> cheat) {
|
||||||
|
return env->NewObject(
|
||||||
|
IDCache::GetCheatClass(), IDCache::GetCheatConstructor(),
|
||||||
|
reinterpret_cast<jlong>(new std::shared_ptr<Cheats::CheatBase>(std::move(cheat))));
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_Cheat_finalize(JNIEnv* env,
|
||||||
|
jobject obj) {
|
||||||
|
delete CheatFromJava(env, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_org_citra_citra_1emu_features_cheats_model_Cheat_getName(JNIEnv* env, jobject obj) {
|
||||||
|
return ToJString(env, (*CheatFromJava(env, obj))->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_org_citra_citra_1emu_features_cheats_model_Cheat_getNotes(JNIEnv* env, jobject obj) {
|
||||||
|
return ToJString(env, (*CheatFromJava(env, obj))->GetComments());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_org_citra_citra_1emu_features_cheats_model_Cheat_getCode(JNIEnv* env, jobject obj) {
|
||||||
|
return ToJString(env, (*CheatFromJava(env, obj))->GetCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_org_citra_citra_1emu_features_cheats_model_Cheat_getEnabled(JNIEnv* env, jobject obj) {
|
||||||
|
return static_cast<jboolean>((*CheatFromJava(env, obj))->IsEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_Cheat_setEnabledImpl(
|
||||||
|
JNIEnv* env, jobject obj, jboolean j_enabled) {
|
||||||
|
(*CheatFromJava(env, obj))->SetEnabled(static_cast<bool>(j_enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_features_cheats_model_Cheat_isValidGatewayCode(
|
||||||
|
JNIEnv* env, jclass, jstring j_code) {
|
||||||
|
const std::string code = GetJString(env, j_code);
|
||||||
|
std::vector<std::string> code_lines;
|
||||||
|
Common::SplitString(code, '\n', code_lines);
|
||||||
|
|
||||||
|
for (int i = 0; i < code_lines.size(); ++i) {
|
||||||
|
Cheats::GatewayCheat::CheatLine cheat_line(code_lines[i]);
|
||||||
|
if (!cheat_line.valid) {
|
||||||
|
return i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_features_cheats_model_Cheat_createGatewayCode(
|
||||||
|
JNIEnv* env, jclass, jstring j_name, jstring j_notes, jstring j_code) {
|
||||||
|
return CheatToJava(env, std::make_shared<Cheats::GatewayCheat>(GetJString(env, j_name),
|
||||||
|
GetJString(env, j_code),
|
||||||
|
GetJString(env, j_notes)));
|
||||||
|
}
|
||||||
|
}
|
14
src/android/app/src/main/jni/cheats/cheat.h
Normal file
14
src/android/app/src/main/jni/cheats/cheat.h
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright 2022 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
namespace Cheats {
|
||||||
|
class CheatBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Cheats::CheatBase>* CheatFromJava(JNIEnv* env, jobject cheat);
|
||||||
|
jobject CheatToJava(JNIEnv* env, std::shared_ptr<Cheats::CheatBase> cheat);
|
51
src/android/app/src/main/jni/cheats/cheat_engine.cpp
Normal file
51
src/android/app/src/main/jni/cheats/cheat_engine.cpp
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright 2022 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "core/cheats/cheat_base.h"
|
||||||
|
#include "core/cheats/cheats.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "jni/cheats/cheat.h"
|
||||||
|
#include "jni/id_cache.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
JNIEXPORT jobjectArray JNICALL
|
||||||
|
Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_getCheats(JNIEnv* env, jclass) {
|
||||||
|
auto cheats = Core::System::GetInstance().CheatEngine().GetCheats();
|
||||||
|
|
||||||
|
const jobjectArray array =
|
||||||
|
env->NewObjectArray(static_cast<jsize>(cheats.size()), IDCache::GetCheatClass(), nullptr);
|
||||||
|
|
||||||
|
jsize i = 0;
|
||||||
|
for (auto& cheat : cheats)
|
||||||
|
env->SetObjectArrayElement(array, i++, CheatToJava(env, std::move(cheat)));
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_addCheat(
|
||||||
|
JNIEnv* env, jclass, jobject j_cheat) {
|
||||||
|
Core::System::GetInstance().CheatEngine().AddCheat(*CheatFromJava(env, j_cheat));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_removeCheat(
|
||||||
|
JNIEnv* env, jclass, jint index) {
|
||||||
|
Core::System::GetInstance().CheatEngine().RemoveCheat(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_updateCheat(
|
||||||
|
JNIEnv* env, jclass, jint index, jobject j_new_cheat) {
|
||||||
|
Core::System::GetInstance().CheatEngine().UpdateCheat(index, *CheatFromJava(env, j_new_cheat));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_saveCheatFile(JNIEnv* env, jclass) {
|
||||||
|
Core::System::GetInstance().CheatEngine().SaveCheatFile();
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,11 +18,12 @@ static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||||
|
|
||||||
static JavaVM* s_java_vm;
|
static JavaVM* s_java_vm;
|
||||||
|
|
||||||
static jclass s_native_library_class;
|
|
||||||
static jclass s_core_error_class;
|
static jclass s_core_error_class;
|
||||||
static jclass s_savestate_info_class;
|
static jclass s_savestate_info_class;
|
||||||
static jclass s_disk_cache_progress_class;
|
static jclass s_disk_cache_progress_class;
|
||||||
static jclass s_load_callback_stage_class;
|
static jclass s_load_callback_stage_class;
|
||||||
|
|
||||||
|
static jclass s_native_library_class;
|
||||||
static jmethodID s_on_core_error;
|
static jmethodID s_on_core_error;
|
||||||
static jmethodID s_display_alert_msg;
|
static jmethodID s_display_alert_msg;
|
||||||
static jmethodID s_display_alert_prompt;
|
static jmethodID s_display_alert_prompt;
|
||||||
|
@ -34,6 +35,10 @@ static jmethodID s_request_camera_permission;
|
||||||
static jmethodID s_request_mic_permission;
|
static jmethodID s_request_mic_permission;
|
||||||
static jmethodID s_disk_cache_load_progress;
|
static jmethodID s_disk_cache_load_progress;
|
||||||
|
|
||||||
|
static jclass s_cheat_class;
|
||||||
|
static jfieldID s_cheat_pointer;
|
||||||
|
static jmethodID s_cheat_constructor;
|
||||||
|
|
||||||
static std::unordered_map<VideoCore::LoadCallbackStage, jobject> s_java_load_callback_stages;
|
static std::unordered_map<VideoCore::LoadCallbackStage, jobject> s_java_load_callback_stages;
|
||||||
|
|
||||||
namespace IDCache {
|
namespace IDCache {
|
||||||
|
@ -57,10 +62,6 @@ JNIEnv* GetEnvForThread() {
|
||||||
return owned.env;
|
return owned.env;
|
||||||
}
|
}
|
||||||
|
|
||||||
jclass GetNativeLibraryClass() {
|
|
||||||
return s_native_library_class;
|
|
||||||
}
|
|
||||||
|
|
||||||
jclass GetCoreErrorClass() {
|
jclass GetCoreErrorClass() {
|
||||||
return s_core_error_class;
|
return s_core_error_class;
|
||||||
}
|
}
|
||||||
|
@ -77,6 +78,10 @@ jclass GetDiskCacheLoadCallbackStageClass() {
|
||||||
return s_load_callback_stage_class;
|
return s_load_callback_stage_class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetNativeLibraryClass() {
|
||||||
|
return s_native_library_class;
|
||||||
|
}
|
||||||
|
|
||||||
jmethodID GetOnCoreError() {
|
jmethodID GetOnCoreError() {
|
||||||
return s_on_core_error;
|
return s_on_core_error;
|
||||||
}
|
}
|
||||||
|
@ -117,6 +122,18 @@ jmethodID GetDiskCacheLoadProgress() {
|
||||||
return s_disk_cache_load_progress;
|
return s_disk_cache_load_progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetCheatClass() {
|
||||||
|
return s_cheat_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jfieldID GetCheatPointer() {
|
||||||
|
return s_cheat_pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetCheatConstructor() {
|
||||||
|
return s_cheat_constructor;
|
||||||
|
}
|
||||||
|
|
||||||
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) {
|
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) {
|
||||||
const auto it = s_java_load_callback_stages.find(stage);
|
const auto it = s_java_load_callback_stages.find(stage);
|
||||||
ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage);
|
ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage);
|
||||||
|
@ -147,9 +164,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
FileUtil::GetUserPath(FileUtil::UserPath::LogDir) + LOG_FILE));
|
FileUtil::GetUserPath(FileUtil::UserPath::LogDir) + LOG_FILE));
|
||||||
LOG_INFO(Frontend, "Logging backend initialised");
|
LOG_INFO(Frontend, "Logging backend initialised");
|
||||||
|
|
||||||
// Initialize Java classes
|
// Initialize misc classes
|
||||||
const jclass native_library_class = env->FindClass("org/citra/citra_emu/NativeLibrary");
|
|
||||||
s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
|
|
||||||
s_savestate_info_class = reinterpret_cast<jclass>(
|
s_savestate_info_class = reinterpret_cast<jclass>(
|
||||||
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo")));
|
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo")));
|
||||||
s_core_error_class = reinterpret_cast<jclass>(
|
s_core_error_class = reinterpret_cast<jclass>(
|
||||||
|
@ -159,7 +174,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
||||||
"org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
"org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
||||||
|
|
||||||
// Initialize Java methods
|
// Initialize NativeLibrary
|
||||||
|
const jclass native_library_class = env->FindClass("org/citra/citra_emu/NativeLibrary");
|
||||||
|
s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
|
||||||
s_on_core_error = env->GetStaticMethodID(
|
s_on_core_error = env->GetStaticMethodID(
|
||||||
s_native_library_class, "OnCoreError",
|
s_native_library_class, "OnCoreError",
|
||||||
"(Lorg/citra/citra_emu/NativeLibrary$CoreError;Ljava/lang/String;)Z");
|
"(Lorg/citra/citra_emu/NativeLibrary$CoreError;Ljava/lang/String;)Z");
|
||||||
|
@ -182,6 +199,14 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
s_disk_cache_load_progress = env->GetStaticMethodID(
|
s_disk_cache_load_progress = env->GetStaticMethodID(
|
||||||
s_disk_cache_progress_class, "loadProgress",
|
s_disk_cache_progress_class, "loadProgress",
|
||||||
"(Lorg/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage;II)V");
|
"(Lorg/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage;II)V");
|
||||||
|
env->DeleteLocalRef(native_library_class);
|
||||||
|
|
||||||
|
// Initialize Cheat
|
||||||
|
const jclass cheat_class = env->FindClass("org/citra/citra_emu/features/cheats/model/Cheat");
|
||||||
|
s_cheat_class = reinterpret_cast<jclass>(env->NewGlobalRef(cheat_class));
|
||||||
|
s_cheat_pointer = env->GetFieldID(cheat_class, "mPointer", "J");
|
||||||
|
s_cheat_constructor = env->GetMethodID(cheat_class, "<init>", "(J)V");
|
||||||
|
env->DeleteLocalRef(cheat_class);
|
||||||
|
|
||||||
// Initialize LoadCallbackStage map
|
// Initialize LoadCallbackStage map
|
||||||
const auto to_java_load_callback_stage = [env](const std::string& stage) {
|
const auto to_java_load_callback_stage = [env](const std::string& stage) {
|
||||||
|
@ -215,11 +240,12 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
env->DeleteGlobalRef(s_native_library_class);
|
|
||||||
env->DeleteGlobalRef(s_savestate_info_class);
|
env->DeleteGlobalRef(s_savestate_info_class);
|
||||||
env->DeleteGlobalRef(s_core_error_class);
|
env->DeleteGlobalRef(s_core_error_class);
|
||||||
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
||||||
env->DeleteGlobalRef(s_load_callback_stage_class);
|
env->DeleteGlobalRef(s_load_callback_stage_class);
|
||||||
|
env->DeleteGlobalRef(s_native_library_class);
|
||||||
|
env->DeleteGlobalRef(s_cheat_class);
|
||||||
|
|
||||||
for (auto& [key, object] : s_java_load_callback_stages) {
|
for (auto& [key, object] : s_java_load_callback_stages) {
|
||||||
env->DeleteGlobalRef(object);
|
env->DeleteGlobalRef(object);
|
||||||
|
|
|
@ -12,11 +12,13 @@
|
||||||
namespace IDCache {
|
namespace IDCache {
|
||||||
|
|
||||||
JNIEnv* GetEnvForThread();
|
JNIEnv* GetEnvForThread();
|
||||||
jclass GetNativeLibraryClass();
|
|
||||||
jclass GetCoreErrorClass();
|
jclass GetCoreErrorClass();
|
||||||
jclass GetSavestateInfoClass();
|
jclass GetSavestateInfoClass();
|
||||||
jclass GetDiskCacheProgressClass();
|
jclass GetDiskCacheProgressClass();
|
||||||
jclass GetDiskCacheLoadCallbackStageClass();
|
jclass GetDiskCacheLoadCallbackStageClass();
|
||||||
|
|
||||||
|
jclass GetNativeLibraryClass();
|
||||||
jmethodID GetOnCoreError();
|
jmethodID GetOnCoreError();
|
||||||
jmethodID GetDisplayAlertMsg();
|
jmethodID GetDisplayAlertMsg();
|
||||||
jmethodID GetDisplayAlertPrompt();
|
jmethodID GetDisplayAlertPrompt();
|
||||||
|
@ -28,6 +30,10 @@ jmethodID GetRequestCameraPermission();
|
||||||
jmethodID GetRequestMicPermission();
|
jmethodID GetRequestMicPermission();
|
||||||
jmethodID GetDiskCacheLoadProgress();
|
jmethodID GetDiskCacheLoadProgress();
|
||||||
|
|
||||||
|
jclass GetCheatClass();
|
||||||
|
jfieldID GetCheatPointer();
|
||||||
|
jmethodID GetCheatConstructor();
|
||||||
|
|
||||||
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage);
|
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage);
|
||||||
|
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
|
9
src/android/app/src/main/res/drawable/ic_add.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_add.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:focusable="true"
|
||||||
|
android:nextFocusLeft="@id/checkbox">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/header_text"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
style="@style/TextAppearance.AppCompat.Headline"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/checkbox"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
tools:text="Max Lives after losing 1" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/checkbox"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:nextFocusRight="@id/root"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/text_name"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
22
src/android/app/src/main/res/layout/activity_cheats.xml
Normal file
22
src/android/app/src/main/res/layout/activity_cheats.xml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.slidingpanelayout.widget.SlidingPaneLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/sliding_pane_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:layout_width="320dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:id="@+id/cheat_list"
|
||||||
|
android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment" />
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:layout_width="320dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:id="@+id/cheat_details"
|
||||||
|
android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment" />
|
||||||
|
|
||||||
|
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
|
163
src/android/app/src/main/res/layout/fragment_cheat_details.xml
Normal file
163
src/android/app/src/main/res/layout/fragment_cheat_details.xml
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/scroll_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/barrier">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/label_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:text="@string/cheats_name"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
android:labelFor="@id/edit_name"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/edit_name" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:layout_marginHorizontal="@dimen/spacing_large"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="text"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/label_name"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/label_notes"
|
||||||
|
tools:text="Max Lives after losing 1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/label_notes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:text="@string/cheats_notes"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
android:labelFor="@id/edit_notes"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/edit_name"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/edit_notes" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_notes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:layout_marginHorizontal="@dimen/spacing_large"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/label_notes"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/label_code" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/label_code"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:text="@string/cheats_code"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
android:labelFor="@id/edit_code"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/edit_notes"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/edit_code" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_code"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="108sp"
|
||||||
|
android:layout_marginHorizontal="@dimen/spacing_large"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:typeface="monospace"
|
||||||
|
android:gravity="start"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/label_code"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
tools:text="D3000000 00000000\n00138C78 E1C023BE" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/barrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="top"
|
||||||
|
app:constraint_referenced_ids="button_delete,button_edit,button_cancel,button_ok" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_delete"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
android:text="@string/cheats_delete"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_edit"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_edit"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
android:text="@string/cheats_edit"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/button_delete"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_cancel"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_cancel"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
android:text="@android:string/cancel"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/button_edit"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_ok"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_ok"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
android:text="@android:string/ok"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/button_cancel"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
27
src/android/app/src/main/res/layout/fragment_cheat_list.xml
Normal file
27
src/android/app/src/main/res/layout/fragment_cheat_list.xml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/cheat_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fab"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_add"
|
||||||
|
android:contentDescription="@string/cheats_add"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
38
src/android/app/src/main/res/layout/list_item_cheat.xml
Normal file
38
src/android/app/src/main/res/layout/list_item_cheat.xml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:focusable="true"
|
||||||
|
android:nextFocusRight="@id/checkbox">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/header_text"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
style="@style/TextAppearance.AppCompat.Headline"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/checkbox"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
tools:text="Max Lives after losing 1" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/checkbox"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:nextFocusLeft="@id/root"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/text_name"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -105,6 +105,11 @@
|
||||||
android:title="@string/emulation_show_overlay"
|
android:title="@string/emulation_show_overlay"
|
||||||
android:checkable="true" />
|
android:checkable="true" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_emulation_open_cheats"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:title="@string/emulation_open_cheats" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_emulation_open_settings"
|
android:id="@+id/menu_emulation_open_settings"
|
||||||
app:showAsAction="never"
|
app:showAsAction="never"
|
||||||
|
|
|
@ -160,6 +160,7 @@
|
||||||
<string name="emulation_control_joystick_rel_center">Relative Stick Center</string>
|
<string name="emulation_control_joystick_rel_center">Relative Stick Center</string>
|
||||||
<string name="emulation_control_dpad_slide_enable">Enable D-Pad Sliding</string>
|
<string name="emulation_control_dpad_slide_enable">Enable D-Pad Sliding</string>
|
||||||
<string name="emulation_open_settings">Open Settings</string>
|
<string name="emulation_open_settings">Open Settings</string>
|
||||||
|
<string name="emulation_open_cheats">Open Cheats</string>
|
||||||
<string name="emulation_switch_screen_layout">Landscape Screen Layout</string>
|
<string name="emulation_switch_screen_layout">Landscape Screen Layout</string>
|
||||||
<string name="emulation_screen_layout_landscape">Default</string>
|
<string name="emulation_screen_layout_landscape">Default</string>
|
||||||
<string name="emulation_screen_layout_portrait">Portrait</string>
|
<string name="emulation_screen_layout_portrait">Portrait</string>
|
||||||
|
@ -223,4 +224,17 @@
|
||||||
<!-- Disk shader cache -->
|
<!-- Disk shader cache -->
|
||||||
<string name="preparing_shaders">Preparing shaders</string>
|
<string name="preparing_shaders">Preparing shaders</string>
|
||||||
<string name="building_shaders">Building shaders</string>
|
<string name="building_shaders">Building shaders</string>
|
||||||
|
|
||||||
|
<!-- Cheats -->
|
||||||
|
<string name="cheats">Cheats</string>
|
||||||
|
<string name="cheats_add">Add Cheat</string>
|
||||||
|
<string name="cheats_name">Name</string>
|
||||||
|
<string name="cheats_notes">Notes</string>
|
||||||
|
<string name="cheats_code">Code</string>
|
||||||
|
<string name="cheats_edit">Edit</string>
|
||||||
|
<string name="cheats_delete">Delete</string>
|
||||||
|
<string name="cheats_delete_confirmation">Are you sure you want to delete \"%1$s\"?</string>
|
||||||
|
<string name="cheats_error_no_name">Name can\'t be empty</string>
|
||||||
|
<string name="cheats_error_no_code_lines">Code can\'t be empty</string>
|
||||||
|
<string name="cheats_error_on_line">Error on line %1$d</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue