Does this work? I hate Git sometimes

This commit is contained in:
Jarrod Norwell 2024-03-13 03:27:27 +08:00
parent 3fd22b8781
commit dab709d300
445 changed files with 85216 additions and 3 deletions

View File

@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2020 sudachi Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
function(copy_sudachi_FFmpeg_deps target_dir)
include(WindowsCopyFiles)
set(DLL_DEST "$<TARGET_FILE_DIR:${target_dir}>/")
file(READ "${FFmpeg_PATH}/requirements.txt" FFmpeg_REQUIRED_DLLS)
string(STRIP "${FFmpeg_REQUIRED_DLLS}" FFmpeg_REQUIRED_DLLS)
windows_copy_files(${target_dir} ${FFmpeg_LIBRARY_DIR} ${DLL_DEST} ${FFmpeg_REQUIRED_DLLS})
endfunction(copy_sudachi_FFmpeg_deps)

View File

@ -0,0 +1,125 @@
# SPDX-FileCopyrightText: 2016 Citra Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
function(copy_sudachi_Qt5_deps target_dir)
include(WindowsCopyFiles)
if (MSVC)
set(DLL_DEST "$<TARGET_FILE_DIR:${target_dir}>/")
set(Qt5_DLL_DIR "${Qt5_DIR}/../../../bin")
else()
set(DLL_DEST "${CMAKE_BINARY_DIR}/bin/")
set(Qt5_DLL_DIR "${Qt5_DIR}/../../../lib/")
endif()
set(Qt5_PLATFORMS_DIR "${Qt5_DIR}/../../../plugins/platforms/")
set(Qt5_PLATFORMTHEMES_DIR "${Qt5_DIR}/../../../plugins/platformthemes/")
set(Qt5_PLATFORMINPUTCONTEXTS_DIR "${Qt5_DIR}/../../../plugins/platforminputcontexts/")
set(Qt5_MEDIASERVICE_DIR "${Qt5_DIR}/../../../plugins/mediaservice/")
set(Qt5_XCBGLINTEGRATIONS_DIR "${Qt5_DIR}/../../../plugins/xcbglintegrations/")
set(Qt5_STYLES_DIR "${Qt5_DIR}/../../../plugins/styles/")
set(Qt5_IMAGEFORMATS_DIR "${Qt5_DIR}/../../../plugins/imageformats/")
set(Qt5_RESOURCES_DIR "${Qt5_DIR}/../../../resources/")
set(PLATFORMS ${DLL_DEST}plugins/platforms/)
set(MEDIASERVICE ${DLL_DEST}mediaservice/)
set(STYLES ${DLL_DEST}plugins/styles/)
set(IMAGEFORMATS ${DLL_DEST}plugins/imageformats/)
if (MSVC)
windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST}
Qt5Core$<$<CONFIG:Debug>:d>.*
Qt5Gui$<$<CONFIG:Debug>:d>.*
Qt5Widgets$<$<CONFIG:Debug>:d>.*
Qt5Network$<$<CONFIG:Debug>:d>.*
)
if (SUDACHI_USE_QT_MULTIMEDIA)
windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST}
Qt5Multimedia$<$<CONFIG:Debug>:d>.*
)
endif()
if (SUDACHI_USE_QT_WEB_ENGINE)
windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST}
Qt5Network$<$<CONFIG:Debug>:d>.*
Qt5Positioning$<$<CONFIG:Debug>:d>.*
Qt5PrintSupport$<$<CONFIG:Debug>:d>.*
Qt5Qml$<$<CONFIG:Debug>:d>.*
Qt5QmlModels$<$<CONFIG:Debug>:d>.*
Qt5Quick$<$<CONFIG:Debug>:d>.*
Qt5QuickWidgets$<$<CONFIG:Debug>:d>.*
Qt5WebChannel$<$<CONFIG:Debug>:d>.*
Qt5WebEngineCore$<$<CONFIG:Debug>:d>.*
Qt5WebEngineWidgets$<$<CONFIG:Debug>:d>.*
QtWebEngineProcess$<$<CONFIG:Debug>:d>.*
)
windows_copy_files(${target_dir} ${Qt5_RESOURCES_DIR} ${DLL_DEST}
icudtl.dat
qtwebengine_devtools_resources.pak
qtwebengine_resources.pak
qtwebengine_resources_100p.pak
qtwebengine_resources_200p.pak
)
endif ()
windows_copy_files(sudachi ${Qt5_PLATFORMS_DIR} ${PLATFORMS} qwindows$<$<CONFIG:Debug>:d>.*)
windows_copy_files(sudachi ${Qt5_STYLES_DIR} ${STYLES} qwindowsvistastyle$<$<CONFIG:Debug>:d>.*)
windows_copy_files(sudachi ${Qt5_IMAGEFORMATS_DIR} ${IMAGEFORMATS}
qjpeg$<$<CONFIG:Debug>:d>.*
qgif$<$<CONFIG:Debug>:d>.*
)
windows_copy_files(sudachi ${Qt5_MEDIASERVICE_DIR} ${MEDIASERVICE}
dsengine$<$<CONFIG:Debug>:d>.*
wmfengine$<$<CONFIG:Debug>:d>.*
)
else()
set(Qt5_DLLS
"${Qt5_DLL_DIR}libQt5Core.so.5"
"${Qt5_DLL_DIR}libQt5DBus.so.5"
"${Qt5_DLL_DIR}libQt5Gui.so.5"
"${Qt5_DLL_DIR}libQt5Widgets.so.5"
"${Qt5_DLL_DIR}libQt5XcbQpa.so.5"
"${Qt5_DLL_DIR}libicudata.so.60"
"${Qt5_DLL_DIR}libicui18n.so.60"
"${Qt5_DLL_DIR}libicuuc.so.60"
)
set(Qt5_IMAGEFORMAT_DLLS
"${Qt5_IMAGEFORMATS_DIR}libqjpeg.so"
"${Qt5_IMAGEFORMATS_DIR}libqgif.so"
"${Qt5_IMAGEFORMATS_DIR}libqico.so"
)
set(Qt5_PLATFORMTHEME_DLLS
"${Qt5_PLATFORMTHEMES_DIR}libqgtk3.so"
"${Qt5_PLATFORMTHEMES_DIR}libqxdgdesktopportal.so"
)
set(Qt5_PLATFORM_DLLS
"${Qt5_PLATFORMS_DIR}libqxcb.so"
)
set(Qt5_PLATFORMINPUTCONTEXT_DLLS
"${Qt5_PLATFORMINPUTCONTEXTS_DIR}libcomposeplatforminputcontextplugin.so"
"${Qt5_PLATFORMINPUTCONTEXTS_DIR}libibusplatforminputcontextplugin.so"
)
set(Qt5_XCBGLINTEGRATION_DLLS
"${Qt5_XCBGLINTEGRATIONS_DIR}libqxcb-glx-integration.so"
)
foreach(LIB ${Qt5_DLLS})
file(COPY ${LIB} DESTINATION "${DLL_DEST}/lib" FOLLOW_SYMLINK_CHAIN)
endforeach()
foreach(LIB ${Qt5_IMAGEFORMAT_DLLS})
file(COPY ${LIB} DESTINATION "${DLL_DEST}plugins/imageformats/" FOLLOW_SYMLINK_CHAIN)
endforeach()
foreach(LIB ${Qt5_PLATFORMTHEME_DLLS})
file(COPY ${LIB} DESTINATION "${DLL_DEST}plugins/platformthemes/" FOLLOW_SYMLINK_CHAIN)
endforeach()
foreach(LIB ${Qt5_PLATFORM_DLLS})
file(COPY ${LIB} DESTINATION "${DLL_DEST}plugins/platforms/" FOLLOW_SYMLINK_CHAIN)
endforeach()
foreach(LIB ${Qt5_PLATFORMINPUTCONTEXT_DLLS})
file(COPY ${LIB} DESTINATION "${DLL_DEST}plugins/platforminputcontexts/" FOLLOW_SYMLINK_CHAIN)
endforeach()
foreach(LIB ${Qt5_XCBGLINTEGRATION_DLLS})
file(COPY ${LIB} DESTINATION "${DLL_DEST}plugins/xcbglintegrations/" FOLLOW_SYMLINK_CHAIN)
endforeach()
endif()
# Create an empty qt.conf file. Qt will detect that this file exists, and use the folder that its in as the root folder.
# This way it'll look for plugins in the root/plugins/ folder
add_custom_command(TARGET sudachi POST_BUILD
COMMAND ${CMAKE_COMMAND} -E touch ${DLL_DEST}qt.conf
)
endfunction(copy_sudachi_Qt5_deps)

View File

@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2016 Citra Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
function(copy_sudachi_SDL_deps target_dir)
include(WindowsCopyFiles)
set(DLL_DEST "$<TARGET_FILE_DIR:${target_dir}>/")
windows_copy_files(${target_dir} ${SDL2_DLL_DIR} ${DLL_DEST} SDL2.dll)
endfunction(copy_sudachi_SDL_deps)

19
dist/72-sudachi-input.rules vendored Normal file
View File

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2023 sudachi Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Allow systemd-logind to manage user access to hidraw with this file
# On most systems, this file should be installed to /etc/udev/rules.d/72-sudachi-input.rules
# Consult your distro if this is not the case
# Switch Pro Controller (USB/Bluetooth)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*057e:2009*", MODE="0660", TAG+="uaccess"
# Joy-Con L (Bluetooth)
KERNEL=="hidraw*", KERNELS=="*057e:2006*", MODE="0660", TAG+="uaccess"
# Joy-Con R (Bluetooth)
KERNEL=="hidraw*", KERNELS=="*057e:2007*", MODE="0660", TAG+="uaccess"
# Joy-Con Charging Grip (USB)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"

16
dist/org.sudachi_emu.sudachi.desktop vendored Normal file
View File

@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2018 sudachi Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
[Desktop Entry]
Version=1.0
Type=Application
Name=sudachi
GenericName=Switch Emulator
Comment=Nintendo Switch video game console emulator
Icon=org.sudachi_emu.sudachi
TryExec=sudachi
Exec=sudachi %f
Categories=Game;Emulator;Qt;
MimeType=application/x-nx-nro;application/x-nx-nso;application/x-nx-nsp;application/x-nx-xci;
Keywords=Nintendo;Switch;
StartupWMClass=sudachi

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: 2021 sudachi Emulator Project
SPDX-License-Identifier: CC0-1.0
-->
<component type="desktop-application">
<id>org.sudachi_emu.sudachi</id>
<metadata_license>CC0-1.0</metadata_license>
<name>sudachi</name>
<summary>Nintendo Switch emulator</summary>
<description>
<p>sudachi is the world's most popular, open-source, Nintendo Switch emulator — started by the creators of Citra.</p>
<p>The emulator is capable of running most commercial games at full speed, provided you meet the necessary hardware requirements.</p>
<p>For a full list of games sudachi support, please visit our Compatibility page.</p>
<p>Check out our website for the latest news on exciting features, monthly progress reports, and more!</p>
</description>
<categories>
<category>Game</category>
<category>Emulator</category>
</categories>
<keywords>
<keyword>switch</keyword>
<keyword>emulator</keyword>
</keywords>
<url type="homepage">https://sudachi-emu.org/</url>
<url type="bugtracker">https://github.com/sudachi-emu/sudachi/issues</url>
<url type="faq">https://sudachi-emu.org/wiki/faq/</url>
<url type="help">https://sudachi-emu.org/wiki/home/</url>
<url type="donation">https://sudachi-emu.org/donate/</url>
<url type="translate">https://www.transifex.com/projects/p/sudachi</url>
<url type="contact">https://community.citra-emu.org/</url>
<url type="vcs-browser">https://github.com/sudachi-emu/sudachi</url>
<url type="contribute">https://sudachi-emu.org/wiki/contributing/</url>
<launchable type="desktop-id">org.sudachi_emu.sudachi.desktop</launchable>
<provides>
<binary>sudachi</binary>
<binary>sudachi-cmd</binary>
</provides>
<supports>
<control>pointing</control>
<control>keyboard</control>
<control>gamepad</control>
</supports>
<requires>
<memory>8192</memory>
</requires>
<recommends>
<memory>16384</memory>
</recommends>
<project_license>GPL-3.0-or-later</project_license>
<developer_name>sudachi Emulator Team</developer_name>
<content_rating type="oars-1.0"/>
<screenshots>
<screenshot type="default"><image>https://raw.githubusercontent.com/sudachi-emu/sudachi-emu.github.io/master/images/screenshots/001-Super%20Mario%20Odyssey%20.png</image></screenshot>
<screenshot><image>https://raw.githubusercontent.com/sudachi-emu/sudachi-emu.github.io/master/images/screenshots/004-The%20Legend%20of%20Zelda%20Skyward%20Sword%20HD.png</image></screenshot>
<screenshot><image>https://raw.githubusercontent.com/sudachi-emu/sudachi-emu.github.io/master/images/screenshots/007-Pokemon%20Sword.png</image></screenshot>
<screenshot><image>https://raw.githubusercontent.com/sudachi-emu/sudachi-emu.github.io/master/images/screenshots/010-Hyrule%20Warriors%20Age%20of%20Calamity.png</image></screenshot>
<screenshot><image>https://raw.githubusercontent.com/sudachi-emu/sudachi-emu.github.io/master/images/screenshots/039-Pok%C3%A9mon%20Mystery%20Dungeon%20Rescue%20Team%20DX.png.png.png</image></screenshot>
</screenshots>
</component>

39
dist/org.sudachi_emu.sudachi.xml vendored Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: 2018 sudachi Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-nx-nro">
<comment>Nintendo Switch homebrew executable</comment>
<acronym>NRO</acronym>
<icon name="org.sudachi_emu.sudachi"/>
<glob pattern="*.nro"/>
<magic><match value="NRO" type="string" offset="16"/></magic>
</mime-type>
<mime-type type="application/x-nx-nso">
<comment>Nintendo Switch homebrew executable</comment>
<acronym>NSO</acronym>
<icon name="org.sudachi_emu.sudachi"/>
<glob pattern="*.nso"/>
<magic><match value="NSO" type="string" offset="0"/></magic>
</mime-type>
<mime-type type="application/x-nx-nsp">
<comment>Nintendo Switch Package</comment>
<acronym>NSP</acronym>
<icon name="org.sudachi_emu.sudachi"/>
<glob pattern="*.nsp"/>
<magic><match value="PFS" type="string" offset="0"/></magic>
</mime-type>
<mime-type type="application/x-nx-xci">
<comment>Nintendo Switch Card Image</comment>
<acronym>XCI</acronym>
<icon name="org.sudachi_emu.sudachi"/>
<glob pattern="*.xci"/>
</mime-type>
</mime-info>

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
dist/sudachi.bmp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

12
dist/sudachi.desktop vendored Normal file
View File

@ -0,0 +1,12 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=sudachi
GenericName=Switch Emulator
Comment=Nintendo Switch video game console emulator
Icon=sudachi
TryExec=sudachi
Exec=sudachi %f
Categories=Game;Emulator;Qt;
MimeType=application/x-nx-nro;application/x-nx-nso;application/x-nx-nsp;application/x-nx-xci;
Keywords=Switch;Nintendo;

BIN
dist/sudachi.icns vendored Normal file

Binary file not shown.

BIN
dist/sudachi.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

58
dist/sudachi.manifest vendored Normal file
View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
SPDX-FileCopyrightText: 2020 sudachi Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<assembly manifestVersion="1.0"
xmlns="urn:schemas-microsoft-com:asm.v1"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<!-- Windows 7/8/8.1/10 -->
<dpiAware
xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
true/pm
</dpiAware>
<!-- Windows 10, version 1607 or later -->
<dpiAwareness
xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
PerMonitorV2
</dpiAwareness>
<!-- Windows 10, version 1703 or later -->
<gdiScaling
xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">
true
</gdiScaling>
<ws2:longPathAware
xmlns:ws3="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
true
</ws2:longPathAware>
</asmv3:windowsSettings>
</asmv3:application>
<compatibility
xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<trustInfo
xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<!--
UAC settings:
- app should run at same integrity level as calling process
- app does not need to manipulate windows belonging to
higher-integrity-level processes
-->
<requestedExecutionLevel
level="asInvoker"
uiAccess="false"
/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

2
externals/SDL vendored

@ -1 +1 @@
Subproject commit ee87132385014449c4cd33236c661d57539071c1 Subproject commit d79f8652510b8bd1f89c90be2ab65fc8940056eb

@ -1 +1 @@
Subproject commit 53a952a7313f2c78d93a4f6805abe570fe35f96b Subproject commit d65908c3d416e331e075c3a5ffe7bc670112a018

View File

@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2015 Citra Emulator Project # SPDX-FileCopyrightText: 2015 Citra Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# Enforce yuzu's whitespace policy # Enforce sudachi's whitespace policy
git config --local core.whitespace tab-in-indent,trailing-space git config --local core.whitespace tab-in-indent,trailing-space
paths_to_check="src/ CMakeLists.txt" paths_to_check="src/ CMakeLists.txt"

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="500"
android:viewportHeight="500">
<path
android:fillColor="#C6C6C6"
android:fillType="nonZero"
android:pathData="M262.66,175.11L262.66,375.05C318.54,375.05 363.85,330.29 363.85,275.08C363.85,219.87 318.54,175.11 262.66,175.11M282.43,197.01C318.67,206 344.09,238.19 344.09,275.11C344.09,312.03 318.67,344.22 282.43,353.2L282.43,197.01"
android:strokeWidth="1.46"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#FFDC00"
android:fillType="nonZero"
android:pathData="M237.31,125.11C181.43,125.11 136.12,169.87 136.12,225.08C136.12,280.29 181.43,325.05 237.31,325.05ZM217.57,147.01L217.57,303.2C189.11,296.16 166.67,274.54 158.84,246.6C151.01,218.65 159,188.71 179.75,168.21C190.16,157.86 203.24,150.53 217.57,147.01"
android:strokeWidth="1.46"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="155.3dp"
android:height="172.55dp"
android:viewportWidth="155.3"
android:viewportHeight="172.55">
<path
android:fillColor="#C6C6C6"
android:pathData="M86.28,34.51v138a69,69 0,0 0,0 -138M99.76,49.63a55.57,55.57 0,0 1,0 107.8V49.63" />
<path
android:fillColor="#FFDC00"
android:pathData="M69,0a69,69 0,0 0,0 138ZM55.54,15.12v107.8A55.55,55.55 0,0 1,29.75 29.75,55.1 55.1,0 0,1 55.54,15.12" />
</vector>

View File

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="340.97dp"
android:height="389.85dp"
android:viewportWidth="340.97"
android:viewportHeight="389.85">
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M341,268.68v73c0,14.5 -2.24,25.24 -6.83,32.82 -5.92,10.15 -16.21,15.32 -30.54,15.32S279,384.61 273,374.27c-4.56,-7.64 -6.8,-18.42 -6.8,-32.92V268.68a4.52,4.52 0,0 1,4.51 -4.51H273a4.5,4.5 0,0 1,4.5 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.52,4.52 0,0 1,4.52 -4.51h2.27A4.5,4.5 0,0 1,341 268.68Z" />
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M246.49,389.85H178.6c-2.35,0 -4.72,-1.88 -4.72,-6.08a8.28,8.28 0,0 1,1.33 -4.48l60.33,-104.47H186a4.51,4.51 0,0 1,-4.51 -4.51v-1.58a4.51,4.51 0,0 1,4.48 -4.51h0.8c58.69,-0.11 59.12,0 59.67,0.07a5.19,5.19 0,0 1,4 5.8,8.69 8.69,0 0,1 -1.33,3.76l-60.6,104.77h58a4.51,4.51 0,0 1,4.51 4.51v2.21A4.51,4.51 0,0 1,246.49 389.85Z" />
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M73.6,268.68v82.06c0,26 -11.8,38.44 -37.12,39.09h-0.12a4.51,4.51 0,0 1,-4.51 -4.51V383a4.51,4.51 0,0 1,4.48 -4.5c18.49,-0.15 26,-8.23 26,-27.9v-2.37A32.34,32.34 0,0 1,59 351.46c-6.39,5.5 -14.5,8.29 -24.07,8.29C12.09,359.75 0,347.34 0,323.86V268.68a4.52,4.52 0,0 1,4.51 -4.51H6.73a4.52,4.52 0,0 1,4.5 4.51v55c0,7.6 1.82,14.22 5,18.18 3.57,4.56 9.17,6.49 18.75,6.49 10.13,0 17.32,-3.76 22,-11.5 3.61,-5.92 5.43,-13.66 5.43,-23V268.68a4.52,4.52 0,0 1,4.51 -4.51h2.22A4.52,4.52 0,0 1,73.6 268.68Z" />
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M163.27,268.68v73c0,14.5 -2.24,25.24 -6.84,32.82 -5.92,10.15 -16.2,15.32 -30.53,15.32s-24.62,-5.23 -30.58,-15.57c-4.56,-7.64 -6.79,-18.42 -6.79,-32.92V268.68A4.51,4.51 0,0 1,93 264.17h2.28a4.51,4.51 0,0 1,4.51 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.51,4.51 0,0 1,4.51 -4.51h2.27A4.51,4.51 0,0 1,163.27 268.68Z" />
<path
android:fillColor="#C6C6C6"
android:pathData="M181.2,42.83V214.17a85.67,85.67 0,0 0,0 -171.34M197.93,61.6a69,69 0,0 1,0 133.8V61.6" />
<path
android:fillColor="#FFDC00"
android:pathData="M159.78,0a85.67,85.67 0,1 0,0 171.33ZM143.05,18.77v133.8A69,69 0,0 1,111 36.92a68.47,68.47 0,0 1,32 -18.15" />
</vector>

View File

@ -0,0 +1,462 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu
import android.content.DialogInterface
import android.net.Uri
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
import org.sudachi.sudachi_emu.activities.EmulationActivity
import org.sudachi.sudachi_emu.fragments.CoreErrorDialogFragment
import org.sudachi.sudachi_emu.utils.DocumentsTree
import org.sudachi.sudachi_emu.utils.FileUtil
import org.sudachi.sudachi_emu.utils.Log
import org.sudachi.sudachi_emu.model.InstallResult
import org.sudachi.sudachi_emu.model.Patch
import org.sudachi.sudachi_emu.model.GameVerificationResult
/**
* Class which contains methods that interact
* with the native side of the Sudachi code.
*/
object NativeLibrary {
@JvmField
var sEmulationActivity = WeakReference<EmulationActivity?>(null)
init {
try {
System.loadLibrary("sudachi-android")
} catch (ex: UnsatisfiedLinkError) {
error("[NativeLibrary] $ex")
}
}
@Keep
@JvmStatic
fun openContentUri(path: String?, openmode: String?): Int {
return if (DocumentsTree.isNativePath(path!!)) {
SudachiApplication.documentsTree!!.openContentUri(path, openmode)
} else {
FileUtil.openContentUri(path, openmode)
}
}
@Keep
@JvmStatic
fun getSize(path: String?): Long {
return if (DocumentsTree.isNativePath(path!!)) {
SudachiApplication.documentsTree!!.getFileSize(path)
} else {
FileUtil.getFileSize(path)
}
}
@Keep
@JvmStatic
fun exists(path: String?): Boolean {
return if (DocumentsTree.isNativePath(path!!)) {
SudachiApplication.documentsTree!!.exists(path)
} else {
FileUtil.exists(path, suppressLog = true)
}
}
@Keep
@JvmStatic
fun isDirectory(path: String?): Boolean {
return if (DocumentsTree.isNativePath(path!!)) {
SudachiApplication.documentsTree!!.isDirectory(path)
} else {
FileUtil.isDirectory(path)
}
}
@Keep
@JvmStatic
fun getParentDirectory(path: String): String =
if (DocumentsTree.isNativePath(path)) {
SudachiApplication.documentsTree!!.getParentDirectory(path)
} else {
path
}
@Keep
@JvmStatic
fun getFilename(path: String): String =
if (DocumentsTree.isNativePath(path)) {
SudachiApplication.documentsTree!!.getFilename(path)
} else {
FileUtil.getFilename(Uri.parse(path))
}
external fun setAppDirectory(directory: String)
/**
* Installs a nsp or xci file to nand
* @param filename String representation of file uri
* @return int representation of [InstallResult]
*/
external fun installFileToNand(
filename: String,
callback: (max: Long, progress: Long) -> Boolean
): Int
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
external fun initializeGpuDriver(
hookLibDir: String?,
customDriverDir: String?,
customDriverName: String?,
fileRedirectDir: String?
)
external fun reloadKeys(): Boolean
external fun initializeSystem(reload: Boolean)
/**
* Begins emulation.
*/
external fun run(path: String?, programIndex: Int, frontendInitiated: Boolean)
// Surface Handling
external fun surfaceChanged(surf: Surface?)
external fun surfaceDestroyed()
/**
* 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 true if emulation is paused.
*/
external fun isPaused(): Boolean
/**
* Returns the performance stats for the current game
*/
external fun getPerfStats(): DoubleArray
/**
* Returns the current CPU backend.
*/
external fun getCpuBackend(): String
/**
* Returns the current GPU Driver.
*/
external fun getGpuDriver(): String
external fun applySettings()
external fun logSettings()
enum class CoreError {
ErrorSystemFiles,
ErrorSavestate,
ErrorUnknown
}
var coreErrorAlertResult = false
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, "coreError")
}
/**
* Handles a core error.
*
* @return true: continue; false: abort
*/
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 { onCoreErrorImpl(title, message) }
// Wait for the lock to notify that it is complete.
synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() }
return coreErrorAlertResult
}
@Keep
@JvmStatic
fun exitEmulationActivity(resultCode: Int) {
val Success = 0
val ErrorNotInitialized = 1
val ErrorGetLoader = 2
val ErrorSystemFiles = 3
val ErrorSharedFont = 4
val ErrorVideoCore = 5
val ErrorUnknown = 6
val ErrorLoader = 7
val captionId: Int
var descriptionId: Int
when (resultCode) {
ErrorVideoCore -> {
captionId = R.string.loader_error_video_core
descriptionId = R.string.loader_error_video_core_description
}
else -> {
captionId = R.string.loader_error_encrypted
descriptionId = R.string.loader_error_encrypted_roms_description
if (!reloadKeys()) {
descriptionId = R.string.loader_error_encrypted_keys_description
}
}
}
val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) {
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.")
return
}
val builder = MaterialAlertDialogBuilder(emulationActivity)
.setTitle(captionId)
.setMessage(
Html.fromHtml(
emulationActivity.getString(descriptionId),
Html.FROM_HTML_MODE_LEGACY
)
)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
emulationActivity.finish()
}
.setOnDismissListener { emulationActivity.finish() }
emulationActivity.runOnUiThread {
val alert = builder.create()
alert.show()
(alert.findViewById<View>(android.R.id.message) as TextView).movementMethod =
LinkMovementMethod.getInstance()
}
}
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
Log.debug("[NativeLibrary] Registering EmulationActivity.")
sEmulationActivity = WeakReference(emulationActivity)
}
fun clearEmulationActivity() {
Log.debug("[NativeLibrary] Unregistering EmulationActivity.")
sEmulationActivity.clear()
}
@Keep
@JvmStatic
fun onEmulationStarted() {
sEmulationActivity.get()!!.onEmulationStarted()
}
@Keep
@JvmStatic
fun onEmulationStopped(status: Int) {
sEmulationActivity.get()!!.onEmulationStopped(status)
}
@Keep
@JvmStatic
fun onProgramChanged(programIndex: Int) {
sEmulationActivity.get()!!.onProgramChanged(programIndex)
}
/**
* Logs the Sudachi version, Android version and, CPU.
*/
external fun logDeviceInfo()
/**
* Submits inline keyboard text. Called on input for buttons that result text.
* @param text Text to submit to the inline software keyboard implementation.
*/
external fun submitInlineKeyboardText(text: String?)
/**
* Submits inline keyboard input. Used to indicate keys pressed that are not text.
* @param key_code Android Key Code associated with the keyboard input.
*/
external fun submitInlineKeyboardInput(key_code: Int)
/**
* Creates a generic user directory if it doesn't exist already
*/
external fun initializeEmptyUserDirectory()
/**
* Gets the launch path for a given applet. It is the caller's responsibility to also
* set the system's current applet ID before trying to launch the nca given by this function.
*
* @param id The applet entry ID
* @return The applet's launch path
*/
external fun getAppletLaunchPath(id: Long): String
/**
* Sets the system's current applet ID before launching.
*
* @param appletId One of the ids in the Service::AM::Applets::AppletId enum
*/
external fun setCurrentAppletId(appletId: Int)
/**
* Sets the cabinet mode for launching the cabinet applet.
*
* @param cabinetMode One of the modes that corresponds to the enum in Service::NFP::CabinetMode
*/
external fun setCabinetMode(cabinetMode: Int)
/**
* Checks whether NAND contents are available and valid.
*
* @return 'true' if firmware is available
*/
external fun isFirmwareAvailable(): Boolean
/**
* Checks the PatchManager for any addons that are available
*
* @param path Path to game file. Can be a [Uri].
* @param programId String representation of a game's program ID
* @return Array of available patches
*/
external fun getPatchesForFile(path: String, programId: String): Array<Patch>?
/**
* Removes an update for a given [programId]
* @param programId String representation of a game's program ID
*/
external fun removeUpdate(programId: String)
/**
* Removes all DLC for a [programId]
* @param programId String representation of a game's program ID
*/
external fun removeDLC(programId: String)
/**
* Removes a mod installed for a given [programId]
* @param programId String representation of a game's program ID
* @param name The name of a mod as given by [getPatchesForFile]. This corresponds with the name
* of the mod's directory in a game's load folder.
*/
external fun removeMod(programId: String, name: String)
/**
* Verifies all installed content
* @param callback UI callback for verification progress. Return true in the callback to cancel.
* @return Array of content that failed verification. Successful if empty.
*/
external fun verifyInstalledContents(
callback: (max: Long, progress: Long) -> Boolean
): Array<String>
/**
* Verifies the contents of a game
* @param path String path to a game
* @param callback UI callback for verification progress. Return true in the callback to cancel.
* @return Int that is meant to be converted to a [GameVerificationResult]
*/
external fun verifyGameContents(
path: String,
callback: (max: Long, progress: Long) -> Boolean
): Int
/**
* Gets the save location for a specific game
*
* @param programId String representation of a game's program ID
* @return Save data path that may not exist yet
*/
external fun getSavePath(programId: String): String
/**
* Gets the root save directory for the default profile as either
* /user/save/account/<user id raw string> or /user/save/000...000/<user id>
*
* @param future If true, returns the /user/save/account/... directory
* @return Save data path that may not exist yet
*/
external fun getDefaultProfileSaveDataRoot(future: Boolean): String
/**
* Adds a file to the manual filesystem provider in our EmulationSession instance
* @param path Path to the file we're adding. Can be a string representation of a [Uri] or
* a normal path
*/
external fun addFileToFilesystemProvider(path: String)
/**
* Clears all files added to the manual filesystem provider in our EmulationSession instance
*/
external fun clearFilesystemProvider()
/**
* Checks if all necessary keys are present for decryption
*/
external fun areKeysPresent(): Boolean
}

View File

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import org.sudachi.sudachi_emu.features.input.NativeInput
import java.io.File
import org.sudachi.sudachi_emu.utils.DirectoryInitialization
import org.sudachi.sudachi_emu.utils.DocumentsTree
import org.sudachi.sudachi_emu.utils.GpuDriverHelper
import org.sudachi.sudachi_emu.utils.Log
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
class SudachiApplication : Application() {
private fun createNotificationChannels() {
val noticeChannel = NotificationChannel(
getString(R.string.notice_notification_channel_id),
getString(R.string.notice_notification_channel_name),
NotificationManager.IMPORTANCE_HIGH
)
noticeChannel.description = getString(R.string.notice_notification_channel_description)
noticeChannel.setSound(null, null)
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(noticeChannel)
}
override fun onCreate() {
super.onCreate()
application = this
documentsTree = DocumentsTree()
DirectoryInitialization.start()
GpuDriverHelper.initializeDriverParameters()
NativeInput.reloadInputDevices()
NativeLibrary.logDeviceInfo()
Log.logDeviceInfo()
createNotificationChannels()
}
companion object {
var documentsTree: DocumentsTree? = null
lateinit var application: SudachiApplication
val appContext: Context
get() = application.applicationContext
}
}

View File

@ -0,0 +1,509 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.activities
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle
import android.util.Rational
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.Surface
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.navigation.fragment.NavHostFragment
import androidx.preference.PreferenceManager
import org.sudachi.sudachi_emu.NativeLibrary
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.databinding.ActivityEmulationBinding
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.settings.model.BooleanSetting
import org.sudachi.sudachi_emu.features.settings.model.IntSetting
import org.sudachi.sudachi_emu.features.settings.model.Settings
import org.sudachi.sudachi_emu.model.EmulationViewModel
import org.sudachi.sudachi_emu.model.Game
import org.sudachi.sudachi_emu.utils.InputHandler
import org.sudachi.sudachi_emu.utils.Log
import org.sudachi.sudachi_emu.utils.MemoryUtil
import org.sudachi.sudachi_emu.utils.NativeConfig
import org.sudachi.sudachi_emu.utils.NfcReader
import org.sudachi.sudachi_emu.utils.ParamPackage
import org.sudachi.sudachi_emu.utils.ThemeHelper
import java.text.NumberFormat
import kotlin.math.roundToInt
class EmulationActivity : AppCompatActivity(), SensorEventListener {
private lateinit var binding: ActivityEmulationBinding
var isActivityRecreated = false
private lateinit var nfcReader: NfcReader
private val gyro = FloatArray(3)
private val accel = FloatArray(3)
private var motionTimestamp: Long = 0
private var flipMotionOrientation: Boolean = false
private val actionPause = "ACTION_EMULATOR_PAUSE"
private val actionPlay = "ACTION_EMULATOR_PLAY"
private val actionMute = "ACTION_EMULATOR_MUTE"
private val actionUnmute = "ACTION_EMULATOR_UNMUTE"
private val emulationViewModel: EmulationViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
Log.gameLaunched = true
ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState)
InputHandler.updateControllerData()
val players = NativeConfig.getInputSettings(true)
var hasConfiguredControllers = false
players.forEach {
if (it.hasMapping()) {
hasConfiguredControllers = true
}
}
if (!hasConfiguredControllers && InputHandler.androidControllers.isNotEmpty()) {
var params: ParamPackage? = null
for (controller in InputHandler.registeredControllers) {
if (controller.get("port", -1) == 0) {
params = controller
break
}
}
if (params != null) {
NativeInput.updateMappingsWithDefault(
0,
params,
params.get("display", getString(R.string.unknown))
)
NativeConfig.saveGlobalConfig()
}
}
binding = ActivityEmulationBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras)
isActivityRecreated = savedInstanceState != null
// Set these options now so that the SurfaceView the game renders into is the right size.
enableFullscreenImmersive()
window.decorView.setBackgroundColor(getColor(android.R.color.black))
nfcReader = NfcReader(this)
nfcReader.initialize()
val preferences = PreferenceManager.getDefaultSharedPreferences(SudachiApplication.appContext)
if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {
if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) {
Toast.makeText(
this,
getString(
R.string.device_memory_inadequate,
MemoryUtil.getDeviceRAM(),
getString(
R.string.memory_formatted,
NumberFormat.getInstance().format(MemoryUtil.REQUIRED_MEMORY),
getString(R.string.memory_gigabyte)
)
),
Toast.LENGTH_LONG
).show()
preferences.edit()
.putBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, true)
.apply()
}
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (event.action == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// Special case, we do not support multiline input, dismiss the keyboard.
val overlayView: View =
this.findViewById(R.id.surface_input_overlay)
val im =
overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
im.hideSoftInputFromWindow(overlayView.windowToken, 0)
} else {
val textChar = event.unicodeChar
if (textChar == 0) {
// No text, button input.
NativeLibrary.submitInlineKeyboardInput(keyCode)
} else {
// Text submitted.
NativeLibrary.submitInlineKeyboardText(textChar.toChar().toString())
}
}
}
return super.onKeyDown(keyCode, event)
}
override fun onResume() {
super.onResume()
nfcReader.startScanning()
startMotionSensorListener()
InputHandler.updateControllerData()
buildPictureInPictureParams()
}
override fun onPause() {
super.onPause()
nfcReader.stopScanning()
stopMotionSensorListener()
}
override fun onUserLeaveHint() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
nfcReader.onNewIntent(intent)
InputHandler.updateControllerData()
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
) {
return super.dispatchKeyEvent(event)
}
if (emulationViewModel.drawerOpen.value) {
return super.dispatchKeyEvent(event)
}
return InputHandler.dispatchKeyEvent(event)
}
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
) {
return super.dispatchGenericMotionEvent(event)
}
if (emulationViewModel.drawerOpen.value) {
return super.dispatchGenericMotionEvent(event)
}
// Don't attempt to do anything if we are disconnecting a device.
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
return true
}
return InputHandler.dispatchGenericMotionEvent(event)
}
override fun onSensorChanged(event: SensorEvent) {
val rotation = this.display?.rotation
if (rotation == Surface.ROTATION_90) {
flipMotionOrientation = true
}
if (rotation == Surface.ROTATION_270) {
flipMotionOrientation = false
}
if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
if (flipMotionOrientation) {
accel[0] = event.values[1] / SensorManager.GRAVITY_EARTH
accel[1] = -event.values[0] / SensorManager.GRAVITY_EARTH
} else {
accel[0] = -event.values[1] / SensorManager.GRAVITY_EARTH
accel[1] = event.values[0] / SensorManager.GRAVITY_EARTH
}
accel[2] = -event.values[2] / SensorManager.GRAVITY_EARTH
}
if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {
// Investigate why sensor value is off by 6x
if (flipMotionOrientation) {
gyro[0] = -event.values[1] / 6.0f
gyro[1] = event.values[0] / 6.0f
} else {
gyro[0] = event.values[1] / 6.0f
gyro[1] = -event.values[0] / 6.0f
}
gyro[2] = event.values[2] / 6.0f
}
// Only update state on accelerometer data
if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) {
return
}
val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
motionTimestamp = event.timestamp
NativeInput.onDeviceMotionEvent(
NativeInput.Player1Device,
deltaTimestamp,
gyro[0],
gyro[1],
gyro[2],
accel[0],
accel[1],
accel[2]
)
NativeInput.onDeviceMotionEvent(
NativeInput.ConsoleDevice,
deltaTimestamp,
gyro[0],
gyro[1],
gyro[2],
accel[0],
accel[1],
accel[2]
)
}
override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
private fun enableFullscreenImmersive() {
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
PictureInPictureParams.Builder {
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) {
0 -> Rational(16, 9)
1 -> Rational(4, 3)
2 -> Rational(21, 9)
3 -> Rational(16, 10)
else -> null // Best fit
}
return this.apply { aspectRatio?.let { setAspectRatio(it) } }
}
private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder():
PictureInPictureParams.Builder {
val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf()
val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
if (NativeLibrary.isPaused()) {
val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play)
val playPendingIntent = PendingIntent.getBroadcast(
this@EmulationActivity,
R.drawable.ic_pip_play,
Intent(actionPlay),
pendingFlags
)
val playRemoteAction = RemoteAction(
playIcon,
getString(R.string.play),
getString(R.string.play),
playPendingIntent
)
pictureInPictureActions.add(playRemoteAction)
} else {
val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause)
val pausePendingIntent = PendingIntent.getBroadcast(
this@EmulationActivity,
R.drawable.ic_pip_pause,
Intent(actionPause),
pendingFlags
)
val pauseRemoteAction = RemoteAction(
pauseIcon,
getString(R.string.pause),
getString(R.string.pause),
pausePendingIntent
)
pictureInPictureActions.add(pauseRemoteAction)
}
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
val unmuteIcon = Icon.createWithResource(
this@EmulationActivity,
R.drawable.ic_pip_unmute
)
val unmutePendingIntent = PendingIntent.getBroadcast(
this@EmulationActivity,
R.drawable.ic_pip_unmute,
Intent(actionUnmute),
pendingFlags
)
val unmuteRemoteAction = RemoteAction(
unmuteIcon,
getString(R.string.unmute),
getString(R.string.unmute),
unmutePendingIntent
)
pictureInPictureActions.add(unmuteRemoteAction)
} else {
val muteIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_mute)
val mutePendingIntent = PendingIntent.getBroadcast(
this@EmulationActivity,
R.drawable.ic_pip_mute,
Intent(actionMute),
pendingFlags
)
val muteRemoteAction = RemoteAction(
muteIcon,
getString(R.string.mute),
getString(R.string.mute),
mutePendingIntent
)
pictureInPictureActions.add(muteRemoteAction)
}
return this.apply { setActions(pictureInPictureActions) }
}
fun buildPictureInPictureParams() {
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val isEmulationActive = emulationViewModel.emulationStarted.value &&
!emulationViewModel.isEmulationStopping.value
pictureInPictureParamsBuilder.setAutoEnterEnabled(
BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive
)
}
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
}
private var pictureInPictureReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == actionPlay) {
if (NativeLibrary.isPaused()) NativeLibrary.unpauseEmulation()
} else if (intent.action == actionPause) {
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
}
if (intent.action == actionUnmute) {
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
BooleanSetting.AUDIO_MUTED.setBoolean(false)
}
} else if (intent.action == actionMute) {
if (!BooleanSetting.AUDIO_MUTED.getBoolean()) {
BooleanSetting.AUDIO_MUTED.setBoolean(true)
}
}
buildPictureInPictureParams()
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
IntentFilter().apply {
addAction(actionPause)
addAction(actionPlay)
addAction(actionMute)
addAction(actionUnmute)
}.also {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(pictureInPictureReceiver, it, RECEIVER_EXPORTED)
} else {
registerReceiver(pictureInPictureReceiver, it)
}
}
} else {
try {
unregisterReceiver(pictureInPictureReceiver)
} catch (ignored: Exception) {
}
// Always resume audio, since there is no UI button
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
BooleanSetting.AUDIO_MUTED.setBoolean(false)
}
}
}
fun onEmulationStarted() {
emulationViewModel.setEmulationStarted(true)
}
fun onEmulationStopped(status: Int) {
if (status == 0 && emulationViewModel.programChanged.value == -1) {
finish()
}
emulationViewModel.setEmulationStopped(true)
}
fun onProgramChanged(programIndex: Int) {
emulationViewModel.setProgramChanged(programIndex)
}
private fun startMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
sensorManager.registerListener(this, gyroSensor, SensorManager.SENSOR_DELAY_GAME)
sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_GAME)
}
private fun stopMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
sensorManager.unregisterListener(this, gyroSensor)
sensorManager.unregisterListener(this, accelSensor)
}
companion object {
const val EXTRA_SELECTED_GAME = "SelectedGame"
fun launch(activity: AppCompatActivity, game: Game) {
val launcher = Intent(activity, EmulationActivity::class.java)
launcher.putExtra(EXTRA_SELECTED_GAME, game)
activity.startActivity(launcher)
}
private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean {
if (view == null) {
return true
}
val viewBounds = Rect()
view.getGlobalVisibleRect(viewBounds)
return !viewBounds.contains(x.roundToInt(), y.roundToInt())
}
}
}

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.annotation.SuppressLint
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
import androidx.recyclerview.widget.RecyclerView
/**
* Generic adapter that implements an [AsyncDifferConfig] and covers some of the basic boilerplate
* code used in every [RecyclerView].
* Type assigned to [Model] must inherit from [Object] in order to be compared properly.
* @param exact Decides whether each item will be compared by reference or by their contents
*/
abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>>(
exact: Boolean = true
) : ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>(exact)).build()) {
override fun onBindViewHolder(holder: Holder, position: Int) =
holder.bind(currentList[position])
private class DiffCallback<Model>(val exact: Boolean) : DiffUtil.ItemCallback<Model>() {
override fun areItemsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
if (exact) {
return oldItem === newItem
}
return oldItem == newItem
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
return oldItem == newItem
}
}
}

View File

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.annotation.SuppressLint
import androidx.recyclerview.widget.RecyclerView
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
/**
* Generic list class meant to take care of basic lists
* @param currentList The list to show initially
*/
abstract class AbstractListAdapter<Model : Any, Holder : AbstractViewHolder<Model>>(
open var currentList: List<Model>
) : RecyclerView.Adapter<Holder>() {
override fun onBindViewHolder(holder: Holder, position: Int) =
holder.bind(currentList[position])
override fun getItemCount(): Int = currentList.size
/**
* Adds an item to [currentList] and notifies the underlying adapter of the change. If no parameter
* is passed in for position, [item] is added to the end of the list. Invokes [callback] last.
* @param item The item to add to the list
* @param position Index where [item] will be added
* @param callback Lambda that's called at the end of the list changes and has the added list
* position passed in as a parameter
*/
open fun addItem(item: Model, position: Int = -1, callback: ((position: Int) -> Unit)? = null) {
val newList = currentList.toMutableList()
val positionToUpdate: Int
if (position == -1) {
newList.add(item)
currentList = newList
positionToUpdate = currentList.size - 1
} else {
newList.add(position, item)
currentList = newList
positionToUpdate = position
}
onItemAdded(positionToUpdate, callback)
}
protected fun onItemAdded(position: Int, callback: ((Int) -> Unit)? = null) {
notifyItemInserted(position)
callback?.invoke(position)
}
/**
* Replaces the [item] at [position] in the [currentList] and notifies the underlying adapter
* of the change. Invokes [callback] last.
* @param item New list item
* @param position Index where [item] will replace the existing list item
* @param callback Lambda that's called at the end of the list changes and has the changed list
* position passed in as a parameter
*/
fun changeItem(item: Model, position: Int, callback: ((position: Int) -> Unit)? = null) {
val newList = currentList.toMutableList()
newList[position] = item
currentList = newList
onItemChanged(position, callback)
}
protected fun onItemChanged(position: Int, callback: ((Int) -> Unit)? = null) {
notifyItemChanged(position)
callback?.invoke(position)
}
/**
* Removes the list item at [position] in [currentList] and notifies the underlying adapter
* of the change. Invokes [callback] last.
* @param position Index where the list item will be removed
* @param callback Lambda that's called at the end of the list changes and has the removed list
* position passed in as a parameter
*/
fun removeItem(position: Int, callback: ((position: Int) -> Unit)? = null) {
val newList = currentList.toMutableList()
newList.removeAt(position)
currentList = newList
onItemRemoved(position, callback)
}
protected fun onItemRemoved(position: Int, callback: ((Int) -> Unit)? = null) {
notifyItemRemoved(position)
callback?.invoke(position)
}
/**
* Replaces [currentList] with [newList] and notifies the underlying adapter of the change.
* @param newList The new list to replace [currentList]
*/
@SuppressLint("NotifyDataSetChanged")
open fun replaceList(newList: List<Model>) {
currentList = newList
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import org.sudachi.sudachi_emu.model.SelectableItem
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
/**
* Generic list class meant to take care of single selection UI updates
* @param currentList The list to show initially
* @param defaultSelection The default selection to use if no list items are selected by
* [SelectableItem.selected] or if the currently selected item is removed from the list
*/
abstract class AbstractSingleSelectionList<
Model : SelectableItem,
Holder : AbstractViewHolder<Model>
>(
final override var currentList: List<Model>,
private val defaultSelection: DefaultSelection = DefaultSelection.Start
) : AbstractListAdapter<Model, Holder>(currentList) {
var selectedItem = getDefaultSelection()
init {
findSelectedItem()
}
/**
* Changes the selection state of the [SelectableItem] that was selected and the previously selected
* item and notifies the underlying adapter of the change for those items. Invokes [callback] last.
* Does nothing if [position] is the same as the currently selected item.
* @param position Index of the item that was selected
* @param callback Lambda that's called at the end of the list changes and has the selected list
* position passed in as a parameter
*/
fun selectItem(position: Int, callback: ((position: Int) -> Unit)? = null) {
if (position == selectedItem) {
return
}
val previouslySelectedItem = selectedItem
selectedItem = position
if (currentList.indices.contains(selectedItem)) {
currentList[selectedItem].onSelectionStateChanged(true)
}
if (currentList.indices.contains(previouslySelectedItem)) {
currentList[previouslySelectedItem].onSelectionStateChanged(false)
}
onItemChanged(previouslySelectedItem)
onItemChanged(selectedItem)
callback?.invoke(position)
}
/**
* Removes a given item from the list and notifies the underlying adapter of the change. If the
* currently selected item was the item that was removed, the item at the position provided
* by [defaultSelection] will be made the new selection. Invokes [callback] last.
* @param position Index of the item that was removed
* @param callback Lambda that's called at the end of the list changes and has the removed and
* selected list positions passed in as parameters
*/
fun removeSelectableItem(
position: Int,
callback: ((removedPosition: Int, selectedPosition: Int) -> Unit)?
) {
removeItem(position)
if (position == selectedItem) {
selectedItem = getDefaultSelection()
currentList[selectedItem].onSelectionStateChanged(true)
onItemChanged(selectedItem)
} else if (position < selectedItem) {
selectedItem--
}
callback?.invoke(position, selectedItem)
}
override fun addItem(item: Model, position: Int, callback: ((Int) -> Unit)?) {
super.addItem(item, position, callback)
if (position <= selectedItem && position != -1) {
selectedItem++
}
}
override fun replaceList(newList: List<Model>) {
super.replaceList(newList)
findSelectedItem()
}
private fun findSelectedItem() {
for (i in currentList.indices) {
if (currentList[i].selected) {
selectedItem = i
break
}
}
}
private fun getDefaultSelection(): Int =
when (defaultSelection) {
DefaultSelection.Start -> currentList.indices.first
DefaultSelection.End -> currentList.indices.last
}
enum class DefaultSelection { Start, End }
}

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import org.sudachi.sudachi_emu.databinding.ListItemAddonBinding
import org.sudachi.sudachi_emu.model.Patch
import org.sudachi.sudachi_emu.model.AddonViewModel
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class AddonAdapter(val addonViewModel: AddonViewModel) :
AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return AddonViewHolder(it) }
}
inner class AddonViewHolder(val binding: ListItemAddonBinding) :
AbstractViewHolder<Patch>(binding) {
override fun bind(model: Patch) {
binding.root.setOnClickListener {
binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked
}
binding.title.text = model.name
binding.version.text = model.version
binding.addonCheckbox.setOnCheckedChangeListener { _, checked ->
model.enabled = checked
}
binding.addonCheckbox.isChecked = model.enabled
binding.buttonDelete.setOnClickListener {
addonViewModel.setAddonToDelete(model)
}
}
}
}

View File

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.FragmentActivity
import androidx.navigation.findNavController
import org.sudachi.sudachi_emu.HomeNavigationDirections
import org.sudachi.sudachi_emu.NativeLibrary
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.databinding.CardSimpleOutlinedBinding
import org.sudachi.sudachi_emu.model.Applet
import org.sudachi.sudachi_emu.model.AppletInfo
import org.sudachi.sudachi_emu.model.Game
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class AppletAdapter(val activity: FragmentActivity, applets: List<Applet>) :
AbstractListAdapter<Applet, AppletAdapter.AppletViewHolder>(applets) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AppletAdapter.AppletViewHolder {
CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return AppletViewHolder(it) }
}
inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
AbstractViewHolder<Applet>(binding) {
override fun bind(model: Applet) {
binding.title.setText(model.titleId)
binding.description.setText(model.descriptionId)
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.context.resources,
model.iconId,
binding.icon.context.theme
)
)
binding.root.setOnClickListener { onClick(model) }
}
fun onClick(applet: Applet) {
val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
if (appletPath.isEmpty()) {
Toast.makeText(
binding.root.context,
R.string.applets_error_applet,
Toast.LENGTH_SHORT
).show()
return
}
if (applet.appletInfo == AppletInfo.Cabinet) {
binding.root.findNavController()
.navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
return
}
NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
val appletGame = Game(
title = SudachiApplication.appContext.getString(applet.titleId),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
binding.root.findNavController().navigate(action)
}
}
}

View File

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import org.sudachi.sudachi_emu.HomeNavigationDirections
import org.sudachi.sudachi_emu.NativeLibrary
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.databinding.DialogListItemBinding
import org.sudachi.sudachi_emu.model.CabinetMode
import org.sudachi.sudachi_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder
import org.sudachi.sudachi_emu.model.AppletInfo
import org.sudachi.sudachi_emu.model.Game
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class CabinetLauncherDialogAdapter(val fragment: Fragment) :
AbstractListAdapter<CabinetMode, CabinetModeViewHolder>(
CabinetMode.values().copyOfRange(1, CabinetMode.entries.size).toList()
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder {
DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return CabinetModeViewHolder(it) }
}
inner class CabinetModeViewHolder(val binding: DialogListItemBinding) :
AbstractViewHolder<CabinetMode>(binding) {
override fun bind(model: CabinetMode) {
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.context.resources,
model.iconId,
binding.icon.context.theme
)
)
binding.title.setText(model.titleId)
binding.root.setOnClickListener { onClick(model) }
}
private fun onClick(mode: CabinetMode) {
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId)
NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId)
NativeLibrary.setCabinetMode(mode.id)
val appletGame = Game(
title = SudachiApplication.appContext.getString(R.string.cabinet_applet),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
fragment.findNavController().navigate(action)
}
}
}

View File

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.databinding.CardDriverOptionBinding
import org.sudachi.sudachi_emu.features.settings.model.StringSetting
import org.sudachi.sudachi_emu.model.Driver
import org.sudachi.sudachi_emu.model.DriverViewModel
import org.sudachi.sudachi_emu.utils.ViewUtils.marquee
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class DriverAdapter(private val driverViewModel: DriverViewModel) :
AbstractSingleSelectionList<Driver, DriverAdapter.DriverViewHolder>(
driverViewModel.driverList.value
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return DriverViewHolder(it) }
}
inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
AbstractViewHolder<Driver>(binding) {
override fun bind(model: Driver) {
binding.apply {
radioButton.isChecked = model.selected
root.setOnClickListener {
selectItem(bindingAdapterPosition) {
driverViewModel.onDriverSelected(it)
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
}
}
buttonDelete.setOnClickListener {
removeSelectableItem(
bindingAdapterPosition
) { removedPosition: Int, selectedPosition: Int ->
driverViewModel.onDriverRemoved(removedPosition, selectedPosition)
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
}
}
// Delay marquee by 3s
title.marquee()
version.marquee()
description.marquee()
title.text = model.title
version.text = model.version
description.text = model.description
buttonDelete.setVisible(
model.title != binding.root.context.getString(R.string.system_gpu_driver)
)
}
}
}
}

View File

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import org.sudachi.sudachi_emu.databinding.CardFolderBinding
import org.sudachi.sudachi_emu.fragments.GameFolderPropertiesDialogFragment
import org.sudachi.sudachi_emu.model.GameDir
import org.sudachi.sudachi_emu.model.GamesViewModel
import org.sudachi.sudachi_emu.utils.ViewUtils.marquee
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
AbstractDiffAdapter<GameDir, FolderAdapter.FolderViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): FolderAdapter.FolderViewHolder {
CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return FolderViewHolder(it) }
}
inner class FolderViewHolder(val binding: CardFolderBinding) :
AbstractViewHolder<GameDir>(binding) {
override fun bind(model: GameDir) {
binding.apply {
path.text = Uri.parse(model.uriString).path
path.marquee()
buttonEdit.setOnClickListener {
GameFolderPropertiesDialogFragment.newInstance(model)
.show(
activity.supportFragmentManager,
GameFolderPropertiesDialogFragment.TAG
)
}
buttonDelete.setOnClickListener {
gamesViewModel.removeFolder(model)
}
}
}
}
}

View File

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.preference.PreferenceManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.sudachi.sudachi_emu.HomeNavigationDirections
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.databinding.CardGameBinding
import org.sudachi.sudachi_emu.model.Game
import org.sudachi.sudachi_emu.model.GamesViewModel
import org.sudachi.sudachi_emu.utils.GameIconUtils
import org.sudachi.sudachi_emu.utils.ViewUtils.marquee
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class GameAdapter(private val activity: AppCompatActivity) :
AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return GameViewHolder(it) }
}
inner class GameViewHolder(val binding: CardGameBinding) :
AbstractViewHolder<Game>(binding) {
override fun bind(model: Game) {
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
GameIconUtils.loadGameIcon(model, binding.imageGameScreen)
binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
binding.textGameTitle.marquee()
binding.cardGame.setOnClickListener { onClick(model) }
binding.cardGame.setOnLongClickListener { onLongClick(model) }
}
fun onClick(game: Game) {
val gameExists = DocumentFile.fromSingleUri(
SudachiApplication.appContext,
Uri.parse(game.path)
)?.exists() == true
if (!gameExists) {
Toast.makeText(
SudachiApplication.appContext,
R.string.loader_error_file_not_found,
Toast.LENGTH_LONG
).show()
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
return
}
val preferences =
PreferenceManager.getDefaultSharedPreferences(SudachiApplication.appContext)
preferences.edit()
.putLong(
game.keyLastPlayedTime,
System.currentTimeMillis()
)
.apply()
activity.lifecycleScope.launch {
withContext(Dispatchers.IO) {
val shortcut =
ShortcutInfoCompat.Builder(SudachiApplication.appContext, game.path)
.setShortLabel(game.title)
.setIcon(GameIconUtils.getShortcutIcon(activity, game))
.setIntent(game.launchIntent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(SudachiApplication.appContext, shortcut)
}
}
val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true)
binding.root.findNavController().navigate(action)
}
fun onLongClick(game: Game): Boolean {
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
binding.root.findNavController().navigate(action)
return true
}
}
}

View File

@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.LifecycleOwner
import org.sudachi.sudachi_emu.databinding.CardInstallableIconBinding
import org.sudachi.sudachi_emu.databinding.CardSimpleOutlinedBinding
import org.sudachi.sudachi_emu.model.GameProperty
import org.sudachi.sudachi_emu.model.InstallableProperty
import org.sudachi.sudachi_emu.model.SubmenuProperty
import org.sudachi.sudachi_emu.utils.ViewUtils.marquee
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
import org.sudachi.sudachi_emu.utils.collect
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class GamePropertiesAdapter(
private val viewLifecycle: LifecycleOwner,
private var properties: List<GameProperty>
) : AbstractListAdapter<GameProperty, AbstractViewHolder<GameProperty>>(properties) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AbstractViewHolder<GameProperty> {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
PropertyType.Submenu.ordinal -> {
SubmenuPropertyViewHolder(
CardSimpleOutlinedBinding.inflate(
inflater,
parent,
false
)
)
}
else -> InstallablePropertyViewHolder(
CardInstallableIconBinding.inflate(
inflater,
parent,
false
)
)
}
}
override fun getItemViewType(position: Int): Int {
return when (properties[position]) {
is SubmenuProperty -> PropertyType.Submenu.ordinal
else -> PropertyType.Installable.ordinal
}
}
inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) :
AbstractViewHolder<GameProperty>(binding) {
override fun bind(model: GameProperty) {
val submenuProperty = model as SubmenuProperty
binding.root.setOnClickListener {
submenuProperty.action.invoke()
}
binding.title.setText(submenuProperty.titleId)
binding.description.setText(submenuProperty.descriptionId)
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.context.resources,
submenuProperty.iconId,
binding.icon.context.theme
)
)
binding.details.marquee()
if (submenuProperty.details != null) {
binding.details.setVisible(true)
binding.details.text = submenuProperty.details.invoke()
} else if (submenuProperty.detailsFlow != null) {
binding.details.setVisible(true)
submenuProperty.detailsFlow.collect(viewLifecycle) { binding.details.text = it }
} else {
binding.details.setVisible(false)
}
}
}
inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) :
AbstractViewHolder<GameProperty>(binding) {
override fun bind(model: GameProperty) {
val installableProperty = model as InstallableProperty
binding.title.setText(installableProperty.titleId)
binding.description.setText(installableProperty.descriptionId)
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.context.resources,
installableProperty.iconId,
binding.icon.context.theme
)
)
binding.buttonInstall.setVisible(installableProperty.install != null)
binding.buttonInstall.setOnClickListener { installableProperty.install?.invoke() }
binding.buttonExport.setVisible(installableProperty.export != null)
binding.buttonExport.setOnClickListener { installableProperty.export?.invoke() }
}
}
enum class PropertyType {
Submenu,
Installable
}
}

View File

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.LifecycleOwner
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.databinding.CardHomeOptionBinding
import org.sudachi.sudachi_emu.fragments.MessageDialogFragment
import org.sudachi.sudachi_emu.model.HomeSetting
import org.sudachi.sudachi_emu.utils.ViewUtils.marquee
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
import org.sudachi.sudachi_emu.utils.collect
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class HomeSettingAdapter(
private val activity: AppCompatActivity,
private val viewLifecycle: LifecycleOwner,
options: List<HomeSetting>
) : AbstractListAdapter<HomeSetting, HomeSettingAdapter.HomeOptionViewHolder>(options) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return HomeOptionViewHolder(it) }
}
inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
AbstractViewHolder<HomeSetting>(binding) {
override fun bind(model: HomeSetting) {
binding.optionTitle.text = activity.resources.getString(model.titleId)
binding.optionDescription.text = activity.resources.getString(model.descriptionId)
binding.optionIcon.setImageDrawable(
ResourcesCompat.getDrawable(
activity.resources,
model.iconId,
activity.theme
)
)
when (model.titleId) {
R.string.get_early_access ->
binding.optionLayout.background =
ContextCompat.getDrawable(
binding.optionCard.context,
R.drawable.premium_background
)
}
if (!model.isEnabled.invoke()) {
binding.optionTitle.alpha = 0.5f
binding.optionDescription.alpha = 0.5f
binding.optionIcon.alpha = 0.5f
}
model.details.collect(viewLifecycle) { updateOptionDetails(it) }
binding.optionDetail.marquee()
binding.root.setOnClickListener { onClick(model) }
}
private fun onClick(model: HomeSetting) {
if (model.isEnabled.invoke()) {
model.onClick.invoke()
} else {
MessageDialogFragment.newInstance(
activity,
titleId = model.disabledTitleId,
descriptionId = model.disabledMessageId
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
}
}
private fun updateOptionDetails(detailString: String) {
if (detailString.isNotEmpty()) {
binding.optionDetail.text = detailString
binding.optionDetail.setVisible(true)
}
}
}
}

View File

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import org.sudachi.sudachi_emu.databinding.CardInstallableBinding
import org.sudachi.sudachi_emu.model.Installable
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class InstallableAdapter(installables: List<Installable>) :
AbstractListAdapter<Installable, InstallableAdapter.InstallableViewHolder>(installables) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): InstallableAdapter.InstallableViewHolder {
CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return InstallableViewHolder(it) }
}
inner class InstallableViewHolder(val binding: CardInstallableBinding) :
AbstractViewHolder<Installable>(binding) {
override fun bind(model: Installable) {
binding.title.setText(model.titleId)
binding.description.setText(model.descriptionId)
binding.buttonInstall.setVisible(model.install != null)
binding.buttonInstall.setOnClickListener { model.install?.invoke() }
binding.buttonExport.setVisible(model.export != null)
binding.buttonExport.setOnClickListener { model.export?.invoke() }
}
}
}

View File

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
import org.sudachi.sudachi_emu.fragments.LicenseBottomSheetDialogFragment
import org.sudachi.sudachi_emu.model.License
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
AbstractListAdapter<License, LicenseAdapter.LicenseViewHolder>(licenses) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return LicenseViewHolder(it) }
}
inner class LicenseViewHolder(val binding: ListItemSettingBinding) :
AbstractViewHolder<License>(binding) {
override fun bind(model: License) {
binding.apply {
textSettingName.text = root.context.getString(model.titleId)
textSettingDescription.text = root.context.getString(model.descriptionId)
textSettingValue.setVisible(false)
root.setOnClickListener { onClick(model) }
}
}
private fun onClick(license: License) {
LicenseBottomSheetDialogFragment.newInstance(license)
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
}
}
}

View File

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.adapters
import android.text.Html
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.button.MaterialButton
import org.sudachi.sudachi_emu.databinding.PageSetupBinding
import org.sudachi.sudachi_emu.model.HomeViewModel
import org.sudachi.sudachi_emu.model.SetupCallback
import org.sudachi.sudachi_emu.model.SetupPage
import org.sudachi.sudachi_emu.model.StepState
import org.sudachi.sudachi_emu.utils.ViewUtils
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return SetupPageViewHolder(it) }
}
inner class SetupPageViewHolder(val binding: PageSetupBinding) :
AbstractViewHolder<SetupPage>(binding), SetupCallback {
override fun bind(model: SetupPage) {
if (model.stepCompleted.invoke() == StepState.COMPLETE) {
binding.buttonAction.setVisible(visible = false, gone = false)
binding.textConfirmation.setVisible(true)
}
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
activity.resources,
model.iconId,
activity.theme
)
)
binding.textTitle.text = activity.resources.getString(model.titleId)
binding.textDescription.text =
Html.fromHtml(activity.resources.getString(model.descriptionId), 0)
binding.buttonAction.apply {
text = activity.resources.getString(model.buttonTextId)
if (model.buttonIconId != 0) {
icon = ResourcesCompat.getDrawable(
activity.resources,
model.buttonIconId,
activity.theme
)
}
iconGravity =
if (model.leftAlignedIcon) {
MaterialButton.ICON_GRAVITY_START
} else {
MaterialButton.ICON_GRAVITY_END
}
setOnClickListener {
model.buttonAction.invoke(this@SetupPageViewHolder)
}
}
}
override fun onStepCompleted() {
ViewUtils.hideView(binding.buttonAction, 200)
ViewUtils.showView(binding.textConfirmation, 200)
ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true)
}
}
}

View File

@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: Copyright 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.applets.keyboard
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsets
import android.view.inputmethod.InputMethodManager
import androidx.annotation.Keep
import androidx.core.view.ViewCompat
import java.io.Serializable
import org.sudachi.sudachi_emu.NativeLibrary
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.applets.keyboard.ui.KeyboardDialogFragment
@Keep
object SoftwareKeyboard {
lateinit var data: KeyboardData
val dataLock = Object()
private fun executeNormalImpl(config: KeyboardConfig) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
data = KeyboardData(SwkbdResult.Cancel.ordinal, "")
val fragment = KeyboardDialogFragment.newInstance(config)
fragment.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG)
}
private fun executeInlineImpl(config: KeyboardConfig) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
val overlayView = emulationActivity!!.findViewById<View>(R.id.surface_input_overlay)
val im =
overlayView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED)
// There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result.
val handler = Handler(Looper.myLooper()!!)
val delayMs = 500
handler.postDelayed(
object : Runnable {
override fun run() {
val insets = ViewCompat.getRootWindowInsets(overlayView)
val isKeyboardVisible = insets!!.isVisible(WindowInsets.Type.ime())
if (isKeyboardVisible) {
handler.postDelayed(this, delayMs.toLong())
return
}
// No longer visible, submit the result.
NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER)
}
},
delayMs.toLong()
)
}
@JvmStatic
fun executeNormal(config: KeyboardConfig): KeyboardData {
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeNormalImpl(config) }
synchronized(dataLock) {
dataLock.wait()
}
return data
}
@JvmStatic
fun executeInline(config: KeyboardConfig) {
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeInlineImpl(config) }
}
// Corresponds to Service::AM::Applets::SwkbdType
enum class SwkbdType {
Normal,
NumberPad,
Qwerty,
Unknown3,
Latin,
SimplifiedChinese,
TraditionalChinese,
Korean
}
// Corresponds to Service::AM::Applets::SwkbdPasswordMode
enum class SwkbdPasswordMode {
Disabled,
Enabled
}
// Corresponds to Service::AM::Applets::SwkbdResult
enum class SwkbdResult {
Ok,
Cancel
}
@Keep
data class KeyboardConfig(
var ok_text: String? = null,
var header_text: String? = null,
var sub_text: String? = null,
var guide_text: String? = null,
var initial_text: String? = null,
var left_optional_symbol_key: Short = 0,
var right_optional_symbol_key: Short = 0,
var max_text_length: Int = 0,
var min_text_length: Int = 0,
var initial_cursor_position: Int = 0,
var type: Int = 0,
var password_mode: Int = 0,
var text_draw_type: Int = 0,
var key_disable_flags: Int = 0,
var use_blur_background: Boolean = false,
var enable_backspace_button: Boolean = false,
var enable_return_button: Boolean = false,
var disable_cancel_button: Boolean = false
) : Serializable
// Corresponds to Frontend::KeyboardData
@Keep
data class KeyboardData(var result: Int, var text: String)
}

View File

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Copyright 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.applets.keyboard.ui
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.InputFilter
import android.text.InputType
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.applets.keyboard.SoftwareKeyboard
import org.sudachi.sudachi_emu.applets.keyboard.SoftwareKeyboard.KeyboardConfig
import org.sudachi.sudachi_emu.databinding.DialogEditTextBinding
import org.sudachi.sudachi_emu.utils.SerializableHelper.serializable
class KeyboardDialogFragment : DialogFragment() {
private lateinit var binding: DialogEditTextBinding
private lateinit var config: KeyboardConfig
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogEditTextBinding.inflate(layoutInflater)
config = requireArguments().serializable(CONFIG)!!
// Set up the input
binding.editText.hint = config.initial_text
binding.editText.isSingleLine = !config.enable_return_button
binding.editText.filters =
arrayOf<InputFilter>(InputFilter.LengthFilter(config.max_text_length))
// Handle input type
var inputType: Int
when (config.type) {
SoftwareKeyboard.SwkbdType.Normal.ordinal,
SoftwareKeyboard.SwkbdType.Qwerty.ordinal,
SoftwareKeyboard.SwkbdType.Unknown3.ordinal,
SoftwareKeyboard.SwkbdType.Latin.ordinal,
SoftwareKeyboard.SwkbdType.SimplifiedChinese.ordinal,
SoftwareKeyboard.SwkbdType.TraditionalChinese.ordinal,
SoftwareKeyboard.SwkbdType.Korean.ordinal -> {
inputType = InputType.TYPE_CLASS_TEXT
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
}
SoftwareKeyboard.SwkbdType.NumberPad.ordinal -> {
inputType = InputType.TYPE_CLASS_NUMBER
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
inputType = inputType or InputType.TYPE_NUMBER_VARIATION_PASSWORD
}
}
else -> {
inputType = InputType.TYPE_CLASS_TEXT
if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) {
inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
}
}
binding.editText.inputType = inputType
val headerText =
config.header_text!!.ifEmpty { resources.getString(R.string.software_keyboard) }
val okText =
config.ok_text!!.ifEmpty { resources.getString(R.string.submit) }
return MaterialAlertDialogBuilder(requireContext())
.setTitle(headerText)
.setView(binding.root)
.setPositiveButton(okText) { _, _ ->
SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Ok.ordinal
SoftwareKeyboard.data.text = binding.editText.text.toString()
}
.setNegativeButton(resources.getString(android.R.string.cancel)) { _, _ ->
SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Cancel.ordinal
}
.create()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
synchronized(SoftwareKeyboard.dataLock) {
SoftwareKeyboard.dataLock.notifyAll()
}
}
companion object {
const val TAG = "KeyboardDialogFragment"
const val CONFIG = "keyboard_config"
fun newInstance(config: KeyboardConfig?): KeyboardDialogFragment {
val frag = KeyboardDialogFragment()
val args = Bundle()
args.putSerializable(CONFIG, config)
frag.arguments = args
return frag
}
}
}

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.disk_shader_cache
import androidx.annotation.Keep
import androidx.lifecycle.ViewModelProvider
import org.sudachi.sudachi_emu.NativeLibrary
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.activities.EmulationActivity
import org.sudachi.sudachi_emu.model.EmulationViewModel
import org.sudachi.sudachi_emu.utils.Log
@Keep
object DiskShaderCacheProgress {
private lateinit var emulationViewModel: EmulationViewModel
private fun prepareViewModel() {
emulationViewModel =
ViewModelProvider(
NativeLibrary.sEmulationActivity.get() as EmulationActivity
)[EmulationViewModel::class.java]
}
@JvmStatic
fun loadProgress(stage: Int, progress: Int, max: Int) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
if (emulationActivity == null) {
Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
return
}
emulationActivity.runOnUiThread {
when (LoadCallbackStage.entries[stage]) {
LoadCallbackStage.Prepare -> prepareViewModel()
LoadCallbackStage.Build -> emulationViewModel.updateProgress(
emulationActivity.getString(R.string.building_shaders),
progress,
max
)
LoadCallbackStage.Complete -> {}
}
}
}
// Equivalent to VideoCore::LoadCallbackStage
enum class LoadCallbackStage {
Prepare, Build, Complete
}
}

View File

@ -0,0 +1,341 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/)
package org.sudachi.sudachi_emu.features
import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap
import java.io.*
import org.sudachi.sudachi_emu.BuildConfig
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.getPublicFilesDir
class DocumentProvider : DocumentsProvider() {
private val baseDirectory: File
get() = File(SudachiApplication.application.getPublicFilesDir().canonicalPath)
companion object {
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
DocumentsContract.Root.COLUMN_ROOT_ID,
DocumentsContract.Root.COLUMN_MIME_TYPES,
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.COLUMN_ICON,
DocumentsContract.Root.COLUMN_TITLE,
DocumentsContract.Root.COLUMN_SUMMARY,
DocumentsContract.Root.COLUMN_DOCUMENT_ID,
DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_FLAGS,
DocumentsContract.Document.COLUMN_SIZE
)
const val AUTHORITY: String = BuildConfig.APPLICATION_ID + ".user"
const val ROOT_ID: String = "root"
}
override fun onCreate(): Boolean {
return true
}
/**
* @return The [File] that corresponds to the document ID supplied by [getDocumentId]
*/
private fun getFile(documentId: String): File {
if (documentId.startsWith(ROOT_ID)) {
val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1))
if (!file.exists()) {
throw FileNotFoundException(
"${file.absolutePath} ($documentId) not found"
)
}
return file
} else {
throw FileNotFoundException("'$documentId' is not in any known root")
}
}
/**
* @return A unique ID for the provided [File]
*/
private fun getDocumentId(file: File): String {
return "$ROOT_ID/${file.toRelativeString(baseDirectory)}"
}
override fun queryRoots(projection: Array<out String>?): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
cursor.newRow().apply {
add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
add(DocumentsContract.Root.COLUMN_SUMMARY, null)
add(
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
)
add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory))
add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*")
add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace)
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_sudachi)
}
return cursor
}
override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
return includeFile(cursor, documentId, null)
}
override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean {
return documentId?.startsWith(parentDocumentId!!) ?: false
}
/**
* @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file
*/
private fun File.resolveWithoutConflict(name: String): File {
var file = resolve(name)
if (file.exists()) {
var noConflictId =
1 // Makes sure two files don't have the same name by adding a number to the end
val extension = name.substringAfterLast('.')
val baseName = name.substringBeforeLast('.')
while (file.exists())
file = resolve("$baseName (${noConflictId++}).$extension")
}
return file
}
override fun createDocument(
parentDocumentId: String?,
mimeType: String?,
displayName: String
): String {
val parentFile = getFile(parentDocumentId!!)
val newFile = parentFile.resolveWithoutConflict(displayName)
try {
if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
if (!newFile.mkdir()) {
throw IOException("Failed to create directory")
}
} else {
if (!newFile.createNewFile()) {
throw IOException("Failed to create file")
}
}
} catch (e: IOException) {
throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}")
}
return getDocumentId(newFile)
}
override fun deleteDocument(documentId: String?) {
val file = getFile(documentId!!)
if (!file.delete()) {
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
}
}
override fun removeDocument(documentId: String, parentDocumentId: String?) {
val parent = getFile(parentDocumentId!!)
val file = getFile(documentId)
if (parent == file || file.parentFile == null || file.parentFile!! == parent) {
if (!file.delete()) {
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
}
} else {
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
}
}
override fun renameDocument(documentId: String?, displayName: String?): String {
if (displayName == null) {
throw FileNotFoundException(
"Couldn't rename document '$documentId' as the new name is null"
)
}
val sourceFile = getFile(documentId!!)
val sourceParentFile = sourceFile.parentFile
?: throw FileNotFoundException(
"Couldn't rename document '$documentId' as it has no parent"
)
val destFile = sourceParentFile.resolve(displayName)
try {
if (!sourceFile.renameTo(destFile)) {
throw FileNotFoundException(
"Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'"
)
}
} catch (e: Exception) {
throw FileNotFoundException(
"Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': " +
"${e.message}"
)
}
return getDocumentId(destFile)
}
private fun copyDocument(
sourceDocumentId: String,
sourceParentDocumentId: String,
targetParentDocumentId: String?
): String {
if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) {
throw FileNotFoundException(
"Couldn't copy document '$sourceDocumentId' as its parent is not " +
"'$sourceParentDocumentId'"
)
}
return copyDocument(sourceDocumentId, targetParentDocumentId)
}
override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String?): String {
val parent = getFile(targetParentDocumentId!!)
val oldFile = getFile(sourceDocumentId)
val newFile = parent.resolveWithoutConflict(oldFile.name)
try {
if (!(
newFile.createNewFile() && newFile.setWritable(true) &&
newFile.setReadable(true)
)
) {
throw IOException("Couldn't create new file")
}
FileInputStream(oldFile).use { inStream ->
FileOutputStream(newFile).use { outStream ->
inStream.copyTo(outStream)
}
}
} catch (e: IOException) {
throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}")
}
return getDocumentId(newFile)
}
override fun moveDocument(
sourceDocumentId: String,
sourceParentDocumentId: String?,
targetParentDocumentId: String?
): String {
try {
val newDocumentId = copyDocument(
sourceDocumentId,
sourceParentDocumentId!!,
targetParentDocumentId
)
removeDocument(sourceDocumentId, sourceParentDocumentId)
return newDocumentId
} catch (e: FileNotFoundException) {
throw FileNotFoundException("Couldn't move document '$sourceDocumentId'")
}
}
private fun includeFile(cursor: MatrixCursor, documentId: String?, file: File?): MatrixCursor {
val localDocumentId = documentId ?: file?.let { getDocumentId(it) }
val localFile = file ?: getFile(documentId!!)
var flags = 0
if (localFile.isDirectory && localFile.canWrite()) {
flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
} else if (localFile.canWrite()) {
flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
}
cursor.newRow().apply {
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId)
add(
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
if (localFile == baseDirectory) {
context!!.getString(R.string.app_name)
} else {
localFile.name
}
)
add(DocumentsContract.Document.COLUMN_SIZE, localFile.length())
add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile))
add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified())
add(DocumentsContract.Document.COLUMN_FLAGS, flags)
if (localFile == baseDirectory) {
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_sudachi)
}
}
return cursor
}
private fun getTypeForFile(file: File): Any {
return if (file.isDirectory) {
DocumentsContract.Document.MIME_TYPE_DIR
} else {
getTypeForName(file.name)
}
}
private fun getTypeForName(name: String): Any {
val lastDot = name.lastIndexOf('.')
if (lastDot >= 0) {
val extension = name.substring(lastDot + 1)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
if (mime != null) {
return mime
}
}
return "application/octect-stream"
}
override fun queryChildDocuments(
parentDocumentId: String?,
projection: Array<out String>?,
sortOrder: String?
): Cursor {
var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
val parent = getFile(parentDocumentId!!)
for (file in parent.listFiles()!!)
cursor = includeFile(cursor, null, file)
return cursor
}
override fun openDocument(
documentId: String?,
mode: String?,
signal: CancellationSignal?
): ParcelFileDescriptor {
val file = documentId?.let { getFile(it) }
val accessMode = ParcelFileDescriptor.parseMode(mode)
return ParcelFileDescriptor.open(file, accessMode)
}
}

View File

@ -0,0 +1,416 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input
import org.sudachi.sudachi_emu.features.input.model.NativeButton
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
import org.sudachi.sudachi_emu.features.input.model.InputType
import org.sudachi.sudachi_emu.features.input.model.ButtonName
import org.sudachi.sudachi_emu.features.input.model.NpadStyleIndex
import org.sudachi.sudachi_emu.utils.NativeConfig
import org.sudachi.sudachi_emu.utils.ParamPackage
import android.view.InputDevice
object NativeInput {
/**
* Default controller id for each device
*/
const val Player1Device = 0
const val Player2Device = 1
const val Player3Device = 2
const val Player4Device = 3
const val Player5Device = 4
const val Player6Device = 5
const val Player7Device = 6
const val Player8Device = 7
const val ConsoleDevice = 8
/**
* Button states
*/
object ButtonState {
const val RELEASED = 0
const val PRESSED = 1
}
/**
* Returns true if pro controller isn't available and handheld is.
* Intended to check where the input overlay should direct its inputs.
*/
external fun isHandheldOnly(): Boolean
/**
* Handles button press events for a gamepad.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param buttonId The Android Keycode corresponding to this event.
* @param action Mask identifying which action is happening (button pressed down, or button released).
*/
external fun onGamePadButtonEvent(
guid: String,
port: Int,
buttonId: Int,
action: Int
)
/**
* Handles axis movement events.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param axis The axis ID.
* @param value Value along the given axis.
*/
external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float)
/**
* Handles motion events.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param deltaTimestamp The finger id corresponding to this event.
* @param xGyro The value of the x-axis for the gyroscope.
* @param yGyro The value of the y-axis for the gyroscope.
* @param zGyro The value of the z-axis for the gyroscope.
* @param xAccel The value of the x-axis for the accelerometer.
* @param yAccel The value of the y-axis for the accelerometer.
* @param zAccel The value of the z-axis for the accelerometer.
*/
external fun onGamePadMotionEvent(
guid: String,
port: Int,
deltaTimestamp: Long,
xGyro: Float,
yGyro: Float,
zGyro: Float,
xAccel: Float,
yAccel: Float,
zAccel: Float
)
/**
* Signals and load a nfc tag
* @param data Byte array containing all the data from a nfc tag.
*/
external fun onReadNfcTag(data: ByteArray?)
/**
* Removes current loaded nfc tag.
*/
external fun onRemoveNfcTag()
/**
* Handles touch press events.
* @param fingerId The finger id corresponding to this event.
* @param xAxis The value of the x-axis on the touchscreen.
* @param yAxis The value of the y-axis on the touchscreen.
*/
external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float)
/**
* Handles touch movement.
* @param fingerId The finger id corresponding to this event.
* @param xAxis The value of the x-axis on the touchscreen.
* @param yAxis The value of the y-axis on the touchscreen.
*/
external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float)
/**
* Handles touch release events.
* @param fingerId The finger id corresponding to this event
*/
external fun onTouchReleased(fingerId: Int)
/**
* Sends a button input to the global virtual controllers.
* @param port Port determined by controller connection order.
* @param button The [NativeButton] corresponding to this event.
* @param action Mask identifying which action is happening (button pressed down, or button released).
*/
fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) =
onOverlayButtonEventImpl(port, button.int, action)
private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int)
/**
* Sends a joystick input to the global virtual controllers.
* @param port Port determined by controller connection order.
* @param stick The [NativeAnalog] corresponding to this event.
* @param xAxis Value along the X axis.
* @param yAxis Value along the Y axis.
*/
fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) =
onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis)
private external fun onOverlayJoystickEventImpl(
port: Int,
stickId: Int,
xAxis: Float,
yAxis: Float
)
/**
* Handles motion events for the global virtual controllers.
* @param port Port determined by controller connection order
* @param deltaTimestamp The finger id corresponding to this event.
* @param xGyro The value of the x-axis for the gyroscope.
* @param yGyro The value of the y-axis for the gyroscope.
* @param zGyro The value of the z-axis for the gyroscope.
* @param xAccel The value of the x-axis for the accelerometer.
* @param yAccel The value of the y-axis for the accelerometer.
* @param zAccel The value of the z-axis for the accelerometer.
*/
external fun onDeviceMotionEvent(
port: Int,
deltaTimestamp: Long,
xGyro: Float,
yGyro: Float,
zGyro: Float,
xAccel: Float,
yAccel: Float,
zAccel: Float
)
/**
* Reloads all input devices from the currently loaded Settings::values.players into HID Core
*/
external fun reloadInputDevices()
/**
* Registers a controller to be used with mapping
* @param device An [InputDevice] or the input overlay wrapped with [SudachiInputDevice]
*/
external fun registerController(device: SudachiInputDevice)
/**
* Gets the names of input devices that have been registered with the input subsystem via [registerController]
*/
external fun getInputDevices(): Array<String>
/**
* Reads all input profiles from disk. Must be called before creating a profile picker.
*/
external fun loadInputProfiles()
/**
* Gets the names of each available input profile.
*/
external fun getInputProfileNames(): Array<String>
/**
* Checks if the user-provided name for an input profile is valid.
* @param name User-provided name for an input profile.
* @return Whether [name] is valid or not.
*/
external fun isProfileNameValid(name: String): Boolean
/**
* Creates a new input profile.
* @param name The new profile's name.
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
* name to this player's config.
* @return Whether creating the profile was successful or not.
*/
external fun createProfile(name: String, playerIndex: Int): Boolean
/**
* Deletes an input profile.
* @param name Name of the profile to delete.
* @param playerIndex Index of the player that's currently being edited. Used to remove the profile
* name from this player's config if they have it loaded.
* @return Whether deleting this profile was successful or not.
*/
external fun deleteProfile(name: String, playerIndex: Int): Boolean
/**
* Loads an input profile.
* @param name Name of the input profile to load.
* @param playerIndex Index of the player that will have this profile loaded.
* @return Whether loading this profile was successful or not.
*/
external fun loadProfile(name: String, playerIndex: Int): Boolean
/**
* Saves an input profile.
* @param name Name of the profile to save.
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
* name to this player's config.
* @return Whether saving the profile was successful or not.
*/
external fun saveProfile(name: String, playerIndex: Int): Boolean
/**
* Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues]
* Must be used while per-game config is loaded.
*/
external fun loadPerGameConfiguration(
playerIndex: Int,
selectedIndex: Int,
selectedProfileName: String
)
/**
* Tells the input subsystem to start listening for inputs to map.
* @param type Type of input to map as shown by the int property in each [InputType].
*/
external fun beginMapping(type: Int)
/**
* Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping.
* Must be run after [beginMapping] and before [stopMapping].
*/
external fun getNextInput(): String
/**
* Tells the input subsystem to stop listening for inputs to map.
*/
external fun stopMapping()
/**
* Updates a controller's mappings with auto-mapping params.
* @param playerIndex Index of the player to auto-map.
* @param deviceParams [ParamPackage] representing the device to auto-map as received
* from [getInputDevices].
* @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams].
* Intended to be a way to provide a default name for a controller if the "display" param is empty.
*/
fun updateMappingsWithDefault(
playerIndex: Int,
deviceParams: ParamPackage,
displayName: String
) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName)
private external fun updateMappingsWithDefaultImpl(
playerIndex: Int,
deviceParams: String,
displayName: String
)
/**
* Gets the params for a specific button.
* @param playerIndex Index of the player to get params from.
* @param button The [NativeButton] to get params for.
* @return A [ParamPackage] representing a player's specific button.
*/
fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage =
ParamPackage(getButtonParamImpl(playerIndex, button.int))
private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String
/**
* Sets the params for a specific button.
* @param playerIndex Index of the player to set params for.
* @param button The [NativeButton] to set params for.
* @param param A [ParamPackage] to set.
*/
fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) =
setButtonParamImpl(playerIndex, button.int, param.serialize())
private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String)
/**
* Gets the params for a specific stick.
* @param playerIndex Index of the player to get params from.
* @param stick The [NativeAnalog] to get params for.
* @return A [ParamPackage] representing a player's specific stick.
*/
fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage =
ParamPackage(getStickParamImpl(playerIndex, stick.int))
private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String
/**
* Sets the params for a specific stick.
* @param playerIndex Index of the player to set params for.
* @param stick The [NativeAnalog] to set params for.
* @param param A [ParamPackage] to set.
*/
fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) =
setStickParamImpl(playerIndex, stick.int, param.serialize())
private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String)
/**
* Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for
* a button/analog/other.
* @param param A [ParamPackage] that represents a specific button's params.
* @return The [ButtonName] for [param].
*/
fun getButtonName(param: ParamPackage): ButtonName =
ButtonName.from(getButtonNameImpl(param.serialize()))
private external fun getButtonNameImpl(param: String): Int
/**
* Gets each supported [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to get supported indexes for.
* @return List of each supported [NpadStyleIndex].
*/
fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> =
getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) }
private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray
/**
* Gets the [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to get an [NpadStyleIndex] from.
* @return The [NpadStyleIndex] for a given player.
*/
fun getStyleIndex(playerIndex: Int): NpadStyleIndex =
NpadStyleIndex.from(getStyleIndexImpl(playerIndex))
private external fun getStyleIndexImpl(playerIndex: Int): Int
/**
* Sets the [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to change.
* @param style The new style to set.
*/
fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) =
setStyleIndexImpl(playerIndex, style.int)
private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int)
/**
* Checks if a device is a controller.
* @param params [ParamPackage] for an input device retrieved from [getInputDevices]
* @return Whether the device is a controller or not.
*/
fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize())
private external fun isControllerImpl(params: String): Boolean
/**
* Checks if a controller is connected
* @param playerIndex Index of the player to check.
* @return Whether the player is connected or not.
*/
external fun getIsConnected(playerIndex: Int): Boolean
/**
* Connects/disconnects a controller and ensures that connection order stays in-tact.
* @param playerIndex Index of the player to connect/disconnect.
* @param connected Whether to connect or disconnect this controller.
*/
fun connectControllers(playerIndex: Int, connected: Boolean = true) {
val connectedControllers = mutableListOf<Boolean>().apply {
if (connected) {
for (i in 0 until 8) {
add(i <= playerIndex)
}
} else {
for (i in 0 until 8) {
add(i < playerIndex)
}
}
}
connectControllersImpl(connectedControllers.toBooleanArray())
}
private external fun connectControllersImpl(connected: BooleanArray)
/**
* Resets all of the button and analog mappings for a player.
* @param playerIndex Index of the player that will have its mappings reset.
*/
external fun resetControllerMappings(playerIndex: Int)
}

View File

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input
import android.view.InputDevice
import androidx.annotation.Keep
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.utils.InputHandler.getGUID
@Keep
interface SudachiInputDevice {
fun getName(): String
fun getGUID(): String
fun getPort(): Int
fun getSupportsVibration(): Boolean
fun vibrate(intensity: Float)
fun getAxes(): Array<Int> = arrayOf()
fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0)
}
class SudachiPhysicalDevice(
private val device: InputDevice,
private val port: Int,
useSystemVibrator: Boolean
) : SudachiInputDevice {
private val vibrator = if (useSystemVibrator) {
SudachiVibrator.getSystemVibrator()
} else {
SudachiVibrator.getControllerVibrator(device)
}
override fun getName(): String {
return device.name
}
override fun getGUID(): String {
return device.getGUID()
}
override fun getPort(): Int {
return port
}
override fun getSupportsVibration(): Boolean {
return vibrator.supportsVibration()
}
override fun vibrate(intensity: Float) {
vibrator.vibrate(intensity)
}
override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray()
override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys)
}
class SudachiInputOverlayDevice(
private val vibration: Boolean,
private val port: Int
) : SudachiInputDevice {
private val vibrator = SudachiVibrator.getSystemVibrator()
override fun getName(): String {
return SudachiApplication.appContext.getString(R.string.input_overlay)
}
override fun getGUID(): String {
return "00000000000000000000000000000000"
}
override fun getPort(): Int {
return port
}
override fun getSupportsVibration(): Boolean {
if (vibration) {
return vibrator.supportsVibration()
}
return false
}
override fun vibrate(intensity: Float) {
if (vibration) {
vibrator.vibrate(intensity)
}
}
}

View File

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input
import android.content.Context
import android.os.Build
import android.os.CombinedVibration
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.InputDevice
import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import org.sudachi.sudachi_emu.SudachiApplication
@Keep
@Suppress("DEPRECATION")
interface SudachiVibrator {
fun supportsVibration(): Boolean
fun vibrate(intensity: Float)
companion object {
fun getControllerVibrator(device: InputDevice): SudachiVibrator =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
SudachiVibratorManager(device.vibratorManager)
} else {
SudachiVibratorManagerCompat(device.vibrator)
}
fun getSystemVibrator(): SudachiVibrator =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = SudachiApplication.appContext
.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
SudachiVibratorManager(vibratorManager)
} else {
val vibrator = SudachiApplication.appContext
.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
SudachiVibratorManagerCompat(vibrator)
}
fun getVibrationEffect(intensity: Float): VibrationEffect? {
if (intensity > 0f) {
return VibrationEffect.createOneShot(
50,
(255.0 * intensity).toInt().coerceIn(1, 255)
)
}
return null
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
class SudachiVibratorManager(private val vibratorManager: VibratorManager) : SudachiVibrator {
override fun supportsVibration(): Boolean {
return vibratorManager.vibratorIds.isNotEmpty()
}
override fun vibrate(intensity: Float) {
val vibration = SudachiVibrator.getVibrationEffect(intensity) ?: return
vibratorManager.vibrate(CombinedVibration.createParallel(vibration))
}
}
class SudachiVibratorManagerCompat(private val vibrator: Vibrator) : SudachiVibrator {
override fun supportsVibration(): Boolean {
return vibrator.hasVibrator()
}
override fun vibrate(intensity: Float) {
val vibration = SudachiVibrator.getVibrationEffect(intensity) ?: return
vibrator.vibrate(vibration)
}
}

View File

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input.model
enum class AnalogDirection(val int: Int, val param: String) {
Up(0, "up"),
Down(1, "down"),
Left(2, "left"),
Right(3, "right")
}

View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input.model
// Loosely matches the enum in common/input.h
enum class ButtonName(val int: Int) {
Invalid(1),
// This will display the engine name instead of the button name
Engine(2),
// This will display the button by value instead of the button name
Value(3);
companion object {
fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid
}
}

View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input.model
// Must match the corresponding enum in input_common/main.h
enum class InputType(val int: Int) {
None(0),
Button(1),
Stick(2),
Motion(3),
Touch(4)
}

View File

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeAnalog(val int: Int) {
LStick(0),
RStick(1);
companion object {
fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick
}
}

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeButton(val int: Int) {
A(0),
B(1),
X(2),
Y(3),
LStick(4),
RStick(5),
L(6),
R(7),
ZL(8),
ZR(9),
Plus(10),
Minus(11),
DLeft(12),
DUp(13),
DRight(14),
DDown(15),
SLLeft(16),
SRLeft(17),
Home(18),
Capture(19),
SLRight(20),
SRRight(21);
companion object {
fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A
}
}

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeTrigger(val int: Int) {
LTrigger(0),
RTrigger(1)
}

View File

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input.model
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.R
// Must match enum in src/core/hid/hid_types.h
enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) {
None(0),
Fullkey(3, R.string.pro_controller),
Handheld(4, R.string.handheld),
HandheldNES(4),
JoyconDual(5, R.string.dual_joycons),
JoyconLeft(6, R.string.left_joycon),
JoyconRight(7, R.string.right_joycon),
GameCube(8, R.string.gamecube_controller),
Pokeball(9),
NES(10),
SNES(12),
N64(13),
SegaGenesis(14),
SystemExt(32),
System(33);
companion object {
fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None
}
}

View File

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.input.model
import androidx.annotation.Keep
@Keep
data class PlayerInput(
var connected: Boolean,
var buttons: Array<String>,
var analogs: Array<String>,
var motions: Array<String>,
var vibrationEnabled: Boolean,
var vibrationStrength: Int,
var bodyColorLeft: Long,
var bodyColorRight: Long,
var buttonColorLeft: Long,
var buttonColorRight: Long,
var profileName: String,
var useSystemVibrator: Boolean
) {
// It's recommended to use the generated equals() and hashCode() methods
// when using arrays in a data class
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PlayerInput
if (connected != other.connected) return false
if (!buttons.contentEquals(other.buttons)) return false
if (!analogs.contentEquals(other.analogs)) return false
if (!motions.contentEquals(other.motions)) return false
if (vibrationEnabled != other.vibrationEnabled) return false
if (vibrationStrength != other.vibrationStrength) return false
if (bodyColorLeft != other.bodyColorLeft) return false
if (bodyColorRight != other.bodyColorRight) return false
if (buttonColorLeft != other.buttonColorLeft) return false
if (buttonColorRight != other.buttonColorRight) return false
if (profileName != other.profileName) return false
return useSystemVibrator == other.useSystemVibrator
}
override fun hashCode(): Int {
var result = connected.hashCode()
result = 31 * result + buttons.contentHashCode()
result = 31 * result + analogs.contentHashCode()
result = 31 * result + motions.contentHashCode()
result = 31 * result + vibrationEnabled.hashCode()
result = 31 * result + vibrationStrength
result = 31 * result + bodyColorLeft.hashCode()
result = 31 * result + bodyColorRight.hashCode()
result = 31 * result + buttonColorLeft.hashCode()
result = 31 * result + buttonColorRight.hashCode()
result = 31 * result + profileName.hashCode()
result = 31 * result + useSystemVibrator.hashCode()
return result
}
fun hasMapping(): Boolean {
var hasMapping = false
buttons.forEach {
if (it != "[empty]" && it.isNotEmpty()) {
hasMapping = true
}
}
analogs.forEach {
if (it != "[empty]" && it.isNotEmpty()) {
hasMapping = true
}
}
motions.forEach {
if (it != "[empty]" && it.isNotEmpty()) {
hasMapping = true
}
}
return hasMapping
}
}

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
interface AbstractBooleanSetting : AbstractSetting {
fun getBoolean(needsGlobal: Boolean = false): Boolean
fun setBoolean(value: Boolean)
}

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
interface AbstractByteSetting : AbstractSetting {
fun getByte(needsGlobal: Boolean = false): Byte
fun setByte(value: Byte)
}

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
interface AbstractFloatSetting : AbstractSetting {
fun getFloat(needsGlobal: Boolean = false): Float
fun setFloat(value: Float)
}

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
interface AbstractIntSetting : AbstractSetting {
fun getInt(needsGlobal: Boolean = false): Int
fun setInt(value: Int)
}

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
interface AbstractLongSetting : AbstractSetting {
fun getLong(needsGlobal: Boolean = false): Long
fun setLong(value: Long)
}

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.utils.NativeConfig
interface AbstractSetting {
val key: String
val defaultValue: Any
val isRuntimeModifiable: Boolean
get() = NativeConfig.getIsRuntimeModifiable(key)
val pairedSettingKey: String
get() = NativeConfig.getPairedSettingKey(key)
val isSwitchable: Boolean
get() = NativeConfig.getIsSwitchable(key)
var global: Boolean
get() = NativeConfig.usingGlobal(key)
set(value) = NativeConfig.setGlobal(key, value)
val isSaveable: Boolean
get() = NativeConfig.getIsSaveable(key)
fun getValueAsString(needsGlobal: Boolean = false): String
fun reset()
}

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
interface AbstractShortSetting : AbstractSetting {
fun getShort(needsGlobal: Boolean = false): Short
fun setShort(value: Short)
}

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
interface AbstractStringSetting : AbstractSetting {
fun getString(needsGlobal: Boolean = false): String
fun setString(value: String)
}

View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.utils.NativeConfig
enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
AUDIO_MUTED("audio_muted"),
CPU_DEBUG_MODE("cpu_debug_mode"),
FASTMEM("cpuopt_fastmem"),
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
USE_DOCKED_MODE("use_docked_mode"),
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"),
RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
RENDERER_DEBUG("debug"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"),
JOYSTICK_REL_CENTER("joystick_rel_center"),
DPAD_SLIDE("dpad_slide"),
HAPTIC_FEEDBACK("haptic_feedback"),
SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"),
SHOW_INPUT_OVERLAY("show_input_overlay"),
TOUCHSCREEN("touchscreen"),
SHOW_THERMAL_OVERLAY("show_thermal_overlay");
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getBoolean(key, needsGlobal)
override fun setBoolean(value: Boolean) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setBoolean(key, value)
}
override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() }
override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString()
override fun reset() = NativeConfig.setBoolean(key, defaultValue)
}

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.utils.NativeConfig
enum class ByteSetting(override val key: String) : AbstractByteSetting {
AUDIO_VOLUME("volume");
override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
override fun setByte(value: Byte) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setByte(key, value)
}
override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() }
override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString()
override fun reset() = NativeConfig.setByte(key, defaultValue)
}

View File

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.utils.NativeConfig
enum class FloatSetting(override val key: String) : AbstractFloatSetting {
// No float settings currently exist
EMPTY_SETTING("");
override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false)
override fun setFloat(value: Float) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setFloat(key, value)
}
override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() }
override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString()
override fun reset() = NativeConfig.setFloat(key, defaultValue)
}

View File

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.utils.NativeConfig
enum class IntSetting(override val key: String) : AbstractIntSetting {
CPU_BACKEND("cpu_backend"),
CPU_ACCURACY("cpu_accuracy"),
REGION_INDEX("region_index"),
LANGUAGE_INDEX("language_index"),
RENDERER_BACKEND("backend"),
RENDERER_ACCURACY("gpu_accuracy"),
RENDERER_RESOLUTION("resolution_setup"),
RENDERER_VSYNC("use_vsync"),
RENDERER_SCALING_FILTER("scaling_filter"),
RENDERER_ANTI_ALIASING("anti_aliasing"),
RENDERER_SCREEN_LAYOUT("screen_layout"),
RENDERER_ASPECT_RATIO("aspect_ratio"),
AUDIO_OUTPUT_ENGINE("output_engine"),
MAX_ANISOTROPY("max_anisotropy"),
THEME("theme"),
THEME_MODE("theme_mode"),
OVERLAY_SCALE("control_scale"),
OVERLAY_OPACITY("control_opacity"),
LOCK_DRAWER("lock_drawer"),
VERTICAL_ALIGNMENT("vertical_alignment"),
FSR_SHARPENING_SLIDER("fsr_sharpening_slider");
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
override fun setInt(value: Int) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setInt(key, value)
}
override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() }
override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString()
override fun reset() = NativeConfig.setInt(key, defaultValue)
}

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.utils.NativeConfig
enum class LongSetting(override val key: String) : AbstractLongSetting {
CUSTOM_RTC("custom_rtc");
override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal)
override fun setLong(value: Long) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setLong(key, value)
}
override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() }
override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString()
override fun reset() = NativeConfig.setLong(key, defaultValue)
}

View File

@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
object Settings {
enum class MenuTag(val titleId: Int = 0) {
SECTION_ROOT(R.string.advanced_settings),
SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics),
SECTION_AUDIO(R.string.preferences_audio),
SECTION_INPUT(R.string.preferences_controls),
SECTION_INPUT_PLAYER_ONE,
SECTION_INPUT_PLAYER_TWO,
SECTION_INPUT_PLAYER_THREE,
SECTION_INPUT_PLAYER_FOUR,
SECTION_INPUT_PLAYER_FIVE,
SECTION_INPUT_PLAYER_SIX,
SECTION_INPUT_PLAYER_SEVEN,
SECTION_INPUT_PLAYER_EIGHT,
SECTION_THEME(R.string.preferences_theme),
SECTION_DEBUG(R.string.preferences_debug);
}
fun getPlayerString(player: Int): String =
SudachiApplication.appContext.getString(R.string.preferences_player, player)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
// Deprecated input overlay preference keys
const val PREF_CONTROL_SCALE = "controlScale"
const val PREF_CONTROL_OPACITY = "controlOpacity"
const val PREF_TOUCH_ENABLED = "isTouchEnabled"
const val PREF_BUTTON_A = "buttonToggle0"
const val PREF_BUTTON_B = "buttonToggle1"
const val PREF_BUTTON_X = "buttonToggle2"
const val PREF_BUTTON_Y = "buttonToggle3"
const val PREF_BUTTON_L = "buttonToggle4"
const val PREF_BUTTON_R = "buttonToggle5"
const val PREF_BUTTON_ZL = "buttonToggle6"
const val PREF_BUTTON_ZR = "buttonToggle7"
const val PREF_BUTTON_PLUS = "buttonToggle8"
const val PREF_BUTTON_MINUS = "buttonToggle9"
const val PREF_BUTTON_DPAD = "buttonToggle10"
const val PREF_STICK_L = "buttonToggle11"
const val PREF_STICK_R = "buttonToggle12"
const val PREF_BUTTON_STICK_L = "buttonToggle13"
const val PREF_BUTTON_STICK_R = "buttonToggle14"
const val PREF_BUTTON_HOME = "buttonToggle15"
const val PREF_BUTTON_SCREENSHOT = "buttonToggle16"
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
val overlayPreferences = listOf(
PREF_BUTTON_A,
PREF_BUTTON_B,
PREF_BUTTON_X,
PREF_BUTTON_Y,
PREF_BUTTON_L,
PREF_BUTTON_R,
PREF_BUTTON_ZL,
PREF_BUTTON_ZR,
PREF_BUTTON_PLUS,
PREF_BUTTON_MINUS,
PREF_BUTTON_DPAD,
PREF_STICK_L,
PREF_STICK_R,
PREF_BUTTON_HOME,
PREF_BUTTON_SCREENSHOT,
PREF_BUTTON_STICK_L,
PREF_BUTTON_STICK_R
)
// Deprecated layout preference keys
const val PREF_LANDSCAPE_SUFFIX = "_Landscape"
const val PREF_PORTRAIT_SUFFIX = "_Portrait"
const val PREF_FOLDABLE_SUFFIX = "_Foldable"
val overlayLayoutSuffixes = listOf(
PREF_LANDSCAPE_SUFFIX,
PREF_PORTRAIT_SUFFIX,
PREF_FOLDABLE_SUFFIX
)
// Deprecated theme preference keys
const val PREF_THEME = "Theme"
const val PREF_THEME_MODE = "ThemeMode"
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
enum class EmulationOrientation(val int: Int) {
Unspecified(0),
SensorLandscape(5),
Landscape(1),
ReverseLandscape(2),
SensorPortrait(6),
Portrait(4),
ReversePortrait(3);
companion object {
fun from(int: Int): EmulationOrientation =
entries.firstOrNull { it.int == int } ?: Unspecified
}
}
enum class EmulationVerticalAlignment(val int: Int) {
Top(1),
Center(0),
Bottom(2);
companion object {
fun from(int: Int): EmulationVerticalAlignment =
entries.firstOrNull { it.int == int } ?: Center
}
}
}

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.utils.NativeConfig
enum class ShortSetting(override val key: String) : AbstractShortSetting {
RENDERER_SPEED_LIMIT("speed_limit");
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
override fun setShort(value: Short) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setShort(key, value)
}
override val defaultValue: Short by lazy { NativeConfig.getDefaultToString(key).toShort() }
override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
override fun reset() = NativeConfig.setShort(key, defaultValue)
}

View File

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model
import org.sudachi.sudachi_emu.utils.NativeConfig
enum class StringSetting(override val key: String) : AbstractStringSetting {
DRIVER_PATH("driver_path"),
DEVICE_NAME("device_name");
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
override fun setString(value: String) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setString(key, value)
}
override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) }
override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal)
override fun reset() = NativeConfig.setString(key, defaultValue)
}

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.AnalogDirection
import org.sudachi.sudachi_emu.features.input.model.InputType
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
import org.sudachi.sudachi_emu.utils.ParamPackage
class AnalogInputSetting(
override val playerIndex: Int,
val nativeAnalog: NativeAnalog,
val analogDirection: AnalogDirection,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val type = TYPE_INPUT
override val inputType = InputType.Stick
override fun getSelectedValue(): String {
val params = NativeInput.getStickParam(playerIndex, nativeAnalog)
val analog = analogToText(params, analogDirection.param)
return getDisplayString(params, analog)
}
override fun setSelectedValue(param: ParamPackage) =
NativeInput.setStickParam(playerIndex, nativeAnalog, param)
}

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.utils.ParamPackage
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.InputType
import org.sudachi.sudachi_emu.features.input.model.NativeButton
class ButtonInputSetting(
override val playerIndex: Int,
val nativeButton: NativeButton,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val type = TYPE_INPUT
override val inputType = InputType.Button
override fun getSelectedValue(): String {
val params = NativeInput.getButtonParam(playerIndex, nativeButton)
val button = buttonToText(params)
return getDisplayString(params, button)
}
override fun setSelectedValue(param: ParamPackage) =
NativeInput.setButtonParam(playerIndex, nativeButton, param)
}

View File

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.settings.model.AbstractLongSetting
class DateTimeSetting(
private val longSetting: AbstractLongSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = ""
) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_DATETIME_SETTING
fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value)
}

View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
class HeaderSetting(
@StringRes titleId: Int = 0,
titleString: String = ""
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
override val type = TYPE_HEADER
}

View File

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.utils.NativeConfig
class InputProfileSetting(private val playerIndex: Int) :
SettingsItem(emptySetting, R.string.profile, "", 0, "") {
override val type = TYPE_INPUT_PROFILE
fun getCurrentProfile(): String =
NativeConfig.getInputSettings(true)[playerIndex].profileName
fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames()
fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name)
fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex)
fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex)
fun loadProfile(name: String): Boolean {
val result = NativeInput.loadProfile(name, playerIndex)
NativeInput.reloadInputDevices()
return result
}
fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex)
}

View File

@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.ButtonName
import org.sudachi.sudachi_emu.features.input.model.InputType
import org.sudachi.sudachi_emu.utils.ParamPackage
sealed class InputSetting(
@StringRes titleId: Int,
titleString: String
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
override val type = TYPE_INPUT
abstract val inputType: InputType
abstract val playerIndex: Int
protected val context get() = SudachiApplication.appContext
abstract fun getSelectedValue(): String
abstract fun setSelectedValue(param: ParamPackage)
protected fun getDisplayString(params: ParamPackage, control: String): String {
val deviceName = params.get("display", "")
deviceName.ifEmpty {
return context.getString(R.string.not_set)
}
return "$deviceName: $control"
}
private fun getDirectionName(direction: String): String =
when (direction) {
"up" -> context.getString(R.string.up)
"down" -> context.getString(R.string.down)
"left" -> context.getString(R.string.left)
"right" -> context.getString(R.string.right)
else -> direction
}
protected fun buttonToText(param: ParamPackage): String {
if (!param.has("engine")) {
return context.getString(R.string.not_set)
}
val toggle = if (param.get("toggle", false)) "~" else ""
val inverted = if (param.get("inverted", false)) "!" else ""
val invert = if (param.get("invert", "+") == "-") "-" else ""
val turbo = if (param.get("turbo", false)) "$" else ""
val commonButtonName = NativeInput.getButtonName(param)
if (commonButtonName == ButtonName.Invalid) {
return context.getString(R.string.invalid)
}
if (commonButtonName == ButtonName.Engine) {
return param.get("engine", "")
}
if (commonButtonName == ButtonName.Value) {
if (param.has("hat")) {
val hat = getDirectionName(param.get("direction", ""))
return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat)
}
if (param.has("axis")) {
val axis = param.get("axis", "")
return context.getString(
R.string.qualified_button_stick_axis,
toggle,
inverted,
invert,
axis
)
}
if (param.has("button")) {
val button = param.get("button", "")
return context.getString(R.string.qualified_button, turbo, toggle, inverted, button)
}
}
return context.getString(R.string.unknown)
}
protected fun analogToText(param: ParamPackage, direction: String): String {
if (!param.has("engine")) {
return context.getString(R.string.not_set)
}
if (param.get("engine", "") == "analog_from_button") {
return buttonToText(ParamPackage(param.get(direction, "")))
}
if (!param.has("axis_x") || !param.has("axis_y")) {
return context.getString(R.string.unknown)
}
val xAxis = param.get("axis_x", "")
val yAxis = param.get("axis_y", "")
val xInvert = param.get("invert_x", "+") == "-"
val yInvert = param.get("invert_y", "+") == "-"
if (direction == "modifier") {
return context.getString(R.string.unused)
}
when (direction) {
"up" -> {
val yInvertString = if (yInvert) "+" else "-"
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
}
"down" -> {
val yInvertString = if (yInvert) "-" else "+"
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
}
"left" -> {
val xInvertString = if (xInvert) "+" else "-"
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
}
"right" -> {
val xInvertString = if (xInvert) "-" else "+"
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
}
}
return context.getString(R.string.unknown)
}
}

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
class IntSingleChoiceSetting(
private val intSetting: AbstractIntSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val choices: Array<String>,
val values: Array<Int>
) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_INT_SINGLE_CHOICE
fun getValueAt(index: Int): Int =
if (values.indices.contains(index)) values[index] else -1
fun getChoiceAt(index: Int): String =
if (choices.indices.contains(index)) choices[index] else ""
fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal)
fun setSelectedValue(value: Int) = intSetting.setInt(value)
val selectedValueIndex: Int
get() {
for (i in values.indices) {
if (values[i] == getSelectedValue()) {
return i
}
}
return -1
}
}

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.InputType
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
import org.sudachi.sudachi_emu.utils.ParamPackage
class ModifierInputSetting(
override val playerIndex: Int,
val nativeAnalog: NativeAnalog,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val inputType = InputType.Button
override fun getSelectedValue(): String {
val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
val modifierParam = ParamPackage(analogParam.get("modifier", ""))
return buttonToText(modifierParam)
}
override fun setSelectedValue(param: ParamPackage) {
val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
newParam.set("modifier", param.serialize())
NativeInput.setStickParam(playerIndex, nativeAnalog, newParam)
}
}

View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
class RunnableSetting(
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val isRunnable: Boolean,
@DrawableRes val iconId: Int = 0,
val runnable: () -> Unit
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_RUNNABLE
}

View File

@ -0,0 +1,391 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.NativeLibrary
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.NpadStyleIndex
import org.sudachi.sudachi_emu.features.settings.model.AbstractBooleanSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractSetting
import org.sudachi.sudachi_emu.features.settings.model.BooleanSetting
import org.sudachi.sudachi_emu.features.settings.model.ByteSetting
import org.sudachi.sudachi_emu.features.settings.model.IntSetting
import org.sudachi.sudachi_emu.features.settings.model.LongSetting
import org.sudachi.sudachi_emu.features.settings.model.ShortSetting
import org.sudachi.sudachi_emu.features.settings.model.StringSetting
import org.sudachi.sudachi_emu.utils.NativeConfig
/**
* 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(
val setting: AbstractSetting,
@StringRes val titleId: Int,
val titleString: String,
@StringRes val descriptionId: Int,
val descriptionString: String
) {
abstract val type: Int
val title: String by lazy {
if (titleId != 0) {
return@lazy SudachiApplication.appContext.getString(titleId)
}
return@lazy titleString
}
val description: String by lazy {
if (descriptionId != 0) {
return@lazy SudachiApplication.appContext.getString(descriptionId)
}
return@lazy descriptionString
}
val isEditable: Boolean
get() {
// Can't change docked mode toggle when using handheld mode
if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) {
return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld
}
// Can't edit settings that aren't saveable in per-game config even if they are switchable
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false
}
if (!NativeLibrary.isRunning()) return true
// Prevent editing settings that were modified in per-game config while editing global
// config
if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) {
return false
}
return setting.isRuntimeModifiable
}
val needsRuntimeGlobal: Boolean
get() = NativeLibrary.isRunning() && !setting.global &&
!NativeConfig.isPerGameConfigLoaded()
val clearable: Boolean
get() = !setting.global && NativeConfig.isPerGameConfigLoaded()
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 = 8
const val TYPE_INT_SINGLE_CHOICE = 9
const val TYPE_INPUT_PROFILE = 10
const val TYPE_STRING_INPUT = 11
const val FASTMEM_COMBINED = "fastmem_combined"
val emptySetting = object : AbstractSetting {
override val key: String = ""
override val defaultValue: Any = false
override val isSaveable = true
override fun getValueAsString(needsGlobal: Boolean): String = ""
override fun reset() {}
}
// Extension for putting SettingsItems into a hashmap without repeating yourself
fun HashMap<String, SettingsItem>.put(item: SettingsItem) {
put(item.setting.key, item)
}
// List of all general
val settingsItems = HashMap<String, SettingsItem>().apply {
put(StringInputSetting(StringSetting.DEVICE_NAME, titleId = R.string.device_name))
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
titleId = R.string.frame_limit_enable,
descriptionId = R.string.frame_limit_enable_description
)
)
put(
SliderSetting(
ShortSetting.RENDERER_SPEED_LIMIT,
titleId = R.string.frame_limit_slider,
descriptionId = R.string.frame_limit_slider_description,
min = 1,
max = 400,
units = "%"
)
)
put(
SingleChoiceSetting(
IntSetting.CPU_BACKEND,
titleId = R.string.cpu_backend,
choicesId = R.array.cpuBackendArm64Names,
valuesId = R.array.cpuBackendArm64Values
)
)
put(
SingleChoiceSetting(
IntSetting.CPU_ACCURACY,
titleId = R.string.cpu_accuracy,
choicesId = R.array.cpuAccuracyNames,
valuesId = R.array.cpuAccuracyValues
)
)
put(
SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE,
titleId = R.string.picture_in_picture,
descriptionId = R.string.picture_in_picture_description
)
)
val dockedModeSetting = object : AbstractBooleanSetting {
override val key = BooleanSetting.USE_DOCKED_MODE.key
override fun getBoolean(needsGlobal: Boolean): Boolean {
if (NativeInput.getStyleIndex(0) == NpadStyleIndex.Handheld) {
return false
}
return BooleanSetting.USE_DOCKED_MODE.getBoolean(needsGlobal)
}
override fun setBoolean(value: Boolean) =
BooleanSetting.USE_DOCKED_MODE.setBoolean(value)
override val defaultValue = BooleanSetting.USE_DOCKED_MODE.defaultValue
override fun getValueAsString(needsGlobal: Boolean): String =
BooleanSetting.USE_DOCKED_MODE.getValueAsString(needsGlobal)
override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset()
}
put(
SwitchSetting(
dockedModeSetting,
titleId = R.string.use_docked_mode,
descriptionId = R.string.use_docked_mode_description
)
)
put(
SingleChoiceSetting(
IntSetting.REGION_INDEX,
titleId = R.string.emulated_region,
choicesId = R.array.regionNames,
valuesId = R.array.regionValues
)
)
put(
SingleChoiceSetting(
IntSetting.LANGUAGE_INDEX,
titleId = R.string.emulated_language,
choicesId = R.array.languageNames,
valuesId = R.array.languageValues
)
)
put(
SwitchSetting(
BooleanSetting.USE_CUSTOM_RTC,
titleId = R.string.use_custom_rtc,
descriptionId = R.string.use_custom_rtc_description
)
)
put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc))
put(
SingleChoiceSetting(
IntSetting.RENDERER_ACCURACY,
titleId = R.string.renderer_accuracy,
choicesId = R.array.rendererAccuracyNames,
valuesId = R.array.rendererAccuracyValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_RESOLUTION,
titleId = R.string.renderer_resolution,
choicesId = R.array.rendererResolutionNames,
valuesId = R.array.rendererResolutionValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_VSYNC,
titleId = R.string.renderer_vsync,
choicesId = R.array.rendererVSyncNames,
valuesId = R.array.rendererVSyncValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCALING_FILTER,
titleId = R.string.renderer_scaling_filter,
choicesId = R.array.rendererScalingFilterNames,
valuesId = R.array.rendererScalingFilterValues
)
)
put(
SliderSetting(
IntSetting.FSR_SHARPENING_SLIDER,
titleId = R.string.fsr_sharpness,
descriptionId = R.string.fsr_sharpness_description,
units = "%"
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING,
titleId = R.string.renderer_anti_aliasing,
choicesId = R.array.rendererAntiAliasingNames,
valuesId = R.array.rendererAntiAliasingValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
titleId = R.string.renderer_screen_layout,
choicesId = R.array.rendererScreenLayoutNames,
valuesId = R.array.rendererScreenLayoutValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO,
titleId = R.string.renderer_aspect_ratio,
choicesId = R.array.rendererAspectRatioNames,
valuesId = R.array.rendererAspectRatioValues
)
)
put(
SingleChoiceSetting(
IntSetting.VERTICAL_ALIGNMENT,
titleId = R.string.vertical_alignment,
descriptionId = 0,
choicesId = R.array.verticalAlignmentEntries,
valuesId = R.array.verticalAlignmentValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
titleId = R.string.use_disk_shader_cache,
descriptionId = R.string.use_disk_shader_cache_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
titleId = R.string.renderer_force_max_clock,
descriptionId = R.string.renderer_force_max_clock_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
titleId = R.string.renderer_asynchronous_shaders,
descriptionId = R.string.renderer_asynchronous_shaders_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_REACTIVE_FLUSHING,
titleId = R.string.renderer_reactive_flushing,
descriptionId = R.string.renderer_reactive_flushing_description
)
)
put(
SingleChoiceSetting(
IntSetting.MAX_ANISOTROPY,
titleId = R.string.anisotropic_filtering,
descriptionId = R.string.anisotropic_filtering_description,
choicesId = R.array.anisoEntries,
valuesId = R.array.anisoValues
)
)
put(
SingleChoiceSetting(
IntSetting.AUDIO_OUTPUT_ENGINE,
titleId = R.string.audio_output_engine,
choicesId = R.array.outputEngineEntries,
valuesId = R.array.outputEngineValues
)
)
put(
SliderSetting(
ByteSetting.AUDIO_VOLUME,
titleId = R.string.audio_volume,
descriptionId = R.string.audio_volume_description,
units = "%"
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_BACKEND,
titleId = R.string.renderer_api,
choicesId = R.array.rendererApiNames,
valuesId = R.array.rendererApiValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_DEBUG,
titleId = R.string.renderer_debug,
descriptionId = R.string.renderer_debug_description
)
)
put(
SwitchSetting(
BooleanSetting.CPU_DEBUG_MODE,
titleId = R.string.cpu_debug_mode,
descriptionId = R.string.cpu_debug_mode_description
)
)
val fastmem = object : AbstractBooleanSetting {
override fun getBoolean(needsGlobal: Boolean): Boolean =
BooleanSetting.FASTMEM.getBoolean() &&
BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean()
override fun setBoolean(value: Boolean) {
BooleanSetting.FASTMEM.setBoolean(value)
BooleanSetting.FASTMEM_EXCLUSIVES.setBoolean(value)
}
override val key: String = FASTMEM_COMBINED
override val isRuntimeModifiable: Boolean = false
override val pairedSettingKey = BooleanSetting.CPU_DEBUG_MODE.key
override val defaultValue: Boolean = true
override val isSwitchable: Boolean = true
override var global: Boolean
get() {
return BooleanSetting.FASTMEM.global &&
BooleanSetting.FASTMEM_EXCLUSIVES.global
}
set(value) {
BooleanSetting.FASTMEM.global = value
BooleanSetting.FASTMEM_EXCLUSIVES.global = value
}
override val isSaveable = true
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean().toString()
override fun reset() = setBoolean(defaultValue)
}
put(SwitchSetting(fastmem, R.string.fastmem))
}
}
}

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.ArrayRes
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractSetting
class SingleChoiceSetting(
setting: AbstractSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
@ArrayRes val choicesId: Int,
@ArrayRes val valuesId: Int
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SINGLE_CHOICE
fun getSelectedValue(needsGlobal: Boolean = false) =
when (setting) {
is AbstractIntSetting -> setting.getInt(needsGlobal)
else -> -1
}
fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value)
}

View File

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.settings.model.AbstractByteSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractFloatSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractShortSetting
import kotlin.math.roundToInt
class SliderSetting(
setting: AbstractSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val min: Int = 0,
val max: Int = 100,
val units: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SLIDER
fun getSelectedValue(needsGlobal: Boolean = false) =
when (setting) {
is AbstractByteSetting -> setting.getByte(needsGlobal).toInt()
is AbstractShortSetting -> setting.getShort(needsGlobal).toInt()
is AbstractIntSetting -> setting.getInt(needsGlobal)
is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt()
else -> -1
}
fun setSelectedValue(value: Int) =
when (setting) {
is AbstractByteSetting -> setting.setByte(value.toByte())
is AbstractShortSetting -> setting.setShort(value.toShort())
is AbstractFloatSetting -> setting.setFloat(value.toFloat())
else -> (setting as AbstractIntSetting).setInt(value)
}
}

View File

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.settings.model.AbstractStringSetting
class StringInputSetting(
setting: AbstractStringSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_STRING_INPUT
fun getSelectedValue(needsGlobal: Boolean = false) = setting.getValueAsString(needsGlobal)
fun setSelectedValue(selection: String) =
(setting as AbstractStringSetting).setString(selection)
}

View File

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.settings.model.AbstractStringSetting
class StringSingleChoiceSetting(
private val stringSetting: AbstractStringSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val choices: Array<String>,
val values: Array<String>
) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_STRING_SINGLE_CHOICE
fun getValueAt(index: Int): String =
if (index >= 0 && index < values.size) values[index] else ""
fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
fun setSelectedValue(value: String) = stringSetting.setString(value)
val selectedValueIndex: Int
get() {
for (i in values.indices) {
if (values[i] == getSelectedValue()) {
return i
}
}
return -1
}
}

View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.settings.model.Settings
class SubmenuSetting(
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
@DrawableRes val iconId: Int = 0,
val menuKey: Settings.MenuTag
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SUBMENU
}

View File

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.sudachi.sudachi_emu.features.settings.model.AbstractBooleanSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractSetting
class SwitchSetting(
setting: AbstractSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SWITCH
fun getIsChecked(needsGlobal: Boolean = false): Boolean {
return when (setting) {
is AbstractIntSetting -> setting.getInt(needsGlobal) == 1
is AbstractBooleanSetting -> setting.getBoolean(needsGlobal)
else -> false
}
}
fun setChecked(value: Boolean) {
when (setting) {
is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
is AbstractBooleanSetting -> setting.setBoolean(value)
}
}
}

View File

@ -0,0 +1,300 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.app.Dialog
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.databinding.DialogMappingBinding
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
import org.sudachi.sudachi_emu.features.input.model.NativeButton
import org.sudachi.sudachi_emu.features.settings.model.view.AnalogInputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.ButtonInputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.InputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.ModifierInputSetting
import org.sudachi.sudachi_emu.utils.InputHandler
import org.sudachi.sudachi_emu.utils.ParamPackage
class InputDialogFragment : DialogFragment() {
private var inputAccepted = false
private var position: Int = 0
private lateinit var inputSetting: InputSetting
private lateinit var binding: DialogMappingBinding
private val settingsViewModel: SettingsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (settingsViewModel.clickedItem == null) dismiss()
position = requireArguments().getInt(POSITION)
InputHandler.updateControllerData()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inputSetting = settingsViewModel.clickedItem as InputSetting
binding = DialogMappingBinding.inflate(layoutInflater)
val builder = MaterialAlertDialogBuilder(requireContext())
.setPositiveButton(android.R.string.cancel) { _, _ ->
NativeInput.stopMapping()
dismiss()
}
.setView(binding.root)
val playButtonMapAnimation = { twoDirections: Boolean ->
val stickAnimation: AnimatedVectorDrawable
val buttonAnimation: AnimatedVectorDrawable
binding.imageStickAnimation.apply {
val anim = if (twoDirections) {
R.drawable.stick_two_direction_anim
} else {
R.drawable.stick_one_direction_anim
}
setBackgroundResource(anim)
stickAnimation = background as AnimatedVectorDrawable
}
binding.imageButtonAnimation.apply {
setBackgroundResource(R.drawable.button_anim)
buttonAnimation = background as AnimatedVectorDrawable
}
stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
buttonAnimation.start()
}
})
buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
stickAnimation.start()
}
})
stickAnimation.start()
}
when (val setting = inputSetting) {
is AnalogInputSetting -> {
when (setting.nativeAnalog) {
NativeAnalog.LStick -> builder.setTitle(
getString(R.string.map_control, getString(R.string.left_stick))
)
NativeAnalog.RStick -> builder.setTitle(
getString(R.string.map_control, getString(R.string.right_stick))
)
}
builder.setMessage(R.string.stick_map_description)
playButtonMapAnimation.invoke(true)
}
is ModifierInputSetting -> {
builder.setTitle(getString(R.string.map_control, setting.title))
.setMessage(R.string.button_map_description)
playButtonMapAnimation.invoke(false)
}
is ButtonInputSetting -> {
if (setting.nativeButton == NativeButton.DUp ||
setting.nativeButton == NativeButton.DDown ||
setting.nativeButton == NativeButton.DLeft ||
setting.nativeButton == NativeButton.DRight
) {
builder.setTitle(getString(R.string.map_dpad_direction, setting.title))
} else {
builder.setTitle(getString(R.string.map_control, setting.title))
}
builder.setMessage(R.string.button_map_description)
playButtonMapAnimation.invoke(false)
}
}
return builder.create()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.requestFocus()
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) }
binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) }
NativeInput.beginMapping(inputSetting.inputType.int)
}
private fun onKeyEvent(event: KeyEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
) {
return false
}
val action = when (event.action) {
KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
else -> return false
}
val controllerData =
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
NativeInput.onGamePadButtonEvent(
controllerData.getGUID(),
controllerData.getPort(),
event.keyCode,
action
)
onInputReceived(event.device)
return true
}
private fun onMotionEvent(event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
) {
return false
}
// Temp workaround for DPads that give both axis and button input. The input system can't
// take in a specific axis direction for a binding so you lose half of the directions for a DPad.
val controllerData =
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
event.device.motionRanges.forEach {
NativeInput.onGamePadAxisEvent(
controllerData.getGUID(),
controllerData.getPort(),
it.axis,
event.getAxisValue(it.axis)
)
onInputReceived(event.device)
}
return true
}
private fun onInputReceived(device: InputDevice) {
val params = ParamPackage(NativeInput.getNextInput())
if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) {
inputAccepted = true
setResult(params, device)
}
}
private fun setResult(params: ParamPackage, device: InputDevice) {
NativeInput.stopMapping()
params.set("display", "${device.name} ${params.get("port", 0)}")
when (val item = settingsViewModel.clickedItem as InputSetting) {
is ModifierInputSetting,
is ButtonInputSetting -> {
// Invert DPad up and left bindings by default
val tempSetting = inputSetting as? ButtonInputSetting
if (tempSetting != null) {
if (tempSetting.nativeButton == NativeButton.DUp ||
tempSetting.nativeButton == NativeButton.DLeft &&
params.has("axis")
) {
params.set("invert", "-")
}
}
item.setSelectedValue(params)
settingsViewModel.setAdapterItemChanged(position)
}
is AnalogInputSetting -> {
var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param)
// Invert Y-Axis by default
analogParam.set("invert_y", "-")
item.setSelectedValue(analogParam)
settingsViewModel.setReloadListAndNotifyDataset(true)
}
}
dismiss()
}
private fun adjustAnalogParam(
inputParam: ParamPackage,
analogParam: ParamPackage,
buttonName: String
): ParamPackage {
// The poller returned a complete axis, so set all the buttons
if (inputParam.has("axis_x") && inputParam.has("axis_y")) {
return inputParam
}
// Check if the current configuration has either no engine or an axis binding.
// Clears out the old binding and adds one with analog_from_button.
if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) {
analogParam.clear()
analogParam.set("engine", "analog_from_button")
}
analogParam.set(buttonName, inputParam.serialize())
return analogParam
}
private fun isInputAcceptable(params: ParamPackage): Boolean {
if (InputHandler.registeredControllers.size == 1) {
return true
}
if (params.has("motion")) {
return true
}
val currentDevice = settingsViewModel.getCurrentDeviceParams(params)
if (currentDevice.get("engine", "any") == "any") {
return true
}
val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") ||
params.get("guid", "") == currentDevice.get("guid2", "")
return params.get("engine", "") == currentDevice.get("engine", "") &&
guidMatch &&
params.get("port", 0) == currentDevice.get("port", 0)
}
companion object {
const val TAG = "InputDialogFragment"
const val POSITION = "Position"
fun newInstance(
inputMappingViewModel: SettingsViewModel,
setting: InputSetting,
position: Int
): InputDialogFragment {
inputMappingViewModel.clickedItem = setting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = InputDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.adapters.AbstractListAdapter
import org.sudachi.sudachi_emu.databinding.ListItemInputProfileBinding
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
import org.sudachi.sudachi_emu.R
class InputProfileAdapter(options: List<ProfileItem>) :
AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AbstractViewHolder<ProfileItem> {
ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return InputProfileViewHolder(it) }
}
inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) :
AbstractViewHolder<ProfileItem>(binding) {
override fun bind(model: ProfileItem) {
when (model) {
is ExistingProfileItem -> {
binding.title.text = model.name
binding.buttonNew.visibility = View.GONE
binding.buttonDelete.visibility = View.VISIBLE
binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() }
binding.buttonSave.visibility = View.VISIBLE
binding.buttonSave.setOnClickListener { model.saveProfile.invoke() }
binding.buttonLoad.visibility = View.VISIBLE
binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() }
}
is NewProfileItem -> {
binding.title.text = model.name
binding.buttonNew.visibility = View.VISIBLE
binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() }
binding.buttonSave.visibility = View.GONE
binding.buttonDelete.visibility = View.GONE
binding.buttonLoad.visibility = View.GONE
}
}
}
}
}
sealed interface ProfileItem {
val name: String
}
data class NewProfileItem(
val createNewProfile: () -> Unit
) : ProfileItem {
override val name: String = SudachiApplication.appContext.getString(R.string.create_new_profile)
}
data class ExistingProfileItem(
override val name: String,
val deleteProfile: () -> Unit,
val saveProfile: () -> Unit,
val loadProfile: () -> Unit
) : ProfileItem

View File

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.databinding.DialogInputProfilesBinding
import org.sudachi.sudachi_emu.features.settings.model.view.InputProfileSetting
import org.sudachi.sudachi_emu.fragments.MessageDialogFragment
import org.sudachi.sudachi_emu.utils.collect
class InputProfileDialogFragment : DialogFragment() {
private var position = 0
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var binding: DialogInputProfilesBinding
private lateinit var setting: InputProfileSetting
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
position = requireArguments().getInt(POSITION)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogInputProfilesBinding.inflate(layoutInflater)
setting = settingsViewModel.clickedItem as InputProfileSetting
val options = mutableListOf<ProfileItem>().apply {
add(
NewProfileItem(
createNewProfile = {
NewInputProfileDialogFragment.newInstance(
settingsViewModel,
setting,
position
).show(parentFragmentManager, NewInputProfileDialogFragment.TAG)
dismiss()
}
)
)
val onActionDismiss = {
settingsViewModel.setReloadListAndNotifyDataset(true)
dismiss()
}
setting.getProfileNames().forEach {
add(
ExistingProfileItem(
it,
deleteProfile = {
settingsViewModel.setShouldShowDeleteProfileDialog(it)
},
saveProfile = {
if (!setting.saveProfile(it)) {
Toast.makeText(
requireContext(),
R.string.failed_to_save_profile,
Toast.LENGTH_SHORT
).show()
}
onActionDismiss.invoke()
},
loadProfile = {
if (!setting.loadProfile(it)) {
Toast.makeText(
requireContext(),
R.string.failed_to_load_profile,
Toast.LENGTH_SHORT
).show()
}
onActionDismiss.invoke()
}
)
)
}
}
binding.listProfiles.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = InputProfileAdapter(options)
}
return MaterialAlertDialogBuilder(requireContext())
.setView(binding.root)
.create()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsViewModel.shouldShowDeleteProfileDialog.collect(viewLifecycleOwner) {
if (it.isNotEmpty()) {
MessageDialogFragment.newInstance(
activity = requireActivity(),
titleId = R.string.delete_input_profile,
descriptionId = R.string.delete_input_profile_description,
positiveAction = {
setting.deleteProfile(it)
settingsViewModel.setReloadListAndNotifyDataset(true)
},
negativeAction = {},
negativeButtonTitleId = android.R.string.cancel
).show(parentFragmentManager, MessageDialogFragment.TAG)
settingsViewModel.setShouldShowDeleteProfileDialog("")
dismiss()
}
}
}
companion object {
const val TAG = "InputProfileDialogFragment"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
profileSetting: InputProfileSetting,
position: Int
): InputProfileDialogFragment {
settingsViewModel.clickedItem = profileSetting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = InputProfileDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.app.Dialog
import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.sudachi.sudachi_emu.databinding.DialogEditTextBinding
import org.sudachi.sudachi_emu.features.settings.model.view.InputProfileSetting
import org.sudachi.sudachi_emu.R
class NewInputProfileDialogFragment : DialogFragment() {
private var position = 0
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var binding: DialogEditTextBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
position = requireArguments().getInt(POSITION)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogEditTextBinding.inflate(layoutInflater)
val setting = settingsViewModel.clickedItem as InputProfileSetting
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.enter_profile_name)
.setPositiveButton(android.R.string.ok) { _, _ ->
val profileName = binding.editText.text.toString()
if (!setting.isProfileNameValid(profileName)) {
Toast.makeText(
requireContext(),
R.string.invalid_profile_name,
Toast.LENGTH_SHORT
).show()
return@setPositiveButton
}
if (!setting.createProfile(profileName)) {
Toast.makeText(
requireContext(),
R.string.profile_name_already_exists,
Toast.LENGTH_SHORT
).show()
} else {
settingsViewModel.setAdapterItemChanged(position)
}
}
.setNegativeButton(android.R.string.cancel, null)
.setView(binding.root)
.show()
}
companion object {
const val TAG = "NewInputProfileDialogFragment"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
profileSetting: InputProfileSetting,
position: Int
): NewInputProfileDialogFragment {
settingsViewModel.clickedItem = profileSetting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = NewInputProfileDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,171 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
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.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors
import org.sudachi.sudachi_emu.NativeLibrary
import java.io.IOException
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.databinding.ActivitySettingsBinding
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.settings.utils.SettingsFile
import org.sudachi.sudachi_emu.fragments.ResetSettingsDialogFragment
import org.sudachi.sudachi_emu.utils.*
class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding
private val args by navArgs<SettingsActivityArgs>()
private val settingsViewModel: SettingsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
SettingsFile.loadCustomConfig(args.game!!)
}
settingsViewModel.game = args.game
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navHostFragment.navController.setGraph(R.navigation.settings_navigation, intent.extras)
WindowCompat.setDecorFitsSystemWindows(window, false)
if (InsetsHelper.getSystemGestureType(applicationContext) !=
InsetsHelper.GESTURE_NAVIGATION
) {
binding.navigationBarShade.setBackgroundColor(
ThemeHelper.getColorWithOpacity(
MaterialColors.getColor(
binding.navigationBarShade,
com.google.android.material.R.attr.colorSurface
),
ThemeHelper.SYSTEM_BAR_ALPHA
)
)
}
settingsViewModel.shouldRecreate.collect(
this,
resetState = { settingsViewModel.setShouldRecreate(false) }
) { if (it) recreate() }
settingsViewModel.shouldNavigateBack.collect(
this,
resetState = { settingsViewModel.setShouldNavigateBack(false) }
) { if (it) navigateBack() }
settingsViewModel.shouldShowResetSettingsDialog.collect(
this,
resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) }
) {
if (it) {
ResetSettingsDialogFragment().show(
supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
}
}
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() = navigateBack()
}
)
setInsets()
}
fun navigateBack() {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
if (navHostFragment.childFragmentManager.backStackEntryCount > 0) {
navHostFragment.navController.popBackStack()
} else {
finish()
}
}
override fun onStart() {
super.onStart()
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
}
}
override fun onStop() {
super.onStop()
Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
if (isFinishing) {
NativeInput.reloadInputDevices()
NativeLibrary.applySettings()
if (args.game == null) {
NativeConfig.saveGlobalConfig()
} else if (NativeConfig.isPerGameConfigLoaded()) {
NativeLibrary.logSettings()
NativeConfig.savePerGameConfig()
NativeConfig.unloadPerGameConfig()
}
}
}
fun onSettingsReset() {
// Delete settings file because the user may have changed values that do not exist in the UI
if (args.game == null) {
NativeConfig.unloadGlobalConfig()
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
}
NativeConfig.initializeGlobalConfig()
} else {
NativeConfig.unloadPerGameConfig()
val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!)
if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
}
}
Toast.makeText(
applicationContext,
getString(R.string.settings_reset),
Toast.LENGTH_LONG
).show()
finish()
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.navigationBarShade
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
// The only situation where we care to have a nav bar shade is when it's at the bottom
// of the screen where scrolling list elements can go behind it.
val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
mlpNavShade.height = barInsets.bottom
binding.navigationBarShade.layoutParams = mlpNavShade
windowInsets
}
}
}

View File

@ -0,0 +1,434 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.content.Context
import android.icu.util.Calendar
import android.icu.util.TimeZone
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SettingsNavigationDirections
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
import org.sudachi.sudachi_emu.databinding.ListItemSettingInputBinding
import org.sudachi.sudachi_emu.databinding.ListItemSettingSwitchBinding
import org.sudachi.sudachi_emu.databinding.ListItemSettingsHeaderBinding
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.AnalogDirection
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
import org.sudachi.sudachi_emu.features.settings.model.view.*
import org.sudachi.sudachi_emu.features.settings.ui.viewholder.*
import org.sudachi.sudachi_emu.utils.ParamPackage
class SettingsAdapter(
private val fragment: Fragment,
private val context: Context
) : ListAdapter<SettingsItem, SettingViewHolder>(
AsyncDifferConfig.Builder(DiffCallback()).build()
) {
private val settingsViewModel: SettingsViewModel
get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java]
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 -> {
InputViewHolder(ListItemSettingInputBinding.inflate(inflater), this)
}
SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_INPUT_PROFILE -> {
InputProfileViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_STRING_INPUT -> {
StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
else -> {
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
}
}
}
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
holder.bind(currentList[position])
}
override fun getItemCount(): Int = currentList.size
override fun getItemViewType(position: Int): Int {
return currentList[position].type
}
fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) {
item.setChecked(checked)
notifyItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
}
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_SINGLE_CHOICE,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_STRING_SINGLE_CHOICE,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_INT_SINGLE_CHOICE,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onDateTimeClick(item: DateTimeSetting, position: Int) {
val storedTime = item.getValue() * 1000
// 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(context)) {
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(
fragment.childFragmentManager,
"TimePicker"
)
}
timePicker.addOnPositiveButtonClickListener {
var epochTime: Long = datePicker.selection!! / 1000
epochTime += timePicker.hour.toLong() * 60 * 60
epochTime += timePicker.minute.toLong() * 60
if (item.getValue() != epochTime) {
notifyItemChanged(position)
item.setValue(epochTime)
}
}
datePicker.show(
fragment.childFragmentManager,
"DatePicker"
)
}
fun onSliderClick(item: SliderSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_SLIDER,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onSubmenuClick(item: SubmenuSetting) {
val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null)
fragment.view?.findNavController()?.navigate(action)
}
fun onInputProfileClick(item: InputProfileSetting, position: Int) {
InputProfileDialogFragment.newInstance(
settingsViewModel,
item,
position
).show(fragment.childFragmentManager, InputProfileDialogFragment.TAG)
}
fun onInputClick(item: InputSetting, position: Int) {
InputDialogFragment.newInstance(
settingsViewModel,
item,
position
).show(fragment.childFragmentManager, InputDialogFragment.TAG)
}
fun onInputOptionsClick(anchor: View, item: InputSetting, position: Int) {
val popup = PopupMenu(context, anchor)
popup.menuInflater.inflate(R.menu.menu_input_options, popup.menu)
popup.menu.apply {
val invertAxis = findItem(R.id.invert_axis)
val invertButton = findItem(R.id.invert_button)
val toggleButton = findItem(R.id.toggle_button)
val turboButton = findItem(R.id.turbo_button)
val setThreshold = findItem(R.id.set_threshold)
val toggleAxis = findItem(R.id.toggle_axis)
when (item) {
is AnalogInputSetting -> {
val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
invertAxis.isVisible = true
invertAxis.isCheckable = true
invertAxis.isChecked = when (item.analogDirection) {
AnalogDirection.Left, AnalogDirection.Right -> {
params.get("invert_x", "+") == "-"
}
AnalogDirection.Up, AnalogDirection.Down -> {
params.get("invert_y", "+") == "-"
}
}
invertAxis.setOnMenuItemClickListener {
if (item.analogDirection == AnalogDirection.Left ||
item.analogDirection == AnalogDirection.Right
) {
val invertValue = params.get("invert_x", "+") == "-"
val invertString = if (invertValue) "+" else "-"
params.set("invert_x", invertString)
} else if (
item.analogDirection == AnalogDirection.Up ||
item.analogDirection == AnalogDirection.Down
) {
val invertValue = params.get("invert_y", "+") == "-"
val invertString = if (invertValue) "+" else "-"
params.set("invert_y", invertString)
}
true
}
popup.setOnDismissListener {
NativeInput.setStickParam(item.playerIndex, item.nativeAnalog, params)
settingsViewModel.setDatasetChanged(true)
}
}
is ButtonInputSetting -> {
val params = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
if (params.has("code") || params.has("button") || params.has("hat")) {
val buttonInvert = params.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = buttonInvert
invertButton.setOnMenuItemClickListener {
params.set("inverted", !buttonInvert)
true
}
val toggle = params.get("toggle", false)
toggleButton.isVisible = true
toggleButton.isCheckable = true
toggleButton.isChecked = toggle
toggleButton.setOnMenuItemClickListener {
params.set("toggle", !toggle)
true
}
val turbo = params.get("turbo", false)
turboButton.isVisible = true
turboButton.isCheckable = true
turboButton.isChecked = turbo
turboButton.setOnMenuItemClickListener {
params.set("turbo", !turbo)
true
}
} else if (params.has("axis")) {
val axisInvert = params.get("invert", "+") == "-"
invertAxis.isVisible = true
invertAxis.isCheckable = true
invertAxis.isChecked = axisInvert
invertAxis.setOnMenuItemClickListener {
params.set("invert", if (!axisInvert) "-" else "+")
true
}
val buttonInvert = params.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = buttonInvert
invertButton.setOnMenuItemClickListener {
params.set("inverted", !buttonInvert)
true
}
setThreshold.isVisible = true
val thresholdSetting = object : AbstractIntSetting {
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
(params.get("threshold", 0.5f) * 100).toInt()
override fun setInt(value: Int) {
params.set("threshold", value.toFloat() / 100)
NativeInput.setButtonParam(
item.playerIndex,
item.nativeButton,
params
)
}
override val defaultValue = 50
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(defaultValue)
}
setThreshold.setOnMenuItemClickListener {
onSliderClick(
SliderSetting(thresholdSetting, R.string.set_threshold),
position
)
true
}
val axisToggle = params.get("toggle", false)
toggleAxis.isVisible = true
toggleAxis.isCheckable = true
toggleAxis.isChecked = axisToggle
toggleAxis.setOnMenuItemClickListener {
params.set("toggle", !axisToggle)
true
}
}
popup.setOnDismissListener {
NativeInput.setButtonParam(item.playerIndex, item.nativeButton, params)
settingsViewModel.setAdapterItemChanged(position)
}
}
is ModifierInputSetting -> {
val stickParams = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
val modifierParams = ParamPackage(stickParams.get("modifier", ""))
val invert = modifierParams.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = invert
invertButton.setOnMenuItemClickListener {
modifierParams.set("inverted", !invert)
stickParams.set("modifier", modifierParams.serialize())
true
}
val toggle = modifierParams.get("toggle", false)
toggleButton.isVisible = true
toggleButton.isCheckable = true
toggleButton.isChecked = toggle
toggleButton.setOnMenuItemClickListener {
modifierParams.set("toggle", !toggle)
stickParams.set("modifier", modifierParams.serialize())
true
}
popup.setOnDismissListener {
NativeInput.setStickParam(
item.playerIndex,
item.nativeAnalog,
stickParams
)
settingsViewModel.setAdapterItemChanged(position)
}
}
}
}
popup.show()
}
fun onStringInputClick(item: StringInputSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_STRING_INPUT,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onLongClick(item: SettingsItem, position: Int): Boolean {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsDialogFragment.TYPE_RESET_SETTING,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
return true
}
fun onClearClick(item: SettingsItem, position: Int) {
item.setting.global = true
notifyItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
}
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
return oldItem.setting.key == newItem.setting.key
}
override fun areContentsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
return oldItem.setting.key == newItem.setting.key
}
}
}

View File

@ -0,0 +1,301 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.databinding.DialogEditTextBinding
import org.sudachi.sudachi_emu.databinding.DialogSliderBinding
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.AnalogDirection
import org.sudachi.sudachi_emu.features.settings.model.view.AnalogInputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.ButtonInputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.IntSingleChoiceSetting
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_emu.features.settings.model.view.SingleChoiceSetting
import org.sudachi.sudachi_emu.features.settings.model.view.SliderSetting
import org.sudachi.sudachi_emu.features.settings.model.view.StringInputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.StringSingleChoiceSetting
import org.sudachi.sudachi_emu.utils.ParamPackage
import org.sudachi.sudachi_emu.utils.collect
class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
private var type = 0
private var position = 0
private var defaultCancelListener =
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var sliderBinding: DialogSliderBinding
private lateinit var stringInputBinding: DialogEditTextBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
type = requireArguments().getInt(TYPE)
position = requireArguments().getInt(POSITION)
if (settingsViewModel.clickedItem == null) dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return when (type) {
TYPE_RESET_SETTING -> {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
when (val item = settingsViewModel.clickedItem) {
is AnalogInputSetting -> {
val stickParam = NativeInput.getStickParam(
item.playerIndex,
item.nativeAnalog
)
if (stickParam.get("engine", "") == "analog_from_button") {
when (item.analogDirection) {
AnalogDirection.Up -> stickParam.erase("up")
AnalogDirection.Down -> stickParam.erase("down")
AnalogDirection.Left -> stickParam.erase("left")
AnalogDirection.Right -> stickParam.erase("right")
}
NativeInput.setStickParam(
item.playerIndex,
item.nativeAnalog,
stickParam
)
settingsViewModel.setAdapterItemChanged(position)
} else {
NativeInput.setStickParam(
item.playerIndex,
item.nativeAnalog,
ParamPackage()
)
settingsViewModel.setDatasetChanged(true)
}
}
is ButtonInputSetting -> {
NativeInput.setButtonParam(
item.playerIndex,
item.nativeButton,
ParamPackage()
)
settingsViewModel.setAdapterItemChanged(position)
}
else -> {
settingsViewModel.clickedItem!!.setting.reset()
settingsViewModel.setAdapterItemChanged(position)
}
}
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
SettingsItem.TYPE_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getSelectionForSingleChoiceValue(item)
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setSingleChoiceItems(item.choicesId, value, this)
.create()
}
SettingsItem.TYPE_SLIDER -> {
sliderBinding = DialogSliderBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as SliderSetting
settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
value = settingsViewModel.sliderProgress.value.toFloat()
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
settingsViewModel.setSliderTextValue(value, item.units)
}
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setView(sliderBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.create()
}
SettingsItem.TYPE_STRING_INPUT -> {
stringInputBinding = DialogEditTextBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as StringInputSetting
stringInputBinding.editText.setText(item.getSelectedValue())
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setView(stringInputBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.create()
}
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
.create()
}
SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as IntSingleChoiceSetting
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
.create()
}
else -> super.onCreateDialog(savedInstanceState)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return when (type) {
SettingsItem.TYPE_SLIDER -> sliderBinding.root
SettingsItem.TYPE_STRING_INPUT -> stringInputBinding.root
else -> super.onCreateView(inflater, container, savedInstanceState)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
when (type) {
SettingsItem.TYPE_SLIDER -> {
settingsViewModel.sliderTextValue.collect(viewLifecycleOwner) {
sliderBinding.textValue.text = it
}
settingsViewModel.sliderProgress.collect(viewLifecycleOwner) {
sliderBinding.slider.value = it.toFloat()
}
}
}
}
override fun onClick(dialog: DialogInterface, which: Int) {
when (settingsViewModel.clickedItem) {
is SingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which)
scSetting.setSelectedValue(value)
}
is StringSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
scSetting.setSelectedValue(value)
}
is IntSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting
val value = scSetting.getValueAt(which)
scSetting.setSelectedValue(value)
}
is SliderSetting -> {
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
}
is StringInputSetting -> {
val stringInputSetting = settingsViewModel.clickedItem as StringInputSetting
stringInputSetting.setSelectedValue(
(stringInputBinding.editText.text ?: "").toString()
)
}
}
closeDialog()
}
private fun closeDialog() {
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.clickedItem = null
settingsViewModel.setSliderProgress(-1f)
dismiss()
}
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
val valuesId = item.valuesId
return if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
valuesArray[which]
} else {
which
}
}
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.getSelectedValue()
val valuesId = item.valuesId
if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
for (index in valuesArray.indices) {
val current = valuesArray[index]
if (current == value) {
return index
}
}
} else {
return value
}
return -1
}
companion object {
const val TAG = "SettingsDialogFragment"
const val TYPE_RESET_SETTING = -1
const val TITLE = "Title"
const val TYPE = "Type"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
clickedItem: SettingsItem,
type: Int,
position: Int
): SettingsDialogFragment {
when (type) {
SettingsItem.TYPE_HEADER,
SettingsItem.TYPE_SWITCH,
SettingsItem.TYPE_SUBMENU,
SettingsItem.TYPE_DATETIME_SETTING,
SettingsItem.TYPE_RUNNABLE ->
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
(clickedItem as SliderSetting).getSelectedValue().toFloat()
)
}
settingsViewModel.clickedItem = clickedItem
val args = Bundle()
args.putInt(TYPE, type)
args.putInt(POSITION, position)
val fragment = SettingsDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.databinding.FragmentSettingsBinding
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.settings.model.Settings
import org.sudachi.sudachi_emu.fragments.MessageDialogFragment
import org.sudachi.sudachi_emu.utils.ViewUtils.updateMargins
import org.sudachi.sudachi_emu.utils.collect
class SettingsFragment : Fragment() {
private lateinit var presenter: SettingsFragmentPresenter
private var settingsAdapter: SettingsAdapter? = null
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private val args by navArgs<SettingsFragmentArgs>()
private val settingsViewModel: SettingsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
val playerIndex = getPlayerIndex()
if (playerIndex != -1) {
NativeInput.loadInputProfiles()
NativeInput.reloadInputDevices()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsBinding.inflate(layoutInflater)
return binding.root
}
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsAdapter = SettingsAdapter(this, requireContext())
presenter = SettingsFragmentPresenter(
settingsViewModel,
settingsAdapter!!,
args.menuTag
)
binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT &&
args.game != null
) {
args.game!!.title
} else {
when (args.menuTag) {
Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1)
Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2)
Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3)
Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4)
Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5)
Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6)
Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7)
Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8)
else -> getString(args.menuTag.titleId)
}
}
binding.listSettings.apply {
adapter = settingsAdapter
layoutManager = LinearLayoutManager(requireContext())
}
binding.toolbarSettings.setNavigationOnClickListener {
settingsViewModel.setShouldNavigateBack(true)
}
settingsViewModel.shouldReloadSettingsList.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldReloadSettingsList(false) }
) { if (it) presenter.loadSettingsList() }
settingsViewModel.adapterItemChanged.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setAdapterItemChanged(-1) }
) { if (it != -1) settingsAdapter?.notifyItemChanged(it) }
settingsViewModel.datasetChanged.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setDatasetChanged(false) }
) { if (it) settingsAdapter?.notifyDataSetChanged() }
settingsViewModel.reloadListAndNotifyDataset.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setReloadListAndNotifyDataset(false) }
) { if (it) presenter.loadSettingsList(true) }
settingsViewModel.shouldShowResetInputDialog.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldShowResetInputDialog(false) }
) {
if (it) {
MessageDialogFragment.newInstance(
activity = requireActivity(),
titleId = R.string.reset_mapping,
descriptionId = R.string.reset_mapping_description,
positiveAction = {
NativeInput.resetControllerMappings(getPlayerIndex())
settingsViewModel.setReloadListAndNotifyDataset(true)
},
negativeAction = {}
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
}
if (args.menuTag == Settings.MenuTag.SECTION_ROOT) {
binding.toolbarSettings.inflateMenu(R.menu.menu_settings)
binding.toolbarSettings.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_search -> {
view.findNavController()
.navigate(R.id.action_settingsFragment_to_settingsSearchFragment)
true
}
else -> false
}
}
}
presenter.onViewCreated()
setInsets()
}
private fun getPlayerIndex(): Int =
when (args.menuTag) {
Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0
Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1
Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2
Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3
Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4
Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5
Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6
Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7
else -> -1
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.listSettings.updateMargins(left = leftInsets, right = rightInsets)
binding.listSettings.updatePadding(bottom = barInsets.bottom)
binding.appbarSettings.updateMargins(left = leftInsets, right = rightInsets)
windowInsets
}
}
}

View File

@ -0,0 +1,975 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import android.annotation.SuppressLint
import android.os.Build
import android.widget.Toast
import org.sudachi.sudachi_emu.NativeLibrary
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.input.model.AnalogDirection
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
import org.sudachi.sudachi_emu.features.input.model.NativeButton
import org.sudachi.sudachi_emu.features.input.model.NpadStyleIndex
import org.sudachi.sudachi_emu.features.settings.model.AbstractBooleanSetting
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
import org.sudachi.sudachi_emu.features.settings.model.BooleanSetting
import org.sudachi.sudachi_emu.features.settings.model.ByteSetting
import org.sudachi.sudachi_emu.features.settings.model.IntSetting
import org.sudachi.sudachi_emu.features.settings.model.LongSetting
import org.sudachi.sudachi_emu.features.settings.model.Settings
import org.sudachi.sudachi_emu.features.settings.model.Settings.MenuTag
import org.sudachi.sudachi_emu.features.settings.model.ShortSetting
import org.sudachi.sudachi_emu.features.settings.model.StringSetting
import org.sudachi.sudachi_emu.features.settings.model.view.*
import org.sudachi.sudachi_emu.utils.InputHandler
import org.sudachi.sudachi_emu.utils.NativeConfig
class SettingsFragmentPresenter(
private val settingsViewModel: SettingsViewModel,
private val adapter: SettingsAdapter,
private var menuTag: MenuTag
) {
private var settingsList = ArrayList<SettingsItem>()
private val context get() = SudachiApplication.appContext
// Extension for altering settings list based on each setting's properties
fun ArrayList<SettingsItem>.add(key: String) {
val item = SettingsItem.settingsItems[key]!!
if (settingsViewModel.game != null && !item.setting.isSwitchable) {
return
}
if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) {
item.setting.global = true
}
val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean(
pairedSettingKey,
if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) {
!NativeConfig.usingGlobal(pairedSettingKey)
} else {
NativeConfig.usingGlobal(pairedSettingKey)
}
)
if (!pairedSettingValue) return
}
add(item)
}
// Allows you to show/hide abstract settings based on the paired setting key
fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) {
val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) {
val pairedSettingsItem =
this.firstOrNull { it.setting.key == pairedSettingKey } ?: return
val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting
if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return
}
add(item)
}
fun onViewCreated() {
loadSettingsList()
}
@SuppressLint("NotifyDataSetChanged")
fun loadSettingsList(notifyDataSetChanged: Boolean = false) {
val sl = ArrayList<SettingsItem>()
when (menuTag) {
MenuTag.SECTION_ROOT -> addConfigSettings(sl)
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
MenuTag.SECTION_INPUT -> addInputSettings(sl)
MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0)
MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1)
MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2)
MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3)
MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4)
MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5)
MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6)
MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
MenuTag.SECTION_THEME -> addThemeSettings(sl)
MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
}
settingsList = sl
adapter.submitList(settingsList) {
if (notifyDataSetChanged) {
adapter.notifyDataSetChanged()
}
}
}
private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(
SubmenuSetting(
titleId = R.string.preferences_system,
descriptionId = R.string.preferences_system_description,
iconId = R.drawable.ic_system_settings,
menuKey = MenuTag.SECTION_SYSTEM
)
)
add(
SubmenuSetting(
titleId = R.string.preferences_graphics,
descriptionId = R.string.preferences_graphics_description,
iconId = R.drawable.ic_graphics,
menuKey = MenuTag.SECTION_RENDERER
)
)
add(
SubmenuSetting(
titleId = R.string.preferences_audio,
descriptionId = R.string.preferences_audio_description,
iconId = R.drawable.ic_audio,
menuKey = MenuTag.SECTION_AUDIO
)
)
add(
SubmenuSetting(
titleId = R.string.preferences_debug,
descriptionId = R.string.preferences_debug_description,
iconId = R.drawable.ic_code,
menuKey = MenuTag.SECTION_DEBUG
)
)
add(
RunnableSetting(
titleId = R.string.reset_to_default,
descriptionId = R.string.reset_to_default_description,
isRunnable = !NativeLibrary.isRunning(),
iconId = R.drawable.ic_restore
) { settingsViewModel.setShouldShowResetSettingsDialog(true) }
)
}
}
private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(StringSetting.DEVICE_NAME.key)
add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_SPEED_LIMIT.key)
add(BooleanSetting.USE_DOCKED_MODE.key)
add(IntSetting.REGION_INDEX.key)
add(IntSetting.LANGUAGE_INDEX.key)
add(BooleanSetting.USE_CUSTOM_RTC.key)
add(LongSetting.CUSTOM_RTC.key)
}
}
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(IntSetting.RENDERER_ACCURACY.key)
add(IntSetting.RENDERER_RESOLUTION.key)
add(IntSetting.RENDERER_VSYNC.key)
add(IntSetting.RENDERER_SCALING_FILTER.key)
add(IntSetting.FSR_SHARPENING_SLIDER.key)
add(IntSetting.RENDERER_ANTI_ALIASING.key)
add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
add(IntSetting.RENDERER_ASPECT_RATIO.key)
add(IntSetting.VERTICAL_ALIGNMENT.key)
add(BooleanSetting.PICTURE_IN_PICTURE.key)
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
}
}
private fun addAudioSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(IntSetting.AUDIO_OUTPUT_ENGINE.key)
add(ByteSetting.AUDIO_VOLUME.key)
}
}
private fun addInputSettings(sl: ArrayList<SettingsItem>) {
settingsViewModel.currentDevice = 0
if (NativeConfig.isPerGameConfigLoaded()) {
NativeInput.loadInputProfiles()
val profiles = NativeInput.getInputProfileNames().toMutableList()
profiles.add(0, "")
val prettyProfiles = profiles.toTypedArray()
prettyProfiles[0] =
context.getString(R.string.use_global_input_configuration)
sl.apply {
for (i in 0 until 8) {
add(
IntSingleChoiceSetting(
getPerGameProfileSetting(profiles, i),
titleString = getPlayerProfileString(i + 1),
choices = prettyProfiles,
values = IntArray(profiles.size) { it }.toTypedArray()
)
)
}
}
return
}
val getConnectedIcon: (Int) -> Int = { playerIndex: Int ->
if (NativeInput.getIsConnected(playerIndex)) {
R.drawable.ic_controller
} else {
R.drawable.ic_controller_disconnected
}
}
val inputSettings = NativeConfig.getInputSettings(true)
sl.apply {
add(
SubmenuSetting(
titleString = Settings.getPlayerString(1),
descriptionString = inputSettings[0].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE,
iconId = getConnectedIcon(0)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(2),
descriptionString = inputSettings[1].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO,
iconId = getConnectedIcon(1)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(3),
descriptionString = inputSettings[2].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE,
iconId = getConnectedIcon(2)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(4),
descriptionString = inputSettings[3].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR,
iconId = getConnectedIcon(3)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(5),
descriptionString = inputSettings[4].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE,
iconId = getConnectedIcon(4)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(6),
descriptionString = inputSettings[5].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX,
iconId = getConnectedIcon(5)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(7),
descriptionString = inputSettings[6].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN,
iconId = getConnectedIcon(6)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(8),
descriptionString = inputSettings[7].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT,
iconId = getConnectedIcon(7)
)
)
}
}
private fun getPlayerProfileString(player: Int): String =
context.getString(R.string.player_num_profile, player)
private fun getPerGameProfileSetting(
profiles: List<String>,
playerIndex: Int
): AbstractIntSetting {
return object : AbstractIntSetting {
private val players
get() = NativeConfig.getInputSettings(false)
override val key = ""
override fun getInt(needsGlobal: Boolean): Int {
val currentProfile = players[playerIndex].profileName
profiles.forEachIndexed { i, profile ->
if (profile == currentProfile) {
return i
}
}
return 0
}
override fun setInt(value: Int) {
NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value])
NativeInput.connectControllers(playerIndex)
NativeConfig.saveControlPlayerValues()
}
override val defaultValue = 0
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override fun reset() = setInt(defaultValue)
override var global = true
override val isRuntimeModifiable = true
override val isSaveable = true
}
}
private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) {
sl.apply {
val connectedSetting = object : AbstractBooleanSetting {
override val key = "connected"
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeInput.getIsConnected(playerIndex)
override fun setBoolean(value: Boolean) =
NativeInput.connectControllers(playerIndex, value)
override val defaultValue = playerIndex == 0
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
}
add(SwitchSetting(connectedSetting, R.string.connected))
val styleTags = NativeInput.getSupportedStyleTags(playerIndex)
val npadType = object : AbstractIntSetting {
override val key = "npad_type"
override fun getInt(needsGlobal: Boolean): Int {
val styleIndex = NativeInput.getStyleIndex(playerIndex)
return styleTags.indexOfFirst { it == styleIndex }
}
override fun setInt(value: Int) {
NativeInput.setStyleIndex(playerIndex, styleTags[value])
settingsViewModel.setReloadListAndNotifyDataset(true)
}
override val defaultValue = NpadStyleIndex.Fullkey.int
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override fun reset() = setInt(defaultValue)
override val pairedSettingKey: String = "connected"
}
addAbstract(
IntSingleChoiceSetting(
npadType,
titleId = R.string.controller_type,
choices = styleTags.map { context.getString(it.nameId) }
.toTypedArray(),
values = IntArray(styleTags.size) { it }.toTypedArray()
)
)
InputHandler.updateControllerData()
val autoMappingSetting = object : AbstractIntSetting {
override val key = "auto_mapping_device"
override fun getInt(needsGlobal: Boolean): Int = -1
override fun setInt(value: Int) {
val registeredController = InputHandler.registeredControllers[value + 1]
val displayName = registeredController.get(
"display",
context.getString(R.string.unknown)
)
NativeInput.updateMappingsWithDefault(
playerIndex,
registeredController,
displayName
)
Toast.makeText(
context,
context.getString(R.string.attempted_auto_map, displayName),
Toast.LENGTH_SHORT
).show()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
override val defaultValue = -1
override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
override fun reset() = setInt(defaultValue)
override val isRuntimeModifiable: Boolean = true
}
val unknownString = context.getString(R.string.unknown)
val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull {
val port = it.get("port", -1)
return@mapNotNull if (port == 100 || port == -1) {
null
} else {
it.get("display", unknownString)
}
}.toTypedArray()
add(
IntSingleChoiceSetting(
autoMappingSetting,
titleId = R.string.auto_map,
descriptionId = R.string.auto_map_description,
choices = prettyAutoMappingControllerList,
values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray()
)
)
val mappingFilterSetting = object : AbstractIntSetting {
override val key = "mapping_filter"
override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice
override fun setInt(value: Int) {
settingsViewModel.currentDevice = value
}
override val defaultValue = 0
override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
override fun reset() = setInt(defaultValue)
override val isRuntimeModifiable: Boolean = true
}
val prettyControllerList = InputHandler.registeredControllers.mapNotNull {
return@mapNotNull if (it.get("port", 0) == 100) {
null
} else {
it.get("display", unknownString)
}
}.toTypedArray()
add(
IntSingleChoiceSetting(
mappingFilterSetting,
titleId = R.string.input_mapping_filter,
descriptionId = R.string.input_mapping_filter_description,
choices = prettyControllerList,
values = IntArray(prettyControllerList.size) { it }.toTypedArray()
)
)
add(InputProfileSetting(playerIndex))
add(
RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) {
settingsViewModel.setShouldShowResetInputDialog(true)
}
)
val styleIndex = NativeInput.getStyleIndex(playerIndex)
// Buttons
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
add(
ButtonInputSetting(
playerIndex,
NativeButton.Capture,
R.string.button_capture
)
)
}
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
add(
ButtonInputSetting(
playerIndex,
NativeButton.Capture,
R.string.button_capture
)
)
}
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause))
}
else -> {
// No-op
}
}
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.dpad))
add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up))
add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down))
add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left))
add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right))
}
else -> {
// No-op
}
}
// Left stick
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.left_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.control_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
}
else -> {
// No-op
}
}
// Right stick
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.right_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.c_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
}
else -> {
// No-op
}
}
// L/R, ZL/ZR, and SL/SR
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
}
NpadStyleIndex.JoyconDual -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLLeft,
R.string.button_sl_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRLeft,
R.string.button_sr_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLRight,
R.string.button_sl_right
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRRight,
R.string.button_sr_right
)
)
}
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLLeft,
R.string.button_sl_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRLeft,
R.string.button_sr_left
)
)
}
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLRight,
R.string.button_sl_right
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRRight,
R.string.button_sr_right
)
)
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r))
}
else -> {
// No-op
}
}
add(HeaderSetting(R.string.vibration))
val vibrationEnabledSetting = object : AbstractBooleanSetting {
override val key = "vibration"
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled
override fun setBoolean(value: Boolean) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].vibrationEnabled = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = true
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
}
add(SwitchSetting(vibrationEnabledSetting, R.string.vibration))
val useSystemVibratorSetting = object : AbstractBooleanSetting {
override val key = ""
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator
override fun setBoolean(value: Boolean) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].useSystemVibrator = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = playerIndex == 0
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
override val pairedSettingKey: String = "vibration"
}
addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator))
val vibrationStrengthSetting = object : AbstractIntSetting {
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength
override fun setInt(value: Int) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].vibrationStrength = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = 100
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(defaultValue)
override val pairedSettingKey: String = "vibration"
}
addAbstract(
SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%")
)
}
}
// Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones
private fun getStickIntSettingFromParam(
playerIndex: Int,
paramName: String,
stick: NativeAnalog,
defaultValue: Float
): AbstractIntSetting =
object : AbstractIntSetting {
val params get() = NativeInput.getStickParam(playerIndex, stick)
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
(params.get(paramName, defaultValue) * 100).toInt()
override fun setInt(value: Int) {
val tempParams = params
tempParams.set(paramName, value.toFloat() / 100)
NativeInput.setStickParam(playerIndex, stick, tempParams)
}
override val defaultValue = (defaultValue * 100).toInt()
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(this.defaultValue)
}
private fun getExtraStickSettings(
playerIndex: Int,
nativeAnalog: NativeAnalog
): List<SettingsItem> {
val stickIsController =
NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog))
val modifierRangeSetting =
getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 0.5f)
val stickRangeSetting =
getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 0.95f)
val stickDeadzoneSetting =
getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 0.15f)
val out = mutableListOf<SettingsItem>().apply {
if (stickIsController) {
add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150))
add(SliderSetting(stickDeadzoneSetting, R.string.deadzone))
} else {
add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier))
add(SliderSetting(modifierRangeSetting, R.string.modifier_range))
}
}
return out
}
private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> =
listOf(
AnalogInputSetting(
player,
stick,
AnalogDirection.Up,
R.string.up
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Down,
R.string.down
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Left,
R.string.left
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Right,
R.string.right
)
)
private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
val theme: AbstractIntSetting = object : AbstractIntSetting {
override fun getInt(needsGlobal: Boolean): Int = IntSetting.THEME.getInt()
override fun setInt(value: Int) {
IntSetting.THEME.setInt(value)
settingsViewModel.setShouldRecreate(true)
}
override val key: String = IntSetting.THEME.key
override val isRuntimeModifiable: Boolean = IntSetting.THEME.isRuntimeModifiable
override fun getValueAsString(needsGlobal: Boolean): String =
IntSetting.THEME.getValueAsString()
override val defaultValue: Int = IntSetting.THEME.defaultValue
override fun reset() = IntSetting.THEME.setInt(defaultValue)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
add(
SingleChoiceSetting(
theme,
titleId = R.string.change_app_theme,
choicesId = R.array.themeEntriesA12,
valuesId = R.array.themeValuesA12
)
)
} else {
add(
SingleChoiceSetting(
theme,
titleId = R.string.change_app_theme,
choicesId = R.array.themeEntries,
valuesId = R.array.themeValues
)
)
}
val themeMode: AbstractIntSetting = object : AbstractIntSetting {
override fun getInt(needsGlobal: Boolean): Int = IntSetting.THEME_MODE.getInt()
override fun setInt(value: Int) {
IntSetting.THEME_MODE.setInt(value)
settingsViewModel.setShouldRecreate(true)
}
override val key: String = IntSetting.THEME_MODE.key
override val isRuntimeModifiable: Boolean =
IntSetting.THEME_MODE.isRuntimeModifiable
override fun getValueAsString(needsGlobal: Boolean): String =
IntSetting.THEME_MODE.getValueAsString()
override val defaultValue: Int = IntSetting.THEME_MODE.defaultValue
override fun reset() {
IntSetting.THEME_MODE.setInt(defaultValue)
settingsViewModel.setShouldRecreate(true)
}
}
add(
SingleChoiceSetting(
themeMode,
titleId = R.string.change_theme_mode,
choicesId = R.array.themeModeEntries,
valuesId = R.array.themeModeValues
)
)
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
override fun getBoolean(needsGlobal: Boolean): Boolean =
BooleanSetting.BLACK_BACKGROUNDS.getBoolean()
override fun setBoolean(value: Boolean) {
BooleanSetting.BLACK_BACKGROUNDS.setBoolean(value)
settingsViewModel.setShouldRecreate(true)
}
override val key: String = BooleanSetting.BLACK_BACKGROUNDS.key
override val isRuntimeModifiable: Boolean =
BooleanSetting.BLACK_BACKGROUNDS.isRuntimeModifiable
override fun getValueAsString(needsGlobal: Boolean): String =
BooleanSetting.BLACK_BACKGROUNDS.getValueAsString()
override val defaultValue: Boolean = BooleanSetting.BLACK_BACKGROUNDS.defaultValue
override fun reset() {
BooleanSetting.BLACK_BACKGROUNDS
.setBoolean(BooleanSetting.BLACK_BACKGROUNDS.defaultValue)
settingsViewModel.setShouldRecreate(true)
}
}
add(
SwitchSetting(
blackBackgrounds,
titleId = R.string.use_black_backgrounds,
descriptionId = R.string.use_black_backgrounds_description
)
)
}
}
private fun addDebugSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(HeaderSetting(R.string.gpu))
add(IntSetting.RENDERER_BACKEND.key)
add(BooleanSetting.RENDERER_DEBUG.key)
add(HeaderSetting(R.string.cpu))
add(IntSetting.CPU_BACKEND.key)
add(IntSetting.CPU_ACCURACY.key)
add(BooleanSetting.CPU_DEBUG_MODE.key)
add(SettingsItem.FASTMEM_COMBINED)
}
}
}

View File

@ -0,0 +1,183 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_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 android.view.inputmethod.InputMethodManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.transition.MaterialSharedAxis
import info.debatty.java.stringsimilarity.Cosine
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.databinding.FragmentSettingsSearchBinding
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_emu.utils.NativeConfig
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
import org.sudachi.sudachi_emu.utils.ViewUtils.updateMargins
import org.sudachi.sudachi_emu.utils.collect
class SettingsSearchFragment : Fragment() {
private var _binding: FragmentSettingsSearchBinding? = null
private val binding get() = _binding!!
private var settingsAdapter: SettingsAdapter? = null
private val settingsViewModel: SettingsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsSearchBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
}
settingsAdapter = SettingsAdapter(this, requireContext())
val dividerDecoration = MaterialDividerItemDecoration(
requireContext(),
LinearLayoutManager.VERTICAL
)
dividerDecoration.isLastItemDecorated = false
binding.settingsList.apply {
adapter = settingsAdapter
layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(dividerDecoration)
}
focusSearch()
binding.backButton.setOnClickListener { settingsViewModel.setShouldNavigateBack(true) }
binding.searchBackground.setOnClickListener { focusSearch() }
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
binding.searchText.doOnTextChanged { _, _, _, _ ->
search()
binding.settingsList.smoothScrollToPosition(0)
}
settingsViewModel.shouldReloadSettingsList.collect(viewLifecycleOwner) {
if (it) {
settingsViewModel.setShouldReloadSettingsList(false)
search()
}
}
search()
setInsets()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
}
private fun search() {
val searchTerm = binding.searchText.text.toString().lowercase()
binding.clearButton.setVisible(visible = searchTerm.isNotEmpty(), gone = false)
if (searchTerm.isEmpty()) {
binding.noResultsView.setVisible(visible = false, gone = false)
settingsAdapter?.submitList(emptyList())
return
}
val baseList = SettingsItem.settingsItems
val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
val title = item.value.title.lowercase()
val similarity = similarityAlgorithm.similarity(searchTerm, title)
if (similarity > 0.08) {
Pair(similarity, item)
} else {
null
}
}.sortedByDescending { it.first }.mapNotNull {
val item = it.second.value
val pairedSettingKey = item.setting.pairedSettingKey
val optionalSetting: SettingsItem? = if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
if (pairedSettingValue) it.second.value else null
} else {
it.second.value
}
optionalSetting
}
settingsAdapter?.submitList(sortedList)
binding.noResultsView.setVisible(visible = sortedList.isEmpty(), gone = false)
}
private fun focusSearch() {
binding.searchText.requestFocus()
val imm = requireActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
val topMargin = resources.getDimensionPixelSize(R.dimen.spacing_chip)
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.settingsList.updatePadding(bottom = barInsets.bottom + extraListSpacing)
binding.frameSearch.updatePadding(
left = leftInsets + sideMargin,
top = barInsets.top + topMargin,
right = rightInsets + sideMargin
)
binding.noResultsView.updatePadding(
left = leftInsets,
right = rightInsets,
bottom = barInsets.bottom
)
binding.settingsList.updateMargins(
left = leftInsets + sideMargin,
right = rightInsets + sideMargin
)
binding.divider.updateMargins(
left = leftInsets + sideMargin,
right = rightInsets + sideMargin
)
windowInsets
}
companion object {
const val SEARCH_TEXT = "SearchText"
}
}

View File

@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.SudachiApplication
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_emu.model.Game
import org.sudachi.sudachi_emu.utils.InputHandler
import org.sudachi.sudachi_emu.utils.ParamPackage
class SettingsViewModel : ViewModel() {
var game: Game? = null
var clickedItem: SettingsItem? = null
var currentDevice = 0
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
private val _shouldRecreate = MutableStateFlow(false)
val shouldNavigateBack: StateFlow<Boolean> get() = _shouldNavigateBack
private val _shouldNavigateBack = MutableStateFlow(false)
val shouldShowResetSettingsDialog: StateFlow<Boolean> get() = _shouldShowResetSettingsDialog
private val _shouldShowResetSettingsDialog = MutableStateFlow(false)
val shouldReloadSettingsList: StateFlow<Boolean> get() = _shouldReloadSettingsList
private val _shouldReloadSettingsList = MutableStateFlow(false)
val sliderProgress: StateFlow<Int> get() = _sliderProgress
private val _sliderProgress = MutableStateFlow(-1)
val sliderTextValue: StateFlow<String> get() = _sliderTextValue
private val _sliderTextValue = MutableStateFlow("")
val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged
private val _adapterItemChanged = MutableStateFlow(-1)
private val _datasetChanged = MutableStateFlow(false)
val datasetChanged = _datasetChanged.asStateFlow()
private val _reloadListAndNotifyDataset = MutableStateFlow(false)
val reloadListAndNotifyDataset = _reloadListAndNotifyDataset.asStateFlow()
private val _shouldShowDeleteProfileDialog = MutableStateFlow("")
val shouldShowDeleteProfileDialog = _shouldShowDeleteProfileDialog.asStateFlow()
private val _shouldShowResetInputDialog = MutableStateFlow(false)
val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow()
fun setShouldRecreate(value: Boolean) {
_shouldRecreate.value = value
}
fun setShouldNavigateBack(value: Boolean) {
_shouldNavigateBack.value = value
}
fun setShouldShowResetSettingsDialog(value: Boolean) {
_shouldShowResetSettingsDialog.value = value
}
fun setShouldReloadSettingsList(value: Boolean) {
_shouldReloadSettingsList.value = value
}
fun setSliderTextValue(value: Float, units: String) {
_sliderProgress.value = value.toInt()
_sliderTextValue.value = String.format(
SudachiApplication.appContext.getString(R.string.value_with_units),
value.toInt().toString(),
units
)
}
fun setSliderProgress(value: Float) {
_sliderProgress.value = value.toInt()
}
fun setAdapterItemChanged(value: Int) {
_adapterItemChanged.value = value
}
fun setDatasetChanged(value: Boolean) {
_datasetChanged.value = value
}
fun setReloadListAndNotifyDataset(value: Boolean) {
_reloadListAndNotifyDataset.value = value
}
fun setShouldShowDeleteProfileDialog(profile: String) {
_shouldShowDeleteProfileDialog.value = profile
}
fun setShouldShowResetInputDialog(value: Boolean) {
_shouldShowResetInputDialog.value = value
}
fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage =
try {
InputHandler.registeredControllers[currentDevice]
} catch (e: IndexOutOfBoundsException) {
defaultParams
}
}

View File

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
import android.view.View
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
import org.sudachi.sudachi_emu.features.settings.model.view.DateTimeSetting
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: DateTimeSetting
override fun bind(item: SettingsItem) {
setting = item as DateTimeSetting
binding.textSettingName.text = item.title
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
binding.textSettingDescription.text = item.description
binding.textSettingValue.setVisible(true)
val epochTime = setting.getValue()
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)
binding.buttonClear.setVisible(setting.clearable)
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding)
}
override fun onClick(clicked: View) {
if (setting.isEditable) {
adapter.onDateTimeClick(setting, bindingAdapterPosition)
}
}
override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) {
return adapter.onLongClick(setting, bindingAdapterPosition)
}
return false
}
}

View File

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
import android.view.View
import org.sudachi.sudachi_emu.databinding.ListItemSettingsHeaderBinding
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_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.text = item.title
}
override fun onClick(clicked: View) {
// no-op
}
override fun onLongClick(clicked: View): Boolean {
// no-op
return true
}
}

View File

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
import android.view.View
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
import org.sudachi.sudachi_emu.features.settings.model.view.InputProfileSetting
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
import org.sudachi.sudachi_emu.R
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: InputProfileSetting
override fun bind(item: SettingsItem) {
setting = item as InputProfileSetting
binding.textSettingName.text = setting.title
binding.textSettingValue.text =
setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) }
binding.textSettingDescription.setVisible(false)
binding.buttonClear.setVisible(false)
binding.icon.setVisible(false)
binding.buttonClear.setVisible(false)
}
override fun onClick(clicked: View) =
adapter.onInputProfileClick(setting, bindingAdapterPosition)
override fun onLongClick(clicked: View): Boolean = false
}

View File

@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
import android.view.View
import org.sudachi.sudachi_emu.databinding.ListItemSettingInputBinding
import org.sudachi.sudachi_emu.features.input.NativeInput
import org.sudachi.sudachi_emu.features.settings.model.view.AnalogInputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.ButtonInputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.InputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.ModifierInputSetting
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: InputSetting
override fun bind(item: SettingsItem) {
setting = item as InputSetting
binding.textSettingName.text = setting.title
binding.textSettingValue.text = setting.getSelectedValue()
when (item) {
is AnalogInputSetting -> {
val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
binding.buttonOptions.setVisible(
param.get("engine", "") == "analog_from_button" ||
param.has("axis_x") || param.has("axis_y")
)
}
is ButtonInputSetting -> {
val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
binding.buttonOptions.setVisible(
param.has("code") || param.has("button") || param.has("hat") ||
param.has("axis")
)
}
is ModifierInputSetting -> {
val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
binding.buttonOptions.setVisible(params.has("modifier"))
}
}
binding.buttonOptions.setOnClickListener(null)
binding.buttonOptions.setOnClickListener {
adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition)
}
}
override fun onClick(clicked: View) =
adapter.onInputClick(setting, bindingAdapterPosition)
override fun onLongClick(clicked: View): Boolean =
adapter.onLongClick(setting, bindingAdapterPosition)
}

View File

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
import android.view.View
import androidx.core.content.res.ResourcesCompat
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
import org.sudachi.sudachi_emu.features.settings.model.view.RunnableSetting
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: RunnableSetting
override fun bind(item: SettingsItem) {
setting = item as RunnableSetting
binding.icon.setVisible(setting.iconId != 0)
if (setting.iconId != 0) {
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.resources,
setting.iconId,
binding.icon.context.theme
)
)
}
binding.textSettingName.text = setting.title
binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
binding.textSettingDescription.text = item.description
binding.textSettingValue.setVisible(false)
binding.buttonClear.setVisible(false)
setStyle(setting.isEditable, binding)
}
override fun onClick(clicked: View) {
if (setting.isRunnable) {
setting.runnable.invoke()
}
}
override fun onLongClick(clicked: View): Boolean {
// no-op
return true
}
}

View File

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
import org.sudachi.sudachi_emu.databinding.ListItemSettingSwitchBinding
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) :
RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
init {
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
}
/**
* Called by the adapter to set this ViewHolder's child views to display the list item
* it must now represent.
*
* @param item The list item that should be represented by this ViewHolder.
*/
abstract fun bind(item: SettingsItem)
/**
* Called when this ViewHolder's view is clicked on. Implementations should usually pass
* this event up to the adapter.
*
* @param clicked The view that was clicked on.
*/
abstract override fun onClick(clicked: View)
abstract override fun onLongClick(clicked: View): Boolean
fun setStyle(isEditable: Boolean, binding: ListItemSettingBinding) {
val opacity = if (isEditable) 1.0f else 0.5f
binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity
binding.textSettingValue.alpha = opacity
binding.buttonClear.isEnabled = isEditable
}
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
binding.switchWidget.isEnabled = isEditable
val opacity = if (isEditable) 1.0f else 0.5f
binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity
binding.buttonClear.isEnabled = isEditable
}
}

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