mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	Android UI Overhaul Part 4/4 (#7235)
* android: Rework cheats Reworks cheats to use the navigation component, kotlin, and a tweaked layout for a better tuned look. * android: Convert remaining files to kotlin and add overlay home button * android: Remove Picasso dependency * android: Fix home option layout centering * android: Adjust logo size in-app
This commit is contained in:
		
							parent
							
								
									d680b79725
								
							
						
					
					
						commit
						762ddfd07b
					
				
					 76 changed files with 3738 additions and 3654 deletions
				
			
		|  | @ -178,10 +178,6 @@ dependencies { | ||||||
|     implementation("com.google.android.material:material:1.9.0") |     implementation("com.google.android.material:material:1.9.0") | ||||||
|     implementation("androidx.core:core-splashscreen:1.0.1") |     implementation("androidx.core:core-splashscreen:1.0.1") | ||||||
|     implementation("androidx.work:work-runtime:2.8.1") |     implementation("androidx.work:work-runtime:2.8.1") | ||||||
| 
 |  | ||||||
|     // For loading huge screenshots from the disk. |  | ||||||
|     implementation("com.squareup.picasso:picasso:2.71828") |  | ||||||
| 
 |  | ||||||
|     implementation("org.ini4j:ini4j:0.5.4") |     implementation("org.ini4j:ini4j:0.5.4") | ||||||
|     implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") |     implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") | ||||||
|     implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") |     implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") | ||||||
|  |  | ||||||
|  | @ -26,10 +26,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||||
| import org.citra.citra_emu.HomeNavigationDirections | import org.citra.citra_emu.HomeNavigationDirections | ||||||
| import org.citra.citra_emu.CitraApplication | import org.citra.citra_emu.CitraApplication | ||||||
| import org.citra.citra_emu.R | import org.citra.citra_emu.R | ||||||
| import org.citra.citra_emu.activities.EmulationActivity |  | ||||||
| import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder | import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder | ||||||
| import org.citra.citra_emu.databinding.CardGameBinding | import org.citra.citra_emu.databinding.CardGameBinding | ||||||
| import org.citra.citra_emu.features.cheats.ui.CheatsActivity | import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections | ||||||
| import org.citra.citra_emu.model.Game | import org.citra.citra_emu.model.Game | ||||||
| import org.citra.citra_emu.utils.GameIconUtils | import org.citra.citra_emu.utils.GameIconUtils | ||||||
| import org.citra.citra_emu.viewmodel.GamesViewModel | import org.citra.citra_emu.viewmodel.GamesViewModel | ||||||
|  | @ -100,7 +99,8 @@ class GameAdapter(private val activity: AppCompatActivity) : | ||||||
|                 .setPositiveButton(android.R.string.ok, null) |                 .setPositiveButton(android.R.string.ok, null) | ||||||
|                 .show() |                 .show() | ||||||
|         } else { |         } else { | ||||||
|             CheatsActivity.launch(view.context, holder.game.titleId) |             val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId) | ||||||
|  |             view.findNavController().navigate(action) | ||||||
|         } |         } | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,129 +0,0 @@ | ||||||
| // Copyright 2020 Citra Emulator Project |  | ||||||
| // Licensed under GPLv2 or any later version |  | ||||||
| // Refer to the license.txt file included. |  | ||||||
| 
 |  | ||||||
| package org.citra.citra_emu.applets; |  | ||||||
| 
 |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.app.Dialog; |  | ||||||
| import android.content.DialogInterface; |  | ||||||
| import android.os.Bundle; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.NativeLibrary; |  | ||||||
| import org.citra.citra_emu.R; |  | ||||||
| import org.citra.citra_emu.activities.EmulationActivity; |  | ||||||
| 
 |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.Objects; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.Keep; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.appcompat.app.AlertDialog; |  | ||||||
| import androidx.fragment.app.DialogFragment; |  | ||||||
| 
 |  | ||||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder; |  | ||||||
| 
 |  | ||||||
| @Keep |  | ||||||
| public final class MiiSelector { |  | ||||||
|     @Keep |  | ||||||
|     public static class MiiSelectorConfig implements java.io.Serializable { |  | ||||||
|         public boolean enable_cancel_button; |  | ||||||
|         public String title; |  | ||||||
|         public long initially_selected_mii_index; |  | ||||||
|         // List of Miis to display |  | ||||||
|         public String[] mii_names; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static class MiiSelectorData { |  | ||||||
|         public long return_code; |  | ||||||
|         public int index; |  | ||||||
| 
 |  | ||||||
|         private MiiSelectorData(long return_code, int index) { |  | ||||||
|             this.return_code = return_code; |  | ||||||
|             this.index = index; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static class MiiSelectorDialogFragment extends DialogFragment { |  | ||||||
|         static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) { |  | ||||||
|             MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment(); |  | ||||||
|             Bundle args = new Bundle(); |  | ||||||
|             args.putSerializable("config", config); |  | ||||||
|             frag.setArguments(args); |  | ||||||
|             return frag; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @NonNull |  | ||||||
|         @Override |  | ||||||
|         public Dialog onCreateDialog(Bundle savedInstanceState) { |  | ||||||
|             final Activity emulationActivity = Objects.requireNonNull(getActivity()); |  | ||||||
| 
 |  | ||||||
|             MiiSelectorConfig config = |  | ||||||
|                     Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments()) |  | ||||||
|                             .getSerializable("config")); |  | ||||||
| 
 |  | ||||||
|             // Note: we intentionally leave out the Standard Mii in the native code so that |  | ||||||
|             // the string can get translated |  | ||||||
|             ArrayList<String> list = new ArrayList<>(); |  | ||||||
|             list.add(emulationActivity.getString(R.string.standard_mii)); |  | ||||||
|             list.addAll(Arrays.asList(config.mii_names)); |  | ||||||
| 
 |  | ||||||
|             final int initialIndex = config.initially_selected_mii_index < list.size() |  | ||||||
|                     ? (int) config.initially_selected_mii_index |  | ||||||
|                     : 0; |  | ||||||
|             data.index = initialIndex; |  | ||||||
|             MaterialAlertDialogBuilder builder = |  | ||||||
|                     new MaterialAlertDialogBuilder(emulationActivity) |  | ||||||
|                             .setTitle(config.title.isEmpty() |  | ||||||
|                                     ? emulationActivity.getString(R.string.mii_selector) |  | ||||||
|                                     : config.title) |  | ||||||
|                             .setSingleChoiceItems(list.toArray(new String[]{}), initialIndex, |  | ||||||
|                                     (dialog, which) -> { |  | ||||||
|                                         data.index = which; |  | ||||||
|                                     }) |  | ||||||
|                             .setPositiveButton(android.R.string.ok, (dialog, which) -> { |  | ||||||
|                                 data.return_code = 0; |  | ||||||
|                                 synchronized (finishLock) { |  | ||||||
|                                     finishLock.notifyAll(); |  | ||||||
|                                 } |  | ||||||
|                             }); |  | ||||||
|             if (config.enable_cancel_button) { |  | ||||||
|                 builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { |  | ||||||
|                     data.return_code = 1; |  | ||||||
|                     synchronized (finishLock) { |  | ||||||
|                         finishLock.notifyAll(); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|             setCancelable(false); |  | ||||||
|             return builder.create(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static MiiSelectorData data; |  | ||||||
|     private static final Object finishLock = new Object(); |  | ||||||
| 
 |  | ||||||
|     private static void ExecuteImpl(MiiSelectorConfig config) { |  | ||||||
|         final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); |  | ||||||
| 
 |  | ||||||
|         data = new MiiSelectorData(0, 0); |  | ||||||
| 
 |  | ||||||
|         MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config); |  | ||||||
|         fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static MiiSelectorData Execute(MiiSelectorConfig config) { |  | ||||||
|         NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); |  | ||||||
| 
 |  | ||||||
|         synchronized (finishLock) { |  | ||||||
|             try { |  | ||||||
|                 finishLock.wait(); |  | ||||||
|             } catch (Exception ignored) { |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return data; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,47 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.applets | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Keep | ||||||
|  | import org.citra.citra_emu.NativeLibrary | ||||||
|  | import org.citra.citra_emu.fragments.MiiSelectorDialogFragment | ||||||
|  | import java.io.Serializable | ||||||
|  | 
 | ||||||
|  | @Keep | ||||||
|  | object MiiSelector { | ||||||
|  |     lateinit var data: MiiSelectorData | ||||||
|  |     val finishLock = Object() | ||||||
|  | 
 | ||||||
|  |     private fun ExecuteImpl(config: MiiSelectorConfig) { | ||||||
|  |         val emulationActivity = NativeLibrary.sEmulationActivity.get() | ||||||
|  |         data = MiiSelectorData(0, 0) | ||||||
|  |         val fragment = MiiSelectorDialogFragment.newInstance(config) | ||||||
|  |         fragment.show(emulationActivity!!.supportFragmentManager, "mii_selector") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JvmStatic | ||||||
|  |     fun Execute(config: MiiSelectorConfig): MiiSelectorData { | ||||||
|  |         NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) } | ||||||
|  |         synchronized(finishLock) { | ||||||
|  |             try { | ||||||
|  |                 finishLock.wait() | ||||||
|  |             } catch (ignored: Exception) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return data | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Keep | ||||||
|  |     class MiiSelectorConfig : Serializable { | ||||||
|  |         var enableCancelButton = false | ||||||
|  |         var title: String? = null | ||||||
|  |         var initiallySelectedMiiIndex: Long = 0 | ||||||
|  | 
 | ||||||
|  |         // List of Miis to display | ||||||
|  |         lateinit var miiNames: Array<String> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     class MiiSelectorData (var returnCode: Long, var index: Int) | ||||||
|  | } | ||||||
|  | @ -1,279 +0,0 @@ | ||||||
| // Copyright 2020 Citra Emulator Project |  | ||||||
| // Licensed under GPLv2 or any later version |  | ||||||
| // Refer to the license.txt file included. |  | ||||||
| 
 |  | ||||||
| package org.citra.citra_emu.applets; |  | ||||||
| 
 |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.app.Dialog; |  | ||||||
| import android.content.DialogInterface; |  | ||||||
| import android.content.res.Resources; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.text.InputFilter; |  | ||||||
| import android.text.Spanned; |  | ||||||
| import android.util.TypedValue; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.EditText; |  | ||||||
| import android.widget.FrameLayout; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.ColorInt; |  | ||||||
| import androidx.annotation.Keep; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.appcompat.app.AlertDialog; |  | ||||||
| import androidx.fragment.app.DialogFragment; |  | ||||||
| 
 |  | ||||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.CitraApplication; |  | ||||||
| import org.citra.citra_emu.NativeLibrary; |  | ||||||
| import org.citra.citra_emu.R; |  | ||||||
| import org.citra.citra_emu.activities.EmulationActivity; |  | ||||||
| import org.citra.citra_emu.utils.Log; |  | ||||||
| 
 |  | ||||||
| import java.util.Objects; |  | ||||||
| 
 |  | ||||||
| @Keep |  | ||||||
| public final class SoftwareKeyboard { |  | ||||||
|     /// Corresponds to Frontend::ButtonConfig |  | ||||||
|     private interface ButtonConfig { |  | ||||||
|         int Single = 0; /// Ok button |  | ||||||
|         int Dual = 1;   /// Cancel | Ok buttons |  | ||||||
|         int Triple = 2; /// Cancel | I Forgot | Ok buttons |  | ||||||
|         int None = 3;   /// No button (returned by swkbdInputText in special cases) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// Corresponds to Frontend::ValidationError |  | ||||||
|     public enum ValidationError { |  | ||||||
|         None, |  | ||||||
|         // Button Selection |  | ||||||
|         ButtonOutOfRange, |  | ||||||
|         // Configured Filters |  | ||||||
|         MaxDigitsExceeded, |  | ||||||
|         AtSignNotAllowed, |  | ||||||
|         PercentNotAllowed, |  | ||||||
|         BackslashNotAllowed, |  | ||||||
|         ProfanityNotAllowed, |  | ||||||
|         CallbackFailed, |  | ||||||
|         // Allowed Input Type |  | ||||||
|         FixedLengthRequired, |  | ||||||
|         MaxLengthExceeded, |  | ||||||
|         BlankInputNotAllowed, |  | ||||||
|         EmptyInputNotAllowed, |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Keep |  | ||||||
|     public static class KeyboardConfig implements java.io.Serializable { |  | ||||||
|         public int button_config; |  | ||||||
|         public int max_text_length; |  | ||||||
|         public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input |  | ||||||
|         public String hint_text;       /// Displayed in the field as a hint before |  | ||||||
|         @Nullable |  | ||||||
|         public String[] button_text; /// Contains the button text that the caller provides |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// Corresponds to Frontend::KeyboardData |  | ||||||
|     public static class KeyboardData { |  | ||||||
|         public int button; |  | ||||||
|         public String text; |  | ||||||
| 
 |  | ||||||
|         private KeyboardData(int button, String text) { |  | ||||||
|             this.button = button; |  | ||||||
|             this.text = text; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static class Filter implements InputFilter { |  | ||||||
|         @Override |  | ||||||
|         public CharSequence filter(CharSequence source, int start, int end, Spanned dest, |  | ||||||
|                                    int dstart, int dend) { |  | ||||||
|             String text = new StringBuilder(dest) |  | ||||||
|                     .replace(dstart, dend, source.subSequence(start, end).toString()) |  | ||||||
|                     .toString(); |  | ||||||
|             if (ValidateFilters(text) == ValidationError.None) { |  | ||||||
|                 return null; // Accept replacement |  | ||||||
|             } |  | ||||||
|             return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static class KeyboardDialogFragment extends DialogFragment { |  | ||||||
|         static KeyboardDialogFragment newInstance(KeyboardConfig config) { |  | ||||||
|             KeyboardDialogFragment frag = new KeyboardDialogFragment(); |  | ||||||
|             Bundle args = new Bundle(); |  | ||||||
|             args.putSerializable("config", config); |  | ||||||
|             frag.setArguments(args); |  | ||||||
|             return frag; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @NonNull |  | ||||||
|         @Override |  | ||||||
|         public Dialog onCreateDialog(Bundle savedInstanceState) { |  | ||||||
|             final Activity emulationActivity = getActivity(); |  | ||||||
|             assert emulationActivity != null; |  | ||||||
| 
 |  | ||||||
|             FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( |  | ||||||
|                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); |  | ||||||
|             params.leftMargin = params.rightMargin = |  | ||||||
|                     CitraApplication.Companion.getAppContext().getResources().getDimensionPixelSize( |  | ||||||
|                             R.dimen.dialog_margin); |  | ||||||
| 
 |  | ||||||
|             KeyboardConfig config = Objects.requireNonNull( |  | ||||||
|                     (KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config")); |  | ||||||
| 
 |  | ||||||
|             // Set up the input |  | ||||||
|             EditText editText = new EditText(CitraApplication.Companion.getAppContext()); |  | ||||||
|             editText.setHint(config.hint_text); |  | ||||||
|             editText.setSingleLine(!config.multiline_mode); |  | ||||||
|             editText.setLayoutParams(params); |  | ||||||
|             editText.setFilters(new InputFilter[]{ |  | ||||||
|                     new Filter(), new InputFilter.LengthFilter(config.max_text_length)}); |  | ||||||
| 
 |  | ||||||
|             TypedValue typedValue = new TypedValue(); |  | ||||||
|             Resources.Theme theme = requireContext().getTheme(); |  | ||||||
|             theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true); |  | ||||||
|             @ColorInt int color = typedValue.data; |  | ||||||
|             editText.setHintTextColor(color); |  | ||||||
|             editText.setTextColor(color); |  | ||||||
| 
 |  | ||||||
|             FrameLayout container = new FrameLayout(emulationActivity); |  | ||||||
|             container.addView(editText); |  | ||||||
| 
 |  | ||||||
|             MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity) |  | ||||||
|                     .setTitle(R.string.software_keyboard) |  | ||||||
|                     .setView(container); |  | ||||||
|             setCancelable(false); |  | ||||||
| 
 |  | ||||||
|             switch (config.button_config) { |  | ||||||
|                 case ButtonConfig.Triple: { |  | ||||||
|                     final String text = config.button_text[1].isEmpty() |  | ||||||
|                             ? emulationActivity.getString(R.string.i_forgot) |  | ||||||
|                             : config.button_text[1]; |  | ||||||
|                     builder.setNeutralButton(text, null); |  | ||||||
|                 } |  | ||||||
|                 // fallthrough |  | ||||||
|                 case ButtonConfig.Dual: { |  | ||||||
|                     final String text = config.button_text[0].isEmpty() |  | ||||||
|                             ? emulationActivity.getString(android.R.string.cancel) |  | ||||||
|                             : config.button_text[0]; |  | ||||||
|                     builder.setNegativeButton(text, null); |  | ||||||
|                 } |  | ||||||
|                 // fallthrough |  | ||||||
|                 case ButtonConfig.Single: { |  | ||||||
|                     final String text = config.button_text[2].isEmpty() |  | ||||||
|                             ? emulationActivity.getString(android.R.string.ok) |  | ||||||
|                             : config.button_text[2]; |  | ||||||
|                     builder.setPositiveButton(text, null); |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final AlertDialog dialog = builder.create(); |  | ||||||
|             dialog.create(); |  | ||||||
|             if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { |  | ||||||
|                 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { |  | ||||||
|                     data.button = config.button_config; |  | ||||||
|                     data.text = editText.getText().toString(); |  | ||||||
|                     final ValidationError error = ValidateInput(data.text); |  | ||||||
|                     if (error != ValidationError.None) { |  | ||||||
|                         HandleValidationError(config, error); |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     dialog.dismiss(); |  | ||||||
| 
 |  | ||||||
|                     synchronized (finishLock) { |  | ||||||
|                         finishLock.notifyAll(); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|             if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { |  | ||||||
|                 dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { |  | ||||||
|                     data.button = 1; |  | ||||||
|                     dialog.dismiss(); |  | ||||||
|                     synchronized (finishLock) { |  | ||||||
|                         finishLock.notifyAll(); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|             if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { |  | ||||||
|                 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { |  | ||||||
|                     data.button = 0; |  | ||||||
|                     dialog.dismiss(); |  | ||||||
|                     synchronized (finishLock) { |  | ||||||
|                         finishLock.notifyAll(); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return dialog; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static KeyboardData data; |  | ||||||
|     private static final Object finishLock = new Object(); |  | ||||||
| 
 |  | ||||||
|     private static void ExecuteImpl(KeyboardConfig config) { |  | ||||||
|         final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); |  | ||||||
| 
 |  | ||||||
|         data = new KeyboardData(0, ""); |  | ||||||
| 
 |  | ||||||
|         KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); |  | ||||||
|         fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static void HandleValidationError(KeyboardConfig config, ValidationError error) { |  | ||||||
|         final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); |  | ||||||
|         String message = ""; |  | ||||||
|         switch (error) { |  | ||||||
|             case FixedLengthRequired: |  | ||||||
|                 message = |  | ||||||
|                         emulationActivity.getString(R.string.fixed_length_required, config.max_text_length); |  | ||||||
|                 break; |  | ||||||
|             case MaxLengthExceeded: |  | ||||||
|                 message = |  | ||||||
|                         emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length); |  | ||||||
|                 break; |  | ||||||
|             case BlankInputNotAllowed: |  | ||||||
|                 message = emulationActivity.getString(R.string.blank_input_not_allowed); |  | ||||||
|                 break; |  | ||||||
|             case EmptyInputNotAllowed: |  | ||||||
|                 message = emulationActivity.getString(R.string.empty_input_not_allowed); |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         new MaterialAlertDialogBuilder(emulationActivity) |  | ||||||
|                 .setTitle(R.string.software_keyboard) |  | ||||||
|                 .setMessage(message) |  | ||||||
|                 .setPositiveButton(android.R.string.ok, null) |  | ||||||
|                 .show(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static KeyboardData Execute(KeyboardConfig config) { |  | ||||||
|         if (config.button_config == ButtonConfig.None) { |  | ||||||
|             Log.error("Unexpected button config None"); |  | ||||||
|             return new KeyboardData(0, ""); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); |  | ||||||
| 
 |  | ||||||
|         synchronized (finishLock) { |  | ||||||
|             try { |  | ||||||
|                 finishLock.wait(); |  | ||||||
|             } catch (Exception ignored) { |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return data; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void ShowError(String error) { |  | ||||||
|         NativeLibrary.displayAlertMsg( |  | ||||||
|                 CitraApplication.Companion.getAppContext().getResources().getString(R.string.software_keyboard), |  | ||||||
|                 error, false); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static native ValidationError ValidateFilters(String text); |  | ||||||
| 
 |  | ||||||
|     private static native ValidationError ValidateInput(String text); |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,152 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.applets | ||||||
|  | 
 | ||||||
|  | import android.text.InputFilter | ||||||
|  | import android.text.Spanned | ||||||
|  | import androidx.annotation.Keep | ||||||
|  | import org.citra.citra_emu.CitraApplication.Companion.appContext | ||||||
|  | import org.citra.citra_emu.NativeLibrary | ||||||
|  | import org.citra.citra_emu.R | ||||||
|  | import org.citra.citra_emu.fragments.KeyboardDialogFragment | ||||||
|  | import org.citra.citra_emu.fragments.MessageDialogFragment | ||||||
|  | import org.citra.citra_emu.utils.Log | ||||||
|  | import java.io.Serializable | ||||||
|  | 
 | ||||||
|  | @Keep | ||||||
|  | object SoftwareKeyboard { | ||||||
|  |     lateinit var data: KeyboardData | ||||||
|  |     val finishLock = Object() | ||||||
|  | 
 | ||||||
|  |     private fun ExecuteImpl(config: KeyboardConfig) { | ||||||
|  |         val emulationActivity = NativeLibrary.sEmulationActivity.get() | ||||||
|  |         data = KeyboardData(0, "") | ||||||
|  |         KeyboardDialogFragment.newInstance(config) | ||||||
|  |             .show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun HandleValidationError(config: KeyboardConfig, error: ValidationError) { | ||||||
|  |         val emulationActivity = NativeLibrary.sEmulationActivity.get()!! | ||||||
|  |         val message: String = when (error) { | ||||||
|  |             ValidationError.FixedLengthRequired -> emulationActivity.getString( | ||||||
|  |                 R.string.fixed_length_required, | ||||||
|  |                 config.maxTextLength | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             ValidationError.MaxLengthExceeded -> | ||||||
|  |                 emulationActivity.getString(R.string.max_length_exceeded, config.maxTextLength) | ||||||
|  | 
 | ||||||
|  |             ValidationError.BlankInputNotAllowed -> | ||||||
|  |                 emulationActivity.getString(R.string.blank_input_not_allowed) | ||||||
|  | 
 | ||||||
|  |             ValidationError.EmptyInputNotAllowed -> | ||||||
|  |                 emulationActivity.getString(R.string.empty_input_not_allowed) | ||||||
|  | 
 | ||||||
|  |             else -> emulationActivity.getString(R.string.invalid_input) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         MessageDialogFragment.newInstance(R.string.software_keyboard, message).show( | ||||||
|  |             NativeLibrary.sEmulationActivity.get()!!.supportFragmentManager, | ||||||
|  |             MessageDialogFragment.TAG | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JvmStatic | ||||||
|  |     fun Execute(config: KeyboardConfig): KeyboardData { | ||||||
|  |         if (config.buttonConfig == ButtonConfig.None) { | ||||||
|  |             Log.error("Unexpected button config None") | ||||||
|  |             return KeyboardData(0, "") | ||||||
|  |         } | ||||||
|  |         NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) } | ||||||
|  |         synchronized(finishLock) { | ||||||
|  |             try { | ||||||
|  |                 finishLock.wait() | ||||||
|  |             } catch (ignored: Exception) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return data | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JvmStatic | ||||||
|  |     fun ShowError(error: String) { | ||||||
|  |         NativeLibrary.displayAlertMsg( | ||||||
|  |             appContext.resources.getString(R.string.software_keyboard), | ||||||
|  |             error, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private external fun ValidateFilters(text: String): ValidationError | ||||||
|  |     external fun ValidateInput(text: String): ValidationError | ||||||
|  | 
 | ||||||
|  |     /// Corresponds to Frontend::ButtonConfig | ||||||
|  |     interface ButtonConfig { | ||||||
|  |         companion object { | ||||||
|  |             const val Single = 0 /// Ok button | ||||||
|  |             const val Dual = 1 /// Cancel | Ok buttons | ||||||
|  |             const val Triple = 2 /// Cancel | I Forgot | Ok buttons | ||||||
|  |             const val None = 3 /// No button (returned by swkbdInputText in special cases) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Corresponds to Frontend::ValidationError | ||||||
|  |     enum class ValidationError { | ||||||
|  |         None, | ||||||
|  | 
 | ||||||
|  |         // Button Selection | ||||||
|  |         ButtonOutOfRange, | ||||||
|  | 
 | ||||||
|  |         // Configured Filters | ||||||
|  |         MaxDigitsExceeded, | ||||||
|  |         AtSignNotAllowed, | ||||||
|  |         PercentNotAllowed, | ||||||
|  |         BackslashNotAllowed, | ||||||
|  |         ProfanityNotAllowed, | ||||||
|  |         CallbackFailed, | ||||||
|  | 
 | ||||||
|  |         // Allowed Input Type | ||||||
|  |         FixedLengthRequired, | ||||||
|  |         MaxLengthExceeded, | ||||||
|  |         BlankInputNotAllowed, | ||||||
|  |         EmptyInputNotAllowed | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Keep | ||||||
|  |     class KeyboardConfig : Serializable { | ||||||
|  |         var buttonConfig = 0 | ||||||
|  |         var maxTextLength = 0 | ||||||
|  | 
 | ||||||
|  |         // True if the keyboard accepts multiple lines of input | ||||||
|  |         var multilineMode = false | ||||||
|  | 
 | ||||||
|  |         // Displayed in the field as a hint before | ||||||
|  |         var hintText: String? = null | ||||||
|  | 
 | ||||||
|  |         // Contains the button text that the caller provides | ||||||
|  |         lateinit var buttonText: Array<String> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Corresponds to Frontend::KeyboardData | ||||||
|  |     class KeyboardData(var button: Int, var text: String) | ||||||
|  |     class Filter : InputFilter { | ||||||
|  |         override fun filter( | ||||||
|  |             source: CharSequence, | ||||||
|  |             start: Int, | ||||||
|  |             end: Int, | ||||||
|  |             dest: Spanned, | ||||||
|  |             dstart: Int, | ||||||
|  |             dend: Int | ||||||
|  |         ): CharSequence? { | ||||||
|  |             val text = StringBuilder(dest) | ||||||
|  |                 .replace(dstart, dend, source.subSequence(start, end).toString()) | ||||||
|  |                 .toString() | ||||||
|  |             return if (ValidateFilters(text) == ValidationError.None) { | ||||||
|  |                 null // Accept replacement | ||||||
|  |             } else { | ||||||
|  |                 dest.subSequence(dstart, dend) // Request the subsequence to be unchanged | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| package org.citra.citra_emu.contracts; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.util.Pair; |  | ||||||
| 
 |  | ||||||
| import androidx.activity.result.contract.ActivityResultContract; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| 
 |  | ||||||
| public class OpenFileResultContract extends ActivityResultContract<Boolean, Intent> { |  | ||||||
|     @NonNull |  | ||||||
|     @Override |  | ||||||
|     public Intent createIntent(@NonNull Context context, Boolean allowMultiple) { |  | ||||||
|         return new Intent(Intent.ACTION_OPEN_DOCUMENT) |  | ||||||
|             .setType("application/octet-stream") |  | ||||||
|             .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public Intent parseResult(int i, @Nullable Intent intent) { |  | ||||||
|         return intent; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.contracts | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import androidx.activity.result.contract.ActivityResultContract | ||||||
|  | 
 | ||||||
|  | class OpenFileResultContract : ActivityResultContract<Boolean?, Intent?>() { | ||||||
|  |     override fun createIntent(context: Context, input: Boolean?): Intent { | ||||||
|  |         return Intent(Intent.ACTION_OPEN_DOCUMENT) | ||||||
|  |             .setType("application/octet-stream") | ||||||
|  |             .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, input) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun parseResult(resultCode: Int, intent: Intent?): Intent? = intent | ||||||
|  | } | ||||||
|  | @ -1,57 +0,0 @@ | ||||||
| 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,48 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.features.cheats.model | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Keep | ||||||
|  | 
 | ||||||
|  | @Keep | ||||||
|  | class Cheat(@field:Keep private val mPointer: Long) { | ||||||
|  |     private var enabledChangedCallback: Runnable? = null | ||||||
|  |     protected external fun finalize() | ||||||
|  | 
 | ||||||
|  |     external fun getName(): String | ||||||
|  | 
 | ||||||
|  |     external fun getNotes(): String | ||||||
|  | 
 | ||||||
|  |     external fun getCode(): String | ||||||
|  | 
 | ||||||
|  |     external fun getEnabled(): Boolean | ||||||
|  | 
 | ||||||
|  |     fun setEnabled(enabled: Boolean) { | ||||||
|  |         setEnabledImpl(enabled) | ||||||
|  |         onEnabledChanged() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private external fun setEnabledImpl(enabled: Boolean) | ||||||
|  | 
 | ||||||
|  |     fun setEnabledChangedCallback(callback: Runnable) { | ||||||
|  |         enabledChangedCallback = callback | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onEnabledChanged() { | ||||||
|  |         enabledChangedCallback?.run() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * If the code is valid, returns 0. Otherwise, returns the 1-based index | ||||||
|  |          * for the line containing the error. | ||||||
|  |          */ | ||||||
|  |         @JvmStatic | ||||||
|  |         external fun isValidGatewayCode(code: String): Int | ||||||
|  | 
 | ||||||
|  |         @JvmStatic | ||||||
|  |         external fun createGatewayCode(name: String, notes: String, code: String): Cheat | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,28 +0,0 @@ | ||||||
| package org.citra.citra_emu.features.cheats.model; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.Keep; |  | ||||||
| 
 |  | ||||||
| public class CheatEngine { |  | ||||||
|     @Keep |  | ||||||
|     private final long mPointer; |  | ||||||
| 
 |  | ||||||
|     @Keep |  | ||||||
|     public CheatEngine(long titleId) { |  | ||||||
|         mPointer = initialize(titleId); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static native long initialize(long titleId); |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected native void finalize(); |  | ||||||
| 
 |  | ||||||
|     public native Cheat[] getCheats(); |  | ||||||
| 
 |  | ||||||
|     public native void addCheat(Cheat cheat); |  | ||||||
| 
 |  | ||||||
|     public native void removeCheat(int index); |  | ||||||
| 
 |  | ||||||
|     public native void updateCheat(int index, Cheat newCheat); |  | ||||||
| 
 |  | ||||||
|     public native void saveCheatFile(); |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.features.cheats.model | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Keep | ||||||
|  | 
 | ||||||
|  | @Keep | ||||||
|  | class CheatEngine(titleId: Long) { | ||||||
|  |     @Keep | ||||||
|  |     private val mPointer: Long | ||||||
|  | 
 | ||||||
|  |     init { | ||||||
|  |         mPointer = initialize(titleId) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected external fun finalize() | ||||||
|  | 
 | ||||||
|  |     external fun getCheats(): Array<Cheat> | ||||||
|  | 
 | ||||||
|  |     external fun addCheat(cheat: Cheat?) | ||||||
|  |     external fun removeCheat(index: Int) | ||||||
|  |     external fun updateCheat(index: Int, newCheat: Cheat?) | ||||||
|  |     external fun saveCheatFile() | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         @JvmStatic | ||||||
|  |         private external fun initialize(titleId: Long): Long | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,187 +0,0 @@ | ||||||
| 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 CheatEngine mCheatEngine; |  | ||||||
|     private Cheat[] mCheats; |  | ||||||
|     private boolean mCheatsNeedSaving = false; |  | ||||||
| 
 |  | ||||||
|     public void initialize(long titleId) { |  | ||||||
|         mCheatEngine = new CheatEngine(titleId); |  | ||||||
|         load(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void load() { |  | ||||||
|         mCheats = mCheatEngine.getCheats(); |  | ||||||
| 
 |  | ||||||
|         for (int i = 0; i < mCheats.length; i++) { |  | ||||||
|             int position = i; |  | ||||||
|             mCheats[i].setEnabledChangedCallback(() -> { |  | ||||||
|                 mCheatsNeedSaving = true; |  | ||||||
|                 notifyCheatUpdated(position); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void saveIfNeeded() { |  | ||||||
|         if (mCheatsNeedSaving) { |  | ||||||
|             mCheatEngine.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; |  | ||||||
| 
 |  | ||||||
|         mCheatEngine.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) { |  | ||||||
|         mCheatEngine.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); |  | ||||||
| 
 |  | ||||||
|         mCheatEngine.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,169 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.features.cheats.model | ||||||
|  | 
 | ||||||
|  | import androidx.lifecycle.ViewModel | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | 
 | ||||||
|  | class CheatsViewModel : ViewModel() { | ||||||
|  |     val selectedCheat get() = _selectedCheat.asStateFlow() | ||||||
|  |     private val _selectedCheat = MutableStateFlow<Cheat?>(null) | ||||||
|  | 
 | ||||||
|  |     val isAdding get() = _isAdding.asStateFlow() | ||||||
|  |     private val _isAdding = MutableStateFlow(false) | ||||||
|  | 
 | ||||||
|  |     val isEditing get() = _isEditing.asStateFlow() | ||||||
|  |     private val _isEditing = MutableStateFlow(false) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * When a cheat is added, the integer stored in the returned StateFlow | ||||||
|  |      * changes to the position of that cheat, then changes back to null. | ||||||
|  |      */ | ||||||
|  |     val cheatAddedEvent get() = _cheatAddedEvent.asStateFlow() | ||||||
|  |     private val _cheatAddedEvent = MutableStateFlow<Int?>(null) | ||||||
|  | 
 | ||||||
|  |     val cheatChangedEvent get() = _cheatChangedEvent.asStateFlow() | ||||||
|  |     private val _cheatChangedEvent = MutableStateFlow<Int?>(null) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * When a cheat is deleted, the integer stored in the returned StateFlow | ||||||
|  |      * changes to the position of that cheat, then changes back to null. | ||||||
|  |      */ | ||||||
|  |     val cheatDeletedEvent get() = _cheatDeletedEvent.asStateFlow() | ||||||
|  |     private val _cheatDeletedEvent = MutableStateFlow<Int?>(null) | ||||||
|  | 
 | ||||||
|  |     val openDetailsViewEvent get() = _openDetailsViewEvent.asStateFlow() | ||||||
|  |     private val _openDetailsViewEvent = MutableStateFlow(false) | ||||||
|  | 
 | ||||||
|  |     val closeDetailsViewEvent get() = _closeDetailsViewEvent.asStateFlow() | ||||||
|  |     private val _closeDetailsViewEvent = MutableStateFlow(false) | ||||||
|  | 
 | ||||||
|  |     val listViewFocusChange get() = _listViewFocusChange.asStateFlow() | ||||||
|  |     private val _listViewFocusChange = MutableStateFlow(false) | ||||||
|  | 
 | ||||||
|  |     val detailsViewFocusChange get() = _detailsViewFocusChange.asStateFlow() | ||||||
|  |     private val _detailsViewFocusChange = MutableStateFlow(false) | ||||||
|  | 
 | ||||||
|  |     private var cheatEngine: CheatEngine? = null | ||||||
|  |     lateinit var cheats: Array<Cheat> | ||||||
|  |     private var cheatsNeedSaving = false | ||||||
|  |     private var selectedCheatPosition = -1 | ||||||
|  | 
 | ||||||
|  |     fun initialize(titleId: Long) { | ||||||
|  |         cheatEngine = CheatEngine(titleId) | ||||||
|  |         load() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun load() { | ||||||
|  |         cheats = cheatEngine!!.getCheats() | ||||||
|  |         for (i in cheats.indices) { | ||||||
|  |             cheats[i].setEnabledChangedCallback { | ||||||
|  |                 cheatsNeedSaving = true | ||||||
|  |                 notifyCheatUpdated(i) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun saveIfNeeded() { | ||||||
|  |         if (cheatsNeedSaving) { | ||||||
|  |             cheatEngine!!.saveCheatFile() | ||||||
|  |             cheatsNeedSaving = false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setSelectedCheat(cheat: Cheat?, position: Int) { | ||||||
|  |         if (isEditing.value) { | ||||||
|  |             setIsEditing(false) | ||||||
|  |         } | ||||||
|  |         _selectedCheat.value = cheat | ||||||
|  |         selectedCheatPosition = position | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setIsEditing(value: Boolean) { | ||||||
|  |         _isEditing.value = value | ||||||
|  |         if (isAdding.value && !value) { | ||||||
|  |             _isAdding.value = false | ||||||
|  |             setSelectedCheat(null, -1) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun notifyCheatAdded(position: Int) { | ||||||
|  |         _cheatAddedEvent.value = position | ||||||
|  |         _cheatAddedEvent.value = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun startAddingCheat() { | ||||||
|  |         _selectedCheat.value = null | ||||||
|  |         selectedCheatPosition = -1 | ||||||
|  |         _isAdding.value = true | ||||||
|  |         _isEditing.value = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun finishAddingCheat(cheat: Cheat?) { | ||||||
|  |         check(isAdding.value) | ||||||
|  |         _isAdding.value = false | ||||||
|  |         _isEditing.value = false | ||||||
|  |         val position = cheats.size | ||||||
|  |         cheatEngine!!.addCheat(cheat) | ||||||
|  |         cheatsNeedSaving = true | ||||||
|  |         load() | ||||||
|  |         notifyCheatAdded(position) | ||||||
|  |         setSelectedCheat(cheats[position], position) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Notifies that an edit has been made to the contents of the cheat at the given position. | ||||||
|  |      */ | ||||||
|  |     private fun notifyCheatUpdated(position: Int) { | ||||||
|  |         _cheatChangedEvent.value = position | ||||||
|  |         _cheatChangedEvent.value = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun updateSelectedCheat(newCheat: Cheat?) { | ||||||
|  |         cheatEngine!!.updateCheat(selectedCheatPosition, newCheat) | ||||||
|  |         cheatsNeedSaving = true | ||||||
|  |         load() | ||||||
|  |         notifyCheatUpdated(selectedCheatPosition) | ||||||
|  |         setSelectedCheat(cheats[selectedCheatPosition], selectedCheatPosition) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Notifies that the cheat at the given position has been deleted. | ||||||
|  |      */ | ||||||
|  |     private fun notifyCheatDeleted(position: Int) { | ||||||
|  |         _cheatDeletedEvent.value = position | ||||||
|  |         _cheatDeletedEvent.value = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun deleteSelectedCheat() { | ||||||
|  |         val position = selectedCheatPosition | ||||||
|  |         setSelectedCheat(null, -1) | ||||||
|  |         cheatEngine!!.removeCheat(position) | ||||||
|  |         cheatsNeedSaving = true | ||||||
|  |         load() | ||||||
|  |         notifyCheatDeleted(position) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun openDetailsView() { | ||||||
|  |         _openDetailsViewEvent.value = true | ||||||
|  |         _openDetailsViewEvent.value = false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun closeDetailsView() { | ||||||
|  |         _closeDetailsViewEvent.value = true | ||||||
|  |         _closeDetailsViewEvent.value = false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onListViewFocusChanged(changed: Boolean) { | ||||||
|  |         _listViewFocusChange.value = changed | ||||||
|  |         _listViewFocusChange.value = false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onDetailsViewFocusChanged(changed: Boolean) { | ||||||
|  |         _detailsViewFocusChange.value = changed | ||||||
|  |         _detailsViewFocusChange.value = false | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,175 +0,0 @@ | ||||||
| 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.fragment.app.Fragment; |  | ||||||
| import androidx.lifecycle.ViewModelProvider; |  | ||||||
| 
 |  | ||||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder; |  | ||||||
| 
 |  | ||||||
| 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(); |  | ||||||
| 
 |  | ||||||
|         new MaterialAlertDialogBuilder(requireContext()) |  | ||||||
|                 .setMessage(getString(R.string.cheats_delete_confirmation, name)) |  | ||||||
|                 .setPositiveButton(android.R.string.yes, |  | ||||||
|                         (dialog, i) -> mViewModel.deleteSelectedCheat()) |  | ||||||
|                 .setNegativeButton(android.R.string.no, null) |  | ||||||
|                 .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,193 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.features.cheats.ui | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.content.DialogInterface | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.core.view.ViewCompat | ||||||
|  | import androidx.core.view.WindowInsetsCompat | ||||||
|  | import androidx.core.view.updatePadding | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.activityViewModels | ||||||
|  | import androidx.lifecycle.Lifecycle | ||||||
|  | import androidx.lifecycle.lifecycleScope | ||||||
|  | import androidx.lifecycle.repeatOnLifecycle | ||||||
|  | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||||
|  | import kotlinx.coroutines.flow.collect | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import org.citra.citra_emu.R | ||||||
|  | import org.citra.citra_emu.databinding.FragmentCheatDetailsBinding | ||||||
|  | import org.citra.citra_emu.features.cheats.model.Cheat | ||||||
|  | import org.citra.citra_emu.features.cheats.model.CheatsViewModel | ||||||
|  | 
 | ||||||
|  | class CheatDetailsFragment : Fragment() { | ||||||
|  |     private val cheatsViewModel: CheatsViewModel by activityViewModels() | ||||||
|  | 
 | ||||||
|  |     private var _binding: FragmentCheatDetailsBinding? = null | ||||||
|  |     private val binding get() = _binding!! | ||||||
|  | 
 | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         _binding = FragmentCheatDetailsBinding.inflate(layoutInflater) | ||||||
|  |         return binding.root | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // This is using the correct scope, lint is just acting up | ||||||
|  |     @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         viewLifecycleOwner.lifecycleScope.apply { | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.selectedCheat.collect { onSelectedCheatUpdated(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.isEditing.collect { onIsEditingUpdated(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         binding.buttonDelete.setOnClickListener { onDeleteClicked() } | ||||||
|  |         binding.buttonEdit.setOnClickListener { onEditClicked() } | ||||||
|  |         binding.buttonCancel.setOnClickListener { onCancelClicked() } | ||||||
|  |         binding.buttonOk.setOnClickListener { 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) { _, hasFocus -> | ||||||
|  |             cheatsViewModel.onDetailsViewFocusChanged(hasFocus) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         binding.toolbarCheatDetails.setNavigationOnClickListener { | ||||||
|  |             cheatsViewModel.closeDetailsView() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setInsets() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         _binding = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun clearEditErrors() { | ||||||
|  |         binding.editName.error = null | ||||||
|  |         binding.editCode.error = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onDeleteClicked() { | ||||||
|  |         val name = binding.editNameInput.text.toString() | ||||||
|  |         MaterialAlertDialogBuilder(requireContext()) | ||||||
|  |             .setMessage(getString(R.string.cheats_delete_confirmation, name)) | ||||||
|  |             .setPositiveButton( | ||||||
|  |                 android.R.string.ok | ||||||
|  |             ) { _: DialogInterface?, _: Int -> cheatsViewModel.deleteSelectedCheat() } | ||||||
|  |             .setNegativeButton(android.R.string.cancel, null) | ||||||
|  |             .show() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onEditClicked() { | ||||||
|  |         cheatsViewModel.setIsEditing(true) | ||||||
|  |         binding.buttonOk.requestFocus() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onCancelClicked() { | ||||||
|  |         cheatsViewModel.setIsEditing(false) | ||||||
|  |         onSelectedCheatUpdated(cheatsViewModel.selectedCheat.value) | ||||||
|  |         binding.buttonDelete.requestFocus() | ||||||
|  |         cheatsViewModel.closeDetailsView() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onOkClicked() { | ||||||
|  |         clearEditErrors() | ||||||
|  |         val name = binding.editNameInput.text.toString() | ||||||
|  |         val notes = binding.editNotesInput.text.toString() | ||||||
|  |         val code = binding.editCodeInput.text.toString() | ||||||
|  |         if (name.isEmpty()) { | ||||||
|  |             binding.editName.error = getString(R.string.cheats_error_no_name) | ||||||
|  |             binding.scrollView.smoothScrollTo(0, binding.editNameInput.top) | ||||||
|  |             return | ||||||
|  |         } else if (code.isEmpty()) { | ||||||
|  |             binding.editCode.error = getString(R.string.cheats_error_no_code_lines) | ||||||
|  |             binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         val validityResult = Cheat.isValidGatewayCode(code) | ||||||
|  |         if (validityResult != 0) { | ||||||
|  |             binding.editCode.error = getString(R.string.cheats_error_on_line, validityResult) | ||||||
|  |             binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         val newCheat = Cheat.createGatewayCode(name, notes, code) | ||||||
|  |         if (cheatsViewModel.isAdding.value == true) { | ||||||
|  |             cheatsViewModel.finishAddingCheat(newCheat) | ||||||
|  |         } else { | ||||||
|  |             cheatsViewModel.updateSelectedCheat(newCheat) | ||||||
|  |         } | ||||||
|  |         binding.buttonEdit.requestFocus() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onSelectedCheatUpdated(cheat: Cheat?) { | ||||||
|  |         clearEditErrors() | ||||||
|  |         val isEditing: Boolean = cheatsViewModel.isEditing.value == true | ||||||
|  | 
 | ||||||
|  |         // 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) { | ||||||
|  |                 binding.editNameInput.setText("") | ||||||
|  |                 binding.editNotesInput.setText("") | ||||||
|  |                 binding.editCodeInput.setText("") | ||||||
|  |             } else { | ||||||
|  |                 binding.editNameInput.setText(cheat.getName()) | ||||||
|  |                 binding.editNotesInput.setText(cheat.getNotes()) | ||||||
|  |                 binding.editCodeInput.setText(cheat.getCode()) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onIsEditingUpdated(isEditing: Boolean) { | ||||||
|  |         if (isEditing) { | ||||||
|  |             binding.root.visibility = View.VISIBLE | ||||||
|  |         } | ||||||
|  |         binding.editNameInput.isEnabled = isEditing | ||||||
|  |         binding.editNotesInput.isEnabled = isEditing | ||||||
|  |         binding.editCodeInput.isEnabled = isEditing | ||||||
|  | 
 | ||||||
|  |         binding.buttonDelete.visibility = if (isEditing) View.GONE else View.VISIBLE | ||||||
|  |         binding.buttonEdit.visibility = if (isEditing) View.GONE else View.VISIBLE | ||||||
|  |         binding.buttonCancel.visibility = if (isEditing) View.VISIBLE else View.GONE | ||||||
|  |         binding.buttonOk.visibility = if (isEditing) View.VISIBLE else View.GONE | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setInsets() = | ||||||
|  |         ViewCompat.setOnApplyWindowInsetsListener( | ||||||
|  |             binding.root | ||||||
|  |         ) { _: View?, windowInsets: WindowInsetsCompat -> | ||||||
|  |             val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||||||
|  |             val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||||||
|  | 
 | ||||||
|  |             val leftInsets = barInsets.left + cutoutInsets.left | ||||||
|  |             val rightInsets = barInsets.right + cutoutInsets.right | ||||||
|  | 
 | ||||||
|  |             val mlpAppBar = binding.toolbarCheatDetails.layoutParams as ViewGroup.MarginLayoutParams | ||||||
|  |             mlpAppBar.leftMargin = leftInsets | ||||||
|  |             mlpAppBar.rightMargin = rightInsets | ||||||
|  |             binding.toolbarCheatDetails.layoutParams = mlpAppBar | ||||||
|  | 
 | ||||||
|  |             binding.scrollView.updatePadding(left = leftInsets, right = rightInsets) | ||||||
|  |             binding.buttonContainer.updatePadding(left = leftInsets, right = rightInsets) | ||||||
|  | 
 | ||||||
|  |             windowInsets | ||||||
|  |         } | ||||||
|  | } | ||||||
|  | @ -1,71 +0,0 @@ | ||||||
| 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.core.graphics.Insets; |  | ||||||
| import androidx.core.view.ViewCompat; |  | ||||||
| import androidx.core.view.WindowInsetsCompat; |  | ||||||
| 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 { |  | ||||||
|     private RecyclerView mRecyclerView; |  | ||||||
|     private FloatingActionButton mFab; |  | ||||||
| 
 |  | ||||||
|     @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) { |  | ||||||
|         mRecyclerView = view.findViewById(R.id.cheat_list); |  | ||||||
|         mFab = view.findViewById(R.id.fab); |  | ||||||
| 
 |  | ||||||
|         CheatsActivity activity = (CheatsActivity) requireActivity(); |  | ||||||
|         CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); |  | ||||||
| 
 |  | ||||||
|         mRecyclerView.setAdapter(new CheatsAdapter(activity, viewModel)); |  | ||||||
|         mRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); |  | ||||||
|         mRecyclerView.addItemDecoration(new DividerItemDecoration(activity, null)); |  | ||||||
| 
 |  | ||||||
|         mFab.setOnClickListener(v -> { |  | ||||||
|             viewModel.startAddingCheat(); |  | ||||||
|             viewModel.openDetailsView(); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         setInsets(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void setInsets() { |  | ||||||
|         ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> { |  | ||||||
|             Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); |  | ||||||
|             v.setPadding(0, 0, 0, insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_fab_list)); |  | ||||||
| 
 |  | ||||||
|             ViewGroup.MarginLayoutParams mlpFab = |  | ||||||
|                     (ViewGroup.MarginLayoutParams) mFab.getLayoutParams(); |  | ||||||
|             int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large); |  | ||||||
|             mlpFab.leftMargin = insets.left + fabPadding; |  | ||||||
|             mlpFab.bottomMargin = insets.bottom + fabPadding; |  | ||||||
|             mlpFab.rightMargin = insets.right + fabPadding; |  | ||||||
|             mFab.setLayoutParams(mlpFab); |  | ||||||
| 
 |  | ||||||
|             return windowInsets; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,143 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.features.cheats.ui | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.view.ViewGroup.MarginLayoutParams | ||||||
|  | import androidx.core.view.ViewCompat | ||||||
|  | import androidx.core.view.WindowInsetsCompat | ||||||
|  | import androidx.core.view.updatePadding | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.activityViewModels | ||||||
|  | import androidx.lifecycle.Lifecycle | ||||||
|  | import androidx.lifecycle.lifecycleScope | ||||||
|  | import androidx.lifecycle.repeatOnLifecycle | ||||||
|  | import androidx.navigation.findNavController | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
|  | import com.google.android.material.divider.MaterialDividerItemDecoration | ||||||
|  | import kotlinx.coroutines.flow.collect | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import org.citra.citra_emu.R | ||||||
|  | import org.citra.citra_emu.databinding.FragmentCheatListBinding | ||||||
|  | import org.citra.citra_emu.features.cheats.model.CheatsViewModel | ||||||
|  | import org.citra.citra_emu.ui.main.MainActivity | ||||||
|  | 
 | ||||||
|  | class CheatListFragment : Fragment() { | ||||||
|  |     private var _binding: FragmentCheatListBinding? = null | ||||||
|  |     private val binding get() = _binding!! | ||||||
|  | 
 | ||||||
|  |     private val cheatsViewModel: CheatsViewModel by activityViewModels() | ||||||
|  | 
 | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         _binding = FragmentCheatListBinding.inflate(layoutInflater) | ||||||
|  |         return binding.root | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // This is using the correct scope, lint is just acting up | ||||||
|  |     @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  | 
 | ||||||
|  |         binding.cheatList.adapter = CheatsAdapter(requireActivity(), cheatsViewModel) | ||||||
|  |         binding.cheatList.layoutManager = LinearLayoutManager(requireContext()) | ||||||
|  |         binding.cheatList.addItemDecoration( | ||||||
|  |             MaterialDividerItemDecoration( | ||||||
|  |                 requireContext(), | ||||||
|  |                 MaterialDividerItemDecoration.VERTICAL | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         viewLifecycleOwner.lifecycleScope.apply { | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.cheatAddedEvent.collect { position: Int? -> | ||||||
|  |                         position?.let { | ||||||
|  |                             binding.cheatList.apply { | ||||||
|  |                                 post { (adapter as CheatsAdapter).notifyItemInserted(it) } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.cheatChangedEvent.collect { position: Int? -> | ||||||
|  |                         position?.let { | ||||||
|  |                             binding.cheatList.apply { | ||||||
|  |                                 post { (adapter as CheatsAdapter).notifyItemChanged(it) } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.cheatDeletedEvent.collect { position: Int? -> | ||||||
|  |                         position?.let { | ||||||
|  |                             binding.cheatList.apply { | ||||||
|  |                                 post { (adapter as CheatsAdapter).notifyItemRemoved(it) } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         binding.fab.setOnClickListener { | ||||||
|  |             cheatsViewModel.startAddingCheat() | ||||||
|  |             cheatsViewModel.openDetailsView() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         binding.toolbarCheatList.setNavigationOnClickListener { | ||||||
|  |             if (requireActivity() is MainActivity) { | ||||||
|  |                 view.findNavController().popBackStack() | ||||||
|  |             } else { | ||||||
|  |                 requireActivity().finish() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setInsets() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setInsets() { | ||||||
|  |         ViewCompat.setOnApplyWindowInsetsListener( | ||||||
|  |             binding.root | ||||||
|  |         ) { _: View, windowInsets: WindowInsetsCompat -> | ||||||
|  |             val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||||||
|  |             val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||||||
|  | 
 | ||||||
|  |             val leftInsets = barInsets.left + cutoutInsets.left | ||||||
|  |             val rightInsets = barInsets.right + cutoutInsets.right | ||||||
|  | 
 | ||||||
|  |             val mlpAppBar = binding.toolbarCheatList.layoutParams as MarginLayoutParams | ||||||
|  |             mlpAppBar.leftMargin = leftInsets | ||||||
|  |             mlpAppBar.rightMargin = rightInsets | ||||||
|  |             binding.toolbarCheatList.layoutParams = mlpAppBar | ||||||
|  | 
 | ||||||
|  |             binding.cheatList.updatePadding( | ||||||
|  |                 left = leftInsets, | ||||||
|  |                 right = rightInsets, | ||||||
|  |                 bottom = barInsets.bottom + | ||||||
|  |                         resources.getDimensionPixelSize(R.dimen.spacing_fab_list) | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             val mlpFab = binding.fab.layoutParams as MarginLayoutParams | ||||||
|  |             val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large) | ||||||
|  |             mlpFab.leftMargin = leftInsets + fabPadding | ||||||
|  |             mlpFab.bottomMargin = barInsets.bottom + fabPadding | ||||||
|  |             mlpFab.rightMargin = rightInsets + fabPadding | ||||||
|  |             binding.fab.layoutParams = mlpFab | ||||||
|  |             windowInsets | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,56 +0,0 @@ | ||||||
| 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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,235 +0,0 @@ | ||||||
| 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.graphics.Insets; |  | ||||||
| import androidx.core.view.ViewCompat; |  | ||||||
| import androidx.core.view.WindowCompat; |  | ||||||
| import androidx.core.view.WindowInsetsAnimationCompat; |  | ||||||
| import androidx.core.view.WindowInsetsCompat; |  | ||||||
| import androidx.lifecycle.ViewModelProvider; |  | ||||||
| import androidx.slidingpanelayout.widget.SlidingPaneLayout; |  | ||||||
| 
 |  | ||||||
| import com.google.android.material.appbar.AppBarLayout; |  | ||||||
| import com.google.android.material.appbar.MaterialToolbar; |  | ||||||
| 
 |  | ||||||
| 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; |  | ||||||
| import org.citra.citra_emu.utils.InsetsHelper; |  | ||||||
| import org.citra.citra_emu.utils.ThemeUtil; |  | ||||||
| 
 |  | ||||||
| import java.util.List; |  | ||||||
| 
 |  | ||||||
| public class CheatsActivity extends AppCompatActivity |  | ||||||
|         implements SlidingPaneLayout.PanelSlideListener { |  | ||||||
|     private static String ARG_TITLE_ID = "title_id"; |  | ||||||
| 
 |  | ||||||
|     private CheatsViewModel mViewModel; |  | ||||||
| 
 |  | ||||||
|     private SlidingPaneLayout mSlidingPaneLayout; |  | ||||||
|     private View mCheatList; |  | ||||||
|     private View mCheatDetails; |  | ||||||
| 
 |  | ||||||
|     private View mCheatListLastFocus; |  | ||||||
|     private View mCheatDetailsLastFocus; |  | ||||||
| 
 |  | ||||||
|     public static void launch(Context context, long titleId) { |  | ||||||
|         Intent intent = new Intent(context, CheatsActivity.class); |  | ||||||
|         intent.putExtra(ARG_TITLE_ID, titleId); |  | ||||||
|         context.startActivity(intent); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         ThemeUtil.INSTANCE.setTheme(this); |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
| 
 |  | ||||||
|         WindowCompat.setDecorFitsSystemWindows(getWindow(), false); |  | ||||||
| 
 |  | ||||||
|         long titleId = getIntent().getLongExtra(ARG_TITLE_ID, -1); |  | ||||||
| 
 |  | ||||||
|         mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class); |  | ||||||
|         mViewModel.initialize(titleId); |  | ||||||
| 
 |  | ||||||
|         setContentView(R.layout.activity_cheats); |  | ||||||
| 
 |  | ||||||
|         mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout); |  | ||||||
|         mCheatList = findViewById(R.id.cheat_list_container); |  | ||||||
|         mCheatDetails = findViewById(R.id.cheat_details_container); |  | ||||||
| 
 |  | ||||||
|         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 |  | ||||||
|         MaterialToolbar toolbar = findViewById(R.id.toolbar_cheats); |  | ||||||
|         setSupportActionBar(toolbar); |  | ||||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); |  | ||||||
| 
 |  | ||||||
|         setInsets(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @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); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void setInsets() { |  | ||||||
|         AppBarLayout appBarLayout = findViewById(R.id.appbar_cheats); |  | ||||||
|         ViewCompat.setOnApplyWindowInsetsListener(mSlidingPaneLayout, (v, windowInsets) -> { |  | ||||||
|             Insets barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); |  | ||||||
|             Insets keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()); |  | ||||||
| 
 |  | ||||||
|             InsetsHelper.insetAppBar(barInsets, appBarLayout); |  | ||||||
|             mSlidingPaneLayout.setPadding(barInsets.left, 0, barInsets.right, 0); |  | ||||||
| 
 |  | ||||||
|             // Set keyboard insets if the system supports smooth keyboard animations |  | ||||||
|             ViewGroup.MarginLayoutParams mlpDetails = |  | ||||||
|                     (ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams(); |  | ||||||
|             if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) { |  | ||||||
|                 if (keyboardInsets.bottom > 0) { |  | ||||||
|                     mlpDetails.bottomMargin = keyboardInsets.bottom; |  | ||||||
|                 } else { |  | ||||||
|                     mlpDetails.bottomMargin = barInsets.bottom; |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 if (mlpDetails.bottomMargin == 0) { |  | ||||||
|                     mlpDetails.bottomMargin = barInsets.bottom; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             mCheatDetails.setLayoutParams(mlpDetails); |  | ||||||
| 
 |  | ||||||
|             return windowInsets; |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         // Update the layout for every frame that the keyboard animates in |  | ||||||
|         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { |  | ||||||
|             ViewCompat.setWindowInsetsAnimationCallback(mCheatDetails, |  | ||||||
|                     new WindowInsetsAnimationCompat.Callback( |  | ||||||
|                             WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) { |  | ||||||
|                         int keyboardInsets = 0; |  | ||||||
|                         int barInsets = 0; |  | ||||||
| 
 |  | ||||||
|                         @NonNull |  | ||||||
|                         @Override |  | ||||||
|                         public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, |  | ||||||
|                                                              @NonNull List<WindowInsetsAnimationCompat> runningAnimations) { |  | ||||||
|                             ViewGroup.MarginLayoutParams mlpDetails = |  | ||||||
|                                     (ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams(); |  | ||||||
|                             keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom; |  | ||||||
|                             barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom; |  | ||||||
|                             mlpDetails.bottomMargin = Math.max(keyboardInsets, barInsets); |  | ||||||
|                             mCheatDetails.setLayoutParams(mlpDetails); |  | ||||||
|                             return insets; |  | ||||||
|                         } |  | ||||||
|                     }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.features.cheats.ui | ||||||
|  | 
 | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.View | ||||||
|  | import android.view.View.OnFocusChangeListener | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.appcompat.app.AppCompatActivity | ||||||
|  | import androidx.core.view.WindowCompat | ||||||
|  | import androidx.navigation.fragment.NavHostFragment | ||||||
|  | import com.google.android.material.color.MaterialColors | ||||||
|  | import org.citra.citra_emu.R | ||||||
|  | import org.citra.citra_emu.databinding.ActivityCheatsBinding | ||||||
|  | import org.citra.citra_emu.utils.InsetsHelper | ||||||
|  | import org.citra.citra_emu.utils.ThemeUtil | ||||||
|  | 
 | ||||||
|  | class CheatsActivity : AppCompatActivity() { | ||||||
|  |     private lateinit var binding: ActivityCheatsBinding | ||||||
|  | 
 | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         ThemeUtil.setTheme(this) | ||||||
|  | 
 | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  | 
 | ||||||
|  |         binding = ActivityCheatsBinding.inflate(layoutInflater) | ||||||
|  |         setContentView(binding.root) | ||||||
|  | 
 | ||||||
|  |         WindowCompat.setDecorFitsSystemWindows(window, false) | ||||||
|  |         if (InsetsHelper.getSystemGestureType(applicationContext) != | ||||||
|  |             InsetsHelper.GESTURE_NAVIGATION | ||||||
|  |         ) { | ||||||
|  |             binding.navigationBarShade.setBackgroundColor( | ||||||
|  |                 ThemeUtil.getColorWithOpacity( | ||||||
|  |                     MaterialColors.getColor( | ||||||
|  |                         binding.navigationBarShade, | ||||||
|  |                         com.google.android.material.R.attr.colorSurface | ||||||
|  |                     ), | ||||||
|  |                     ThemeUtil.SYSTEM_BAR_ALPHA | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val navHostFragment = | ||||||
|  |             supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment | ||||||
|  |         val navController = navHostFragment.navController | ||||||
|  |         navController.setGraph(R.navigation.cheats_navigation, intent.extras) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         fun setOnFocusChangeListenerRecursively(view: View, listener: OnFocusChangeListener?) { | ||||||
|  |             view.onFocusChangeListener = listener | ||||||
|  |             if (view is ViewGroup) { | ||||||
|  |                 for (i in 0 until view.childCount) { | ||||||
|  |                     val child = view.getChildAt(i) | ||||||
|  |                     setOnFocusChangeListenerRecursively(child, listener) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,72 +0,0 @@ | ||||||
| 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,69 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.features.cheats.ui | ||||||
|  | 
 | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.CompoundButton | ||||||
|  | import androidx.fragment.app.FragmentActivity | ||||||
|  | import androidx.lifecycle.ViewModelProvider | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import org.citra.citra_emu.databinding.ListItemCheatBinding | ||||||
|  | import org.citra.citra_emu.features.cheats.model.Cheat | ||||||
|  | import org.citra.citra_emu.features.cheats.model.CheatsViewModel | ||||||
|  | 
 | ||||||
|  | class CheatsAdapter( | ||||||
|  |     private val activity: FragmentActivity, | ||||||
|  |     private val viewModel: CheatsViewModel | ||||||
|  | ) : RecyclerView.Adapter<CheatsAdapter.CheatViewHolder>() { | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheatViewHolder { | ||||||
|  |         val binding = | ||||||
|  |             ListItemCheatBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||||||
|  |         addViewListeners(binding.root) | ||||||
|  |         return CheatViewHolder(binding) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onBindViewHolder(holder: CheatViewHolder, position: Int) = | ||||||
|  |         holder.bind(activity, viewModel.cheats[position], position) | ||||||
|  | 
 | ||||||
|  |     override fun getItemCount(): Int = viewModel.cheats.size | ||||||
|  | 
 | ||||||
|  |     private fun 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) { _, hasFocus -> | ||||||
|  |             viewModel.onListViewFocusChanged(hasFocus) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     inner class CheatViewHolder(private val binding: ListItemCheatBinding) : | ||||||
|  |         RecyclerView.ViewHolder(binding.root), View.OnClickListener, | ||||||
|  |         CompoundButton.OnCheckedChangeListener { | ||||||
|  |         private lateinit var viewModel: CheatsViewModel | ||||||
|  |         private lateinit var cheat: Cheat | ||||||
|  |         private var position = 0 | ||||||
|  | 
 | ||||||
|  |         fun bind(activity: FragmentActivity, cheat: Cheat, position: Int) { | ||||||
|  |             viewModel = ViewModelProvider(activity)[CheatsViewModel::class.java] | ||||||
|  |             this.cheat = cheat | ||||||
|  |             this.position = position | ||||||
|  |             binding.textName.text = this.cheat.getName() | ||||||
|  |             binding.cheatSwitch.isChecked = this.cheat.getEnabled() | ||||||
|  |             binding.cheatContainer.setOnClickListener(this) | ||||||
|  |             binding.cheatSwitch.setOnCheckedChangeListener(this) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         override fun onClick(root: View) { | ||||||
|  |             viewModel.setSelectedCheat(cheat, position) | ||||||
|  |             viewModel.openDetailsView() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { | ||||||
|  |             cheat.setEnabled(isChecked) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,244 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.features.cheats.ui | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.os.Build | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.activity.OnBackPressedCallback | ||||||
|  | import androidx.core.view.ViewCompat | ||||||
|  | import androidx.core.view.WindowInsetsAnimationCompat | ||||||
|  | import androidx.core.view.WindowInsetsCompat | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.activityViewModels | ||||||
|  | import androidx.lifecycle.Lifecycle | ||||||
|  | import androidx.lifecycle.lifecycleScope | ||||||
|  | import androidx.lifecycle.repeatOnLifecycle | ||||||
|  | import androidx.navigation.findNavController | ||||||
|  | import androidx.navigation.fragment.navArgs | ||||||
|  | import androidx.slidingpanelayout.widget.SlidingPaneLayout | ||||||
|  | import com.google.android.material.transition.MaterialSharedAxis | ||||||
|  | import kotlinx.coroutines.flow.collect | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import org.citra.citra_emu.databinding.FragmentCheatsBinding | ||||||
|  | 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 | ||||||
|  | import org.citra.citra_emu.ui.main.MainActivity | ||||||
|  | import org.citra.citra_emu.viewmodel.HomeViewModel | ||||||
|  | 
 | ||||||
|  | class CheatsFragment : Fragment(), SlidingPaneLayout.PanelSlideListener { | ||||||
|  |     private var cheatListLastFocus: View? = null | ||||||
|  |     private var cheatDetailsLastFocus: View? = null | ||||||
|  | 
 | ||||||
|  |     private var _binding: FragmentCheatsBinding? = null | ||||||
|  |     private val binding get() = _binding!! | ||||||
|  | 
 | ||||||
|  |     private val cheatsViewModel: CheatsViewModel by activityViewModels() | ||||||
|  |     private val homeViewModel: HomeViewModel by activityViewModels() | ||||||
|  | 
 | ||||||
|  |     private val args by navArgs<CheatsFragmentArgs>() | ||||||
|  | 
 | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) | ||||||
|  |         returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         _binding = FragmentCheatsBinding.inflate(inflater) | ||||||
|  |         return binding.root | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // This is using the correct scope, lint is just acting up | ||||||
|  |     @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  | 
 | ||||||
|  |         homeViewModel.setNavigationVisibility(visible = false, animated = true) | ||||||
|  |         homeViewModel.setStatusBarShadeVisibility(visible = false) | ||||||
|  | 
 | ||||||
|  |         cheatsViewModel.initialize(args.titleId) | ||||||
|  | 
 | ||||||
|  |         cheatListLastFocus = binding.cheatListContainer | ||||||
|  |         cheatDetailsLastFocus = binding.cheatDetailsContainer | ||||||
|  |         binding.slidingPaneLayout.addPanelSlideListener(this) | ||||||
|  |         requireActivity().onBackPressedDispatcher.addCallback( | ||||||
|  |             viewLifecycleOwner, | ||||||
|  |             TwoPaneOnBackPressedCallback(binding.slidingPaneLayout) | ||||||
|  |         ) | ||||||
|  |         requireActivity().onBackPressedDispatcher.addCallback( | ||||||
|  |             viewLifecycleOwner, | ||||||
|  |             object : OnBackPressedCallback(true) { | ||||||
|  |                 override fun handleOnBackPressed() { | ||||||
|  |                     if (binding.slidingPaneLayout.isOpen) { | ||||||
|  |                         binding.slidingPaneLayout.close() | ||||||
|  |                     } else { | ||||||
|  |                         if (requireActivity() is MainActivity) { | ||||||
|  |                             view.findNavController().popBackStack() | ||||||
|  |                         } else { | ||||||
|  |                             requireActivity().finish() | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         viewLifecycleOwner.lifecycleScope.apply { | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.selectedCheat.collect { onSelectedCheatChanged(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.isEditing.collect { onIsEditingChanged(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.openDetailsViewEvent.collect { openDetailsView(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.closeDetailsViewEvent.collect { closeDetailsView(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.listViewFocusChange.collect { onListViewFocusChange(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             launch { | ||||||
|  |                 repeatOnLifecycle(Lifecycle.State.CREATED) { | ||||||
|  |                     cheatsViewModel.detailsViewFocusChange.collect { onDetailsViewFocusChange(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setInsets() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onStop() { | ||||||
|  |         super.onStop() | ||||||
|  |         cheatsViewModel.saveIfNeeded() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         _binding = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onPanelSlide(panel: View, slideOffset: Float) {} | ||||||
|  |     override fun onPanelOpened(panel: View) { | ||||||
|  |         val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL | ||||||
|  |         cheatDetailsLastFocus!!.requestFocus(if (rtl) View.FOCUS_LEFT else View.FOCUS_RIGHT) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onPanelClosed(panel: View) { | ||||||
|  |         val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL | ||||||
|  |         cheatListLastFocus!!.requestFocus(if (rtl) View.FOCUS_RIGHT else View.FOCUS_LEFT) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onIsEditingChanged(isEditing: Boolean) { | ||||||
|  |         if (isEditing) { | ||||||
|  |             binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_UNLOCKED | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun onSelectedCheatChanged(selectedCheat: Cheat?) { | ||||||
|  |         val cheatSelected = selectedCheat != null || cheatsViewModel.isEditing.value!! | ||||||
|  |         if (!cheatSelected && binding.slidingPaneLayout.isOpen) { | ||||||
|  |             binding.slidingPaneLayout.close() | ||||||
|  |         } | ||||||
|  |         binding.slidingPaneLayout.lockMode = | ||||||
|  |             if (cheatSelected) SlidingPaneLayout.LOCK_MODE_UNLOCKED else SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onListViewFocusChange(hasFocus: Boolean) { | ||||||
|  |         if (hasFocus) { | ||||||
|  |             cheatListLastFocus = binding.cheatListContainer.findFocus() | ||||||
|  |             if (cheatListLastFocus == null) throw NullPointerException() | ||||||
|  |             binding.slidingPaneLayout.close() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onDetailsViewFocusChange(hasFocus: Boolean) { | ||||||
|  |         if (hasFocus) { | ||||||
|  |             cheatDetailsLastFocus = binding.cheatDetailsContainer.findFocus() | ||||||
|  |             if (cheatDetailsLastFocus == null) { | ||||||
|  |                 throw NullPointerException() | ||||||
|  |             } | ||||||
|  |             binding.slidingPaneLayout.open() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun openDetailsView(open: Boolean) { | ||||||
|  |         if (open) { | ||||||
|  |             binding.slidingPaneLayout.open() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun closeDetailsView(close: Boolean) { | ||||||
|  |         if (close) { | ||||||
|  |             binding.slidingPaneLayout.close() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setInsets() { | ||||||
|  |         ViewCompat.setOnApplyWindowInsetsListener( | ||||||
|  |             binding.slidingPaneLayout | ||||||
|  |         ) { _: View?, windowInsets: WindowInsetsCompat -> | ||||||
|  |             val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||||||
|  |             val keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) | ||||||
|  | 
 | ||||||
|  |             // Set keyboard insets if the system supports smooth keyboard animations | ||||||
|  |             val mlpDetails = binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams | ||||||
|  |             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { | ||||||
|  |                 if (keyboardInsets.bottom > 0) { | ||||||
|  |                     mlpDetails.bottomMargin = keyboardInsets.bottom | ||||||
|  |                 } else { | ||||||
|  |                     mlpDetails.bottomMargin = barInsets.bottom | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if (mlpDetails.bottomMargin == 0) { | ||||||
|  |                     mlpDetails.bottomMargin = barInsets.bottom | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             binding.cheatDetailsContainer.layoutParams = mlpDetails | ||||||
|  |             windowInsets | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Update the layout for every frame that the keyboard animates in | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||||||
|  |             ViewCompat.setWindowInsetsAnimationCallback( | ||||||
|  |                 binding.cheatDetailsContainer, | ||||||
|  |                 object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { | ||||||
|  |                     var keyboardInsets = 0 | ||||||
|  |                     var barInsets = 0 | ||||||
|  |                     override fun onProgress( | ||||||
|  |                         insets: WindowInsetsCompat, | ||||||
|  |                         runningAnimations: List<WindowInsetsAnimationCompat> | ||||||
|  |                     ): WindowInsetsCompat { | ||||||
|  |                         val mlpDetails = | ||||||
|  |                             binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams | ||||||
|  |                         keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom | ||||||
|  |                         barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom | ||||||
|  |                         mlpDetails.bottomMargin = keyboardInsets.coerceAtLeast(barInsets) | ||||||
|  |                         binding.cheatDetailsContainer.layoutParams = mlpDetails | ||||||
|  |                         return insets | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -2,9 +2,7 @@ | ||||||
| // Licensed under GPLv2 or any later version | // Licensed under GPLv2 or any later version | ||||||
| // Refer to the license.txt file included. | // Refer to the license.txt file included. | ||||||
| 
 | 
 | ||||||
| package org.citra.citra_emu.features.settings.model.view | package org.citra.citra_emu.features.settings.model | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.features.settings.model.AbstractSetting |  | ||||||
| 
 | 
 | ||||||
| interface AbstractShortSetting : AbstractSetting { | interface AbstractShortSetting : AbstractSetting { | ||||||
|     var short: Short |     var short: Short | ||||||
|  | @ -6,6 +6,7 @@ package org.citra.citra_emu.features.settings.model.view | ||||||
| 
 | 
 | ||||||
| import org.citra.citra_emu.features.settings.model.AbstractIntSetting | import org.citra.citra_emu.features.settings.model.AbstractIntSetting | ||||||
| import org.citra.citra_emu.features.settings.model.AbstractSetting | import org.citra.citra_emu.features.settings.model.AbstractSetting | ||||||
|  | import org.citra.citra_emu.features.settings.model.AbstractShortSetting | ||||||
| 
 | 
 | ||||||
| class SingleChoiceSetting( | class SingleChoiceSetting( | ||||||
|     setting: AbstractSetting?, |     setting: AbstractSetting?, | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ | ||||||
| package org.citra.citra_emu.features.settings.model.view | package org.citra.citra_emu.features.settings.model.view | ||||||
| 
 | 
 | ||||||
| import org.citra.citra_emu.features.settings.model.AbstractSetting | import org.citra.citra_emu.features.settings.model.AbstractSetting | ||||||
|  | import org.citra.citra_emu.features.settings.model.AbstractShortSetting | ||||||
| import org.citra.citra_emu.features.settings.model.AbstractStringSetting | import org.citra.citra_emu.features.settings.model.AbstractStringSetting | ||||||
| 
 | 
 | ||||||
| class StringSingleChoiceSetting( | class StringSingleChoiceSetting( | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ import org.citra.citra_emu.features.settings.model.AbstractSetting | ||||||
| import org.citra.citra_emu.features.settings.model.AbstractStringSetting | import org.citra.citra_emu.features.settings.model.AbstractStringSetting | ||||||
| import org.citra.citra_emu.features.settings.model.FloatSetting | import org.citra.citra_emu.features.settings.model.FloatSetting | ||||||
| import org.citra.citra_emu.features.settings.model.ScaledFloatSetting | import org.citra.citra_emu.features.settings.model.ScaledFloatSetting | ||||||
| import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting | import org.citra.citra_emu.features.settings.model.AbstractShortSetting | ||||||
| import org.citra.citra_emu.features.settings.model.view.DateTimeSetting | import org.citra.citra_emu.features.settings.model.view.DateTimeSetting | ||||||
| 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.model.view.SettingsItem | import org.citra.citra_emu.features.settings.model.view.SettingsItem | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ import org.citra.citra_emu.features.settings.model.IntSetting | ||||||
| import org.citra.citra_emu.features.settings.model.ScaledFloatSetting | import org.citra.citra_emu.features.settings.model.ScaledFloatSetting | ||||||
| import org.citra.citra_emu.features.settings.model.Settings | import org.citra.citra_emu.features.settings.model.Settings | ||||||
| import org.citra.citra_emu.features.settings.model.StringSetting | import org.citra.citra_emu.features.settings.model.StringSetting | ||||||
| import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting | import org.citra.citra_emu.features.settings.model.AbstractShortSetting | ||||||
| import org.citra.citra_emu.features.settings.model.view.DateTimeSetting | import org.citra.citra_emu.features.settings.model.view.DateTimeSetting | ||||||
| import org.citra.citra_emu.features.settings.model.view.HeaderSetting | import org.citra.citra_emu.features.settings.model.view.HeaderSetting | ||||||
| import org.citra.citra_emu.features.settings.model.view.InputBindingSetting | import org.citra.citra_emu.features.settings.model.view.InputBindingSetting | ||||||
|  |  | ||||||
|  | @ -139,9 +139,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram | ||||||
|         emulationActivity = requireActivity() as EmulationActivity |         emulationActivity = requireActivity() as EmulationActivity | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Initialize the UI and start emulation in here. |  | ||||||
|      */ |  | ||||||
|     override fun onCreateView( |     override fun onCreateView( | ||||||
|         inflater: LayoutInflater, |         inflater: LayoutInflater, | ||||||
|         container: ViewGroup?, |         container: ViewGroup?, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,115 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.fragments | ||||||
|  | 
 | ||||||
|  | import android.app.Dialog | ||||||
|  | import android.content.DialogInterface | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.text.InputFilter | ||||||
|  | import androidx.fragment.app.DialogFragment | ||||||
|  | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||||
|  | import org.citra.citra_emu.R | ||||||
|  | import org.citra.citra_emu.applets.SoftwareKeyboard | ||||||
|  | import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding | ||||||
|  | import org.citra.citra_emu.utils.SerializableHelper.serializable | ||||||
|  | 
 | ||||||
|  | class KeyboardDialogFragment : DialogFragment() { | ||||||
|  |     private lateinit var config: SoftwareKeyboard.KeyboardConfig | ||||||
|  | 
 | ||||||
|  |     private var _binding: DialogSoftwareKeyboardBinding? = null | ||||||
|  |     private val binding get() = _binding!! | ||||||
|  | 
 | ||||||
|  |     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||||||
|  |         _binding = DialogSoftwareKeyboardBinding.inflate(layoutInflater) | ||||||
|  | 
 | ||||||
|  |         config = requireArguments().serializable<SoftwareKeyboard.KeyboardConfig>(CONFIG)!! | ||||||
|  | 
 | ||||||
|  |         binding.apply { | ||||||
|  |             editText.hint = config.hintText | ||||||
|  |             editTextInput.isSingleLine = !config.multilineMode | ||||||
|  |             editTextInput.filters = | ||||||
|  |                 arrayOf(SoftwareKeyboard.Filter(), InputFilter.LengthFilter(config.maxTextLength)) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val builder = MaterialAlertDialogBuilder(requireContext()) | ||||||
|  |             .setTitle(R.string.software_keyboard) | ||||||
|  |             .setView(binding.root) | ||||||
|  | 
 | ||||||
|  |         isCancelable = false | ||||||
|  | 
 | ||||||
|  |         when (config.buttonConfig) { | ||||||
|  |             SoftwareKeyboard.ButtonConfig.Triple -> { | ||||||
|  |                 val negativeText = | ||||||
|  |                     config.buttonText[0].ifEmpty { getString(android.R.string.cancel) } | ||||||
|  |                 val neutralText = config.buttonText[1].ifEmpty { getString(R.string.i_forgot) } | ||||||
|  |                 val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) } | ||||||
|  |                 builder.setNegativeButton(negativeText, null) | ||||||
|  |                     .setNeutralButton(neutralText, null) | ||||||
|  |                     .setPositiveButton(positiveText, null) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             SoftwareKeyboard.ButtonConfig.Dual -> { | ||||||
|  |                 val negativeText = | ||||||
|  |                     config.buttonText[0].ifEmpty { getString(android.R.string.cancel) } | ||||||
|  |                 val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) } | ||||||
|  |                 builder.setNegativeButton(negativeText, null) | ||||||
|  |                     .setPositiveButton(positiveText, null) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             SoftwareKeyboard.ButtonConfig.Single -> { | ||||||
|  |                 val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) } | ||||||
|  |                 builder.setPositiveButton(positiveText, null) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // This overrides the default alert dialog behavior to prevent dismissing the keyboard | ||||||
|  |         // dialog while we show an error message | ||||||
|  |         val alertDialog = builder.create() | ||||||
|  |         alertDialog.create() | ||||||
|  |         if (alertDialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { | ||||||
|  |             alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { | ||||||
|  |                 SoftwareKeyboard.data.button = config.buttonConfig | ||||||
|  |                 SoftwareKeyboard.data.text = binding.editTextInput.text.toString() | ||||||
|  |                 val error = SoftwareKeyboard.ValidateInput(SoftwareKeyboard.data.text) | ||||||
|  |                 if (error != SoftwareKeyboard.ValidationError.None) { | ||||||
|  |                     SoftwareKeyboard.HandleValidationError(config, error) | ||||||
|  |                     return@setOnClickListener | ||||||
|  |                 } | ||||||
|  |                 dismiss() | ||||||
|  |                 synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { | ||||||
|  |             alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener { | ||||||
|  |                 SoftwareKeyboard.data.button = 1 | ||||||
|  |                 dismiss() | ||||||
|  |                 synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { | ||||||
|  |             alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener { | ||||||
|  |                 SoftwareKeyboard.data.button = 0 | ||||||
|  |                 dismiss() | ||||||
|  |                 synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return alertDialog | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         const val TAG = "KeyboardDialogFragment" | ||||||
|  | 
 | ||||||
|  |         const val CONFIG = "config" | ||||||
|  | 
 | ||||||
|  |         fun newInstance(config: SoftwareKeyboard.KeyboardConfig): KeyboardDialogFragment { | ||||||
|  |             val frag = KeyboardDialogFragment() | ||||||
|  |             val args = Bundle() | ||||||
|  |             args.putSerializable(CONFIG, config) | ||||||
|  |             frag.arguments = args | ||||||
|  |             return frag | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.fragments | ||||||
|  | 
 | ||||||
|  | import android.app.Dialog | ||||||
|  | import android.content.DialogInterface | ||||||
|  | import android.os.Bundle | ||||||
|  | import androidx.fragment.app.DialogFragment | ||||||
|  | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||||
|  | import org.citra.citra_emu.R | ||||||
|  | import org.citra.citra_emu.applets.MiiSelector | ||||||
|  | import org.citra.citra_emu.utils.SerializableHelper.serializable | ||||||
|  | 
 | ||||||
|  | class MiiSelectorDialogFragment : DialogFragment() { | ||||||
|  |     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||||||
|  |         val config = requireArguments().serializable<MiiSelector.MiiSelectorConfig>(CONFIG)!! | ||||||
|  | 
 | ||||||
|  |         // Note: we intentionally leave out the Standard Mii in the native code so that | ||||||
|  |         // the string can get translated | ||||||
|  |         val list = mutableListOf<String>() | ||||||
|  |         list.add(getString(R.string.standard_mii)) | ||||||
|  |         list.addAll(config.miiNames) | ||||||
|  |         val initialIndex = | ||||||
|  |             if (config.initiallySelectedMiiIndex < list.size) config.initiallySelectedMiiIndex.toInt() else 0 | ||||||
|  |         MiiSelector.data.index = initialIndex | ||||||
|  |         val builder = MaterialAlertDialogBuilder(requireActivity()) | ||||||
|  |             .setTitle(if (config.title!!.isEmpty()) getString(R.string.mii_selector) else config.title) | ||||||
|  |             .setSingleChoiceItems(list.toTypedArray(), initialIndex) { _: DialogInterface?, which: Int -> | ||||||
|  |                 MiiSelector.data.index = which | ||||||
|  |             } | ||||||
|  |             .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> | ||||||
|  |                 MiiSelector.data.returnCode = 0 | ||||||
|  |                 synchronized(MiiSelector.finishLock) { MiiSelector.finishLock.notifyAll() } | ||||||
|  |             } | ||||||
|  |         if (config.enableCancelButton) { | ||||||
|  |             builder.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> | ||||||
|  |                 MiiSelector.data.returnCode = 1 | ||||||
|  |                 synchronized(MiiSelector.finishLock) { MiiSelector.finishLock.notifyAll() } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         isCancelable = false | ||||||
|  |         return builder.create() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         const val TAG = "MiiSelectorDialogFragment" | ||||||
|  | 
 | ||||||
|  |         const val CONFIG = "config" | ||||||
|  | 
 | ||||||
|  |         fun newInstance(config: MiiSelector.MiiSelectorConfig): MiiSelectorDialogFragment { | ||||||
|  |             val frag = MiiSelectorDialogFragment() | ||||||
|  |             val args = Bundle() | ||||||
|  |             args.putSerializable(CONFIG, config) | ||||||
|  |             frag.arguments = args | ||||||
|  |             return frag | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,36 +0,0 @@ | ||||||
| package org.citra.citra_emu.model; |  | ||||||
| 
 |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.provider.DocumentsContract; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * A struct that is much more "cheaper" than DocumentFile. |  | ||||||
|  * Only contains the information we needed. |  | ||||||
|  */ |  | ||||||
| public class CheapDocument { |  | ||||||
|     private final String filename; |  | ||||||
|     private final Uri uri; |  | ||||||
|     private final String mimeType; |  | ||||||
| 
 |  | ||||||
|     public CheapDocument(String filename, String mimeType, Uri uri) { |  | ||||||
|         this.filename = filename; |  | ||||||
|         this.mimeType = mimeType; |  | ||||||
|         this.uri = uri; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public String getFilename() { |  | ||||||
|         return filename; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Uri getUri() { |  | ||||||
|         return uri; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public String getMimeType() { |  | ||||||
|         return mimeType; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean isDirectory() { |  | ||||||
|         return mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.model | ||||||
|  | 
 | ||||||
|  | import android.net.Uri | ||||||
|  | import android.provider.DocumentsContract | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A struct that is much more "cheaper" than DocumentFile. | ||||||
|  |  * Only contains the information we needed. | ||||||
|  |  */ | ||||||
|  | class CheapDocument(val filename: String, val mimeType: String, val uri: Uri) { | ||||||
|  |     val isDirectory: Boolean | ||||||
|  |         get() = mimeType == DocumentsContract.Document.MIME_TYPE_DIR | ||||||
|  | } | ||||||
|  | @ -1,766 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Copyright 2013 Dolphin Emulator Project |  | ||||||
|  * Licensed under GPLv2+ |  | ||||||
|  * Refer to the license.txt file included. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| package org.citra.citra_emu.overlay; |  | ||||||
| 
 |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.SharedPreferences; |  | ||||||
| import android.content.res.Configuration; |  | ||||||
| import android.content.res.Resources; |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.graphics.BitmapFactory; |  | ||||||
| import android.graphics.Canvas; |  | ||||||
| import android.graphics.Rect; |  | ||||||
| import android.graphics.drawable.Drawable; |  | ||||||
| import android.preference.PreferenceManager; |  | ||||||
| import android.util.AttributeSet; |  | ||||||
| import android.util.DisplayMetrics; |  | ||||||
| import android.view.Display; |  | ||||||
| import android.view.MotionEvent; |  | ||||||
| import android.view.SurfaceView; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.View.OnTouchListener; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.NativeLibrary; |  | ||||||
| import org.citra.citra_emu.NativeLibrary.ButtonState; |  | ||||||
| import org.citra.citra_emu.NativeLibrary.ButtonType; |  | ||||||
| import org.citra.citra_emu.R; |  | ||||||
| import org.citra.citra_emu.utils.EmulationMenuSettings; |  | ||||||
| 
 |  | ||||||
| import java.util.HashSet; |  | ||||||
| import java.util.Set; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Draws the interactive input overlay on top of the |  | ||||||
|  * {@link SurfaceView} that is rendering emulation. |  | ||||||
|  */ |  | ||||||
| public final class InputOverlay extends SurfaceView implements OnTouchListener { |  | ||||||
|     private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<>(); |  | ||||||
|     private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>(); |  | ||||||
|     private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<>(); |  | ||||||
| 
 |  | ||||||
|     private boolean mIsInEditMode = false; |  | ||||||
|     private InputOverlayDrawableButton mButtonBeingConfigured; |  | ||||||
|     private InputOverlayDrawableDpad mDpadBeingConfigured; |  | ||||||
|     private InputOverlayDrawableJoystick mJoystickBeingConfigured; |  | ||||||
| 
 |  | ||||||
|     private SharedPreferences mPreferences; |  | ||||||
| 
 |  | ||||||
|     // Stores the ID of the pointer that interacted with the 3DS touchscreen. |  | ||||||
|     private int mTouchscreenPointerId = -1; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Constructor |  | ||||||
|      * |  | ||||||
|      * @param context The current {@link Context}. |  | ||||||
|      * @param attrs   {@link AttributeSet} for parsing XML attributes. |  | ||||||
|      */ |  | ||||||
|     public InputOverlay(Context context, AttributeSet attrs) { |  | ||||||
|         super(context, attrs); |  | ||||||
| 
 |  | ||||||
|         mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); |  | ||||||
|         if (!mPreferences.getBoolean("OverlayInit", false)) { |  | ||||||
|             defaultOverlay(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Reset 3ds touchscreen pointer ID |  | ||||||
|         mTouchscreenPointerId = -1; |  | ||||||
| 
 |  | ||||||
|         // Load the controls. |  | ||||||
|         refreshControls(); |  | ||||||
| 
 |  | ||||||
|         // Set the on touch listener. |  | ||||||
|         setOnTouchListener(this); |  | ||||||
| 
 |  | ||||||
|         // Force draw |  | ||||||
|         setWillNotDraw(false); |  | ||||||
| 
 |  | ||||||
|         // Request focus for the overlay so it has priority on presses. |  | ||||||
|         requestFocus(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Resizes a {@link Bitmap} by a given scale factor |  | ||||||
|      * |  | ||||||
|      * @param context The current {@link Context} |  | ||||||
|      * @param bitmap  The {@link Bitmap} to scale. |  | ||||||
|      * @param scale   The scale factor for the bitmap. |  | ||||||
|      * @return The scaled {@link Bitmap} |  | ||||||
|      */ |  | ||||||
|     public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) { |  | ||||||
|         // Determine the button size based on the smaller screen dimension. |  | ||||||
|         // This makes sure the buttons are the same size in both portrait and landscape. |  | ||||||
|         DisplayMetrics dm = context.getResources().getDisplayMetrics(); |  | ||||||
|         int minDimension = Math.min(dm.widthPixels, dm.heightPixels); |  | ||||||
| 
 |  | ||||||
|         return Bitmap.createScaledBitmap(bitmap, |  | ||||||
|                 (int) (minDimension * scale), |  | ||||||
|                 (int) (minDimension * scale), |  | ||||||
|                 true); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Initializes an InputOverlayDrawableButton, given by resId, with all of the |  | ||||||
|      * parameters set for it to be properly shown on the InputOverlay. |  | ||||||
|      * <p> |  | ||||||
|      * This works due to the way the X and Y coordinates are stored within |  | ||||||
|      * the {@link SharedPreferences}. |  | ||||||
|      * <p> |  | ||||||
|      * In the input overlay configuration menu, |  | ||||||
|      * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). |  | ||||||
|      * the X and Y coordinates of the button at the END of its touch event |  | ||||||
|      * (when you remove your finger/stylus from the touchscreen) are then stored |  | ||||||
|      * within a SharedPreferences instance so that those values can be retrieved here. |  | ||||||
|      * <p> |  | ||||||
|      * This has a few benefits over the conventional way of storing the values |  | ||||||
|      * (ie. within the Citra ini file). |  | ||||||
|      * <ul> |  | ||||||
|      * <li>No native calls</li> |  | ||||||
|      * <li>Keeps Android-only values inside the Android environment</li> |  | ||||||
|      * </ul> |  | ||||||
|      * <p> |  | ||||||
|      * Technically no modifications should need to be performed on the returned |  | ||||||
|      * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait |  | ||||||
|      * for Android to call the onDraw method. |  | ||||||
|      * |  | ||||||
|      * @param context      The current {@link Context}. |  | ||||||
|      * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State). |  | ||||||
|      * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State). |  | ||||||
|      * @param buttonId     Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. |  | ||||||
|      * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. |  | ||||||
|      */ |  | ||||||
|     private static InputOverlayDrawableButton initializeOverlayButton(Context context, |  | ||||||
|                                                                       int defaultResId, int pressedResId, int buttonId, String orientation) { |  | ||||||
|         // Resources handle for fetching the initial Drawable resource. |  | ||||||
|         final Resources res = context.getResources(); |  | ||||||
| 
 |  | ||||||
|         // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. |  | ||||||
|         final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); |  | ||||||
| 
 |  | ||||||
|         // Decide scale based on button ID and user preference |  | ||||||
|         float scale; |  | ||||||
| 
 |  | ||||||
|         switch (buttonId) { |  | ||||||
|             case ButtonType.BUTTON_HOME: |  | ||||||
|             case ButtonType.BUTTON_START: |  | ||||||
|             case ButtonType.BUTTON_SELECT: |  | ||||||
|                 scale = 0.08f; |  | ||||||
|                 break; |  | ||||||
|             case ButtonType.TRIGGER_L: |  | ||||||
|             case ButtonType.TRIGGER_R: |  | ||||||
|             case ButtonType.BUTTON_ZL: |  | ||||||
|             case ButtonType.BUTTON_ZR: |  | ||||||
|                 scale = 0.18f; |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 scale = 0.11f; |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         scale *= (sPrefs.getInt("controlScale", 50) + 50); |  | ||||||
|         scale /= 100; |  | ||||||
| 
 |  | ||||||
|         // Initialize the InputOverlayDrawableButton. |  | ||||||
|         final Bitmap defaultStateBitmap = |  | ||||||
|                 resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); |  | ||||||
|         final Bitmap pressedStateBitmap = |  | ||||||
|                 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale); |  | ||||||
|         final InputOverlayDrawableButton overlayDrawable = |  | ||||||
|                 new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId); |  | ||||||
| 
 |  | ||||||
|         // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. |  | ||||||
|         // These were set in the input overlay configuration menu. |  | ||||||
|         String xKey; |  | ||||||
|         String yKey; |  | ||||||
| 
 |  | ||||||
|         xKey = buttonId + orientation + "-X"; |  | ||||||
|         yKey = buttonId + orientation + "-Y"; |  | ||||||
| 
 |  | ||||||
|         int drawableX = (int) sPrefs.getFloat(xKey, 0f); |  | ||||||
|         int drawableY = (int) sPrefs.getFloat(yKey, 0f); |  | ||||||
| 
 |  | ||||||
|         int width = overlayDrawable.getWidth(); |  | ||||||
|         int height = overlayDrawable.getHeight(); |  | ||||||
| 
 |  | ||||||
|         // Now set the bounds for the InputOverlayDrawableButton. |  | ||||||
|         // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. |  | ||||||
|         overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); |  | ||||||
| 
 |  | ||||||
|         // Need to set the image's position |  | ||||||
|         overlayDrawable.setPosition(drawableX, drawableY); |  | ||||||
| 
 |  | ||||||
|         return overlayDrawable; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Initializes an {@link InputOverlayDrawableDpad} |  | ||||||
|      * |  | ||||||
|      * @param context                   The current {@link Context}. |  | ||||||
|      * @param defaultResId              The {@link Bitmap} resource ID of the default sate. |  | ||||||
|      * @param pressedOneDirectionResId  The {@link Bitmap} resource ID of the pressed sate in one direction. |  | ||||||
|      * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions. |  | ||||||
|      * @param buttonUp                  Identifier for the up button. |  | ||||||
|      * @param buttonDown                Identifier for the down button. |  | ||||||
|      * @param buttonLeft                Identifier for the left button. |  | ||||||
|      * @param buttonRight               Identifier for the right button. |  | ||||||
|      * @return the initialized {@link InputOverlayDrawableDpad} |  | ||||||
|      */ |  | ||||||
|     private static InputOverlayDrawableDpad initializeOverlayDpad(Context context, |  | ||||||
|                                                                   int defaultResId, |  | ||||||
|                                                                   int pressedOneDirectionResId, |  | ||||||
|                                                                   int pressedTwoDirectionsResId, |  | ||||||
|                                                                   int buttonUp, |  | ||||||
|                                                                   int buttonDown, |  | ||||||
|                                                                   int buttonLeft, |  | ||||||
|                                                                   int buttonRight, |  | ||||||
|                                                                   String orientation) { |  | ||||||
|         // Resources handle for fetching the initial Drawable resource. |  | ||||||
|         final Resources res = context.getResources(); |  | ||||||
| 
 |  | ||||||
|         // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. |  | ||||||
|         final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); |  | ||||||
| 
 |  | ||||||
|         // Decide scale based on button ID and user preference |  | ||||||
|         float scale = 0.22f; |  | ||||||
| 
 |  | ||||||
|         scale *= (sPrefs.getInt("controlScale", 50) + 50); |  | ||||||
|         scale /= 100; |  | ||||||
| 
 |  | ||||||
|         // Initialize the InputOverlayDrawableDpad. |  | ||||||
|         final Bitmap defaultStateBitmap = |  | ||||||
|                 resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); |  | ||||||
|         final Bitmap pressedOneDirectionStateBitmap = |  | ||||||
|                 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId), |  | ||||||
|                         scale); |  | ||||||
|         final Bitmap pressedTwoDirectionsStateBitmap = |  | ||||||
|                 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId), |  | ||||||
|                         scale); |  | ||||||
|         final InputOverlayDrawableDpad overlayDrawable = |  | ||||||
|                 new InputOverlayDrawableDpad(res, defaultStateBitmap, |  | ||||||
|                         pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap, |  | ||||||
|                         buttonUp, buttonDown, buttonLeft, buttonRight); |  | ||||||
| 
 |  | ||||||
|         // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. |  | ||||||
|         // These were set in the input overlay configuration menu. |  | ||||||
|         int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f); |  | ||||||
|         int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f); |  | ||||||
| 
 |  | ||||||
|         int width = overlayDrawable.getWidth(); |  | ||||||
|         int height = overlayDrawable.getHeight(); |  | ||||||
| 
 |  | ||||||
|         // Now set the bounds for the InputOverlayDrawableDpad. |  | ||||||
|         // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. |  | ||||||
|         overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); |  | ||||||
| 
 |  | ||||||
|         // Need to set the image's position |  | ||||||
|         overlayDrawable.setPosition(drawableX, drawableY); |  | ||||||
| 
 |  | ||||||
|         return overlayDrawable; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Initializes an {@link InputOverlayDrawableJoystick} |  | ||||||
|      * |  | ||||||
|      * @param context         The current {@link Context} |  | ||||||
|      * @param resOuter        Resource ID for the outer image of the joystick (the static image that shows the circular bounds). |  | ||||||
|      * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). |  | ||||||
|      * @param pressedResInner Resource ID for the pressed inner image of the joystick. |  | ||||||
|      * @param joystick        Identifier for which joystick this is. |  | ||||||
|      * @return the initialized {@link InputOverlayDrawableJoystick}. |  | ||||||
|      */ |  | ||||||
|     private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, |  | ||||||
|                                                                           int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation) { |  | ||||||
|         // Resources handle for fetching the initial Drawable resource. |  | ||||||
|         final Resources res = context.getResources(); |  | ||||||
| 
 |  | ||||||
|         // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. |  | ||||||
|         final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); |  | ||||||
| 
 |  | ||||||
|         // Decide scale based on user preference |  | ||||||
|         float scale = 0.275f; |  | ||||||
|         scale *= (sPrefs.getInt("controlScale", 50) + 50); |  | ||||||
|         scale /= 100; |  | ||||||
| 
 |  | ||||||
|         // Initialize the InputOverlayDrawableJoystick. |  | ||||||
|         final Bitmap bitmapOuter = |  | ||||||
|                 resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale); |  | ||||||
|         final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner); |  | ||||||
|         final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner); |  | ||||||
| 
 |  | ||||||
|         // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. |  | ||||||
|         // These were set in the input overlay configuration menu. |  | ||||||
|         int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f); |  | ||||||
|         int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f); |  | ||||||
| 
 |  | ||||||
|         // Decide inner scale based on joystick ID |  | ||||||
|         float outerScale = 1.f; |  | ||||||
|         if (joystick == ButtonType.STICK_C) { |  | ||||||
|             outerScale = 2.f; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Now set the bounds for the InputOverlayDrawableJoystick. |  | ||||||
|         // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. |  | ||||||
|         int outerSize = bitmapOuter.getWidth(); |  | ||||||
|         Rect outerRect = new Rect(drawableX, drawableY, drawableX + (int) (outerSize / outerScale), drawableY + (int) (outerSize / outerScale)); |  | ||||||
|         Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale)); |  | ||||||
| 
 |  | ||||||
|         // Send the drawableId to the joystick so it can be referenced when saving control position. |  | ||||||
|         final InputOverlayDrawableJoystick overlayDrawable |  | ||||||
|                 = new InputOverlayDrawableJoystick(res, bitmapOuter, |  | ||||||
|                 bitmapInnerDefault, bitmapInnerPressed, |  | ||||||
|                 outerRect, innerRect, joystick); |  | ||||||
| 
 |  | ||||||
|         // Need to set the image's position |  | ||||||
|         overlayDrawable.setPosition(drawableX, drawableY); |  | ||||||
| 
 |  | ||||||
|         return overlayDrawable; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void draw(Canvas canvas) { |  | ||||||
|         super.draw(canvas); |  | ||||||
| 
 |  | ||||||
|         for (InputOverlayDrawableButton button : overlayButtons) { |  | ||||||
|             button.draw(canvas); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (InputOverlayDrawableDpad dpad : overlayDpads) { |  | ||||||
|             dpad.draw(canvas); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { |  | ||||||
|             joystick.draw(canvas); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public boolean onTouch(View v, MotionEvent event) { |  | ||||||
|         if (isInEditMode()) { |  | ||||||
|             return onTouchWhileEditing(event); |  | ||||||
|         } |  | ||||||
|         boolean shouldUpdateView = false; |  | ||||||
|         for (InputOverlayDrawableButton button : overlayButtons) { |  | ||||||
|             if (!button.updateStatus(event)) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), button.getStatus()); |  | ||||||
|             shouldUpdateView = true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (InputOverlayDrawableDpad dpad : overlayDpads) { |  | ||||||
|             if (!dpad.updateStatus(event, EmulationMenuSettings.INSTANCE.getDpadSlide())) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getUpId(), dpad.getUpStatus()); |  | ||||||
|             NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getDownId(), dpad.getDownStatus()); |  | ||||||
|             NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getLeftId(), dpad.getLeftStatus()); |  | ||||||
|             NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getRightId(), dpad.getRightStatus()); |  | ||||||
|             shouldUpdateView = true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { |  | ||||||
|             if (!joystick.updateStatus(event)) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             int axisID = joystick.getJoystickId(); |  | ||||||
|             NativeLibrary.INSTANCE |  | ||||||
|                     .onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, joystick.getXAxis(), joystick.getYAxis()); |  | ||||||
|             shouldUpdateView = true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (shouldUpdateView) { |  | ||||||
|             invalidate(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!mPreferences.getBoolean("isTouchEnabled", true)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         int pointerIndex = event.getActionIndex(); |  | ||||||
|         int xPosition = (int) event.getX(pointerIndex); |  | ||||||
|         int yPosition = (int) event.getY(pointerIndex); |  | ||||||
|         int pointerId = event.getPointerId(pointerIndex); |  | ||||||
|         int motionEvent = event.getAction() & MotionEvent.ACTION_MASK; |  | ||||||
|         boolean isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN; |  | ||||||
|         boolean isActionMove = motionEvent == MotionEvent.ACTION_MOVE; |  | ||||||
|         boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP; |  | ||||||
| 
 |  | ||||||
|         if (isActionDown && !isTouchInputConsumed(pointerId)) { |  | ||||||
|             NativeLibrary.INSTANCE.onTouchEvent(xPosition, yPosition, true); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (isActionMove) { |  | ||||||
|             for (int i = 0; i < event.getPointerCount(); i++) { |  | ||||||
|                 int fingerId = event.getPointerId(i); |  | ||||||
|                 if (isTouchInputConsumed(fingerId)) { |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|                 NativeLibrary.INSTANCE.onTouchMoved(xPosition, yPosition); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (isActionUp && !isTouchInputConsumed(pointerId)) { |  | ||||||
|             NativeLibrary.INSTANCE.onTouchEvent(0, 0, false); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private boolean isTouchInputConsumed(int trackId) { |  | ||||||
|         for (InputOverlayDrawableButton button : overlayButtons) { |  | ||||||
|             if (button.getTrackId() == trackId) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         for (InputOverlayDrawableDpad dpad : overlayDpads) { |  | ||||||
|             if (dpad.getTrackId() == trackId) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { |  | ||||||
|             if (joystick.getTrackId() == trackId) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean onTouchWhileEditing(MotionEvent event) { |  | ||||||
|         int pointerIndex = event.getActionIndex(); |  | ||||||
|         int fingerPositionX = (int) event.getX(pointerIndex); |  | ||||||
|         int fingerPositionY = (int) event.getY(pointerIndex); |  | ||||||
| 
 |  | ||||||
|         String orientation = |  | ||||||
|                 getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? |  | ||||||
|                         "-Portrait" : ""; |  | ||||||
| 
 |  | ||||||
|         // Maybe combine Button and Joystick as subclasses of the same parent? |  | ||||||
|         // Or maybe create an interface like IMoveableHUDControl? |  | ||||||
| 
 |  | ||||||
|         for (InputOverlayDrawableButton button : overlayButtons) { |  | ||||||
|             // Determine the button state to apply based on the MotionEvent action flag. |  | ||||||
|             switch (event.getAction() & MotionEvent.ACTION_MASK) { |  | ||||||
|                 case MotionEvent.ACTION_DOWN: |  | ||||||
|                 case MotionEvent.ACTION_POINTER_DOWN: |  | ||||||
|                     // If no button is being moved now, remember the currently touched button to move. |  | ||||||
|                     if (mButtonBeingConfigured == null && |  | ||||||
|                             button.getBounds().contains(fingerPositionX, fingerPositionY)) { |  | ||||||
|                         mButtonBeingConfigured = button; |  | ||||||
|                         mButtonBeingConfigured.onConfigureTouch(event); |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
|                 case MotionEvent.ACTION_MOVE: |  | ||||||
|                     if (mButtonBeingConfigured != null) { |  | ||||||
|                         mButtonBeingConfigured.onConfigureTouch(event); |  | ||||||
|                         invalidate(); |  | ||||||
|                         return true; |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
| 
 |  | ||||||
|                 case MotionEvent.ACTION_UP: |  | ||||||
|                 case MotionEvent.ACTION_POINTER_UP: |  | ||||||
|                     if (mButtonBeingConfigured == button) { |  | ||||||
|                         // Persist button position by saving new place. |  | ||||||
|                         saveControlPosition(mButtonBeingConfigured.getId(), |  | ||||||
|                                 mButtonBeingConfigured.getBounds().left, |  | ||||||
|                                 mButtonBeingConfigured.getBounds().top, orientation); |  | ||||||
|                         mButtonBeingConfigured = null; |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (InputOverlayDrawableDpad dpad : overlayDpads) { |  | ||||||
|             // Determine the button state to apply based on the MotionEvent action flag. |  | ||||||
|             switch (event.getAction() & MotionEvent.ACTION_MASK) { |  | ||||||
|                 case MotionEvent.ACTION_DOWN: |  | ||||||
|                 case MotionEvent.ACTION_POINTER_DOWN: |  | ||||||
|                     // If no button is being moved now, remember the currently touched button to move. |  | ||||||
|                     if (mButtonBeingConfigured == null && |  | ||||||
|                             dpad.getBounds().contains(fingerPositionX, fingerPositionY)) { |  | ||||||
|                         mDpadBeingConfigured = dpad; |  | ||||||
|                         mDpadBeingConfigured.onConfigureTouch(event); |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
|                 case MotionEvent.ACTION_MOVE: |  | ||||||
|                     if (mDpadBeingConfigured != null) { |  | ||||||
|                         mDpadBeingConfigured.onConfigureTouch(event); |  | ||||||
|                         invalidate(); |  | ||||||
|                         return true; |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
| 
 |  | ||||||
|                 case MotionEvent.ACTION_UP: |  | ||||||
|                 case MotionEvent.ACTION_POINTER_UP: |  | ||||||
|                     if (mDpadBeingConfigured == dpad) { |  | ||||||
|                         // Persist button position by saving new place. |  | ||||||
|                         saveControlPosition(mDpadBeingConfigured.getUpId(), |  | ||||||
|                                 mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top, |  | ||||||
|                                 orientation); |  | ||||||
|                         mDpadBeingConfigured = null; |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { |  | ||||||
|             switch (event.getAction()) { |  | ||||||
|                 case MotionEvent.ACTION_DOWN: |  | ||||||
|                 case MotionEvent.ACTION_POINTER_DOWN: |  | ||||||
|                     if (mJoystickBeingConfigured == null && |  | ||||||
|                             joystick.getBounds().contains(fingerPositionX, fingerPositionY)) { |  | ||||||
|                         mJoystickBeingConfigured = joystick; |  | ||||||
|                         mJoystickBeingConfigured.onConfigureTouch(event); |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
|                 case MotionEvent.ACTION_MOVE: |  | ||||||
|                     if (mJoystickBeingConfigured != null) { |  | ||||||
|                         mJoystickBeingConfigured.onConfigureTouch(event); |  | ||||||
|                         invalidate(); |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
|                 case MotionEvent.ACTION_UP: |  | ||||||
|                 case MotionEvent.ACTION_POINTER_UP: |  | ||||||
|                     if (mJoystickBeingConfigured != null) { |  | ||||||
|                         saveControlPosition(mJoystickBeingConfigured.getJoystickId(), |  | ||||||
|                                 mJoystickBeingConfigured.getBounds().left, |  | ||||||
|                                 mJoystickBeingConfigured.getBounds().top, orientation); |  | ||||||
|                         mJoystickBeingConfigured = null; |  | ||||||
|                     } |  | ||||||
|                     break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void addOverlayControls(String orientation) { |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle0", true)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a, |  | ||||||
|                     R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle1", true)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b, |  | ||||||
|                     R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle2", true)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x, |  | ||||||
|                     R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle3", true)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y, |  | ||||||
|                     R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle4", true)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l, |  | ||||||
|                     R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle5", true)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r, |  | ||||||
|                     R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle6", false)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl, |  | ||||||
|                     R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle7", false)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr, |  | ||||||
|                     R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle8", true)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start, |  | ||||||
|                     R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle9", true)) { |  | ||||||
|             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select, |  | ||||||
|                     R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle10", true)) { |  | ||||||
|             overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad, |  | ||||||
|                     R.drawable.dpad_pressed_one_direction, |  | ||||||
|                     R.drawable.dpad_pressed_two_directions, |  | ||||||
|                     ButtonType.DPAD_UP, ButtonType.DPAD_DOWN, |  | ||||||
|                     ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle11", true)) { |  | ||||||
|             overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range, |  | ||||||
|                     R.drawable.stick_main, R.drawable.stick_main_pressed, |  | ||||||
|                     ButtonType.STICK_LEFT, orientation)); |  | ||||||
|         } |  | ||||||
|         if (mPreferences.getBoolean("buttonToggle12", false)) { |  | ||||||
|             overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_c_range, |  | ||||||
|                     R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void refreshControls() { |  | ||||||
|         // Remove all the overlay buttons from the HashSet. |  | ||||||
|         overlayButtons.clear(); |  | ||||||
|         overlayDpads.clear(); |  | ||||||
|         overlayJoysticks.clear(); |  | ||||||
| 
 |  | ||||||
|         String orientation = |  | ||||||
|                 getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? |  | ||||||
|                         "-Portrait" : ""; |  | ||||||
| 
 |  | ||||||
|         // Add all the enabled overlay items back to the HashSet. |  | ||||||
|         if (EmulationMenuSettings.INSTANCE.getShowOverlay()) { |  | ||||||
|             addOverlayControls(orientation); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         invalidate(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) { |  | ||||||
|         final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); |  | ||||||
|         SharedPreferences.Editor sPrefsEditor = sPrefs.edit(); |  | ||||||
|         sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x); |  | ||||||
|         sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y); |  | ||||||
|         sPrefsEditor.apply(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setIsInEditMode(boolean isInEditMode) { |  | ||||||
|         mIsInEditMode = isInEditMode; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void defaultOverlay() { |  | ||||||
|         if (!mPreferences.getBoolean("OverlayInit", false)) { |  | ||||||
|             // It's possible that a user has created their overlay before this was added |  | ||||||
|             // Only change the overlay if the 'A' button is not in the upper corner. |  | ||||||
|             if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) { |  | ||||||
|                 defaultOverlayLandscape(); |  | ||||||
|             } |  | ||||||
|             if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) { |  | ||||||
|                 defaultOverlayPortrait(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); |  | ||||||
|         sPrefsEditor.putBoolean("OverlayInit", true); |  | ||||||
|         sPrefsEditor.apply(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void resetButtonPlacement() { |  | ||||||
|         boolean isLandscape = |  | ||||||
|                 getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; |  | ||||||
| 
 |  | ||||||
|         if (isLandscape) { |  | ||||||
|             defaultOverlayLandscape(); |  | ||||||
|         } else { |  | ||||||
|             defaultOverlayPortrait(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         refreshControls(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void defaultOverlayLandscape() { |  | ||||||
|         SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); |  | ||||||
|         // Get screen size |  | ||||||
|         Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); |  | ||||||
|         DisplayMetrics outMetrics = new DisplayMetrics(); |  | ||||||
|         display.getMetrics(outMetrics); |  | ||||||
|         float maxX = outMetrics.heightPixels; |  | ||||||
|         float maxY = outMetrics.widthPixels; |  | ||||||
|         // Height and width changes depending on orientation. Use the larger value for height. |  | ||||||
|         if (maxY > maxX) { |  | ||||||
|             float tmp = maxX; |  | ||||||
|             maxX = maxY; |  | ||||||
|             maxY = tmp; |  | ||||||
|         } |  | ||||||
|         Resources res = getResources(); |  | ||||||
| 
 |  | ||||||
|         // Each value is a percent from max X/Y stored as an int. Have to bring that value down |  | ||||||
|         // to a decimal before multiplying by MAX X/Y. |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY)); |  | ||||||
| 
 |  | ||||||
|         // We want to commit right away, otherwise the overlay could load before this is saved. |  | ||||||
|         sPrefsEditor.commit(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void defaultOverlayPortrait() { |  | ||||||
|         SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); |  | ||||||
|         // Get screen size |  | ||||||
|         Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); |  | ||||||
|         DisplayMetrics outMetrics = new DisplayMetrics(); |  | ||||||
|         display.getMetrics(outMetrics); |  | ||||||
|         float maxX = outMetrics.heightPixels; |  | ||||||
|         float maxY = outMetrics.widthPixels; |  | ||||||
|         // Height and width changes depending on orientation. Use the larger value for height. |  | ||||||
|         if (maxY < maxX) { |  | ||||||
|             float tmp = maxX; |  | ||||||
|             maxX = maxY; |  | ||||||
|             maxY = tmp; |  | ||||||
|         } |  | ||||||
|         Resources res = getResources(); |  | ||||||
|         String portrait = "-Portrait"; |  | ||||||
| 
 |  | ||||||
|         // Each value is a percent from max X/Y stored as an int. Have to bring that value down |  | ||||||
|         // to a decimal before multiplying by MAX X/Y. |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX)); |  | ||||||
|         sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY)); |  | ||||||
| 
 |  | ||||||
|         // We want to commit right away, otherwise the overlay could load before this is saved. |  | ||||||
|         sPrefsEditor.commit(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean isInEditMode() { |  | ||||||
|         return mIsInEditMode; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1,159 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Copyright 2013 Dolphin Emulator Project |  | ||||||
|  * Licensed under GPLv2+ |  | ||||||
|  * Refer to the license.txt file included. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| package org.citra.citra_emu.overlay; |  | ||||||
| 
 |  | ||||||
| import android.content.res.Resources; |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.graphics.Canvas; |  | ||||||
| import android.graphics.Rect; |  | ||||||
| import android.graphics.drawable.BitmapDrawable; |  | ||||||
| import android.view.MotionEvent; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.NativeLibrary; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Custom {@link BitmapDrawable} that is capable |  | ||||||
|  * of storing it's own ID. |  | ||||||
|  */ |  | ||||||
| public final class InputOverlayDrawableButton { |  | ||||||
|     // The ID identifying what type of button this Drawable represents. |  | ||||||
|     private int mButtonType; |  | ||||||
|     private int mTrackId; |  | ||||||
|     private int mPreviousTouchX, mPreviousTouchY; |  | ||||||
|     private int mControlPositionX, mControlPositionY; |  | ||||||
|     private int mWidth; |  | ||||||
|     private int mHeight; |  | ||||||
|     private BitmapDrawable mDefaultStateBitmap; |  | ||||||
|     private BitmapDrawable mPressedStateBitmap; |  | ||||||
|     private boolean mPressedState = false; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Constructor |  | ||||||
|      * |  | ||||||
|      * @param res                {@link Resources} instance. |  | ||||||
|      * @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable. |  | ||||||
|      * @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable. |  | ||||||
|      * @param buttonType         Identifier for this type of button. |  | ||||||
|      */ |  | ||||||
|     public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap, |  | ||||||
|                                       Bitmap pressedStateBitmap, int buttonType) { |  | ||||||
|         mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); |  | ||||||
|         mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap); |  | ||||||
|         mButtonType = buttonType; |  | ||||||
|         mTrackId = -1; |  | ||||||
| 
 |  | ||||||
|         mWidth = mDefaultStateBitmap.getIntrinsicWidth(); |  | ||||||
|         mHeight = mDefaultStateBitmap.getIntrinsicHeight(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Updates button status based on the motion event. |  | ||||||
|      * |  | ||||||
|      * @return true if value was changed |  | ||||||
|      */ |  | ||||||
|     public boolean updateStatus(MotionEvent event) { |  | ||||||
|         int pointerIndex = event.getActionIndex(); |  | ||||||
|         int xPosition = (int) event.getX(pointerIndex); |  | ||||||
|         int yPosition = (int) event.getY(pointerIndex); |  | ||||||
|         int pointerId = event.getPointerId(pointerIndex); |  | ||||||
|         int motionEvent = event.getAction() & MotionEvent.ACTION_MASK; |  | ||||||
|         boolean isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN; |  | ||||||
|         boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP; |  | ||||||
| 
 |  | ||||||
|         if (isActionDown) { |  | ||||||
|             if (!getBounds().contains(xPosition, yPosition)) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             mPressedState = true; |  | ||||||
|             mTrackId = pointerId; |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (isActionUp) { |  | ||||||
|             if (mTrackId != pointerId) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             mPressedState = false; |  | ||||||
|             mTrackId = -1; |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean onConfigureTouch(MotionEvent event) { |  | ||||||
|         int pointerIndex = event.getActionIndex(); |  | ||||||
|         int fingerPositionX = (int) event.getX(pointerIndex); |  | ||||||
|         int fingerPositionY = (int) event.getY(pointerIndex); |  | ||||||
|         switch (event.getAction()) { |  | ||||||
|             case MotionEvent.ACTION_DOWN: |  | ||||||
|                 mPreviousTouchX = fingerPositionX; |  | ||||||
|                 mPreviousTouchY = fingerPositionY; |  | ||||||
|                 break; |  | ||||||
|             case MotionEvent.ACTION_MOVE: |  | ||||||
|                 mControlPositionX += fingerPositionX - mPreviousTouchX; |  | ||||||
|                 mControlPositionY += fingerPositionY - mPreviousTouchY; |  | ||||||
|                 setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, |  | ||||||
|                         getHeight() + mControlPositionY); |  | ||||||
|                 mPreviousTouchX = fingerPositionX; |  | ||||||
|                 mPreviousTouchY = fingerPositionY; |  | ||||||
|                 break; |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setPosition(int x, int y) { |  | ||||||
|         mControlPositionX = x; |  | ||||||
|         mControlPositionY = y; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void draw(Canvas canvas) { |  | ||||||
|         getCurrentStateBitmapDrawable().draw(canvas); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private BitmapDrawable getCurrentStateBitmapDrawable() { |  | ||||||
|         return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setBounds(int left, int top, int right, int bottom) { |  | ||||||
|         mDefaultStateBitmap.setBounds(left, top, right, bottom); |  | ||||||
|         mPressedStateBitmap.setBounds(left, top, right, bottom); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getId() { |  | ||||||
|         return mButtonType; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getTrackId() { |  | ||||||
|         return mTrackId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setTrackId(int trackId) { |  | ||||||
|         mTrackId = trackId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getStatus() { |  | ||||||
|         return mPressedState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Rect getBounds() { |  | ||||||
|         return mDefaultStateBitmap.getBounds(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getWidth() { |  | ||||||
|         return mWidth; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getHeight() { |  | ||||||
|         return mHeight; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setPressedState(boolean isPressed) { |  | ||||||
|         mPressedState = isPressed; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,128 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.overlay | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources | ||||||
|  | import android.graphics.Bitmap | ||||||
|  | import android.graphics.Canvas | ||||||
|  | import android.graphics.Rect | ||||||
|  | import android.graphics.drawable.BitmapDrawable | ||||||
|  | import android.view.MotionEvent | ||||||
|  | import org.citra.citra_emu.NativeLibrary | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Custom [BitmapDrawable] that is capable | ||||||
|  |  * of storing it's own ID. | ||||||
|  |  * | ||||||
|  |  * @param res                [Resources] instance. | ||||||
|  |  * @param defaultStateBitmap [Bitmap] to use with the default state Drawable. | ||||||
|  |  * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable. | ||||||
|  |  * @param id                 Identifier for this type of button. | ||||||
|  |  */ | ||||||
|  | class InputOverlayDrawableButton( | ||||||
|  |     res: Resources, | ||||||
|  |     defaultStateBitmap: Bitmap, | ||||||
|  |     pressedStateBitmap: Bitmap, | ||||||
|  |     val id: Int | ||||||
|  | ) { | ||||||
|  |     var trackId: Int | ||||||
|  |     private var previousTouchX = 0 | ||||||
|  |     private var previousTouchY = 0 | ||||||
|  |     private var controlPositionX = 0 | ||||||
|  |     private var controlPositionY = 0 | ||||||
|  |     val width: Int | ||||||
|  |     val height: Int | ||||||
|  |     private val defaultStateBitmap: BitmapDrawable | ||||||
|  |     private val pressedStateBitmap: BitmapDrawable | ||||||
|  |     private var pressedState = false | ||||||
|  | 
 | ||||||
|  |     init { | ||||||
|  |         this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap) | ||||||
|  |         this.pressedStateBitmap = BitmapDrawable(res, pressedStateBitmap) | ||||||
|  |         trackId = -1 | ||||||
|  |         width = this.defaultStateBitmap.intrinsicWidth | ||||||
|  |         height = this.defaultStateBitmap.intrinsicHeight | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Updates button status based on the motion event. | ||||||
|  |      * | ||||||
|  |      * @return true if value was changed | ||||||
|  |      */ | ||||||
|  |     fun updateStatus(event: MotionEvent): Boolean { | ||||||
|  |         val pointerIndex = event.actionIndex | ||||||
|  |         val xPosition = event.getX(pointerIndex).toInt() | ||||||
|  |         val yPosition = event.getY(pointerIndex).toInt() | ||||||
|  |         val pointerId = event.getPointerId(pointerIndex) | ||||||
|  |         val motionEvent = event.action and MotionEvent.ACTION_MASK | ||||||
|  |         val isActionDown = | ||||||
|  |             motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN | ||||||
|  |         val isActionUp = | ||||||
|  |             motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP | ||||||
|  |         if (isActionDown) { | ||||||
|  |             if (!bounds.contains(xPosition, yPosition)) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             pressedState = true | ||||||
|  |             trackId = pointerId | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         if (isActionUp) { | ||||||
|  |             if (trackId != pointerId) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             pressedState = false | ||||||
|  |             trackId = -1 | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onConfigureTouch(event: MotionEvent): Boolean { | ||||||
|  |         val pointerIndex = event.actionIndex | ||||||
|  |         val fingerPositionX = event.getX(pointerIndex).toInt() | ||||||
|  |         val fingerPositionY = event.getY(pointerIndex).toInt() | ||||||
|  |         when (event.action) { | ||||||
|  |             MotionEvent.ACTION_DOWN -> { | ||||||
|  |                 previousTouchX = fingerPositionX | ||||||
|  |                 previousTouchY = fingerPositionY | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             MotionEvent.ACTION_MOVE -> { | ||||||
|  |                 controlPositionX += fingerPositionX - previousTouchX | ||||||
|  |                 controlPositionY += fingerPositionY - previousTouchY | ||||||
|  |                 setBounds( | ||||||
|  |                     controlPositionX, | ||||||
|  |                     controlPositionY, | ||||||
|  |                     width + controlPositionX, | ||||||
|  |                     height + controlPositionY | ||||||
|  |                 ) | ||||||
|  |                 previousTouchX = fingerPositionX | ||||||
|  |                 previousTouchY = fingerPositionY | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setPosition(x: Int, y: Int) { | ||||||
|  |         controlPositionX = x | ||||||
|  |         controlPositionY = y | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun draw(canvas: Canvas) = currentStateBitmapDrawable.draw(canvas) | ||||||
|  | 
 | ||||||
|  |     private val currentStateBitmapDrawable: BitmapDrawable | ||||||
|  |         get() = if (pressedState) pressedStateBitmap else defaultStateBitmap | ||||||
|  | 
 | ||||||
|  |     fun setBounds(left: Int, top: Int, right: Int, bottom: Int) { | ||||||
|  |         defaultStateBitmap.setBounds(left, top, right, bottom) | ||||||
|  |         pressedStateBitmap.setBounds(left, top, right, bottom) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     val status: Int | ||||||
|  |         get() = if (pressedState) NativeLibrary.ButtonState.PRESSED else NativeLibrary.ButtonState.RELEASED | ||||||
|  |     val bounds: Rect | ||||||
|  |         get() = defaultStateBitmap.bounds | ||||||
|  | } | ||||||
|  | @ -1,299 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Copyright 2016 Dolphin Emulator Project |  | ||||||
|  * Licensed under GPLv2+ |  | ||||||
|  * Refer to the license.txt file included. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| package org.citra.citra_emu.overlay; |  | ||||||
| 
 |  | ||||||
| import android.content.res.Resources; |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.graphics.Canvas; |  | ||||||
| import android.graphics.Rect; |  | ||||||
| import android.graphics.drawable.BitmapDrawable; |  | ||||||
| import android.view.MotionEvent; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.NativeLibrary; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Custom {@link BitmapDrawable} that is capable |  | ||||||
|  * of storing it's own ID. |  | ||||||
|  */ |  | ||||||
| public final class InputOverlayDrawableDpad { |  | ||||||
|     public static final float VIRT_AXIS_DEADZONE = 0.5f; |  | ||||||
|     // The ID identifying what type of button this Drawable represents. |  | ||||||
|     private int mUpButtonId; |  | ||||||
|     private int mDownButtonId; |  | ||||||
|     private int mLeftButtonId; |  | ||||||
|     private int mRightButtonId; |  | ||||||
|     private int mTrackId; |  | ||||||
|     private int mPreviousTouchX, mPreviousTouchY; |  | ||||||
|     private int mControlPositionX, mControlPositionY; |  | ||||||
|     private int mWidth; |  | ||||||
|     private int mHeight; |  | ||||||
|     private BitmapDrawable mDefaultStateBitmap; |  | ||||||
|     private BitmapDrawable mPressedOneDirectionStateBitmap; |  | ||||||
|     private BitmapDrawable mPressedTwoDirectionsStateBitmap; |  | ||||||
|     private boolean mUpButtonState; |  | ||||||
|     private boolean mDownButtonState; |  | ||||||
|     private boolean mLeftButtonState; |  | ||||||
|     private boolean mRightButtonState; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Constructor |  | ||||||
|      * |  | ||||||
|      * @param res                             {@link Resources} instance. |  | ||||||
|      * @param defaultStateBitmap              {@link Bitmap} of the default state. |  | ||||||
|      * @param pressedOneDirectionStateBitmap  {@link Bitmap} of the pressed state in one direction. |  | ||||||
|      * @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction. |  | ||||||
|      * @param buttonUp                        Identifier for the up button. |  | ||||||
|      * @param buttonDown                      Identifier for the down button. |  | ||||||
|      * @param buttonLeft                      Identifier for the left button. |  | ||||||
|      * @param buttonRight                     Identifier for the right button. |  | ||||||
|      */ |  | ||||||
|     public InputOverlayDrawableDpad(Resources res, |  | ||||||
|                                     Bitmap defaultStateBitmap, |  | ||||||
|                                     Bitmap pressedOneDirectionStateBitmap, |  | ||||||
|                                     Bitmap pressedTwoDirectionsStateBitmap, |  | ||||||
|                                     int buttonUp, int buttonDown, |  | ||||||
|                                     int buttonLeft, int buttonRight) { |  | ||||||
|         mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); |  | ||||||
|         mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap); |  | ||||||
|         mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap); |  | ||||||
| 
 |  | ||||||
|         mWidth = mDefaultStateBitmap.getIntrinsicWidth(); |  | ||||||
|         mHeight = mDefaultStateBitmap.getIntrinsicHeight(); |  | ||||||
| 
 |  | ||||||
|         mUpButtonId = buttonUp; |  | ||||||
|         mDownButtonId = buttonDown; |  | ||||||
|         mLeftButtonId = buttonLeft; |  | ||||||
|         mRightButtonId = buttonRight; |  | ||||||
| 
 |  | ||||||
|         mTrackId = -1; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean updateStatus(MotionEvent event, boolean dpadSlide) { |  | ||||||
|         int pointerIndex = event.getActionIndex(); |  | ||||||
|         int xPosition = (int) event.getX(pointerIndex); |  | ||||||
|         int yPosition = (int) event.getY(pointerIndex); |  | ||||||
|         int pointerId = event.getPointerId(pointerIndex); |  | ||||||
|         int motionEvent = event.getAction() & MotionEvent.ACTION_MASK; |  | ||||||
|         boolean isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN; |  | ||||||
|         boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP; |  | ||||||
| 
 |  | ||||||
|         if (isActionDown) { |  | ||||||
|             if (!getBounds().contains(xPosition, yPosition)) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             mTrackId = pointerId; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (isActionUp) { |  | ||||||
|             if (mTrackId != pointerId) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             mTrackId = -1; |  | ||||||
|             mUpButtonState = false; |  | ||||||
|             mDownButtonState = false; |  | ||||||
|             mLeftButtonState = false; |  | ||||||
|             mRightButtonState = false; |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (mTrackId == -1) { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!dpadSlide && !isActionDown) { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (int i = 0; i < event.getPointerCount(); i++) { |  | ||||||
|             if (mTrackId != event.getPointerId(i)) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             float touchX = event.getX(i); |  | ||||||
|             float touchY = event.getY(i); |  | ||||||
|             float maxY = getBounds().bottom; |  | ||||||
|             float maxX = getBounds().right; |  | ||||||
|             touchX -= getBounds().centerX(); |  | ||||||
|             maxX -= getBounds().centerX(); |  | ||||||
|             touchY -= getBounds().centerY(); |  | ||||||
|             maxY -= getBounds().centerY(); |  | ||||||
|             final float AxisX = touchX / maxX; |  | ||||||
|             final float AxisY = touchY / maxY; |  | ||||||
|             final boolean upState = mUpButtonState; |  | ||||||
|             final boolean downState = mDownButtonState; |  | ||||||
|             final boolean leftState = mLeftButtonState; |  | ||||||
|             final boolean rightState = mRightButtonState; |  | ||||||
| 
 |  | ||||||
|             mUpButtonState = AxisY < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE; |  | ||||||
|             mDownButtonState = AxisY > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE; |  | ||||||
|             mLeftButtonState = AxisX < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE; |  | ||||||
|             mRightButtonState = AxisX > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE; |  | ||||||
|             return upState != mUpButtonState || downState != mDownButtonState || leftState != mLeftButtonState || rightState != mRightButtonState; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void draw(Canvas canvas) { |  | ||||||
|         int px = mControlPositionX + (getWidth() / 2); |  | ||||||
|         int py = mControlPositionY + (getHeight() / 2); |  | ||||||
| 
 |  | ||||||
|         // Pressed up |  | ||||||
|         if (mUpButtonState && !mLeftButtonState && !mRightButtonState) { |  | ||||||
|             mPressedOneDirectionStateBitmap.draw(canvas); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Pressed down |  | ||||||
|         if (mDownButtonState && !mLeftButtonState && !mRightButtonState) { |  | ||||||
|             canvas.save(); |  | ||||||
|             canvas.rotate(180, px, py); |  | ||||||
|             mPressedOneDirectionStateBitmap.draw(canvas); |  | ||||||
|             canvas.restore(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Pressed left |  | ||||||
|         if (mLeftButtonState && !mUpButtonState && !mDownButtonState) { |  | ||||||
|             canvas.save(); |  | ||||||
|             canvas.rotate(270, px, py); |  | ||||||
|             mPressedOneDirectionStateBitmap.draw(canvas); |  | ||||||
|             canvas.restore(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Pressed right |  | ||||||
|         if (mRightButtonState && !mUpButtonState && !mDownButtonState) { |  | ||||||
|             canvas.save(); |  | ||||||
|             canvas.rotate(90, px, py); |  | ||||||
|             mPressedOneDirectionStateBitmap.draw(canvas); |  | ||||||
|             canvas.restore(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Pressed up left |  | ||||||
|         if (mUpButtonState && mLeftButtonState && !mRightButtonState) { |  | ||||||
|             mPressedTwoDirectionsStateBitmap.draw(canvas); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Pressed up right |  | ||||||
|         if (mUpButtonState && !mLeftButtonState && mRightButtonState) { |  | ||||||
|             canvas.save(); |  | ||||||
|             canvas.rotate(90, px, py); |  | ||||||
|             mPressedTwoDirectionsStateBitmap.draw(canvas); |  | ||||||
|             canvas.restore(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Pressed down left |  | ||||||
|         if (mDownButtonState && mLeftButtonState && !mRightButtonState) { |  | ||||||
|             canvas.save(); |  | ||||||
|             canvas.rotate(270, px, py); |  | ||||||
|             mPressedTwoDirectionsStateBitmap.draw(canvas); |  | ||||||
|             canvas.restore(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Pressed down right |  | ||||||
|         if (mDownButtonState && !mLeftButtonState && mRightButtonState) { |  | ||||||
|             canvas.save(); |  | ||||||
|             canvas.rotate(180, px, py); |  | ||||||
|             mPressedTwoDirectionsStateBitmap.draw(canvas); |  | ||||||
|             canvas.restore(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Not pressed |  | ||||||
|         mDefaultStateBitmap.draw(canvas); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getUpId() { |  | ||||||
|         return mUpButtonId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getDownId() { |  | ||||||
|         return mDownButtonId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getLeftId() { |  | ||||||
|         return mLeftButtonId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getRightId() { |  | ||||||
|         return mRightButtonId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getTrackId() { |  | ||||||
|         return mTrackId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setTrackId(int trackId) { |  | ||||||
|         mTrackId = trackId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getUpStatus() { |  | ||||||
|         return mUpButtonState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getDownStatus() { |  | ||||||
|         return mDownButtonState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getLeftStatus() { |  | ||||||
|         return mLeftButtonState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getRightStatus() { |  | ||||||
|         return mRightButtonState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean onConfigureTouch(MotionEvent event) { |  | ||||||
|         int pointerIndex = event.getActionIndex(); |  | ||||||
|         int fingerPositionX = (int) event.getX(pointerIndex); |  | ||||||
|         int fingerPositionY = (int) event.getY(pointerIndex); |  | ||||||
|         switch (event.getAction()) { |  | ||||||
|             case MotionEvent.ACTION_DOWN: |  | ||||||
|                 mPreviousTouchX = fingerPositionX; |  | ||||||
|                 mPreviousTouchY = fingerPositionY; |  | ||||||
|                 break; |  | ||||||
|             case MotionEvent.ACTION_MOVE: |  | ||||||
|                 mControlPositionX += fingerPositionX - mPreviousTouchX; |  | ||||||
|                 mControlPositionY += fingerPositionY - mPreviousTouchY; |  | ||||||
|                 setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, |  | ||||||
|                         getHeight() + mControlPositionY); |  | ||||||
|                 mPreviousTouchX = fingerPositionX; |  | ||||||
|                 mPreviousTouchY = fingerPositionY; |  | ||||||
|                 break; |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setPosition(int x, int y) { |  | ||||||
|         mControlPositionX = x; |  | ||||||
|         mControlPositionY = y; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setBounds(int left, int top, int right, int bottom) { |  | ||||||
|         mDefaultStateBitmap.setBounds(left, top, right, bottom); |  | ||||||
|         mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom); |  | ||||||
|         mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Rect getBounds() { |  | ||||||
|         return mDefaultStateBitmap.getBounds(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getWidth() { |  | ||||||
|         return mWidth; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getHeight() { |  | ||||||
|         return mHeight; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,262 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.overlay | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources | ||||||
|  | import android.graphics.Bitmap | ||||||
|  | import android.graphics.Canvas | ||||||
|  | import android.graphics.Rect | ||||||
|  | import android.graphics.drawable.BitmapDrawable | ||||||
|  | import android.view.MotionEvent | ||||||
|  | import org.citra.citra_emu.NativeLibrary | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Custom [BitmapDrawable] that is capable | ||||||
|  |  * of storing it's own ID. | ||||||
|  |  * | ||||||
|  |  * @param res                             [Resources] instance. | ||||||
|  |  * @param defaultStateBitmap              [Bitmap] of the default state. | ||||||
|  |  * @param pressedOneDirectionStateBitmap  [Bitmap] of the pressed state in one direction. | ||||||
|  |  * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction. | ||||||
|  |  * @param upId                            Identifier for the up button. | ||||||
|  |  * @param downId                          Identifier for the down button. | ||||||
|  |  * @param leftId                          Identifier for the left button. | ||||||
|  |  * @param rightId                         Identifier for the right button. | ||||||
|  |  */ | ||||||
|  | class InputOverlayDrawableDpad( | ||||||
|  |     res: Resources, | ||||||
|  |     defaultStateBitmap: Bitmap, | ||||||
|  |     pressedOneDirectionStateBitmap: Bitmap, | ||||||
|  |     pressedTwoDirectionsStateBitmap: Bitmap, | ||||||
|  |     val upId: Int, | ||||||
|  |     val downId: Int, | ||||||
|  |     val leftId: Int, | ||||||
|  |     val rightId: Int | ||||||
|  | ) { | ||||||
|  |     var trackId: Int | ||||||
|  |     private var previousTouchX = 0 | ||||||
|  |     private var previousTouchY = 0 | ||||||
|  |     private var controlPositionX = 0 | ||||||
|  |     private var controlPositionY = 0 | ||||||
|  |     val width: Int | ||||||
|  |     val height: Int | ||||||
|  |     private val defaultStateBitmap: BitmapDrawable | ||||||
|  |     private val pressedOneDirectionStateBitmap: BitmapDrawable | ||||||
|  |     private val pressedTwoDirectionsStateBitmap: BitmapDrawable | ||||||
|  |     private var upButtonState = false | ||||||
|  |     private var downButtonState = false | ||||||
|  |     private var leftButtonState = false | ||||||
|  |     private var rightButtonState = false | ||||||
|  | 
 | ||||||
|  |     init { | ||||||
|  |         this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap) | ||||||
|  |         this.pressedOneDirectionStateBitmap = BitmapDrawable(res, pressedOneDirectionStateBitmap) | ||||||
|  |         this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap) | ||||||
|  |         width = this.defaultStateBitmap.intrinsicWidth | ||||||
|  |         height = this.defaultStateBitmap.intrinsicHeight | ||||||
|  |         trackId = -1 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun updateStatus(event: MotionEvent, dpadSlide: Boolean): Boolean { | ||||||
|  |         val pointerIndex = event.actionIndex | ||||||
|  |         val xPosition = event.getX(pointerIndex).toInt() | ||||||
|  |         val yPosition = event.getY(pointerIndex).toInt() | ||||||
|  |         val pointerId = event.getPointerId(pointerIndex) | ||||||
|  |         val motionEvent = event.action and MotionEvent.ACTION_MASK | ||||||
|  |         val isActionDown = | ||||||
|  |             motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN | ||||||
|  |         val isActionUp = | ||||||
|  |             motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP | ||||||
|  |         if (isActionDown) { | ||||||
|  |             if (!bounds.contains(xPosition, yPosition)) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             trackId = pointerId | ||||||
|  |         } | ||||||
|  |         if (isActionUp) { | ||||||
|  |             if (trackId != pointerId) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             trackId = -1 | ||||||
|  |             upButtonState = false | ||||||
|  |             downButtonState = false | ||||||
|  |             leftButtonState = false | ||||||
|  |             rightButtonState = false | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         if (trackId == -1) { | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         if (!dpadSlide && !isActionDown) { | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         for (i in 0 until event.pointerCount) { | ||||||
|  |             if (trackId != event.getPointerId(i)) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             var touchX = event.getX(i) | ||||||
|  |             var touchY = event.getY(i) | ||||||
|  |             var maxY = bounds.bottom.toFloat() | ||||||
|  |             var maxX = bounds.right.toFloat() | ||||||
|  |             touchX -= bounds.centerX().toFloat() | ||||||
|  |             maxX -= bounds.centerX().toFloat() | ||||||
|  |             touchY -= bounds.centerY().toFloat() | ||||||
|  |             maxY -= bounds.centerY().toFloat() | ||||||
|  |             val xAxis = touchX / maxX | ||||||
|  |             val yAxis = touchY / maxY | ||||||
|  |             val upState = upButtonState | ||||||
|  |             val downState = downButtonState | ||||||
|  |             val leftState = leftButtonState | ||||||
|  |             val rightState = rightButtonState | ||||||
|  |             upButtonState = yAxis < -VIRT_AXIS_DEADZONE | ||||||
|  |             downButtonState = yAxis > VIRT_AXIS_DEADZONE | ||||||
|  |             leftButtonState = xAxis < -VIRT_AXIS_DEADZONE | ||||||
|  |             rightButtonState = xAxis > VIRT_AXIS_DEADZONE | ||||||
|  |             return upState != upButtonState || downState != downButtonState || leftState != leftButtonState || rightState != rightButtonState | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun draw(canvas: Canvas) { | ||||||
|  |         val px = controlPositionX + width / 2 | ||||||
|  |         val py = controlPositionY + height / 2 | ||||||
|  | 
 | ||||||
|  |         // Pressed up | ||||||
|  |         if (upButtonState && !leftButtonState && !rightButtonState) { | ||||||
|  |             pressedOneDirectionStateBitmap.draw(canvas) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Pressed down | ||||||
|  |         if (downButtonState && !leftButtonState && !rightButtonState) { | ||||||
|  |             canvas.save() | ||||||
|  |             canvas.rotate(180f, px.toFloat(), py.toFloat()) | ||||||
|  |             pressedOneDirectionStateBitmap.draw(canvas) | ||||||
|  |             canvas.restore() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Pressed left | ||||||
|  |         if (leftButtonState && !upButtonState && !downButtonState) { | ||||||
|  |             canvas.save() | ||||||
|  |             canvas.rotate(270f, px.toFloat(), py.toFloat()) | ||||||
|  |             pressedOneDirectionStateBitmap.draw(canvas) | ||||||
|  |             canvas.restore() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Pressed right | ||||||
|  |         if (rightButtonState && !upButtonState && !downButtonState) { | ||||||
|  |             canvas.save() | ||||||
|  |             canvas.rotate(90f, px.toFloat(), py.toFloat()) | ||||||
|  |             pressedOneDirectionStateBitmap.draw(canvas) | ||||||
|  |             canvas.restore() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Pressed up left | ||||||
|  |         if (upButtonState && leftButtonState && !rightButtonState) { | ||||||
|  |             pressedTwoDirectionsStateBitmap.draw(canvas) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Pressed up right | ||||||
|  |         if (upButtonState && !leftButtonState && rightButtonState) { | ||||||
|  |             canvas.save() | ||||||
|  |             canvas.rotate(90f, px.toFloat(), py.toFloat()) | ||||||
|  |             pressedTwoDirectionsStateBitmap.draw(canvas) | ||||||
|  |             canvas.restore() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Pressed down left | ||||||
|  |         if (downButtonState && leftButtonState && !rightButtonState) { | ||||||
|  |             canvas.save() | ||||||
|  |             canvas.rotate(270f, px.toFloat(), py.toFloat()) | ||||||
|  |             pressedTwoDirectionsStateBitmap.draw(canvas) | ||||||
|  |             canvas.restore() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Pressed down right | ||||||
|  |         if (downButtonState && !leftButtonState && rightButtonState) { | ||||||
|  |             canvas.save() | ||||||
|  |             canvas.rotate(180f, px.toFloat(), py.toFloat()) | ||||||
|  |             pressedTwoDirectionsStateBitmap.draw(canvas) | ||||||
|  |             canvas.restore() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Not pressed | ||||||
|  |         defaultStateBitmap.draw(canvas) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     val upStatus: Int | ||||||
|  |         get() = if (upButtonState) { | ||||||
|  |             NativeLibrary.ButtonState.PRESSED | ||||||
|  |         } else { | ||||||
|  |             NativeLibrary.ButtonState.RELEASED | ||||||
|  |         } | ||||||
|  |     val downStatus: Int | ||||||
|  |         get() = if (downButtonState) { | ||||||
|  |             NativeLibrary.ButtonState.PRESSED | ||||||
|  |         } else { | ||||||
|  |             NativeLibrary.ButtonState.RELEASED | ||||||
|  |         } | ||||||
|  |     val leftStatus: Int | ||||||
|  |         get() = if (leftButtonState) { | ||||||
|  |             NativeLibrary.ButtonState.PRESSED | ||||||
|  |         } else { | ||||||
|  |             NativeLibrary.ButtonState.RELEASED | ||||||
|  |         } | ||||||
|  |     val rightStatus: Int | ||||||
|  |         get() = if (rightButtonState) { | ||||||
|  |             NativeLibrary.ButtonState.PRESSED | ||||||
|  |         } else { | ||||||
|  |             NativeLibrary.ButtonState.RELEASED | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     fun onConfigureTouch(event: MotionEvent): Boolean { | ||||||
|  |         val pointerIndex = event.actionIndex | ||||||
|  |         val fingerPositionX = event.getX(pointerIndex).toInt() | ||||||
|  |         val fingerPositionY = event.getY(pointerIndex).toInt() | ||||||
|  |         when (event.action) { | ||||||
|  |             MotionEvent.ACTION_DOWN -> { | ||||||
|  |                 previousTouchX = fingerPositionX | ||||||
|  |                 previousTouchY = fingerPositionY | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             MotionEvent.ACTION_MOVE -> { | ||||||
|  |                 controlPositionX += fingerPositionX - previousTouchX | ||||||
|  |                 controlPositionY += fingerPositionY - previousTouchY | ||||||
|  |                 setBounds( | ||||||
|  |                     controlPositionX, controlPositionY, width + controlPositionX, | ||||||
|  |                     height + controlPositionY | ||||||
|  |                 ) | ||||||
|  |                 previousTouchX = fingerPositionX | ||||||
|  |                 previousTouchY = fingerPositionY | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setPosition(x: Int, y: Int) { | ||||||
|  |         controlPositionX = x | ||||||
|  |         controlPositionY = y | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setBounds(left: Int, top: Int, right: Int, bottom: Int) { | ||||||
|  |         defaultStateBitmap.setBounds(left, top, right, bottom) | ||||||
|  |         pressedOneDirectionStateBitmap.setBounds(left, top, right, bottom) | ||||||
|  |         pressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     val bounds: Rect | ||||||
|  |         get() = defaultStateBitmap.bounds | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         private const val VIRT_AXIS_DEADZONE = 0.5f | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,267 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Copyright 2013 Dolphin Emulator Project |  | ||||||
|  * Licensed under GPLv2+ |  | ||||||
|  * Refer to the license.txt file included. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| package org.citra.citra_emu.overlay; |  | ||||||
| 
 |  | ||||||
| import android.content.res.Resources; |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.graphics.Canvas; |  | ||||||
| import android.graphics.Rect; |  | ||||||
| import android.graphics.drawable.BitmapDrawable; |  | ||||||
| import android.view.MotionEvent; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.NativeLibrary.ButtonType; |  | ||||||
| import org.citra.citra_emu.utils.EmulationMenuSettings; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Custom {@link BitmapDrawable} that is capable |  | ||||||
|  * of storing it's own ID. |  | ||||||
|  */ |  | ||||||
| public final class InputOverlayDrawableJoystick { |  | ||||||
|     // The ID value what type of joystick this Drawable represents. |  | ||||||
|     private int mJoystickId; |  | ||||||
|     // The ID value what motion event is tracking |  | ||||||
|     private int mTrackId = -1; |  | ||||||
|     private float mXAxis; |  | ||||||
|     private float mYAxis; |  | ||||||
|     private int mControlPositionX, mControlPositionY; |  | ||||||
|     private int mPreviousTouchX, mPreviousTouchY; |  | ||||||
|     private int mWidth; |  | ||||||
|     private int mHeight; |  | ||||||
|     private Rect mVirtBounds; |  | ||||||
|     private Rect mOrigBounds; |  | ||||||
|     private BitmapDrawable mOuterBitmap; |  | ||||||
|     private BitmapDrawable mDefaultStateInnerBitmap; |  | ||||||
|     private BitmapDrawable mPressedStateInnerBitmap; |  | ||||||
|     private BitmapDrawable mBoundsBoxBitmap; |  | ||||||
|     private boolean mPressedState = false; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Constructor |  | ||||||
|      * |  | ||||||
|      * @param res                {@link Resources} instance. |  | ||||||
|      * @param bitmapOuter        {@link Bitmap} which represents the outer non-movable part of the joystick. |  | ||||||
|      * @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick. |  | ||||||
|      * @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick. |  | ||||||
|      * @param rectOuter          {@link Rect} which represents the outer joystick bounds. |  | ||||||
|      * @param rectInner          {@link Rect} which represents the inner joystick bounds. |  | ||||||
|      * @param joystick           Identifier for which joystick this is. |  | ||||||
|      */ |  | ||||||
|     public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter, |  | ||||||
|                                         Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed, |  | ||||||
|                                         Rect rectOuter, Rect rectInner, int joystick) { |  | ||||||
|         mJoystickId = joystick; |  | ||||||
| 
 |  | ||||||
|         mOuterBitmap = new BitmapDrawable(res, bitmapOuter); |  | ||||||
|         mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault); |  | ||||||
|         mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed); |  | ||||||
|         mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter); |  | ||||||
|         mWidth = bitmapOuter.getWidth(); |  | ||||||
|         mHeight = bitmapOuter.getHeight(); |  | ||||||
| 
 |  | ||||||
|         setBounds(rectOuter); |  | ||||||
|         mDefaultStateInnerBitmap.setBounds(rectInner); |  | ||||||
|         mPressedStateInnerBitmap.setBounds(rectInner); |  | ||||||
|         mVirtBounds = getBounds(); |  | ||||||
|         mOrigBounds = mOuterBitmap.copyBounds(); |  | ||||||
|         mBoundsBoxBitmap.setAlpha(0); |  | ||||||
|         mBoundsBoxBitmap.setBounds(getVirtBounds()); |  | ||||||
|         SetInnerBounds(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void draw(Canvas canvas) { |  | ||||||
|         mOuterBitmap.draw(canvas); |  | ||||||
|         getCurrentStateBitmapDrawable().draw(canvas); |  | ||||||
|         mBoundsBoxBitmap.draw(canvas); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean updateStatus(MotionEvent event) { |  | ||||||
|         int pointerIndex = event.getActionIndex(); |  | ||||||
|         int xPosition = (int) event.getX(pointerIndex); |  | ||||||
|         int yPosition = (int) event.getY(pointerIndex); |  | ||||||
|         int pointerId = event.getPointerId(pointerIndex); |  | ||||||
|         int motionEvent = event.getAction() & MotionEvent.ACTION_MASK; |  | ||||||
|         boolean isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN; |  | ||||||
|         boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP; |  | ||||||
| 
 |  | ||||||
|         if (isActionDown) { |  | ||||||
|             if (!getBounds().contains(xPosition, yPosition)) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             mPressedState = true; |  | ||||||
|             mOuterBitmap.setAlpha(0); |  | ||||||
|             mBoundsBoxBitmap.setAlpha(255); |  | ||||||
|             if (EmulationMenuSettings.INSTANCE.getJoystickRelCenter()) { |  | ||||||
|                 getVirtBounds().offset(xPosition - getVirtBounds().centerX(), |  | ||||||
|                         yPosition - getVirtBounds().centerY()); |  | ||||||
|             } |  | ||||||
|             mBoundsBoxBitmap.setBounds(getVirtBounds()); |  | ||||||
|             mTrackId = pointerId; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (isActionUp) { |  | ||||||
|             if (mTrackId != pointerId) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|             mPressedState = false; |  | ||||||
|             mXAxis = 0.0f; |  | ||||||
|             mYAxis = 0.0f; |  | ||||||
|             mOuterBitmap.setAlpha(255); |  | ||||||
|             mBoundsBoxBitmap.setAlpha(0); |  | ||||||
|             setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, |  | ||||||
|                     mOrigBounds.bottom)); |  | ||||||
|             setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, |  | ||||||
|                     mOrigBounds.bottom)); |  | ||||||
|             SetInnerBounds(); |  | ||||||
|             mTrackId = -1; |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (mTrackId == -1) |  | ||||||
|             return false; |  | ||||||
| 
 |  | ||||||
|         for (int i = 0; i < event.getPointerCount(); i++) { |  | ||||||
|             if (mTrackId != event.getPointerId(i)) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             float touchX = event.getX(i); |  | ||||||
|             float touchY = event.getY(i); |  | ||||||
|             float maxY = getVirtBounds().bottom; |  | ||||||
|             float maxX = getVirtBounds().right; |  | ||||||
|             touchX -= getVirtBounds().centerX(); |  | ||||||
|             maxX -= getVirtBounds().centerX(); |  | ||||||
|             touchY -= getVirtBounds().centerY(); |  | ||||||
|             maxY -= getVirtBounds().centerY(); |  | ||||||
|             final float AxisX = touchX / maxX; |  | ||||||
|             final float AxisY = touchY / maxY; |  | ||||||
|             final float oldXAxis = mXAxis; |  | ||||||
|             final float oldYAxis = mYAxis; |  | ||||||
| 
 |  | ||||||
|             // Clamp the circle pad input to a circle |  | ||||||
|             final float angle = (float) Math.atan2(AxisY, AxisX); |  | ||||||
|             float radius = (float) Math.sqrt(AxisX * AxisX + AxisY * AxisY); |  | ||||||
|             if (radius > 1.0f) { |  | ||||||
|                 radius = 1.0f; |  | ||||||
|             } |  | ||||||
|             mXAxis = ((float) Math.cos(angle) * radius); |  | ||||||
|             mYAxis = ((float) Math.sin(angle) * radius); |  | ||||||
|             SetInnerBounds(); |  | ||||||
|             return oldXAxis != mXAxis && oldYAxis != mYAxis; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean onConfigureTouch(MotionEvent event) { |  | ||||||
|         int pointerIndex = event.getActionIndex(); |  | ||||||
|         int fingerPositionX = (int) event.getX(pointerIndex); |  | ||||||
|         int fingerPositionY = (int) event.getY(pointerIndex); |  | ||||||
| 
 |  | ||||||
|         int scale = 1; |  | ||||||
|         if (mJoystickId == ButtonType.STICK_C) { |  | ||||||
|             // C-stick is scaled down to be half the size of the circle pad |  | ||||||
|             scale = 2; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         switch (event.getAction()) { |  | ||||||
|             case MotionEvent.ACTION_DOWN: |  | ||||||
|                 mPreviousTouchX = fingerPositionX; |  | ||||||
|                 mPreviousTouchY = fingerPositionY; |  | ||||||
|                 break; |  | ||||||
|             case MotionEvent.ACTION_MOVE: |  | ||||||
|                 int deltaX = fingerPositionX - mPreviousTouchX; |  | ||||||
|                 int deltaY = fingerPositionY - mPreviousTouchY; |  | ||||||
|                 mControlPositionX += deltaX; |  | ||||||
|                 mControlPositionY += deltaY; |  | ||||||
|                 setBounds(new Rect(mControlPositionX, mControlPositionY, |  | ||||||
|                         mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, |  | ||||||
|                         mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); |  | ||||||
|                 setVirtBounds(new Rect(mControlPositionX, mControlPositionY, |  | ||||||
|                         mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, |  | ||||||
|                         mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); |  | ||||||
|                 SetInnerBounds(); |  | ||||||
|                 setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY, |  | ||||||
|                         mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, |  | ||||||
|                         mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY))); |  | ||||||
|                 mPreviousTouchX = fingerPositionX; |  | ||||||
|                 mPreviousTouchY = fingerPositionY; |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getJoystickId() { |  | ||||||
|         return mJoystickId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public float getXAxis() { |  | ||||||
|         return mXAxis; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public float getYAxis() { |  | ||||||
|         return mYAxis; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getTrackId() { |  | ||||||
|         return mTrackId; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void SetInnerBounds() { |  | ||||||
|         int X = getVirtBounds().centerX() + (int) ((mXAxis) * (getVirtBounds().width() / 2)); |  | ||||||
|         int Y = getVirtBounds().centerY() + (int) ((mYAxis) * (getVirtBounds().height() / 2)); |  | ||||||
| 
 |  | ||||||
|         if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2)) |  | ||||||
|             X = getVirtBounds().centerX() + (getVirtBounds().width() / 2); |  | ||||||
|         if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2)) |  | ||||||
|             X = getVirtBounds().centerX() - (getVirtBounds().width() / 2); |  | ||||||
|         if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2)) |  | ||||||
|             Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2); |  | ||||||
|         if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2)) |  | ||||||
|             Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2); |  | ||||||
| 
 |  | ||||||
|         int width = mPressedStateInnerBitmap.getBounds().width() / 2; |  | ||||||
|         int height = mPressedStateInnerBitmap.getBounds().height() / 2; |  | ||||||
|         mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height); |  | ||||||
|         mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setPosition(int x, int y) { |  | ||||||
|         mControlPositionX = x; |  | ||||||
|         mControlPositionY = y; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private BitmapDrawable getCurrentStateBitmapDrawable() { |  | ||||||
|         return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Rect getBounds() { |  | ||||||
|         return mOuterBitmap.getBounds(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setBounds(Rect bounds) { |  | ||||||
|         mOuterBitmap.setBounds(bounds); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void setOrigBounds(Rect bounds) { |  | ||||||
|         mOrigBounds = bounds; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private Rect getVirtBounds() { |  | ||||||
|         return mVirtBounds; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void setVirtBounds(Rect bounds) { |  | ||||||
|         mVirtBounds = bounds; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getWidth() { |  | ||||||
|         return mWidth; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public int getHeight() { |  | ||||||
|         return mHeight; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,238 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.overlay | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources | ||||||
|  | import android.graphics.Bitmap | ||||||
|  | import android.graphics.Canvas | ||||||
|  | import android.graphics.Rect | ||||||
|  | import android.graphics.drawable.BitmapDrawable | ||||||
|  | import android.view.MotionEvent | ||||||
|  | import org.citra.citra_emu.NativeLibrary | ||||||
|  | import org.citra.citra_emu.utils.EmulationMenuSettings | ||||||
|  | import kotlin.math.atan2 | ||||||
|  | import kotlin.math.cos | ||||||
|  | import kotlin.math.sin | ||||||
|  | import kotlin.math.sqrt | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Custom [BitmapDrawable] that is capable | ||||||
|  |  * of storing it's own ID. | ||||||
|  |  * | ||||||
|  |  * @param res                [Resources] instance. | ||||||
|  |  * @param bitmapOuter        [Bitmap] which represents the outer non-movable part of the joystick. | ||||||
|  |  * @param bitmapInnerDefault [Bitmap] which represents the default inner movable part of the joystick. | ||||||
|  |  * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick. | ||||||
|  |  * @param rectOuter          [Rect] which represents the outer joystick bounds. | ||||||
|  |  * @param rectInner          [Rect] which represents the inner joystick bounds. | ||||||
|  |  * @param joystickId         Identifier for which joystick this is. | ||||||
|  |  */ | ||||||
|  | class InputOverlayDrawableJoystick( | ||||||
|  |     res: Resources, | ||||||
|  |     bitmapOuter: Bitmap, | ||||||
|  |     bitmapInnerDefault: Bitmap, | ||||||
|  |     bitmapInnerPressed: Bitmap, | ||||||
|  |     rectOuter: Rect, | ||||||
|  |     rectInner: Rect, | ||||||
|  |     val joystickId: Int | ||||||
|  | ) { | ||||||
|  |     var trackId = -1 | ||||||
|  |     var xAxis = 0f | ||||||
|  |     var yAxis = 0f | ||||||
|  |     private var controlPositionX = 0 | ||||||
|  |     private var controlPositionY = 0 | ||||||
|  |     private var previousTouchX = 0 | ||||||
|  |     private var previousTouchY = 0 | ||||||
|  |     val width: Int | ||||||
|  |     val height: Int | ||||||
|  |     private var virtBounds: Rect | ||||||
|  |     private var origBounds: Rect | ||||||
|  |     private val outerBitmap: BitmapDrawable | ||||||
|  |     private val defaultStateInnerBitmap: BitmapDrawable | ||||||
|  |     private val pressedStateInnerBitmap: BitmapDrawable | ||||||
|  |     private val boundsBoxBitmap: BitmapDrawable | ||||||
|  |     private var pressedState = false | ||||||
|  | 
 | ||||||
|  |     var bounds: Rect | ||||||
|  |         get() = outerBitmap.bounds | ||||||
|  |         set(bounds) { | ||||||
|  |             outerBitmap.bounds = bounds | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     init { | ||||||
|  |         outerBitmap = BitmapDrawable(res, bitmapOuter) | ||||||
|  |         defaultStateInnerBitmap = BitmapDrawable(res, bitmapInnerDefault) | ||||||
|  |         pressedStateInnerBitmap = BitmapDrawable(res, bitmapInnerPressed) | ||||||
|  |         boundsBoxBitmap = BitmapDrawable(res, bitmapOuter) | ||||||
|  |         width = bitmapOuter.width | ||||||
|  |         height = bitmapOuter.height | ||||||
|  |         bounds = rectOuter | ||||||
|  |         defaultStateInnerBitmap.bounds = rectInner | ||||||
|  |         pressedStateInnerBitmap.bounds = rectInner | ||||||
|  |         virtBounds = bounds | ||||||
|  |         origBounds = outerBitmap.copyBounds() | ||||||
|  |         boundsBoxBitmap.alpha = 0 | ||||||
|  |         boundsBoxBitmap.bounds = virtBounds | ||||||
|  |         setInnerBounds() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun draw(canvas: Canvas?) { | ||||||
|  |         outerBitmap.draw(canvas!!) | ||||||
|  |         currentStateBitmapDrawable.draw(canvas) | ||||||
|  |         boundsBoxBitmap.draw(canvas) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun updateStatus(event: MotionEvent): Boolean { | ||||||
|  |         val pointerIndex = event.actionIndex | ||||||
|  |         val xPosition = event.getX(pointerIndex).toInt() | ||||||
|  |         val yPosition = event.getY(pointerIndex).toInt() | ||||||
|  |         val pointerId = event.getPointerId(pointerIndex) | ||||||
|  |         val motionEvent = event.action and MotionEvent.ACTION_MASK | ||||||
|  |         val isActionDown = | ||||||
|  |             motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN | ||||||
|  |         val isActionUp = | ||||||
|  |             motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP | ||||||
|  |         if (isActionDown) { | ||||||
|  |             if (!bounds.contains(xPosition, yPosition)) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             pressedState = true | ||||||
|  |             outerBitmap.alpha = 0 | ||||||
|  |             boundsBoxBitmap.alpha = 255 | ||||||
|  |             if (EmulationMenuSettings.joystickRelCenter) { | ||||||
|  |                 virtBounds.offset( | ||||||
|  |                     xPosition - virtBounds.centerX(), | ||||||
|  |                     yPosition - virtBounds.centerY() | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |             boundsBoxBitmap.bounds = virtBounds | ||||||
|  |             trackId = pointerId | ||||||
|  |         } | ||||||
|  |         if (isActionUp) { | ||||||
|  |             if (trackId != pointerId) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             pressedState = false | ||||||
|  |             xAxis = 0.0f | ||||||
|  |             yAxis = 0.0f | ||||||
|  |             outerBitmap.alpha = 255 | ||||||
|  |             boundsBoxBitmap.alpha = 0 | ||||||
|  |             virtBounds = Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom) | ||||||
|  |             bounds = Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom) | ||||||
|  |             setInnerBounds() | ||||||
|  |             trackId = -1 | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         if (trackId == -1) return false | ||||||
|  |         for (i in 0 until event.pointerCount) { | ||||||
|  |             if (trackId != event.getPointerId(i)) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             var touchX = event.getX(i) | ||||||
|  |             var touchY = event.getY(i) | ||||||
|  |             var maxY = virtBounds.bottom.toFloat() | ||||||
|  |             var maxX = virtBounds.right.toFloat() | ||||||
|  |             touchX -= virtBounds.centerX().toFloat() | ||||||
|  |             maxX -= virtBounds.centerX().toFloat() | ||||||
|  |             touchY -= virtBounds.centerY().toFloat() | ||||||
|  |             maxY -= virtBounds.centerY().toFloat() | ||||||
|  |             val xAxis = touchX / maxX | ||||||
|  |             val yAxis = touchY / maxY | ||||||
|  |             val oldXAxis = this.xAxis | ||||||
|  |             val oldYAxis = this.yAxis | ||||||
|  | 
 | ||||||
|  |             // Clamp the circle pad input to a circle | ||||||
|  |             val angle = atan2(yAxis.toDouble(), xAxis.toDouble()).toFloat() | ||||||
|  |             var radius = sqrt((xAxis * xAxis + yAxis * yAxis).toDouble()).toFloat() | ||||||
|  |             if (radius > 1.0f) { | ||||||
|  |                 radius = 1.0f | ||||||
|  |             } | ||||||
|  |             this.xAxis = cos(angle.toDouble()).toFloat() * radius | ||||||
|  |             this.yAxis = sin(angle.toDouble()).toFloat() * radius | ||||||
|  |             setInnerBounds() | ||||||
|  |             return oldXAxis != this.xAxis && oldYAxis != this.yAxis | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onConfigureTouch(event: MotionEvent): Boolean { | ||||||
|  |         val pointerIndex = event.actionIndex | ||||||
|  |         val fingerPositionX = event.getX(pointerIndex).toInt() | ||||||
|  |         val fingerPositionY = event.getY(pointerIndex).toInt() | ||||||
|  |         var scale = 1 | ||||||
|  |         if (joystickId == NativeLibrary.ButtonType.STICK_C) { | ||||||
|  |             // C-stick is scaled down to be half the size of the circle pad | ||||||
|  |             scale = 2 | ||||||
|  |         } | ||||||
|  |         when (event.action) { | ||||||
|  |             MotionEvent.ACTION_DOWN -> { | ||||||
|  |                 previousTouchX = fingerPositionX | ||||||
|  |                 previousTouchY = fingerPositionY | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             MotionEvent.ACTION_MOVE -> { | ||||||
|  |                 val deltaX = fingerPositionX - previousTouchX | ||||||
|  |                 val deltaY = fingerPositionY - previousTouchY | ||||||
|  |                 controlPositionX += deltaX | ||||||
|  |                 controlPositionY += deltaY | ||||||
|  |                 bounds = Rect( | ||||||
|  |                     controlPositionX, | ||||||
|  |                     controlPositionY, | ||||||
|  |                     outerBitmap.intrinsicWidth / scale + controlPositionX, | ||||||
|  |                     outerBitmap.intrinsicHeight / scale + controlPositionY | ||||||
|  |                 ) | ||||||
|  |                 virtBounds = Rect( | ||||||
|  |                     controlPositionX, | ||||||
|  |                     controlPositionY, | ||||||
|  |                     outerBitmap.intrinsicWidth / scale + controlPositionX, | ||||||
|  |                     outerBitmap.intrinsicHeight / scale + controlPositionY | ||||||
|  |                 ) | ||||||
|  |                 setInnerBounds() | ||||||
|  |                 setOrigBounds( | ||||||
|  |                     Rect( | ||||||
|  |                         Rect( | ||||||
|  |                             controlPositionX, | ||||||
|  |                             controlPositionY, | ||||||
|  |                             outerBitmap.intrinsicWidth / scale + controlPositionX, | ||||||
|  |                             outerBitmap.intrinsicHeight / scale + controlPositionY | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |                 previousTouchX = fingerPositionX | ||||||
|  |                 previousTouchY = fingerPositionY | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setInnerBounds() { | ||||||
|  |         var x = virtBounds.centerX() + (xAxis * (virtBounds.width() / 2)).toInt() | ||||||
|  |         var y = virtBounds.centerY() + (yAxis * (virtBounds.height() / 2)).toInt() | ||||||
|  |         if (x > virtBounds.centerX() + virtBounds.width() / 2) x = | ||||||
|  |             virtBounds.centerX() + virtBounds.width() / 2 | ||||||
|  |         if (x < virtBounds.centerX() - virtBounds.width() / 2) x = | ||||||
|  |             virtBounds.centerX() - virtBounds.width() / 2 | ||||||
|  |         if (y > virtBounds.centerY() + virtBounds.height() / 2) y = | ||||||
|  |             virtBounds.centerY() + virtBounds.height() / 2 | ||||||
|  |         if (y < virtBounds.centerY() - virtBounds.height() / 2) y = | ||||||
|  |             virtBounds.centerY() - virtBounds.height() / 2 | ||||||
|  |         val width = pressedStateInnerBitmap.bounds.width() / 2 | ||||||
|  |         val height = pressedStateInnerBitmap.bounds.height() / 2 | ||||||
|  |         defaultStateInnerBitmap.setBounds(x - width, y - height, x + width, y + height) | ||||||
|  |         pressedStateInnerBitmap.bounds = defaultStateInnerBitmap.bounds | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setPosition(x: Int, y: Int) { | ||||||
|  |         controlPositionX = x | ||||||
|  |         controlPositionY = y | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private val currentStateBitmapDrawable: BitmapDrawable | ||||||
|  |         get() = if (pressedState) pressedStateInnerBitmap else defaultStateInnerBitmap | ||||||
|  | 
 | ||||||
|  |     private fun setOrigBounds(bounds: Rect) { | ||||||
|  |         origBounds = bounds | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,130 +0,0 @@ | ||||||
| package org.citra.citra_emu.ui; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.res.TypedArray; |  | ||||||
| import android.graphics.Canvas; |  | ||||||
| import android.graphics.Rect; |  | ||||||
| import android.graphics.drawable.Drawable; |  | ||||||
| import android.util.AttributeSet; |  | ||||||
| import android.view.View; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; |  | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Implementation from: |  | ||||||
|  * https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36 |  | ||||||
|  */ |  | ||||||
| public class DividerItemDecoration extends RecyclerView.ItemDecoration { |  | ||||||
| 
 |  | ||||||
|     private Drawable mDivider; |  | ||||||
|     private boolean mShowFirstDivider = false; |  | ||||||
|     private boolean mShowLastDivider = false; |  | ||||||
| 
 |  | ||||||
|     public DividerItemDecoration(Context context, AttributeSet attrs) { |  | ||||||
|         final TypedArray a = context |  | ||||||
|                 .obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider}); |  | ||||||
|         mDivider = a.getDrawable(0); |  | ||||||
|         a.recycle(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider, |  | ||||||
|                                  boolean showLastDivider) { |  | ||||||
|         this(context, attrs); |  | ||||||
|         mShowFirstDivider = showFirstDivider; |  | ||||||
|         mShowLastDivider = showLastDivider; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public DividerItemDecoration(Drawable divider) { |  | ||||||
|         mDivider = divider; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public DividerItemDecoration(Drawable divider, boolean showFirstDivider, |  | ||||||
|                                  boolean showLastDivider) { |  | ||||||
|         this(divider); |  | ||||||
|         mShowFirstDivider = showFirstDivider; |  | ||||||
|         mShowLastDivider = showLastDivider; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, |  | ||||||
|                                @NonNull RecyclerView.State state) { |  | ||||||
|         super.getItemOffsets(outRect, view, parent, state); |  | ||||||
|         if (mDivider == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         if (parent.getChildAdapterPosition(view) < 1) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (getOrientation(parent) == LinearLayoutManager.VERTICAL) { |  | ||||||
|             outRect.top = mDivider.getIntrinsicHeight(); |  | ||||||
|         } else { |  | ||||||
|             outRect.left = mDivider.getIntrinsicWidth(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { |  | ||||||
|         if (mDivider == null) { |  | ||||||
|             super.onDrawOver(c, parent, state); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Initialization needed to avoid compiler warning |  | ||||||
|         int left = 0, right = 0, top = 0, bottom = 0, size; |  | ||||||
|         int orientation = getOrientation(parent); |  | ||||||
|         int childCount = parent.getChildCount(); |  | ||||||
| 
 |  | ||||||
|         if (orientation == LinearLayoutManager.VERTICAL) { |  | ||||||
|             size = mDivider.getIntrinsicHeight(); |  | ||||||
|             left = parent.getPaddingLeft(); |  | ||||||
|             right = parent.getWidth() - parent.getPaddingRight(); |  | ||||||
|         } else { //horizontal |  | ||||||
|             size = mDivider.getIntrinsicWidth(); |  | ||||||
|             top = parent.getPaddingTop(); |  | ||||||
|             bottom = parent.getHeight() - parent.getPaddingBottom(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++) { |  | ||||||
|             View child = parent.getChildAt(i); |  | ||||||
|             RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); |  | ||||||
| 
 |  | ||||||
|             if (orientation == LinearLayoutManager.VERTICAL) { |  | ||||||
|                 top = child.getTop() - params.topMargin; |  | ||||||
|                 bottom = top + size; |  | ||||||
|             } else { //horizontal |  | ||||||
|                 left = child.getLeft() - params.leftMargin; |  | ||||||
|                 right = left + size; |  | ||||||
|             } |  | ||||||
|             mDivider.setBounds(left, top, right, bottom); |  | ||||||
|             mDivider.draw(c); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // show last divider |  | ||||||
|         if (mShowLastDivider && childCount > 0) { |  | ||||||
|             View child = parent.getChildAt(childCount - 1); |  | ||||||
|             RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); |  | ||||||
|             if (orientation == LinearLayoutManager.VERTICAL) { |  | ||||||
|                 top = child.getBottom() + params.bottomMargin; |  | ||||||
|                 bottom = top + size; |  | ||||||
|             } else { // horizontal |  | ||||||
|                 left = child.getRight() + params.rightMargin; |  | ||||||
|                 right = left + size; |  | ||||||
|             } |  | ||||||
|             mDivider.setBounds(left, top, right, bottom); |  | ||||||
|             mDivider.draw(c); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private int getOrientation(RecyclerView parent) { |  | ||||||
|         if (parent.getLayoutManager() instanceof LinearLayoutManager) { |  | ||||||
|             LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager(); |  | ||||||
|             return layoutManager.getOrientation(); |  | ||||||
|         } else { |  | ||||||
|             throw new IllegalStateException( |  | ||||||
|                     "DividerItemDecoration can only be used with a LinearLayoutManager."); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,46 +0,0 @@ | ||||||
| package org.citra.citra_emu.ui; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.inputmethod.InputMethodManager; |  | ||||||
| 
 |  | ||||||
| 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) { |  | ||||||
|         closeKeyboard(); |  | ||||||
|         setEnabled(false); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void closeKeyboard() { |  | ||||||
|         InputMethodManager manager = (InputMethodManager) mSlidingPaneLayout.getContext() |  | ||||||
|                 .getSystemService(Context.INPUT_METHOD_SERVICE); |  | ||||||
|         manager.hideSoftInputFromWindow(mSlidingPaneLayout.getRootView().getWindowToken(), 0); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,40 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.ui | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import android.view.View | ||||||
|  | import android.view.inputmethod.InputMethodManager | ||||||
|  | import androidx.activity.OnBackPressedCallback | ||||||
|  | import androidx.slidingpanelayout.widget.SlidingPaneLayout | ||||||
|  | import androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener | ||||||
|  | 
 | ||||||
|  | class TwoPaneOnBackPressedCallback(private val slidingPaneLayout: SlidingPaneLayout) : | ||||||
|  |     OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen), | ||||||
|  |     PanelSlideListener { | ||||||
|  |     init { | ||||||
|  |         slidingPaneLayout.addPanelSlideListener(this) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun handleOnBackPressed() { | ||||||
|  |         slidingPaneLayout.close() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onPanelSlide(panel: View, slideOffset: Float) {} | ||||||
|  |     override fun onPanelOpened(panel: View) { | ||||||
|  |         isEnabled = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onPanelClosed(panel: View) { | ||||||
|  |         closeKeyboard() | ||||||
|  |         isEnabled = false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun closeKeyboard() { | ||||||
|  |         val manager = slidingPaneLayout.context | ||||||
|  |             .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager | ||||||
|  |         manager.hideSoftInputFromWindow(slidingPaneLayout.rootView.windowToken, 0) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| package org.citra.citra_emu.utils; |  | ||||||
| 
 |  | ||||||
| public interface Action1<T> { |  | ||||||
|     void call(T t); |  | ||||||
| } |  | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| package org.citra.citra_emu.utils; |  | ||||||
| 
 |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.Map; |  | ||||||
| 
 |  | ||||||
| public class BiMap<K, V> { |  | ||||||
|     private Map<K, V> forward = new HashMap<K, V>(); |  | ||||||
|     private Map<V, K> backward = new HashMap<V, K>(); |  | ||||||
| 
 |  | ||||||
|     public synchronized void add(K key, V value) { |  | ||||||
|         forward.put(key, value); |  | ||||||
|         backward.put(value, key); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public synchronized V getForward(K key) { |  | ||||||
|         return forward.get(key); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public synchronized K getBackward(V key) { |  | ||||||
|         return backward.get(key); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.utils | ||||||
|  | 
 | ||||||
|  | class BiMap<K, V> { | ||||||
|  |     private val forward: MutableMap<K, V> = HashMap() | ||||||
|  |     private val backward: MutableMap<V, K> = HashMap() | ||||||
|  | 
 | ||||||
|  |     @Synchronized | ||||||
|  |     fun add(key: K, value: V) { | ||||||
|  |         forward[key] = value | ||||||
|  |         backward[value] = key | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Synchronized | ||||||
|  |     fun getForward(key: K): V? = forward[key] | ||||||
|  | 
 | ||||||
|  |     @Synchronized | ||||||
|  |     fun getBackward(key: V): K? = backward[key] | ||||||
|  | } | ||||||
|  | @ -1,153 +0,0 @@ | ||||||
| package org.citra.citra_emu.utils; |  | ||||||
| 
 |  | ||||||
| import android.app.Notification; |  | ||||||
| import android.app.NotificationManager; |  | ||||||
| import android.app.PendingIntent; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.widget.Toast; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.core.app.NotificationCompat; |  | ||||||
| import androidx.work.ForegroundInfo; |  | ||||||
| import androidx.work.Worker; |  | ||||||
| import androidx.work.WorkerParameters; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.NativeLibrary.InstallStatus; |  | ||||||
| import org.citra.citra_emu.R; |  | ||||||
| 
 |  | ||||||
| public class CiaInstallWorker extends Worker { |  | ||||||
|     private final Context mContext = getApplicationContext(); |  | ||||||
| 
 |  | ||||||
|     private final NotificationManager mNotificationManager = |  | ||||||
|             mContext.getSystemService(NotificationManager.class); |  | ||||||
| 
 |  | ||||||
|     static final String GROUP_KEY_CIA_INSTALL_STATUS = "org.citra.citra_emu.CIA_INSTALL_STATUS"; |  | ||||||
| 
 |  | ||||||
|     private final NotificationCompat.Builder mInstallProgressBuilder = new NotificationCompat.Builder( |  | ||||||
|             mContext, mContext.getString(R.string.cia_install_notification_channel_id)) |  | ||||||
|             .setContentTitle(mContext.getString(R.string.install_cia_title)) |  | ||||||
|             .setContentIntent(PendingIntent.getBroadcast(mContext, 0, |  | ||||||
|                     new Intent("CitraDoNothing"), PendingIntent.FLAG_IMMUTABLE)) |  | ||||||
|             .setSmallIcon(R.drawable.ic_stat_notification_logo); |  | ||||||
| 
 |  | ||||||
|     private final NotificationCompat.Builder mInstallStatusBuilder = new NotificationCompat.Builder( |  | ||||||
|             mContext, mContext.getString(R.string.cia_install_notification_channel_id)) |  | ||||||
|             .setContentTitle(mContext.getString(R.string.install_cia_title)) |  | ||||||
|             .setSmallIcon(R.drawable.ic_stat_notification_logo) |  | ||||||
|             .setGroup(GROUP_KEY_CIA_INSTALL_STATUS); |  | ||||||
| 
 |  | ||||||
|     private final Notification mSummaryNotification = |  | ||||||
|             new NotificationCompat.Builder(mContext, mContext.getString(R.string.cia_install_notification_channel_id)) |  | ||||||
|                     .setContentTitle(mContext.getString(R.string.install_cia_title)) |  | ||||||
|                     .setSmallIcon(R.drawable.ic_stat_notification_logo) |  | ||||||
|                     .setGroup(GROUP_KEY_CIA_INSTALL_STATUS) |  | ||||||
|                     .setGroupSummary(true) |  | ||||||
|                     .build(); |  | ||||||
| 
 |  | ||||||
|     private static long mLastNotifiedTime = 0; |  | ||||||
| 
 |  | ||||||
|     private static final int SUMMARY_NOTIFICATION_ID = 0xC1A0000; |  | ||||||
|     private static final int PROGRESS_NOTIFICATION_ID = SUMMARY_NOTIFICATION_ID + 1; |  | ||||||
|     private static int mStatusNotificationId = SUMMARY_NOTIFICATION_ID + 2; |  | ||||||
| 
 |  | ||||||
|     public CiaInstallWorker( |  | ||||||
|             @NonNull Context context, |  | ||||||
|             @NonNull WorkerParameters params) { |  | ||||||
|         super(context, params); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void notifyInstallStatus(String filename, InstallStatus status) { |  | ||||||
|         switch(status){ |  | ||||||
|             case Success: |  | ||||||
|                 mInstallStatusBuilder.setContentTitle( |  | ||||||
|                         mContext.getString(R.string.cia_install_notification_success_title)); |  | ||||||
|                 mInstallStatusBuilder.setContentText( |  | ||||||
|                         mContext.getString(R.string.cia_install_success, filename)); |  | ||||||
|                 break; |  | ||||||
|             case ErrorAborted: |  | ||||||
|                 mInstallStatusBuilder.setContentTitle( |  | ||||||
|                         mContext.getString(R.string.cia_install_notification_error_title)); |  | ||||||
|                 mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle() |  | ||||||
|                                 .bigText(mContext.getString( |  | ||||||
|                                          R.string.cia_install_error_aborted, filename))); |  | ||||||
|                 break; |  | ||||||
|             case ErrorInvalid: |  | ||||||
|                 mInstallStatusBuilder.setContentTitle( |  | ||||||
|                         mContext.getString(R.string.cia_install_notification_error_title)); |  | ||||||
|                 mInstallStatusBuilder.setContentText( |  | ||||||
|                         mContext.getString(R.string.cia_install_error_invalid, filename)); |  | ||||||
|                 break; |  | ||||||
|             case ErrorEncrypted: |  | ||||||
|                 mInstallStatusBuilder.setContentTitle( |  | ||||||
|                         mContext.getString(R.string.cia_install_notification_error_title)); |  | ||||||
|                 mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle() |  | ||||||
|                         .bigText(mContext.getString( |  | ||||||
|                                  R.string.cia_install_error_encrypted, filename))); |  | ||||||
|                 break; |  | ||||||
|             case ErrorFailedToOpenFile: |  | ||||||
|                 // TODO: |  | ||||||
|             case ErrorFileNotFound: |  | ||||||
|                 // shouldn't happen |  | ||||||
|             default: |  | ||||||
|                 mInstallStatusBuilder.setContentTitle( |  | ||||||
|                         mContext.getString(R.string.cia_install_notification_error_title)); |  | ||||||
|                 mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle() |  | ||||||
|                         .bigText(mContext.getString(R.string.cia_install_error_unknown, filename))); |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|         // Even if newer versions of Android don't show the group summary text that you design, |  | ||||||
|         // you always need to manually set a summary to enable grouped notifications. |  | ||||||
|         mNotificationManager.notify(SUMMARY_NOTIFICATION_ID, mSummaryNotification); |  | ||||||
|         mNotificationManager.notify(mStatusNotificationId++, mInstallStatusBuilder.build()); |  | ||||||
|     } |  | ||||||
|     @NonNull |  | ||||||
|     @Override |  | ||||||
|     public Result doWork() { |  | ||||||
|         String[] selectedFiles = getInputData().getStringArray("CIA_FILES"); |  | ||||||
|         assert selectedFiles != null; |  | ||||||
|         final CharSequence toastText = mContext.getResources().getQuantityString(R.plurals.cia_install_toast, |  | ||||||
|                 selectedFiles.length, selectedFiles.length); |  | ||||||
| 
 |  | ||||||
|         getApplicationContext().getMainExecutor().execute(() -> Toast.makeText(mContext, toastText, |  | ||||||
|                 Toast.LENGTH_LONG).show()); |  | ||||||
| 
 |  | ||||||
|         // Issue the initial notification with zero progress |  | ||||||
|         mInstallProgressBuilder.setOngoing(true); |  | ||||||
|         setProgressCallback(100, 0); |  | ||||||
| 
 |  | ||||||
|         int i = 0; |  | ||||||
|         for (String file : selectedFiles) { |  | ||||||
|             String filename = FileUtil.getFilename(Uri.parse(file)); |  | ||||||
|             mInstallProgressBuilder.setContentText(mContext.getString( |  | ||||||
|                     R.string.cia_install_notification_installing, filename, ++i, selectedFiles.length)); |  | ||||||
|             InstallStatus res = installCIA(file); |  | ||||||
|             notifyInstallStatus(filename, res); |  | ||||||
|         } |  | ||||||
|         mNotificationManager.cancel(PROGRESS_NOTIFICATION_ID); |  | ||||||
| 
 |  | ||||||
|         return Result.success(); |  | ||||||
|     } |  | ||||||
|     public void setProgressCallback(int max, int progress) { |  | ||||||
|         long currentTime = System.currentTimeMillis(); |  | ||||||
|         // Android applies a rate limit when updating a notification. |  | ||||||
|         // If you post updates to a single notification too frequently, |  | ||||||
|         // such as many in less than one second, the system might drop updates. |  | ||||||
|         // TODO: consider moving to C++ side |  | ||||||
|         if (currentTime - mLastNotifiedTime < 500 /* ms */){ |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         mLastNotifiedTime = currentTime; |  | ||||||
|         mInstallProgressBuilder.setProgress(max, progress, false); |  | ||||||
|         mNotificationManager.notify(PROGRESS_NOTIFICATION_ID, mInstallProgressBuilder.build()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @NonNull |  | ||||||
|     @Override |  | ||||||
|     public ForegroundInfo getForegroundInfo() { |  | ||||||
|         return new ForegroundInfo(PROGRESS_NOTIFICATION_ID, mInstallProgressBuilder.build()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private native InstallStatus installCIA(String path); |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,168 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.utils | ||||||
|  | 
 | ||||||
|  | import android.app.NotificationManager | ||||||
|  | import android.content.Context | ||||||
|  | import android.net.Uri | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.core.app.NotificationCompat | ||||||
|  | import androidx.work.ForegroundInfo | ||||||
|  | import androidx.work.Worker | ||||||
|  | import androidx.work.WorkerParameters | ||||||
|  | import org.citra.citra_emu.NativeLibrary.InstallStatus | ||||||
|  | import org.citra.citra_emu.R | ||||||
|  | import org.citra.citra_emu.utils.FileUtil.getFilename | ||||||
|  | 
 | ||||||
|  | class CiaInstallWorker( | ||||||
|  |     val context: Context, | ||||||
|  |     params: WorkerParameters | ||||||
|  | ) : Worker(context, params) { | ||||||
|  |     private val GROUP_KEY_CIA_INSTALL_STATUS = "org.citra.citra_emu.CIA_INSTALL_STATUS" | ||||||
|  |     private var lastNotifiedTime: Long = 0 | ||||||
|  |     private val SUMMARY_NOTIFICATION_ID = 0xC1A0000 | ||||||
|  |     private val PROGRESS_NOTIFICATION_ID = SUMMARY_NOTIFICATION_ID + 1 | ||||||
|  |     private var statusNotificationId = SUMMARY_NOTIFICATION_ID + 2 | ||||||
|  | 
 | ||||||
|  |     private val notificationManager = context.getSystemService(NotificationManager::class.java) | ||||||
|  |     private val installProgressBuilder = NotificationCompat.Builder( | ||||||
|  |         context, | ||||||
|  |         context.getString(R.string.cia_install_notification_channel_id) | ||||||
|  |     ) | ||||||
|  |         .setContentTitle(context.getString(R.string.install_cia_title)) | ||||||
|  |         .setSmallIcon(R.drawable.ic_stat_notification_logo) | ||||||
|  |     private val installStatusBuilder = NotificationCompat.Builder( | ||||||
|  |         context, | ||||||
|  |         context.getString(R.string.cia_install_notification_channel_id) | ||||||
|  |     ) | ||||||
|  |         .setContentTitle(context.getString(R.string.install_cia_title)) | ||||||
|  |         .setSmallIcon(R.drawable.ic_stat_notification_logo) | ||||||
|  |         .setGroup(GROUP_KEY_CIA_INSTALL_STATUS) | ||||||
|  |     private val summaryNotification = NotificationCompat.Builder( | ||||||
|  |         context, | ||||||
|  |         context.getString(R.string.cia_install_notification_channel_id) | ||||||
|  |     ) | ||||||
|  |         .setContentTitle(context.getString(R.string.install_cia_title)) | ||||||
|  |         .setSmallIcon(R.drawable.ic_stat_notification_logo) | ||||||
|  |         .setGroup(GROUP_KEY_CIA_INSTALL_STATUS) | ||||||
|  |         .setGroupSummary(true) | ||||||
|  |         .build() | ||||||
|  | 
 | ||||||
|  |     private fun notifyInstallStatus(filename: String, status: InstallStatus) { | ||||||
|  |         when (status) { | ||||||
|  |             InstallStatus.Success -> { | ||||||
|  |                 installStatusBuilder.setContentTitle( | ||||||
|  |                     context.getString(R.string.cia_install_notification_success_title) | ||||||
|  |                 ) | ||||||
|  |                 installStatusBuilder.setContentText( | ||||||
|  |                     context.getString(R.string.cia_install_success, filename) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             InstallStatus.ErrorAborted -> { | ||||||
|  |                 installStatusBuilder.setContentTitle( | ||||||
|  |                     context.getString(R.string.cia_install_notification_error_title) | ||||||
|  |                 ) | ||||||
|  |                 installStatusBuilder.setStyle( | ||||||
|  |                     NotificationCompat.BigTextStyle() | ||||||
|  |                         .bigText(context.getString(R.string.cia_install_error_aborted, filename)) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             InstallStatus.ErrorInvalid -> { | ||||||
|  |                 installStatusBuilder.setContentTitle( | ||||||
|  |                     context.getString(R.string.cia_install_notification_error_title) | ||||||
|  |                 ) | ||||||
|  |                 installStatusBuilder.setContentText( | ||||||
|  |                     context.getString(R.string.cia_install_error_invalid, filename) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             InstallStatus.ErrorEncrypted -> { | ||||||
|  |                 installStatusBuilder.setContentTitle( | ||||||
|  |                     context.getString(R.string.cia_install_notification_error_title) | ||||||
|  |                 ) | ||||||
|  |                 installStatusBuilder.setStyle( | ||||||
|  |                     NotificationCompat.BigTextStyle() | ||||||
|  |                         .bigText(context.getString(R.string.cia_install_error_encrypted, filename)) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             InstallStatus.ErrorFailedToOpenFile, InstallStatus.ErrorFileNotFound -> { | ||||||
|  |                 installStatusBuilder.setContentTitle( | ||||||
|  |                     context.getString(R.string.cia_install_notification_error_title) | ||||||
|  |                 ) | ||||||
|  |                 installStatusBuilder.setStyle( | ||||||
|  |                     NotificationCompat.BigTextStyle() | ||||||
|  |                         .bigText(context.getString(R.string.cia_install_error_unknown, filename)) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             else -> { | ||||||
|  |                 installStatusBuilder.setContentTitle( | ||||||
|  |                     context.getString(R.string.cia_install_notification_error_title) | ||||||
|  |                 ) | ||||||
|  |                 installStatusBuilder.setStyle( | ||||||
|  |                     NotificationCompat.BigTextStyle() | ||||||
|  |                         .bigText(context.getString(R.string.cia_install_error_unknown, filename)) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Even if newer versions of Android don't show the group summary text that you design, | ||||||
|  |         // you always need to manually set a summary to enable grouped notifications. | ||||||
|  |         notificationManager.notify(SUMMARY_NOTIFICATION_ID, summaryNotification) | ||||||
|  |         notificationManager.notify(statusNotificationId++, installStatusBuilder.build()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun doWork(): Result { | ||||||
|  |         val selectedFiles = inputData.getStringArray("CIA_FILES")!! | ||||||
|  |         val toastText: CharSequence = context.resources.getQuantityString( | ||||||
|  |             R.plurals.cia_install_toast, | ||||||
|  |             selectedFiles.size, selectedFiles.size | ||||||
|  |         ) | ||||||
|  |         context.mainExecutor.execute { | ||||||
|  |             Toast.makeText(context, toastText, Toast.LENGTH_LONG).show() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Issue the initial notification with zero progress | ||||||
|  |         installProgressBuilder.setOngoing(true) | ||||||
|  |         setProgressCallback(100, 0) | ||||||
|  |         selectedFiles.forEachIndexed { i, file -> | ||||||
|  |             val filename = getFilename(Uri.parse(file)) | ||||||
|  |             installProgressBuilder.setContentText( | ||||||
|  |                 context.getString( | ||||||
|  |                     R.string.cia_install_notification_installing, | ||||||
|  |                     filename, | ||||||
|  |                     i, | ||||||
|  |                     selectedFiles.size | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             val res = installCIA(file) | ||||||
|  |             notifyInstallStatus(filename, res) | ||||||
|  |         } | ||||||
|  |         notificationManager.cancel(PROGRESS_NOTIFICATION_ID) | ||||||
|  |         return Result.success() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setProgressCallback(max: Int, progress: Int) { | ||||||
|  |         val currentTime = System.currentTimeMillis() | ||||||
|  |         // Android applies a rate limit when updating a notification. | ||||||
|  |         // If you post updates to a single notification too frequently, | ||||||
|  |         // such as many in less than one second, the system might drop updates. | ||||||
|  |         // TODO: consider moving to C++ side | ||||||
|  |         if (currentTime - lastNotifiedTime < 500 /* ms */) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         lastNotifiedTime = currentTime | ||||||
|  |         installProgressBuilder.setProgress(max, progress, false) | ||||||
|  |         notificationManager.notify(PROGRESS_NOTIFICATION_ID, installProgressBuilder.build()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getForegroundInfo(): ForegroundInfo = | ||||||
|  |         ForegroundInfo(PROGRESS_NOTIFICATION_ID, installProgressBuilder.build()) | ||||||
|  | 
 | ||||||
|  |     private external fun installCIA(path: String): InstallStatus | ||||||
|  | } | ||||||
|  | @ -1,50 +0,0 @@ | ||||||
| package org.citra.citra_emu.utils; |  | ||||||
| 
 |  | ||||||
| import android.content.ClipData; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.net.Uri; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.documentfile.provider.DocumentFile; |  | ||||||
| 
 |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
| 
 |  | ||||||
| public final class FileBrowserHelper { |  | ||||||
| 
 |  | ||||||
|     @Nullable |  | ||||||
|     public static String[] getSelectedFiles(Intent result, Context context, List<String> extension) { |  | ||||||
|         ClipData clipData = result.getClipData(); |  | ||||||
|         List<DocumentFile> files = new ArrayList<>(); |  | ||||||
|         if (clipData == null) { |  | ||||||
|             files.add(DocumentFile.fromSingleUri(context, result.getData())); |  | ||||||
|         } else { |  | ||||||
|             for (int i = 0; i < clipData.getItemCount(); i++) { |  | ||||||
|                 ClipData.Item item = clipData.getItemAt(i); |  | ||||||
|                 Uri uri = item.getUri(); |  | ||||||
|                 files.add(DocumentFile.fromSingleUri(context, uri)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (!files.isEmpty()) { |  | ||||||
|             List<String> filePaths = new ArrayList<>(); |  | ||||||
|             for (int i = 0; i < files.size(); i++) { |  | ||||||
|                 DocumentFile file = files.get(i); |  | ||||||
|                 String filename = file.getName(); |  | ||||||
|                 int extensionStart = filename.lastIndexOf('.'); |  | ||||||
|                 if (extensionStart > 0) { |  | ||||||
|                     String fileExtension = filename.substring(extensionStart + 1); |  | ||||||
|                     if (extension.contains(fileExtension)) { |  | ||||||
|                         filePaths.add(file.getUri().toString()); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             if (filePaths.isEmpty()) { |  | ||||||
|                 return null; |  | ||||||
|             } |  | ||||||
|             return filePaths.toArray(new String[0]); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,44 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.utils | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import androidx.documentfile.provider.DocumentFile | ||||||
|  | 
 | ||||||
|  | object FileBrowserHelper { | ||||||
|  |     fun getSelectedFiles( | ||||||
|  |         result: Intent, | ||||||
|  |         context: Context, | ||||||
|  |         extension: List<String?> | ||||||
|  |     ): Array<String>? { | ||||||
|  |         val clipData = result.clipData | ||||||
|  |         val files: MutableList<DocumentFile?> = ArrayList() | ||||||
|  |         if (clipData == null) { | ||||||
|  |             files.add(DocumentFile.fromSingleUri(context, result.data!!)) | ||||||
|  |         } else { | ||||||
|  |             for (i in 0 until clipData.itemCount) { | ||||||
|  |                 val item = clipData.getItemAt(i) | ||||||
|  |                 files.add(DocumentFile.fromSingleUri(context, item.uri)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (files.isNotEmpty()) { | ||||||
|  |             val filePaths: MutableList<String> = ArrayList() | ||||||
|  |             for (i in files.indices) { | ||||||
|  |                 val file = files[i] | ||||||
|  |                 val filename = file?.name | ||||||
|  |                 val extensionStart = filename?.lastIndexOf('.') ?: 0 | ||||||
|  |                 if (extensionStart > 0) { | ||||||
|  |                     val fileExtension = filename?.substring(extensionStart + 1) | ||||||
|  |                     if (extension.contains(fileExtension)) { | ||||||
|  |                         filePaths.add(file?.uri.toString()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return if (filePaths.isEmpty()) null else filePaths.toTypedArray<String>() | ||||||
|  |         } | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,33 +0,0 @@ | ||||||
| package org.citra.citra_emu.utils; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.res.Resources; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| 
 |  | ||||||
| import androidx.core.graphics.Insets; |  | ||||||
| 
 |  | ||||||
| import com.google.android.material.appbar.AppBarLayout; |  | ||||||
| 
 |  | ||||||
| public class InsetsHelper { |  | ||||||
|     public static final int THREE_BUTTON_NAVIGATION = 0; |  | ||||||
|     public static final int TWO_BUTTON_NAVIGATION = 1; |  | ||||||
|     public static final int GESTURE_NAVIGATION = 2; |  | ||||||
| 
 |  | ||||||
|     public static void insetAppBar(Insets insets, AppBarLayout appBarLayout) |  | ||||||
|     { |  | ||||||
|         ViewGroup.MarginLayoutParams mlpAppBar = |  | ||||||
|                 (ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams(); |  | ||||||
|         mlpAppBar.leftMargin = insets.left; |  | ||||||
|         mlpAppBar.rightMargin = insets.right; |  | ||||||
|         appBarLayout.setLayoutParams(mlpAppBar); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static int getSystemGestureType(Context context) { |  | ||||||
|         Resources resources = context.getResources(); |  | ||||||
|         int resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android"); |  | ||||||
|         if (resourceId != 0) { |  | ||||||
|             return resources.getInteger(resourceId); |  | ||||||
|         } |  | ||||||
|         return 0; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.utils | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.content.Context | ||||||
|  | 
 | ||||||
|  | object InsetsHelper { | ||||||
|  |     const val THREE_BUTTON_NAVIGATION = 0 | ||||||
|  |     const val TWO_BUTTON_NAVIGATION = 1 | ||||||
|  |     const val GESTURE_NAVIGATION = 2 | ||||||
|  | 
 | ||||||
|  |     @SuppressLint("DiscouragedApi") | ||||||
|  |     fun getSystemGestureType(context: Context): Int { | ||||||
|  |         val resources = context.resources | ||||||
|  |         val resourceId = resources.getIdentifier( | ||||||
|  |             "config_navBarInteractionMode", | ||||||
|  |             "integer", | ||||||
|  |             "android" | ||||||
|  |         ) | ||||||
|  |         return if (resourceId != 0) resources.getInteger(resourceId) else 0 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,42 +0,0 @@ | ||||||
| package org.citra.citra_emu.utils; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.BuildConfig; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Contains methods that call through to {@link android.util.Log}, but |  | ||||||
|  * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log |  | ||||||
|  * levels in release builds. |  | ||||||
|  */ |  | ||||||
| public final class Log { |  | ||||||
|     // Tracks whether we should share the old log or the current log |  | ||||||
|     public static boolean gameLaunched = false; |  | ||||||
| 
 |  | ||||||
|     private static final String TAG = "Citra Frontend"; |  | ||||||
| 
 |  | ||||||
|     private Log() { |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void verbose(String message) { |  | ||||||
|         if (BuildConfig.DEBUG) { |  | ||||||
|             android.util.Log.v(TAG, message); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void debug(String message) { |  | ||||||
|         if (BuildConfig.DEBUG) { |  | ||||||
|             android.util.Log.d(TAG, message); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void info(String message) { |  | ||||||
|         android.util.Log.i(TAG, message); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void warning(String message) { |  | ||||||
|         android.util.Log.w(TAG, message); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void error(String message) { |  | ||||||
|         android.util.Log.e(TAG, message); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | // Copyright 2023 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.utils | ||||||
|  | 
 | ||||||
|  | import android.util.Log | ||||||
|  | import org.citra.citra_emu.BuildConfig | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Contains methods that call through to [android.util.Log], but | ||||||
|  |  * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log | ||||||
|  |  * levels in release builds. | ||||||
|  |  */ | ||||||
|  | object Log { | ||||||
|  |     // Tracks whether we should share the old log or the current log | ||||||
|  |     var gameLaunched = false | ||||||
|  |     private const val TAG = "Citra Frontend" | ||||||
|  | 
 | ||||||
|  |     fun verbose(message: String?) { | ||||||
|  |         if (BuildConfig.DEBUG) { | ||||||
|  |             Log.v(TAG, message!!) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun debug(message: String?) { | ||||||
|  |         if (BuildConfig.DEBUG) { | ||||||
|  |             Log.d(TAG, message!!) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun info(message: String?) = Log.i(TAG, message!!) | ||||||
|  | 
 | ||||||
|  |     fun warning(message: String?) = Log.w(TAG, message!!) | ||||||
|  | 
 | ||||||
|  |     fun error(message: String?) = Log.e(TAG, message!!) | ||||||
|  | } | ||||||
|  | @ -1,27 +0,0 @@ | ||||||
| package org.citra.citra_emu.utils; |  | ||||||
| 
 |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.net.Uri; |  | ||||||
| 
 |  | ||||||
| import com.squareup.picasso.Picasso; |  | ||||||
| 
 |  | ||||||
| import java.io.IOException; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| 
 |  | ||||||
| public class PicassoUtils { |  | ||||||
|     // Blocking call. Load image from file and crop/resize it to fit in width x height. |  | ||||||
|     @Nullable |  | ||||||
|     public static Bitmap LoadBitmapFromFile(String uri, int width, int height) { |  | ||||||
|         try { |  | ||||||
|             return Picasso.get() |  | ||||||
|                     .load(Uri.parse(uri)) |  | ||||||
|                     .config(Bitmap.Config.ARGB_8888) |  | ||||||
|                     .centerCrop() |  | ||||||
|                     .resize(width, height) |  | ||||||
|                     .get(); |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,46 +0,0 @@ | ||||||
| package org.citra.citra_emu.viewholders; |  | ||||||
| 
 |  | ||||||
| import android.view.View; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.TextView; |  | ||||||
| 
 |  | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| 
 |  | ||||||
| import org.citra.citra_emu.R; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * A simple class that stores references to views so that the GameAdapter doesn't need to |  | ||||||
|  * keep calling findViewById(), which is expensive. |  | ||||||
|  */ |  | ||||||
| public class GameViewHolder extends RecyclerView.ViewHolder { |  | ||||||
|     private View itemView; |  | ||||||
|     public ImageView imageIcon; |  | ||||||
|     public TextView textGameTitle; |  | ||||||
|     public TextView textCompany; |  | ||||||
|     public TextView textFileName; |  | ||||||
| 
 |  | ||||||
|     public String gameId; |  | ||||||
| 
 |  | ||||||
|     // TODO Not need any of this stuff. Currently only the properties dialog needs it. |  | ||||||
|     public String path; |  | ||||||
|     public String title; |  | ||||||
|     public String description; |  | ||||||
|     public String regions; |  | ||||||
|     public String company; |  | ||||||
| 
 |  | ||||||
|     public GameViewHolder(View itemView) { |  | ||||||
|         super(itemView); |  | ||||||
| 
 |  | ||||||
|         this.itemView = itemView; |  | ||||||
|         itemView.setTag(this); |  | ||||||
| 
 |  | ||||||
|         imageIcon = itemView.findViewById(R.id.image_game_screen); |  | ||||||
|         textGameTitle = itemView.findViewById(R.id.text_game_title); |  | ||||||
|         textCompany = itemView.findViewById(R.id.text_company); |  | ||||||
|         textFileName = itemView.findViewById(R.id.text_filename); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public View getItemView() { |  | ||||||
|         return itemView; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -23,14 +23,13 @@ void AndroidMiiSelector::Setup(const Frontend::MiiSelectorConfig& config) { | ||||||
|     // Create the Java MiiSelectorConfig object
 |     // Create the Java MiiSelectorConfig object
 | ||||||
|     jobject java_config = env->AllocObject(s_mii_selector_config_class); |     jobject java_config = env->AllocObject(s_mii_selector_config_class); | ||||||
|     env->SetBooleanField(java_config, |     env->SetBooleanField(java_config, | ||||||
|                          env->GetFieldID(s_mii_selector_config_class, "enable_cancel_button", "Z"), |                          env->GetFieldID(s_mii_selector_config_class, "enableCancelButton", "Z"), | ||||||
|                          static_cast<jboolean>(config.enable_cancel_button)); |                          static_cast<jboolean>(config.enable_cancel_button)); | ||||||
|     env->SetObjectField(java_config, |     env->SetObjectField(java_config, | ||||||
|                         env->GetFieldID(s_mii_selector_config_class, "title", "Ljava/lang/String;"), |                         env->GetFieldID(s_mii_selector_config_class, "title", "Ljava/lang/String;"), | ||||||
|                         ToJString(env, config.title)); |                         ToJString(env, config.title)); | ||||||
|     env->SetLongField( |     env->SetLongField( | ||||||
|         java_config, |         java_config, env->GetFieldID(s_mii_selector_config_class, "initiallySelectedMiiIndex", "J"), | ||||||
|         env->GetFieldID(s_mii_selector_config_class, "initially_selected_mii_index", "J"), |  | ||||||
|         static_cast<jlong>(config.initially_selected_mii_index)); |         static_cast<jlong>(config.initially_selected_mii_index)); | ||||||
| 
 | 
 | ||||||
|     // List mii names
 |     // List mii names
 | ||||||
|  | @ -44,14 +43,14 @@ void AndroidMiiSelector::Setup(const Frontend::MiiSelectorConfig& config) { | ||||||
|     } |     } | ||||||
|     env->SetObjectField( |     env->SetObjectField( | ||||||
|         java_config, |         java_config, | ||||||
|         env->GetFieldID(s_mii_selector_config_class, "mii_names", "[Ljava/lang/String;"), array); |         env->GetFieldID(s_mii_selector_config_class, "miiNames", "[Ljava/lang/String;"), array); | ||||||
| 
 | 
 | ||||||
|     // Invoke backend Execute method
 |     // Invoke backend Execute method
 | ||||||
|     jobject data = |     jobject data = | ||||||
|         env->CallStaticObjectMethod(s_mii_selector_class, s_mii_selector_execute, java_config); |         env->CallStaticObjectMethod(s_mii_selector_class, s_mii_selector_execute, java_config); | ||||||
| 
 | 
 | ||||||
|     const u32 return_code = static_cast<u32>( |     const u32 return_code = static_cast<u32>( | ||||||
|         env->GetLongField(data, env->GetFieldID(s_mii_selector_data_class, "return_code", "J"))); |         env->GetLongField(data, env->GetFieldID(s_mii_selector_data_class, "returnCode", "J"))); | ||||||
|     if (return_code == 1) { |     if (return_code == 1) { | ||||||
|         Finalize(return_code, Mii::MiiData{}); |         Finalize(return_code, Mii::MiiData{}); | ||||||
|         return; |         return; | ||||||
|  |  | ||||||
|  | @ -23,14 +23,14 @@ namespace SoftwareKeyboard { | ||||||
| static jobject ToJavaKeyboardConfig(const Frontend::KeyboardConfig& config) { | static jobject ToJavaKeyboardConfig(const Frontend::KeyboardConfig& config) { | ||||||
|     JNIEnv* env = IDCache::GetEnvForThread(); |     JNIEnv* env = IDCache::GetEnvForThread(); | ||||||
|     jobject object = env->AllocObject(s_keyboard_config_class); |     jobject object = env->AllocObject(s_keyboard_config_class); | ||||||
|     env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "button_config", "I"), |     env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "buttonConfig", "I"), | ||||||
|                      static_cast<jint>(config.button_config)); |                      static_cast<jint>(config.button_config)); | ||||||
|     env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"), |     env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "maxTextLength", "I"), | ||||||
|                      static_cast<jint>(config.max_text_length)); |                      static_cast<jint>(config.max_text_length)); | ||||||
|     env->SetBooleanField(object, env->GetFieldID(s_keyboard_config_class, "multiline_mode", "Z"), |     env->SetBooleanField(object, env->GetFieldID(s_keyboard_config_class, "multilineMode", "Z"), | ||||||
|                          static_cast<jboolean>(config.multiline_mode)); |                          static_cast<jboolean>(config.multiline_mode)); | ||||||
|     env->SetObjectField(object, |     env->SetObjectField(object, | ||||||
|                         env->GetFieldID(s_keyboard_config_class, "hint_text", "Ljava/lang/String;"), |                         env->GetFieldID(s_keyboard_config_class, "hintText", "Ljava/lang/String;"), | ||||||
|                         ToJString(env, config.hint_text)); |                         ToJString(env, config.hint_text)); | ||||||
| 
 | 
 | ||||||
|     const jclass string_class = reinterpret_cast<jclass>(env->FindClass("java/lang/String")); |     const jclass string_class = reinterpret_cast<jclass>(env->FindClass("java/lang/String")); | ||||||
|  | @ -42,7 +42,7 @@ static jobject ToJavaKeyboardConfig(const Frontend::KeyboardConfig& config) { | ||||||
|                                    ToJString(env, config.button_text[i])); |                                    ToJString(env, config.button_text[i])); | ||||||
|     } |     } | ||||||
|     env->SetObjectField( |     env->SetObjectField( | ||||||
|         object, env->GetFieldID(s_keyboard_config_class, "button_text", "[Ljava/lang/String;"), |         object, env->GetFieldID(s_keyboard_config_class, "buttonText", "[Ljava/lang/String;"), | ||||||
|         array); |         array); | ||||||
| 
 | 
 | ||||||
|     return object; |     return object; | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								src/android/app/src/main/res/drawable/button_home.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/android/app/src/main/res/drawable/button_home.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="100dp" | ||||||
|  |     android:height="100dp" | ||||||
|  |     android:viewportWidth="99.27" | ||||||
|  |     android:viewportHeight="99.27"> | ||||||
|  |     <path | ||||||
|  |         android:fillAlpha="0.5" | ||||||
|  |         android:fillColor="#eaeaea" | ||||||
|  |         android:pathData="M49.64,49.64m-49.64,0a49.64,49.64 0,1 1,99.28 0a49.64,49.64 0,1 1,-99.28 0" | ||||||
|  |         android:strokeAlpha="0.5" /> | ||||||
|  |     <path | ||||||
|  |         android:fillAlpha="0.75" | ||||||
|  |         android:fillColor="#FF000000" | ||||||
|  |         android:pathData="m75.99,45.27l-25.31,-23.18c-0.58,-0.56 -1.5,-0.56 -2.08,0l-25.31,23.18c-0.95,0.94 -0.3,2.56 1.04,2.56h4.3c0.53,0 0.96,0.43 0.96,0.96v21.33c0,0.82 0.67,1.49 1.49,1.49h37.14c0.82,0 1.49,-0.67 1.49,-1.49v-21.33c0,-0.53 0.43,-0.96 0.96,-0.96h4.3c1.34,0 1.99,-1.62 1.04,-2.56ZM57.81,60.01c0,0.66 -0.53,1.19 -1.19,1.19h-13.96c-0.66,0 -1.19,-0.53 -1.19,-1.19v-10.99c0,-0.66 0.53,-1.19 1.19,-1.19h13.96c0.66,0 1.19,0.53 1.19,1.19v10.99Z" | ||||||
|  |         android:strokeAlpha="0.75" /> | ||||||
|  | </vector> | ||||||
|  | @ -0,0 +1,16 @@ | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="100dp" | ||||||
|  |     android:height="100dp" | ||||||
|  |     android:viewportWidth="99.27" | ||||||
|  |     android:viewportHeight="99.27"> | ||||||
|  |     <path | ||||||
|  |         android:fillAlpha="0.5" | ||||||
|  |         android:fillColor="#151515" | ||||||
|  |         android:pathData="M49.64,49.64m-49.64,0a49.64,49.64 0,1 1,99.28 0a49.64,49.64 0,1 1,-99.28 0" | ||||||
|  |         android:strokeAlpha="0.5" /> | ||||||
|  |     <path | ||||||
|  |         android:fillAlpha="0.75" | ||||||
|  |         android:fillColor="#fff" | ||||||
|  |         android:pathData="m75.99,45.27l-25.31,-23.18c-0.58,-0.56 -1.5,-0.56 -2.08,0l-25.31,23.18c-0.95,0.94 -0.3,2.56 1.04,2.56h4.3c0.53,0 0.96,0.43 0.96,0.96v21.33c0,0.82 0.67,1.49 1.49,1.49h37.14c0.82,0 1.49,-0.67 1.49,-1.49v-21.33c0,-0.53 0.43,-0.96 0.96,-0.96h4.3c1.34,0 1.99,-1.62 1.04,-2.56ZM57.81,60.01c0,0.66 -0.53,1.19 -1.19,1.19h-13.96c-0.66,0 -1.19,-0.53 -1.19,-1.19v-10.99c0,-0.66 0.53,-1.19 1.19,-1.19h13.96c0.66,0 1.19,0.53 1.19,1.19v10.99Z" | ||||||
|  |         android:strokeAlpha="0.75" /> | ||||||
|  | </vector> | ||||||
|  | @ -1,37 +1,41 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <androidx.constraintlayout.widget.ConstraintLayout | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|     android:id="@+id/root" |     android:id="@+id/cheat_container" | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="wrap_content" |     android:layout_height="wrap_content" | ||||||
|  |     android:background="?attr/selectableItemBackground" | ||||||
|  |     android:paddingVertical="16dp" | ||||||
|  |     android:paddingHorizontal="20dp" | ||||||
|     android:focusable="true" |     android:focusable="true" | ||||||
|     android:nextFocusLeft="@id/checkbox"> |     android:nextFocusLeft="@id/cheat_switch"> | ||||||
| 
 | 
 | ||||||
|     <TextView |     <TextView | ||||||
|         android:id="@+id/text_name" |         android:id="@+id/text_name" | ||||||
|  |         style="@style/TextAppearance.AppCompat.Headline" | ||||||
|         android:layout_width="0dp" |         android:layout_width="0dp" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:textSize="16sp" |  | ||||||
|         android:layout_margin="@dimen/spacing_large" |         android:layout_margin="@dimen/spacing_large" | ||||||
|         style="@style/TextAppearance.AppCompat.Headline" |         android:textSize="16sp" | ||||||
|         app:layout_constraintStart_toStartOf="parent" |  | ||||||
|         app:layout_constraintEnd_toStartOf="@id/checkbox" |  | ||||||
|         app:layout_constraintTop_toTopOf="parent" |  | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/cheat_switch" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|         tools:text="Max Lives after losing 1" /> |         tools:text="Max Lives after losing 1" /> | ||||||
| 
 | 
 | ||||||
|     <CheckBox |     <com.google.android.material.materialswitch.MaterialSwitch | ||||||
|         android:id="@+id/checkbox" |         android:id="@+id/cheat_switch" | ||||||
|         android:layout_width="48dp" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="64dp" |         android:layout_height="wrap_content" | ||||||
|         android:focusable="true" |         android:focusable="true" | ||||||
|         android:gravity="center" |         android:gravity="center" | ||||||
|         android:nextFocusRight="@id/root" |         android:nextFocusRight="@id/cheat_container" | ||||||
|  |         android:paddingEnd="8dp" | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|         app:layout_constraintStart_toEndOf="@id/text_name" |         app:layout_constraintStart_toEndOf="@id/text_name" | ||||||
|         app:layout_constraintTop_toTopOf="parent" /> |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         tools:ignore="RtlSymmetry" /> | ||||||
| 
 | 
 | ||||||
| </androidx.constraintlayout.widget.ConstraintLayout> | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
|  |  | ||||||
|  | @ -1,60 +1,26 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <androidx.constraintlayout.widget.ConstraintLayout | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  | 
 | ||||||
|  |     <androidx.fragment.app.FragmentContainerView | ||||||
|  |         android:id="@+id/fragment_container" | ||||||
|  |         android:name="androidx.navigation.fragment.NavHostFragment" | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="match_parent" |         android:layout_height="match_parent" | ||||||
|     android:background="?attr/colorSurface"> |         android:keepScreenOn="true" | ||||||
|  |         app:defaultNavHost="true" /> | ||||||
| 
 | 
 | ||||||
|     <androidx.coordinatorlayout.widget.CoordinatorLayout |     <View | ||||||
|         android:id="@+id/coordinator_cheats" |         android:id="@+id/navigation_bar_shade" | ||||||
|         android:layout_width="match_parent" |         android:layout_width="0dp" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="1px" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         android:background="@android:color/transparent" | ||||||
|         app:layout_constraintStart_toStartOf="parent" |         android:clickable="false" | ||||||
|         app:layout_constraintTop_toTopOf="parent"> |         android:focusable="false" | ||||||
| 
 |  | ||||||
|         <com.google.android.material.appbar.AppBarLayout |  | ||||||
|             android:id="@+id/appbar_cheats" |  | ||||||
|             android:layout_width="match_parent" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:fitsSystemWindows="true"> |  | ||||||
| 
 |  | ||||||
|             <com.google.android.material.appbar.MaterialToolbar |  | ||||||
|                 android:id="@+id/toolbar_cheats" |  | ||||||
|                 android:layout_width="match_parent" |  | ||||||
|                 android:layout_height="?attr/actionBarSize" /> |  | ||||||
| 
 |  | ||||||
|         </com.google.android.material.appbar.AppBarLayout> |  | ||||||
| 
 |  | ||||||
|     </androidx.coordinatorlayout.widget.CoordinatorLayout> |  | ||||||
| 
 |  | ||||||
|     <androidx.slidingpanelayout.widget.SlidingPaneLayout |  | ||||||
|         android:id="@+id/sliding_pane_layout" |  | ||||||
|         android:layout_width="match_parent" |  | ||||||
|         android:layout_height="0dp" |  | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|         app:layout_constraintStart_toStartOf="parent" |         app:layout_constraintStart_toStartOf="parent" /> | ||||||
|         app:layout_constraintTop_toBottomOf="@id/coordinator_cheats"> |  | ||||||
| 
 |  | ||||||
|         <androidx.fragment.app.FragmentContainerView |  | ||||||
|             android:layout_width="match_parent" |  | ||||||
|             android:layout_height="match_parent" |  | ||||||
|             android:layout_weight="1" |  | ||||||
|             android:id="@+id/cheat_list_container" |  | ||||||
|             android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment" |  | ||||||
|             tools:layout="@layout/fragment_cheat_list" /> |  | ||||||
| 
 |  | ||||||
|         <androidx.fragment.app.FragmentContainerView |  | ||||||
|             android:layout_width="match_parent" |  | ||||||
|             android:layout_height="match_parent" |  | ||||||
|             android:layout_weight="1" |  | ||||||
|             android:id="@+id/cheat_details_container" |  | ||||||
|             android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment" |  | ||||||
|             tools:layout="@layout/fragment_cheat_details" /> |  | ||||||
| 
 |  | ||||||
|     </androidx.slidingpanelayout.widget.SlidingPaneLayout> |  | ||||||
| 
 | 
 | ||||||
| </androidx.constraintlayout.widget.ConstraintLayout> | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
|  |  | ||||||
|  | @ -6,8 +6,8 @@ | ||||||
|     android:id="@+id/option_card" |     android:id="@+id/option_card" | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="wrap_content" |     android:layout_height="wrap_content" | ||||||
|     android:layout_marginVertical="12dp" |     android:layout_marginBottom="24dp" | ||||||
|     android:layout_marginHorizontal="16dp" |     android:layout_marginHorizontal="12dp" | ||||||
|     android:background="?attr/selectableItemBackground" |     android:background="?attr/selectableItemBackground" | ||||||
|     android:backgroundTint="?attr/colorSurfaceVariant" |     android:backgroundTint="?attr/colorSurfaceVariant" | ||||||
|     android:clickable="true" |     android:clickable="true" | ||||||
|  | @ -16,7 +16,8 @@ | ||||||
|     <LinearLayout |     <LinearLayout | ||||||
|         android:id="@+id/option_layout" |         android:id="@+id/option_layout" | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="wrap_content"> |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_gravity="center_vertical"> | ||||||
| 
 | 
 | ||||||
|         <ImageView |         <ImageView | ||||||
|             android:id="@+id/option_icon" |             android:id="@+id/option_icon" | ||||||
|  | @ -44,7 +45,7 @@ | ||||||
|                 tools:text="@string/about" /> |                 tools:text="@string/about" /> | ||||||
| 
 | 
 | ||||||
|             <com.google.android.material.textview.MaterialTextView |             <com.google.android.material.textview.MaterialTextView | ||||||
|                 style="@style/TextAppearance.Material3.LabelMedium" |                 style="@style/TextAppearance.Material3.BodySmall" | ||||||
|                 android:id="@+id/option_description" |                 android:id="@+id/option_description" | ||||||
|                 android:layout_width="match_parent" |                 android:layout_width="match_parent" | ||||||
|                 android:layout_height="wrap_content" |                 android:layout_height="wrap_content" | ||||||
|  | @ -67,7 +68,8 @@ | ||||||
|                 android:requiresFadingEdge="horizontal" |                 android:requiresFadingEdge="horizontal" | ||||||
|                 android:layout_marginTop="5dp" |                 android:layout_marginTop="5dp" | ||||||
|                 android:visibility="gone" |                 android:visibility="gone" | ||||||
|                 tools:text="@string/about_description" /> |                 tools:visibility="visible" | ||||||
|  |                 tools:text="/tree/primary:Games" /> | ||||||
| 
 | 
 | ||||||
|         </LinearLayout> |         </LinearLayout> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -38,8 +38,8 @@ | ||||||
| 
 | 
 | ||||||
|             <ImageView |             <ImageView | ||||||
|                 android:id="@+id/image_logo" |                 android:id="@+id/image_logo" | ||||||
|                 android:layout_width="175dp" |                 android:layout_width="104dp" | ||||||
|                 android:layout_height="175dp" |                 android:layout_height="104dp" | ||||||
|                 android:layout_marginTop="20dp" |                 android:layout_marginTop="20dp" | ||||||
|                 android:layout_gravity="center_horizontal" |                 android:layout_gravity="center_horizontal" | ||||||
|                 android:src="@drawable/ic_citra_full" /> |                 android:src="@drawable/ic_citra_full" /> | ||||||
|  |  | ||||||
|  | @ -1,163 +1,177 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <androidx.constraintlayout.widget.ConstraintLayout | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|     android:id="@+id/root" |  | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="match_parent"> |     android:layout_height="match_parent"> | ||||||
| 
 | 
 | ||||||
|     <ScrollView |     <androidx.coordinatorlayout.widget.CoordinatorLayout | ||||||
|         android:id="@+id/scroll_view" |  | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="0dp" |         android:layout_height="0dp" | ||||||
|         app:layout_constraintStart_toStartOf="parent" |         app:layout_constraintBottom_toTopOf="@id/button_layout" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|         app:layout_constraintTop_toTopOf="parent" |         app:layout_constraintStart_toStartOf="parent" | ||||||
|         app:layout_constraintBottom_toTopOf="@id/barrier"> |         app:layout_constraintTop_toTopOf="parent"> | ||||||
| 
 | 
 | ||||||
|         <androidx.constraintlayout.widget.ConstraintLayout |         <com.google.android.material.appbar.AppBarLayout | ||||||
|             android:layout_width="match_parent" |             android:id="@+id/appbar_cheat_details" | ||||||
|             android:layout_height="wrap_content"> |  | ||||||
| 
 |  | ||||||
|             <TextView |  | ||||||
|                 android:id="@+id/label_name" |  | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|             android:layout_height="wrap_content" |             android:layout_height="wrap_content" | ||||||
|                 style="@style/TextAppearance.MaterialComponents.Headline5" |             android:fitsSystemWindows="true"> | ||||||
|                 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 |             <com.google.android.material.appbar.MaterialToolbar | ||||||
|  |                 android:id="@+id/toolbar_cheat_details" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="?attr/actionBarSize" | ||||||
|  |                 app:navigationIcon="@drawable/ic_back" | ||||||
|  |                 app:title="@string/cheats" /> | ||||||
|  | 
 | ||||||
|  |         </com.google.android.material.appbar.AppBarLayout> | ||||||
|  | 
 | ||||||
|  |         <androidx.core.widget.NestedScrollView | ||||||
|  |             android:id="@+id/scroll_view" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="match_parent" | ||||||
|  |             app:layout_behavior="@string/appbar_scrolling_view_behavior"> | ||||||
|  | 
 | ||||||
|  |             <LinearLayout | ||||||
|  |                 android:id="@+id/input_layout" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:orientation="vertical"> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.textfield.TextInputLayout | ||||||
|                     android:id="@+id/edit_name" |                     android:id="@+id/edit_name" | ||||||
|                     android:layout_width="match_parent" |                     android:layout_width="match_parent" | ||||||
|                     android:layout_height="wrap_content" |                     android:layout_height="wrap_content" | ||||||
|                 android:minHeight="48dp" |  | ||||||
|                     android:layout_marginHorizontal="@dimen/spacing_large" |                     android:layout_marginHorizontal="@dimen/spacing_large" | ||||||
|                 android:importantForAutofill="no" |                     android:layout_marginVertical="@dimen/spacing_small" | ||||||
|                 android:inputType="text" |                     android:hint="@string/cheats_name" | ||||||
|                 app:layout_constraintStart_toStartOf="parent" |                     android:paddingTop="@dimen/spacing_medlarge" | ||||||
|                 app:layout_constraintEnd_toEndOf="parent" |                     app:errorEnabled="true"> | ||||||
|                 app:layout_constraintTop_toBottomOf="@id/label_name" |  | ||||||
|                 app:layout_constraintBottom_toTopOf="@id/label_notes" |  | ||||||
|                 tools:text="Max Lives after losing 1" /> |  | ||||||
| 
 | 
 | ||||||
|             <TextView |                     <com.google.android.material.textfield.TextInputEditText | ||||||
|                 android:id="@+id/label_notes" |                         android:id="@+id/edit_name_input" | ||||||
|                         android:layout_width="match_parent" |                         android:layout_width="match_parent" | ||||||
|                         android:layout_height="wrap_content" |                         android:layout_height="wrap_content" | ||||||
|                 style="@style/TextAppearance.MaterialComponents.Headline5" |                         android:importantForAutofill="no" | ||||||
|                 android:textSize="18sp" |                         android:inputType="text" | ||||||
|                 android:text="@string/cheats_notes" |                         android:minHeight="48dp" | ||||||
|                 android:layout_margin="@dimen/spacing_large" |                         android:textAlignment="viewStart" | ||||||
|                 android:labelFor="@id/edit_notes" |                         android:nextFocusDown="@id/edit_notes_input" | ||||||
|                 app:layout_constraintStart_toStartOf="parent" |                         tools:text="Hyrule Field Speed Hack" /> | ||||||
|                 app:layout_constraintEnd_toEndOf="parent" |  | ||||||
|                 app:layout_constraintTop_toBottomOf="@id/edit_name" |  | ||||||
|                 app:layout_constraintBottom_toTopOf="@id/edit_notes" /> |  | ||||||
| 
 | 
 | ||||||
|             <EditText |                 </com.google.android.material.textfield.TextInputLayout> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.textfield.TextInputLayout | ||||||
|                     android:id="@+id/edit_notes" |                     android:id="@+id/edit_notes" | ||||||
|                     android:layout_width="match_parent" |                     android:layout_width="match_parent" | ||||||
|                     android:layout_height="wrap_content" |                     android:layout_height="wrap_content" | ||||||
|                 android:minHeight="48dp" |  | ||||||
|                     android:layout_marginHorizontal="@dimen/spacing_large" |                     android:layout_marginHorizontal="@dimen/spacing_large" | ||||||
|                 android:importantForAutofill="no" |                     android:layout_marginBottom="24dp" | ||||||
|                 android:inputType="textMultiLine" |                     android:hint="@string/cheats_notes"> | ||||||
|                 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 |                     <com.google.android.material.textfield.TextInputEditText | ||||||
|                 android:id="@+id/label_code" |                         android:id="@+id/edit_notes_input" | ||||||
|                         android:layout_width="match_parent" |                         android:layout_width="match_parent" | ||||||
|                         android:layout_height="wrap_content" |                         android:layout_height="wrap_content" | ||||||
|                 style="@style/TextAppearance.MaterialComponents.Headline5" |                         android:importantForAutofill="no" | ||||||
|                 android:textSize="18sp" |                         android:inputType="textMultiLine" | ||||||
|                 android:text="@string/cheats_code" |                         android:minHeight="48dp" | ||||||
|                 android:layout_margin="@dimen/spacing_large" |                         android:textAlignment="viewStart" | ||||||
|                 android:labelFor="@id/edit_code" |                         android:nextFocusDown="@id/edit_code_input" /> | ||||||
|                 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 |                 </com.google.android.material.textfield.TextInputLayout> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.textfield.TextInputLayout | ||||||
|                     android:id="@+id/edit_code" |                     android:id="@+id/edit_code" | ||||||
|                     android:layout_width="match_parent" |                     android:layout_width="match_parent" | ||||||
|                     android:layout_height="wrap_content" |                     android:layout_height="wrap_content" | ||||||
|                 android:minHeight="108sp" |  | ||||||
|                     android:layout_marginHorizontal="@dimen/spacing_large" |                     android:layout_marginHorizontal="@dimen/spacing_large" | ||||||
|  |                     android:layout_marginVertical="@dimen/spacing_small" | ||||||
|  |                     android:hint="@string/cheats_code" | ||||||
|  |                     app:errorEnabled="true"> | ||||||
|  | 
 | ||||||
|  |                     <com.google.android.material.textfield.TextInputEditText | ||||||
|  |                         android:id="@+id/edit_code_input" | ||||||
|  |                         android:layout_width="match_parent" | ||||||
|  |                         android:layout_height="wrap_content" | ||||||
|  |                         android:gravity="start" | ||||||
|                         android:importantForAutofill="no" |                         android:importantForAutofill="no" | ||||||
|                         android:inputType="textMultiLine" |                         android:inputType="textMultiLine" | ||||||
|  |                         android:minHeight="108sp" | ||||||
|  |                         android:textAlignment="viewStart" | ||||||
|                         android:typeface="monospace" |                         android:typeface="monospace" | ||||||
|                 android:gravity="start" |                         android:nextFocusDown="@id/button_cancel" | ||||||
|                 app:layout_constraintStart_toStartOf="parent" |                         tools:text="0x8003d63c:dword:0x60000000\n0x8003d658:dword:0x60000000" /> | ||||||
|                 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> |                 </com.google.android.material.textfield.TextInputLayout> | ||||||
| 
 | 
 | ||||||
|     </ScrollView> |             </LinearLayout> | ||||||
| 
 | 
 | ||||||
|     <androidx.constraintlayout.widget.Barrier |         </androidx.core.widget.NestedScrollView> | ||||||
|         android:id="@+id/barrier" | 
 | ||||||
|         android:layout_width="wrap_content" |     </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||||
|  | 
 | ||||||
|  |     <androidx.constraintlayout.widget.ConstraintLayout | ||||||
|  |         android:id="@+id/button_layout" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         app:barrierDirection="top" |         android:background="@android:color/transparent" | ||||||
|         app:constraint_referenced_ids="button_delete,button_edit,button_cancel,button_ok" /> |         app:layout_constraintBottom_toBottomOf="parent"> | ||||||
|  | 
 | ||||||
|  |         <com.google.android.material.divider.MaterialDivider | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="0dp" | ||||||
|  |             app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  | 
 | ||||||
|  |         <LinearLayout | ||||||
|  |             android:id="@+id/button_container" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:orientation="horizontal" | ||||||
|  |             app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |             app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |             app:layout_constraintStart_toStartOf="parent"> | ||||||
| 
 | 
 | ||||||
|             <Button |             <Button | ||||||
|                 android:id="@+id/button_delete" |                 android:id="@+id/button_delete" | ||||||
|                 android:layout_width="0dp" |                 android:layout_width="0dp" | ||||||
|                 android:layout_height="wrap_content" |                 android:layout_height="wrap_content" | ||||||
|                 android:layout_margin="@dimen/spacing_large" |                 android:layout_margin="@dimen/spacing_large" | ||||||
|         android:text="@string/cheats_delete" |                 android:layout_weight="1" | ||||||
|         app:layout_constraintStart_toStartOf="parent" |                 android:nextFocusUp="@id/appbar_cheat_details" | ||||||
|         app:layout_constraintEnd_toStartOf="@id/button_edit" |                 android:text="@string/cheats_delete" /> | ||||||
|         app:layout_constraintTop_toBottomOf="@id/barrier" |  | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" /> |  | ||||||
| 
 | 
 | ||||||
|             <Button |             <Button | ||||||
|                 android:id="@+id/button_edit" |                 android:id="@+id/button_edit" | ||||||
|                 android:layout_width="0dp" |                 android:layout_width="0dp" | ||||||
|                 android:layout_height="wrap_content" |                 android:layout_height="wrap_content" | ||||||
|                 android:layout_margin="@dimen/spacing_large" |                 android:layout_margin="@dimen/spacing_large" | ||||||
|         android:text="@string/cheats_edit" |                 android:layout_weight="1" | ||||||
|         app:layout_constraintStart_toEndOf="@id/button_delete" |                 android:nextFocusUp="@id/appbar_cheat_details" | ||||||
|         app:layout_constraintEnd_toStartOf="@id/button_cancel" |                 android:text="@string/cheats_edit" /> | ||||||
|         app:layout_constraintTop_toBottomOf="@id/barrier" |  | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" /> |  | ||||||
| 
 | 
 | ||||||
|             <Button |             <Button | ||||||
|                 android:id="@+id/button_cancel" |                 android:id="@+id/button_cancel" | ||||||
|                 android:layout_width="0dp" |                 android:layout_width="0dp" | ||||||
|                 android:layout_height="wrap_content" |                 android:layout_height="wrap_content" | ||||||
|                 android:layout_margin="@dimen/spacing_large" |                 android:layout_margin="@dimen/spacing_large" | ||||||
|         android:text="@android:string/cancel" |                 android:layout_weight="1" | ||||||
|         app:layout_constraintStart_toEndOf="@id/button_edit" |                 android:nextFocusUp="@id/edit_code_input" | ||||||
|         app:layout_constraintEnd_toStartOf="@id/button_ok" |                 android:text="@android:string/cancel" /> | ||||||
|         app:layout_constraintTop_toBottomOf="@id/barrier" |  | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" /> |  | ||||||
| 
 | 
 | ||||||
|             <Button |             <Button | ||||||
|                 android:id="@+id/button_ok" |                 android:id="@+id/button_ok" | ||||||
|                 android:layout_width="0dp" |                 android:layout_width="0dp" | ||||||
|                 android:layout_height="wrap_content" |                 android:layout_height="wrap_content" | ||||||
|                 android:layout_margin="@dimen/spacing_large" |                 android:layout_margin="@dimen/spacing_large" | ||||||
|         android:text="@android:string/ok" |                 android:layout_weight="1" | ||||||
|         app:layout_constraintStart_toEndOf="@id/button_cancel" |                 android:nextFocusUp="@id/edit_code_input" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |                 android:text="@android:string/ok" /> | ||||||
|         app:layout_constraintTop_toBottomOf="@id/barrier" | 
 | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" /> |         </LinearLayout> | ||||||
|  | 
 | ||||||
|  |     </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
| 
 | 
 | ||||||
| </androidx.constraintlayout.widget.ConstraintLayout> | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
|  |  | ||||||
|  | @ -5,15 +5,36 @@ | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="match_parent"> |     android:layout_height="match_parent"> | ||||||
| 
 | 
 | ||||||
|  |     <androidx.coordinatorlayout.widget.CoordinatorLayout | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent"> | ||||||
|  | 
 | ||||||
|  |         <com.google.android.material.appbar.AppBarLayout | ||||||
|  |             android:id="@+id/appbar_cheat_list" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:fitsSystemWindows="true"> | ||||||
|  | 
 | ||||||
|  |             <com.google.android.material.appbar.MaterialToolbar | ||||||
|  |                 android:id="@+id/toolbar_cheat_list" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="?attr/actionBarSize" | ||||||
|  |                 app:title="@string/cheats" | ||||||
|  |                 app:navigationIcon="@drawable/ic_back" /> | ||||||
|  | 
 | ||||||
|  |         </com.google.android.material.appbar.AppBarLayout> | ||||||
|  | 
 | ||||||
|         <androidx.recyclerview.widget.RecyclerView |         <androidx.recyclerview.widget.RecyclerView | ||||||
|             android:id="@+id/cheat_list" |             android:id="@+id/cheat_list" | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|         android:layout_height="0dp" |             android:layout_height="match_parent" | ||||||
|             android:clipToPadding="false" |             android:clipToPadding="false" | ||||||
|         app:layout_constraintStart_toStartOf="parent" |             app:layout_behavior="@string/appbar_scrolling_view_behavior" /> | ||||||
|         app:layout_constraintEnd_toEndOf="parent" | 
 | ||||||
|         app:layout_constraintTop_toTopOf="parent" |     </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" /> |  | ||||||
| 
 | 
 | ||||||
|     <com.google.android.material.floatingactionbutton.FloatingActionButton |     <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||||
|         android:id="@+id/fab" |         android:id="@+id/fab" | ||||||
|  | @ -21,7 +42,6 @@ | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:src="@drawable/ic_add" |         android:src="@drawable/ic_add" | ||||||
|         android:contentDescription="@string/cheats_add" |         android:contentDescription="@string/cheats_add" | ||||||
|         android:layout_margin="16dp" |  | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" /> |         app:layout_constraintBottom_toBottomOf="parent" /> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								src/android/app/src/main/res/layout/fragment_cheats.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/android/app/src/main/res/layout/fragment_cheats.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.slidingpanelayout.widget.SlidingPaneLayout | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:id="@+id/sliding_pane_layout" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:background="?attr/colorSurface"> | ||||||
|  | 
 | ||||||
|  |     <androidx.fragment.app.FragmentContainerView | ||||||
|  |         android:id="@+id/cheat_list_container" | ||||||
|  |         android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:layout_weight="1" | ||||||
|  |         tools:layout="@layout/fragment_cheat_list" /> | ||||||
|  | 
 | ||||||
|  |     <androidx.fragment.app.FragmentContainerView | ||||||
|  |         android:id="@+id/cheat_details_container" | ||||||
|  |         android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:layout_weight="1" | ||||||
|  |         tools:layout="@layout/fragment_cheat_details" /> | ||||||
|  | 
 | ||||||
|  | </androidx.slidingpanelayout.widget.SlidingPaneLayout> | ||||||
|  | @ -18,9 +18,9 @@ | ||||||
| 
 | 
 | ||||||
|         <ImageView |         <ImageView | ||||||
|             android:id="@+id/logo_image" |             android:id="@+id/logo_image" | ||||||
|             android:layout_width="175dp" |             android:layout_width="104dp" | ||||||
|             android:layout_height="175dp" |             android:layout_height="104dp" | ||||||
|             android:layout_margin="64dp" |             android:layout_margin="32dp" | ||||||
|             android:layout_gravity="center_horizontal" |             android:layout_gravity="center_horizontal" | ||||||
|             android:src="@drawable/ic_citra_full" /> |             android:src="@drawable/ic_citra_full" /> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,35 +1,37 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <androidx.constraintlayout.widget.ConstraintLayout | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|     android:id="@+id/root" |     android:id="@+id/cheat_container" | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="wrap_content" |     android:layout_height="wrap_content" | ||||||
|  |     android:background="?attr/selectableItemBackground" | ||||||
|  |     android:paddingVertical="16dp" | ||||||
|  |     android:paddingHorizontal="20dp" | ||||||
|     android:focusable="true" |     android:focusable="true" | ||||||
|     android:nextFocusRight="@id/checkbox"> |     android:nextFocusRight="@id/cheat_switch"> | ||||||
| 
 | 
 | ||||||
|     <TextView |     <TextView | ||||||
|         android:id="@+id/text_name" |         android:id="@+id/text_name" | ||||||
|  |         style="@style/TextAppearance.AppCompat.Headline" | ||||||
|         android:layout_width="0dp" |         android:layout_width="0dp" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginVertical="16dp" | ||||||
|  |         android:layout_marginEnd="16dp" | ||||||
|         android:textSize="16sp" |         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" |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/cheat_switch" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|         tools:text="Max Lives after losing 1" /> |         tools:text="Max Lives after losing 1" /> | ||||||
| 
 | 
 | ||||||
|     <CheckBox |     <com.google.android.material.materialswitch.MaterialSwitch | ||||||
|         android:id="@+id/checkbox" |         android:id="@+id/cheat_switch" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:focusable="true" |         android:focusable="true" | ||||||
|         android:gravity="center" |         android:gravity="center" | ||||||
|         android:nextFocusLeft="@id/root" |         android:nextFocusLeft="@id/cheat_container" | ||||||
|         android:layout_marginEnd="8dp" |  | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|         app:layout_constraintStart_toEndOf="@id/text_name" |         app:layout_constraintStart_toEndOf="@id/text_name" | ||||||
|  |  | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <navigation 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/cheats_navigation" | ||||||
|  |     app:startDestination="@id/cheatsFragment"> | ||||||
|  | 
 | ||||||
|  |     <fragment | ||||||
|  |         android:id="@+id/cheatsFragment" | ||||||
|  |         android:name="org.citra.citra_emu.features.cheats.ui.CheatsFragment" | ||||||
|  |         android:label="fragment_cheats" | ||||||
|  |         tools:layout="@layout/fragment_cheats"> | ||||||
|  |         <argument | ||||||
|  |             android:name="titleId" | ||||||
|  |             app:argType="long" | ||||||
|  |             android:defaultValue="-1L" /> | ||||||
|  |     </fragment> | ||||||
|  | 
 | ||||||
|  | </navigation> | ||||||
|  | @ -75,6 +75,20 @@ | ||||||
|         android:name="org.citra.citra_emu.fragments.SystemFilesFragment" |         android:name="org.citra.citra_emu.fragments.SystemFilesFragment" | ||||||
|         android:label="SystemFilesFragment" /> |         android:label="SystemFilesFragment" /> | ||||||
| 
 | 
 | ||||||
|  |     <fragment | ||||||
|  |         android:id="@+id/cheatsFragment" | ||||||
|  |         android:name="org.citra.citra_emu.features.cheats.ui.CheatsFragment" | ||||||
|  |         android:label="CheatsFragment" > | ||||||
|  |         <argument | ||||||
|  |             android:name="titleId" | ||||||
|  |             app:argType="long" | ||||||
|  |             android:defaultValue="-1L" /> | ||||||
|  |     </fragment> | ||||||
|  | 
 | ||||||
|  |     <action | ||||||
|  |         android:id="@+id/action_global_cheatsFragment" | ||||||
|  |         app:destination="@id/cheatsFragment" /> | ||||||
|  | 
 | ||||||
|     <fragment |     <fragment | ||||||
|         android:id="@+id/driverManagerFragment" |         android:id="@+id/driverManagerFragment" | ||||||
|         android:name="org.citra.citra_emu.fragments.DriverManagerFragment" |         android:name="org.citra.citra_emu.fragments.DriverManagerFragment" | ||||||
|  |  | ||||||
|  | @ -77,6 +77,7 @@ | ||||||
|         <item>@string/controller_dpad</item> |         <item>@string/controller_dpad</item> | ||||||
|         <item>@string/controller_circlepad</item> |         <item>@string/controller_circlepad</item> | ||||||
|         <item>@string/controller_c</item> |         <item>@string/controller_c</item> | ||||||
|  |         <item>@string/button_home</item> | ||||||
|     </string-array> |     </string-array> | ||||||
| 
 | 
 | ||||||
|     <string-array name="cameraImageSourceNames"> |     <string-array name="cameraImageSourceNames"> | ||||||
|  |  | ||||||
|  | @ -29,7 +29,7 @@ | ||||||
|     <integer name="N3DS_BUTTON_SELECT_Y">850</integer> |     <integer name="N3DS_BUTTON_SELECT_Y">850</integer> | ||||||
|     <integer name="N3DS_BUTTON_START_X">550</integer> |     <integer name="N3DS_BUTTON_START_X">550</integer> | ||||||
|     <integer name="N3DS_BUTTON_START_Y">850</integer> |     <integer name="N3DS_BUTTON_START_Y">850</integer> | ||||||
|     <integer name="N3DS_BUTTON_HOME_X">450</integer> |     <integer name="N3DS_BUTTON_HOME_X">510</integer> | ||||||
|     <integer name="N3DS_BUTTON_HOME_Y">850</integer> |     <integer name="N3DS_BUTTON_HOME_Y">850</integer> | ||||||
| 
 | 
 | ||||||
|     <!-- Default N3DS portrait layout --> |     <!-- Default N3DS portrait layout --> | ||||||
|  | @ -55,8 +55,8 @@ | ||||||
|     <integer name="N3DS_STICK_C_PORTRAIT_Y">710</integer> |     <integer name="N3DS_STICK_C_PORTRAIT_Y">710</integer> | ||||||
|     <integer name="N3DS_STICK_MAIN_PORTRAIT_X">80</integer> |     <integer name="N3DS_STICK_MAIN_PORTRAIT_X">80</integer> | ||||||
|     <integer name="N3DS_STICK_MAIN_PORTRAIT_Y">840</integer> |     <integer name="N3DS_STICK_MAIN_PORTRAIT_Y">840</integer> | ||||||
|     <integer name="N3DS_BUTTON_HOME_PORTRAIT_X">360</integer> |     <integer name="N3DS_BUTTON_HOME_PORTRAIT_X">460</integer> | ||||||
|     <integer name="N3DS_BUTTON_HOME_PORTRAIT_Y">794</integer> |     <integer name="N3DS_BUTTON_HOME_PORTRAIT_Y">840</integer> | ||||||
|     <integer name="N3DS_BUTTON_SELECT_PORTRAIT_X">400</integer> |     <integer name="N3DS_BUTTON_SELECT_PORTRAIT_X">400</integer> | ||||||
|     <integer name="N3DS_BUTTON_SELECT_PORTRAIT_Y">794</integer> |     <integer name="N3DS_BUTTON_SELECT_PORTRAIT_Y">794</integer> | ||||||
|     <integer name="N3DS_BUTTON_START_PORTRAIT_X">520</integer> |     <integer name="N3DS_BUTTON_START_PORTRAIT_X">520</integer> | ||||||
|  |  | ||||||
|  | @ -376,6 +376,7 @@ | ||||||
|     <string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string> |     <string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string> | ||||||
|     <string name="blank_input_not_allowed">Blank input is not allowed</string> |     <string name="blank_input_not_allowed">Blank input is not allowed</string> | ||||||
|     <string name="empty_input_not_allowed">Empty input is not allowed</string> |     <string name="empty_input_not_allowed">Empty input is not allowed</string> | ||||||
|  |     <string name="invalid_input">Invalid input</string> | ||||||
| 
 | 
 | ||||||
|     <!-- Mii Selector --> |     <!-- Mii Selector --> | ||||||
|     <string name="mii_selector">Mii Selector</string> |     <string name="mii_selector">Mii Selector</string> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue