Does this work? I hate Git sometimes
This commit is contained in:
parent
3fd22b8781
commit
dab709d300
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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"
|
|
@ -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
|
|
@ -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>
|
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 256 KiB |
|
@ -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;
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -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>
|
|
@ -1 +1 @@
|
|||
Subproject commit ee87132385014449c4cd33236c661d57539071c1
|
||||
Subproject commit d79f8652510b8bd1f89c90be2ab65fc8940056eb
|
|
@ -1 +1 @@
|
|||
Subproject commit 53a952a7313f2c78d93a4f6805abe570fe35f96b
|
||||
Subproject commit d65908c3d416e331e075c3a5ffe7bc670112a018
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||
# 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
|
||||
|
||||
paths_to_check="src/ CMakeLists.txt"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue