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