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" | 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue