mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	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" | ||||
| 
 | ||||
| android { | ||||
|     compileSdkVersion 29 | ||||
|     compileSdkVersion 31 | ||||
|     ndkVersion "23.1.7779620" | ||||
| 
 | ||||
|     compileOptions { | ||||
|  | @ -109,6 +109,10 @@ dependencies { | |||
|     implementation 'androidx.exifinterface:exifinterface:1.2.0' | ||||
|     implementation 'androidx.cardview:cardview:1.0.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' | ||||
| 
 | ||||
|     // For loading huge screenshots from the disk. | ||||
|  |  | |||
|  | @ -68,6 +68,12 @@ | |||
|             </intent-filter> | ||||
|         </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"/> | ||||
| 
 | ||||
|         <provider | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ import androidx.fragment.app.FragmentActivity; | |||
| import org.citra.citra_emu.CitraApplication; | ||||
| import org.citra.citra_emu.NativeLibrary; | ||||
| 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.ui.SettingsActivity; | ||||
| 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_JOYSTICK_REL_CENTER = 15; | ||||
|     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; | ||||
|     private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; | ||||
|  | @ -110,6 +112,8 @@ public final class EmulationActivity extends AppCompatActivity { | |||
|                 EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER); | ||||
|         buttonsActionsMap.append(R.id.menu_emulation_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; | ||||
|  | @ -466,11 +470,16 @@ public final class EmulationActivity extends AppCompatActivity { | |||
|                 EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled); | ||||
|                 item.setChecked(isJoystickRelCenterEnabled); | ||||
|                 break; | ||||
| 
 | ||||
|             case MENU_ACTION_DPAD_SLIDE_ENABLE: | ||||
|                 final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable(); | ||||
|                 EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled); | ||||
|                 item.setChecked(isDpadSlideEnabled); | ||||
|                 break; | ||||
| 
 | ||||
|             case MENU_ACTION_OPEN_CHEATS: | ||||
|                 CheatsActivity.launch(this); | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
|         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/still_image_camera.cpp | ||||
|     camera/still_image_camera.h | ||||
|     cheats/cheat.cpp | ||||
|     cheats/cheat.h | ||||
|     cheats/cheat_engine.cpp | ||||
|     config.cpp | ||||
|     config.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 jclass s_native_library_class; | ||||
| static jclass s_core_error_class; | ||||
| static jclass s_savestate_info_class; | ||||
| static jclass s_disk_cache_progress_class; | ||||
| static jclass s_load_callback_stage_class; | ||||
| 
 | ||||
| static jclass s_native_library_class; | ||||
| static jmethodID s_on_core_error; | ||||
| static jmethodID s_display_alert_msg; | ||||
| 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_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; | ||||
| 
 | ||||
| namespace IDCache { | ||||
|  | @ -57,10 +62,6 @@ JNIEnv* GetEnvForThread() { | |||
|     return owned.env; | ||||
| } | ||||
| 
 | ||||
| jclass GetNativeLibraryClass() { | ||||
|     return s_native_library_class; | ||||
| } | ||||
| 
 | ||||
| jclass GetCoreErrorClass() { | ||||
|     return s_core_error_class; | ||||
| } | ||||
|  | @ -77,6 +78,10 @@ jclass GetDiskCacheLoadCallbackStageClass() { | |||
|     return s_load_callback_stage_class; | ||||
| } | ||||
| 
 | ||||
| jclass GetNativeLibraryClass() { | ||||
|     return s_native_library_class; | ||||
| } | ||||
| 
 | ||||
| jmethodID GetOnCoreError() { | ||||
|     return s_on_core_error; | ||||
| } | ||||
|  | @ -117,6 +122,18 @@ jmethodID GetDiskCacheLoadProgress() { | |||
|     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) { | ||||
|     const auto it = s_java_load_callback_stages.find(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)); | ||||
|     LOG_INFO(Frontend, "Logging backend initialised"); | ||||
| 
 | ||||
|     // Initialize Java 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)); | ||||
|     // Initialize misc classes
 | ||||
|     s_savestate_info_class = reinterpret_cast<jclass>( | ||||
|         env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo"))); | ||||
|     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( | ||||
|         "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_native_library_class, "OnCoreError", | ||||
|         "(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_progress_class, "loadProgress", | ||||
|         "(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
 | ||||
|     const auto to_java_load_callback_stage = [env](const std::string& stage) { | ||||
|  | @ -215,11 +240,12 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     env->DeleteGlobalRef(s_native_library_class); | ||||
|     env->DeleteGlobalRef(s_savestate_info_class); | ||||
|     env->DeleteGlobalRef(s_core_error_class); | ||||
|     env->DeleteGlobalRef(s_disk_cache_progress_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) { | ||||
|         env->DeleteGlobalRef(object); | ||||
|  |  | |||
|  | @ -12,11 +12,13 @@ | |||
| namespace IDCache { | ||||
| 
 | ||||
| JNIEnv* GetEnvForThread(); | ||||
| jclass GetNativeLibraryClass(); | ||||
| 
 | ||||
| jclass GetCoreErrorClass(); | ||||
| jclass GetSavestateInfoClass(); | ||||
| jclass GetDiskCacheProgressClass(); | ||||
| jclass GetDiskCacheLoadCallbackStageClass(); | ||||
| 
 | ||||
| jclass GetNativeLibraryClass(); | ||||
| jmethodID GetOnCoreError(); | ||||
| jmethodID GetDisplayAlertMsg(); | ||||
| jmethodID GetDisplayAlertPrompt(); | ||||
|  | @ -28,6 +30,10 @@ jmethodID GetRequestCameraPermission(); | |||
| jmethodID GetRequestMicPermission(); | ||||
| jmethodID GetDiskCacheLoadProgress(); | ||||
| 
 | ||||
| jclass GetCheatClass(); | ||||
| jfieldID GetCheatPointer(); | ||||
| jmethodID GetCheatConstructor(); | ||||
| 
 | ||||
| jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage); | ||||
| 
 | ||||
| } // 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:checkable="true" /> | ||||
| 
 | ||||
|     <item | ||||
|         android:id="@+id/menu_emulation_open_cheats" | ||||
|         app:showAsAction="never" | ||||
|         android:title="@string/emulation_open_cheats" /> | ||||
| 
 | ||||
|     <item | ||||
|         android:id="@+id/menu_emulation_open_settings" | ||||
|         app:showAsAction="never" | ||||
|  |  | |||
|  | @ -160,6 +160,7 @@ | |||
|     <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_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_screen_layout_landscape">Default</string> | ||||
|     <string name="emulation_screen_layout_portrait">Portrait</string> | ||||
|  | @ -223,4 +224,17 @@ | |||
|     <!-- Disk shader cache --> | ||||
|     <string name="preparing_shaders">Preparing 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> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue