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