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