summaryrefslogtreecommitdiffstats
path: root/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java174
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java46
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java56
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java161
-rw-r--r--src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java72
5 files changed, 509 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java
new file mode 100644
index 000000000..762cdb80e
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java
@@ -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);
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java
new file mode 100644
index 000000000..6c67a31d4
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java
@@ -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();
+ });
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java
new file mode 100644
index 000000000..8ba8f86e7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java
@@ -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);
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
new file mode 100644
index 000000000..a36bf427c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
@@ -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);
+ }
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java
new file mode 100644
index 000000000..9cb2ce8d8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java
@@ -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];
+ }
+}