mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 13:50:03 +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("androidx.core:core-splashscreen:1.0.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("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") | ||||
|     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.CitraApplication | ||||
| 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.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.utils.GameIconUtils | ||||
| import org.citra.citra_emu.viewmodel.GamesViewModel | ||||
|  | @ -100,7 +99,8 @@ class GameAdapter(private val activity: AppCompatActivity) : | |||
|                 .setPositiveButton(android.R.string.ok, null) | ||||
|                 .show() | ||||
|         } else { | ||||
|             CheatsActivity.launch(view.context, holder.game.titleId) | ||||
|             val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId) | ||||
|             view.findNavController().navigate(action) | ||||
|         } | ||||
|         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 | ||||
| // Refer to the license.txt file included. | ||||
| 
 | ||||
| package org.citra.citra_emu.features.settings.model.view | ||||
| 
 | ||||
| import org.citra.citra_emu.features.settings.model.AbstractSetting | ||||
| package org.citra.citra_emu.features.settings.model | ||||
| 
 | ||||
| interface AbstractShortSetting : AbstractSetting { | ||||
|     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.AbstractSetting | ||||
| import org.citra.citra_emu.features.settings.model.AbstractShortSetting | ||||
| 
 | ||||
| class SingleChoiceSetting( | ||||
|     setting: AbstractSetting?, | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| 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.AbstractShortSetting | ||||
| import org.citra.citra_emu.features.settings.model.AbstractStringSetting | ||||
| 
 | ||||
| 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.FloatSetting | ||||
| 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.InputBindingSetting | ||||
| 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.Settings | ||||
| 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.HeaderSetting | ||||
| 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 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the UI and start emulation in here. | ||||
|      */ | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         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
 | ||||
|     jobject java_config = env->AllocObject(s_mii_selector_config_class); | ||||
|     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)); | ||||
|     env->SetObjectField(java_config, | ||||
|                         env->GetFieldID(s_mii_selector_config_class, "title", "Ljava/lang/String;"), | ||||
|                         ToJString(env, config.title)); | ||||
|     env->SetLongField( | ||||
|         java_config, | ||||
|         env->GetFieldID(s_mii_selector_config_class, "initially_selected_mii_index", "J"), | ||||
|         java_config, env->GetFieldID(s_mii_selector_config_class, "initiallySelectedMiiIndex", "J"), | ||||
|         static_cast<jlong>(config.initially_selected_mii_index)); | ||||
| 
 | ||||
|     // List mii names
 | ||||
|  | @ -44,14 +43,14 @@ void AndroidMiiSelector::Setup(const Frontend::MiiSelectorConfig& config) { | |||
|     } | ||||
|     env->SetObjectField( | ||||
|         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
 | ||||
|     jobject data = | ||||
|         env->CallStaticObjectMethod(s_mii_selector_class, s_mii_selector_execute, java_config); | ||||
| 
 | ||||
|     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) { | ||||
|         Finalize(return_code, Mii::MiiData{}); | ||||
|         return; | ||||
|  |  | |||
|  | @ -23,14 +23,14 @@ namespace SoftwareKeyboard { | |||
| static jobject ToJavaKeyboardConfig(const Frontend::KeyboardConfig& config) { | ||||
|     JNIEnv* env = IDCache::GetEnvForThread(); | ||||
|     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)); | ||||
|     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)); | ||||
|     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)); | ||||
|     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)); | ||||
| 
 | ||||
|     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])); | ||||
|     } | ||||
|     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); | ||||
| 
 | ||||
|     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"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:id="@+id/root" | ||||
|     android:id="@+id/cheat_container" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:background="?attr/selectableItemBackground" | ||||
|     android:paddingVertical="16dp" | ||||
|     android:paddingHorizontal="20dp" | ||||
|     android:focusable="true" | ||||
|     android:nextFocusLeft="@id/checkbox"> | ||||
|     android:nextFocusLeft="@id/cheat_switch"> | ||||
| 
 | ||||
|     <TextView | ||||
|         android:id="@+id/text_name" | ||||
|         style="@style/TextAppearance.AppCompat.Headline" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         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" | ||||
|         android:textSize="16sp" | ||||
|         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" /> | ||||
| 
 | ||||
|     <CheckBox | ||||
|         android:id="@+id/checkbox" | ||||
|         android:layout_width="48dp" | ||||
|         android:layout_height="64dp" | ||||
|     <com.google.android.material.materialswitch.MaterialSwitch | ||||
|         android:id="@+id/cheat_switch" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:focusable="true" | ||||
|         android:gravity="center" | ||||
|         android:nextFocusRight="@id/root" | ||||
|         android:nextFocusRight="@id/cheat_container" | ||||
|         android:paddingEnd="8dp" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toEndOf="@id/text_name" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         tools:ignore="RtlSymmetry" /> | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  |  | |||
|  | @ -1,60 +1,26 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="?attr/colorSurface"> | ||||
|     android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <androidx.coordinatorlayout.widget.CoordinatorLayout | ||||
|         android:id="@+id/coordinator_cheats" | ||||
|     <androidx.fragment.app.FragmentContainerView | ||||
|         android:id="@+id/fragment_container" | ||||
|         android:name="androidx.navigation.fragment.NavHostFragment" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent"> | ||||
|         android:layout_height="match_parent" | ||||
|         android:keepScreenOn="true" | ||||
|         app:defaultNavHost="true" /> | ||||
| 
 | ||||
|         <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" | ||||
|     <View | ||||
|         android:id="@+id/navigation_bar_shade" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="1px" | ||||
|         android:background="@android:color/transparent" | ||||
|         android:clickable="false" | ||||
|         android:focusable="false" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="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> | ||||
|         app:layout_constraintStart_toStartOf="parent" /> | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  |  | |||
|  | @ -6,8 +6,8 @@ | |||
|     android:id="@+id/option_card" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginVertical="12dp" | ||||
|     android:layout_marginHorizontal="16dp" | ||||
|     android:layout_marginBottom="24dp" | ||||
|     android:layout_marginHorizontal="12dp" | ||||
|     android:background="?attr/selectableItemBackground" | ||||
|     android:backgroundTint="?attr/colorSurfaceVariant" | ||||
|     android:clickable="true" | ||||
|  | @ -16,7 +16,8 @@ | |||
|     <LinearLayout | ||||
|         android:id="@+id/option_layout" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content"> | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="center_vertical"> | ||||
| 
 | ||||
|         <ImageView | ||||
|             android:id="@+id/option_icon" | ||||
|  | @ -44,7 +45,7 @@ | |||
|                 tools:text="@string/about" /> | ||||
| 
 | ||||
|             <com.google.android.material.textview.MaterialTextView | ||||
|                 style="@style/TextAppearance.Material3.LabelMedium" | ||||
|                 style="@style/TextAppearance.Material3.BodySmall" | ||||
|                 android:id="@+id/option_description" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|  | @ -67,7 +68,8 @@ | |||
|                 android:requiresFadingEdge="horizontal" | ||||
|                 android:layout_marginTop="5dp" | ||||
|                 android:visibility="gone" | ||||
|                 tools:text="@string/about_description" /> | ||||
|                 tools:visibility="visible" | ||||
|                 tools:text="/tree/primary:Games" /> | ||||
| 
 | ||||
|         </LinearLayout> | ||||
| 
 | ||||
|  |  | |||
|  | @ -38,8 +38,8 @@ | |||
| 
 | ||||
|             <ImageView | ||||
|                 android:id="@+id/image_logo" | ||||
|                 android:layout_width="175dp" | ||||
|                 android:layout_height="175dp" | ||||
|                 android:layout_width="104dp" | ||||
|                 android:layout_height="104dp" | ||||
|                 android:layout_marginTop="20dp" | ||||
|                 android:layout_gravity="center_horizontal" | ||||
|                 android:src="@drawable/ic_citra_full" /> | ||||
|  |  | |||
|  | @ -1,163 +1,177 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:id="@+id/root" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <ScrollView | ||||
|         android:id="@+id/scroll_view" | ||||
|     <androidx.coordinatorlayout.widget.CoordinatorLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="0dp" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintBottom_toTopOf="@id/button_layout" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintBottom_toTopOf="@id/barrier"> | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent"> | ||||
| 
 | ||||
|         <androidx.constraintlayout.widget.ConstraintLayout | ||||
|         <com.google.android.material.appbar.AppBarLayout | ||||
|             android:id="@+id/appbar_cheat_details" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content"> | ||||
|             android:layout_height="wrap_content" | ||||
|             android:fitsSystemWindows="true"> | ||||
| 
 | ||||
|             <TextView | ||||
|                 android:id="@+id/label_name" | ||||
|             <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" | ||||
|                 style="@style/TextAppearance.MaterialComponents.Headline5" | ||||
|                 android:textSize="18sp" | ||||
|                 android:text="@string/cheats_name" | ||||
|                 android:orientation="vertical"> | ||||
| 
 | ||||
|                 <com.google.android.material.textfield.TextInputLayout | ||||
|                     android:id="@+id/edit_name" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_marginHorizontal="@dimen/spacing_large" | ||||
|                     android:layout_marginVertical="@dimen/spacing_small" | ||||
|                     android:hint="@string/cheats_name" | ||||
|                     android:paddingTop="@dimen/spacing_medlarge" | ||||
|                     app:errorEnabled="true"> | ||||
| 
 | ||||
|                     <com.google.android.material.textfield.TextInputEditText | ||||
|                         android:id="@+id/edit_name_input" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:importantForAutofill="no" | ||||
|                         android:inputType="text" | ||||
|                         android:minHeight="48dp" | ||||
|                         android:textAlignment="viewStart" | ||||
|                         android:nextFocusDown="@id/edit_notes_input" | ||||
|                         tools:text="Hyrule Field Speed Hack" /> | ||||
| 
 | ||||
|                 </com.google.android.material.textfield.TextInputLayout> | ||||
| 
 | ||||
|                 <com.google.android.material.textfield.TextInputLayout | ||||
|                     android:id="@+id/edit_notes" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_marginHorizontal="@dimen/spacing_large" | ||||
|                     android:layout_marginBottom="24dp" | ||||
|                     android:hint="@string/cheats_notes"> | ||||
| 
 | ||||
|                     <com.google.android.material.textfield.TextInputEditText | ||||
|                         android:id="@+id/edit_notes_input" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:importantForAutofill="no" | ||||
|                         android:inputType="textMultiLine" | ||||
|                         android:minHeight="48dp" | ||||
|                         android:textAlignment="viewStart" | ||||
|                         android:nextFocusDown="@id/edit_code_input" /> | ||||
| 
 | ||||
|                 </com.google.android.material.textfield.TextInputLayout> | ||||
| 
 | ||||
|                 <com.google.android.material.textfield.TextInputLayout | ||||
|                     android:id="@+id/edit_code" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     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:inputType="textMultiLine" | ||||
|                         android:minHeight="108sp" | ||||
|                         android:textAlignment="viewStart" | ||||
|                         android:typeface="monospace" | ||||
|                         android:nextFocusDown="@id/button_cancel" | ||||
|                         tools:text="0x8003d63c:dword:0x60000000\n0x8003d658:dword:0x60000000" /> | ||||
| 
 | ||||
|                 </com.google.android.material.textfield.TextInputLayout> | ||||
| 
 | ||||
|             </LinearLayout> | ||||
| 
 | ||||
|         </androidx.core.widget.NestedScrollView> | ||||
| 
 | ||||
|     </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||
| 
 | ||||
|     <androidx.constraintlayout.widget.ConstraintLayout | ||||
|         android:id="@+id/button_layout" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:background="@android:color/transparent" | ||||
|         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 | ||||
|                 android:id="@+id/button_delete" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 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" /> | ||||
|                 android:layout_weight="1" | ||||
|                 android:nextFocusUp="@id/appbar_cheat_details" | ||||
|                 android:text="@string/cheats_delete" /> | ||||
| 
 | ||||
|             <EditText | ||||
|                 android:id="@+id/edit_name" | ||||
|                 android:layout_width="match_parent" | ||||
|             <Button | ||||
|                 android:id="@+id/button_edit" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:minHeight="48dp" | ||||
|                 android:layout_marginHorizontal="@dimen/spacing_large" | ||||
|                 android:importantForAutofill="no" | ||||
|                 android:inputType="text" | ||||
|                 app:layout_constraintStart_toStartOf="parent" | ||||
|                 app:layout_constraintEnd_toEndOf="parent" | ||||
|                 app:layout_constraintTop_toBottomOf="@id/label_name" | ||||
|                 app:layout_constraintBottom_toTopOf="@id/label_notes" | ||||
|                 tools:text="Max Lives after losing 1" /> | ||||
| 
 | ||||
|             <TextView | ||||
|                 android:id="@+id/label_notes" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 style="@style/TextAppearance.MaterialComponents.Headline5" | ||||
|                 android:textSize="18sp" | ||||
|                 android:text="@string/cheats_notes" | ||||
|                 android:layout_margin="@dimen/spacing_large" | ||||
|                 android:labelFor="@id/edit_notes" | ||||
|                 app:layout_constraintStart_toStartOf="parent" | ||||
|                 app:layout_constraintEnd_toEndOf="parent" | ||||
|                 app:layout_constraintTop_toBottomOf="@id/edit_name" | ||||
|                 app:layout_constraintBottom_toTopOf="@id/edit_notes" /> | ||||
|                 android:layout_weight="1" | ||||
|                 android:nextFocusUp="@id/appbar_cheat_details" | ||||
|                 android:text="@string/cheats_edit" /> | ||||
| 
 | ||||
|             <EditText | ||||
|                 android:id="@+id/edit_notes" | ||||
|                 android:layout_width="match_parent" | ||||
|             <Button | ||||
|                 android:id="@+id/button_cancel" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:minHeight="48dp" | ||||
|                 android:layout_marginHorizontal="@dimen/spacing_large" | ||||
|                 android:importantForAutofill="no" | ||||
|                 android:inputType="textMultiLine" | ||||
|                 app:layout_constraintStart_toStartOf="parent" | ||||
|                 app:layout_constraintEnd_toEndOf="parent" | ||||
|                 app:layout_constraintTop_toBottomOf="@id/label_notes" | ||||
|                 app:layout_constraintBottom_toTopOf="@id/label_code" /> | ||||
| 
 | ||||
|             <TextView | ||||
|                 android:id="@+id/label_code" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 style="@style/TextAppearance.MaterialComponents.Headline5" | ||||
|                 android:textSize="18sp" | ||||
|                 android:text="@string/cheats_code" | ||||
|                 android:layout_margin="@dimen/spacing_large" | ||||
|                 android:labelFor="@id/edit_code" | ||||
|                 app:layout_constraintStart_toStartOf="parent" | ||||
|                 app:layout_constraintEnd_toEndOf="parent" | ||||
|                 app:layout_constraintTop_toBottomOf="@id/edit_notes" | ||||
|                 app:layout_constraintBottom_toTopOf="@id/edit_code" /> | ||||
|                 android:layout_weight="1" | ||||
|                 android:nextFocusUp="@id/edit_code_input" | ||||
|                 android:text="@android:string/cancel" /> | ||||
| 
 | ||||
|             <EditText | ||||
|                 android:id="@+id/edit_code" | ||||
|                 android:layout_width="match_parent" | ||||
|             <Button | ||||
|                 android:id="@+id/button_ok" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:minHeight="108sp" | ||||
|                 android:layout_marginHorizontal="@dimen/spacing_large" | ||||
|                 android:importantForAutofill="no" | ||||
|                 android:inputType="textMultiLine" | ||||
|                 android:typeface="monospace" | ||||
|                 android:gravity="start" | ||||
|                 app:layout_constraintStart_toStartOf="parent" | ||||
|                 app:layout_constraintEnd_toEndOf="parent" | ||||
|                 app:layout_constraintTop_toBottomOf="@id/label_code" | ||||
|                 app:layout_constraintBottom_toBottomOf="parent" | ||||
|                 tools:text="D3000000 00000000\n00138C78 E1C023BE" /> | ||||
|                 android:layout_margin="@dimen/spacing_large" | ||||
|                 android:layout_weight="1" | ||||
|                 android:nextFocusUp="@id/edit_code_input" | ||||
|                 android:text="@android:string/ok" /> | ||||
| 
 | ||||
|         </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|         </LinearLayout> | ||||
| 
 | ||||
|     </ScrollView> | ||||
| 
 | ||||
|     <androidx.constraintlayout.widget.Barrier | ||||
|         android:id="@+id/barrier" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         app:barrierDirection="top" | ||||
|         app:constraint_referenced_ids="button_delete,button_edit,button_cancel,button_ok" /> | ||||
| 
 | ||||
|     <Button | ||||
|         android:id="@+id/button_delete" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_margin="@dimen/spacing_large" | ||||
|         android:text="@string/cheats_delete" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintEnd_toStartOf="@id/button_edit" | ||||
|         app:layout_constraintTop_toBottomOf="@id/barrier" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" /> | ||||
| 
 | ||||
|     <Button | ||||
|         android:id="@+id/button_edit" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_margin="@dimen/spacing_large" | ||||
|         android:text="@string/cheats_edit" | ||||
|         app:layout_constraintStart_toEndOf="@id/button_delete" | ||||
|         app:layout_constraintEnd_toStartOf="@id/button_cancel" | ||||
|         app:layout_constraintTop_toBottomOf="@id/barrier" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" /> | ||||
| 
 | ||||
|     <Button | ||||
|         android:id="@+id/button_cancel" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_margin="@dimen/spacing_large" | ||||
|         android:text="@android:string/cancel" | ||||
|         app:layout_constraintStart_toEndOf="@id/button_edit" | ||||
|         app:layout_constraintEnd_toStartOf="@id/button_ok" | ||||
|         app:layout_constraintTop_toBottomOf="@id/barrier" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" /> | ||||
| 
 | ||||
|     <Button | ||||
|         android:id="@+id/button_ok" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_margin="@dimen/spacing_large" | ||||
|         android:text="@android:string/ok" | ||||
|         app:layout_constraintStart_toEndOf="@id/button_cancel" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@id/barrier" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" /> | ||||
|     </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  |  | |||
|  | @ -5,15 +5,36 @@ | |||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/cheat_list" | ||||
|     <androidx.coordinatorlayout.widget.CoordinatorLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="0dp" | ||||
|         android:clipToPadding="false" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         android:layout_height="match_parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintBottom_toBottomOf="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 | ||||
|             android:id="@+id/cheat_list" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="match_parent" | ||||
|             android:clipToPadding="false" | ||||
|             app:layout_behavior="@string/appbar_scrolling_view_behavior" /> | ||||
| 
 | ||||
|     </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||
| 
 | ||||
|     <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
|         android:id="@+id/fab" | ||||
|  | @ -21,7 +42,6 @@ | |||
|         android:layout_height="wrap_content" | ||||
|         android:src="@drawable/ic_add" | ||||
|         android:contentDescription="@string/cheats_add" | ||||
|         android:layout_margin="16dp" | ||||
|         app:layout_constraintEnd_toEndOf="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 | ||||
|             android:id="@+id/logo_image" | ||||
|             android:layout_width="175dp" | ||||
|             android:layout_height="175dp" | ||||
|             android:layout_margin="64dp" | ||||
|             android:layout_width="104dp" | ||||
|             android:layout_height="104dp" | ||||
|             android:layout_margin="32dp" | ||||
|             android:layout_gravity="center_horizontal" | ||||
|             android:src="@drawable/ic_citra_full" /> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,35 +1,37 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:id="@+id/root" | ||||
|     android:id="@+id/cheat_container" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:background="?attr/selectableItemBackground" | ||||
|     android:paddingVertical="16dp" | ||||
|     android:paddingHorizontal="20dp" | ||||
|     android:focusable="true" | ||||
|     android:nextFocusRight="@id/checkbox"> | ||||
|     android:nextFocusRight="@id/cheat_switch"> | ||||
| 
 | ||||
|     <TextView | ||||
|         android:id="@+id/text_name" | ||||
|         style="@style/TextAppearance.AppCompat.Headline" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginVertical="16dp" | ||||
|         android:layout_marginEnd="16dp" | ||||
|         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_constraintEnd_toStartOf="@id/cheat_switch" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         tools:text="Max Lives after losing 1" /> | ||||
| 
 | ||||
|     <CheckBox | ||||
|         android:id="@+id/checkbox" | ||||
|     <com.google.android.material.materialswitch.MaterialSwitch | ||||
|         android:id="@+id/cheat_switch" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:focusable="true" | ||||
|         android:gravity="center" | ||||
|         android:nextFocusLeft="@id/root" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:nextFocusLeft="@id/cheat_container" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         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: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 | ||||
|         android:id="@+id/driverManagerFragment" | ||||
|         android:name="org.citra.citra_emu.fragments.DriverManagerFragment" | ||||
|  |  | |||
|  | @ -77,6 +77,7 @@ | |||
|         <item>@string/controller_dpad</item> | ||||
|         <item>@string/controller_circlepad</item> | ||||
|         <item>@string/controller_c</item> | ||||
|         <item>@string/button_home</item> | ||||
|     </string-array> | ||||
| 
 | ||||
|     <string-array name="cameraImageSourceNames"> | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ | |||
|     <integer name="N3DS_BUTTON_SELECT_Y">850</integer> | ||||
|     <integer name="N3DS_BUTTON_START_X">550</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> | ||||
| 
 | ||||
|     <!-- Default N3DS portrait layout --> | ||||
|  | @ -55,8 +55,8 @@ | |||
|     <integer name="N3DS_STICK_C_PORTRAIT_Y">710</integer> | ||||
|     <integer name="N3DS_STICK_MAIN_PORTRAIT_X">80</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_Y">794</integer> | ||||
|     <integer name="N3DS_BUTTON_HOME_PORTRAIT_X">460</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_Y">794</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="blank_input_not_allowed">Blank 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 --> | ||||
|     <string name="mii_selector">Mii Selector</string> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue