Merge branch 'master' of https://github.com/citra-emu/citra into vulkan

This commit is contained in:
zorn 2023-12-28 03:46:16 +10:00
commit 0d7eed8bb9
160 changed files with 6952 additions and 4778 deletions

View File

@ -1,13 +1,15 @@
#!/bin/sh -ex
#!/bin/bash -ex
if [ "$TARGET" = "appimage" ]; then
export COMPILER_FLAGS=(-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_LINKER=/etc/bin/ld.lld)
fi
mkdir build && cd build
cmake .. -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_LINKER=/etc/bin/ld.lld \
"${COMPILER_FLAGS[@]}" \
-DENABLE_QT_TRANSLATION=ON \
-DCITRA_ENABLE_COMPATIBILITY_REPORTING=ON \
-DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \

View File

@ -61,12 +61,20 @@ function pack_artifacts() {
fi
}
if [ -z "$PACK_INDIVIDUALLY" ]; then
# Pack all of the artifacts at once.
pack_artifacts build/bundle
else
if [ -n "$UNPACKED" ]; then
# Copy the artifacts to be uploaded unpacked.
for ARTIFACT in build/bundle/*; do
FILENAME=$(basename "$ARTIFACT")
EXTENSION="${FILENAME##*.}"
mv "$ARTIFACT" "artifacts/$REV_NAME.$EXTENSION"
done
elif [ -n "$PACK_INDIVIDUALLY" ]; then
# Pack and upload the artifacts one-by-one.
for ARTIFACT in build/bundle/*; do
pack_artifacts "$ARTIFACT"
done
else
# Pack all of the artifacts into a single archive.
pack_artifacts build/bundle
fi

View File

@ -76,7 +76,8 @@ CMAKE_DEPENDENT_OPTION(ENABLE_DEDICATED_ROOM "Enable generating dedicated room e
option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
option(ENABLE_SCRIPTING "Enable RPC server for scripting" ON)
CMAKE_DEPENDENT_OPTION(ENABLE_CUBEB "Enables the cubeb audio backend" ON "NOT IOS" OFF)
# TODO: cubeb currently causes issues on macOS, see: https://github.com/mozilla/cubeb/issues/771
CMAKE_DEPENDENT_OPTION(ENABLE_CUBEB "Enables the cubeb audio backend" ON "NOT APPLE" OFF)
option(ENABLE_OPENAL "Enables the OpenAL audio backend" ON)
CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support" ON "NOT IOS" OFF)

View File

@ -7,3 +7,7 @@ source_file = en.ts
source_lang = en
type = QT
[o:citra:p:citra:r:android]
file_filter = ../../src/android/app/src/main/res/values-<lang>/strings.xml
source_file = ../../src/android/app/src/main/res/values/strings.xml
type = ANDROID

View File

@ -323,6 +323,10 @@ if(ANDROID)
target_link_libraries(httplib INTERFACE ifaddrs)
endif()
if (UNIX AND NOT APPLE)
add_subdirectory(gamemode)
endif()
# cpp-jwt
if (ENABLE_WEB_SERVICE)
if (USE_SYSTEM_CPP_JWT)

9
externals/gamemode/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
project(gamemode LANGUAGES CXX C)
add_library(gamemode include/gamemode_client.h)
target_include_directories(gamemode PUBLIC include)
set_target_properties(gamemode PROPERTIES LINKER_LANGUAGE C)

View File

@ -0,0 +1,379 @@
// SPDX-FileCopyrightText: Copyright 2017-2019 Feral Interactive
// SPDX-License-Identifier: BSD-3-Clause
/*
Copyright (c) 2017-2019, Feral Interactive
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Feral Interactive nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef CLIENT_GAMEMODE_H
#define CLIENT_GAMEMODE_H
/*
* GameMode supports the following client functions
* Requests are refcounted in the daemon
*
* int gamemode_request_start() - Request gamemode starts
* 0 if the request was sent successfully
* -1 if the request failed
*
* int gamemode_request_end() - Request gamemode ends
* 0 if the request was sent successfully
* -1 if the request failed
*
* GAMEMODE_AUTO can be defined to make the above two functions apply during static init and
* destruction, as appropriate. In this configuration, errors will be printed to stderr
*
* int gamemode_query_status() - Query the current status of gamemode
* 0 if gamemode is inactive
* 1 if gamemode is active
* 2 if gamemode is active and this client is registered
* -1 if the query failed
*
* int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process
* 0 if the request was sent successfully
* -1 if the request failed
* -2 if the request was rejected
*
* int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process
* 0 if the request was sent successfully
* -1 if the request failed
* -2 if the request was rejected
*
* int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process
* 0 if gamemode is inactive
* 1 if gamemode is active
* 2 if gamemode is active and this client is registered
* -1 if the query failed
*
* const char* gamemode_error_string() - Get an error string
* returns a string describing any of the above errors
*
* Note: All the above requests can be blocking - dbus requests can and will block while the daemon
* handles the request. It is not recommended to make these calls in performance critical code
*/
#include <stdbool.h>
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
static char internal_gamemode_client_error_string[512] = { 0 };
/**
* Load libgamemode dynamically to dislodge us from most dependencies.
* This allows clients to link and/or use this regardless of runtime.
* See SDL2 for an example of the reasoning behind this in terms of
* dynamic versioning as well.
*/
static volatile int internal_libgamemode_loaded = 1;
/* Typedefs for the functions to load */
typedef int (*api_call_return_int)(void);
typedef const char *(*api_call_return_cstring)(void);
typedef int (*api_call_pid_return_int)(pid_t);
/* Storage for functors */
static api_call_return_int REAL_internal_gamemode_request_start = NULL;
static api_call_return_int REAL_internal_gamemode_request_end = NULL;
static api_call_return_int REAL_internal_gamemode_query_status = NULL;
static api_call_return_cstring REAL_internal_gamemode_error_string = NULL;
static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL;
static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL;
static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL;
/**
* Internal helper to perform the symbol binding safely.
*
* Returns 0 on success and -1 on failure
*/
__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol(
void *handle, const char *name, void **out_func, size_t func_size, bool required)
{
void *symbol_lookup = NULL;
char *dl_error = NULL;
/* Safely look up the symbol */
symbol_lookup = dlsym(handle, name);
dl_error = dlerror();
if (required && (dl_error || !symbol_lookup)) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"dlsym failed - %s",
dl_error);
return -1;
}
/* Have the symbol correctly, copy it to make it usable */
memcpy(out_func, &symbol_lookup, func_size);
return 0;
}
/**
* Loads libgamemode and needed functions
*
* Returns 0 on success and -1 on failure
*/
__attribute__((always_inline)) static inline int internal_load_libgamemode(void)
{
/* We start at 1, 0 is a success and -1 is a fail */
if (internal_libgamemode_loaded != 1) {
return internal_libgamemode_loaded;
}
/* Anonymous struct type to define our bindings */
struct binding {
const char *name;
void **functor;
size_t func_size;
bool required;
} bindings[] = {
{ "real_gamemode_request_start",
(void **)&REAL_internal_gamemode_request_start,
sizeof(REAL_internal_gamemode_request_start),
true },
{ "real_gamemode_request_end",
(void **)&REAL_internal_gamemode_request_end,
sizeof(REAL_internal_gamemode_request_end),
true },
{ "real_gamemode_query_status",
(void **)&REAL_internal_gamemode_query_status,
sizeof(REAL_internal_gamemode_query_status),
false },
{ "real_gamemode_error_string",
(void **)&REAL_internal_gamemode_error_string,
sizeof(REAL_internal_gamemode_error_string),
true },
{ "real_gamemode_request_start_for",
(void **)&REAL_internal_gamemode_request_start_for,
sizeof(REAL_internal_gamemode_request_start_for),
false },
{ "real_gamemode_request_end_for",
(void **)&REAL_internal_gamemode_request_end_for,
sizeof(REAL_internal_gamemode_request_end_for),
false },
{ "real_gamemode_query_status_for",
(void **)&REAL_internal_gamemode_query_status_for,
sizeof(REAL_internal_gamemode_query_status_for),
false },
};
void *libgamemode = NULL;
/* Try and load libgamemode */
libgamemode = dlopen("libgamemode.so.0", RTLD_NOW);
if (!libgamemode) {
/* Attempt to load unversioned library for compatibility with older
* versions (as of writing, there are no ABI changes between the two -
* this may need to change if ever ABI-breaking changes are made) */
libgamemode = dlopen("libgamemode.so", RTLD_NOW);
if (!libgamemode) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"dlopen failed - %s",
dlerror());
internal_libgamemode_loaded = -1;
return -1;
}
}
/* Attempt to bind all symbols */
for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) {
struct binding *binder = &bindings[i];
if (internal_bind_libgamemode_symbol(libgamemode,
binder->name,
binder->functor,
binder->func_size,
binder->required)) {
internal_libgamemode_loaded = -1;
return -1;
};
}
/* Success */
internal_libgamemode_loaded = 0;
return 0;
}
/**
* Redirect to the real libgamemode
*/
__attribute__((always_inline)) static inline const char *gamemode_error_string(void)
{
/* If we fail to load the system gamemode, or we have an error string already, return our error
* string instead of diverting to the system version */
if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') {
return internal_gamemode_client_error_string;
}
/* Assert for static analyser that the function is not NULL */
assert(REAL_internal_gamemode_error_string != NULL);
return REAL_internal_gamemode_error_string();
}
/**
* Redirect to the real libgamemode
* Allow automatically requesting game mode
* Also prints errors as they happen.
*/
#ifdef GAMEMODE_AUTO
__attribute__((constructor))
#else
__attribute__((always_inline)) static inline
#endif
int gamemode_request_start(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
/* Assert for static analyser that the function is not NULL */
assert(REAL_internal_gamemode_request_start != NULL);
if (REAL_internal_gamemode_request_start() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
return 0;
}
/* Redirect to the real libgamemode */
#ifdef GAMEMODE_AUTO
__attribute__((destructor))
#else
__attribute__((always_inline)) static inline
#endif
int gamemode_request_end(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
/* Assert for static analyser that the function is not NULL */
assert(REAL_internal_gamemode_request_end != NULL);
if (REAL_internal_gamemode_request_end() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
return 0;
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_query_status(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_query_status == NULL) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"gamemode_query_status missing (older host?)");
return -1;
}
return REAL_internal_gamemode_query_status();
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_request_start_for == NULL) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"gamemode_request_start_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_request_start_for(pid);
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_request_end_for == NULL) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"gamemode_request_end_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_request_end_for(pid);
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_query_status_for == NULL) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"gamemode_query_status_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_query_status_for(pid);
}
#endif // CLIENT_GAMEMODE_H

View File

@ -124,6 +124,13 @@ else()
add_compile_options("-stdlib=libc++")
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL GNU)
# GCC may warn when it ignores attributes like maybe_unused,
# which is a problem for older versions (e.g. GCC 11).
add_compile_options("-Wno-attributes")
add_compile_options("-Wno-interference-size")
endif()
if (MINGW)
add_definitions(-DMINGW_HAS_SECURE_API)
if (COMPILE_WITH_DWARF)
@ -148,6 +155,10 @@ else()
endif()
endif()
if (NOT APPLE)
add_compile_definitions(HAS_OPENGL)
endif()
add_subdirectory(common)
add_subdirectory(core)
add_subdirectory(video_core)

View File

@ -29,7 +29,7 @@ android {
namespace = "org.citra.citra_emu"
compileSdkVersion = "android-34"
ndkVersion = "25.2.9519653"
ndkVersion = "26.1.10909125"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@ -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")

View File

@ -20,7 +20,6 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
@ -32,13 +31,15 @@ import org.citra.citra_emu.R
import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
import org.citra.citra_emu.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.utils.ControllerMappingHelper
import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.FileBrowserHelper
import org.citra.citra_emu.utils.ForegroundService
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.ThemeUtil
import org.citra.citra_emu.viewmodel.EmulationViewModel
@ -52,6 +53,8 @@ class EmulationActivity : AppCompatActivity() {
private val emulationViewModel: EmulationViewModel by viewModels()
private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.setTheme(this)
@ -61,6 +64,8 @@ class EmulationActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings)
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil)
setContentView(binding.root)
val navHostFragment =
@ -73,15 +78,11 @@ class EmulationActivity : AppCompatActivity() {
// Set these options now so that the SurfaceView the game renders into is the right size.
enableFullscreenImmersive()
// Override Citra core INI with the one set by our in game menu
NativeLibrary.swapScreens(
EmulationMenuSettings.swapScreens,
windowManager.defaultDisplay.rotation
)
// Start a foreground service to prevent the app from getting killed in the background
foregroundService = Intent(this, ForegroundService::class.java)
startForegroundService(foregroundService)
EmulationLifecycleUtil.addShutdownHook(hook = { this.finish() })
}
// On some devices, the system bars will not disappear on first boot or after some
@ -103,6 +104,7 @@ class EmulationActivity : AppCompatActivity() {
}
override fun onDestroy() {
EmulationLifecycleUtil.clear()
stopForegroundService(this)
super.onDestroy()
}
@ -188,6 +190,8 @@ class EmulationActivity : AppCompatActivity() {
onBackPressed()
}
hotkeyUtility.handleHotkey(button)
// Normal key events.
NativeLibrary.ButtonState.PRESSED
}

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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)
}

View File

@ -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);
}

View File

@ -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
}
}
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -0,0 +1,42 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.display
import android.view.WindowManager
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.utils.EmulationMenuSettings
class ScreenAdjustmentUtil(private val windowManager: WindowManager,
private val settings: Settings) {
fun swapScreen() {
val isEnabled = !EmulationMenuSettings.swapScreens
EmulationMenuSettings.swapScreens = isEnabled
NativeLibrary.swapScreens(
isEnabled,
windowManager.defaultDisplay.rotation
)
BooleanSetting.SWAP_SCREEN.boolean = isEnabled
settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG)
}
fun cycleLayouts() {
val nextLayout = (EmulationMenuSettings.landscapeScreenLayout + 1) % ScreenLayout.entries.size
changeScreenOrientation(ScreenLayout.from(nextLayout))
}
fun changeScreenOrientation(layoutOption: ScreenLayout) {
EmulationMenuSettings.landscapeScreenLayout = layoutOption.int
NativeLibrary.notifyOrientationChange(
EmulationMenuSettings.landscapeScreenLayout,
windowManager.defaultDisplay.rotation
)
IntSetting.SCREEN_LAYOUT.int = layoutOption.int
settings.saveSetting(IntSetting.SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG)
}
}

View File

@ -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.display
enum class ScreenLayout(val int: Int) {
// These must match what is defined in src/common/settings.h
DEFAULT(0),
SINGLE_SCREEN(1),
LARGE_SCREEN(2),
SIDE_SCREEN(3),
HYBRID_SCREEN(4),
MOBILE_PORTRAIT(5),
MOBILE_LANDSCAPE(6);
companion object {
fun from(int: Int): ScreenLayout {
return entries.firstOrNull { it.int == int } ?: DEFAULT
}
}
}

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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();
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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;
});
}
}

View File

@ -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
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
});
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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];
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
})
}
}
}

View File

@ -0,0 +1,12 @@
// 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.hotkeys
enum class Hotkey(val button: Int) {
SWAP_SCREEN(10001),
CYCLE_LAYOUT(10002),
CLOSE_GAME(10003),
PAUSE_OR_RESUME(10004);
}

View File

@ -0,0 +1,27 @@
// 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.hotkeys
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.display.ScreenAdjustmentUtil
class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
val hotkeyButtons = Hotkey.entries.map { it.button }
fun handleHotkey(bindedButton: Int): Boolean {
if(hotkeyButtons.contains(bindedButton)) {
when (bindedButton) {
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
else -> {}
}
return true
}
return false
}
}

View File

@ -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

View File

@ -12,7 +12,8 @@ enum class BooleanSetting(
SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true);
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true),
SWAP_SCREEN("swap_screen", Settings.SECTION_LAYOUT, false);
override var boolean: Boolean = defaultValue

View File

@ -22,6 +22,7 @@ enum class IntSetting(
CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85),
CARDBOARD_X_SHIFT("cardboard_x_shift", Settings.SECTION_LAYOUT, 0),
CARDBOARD_Y_SHIFT("cardboard_y_shift", Settings.SECTION_LAYOUT, 0),
SCREEN_LAYOUT("layout_option", Settings.SECTION_LAYOUT, 0),
AUDIO_INPUT_TYPE("output_type", Settings.SECTION_AUDIO, 0),
NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, 1),
CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100),

View File

@ -94,6 +94,10 @@ class Settings {
}
}
fun saveSetting(setting: AbstractSetting, filename: String) {
SettingsFile.saveFile(filename, setting)
}
companion object {
const val SECTION_CORE = "Core"
const val SECTION_SYSTEM = "System"
@ -128,6 +132,11 @@ class Settings {
const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"
const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"
const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap"
const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout"
const val HOTKEY_CLOSE_GAME = "hotkey_close_game"
const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game"
val buttonKeys = listOf(
KEY_BUTTON_A,
KEY_BUTTON_B,
@ -174,6 +183,18 @@ class Settings {
R.string.button_zl,
R.string.button_zr
)
val hotKeys = listOf(
HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME,
HOTKEY_PAUSE_OR_RESUME
)
val hotkeyTitles = listOf(
R.string.emulation_swap_screens,
R.string.emulation_cycle_landscape_layouts,
R.string.emulation_close_game,
R.string.emulation_toggle_pause
)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MATERIAL_YOU = "MaterialYouTheme"

View File

@ -6,14 +6,15 @@ package org.citra.citra_emu.features.settings.model.view
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import android.view.InputDevice
import android.view.InputDevice.MotionRange
import android.view.KeyEvent
import android.widget.Toast
import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.features.hotkeys.Hotkey
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.Settings
@ -127,6 +128,11 @@ class InputBindingSetting(
Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
Settings.HOTKEY_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button
Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button
Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button
Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button
else -> -1
}

View File

@ -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?,

View File

@ -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(

View File

@ -224,7 +224,7 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
setUsername("CITRA")
setBirthday(3, 25)
setSystemLanguage(1)
setSoundOutputMode(2)
setSoundOutputMode(1)
setCountryCode(49)
setPlayCoins(42)
}

View File

@ -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

View File

@ -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
@ -38,8 +38,8 @@ import org.citra.citra_emu.features.settings.model.view.SwitchSetting
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.fragments.ResetSettingsDialogFragment
import org.citra.citra_emu.utils.BirthdayMonth
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.ThemeUtil
class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
@ -620,6 +620,12 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
val button = getInputObject(key)
add(InputBindingSetting(button, Settings.triggerTitles[i]))
}
add(HeaderSetting(R.string.controller_hotkeys))
Settings.hotKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key)
add(InputBindingSetting(button, Settings.hotkeyTitles[i]))
}
}
}
@ -874,7 +880,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
override val section = null
override val isRuntimeEditable = false
override val valueAsString = int.toString()
override val defaultValue = 2
override val defaultValue = 1
}
add(
SingleChoiceSetting(

View File

@ -8,7 +8,6 @@ import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.BooleanSetting
@ -23,9 +22,11 @@ import org.citra.citra_emu.utils.BiMap
import org.citra.citra_emu.utils.DirectoryInitialization.userDirectory
import org.citra.citra_emu.utils.Log
import org.ini4j.Wini
import java.io.*
import java.lang.NumberFormatException
import java.util.*
import java.io.BufferedReader
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStreamReader
import java.util.TreeMap
/**
@ -146,6 +147,26 @@ object SettingsFile {
}
}
fun saveFile(
fileName: String,
setting: AbstractSetting
) {
val ini = getSettingsFile(fileName)
try {
val context: Context = CitraApplication.appContext
val inputStream = context.contentResolver.openInputStream(ini.uri)
val writer = Wini(inputStream)
writer.put(setting.section, setting.key, setting.valueAsString)
inputStream!!.close()
val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt")
writer.store(outputStream)
outputStream!!.flush()
outputStream.close()
} catch (e: Exception) {
Log.error("[SettingsFile] File not found: $fileName.ini: ${e.message}")
}
}
private fun mapSectionNameFromIni(generalSectionName: String): String? {
return if (sectionsMap.getForward(generalSectionName) != null) {
sectionsMap.getForward(generalSectionName)

View File

@ -15,7 +15,6 @@ import android.os.Looper
import android.os.SystemClock
import android.view.Choreographer
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.Surface
import android.view.SurfaceHolder
@ -33,6 +32,7 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@ -51,6 +51,9 @@ import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.databinding.DialogCheckboxBinding
import org.citra.citra_emu.databinding.DialogSliderBinding
import org.citra.citra_emu.databinding.FragmentEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.display.ScreenLayout
import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.model.Game
@ -60,10 +63,10 @@ import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.FileUtil
import org.citra.citra_emu.utils.GameHelper
import org.citra.citra_emu.utils.GameIconUtils
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.ViewUtils
import org.citra.citra_emu.viewmodel.EmulationViewModel
import java.lang.NullPointerException
class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.FrameCallback {
private val preferences: SharedPreferences
@ -80,8 +83,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private val args by navArgs<EmulationFragmentArgs>()
private lateinit var game: Game
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private val emulationViewModel: EmulationViewModel by activityViewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
override fun onAttach(context: Context) {
super.onAttach(context)
@ -137,11 +142,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
retainInstance = true
emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity
screenAdjustmentUtil = ScreenAdjustmentUtil(emulationActivity.windowManager, settingsViewModel.settings)
EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() })
EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() })
}
/**
* Initialize the UI and start emulation in here.
*/
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -261,12 +266,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
}
R.id.menu_swap_screens -> {
val isEnabled = !EmulationMenuSettings.swapScreens
EmulationMenuSettings.swapScreens = isEnabled
NativeLibrary.swapScreens(
isEnabled,
requireActivity().windowManager.defaultDisplay.rotation
)
screenAdjustmentUtil.swapScreen()
true
}
@ -318,8 +318,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
.setTitle(R.string.emulation_close_game)
.setMessage(R.string.emulation_close_game_message)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
emulationState.stop()
requireActivity().finish()
EmulationLifecycleUtil.closeGame()
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
NativeLibrary.unPauseEmulation()
@ -413,6 +412,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
setInsets()
}
private fun togglePause() {
if(emulationState.isPaused) {
emulationState.unpause()
} else {
emulationState.pause()
}
}
override fun onResume() {
super.onResume()
Choreographer.getInstance().postFrameCallback(this)
@ -669,15 +676,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menuInflater.inflate(R.menu.menu_landscape_screen_layout, popupMenu.menu)
val layoutOptionMenuItem = when (EmulationMenuSettings.landscapeScreenLayout) {
EmulationMenuSettings.LayoutOption_SingleScreen ->
ScreenLayout.SINGLE_SCREEN.int ->
R.id.menu_screen_layout_single
EmulationMenuSettings.LayoutOption_SideScreen ->
ScreenLayout.SIDE_SCREEN.int ->
R.id.menu_screen_layout_sidebyside
EmulationMenuSettings.LayoutOption_MobilePortrait ->
ScreenLayout.MOBILE_PORTRAIT.int ->
R.id.menu_screen_layout_portrait
ScreenLayout.HYBRID_SCREEN.int ->
R.id.menu_screen_layout_hybrid
else -> R.id.menu_screen_layout_landscape
}
popupMenu.menu.findItem(layoutOptionMenuItem).setChecked(true)
@ -685,22 +695,27 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_screen_layout_landscape -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, it)
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.MOBILE_LANDSCAPE)
true
}
R.id.menu_screen_layout_portrait -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, it)
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.MOBILE_PORTRAIT)
true
}
R.id.menu_screen_layout_single -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, it)
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.SINGLE_SCREEN)
true
}
R.id.menu_screen_layout_sidebyside -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, it)
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.SIDE_SCREEN)
true
}
R.id.menu_screen_layout_hybrid -> {
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.HYBRID_SCREEN)
true
}
@ -711,15 +726,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show()
}
private fun changeScreenOrientation(layoutOption: Int, item: MenuItem) {
item.setChecked(true)
NativeLibrary.notifyOrientationChange(
layoutOption,
requireActivity().windowManager.defaultDisplay.rotation
)
EmulationMenuSettings.landscapeScreenLayout = layoutOption
}
private fun editControlsPlacement() {
if (binding.surfaceInputOverlay.isInEditMode) {
binding.doneControlConfig.visibility = View.GONE

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -1,5 +0,0 @@
package org.citra.citra_emu.utils;
public interface Action1<T> {
void call(T t);
}

View File

@ -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);
}
}

View File

@ -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]
}

View File

@ -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);
}

View File

@ -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
}

View File

@ -0,0 +1,32 @@
// 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
object EmulationLifecycleUtil {
private var shutdownHooks: MutableList<Runnable> = ArrayList()
private var pauseResumeHooks: MutableList<Runnable> = ArrayList()
fun closeGame() {
shutdownHooks.forEach(Runnable::run)
}
fun pauseOrResume() {
pauseResumeHooks.forEach(Runnable::run)
}
fun addShutdownHook(hook: Runnable) {
shutdownHooks.add(hook)
}
fun addPauseResumeHook(hook: Runnable) {
pauseResumeHooks.add(hook)
}
fun clear() {
pauseResumeHooks.clear()
shutdownHooks.clear()
}
}

View File

@ -7,19 +7,12 @@ package org.citra.citra_emu.utils
import androidx.drawerlayout.widget.DrawerLayout
import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.display.ScreenLayout
object EmulationMenuSettings {
private val preferences =
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
// These must match what is defined in src/common/settings.h
const val LayoutOption_Default = 0
const val LayoutOption_SingleScreen = 1
const val LayoutOption_LargeScreen = 2
const val LayoutOption_SideScreen = 3
const val LayoutOption_MobilePortrait = 5
const val LayoutOption_MobileLandscape = 6
var joystickRelCenter: Boolean
get() = preferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true)
set(value) {
@ -37,7 +30,7 @@ object EmulationMenuSettings {
var landscapeScreenLayout: Int
get() = preferences.getInt(
"EmulationMenuSettings_LandscapeScreenLayout",
LayoutOption_MobileLandscape
ScreenLayout.MOBILE_LANDSCAPE.int
)
set(value) {
preferences.edit()

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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!!)
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View 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>

View 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="#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>

View File

@ -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>

View File

@ -1,60 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator_cheats"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="match_parent"
android:keepScreenOn="true"
app:defaultNavHost="true" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_cheats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_cheats"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
<View
android:id="@+id/navigation_bar_shade"
android:layout_width="0dp"
android:layout_height="1px"
android:background="@android:color/transparent"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/coordinator_cheats">
<androidx.fragment.app.FragmentContainerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/cheat_list_container"
android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment"
tools:layout="@layout/fragment_cheat_list" />
<androidx.fragment.app.FragmentContainerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/cheat_details_container"
android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment"
tools:layout="@layout/fragment_cheat_details" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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>

View File

@ -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" />

View File

@ -1,163 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scroll_view"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/button_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/barrier">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_cheat_details"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<TextView
android:id="@+id/label_name"
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_cheat_details"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back"
app:title="@string/cheats" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:textSize="18sp"
android:text="@string/cheats_name"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_large"
android:layout_marginVertical="@dimen/spacing_small"
android:hint="@string/cheats_name"
android:paddingTop="@dimen/spacing_medlarge"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_name_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="text"
android:minHeight="48dp"
android:textAlignment="viewStart"
android:nextFocusDown="@id/edit_notes_input"
tools:text="Hyrule Field Speed Hack" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/edit_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_large"
android:layout_marginBottom="24dp"
android:hint="@string/cheats_notes">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_notes_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:minHeight="48dp"
android:textAlignment="viewStart"
android:nextFocusDown="@id/edit_code_input" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/edit_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_large"
android:layout_marginVertical="@dimen/spacing_small"
android:hint="@string/cheats_code"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_code_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:minHeight="108sp"
android:textAlignment="viewStart"
android:typeface="monospace"
android:nextFocusDown="@id/button_cancel"
tools:text="0x8003d63c:dword:0x60000000\n0x8003d658:dword:0x60000000" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/button_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/button_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/button_delete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:labelFor="@id/edit_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/edit_name" />
android:layout_weight="1"
android:nextFocusUp="@id/appbar_cheat_details"
android:text="@string/cheats_delete" />
<EditText
android:id="@+id/edit_name"
android:layout_width="match_parent"
<Button
android:id="@+id/button_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginHorizontal="@dimen/spacing_large"
android:importantForAutofill="no"
android:inputType="text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_name"
app:layout_constraintBottom_toTopOf="@id/label_notes"
tools:text="Max Lives after losing 1" />
<TextView
android:id="@+id/label_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:textSize="18sp"
android:text="@string/cheats_notes"
android:layout_margin="@dimen/spacing_large"
android:labelFor="@id/edit_notes"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_name"
app:layout_constraintBottom_toTopOf="@id/edit_notes" />
android:layout_weight="1"
android:nextFocusUp="@id/appbar_cheat_details"
android:text="@string/cheats_edit" />
<EditText
android:id="@+id/edit_notes"
android:layout_width="match_parent"
<Button
android:id="@+id/button_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginHorizontal="@dimen/spacing_large"
android:importantForAutofill="no"
android:inputType="textMultiLine"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_notes"
app:layout_constraintBottom_toTopOf="@id/label_code" />
<TextView
android:id="@+id/label_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:textSize="18sp"
android:text="@string/cheats_code"
android:layout_margin="@dimen/spacing_large"
android:labelFor="@id/edit_code"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_notes"
app:layout_constraintBottom_toTopOf="@id/edit_code" />
android:layout_weight="1"
android:nextFocusUp="@id/edit_code_input"
android:text="@android:string/cancel" />
<EditText
android:id="@+id/edit_code"
android:layout_width="match_parent"
<Button
android:id="@+id/button_ok"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="108sp"
android:layout_marginHorizontal="@dimen/spacing_large"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:typeface="monospace"
android:gravity="start"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_code"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="D3000000 00000000\n00138C78 E1C023BE" />
android:layout_margin="@dimen/spacing_large"
android:layout_weight="1"
android:nextFocusUp="@id/edit_code_input"
android:text="@android:string/ok" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="button_delete,button_edit,button_cancel,button_ok" />
<Button
android:id="@+id/button_delete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:text="@string/cheats_delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_edit"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="@+id/button_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:text="@string/cheats_edit"
app:layout_constraintStart_toEndOf="@id/button_delete"
app:layout_constraintEnd_toStartOf="@id/button_cancel"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="@+id/button_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:text="@android:string/cancel"
app:layout_constraintStart_toEndOf="@id/button_edit"
app:layout_constraintEnd_toStartOf="@id/button_ok"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="@+id/button_ok"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:text="@android:string/ok"
app:layout_constraintStart_toEndOf="@id/button_cancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,15 +5,36 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/cheat_list"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
app:layout_constraintStart_toStartOf="parent"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_cheat_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_cheat_list"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/cheats"
app:navigationIcon="@drawable/ic_back" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/cheat_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
@ -21,7 +42,6 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_add"
android:contentDescription="@string/cheats_add"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />

View 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>

View File

@ -27,6 +27,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:visibility="invisible" />

View File

@ -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" />

View File

@ -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"

View File

@ -83,6 +83,10 @@
<item
android:id="@+id/menu_screen_layout_sidebyside"
android:title="@string/emulation_screen_layout_sidebyside" />
<item
android:id="@+id/menu_screen_layout_hybrid"
android:title="@string/emulation_screen_layout_hybrid" />
</group>
</menu>
</item>

View File

@ -19,6 +19,10 @@
android:id="@+id/menu_screen_layout_sidebyside"
android:title="@string/emulation_screen_layout_sidebyside" />
<item
android:id="@+id/menu_screen_layout_hybrid"
android:title="@string/emulation_screen_layout_hybrid" />
</group>
</menu>

View File

@ -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>

View File

@ -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"

View File

@ -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">

View File

@ -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>

View File

@ -104,6 +104,7 @@
<!-- Input related strings -->
<string name="controller_circlepad">Circle Pad</string>
<string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Hotkeys</string>
<string name="controller_triggers">Triggers</string>
<string name="controller_trigger">Trigger</string>
<string name="controller_dpad">D-Pad</string>
@ -336,10 +337,13 @@
<string name="emulation_screen_layout_portrait">Portrait</string>
<string name="emulation_screen_layout_single">Single Screen</string>
<string name="emulation_screen_layout_sidebyside">Side by Side Screens</string>
<string name="emulation_screen_layout_hybrid">Hybrid Screens</string>
<string name="emulation_cycle_landscape_layouts">Cycle Landscape Layouts</string>
<string name="emulation_swap_screens">Swap Screens</string>
<string name="emulation_touch_overlay_reset">Reset Overlay</string>
<string name="emulation_show_overlay">Show Overlay</string>
<string name="emulation_close_game">Close Game</string>
<string name="emulation_toggle_pause">Toggle Pause</string>
<string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string>
<string name="menu_emulation_amiibo">Amiibo</string>
<string name="menu_emulation_amiibo_load">Load</string>
@ -376,6 +380,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>

Some files were not shown because too many files have changed in this diff Show More