Compare commits
64 Commits
file-watch
...
reslimit
Author | SHA1 | Date | |
---|---|---|---|
8f054c47f6 | |||
a8d869c347 | |||
9da78f6126 | |||
0842ee6d7b | |||
6ec079ede8 | |||
83b329f6e1 | |||
db7b929e47 | |||
dc8425a986 | |||
670e9936a4 | |||
c0ecdb689d | |||
68e6a2185d | |||
09b36c589b | |||
1dc0fa7bb5 | |||
85bd1be852 | |||
c17ec1d1aa | |||
33a1f27a99 | |||
5733c8681e | |||
f8ae41dfe3 | |||
52254537b7 | |||
98f17f8f04 | |||
ca6dae1744 | |||
b6acebcb11 | |||
ba702043f0 | |||
2a4c60c1dd | |||
a1532f813b | |||
26d5727b19 | |||
680e132318 | |||
90a5d989e7 | |||
de40153fa4 | |||
e9936e01c2 | |||
e28c2a390c | |||
63d1830429 | |||
88cc6acb4d | |||
3b31720c4d | |||
f9bbae81aa | |||
1c793deece | |||
d5b50a9fc0 | |||
168f168c33 | |||
312068eebf | |||
5118798c30 | |||
831c9c4a38 | |||
23ca10472a | |||
6f05dd9d1d | |||
19cc8e626b | |||
ceeda05798 | |||
222b1cc0d7 | |||
b74c91457e | |||
1c75d895fc | |||
271218b733 | |||
80213bf88f | |||
fa08df21a5 | |||
80ac6c03b5 | |||
d4f31bc617 | |||
13d02c14e0 | |||
84f9e9a10f | |||
fcc0fd671a | |||
ee372572a6 | |||
7930e1ea86 | |||
4dd6e12e46 | |||
1d4d421097 | |||
3f4b57635e | |||
86566f1c14 | |||
3f1f0aa7c2 | |||
8fe147b8f9 |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: linux
|
||||
TARGET: ${{ matrix.target }}
|
||||
steps:
|
||||
@ -66,7 +66,7 @@ jobs:
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: macos
|
||||
TARGET: ${{ matrix.target }}
|
||||
steps:
|
||||
@ -133,7 +133,7 @@ jobs:
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: windows
|
||||
TARGET: ${{ matrix.target }}
|
||||
steps:
|
||||
@ -188,7 +188,7 @@ jobs:
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: android
|
||||
TARGET: universal
|
||||
steps:
|
||||
@ -239,7 +239,7 @@ jobs:
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: ios
|
||||
TARGET: arm64
|
||||
steps:
|
||||
|
@ -1,4 +1,6 @@
|
||||
|
||||
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
# This function downloads Qt using aqt. The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
|
||||
# Params:
|
||||
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
|
||||
@ -52,16 +54,17 @@ function(download_qt target)
|
||||
get_external_prefix(qt base_path)
|
||||
file(MAKE_DIRECTORY "${base_path}")
|
||||
|
||||
set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini")
|
||||
if (DOWNLOAD_QT_TOOL)
|
||||
set(prefix "${base_path}/Tools")
|
||||
set(install_args install-tool --outputdir ${base_path} ${host} desktop ${target})
|
||||
set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
|
||||
else()
|
||||
set(prefix "${base_path}/${target}/${arch_path}")
|
||||
if (host_arch_path)
|
||||
set(host_flag "--autodesktop")
|
||||
set(host_prefix "${base_path}/${target}/${host_arch_path}")
|
||||
endif()
|
||||
set(install_args install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag}
|
||||
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag}
|
||||
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
||||
endif()
|
||||
|
||||
|
29
CMakeModules/aqt_config.ini
Normal file
29
CMakeModules/aqt_config.ini
Normal file
@ -0,0 +1,29 @@
|
||||
[aqt]
|
||||
concurrency: 2
|
||||
|
||||
[mirrors]
|
||||
trusted_mirrors:
|
||||
https://download.qt.io
|
||||
blacklist:
|
||||
https://qt.mirror.constant.com
|
||||
https://mirrors.ocf.berkeley.edu
|
||||
https://mirrors.ustc.edu.cn
|
||||
https://mirrors.tuna.tsinghua.edu.cn
|
||||
https://mirrors.geekpie.club
|
||||
https://mirrors-wan.geekpie.club
|
||||
https://mirrors.sjtug.sjtu.edu.cn
|
||||
fallbacks:
|
||||
https://qtproject.mirror.liquidtelecom.com/
|
||||
https://mirrors.aliyun.com/qt/
|
||||
https://ftp.jaist.ac.jp/pub/qtproject/
|
||||
https://ftp.yz.yamagata-u.ac.jp/pub/qtproject/
|
||||
https://qt-mirror.dannhauer.de/
|
||||
https://ftp.fau.de/qtproject/
|
||||
https://mirror.netcologne.de/qtproject/
|
||||
https://mirrors.dotsrc.org/qtproject/
|
||||
https://www.nic.funet.fi/pub/mirrors/download.qt-project.org/
|
||||
https://master.qt.io/
|
||||
https://mirrors.ukfast.co.uk/sites/qt.io/
|
||||
https://ftp2.nluug.nl/languages/qt/
|
||||
https://ftp1.nluug.nl/languages/qt/
|
||||
|
2
dist/apple/Info.plist.in
vendored
2
dist/apple/Info.plist.in
vendored
@ -21,6 +21,8 @@
|
||||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||
<!-- Fixed -->
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
|
67
externals/CMakeLists.txt
vendored
67
externals/CMakeLists.txt
vendored
@ -41,9 +41,15 @@ else()
|
||||
endif()
|
||||
|
||||
# Catch2
|
||||
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
||||
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
||||
add_subdirectory(catch2)
|
||||
add_library(catch2 INTERFACE)
|
||||
if(USE_SYSTEM_CATCH2)
|
||||
find_package(Catch2 3.0.0 REQUIRED)
|
||||
else()
|
||||
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
||||
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
||||
add_subdirectory(catch2)
|
||||
endif()
|
||||
target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain)
|
||||
|
||||
# Crypto++
|
||||
if(USE_SYSTEM_CRYPTOPP)
|
||||
@ -327,7 +333,13 @@ if (ENABLE_WEB_SERVICE)
|
||||
endif()
|
||||
|
||||
# lodepng
|
||||
add_subdirectory(lodepng)
|
||||
if(USE_SYSTEM_LODEPNG)
|
||||
add_library(lodepng INTERFACE)
|
||||
find_package(lodepng REQUIRED)
|
||||
target_link_libraries(lodepng INTERFACE lodepng::lodepng)
|
||||
else()
|
||||
add_subdirectory(lodepng)
|
||||
endif()
|
||||
|
||||
# (xperia64): Only use libyuv on Android b/c of build issues on Windows and mandatory JPEG
|
||||
if(ANDROID)
|
||||
@ -338,24 +350,47 @@ endif()
|
||||
|
||||
# OpenAL Soft
|
||||
if (ENABLE_OPENAL)
|
||||
set(ALSOFT_EMBED_HRTF_DATA OFF CACHE BOOL "")
|
||||
set(ALSOFT_EXAMPLES OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_CONFIG OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_HRTF_DATA OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_AMBDEC_PRESETS OFF CACHE BOOL "")
|
||||
set(ALSOFT_UTILS OFF CACHE BOOL "")
|
||||
set(LIBTYPE "STATIC" CACHE STRING "")
|
||||
add_subdirectory(openal-soft EXCLUDE_FROM_ALL)
|
||||
if(USE_SYSTEM_OPENAL)
|
||||
add_library(OpenAL INTERFACE)
|
||||
find_package(OpenAL REQUIRED)
|
||||
target_link_libraries(OpenAL INTERFACE OpenAL::OpenAL)
|
||||
else()
|
||||
set(ALSOFT_EMBED_HRTF_DATA OFF CACHE BOOL "")
|
||||
set(ALSOFT_EXAMPLES OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_CONFIG OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_HRTF_DATA OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_AMBDEC_PRESETS OFF CACHE BOOL "")
|
||||
set(ALSOFT_UTILS OFF CACHE BOOL "")
|
||||
set(LIBTYPE "STATIC" CACHE STRING "")
|
||||
add_subdirectory(openal-soft EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# VMA
|
||||
add_library(vma INTERFACE)
|
||||
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
|
||||
if(USE_SYSTEM_VMA)
|
||||
add_library(vma INTERFACE)
|
||||
find_package(VulkanMemoryAllocator REQUIRED)
|
||||
if(TARGET GPUOpen::VulkanMemoryAllocator)
|
||||
message(STATUS "Found VulkanMemoryAllocator")
|
||||
target_link_libraries(vma INTERFACE GPUOpen::VulkanMemoryAllocator)
|
||||
endif()
|
||||
else()
|
||||
add_library(vma INTERFACE)
|
||||
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
|
||||
endif()
|
||||
|
||||
# vulkan-headers
|
||||
add_library(vulkan-headers INTERFACE)
|
||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
||||
if(USE_SYSTEM_VULKAN_HEADERS)
|
||||
find_package(Vulkan REQUIRED)
|
||||
if(TARGET Vulkan::Headers)
|
||||
message(STATUS "Found Vulkan headers")
|
||||
target_link_libraries(vulkan-headers INTERFACE Vulkan::Headers)
|
||||
endif()
|
||||
else()
|
||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
||||
endif()
|
||||
if (APPLE)
|
||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/MoltenVK)
|
||||
endif()
|
||||
|
@ -21,6 +21,11 @@ option(USE_SYSTEM_ZSTD "Use the system Zstandard library (instead of the bundled
|
||||
option(USE_SYSTEM_ENET "Use the system libenet (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_CRYPTOPP "Use the system cryptopp (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_CUBEB "Use the system cubeb (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_LODEPNG "Use the system lodepng (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_OPENAL "Use the system OpenAL (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_VMA "Use the system VulkanMemoryAllocator (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_VULKAN_HEADERS "Use the system Vulkan headers (instead of the bundled ones)" OFF)
|
||||
option(USE_SYSTEM_CATCH2 "Use the system Catch2 (instead of the bundled one)" OFF)
|
||||
|
||||
# Qt and MoltenVK are handled separately
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SDL2 "Disable system SDL2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
@ -41,6 +46,11 @@ CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_ZSTD "Disable system Zstandard" OFF "USE_S
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_ENET "Disable system libenet" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CRYPTOPP "Disable system cryptopp" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CUBEB "Disable system cubeb" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_LODEPNG "Disable system lodepng" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_OPENAL "Disable system OpenAL" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VMA "Disable system VulkanMemoryAllocator" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VULKAN_HEADERS "Disable system Vulkan headers" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CATCH2 "Disable system Catch2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
|
||||
set(LIB_VAR_LIST
|
||||
SDL2
|
||||
@ -61,6 +71,11 @@ set(LIB_VAR_LIST
|
||||
ENET
|
||||
CRYPTOPP
|
||||
CUBEB
|
||||
LODEPNG
|
||||
OPENAL
|
||||
VMA
|
||||
VULKAN_HEADERS
|
||||
CATCH2
|
||||
)
|
||||
|
||||
# First, check that USE_SYSTEM_XXX is not used with USE_SYSTEM_LIBS
|
||||
|
36
externals/cmake-modules/FindOpenAL.cmake
vendored
Normal file
36
externals/cmake-modules/FindOpenAL.cmake
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
if(NOT OPENAL_FOUND)
|
||||
pkg_check_modules(OPENAL_TMP openal)
|
||||
|
||||
find_path(OPENAL_INCLUDE_DIRS NAMES al.h
|
||||
PATHS
|
||||
${OPENAL_TMP_INCLUDE_DIRS}
|
||||
/usr/include/AL
|
||||
/usr/include
|
||||
/usr/local/include/AL
|
||||
/usr/local/include
|
||||
)
|
||||
|
||||
find_library(OPENAL_LIBRARY_DIRS NAMES openal
|
||||
PATHS
|
||||
${OPENAL_TMP_LIBRARY_DIRS}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
)
|
||||
|
||||
if(OPENAL_INCLUDE_DIRS AND OPENAL_LIBRARY_DIRS)
|
||||
set(OPENAL_FOUND TRUE CACHE INTERNAL "OpenAL found")
|
||||
message(STATUS "Found OpenAL: ${OPENAL_LIBRARY_DIRS}, ${OPENAL_INCLUDE_DIRS}")
|
||||
else()
|
||||
set(OPENAL_FOUND FALSE CACHE INTERNAL "OpenAL found")
|
||||
message(STATUS "OpenAL not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(OPENAL_FOUND AND NOT TARGET OpenAL::OpenAL)
|
||||
add_library(OpenAL::OpenAL UNKNOWN IMPORTED)
|
||||
set_target_properties(OpenAL::OpenAL PROPERTIES
|
||||
INCLUDE_DIRECTORIES ${OPENAL_INCLUDE_DIRS}
|
||||
INTERFACE_LINK_LIBRARIES ${OPENAL_LIBRARY_DIRS}
|
||||
IMPORTED_LOCATION ${OPENAL_LIBRARY_DIRS}
|
||||
)
|
||||
endif()
|
9
externals/cmake-modules/Findcryptopp.cmake
vendored
9
externals/cmake-modules/Findcryptopp.cmake
vendored
@ -1,20 +1,19 @@
|
||||
if(NOT CRYPTOPP_FOUND)
|
||||
pkg_check_modules(CRYPTOPP_TMP libcrypto++)
|
||||
pkg_search_module(CRYPTOPP_TMP crypto++ cryptopp)
|
||||
|
||||
find_path(CRYPTOPP_INCLUDE_DIRS NAMES cryptlib.h
|
||||
PATHS
|
||||
${CRYPTOPP_TMP_INCLUDE_DIRS}
|
||||
/usr/include
|
||||
/usr/include/crypto++
|
||||
/usr/local/include
|
||||
/usr/local/include/crypto++
|
||||
PATH_SUFFIXES crypto++ cryptopp
|
||||
)
|
||||
|
||||
find_library(CRYPTOPP_LIBRARY_DIRS NAMES crypto++
|
||||
find_library(CRYPTOPP_LIBRARY_DIRS NAMES crypto++ cryptopp
|
||||
PATHS
|
||||
${CRYPTOPP_TMP_LIBRARY_DIRS}
|
||||
/usr/lib
|
||||
/usr/locallib
|
||||
/usr/local/lib
|
||||
)
|
||||
|
||||
if(CRYPTOPP_INCLUDE_DIRS AND CRYPTOPP_LIBRARY_DIRS)
|
||||
|
31
externals/cmake-modules/Findlodepng.cmake
vendored
Normal file
31
externals/cmake-modules/Findlodepng.cmake
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
if(NOT LODEPNG_FOUND)
|
||||
find_path(LODEPNG_INCLUDE_DIRS NAMES lodepng.h
|
||||
PATHS
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
)
|
||||
|
||||
find_library(LODEPNG_LIBRARY_DIRS NAMES lodepng
|
||||
PATHS
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
)
|
||||
|
||||
if(LODEPNG_INCLUDE_DIRS AND LODEPNG_LIBRARY_DIRS)
|
||||
set(LODEPNG_FOUND TRUE CACHE INTERNAL "Found lodepng")
|
||||
message(STATUS "Found lodepng: ${LODEPNG_LIBRARY_DIRS}, ${LODEPNG_INCLUDE_DIRS}")
|
||||
else()
|
||||
set(LODEPNG_FOUND FALSE CACHE INTERNAL "Found lodepng")
|
||||
message(STATUS "Lodepng not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(LODEPNG_FOUND AND NOT TARGET lodepng::lodepng)
|
||||
add_library(lodepng::lodepng UNKNOWN IMPORTED)
|
||||
set_target_properties(lodepng::lodepng PROPERTIES
|
||||
INCLUDE_DIRECTORIES ${LODEPNG_INCLUDE_DIRS}
|
||||
INTERFACE_LINK_LIBRARIES ${LODEPNG_LIBRARY_DIRS}
|
||||
IMPORTED_LOCATION ${LODEPNG_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
endif()
|
26
externals/faad2/CMakeLists.txt
vendored
26
externals/faad2/CMakeLists.txt
vendored
@ -1,18 +1,5 @@
|
||||
# Copy source to build directory for some modifications.
|
||||
set(FAAD2_SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/faad2/libfaad")
|
||||
if (NOT EXISTS "${FAAD2_SOURCE_DIR}")
|
||||
file(COPY faad2/libfaad/ DESTINATION "${FAAD2_SOURCE_DIR}/")
|
||||
|
||||
# These are fixed defines for some reason and not controllable with compile flags.
|
||||
file(READ "${FAAD2_SOURCE_DIR}/common.h" FAAD2_COMMON_H)
|
||||
# Disable SBR decoding since we don't want it for AAC-LC.
|
||||
string(REGEX REPLACE "#define SBR_DEC" "" FAAD2_COMMON_H "${FAAD2_COMMON_H}")
|
||||
# Disable PS decoding. This can cause mono to be upmixed to stereo, which we don't want.
|
||||
string(REGEX REPLACE "#define PS_DEC" "" FAAD2_COMMON_H "${FAAD2_COMMON_H}")
|
||||
file(WRITE "${FAAD2_SOURCE_DIR}/common.h" "${FAAD2_COMMON_H}")
|
||||
endif()
|
||||
|
||||
# Source list from faad2/libfaad/Makefile.am, cut down to just what we need for AAC-LC.
|
||||
# Sources cut down to just what we need for AAC-LC.
|
||||
set(FAAD2_SOURCE_DIR "faad2/libfaad")
|
||||
add_library(faad2 STATIC EXCLUDE_FROM_ALL
|
||||
"${FAAD2_SOURCE_DIR}/bits.c"
|
||||
"${FAAD2_SOURCE_DIR}/cfft.c"
|
||||
@ -37,10 +24,9 @@ target_include_directories(faad2 PUBLIC faad2/include PRIVATE "${FAAD2_SOURCE_DI
|
||||
|
||||
# Configure compile definitions.
|
||||
|
||||
# Read version from autoconf script for configuring constant.
|
||||
file(READ faad2/configure.ac CONFIGURE_SCRIPT)
|
||||
string(REGEX MATCH "AC_INIT\\(faad2, ([0-9.]+)\\)" _ ${CONFIGURE_SCRIPT})
|
||||
set(FAAD_VERSION ${CMAKE_MATCH_1})
|
||||
# Read version from properties file for configuring constant.
|
||||
file(READ faad2/properties.json FAAD_PROPERTIES_JSON)
|
||||
string(JSON FAAD_VERSION GET ${FAAD_PROPERTIES_JSON} PACKAGE_VERSION)
|
||||
message(STATUS "Building faad2 version ${FAAD_VERSION}")
|
||||
|
||||
# Check for functions and headers.
|
||||
@ -98,5 +84,5 @@ target_compile_definitions(faad2 PRIVATE
|
||||
-DHAVE_SYS_TYPES_H=${HAVE_SYS_TYPES_H}
|
||||
-DHAVE_UNISTD_H=${HAVE_UNISTD_H}
|
||||
# Only compile for AAC-LC decoding.
|
||||
-DLC_ONLY_DECODER
|
||||
-DLC_ONLY_DECODER -DDISABLE_SBR
|
||||
)
|
||||
|
2
externals/faad2/faad2
vendored
2
externals/faad2/faad2
vendored
Submodule externals/faad2/faad2 updated: 3918dee560...09b3c850c6
6
externals/glad/Readme.md
vendored
6
externals/glad/Readme.md
vendored
@ -3,3 +3,9 @@ These files were generated by the [glad](https://github.com/Dav1dde/glad) OpenGL
|
||||
```
|
||||
python -m glad --profile core --out-path glad/ --api "gl=4.3,gles2=3.2" --generator=c
|
||||
```
|
||||
|
||||
You can also generate the source using [this site](https://glad.dav1d.de/):
|
||||
1. Select '4.3' for GL, '3.2' for GLES2, and 'Core' for Profile.
|
||||
2. Input the currently supported extensions from [here](https://github.com/citra-emu/citra/blob/master/externals/glad/include/glad/glad.h#L9), plus any new required extensions.
|
||||
3. Click Generate and download the generated source zip.
|
||||
4. Unzip the new source over the current glad source files.
|
27
externals/glad/include/glad/glad.h
vendored
27
externals/glad/include/glad/glad.h
vendored
@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
OpenGL, OpenGL ES loader generated by glad 0.1.34 on Sat Aug 26 18:38:43 2023.
|
||||
OpenGL, OpenGL ES loader generated by glad 0.1.36 on Fri Nov 10 04:24:01 2023.
|
||||
|
||||
Language/Generator: C/C++
|
||||
Specification: gl
|
||||
@ -10,6 +10,7 @@
|
||||
GL_AMD_blend_minmax_factor,
|
||||
GL_ARB_buffer_storage,
|
||||
GL_ARB_clear_texture,
|
||||
GL_ARB_fragment_shader_interlock,
|
||||
GL_ARB_get_texture_sub_image,
|
||||
GL_ARB_texture_compression_bptc,
|
||||
GL_ARM_shader_framebuffer_fetch,
|
||||
@ -17,16 +18,18 @@
|
||||
GL_EXT_clip_cull_distance,
|
||||
GL_EXT_shader_framebuffer_fetch,
|
||||
GL_EXT_texture_compression_s3tc,
|
||||
GL_NV_blend_minmax_factor
|
||||
GL_INTEL_fragment_shader_ordering,
|
||||
GL_NV_blend_minmax_factor,
|
||||
GL_NV_fragment_shader_interlock
|
||||
Loader: True
|
||||
Local files: False
|
||||
Omit khrplatform: False
|
||||
Reproducible: False
|
||||
|
||||
Commandline:
|
||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_NV_blend_minmax_factor"
|
||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_fragment_shader_interlock,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_INTEL_fragment_shader_ordering,GL_NV_blend_minmax_factor,GL_NV_fragment_shader_interlock"
|
||||
Online:
|
||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_NV_blend_minmax_factor
|
||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_fragment_shader_interlock&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_INTEL_fragment_shader_ordering&extensions=GL_NV_blend_minmax_factor&extensions=GL_NV_fragment_shader_interlock
|
||||
*/
|
||||
|
||||
|
||||
@ -3384,6 +3387,10 @@ typedef void (APIENTRYP PFNGLCLEARTEXSUBIMAGEPROC)(GLuint texture, GLint level,
|
||||
GLAPI PFNGLCLEARTEXSUBIMAGEPROC glad_glClearTexSubImage;
|
||||
#define glClearTexSubImage glad_glClearTexSubImage
|
||||
#endif
|
||||
#ifndef GL_ARB_fragment_shader_interlock
|
||||
#define GL_ARB_fragment_shader_interlock 1
|
||||
GLAPI int GLAD_GL_ARB_fragment_shader_interlock;
|
||||
#endif
|
||||
#ifndef GL_ARB_get_texture_sub_image
|
||||
#define GL_ARB_get_texture_sub_image 1
|
||||
GLAPI int GLAD_GL_ARB_get_texture_sub_image;
|
||||
@ -3406,10 +3413,18 @@ GLAPI int GLAD_GL_EXT_shader_framebuffer_fetch;
|
||||
#define GL_EXT_texture_compression_s3tc 1
|
||||
GLAPI int GLAD_GL_EXT_texture_compression_s3tc;
|
||||
#endif
|
||||
#ifndef GL_INTEL_fragment_shader_ordering
|
||||
#define GL_INTEL_fragment_shader_ordering 1
|
||||
GLAPI int GLAD_GL_INTEL_fragment_shader_ordering;
|
||||
#endif
|
||||
#ifndef GL_NV_blend_minmax_factor
|
||||
#define GL_NV_blend_minmax_factor 1
|
||||
GLAPI int GLAD_GL_NV_blend_minmax_factor;
|
||||
#endif
|
||||
#ifndef GL_NV_fragment_shader_interlock
|
||||
#define GL_NV_fragment_shader_interlock 1
|
||||
GLAPI int GLAD_GL_NV_fragment_shader_interlock;
|
||||
#endif
|
||||
#ifndef GL_ARM_shader_framebuffer_fetch
|
||||
#define GL_ARM_shader_framebuffer_fetch 1
|
||||
GLAPI int GLAD_GL_ARM_shader_framebuffer_fetch;
|
||||
@ -3437,6 +3452,10 @@ GLAPI int GLAD_GL_EXT_texture_compression_s3tc;
|
||||
#define GL_NV_blend_minmax_factor 1
|
||||
GLAPI int GLAD_GL_NV_blend_minmax_factor;
|
||||
#endif
|
||||
#ifndef GL_NV_fragment_shader_interlock
|
||||
#define GL_NV_fragment_shader_interlock 1
|
||||
GLAPI int GLAD_GL_NV_fragment_shader_interlock;
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
18
externals/glad/src/glad.c
vendored
18
externals/glad/src/glad.c
vendored
@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
OpenGL, OpenGL ES loader generated by glad 0.1.34 on Sat Aug 26 18:38:43 2023.
|
||||
OpenGL, OpenGL ES loader generated by glad 0.1.36 on Fri Nov 10 04:24:01 2023.
|
||||
|
||||
Language/Generator: C/C++
|
||||
Specification: gl
|
||||
@ -10,6 +10,7 @@
|
||||
GL_AMD_blend_minmax_factor,
|
||||
GL_ARB_buffer_storage,
|
||||
GL_ARB_clear_texture,
|
||||
GL_ARB_fragment_shader_interlock,
|
||||
GL_ARB_get_texture_sub_image,
|
||||
GL_ARB_texture_compression_bptc,
|
||||
GL_ARM_shader_framebuffer_fetch,
|
||||
@ -17,16 +18,18 @@
|
||||
GL_EXT_clip_cull_distance,
|
||||
GL_EXT_shader_framebuffer_fetch,
|
||||
GL_EXT_texture_compression_s3tc,
|
||||
GL_NV_blend_minmax_factor
|
||||
GL_INTEL_fragment_shader_ordering,
|
||||
GL_NV_blend_minmax_factor,
|
||||
GL_NV_fragment_shader_interlock
|
||||
Loader: True
|
||||
Local files: False
|
||||
Omit khrplatform: False
|
||||
Reproducible: False
|
||||
|
||||
Commandline:
|
||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_NV_blend_minmax_factor"
|
||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_fragment_shader_interlock,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_INTEL_fragment_shader_ordering,GL_NV_blend_minmax_factor,GL_NV_fragment_shader_interlock"
|
||||
Online:
|
||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_NV_blend_minmax_factor
|
||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_fragment_shader_interlock&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_INTEL_fragment_shader_ordering&extensions=GL_NV_blend_minmax_factor&extensions=GL_NV_fragment_shader_interlock
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
@ -860,6 +863,7 @@ PFNGLWAITSYNCPROC glad_glWaitSync = NULL;
|
||||
int GLAD_GL_AMD_blend_minmax_factor = 0;
|
||||
int GLAD_GL_ARB_buffer_storage = 0;
|
||||
int GLAD_GL_ARB_clear_texture = 0;
|
||||
int GLAD_GL_ARB_fragment_shader_interlock = 0;
|
||||
int GLAD_GL_ARB_get_texture_sub_image = 0;
|
||||
int GLAD_GL_ARB_texture_compression_bptc = 0;
|
||||
int GLAD_GL_ARM_shader_framebuffer_fetch = 0;
|
||||
@ -867,7 +871,9 @@ int GLAD_GL_EXT_buffer_storage = 0;
|
||||
int GLAD_GL_EXT_clip_cull_distance = 0;
|
||||
int GLAD_GL_EXT_shader_framebuffer_fetch = 0;
|
||||
int GLAD_GL_EXT_texture_compression_s3tc = 0;
|
||||
int GLAD_GL_INTEL_fragment_shader_ordering = 0;
|
||||
int GLAD_GL_NV_blend_minmax_factor = 0;
|
||||
int GLAD_GL_NV_fragment_shader_interlock = 0;
|
||||
PFNGLBUFFERSTORAGEPROC glad_glBufferStorage = NULL;
|
||||
PFNGLCLEARTEXIMAGEPROC glad_glClearTexImage = NULL;
|
||||
PFNGLCLEARTEXSUBIMAGEPROC glad_glClearTexSubImage = NULL;
|
||||
@ -1509,11 +1515,14 @@ static int find_extensionsGL(void) {
|
||||
GLAD_GL_AMD_blend_minmax_factor = has_ext("GL_AMD_blend_minmax_factor");
|
||||
GLAD_GL_ARB_buffer_storage = has_ext("GL_ARB_buffer_storage");
|
||||
GLAD_GL_ARB_clear_texture = has_ext("GL_ARB_clear_texture");
|
||||
GLAD_GL_ARB_fragment_shader_interlock = has_ext("GL_ARB_fragment_shader_interlock");
|
||||
GLAD_GL_ARB_get_texture_sub_image = has_ext("GL_ARB_get_texture_sub_image");
|
||||
GLAD_GL_ARB_texture_compression_bptc = has_ext("GL_ARB_texture_compression_bptc");
|
||||
GLAD_GL_EXT_shader_framebuffer_fetch = has_ext("GL_EXT_shader_framebuffer_fetch");
|
||||
GLAD_GL_EXT_texture_compression_s3tc = has_ext("GL_EXT_texture_compression_s3tc");
|
||||
GLAD_GL_INTEL_fragment_shader_ordering = has_ext("GL_INTEL_fragment_shader_ordering");
|
||||
GLAD_GL_NV_blend_minmax_factor = has_ext("GL_NV_blend_minmax_factor");
|
||||
GLAD_GL_NV_fragment_shader_interlock = has_ext("GL_NV_fragment_shader_interlock");
|
||||
free_exts();
|
||||
return 1;
|
||||
}
|
||||
@ -1988,6 +1997,7 @@ static int find_extensionsGLES2(void) {
|
||||
GLAD_GL_EXT_shader_framebuffer_fetch = has_ext("GL_EXT_shader_framebuffer_fetch");
|
||||
GLAD_GL_EXT_texture_compression_s3tc = has_ext("GL_EXT_texture_compression_s3tc");
|
||||
GLAD_GL_NV_blend_minmax_factor = has_ext("GL_NV_blend_minmax_factor");
|
||||
GLAD_GL_NV_fragment_shader_interlock = has_ext("GL_NV_fragment_shader_interlock");
|
||||
free_exts();
|
||||
return 1;
|
||||
}
|
||||
|
BIN
keys.tar.enc
BIN
keys.tar.enc
Binary file not shown.
@ -51,6 +51,10 @@ if (MSVC)
|
||||
/Zc:throwingNew
|
||||
/GT
|
||||
|
||||
# Some flags for more deterministic builds, to aid caching.
|
||||
/experimental:deterministic
|
||||
/d1trimfile:"${CMAKE_SOURCE_DIR}"
|
||||
|
||||
# External headers diagnostics
|
||||
/experimental:external # Enables the external headers options. This option isn't required in Visual Studio 2019 version 16.10 and later
|
||||
/external:anglebrackets # Treats all headers included by #include <header>, where the header file is enclosed in angle brackets (< >), as external headers
|
||||
@ -87,7 +91,8 @@ if (MSVC)
|
||||
|
||||
# Since MSVC's debugging information is not very deterministic, so we have to disable it
|
||||
# when using ccache or other caching tools
|
||||
if (CITRA_USE_CCACHE OR CITRA_USE_PRECOMPILED_HEADERS)
|
||||
if (CMAKE_C_COMPILER_LAUNCHER STREQUAL "ccache" OR CMAKE_CXX_COMPILER_LAUNCHER STREQUAL "ccache"
|
||||
OR CITRA_USE_PRECOMPILED_HEADERS)
|
||||
# Precompiled headers are deleted if not using /Z7. See https://github.com/nanoant/CMakePCHCompiler/issues/21
|
||||
add_compile_options(/Z7)
|
||||
else()
|
||||
@ -98,11 +103,17 @@ if (MSVC)
|
||||
add_compile_options("$<$<CONFIG:Release>:/GS->")
|
||||
|
||||
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG /MANIFEST:NO" CACHE STRING "" FORCE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF /PDBALTPATH:%_PDB%" CACHE STRING "" FORCE)
|
||||
else()
|
||||
add_compile_options(
|
||||
-Wall
|
||||
-Wno-attributes
|
||||
# In case a flag isn't supported on e.g. a certain architecture, don't error.
|
||||
-Wno-unused-command-line-argument
|
||||
# Build fortification options
|
||||
-Wp,-D_FORTIFY_SOURCE=2
|
||||
-Wp,-D_GLIBCXX_ASSERTIONS
|
||||
-fstack-protector-strong
|
||||
-fstack-clash-protection
|
||||
)
|
||||
|
||||
if (CITRA_WARNINGS_AS_ERRORS)
|
||||
|
@ -2,15 +2,18 @@
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
import android.databinding.tool.ext.capitalizeUS
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("de.undercouch.download") version "5.5.0"
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization") version "1.8.21"
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
}
|
||||
|
||||
import android.databinding.tool.ext.capitalizeUS
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
|
||||
/**
|
||||
* Use the number of seconds/10 since Jan 1 2016 as the versionCode.
|
||||
* This lets us upload a new build at most every 10 seconds for the
|
||||
@ -25,7 +28,7 @@ val downloadedJniLibsPath = "${buildDir}/downloadedJniLibs"
|
||||
android {
|
||||
namespace = "org.citra.citra_emu"
|
||||
|
||||
compileSdkVersion = "android-33"
|
||||
compileSdkVersion = "android-34"
|
||||
ndkVersion = "25.2.9519653"
|
||||
|
||||
compileOptions {
|
||||
@ -37,6 +40,11 @@ android {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
packaging {
|
||||
// This is necessary for libadrenotools custom driver loading
|
||||
jniLibs.useLegacyPackaging = true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
@ -51,7 +59,7 @@ android {
|
||||
// TODO If this is ever modified, change application_id in strings.xml
|
||||
applicationId = "org.citra.citra_emu"
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
targetSdk = 34
|
||||
versionCode = autoVersion
|
||||
versionName = getGitVersion()
|
||||
|
||||
@ -69,6 +77,9 @@ android {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
|
||||
buildConfigField("String", "BRANCH", "\"${getBranch()}\"")
|
||||
}
|
||||
|
||||
val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE")
|
||||
@ -92,6 +103,12 @@ android {
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
|
||||
// builds a release build that doesn't need signing
|
||||
@ -101,9 +118,15 @@ android {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
isDefault = true
|
||||
}
|
||||
|
||||
// Signed by debug key disallowing distribution on Play Store.
|
||||
@ -145,8 +168,9 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.activity:activity-ktx:1.7.2")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.activity:activity-ktx:1.8.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||
@ -158,15 +182,14 @@ dependencies {
|
||||
// For loading huge screenshots from the disk.
|
||||
implementation("com.squareup.picasso:picasso:2.71828")
|
||||
|
||||
// Allows FRP-style asynchronous operations in Android.
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
|
||||
implementation("org.ini4j:ini4j:0.5.4")
|
||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
|
||||
// Please don't upgrade the billing library as the newer version is not GPL-compatible
|
||||
implementation("com.android.billingclient:billing:2.0.3")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
|
||||
implementation("info.debatty:java-string-similarity:2.0.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("io.coil-kt:coil:2.2.2")
|
||||
}
|
||||
|
||||
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
||||
@ -216,6 +239,34 @@ fun getGitVersion(): String {
|
||||
return versionName
|
||||
}
|
||||
|
||||
fun getGitHash(): String =
|
||||
runGitCommand(ProcessBuilder("git", "rev-parse", "--short", "HEAD")) ?: "dummy-hash"
|
||||
|
||||
fun getBranch(): String =
|
||||
runGitCommand(ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")) ?: "dummy-branch"
|
||||
|
||||
fun runGitCommand(command: ProcessBuilder) : String? {
|
||||
try {
|
||||
command.directory(project.rootDir)
|
||||
val process = command.start()
|
||||
val inputStream = process.inputStream
|
||||
val errorStream = process.errorStream
|
||||
process.waitFor()
|
||||
|
||||
return if (process.exitValue() == 0) {
|
||||
inputStream.bufferedReader()
|
||||
.use { it.readText().trim() } // return the value of gitHash
|
||||
} else {
|
||||
val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
|
||||
logger.error("Error running git command: $errorMessage")
|
||||
return null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$e: Cannot find git")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.configureEach {
|
||||
val variant = this
|
||||
val capitalizedName = variant.name.capitalizeUS()
|
||||
|
40
src/android/app/proguard-rules.pro
vendored
40
src/android/app/proguard-rules.pro
vendored
@ -1,21 +1,25 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
# Copyright 2023 Citra Emulator Project
|
||||
# Licensed under GPLv2 or any later version
|
||||
# Refer to the license.txt file included.
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
# To get usable stack traces
|
||||
-dontobfuscate
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
# Prevents crashing when using Wini
|
||||
-keep class org.ini4j.spi.IniParser
|
||||
-keep class org.ini4j.spi.IniBuilder
|
||||
-keep class org.ini4j.spi.IniFormatter
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# Suppress warnings for R8
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||
-dontwarn org.conscrypt.Conscrypt$Version
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
-dontwarn java.beans.Introspector
|
||||
-dontwarn java.beans.VetoableChangeListener
|
||||
-dontwarn java.beans.VetoableChangeSupport
|
||||
|
@ -29,6 +29,7 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
@ -44,8 +45,7 @@
|
||||
<activity
|
||||
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
||||
android:theme="@style/Theme.Citra.Splash.Main"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="false">
|
||||
android:exported="true">
|
||||
|
||||
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
|
||||
<intent-filter>
|
||||
@ -68,21 +68,15 @@
|
||||
android:theme="@style/Theme.Citra.Main"
|
||||
android:launchMode="singleTop"/>
|
||||
|
||||
<service android:name="org.citra.citra_emu.utils.ForegroundService"/>
|
||||
<service android:name="org.citra.citra_emu.utils.ForegroundService" android:foregroundServiceType="specialUse">
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Citra.Main"
|
||||
android:label="@string/cheats"/>
|
||||
|
||||
|
||||
<provider
|
||||
android:name="org.citra.citra_emu.model.GameProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -1,76 +0,0 @@
|
||||
// Copyright 2019 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.DocumentsTree;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
public class CitraApplication extends Application {
|
||||
public static GameDatabase databaseHelper;
|
||||
public static DocumentsTree documentsTree;
|
||||
private static CitraApplication application;
|
||||
|
||||
private void createNotificationChannel() {
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
{
|
||||
// General notification
|
||||
CharSequence name = getString(R.string.app_notification_channel_name);
|
||||
String description = getString(R.string.app_notification_channel_description);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
getString(R.string.app_notification_channel_id), name,
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription(description);
|
||||
channel.setSound(null, null);
|
||||
channel.setVibrationPattern(null);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
{
|
||||
// CIA Install notifications
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
getString(R.string.cia_install_notification_channel_id),
|
||||
getString(R.string.cia_install_notification_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.setDescription(getString(R.string.cia_install_notification_channel_description));
|
||||
channel.setSound(null, null);
|
||||
channel.setVibrationPattern(null);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
application = this;
|
||||
documentsTree = new DocumentsTree();
|
||||
|
||||
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
|
||||
DirectoryInitialization.start(getApplicationContext());
|
||||
}
|
||||
|
||||
NativeLibrary.LogDeviceInfo();
|
||||
createNotificationChannel();
|
||||
|
||||
databaseHelper = new GameDatabase(this);
|
||||
}
|
||||
|
||||
public static Context getAppContext() {
|
||||
return application.getApplicationContext();
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||
import org.citra.citra_emu.utils.DocumentsTree
|
||||
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||
import org.citra.citra_emu.utils.PermissionsHandler
|
||||
|
||||
class CitraApplication : Application() {
|
||||
private fun createNotificationChannel() {
|
||||
with(getSystemService(NotificationManager::class.java)) {
|
||||
// General notification
|
||||
val name: CharSequence = getString(R.string.app_notification_channel_name)
|
||||
val description = getString(R.string.app_notification_channel_description)
|
||||
val generalChannel = NotificationChannel(
|
||||
getString(R.string.app_notification_channel_id),
|
||||
name,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
generalChannel.description = description
|
||||
generalChannel.setSound(null, null)
|
||||
generalChannel.vibrationPattern = null
|
||||
createNotificationChannel(generalChannel)
|
||||
|
||||
// CIA Install notifications
|
||||
val ciaChannel = NotificationChannel(
|
||||
getString(R.string.cia_install_notification_channel_id),
|
||||
getString(R.string.cia_install_notification_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
ciaChannel.description =
|
||||
getString(R.string.cia_install_notification_channel_description)
|
||||
ciaChannel.setSound(null, null)
|
||||
ciaChannel.vibrationPattern = null
|
||||
createNotificationChannel(ciaChannel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
application = this
|
||||
documentsTree = DocumentsTree()
|
||||
if (PermissionsHandler.hasWriteAccess(applicationContext)) {
|
||||
DirectoryInitialization.start()
|
||||
}
|
||||
|
||||
NativeLibrary.logDeviceInfo()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var application: CitraApplication? = null
|
||||
|
||||
val appContext: Context get() = application!!.applicationContext
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var documentsTree: DocumentsTree
|
||||
}
|
||||
}
|
@ -1,720 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Dolphin Emulator Project
|
||||
* Licensed under GPLv2+
|
||||
* Refer to the license.txt file included.
|
||||
*/
|
||||
|
||||
package org.citra.citra_emu;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.Surface;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.applets.SoftwareKeyboard;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import static android.Manifest.permission.CAMERA;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
/**
|
||||
* Class which contains methods that interact
|
||||
* with the native side of the Citra code.
|
||||
*/
|
||||
public final class NativeLibrary {
|
||||
/**
|
||||
* Default touchscreen device
|
||||
*/
|
||||
public static final String TouchScreenDevice = "Touchscreen";
|
||||
public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null);
|
||||
|
||||
private static boolean alertResult = false;
|
||||
private static String alertPromptResult = "";
|
||||
private static int alertPromptButton = 0;
|
||||
private static final Object alertPromptLock = new Object();
|
||||
private static boolean alertPromptInProgress = false;
|
||||
private static String alertPromptCaption = "";
|
||||
private static int alertPromptButtonConfig = 0;
|
||||
private static EditText alertPromptEditText = null;
|
||||
|
||||
static {
|
||||
try {
|
||||
System.loadLibrary("citra-android");
|
||||
} catch (UnsatisfiedLinkError ex) {
|
||||
Log.error("[NativeLibrary] " + ex.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private NativeLibrary() {
|
||||
// Disallows instantiation.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles button press events for a gamepad.
|
||||
*
|
||||
* @param Device The input descriptor of the gamepad.
|
||||
* @param Button Key code identifying which button was pressed.
|
||||
* @param Action Mask identifying which action is happening (button pressed down, or button released).
|
||||
* @return If we handled the button press.
|
||||
*/
|
||||
public static native boolean onGamePadEvent(String Device, int Button, int Action);
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param Device The device ID of the gamepad.
|
||||
* @param Axis The axis ID
|
||||
* @param x_axis The value of the x-axis represented by the given ID.
|
||||
* @param y_axis The value of the y-axis represented by the given ID
|
||||
*/
|
||||
public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis);
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param Device The device ID of the gamepad.
|
||||
* @param Axis_id The axis ID
|
||||
* @param axis_val The value of the axis represented by the given ID.
|
||||
*/
|
||||
public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val);
|
||||
|
||||
/**
|
||||
* Handles touch events.
|
||||
*
|
||||
* @param x_axis The value of the x-axis.
|
||||
* @param y_axis The value of the y-axis
|
||||
* @param pressed To identify if the touch held down or released.
|
||||
* @return true if the pointer is within the touchscreen
|
||||
*/
|
||||
public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed);
|
||||
|
||||
/**
|
||||
* Handles touch movement.
|
||||
*
|
||||
* @param x_axis The value of the instantaneous x-axis.
|
||||
* @param y_axis The value of the instantaneous y-axis.
|
||||
*/
|
||||
public static native void onTouchMoved(float x_axis, float y_axis);
|
||||
|
||||
public static native void ReloadSettings();
|
||||
|
||||
public static native String GetUserSetting(String gameID, String Section, String Key);
|
||||
|
||||
public static native void SetUserSetting(String gameID, String Section, String Key, String Value);
|
||||
|
||||
public static native void InitGameIni(String gameID);
|
||||
|
||||
public static native long GetTitleId(String filename);
|
||||
|
||||
public static native String GetGitRevision();
|
||||
|
||||
/**
|
||||
* Sets the current working user directory
|
||||
* If not set, it auto-detects a location
|
||||
*/
|
||||
public static native void SetUserDirectory(String directory);
|
||||
|
||||
public static native String[] GetInstalledGamePaths();
|
||||
|
||||
// Create the config.ini file.
|
||||
public static native void CreateConfigFile();
|
||||
|
||||
public static native void CreateLogFile();
|
||||
|
||||
public static native void LogUserDirectory(String directory);
|
||||
|
||||
public static native int DefaultCPUCore();
|
||||
|
||||
/**
|
||||
* Begins emulation.
|
||||
*/
|
||||
public static native void Run(String path);
|
||||
|
||||
public static native String[] GetTextureFilterNames();
|
||||
|
||||
/**
|
||||
* Begins emulation from the specified savestate.
|
||||
*/
|
||||
public static native void Run(String path, String savestatePath, boolean deleteSavestate);
|
||||
|
||||
// Surface Handling
|
||||
public static native void SurfaceChanged(Surface surf);
|
||||
|
||||
public static native void SurfaceDestroyed();
|
||||
|
||||
public static native void DoFrame();
|
||||
|
||||
/**
|
||||
* Unpauses emulation from a paused state.
|
||||
*/
|
||||
public static native void UnPauseEmulation();
|
||||
|
||||
/**
|
||||
* Pauses emulation.
|
||||
*/
|
||||
public static native void PauseEmulation();
|
||||
|
||||
/**
|
||||
* Stops emulation.
|
||||
*/
|
||||
public static native void StopEmulation();
|
||||
|
||||
/**
|
||||
* Returns true if emulation is running (or is paused).
|
||||
*/
|
||||
public static native boolean IsRunning();
|
||||
|
||||
/**
|
||||
* Returns the title ID of the currently running title, or 0 on failure.
|
||||
*/
|
||||
public static native long GetRunningTitleId();
|
||||
|
||||
/**
|
||||
* Returns the performance stats for the current game
|
||||
**/
|
||||
public static native double[] GetPerfStats();
|
||||
|
||||
/**
|
||||
* Notifies the core emulation that the orientation has changed.
|
||||
*/
|
||||
public static native void NotifyOrientationChange(int layout_option, int rotation);
|
||||
|
||||
/**
|
||||
* Swaps the top and bottom screens.
|
||||
*/
|
||||
public static native void SwapScreens(boolean swap_screens, int rotation);
|
||||
|
||||
public enum CoreError {
|
||||
ErrorSystemFiles,
|
||||
ErrorSavestate,
|
||||
ErrorUnknown,
|
||||
}
|
||||
|
||||
private static boolean coreErrorAlertResult = false;
|
||||
private static final Object coreErrorAlertLock = new Object();
|
||||
|
||||
public static class CoreErrorDialogFragment extends DialogFragment {
|
||||
static CoreErrorDialogFragment newInstance(String title, String message) {
|
||||
CoreErrorDialogFragment frag = new CoreErrorDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("title", title);
|
||||
args.putString("message", message);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity emulationActivity = Objects.requireNonNull(getActivity());
|
||||
|
||||
final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
|
||||
final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
|
||||
|
||||
return new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.continue_button, (dialog, which) -> {
|
||||
coreErrorAlertResult = true;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.abort_button, (dialog, which) -> {
|
||||
coreErrorAlertResult = false;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
}).setOnDismissListener(dialog -> {
|
||||
coreErrorAlertResult = true;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
}).create();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnCoreErrorImpl(String title, String message) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return;
|
||||
}
|
||||
|
||||
CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message);
|
||||
fragment.show(emulationActivity.getSupportFragmentManager(), "coreError");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a core error.
|
||||
* @return true: continue; false: abort
|
||||
*/
|
||||
public static boolean OnCoreError(CoreError error, String details) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
|
||||
String title, message;
|
||||
switch (error) {
|
||||
case ErrorSystemFiles: {
|
||||
title = emulationActivity.getString(R.string.system_archive_not_found);
|
||||
message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details);
|
||||
break;
|
||||
}
|
||||
case ErrorSavestate: {
|
||||
title = emulationActivity.getString(R.string.save_load_error);
|
||||
message = details;
|
||||
break;
|
||||
}
|
||||
case ErrorUnknown: {
|
||||
title = emulationActivity.getString(R.string.fatal_error);
|
||||
message = emulationActivity.getString(R.string.fatal_error_message);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message));
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized (coreErrorAlertLock) {
|
||||
try {
|
||||
coreErrorAlertLock.wait();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return coreErrorAlertResult;
|
||||
}
|
||||
|
||||
public static boolean isPortraitMode() {
|
||||
return CitraApplication.getAppContext().getResources().getConfiguration().orientation ==
|
||||
Configuration.ORIENTATION_PORTRAIT;
|
||||
}
|
||||
|
||||
public static int landscapeScreenLayout() {
|
||||
return EmulationMenuSettings.getLandscapeScreenLayout();
|
||||
}
|
||||
|
||||
public static boolean displayAlertMsg(final String caption, final String text,
|
||||
final boolean yesNo) {
|
||||
Log.error("[NativeLibrary] Alert: " + text);
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
boolean result = false;
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.");
|
||||
} else {
|
||||
// Create object used for waiting.
|
||||
final Object lock = new Object();
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(caption)
|
||||
.setMessage(text);
|
||||
|
||||
// If not yes/no dialog just have one button that dismisses modal,
|
||||
// otherwise have a yes and no button that sets alertResult accordingly.
|
||||
if (!yesNo) {
|
||||
builder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) ->
|
||||
{
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alertResult = false;
|
||||
|
||||
builder
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
|
||||
{
|
||||
alertResult = true;
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, (dialog, whichButton) ->
|
||||
{
|
||||
alertResult = false;
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread(builder::show);
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (yesNo)
|
||||
result = alertResult;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void retryDisplayAlertPrompt() {
|
||||
if (!alertPromptInProgress) {
|
||||
return;
|
||||
}
|
||||
displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show();
|
||||
}
|
||||
|
||||
public static String displayAlertPrompt(String caption, String text, int buttonConfig) {
|
||||
alertPromptCaption = caption;
|
||||
alertPromptButtonConfig = buttonConfig;
|
||||
alertPromptInProgress = true;
|
||||
|
||||
// Show the AlertDialog on the main thread
|
||||
sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show());
|
||||
|
||||
// Wait for the lock to notify that it is complete
|
||||
synchronized (alertPromptLock) {
|
||||
try {
|
||||
alertPromptLock.wait();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
alertPromptInProgress = false;
|
||||
|
||||
return alertPromptResult;
|
||||
}
|
||||
|
||||
public static MaterialAlertDialogBuilder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
alertPromptResult = "";
|
||||
alertPromptButton = 0;
|
||||
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
|
||||
// Set up the input
|
||||
alertPromptEditText = new EditText(CitraApplication.getAppContext());
|
||||
alertPromptEditText.setText(text);
|
||||
alertPromptEditText.setSingleLine();
|
||||
alertPromptEditText.setLayoutParams(params);
|
||||
|
||||
FrameLayout container = new FrameLayout(emulationActivity);
|
||||
container.addView(alertPromptEditText);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(caption)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
|
||||
{
|
||||
alertPromptButton = buttonConfig;
|
||||
alertPromptResult = alertPromptEditText.getText().toString();
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
})
|
||||
.setOnDismissListener(dialogInterface ->
|
||||
{
|
||||
alertPromptResult = "";
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
});
|
||||
|
||||
if (buttonConfig > 0) {
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
|
||||
{
|
||||
alertPromptResult = "";
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static int alertPromptButton() {
|
||||
return alertPromptButton;
|
||||
}
|
||||
|
||||
public static void exitEmulationActivity(int resultCode) {
|
||||
final int Success = 0;
|
||||
final int ErrorNotInitialized = 1;
|
||||
final int ErrorGetLoader = 2;
|
||||
final int ErrorSystemMode = 3;
|
||||
final int ErrorLoader = 4;
|
||||
final int ErrorLoader_ErrorEncrypted = 5;
|
||||
final int ErrorLoader_ErrorInvalidFormat = 6;
|
||||
final int ErrorSystemFiles = 7;
|
||||
final int ShutdownRequested = 11;
|
||||
final int ErrorUnknown = 12;
|
||||
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.");
|
||||
return;
|
||||
}
|
||||
|
||||
int captionId = R.string.loader_error_invalid_format;
|
||||
if (resultCode == ErrorLoader_ErrorEncrypted) {
|
||||
captionId = R.string.loader_error_encrypted;
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(captionId)
|
||||
.setMessage(Html.fromHtml("Please follow the guides to redump your <a href=\"https://citra-emu.org/wiki/dumping-game-cartridges/\">game cartidges</a> or <a href=\"https://citra-emu.org/wiki/dumping-installed-titles/\">installed titles</a>.", Html.FROM_HTML_MODE_LEGACY))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish())
|
||||
.setOnDismissListener(dialogInterface -> emulationActivity.finish());
|
||||
emulationActivity.runOnUiThread(() -> {
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
});
|
||||
}
|
||||
|
||||
public static void setEmulationActivity(EmulationActivity emulationActivity) {
|
||||
Log.verbose("[NativeLibrary] Registering EmulationActivity.");
|
||||
sEmulationActivity = new WeakReference<>(emulationActivity);
|
||||
}
|
||||
|
||||
public static void clearEmulationActivity() {
|
||||
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.");
|
||||
|
||||
sEmulationActivity.clear();
|
||||
}
|
||||
|
||||
private static final Object cameraPermissionLock = new Object();
|
||||
private static boolean cameraPermissionGranted = false;
|
||||
public static final int REQUEST_CODE_NATIVE_CAMERA = 800;
|
||||
|
||||
public static boolean RequestCameraPermission() {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission already granted
|
||||
return true;
|
||||
}
|
||||
emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA);
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized (cameraPermissionLock) {
|
||||
try {
|
||||
cameraPermissionLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return cameraPermissionGranted;
|
||||
}
|
||||
|
||||
public static void CameraPermissionResult(boolean granted) {
|
||||
cameraPermissionGranted = granted;
|
||||
synchronized (cameraPermissionLock) {
|
||||
cameraPermissionLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
private static final Object micPermissionLock = new Object();
|
||||
private static boolean micPermissionGranted = false;
|
||||
public static final int REQUEST_CODE_NATIVE_MIC = 900;
|
||||
|
||||
public static boolean RequestMicPermission() {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission already granted
|
||||
return true;
|
||||
}
|
||||
emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC);
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized (micPermissionLock) {
|
||||
try {
|
||||
micPermissionLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return micPermissionGranted;
|
||||
}
|
||||
|
||||
public static void MicPermissionResult(boolean granted) {
|
||||
micPermissionGranted = granted;
|
||||
synchronized (micPermissionLock) {
|
||||
micPermissionLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
||||
public static native void ReloadCameraDevices();
|
||||
|
||||
public static native boolean LoadAmiibo(String path);
|
||||
|
||||
public static native void RemoveAmiibo();
|
||||
|
||||
public static final int SAVESTATE_SLOT_COUNT = 10;
|
||||
|
||||
public static final class SavestateInfo {
|
||||
public int slot;
|
||||
public Date time;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static native SavestateInfo[] GetSavestateInfo();
|
||||
|
||||
public static native void SaveState(int slot);
|
||||
public static native void LoadState(int slot);
|
||||
|
||||
/**
|
||||
* Logs the Citra version, Android version and, CPU.
|
||||
*/
|
||||
public static native void LogDeviceInfo();
|
||||
|
||||
/**
|
||||
* Button type for use in onTouchEvent
|
||||
*/
|
||||
public static final class ButtonType {
|
||||
public static final int BUTTON_A = 700;
|
||||
public static final int BUTTON_B = 701;
|
||||
public static final int BUTTON_X = 702;
|
||||
public static final int BUTTON_Y = 703;
|
||||
public static final int BUTTON_START = 704;
|
||||
public static final int BUTTON_SELECT = 705;
|
||||
public static final int BUTTON_HOME = 706;
|
||||
public static final int BUTTON_ZL = 707;
|
||||
public static final int BUTTON_ZR = 708;
|
||||
public static final int DPAD_UP = 709;
|
||||
public static final int DPAD_DOWN = 710;
|
||||
public static final int DPAD_LEFT = 711;
|
||||
public static final int DPAD_RIGHT = 712;
|
||||
public static final int STICK_LEFT = 713;
|
||||
public static final int STICK_LEFT_UP = 714;
|
||||
public static final int STICK_LEFT_DOWN = 715;
|
||||
public static final int STICK_LEFT_LEFT = 716;
|
||||
public static final int STICK_LEFT_RIGHT = 717;
|
||||
public static final int STICK_C = 718;
|
||||
public static final int STICK_C_UP = 719;
|
||||
public static final int STICK_C_DOWN = 720;
|
||||
public static final int STICK_C_LEFT = 771;
|
||||
public static final int STICK_C_RIGHT = 772;
|
||||
public static final int TRIGGER_L = 773;
|
||||
public static final int TRIGGER_R = 774;
|
||||
public static final int DPAD = 780;
|
||||
public static final int BUTTON_DEBUG = 781;
|
||||
public static final int BUTTON_GPIO14 = 782;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button states
|
||||
*/
|
||||
public static final class ButtonState {
|
||||
public static final int RELEASED = 0;
|
||||
public static final int PRESSED = 1;
|
||||
}
|
||||
public static boolean createFile(String directory, String filename) {
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
return CitraApplication.documentsTree.createFile(directory, filename);
|
||||
}
|
||||
return FileUtil.createFile(CitraApplication.getAppContext(), directory, filename) != null;
|
||||
}
|
||||
|
||||
public static boolean createDir(String directory, String directoryName) {
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
return CitraApplication.documentsTree.createDir(directory, directoryName);
|
||||
}
|
||||
return FileUtil.createDir(CitraApplication.getAppContext(), directory, directoryName) != null;
|
||||
}
|
||||
|
||||
public static int openContentUri(String path, String openMode) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.openContentUri(path, openMode);
|
||||
}
|
||||
return FileUtil.openContentUri(CitraApplication.getAppContext(), path, openMode);
|
||||
}
|
||||
|
||||
public static String[] getFilesName(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.getFilesName(path);
|
||||
}
|
||||
return FileUtil.getFilesName(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static long getSize(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.getFileSize(path);
|
||||
}
|
||||
return FileUtil.getFileSize(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean fileExists(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.Exists(path);
|
||||
}
|
||||
return FileUtil.Exists(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean isDirectory(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.isDirectory(path);
|
||||
}
|
||||
return FileUtil.isDirectory(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean copyFile(String sourcePath, String destinationParentPath, String destinationFilename) {
|
||||
if (FileUtil.isNativePath(sourcePath) && FileUtil.isNativePath(destinationParentPath)) {
|
||||
return CitraApplication.documentsTree.copyFile(sourcePath, destinationParentPath, destinationFilename);
|
||||
}
|
||||
return FileUtil.copyFile(CitraApplication.getAppContext(), sourcePath, destinationParentPath, destinationFilename);
|
||||
}
|
||||
|
||||
public static boolean renameFile(String path, String destinationFilename) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.renameFile(path, destinationFilename);
|
||||
}
|
||||
return FileUtil.renameFile(CitraApplication.getAppContext(), path, destinationFilename);
|
||||
}
|
||||
|
||||
public static boolean deleteDocument(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.deleteDocument(path);
|
||||
}
|
||||
return FileUtil.deleteDocument(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
}
|
@ -0,0 +1,720 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.citra.citra_emu.activities.EmulationActivity
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
import org.citra.citra_emu.utils.FileUtil
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Class which contains methods that interact
|
||||
* with the native side of the Citra code.
|
||||
*/
|
||||
object NativeLibrary {
|
||||
/**
|
||||
* Default touchscreen device
|
||||
*/
|
||||
const val TouchScreenDevice = "Touchscreen"
|
||||
|
||||
@JvmField
|
||||
var sEmulationActivity = WeakReference<EmulationActivity?>(null)
|
||||
private var alertResult = false
|
||||
val alertLock = Object()
|
||||
|
||||
init {
|
||||
try {
|
||||
System.loadLibrary("citra-android")
|
||||
} catch (ex: UnsatisfiedLinkError) {
|
||||
Log.error("[NativeLibrary] $ex")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles button press events for a gamepad.
|
||||
*
|
||||
* @param device The input descriptor of the gamepad.
|
||||
* @param button Key code identifying which button was pressed.
|
||||
* @param action Mask identifying which action is happening (button pressed down, or button released).
|
||||
* @return If we handled the button press.
|
||||
*/
|
||||
external fun onGamePadEvent(device: String, button: Int, action: Int): Boolean
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param device The device ID of the gamepad.
|
||||
* @param axis The axis ID
|
||||
* @param xAxis The value of the x-axis represented by the given ID.
|
||||
* @param yAxis The value of the y-axis represented by the given ID
|
||||
*/
|
||||
external fun onGamePadMoveEvent(device: String, axis: Int, xAxis: Float, yAxis: Float): Boolean
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param device The device ID of the gamepad.
|
||||
* @param axisId The axis ID
|
||||
* @param axisVal The value of the axis represented by the given ID.
|
||||
*/
|
||||
external fun onGamePadAxisEvent(device: String?, axisId: Int, axisVal: Float): Boolean
|
||||
|
||||
/**
|
||||
* Handles touch events.
|
||||
*
|
||||
* @param xAxis The value of the x-axis.
|
||||
* @param yAxis The value of the y-axis
|
||||
* @param pressed To identify if the touch held down or released.
|
||||
* @return true if the pointer is within the touchscreen
|
||||
*/
|
||||
external fun onTouchEvent(xAxis: Float, yAxis: Float, pressed: Boolean): Boolean
|
||||
|
||||
/**
|
||||
* Handles touch movement.
|
||||
*
|
||||
* @param xAxis The value of the instantaneous x-axis.
|
||||
* @param yAxis The value of the instantaneous y-axis.
|
||||
*/
|
||||
external fun onTouchMoved(xAxis: Float, yAxis: Float)
|
||||
|
||||
external fun reloadSettings()
|
||||
|
||||
external fun getTitleId(filename: String): Long
|
||||
|
||||
external fun getIsSystemTitle(path: String): Boolean
|
||||
|
||||
/**
|
||||
* Sets the current working user directory
|
||||
* If not set, it auto-detects a location
|
||||
*/
|
||||
external fun setUserDirectory(directory: String)
|
||||
external fun getInstalledGamePaths(): Array<String?>
|
||||
|
||||
// Create the config.ini file.
|
||||
external fun createConfigFile()
|
||||
external fun createLogFile()
|
||||
external fun logUserDirectory(directory: String)
|
||||
|
||||
/**
|
||||
* Begins emulation.
|
||||
*/
|
||||
external fun run(path: String)
|
||||
|
||||
// Surface Handling
|
||||
external fun surfaceChanged(surf: Surface)
|
||||
external fun surfaceDestroyed()
|
||||
external fun doFrame()
|
||||
|
||||
/**
|
||||
* Unpauses emulation from a paused state.
|
||||
*/
|
||||
external fun unPauseEmulation()
|
||||
|
||||
/**
|
||||
* Pauses emulation.
|
||||
*/
|
||||
external fun pauseEmulation()
|
||||
|
||||
/**
|
||||
* Stops emulation.
|
||||
*/
|
||||
external fun stopEmulation()
|
||||
|
||||
/**
|
||||
* Returns true if emulation is running (or is paused).
|
||||
*/
|
||||
external fun isRunning(): Boolean
|
||||
|
||||
/**
|
||||
* Returns the title ID of the currently running title, or 0 on failure.
|
||||
*/
|
||||
external fun getRunningTitleId(): Long
|
||||
|
||||
/**
|
||||
* Returns the performance stats for the current game
|
||||
*/
|
||||
external fun getPerfStats(): DoubleArray
|
||||
|
||||
/**
|
||||
* Notifies the core emulation that the orientation has changed.
|
||||
*/
|
||||
external fun notifyOrientationChange(layoutOption: Int, rotation: Int)
|
||||
|
||||
/**
|
||||
* Swaps the top and bottom screens.
|
||||
*/
|
||||
external fun swapScreens(swapScreens: Boolean, rotation: Int)
|
||||
|
||||
external fun initializeGpuDriver(
|
||||
hookLibDir: String?,
|
||||
customDriverDir: String?,
|
||||
customDriverName: String?,
|
||||
fileRedirectDir: String?
|
||||
)
|
||||
|
||||
external fun areKeysAvailable(): Boolean
|
||||
|
||||
external fun getHomeMenuPath(region: Int): String
|
||||
|
||||
external fun getSystemTitleIds(systemType: Int, region: Int): LongArray
|
||||
|
||||
external fun downloadTitleFromNus(title: Long): InstallStatus
|
||||
|
||||
private var coreErrorAlertResult = false
|
||||
private val coreErrorAlertLock = Object()
|
||||
|
||||
private fun onCoreErrorImpl(title: String, message: String) {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||
return
|
||||
}
|
||||
val fragment = CoreErrorDialogFragment.newInstance(title, message)
|
||||
fragment.show(emulationActivity.supportFragmentManager, CoreErrorDialogFragment.TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a core error.
|
||||
* @return true: continue; false: abort
|
||||
*/
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun onCoreError(error: CoreError?, details: String): Boolean {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||
return false
|
||||
}
|
||||
val title: String
|
||||
val message: String
|
||||
when (error) {
|
||||
CoreError.ErrorSystemFiles -> {
|
||||
title = emulationActivity.getString(R.string.system_archive_not_found)
|
||||
message = emulationActivity.getString(
|
||||
R.string.system_archive_not_found_message,
|
||||
details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
|
||||
)
|
||||
}
|
||||
|
||||
CoreError.ErrorSavestate -> {
|
||||
title = emulationActivity.getString(R.string.save_load_error)
|
||||
message = details
|
||||
}
|
||||
|
||||
CoreError.ErrorUnknown -> {
|
||||
title = emulationActivity.getString(R.string.fatal_error)
|
||||
message = emulationActivity.getString(R.string.fatal_error_message)
|
||||
}
|
||||
|
||||
else -> {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) })
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized(coreErrorAlertLock) {
|
||||
try {
|
||||
coreErrorAlertLock.wait()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
return coreErrorAlertResult
|
||||
}
|
||||
|
||||
@get:Keep
|
||||
@get:JvmStatic
|
||||
val isPortraitMode: Boolean
|
||||
get() = CitraApplication.appContext.resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_PORTRAIT
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun landscapeScreenLayout(): Int = EmulationMenuSettings.getLandscapeScreenLayout()
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun displayAlertMsg(title: String, message: String, yesNo: Boolean): Boolean {
|
||||
Log.error("[NativeLibrary] Alert: $message")
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
var result = false
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.")
|
||||
} else {
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread {
|
||||
AlertMessageDialogFragment.newInstance(title, message, yesNo).showNow(
|
||||
emulationActivity.supportFragmentManager,
|
||||
AlertMessageDialogFragment.TAG
|
||||
)
|
||||
}
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized(alertLock) {
|
||||
try {
|
||||
alertLock.wait()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
if (yesNo) result = alertResult
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
class AlertMessageDialogFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create object used for waiting.
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(requireArguments().getString(TITLE))
|
||||
.setMessage(requireArguments().getString(MESSAGE))
|
||||
|
||||
// If not yes/no dialog just have one button that dismisses modal,
|
||||
// otherwise have a yes and no button that sets alertResult accordingly.
|
||||
if (!requireArguments().getBoolean(YES_NO)) {
|
||||
builder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
synchronized(alertLock) { alertLock.notify() }
|
||||
}
|
||||
} else {
|
||||
alertResult = false
|
||||
builder
|
||||
.setPositiveButton(android.R.string.yes) { _: DialogInterface, _: Int ->
|
||||
alertResult = true
|
||||
synchronized(alertLock) { alertLock.notify() }
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _: DialogInterface, _: Int ->
|
||||
alertResult = false
|
||||
synchronized(alertLock) { alertLock.notify() }
|
||||
}
|
||||
}
|
||||
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "AlertMessageDialogFragment"
|
||||
|
||||
const val TITLE = "title"
|
||||
const val MESSAGE = "message"
|
||||
const val YES_NO = "yesNo"
|
||||
|
||||
fun newInstance(
|
||||
title: String,
|
||||
message: String,
|
||||
yesNo: Boolean
|
||||
): AlertMessageDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putString(TITLE, title)
|
||||
args.putString(MESSAGE, message)
|
||||
args.putBoolean(YES_NO, yesNo)
|
||||
val fragment = AlertMessageDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun exitEmulationActivity(resultCode: Int) {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.")
|
||||
return
|
||||
}
|
||||
|
||||
emulationActivity.runOnUiThread {
|
||||
EmulationErrorDialogFragment.newInstance(resultCode).showNow(
|
||||
emulationActivity.supportFragmentManager,
|
||||
EmulationErrorDialogFragment.TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EmulationErrorDialogFragment : DialogFragment() {
|
||||
private lateinit var emulationActivity: EmulationActivity
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
emulationActivity = requireActivity() as EmulationActivity
|
||||
|
||||
var captionId = R.string.loader_error_invalid_format
|
||||
if (requireArguments().getInt(RESULT_CODE) == ErrorLoader_ErrorEncrypted) {
|
||||
captionId = R.string.loader_error_encrypted
|
||||
}
|
||||
|
||||
val alert = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(captionId)
|
||||
.setMessage(
|
||||
Html.fromHtml(
|
||||
CitraApplication.appContext.resources.getString(R.string.redump_games),
|
||||
Html.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
emulationActivity.finish()
|
||||
}
|
||||
.create()
|
||||
alert.show()
|
||||
|
||||
val alertMessage = alert.findViewById<View>(android.R.id.message) as TextView
|
||||
alertMessage.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
isCancelable = false
|
||||
return alert
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "EmulationErrorDialogFragment"
|
||||
|
||||
const val RESULT_CODE = "resultcode"
|
||||
|
||||
const val Success = 0
|
||||
const val ErrorNotInitialized = 1
|
||||
const val ErrorGetLoader = 2
|
||||
const val ErrorSystemMode = 3
|
||||
const val ErrorLoader = 4
|
||||
const val ErrorLoader_ErrorEncrypted = 5
|
||||
const val ErrorLoader_ErrorInvalidFormat = 6
|
||||
const val ErrorSystemFiles = 7
|
||||
const val ShutdownRequested = 11
|
||||
const val ErrorUnknown = 12
|
||||
|
||||
fun newInstance(resultCode: Int): EmulationErrorDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putInt(RESULT_CODE, resultCode)
|
||||
val fragment = EmulationErrorDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
|
||||
Log.verbose("[NativeLibrary] Registering EmulationActivity.")
|
||||
sEmulationActivity = WeakReference(emulationActivity)
|
||||
}
|
||||
|
||||
fun clearEmulationActivity() {
|
||||
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
|
||||
sEmulationActivity.clear()
|
||||
}
|
||||
|
||||
private val cameraPermissionLock = Object()
|
||||
private var cameraPermissionGranted = false
|
||||
const val REQUEST_CODE_NATIVE_CAMERA = 800
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun requestCameraPermission(): Boolean {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||
return false
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, permission.CAMERA) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// Permission already granted
|
||||
return true
|
||||
}
|
||||
emulationActivity.requestPermissions(arrayOf(permission.CAMERA), REQUEST_CODE_NATIVE_CAMERA)
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized(cameraPermissionLock) {
|
||||
try {
|
||||
cameraPermissionLock.wait()
|
||||
} catch (ignored: InterruptedException) {
|
||||
}
|
||||
}
|
||||
return cameraPermissionGranted
|
||||
}
|
||||
|
||||
fun cameraPermissionResult(granted: Boolean) {
|
||||
cameraPermissionGranted = granted
|
||||
synchronized(cameraPermissionLock) { cameraPermissionLock.notify() }
|
||||
}
|
||||
|
||||
private val micPermissionLock = Object()
|
||||
private var micPermissionGranted = false
|
||||
const val REQUEST_CODE_NATIVE_MIC = 900
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun requestMicPermission(): Boolean {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||
return false
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, permission.RECORD_AUDIO) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// Permission already granted
|
||||
return true
|
||||
}
|
||||
emulationActivity.requestPermissions(
|
||||
arrayOf(permission.RECORD_AUDIO),
|
||||
REQUEST_CODE_NATIVE_MIC
|
||||
)
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized(micPermissionLock) {
|
||||
try {
|
||||
micPermissionLock.wait()
|
||||
} catch (ignored: InterruptedException) {
|
||||
}
|
||||
}
|
||||
return micPermissionGranted
|
||||
}
|
||||
|
||||
fun micPermissionResult(granted: Boolean) {
|
||||
micPermissionGranted = granted
|
||||
synchronized(micPermissionLock) { micPermissionLock.notify() }
|
||||
}
|
||||
|
||||
// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
||||
external fun reloadCameraDevices()
|
||||
|
||||
external fun loadAmiibo(path: String?): Boolean
|
||||
|
||||
external fun removeAmiibo()
|
||||
|
||||
const val SAVESTATE_SLOT_COUNT = 10
|
||||
|
||||
external fun getSavestateInfo(): Array<SaveStateInfo>?
|
||||
|
||||
external fun saveState(slot: Int)
|
||||
|
||||
external fun loadState(slot: Int)
|
||||
|
||||
/**
|
||||
* Logs the Citra version, Android version and, CPU.
|
||||
*/
|
||||
external fun logDeviceInfo()
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createFile(directory: String, filename: String): Boolean =
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
CitraApplication.documentsTree.createFile(directory, filename)
|
||||
} else {
|
||||
FileUtil.createFile(directory, filename) != null
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createDir(directory: String, directoryName: String): Boolean =
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
CitraApplication.documentsTree.createDir(directory, directoryName)
|
||||
} else {
|
||||
FileUtil.createDir(directory, directoryName) != null
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun openContentUri(path: String, openMode: String): Int =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.openContentUri(path, openMode)
|
||||
} else {
|
||||
FileUtil.openContentUri(path, openMode)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getFilesName(path: String): Array<String?> =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.getFilesName(path)
|
||||
} else {
|
||||
FileUtil.getFilesName(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getSize(path: String): Long =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.getFileSize(path)
|
||||
} else {
|
||||
FileUtil.getFileSize(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun fileExists(path: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.exists(path)
|
||||
} else {
|
||||
FileUtil.exists(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun isDirectory(path: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.isDirectory(path)
|
||||
} else {
|
||||
FileUtil.isDirectory(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun copyFile(
|
||||
sourcePath: String,
|
||||
destinationParentPath: String,
|
||||
destinationFilename: String
|
||||
): Boolean =
|
||||
if (FileUtil.isNativePath(sourcePath) &&
|
||||
FileUtil.isNativePath(destinationParentPath)
|
||||
) {
|
||||
CitraApplication.documentsTree
|
||||
.copyFile(sourcePath, destinationParentPath, destinationFilename)
|
||||
} else {
|
||||
FileUtil.copyFile(
|
||||
Uri.parse(sourcePath),
|
||||
Uri.parse(destinationParentPath),
|
||||
destinationFilename
|
||||
)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun renameFile(path: String, destinationFilename: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.renameFile(path, destinationFilename)
|
||||
} else {
|
||||
FileUtil.renameFile(path, destinationFilename)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun deleteDocument(path: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.deleteDocument(path)
|
||||
} else {
|
||||
FileUtil.deleteDocument(path)
|
||||
}
|
||||
|
||||
enum class CoreError {
|
||||
ErrorSystemFiles,
|
||||
ErrorSavestate,
|
||||
ErrorUnknown
|
||||
}
|
||||
|
||||
enum class InstallStatus {
|
||||
Success,
|
||||
ErrorFailedToOpenFile,
|
||||
ErrorFileNotFound,
|
||||
ErrorAborted,
|
||||
ErrorInvalid,
|
||||
ErrorEncrypted,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
class CoreErrorDialogFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val title = requireArguments().getString(TITLE)
|
||||
val message = requireArguments().getString(MESSAGE)
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int ->
|
||||
coreErrorAlertResult = true
|
||||
}
|
||||
.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
|
||||
coreErrorAlertResult = false
|
||||
}.show()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
coreErrorAlertResult = true
|
||||
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "CoreErrorDialogFragment"
|
||||
|
||||
const val TITLE = "title"
|
||||
const val MESSAGE = "message"
|
||||
|
||||
fun newInstance(title: String, message: String): CoreErrorDialogFragment {
|
||||
val frag = CoreErrorDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putString(TITLE, title)
|
||||
args.putString(MESSAGE, message)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
class SaveStateInfo {
|
||||
var slot = 0
|
||||
var time: Date? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Button type for use in onTouchEvent
|
||||
*/
|
||||
object ButtonType {
|
||||
const val BUTTON_A = 700
|
||||
const val BUTTON_B = 701
|
||||
const val BUTTON_X = 702
|
||||
const val BUTTON_Y = 703
|
||||
const val BUTTON_START = 704
|
||||
const val BUTTON_SELECT = 705
|
||||
const val BUTTON_HOME = 706
|
||||
const val BUTTON_ZL = 707
|
||||
const val BUTTON_ZR = 708
|
||||
const val DPAD_UP = 709
|
||||
const val DPAD_DOWN = 710
|
||||
const val DPAD_LEFT = 711
|
||||
const val DPAD_RIGHT = 712
|
||||
const val STICK_LEFT = 713
|
||||
const val STICK_LEFT_UP = 714
|
||||
const val STICK_LEFT_DOWN = 715
|
||||
const val STICK_LEFT_LEFT = 716
|
||||
const val STICK_LEFT_RIGHT = 717
|
||||
const val STICK_C = 718
|
||||
const val STICK_C_UP = 719
|
||||
const val STICK_C_DOWN = 720
|
||||
const val STICK_C_LEFT = 771
|
||||
const val STICK_C_RIGHT = 772
|
||||
const val TRIGGER_L = 773
|
||||
const val TRIGGER_R = 774
|
||||
const val DPAD = 780
|
||||
const val BUTTON_DEBUG = 781
|
||||
const val BUTTON_GPIO14 = 782
|
||||
}
|
||||
|
||||
/**
|
||||
* Button states
|
||||
*/
|
||||
object ButtonState {
|
||||
const val RELEASED = 0
|
||||
const val PRESSED = 1
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@ -48,6 +49,7 @@ import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.ForegroundService;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
import java.io.File;
|
||||
@ -169,8 +171,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
ThemeUtil.applyTheme(this);
|
||||
|
||||
Log.gameLaunched = true;
|
||||
ThemeUtil.INSTANCE.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
@ -210,7 +212,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
startForegroundService(foregroundService);
|
||||
|
||||
// Override Citra core INI with the one set by our in game menu
|
||||
NativeLibrary.SwapScreens(EmulationMenuSettings.getSwapScreens(),
|
||||
NativeLibrary.INSTANCE.swapScreens(EmulationMenuSettings.getSwapScreens(),
|
||||
getWindowManager().getDefaultDisplay().getRotation());
|
||||
}
|
||||
|
||||
@ -224,15 +226,12 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
protected void restoreState(Bundle savedInstanceState) {
|
||||
mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
|
||||
mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
|
||||
|
||||
// If an alert prompt was in progress when state was restored, retry displaying it
|
||||
NativeLibrary.retryDisplayAlertPrompt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestart() {
|
||||
super.onRestart();
|
||||
NativeLibrary.ReloadCameraDevices();
|
||||
NativeLibrary.INSTANCE.reloadCameraDevices();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -257,7 +256,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
NativeLibrary.INSTANCE.cameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
break;
|
||||
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
@ -268,7 +267,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
NativeLibrary.INSTANCE.micPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
break;
|
||||
default:
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
@ -281,6 +280,10 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void enableFullscreenImmersive() {
|
||||
// TODO: Remove this once we properly account for display insets in the input overlay
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
|
||||
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
@ -323,7 +326,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void DisplaySavestateWarning() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
|
||||
if (preferences.getBoolean("savestateWarningShown", false)) {
|
||||
return;
|
||||
}
|
||||
@ -350,7 +353,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void updateSavestateMenuOptions(Menu menu) {
|
||||
final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo();
|
||||
final NativeLibrary.SaveStateInfo[] savestates = NativeLibrary.INSTANCE.getSavestateInfo();
|
||||
if (savestates == null) {
|
||||
menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
|
||||
menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
|
||||
@ -370,18 +373,18 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
final String text = getString(R.string.emulation_empty_state_slot, slot);
|
||||
saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> {
|
||||
DisplaySavestateWarning();
|
||||
NativeLibrary.SaveState(slot);
|
||||
NativeLibrary.INSTANCE.saveState(slot);
|
||||
return true;
|
||||
});
|
||||
loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> {
|
||||
NativeLibrary.LoadState(slot);
|
||||
NativeLibrary.INSTANCE.loadState(slot);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
for (final NativeLibrary.SavestateInfo info : savestates) {
|
||||
final String text = getString(R.string.emulation_occupied_state_slot, info.slot, info.time);
|
||||
saveStateMenu.getItem(info.slot - 1).setTitle(text);
|
||||
loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true);
|
||||
for (final NativeLibrary.SaveStateInfo info : savestates) {
|
||||
final String text = getString(R.string.emulation_occupied_state_slot, info.getSlot(), info.getTime());
|
||||
saveStateMenu.getItem(info.getSlot() - 1).setTitle(text);
|
||||
loadStateMenu.getItem(info.getSlot() - 1).setTitle(text).setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -441,7 +444,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
EmulationMenuSettings.setSwapScreens(isEnabled);
|
||||
item.setChecked(isEnabled);
|
||||
|
||||
NativeLibrary.SwapScreens(isEnabled, getWindowManager().getDefaultDisplay()
|
||||
NativeLibrary.INSTANCE.swapScreens(isEnabled, getWindowManager().getDefaultDisplay()
|
||||
.getRotation());
|
||||
break;
|
||||
}
|
||||
@ -491,11 +494,11 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
break;
|
||||
|
||||
case MENU_ACTION_OPEN_CHEATS:
|
||||
CheatsActivity.launch(this, NativeLibrary.GetRunningTitleId());
|
||||
CheatsActivity.launch(this, NativeLibrary.INSTANCE.getRunningTitleId());
|
||||
break;
|
||||
|
||||
case MENU_ACTION_CLOSE_GAME:
|
||||
NativeLibrary.PauseEmulation();
|
||||
NativeLibrary.INSTANCE.pauseEmulation();
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.emulation_close_game)
|
||||
.setMessage(R.string.emulation_close_game_message)
|
||||
@ -504,8 +507,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
mEmulationFragment.stopEmulation();
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.UnPauseEmulation())
|
||||
.setOnCancelListener(dialogInterface -> NativeLibrary.UnPauseEmulation())
|
||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.INSTANCE.unPauseEmulation())
|
||||
.setOnCancelListener(dialogInterface -> NativeLibrary.INSTANCE.unPauseEmulation())
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
@ -515,7 +518,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
|
||||
private void changeScreenOrientation(int layoutOption, MenuItem item) {
|
||||
item.setChecked(true);
|
||||
NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
|
||||
NativeLibrary.INSTANCE.notifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
|
||||
.getRotation());
|
||||
EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
|
||||
}
|
||||
@ -532,7 +535,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int action;
|
||||
int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
|
||||
int button = mPreferences.getInt(InputBindingSetting.Companion.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
|
||||
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
@ -558,7 +561,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action);
|
||||
return NativeLibrary.INSTANCE.onGamePadEvent(input.getDescriptor(), button, action);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -570,7 +573,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void onAmiiboSelected(String selectedFile) {
|
||||
boolean success = NativeLibrary.LoadAmiibo(selectedFile);
|
||||
boolean success = NativeLibrary.INSTANCE.loadAmiibo(selectedFile);
|
||||
|
||||
if (!success) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
@ -582,7 +585,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void RemoveAmiibo() {
|
||||
NativeLibrary.RemoveAmiibo();
|
||||
NativeLibrary.INSTANCE.removeAmiibo();
|
||||
}
|
||||
|
||||
private void toggleControls() {
|
||||
@ -690,8 +693,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
int axis = range.getAxis();
|
||||
float origValue = event.getAxisValue(axis);
|
||||
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
|
||||
int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
|
||||
int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
|
||||
int nextMapping = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisButtonKey(axis), -1);
|
||||
int guestOrientation = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisOrientationKey(axis), -1);
|
||||
|
||||
if (nextMapping == -1 || guestOrientation == -1) {
|
||||
// Axis is unmapped
|
||||
@ -725,47 +728,47 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
// Circle-Pad and C-Stick status
|
||||
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
|
||||
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
|
||||
NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
|
||||
NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
|
||||
|
||||
// Triggers L/R and ZL/ZR
|
||||
if (isTriggerPressedLMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedRMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedZLMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedZRMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
|
||||
// Work-around to allow D-pad axis to be bound to emulated buttons
|
||||
if (axisValuesDPad[0] == 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[0] < 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[0] > 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
|
||||
}
|
||||
if (axisValuesDPad[1] == 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[1] < 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[1] > 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -0,0 +1,119 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.databinding.CardDriverOptionBinding
|
||||
import org.citra.citra_emu.utils.GpuDriverMetadata
|
||||
import org.citra.citra_emu.viewmodel.DriverViewModel
|
||||
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||
|
||||
class DriverAdapter(private val driverViewModel: DriverViewModel) :
|
||||
ListAdapter<Pair<Uri, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>(
|
||||
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||
) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
|
||||
val binding =
|
||||
CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return DriverViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = currentList.size
|
||||
|
||||
override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
private fun onSelectDriver(position: Int) {
|
||||
driverViewModel.setSelectedDriverIndex(position)
|
||||
notifyItemChanged(driverViewModel.previouslySelectedDriver)
|
||||
notifyItemChanged(driverViewModel.selectedDriver)
|
||||
}
|
||||
|
||||
private fun onDeleteDriver(driverData: Pair<Uri, GpuDriverMetadata>, position: Int) {
|
||||
if (driverViewModel.selectedDriver > position) {
|
||||
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
|
||||
}
|
||||
if (GpuDriverHelper.customDriverData == driverData.second) {
|
||||
driverViewModel.setSelectedDriverIndex(0)
|
||||
}
|
||||
driverViewModel.driversToDelete.add(driverData.first)
|
||||
driverViewModel.removeDriver(driverData)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemChanged(driverViewModel.selectedDriver)
|
||||
}
|
||||
|
||||
inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private lateinit var driverData: Pair<Uri, GpuDriverMetadata>
|
||||
|
||||
fun bind(driverData: Pair<Uri, GpuDriverMetadata>) {
|
||||
this.driverData = driverData
|
||||
val driver = driverData.second
|
||||
|
||||
binding.apply {
|
||||
radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition
|
||||
root.setOnClickListener {
|
||||
onSelectDriver(bindingAdapterPosition)
|
||||
}
|
||||
buttonDelete.setOnClickListener {
|
||||
onDeleteDriver(driverData, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
// Delay marquee by 3s
|
||||
title.postDelayed(
|
||||
{
|
||||
title.isSelected = true
|
||||
title.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
version.isSelected = true
|
||||
version.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
description.isSelected = true
|
||||
description.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
},
|
||||
3000
|
||||
)
|
||||
if (driver.name == null) {
|
||||
title.setText(R.string.system_gpu_driver)
|
||||
description.text = ""
|
||||
version.text = ""
|
||||
version.visibility = View.GONE
|
||||
description.visibility = View.GONE
|
||||
buttonDelete.visibility = View.GONE
|
||||
} else {
|
||||
title.text = driver.name
|
||||
version.text = driver.version
|
||||
description.text = driver.description
|
||||
version.visibility = View.VISIBLE
|
||||
description.visibility = View.VISIBLE
|
||||
buttonDelete.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Pair<Uri, GpuDriverMetadata>>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: Pair<Uri, GpuDriverMetadata>,
|
||||
newItem: Pair<Uri, GpuDriverMetadata>
|
||||
): Boolean {
|
||||
return oldItem.first == newItem.first
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: Pair<Uri, GpuDriverMetadata>,
|
||||
newItem: Pair<Uri, GpuDriverMetadata>
|
||||
): Boolean {
|
||||
return oldItem.second == newItem.second
|
||||
}
|
||||
}
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
package org.citra.citra_emu.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
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.features.cheats.ui.CheatsActivity;
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.PicassoUtils;
|
||||
import org.citra.citra_emu.viewholders.GameViewHolder;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* This adapter gets its information from a database Cursor. This fact, paired with the usage of
|
||||
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
|
||||
* large dataset.
|
||||
*/
|
||||
public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> {
|
||||
private Cursor mCursor;
|
||||
private GameDataSetObserver mObserver;
|
||||
|
||||
private boolean mDatasetValid;
|
||||
private long mLastClickTime = 0;
|
||||
|
||||
/**
|
||||
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
|
||||
* display no data until a Cursor is supplied by a CursorLoader.
|
||||
*/
|
||||
public GameAdapter() {
|
||||
mDatasetValid = false;
|
||||
mObserver = new GameDataSetObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager when it is necessary to create a new view.
|
||||
*
|
||||
* @param parent The RecyclerView (I think?) the created view will be thrown into.
|
||||
* @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
|
||||
* @return The created ViewHolder with references to all the child view's members.
|
||||
*/
|
||||
@Override
|
||||
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
// Create a new view.
|
||||
View gameCard = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.card_game, parent, false);
|
||||
|
||||
gameCard.setOnClickListener(this::onClick);
|
||||
gameCard.setOnLongClickListener(this::onLongClick);
|
||||
|
||||
// Use that view to create a ViewHolder.
|
||||
return new GameViewHolder(gameCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager when a new view is not necessary because we can recycle
|
||||
* an existing one (for example, if a view just scrolled onto the screen from the bottom, we
|
||||
* can use the view that just scrolled off the top instead of inflating a new one.)
|
||||
*
|
||||
* @param holder A ViewHolder representing the view we're recycling.
|
||||
* @param position The position of the 'new' view in the dataset.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
|
||||
if (mDatasetValid) {
|
||||
if (mCursor.moveToPosition(position)) {
|
||||
PicassoUtils.loadGameIcon(holder.imageIcon,
|
||||
mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
|
||||
|
||||
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
|
||||
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
||||
|
||||
String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
||||
String filename;
|
||||
if (FileUtil.isNativePath(filepath)) {
|
||||
filename = CitraApplication.documentsTree.getFilename(filepath);
|
||||
} else {
|
||||
filename = FileUtil.getFilename(CitraApplication.getAppContext(), filepath);
|
||||
}
|
||||
holder.textFileName.setText(filename);
|
||||
|
||||
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
|
||||
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
|
||||
holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
||||
holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
|
||||
holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
|
||||
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
|
||||
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY);
|
||||
|
||||
final int backgroundColorId = isValidGame(holder.path) ? R.attr.colorSurface : R.attr.colorErrorContainer;
|
||||
View itemView = holder.getItemView();
|
||||
itemView.setBackgroundColor(MaterialColors.getColor(itemView, backgroundColorId));
|
||||
} else {
|
||||
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
|
||||
}
|
||||
} else {
|
||||
Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager to find out how much data we have.
|
||||
*
|
||||
* @return Size of the dataset.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (mDatasetValid && mCursor != null) {
|
||||
return mCursor.getCount();
|
||||
}
|
||||
Log.error("[GameAdapter] Dataset is not valid.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of the _id column for a given row.
|
||||
*
|
||||
* @param position The row for which Android wants an ID.
|
||||
* @return A valid ID from the database, or 0 if not available.
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (mDatasetValid && mCursor != null) {
|
||||
if (mCursor.moveToPosition(position)) {
|
||||
return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
|
||||
}
|
||||
}
|
||||
|
||||
Log.error("[GameAdapter] Dataset is not valid.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell Android whether or not each item in the dataset has a stable identifier.
|
||||
* Which it does, because it's a database, so always tell Android 'true'.
|
||||
*
|
||||
* @param hasStableIds ignored.
|
||||
*/
|
||||
@Override
|
||||
public void setHasStableIds(boolean hasStableIds) {
|
||||
super.setHasStableIds(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a load is finished, call this to replace the existing data with the newly-loaded
|
||||
* data.
|
||||
*
|
||||
* @param cursor The newly-loaded Cursor.
|
||||
*/
|
||||
public void swapCursor(Cursor cursor) {
|
||||
// Sanity check.
|
||||
if (cursor == mCursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before getting rid of the old cursor, disassociate it from the Observer.
|
||||
final Cursor oldCursor = mCursor;
|
||||
if (oldCursor != null && mObserver != null) {
|
||||
oldCursor.unregisterDataSetObserver(mObserver);
|
||||
}
|
||||
|
||||
mCursor = cursor;
|
||||
if (mCursor != null) {
|
||||
// Attempt to associate the new Cursor with the Observer.
|
||||
if (mObserver != null) {
|
||||
mCursor.registerDataSetObserver(mObserver);
|
||||
}
|
||||
|
||||
mDatasetValid = true;
|
||||
} else {
|
||||
mDatasetValid = false;
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the game that was clicked on.
|
||||
*
|
||||
* @param view The view representing the game the user wants to play.
|
||||
*/
|
||||
private void onClick(View view) {
|
||||
// Double-click prevention, using threshold of 1000 ms
|
||||
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
|
||||
return;
|
||||
}
|
||||
mLastClickTime = SystemClock.elapsedRealtime();
|
||||
|
||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
||||
|
||||
EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cheats settings for the game that was clicked on.
|
||||
*
|
||||
* @param view The view representing the game the user wants to play.
|
||||
*/
|
||||
private boolean onLongClick(View view) {
|
||||
Context context = view.getContext();
|
||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
||||
|
||||
final long titleId = NativeLibrary.GetTitleId(holder.path);
|
||||
|
||||
if (titleId == 0) {
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.properties)
|
||||
.setMessage(R.string.properties_not_loaded)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
} else {
|
||||
CheatsActivity.launch(context, titleId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isValidGame(String path) {
|
||||
return Stream.of(
|
||||
".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
|
||||
}
|
||||
|
||||
private final class GameDataSetObserver extends DataSetObserver {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
super.onChanged();
|
||||
|
||||
mDatasetValid = true;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidated() {
|
||||
super.onInvalidated();
|
||||
|
||||
mDatasetValid = false;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
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.model.Game
|
||||
import org.citra.citra_emu.utils.GameIconUtils
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity) :
|
||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||
View.OnClickListener, View.OnLongClickListener {
|
||||
private var lastClickTime = 0L
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||
// Create a new view.
|
||||
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.cardGame.setOnClickListener(this)
|
||||
binding.cardGame.setOnLongClickListener(this)
|
||||
|
||||
// Use that view to create a ViewHolder.
|
||||
return GameViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
|
||||
holder.bind(currentList[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = currentList.size
|
||||
|
||||
/**
|
||||
* Launches the game that was clicked on.
|
||||
*
|
||||
* @param view The card representing the game the user wants to play.
|
||||
*/
|
||||
override fun onClick(view: View) {
|
||||
// Double-click prevention, using threshold of 1000 ms
|
||||
if (SystemClock.elapsedRealtime() - lastClickTime < 1000) {
|
||||
return
|
||||
}
|
||||
lastClickTime = SystemClock.elapsedRealtime()
|
||||
|
||||
val holder = view.tag as GameViewHolder
|
||||
gameExists(holder)
|
||||
|
||||
val preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
preferences.edit()
|
||||
.putLong(
|
||||
holder.game.keyLastPlayedTime,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
.apply()
|
||||
|
||||
EmulationActivity.launch(activity, holder.game.path, holder.game.title)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cheats settings for the game that was clicked on.
|
||||
*
|
||||
* @param view The view representing the game the user wants to play.
|
||||
*/
|
||||
override fun onLongClick(view: View): Boolean {
|
||||
val context = view.context
|
||||
val holder = view.tag as GameViewHolder
|
||||
gameExists(holder)
|
||||
|
||||
if (holder.game.titleId == 0L) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.properties)
|
||||
.setMessage(R.string.properties_not_loaded)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
CheatsActivity.launch(view.context, holder.game.titleId)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Triggers a library refresh if the user clicks on stale data
|
||||
private fun gameExists(holder: GameViewHolder): Boolean {
|
||||
if (holder.game.isInstalled) {
|
||||
return true
|
||||
}
|
||||
|
||||
val gameExists = DocumentFile.fromSingleUri(
|
||||
CitraApplication.appContext,
|
||||
Uri.parse(holder.game.path)
|
||||
)?.exists() == true
|
||||
return if (!gameExists) {
|
||||
Toast.makeText(
|
||||
CitraApplication.appContext,
|
||||
R.string.loader_error_file_not_found,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
inner class GameViewHolder(val binding: CardGameBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var game: Game
|
||||
|
||||
init {
|
||||
binding.cardGame.tag = this
|
||||
}
|
||||
|
||||
fun bind(game: Game) {
|
||||
this.game = game
|
||||
|
||||
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
GameIconUtils.loadGameIcon(activity, game, binding.imageGameScreen)
|
||||
|
||||
binding.textGameTitle.visibility = if (game.title.isEmpty()) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
binding.textCompany.visibility = if (game.company.isEmpty()) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
|
||||
binding.textGameTitle.text = game.title
|
||||
binding.textCompany.text = game.company
|
||||
binding.textFilename.text = game.filename
|
||||
|
||||
val backgroundColorId =
|
||||
if (
|
||||
isValidGame(game.filename.substring(game.filename.lastIndexOf(".") + 1).lowercase())
|
||||
) {
|
||||
R.attr.colorSurface
|
||||
} else {
|
||||
R.attr.colorErrorContainer
|
||||
}
|
||||
binding.cardContents.setBackgroundColor(
|
||||
MaterialColors.getColor(
|
||||
binding.cardContents,
|
||||
backgroundColorId
|
||||
)
|
||||
)
|
||||
|
||||
binding.textGameTitle.postDelayed(
|
||||
{
|
||||
binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.textGameTitle.isSelected = true
|
||||
|
||||
binding.textCompany.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.textCompany.isSelected = true
|
||||
|
||||
binding.textFilename.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.textFilename.isSelected = true
|
||||
},
|
||||
3000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidGame(extension: String): Boolean {
|
||||
return Game.badExtensions.stream()
|
||||
.noneMatch { extension == it.lowercase() }
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
||||
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||
return oldItem.titleId == newItem.titleId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.databinding.CardHomeOptionBinding
|
||||
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||
import org.citra.citra_emu.model.HomeSetting
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
||||
class HomeSettingAdapter(
|
||||
private val activity: AppCompatActivity,
|
||||
private val viewLifecycle: LifecycleOwner,
|
||||
var options: List<HomeSetting>
|
||||
) : RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(), View.OnClickListener {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
|
||||
val binding =
|
||||
CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.root.setOnClickListener(this)
|
||||
return HomeOptionViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return options.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
|
||||
holder.bind(options[position])
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val holder = view.tag as HomeOptionViewHolder
|
||||
if (holder.option.isEnabled.invoke()) {
|
||||
holder.option.onClick.invoke()
|
||||
} else {
|
||||
MessageDialogFragment.newInstance(
|
||||
holder.option.disabledTitleId,
|
||||
holder.option.disabledMessageId
|
||||
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var option: HomeSetting
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(option: HomeSetting) {
|
||||
this.option = option
|
||||
|
||||
binding.optionTitle.text = activity.resources.getString(option.titleId)
|
||||
binding.optionDescription.text = activity.resources.getString(option.descriptionId)
|
||||
binding.optionIcon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
option.iconId,
|
||||
activity.theme
|
||||
)
|
||||
)
|
||||
|
||||
viewLifecycle.lifecycleScope.launch {
|
||||
viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
option.details.collect { updateOptionDetails(it) }
|
||||
}
|
||||
}
|
||||
binding.optionDetail.postDelayed(
|
||||
{
|
||||
binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.optionDetail.isSelected = true
|
||||
},
|
||||
3000
|
||||
)
|
||||
|
||||
if (option.isEnabled.invoke()) {
|
||||
binding.optionTitle.alpha = 1f
|
||||
binding.optionDescription.alpha = 1f
|
||||
binding.optionIcon.alpha = 1f
|
||||
} else {
|
||||
binding.optionTitle.alpha = 0.5f
|
||||
binding.optionDescription.alpha = 0.5f
|
||||
binding.optionIcon.alpha = 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOptionDetails(detailString: String) {
|
||||
if (detailString != "") {
|
||||
binding.optionDetail.text = detailString
|
||||
binding.optionDetail.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.databinding.ListItemSettingBinding
|
||||
import org.citra.citra_emu.fragments.LicenseBottomSheetDialogFragment
|
||||
import org.citra.citra_emu.model.License
|
||||
|
||||
class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) :
|
||||
RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(),
|
||||
View.OnClickListener {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
|
||||
val binding =
|
||||
ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.root.setOnClickListener(this)
|
||||
return LicenseViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = licenses.size
|
||||
|
||||
override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) {
|
||||
holder.bind(licenses[position])
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val license = (view.tag as LicenseViewHolder).license
|
||||
LicenseBottomSheetDialogFragment.newInstance(license)
|
||||
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
|
||||
}
|
||||
|
||||
inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) {
|
||||
lateinit var license: License
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(license: License) {
|
||||
this.license = license
|
||||
|
||||
val context = CitraApplication.appContext
|
||||
binding.textSettingName.text = context.getString(license.titleId)
|
||||
binding.textSettingDescription.text = context.getString(license.descriptionId)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.citra.citra_emu.databinding.PageSetupBinding
|
||||
import org.citra.citra_emu.model.SetupCallback
|
||||
import org.citra.citra_emu.model.SetupPage
|
||||
import org.citra.citra_emu.model.StepState
|
||||
import org.citra.citra_emu.utils.ViewUtils
|
||||
|
||||
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
|
||||
RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
|
||||
val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SetupPageViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pages.size
|
||||
|
||||
override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
|
||||
holder.bind(pages[position])
|
||||
|
||||
inner class SetupPageViewHolder(val binding: PageSetupBinding) :
|
||||
RecyclerView.ViewHolder(binding.root), SetupCallback {
|
||||
lateinit var page: SetupPage
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(page: SetupPage) {
|
||||
this.page = page
|
||||
|
||||
if (page.stepCompleted.invoke() == StepState.STEP_COMPLETE) {
|
||||
onStepCompleted()
|
||||
}
|
||||
|
||||
binding.icon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
page.iconId,
|
||||
activity.theme
|
||||
)
|
||||
)
|
||||
binding.textTitle.text = activity.resources.getString(page.titleId)
|
||||
binding.textDescription.text =
|
||||
Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
|
||||
binding.textDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
binding.buttonAction.apply {
|
||||
text = activity.resources.getString(page.buttonTextId)
|
||||
if (page.buttonIconId != 0) {
|
||||
icon = ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
page.buttonIconId,
|
||||
activity.theme
|
||||
)
|
||||
}
|
||||
iconGravity =
|
||||
if (page.leftAlignedIcon) {
|
||||
MaterialButton.ICON_GRAVITY_START
|
||||
} else {
|
||||
MaterialButton.ICON_GRAVITY_END
|
||||
}
|
||||
setOnClickListener {
|
||||
page.buttonAction.invoke(this@SetupPageViewHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStepCompleted() {
|
||||
ViewUtils.hideView(binding.buttonAction, 200)
|
||||
ViewUtils.showView(binding.textConfirmation, 200)
|
||||
}
|
||||
}
|
||||
}
|
@ -18,13 +18,16 @@ 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;
|
||||
|
@ -7,13 +7,17 @@ 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;
|
||||
@ -29,6 +33,7 @@ import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Keep
|
||||
public final class SoftwareKeyboard {
|
||||
/// Corresponds to Frontend::ButtonConfig
|
||||
private interface ButtonConfig {
|
||||
@ -57,6 +62,7 @@ public final class SoftwareKeyboard {
|
||||
EmptyInputNotAllowed,
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static class KeyboardConfig implements java.io.Serializable {
|
||||
public int button_config;
|
||||
public int max_text_length;
|
||||
@ -109,20 +115,27 @@ public final class SoftwareKeyboard {
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = params.rightMargin =
|
||||
CitraApplication.getAppContext().getResources().getDimensionPixelSize(
|
||||
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.getAppContext());
|
||||
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);
|
||||
|
||||
@ -256,7 +269,7 @@ public final class SoftwareKeyboard {
|
||||
|
||||
public static void ShowError(String error) {
|
||||
NativeLibrary.displayAlertMsg(
|
||||
CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
|
||||
CitraApplication.Companion.getAppContext().getResources().getString(R.string.software_keyboard),
|
||||
error, false);
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.utils.PicassoUtils;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
// Used in native code.
|
||||
@ -23,6 +24,7 @@ public final class StillImageCameraHelper {
|
||||
String filePickerPath;
|
||||
|
||||
// Opens file picker for camera.
|
||||
@Keep
|
||||
public static @Nullable
|
||||
String OpenFilePicker() {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
@ -58,6 +60,7 @@ public final class StillImageCameraHelper {
|
||||
}
|
||||
|
||||
// Blocking call. Load image from file and crop/resize it to fit in width x height.
|
||||
@Keep
|
||||
@Nullable
|
||||
public static Bitmap LoadImageFromFile(String uri, int width, int height) {
|
||||
return PicassoUtils.LoadBitmapFromFile(uri, width, height);
|
||||
|
@ -1,91 +0,0 @@
|
||||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import java.util.Objects;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
public class CitraDirectoryDialog extends DialogFragment {
|
||||
public static final String TAG = "citra_directory_dialog_fragment";
|
||||
|
||||
private static final String MOVE_DATE_ENABLE = "IS_MODE_DATA_ENABLE";
|
||||
|
||||
TextView pathView;
|
||||
|
||||
TextView spaceView;
|
||||
|
||||
CheckBox checkBox;
|
||||
|
||||
AlertDialog dialog;
|
||||
|
||||
Listener listener;
|
||||
|
||||
public interface Listener {
|
||||
void onPressPositiveButton(boolean moveData, Uri path);
|
||||
}
|
||||
|
||||
public static CitraDirectoryDialog newInstance(String path, Listener listener) {
|
||||
CitraDirectoryDialog frag = new CitraDirectoryDialog();
|
||||
frag.listener = listener;
|
||||
Bundle args = new Bundle();
|
||||
args.putString("path", path);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = requireActivity();
|
||||
final Uri path = Uri.parse(Objects.requireNonNull(requireArguments().getString("path")));
|
||||
SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
String freeSpaceText =
|
||||
getResources().getString(R.string.free_space, FileUtil.getFreeSpace(activity, path));
|
||||
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.dialog_citra_directory, null);
|
||||
|
||||
checkBox = view.findViewById(R.id.checkBox);
|
||||
pathView = view.findViewById(R.id.path);
|
||||
spaceView = view.findViewById(R.id.space);
|
||||
|
||||
checkBox.setChecked(mPreferences.getBoolean(MOVE_DATE_ENABLE, true));
|
||||
if (!PermissionsHandler.hasWriteAccess(activity)) {
|
||||
checkBox.setVisibility(View.GONE);
|
||||
}
|
||||
checkBox.setOnCheckedChangeListener(
|
||||
(v, isChecked)
|
||||
// record move data selection with SharedPreferences
|
||||
-> mPreferences.edit().putBoolean(MOVE_DATE_ENABLE, checkBox.isChecked()).apply());
|
||||
|
||||
pathView.setText(path.getPath());
|
||||
spaceView.setText(freeSpaceText);
|
||||
|
||||
setCancelable(false);
|
||||
|
||||
dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setView(view)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.app_name)
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(d, v) -> listener.onPressPositiveButton(checkBox.isChecked(), path))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
return dialog;
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import org.citra.citra_emu.R;
|
||||
|
||||
public class CopyDirProgressDialog extends DialogFragment {
|
||||
public static final String TAG = "copy_dir_progress_dialog";
|
||||
ProgressBar progressBar;
|
||||
|
||||
TextView progressText;
|
||||
|
||||
AlertDialog dialog;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = requireActivity();
|
||||
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
|
||||
|
||||
progressBar = view.findViewById(R.id.progress_bar);
|
||||
progressText = view.findViewById(R.id.progress_text);
|
||||
progressText.setText("");
|
||||
|
||||
setCancelable(false);
|
||||
|
||||
dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setView(view)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.move_data)
|
||||
.setMessage("")
|
||||
.create();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public void onUpdateSearchProgress(String msg) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
dialog.setMessage(getResources().getString(R.string.searching_direcotry, msg));
|
||||
});
|
||||
}
|
||||
|
||||
public void onUpdateCopyProgress(String msg, int progress, int max) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
progressBar.setProgress(progress);
|
||||
progressBar.setMax(max);
|
||||
progressText.setText(String.format("%d/%d", progress, max));
|
||||
dialog.setMessage(getResources().getString(R.string.copy_file_name, msg));
|
||||
});
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AlertDialog} derivative that listens for
|
||||
* motion events from controllers and joysticks.
|
||||
*/
|
||||
public final class MotionAlertDialog extends AlertDialog {
|
||||
// The selected input preference
|
||||
private final InputBindingSetting setting;
|
||||
private final ArrayList<Float> mPreviousValues = new ArrayList<>();
|
||||
private int mPrevDeviceId = 0;
|
||||
private boolean mWaitingForEvent = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context The current {@link Context}.
|
||||
* @param setting The Preference to show this dialog for.
|
||||
*/
|
||||
public MotionAlertDialog(Context context, InputBindingSetting setting) {
|
||||
super(context);
|
||||
|
||||
this.setting = setting;
|
||||
}
|
||||
|
||||
public boolean onKeyEvent(int keyCode, KeyEvent event) {
|
||||
Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_UP:
|
||||
setting.onKeyInput(event);
|
||||
dismiss();
|
||||
// Even if we ignore the key, we still consume it. Thus return true regardless.
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
|
||||
return super.onKeyLongPress(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// Handle this key if we care about it, otherwise pass it down the framework
|
||||
return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
|
||||
// Handle this event if we care about it, otherwise pass it down the framework
|
||||
return onMotionEvent(event) || super.dispatchGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
private boolean onMotionEvent(MotionEvent event) {
|
||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
|
||||
return false;
|
||||
if (event.getAction() != MotionEvent.ACTION_MOVE)
|
||||
return false;
|
||||
|
||||
InputDevice input = event.getDevice();
|
||||
|
||||
List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
|
||||
|
||||
if (input.getId() != mPrevDeviceId) {
|
||||
mPreviousValues.clear();
|
||||
}
|
||||
mPrevDeviceId = input.getId();
|
||||
boolean firstEvent = mPreviousValues.isEmpty();
|
||||
|
||||
int numMovedAxis = 0;
|
||||
float axisMoveValue = 0.0f;
|
||||
InputDevice.MotionRange lastMovedRange = null;
|
||||
char lastMovedDir = '?';
|
||||
if (mWaitingForEvent) {
|
||||
for (int i = 0; i < motionRanges.size(); i++) {
|
||||
InputDevice.MotionRange range = motionRanges.get(i);
|
||||
int axis = range.getAxis();
|
||||
float origValue = event.getAxisValue(axis);
|
||||
float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue);
|
||||
if (firstEvent) {
|
||||
mPreviousValues.add(value);
|
||||
} else {
|
||||
float previousValue = mPreviousValues.get(i);
|
||||
|
||||
// Only handle the axes that are not neutral (more than 0.5)
|
||||
// but ignore any axis that has a constant value (e.g. always 1)
|
||||
if (Math.abs(value) > 0.5f && value != previousValue) {
|
||||
// It is common to have multiple axes with the same physical input. For example,
|
||||
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
|
||||
// To handle this, we ignore an axis motion that's the exact same as a motion
|
||||
// we already saw. This way, we ignore axes with two names, but catch the case
|
||||
// where a joystick is moved in two directions.
|
||||
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
|
||||
if (value != axisMoveValue) {
|
||||
axisMoveValue = value;
|
||||
numMovedAxis++;
|
||||
lastMovedRange = range;
|
||||
lastMovedDir = value < 0.0f ? '-' : '+';
|
||||
}
|
||||
}
|
||||
// Special case for d-pads (axis value jumps between 0 and 1 without any values
|
||||
// in between). Without this, the user would need to press the d-pad twice
|
||||
// due to the first press being caught by the "if (firstEvent)" case further up.
|
||||
else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) {
|
||||
numMovedAxis++;
|
||||
lastMovedRange = range;
|
||||
lastMovedDir = previousValue < 0.0f ? '-' : '+';
|
||||
}
|
||||
}
|
||||
|
||||
mPreviousValues.set(i, value);
|
||||
}
|
||||
|
||||
// If only one axis moved, that's the winner.
|
||||
if (numMovedAxis == 1) {
|
||||
mWaitingForEvent = false;
|
||||
setting.onMotionInput(input, lastMovedRange, lastMovedDir);
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -51,8 +51,7 @@ public class CheatsActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
ThemeUtil.applyTheme(this);
|
||||
|
||||
ThemeUtil.INSTANCE.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
@ -0,0 +1,9 @@
|
||||
// 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.settings.model
|
||||
|
||||
interface AbstractBooleanSetting : AbstractSetting {
|
||||
var boolean: Boolean
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
// 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.settings.model
|
||||
|
||||
interface AbstractFloatSetting : AbstractSetting {
|
||||
var float: Float
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
// 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.settings.model
|
||||
|
||||
interface AbstractIntSetting : AbstractSetting {
|
||||
var int: Int
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
// 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.settings.model
|
||||
|
||||
interface AbstractSetting {
|
||||
val key: String?
|
||||
val section: String?
|
||||
val isRuntimeEditable: Boolean
|
||||
val valueAsString: String
|
||||
val defaultValue: Any
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
// 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.settings.model
|
||||
|
||||
interface AbstractStringSetting : AbstractSetting {
|
||||
var string: String
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
public final class BooleanSetting extends Setting {
|
||||
private boolean mValue;
|
||||
|
||||
public BooleanSetting(String key, String section, boolean value) {
|
||||
super(key, section);
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public boolean getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(boolean value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return mValue ? "True" : "False";
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
// 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.settings.model
|
||||
|
||||
enum class BooleanSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: Boolean
|
||||
) : AbstractBooleanSetting {
|
||||
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);
|
||||
|
||||
override var boolean: Boolean = defaultValue
|
||||
|
||||
override val valueAsString: String
|
||||
get() = boolean.toString()
|
||||
|
||||
override val isRuntimeEditable: Boolean
|
||||
get() {
|
||||
for (setting in NOT_RUNTIME_EDITABLE) {
|
||||
if (setting == this) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NOT_RUNTIME_EDITABLE = listOf(
|
||||
PLUGIN_LOADER,
|
||||
ALLOW_PLUGIN_LOADER
|
||||
)
|
||||
|
||||
fun from(key: String): BooleanSetting? =
|
||||
BooleanSetting.values().firstOrNull { it.key == key }
|
||||
|
||||
fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
public final class FloatSetting extends Setting {
|
||||
private float mValue;
|
||||
|
||||
public FloatSetting(String key, String section, float value) {
|
||||
super(key, section);
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public float getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(float value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return Float.toString(mValue);
|
||||
}
|
||||
}
|
@ -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.features.settings.model
|
||||
|
||||
enum class FloatSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: Float
|
||||
) : AbstractFloatSetting {
|
||||
// There are no float settings currently
|
||||
EMPTY_SETTING("", "", 0.0f);
|
||||
|
||||
override var float: Float = defaultValue
|
||||
|
||||
override val valueAsString: String
|
||||
get() = float.toString()
|
||||
|
||||
override val isRuntimeEditable: Boolean
|
||||
get() {
|
||||
for (setting in NOT_RUNTIME_EDITABLE) {
|
||||
if (setting == this) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
|
||||
|
||||
fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
|
||||
|
||||
fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
public final class IntSetting extends Setting {
|
||||
private int mValue;
|
||||
|
||||
public IntSetting(String key, String section, int value) {
|
||||
super(key, section);
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(int value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return Integer.toString(mValue);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
// 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.settings.model
|
||||
|
||||
enum class IntSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: Int
|
||||
) : AbstractIntSetting {
|
||||
FRAME_LIMIT("frame_limit", Settings.SECTION_RENDERER, 100),
|
||||
EMULATED_REGION("region_value", Settings.SECTION_SYSTEM, -1),
|
||||
INIT_CLOCK("init_clock", Settings.SECTION_SYSTEM, 0),
|
||||
CAMERA_INNER_FLIP("camera_inner_flip", Settings.SECTION_CAMERA, 0),
|
||||
CAMERA_OUTER_LEFT_FLIP("camera_outer_left_flip", Settings.SECTION_CAMERA, 0),
|
||||
CAMERA_OUTER_RIGHT_FLIP("camera_outer_right_flip", Settings.SECTION_CAMERA, 0),
|
||||
GRAPHICS_API("graphics_api", Settings.SECTION_RENDERER, 1),
|
||||
RESOLUTION_FACTOR("resolution_factor", Settings.SECTION_RENDERER, 1),
|
||||
STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 0),
|
||||
STEREOSCOPIC_3D_DEPTH("factor_3d", Settings.SECTION_RENDERER, 0),
|
||||
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),
|
||||
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),
|
||||
LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, 1),
|
||||
SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, 0),
|
||||
DISK_SHADER_CACHE("use_disk_shader_cache", Settings.SECTION_RENDERER, 1),
|
||||
DUMP_TEXTURES("dump_textures", Settings.SECTION_UTILITY, 0),
|
||||
CUSTOM_TEXTURES("custom_textures", Settings.SECTION_UTILITY, 0),
|
||||
ASYNC_CUSTOM_LOADING("async_custom_loading", Settings.SECTION_UTILITY, 1),
|
||||
PRELOAD_TEXTURES("preload_textures", Settings.SECTION_UTILITY, 0),
|
||||
ENABLE_AUDIO_STRETCHING("enable_audio_stretching", Settings.SECTION_AUDIO, 1),
|
||||
CPU_JIT("use_cpu_jit", Settings.SECTION_CORE, 1),
|
||||
HW_SHADER("use_hw_shader", Settings.SECTION_RENDERER, 1),
|
||||
VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1),
|
||||
DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0),
|
||||
TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0),
|
||||
USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1);
|
||||
|
||||
override var int: Int = defaultValue
|
||||
|
||||
override val valueAsString: String
|
||||
get() = int.toString()
|
||||
|
||||
override val isRuntimeEditable: Boolean
|
||||
get() {
|
||||
for (setting in NOT_RUNTIME_EDITABLE) {
|
||||
if (setting == this) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NOT_RUNTIME_EDITABLE = listOf(
|
||||
EMULATED_REGION,
|
||||
INIT_CLOCK,
|
||||
NEW_3DS,
|
||||
GRAPHICS_API,
|
||||
VSYNC,
|
||||
DEBUG_RENDERER,
|
||||
CPU_JIT,
|
||||
ASYNC_CUSTOM_LOADING,
|
||||
AUDIO_INPUT_TYPE
|
||||
)
|
||||
|
||||
fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
|
||||
|
||||
fun clear() = IntSetting.values().forEach { it.int = it.defaultValue }
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
// 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.settings.model
|
||||
|
||||
enum class ScaledFloatSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: Float,
|
||||
val scale: Int
|
||||
) : AbstractFloatSetting {
|
||||
AUDIO_VOLUME("volume", Settings.SECTION_AUDIO, 1.0f, 100);
|
||||
|
||||
override var float: Float = defaultValue
|
||||
get() = field * scale
|
||||
set(value) {
|
||||
field = value / scale
|
||||
}
|
||||
|
||||
override val valueAsString: String get() = (float / scale).toString()
|
||||
|
||||
override val isRuntimeEditable: Boolean
|
||||
get() {
|
||||
for (setting in NOT_RUNTIME_EDITABLE) {
|
||||
if (setting == this) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NOT_RUNTIME_EDITABLE = emptyList<ScaledFloatSetting>()
|
||||
|
||||
fun from(key: String): ScaledFloatSetting? =
|
||||
ScaledFloatSetting.values().firstOrNull { it.key == key }
|
||||
|
||||
fun clear() = ScaledFloatSetting.values().forEach { it.float = it.defaultValue * it.scale }
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
/**
|
||||
* Abstraction for a setting item as read from / written to Citra's configuration ini files.
|
||||
* These files generally consist of a key/value pair, though the type of value is ambiguous and
|
||||
* must be inferred at read-time. The type of value determines which child of this class is used
|
||||
* to represent the Setting.
|
||||
*/
|
||||
public abstract class Setting {
|
||||
private String mKey;
|
||||
private String mSection;
|
||||
|
||||
/**
|
||||
* Base constructor.
|
||||
*
|
||||
* @param key Everything to the left of the = in a line from the ini file.
|
||||
* @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets.
|
||||
*/
|
||||
public Setting(String key, String section) {
|
||||
mKey = key;
|
||||
mSection = section;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The identifier used to write this setting to the ini file.
|
||||
*/
|
||||
public String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The name of the header under which this Setting should be written in the ini file.
|
||||
*/
|
||||
public String getSection() {
|
||||
return mSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A representation of this Setting's backing value converted to a String (e.g. for serialization).
|
||||
*/
|
||||
public abstract String getValueAsString();
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A semantically-related group of Settings objects. These Settings are
|
||||
* internally stored as a HashMap.
|
||||
*/
|
||||
public final class SettingSection {
|
||||
private String mName;
|
||||
|
||||
private HashMap<String, Setting> mSettings = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Create a new SettingSection with no Settings in it.
|
||||
*
|
||||
* @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets.
|
||||
*/
|
||||
public SettingSection(String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method; inserts a value directly into the backing HashMap.
|
||||
*
|
||||
* @param setting The Setting to be inserted.
|
||||
*/
|
||||
public void putSetting(Setting setting) {
|
||||
mSettings.put(setting.getKey(), setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method; gets a value directly from the backing HashMap.
|
||||
*
|
||||
* @param key Used to retrieve the Setting.
|
||||
* @return A Setting object (you should probably cast this before using)
|
||||
*/
|
||||
public Setting getSetting(String key) {
|
||||
return mSettings.get(key);
|
||||
}
|
||||
|
||||
public HashMap<String, Setting> getSettings() {
|
||||
return mSettings;
|
||||
}
|
||||
|
||||
public void mergeSection(SettingSection settingSection) {
|
||||
for (Setting setting : settingSection.mSettings.values()) {
|
||||
putSetting(setting);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
// 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.settings.model
|
||||
|
||||
/**
|
||||
* A semantically-related group of Settings objects. These Settings are
|
||||
* internally stored as a HashMap.
|
||||
*/
|
||||
class SettingSection(val name: String) {
|
||||
val settings = HashMap<String, AbstractSetting>()
|
||||
|
||||
/**
|
||||
* Convenience method; inserts a value directly into the backing HashMap.
|
||||
*
|
||||
* @param setting The Setting to be inserted.
|
||||
*/
|
||||
fun putSetting(setting: AbstractSetting) {
|
||||
settings[setting.key!!] = setting
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method; gets a value directly from the backing HashMap.
|
||||
*
|
||||
* @param key Used to retrieve the Setting.
|
||||
* @return A Setting object (you should probably cast this before using)
|
||||
*/
|
||||
fun getSetting(key: String): AbstractSetting? {
|
||||
return settings[key]
|
||||
}
|
||||
|
||||
fun mergeSection(settingSection: SettingSection) {
|
||||
for (setting in settingSection.settings.values) {
|
||||
putSetting(setting)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class Settings {
|
||||
public static final String SECTION_PREMIUM = "Premium";
|
||||
public static final String SECTION_CORE = "Core";
|
||||
public static final String SECTION_SYSTEM = "System";
|
||||
public static final String SECTION_CAMERA = "Camera";
|
||||
public static final String SECTION_CONTROLS = "Controls";
|
||||
public static final String SECTION_RENDERER = "Renderer";
|
||||
public static final String SECTION_LAYOUT = "Layout";
|
||||
public static final String SECTION_UTILITY = "Utility";
|
||||
public static final String SECTION_AUDIO = "Audio";
|
||||
public static final String SECTION_DEBUG = "Debug";
|
||||
|
||||
private String gameId;
|
||||
|
||||
private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_PREMIUM, SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG));
|
||||
}
|
||||
|
||||
/**
|
||||
* A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null
|
||||
* when getting a key not already in the map
|
||||
*/
|
||||
public static final class SettingsSectionMap extends HashMap<String, SettingSection> {
|
||||
@Override
|
||||
public SettingSection get(Object key) {
|
||||
if (!(key instanceof String)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String stringKey = (String) key;
|
||||
|
||||
if (!super.containsKey(stringKey)) {
|
||||
SettingSection section = new SettingSection(stringKey);
|
||||
super.put(stringKey, section);
|
||||
return section;
|
||||
}
|
||||
return super.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
|
||||
|
||||
public SettingSection getSection(String sectionName) {
|
||||
return sections.get(sectionName);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return sections.isEmpty();
|
||||
}
|
||||
|
||||
public HashMap<String, SettingSection> getSections() {
|
||||
return sections;
|
||||
}
|
||||
|
||||
public void loadSettings(SettingsActivityView view) {
|
||||
sections = new Settings.SettingsSectionMap();
|
||||
loadCitraSettings(view);
|
||||
|
||||
if (!TextUtils.isEmpty(gameId)) {
|
||||
loadCustomGameSettings(gameId, view);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCitraSettings(SettingsActivityView view) {
|
||||
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
|
||||
String fileName = entry.getKey();
|
||||
sections.putAll(SettingsFile.readFile(fileName, view));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCustomGameSettings(String gameId, SettingsActivityView view) {
|
||||
// custom game settings
|
||||
mergeSections(SettingsFile.readCustomGameSettings(gameId, view));
|
||||
}
|
||||
|
||||
private void mergeSections(HashMap<String, SettingSection> updatedSections) {
|
||||
for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) {
|
||||
if (sections.containsKey(entry.getKey())) {
|
||||
SettingSection originalSection = sections.get(entry.getKey());
|
||||
SettingSection updatedSection = entry.getValue();
|
||||
originalSection.mergeSection(updatedSection);
|
||||
} else {
|
||||
sections.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadSettings(String gameId, SettingsActivityView view) {
|
||||
this.gameId = gameId;
|
||||
loadSettings(view);
|
||||
}
|
||||
|
||||
public void saveSettings(SettingsActivityView view) {
|
||||
if (TextUtils.isEmpty(gameId)) {
|
||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.ini_saved), false);
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
|
||||
String fileName = entry.getKey();
|
||||
List<String> sectionNames = entry.getValue();
|
||||
TreeMap<String, SettingSection> iniSections = new TreeMap<>();
|
||||
for (String section : sectionNames) {
|
||||
iniSections.put(section, sections.get(section));
|
||||
}
|
||||
|
||||
SettingsFile.saveFile(fileName, iniSections, view);
|
||||
}
|
||||
} else {
|
||||
// custom game settings
|
||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.gameid_saved, gameId), false);
|
||||
|
||||
SettingsFile.saveCustomGameSettings(gameId, sections);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
// 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.settings.model
|
||||
|
||||
import android.text.TextUtils
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivityView
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import java.util.TreeMap
|
||||
|
||||
class Settings {
|
||||
private var gameId: String? = null
|
||||
|
||||
var isLoaded = false
|
||||
|
||||
/**
|
||||
* A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
|
||||
* when getting a key not already in the map
|
||||
*/
|
||||
class SettingsSectionMap : HashMap<String, SettingSection?>() {
|
||||
override operator fun get(key: String): SettingSection? {
|
||||
if (!super.containsKey(key)) {
|
||||
val section = SettingSection(key)
|
||||
super.put(key, section)
|
||||
return section
|
||||
}
|
||||
return super.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
|
||||
|
||||
fun getSection(sectionName: String): SettingSection? {
|
||||
return sections[sectionName]
|
||||
}
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = sections.isEmpty()
|
||||
|
||||
fun loadSettings(view: SettingsActivityView? = null) {
|
||||
sections = SettingsSectionMap()
|
||||
loadCitraSettings(view)
|
||||
if (!TextUtils.isEmpty(gameId)) {
|
||||
loadCustomGameSettings(gameId!!, view)
|
||||
}
|
||||
isLoaded = true
|
||||
}
|
||||
|
||||
private fun loadCitraSettings(view: SettingsActivityView?) {
|
||||
for ((fileName) in configFileSectionsMap) {
|
||||
sections.putAll(SettingsFile.readFile(fileName, view))
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
|
||||
// Custom game settings
|
||||
mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
|
||||
}
|
||||
|
||||
private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
|
||||
for ((key, updatedSection) in updatedSections) {
|
||||
if (sections.containsKey(key)) {
|
||||
val originalSection = sections[key]
|
||||
originalSection!!.mergeSection(updatedSection!!)
|
||||
} else {
|
||||
sections[key] = updatedSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSettings(gameId: String, view: SettingsActivityView) {
|
||||
this.gameId = gameId
|
||||
loadSettings(view)
|
||||
}
|
||||
|
||||
fun saveSettings(view: SettingsActivityView) {
|
||||
if (TextUtils.isEmpty(gameId)) {
|
||||
view.showToastMessage(
|
||||
CitraApplication.appContext.getString(R.string.ini_saved),
|
||||
false
|
||||
)
|
||||
for ((fileName, sectionNames) in configFileSectionsMap.entries) {
|
||||
val iniSections = TreeMap<String, SettingSection?>()
|
||||
for (section in sectionNames) {
|
||||
iniSections[section] = sections[section]
|
||||
}
|
||||
SettingsFile.saveFile(fileName, iniSections, view)
|
||||
}
|
||||
} else {
|
||||
// TODO: Implement per game settings
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SECTION_CORE = "Core"
|
||||
const val SECTION_SYSTEM = "System"
|
||||
const val SECTION_CAMERA = "Camera"
|
||||
const val SECTION_CONTROLS = "Controls"
|
||||
const val SECTION_RENDERER = "Renderer"
|
||||
const val SECTION_LAYOUT = "Layout"
|
||||
const val SECTION_UTILITY = "Utility"
|
||||
const val SECTION_AUDIO = "Audio"
|
||||
const val SECTION_DEBUG = "Debugging"
|
||||
const val SECTION_THEME = "Theme"
|
||||
|
||||
const val KEY_BUTTON_A = "button_a"
|
||||
const val KEY_BUTTON_B = "button_b"
|
||||
const val KEY_BUTTON_X = "button_x"
|
||||
const val KEY_BUTTON_Y = "button_y"
|
||||
const val KEY_BUTTON_SELECT = "button_select"
|
||||
const val KEY_BUTTON_START = "button_start"
|
||||
const val KEY_BUTTON_HOME = "button_home"
|
||||
const val KEY_BUTTON_UP = "button_up"
|
||||
const val KEY_BUTTON_DOWN = "button_down"
|
||||
const val KEY_BUTTON_LEFT = "button_left"
|
||||
const val KEY_BUTTON_RIGHT = "button_right"
|
||||
const val KEY_BUTTON_L = "button_l"
|
||||
const val KEY_BUTTON_R = "button_r"
|
||||
const val KEY_BUTTON_ZL = "button_zl"
|
||||
const val KEY_BUTTON_ZR = "button_zr"
|
||||
const val KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"
|
||||
const val KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"
|
||||
const val KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"
|
||||
const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"
|
||||
const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"
|
||||
const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"
|
||||
|
||||
val buttonKeys = listOf(
|
||||
KEY_BUTTON_A,
|
||||
KEY_BUTTON_B,
|
||||
KEY_BUTTON_X,
|
||||
KEY_BUTTON_Y,
|
||||
KEY_BUTTON_SELECT,
|
||||
KEY_BUTTON_START,
|
||||
KEY_BUTTON_HOME
|
||||
)
|
||||
val buttonTitles = listOf(
|
||||
R.string.button_a,
|
||||
R.string.button_b,
|
||||
R.string.button_x,
|
||||
R.string.button_y,
|
||||
R.string.button_select,
|
||||
R.string.button_start,
|
||||
R.string.button_home
|
||||
)
|
||||
val circlePadKeys = listOf(
|
||||
KEY_CIRCLEPAD_AXIS_VERTICAL,
|
||||
KEY_CIRCLEPAD_AXIS_HORIZONTAL
|
||||
)
|
||||
val cStickKeys = listOf(
|
||||
KEY_CSTICK_AXIS_VERTICAL,
|
||||
KEY_CSTICK_AXIS_HORIZONTAL
|
||||
)
|
||||
val dPadKeys = listOf(
|
||||
KEY_DPAD_AXIS_VERTICAL,
|
||||
KEY_DPAD_AXIS_HORIZONTAL
|
||||
)
|
||||
val axisTitles = listOf(
|
||||
R.string.controller_axis_vertical,
|
||||
R.string.controller_axis_horizontal
|
||||
)
|
||||
val triggerKeys = listOf(
|
||||
KEY_BUTTON_L,
|
||||
KEY_BUTTON_R,
|
||||
KEY_BUTTON_ZL,
|
||||
KEY_BUTTON_ZR
|
||||
)
|
||||
val triggerTitles = listOf(
|
||||
R.string.button_l,
|
||||
R.string.button_r,
|
||||
R.string.button_zl,
|
||||
R.string.button_zr
|
||||
)
|
||||
|
||||
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
|
||||
const val PREF_MATERIAL_YOU = "MaterialYouTheme"
|
||||
const val PREF_THEME_MODE = "ThemeMode"
|
||||
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
|
||||
const val PREF_SHOW_HOME_APPS = "ShowHomeApps"
|
||||
|
||||
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
|
||||
|
||||
init {
|
||||
configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
|
||||
listOf(
|
||||
SECTION_CORE,
|
||||
SECTION_SYSTEM,
|
||||
SECTION_CAMERA,
|
||||
SECTION_CONTROLS,
|
||||
SECTION_RENDERER,
|
||||
SECTION_LAYOUT,
|
||||
SECTION_UTILITY,
|
||||
SECTION_AUDIO,
|
||||
SECTION_DEBUG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
// 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.settings.model
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class SettingsViewModel : ViewModel() {
|
||||
val settings = Settings()
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
public final class StringSetting extends Setting {
|
||||
private String mValue;
|
||||
|
||||
public StringSetting(String key, String section, String value) {
|
||||
super(key, section);
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return mValue;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
// 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.settings.model
|
||||
|
||||
enum class StringSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: String
|
||||
) : AbstractStringSetting {
|
||||
INIT_TIME("init_time", Settings.SECTION_SYSTEM, "946731601"),
|
||||
CAMERA_INNER_NAME("camera_inner_name", Settings.SECTION_CAMERA, "ndk"),
|
||||
CAMERA_INNER_CONFIG("camera_inner_config", Settings.SECTION_CAMERA, "_front"),
|
||||
CAMERA_OUTER_LEFT_NAME("camera_outer_left_name", Settings.SECTION_CAMERA, "ndk"),
|
||||
CAMERA_OUTER_LEFT_CONFIG("camera_outer_left_config", Settings.SECTION_CAMERA, "_back"),
|
||||
CAMERA_OUTER_RIGHT_NAME("camera_outer_right_name", Settings.SECTION_CAMERA, "ndk"),
|
||||
CAMERA_OUTER_RIGHT_CONFIG("camera_outer_right_config", Settings.SECTION_CAMERA, "_back");
|
||||
|
||||
override var string: String = defaultValue
|
||||
|
||||
override val valueAsString: String
|
||||
get() = string
|
||||
|
||||
override val isRuntimeEditable: Boolean
|
||||
get() {
|
||||
for (setting in NOT_RUNTIME_EDITABLE) {
|
||||
if (setting == this) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NOT_RUNTIME_EDITABLE = listOf(
|
||||
INIT_TIME,
|
||||
CAMERA_INNER_NAME,
|
||||
CAMERA_INNER_CONFIG,
|
||||
CAMERA_OUTER_LEFT_NAME,
|
||||
CAMERA_OUTER_LEFT_CONFIG,
|
||||
CAMERA_OUTER_RIGHT_NAME,
|
||||
CAMERA_OUTER_RIGHT_CONFIG
|
||||
)
|
||||
|
||||
fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
|
||||
|
||||
fun clear() = StringSetting.values().forEach { it.string = it.defaultValue }
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
// 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.settings.model.view
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
|
||||
interface AbstractShortSetting : AbstractSetting {
|
||||
var short: Short
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
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.Setting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
|
||||
|
||||
public final class CheckBoxSetting extends SettingsItem {
|
||||
private boolean mDefaultValue;
|
||||
private boolean mShowPerformanceWarning;
|
||||
private SettingsFragmentView mView;
|
||||
|
||||
public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
|
||||
boolean defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mDefaultValue = defaultValue;
|
||||
mShowPerformanceWarning = false;
|
||||
}
|
||||
|
||||
public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
|
||||
boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mDefaultValue = defaultValue;
|
||||
mView = view;
|
||||
mShowPerformanceWarning = show_performance_warning;
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
if (getSetting() == null) {
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
// Try integer setting
|
||||
try {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
return setting.getValue() == 1;
|
||||
} catch (ClassCastException exception) {
|
||||
}
|
||||
|
||||
// Try boolean setting
|
||||
try {
|
||||
BooleanSetting setting = (BooleanSetting) getSetting();
|
||||
return setting.getValue() == true;
|
||||
} catch (ClassCastException exception) {
|
||||
}
|
||||
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing boolean. If that boolean was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param checked Pretty self explanatory.
|
||||
* @return null if overwritten successfully; otherwise, a newly created BooleanSetting.
|
||||
*/
|
||||
public IntSetting setChecked(boolean checked) {
|
||||
// Show a performance warning if the setting has been disabled
|
||||
if (mShowPerformanceWarning && !checked) {
|
||||
mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.performance_warning), true);
|
||||
}
|
||||
|
||||
if (getSetting() == null) {
|
||||
IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
setting.setValue(checked ? 1 : 0);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_CHECKBOX;
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
|
||||
public final class DateTimeSetting extends SettingsItem {
|
||||
private String mDefaultValue;
|
||||
|
||||
public DateTimeSetting(String key, String section, int titleId, int descriptionId,
|
||||
String defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
if (getSetting() != null) {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
return setting.getValue();
|
||||
} else {
|
||||
return mDefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public StringSetting setSelectedValue(String datetime) {
|
||||
if (getSetting() == null) {
|
||||
StringSetting setting = new StringSetting(getKey(), getSection(), datetime);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
setting.setValue(datetime);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_DATETIME_SETTING;
|
||||
}
|
||||
}
|
@ -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.features.settings.model.view
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
|
||||
|
||||
class DateTimeSetting(
|
||||
setting: AbstractSetting?,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val key: String? = null,
|
||||
private val defaultValue: String? = null
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_DATETIME_SETTING
|
||||
|
||||
val value: String
|
||||
get() = if (setting != null) {
|
||||
val setting = setting as AbstractStringSetting
|
||||
setting.string
|
||||
} else {
|
||||
defaultValue!!
|
||||
}
|
||||
|
||||
fun setSelectedValue(datetime: String): AbstractStringSetting {
|
||||
val stringSetting = setting as AbstractStringSetting
|
||||
stringSetting.string = datetime
|
||||
return stringSetting
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class HeaderSetting extends SettingsItem {
|
||||
public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) {
|
||||
super(key, null, setting, titleId, descriptionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return SettingsItem.TYPE_HEADER;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
// 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.settings.model.view
|
||||
|
||||
class HeaderSetting(titleId: Int) : SettingsItem(null, titleId, 0) {
|
||||
override val type = TYPE_HEADER
|
||||
}
|
@ -1,382 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.Toast;
|
||||
|
||||
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.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
|
||||
public final class InputBindingSetting extends SettingsItem {
|
||||
private static final String INPUT_MAPPING_PREFIX = "InputMapping";
|
||||
|
||||
public InputBindingSetting(String key, String section, int titleId, Setting setting) {
|
||||
super(key, section, setting, titleId, 0);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
if (getSetting() == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
return setting.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS Circle Pad
|
||||
*/
|
||||
private boolean IsCirclePad() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
|
||||
*/
|
||||
public boolean IsHorizontalOrientation() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS C-Stick
|
||||
*/
|
||||
private boolean IsCStick() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_CSTICK_AXIS_VERTICAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS D-Pad
|
||||
*/
|
||||
private boolean IsDPad() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_DPAD_AXIS_VERTICAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
|
||||
* triggers on the 3DS, but we support them as such on a physical gamepad.
|
||||
*/
|
||||
public boolean IsTrigger() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_BUTTON_L:
|
||||
case SettingsFile.KEY_BUTTON_R:
|
||||
case SettingsFile.KEY_BUTTON_ZL:
|
||||
case SettingsFile.KEY_BUTTON_ZR:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gamepad axis can be used to map this key.
|
||||
*/
|
||||
public boolean IsAxisMappingSupported() {
|
||||
return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gamepad button can be used to map this key.
|
||||
*/
|
||||
private boolean IsButtonMappingSupported() {
|
||||
return !IsAxisMappingSupported() || IsTrigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Citra button code for the settings key.
|
||||
*/
|
||||
private int getButtonCode() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_BUTTON_A:
|
||||
return NativeLibrary.ButtonType.BUTTON_A;
|
||||
case SettingsFile.KEY_BUTTON_B:
|
||||
return NativeLibrary.ButtonType.BUTTON_B;
|
||||
case SettingsFile.KEY_BUTTON_X:
|
||||
return NativeLibrary.ButtonType.BUTTON_X;
|
||||
case SettingsFile.KEY_BUTTON_Y:
|
||||
return NativeLibrary.ButtonType.BUTTON_Y;
|
||||
case SettingsFile.KEY_BUTTON_L:
|
||||
return NativeLibrary.ButtonType.TRIGGER_L;
|
||||
case SettingsFile.KEY_BUTTON_R:
|
||||
return NativeLibrary.ButtonType.TRIGGER_R;
|
||||
case SettingsFile.KEY_BUTTON_ZL:
|
||||
return NativeLibrary.ButtonType.BUTTON_ZL;
|
||||
case SettingsFile.KEY_BUTTON_ZR:
|
||||
return NativeLibrary.ButtonType.BUTTON_ZR;
|
||||
case SettingsFile.KEY_BUTTON_SELECT:
|
||||
return NativeLibrary.ButtonType.BUTTON_SELECT;
|
||||
case SettingsFile.KEY_BUTTON_START:
|
||||
return NativeLibrary.ButtonType.BUTTON_START;
|
||||
case SettingsFile.KEY_BUTTON_UP:
|
||||
return NativeLibrary.ButtonType.DPAD_UP;
|
||||
case SettingsFile.KEY_BUTTON_DOWN:
|
||||
return NativeLibrary.ButtonType.DPAD_DOWN;
|
||||
case SettingsFile.KEY_BUTTON_LEFT:
|
||||
return NativeLibrary.ButtonType.DPAD_LEFT;
|
||||
case SettingsFile.KEY_BUTTON_RIGHT:
|
||||
return NativeLibrary.ButtonType.DPAD_RIGHT;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the settings key for the specified Citra button code.
|
||||
*/
|
||||
private static String getButtonKey(int buttonCode) {
|
||||
switch (buttonCode) {
|
||||
case NativeLibrary.ButtonType.BUTTON_A:
|
||||
return SettingsFile.KEY_BUTTON_A;
|
||||
case NativeLibrary.ButtonType.BUTTON_B:
|
||||
return SettingsFile.KEY_BUTTON_B;
|
||||
case NativeLibrary.ButtonType.BUTTON_X:
|
||||
return SettingsFile.KEY_BUTTON_X;
|
||||
case NativeLibrary.ButtonType.BUTTON_Y:
|
||||
return SettingsFile.KEY_BUTTON_Y;
|
||||
case NativeLibrary.ButtonType.TRIGGER_L:
|
||||
return SettingsFile.KEY_BUTTON_L;
|
||||
case NativeLibrary.ButtonType.TRIGGER_R:
|
||||
return SettingsFile.KEY_BUTTON_R;
|
||||
case NativeLibrary.ButtonType.BUTTON_ZL:
|
||||
return SettingsFile.KEY_BUTTON_ZL;
|
||||
case NativeLibrary.ButtonType.BUTTON_ZR:
|
||||
return SettingsFile.KEY_BUTTON_ZR;
|
||||
case NativeLibrary.ButtonType.BUTTON_SELECT:
|
||||
return SettingsFile.KEY_BUTTON_SELECT;
|
||||
case NativeLibrary.ButtonType.BUTTON_START:
|
||||
return SettingsFile.KEY_BUTTON_START;
|
||||
case NativeLibrary.ButtonType.DPAD_UP:
|
||||
return SettingsFile.KEY_BUTTON_UP;
|
||||
case NativeLibrary.ButtonType.DPAD_DOWN:
|
||||
return SettingsFile.KEY_BUTTON_DOWN;
|
||||
case NativeLibrary.ButtonType.DPAD_LEFT:
|
||||
return SettingsFile.KEY_BUTTON_LEFT;
|
||||
case NativeLibrary.ButtonType.DPAD_RIGHT:
|
||||
return SettingsFile.KEY_BUTTON_RIGHT;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
|
||||
* settings on re-mapping or clearing of a setting.
|
||||
*/
|
||||
private String getReverseKey() {
|
||||
String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey();
|
||||
|
||||
if (IsAxisMappingSupported() && !IsTrigger()) {
|
||||
// Triggers are the only axis-supported mappings without orientation
|
||||
reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1);
|
||||
}
|
||||
|
||||
return reverseKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
|
||||
*/
|
||||
public void removeOldMapping() {
|
||||
// Get preferences editor
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
// Try remove all possible keys we wrote for this setting
|
||||
String oldKey = preferences.getString(getReverseKey(), "");
|
||||
if (!oldKey.equals("")) {
|
||||
editor.remove(getKey()); // Used for ui text
|
||||
editor.remove(oldKey); // Used for button mapping
|
||||
editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation
|
||||
editor.remove(oldKey + "_GuestButton"); // Used for axis button
|
||||
}
|
||||
|
||||
// Apply changes
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad button.
|
||||
*/
|
||||
public static String getInputButtonKey(int keyCode) {
|
||||
return INPUT_MAPPING_PREFIX + "_Button_" + keyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis.
|
||||
*/
|
||||
public static String getInputAxisKey(int axis) {
|
||||
return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis button (stick or trigger).
|
||||
*/
|
||||
public static String getInputAxisButtonKey(int axis) {
|
||||
return getInputAxisKey(axis) + "_GuestButton";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis orientation.
|
||||
*/
|
||||
public static String getInputAxisOrientationKey(int axis) {
|
||||
return getInputAxisKey(axis) + "_GuestOrientation";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to write a gamepad button mapping for the setting.
|
||||
*/
|
||||
private void WriteButtonMapping(String key) {
|
||||
// Get preferences editor
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
// Remove mapping for another setting using this input
|
||||
int oldButtonCode = preferences.getInt(key, -1);
|
||||
if (oldButtonCode != -1) {
|
||||
String oldKey = getButtonKey(oldButtonCode);
|
||||
editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten
|
||||
}
|
||||
|
||||
// Cleanup old mapping for this setting
|
||||
removeOldMapping();
|
||||
|
||||
// Write new mapping
|
||||
editor.putInt(key, getButtonCode());
|
||||
|
||||
// Write next reverse mapping for future cleanup
|
||||
editor.putString(getReverseKey(), key);
|
||||
|
||||
// Apply changes
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to write a gamepad axis mapping for the setting.
|
||||
*/
|
||||
private void WriteAxisMapping(int axis, int value) {
|
||||
// Get preferences editor
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
// Cleanup old mapping
|
||||
removeOldMapping();
|
||||
|
||||
// Write new mapping
|
||||
editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1);
|
||||
editor.putInt(getInputAxisButtonKey(axis), value);
|
||||
|
||||
// Write next reverse mapping for future cleanup
|
||||
editor.putString(getReverseKey(), getInputAxisKey(axis));
|
||||
|
||||
// Apply changes
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided key input setting as an Android preference.
|
||||
*
|
||||
* @param keyEvent KeyEvent of this key press.
|
||||
*/
|
||||
public void onKeyInput(KeyEvent keyEvent) {
|
||||
if (!IsButtonMappingSupported()) {
|
||||
Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
InputDevice device = keyEvent.getDevice();
|
||||
|
||||
WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode()));
|
||||
|
||||
String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
|
||||
setUiString(uiString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided motion input setting as an Android preference.
|
||||
*
|
||||
* @param device InputDevice from which the input event originated.
|
||||
* @param motionRange MotionRange of the movement
|
||||
* @param axisDir Either '-' or '+' (currently unused)
|
||||
*/
|
||||
public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
|
||||
char axisDir) {
|
||||
if (!IsAxisMappingSupported()) {
|
||||
Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
int button;
|
||||
if (IsCirclePad()) {
|
||||
button = NativeLibrary.ButtonType.STICK_LEFT;
|
||||
} else if (IsCStick()) {
|
||||
button = NativeLibrary.ButtonType.STICK_C;
|
||||
} else if (IsDPad()) {
|
||||
button = NativeLibrary.ButtonType.DPAD;
|
||||
} else {
|
||||
button = getButtonCode();
|
||||
}
|
||||
|
||||
WriteAxisMapping(motionRange.getAxis(), button);
|
||||
|
||||
String uiString = device.getName() + ": Axis " + motionRange.getAxis();
|
||||
setUiString(uiString);
|
||||
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string to use in the configuration UI for the gamepad input.
|
||||
*/
|
||||
private StringSetting setUiString(String ui) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
if (getSetting() == null) {
|
||||
StringSetting setting = new StringSetting(getKey(), getSection(), "");
|
||||
setSetting(setting);
|
||||
|
||||
editor.putString(setting.getKey(), ui);
|
||||
editor.apply();
|
||||
|
||||
return setting;
|
||||
} else {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
|
||||
editor.putString(setting.getKey(), ui);
|
||||
editor.apply();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_INPUT_BINDING;
|
||||
}
|
||||
}
|
@ -0,0 +1,299 @@
|
||||
// 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.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 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.Settings
|
||||
|
||||
class InputBindingSetting(
|
||||
val abstractSetting: AbstractSetting,
|
||||
titleId: Int
|
||||
) : SettingsItem(abstractSetting, titleId, 0) {
|
||||
private val context: Context get() = CitraApplication.appContext
|
||||
private val preferences: SharedPreferences
|
||||
get() = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
var value: String
|
||||
get() = preferences.getString(abstractSetting.key, "")!!
|
||||
set(string) {
|
||||
preferences.edit()
|
||||
.putString(abstractSetting.key, string)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS Circle Pad
|
||||
*/
|
||||
fun isCirclePad(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL,
|
||||
Settings.KEY_CIRCLEPAD_AXIS_VERTICAL -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
|
||||
*/
|
||||
fun isHorizontalOrientation(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL,
|
||||
Settings.KEY_CSTICK_AXIS_HORIZONTAL,
|
||||
Settings.KEY_DPAD_AXIS_HORIZONTAL -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS C-Stick
|
||||
*/
|
||||
fun isCStick(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_CSTICK_AXIS_HORIZONTAL,
|
||||
Settings.KEY_CSTICK_AXIS_VERTICAL -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS D-Pad
|
||||
*/
|
||||
fun isDPad(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_DPAD_AXIS_HORIZONTAL,
|
||||
Settings.KEY_DPAD_AXIS_VERTICAL -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
|
||||
* triggers on the 3DS, but we support them as such on a physical gamepad.
|
||||
*/
|
||||
fun isTrigger(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_BUTTON_L,
|
||||
Settings.KEY_BUTTON_R,
|
||||
Settings.KEY_BUTTON_ZL,
|
||||
Settings.KEY_BUTTON_ZR -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gamepad axis can be used to map this key.
|
||||
*/
|
||||
fun isAxisMappingSupported(): Boolean {
|
||||
return isCirclePad() || isCStick() || isDPad() || isTrigger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gamepad button can be used to map this key.
|
||||
*/
|
||||
fun isButtonMappingSupported(): Boolean {
|
||||
return !isAxisMappingSupported() || isTrigger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Citra button code for the settings key.
|
||||
*/
|
||||
private val buttonCode: Int
|
||||
get() =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
|
||||
Settings.KEY_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
|
||||
Settings.KEY_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
|
||||
Settings.KEY_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
|
||||
Settings.KEY_BUTTON_L -> NativeLibrary.ButtonType.TRIGGER_L
|
||||
Settings.KEY_BUTTON_R -> NativeLibrary.ButtonType.TRIGGER_R
|
||||
Settings.KEY_BUTTON_ZL -> NativeLibrary.ButtonType.BUTTON_ZL
|
||||
Settings.KEY_BUTTON_ZR -> NativeLibrary.ButtonType.BUTTON_ZR
|
||||
Settings.KEY_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_SELECT
|
||||
Settings.KEY_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_START
|
||||
Settings.KEY_BUTTON_HOME -> NativeLibrary.ButtonType.BUTTON_HOME
|
||||
Settings.KEY_BUTTON_UP -> NativeLibrary.ButtonType.DPAD_UP
|
||||
Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
|
||||
Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
|
||||
Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
|
||||
else -> -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
|
||||
* settings on re-mapping or clearing of a setting.
|
||||
*/
|
||||
private val reverseKey: String
|
||||
get() {
|
||||
var reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${abstractSetting.key}"
|
||||
if (isAxisMappingSupported() && !isTrigger()) {
|
||||
// Triggers are the only axis-supported mappings without orientation
|
||||
reverseKey += "_" + if (isHorizontalOrientation()) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
return reverseKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
|
||||
*/
|
||||
fun removeOldMapping() {
|
||||
// Try remove all possible keys we wrote for this setting
|
||||
val oldKey = preferences.getString(reverseKey, "")
|
||||
if (oldKey != "") {
|
||||
preferences.edit()
|
||||
.remove(abstractSetting.key) // Used for ui text
|
||||
.remove(oldKey) // Used for button mapping
|
||||
.remove(oldKey + "_GuestOrientation") // Used for axis orientation
|
||||
.remove(oldKey + "_GuestButton") // Used for axis button
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to write a gamepad button mapping for the setting.
|
||||
*/
|
||||
private fun writeButtonMapping(key: String) {
|
||||
val editor = preferences.edit()
|
||||
|
||||
// Remove mapping for another setting using this input
|
||||
val oldButtonCode = preferences.getInt(key, -1)
|
||||
if (oldButtonCode != -1) {
|
||||
val oldKey = getButtonKey(oldButtonCode)
|
||||
editor.remove(oldKey) // Only need to remove UI text setting, others will be overwritten
|
||||
}
|
||||
|
||||
// Cleanup old mapping for this setting
|
||||
removeOldMapping()
|
||||
|
||||
// Write new mapping
|
||||
editor.putInt(key, buttonCode)
|
||||
|
||||
// Write next reverse mapping for future cleanup
|
||||
editor.putString(reverseKey, key)
|
||||
|
||||
// Apply changes
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to write a gamepad axis mapping for the setting.
|
||||
*/
|
||||
private fun writeAxisMapping(axis: Int, value: Int) {
|
||||
// Cleanup old mapping
|
||||
removeOldMapping()
|
||||
|
||||
// Write new mapping
|
||||
preferences.edit()
|
||||
.putInt(getInputAxisOrientationKey(axis), if (isHorizontalOrientation()) 0 else 1)
|
||||
.putInt(getInputAxisButtonKey(axis), value)
|
||||
// Write next reverse mapping for future cleanup
|
||||
.putString(reverseKey, getInputAxisKey(axis))
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided key input setting as an Android preference.
|
||||
*
|
||||
* @param keyEvent KeyEvent of this key press.
|
||||
*/
|
||||
fun onKeyInput(keyEvent: KeyEvent) {
|
||||
if (!isButtonMappingSupported()) {
|
||||
Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
writeButtonMapping(getInputButtonKey(keyEvent.keyCode))
|
||||
val uiString = "${keyEvent.device.name}: Button ${keyEvent.keyCode}"
|
||||
value = uiString
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided motion input setting as an Android preference.
|
||||
*
|
||||
* @param device InputDevice from which the input event originated.
|
||||
* @param motionRange MotionRange of the movement
|
||||
* @param axisDir Either '-' or '+' (currently unused)
|
||||
*/
|
||||
fun onMotionInput(device: InputDevice, motionRange: MotionRange, axisDir: Char) {
|
||||
if (!isAxisMappingSupported()) {
|
||||
Toast.makeText(context, R.string.input_message_button_only, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
val button = if (isCirclePad()) {
|
||||
NativeLibrary.ButtonType.STICK_LEFT
|
||||
} else if (isCStick()) {
|
||||
NativeLibrary.ButtonType.STICK_C
|
||||
} else if (isDPad()) {
|
||||
NativeLibrary.ButtonType.DPAD
|
||||
} else {
|
||||
buttonCode
|
||||
}
|
||||
writeAxisMapping(motionRange.axis, button)
|
||||
val uiString = "${device.name}: Axis ${motionRange.axis}"
|
||||
value = uiString
|
||||
}
|
||||
|
||||
override val type = TYPE_INPUT_BINDING
|
||||
|
||||
companion object {
|
||||
private const val INPUT_MAPPING_PREFIX = "InputMapping"
|
||||
|
||||
/**
|
||||
* Returns the settings key for the specified Citra button code.
|
||||
*/
|
||||
private fun getButtonKey(buttonCode: Int): String =
|
||||
when (buttonCode) {
|
||||
NativeLibrary.ButtonType.BUTTON_A -> Settings.KEY_BUTTON_A
|
||||
NativeLibrary.ButtonType.BUTTON_B -> Settings.KEY_BUTTON_B
|
||||
NativeLibrary.ButtonType.BUTTON_X -> Settings.KEY_BUTTON_X
|
||||
NativeLibrary.ButtonType.BUTTON_Y -> Settings.KEY_BUTTON_Y
|
||||
NativeLibrary.ButtonType.TRIGGER_L -> Settings.KEY_BUTTON_L
|
||||
NativeLibrary.ButtonType.TRIGGER_R -> Settings.KEY_BUTTON_R
|
||||
NativeLibrary.ButtonType.BUTTON_ZL -> Settings.KEY_BUTTON_ZL
|
||||
NativeLibrary.ButtonType.BUTTON_ZR -> Settings.KEY_BUTTON_ZR
|
||||
NativeLibrary.ButtonType.BUTTON_SELECT -> Settings.KEY_BUTTON_SELECT
|
||||
NativeLibrary.ButtonType.BUTTON_START -> Settings.KEY_BUTTON_START
|
||||
NativeLibrary.ButtonType.BUTTON_HOME -> Settings.KEY_BUTTON_HOME
|
||||
NativeLibrary.ButtonType.DPAD_UP -> Settings.KEY_BUTTON_UP
|
||||
NativeLibrary.ButtonType.DPAD_DOWN -> Settings.KEY_BUTTON_DOWN
|
||||
NativeLibrary.ButtonType.DPAD_LEFT -> Settings.KEY_BUTTON_LEFT
|
||||
NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT
|
||||
else -> ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad button.
|
||||
*/
|
||||
fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}"
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis.
|
||||
*/
|
||||
fun getInputAxisKey(axis: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${axis}"
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis button (stick or trigger).
|
||||
*/
|
||||
fun getInputAxisButtonKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestButton"
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis orientation.
|
||||
*/
|
||||
fun getInputAxisOrientationKey(axis: Int): String =
|
||||
"${getInputAxisKey(axis)}_GuestOrientation"
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class PremiumHeader extends SettingsItem {
|
||||
public PremiumHeader() {
|
||||
super(null, null, null, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return SettingsItem.TYPE_PREMIUM;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
|
||||
|
||||
public final class PremiumSingleChoiceSetting extends SettingsItem {
|
||||
private int mDefaultValue;
|
||||
|
||||
private int mChoicesId;
|
||||
private int mValuesId;
|
||||
private SettingsFragmentView mView;
|
||||
|
||||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
|
||||
public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
|
||||
int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mValuesId = valuesId;
|
||||
mChoicesId = choicesId;
|
||||
mDefaultValue = defaultValue;
|
||||
mView = view;
|
||||
}
|
||||
|
||||
public int getChoicesId() {
|
||||
return mChoicesId;
|
||||
}
|
||||
|
||||
public int getValuesId() {
|
||||
return mValuesId;
|
||||
}
|
||||
|
||||
public int getSelectedValue() {
|
||||
return mPreferences.getInt(getKey(), mDefaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
||||
*/
|
||||
public void setSelectedValue(int selection) {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putInt(getKey(), selection);
|
||||
editor.apply();
|
||||
mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.design_updated), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_SINGLE_CHOICE;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
// 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.settings.model.view
|
||||
|
||||
class RunnableSetting(
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val isRuntimeRunnable: Boolean,
|
||||
val runnable: () -> Unit,
|
||||
val value: (() -> String)? = null
|
||||
) : SettingsItem(null, titleId, descriptionId) {
|
||||
override val type = TYPE_RUNNABLE
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
/**
|
||||
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
|
||||
* Each one corresponds to a {@link Setting} object, so this class's subclasses
|
||||
* should vaguely correspond to those subclasses. There are a few with multiple analogues
|
||||
* and a few with none (Headers, for example, do not correspond to anything in the ini
|
||||
* file.)
|
||||
*/
|
||||
public abstract class SettingsItem {
|
||||
public static final int TYPE_HEADER = 0;
|
||||
public static final int TYPE_CHECKBOX = 1;
|
||||
public static final int TYPE_SINGLE_CHOICE = 2;
|
||||
public static final int TYPE_SLIDER = 3;
|
||||
public static final int TYPE_SUBMENU = 4;
|
||||
public static final int TYPE_INPUT_BINDING = 5;
|
||||
public static final int TYPE_STRING_SINGLE_CHOICE = 6;
|
||||
public static final int TYPE_DATETIME_SETTING = 7;
|
||||
public static final int TYPE_PREMIUM = 8;
|
||||
|
||||
private String mKey;
|
||||
private String mSection;
|
||||
|
||||
private Setting mSetting;
|
||||
|
||||
private int mNameId;
|
||||
private int mDescriptionId;
|
||||
private boolean mIsPremium;
|
||||
|
||||
/**
|
||||
* Base constructor. Takes a key / section name in case the third parameter, the Setting,
|
||||
* is null; in which case, one can be constructed and saved using the key / section.
|
||||
*
|
||||
* @param key Identifier for the Setting represented by this Item.
|
||||
* @param section Section to which the Setting belongs.
|
||||
* @param setting A possibly-null backing Setting, to be modified on UI events.
|
||||
* @param nameId Resource ID for a text string to be displayed as this setting's name.
|
||||
* @param descriptionId Resource ID for a text string to be displayed as this setting's description.
|
||||
*/
|
||||
public SettingsItem(String key, String section, Setting setting, int nameId,
|
||||
int descriptionId) {
|
||||
mKey = key;
|
||||
mSection = section;
|
||||
mSetting = setting;
|
||||
mNameId = nameId;
|
||||
mDescriptionId = descriptionId;
|
||||
mIsPremium = (section == Settings.SECTION_PREMIUM);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The identifier for the backing Setting.
|
||||
*/
|
||||
public String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The header under which the backing Setting belongs.
|
||||
*/
|
||||
public String getSection() {
|
||||
return mSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The backing Setting, possibly null.
|
||||
*/
|
||||
public Setting getSetting() {
|
||||
return mSetting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the backing setting with a new one. Generally used in cases where
|
||||
* the backing setting is null.
|
||||
*
|
||||
* @param setting A non-null Setting.
|
||||
*/
|
||||
public void setSetting(Setting setting) {
|
||||
mSetting = setting;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A resource ID for a text string representing this Setting's name.
|
||||
*/
|
||||
public int getNameId() {
|
||||
return mNameId;
|
||||
}
|
||||
|
||||
public int getDescriptionId() {
|
||||
return mDescriptionId;
|
||||
}
|
||||
|
||||
public boolean isPremium() {
|
||||
return mIsPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by {@link SettingsAdapter}'s onCreateViewHolder()
|
||||
* method to determine which type of ViewHolder should be created.
|
||||
*
|
||||
* @return An integer (ideally, one of the constants defined in this file)
|
||||
*/
|
||||
public abstract int getType();
|
||||
}
|
@ -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.features.settings.model.view
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
|
||||
/**
|
||||
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
|
||||
* Each one corresponds to a [AbstractSetting] object, so this class's subclasses
|
||||
* should vaguely correspond to those subclasses. There are a few with multiple analogues
|
||||
* and a few with none (Headers, for example, do not correspond to anything in the ini
|
||||
* file.)
|
||||
*/
|
||||
abstract class SettingsItem(
|
||||
var setting: AbstractSetting?,
|
||||
val nameId: Int,
|
||||
val descriptionId: Int
|
||||
) {
|
||||
abstract val type: Int
|
||||
|
||||
val isEditable: Boolean
|
||||
get() {
|
||||
if (!NativeLibrary.isRunning()) return true
|
||||
return setting?.isRuntimeEditable ?: false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_HEADER = 0
|
||||
const val TYPE_SWITCH = 1
|
||||
const val TYPE_SINGLE_CHOICE = 2
|
||||
const val TYPE_SLIDER = 3
|
||||
const val TYPE_SUBMENU = 4
|
||||
const val TYPE_STRING_SINGLE_CHOICE = 5
|
||||
const val TYPE_DATETIME_SETTING = 6
|
||||
const val TYPE_RUNNABLE = 7
|
||||
const val TYPE_INPUT_BINDING = 8
|
||||
const val TYPE_STRING_INPUT = 9
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class SingleChoiceSetting extends SettingsItem {
|
||||
private int mDefaultValue;
|
||||
|
||||
private int mChoicesId;
|
||||
private int mValuesId;
|
||||
|
||||
public SingleChoiceSetting(String key, String section, int titleId, int descriptionId,
|
||||
int choicesId, int valuesId, int defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mValuesId = valuesId;
|
||||
mChoicesId = choicesId;
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public int getChoicesId() {
|
||||
return mChoicesId;
|
||||
}
|
||||
|
||||
public int getValuesId() {
|
||||
return mValuesId;
|
||||
}
|
||||
|
||||
public int getSelectedValue() {
|
||||
if (getSetting() != null) {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
return setting.getValue();
|
||||
} else {
|
||||
return mDefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
||||
*/
|
||||
public IntSetting setSelectedValue(int selection) {
|
||||
if (getSetting() == null) {
|
||||
IntSetting setting = new IntSetting(getKey(), getSection(), selection);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
setting.setValue(selection);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_SINGLE_CHOICE;
|
||||
}
|
||||
}
|
@ -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.features.settings.model.view
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.AbstractIntSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
|
||||
class SingleChoiceSetting(
|
||||
setting: AbstractSetting?,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val choicesId: Int,
|
||||
val valuesId: Int,
|
||||
val key: String? = null,
|
||||
val defaultValue: Int? = null
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SINGLE_CHOICE
|
||||
|
||||
val selectedValue: Int
|
||||
get() {
|
||||
if (setting == null) {
|
||||
return defaultValue!!
|
||||
}
|
||||
|
||||
try {
|
||||
val setting = setting as AbstractIntSetting
|
||||
return setting.int
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
|
||||
try {
|
||||
val setting = setting as AbstractShortSetting
|
||||
return setting.short.toInt()
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
|
||||
return defaultValue!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return the existing setting with the new value applied.
|
||||
*/
|
||||
fun setSelectedValue(selection: Int): AbstractIntSetting {
|
||||
val intSetting = setting as AbstractIntSetting
|
||||
intSetting.int = selection
|
||||
return intSetting
|
||||
}
|
||||
|
||||
fun setSelectedValue(selection: Short): AbstractShortSetting {
|
||||
val shortSetting = setting as AbstractShortSetting
|
||||
shortSetting.short = selection
|
||||
return shortSetting
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.FloatSetting;
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
public final class SliderSetting extends SettingsItem {
|
||||
private int mMin;
|
||||
private int mMax;
|
||||
private int mDefaultValue;
|
||||
|
||||
private String mUnits;
|
||||
|
||||
public SliderSetting(String key, String section, int titleId, int descriptionId,
|
||||
int min, int max, String units, int defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mMin = min;
|
||||
mMax = max;
|
||||
mUnits = units;
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public int getMin() {
|
||||
return mMin;
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return mMax;
|
||||
}
|
||||
|
||||
public int getDefaultValue() {
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
public int getSelectedValue() {
|
||||
Setting setting = getSetting();
|
||||
|
||||
if (setting == null) {
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
if (setting instanceof IntSetting) {
|
||||
IntSetting intSetting = (IntSetting) setting;
|
||||
return intSetting.getValue();
|
||||
} else if (setting instanceof FloatSetting) {
|
||||
FloatSetting floatSetting = (FloatSetting) setting;
|
||||
return Math.round(floatSetting.getValue());
|
||||
} else {
|
||||
Log.error("[SliderSetting] Error casting setting type.");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
||||
*/
|
||||
public IntSetting setSelectedValue(int selection) {
|
||||
if (getSetting() == null) {
|
||||
IntSetting setting = new IntSetting(getKey(), getSection(), selection);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
setting.setValue(selection);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing float. If that float was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the float.
|
||||
* @return null if overwritten successfully otherwise; a newly created FloatSetting.
|
||||
*/
|
||||
public FloatSetting setSelectedValue(float selection) {
|
||||
if (getSetting() == null) {
|
||||
FloatSetting setting = new FloatSetting(getKey(), getSection(), selection);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
FloatSetting setting = (FloatSetting) getSetting();
|
||||
setting.setValue(selection);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getUnits() {
|
||||
return mUnits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_SLIDER;
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
// 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.settings.model.view
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.AbstractFloatSetting
|
||||
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.FloatSetting
|
||||
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SliderSetting(
|
||||
setting: AbstractSetting?,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val min: Int,
|
||||
val max: Int,
|
||||
val units: String,
|
||||
val key: String? = null,
|
||||
val defaultValue: Float? = null
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SLIDER
|
||||
|
||||
val selectedValue: Int
|
||||
get() {
|
||||
val setting = setting ?: return defaultValue!!.toInt()
|
||||
return when (setting) {
|
||||
is AbstractIntSetting -> setting.int
|
||||
is FloatSetting -> setting.float.roundToInt()
|
||||
is ScaledFloatSetting -> setting.float.roundToInt()
|
||||
else -> {
|
||||
Log.error("[SliderSetting] Error casting setting type.")
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return the existing setting with the new value applied.
|
||||
*/
|
||||
fun setSelectedValue(selection: Int): AbstractIntSetting {
|
||||
val intSetting = setting as AbstractIntSetting
|
||||
intSetting.int = selection
|
||||
return intSetting
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing float. If that float was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the float.
|
||||
* @return the existing setting with the new value applied.
|
||||
*/
|
||||
fun setSelectedValue(selection: Float): AbstractFloatSetting {
|
||||
val floatSetting = setting as AbstractFloatSetting
|
||||
if (floatSetting is ScaledFloatSetting) {
|
||||
floatSetting.float = selection
|
||||
} else {
|
||||
floatSetting.float = selection
|
||||
}
|
||||
return floatSetting
|
||||
}
|
||||
}
|
@ -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.settings.model.view
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
|
||||
|
||||
class StringInputSetting(
|
||||
setting: AbstractSetting?,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val defaultValue: String,
|
||||
val characterLimit: Int = 0
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_STRING_INPUT
|
||||
|
||||
val selectedValue: String
|
||||
get() = setting?.valueAsString ?: defaultValue
|
||||
|
||||
fun setSelectedValue(selection: String): AbstractStringSetting {
|
||||
val stringSetting = setting as AbstractStringSetting
|
||||
stringSetting.string = selection
|
||||
return stringSetting
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
|
||||
public class StringSingleChoiceSetting extends SettingsItem {
|
||||
private String mDefaultValue;
|
||||
|
||||
private String[] mChoicesId;
|
||||
private String[] mValuesId;
|
||||
|
||||
public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
|
||||
String[] choicesId, String[] valuesId, String defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mValuesId = valuesId;
|
||||
mChoicesId = choicesId;
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public String[] getChoicesId() {
|
||||
return mChoicesId;
|
||||
}
|
||||
|
||||
public String[] getValuesId() {
|
||||
return mValuesId;
|
||||
}
|
||||
|
||||
public String getValueAt(int index) {
|
||||
if (mValuesId == null)
|
||||
return null;
|
||||
|
||||
if (index >= 0 && index < mValuesId.length) {
|
||||
return mValuesId[index];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getSelectedValue() {
|
||||
if (getSetting() != null) {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
return setting.getValue();
|
||||
} else {
|
||||
return mDefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public int getSelectValueIndex() {
|
||||
String selectedValue = getSelectedValue();
|
||||
for (int i = 0; i < mValuesId.length; i++) {
|
||||
if (mValuesId[i].equals(selectedValue)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
||||
*/
|
||||
public StringSetting setSelectedValue(String selection) {
|
||||
if (getSetting() == null) {
|
||||
StringSetting setting = new StringSetting(getKey(), getSection(), selection);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
setting.setValue(selection);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_STRING_SINGLE_CHOICE;
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
// 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.settings.model.view
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
|
||||
|
||||
class StringSingleChoiceSetting(
|
||||
setting: AbstractSetting?,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val choices: Array<String>,
|
||||
val values: Array<String>?,
|
||||
val key: String? = null,
|
||||
private val defaultValue: String? = null
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_STRING_SINGLE_CHOICE
|
||||
|
||||
fun getValueAt(index: Int): String? {
|
||||
if (values == null) return null
|
||||
return if (index >= 0 && index < values.size) {
|
||||
values[index]
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
val selectedValue: String
|
||||
get() {
|
||||
if (setting == null) {
|
||||
return defaultValue!!
|
||||
}
|
||||
|
||||
try {
|
||||
val setting = setting as AbstractStringSetting
|
||||
return setting.string
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
|
||||
try {
|
||||
val setting = setting as AbstractShortSetting
|
||||
return setting.short.toString()
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
return defaultValue!!
|
||||
}
|
||||
val selectValueIndex: Int
|
||||
get() {
|
||||
val selectedValue = selectedValue
|
||||
for (i in values!!.indices) {
|
||||
if (values[i] == selectedValue) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return the existing setting with the new value applied.
|
||||
*/
|
||||
fun setSelectedValue(selection: String): AbstractStringSetting {
|
||||
val stringSetting = setting as AbstractStringSetting
|
||||
stringSetting.string = selection
|
||||
return stringSetting
|
||||
}
|
||||
|
||||
fun setSelectedValue(selection: Short): AbstractShortSetting {
|
||||
val shortSetting = setting as AbstractShortSetting
|
||||
shortSetting.short = selection
|
||||
return shortSetting
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class SubmenuSetting extends SettingsItem {
|
||||
private String mMenuKey;
|
||||
|
||||
public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) {
|
||||
super(key, null, setting, titleId, descriptionId);
|
||||
mMenuKey = menuKey;
|
||||
}
|
||||
|
||||
public String getMenuKey() {
|
||||
return mMenuKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_SUBMENU;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
// 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.settings.model.view
|
||||
|
||||
class SubmenuSetting(
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val menuKey: String
|
||||
) : SettingsItem(null, titleId, descriptionId) {
|
||||
override val type = TYPE_SUBMENU
|
||||
}
|
@ -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.settings.model.view
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractIntSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
|
||||
class SwitchSetting(
|
||||
setting: AbstractSetting,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val key: String? = null,
|
||||
val defaultValue: Any? = null
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SWITCH
|
||||
|
||||
val isChecked: Boolean
|
||||
get() {
|
||||
if (setting == null) {
|
||||
return defaultValue as Boolean
|
||||
}
|
||||
|
||||
// Try integer setting
|
||||
try {
|
||||
val setting = setting as AbstractIntSetting
|
||||
return setting.int == 1
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
|
||||
// Try boolean setting
|
||||
try {
|
||||
val setting = setting as AbstractBooleanSetting
|
||||
return setting.boolean
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
return defaultValue as Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing boolean. If that boolean was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param checked Pretty self explanatory.
|
||||
* @return the existing setting with the new value applied.
|
||||
*/
|
||||
fun setChecked(checked: Boolean): AbstractSetting {
|
||||
// Try integer setting
|
||||
try {
|
||||
val setting = setting as AbstractIntSetting
|
||||
setting.int = if (checked) 1 else 0
|
||||
return setting
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
|
||||
// Try boolean setting
|
||||
val setting = setting as AbstractBooleanSetting
|
||||
setting.boolean = checked
|
||||
return setting
|
||||
}
|
||||
}
|
@ -1,242 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
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.WindowInsetsCompat;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.InsetsHelper;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView {
|
||||
private static final String ARG_MENU_TAG = "menu_tag";
|
||||
private static final String ARG_GAME_ID = "game_id";
|
||||
private static final String FRAGMENT_TAG = "settings";
|
||||
private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this);
|
||||
|
||||
private ProgressDialog dialog;
|
||||
|
||||
public static void launch(Context context, String menuTag, String gameId) {
|
||||
Intent settings = new Intent(context, SettingsActivity.class);
|
||||
settings.putExtra(ARG_MENU_TAG, menuTag);
|
||||
settings.putExtra(ARG_GAME_ID, gameId);
|
||||
context.startActivity(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
ThemeUtil.applyTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
Intent launcher = getIntent();
|
||||
String gameID = launcher.getStringExtra(ARG_GAME_ID);
|
||||
String menuTag = launcher.getStringExtra(ARG_MENU_TAG);
|
||||
|
||||
mPresenter.onCreate(savedInstanceState, menuTag, gameID);
|
||||
|
||||
// Show "Back" button in the action bar for navigation
|
||||
MaterialToolbar toolbar = findViewById(R.id.toolbar_settings);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
setInsets();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_settings, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
// Critical: If super method is not called, rotations will be busted.
|
||||
super.onSaveInstanceState(outState);
|
||||
mPresenter.saveState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
mPresenter.onStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is called, the user has left the settings screen (potentially through the
|
||||
* home button) and will expect their changes to be persisted. So we kick off an
|
||||
* IntentService which will do so on a background thread.
|
||||
*/
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
mPresenter.onStop(isFinishing());
|
||||
|
||||
// Update framebuffer layout when closing the settings
|
||||
NativeLibrary.NotifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(),
|
||||
getWindowManager().getDefaultDisplay().getRotation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) {
|
||||
if (!addToStack && getFragment() != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
|
||||
if (addToStack) {
|
||||
if (areSystemAnimationsEnabled()) {
|
||||
transaction.setCustomAnimations(
|
||||
R.anim.anim_settings_fragment_in,
|
||||
R.anim.anim_settings_fragment_out,
|
||||
0,
|
||||
R.anim.anim_pop_settings_fragment_out);
|
||||
}
|
||||
|
||||
transaction.addToBackStack(null);
|
||||
}
|
||||
transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG);
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
private boolean areSystemAnimationsEnabled() {
|
||||
float duration = Settings.Global.getFloat(
|
||||
getContentResolver(),
|
||||
Settings.Global.ANIMATOR_DURATION_SCALE, 1);
|
||||
float transition = Settings.Global.getFloat(
|
||||
getContentResolver(),
|
||||
Settings.Global.TRANSITION_ANIMATION_SCALE, 1);
|
||||
return duration != 0 && transition != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) {
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||
receiver,
|
||||
filter);
|
||||
DirectoryInitialization.start(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
if (dialog == null) {
|
||||
dialog = new ProgressDialog(this);
|
||||
dialog.setMessage(getString(R.string.load_settings));
|
||||
dialog.setIndeterminate(true);
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showPermissionNeededHint() {
|
||||
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showExternalStorageNotMountedHint() {
|
||||
Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.citra.citra_emu.features.settings.model.Settings getSettings() {
|
||||
return mPresenter.getSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) {
|
||||
mPresenter.setSettings(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) {
|
||||
SettingsFragmentView fragment = getFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
fragment.onSettingsFileLoaded(settings);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsFileNotFound() {
|
||||
SettingsFragmentView fragment = getFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
fragment.loadDefaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showToastMessage(String message, boolean is_long) {
|
||||
Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingChanged() {
|
||||
mPresenter.onSettingChanged();
|
||||
}
|
||||
|
||||
private SettingsFragment getFragment() {
|
||||
return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
private void setInsets() {
|
||||
AppBarLayout appBar = findViewById(R.id.appbar_settings);
|
||||
FrameLayout frame = findViewById(R.id.frame_content);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
InsetsHelper.insetAppBar(insets, appBar);
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
// 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.settings.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.databinding.ActivitySettingsBinding
|
||||
import java.io.IOException
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.FloatSetting
|
||||
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.SettingsViewModel
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||
import org.citra.citra_emu.utils.InsetsHelper
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
|
||||
class SettingsActivity : AppCompatActivity(), SettingsActivityView {
|
||||
private val presenter = SettingsActivityPresenter(this)
|
||||
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
|
||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
|
||||
override val settings: Settings get() = settingsViewModel.settings
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeUtil.setTheme(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
val launcher = intent
|
||||
val gameID = launcher.getStringExtra(ARG_GAME_ID)
|
||||
val menuTag = launcher.getStringExtra(ARG_MENU_TAG)
|
||||
presenter.onCreate(savedInstanceState, menuTag!!, gameID!!)
|
||||
|
||||
// Show "Back" button in the action bar for navigation
|
||||
setSupportActionBar(binding.toolbarSettings)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() = navigateBack()
|
||||
}
|
||||
)
|
||||
|
||||
setInsets()
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
navigateBack()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
// Critical: If super method is not called, rotations will be busted.
|
||||
super.onSaveInstanceState(outState)
|
||||
presenter.saveState(outState)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
presenter.onStart()
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is called, the user has left the settings screen (potentially through the
|
||||
* home button) and will expect their changes to be persisted. So we kick off an
|
||||
* IntentService which will do so on a background thread.
|
||||
*/
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
presenter.onStop(isFinishing)
|
||||
}
|
||||
|
||||
override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) {
|
||||
if (!addToStack && settingsFragment != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
if (addToStack) {
|
||||
if (areSystemAnimationsEnabled()) {
|
||||
transaction.setCustomAnimations(
|
||||
R.anim.anim_settings_fragment_in,
|
||||
R.anim.anim_settings_fragment_out,
|
||||
0,
|
||||
R.anim.anim_pop_settings_fragment_out
|
||||
)
|
||||
}
|
||||
transaction.addToBackStack(null)
|
||||
}
|
||||
transaction.replace(
|
||||
R.id.frame_content,
|
||||
SettingsFragment.newInstance(menuTag, gameId),
|
||||
FRAGMENT_TAG
|
||||
)
|
||||
transaction.commit()
|
||||
}
|
||||
|
||||
private fun areSystemAnimationsEnabled(): Boolean {
|
||||
val duration = android.provider.Settings.Global.getFloat(
|
||||
contentResolver,
|
||||
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||
1f
|
||||
)
|
||||
val transition = android.provider.Settings.Global.getFloat(
|
||||
contentResolver,
|
||||
android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
|
||||
1f
|
||||
)
|
||||
return duration != 0f && transition != 0f
|
||||
}
|
||||
|
||||
override fun onSettingsFileLoaded() {
|
||||
val fragment: SettingsFragmentView? = settingsFragment
|
||||
fragment?.loadSettingsList()
|
||||
}
|
||||
|
||||
override fun onSettingsFileNotFound() {
|
||||
val fragment: SettingsFragmentView? = settingsFragment
|
||||
fragment?.loadSettingsList()
|
||||
}
|
||||
|
||||
override fun showToastMessage(message: String, isLong: Boolean) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
message,
|
||||
if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onSettingChanged() {
|
||||
presenter.onSettingChanged()
|
||||
}
|
||||
|
||||
fun onSettingsReset() {
|
||||
// Prevents saving to a non-existent settings file
|
||||
presenter.onSettingsReset()
|
||||
|
||||
val controllerKeys = Settings.buttonKeys + Settings.circlePadKeys + Settings.cStickKeys +
|
||||
Settings.dPadKeys + Settings.triggerKeys
|
||||
val editor =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext).edit()
|
||||
controllerKeys.forEach { editor.remove(it) }
|
||||
editor.apply()
|
||||
|
||||
// Reset the static memory representation of each setting
|
||||
BooleanSetting.clear()
|
||||
FloatSetting.clear()
|
||||
ScaledFloatSetting.clear()
|
||||
IntSetting.clear()
|
||||
StringSetting.clear()
|
||||
|
||||
// Delete settings file because the user may have changed values that do not exist in the UI
|
||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
||||
if (!settingsFile.delete()) {
|
||||
throw IOException("Failed to delete $settingsFile")
|
||||
}
|
||||
|
||||
// Set the root of the document tree before we create a new config file or the native code
|
||||
// will fail when creating the file.
|
||||
if (DirectoryInitialization.setCitraUserDirectory()) {
|
||||
CitraApplication.documentsTree.setRoot(Uri.parse(DirectoryInitialization.userPath))
|
||||
NativeLibrary.createConfigFile()
|
||||
} else {
|
||||
throw IllegalStateException("Citra directory unavailable when accessing config file!")
|
||||
}
|
||||
|
||||
// Set default values for system config file
|
||||
SystemSaveGame.apply {
|
||||
setUsername("CITRA")
|
||||
setBirthday(3, 25)
|
||||
setSystemLanguage(1)
|
||||
setSoundOutputMode(2)
|
||||
setCountryCode(49)
|
||||
setPlayCoins(42)
|
||||
}
|
||||
|
||||
showToastMessage(getString(R.string.settings_reset), true)
|
||||
finish()
|
||||
}
|
||||
|
||||
fun setToolbarTitle(title: String) {
|
||||
binding.toolbarSettingsLayout.title = title
|
||||
}
|
||||
|
||||
private val settingsFragment: SettingsFragment?
|
||||
get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment?
|
||||
|
||||
private fun setInsets() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.frameContent
|
||||
) { view: View, windowInsets: WindowInsetsCompat ->
|
||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
view.updatePadding(
|
||||
left = barInsets.left + cutoutInsets.left,
|
||||
right = barInsets.right + cutoutInsets.right
|
||||
)
|
||||
|
||||
val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
|
||||
mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left
|
||||
mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right
|
||||
binding.appbarSettings.layoutParams = mlpAppBar
|
||||
|
||||
val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
|
||||
mlpShade.height = barInsets.bottom
|
||||
binding.navigationBarShade.layoutParams = mlpShade
|
||||
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_MENU_TAG = "menu_tag"
|
||||
private const val ARG_GAME_ID = "game_id"
|
||||
private const val FRAGMENT_TAG = "settings"
|
||||
|
||||
@JvmStatic
|
||||
fun launch(context: Context, menuTag: String?, gameId: String?) {
|
||||
val settings = Intent(context, SettingsActivity::class.java)
|
||||
settings.putExtra(ARG_MENU_TAG, menuTag)
|
||||
settings.putExtra(ARG_GAME_ID, gameId)
|
||||
context.startActivity(settings)
|
||||
}
|
||||
|
||||
fun launch(
|
||||
context: Context,
|
||||
launcher: ActivityResultLauncher<Intent>,
|
||||
menuTag: String?,
|
||||
gameId: String?
|
||||
) {
|
||||
val settings = Intent(context, SettingsActivity::class.java)
|
||||
settings.putExtra(ARG_MENU_TAG, menuTag)
|
||||
settings.putExtra(ARG_GAME_ID, gameId)
|
||||
launcher.launch(settings)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import java.io.File;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
public final class SettingsActivityPresenter {
|
||||
private static final String KEY_SHOULD_SAVE = "should_save";
|
||||
|
||||
private SettingsActivityView mView;
|
||||
|
||||
private Settings mSettings = new Settings();
|
||||
|
||||
private boolean mShouldSave;
|
||||
|
||||
private DirectoryStateReceiver directoryStateReceiver;
|
||||
|
||||
private String menuTag;
|
||||
private String gameId;
|
||||
|
||||
public SettingsActivityPresenter(SettingsActivityView view) {
|
||||
mView = view;
|
||||
}
|
||||
|
||||
public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) {
|
||||
if (savedInstanceState == null) {
|
||||
this.menuTag = menuTag;
|
||||
this.gameId = gameId;
|
||||
} else {
|
||||
mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE);
|
||||
}
|
||||
}
|
||||
|
||||
public void onStart() {
|
||||
prepareCitraDirectoriesIfNeeded();
|
||||
}
|
||||
|
||||
void loadSettingsUI() {
|
||||
if (mSettings.isEmpty()) {
|
||||
if (!TextUtils.isEmpty(gameId)) {
|
||||
mSettings.loadSettings(gameId, mView);
|
||||
} else {
|
||||
mSettings.loadSettings(mView);
|
||||
}
|
||||
}
|
||||
|
||||
mView.showSettingsFragment(menuTag, false, gameId);
|
||||
mView.onSettingsFileLoaded(mSettings);
|
||||
}
|
||||
|
||||
private void prepareCitraDirectoriesIfNeeded() {
|
||||
DocumentFile configFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG);
|
||||
if (configFile == null || !configFile.exists()) {
|
||||
Log.error("Citra config file could not be found!");
|
||||
}
|
||||
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||
loadSettingsUI();
|
||||
} else {
|
||||
mView.showLoading();
|
||||
IntentFilter statusIntentFilter = new IntentFilter(
|
||||
DirectoryInitialization.BROADCAST_ACTION);
|
||||
|
||||
directoryStateReceiver =
|
||||
new DirectoryStateReceiver(directoryInitializationState ->
|
||||
{
|
||||
if (directoryInitializationState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
|
||||
mView.hideLoading();
|
||||
loadSettingsUI();
|
||||
} else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
|
||||
mView.showPermissionNeededHint();
|
||||
mView.hideLoading();
|
||||
} else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
||||
mView.showExternalStorageNotMountedHint();
|
||||
mView.hideLoading();
|
||||
}
|
||||
});
|
||||
|
||||
mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSettings(Settings settings) {
|
||||
mSettings = settings;
|
||||
}
|
||||
|
||||
public Settings getSettings() {
|
||||
return mSettings;
|
||||
}
|
||||
|
||||
public void onStop(boolean finishing) {
|
||||
if (directoryStateReceiver != null) {
|
||||
mView.stopListeningToDirectoryInitializationService(directoryStateReceiver);
|
||||
directoryStateReceiver = null;
|
||||
}
|
||||
|
||||
if (mSettings != null && finishing && mShouldSave) {
|
||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
|
||||
mSettings.saveSettings(mView);
|
||||
}
|
||||
|
||||
NativeLibrary.ReloadSettings();
|
||||
}
|
||||
|
||||
public void onSettingChanged() {
|
||||
mShouldSave = true;
|
||||
}
|
||||
|
||||
public void saveState(Bundle outState) {
|
||||
outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave);
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
// 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.settings.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||
import org.citra.citra_emu.utils.Log
|
||||
|
||||
class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
|
||||
val settings: Settings get() = activityView.settings
|
||||
|
||||
private var shouldSave = false
|
||||
private lateinit var menuTag: String
|
||||
private lateinit var gameId: String
|
||||
|
||||
fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
|
||||
this.menuTag = menuTag
|
||||
this.gameId = gameId
|
||||
if (savedInstanceState != null) {
|
||||
shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
|
||||
}
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
SystemSaveGame.load()
|
||||
prepareDirectoriesIfNeeded()
|
||||
}
|
||||
|
||||
private fun loadSettingsUI() {
|
||||
if (!settings.isLoaded) {
|
||||
if (!TextUtils.isEmpty(gameId)) {
|
||||
settings.loadSettings(gameId, activityView)
|
||||
} else {
|
||||
settings.loadSettings(activityView)
|
||||
}
|
||||
}
|
||||
activityView.showSettingsFragment(menuTag, false, gameId)
|
||||
activityView.onSettingsFileLoaded()
|
||||
}
|
||||
|
||||
private fun prepareDirectoriesIfNeeded() {
|
||||
if (!DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||
DirectoryInitialization.start()
|
||||
}
|
||||
loadSettingsUI()
|
||||
}
|
||||
|
||||
fun onStop(finishing: Boolean) {
|
||||
if (finishing && shouldSave) {
|
||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
||||
settings.saveSettings(activityView)
|
||||
SystemSaveGame.save()
|
||||
}
|
||||
NativeLibrary.reloadSettings()
|
||||
}
|
||||
|
||||
fun onSettingChanged() {
|
||||
shouldSave = true
|
||||
}
|
||||
|
||||
fun onSettingsReset() {
|
||||
shouldSave = false
|
||||
}
|
||||
|
||||
fun saveState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_SHOULD_SAVE, shouldSave)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_SHOULD_SAVE = "should_save"
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
||||
|
||||
/**
|
||||
* Abstraction for the Activity that manages SettingsFragments.
|
||||
*/
|
||||
public interface SettingsActivityView {
|
||||
/**
|
||||
* Show a new SettingsFragment.
|
||||
*
|
||||
* @param menuTag Identifier for the settings group that should be displayed.
|
||||
* @param addToStack Whether or not this fragment should replace a previous one.
|
||||
*/
|
||||
void showSettingsFragment(String menuTag, boolean addToStack, String gameId);
|
||||
|
||||
/**
|
||||
* Called by a contained Fragment to get access to the Setting HashMap
|
||||
* loaded from disk, so that each Fragment doesn't need to perform its own
|
||||
* read operation.
|
||||
*
|
||||
* @return A possibly null HashMap of Settings.
|
||||
*/
|
||||
Settings getSettings();
|
||||
|
||||
/**
|
||||
* Used to provide the Activity with Settings HashMaps if a Fragment already
|
||||
* has one; for example, if a rotation occurs, the Fragment will not be killed,
|
||||
* but the Activity will, so the Activity needs to have its HashMaps resupplied.
|
||||
*
|
||||
* @param settings The ArrayList of all the Settings HashMaps.
|
||||
*/
|
||||
void setSettings(Settings settings);
|
||||
|
||||
/**
|
||||
* Called when an asynchronous load operation completes.
|
||||
*
|
||||
* @param settings The (possibly null) result of the ini load operation.
|
||||
*/
|
||||
void onSettingsFileLoaded(Settings settings);
|
||||
|
||||
/**
|
||||
* Called when an asynchronous load operation fails.
|
||||
*/
|
||||
void onSettingsFileNotFound();
|
||||
|
||||
/**
|
||||
* Display a popup text message on screen.
|
||||
*
|
||||
* @param message The contents of the onscreen message.
|
||||
* @param is_long Whether this should be a long Toast or short one.
|
||||
*/
|
||||
void showToastMessage(String message, boolean is_long);
|
||||
|
||||
/**
|
||||
* End the activity.
|
||||
*/
|
||||
void finish();
|
||||
|
||||
/**
|
||||
* Called by a containing Fragment to tell the Activity that a setting was changed;
|
||||
* unless this has been called, the Activity will not save to disk.
|
||||
*/
|
||||
void onSettingChanged();
|
||||
|
||||
/**
|
||||
* Show loading dialog while loading the settings
|
||||
*/
|
||||
void showLoading();
|
||||
|
||||
/**
|
||||
* Hide the loading the dialog
|
||||
*/
|
||||
void hideLoading();
|
||||
|
||||
/**
|
||||
* Show a hint to the user that the app needs write to external storage access
|
||||
*/
|
||||
void showPermissionNeededHint();
|
||||
|
||||
/**
|
||||
* Show a hint to the user that the app needs the external storage to be mounted
|
||||
*/
|
||||
void showExternalStorageNotMountedHint();
|
||||
|
||||
/**
|
||||
* Start the DirectoryInitialization and listen for the result.
|
||||
*
|
||||
* @param receiver the broadcast receiver for the DirectoryInitialization
|
||||
* @param filter the Intent broadcasts to be received.
|
||||
*/
|
||||
void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter);
|
||||
|
||||
/**
|
||||
* Stop listening to the DirectoryInitialization.
|
||||
*
|
||||
* @param receiver The broadcast receiver to unregister.
|
||||
*/
|
||||
void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver);
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
// 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.settings.ui
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
|
||||
/**
|
||||
* Abstraction for the Activity that manages SettingsFragments.
|
||||
*/
|
||||
interface SettingsActivityView {
|
||||
/**
|
||||
* Show a new SettingsFragment.
|
||||
*
|
||||
* @param menuTag Identifier for the settings group that should be displayed.
|
||||
* @param addToStack Whether or not this fragment should replace a previous one.
|
||||
*/
|
||||
fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String)
|
||||
|
||||
/**
|
||||
* Called by a contained Fragment to get access to the Setting HashMap
|
||||
* loaded from disk, so that each Fragment doesn't need to perform its own
|
||||
* read operation.
|
||||
*
|
||||
* @return A HashMap of Settings.
|
||||
*/
|
||||
val settings: Settings
|
||||
|
||||
/**
|
||||
* Called when a load operation completes.
|
||||
*/
|
||||
fun onSettingsFileLoaded()
|
||||
|
||||
/**
|
||||
* Called when a load operation fails.
|
||||
*/
|
||||
fun onSettingsFileNotFound()
|
||||
|
||||
/**
|
||||
* Display a popup text message on screen.
|
||||
*
|
||||
* @param message The contents of the onscreen message.
|
||||
* @param isLong Whether this should be a long Toast or short one.
|
||||
*/
|
||||
fun showToastMessage(message: String, isLong: Boolean)
|
||||
|
||||
/**
|
||||
* End the activity.
|
||||
*/
|
||||
fun finish()
|
||||
|
||||
/**
|
||||
* Called by a containing Fragment to tell the Activity that a setting was changed;
|
||||
* unless this has been called, the Activity will not save to disk.
|
||||
*/
|
||||
fun onSettingChanged()
|
||||
}
|
@ -1,474 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TimePicker;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.slider.Slider;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.dialogs.MotionAlertDialog;
|
||||
import org.citra.citra_emu.features.settings.model.FloatSetting;
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
|
||||
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.PremiumSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> implements DialogInterface.OnClickListener, Slider.OnChangeListener {
|
||||
private SettingsFragmentView mView;
|
||||
private Context mContext;
|
||||
private ArrayList<SettingsItem> mSettings;
|
||||
|
||||
private SettingsItem mClickedItem;
|
||||
private int mClickedPosition;
|
||||
private int mSliderProgress;
|
||||
|
||||
private AlertDialog mDialog;
|
||||
private TextView mTextSliderValue;
|
||||
|
||||
public SettingsAdapter(SettingsFragmentView view, Context context) {
|
||||
mView = view;
|
||||
mContext = context;
|
||||
mClickedPosition = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view;
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
|
||||
switch (viewType) {
|
||||
case SettingsItem.TYPE_HEADER:
|
||||
view = inflater.inflate(R.layout.list_item_settings_header, parent, false);
|
||||
return new HeaderViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_CHECKBOX:
|
||||
view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false);
|
||||
return new CheckBoxSettingViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_SINGLE_CHOICE:
|
||||
case SettingsItem.TYPE_STRING_SINGLE_CHOICE:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new SingleChoiceViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_SLIDER:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new SliderViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_SUBMENU:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new SubmenuViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_INPUT_BINDING:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new InputBindingSettingViewHolder(view, this, mContext);
|
||||
|
||||
case SettingsItem.TYPE_DATETIME_SETTING:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new DateTimeViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_PREMIUM:
|
||||
view = inflater.inflate(R.layout.premium_item_setting, parent, false);
|
||||
return new PremiumViewHolder(view, this, mView);
|
||||
|
||||
default:
|
||||
Log.error("[SettingsAdapter] Invalid view type: " + viewType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SettingViewHolder holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
private SettingsItem getItem(int position) {
|
||||
return mSettings.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (mSettings != null) {
|
||||
return mSettings.size();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return getItem(position).getType();
|
||||
}
|
||||
|
||||
public void setSettings(ArrayList<SettingsItem> settings) {
|
||||
mSettings = settings;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) {
|
||||
IntSetting setting = item.setChecked(checked);
|
||||
notifyItemChanged(position);
|
||||
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
|
||||
mView.onSettingChanged();
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(PremiumSingleChoiceSetting item) {
|
||||
mClickedItem = item;
|
||||
|
||||
int value = getSelectionForSingleChoiceValue(item);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setTitle(item.getNameId())
|
||||
.setSingleChoiceItems(item.getChoicesId(), value, this);
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(SingleChoiceSetting item) {
|
||||
mClickedItem = item;
|
||||
|
||||
int value = getSelectionForSingleChoiceValue(item);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setTitle(item.getNameId())
|
||||
.setSingleChoiceItems(item.getChoicesId(), value, this);
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
|
||||
mClickedPosition = position;
|
||||
|
||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
||||
// Setting is either not Premium, or the user has Premium
|
||||
onSingleChoiceClick(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// User needs Premium, invoke the billing flow
|
||||
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) {
|
||||
mClickedPosition = position;
|
||||
|
||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
||||
// Setting is either not Premium, or the user has Premium
|
||||
onSingleChoiceClick(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// User needs Premium, invoke the billing flow
|
||||
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
|
||||
}
|
||||
|
||||
public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
|
||||
mClickedItem = item;
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setTitle(item.getNameId())
|
||||
.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this);
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
|
||||
mClickedPosition = position;
|
||||
|
||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
||||
// Setting is either not Premium, or the user has Premium
|
||||
onStringSingleChoiceClick(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// User needs Premium, invoke the billing flow
|
||||
MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item));
|
||||
}
|
||||
|
||||
DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog();
|
||||
|
||||
public void onDateTimeClick(DateTimeSetting item, int position) {
|
||||
mClickedItem = item;
|
||||
mClickedPosition = position;
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
|
||||
View view = inflater.inflate(R.layout.sysclock_datetime_picker, null);
|
||||
|
||||
DatePicker dp = view.findViewById(R.id.date_picker);
|
||||
TimePicker tp = view.findViewById(R.id.time_picker);
|
||||
|
||||
//set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69)
|
||||
String settingValue = item.getValue();
|
||||
dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10)));
|
||||
|
||||
tp.setIs24HourView(true);
|
||||
tp.setHour(Integer.parseInt(settingValue.substring(11, 13)));
|
||||
tp.setMinute(Integer.parseInt(settingValue.substring(14, 16)));
|
||||
|
||||
DialogInterface.OnClickListener ok = (dialog, which) -> {
|
||||
//set it
|
||||
int year = dp.getYear();
|
||||
if (year < 2000) {
|
||||
year = 2000;
|
||||
}
|
||||
String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length());
|
||||
String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length());
|
||||
String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length());
|
||||
String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length());
|
||||
String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01";
|
||||
|
||||
StringSetting setting = item.setSelectedValue(datetime);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
|
||||
mView.onSettingChanged();
|
||||
|
||||
mClickedItem = null;
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, ok)
|
||||
.setNegativeButton(android.R.string.cancel, defaultCancelListener);
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onSliderClick(SliderSetting item, int position) {
|
||||
mClickedItem = item;
|
||||
mClickedPosition = position;
|
||||
mSliderProgress = item.getSelectedValue();
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
|
||||
View view = inflater.inflate(R.layout.dialog_slider, null);
|
||||
|
||||
Slider slider = view.findViewById(R.id.slider);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setTitle(item.getNameId())
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
|
||||
.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> {
|
||||
slider.setValue(item.getDefaultValue());
|
||||
onClick(dialog, which);
|
||||
});
|
||||
mDialog = builder.show();
|
||||
|
||||
mTextSliderValue = view.findViewById(R.id.text_value);
|
||||
mTextSliderValue.setText(String.valueOf(mSliderProgress));
|
||||
|
||||
TextView units = view.findViewById(R.id.text_units);
|
||||
units.setText(item.getUnits());
|
||||
|
||||
slider.setValueFrom(item.getMin());
|
||||
slider.setValueTo(item.getMax());
|
||||
slider.setValue(mSliderProgress);
|
||||
|
||||
slider.addOnChangeListener(this);
|
||||
}
|
||||
|
||||
public void onSubmenuClick(SubmenuSetting item) {
|
||||
mView.loadSubMenu(item.getMenuKey());
|
||||
}
|
||||
|
||||
public void onInputBindingClick(final InputBindingSetting item, final int position) {
|
||||
final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item);
|
||||
dialog.setTitle(R.string.input_binding);
|
||||
|
||||
int messageResId = R.string.input_binding_description;
|
||||
if (item.IsAxisMappingSupported() && !item.IsTrigger()) {
|
||||
// Use specialized message for axis left/right or up/down
|
||||
if (item.IsHorizontalOrientation()) {
|
||||
messageResId = R.string.input_binding_description_horizontal_axis;
|
||||
} else {
|
||||
messageResId = R.string.input_binding_description_vertical_axis;
|
||||
}
|
||||
}
|
||||
|
||||
dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId())));
|
||||
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this);
|
||||
dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) ->
|
||||
item.removeOldMapping());
|
||||
dialog.setOnDismissListener(dialog1 ->
|
||||
{
|
||||
StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue());
|
||||
notifyItemChanged(position);
|
||||
|
||||
mView.putSetting(setting);
|
||||
|
||||
mView.onSettingChanged();
|
||||
});
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (mClickedItem instanceof SingleChoiceSetting) {
|
||||
SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem;
|
||||
|
||||
int value = getValueForSingleChoiceSelection(scSetting, which);
|
||||
if (scSetting.getSelectedValue() != value) {
|
||||
mView.onSettingChanged();
|
||||
}
|
||||
|
||||
// Get the backing Setting, which may be null (if for example it was missing from the file)
|
||||
IntSetting setting = scSetting.setSelectedValue(value);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
} else if (mClickedItem instanceof PremiumSingleChoiceSetting) {
|
||||
PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem;
|
||||
scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which));
|
||||
closeDialog();
|
||||
} else if (mClickedItem instanceof StringSingleChoiceSetting) {
|
||||
StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem;
|
||||
String value = scSetting.getValueAt(which);
|
||||
if (!scSetting.getSelectedValue().equals(value))
|
||||
mView.onSettingChanged();
|
||||
|
||||
StringSetting setting = scSetting.setSelectedValue(value);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
} else if (mClickedItem instanceof SliderSetting) {
|
||||
SliderSetting sliderSetting = (SliderSetting) mClickedItem;
|
||||
if (sliderSetting.getSelectedValue() != mSliderProgress) {
|
||||
mView.onSettingChanged();
|
||||
}
|
||||
|
||||
if (sliderSetting.getSetting() instanceof FloatSetting) {
|
||||
float value = (float) mSliderProgress;
|
||||
|
||||
FloatSetting setting = sliderSetting.setSelectedValue(value);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
} else {
|
||||
IntSetting setting = sliderSetting.setSelectedValue(mSliderProgress);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
mClickedItem = null;
|
||||
mSliderProgress = -1;
|
||||
}
|
||||
|
||||
public void closeDialog() {
|
||||
if (mDialog != null) {
|
||||
if (mClickedPosition != -1) {
|
||||
notifyItemChanged(mClickedPosition);
|
||||
mClickedPosition = -1;
|
||||
}
|
||||
mDialog.dismiss();
|
||||
mDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) {
|
||||
int valuesId = item.getValuesId();
|
||||
|
||||
if (valuesId > 0) {
|
||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
||||
return valuesArray[which];
|
||||
} else {
|
||||
return which;
|
||||
}
|
||||
}
|
||||
|
||||
private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) {
|
||||
int valuesId = item.getValuesId();
|
||||
|
||||
if (valuesId > 0) {
|
||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
||||
return valuesArray[which];
|
||||
} else {
|
||||
return which;
|
||||
}
|
||||
}
|
||||
|
||||
private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) {
|
||||
int value = item.getSelectedValue();
|
||||
int valuesId = item.getValuesId();
|
||||
|
||||
if (valuesId > 0) {
|
||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
||||
for (int index = 0; index < valuesArray.length; index++) {
|
||||
int current = valuesArray[index];
|
||||
if (current == value) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) {
|
||||
int value = item.getSelectedValue();
|
||||
int valuesId = item.getValuesId();
|
||||
|
||||
if (valuesId > 0) {
|
||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
||||
for (int index = 0; index < valuesArray.length; index++) {
|
||||
int current = valuesArray[index];
|
||||
if (current == value) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
|
||||
mSliderProgress = (int) value;
|
||||
mTextSliderValue.setText(String.valueOf(mSliderProgress));
|
||||
}
|
||||
}
|
@ -0,0 +1,503 @@
|
||||
// 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.settings.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.icu.util.Calendar
|
||||
import android.icu.util.TimeZone
|
||||
import android.text.InputFilter
|
||||
import android.text.format.DateFormat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import com.google.android.material.timepicker.TimeFormat
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.databinding.DialogSliderBinding
|
||||
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
|
||||
import org.citra.citra_emu.databinding.ListItemSettingBinding
|
||||
import org.citra.citra_emu.databinding.ListItemSettingSwitchBinding
|
||||
import org.citra.citra_emu.databinding.ListItemSettingsHeaderBinding
|
||||
import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractFloatSetting
|
||||
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.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.view.DateTimeSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem
|
||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.StringInputSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.SwitchSetting
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.RunnableViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.StringInputViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder
|
||||
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||
import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.NumberFormatException
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class SettingsAdapter(
|
||||
private val fragmentView: SettingsFragmentView,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener {
|
||||
private var settings: ArrayList<SettingsItem>? = null
|
||||
private var clickedItem: SettingsItem? = null
|
||||
private var clickedPosition: Int
|
||||
private var dialog: AlertDialog? = null
|
||||
private var sliderProgress = 0
|
||||
private var textSliderValue: TextView? = null
|
||||
private var textInputValue: String = ""
|
||||
|
||||
private var defaultCancelListener =
|
||||
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
|
||||
|
||||
init {
|
||||
clickedPosition = -1
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
SettingsItem.TYPE_HEADER -> {
|
||||
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_SWITCH -> {
|
||||
SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
|
||||
SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_SLIDER -> {
|
||||
SliderViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_SUBMENU -> {
|
||||
SubmenuViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_DATETIME_SETTING -> {
|
||||
DateTimeViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_RUNNABLE -> {
|
||||
RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_INPUT_BINDING -> {
|
||||
InputBindingSettingViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_STRING_INPUT -> {
|
||||
StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// TODO: Create an error view since we can't return null now
|
||||
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
private fun getItem(position: Int): SettingsItem {
|
||||
return settings!![position]
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (settings != null) {
|
||||
settings!!.size
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return getItem(position).type
|
||||
}
|
||||
|
||||
fun setSettingsList(settings: ArrayList<SettingsItem>?) {
|
||||
this.settings = settings
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
|
||||
val setting = item.setChecked(checked)
|
||||
fragmentView.putSetting(setting)
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
|
||||
private fun onSingleChoiceClick(item: SingleChoiceSetting) {
|
||||
clickedItem = item
|
||||
val value = getSelectionForSingleChoiceValue(item)
|
||||
dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(item.nameId)
|
||||
.setSingleChoiceItems(item.choicesId, value, this)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
||||
clickedPosition = position
|
||||
onSingleChoiceClick(item)
|
||||
}
|
||||
|
||||
private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) {
|
||||
clickedItem = item
|
||||
dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(item.nameId)
|
||||
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
|
||||
clickedPosition = position
|
||||
onStringSingleChoiceClick(item)
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun onDateTimeClick(item: DateTimeSetting, position: Int) {
|
||||
clickedItem = item
|
||||
clickedPosition = position
|
||||
|
||||
val storedTime: Long = try {
|
||||
java.lang.Long.decode(item.value) * 1000
|
||||
} catch (e: NumberFormatException) {
|
||||
val date = item.value.substringBefore(" ")
|
||||
val time = item.value.substringAfter(" ")
|
||||
|
||||
val formatter = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZZZZ")
|
||||
val gmt = formatter.parse("${date}T${time}+0000")
|
||||
gmt!!.time
|
||||
}
|
||||
|
||||
// Helper to extract hour and minute from epoch time
|
||||
val calendar: Calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = storedTime
|
||||
calendar.timeZone = TimeZone.getTimeZone("UTC")
|
||||
|
||||
var timeFormat: Int = TimeFormat.CLOCK_12H
|
||||
if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) {
|
||||
timeFormat = TimeFormat.CLOCK_24H
|
||||
}
|
||||
|
||||
val datePicker: MaterialDatePicker<Long> = MaterialDatePicker.Builder.datePicker()
|
||||
.setSelection(storedTime)
|
||||
.setTitleText(R.string.select_rtc_date)
|
||||
.build()
|
||||
val timePicker: MaterialTimePicker = MaterialTimePicker.Builder()
|
||||
.setTimeFormat(timeFormat)
|
||||
.setHour(calendar.get(Calendar.HOUR_OF_DAY))
|
||||
.setMinute(calendar.get(Calendar.MINUTE))
|
||||
.setTitleText(R.string.select_rtc_time)
|
||||
.build()
|
||||
|
||||
datePicker.addOnPositiveButtonClickListener {
|
||||
timePicker.show(
|
||||
(fragmentView.activityView as AppCompatActivity).supportFragmentManager,
|
||||
"TimePicker"
|
||||
)
|
||||
}
|
||||
timePicker.addOnPositiveButtonClickListener {
|
||||
var epochTime: Long = datePicker.selection!! / 1000
|
||||
epochTime += timePicker.hour.toLong() * 60 * 60
|
||||
epochTime += timePicker.minute.toLong() * 60
|
||||
val rtcString = epochTime.toString()
|
||||
if (item.value != rtcString) {
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
notifyItemChanged(clickedPosition)
|
||||
val setting = item.setSelectedValue(rtcString)
|
||||
fragmentView.putSetting(setting)
|
||||
clickedItem = null
|
||||
}
|
||||
datePicker.show(
|
||||
(fragmentView.activityView as AppCompatActivity).supportFragmentManager,
|
||||
"DatePicker"
|
||||
)
|
||||
}
|
||||
|
||||
fun onSliderClick(item: SliderSetting, position: Int) {
|
||||
clickedItem = item
|
||||
clickedPosition = position
|
||||
sliderProgress = item.selectedValue
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val sliderBinding = DialogSliderBinding.inflate(inflater)
|
||||
|
||||
textSliderValue = sliderBinding.textValue
|
||||
textSliderValue!!.text = sliderProgress.toString()
|
||||
sliderBinding.textUnits.text = item.units
|
||||
|
||||
sliderBinding.slider.apply {
|
||||
valueFrom = item.min.toFloat()
|
||||
valueTo = item.max.toFloat()
|
||||
value = sliderProgress.toFloat()
|
||||
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
|
||||
sliderProgress = value.toInt()
|
||||
textSliderValue!!.text = sliderProgress.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(item.nameId)
|
||||
.setView(sliderBinding.root)
|
||||
.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
|
||||
.setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int ->
|
||||
sliderBinding.slider.value = when (item.setting) {
|
||||
is ScaledFloatSetting -> {
|
||||
val scaledSetting = item.setting as ScaledFloatSetting
|
||||
scaledSetting.defaultValue * scaledSetting.scale
|
||||
}
|
||||
|
||||
is FloatSetting -> (item.setting as FloatSetting).defaultValue
|
||||
else -> item.defaultValue!!
|
||||
}
|
||||
onClick(dialog, which)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun onSubmenuClick(item: SubmenuSetting) {
|
||||
fragmentView.loadSubMenu(item.menuKey)
|
||||
}
|
||||
|
||||
fun onInputBindingClick(item: InputBindingSetting, position: Int) {
|
||||
val activity = fragmentView.activityView as FragmentActivity
|
||||
MotionBottomSheetDialogFragment.newInstance(
|
||||
item,
|
||||
{ closeDialog() },
|
||||
{
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
).show(activity.supportFragmentManager, MotionBottomSheetDialogFragment.TAG)
|
||||
}
|
||||
|
||||
fun onStringInputClick(item: StringInputSetting, position: Int) {
|
||||
clickedItem = item
|
||||
clickedPosition = position
|
||||
textInputValue = item.selectedValue
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
|
||||
|
||||
inputBinding.editTextInput.setText(textInputValue)
|
||||
inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
|
||||
textInputValue = text.toString()
|
||||
}
|
||||
if (item.characterLimit != 0) {
|
||||
inputBinding.editTextInput.filters =
|
||||
arrayOf(InputFilter.LengthFilter(item.characterLimit))
|
||||
}
|
||||
|
||||
dialog = MaterialAlertDialogBuilder(context)
|
||||
.setView(inputBinding.root)
|
||||
.setTitle(item.nameId)
|
||||
.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
when (clickedItem) {
|
||||
is SingleChoiceSetting -> {
|
||||
val scSetting = clickedItem as SingleChoiceSetting
|
||||
val setting = when (scSetting.setting) {
|
||||
is AbstractIntSetting -> {
|
||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||
if (scSetting.selectedValue != value) {
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
scSetting.setSelectedValue(value)
|
||||
}
|
||||
|
||||
is AbstractShortSetting -> {
|
||||
val value = getValueForSingleChoiceSelection(scSetting, which).toShort()
|
||||
if (scSetting.selectedValue.toShort() != value) {
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
scSetting.setSelectedValue(value)
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
|
||||
}
|
||||
|
||||
fragmentView.putSetting(setting)
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
is StringSingleChoiceSetting -> {
|
||||
val scSetting = clickedItem as StringSingleChoiceSetting
|
||||
val setting = when (scSetting.setting) {
|
||||
is AbstractStringSetting -> {
|
||||
val value = scSetting.getValueAt(which)
|
||||
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
|
||||
scSetting.setSelectedValue(value!!)
|
||||
}
|
||||
|
||||
is AbstractShortSetting -> {
|
||||
if (scSetting.selectValueIndex != which) fragmentView.onSettingChanged()
|
||||
scSetting.setSelectedValue(scSetting.getValueAt(which)?.toShort() ?: 1)
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unrecognized type used for StringSingleChoiceSetting!")
|
||||
}
|
||||
|
||||
fragmentView.putSetting(setting)
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
is SliderSetting -> {
|
||||
val sliderSetting = clickedItem as SliderSetting
|
||||
if (sliderSetting.selectedValue != sliderProgress) {
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
when (sliderSetting.setting) {
|
||||
is FloatSetting,
|
||||
is ScaledFloatSetting -> {
|
||||
val value = sliderProgress.toFloat()
|
||||
val setting = sliderSetting.setSelectedValue(value)
|
||||
fragmentView.putSetting(setting)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val setting = sliderSetting.setSelectedValue(sliderProgress)
|
||||
fragmentView.putSetting(setting)
|
||||
}
|
||||
}
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
is StringInputSetting -> {
|
||||
val inputSetting = clickedItem as StringInputSetting
|
||||
if (inputSetting.selectedValue != textInputValue) {
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
val setting = inputSetting.setSelectedValue(textInputValue)
|
||||
fragmentView.putSetting(setting)
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
clickedItem = null
|
||||
sliderProgress = -1
|
||||
textInputValue = ""
|
||||
}
|
||||
|
||||
fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.reset_setting_confirmation)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
when (setting) {
|
||||
is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean
|
||||
is AbstractFloatSetting -> {
|
||||
if (setting is ScaledFloatSetting) {
|
||||
setting.float = setting.defaultValue * setting.scale
|
||||
} else {
|
||||
setting.float = setting.defaultValue as Float
|
||||
}
|
||||
}
|
||||
|
||||
is AbstractIntSetting -> setting.int = setting.defaultValue as Int
|
||||
is AbstractStringSetting -> setting.string = setting.defaultValue as String
|
||||
is AbstractShortSetting -> setting.short = setting.defaultValue as Short
|
||||
}
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun onClickDisabledSetting() {
|
||||
MessageDialogFragment.newInstance(
|
||||
R.string.setting_not_editable,
|
||||
R.string.setting_not_editable_description
|
||||
).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
|
||||
fun onClickRegenerateConsoleId() {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.regenerate_console_id)
|
||||
.setMessage(R.string.regenerate_console_id_description)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
SystemSaveGame.regenerateConsoleId()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun closeDialog() {
|
||||
if (dialog != null) {
|
||||
if (clickedPosition != -1) {
|
||||
notifyItemChanged(clickedPosition)
|
||||
clickedPosition = -1
|
||||
}
|
||||
dialog!!.dismiss()
|
||||
dialog = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
|
||||
val valuesId = item.valuesId
|
||||
return if (valuesId > 0) {
|
||||
val valuesArray = context.resources.getIntArray(valuesId)
|
||||
valuesArray[which]
|
||||
} else {
|
||||
which
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
|
||||
val value = item.selectedValue
|
||||
val valuesId = item.valuesId
|
||||
if (valuesId > 0) {
|
||||
val valuesArray = context.resources.getIntArray(valuesId)
|
||||
for (index in valuesArray.indices) {
|
||||
val current = valuesArray[index]
|
||||
if (current == value) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.content.Context;
|
||||
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.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.ui.DividerItemDecoration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class SettingsFragment extends Fragment implements SettingsFragmentView {
|
||||
private static final String ARGUMENT_MENU_TAG = "menu_tag";
|
||||
private static final String ARGUMENT_GAME_ID = "game_id";
|
||||
|
||||
private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this);
|
||||
private SettingsActivityView mActivity;
|
||||
|
||||
private SettingsAdapter mAdapter;
|
||||
|
||||
private RecyclerView mRecyclerView;
|
||||
|
||||
public static Fragment newInstance(String menuTag, String gameId) {
|
||||
SettingsFragment fragment = new SettingsFragment();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString(ARGUMENT_MENU_TAG, menuTag);
|
||||
arguments.putString(ARGUMENT_GAME_ID, gameId);
|
||||
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
mActivity = (SettingsActivityView) context;
|
||||
mPresenter.onAttach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setRetainInstance(true);
|
||||
String menuTag = getArguments().getString(ARGUMENT_MENU_TAG);
|
||||
String gameId = getArguments().getString(ARGUMENT_GAME_ID);
|
||||
|
||||
mAdapter = new SettingsAdapter(this, getActivity());
|
||||
|
||||
mPresenter.onCreate(menuTag, gameId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_settings, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
LinearLayoutManager manager = new LinearLayoutManager(getActivity());
|
||||
|
||||
mRecyclerView = view.findViewById(R.id.list_settings);
|
||||
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
mRecyclerView.setLayoutManager(manager);
|
||||
mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
|
||||
|
||||
SettingsActivityView activity = (SettingsActivityView) getActivity();
|
||||
|
||||
mPresenter.onViewCreated(activity.getSettings());
|
||||
|
||||
setInsets();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
mActivity = null;
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsFileLoaded(Settings settings) {
|
||||
mPresenter.setSettings(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void passSettingsToActivity(Settings settings) {
|
||||
if (mActivity != null) {
|
||||
mActivity.setSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSettingsList(ArrayList<SettingsItem> settingsList) {
|
||||
mAdapter.setSettings(settingsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadDefaultSettings() {
|
||||
mPresenter.loadDefaultSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSubMenu(String menuKey) {
|
||||
mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showToastMessage(String message, boolean is_long) {
|
||||
mActivity.showToastMessage(message, is_long);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putSetting(Setting setting) {
|
||||
mPresenter.putSetting(setting);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingChanged() {
|
||||
mActivity.onSettingChanged();
|
||||
}
|
||||
|
||||
private void setInsets() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(insets.left, 0, insets.right, insets.bottom);
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
}
|
@ -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.features.settings.ui
|
||||
|
||||
import android.content.Context
|
||||
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.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||
import org.citra.citra_emu.databinding.FragmentSettingsBinding
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem
|
||||
|
||||
class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
override var activityView: SettingsActivityView? = null
|
||||
|
||||
private val fragmentPresenter = SettingsFragmentPresenter(this)
|
||||
private var settingsAdapter: SettingsAdapter? = null
|
||||
|
||||
private var _binding: FragmentSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
activityView = requireActivity() as SettingsActivityView
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG)
|
||||
val gameId = requireArguments().getString(ARGUMENT_GAME_ID)
|
||||
fragmentPresenter.onCreate(menuTag!!, gameId!!)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentSettingsBinding.inflate(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
settingsAdapter = SettingsAdapter(this, requireActivity())
|
||||
val dividerDecoration = MaterialDividerItemDecoration(
|
||||
requireContext(),
|
||||
LinearLayoutManager.VERTICAL
|
||||
)
|
||||
dividerDecoration.isLastItemDecorated = false
|
||||
binding.listSettings.apply {
|
||||
adapter = settingsAdapter
|
||||
layoutManager = LinearLayoutManager(activity)
|
||||
addItemDecoration(dividerDecoration)
|
||||
}
|
||||
fragmentPresenter.onViewCreated(settingsAdapter!!)
|
||||
|
||||
setInsets()
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
activityView = null
|
||||
if (settingsAdapter != null) {
|
||||
settingsAdapter!!.closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
|
||||
settingsAdapter!!.setSettingsList(settingsList)
|
||||
}
|
||||
|
||||
override fun loadSettingsList() {
|
||||
fragmentPresenter.loadSettingsList()
|
||||
}
|
||||
|
||||
override fun loadSubMenu(menuKey: String) {
|
||||
activityView!!.showSettingsFragment(
|
||||
menuKey,
|
||||
true,
|
||||
requireArguments().getString(ARGUMENT_GAME_ID)!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun showToastMessage(message: String?, is_long: Boolean) {
|
||||
activityView!!.showToastMessage(message!!, is_long)
|
||||
}
|
||||
|
||||
override fun putSetting(setting: AbstractSetting) {
|
||||
fragmentPresenter.putSetting(setting)
|
||||
}
|
||||
|
||||
override fun onSettingChanged() {
|
||||
activityView!!.onSettingChanged()
|
||||
}
|
||||
|
||||
private fun setInsets() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.listSettings
|
||||
) { view: View, windowInsets: WindowInsetsCompat ->
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.updatePadding(bottom = insets.bottom)
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARGUMENT_MENU_TAG = "menu_tag"
|
||||
private const val ARGUMENT_GAME_ID = "game_id"
|
||||
|
||||
fun newInstance(menuTag: String?, gameId: String?): Fragment {
|
||||
val fragment = SettingsFragment()
|
||||
val arguments = Bundle()
|
||||
arguments.putString(ARGUMENT_MENU_TAG, menuTag)
|
||||
arguments.putString(ARGUMENT_GAME_ID, gameId)
|
||||
fragment.arguments = arguments
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
@ -1,433 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.hardware.camera2.CameraAccessException;
|
||||
import android.hardware.camera2.CameraCharacteristics;
|
||||
import android.hardware.camera2.CameraManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.SettingSection;
|
||||
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.CheckBoxSetting;
|
||||
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;
|
||||
import org.citra.citra_emu.features.settings.model.view.PremiumHeader;
|
||||
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SettingsFragmentPresenter {
|
||||
private SettingsFragmentView mView;
|
||||
|
||||
private String mMenuTag;
|
||||
private String mGameID;
|
||||
|
||||
private Settings mSettings;
|
||||
private ArrayList<SettingsItem> mSettingsList;
|
||||
|
||||
public SettingsFragmentPresenter(SettingsFragmentView view) {
|
||||
mView = view;
|
||||
}
|
||||
|
||||
public void onCreate(String menuTag, String gameId) {
|
||||
mGameID = gameId;
|
||||
mMenuTag = menuTag;
|
||||
}
|
||||
|
||||
public void onViewCreated(Settings settings) {
|
||||
setSettings(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the screen is rotated, the Activity will forget the settings map. This fragment
|
||||
* won't, though; so rather than have the Activity reload from disk, have the fragment pass
|
||||
* the settings map back to the Activity.
|
||||
*/
|
||||
public void onAttach() {
|
||||
if (mSettings != null) {
|
||||
mView.passSettingsToActivity(mSettings);
|
||||
}
|
||||
}
|
||||
|
||||
public void putSetting(Setting setting) {
|
||||
mSettings.getSection(setting.getSection()).putSetting(setting);
|
||||
}
|
||||
|
||||
private StringSetting asStringSetting(Setting setting) {
|
||||
if (setting == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString());
|
||||
putSetting(stringSetting);
|
||||
return stringSetting;
|
||||
}
|
||||
|
||||
public void loadDefaultSettings() {
|
||||
loadSettingsList();
|
||||
}
|
||||
|
||||
public void setSettings(Settings settings) {
|
||||
if (mSettingsList == null && settings != null) {
|
||||
mSettings = settings;
|
||||
|
||||
loadSettingsList();
|
||||
} else {
|
||||
mView.getActivity().setTitle(R.string.preferences_settings);
|
||||
mView.showSettingsList(mSettingsList);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSettingsList() {
|
||||
if (!TextUtils.isEmpty(mGameID)) {
|
||||
mView.getActivity().setTitle("Game Settings: " + mGameID);
|
||||
}
|
||||
ArrayList<SettingsItem> sl = new ArrayList<>();
|
||||
|
||||
if (mMenuTag == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mMenuTag) {
|
||||
case SettingsFile.FILE_NAME_CONFIG:
|
||||
addConfigSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_PREMIUM:
|
||||
addPremiumSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_CORE:
|
||||
addGeneralSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_SYSTEM:
|
||||
addSystemSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_CAMERA:
|
||||
addCameraSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_CONTROLS:
|
||||
addInputSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_RENDERER:
|
||||
addGraphicsSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_AUDIO:
|
||||
addAudioSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_DEBUG:
|
||||
addDebugSettings(sl);
|
||||
break;
|
||||
default:
|
||||
mView.showToastMessage("Unimplemented menu", false);
|
||||
return;
|
||||
}
|
||||
|
||||
mSettingsList = sl;
|
||||
mView.showSettingsList(mSettingsList);
|
||||
}
|
||||
|
||||
private void addConfigSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_settings);
|
||||
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG));
|
||||
}
|
||||
|
||||
private void addPremiumSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_premium);
|
||||
|
||||
SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM);
|
||||
Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN);
|
||||
|
||||
sl.add(new PremiumHeader());
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView));
|
||||
} else {
|
||||
// Pre-Android 10 does not support System Default
|
||||
sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView));
|
||||
}
|
||||
|
||||
Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME);
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_PREMIUM, R.string.texture_filter_name, 0, R.array.textureFilterNames, R.array.textureFilterValues, 0, textureFilterName));
|
||||
}
|
||||
|
||||
private void addGeneralSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_general);
|
||||
|
||||
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
|
||||
Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED);
|
||||
Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT);
|
||||
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue));
|
||||
}
|
||||
|
||||
private void addSystemSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_system);
|
||||
|
||||
SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM);
|
||||
Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE);
|
||||
Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE);
|
||||
Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK);
|
||||
Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME);
|
||||
Setting pluginLoader = systemSection.getSetting(SettingsFile.KEY_PLUGIN_LOADER);
|
||||
Setting allowPluginLoader = systemSection.getSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER);
|
||||
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.clock, 0));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock));
|
||||
sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.plugin_loader, 0));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.plugin_loader, R.string.plugin_loader_description, false, pluginLoader));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.allow_plugin_loader, R.string.allow_plugin_loader_description, true, allowPluginLoader));
|
||||
}
|
||||
|
||||
private void addCameraSettings(ArrayList<SettingsItem> sl) {
|
||||
final Activity activity = mView.getActivity();
|
||||
activity.setTitle(R.string.preferences_camera);
|
||||
|
||||
// Get the camera IDs
|
||||
CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
|
||||
ArrayList<String> supportedCameraNameList = new ArrayList<>();
|
||||
ArrayList<String> supportedCameraIdList = new ArrayList<>();
|
||||
if (cameraManager != null) {
|
||||
try {
|
||||
for (String id : cameraManager.getCameraIdList()) {
|
||||
final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
|
||||
if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
|
||||
continue; // Legacy cameras cannot be used with the NDK
|
||||
}
|
||||
|
||||
supportedCameraIdList.add(id);
|
||||
|
||||
final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING));
|
||||
int stringId = R.string.camera_facing_external;
|
||||
switch (facing) {
|
||||
case CameraCharacteristics.LENS_FACING_FRONT:
|
||||
stringId = R.string.camera_facing_front;
|
||||
break;
|
||||
case CameraCharacteristics.LENS_FACING_BACK:
|
||||
stringId = R.string.camera_facing_back;
|
||||
break;
|
||||
case CameraCharacteristics.LENS_FACING_EXTERNAL:
|
||||
stringId = R.string.camera_facing_external;
|
||||
break;
|
||||
}
|
||||
supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId)));
|
||||
}
|
||||
} catch (CameraAccessException e) {
|
||||
Log.error("Couldn't retrieve camera list");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Create the names and values for display
|
||||
ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames)));
|
||||
cameraDeviceNameList.addAll(supportedCameraNameList);
|
||||
ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues)));
|
||||
cameraDeviceValueList.addAll(supportedCameraIdList);
|
||||
|
||||
final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{});
|
||||
final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{});
|
||||
|
||||
final boolean haveCameraDevices = !supportedCameraIdList.isEmpty();
|
||||
|
||||
String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames);
|
||||
String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues);
|
||||
if (!haveCameraDevices) {
|
||||
// Remove the last entry (ndk / Device Camera)
|
||||
imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1);
|
||||
imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1);
|
||||
}
|
||||
|
||||
final String defaultImageSource = haveCameraDevices ? "ndk" : "image";
|
||||
|
||||
SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA);
|
||||
|
||||
Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME);
|
||||
Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG));
|
||||
Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP);
|
||||
sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0));
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource));
|
||||
if (haveCameraDevices)
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip));
|
||||
|
||||
Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME);
|
||||
Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG));
|
||||
Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP);
|
||||
sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0));
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource));
|
||||
if (haveCameraDevices)
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip));
|
||||
|
||||
Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME);
|
||||
Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG));
|
||||
Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP);
|
||||
sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0));
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource));
|
||||
if (haveCameraDevices)
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip));
|
||||
}
|
||||
|
||||
private void addInputSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_controls);
|
||||
|
||||
SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS);
|
||||
Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A);
|
||||
Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B);
|
||||
Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X);
|
||||
Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y);
|
||||
Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT);
|
||||
Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START);
|
||||
Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL);
|
||||
Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL);
|
||||
Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL);
|
||||
Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL);
|
||||
Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL);
|
||||
Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL);
|
||||
// Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP);
|
||||
// Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN);
|
||||
// Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT);
|
||||
// Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT);
|
||||
Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L);
|
||||
Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R);
|
||||
Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL);
|
||||
Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR);
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.controller_c, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz));
|
||||
|
||||
// TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing.
|
||||
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp));
|
||||
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown));
|
||||
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft));
|
||||
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR));
|
||||
}
|
||||
|
||||
private void addGraphicsSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_graphics);
|
||||
|
||||
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
|
||||
Setting graphicsApi = rendererSection.getSetting(SettingsFile.KEY_GRAPHICS_API);
|
||||
Setting spirvShaderGen = rendererSection.getSetting(SettingsFile.KEY_SPIRV_SHADER_GEN);
|
||||
Setting asyncShaders = rendererSection.getSetting(SettingsFile.KEY_ASYNC_SHADERS);
|
||||
Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR);
|
||||
Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE);
|
||||
Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL);
|
||||
Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D);
|
||||
Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D);
|
||||
Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE);
|
||||
SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT);
|
||||
Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE);
|
||||
Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT);
|
||||
Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT);
|
||||
SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY);
|
||||
Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES);
|
||||
Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES);
|
||||
Setting asyncCustomLoading = utilitySection.getSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING);
|
||||
//Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES);
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.renderer, 0));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_GRAPHICS_API, Settings.SECTION_RENDERER, R.string.graphics_api, 0, R.array.graphicsApiNames, R.array.graphicsApiValues, 0, graphicsApi));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_SPIRV_SHADER_GEN, Settings.SECTION_RENDERER, R.string.spirv_shader_gen, R.string.spirv_shader_gen_description, true, spirvShaderGen));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_SHADERS, Settings.SECTION_RENDERER, R.string.async_shaders, R.string.async_shaders_description, false, asyncShaders));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE, Settings.SECTION_LAYOUT, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, 30, 100, "%", 85, cardboardScreenSize));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, -100, 100, "%", 0, cardboardXShift));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, -100, 100, "%", 0, cardboardYShift));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.utility, 0));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING, Settings.SECTION_UTILITY, R.string.async_custom_loading, R.string.async_custom_loading_description, true, asyncCustomLoading));
|
||||
//Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra.
|
||||
//sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures));
|
||||
}
|
||||
|
||||
private void addAudioSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_audio);
|
||||
|
||||
SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO);
|
||||
Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING);
|
||||
Setting audioInputType = audioSection.getSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE);
|
||||
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 0, audioInputType));
|
||||
}
|
||||
|
||||
private void addDebugSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_debug);
|
||||
|
||||
SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE);
|
||||
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
|
||||
Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT);
|
||||
Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER);
|
||||
Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC);
|
||||
Setting rendererDebug = rendererSection.getSetting(SettingsFile.KEY_RENDERER_DEBUG);
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_RENDERER_DEBUG, Settings.SECTION_DEBUG, R.string.renderer_debug, R.string.renderer_debug_description, false, rendererDebug));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,78 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Abstraction for a screen showing a list of settings. Instances of
|
||||
* this type of view will each display a layer of the setting hierarchy.
|
||||
*/
|
||||
public interface SettingsFragmentView {
|
||||
/**
|
||||
* Called by the containing Activity to notify the Fragment that an
|
||||
* asynchronous load operation completed.
|
||||
*
|
||||
* @param settings The (possibly null) result of the ini load operation.
|
||||
*/
|
||||
void onSettingsFileLoaded(Settings settings);
|
||||
|
||||
/**
|
||||
* Pass a settings HashMap to the containing activity, so that it can
|
||||
* share the HashMap with other SettingsFragments; useful so that rotations
|
||||
* do not require an additional load operation.
|
||||
*
|
||||
* @param settings An ArrayList containing all the settings HashMaps.
|
||||
*/
|
||||
void passSettingsToActivity(Settings settings);
|
||||
|
||||
/**
|
||||
* Pass an ArrayList to the View so that it can be displayed on screen.
|
||||
*
|
||||
* @param settingsList The result of converting the HashMap to an ArrayList
|
||||
*/
|
||||
void showSettingsList(ArrayList<SettingsItem> settingsList);
|
||||
|
||||
/**
|
||||
* Called by the containing Activity when an asynchronous load operation fails.
|
||||
* Instructs the Fragment to load the settings screen with defaults selected.
|
||||
*/
|
||||
void loadDefaultSettings();
|
||||
|
||||
/**
|
||||
* @return The Fragment's containing activity.
|
||||
*/
|
||||
FragmentActivity getActivity();
|
||||
|
||||
/**
|
||||
* Tell the Fragment to tell the containing Activity to show a new
|
||||
* Fragment containing a submenu of settings.
|
||||
*
|
||||
* @param menuKey Identifier for the settings group that should be shown.
|
||||
*/
|
||||
void loadSubMenu(String menuKey);
|
||||
|
||||
/**
|
||||
* Tell the Fragment to tell the containing activity to display a toast message.
|
||||
*
|
||||
* @param message Text to be shown in the Toast
|
||||
* @param is_long Whether this should be a long Toast or short one.
|
||||
*/
|
||||
void showToastMessage(String message, boolean is_long);
|
||||
|
||||
/**
|
||||
* Have the fragment add a setting to the HashMap.
|
||||
*
|
||||
* @param setting The (possibly previously missing) new setting.
|
||||
*/
|
||||
void putSetting(Setting setting);
|
||||
|
||||
/**
|
||||
* Have the fragment tell the containing Activity that a setting was modified.
|
||||
*/
|
||||
void onSettingChanged();
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
// 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.settings.ui
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem
|
||||
|
||||
/**
|
||||
* Abstraction for a screen showing a list of settings. Instances of
|
||||
* this type of view will each display a layer of the setting hierarchy.
|
||||
*/
|
||||
interface SettingsFragmentView {
|
||||
/**
|
||||
* Pass an ArrayList to the View so that it can be displayed on screen.
|
||||
*
|
||||
* @param settingsList The result of converting the HashMap to an ArrayList
|
||||
*/
|
||||
fun showSettingsList(settingsList: ArrayList<SettingsItem>)
|
||||
|
||||
/**
|
||||
* Instructs the Fragment to load the settings screen.
|
||||
*/
|
||||
fun loadSettingsList()
|
||||
|
||||
/**
|
||||
* @return The Fragment's containing activity.
|
||||
*/
|
||||
val activityView: SettingsActivityView?
|
||||
|
||||
/**
|
||||
* Tell the Fragment to tell the containing Activity to show a new
|
||||
* Fragment containing a submenu of settings.
|
||||
*
|
||||
* @param menuKey Identifier for the settings group that should be shown.
|
||||
*/
|
||||
fun loadSubMenu(menuKey: String)
|
||||
|
||||
/**
|
||||
* Tell the Fragment to tell the containing activity to display a toast message.
|
||||
*
|
||||
* @param message Text to be shown in the Toast
|
||||
* @param is_long Whether this should be a long Toast or short one.
|
||||
*/
|
||||
fun showToastMessage(message: String?, is_long: Boolean)
|
||||
|
||||
/**
|
||||
* Have the fragment add a setting to the HashMap.
|
||||
*
|
||||
* @param setting The (possibly previously missing) new setting.
|
||||
*/
|
||||
fun putSetting(setting: AbstractSetting)
|
||||
|
||||
/**
|
||||
* Have the fragment tell the containing Activity that a setting was modified.
|
||||
*/
|
||||
fun onSettingChanged()
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class CheckBoxSettingViewHolder extends SettingViewHolder {
|
||||
private CheckBoxSetting mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
private CheckBox mCheckbox;
|
||||
|
||||
public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
mCheckbox = root.findViewById(R.id.checkbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mItem = (CheckBoxSetting) item;
|
||||
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
|
||||
if (item.getDescriptionId() > 0) {
|
||||
mTextSettingDescription.setText(item.getDescriptionId());
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTextSettingDescription.setText("");
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
mCheckbox.setChecked(mItem.isChecked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
mCheckbox.toggle();
|
||||
|
||||
getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked());
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
public final class DateTimeViewHolder extends SettingViewHolder {
|
||||
private DateTimeSetting mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
public DateTimeViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
Log.error("test " + mTextSettingName);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
Log.error("test " + mTextSettingDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mItem = (DateTimeSetting) item;
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
|
||||
if (item.getDescriptionId() > 0) {
|
||||
mTextSettingDescription.setText(item.getDescriptionId());
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
getAdapter().onDateTimeClick(mItem, getAdapterPosition());
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
// 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.settings.ui.viewholder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import org.citra.citra_emu.databinding.ListItemSettingBinding
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||
SettingViewHolder(binding.root, adapter) {
|
||||
private lateinit var setting: DateTimeSetting
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item as DateTimeSetting
|
||||
binding.textSettingName.setText(item.nameId)
|
||||
if (item.descriptionId != 0) {
|
||||
binding.textSettingDescription.visibility = View.VISIBLE
|
||||
binding.textSettingDescription.setText(item.descriptionId)
|
||||
} else {
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
}
|
||||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
val epochTime = try {
|
||||
setting.value.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
val date = setting.value.substringBefore(" ")
|
||||
val time = setting.value.substringAfter(" ")
|
||||
|
||||
val formatter = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZZZZ")
|
||||
val gmt = formatter.parse("${date}T${time}+0000")
|
||||
gmt!!.time / 1000
|
||||
}
|
||||
val instant = Instant.ofEpochMilli(epochTime * 1000)
|
||||
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
|
||||
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
||||
binding.textSettingValue.text = dateFormatter.format(zonedTime)
|
||||
|
||||
if (setting.isEditable) {
|
||||
binding.textSettingName.alpha = 1f
|
||||
binding.textSettingDescription.alpha = 1f
|
||||
binding.textSettingValue.alpha = 1f
|
||||
} else {
|
||||
binding.textSettingName.alpha = 0.5f
|
||||
binding.textSettingDescription.alpha = 0.5f
|
||||
binding.textSettingValue.alpha = 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
if (setting.isEditable) {
|
||||
adapter.onDateTimeClick(setting, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
if (setting.isEditable) {
|
||||
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class HeaderViewHolder extends SettingViewHolder {
|
||||
private TextView mHeaderName;
|
||||
|
||||
public HeaderViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mHeaderName = root.findViewById(R.id.text_header_name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mHeaderName.setText(item.getNameId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
// no-op
|
||||
}
|
||||
}
|
@ -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.settings.ui.viewholder
|
||||
|
||||
import android.view.View
|
||||
import org.citra.citra_emu.databinding.ListItemSettingsHeaderBinding
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter
|
||||
|
||||
class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: SettingsAdapter) :
|
||||
SettingViewHolder(binding.root, adapter) {
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener(null)
|
||||
}
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
binding.textHeaderName.setText(item.nameId)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
// no-op
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class InputBindingSettingViewHolder extends SettingViewHolder {
|
||||
private InputBindingSetting mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
private Context mContext;
|
||||
|
||||
public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) {
|
||||
super(itemView, adapter);
|
||||
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
|
||||
|
||||
mItem = (InputBindingSetting) item;
|
||||
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
|
||||
String key = sharedPreferences.getString(mItem.getKey(), "");
|
||||
if (key != null && !key.isEmpty()) {
|
||||
mTextSettingDescription.setText(key);
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
getAdapter().onInputBindingClick(mItem, getAdapterPosition());
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user