Remove Qt from emulator (#3733)

* actions removal

* removed qt dir

# Conflicts:
#	src/qt_gui/check_update.cpp
#	src/qt_gui/translations/ar_SA.ts
#	src/qt_gui/translations/ca_ES.ts
#	src/qt_gui/translations/da_DK.ts
#	src/qt_gui/translations/de_DE.ts
#	src/qt_gui/translations/el_GR.ts
#	src/qt_gui/translations/en_US.ts
#	src/qt_gui/translations/es_ES.ts
#	src/qt_gui/translations/fa_IR.ts
#	src/qt_gui/translations/fi_FI.ts
#	src/qt_gui/translations/fr_FR.ts
#	src/qt_gui/translations/hu_HU.ts
#	src/qt_gui/translations/id_ID.ts
#	src/qt_gui/translations/it_IT.ts
#	src/qt_gui/translations/ja_JP.ts
#	src/qt_gui/translations/ko_KR.ts
#	src/qt_gui/translations/lt_LT.ts
#	src/qt_gui/translations/nb_NO.ts
#	src/qt_gui/translations/nl_NL.ts
#	src/qt_gui/translations/pl_PL.ts
#	src/qt_gui/translations/pt_BR.ts
#	src/qt_gui/translations/pt_PT.ts
#	src/qt_gui/translations/ro_RO.ts
#	src/qt_gui/translations/ru_RU.ts
#	src/qt_gui/translations/sl_SI.ts
#	src/qt_gui/translations/sq_AL.ts
#	src/qt_gui/translations/sr_CS.ts
#	src/qt_gui/translations/sv_SE.ts
#	src/qt_gui/translations/tr_TR.ts
#	src/qt_gui/translations/uk_UA.ts
#	src/qt_gui/translations/ur_PK.ts
#	src/qt_gui/translations/vi_VN.ts
#	src/qt_gui/translations/zh_CN.ts
#	src/qt_gui/translations/zh_TW.ts

* removed CMakePresets for qt builds

* clear cmakelists from qt

* sync config file with qtlauncher

* fixing review stuff

* Remove Qt code from memory patcher and add non-Qt fallback for automatic loading of patches
The second feature is disabled if IPC is present, to avoid conflicts with it.

* Add json submodule

* More Qt removal

* Documentation update

* fix build

* fix REUSE?

* removed qrc file

* fix clang

* Simplify Qt installation instructions for macOS

Removed instructions for installing x86_64 Qt on ARM and x86_64 Macs.

* Remove Qt installation instructions from guide

Removed instructions for downloading and configuring Qt.

---------

Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com>
This commit is contained in:
georgemoralis 2025-10-31 10:28:39 +02:00 committed by GitHub
parent 5cabd6ddd8
commit ed9ffbfb64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
115 changed files with 178 additions and 103623 deletions

View File

@ -1,33 +0,0 @@
# SPDX-FileCopyrightText: 2024 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
#!/bin/bash
if [[ -z $GITHUB_WORKSPACE ]]; then
GITHUB_WORKSPACE="${PWD%/*}"
fi
export Qt6_DIR="/usr/lib/qt6"
export PATH="$Qt6_DIR/bin:$PATH"
export EXTRA_QT_PLUGINS="waylandcompositor"
export EXTRA_PLATFORM_PLUGINS="libqwayland-egl.so;libqwayland-generic.so"
# Prepare Tools for building the AppImage
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-checkrt/releases/download/continuous/linuxdeploy-plugin-checkrt-x86_64.sh
chmod a+x linuxdeploy-x86_64.AppImage
chmod a+x linuxdeploy-plugin-qt-x86_64.AppImage
chmod a+x linuxdeploy-plugin-checkrt-x86_64.sh
# Build AppImage
./linuxdeploy-x86_64.AppImage --appdir AppDir
./linuxdeploy-plugin-checkrt-x86_64.sh --appdir AppDir
cp -a "$GITHUB_WORKSPACE/build/translations" AppDir/usr/bin
./linuxdeploy-x86_64.AppImage --appdir AppDir -d "$GITHUB_WORKSPACE"/dist/net.shadps4.shadPS4.desktop -e "$GITHUB_WORKSPACE"/build/shadps4 -i "$GITHUB_WORKSPACE"/src/images/net.shadps4.shadPS4.svg --plugin qt
rm AppDir/usr/plugins/multimedia/libgstreamermediaplugin.so
./linuxdeploy-x86_64.AppImage --appdir AppDir --output appimage
mv shadPS4-x86_64.AppImage Shadps4-qt.AppImage

View File

@ -95,64 +95,6 @@ jobs:
name: shadps4-win64-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: ${{github.workspace}}/build/shadPS4.exe
windows-qt:
runs-on: windows-2025
needs: get-info
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Setup Qt
uses: jurplel/install-qt-action@v4
with:
version: 6.9.3
host: windows
target: desktop
arch: win64_msvc2022_64
archives: qtbase qttools
modules: qtmultimedia
- name: Cache CMake Configuration
uses: actions/cache@v4
env:
cache-name: ${{ runner.os }}-qt-ninja-cache-cmake-configuration
with:
path: |
${{github.workspace}}/build
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
restore-keys: |
${{ env.cache-name }}-
- name: Cache CMake Build
uses: hendrikmuhs/ccache-action@v1.2.19
env:
cache-name: ${{ runner.os }}-qt-cache-cmake-build
with:
append-timestamp: false
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
- name: Configure CMake
run: cmake --fresh -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $env:NUMBER_OF_PROCESSORS
- name: Deploy and Package
run: |
mkdir upload
mkdir upload/qtplugins
move build/shadPS4.exe upload
cp dist/qt.conf upload/qt.conf
windeployqt --plugindir upload/qtplugins --no-compiler-runtime --no-system-d3d-compiler --no-system-dxc-compiler --dir upload upload/shadPS4.exe
Compress-Archive -Path upload/* -DestinationPath shadps4-win64-qt-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}.zip
- name: Upload Windows Qt artifact
uses: actions/upload-artifact@v4
with:
name: shadps4-win64-qt-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: upload/
macos-sdl:
runs-on: macos-15
needs: get-info
@ -204,67 +146,6 @@ jobs:
name: shadps4-macos-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: upload/
macos-qt:
runs-on: macos-15
needs: get-info
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Setup latest Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest
- name: Setup Qt
uses: jurplel/install-qt-action@v4
with:
version: 6.9.3
host: mac
target: desktop
arch: clang_64
archives: qtbase qttools
modules: qtmultimedia
- name: Cache CMake Configuration
uses: actions/cache@v4
env:
cache-name: ${{ runner.os }}-qt-cache-cmake-configuration
with:
path: |
${{github.workspace}}/build
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
restore-keys: |
${{ env.cache-name }}-
- name: Cache CMake Build
uses: hendrikmuhs/ccache-action@v1.2.19
env:
cache-name: ${{runner.os}}-qt-cache-cmake-build
with:
append-timestamp: false
create-symlink: true
key: ${{env.cache-name}}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
variant: sccache
- name: Configure CMake
run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_OSX_ARCHITECTURES=x86_64 -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(sysctl -n hw.ncpu)
- name: Package and Upload macOS Qt artifact
run: |
mkdir upload
mv ${{github.workspace}}/build/shadps4.app upload
macdeployqt upload/shadps4.app
tar cf shadps4-macos-qt.tar.gz -C upload .
- uses: actions/upload-artifact@v4
with:
name: shadps4-macos-qt-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: shadps4-macos-qt.tar.gz
linux-sdl:
runs-on: ubuntu-24.04
needs: get-info
@ -326,58 +207,6 @@ jobs:
name: shadps4-linux-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: Shadps4-sdl.AppImage
linux-qt:
runs-on: ubuntu-24.04
needs: get-info
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Add LLVM repository
run: |
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo add-apt-repository 'deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main'
- name: Install dependencies
run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 clang-19 mold build-essential qt6-base-dev qt6-tools-dev qt6-multimedia-dev libasound2-dev libpulse-dev libopenal-dev libudev-dev
- name: Cache CMake Configuration
uses: actions/cache@v4
env:
cache-name: ${{ runner.os }}-qt-cache-cmake-configuration
with:
path: |
${{github.workspace}}/build
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
restore-keys: |
${{ env.cache-name }}-
- name: Cache CMake Build
uses: hendrikmuhs/ccache-action@v1.2.19
env:
cache-name: ${{ runner.os }}-qt-cache-cmake-build
with:
append-timestamp: false
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
- name: Configure CMake
run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang-19 -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=mold" -DCMAKE_SHARED_LINKER_FLAGS="-fuse-ld=mold" -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc)
- name: Run AppImage packaging script
run: ./.github/linux-appimage-qt.sh
- name: Package and Upload Linux Qt artifact
run: |
tar cf shadps4-linux-qt.tar.gz -C ${{github.workspace}}/build shadps4
- uses: actions/upload-artifact@v4
with:
name: shadps4-linux-qt-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: Shadps4-qt.AppImage
linux-sdl-gcc:
runs-on: ubuntu-24.04
needs: get-info
@ -414,45 +243,9 @@ jobs:
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc)
linux-qt-gcc:
runs-on: ubuntu-24.04
needs: get-info
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Install dependencies
run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 gcc-14 mold build-essential qt6-base-dev qt6-tools-dev qt6-multimedia-dev libasound2-dev libpulse-dev libopenal-dev libudev-dev
- name: Cache CMake Configuration
uses: actions/cache@v4
env:
cache-name: ${{ runner.os }}-qt-gcc-cache-cmake-configuration
with:
path: |
${{github.workspace}}/build
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
restore-keys: |
${{ env.cache-name }}-
- name: Cache CMake Build
uses: hendrikmuhs/ccache-action@v1.2.19
env:
cache-name: ${{ runner.os }}-qt-gcc-cache-cmake-build
with:
append-timestamp: false
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
- name: Configure CMake
run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=mold" -DCMAKE_SHARED_LINKER_FLAGS="-fuse-ld=mold" -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc)
pre-release:
if: github.ref == 'refs/heads/main' && github.repository == 'shadps4-emu/shadPS4' && github.event_name == 'push'
needs: [get-info, windows-sdl, windows-qt, macos-sdl, macos-qt, linux-sdl, linux-qt]
needs: [get-info, windows-sdl, macos-sdl, linux-sdl]
runs-on: ubuntu-latest
steps:
- name: Download all artifacts

3
.gitmodules vendored
View File

@ -110,3 +110,6 @@
path = externals/MoltenVK
url = https://github.com/KhronosGroup/MoltenVK.git
shallow = true
[submodule "externals/json"]
path = externals/json
url = https://github.com/nlohmann/json.git

View File

@ -31,7 +31,6 @@ if(UNIX AND NOT APPLE)
endif()
endif()
option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses a minimal SDL-based UI instead" OFF)
option(ENABLE_DISCORD_RPC "Enable the Discord RPC integration" ON)
option(ENABLE_UPDATER "Enables the options to updater" ON)
@ -220,10 +219,6 @@ if(NOT (GIT_REMOTE_URL_LOWER MATCHES "shadps4-emu/shadps4" AND (GIT_BRANCH STREQ
set(ENABLE_UPDATER OFF)
endif()
if(WIN32 AND ENABLE_QT_GUI AND NOT CMAKE_PREFIX_PATH)
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/DetectQtInstallation.cmake")
endif ()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(Boost 1.84.0 CONFIG)
find_package(FFmpeg 5.1.2 MODULE)
@ -262,30 +257,6 @@ endif()
add_subdirectory(externals)
include_directories(src)
if(ENABLE_QT_GUI)
find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent LinguistTools Network Multimedia)
qt_standard_project_setup()
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(QT_TRANSLATIONS "${PROJECT_SOURCE_DIR}/src/qt_gui/translations")
file(GLOB_RECURSE TRANSLATIONS_TS ${QT_TRANSLATIONS}/*.ts)
set_source_files_properties(${TRANSLATIONS_TS} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/translations")
qt_add_translation(TRANSLATIONS_QM ${TRANSLATIONS_TS})
set(TRANSLATIONS_QRC ${CMAKE_CURRENT_BINARY_DIR}/translations/translations.qrc)
file(WRITE ${TRANSLATIONS_QRC} "<RCC><qresource prefix=\"translations\">\n")
foreach (QM ${TRANSLATIONS_QM})
get_filename_component(QM_FILE ${QM} NAME)
file(APPEND ${TRANSLATIONS_QRC} "<file>${QM_FILE}</file>\n")
endforeach (QM)
file(APPEND ${TRANSLATIONS_QRC} "</qresource></RCC>")
qt_add_resources(TRANSLATIONS ${TRANSLATIONS_QRC})
endif()
set(AJM_LIB src/core/libraries/ajm/ajm.cpp
src/core/libraries/ajm/ajm.h
src/core/libraries/ajm/ajm_at9.cpp
@ -1066,112 +1037,26 @@ set(EMULATOR src/emulator.cpp
src/sdl_window.cpp
)
# The above is shared in SDL and Qt version (TODO share them all)
if(ENABLE_QT_GUI)
qt_add_resources(RESOURCE_FILES src/shadps4.qrc)
if (ENABLE_UPDATER)
set(UPDATER src/qt_gui/check_update.cpp
src/qt_gui/check_update.h
)
endif()
set(QT_GUI src/qt_gui/about_dialog.cpp
src/qt_gui/about_dialog.h
src/qt_gui/about_dialog.ui
src/qt_gui/background_music_player.cpp
src/qt_gui/background_music_player.h
src/qt_gui/cheats_patches.cpp
src/qt_gui/cheats_patches.h
src/qt_gui/compatibility_info.cpp
src/qt_gui/compatibility_info.h
src/qt_gui/control_settings.cpp
src/qt_gui/control_settings.h
src/qt_gui/control_settings.ui
src/qt_gui/kbm_gui.cpp
src/qt_gui/kbm_gui.h
src/qt_gui/kbm_gui.ui
src/qt_gui/main_window_ui.h
src/qt_gui/main_window.cpp
src/qt_gui/main_window.h
src/qt_gui/gui_context_menus.h
src/qt_gui/game_list_utils.h
src/qt_gui/game_info.cpp
src/qt_gui/game_info.h
src/qt_gui/game_list_frame.cpp
src/qt_gui/game_list_frame.h
src/qt_gui/game_grid_frame.cpp
src/qt_gui/game_grid_frame.h
src/qt_gui/game_install_dialog.cpp
src/qt_gui/game_install_dialog.h
src/qt_gui/trophy_viewer.cpp
src/qt_gui/trophy_viewer.h
src/qt_gui/elf_viewer.cpp
src/qt_gui/elf_viewer.h
src/qt_gui/kbm_config_dialog.cpp
src/qt_gui/kbm_config_dialog.h
src/qt_gui/kbm_help_dialog.cpp
src/qt_gui/kbm_help_dialog.h
src/qt_gui/main_window_themes.cpp
src/qt_gui/main_window_themes.h
src/qt_gui/log_presets_dialog.cpp
src/qt_gui/log_presets_dialog.h
src/qt_gui/settings_dialog.cpp
src/qt_gui/settings_dialog.h
src/qt_gui/settings_dialog.ui
src/qt_gui/main.cpp
src/qt_gui/gui_settings.cpp
src/qt_gui/gui_settings.h
src/qt_gui/settings.cpp
src/qt_gui/settings.h
src/qt_gui/sdl_event_wrapper.cpp
src/qt_gui/sdl_event_wrapper.h
src/qt_gui/hotkeys.h
src/qt_gui/hotkeys.cpp
src/qt_gui/hotkeys.ui
${EMULATOR}
${RESOURCE_FILES}
${TRANSLATIONS}
${UPDATER}
add_executable(shadps4
${AUDIO_CORE}
${IMGUI}
${INPUT}
${COMMON}
${CORE}
${SHADER_RECOMPILER}
${VIDEO_CORE}
${EMULATOR}
src/main.cpp
src/emulator.cpp
src/emulator.h
src/sdl_window.h
src/sdl_window.cpp
)
endif()
if (ENABLE_QT_GUI)
qt_add_executable(shadps4
${AUDIO_CORE}
${IMGUI}
${INPUT}
${QT_GUI}
${COMMON}
${CORE}
${SHADER_RECOMPILER}
${VIDEO_CORE}
${EMULATOR}
src/images/shadPS4.icns
)
else()
add_executable(shadps4
${AUDIO_CORE}
${IMGUI}
${INPUT}
${COMMON}
${CORE}
${SHADER_RECOMPILER}
${VIDEO_CORE}
${EMULATOR}
src/main.cpp
src/emulator.cpp
src/emulator.h
src/sdl_window.h
src/sdl_window.cpp
)
endif()
create_target_directory_groups(shadps4)
target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG)
target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 pugixml::pugixml stb::headers libusb::usb lfreist-hwinfo::hwinfo)
target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 pugixml::pugixml stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json)
target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h")
target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h")
@ -1191,20 +1076,9 @@ endif()
if (APPLE)
# Include MoltenVK, along with an ICD file so it can be found by the system Vulkan loader if used for loading layers.
if (ENABLE_QT_GUI)
set(MVK_BUNDLE_PATH "Resources/vulkan/icd.d")
set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path/../${MVK_BUNDLE_PATH}")
set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR}/shadps4.app/Contents/${MVK_BUNDLE_PATH})
add_custom_command(
OUTPUT ${MVK_DST}
COMMAND ${CMAKE_COMMAND} -E make_directory ${MVK_DST})
else()
set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path")
set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR})
endif()
set(MVK_DYLIB_SRC ${CMAKE_CURRENT_BINARY_DIR}/externals/MoltenVK/MoltenVK/libMoltenVK.dylib)
set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path")
set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR})
set(MVK_DYLIB_SRC ${CMAKE_CURRENT_BINARY_DIR}/externals/MoltenVK/libMoltenVK.dylib)
set(MVK_DYLIB_DST ${MVK_DST}/libMoltenVK.dylib)
set(MVK_ICD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/externals/MoltenVK/MoltenVK/icd/MoltenVK_icd.json)
set(MVK_ICD_DST ${MVK_DST}/MoltenVK_icd.json)
@ -1230,14 +1104,6 @@ if (APPLE)
target_link_libraries(shadps4 PRIVATE date::date-tz epoll-shim)
endif()
if (ENABLE_QT_GUI)
target_link_libraries(shadps4 PRIVATE Qt6::Widgets Qt6::Concurrent Qt6::Network Qt6::Multimedia)
add_definitions(-DENABLE_QT_GUI)
if (ENABLE_UPDATER)
add_definitions(-DENABLE_UPDATER)
endif()
endif()
if (WIN32)
target_link_libraries(shadps4 PRIVATE mincore wepoll)
@ -1315,25 +1181,6 @@ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/imgui/renderer)
add_dependencies(shadps4 ImGui_Resources)
target_include_directories(shadps4 PRIVATE ${IMGUI_RESOURCES_INCLUDE})
if (ENABLE_QT_GUI)
set_target_properties(shadps4 PROPERTIES
# WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/dist/MacOSBundleInfo.plist.in"
MACOSX_BUNDLE_ICON_FILE "shadPS4.icns"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APP_VERSION}"
)
set_source_files_properties(src/images/shadPS4.icns PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
endif()
if (UNIX AND NOT APPLE)
if (ENABLE_QT_GUI)
find_package(OpenSSL REQUIRED)
target_link_libraries(shadps4 PRIVATE ${OPENSSL_LIBRARIES})
endif()
endif()
# Discord RPC
if (ENABLE_DISCORD_RPC)
@ -1342,10 +1189,3 @@ endif()
# Install rules
install(TARGETS shadps4 BUNDLE DESTINATION .)
if (ENABLE_QT_GUI AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
install(FILES "dist/net.shadps4.shadPS4.desktop" DESTINATION "share/applications")
install(FILES "dist/net.shadps4.shadPS4.metainfo.xml" DESTINATION "share/metainfo")
install(FILES ".github/shadps4.png" DESTINATION "share/icons/hicolor/512x512/apps" RENAME "net.shadps4.shadPS4.png")
install(FILES "src/images/net.shadps4.shadPS4.svg" DESTINATION "share/icons/hicolor/scalable/apps")
endif()

View File

@ -15,15 +15,6 @@
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "x64-Clang-Debug-Qt",
"displayName": "Clang x64 Debug with Qt",
"inherits": ["x64-Clang-Base"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"ENABLE_QT_GUI": "ON"
}
},
{
"name": "x64-Clang-Release",
"displayName": "Clang x64 Release",
@ -32,15 +23,6 @@
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "x64-Clang-Release-Qt",
"displayName": "Clang x64 Release with Qt",
"inherits": ["x64-Clang-Base"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"ENABLE_QT_GUI": "ON"
}
},
{
"name": "x64-Clang-RelWithDebInfo",
"displayName": "Clang x64 RelWithDebInfo",
@ -48,15 +30,6 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "x64-Clang-RelWithDebInfo-Qt",
"displayName": "Clang x64 RelWithDebInfo with Qt",
"inherits": ["x64-Clang-Base"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"ENABLE_QT_GUI": "ON"
}
}
]
}

View File

@ -12,18 +12,6 @@
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-Release-Qt",
"generator": "Ninja",
"configurationType": "Release",
"buildRoot": "${projectDir}\\Build\\${name}",
"installRoot": "${projectDir}\\Install\\${name}",
"cmakeCommandArgs": "-DENABLE_QT_GUI=ON",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-Debug",
"generator": "Ninja",
@ -36,18 +24,6 @@
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-Debug-Qt",
"generator": "Ninja",
"configurationType": "Debug",
"buildRoot": "${projectDir}\\Build\\${name}",
"installRoot": "${projectDir}\\Install\\${name}",
"cmakeCommandArgs": "-DENABLE_QT_GUI=ON",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-RelWithDebInfo",
"generator": "Ninja",
@ -59,18 +35,6 @@
"ctestCommandArgs": "",
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-RelWithDebInfo-Qt",
"generator": "Ninja",
"configurationType": "RelWithDebInfo",
"buildRoot": "${projectDir}\\Build\\${name}",
"installRoot": "${projectDir}\\Install\\${name}",
"cmakeCommandArgs": "-DENABLE_QT_GUI=ON",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
}
]
}

View File

@ -36,6 +36,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
**shadPS4** is an early **PlayStation 4** emulator for **Windows**, **Linux** and **macOS** written in C++.
> [!IMPORTANT]
> This is the emulator core, which does not include a GUI. If you just want to use the emulator as an end user, download the [**QtLauncher**](https://github.com/shadps4-emu/shadps4-qtlauncher/releases) instead.
If you encounter problems or have doubts, do not hesitate to look at the [**Quickstart**](https://github.com/shadps4-emu/shadPS4/wiki/I.-Quick-start-%5BUsers%5D).\
To verify that a game works, you can look at [**shadPS4 Game Compatibility**](https://github.com/shadps4-compatibility/shadps4-game-compatibility).\
To discuss shadPS4 development, suggest ideas or to ask for help, join our [**Discord server**](https://discord.gg/bFJxfftGW6).\
@ -55,9 +58,6 @@ This project began for fun. Given our limited free time, it may take some time b
# Building
> [!IMPORTANT]
> If you want to use shadPS4 to play your games, you don't have to follow the build instructions, you can simply download the emulator from either the [**release tab**](https://github.com/shadps4-emu/shadPS4/releases) or the [**action tab**](https://github.com/shadps4-emu/shadPS4/actions).
## Windows
Check the build instructions for [**Windows**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-windows.md).
@ -73,6 +73,22 @@ Check the build instructions for [**macOS**](https://github.com/shadps4-emu/shad
> [!IMPORTANT]
> macOS users need at least macOS 15.4 to run shadPS4. Due to GPU issues there are currently heavy bugs on Intel Macs.
# Usage examples
> [!IMPORTANT]
> For a user-friendly GUI, download the [**QtLauncher**](https://github.com/shadps4-emu/shadps4-qtlauncher/releases).
To get the list of all available commands and also a more detailed description of what each command does, please refer to the `--help` flag's output.
Below is a list of commonly used command patterns:
```sh
shadPS4 CUSA00001 # Searches for a game folder called CUSA00001 in the list of game install folders, and boots it.
shadPS4 --fullscreen true --config-clean CUSA00001 # the game argument is always the last one,
shadPS4 -g CUSA00001 --fullscreen true --config-clean # ...unless manually specified otherwise.
shadPS4 /path/to/game.elf # Boots a PS4 ELF file directly. Useful if you want to boot an executable that is not named eboot.bin.
shadPS4 CUSA00001 -- -flag1 -flag2 # Passes '-flag1' and '-flag2' to the game executable in argv.
```
# Debugging and reporting issues
For more information on how to test, debug and report issues with the emulator or games, read the [**Debugging documentation**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/Debugging/Debugging.md).
@ -160,15 +176,6 @@ Logo is done by [**Xphalnos**](https://github.com/Xphalnos)
If you want to contribute, please read the [**CONTRIBUTING.md**](https://github.com/shadps4-emu/shadPS4/blob/main/CONTRIBUTING.md) file.\
Open a PR and we'll check it :)
# Translations
If you want to translate shadPS4 to your language we use [**Crowdin**](https://crowdin.com/project/shadps4-emulator).
# Contributors
<a href="https://github.com/shadps4-emu/shadPS4/graphs/contributors">
<img src="https://contrib.rocks/image?repo=shadps4-emu/shadPS4&max=24">
</a>
# Special Thanks

View File

@ -74,7 +74,6 @@ path = [
"src/images/trophy.wav",
"src/images/hotkey.png",
"src/images/game_settings.png",
"src/shadps4.qrc",
"src/shadps4.rc",
]
precedence = "aggregate"
@ -130,9 +129,4 @@ SPDX-License-Identifier = "MIT"
[[annotations]]
path = "src/video_core/host_shaders/fsr/*"
SPDX-FileCopyrightText = "Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved."
SPDX-License-Identifier = "MIT"
[[annotations]]
path = "dist/qt.conf"
SPDX-FileCopyrightText = "shadPS4 Emulator Project"
SPDX-License-Identifier = "GPL-2.0-or-later"
SPDX-License-Identifier = "MIT"

View File

@ -1,28 +0,0 @@
# SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
set(highest_version "0")
set(CANDIDATE_DRIVES A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
foreach(drive ${CANDIDATE_DRIVES})
file(GLOB kits LIST_DIRECTORIES true CONFIGURE_DEPENDS "${drive}:/Qt/*/msvc*_64")
foreach(kit IN LISTS kits)
get_filename_component(version_dir "${kit}" DIRECTORY)
get_filename_component(kit_version "${version_dir}" NAME)
message(STATUS "DetectQtInstallation.cmake: Detected Qt: ${kit}")
if (kit_version VERSION_GREATER highest_version)
set(highest_version "${kit_version}")
set(QT_PREFIX "${kit}")
endif()
endforeach()
endforeach()
if(QT_PREFIX)
set(CMAKE_PREFIX_PATH "${QT_PREFIX}" CACHE PATH "Qt prefix autodetected" FORCE)
message(STATUS "DetectQtInstallation.cmake: Choose newest Qt: ${QT_PREFIX}")
else()
message(STATUS "DetectQtInstallation.cmake: No QtDirectory found in <drive>:/Qt please set CMAKE_PREFIX_PATH manually")
endif()

2
dist/qt.conf vendored
View File

@ -1,2 +0,0 @@
[Paths]
plugins = "./qtplugins"

View File

@ -17,8 +17,7 @@ First and foremost, Clang 18 is the **recommended compiler** as it is used for o
sudo apt install build-essential clang git cmake libasound2-dev \
libpulse-dev libopenal-dev libssl-dev zlib1g-dev libedit-dev \
libudev-dev libevdev-dev libsdl2-dev libjack-dev libsndio-dev \
qt6-base-dev qt6-tools-dev qt6-multimedia-dev libvulkan-dev \
vulkan-validationlayers libpng-dev
libvulkan-dev vulkan-validationlayers libpng-dev
```
#### Fedora
@ -27,8 +26,6 @@ sudo apt install build-essential clang git cmake libasound2-dev \
sudo dnf install clang git cmake libatomic alsa-lib-devel \
pipewire-jack-audio-connection-kit-devel openal-soft-devel \
openssl-devel libevdev-devel libudev-devel libXext-devel \
qt6-qtbase-devel qt6-qtbase-private-devel \
qt6-qtmultimedia-devel qt6-qtsvg-devel qt6-qttools-devel \
vulkan-devel vulkan-validation-layers libpng-devel libuuid-devel
```
@ -36,8 +33,7 @@ sudo dnf install clang git cmake libatomic alsa-lib-devel \
```bash
sudo pacman -S base-devel clang git cmake sndio jack2 openal \
qt6-base qt6-declarative qt6-multimedia qt6-tools sdl2 \
vulkan-validation-layers libpng
sdl2 vulkan-validation-layers libpng
```
**Note**: The `shadps4-git` AUR package is not maintained by any of the developers, and it uses the default compiler, which is often set to GCC. Use at your own discretion.
@ -48,9 +44,7 @@ sudo pacman -S base-devel clang git cmake sndio jack2 openal \
sudo zypper install clang git cmake libasound2 libpulse-devel \
libsndio7 libjack-devel openal-soft-devel libopenssl-devel \
zlib-devel libedit-devel systemd-devel libevdev-devel \
qt6-base-devel qt6-multimedia-devel qt6-svg-devel \
qt6-linguist-devel qt6-gui-private-devel vulkan-devel \
vulkan-validationlayers libpng-devel
vulkan-devel vulkan-validationlayers libpng-devel
```
#### NixOS
@ -90,12 +84,12 @@ There are 3 options you can choose from. Option 1 is **highly recommended**.
1. Generate the build directory in the shadPS4 directory.
```bash
cmake -S . -B build/ -DENABLE_QT_GUI=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
cmake -S . -B build/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
```
To disable the Qt GUI, remove the `-DENABLE_QT_GUI=ON` flag. To change the build type (for debugging), add `-DCMAKE_BUILD_TYPE=Debug`.
To change the build type (for debugging), add `-DCMAKE_BUILD_TYPE=Debug`.
2. Use CMake to build the project:
1. Use CMake to build the project:
```bash
cmake --build ./build --parallel$(nproc)
@ -103,18 +97,12 @@ cmake --build ./build --parallel$(nproc)
If your computer freezes during this step, this could be caused by excessive system resource usage. In that case, remove `--parallel$(nproc)`.
Now run the emulator. If Qt was enabled at configure time:
Now run the emulator to get the list of options:
```bash
./build/shadps4
```
Otherwise, specify the path to your game's boot file:
```bash
./build/shadps4 /"PATH"/"TO"/"GAME"/"FOLDER"/eboot.bin
```
You can also specify the Game ID as an argument for which game to boot, as long as the folder containing the games is specified in config.toml (example: Bloodborne (US) is CUSA00900).
#### Option 2: Configuring with cmake-gui
@ -142,10 +130,6 @@ Go to Settings, filter by `@ext:ms-vscode.cmake-tools configure` and disable thi
![image](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/Linux/1.png)
If you wish to build with the Qt GUI, add `-DENABLE_QT_GUI=ON` to the configure arguments:
![image](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/Linux/2.png)
On the CMake tab, change the options as you wish, but make sure that it looks similar to or exactly like this:
![image](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/Linux/4.png)

View File

@ -24,21 +24,6 @@ eval $(/opt/homebrew/bin/brew shellenv)
brew install clang-format cmake
```
Next, install x86_64 Qt. You can skip these steps and move on to **Cloning and compiling** if you do not intend to build the Qt GUI.
**If you are on an ARM Mac:**
```
# Installs x86_64 Homebrew to /usr/local
arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Installs libraries.
arch -x86_64 /usr/local/bin/brew install qt@6
```
**If you are on an x86_64 Mac:**
```
brew install qt@6
```
### Cloning and compiling:
Clone the repository recursively:
@ -52,8 +37,6 @@ Generate the build directory in the shadPS4 directory:
cmake -S . -B build/ -DCMAKE_OSX_ARCHITECTURES=x86_64
```
If you want to build the Qt GUI, add `-DENABLE_QT_GUI=ON` to the end of this command as well.
Enter the directory:
```
cd build/

View File

@ -22,23 +22,6 @@ Once you are within the installer:
2. Go to "Individual Components" tab then search and select both `C++ Clang Compiler for Windows` and `MSBuild support for LLVM`
3. Continue the installation
### (Prerequisite) Download [**Qt**](https://doc.qt.io/qt-6/get-and-install-qt.html)
Beware, this requires you to create a Qt account. If you do not want to do this, please follow the MSYS2/MinGW compilation method instead.
1. Under the current, non beta version of Qt, select the option `MSVC 2022 64-bit` or similar, as well as `QT Multimedia`.
If you are on Windows on ARM / Qualcomm Snapdragon Elite X, select `MSVC 2022 ARM64` instead.
Go through the installation normally. If you know what you are doing, you may unselect individual components that eat up too much disk space.
2. Download and install [Qt Visual Studio Tools](https://marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools2022)
Once you are finished, you will have to configure Qt within Visual Studio:
1. Tools -> Options -> Qt -> Versions
2. Add a new Qt version and navigate it to the correct folder. Should look like so: `C:\Qt\<QtVersion>\msvc2022_64`
3. Enable the default checkmark on the new version you just created.
### (Prerequisite) Download [**Git for Windows**](https://git-scm.com/download/win)
Go through the Git for Windows installation as normal
@ -79,13 +62,10 @@ Normal x86-based computers, follow:
1. Open "MSYS2 MINGW64" from your new applications
2. Run `pacman -Syu`, let it complete;
3. Run `pacman -S --needed git mingw-w64-x86_64-binutils mingw-w64-x86_64-clang mingw-w64-x86_64-cmake mingw-w64-x86_64-rapidjson mingw-w64-x86_64-ninja mingw-w64-x86_64-ffmpeg`
1. Optional (Qt only): run `pacman -S --needed mingw-w64-x86_64-qt6-base mingw-w64-x86_64-qt6-tools mingw-w64-x86_64-qt6-multimedia`
4. Run `git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4`
5. Run `cd shadPS4`
6. Run `cmake -S . -B build -DCMAKE_C_COMPILER="clang.exe" -DCMAKE_CXX_COMPILER="clang++.exe" -DCMAKE_CXX_FLAGS="-O2 -march=native"`
1. Optional (Qt only): add `-DENABLE_QT_GUI=ON`
7. Run `cmake --build build`
1. Optional (Qt only): run `windeployqt6 build/shadps4.exe`
8. To run the finished product, run `./build/shadPS4.exe`
ARM64-based computers, follow:
@ -97,9 +77,7 @@ ARM64-based computers, follow:
4. Run `git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4`
5. Run `cd shadPS4`
6. Run `cmake -S . -B build -DCMAKE_C_COMPILER="clang.exe" -DCMAKE_CXX_COMPILER="clang++.exe" -DCMAKE_CXX_FLAGS="-O2 -march=native"`
1. Optional (Qt only): add `-DENABLE_QT_GUI=ON`
7. Run `cmake --build build`
1. Optional (Qt only): run `windeployqt6 build/shadps4.exe`
8. To run the finished product, run `./build/shadPS4.exe`
## Note on MSYS2 builds

View File

@ -245,3 +245,7 @@ endif()
if (WIN32)
add_subdirectory(ext-wepoll)
endif()
#nlohmann json
set(JSON_BuildTests OFF CACHE INTERNAL "")
add_subdirectory(json)

1
externals/json vendored Submodule

@ -0,0 +1 @@
Subproject commit 55f93686c01528224f448c19128836e7df245f72

View File

@ -17,7 +17,6 @@ pkgs.mkShell {
pkgs.alsa-lib
pkgs.libpulseaudio
pkgs.openal
pkgs.openssl
pkgs.zlib
pkgs.libedit
pkgs.udev
@ -25,9 +24,6 @@ pkgs.mkShell {
pkgs.SDL2
pkgs.jack2
pkgs.sndio
pkgs.qt6.qtbase
pkgs.qt6.qttools
pkgs.qt6.qtmultimedia
pkgs.vulkan-headers
pkgs.vulkan-utility-libraries
@ -44,16 +40,12 @@ pkgs.mkShell {
pkgs.xorg.xcbutilwm
pkgs.sdl3
pkgs.stb
pkgs.qt6.qtwayland
pkgs.wayland-protocols
pkgs.libpng
];
shellHook = ''
echo "Entering shadPS4 dev shell"
export QT_QPA_PLATFORM="wayland"
export QT_PLUGIN_PATH="${pkgs.qt6.qtwayland}/lib/qt-6/plugins:${pkgs.qt6.qtbase}/lib/qt-6/plugins"
export QML2_IMPORT_PATH="${pkgs.qt6.qtbase}/lib/qt-6/qml"
export CMAKE_PREFIX_PATH="${pkgs.vulkan-headers}:$CMAKE_PREFIX_PATH"
# OpenGL

View File

@ -139,13 +139,10 @@ static ConfigEntry<double> trophyNotificationDuration(6.0);
static ConfigEntry<string> logFilter("");
static ConfigEntry<string> logType("sync");
static ConfigEntry<string> userName("shadPS4");
static ConfigEntry<string> chooseHomeTab("General");
static ConfigEntry<bool> isShowSplash(false);
static ConfigEntry<string> isSideTrophy("right");
static ConfigEntry<bool> isConnectedToNetwork(false);
static bool enableDiscordRPC = false;
static bool checkCompatibilityOnStartup = false;
static bool compatibilityData = false;
static std::filesystem::path sys_modules_path = {};
// Input
@ -203,7 +200,6 @@ static ConfigEntry<bool> isFpsColor(true);
static ConfigEntry<bool> logEnabled(true);
// GUI
static bool load_game_size = true;
static std::vector<GameInstallDir> settings_install_dirs = {};
std::vector<bool> install_dirs_enabled = {};
std::filesystem::path settings_addon_install_dir = {};
@ -218,9 +214,19 @@ static string trophyKey = "";
// Config version, used to determine if a user's config file is outdated.
static string config_version = Common::g_scm_rev;
// These two entries aren't stored in the config
// These entries aren't stored in the config
static bool overrideControllerColor = false;
static int controllerCustomColorRGB[3] = {0, 0, 255};
static bool isGameRunning = false;
static bool load_auto_patches = true;
bool getGameRunning() {
return isGameRunning;
}
void setGameRunning(bool running) {
isGameRunning = running;
}
std::filesystem::path getSysModulesPath() {
if (sys_modules_path.empty()) {
@ -278,10 +284,6 @@ void setTrophyKey(string key) {
trophyKey = key;
}
bool GetLoadGameSizeEnabled() {
return load_game_size;
}
std::filesystem::path GetSaveDataPath() {
if (save_data_path.empty()) {
return Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "savedata";
@ -293,10 +295,6 @@ void setVolumeSlider(int volumeValue, bool is_game_specific) {
volumeSlider.set(volumeValue, is_game_specific);
}
void setLoadGameSizeEnabled(bool enable) {
load_game_size = enable;
}
bool isNeoModeConsole() {
return isNeo.get();
}
@ -309,8 +307,10 @@ int getExtraDmemInMbytes() {
return extraDmemInMbytes.get();
}
void setExtraDmemInMbytes(int value) {
extraDmemInMbytes.base_value = 0;
void setExtraDmemInMbytes(int value, bool is_game_specific) {
// Disable setting in global config
is_game_specific ? extraDmemInMbytes.game_specific_value = value
: extraDmemInMbytes.base_value = 0;
}
bool getIsFullscreen() {
@ -389,10 +389,6 @@ string getUserName() {
return userName.get();
}
string getChooseHomeTab() {
return chooseHomeTab.get();
}
bool getUseSpecialPad() {
return useSpecialPad.get();
}
@ -508,14 +504,6 @@ void setVkGuestMarkersEnabled(bool enable, bool is_game_specific) {
vkGuestMarkers.set(enable, is_game_specific);
}
bool getCompatibilityEnabled() {
return compatibilityData;
}
bool getCheckCompatibilityOnStartup() {
return checkCompatibilityOnStartup;
}
bool getIsConnectedToNetwork() {
return isConnectedToNetwork.get();
}
@ -600,6 +588,14 @@ void setVkSyncValidation(bool enable, bool is_game_specific) {
vkValidationSync.set(enable, is_game_specific);
}
void setVkCoreValidation(bool enable, bool is_game_specific) {
vkValidationCore.set(enable, is_game_specific);
}
void setVkGpuValidation(bool enable, bool is_game_specific) {
vkValidationGpu.set(enable, is_game_specific);
}
void setRdocEnabled(bool enable, bool is_game_specific) {
rdocEnable.set(enable, is_game_specific);
}
@ -680,10 +676,6 @@ void setUserName(const string& name, bool is_game_specific) {
userName.set(name, is_game_specific);
}
void setChooseHomeTab(const string& type, bool is_game_specific) {
chooseHomeTab.set(type, is_game_specific);
}
void setUseSpecialPad(bool use) {
useSpecialPad.base_value = use;
}
@ -696,14 +688,6 @@ void setIsMotionControlsEnabled(bool use, bool is_game_specific) {
isMotionControlsEnabled.set(use, is_game_specific);
}
void setCompatibilityEnabled(bool use) {
compatibilityData = use;
}
void setCheckCompatibilityOnStartup(bool use) {
checkCompatibilityOnStartup = use;
}
bool addGameInstallDir(const std::filesystem::path& dir, bool enabled) {
for (const auto& install_dir : settings_install_dirs) {
if (install_dir.path == dir) {
@ -833,6 +817,13 @@ void setRcasAttenuation(int value, bool is_game_specific) {
rcasAttenuation.set(value, is_game_specific);
}
bool getLoadAutoPatches() {
return load_auto_patches;
}
void setLoadAutoPatches(bool enable) {
load_auto_patches = enable;
}
void load(const std::filesystem::path& path, bool is_game_specific) {
// If the configuration file does not exist, create it and return, unless it is game specific
std::error_code error;
@ -874,12 +865,8 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
userName.setFromToml(general, "userName", is_game_specific);
isShowSplash.setFromToml(general, "showSplash", is_game_specific);
isSideTrophy.setFromToml(general, "sideTrophy", is_game_specific);
compatibilityData = toml::find_or<bool>(general, "compatibilityEnabled", compatibilityData);
checkCompatibilityOnStartup = toml::find_or<bool>(general, "checkCompatibilityOnStartup",
checkCompatibilityOnStartup);
isConnectedToNetwork.setFromToml(general, "isConnectedToNetwork", is_game_specific);
chooseHomeTab.setFromToml(general, "chooseHomeTab", is_game_specific);
defaultControllerID.setFromToml(general, "defaultControllerID", is_game_specific);
sys_modules_path = toml::find_fs_path_or(general, "sysModulesPath", sys_modules_path);
}
@ -957,8 +944,6 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
if (data.contains("GUI")) {
const toml::value& gui = data.at("GUI");
load_game_size = toml::find_or<bool>(gui, "loadGameSizeEnabled", load_game_size);
const auto install_dir_array =
toml::find_or<std::vector<std::u8string>>(gui, "installDirs", {});
@ -1062,7 +1047,6 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
logFilter.setTomlValue(data, "General", "logFilter", is_game_specific);
logType.setTomlValue(data, "General", "logType", is_game_specific);
userName.setTomlValue(data, "General", "userName", is_game_specific);
chooseHomeTab.setTomlValue(data, "General", "chooseHomeTab", is_game_specific);
isShowSplash.setTomlValue(data, "General", "showSplash", is_game_specific);
isSideTrophy.setTomlValue(data, "General", "sideTrophy", is_game_specific);
isNeo.setTomlValue(data, "General", "isPS4Pro", is_game_specific);
@ -1104,6 +1088,8 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
gpuId.setTomlValue(data, "Vulkan", "gpuId", is_game_specific);
vkValidation.setTomlValue(data, "Vulkan", "validation", is_game_specific);
vkValidationSync.setTomlValue(data, "Vulkan", "validation_sync", is_game_specific);
vkValidationCore.setTomlValue(data, "Vulkan", "validation_core", is_game_specific);
vkValidationGpu.setTomlValue(data, "Vulkan", "validation_gpu", is_game_specific);
vkCrashDiagnostic.setTomlValue(data, "Vulkan", "crashDiagnostic", is_game_specific);
vkHostMarkers.setTomlValue(data, "Vulkan", "hostMarkers", is_game_specific);
vkGuestMarkers.setTomlValue(data, "Vulkan", "guestMarkers", is_game_specific);
@ -1149,13 +1135,10 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
// Non game-specific entries
data["General"]["enableDiscordRPC"] = enableDiscordRPC;
data["General"]["compatibilityEnabled"] = compatibilityData;
data["General"]["checkCompatibilityOnStartup"] = checkCompatibilityOnStartup;
data["General"]["sysModulesPath"] = string{fmt::UTF(sys_modules_path.u8string()).data};
data["GUI"]["installDirs"] = install_dirs;
data["GUI"]["installDirsEnabled"] = install_dirs_enabled;
data["GUI"]["saveDataPath"] = string{fmt::UTF(save_data_path.u8string()).data};
data["GUI"]["loadGameSizeEnabled"] = load_game_size;
data["GUI"]["addonInstallDir"] =
string{fmt::UTF(settings_addon_install_dir.u8string()).data};
data["Debug"]["ConfigVersion"] = config_version;
@ -1169,8 +1152,6 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
data["GPU"]["internalScreenWidth"] = internalScreenWidth.base_value;
data["GPU"]["internalScreenHeight"] = internalScreenHeight.base_value;
data["GPU"]["patchShaders"] = shouldPatchShaders.base_value;
data["Vulkan"]["validation_core"] = vkValidationCore.base_value;
data["Vulkan"]["validation_gpu"] = vkValidationGpu.base_value;
data["Debug"]["FPSColor"] = isFpsColor.base_value;
}
@ -1205,7 +1186,6 @@ void setDefaultValues(bool is_game_specific) {
logFilter.set("", is_game_specific);
logType.set("sync", is_game_specific);
userName.set("shadPS4", is_game_specific);
chooseHomeTab.set("General", is_game_specific);
isShowSplash.set(false, is_game_specific);
isSideTrophy.set("right", is_game_specific);
@ -1258,8 +1238,6 @@ void setDefaultValues(bool is_game_specific) {
// General
enableDiscordRPC = false;
compatibilityData = false;
checkCompatibilityOnStartup = false;
// Input
useSpecialPad.base_value = false;
@ -1278,9 +1256,6 @@ void setDefaultValues(bool is_game_specific) {
internalScreenWidth.base_value = 1280;
internalScreenHeight.base_value = 720;
// GUI
load_game_size = true;
// Debug
isFpsColor.base_value = true;
}

View File

@ -27,6 +27,8 @@ void load(const std::filesystem::path& path, bool is_game_specific = false);
void save(const std::filesystem::path& path, bool is_game_specific = false);
void resetGameSpecificValue(std::string entry);
bool getGameRunning();
void setGameRunning(bool running);
int getVolumeSlider();
void setVolumeSlider(int volumeValue, bool is_game_specific = false);
std::string getTrophyKey();
@ -79,6 +81,10 @@ bool vkValidationEnabled();
void setVkValidation(bool enable, bool is_game_specific = false);
bool vkValidationSyncEnabled();
void setVkSyncValidation(bool enable, bool is_game_specific = false);
bool vkValidationGpuEnabled();
void setVkGpuValidation(bool enable, bool is_game_specific = false);
bool vkValidationCoreEnabled();
void setVkCoreValidation(bool enable, bool is_game_specific = false);
bool getVkCrashDiagnosticEnabled();
void setVkCrashDiagnosticEnabled(bool enable, bool is_game_specific = false);
bool getVkHostMarkersEnabled();
@ -97,11 +103,10 @@ double getTrophyNotificationDuration();
void setTrophyNotificationDuration(double newTrophyNotificationDuration,
bool is_game_specific = false);
int getCursorHideTimeout();
void setCursorHideTimeout(int newcursorHideTimeout);
std::string getMainOutputDevice();
void setMainOutputDevice(std::string device);
void setMainOutputDevice(std::string device, bool is_game_specific = false);
std::string getPadSpkOutputDevice();
void setPadSpkOutputDevice(std::string device);
void setPadSpkOutputDevice(std::string device, bool is_game_specific = false);
std::string getMicDevice();
void setCursorHideTimeout(int newcursorHideTimeout, bool is_game_specific = false);
void setMicDevice(std::string device, bool is_game_specific = false);
@ -122,10 +127,8 @@ void setNeoMode(bool enable, bool is_game_specific = false);
bool isDevKitConsole();
void setDevKitConsole(bool enable, bool is_game_specific = false);
bool vkValidationCoreEnabled(); // no set
bool vkValidationGpuEnabled(); // no set
int getExtraDmemInMbytes();
void setExtraDmemInMbytes(int value);
void setExtraDmemInMbytes(int value, bool is_game_specific = false);
bool getIsMotionControlsEnabled();
void setIsMotionControlsEnabled(bool use, bool is_game_specific = false);
std::string getDefaultControllerID();
@ -143,18 +146,14 @@ void setRcasAttenuation(int value, bool is_game_specific = false);
bool getIsConnectedToNetwork();
void setConnectedToNetwork(bool enable, bool is_game_specific = false);
void setUserName(const std::string& name, bool is_game_specific = false);
void setChooseHomeTab(const std::string& type, bool is_game_specific = false);
std::filesystem::path getSysModulesPath();
void setSysModulesPath(const std::filesystem::path& path);
bool getLoadAutoPatches();
void setLoadAutoPatches(bool enable);
// TODO
bool GetLoadGameSizeEnabled();
std::filesystem::path GetSaveDataPath();
void setLoadGameSizeEnabled(bool enable);
bool getCompatibilityEnabled();
bool getCheckCompatibilityOnStartup();
std::string getUserName();
std::string getChooseHomeTab();
bool GetUseUnifiedInputConfig();
void SetUseUnifiedInputConfig(bool use);
bool GetOverrideControllerColor();
@ -164,8 +163,6 @@ void SetControllerCustomColor(int r, int b, int g);
void setGameInstallDirs(const std::vector<std::filesystem::path>& dirs_config);
void setAllGameInstallDirs(const std::vector<GameInstallDir>& dirs_config);
void setSaveDataPath(const std::filesystem::path& path);
void setCompatibilityEnabled(bool use);
void setCheckCompatibilityOnStartup(bool use);
// Gui
bool addGameInstallDir(const std::filesystem::path& dir, bool enabled = true);
void removeGameInstallDir(const std::filesystem::path& dir);

View File

@ -3,20 +3,12 @@
#include <algorithm>
#include <codecvt>
#include <fstream>
#include <sstream>
#include <string>
#include <nlohmann/json.hpp>
#include <pugixml.hpp>
#ifdef ENABLE_QT_GUI
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QListView>
#include <QMessageBox>
#include <QString>
#include <QXmlStreamReader>
#endif
#include "common/config.h"
#include "common/elf_info.h"
#include "common/logging/log.h"
#include "common/path_util.h"
@ -28,7 +20,7 @@ namespace MemoryPatcher {
EXPORT uintptr_t g_eboot_address;
uint64_t g_eboot_image_size;
std::string g_game_serial;
std::string patchFile;
std::string patch_file;
bool patches_applied = false;
std::vector<patchInfo> pending_patches;
@ -122,223 +114,55 @@ std::string convertValueToHex(const std::string type, const std::string valueStr
void ApplyPendingPatches();
void OnGameLoaded() {
if (!patchFile.empty()) {
std::filesystem::path patchDir = Common::FS::GetUserPath(Common::FS::PathType::PatchesDir);
void ApplyPatchesFromXML(std::filesystem::path path) {
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(path.c_str());
auto filePath = (patchDir / patchFile).native();
auto* param_sfo = Common::Singleton<PSF>::Instance();
auto app_version = param_sfo->GetString("APP_VER").value_or("Unknown version");
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(filePath.c_str());
if (result) {
auto patchXML = doc.child("Patch");
for (pugi::xml_node_iterator it = patchXML.children().begin();
it != patchXML.children().end(); ++it) {
auto* param_sfo = Common::Singleton<PSF>::Instance();
auto app_version = param_sfo->GetString("APP_VER").value_or("Unknown version");
if (std::string(it->name()) == "Metadata") {
if (std::string(it->attribute("isEnabled").value()) == "true") {
std::string currentPatchName = it->attribute("Name").value();
std::string metadataAppVer = it->attribute("AppVer").value();
bool versionMatches = metadataAppVer == app_version;
if (result) {
auto patchXML = doc.child("Patch");
for (pugi::xml_node_iterator it = patchXML.children().begin();
it != patchXML.children().end(); ++it) {
auto patchList = it->first_child();
for (pugi::xml_node_iterator patchLineIt = patchList.children().begin();
patchLineIt != patchList.children().end(); ++patchLineIt) {
std::string type = patchLineIt->attribute("Type").value();
if (!versionMatches && type != "mask" && type != "mask_jump32")
continue;
if (std::string(it->name()) == "Metadata") {
if (std::string(it->attribute("isEnabled").value()) == "true") {
std::string currentPatchName = it->attribute("Name").value();
std::string metadataAppVer = it->attribute("AppVer").value();
bool versionMatches = metadataAppVer == app_version;
auto patchList = it->first_child();
for (pugi::xml_node_iterator patchLineIt = patchList.children().begin();
patchLineIt != patchList.children().end(); ++patchLineIt) {
std::string type = patchLineIt->attribute("Type").value();
if (!versionMatches && type != "mask" && type != "mask_jump32")
continue;
std::string currentPatchName = it->attribute("Name").value();
for (pugi::xml_node_iterator patchLineIt = patchList.children().begin();
patchLineIt != patchList.children().end(); ++patchLineIt) {
std::string type = patchLineIt->attribute("Type").value();
std::string address = patchLineIt->attribute("Address").value();
std::string patchValue = patchLineIt->attribute("Value").value();
std::string maskOffsetStr =
patchLineIt->attribute("Offset").value();
std::string targetStr = "";
std::string sizeStr = "";
if (type == "mask_jump32") {
targetStr = patchLineIt->attribute("Target").value();
sizeStr = patchLineIt->attribute("Size").value();
} else {
patchValue = convertValueToHex(type, patchValue);
}
bool littleEndian = false;
if (type == "bytes16" || type == "bytes32" || type == "bytes64") {
littleEndian = true;
}
MemoryPatcher::PatchMask patchMask = MemoryPatcher::PatchMask::None;
int maskOffsetValue = 0;
if (type == "mask")
patchMask = MemoryPatcher::PatchMask::Mask;
if (type == "mask_jump32")
patchMask = MemoryPatcher::PatchMask::Mask_Jump32;
if ((type == "mask" || type == "mask_jump32") &&
!maskOffsetStr.empty()) {
maskOffsetValue = std::stoi(maskOffsetStr, 0, 10);
}
MemoryPatcher::PatchMemory(currentPatchName, address, patchValue,
targetStr, sizeStr, false, littleEndian,
patchMask, maskOffsetValue);
}
}
}
}
}
} else {
LOG_ERROR(Loader, "couldnt patch parse xml : {}", result.description());
}
}
ApplyPendingPatches();
#ifdef ENABLE_QT_GUI
// We use the QT headers for the xml and json parsing, this define is only true on QT builds
QString patchDir;
Common::FS::PathToQString(patchDir, Common::FS::GetUserPath(Common::FS::PathType::PatchesDir));
QDir dir(patchDir);
QStringList folders = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString& folder : folders) {
QString filesJsonPath = patchDir + "/" + folder + "/files.json";
QFile jsonFile(filesJsonPath);
if (!jsonFile.open(QIODevice::ReadOnly)) {
LOG_ERROR(Loader, "Unable to open files.json for reading in repository {}",
folder.toStdString());
continue;
}
QByteArray jsonData = jsonFile.readAll();
jsonFile.close();
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
QJsonObject jsonObject = jsonDoc.object();
QString selectedFileName;
QString serial = QString::fromStdString(g_game_serial);
for (auto it = jsonObject.constBegin(); it != jsonObject.constEnd(); ++it) {
QString filePath = it.key();
QJsonArray idsArray = it.value().toArray();
if (idsArray.contains(QJsonValue(serial))) {
selectedFileName = filePath;
break;
}
}
if (selectedFileName.isEmpty()) {
LOG_ERROR(Loader, "No patch file found for the current serial in repository {}",
folder.toStdString());
continue;
}
QString filePath = patchDir + "/" + folder + "/" + selectedFileName;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
LOG_ERROR(Loader, "Unable to open the file for reading.");
continue;
}
QByteArray xmlData = file.readAll();
file.close();
QString newXmlData;
QXmlStreamReader xmlReader(xmlData);
bool isEnabled = false;
std::string currentPatchName;
auto* param_sfo = Common::Singleton<PSF>::Instance();
auto app_version = param_sfo->GetString("APP_VER").value_or("Unknown version");
bool versionMatches = true;
while (!xmlReader.atEnd()) {
xmlReader.readNext();
if (xmlReader.isStartElement()) {
QJsonArray patchLines;
if (xmlReader.name() == QStringLiteral("Metadata")) {
QString name = xmlReader.attributes().value("Name").toString();
currentPatchName = name.toStdString();
QString appVer = xmlReader.attributes().value("AppVer").toString();
// Check and update the isEnabled attribute
isEnabled = false;
for (const QXmlStreamAttribute& attr : xmlReader.attributes()) {
if (attr.name() == QStringLiteral("isEnabled")) {
isEnabled = (attr.value().toString() == "true");
}
}
versionMatches = (appVer.toStdString() == app_version);
} else if (xmlReader.name() == QStringLiteral("PatchList")) {
QJsonArray linesArray;
while (!xmlReader.atEnd() &&
!(xmlReader.tokenType() == QXmlStreamReader::EndElement &&
xmlReader.name() == QStringLiteral("PatchList"))) {
xmlReader.readNext();
if (xmlReader.tokenType() == QXmlStreamReader::StartElement &&
xmlReader.name() == QStringLiteral("Line")) {
QXmlStreamAttributes attributes = xmlReader.attributes();
QJsonObject lineObject;
lineObject["Type"] = attributes.value("Type").toString();
lineObject["Address"] = attributes.value("Address").toString();
lineObject["Value"] = attributes.value("Value").toString();
lineObject["Offset"] = attributes.value("Offset").toString();
if (lineObject["Type"].toString() == "mask_jump32") {
lineObject["Target"] = attributes.value("Target").toString();
lineObject["Size"] = attributes.value("Size").toString();
}
linesArray.append(lineObject);
}
}
patchLines = linesArray;
if (isEnabled) {
foreach (const QJsonValue& value, patchLines) {
QJsonObject lineObject = value.toObject();
QString type = lineObject["Type"].toString();
if ((type != "mask" && type != "mask_jump32") && !versionMatches)
continue;
QString address = lineObject["Address"].toString();
QString patchValue = lineObject["Value"].toString();
QString maskOffsetStr = lineObject["Offset"].toString();
QString targetStr;
QString sizeStr;
std::string address = patchLineIt->attribute("Address").value();
std::string patchValue = patchLineIt->attribute("Value").value();
std::string maskOffsetStr = patchLineIt->attribute("Offset").value();
std::string targetStr = "";
std::string sizeStr = "";
if (type == "mask_jump32") {
targetStr = lineObject["Target"].toString();
sizeStr = lineObject["Size"].toString();
targetStr = patchLineIt->attribute("Target").value();
sizeStr = patchLineIt->attribute("Size").value();
} else {
patchValue = QString::fromStdString(convertValueToHex(
type.toStdString(), patchValue.toStdString()));
patchValue = convertValueToHex(type, patchValue);
}
bool littleEndian = false;
if (type == "bytes16" || type == "bytes32" || type == "bytes64")
if (type == "bytes16" || type == "bytes32" || type == "bytes64") {
littleEndian = true;
}
MemoryPatcher::PatchMask patchMask = MemoryPatcher::PatchMask::None;
int maskOffsetValue = 0;
@ -350,28 +174,53 @@ void OnGameLoaded() {
patchMask = MemoryPatcher::PatchMask::Mask_Jump32;
if ((type == "mask" || type == "mask_jump32") &&
!maskOffsetStr.toStdString().empty()) {
maskOffsetValue = std::stoi(maskOffsetStr.toStdString(), 0, 10);
!maskOffsetStr.empty()) {
maskOffsetValue = std::stoi(maskOffsetStr, 0, 10);
}
MemoryPatcher::PatchMemory(
currentPatchName, address.toStdString(), patchValue.toStdString(),
targetStr.toStdString(), sizeStr.toStdString(), false, littleEndian,
patchMask, maskOffsetValue);
MemoryPatcher::PatchMemory(currentPatchName, address, patchValue,
targetStr, sizeStr, false, littleEndian,
patchMask, maskOffsetValue);
}
}
}
}
}
if (xmlReader.hasError()) {
LOG_ERROR(Loader, "Failed to parse XML for {}", g_game_serial);
} else {
LOG_INFO(Loader, "Patches loaded successfully, repository: {}", folder.toStdString());
}
ApplyPendingPatches();
} else {
LOG_ERROR(Loader, "Could not parse patch XML: {}", result.description());
}
#endif
}
void OnGameLoaded() {
std::filesystem::path patch_dir = Common::FS::GetUserPath(Common::FS::PathType::PatchesDir);
if (!patch_file.empty()) {
auto file_path = (patch_dir / patch_file).native();
if (std::filesystem::exists(patch_file)) {
ApplyPatchesFromXML(patch_file);
} else {
ApplyPatchesFromXML(file_path);
}
} else if (Config::getLoadAutoPatches()) {
for (auto const& repo : std::filesystem::directory_iterator(patch_dir)) {
if (!repo.is_directory()) {
continue;
}
std::ifstream json_file{repo.path() / "files.json"};
nlohmann::json available_patches = nlohmann::json::parse(json_file);
std::filesystem::path game_patch_file;
for (auto const& [filename, serials] : available_patches.items()) {
if (std::find(serials.begin(), serials.end(), g_game_serial) != serials.end()) {
game_patch_file = repo.path() / filename;
break;
}
}
if (std::filesystem::exists(game_patch_file)) {
ApplyPatchesFromXML(game_patch_file);
}
}
}
ApplyPendingPatches();
}
void AddPatchToQueue(patchInfo patchToAdd) {

View File

@ -17,7 +17,7 @@ namespace MemoryPatcher {
extern EXPORT uintptr_t g_eboot_address;
extern uint64_t g_eboot_image_size;
extern std::string g_game_serial;
extern std::string patchFile;
extern std::string patch_file;
enum PatchMask : uint8_t {
None,

View File

@ -25,10 +25,6 @@
#endif
#endif
#ifdef ENABLE_QT_GUI
#include <QString>
#endif
namespace Common::FS {
namespace fs = std::filesystem;
@ -88,13 +84,6 @@ static std::optional<std::filesystem::path> GetBundleParentDirectory() {
#endif
static auto UserPaths = [] {
#if defined(__APPLE__) && defined(ENABLE_QT_GUI)
// Set the current path to the directory containing the app bundle.
if (const auto bundle_dir = GetBundleParentDirectory()) {
std::filesystem::current_path(*bundle_dir);
}
#endif
// Try the portable user directory first.
auto user_dir = std::filesystem::current_path() / PORTABLE_DIR;
if (!std::filesystem::exists(user_dir)) {
@ -229,22 +218,4 @@ std::optional<fs::path> FindGameByID(const fs::path& dir, const std::string& gam
return std::nullopt;
}
#ifdef ENABLE_QT_GUI
void PathToQString(QString& result, const std::filesystem::path& path) {
#ifdef _WIN32
result = QString::fromStdWString(path.wstring());
#else
result = QString::fromStdString(path.string());
#endif
}
std::filesystem::path PathFromQString(const QString& path) {
#ifdef _WIN32
return std::filesystem::path(path.toStdWString());
#else
return std::filesystem::path(path.toStdString());
#endif
}
#endif
} // namespace Common::FS

View File

@ -7,10 +7,6 @@
#include <optional>
#include <vector>
#ifdef ENABLE_QT_GUI
class QString; // to avoid including <QString> in this header
#endif
namespace Common::FS {
enum class PathType {
@ -99,25 +95,6 @@ constexpr auto LOG_FILE = "shad_log.txt";
*/
void SetUserPath(PathType user_path, const std::filesystem::path& new_path);
#ifdef ENABLE_QT_GUI
/**
* Converts an std::filesystem::path to a QString.
* The native underlying string of a path is wstring on Windows and string on POSIX.
*
* @param result The resulting QString
* @param path The path to convert
*/
void PathToQString(QString& result, const std::filesystem::path& path);
/**
* Converts a QString to an std::filesystem::path.
* The native underlying string of a path is wstring on Windows and string on POSIX.
*
* @param path The path to convert
*/
[[nodiscard]] std::filesystem::path PathFromQString(const QString& path);
#endif
/**
* Recursively searches for a game directory by its ID.
* Limits search depth to prevent excessive filesystem traversal.

View File

@ -70,6 +70,8 @@ void IPC::Init() {
return;
}
Config::setLoadAutoPatches(false);
input_thread = std::jthread([this] {
Common::SetCurrentThreadName("IPC Read thread");
this->InputLoop();

View File

@ -15,10 +15,6 @@
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "core/ipc/ipc.h"
#ifdef ENABLE_QT_GUI
#include <QtCore>
#endif
#include "common/assert.h"
#ifdef ENABLE_DISCORD_RPC
#include "common/discord_rpc_handler.h"
#endif
@ -341,8 +337,6 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
}
#endif
// Start the timer (Play Time)
#ifdef ENABLE_QT_GUI
if (!id.empty()) {
start_time = std::chrono::steady_clock::now();
@ -354,7 +348,6 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
}
}).detach();
}
#endif
args.insert(args.begin(), eboot_name.generic_string());
linker->Execute(args);
@ -364,9 +357,7 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
window->WaitEvent();
}
#ifdef ENABLE_QT_GUI
UpdatePlayTime(id);
#endif
std::quick_exit(0);
}
@ -389,9 +380,9 @@ void Emulator::Restart(std::filesystem::path eboot_path,
args.push_back("--ignore-game-patch");
}
if (!MemoryPatcher::patchFile.empty()) {
if (!MemoryPatcher::patch_file.empty()) {
args.push_back("--patch");
args.push_back(MemoryPatcher::patchFile);
args.push_back(MemoryPatcher::patch_file);
}
args.push_back("--wait-for-pid");

View File

@ -86,13 +86,14 @@ int main(int argc, char* argv[]) {
{"-p",
[&](int& i) {
if (i + 1 < argc) {
MemoryPatcher::patchFile = argv[++i];
MemoryPatcher::patch_file = argv[++i];
} else {
std::cerr << "Error: Missing argument for -p/--patch\n";
exit(1);
}
}},
{"--patch", [&](int& i) { arg_map["-p"](i); }},
{"-i", [&](int&) { Core::FileSys::MntPoints::ignore_game_patches = true; }},
{"--ignore-game-patch", [&](int& i) { arg_map["-i"](i); }},
{"-f",

View File

@ -1,195 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QDesktopServices>
#include <QEvent>
#include <QGraphicsDropShadowEffect>
#include <QImage>
#include <QLabel>
#include <QPixmap>
#include <common/config.h>
#include "about_dialog.h"
#include "main_window_themes.h"
#include "ui_about_dialog.h"
AboutDialog::AboutDialog(std::shared_ptr<gui_settings> gui_settings, QWidget* parent)
: QDialog(parent), ui(new Ui::AboutDialog), m_gui_settings(std::move(gui_settings)) {
ui->setupUi(this);
preloadImages();
ui->image_1->setAttribute(Qt::WA_Hover, true);
ui->image_2->setAttribute(Qt::WA_Hover, true);
ui->image_3->setAttribute(Qt::WA_Hover, true);
ui->image_4->setAttribute(Qt::WA_Hover, true);
ui->image_5->setAttribute(Qt::WA_Hover, true);
ui->image_1->installEventFilter(this);
ui->image_2->installEventFilter(this);
ui->image_3->installEventFilter(this);
ui->image_4->installEventFilter(this);
ui->image_5->installEventFilter(this);
}
AboutDialog::~AboutDialog() {
delete ui;
}
void AboutDialog::preloadImages() {
originalImages[0] = ui->image_1->pixmap().copy();
originalImages[1] = ui->image_2->pixmap().copy();
originalImages[2] = ui->image_3->pixmap().copy();
originalImages[3] = ui->image_4->pixmap().copy();
originalImages[4] = ui->image_5->pixmap().copy();
for (int i = 0; i < 5; ++i) {
QImage image = originalImages[i].toImage();
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
QColor color = image.pixelColor(x, y);
color.setRed(255 - color.red());
color.setGreen(255 - color.green());
color.setBlue(255 - color.blue());
image.setPixelColor(x, y, color);
}
}
invertedImages[i] = QPixmap::fromImage(image);
}
updateImagesForCurrentTheme();
}
void AboutDialog::updateImagesForCurrentTheme() {
Theme currentTheme = static_cast<Theme>(m_gui_settings->GetValue(gui::gen_theme).toInt());
bool isDarkTheme = (currentTheme == Theme::Dark || currentTheme == Theme::Green ||
currentTheme == Theme::Blue || currentTheme == Theme::Violet);
if (isDarkTheme) {
ui->image_1->setPixmap(invertedImages[0]);
ui->image_2->setPixmap(invertedImages[1]);
ui->image_3->setPixmap(invertedImages[2]);
ui->image_4->setPixmap(invertedImages[3]);
ui->image_5->setPixmap(invertedImages[4]);
} else {
ui->image_1->setPixmap(originalImages[0]);
ui->image_2->setPixmap(originalImages[1]);
ui->image_3->setPixmap(originalImages[2]);
ui->image_4->setPixmap(originalImages[3]);
ui->image_5->setPixmap(originalImages[4]);
}
}
bool AboutDialog::eventFilter(QObject* obj, QEvent* event) {
if (event->type() == QEvent::Enter) {
if (obj == ui->image_1) {
if (isDarkTheme()) {
ui->image_1->setPixmap(originalImages[0]);
} else {
ui->image_1->setPixmap(invertedImages[0]);
}
applyHoverEffect(ui->image_1);
} else if (obj == ui->image_2) {
if (isDarkTheme()) {
ui->image_2->setPixmap(originalImages[1]);
} else {
ui->image_2->setPixmap(invertedImages[1]);
}
applyHoverEffect(ui->image_2);
} else if (obj == ui->image_3) {
if (isDarkTheme()) {
ui->image_3->setPixmap(originalImages[2]);
} else {
ui->image_3->setPixmap(invertedImages[2]);
}
applyHoverEffect(ui->image_3);
} else if (obj == ui->image_4) {
if (isDarkTheme()) {
ui->image_4->setPixmap(originalImages[3]);
} else {
ui->image_4->setPixmap(invertedImages[3]);
}
applyHoverEffect(ui->image_4);
} else if (obj == ui->image_5) {
if (isDarkTheme()) {
ui->image_5->setPixmap(originalImages[4]);
} else {
ui->image_5->setPixmap(invertedImages[4]);
}
applyHoverEffect(ui->image_5);
}
} else if (event->type() == QEvent::Leave) {
if (obj == ui->image_1) {
if (isDarkTheme()) {
ui->image_1->setPixmap(invertedImages[0]);
} else {
ui->image_1->setPixmap(originalImages[0]);
}
removeHoverEffect(ui->image_1);
} else if (obj == ui->image_2) {
if (isDarkTheme()) {
ui->image_2->setPixmap(invertedImages[1]);
} else {
ui->image_2->setPixmap(originalImages[1]);
}
removeHoverEffect(ui->image_2);
} else if (obj == ui->image_3) {
if (isDarkTheme()) {
ui->image_3->setPixmap(invertedImages[2]);
} else {
ui->image_3->setPixmap(originalImages[2]);
}
removeHoverEffect(ui->image_3);
} else if (obj == ui->image_4) {
if (isDarkTheme()) {
ui->image_4->setPixmap(invertedImages[3]);
} else {
ui->image_4->setPixmap(originalImages[3]);
}
removeHoverEffect(ui->image_4);
} else if (obj == ui->image_5) {
if (isDarkTheme()) {
ui->image_5->setPixmap(invertedImages[4]);
} else {
ui->image_5->setPixmap(originalImages[4]);
}
removeHoverEffect(ui->image_5);
}
} else if (event->type() == QEvent::MouseButtonPress) {
if (obj == ui->image_1) {
QDesktopServices::openUrl(QUrl("https://github.com/shadps4-emu/shadPS4"));
} else if (obj == ui->image_2) {
QDesktopServices::openUrl(QUrl("https://discord.gg/bFJxfftGW6"));
} else if (obj == ui->image_3) {
QDesktopServices::openUrl(QUrl("https://www.youtube.com/@shadPS4/videos"));
} else if (obj == ui->image_4) {
QDesktopServices::openUrl(QUrl("https://ko-fi.com/shadps4"));
} else if (obj == ui->image_5) {
QDesktopServices::openUrl(QUrl("https://shadps4.net"));
}
return true;
}
return QDialog::eventFilter(obj, event);
}
void AboutDialog::applyHoverEffect(QLabel* label) {
QColor shadowColor = isDarkTheme() ? QColor(0, 0, 0) : QColor(169, 169, 169);
QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect;
shadow->setBlurRadius(5);
shadow->setXOffset(2);
shadow->setYOffset(2);
shadow->setColor(shadowColor);
label->setGraphicsEffect(shadow);
}
void AboutDialog::removeHoverEffect(QLabel* label) {
QColor shadowColor = isDarkTheme() ? QColor(50, 50, 50) : QColor(169, 169, 169);
QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect;
shadow->setBlurRadius(3);
shadow->setXOffset(0);
shadow->setYOffset(0);
shadow->setColor(shadowColor);
label->setGraphicsEffect(shadow);
}
bool AboutDialog::isDarkTheme() const {
Theme currentTheme = static_cast<Theme>(m_gui_settings->GetValue(gui::gen_theme).toInt());
return currentTheme == Theme::Dark || currentTheme == Theme::Green ||
currentTheme == Theme::Blue || currentTheme == Theme::Violet;
}

View File

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDesktopServices>
#include <QDialog>
#include <QLabel>
#include <QPixmap>
#include <QUrl>
#include "gui_settings.h"
namespace Ui {
class AboutDialog;
}
class AboutDialog : public QDialog {
Q_OBJECT
public:
explicit AboutDialog(std::shared_ptr<gui_settings> gui_settings, QWidget* parent = nullptr);
~AboutDialog();
bool eventFilter(QObject* obj, QEvent* event);
private:
Ui::AboutDialog* ui;
void preloadImages();
void updateImagesForCurrentTheme();
void applyHoverEffect(QLabel* label);
void removeHoverEffect(QLabel* label);
bool isDarkTheme() const;
QPixmap originalImages[5];
QPixmap invertedImages[5];
std::shared_ptr<gui_settings> m_gui_settings;
};

View File

@ -1,233 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later -->
<ui version="4.0">
<class>AboutDialog</class>
<widget class="QDialog" name="AboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>780</width>
<height>310</height>
</rect>
</property>
<property name="windowTitle">
<string>About shadPS4</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/images/shadps4.ico</normaloff>:/images/shadps4.ico</iconset>
</property>
<widget class="QLabel" name="shad_logo">
<property name="geometry">
<rect>
<x>15</x>
<y>15</y>
<width>271</width>
<height>271</height>
</rect>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>:/images/shadps4.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="shad_title">
<property name="geometry">
<rect>
<x>310</x>
<y>15</y>
<width>171</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>24</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string notr="true">shadPS4</string>
</property>
</widget>
<widget class="QLabel" name="shad_text">
<property name="geometry">
<rect>
<x>310</x>
<y>60</y>
<width>451</width>
<height>70</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string>shadPS4 is an experimental open-source emulator for the PlayStation 4.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="shad_text_2">
<property name="geometry">
<rect>
<x>310</x>
<y>130</y>
<width>451</width>
<height>70</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string>This software should not be used to play games you have not legally obtained.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="image_1">
<property name="geometry">
<rect>
<x>310</x>
<y>210</y>
<width>80</width>
<height>80</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>:/images/github.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="image_2">
<property name="geometry">
<rect>
<x>400</x>
<y>210</y>
<width>80</width>
<height>80</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>:/images/discord.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="image_3">
<property name="geometry">
<rect>
<x>490</x>
<y>210</y>
<width>80</width>
<height>80</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>:/images/youtube.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="image_4">
<property name="geometry">
<rect>
<x>580</x>
<y>210</y>
<width>80</width>
<height>80</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>:/images/ko-fi.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="image_5">
<property name="geometry">
<rect>
<x>670</x>
<y>210</y>
<width>80</width>
<height>80</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>:/images/website.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "background_music_player.h"
BackgroundMusicPlayer::BackgroundMusicPlayer(QObject* parent) : QObject(parent) {
m_mediaPlayer = new QMediaPlayer(this);
m_audioOutput = new QAudioOutput(this);
m_mediaPlayer->setAudioOutput(m_audioOutput);
}
void BackgroundMusicPlayer::setVolume(int volume) {
float linearVolume = QAudio::convertVolume(volume / 100.0f, QAudio::LogarithmicVolumeScale,
QAudio::LinearVolumeScale);
m_audioOutput->setVolume(linearVolume);
}
void BackgroundMusicPlayer::playMusic(const QString& snd0path, bool loops) {
if (snd0path.isEmpty()) {
stopMusic();
return;
}
const auto newMusic = QUrl::fromLocalFile(snd0path);
if (m_mediaPlayer->playbackState() == QMediaPlayer::PlayingState &&
m_currentMusic == newMusic) {
// already playing the correct music
return;
}
if (loops) {
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
} else {
m_mediaPlayer->setLoops(1);
}
m_currentMusic = newMusic;
m_mediaPlayer->setSource(newMusic);
m_mediaPlayer->play();
}
void BackgroundMusicPlayer::stopMusic() {
m_mediaPlayer->stop();
m_mediaPlayer->setSource(QUrl(""));
}

View File

@ -1,29 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAudioOutput>
#include <QMediaPlayer>
#include <QObject>
class BackgroundMusicPlayer : public QObject {
Q_OBJECT
public:
static BackgroundMusicPlayer& getInstance() {
static BackgroundMusicPlayer instance;
return instance;
}
void setVolume(int volume);
void playMusic(const QString& snd0path, bool loops = true);
void stopMusic();
private:
BackgroundMusicPlayer(QObject* parent = nullptr);
QMediaPlayer* m_mediaPlayer;
QAudioOutput* m_audioOutput;
QUrl m_currentMusic;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,120 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef CHEATS_PATCHES_H
#define CHEATS_PATCHES_H
#include <QCheckBox>
#include <QComboBox>
#include <QGroupBox>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QListView>
#include <QMap>
#include <QNetworkAccessManager>
#include <QPixmap>
#include <QPushButton>
#include <QScrollArea>
#include <QString>
#include <QTabWidget>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QVector>
#include <QWidget>
class CheatsPatches : public QWidget {
Q_OBJECT
public:
CheatsPatches(const QString& gameName, const QString& gameSerial, const QString& gameVersion,
const QString& gameSize, const QPixmap& gameImage, QWidget* parent = nullptr);
~CheatsPatches();
void downloadCheats(const QString& source, const QString& m_gameSerial,
const QString& m_gameVersion, bool showMessageBox);
void downloadPatches(const QString repository, const bool showMessageBox);
void createFilesJson(const QString& repository);
void clearListCheats();
void compatibleVersionNotice(const QString repository);
signals:
void downloadFinished();
private:
// UI Setup and Event Handlers
void setupUI();
void onSaveButtonClicked();
QCheckBox* findCheckBoxByName(const QString& name);
bool eventFilter(QObject* obj, QEvent* event);
void onPatchCheckBoxHovered(QCheckBox* checkBox, bool hovered);
// Cheat and Patch Management
void populateFileListCheats();
void populateFileListPatches();
void addCheatsToLayout(const QJsonArray& modsArray, const QJsonArray& creditsArray);
void addPatchesToLayout(const QString& serial);
void applyCheat(const QString& modName, bool enabled);
void applyPatch(const QString& patchName, bool enabled);
void uncheckAllCheatCheckBoxes();
void updateNoteTextEdit(const QString& patchName);
// Network Manager
QNetworkAccessManager* manager;
// Patch Info Structures
struct MemoryMod {
QString offset;
QString on;
QString off;
};
struct Cheat {
QString name;
QString type;
bool hasHint;
QVector<MemoryMod> memoryMods;
};
struct PatchInfo {
QString name;
QString author;
QString note;
QJsonArray linesArray;
QString serial;
};
// Members
QString m_gameName;
QString m_gameSerial;
QString m_gameVersion;
QString m_gameSize;
QPixmap m_gameImage;
QString m_cheatFilePath;
QMap<QString, Cheat> m_cheats;
QMap<QString, PatchInfo> m_patchInfos;
QVector<QCheckBox*> m_cheatCheckBoxes;
// UI Elements
QVBoxLayout* rightLayout;
QVBoxLayout* patchesGroupBoxLayout;
QGroupBox* patchesGroupBox;
QVBoxLayout* patchesLayout;
QTextEdit* instructionsTextEdit;
QListView* listView_selectFile;
QItemSelectionModel* selectionModel;
QComboBox* patchesComboBox;
QListView* patchesListView;
// Strings
QString defaultTextEdit_MSG;
QString CheatsNotFound_MSG;
QString CheatsDownloadedSuccessfully_MSG;
QString DownloadComplete_MSG;
};
#endif // CHEATS_PATCHES_H

View File

@ -1,633 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QProcess>
#include <QProgressBar>
#include <QPushButton>
#include <QStandardPaths>
#include <QString>
#include <QStringList>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <common/config.h>
#include <common/path_util.h>
#include <common/scm_rev.h>
#include "check_update.h"
using namespace Common::FS;
CheckUpdate::CheckUpdate(std::shared_ptr<gui_settings> gui_settings, const bool showMessage,
QWidget* parent)
: QDialog(parent), m_gui_settings(std::move(gui_settings)),
networkManager(new QNetworkAccessManager(this)) {
setWindowTitle(tr("Auto Updater"));
setFixedSize(0, 0);
CheckForUpdates(showMessage);
}
CheckUpdate::~CheckUpdate() {}
void CheckUpdate::CheckForUpdates(const bool showMessage) {
QString updateChannel;
QUrl url;
bool checkName = true;
while (checkName) {
updateChannel = m_gui_settings->GetValue(gui::gen_updateChannel).toString();
if (updateChannel == "Nightly") {
url = QUrl("https://api.github.com/repos/shadps4-emu/shadPS4/releases");
checkName = false;
} else if (updateChannel == "Release") {
url = QUrl("https://api.github.com/repos/shadps4-emu/shadPS4/releases/latest");
checkName = false;
} else {
if (Common::g_is_release) {
m_gui_settings->SetValue(gui::gen_updateChannel, "Release");
} else {
m_gui_settings->SetValue(gui::gen_updateChannel, "Nightly");
}
}
}
QNetworkRequest request(url);
QNetworkReply* reply = networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, showMessage, updateChannel]() {
if (reply->error() != QNetworkReply::NoError) {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 403) {
QString response = reply->readAll();
if (response.startsWith("{\"message\":\"API rate limit exceeded for")) {
QMessageBox::warning(
this, tr("Auto Updater"),
// clang-format off
tr("The Auto Updater allows up to 60 update checks per hour.\\nYou have reached this limit. Please try again later.").replace("\\n", "\n"));
// clang-format on
} else {
QMessageBox::warning(
this, tr("Error"),
QString(tr("Network error:") + "\n" + reply->errorString()));
}
} else {
QMessageBox::warning(this, tr("Error"),
QString(tr("Network error:") + "\n" + reply->errorString()));
}
reply->deleteLater();
return;
}
QByteArray response = reply->readAll();
QJsonDocument jsonDoc(QJsonDocument::fromJson(response));
if (jsonDoc.isNull()) {
QMessageBox::warning(this, tr("Error"), tr("Failed to parse update information."));
reply->deleteLater();
return;
}
QString downloadUrl;
QString latestVersion;
QString latestRev;
QString latestDate;
QString platformString;
#ifdef Q_OS_WIN
platformString = "win64-qt";
#elif defined(Q_OS_LINUX)
platformString = "linux-qt";
#elif defined(Q_OS_MAC)
platformString = "macos-qt";
#endif
QJsonObject jsonObj;
if (updateChannel == "Nightly") {
QJsonArray jsonArray = jsonDoc.array();
for (const QJsonValue& value : jsonArray) {
jsonObj = value.toObject();
if (jsonObj.contains("prerelease") && jsonObj["prerelease"].toBool()) {
break;
}
}
if (!jsonObj.isEmpty()) {
latestVersion = jsonObj["tag_name"].toString();
} else {
QMessageBox::warning(this, tr("Error"), tr("No pre-releases found."));
reply->deleteLater();
return;
}
} else {
jsonObj = jsonDoc.object();
if (jsonObj.contains("tag_name")) {
latestVersion = jsonObj["tag_name"].toString();
} else {
QMessageBox::warning(this, tr("Error"), tr("Invalid release data."));
reply->deleteLater();
return;
}
}
latestRev = latestVersion.right(40);
latestDate = jsonObj["published_at"].toString();
QJsonArray assets = jsonObj["assets"].toArray();
bool found = false;
for (const QJsonValue& assetValue : assets) {
QJsonObject assetObj = assetValue.toObject();
if (assetObj["name"].toString().contains(platformString)) {
downloadUrl = assetObj["browser_download_url"].toString();
found = true;
break;
}
}
if (!found) {
QMessageBox::warning(
this, tr("Auto Updater"),
// clang-format off
tr("<b>Notice:</b><br><br> Starting from version <b>0.12.0</b>, the Qt version of the emulator will no longer receive direct updates.<br><br>However, the Qt interface remains available through the new official launcher:<br><br><a href='https://github.com/shadps4-emu/shadps4-qtlauncher/releases/'>Qt Launcher</a> - based on the original shadPS4 source code.<br><br>We recommend switching to this launcher to continue receiving updates."));
// clang-format on
reply->deleteLater();
return;
}
QString currentRev = (updateChannel == "Nightly")
? QString::fromStdString(Common::g_scm_rev)
: "v." + QString::fromStdString(Common::g_version);
QString currentDate = Common::g_scm_date;
QDateTime dateTime = QDateTime::fromString(latestDate, Qt::ISODate);
latestDate = dateTime.isValid() ? dateTime.toString("yyyy-MM-dd HH:mm:ss") : "Unknown date";
if (latestRev == currentRev) {
if (showMessage) {
QMessageBox::information(this, tr("Auto Updater"),
tr("Your version is already up to date!"));
}
close();
return;
} else {
setupUI(downloadUrl, latestDate, latestRev, currentDate, currentRev);
}
reply->deleteLater();
});
}
void CheckUpdate::setupUI(const QString& downloadUrl, const QString& latestDate,
const QString& latestRev, const QString& currentDate,
const QString& currentRev) {
QVBoxLayout* layout = new QVBoxLayout(this);
QHBoxLayout* titleLayout = new QHBoxLayout();
QLabel* imageLabel = new QLabel(this);
QPixmap pixmap(":/images/shadps4.png");
imageLabel->setPixmap(pixmap);
imageLabel->setScaledContents(true);
imageLabel->setFixedSize(50, 50);
QLabel* titleLabel = new QLabel("<h1>" + tr("Update Available") + "</h1>", this);
titleLayout->addWidget(imageLabel);
titleLayout->addWidget(titleLabel);
layout->addLayout(titleLayout);
QString updateChannel = m_gui_settings->GetValue(gui::gen_updateChannel).toString();
QString updateText =
QString("<p><b>" + tr("Update Channel") + ": </b>" + updateChannel +
"<br>"
"<table><tr>"
"<td><b>" +
tr("Current Version") +
":</b></td>"
"<td>%1</td>"
"<td>(%2)</td>"
"</tr><tr>"
"<td><b>" +
tr("Latest Version") +
":</b></td>"
"<td>%3</td>"
"<td>(%4)</td>"
"</tr></table></p>")
.arg(updateChannel == "Nightly" ? currentRev.left(7) : currentRev.left(8), currentDate,
updateChannel == "Nightly" ? latestRev.left(7) : latestRev.left(8), latestDate);
QLabel* updateLabel = new QLabel(updateText, this);
layout->addWidget(updateLabel);
// Setup bottom layout with action buttons
autoUpdateCheckBox = new QCheckBox(tr("Check for Updates at Startup"), this);
layout->addWidget(autoUpdateCheckBox);
QHBoxLayout* updatePromptLayout = new QHBoxLayout();
QLabel* updatePromptLabel = new QLabel(tr("Do you want to update?"), this);
updatePromptLayout->addWidget(updatePromptLabel);
yesButton = new QPushButton(tr("Update"), this);
noButton = new QPushButton(tr("No"), this);
yesButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
noButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
QSpacerItem* spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
updatePromptLayout->addItem(spacer);
updatePromptLayout->addWidget(yesButton);
updatePromptLayout->addWidget(noButton);
layout->addLayout(updatePromptLayout);
// Don't show changelog button if:
// The current version is a pre-release and the version to be downloaded is a release.
bool current_isWIP = currentRev.endsWith("WIP", Qt::CaseInsensitive);
bool latest_isWIP = latestRev.endsWith("WIP", Qt::CaseInsensitive);
if (current_isWIP && !latest_isWIP) {
} else {
QTextBrowser* textField = new QTextBrowser(this);
textField->setReadOnly(true);
textField->setFixedWidth(500);
textField->setFixedHeight(200);
textField->setVisible(false);
layout->addWidget(textField);
QPushButton* toggleButton = new QPushButton(tr("Show Changelog"), this);
layout->addWidget(toggleButton);
connect(toggleButton, &QPushButton::clicked,
[this, textField, toggleButton, currentRev, latestRev, downloadUrl, latestDate,
currentDate]() {
if (!textField->isVisible()) {
requestChangelog(currentRev, latestRev, downloadUrl, latestDate,
currentDate);
textField->setVisible(true);
toggleButton->setText(tr("Hide Changelog"));
adjustSize();
textField->setFixedWidth(textField->width() + 20);
} else {
textField->setVisible(false);
toggleButton->setText(tr("Show Changelog"));
adjustSize();
}
});
if (m_gui_settings->GetValue(gui::gen_showChangeLog).toBool()) {
requestChangelog(currentRev, latestRev, downloadUrl, latestDate, currentDate);
textField->setVisible(true);
toggleButton->setText(tr("Hide Changelog"));
adjustSize();
textField->setFixedWidth(textField->width() + 20);
}
}
connect(yesButton, &QPushButton::clicked, this, [this, downloadUrl]() {
yesButton->setEnabled(false);
noButton->setEnabled(false);
DownloadUpdate(downloadUrl);
});
connect(noButton, &QPushButton::clicked, this, [this]() { close(); });
autoUpdateCheckBox->setChecked(m_gui_settings->GetValue(gui::gen_checkForUpdates).toBool());
#if (QT_VERSION < QT_VERSION_CHECK(6, 7, 0))
connect(autoUpdateCheckBox, &QCheckBox::stateChanged, this, [this](int state) {
#else
connect(autoUpdateCheckBox, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state) {
#endif
const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
m_gui_settings->SetValue(gui::gen_checkForUpdates, (state == Qt::Checked));
Config::save(user_dir / "config.toml");
});
setLayout(layout);
}
void CheckUpdate::requestChangelog(const QString& currentRev, const QString& latestRev,
const QString& downloadUrl, const QString& latestDate,
const QString& currentDate) {
QString compareUrlString =
QString("https://api.github.com/repos/shadps4-emu/shadPS4/compare/%1...%2")
.arg(currentRev)
.arg(latestRev);
QUrl compareUrl(compareUrlString);
QNetworkRequest compareRequest(compareUrl);
QNetworkReply* compareReply = networkManager->get(compareRequest);
connect(compareReply, &QNetworkReply::finished, this,
[this, compareReply, downloadUrl, latestDate, latestRev, currentDate, currentRev]() {
if (compareReply->error() != QNetworkReply::NoError) {
QMessageBox::warning(
this, tr("Error"),
QString(tr("Network error:") + "\n%1").arg(compareReply->errorString()));
compareReply->deleteLater();
return;
}
QByteArray compareResponse = compareReply->readAll();
QJsonDocument compareJsonDoc(QJsonDocument::fromJson(compareResponse));
QJsonObject compareJsonObj = compareJsonDoc.object();
QJsonArray commits = compareJsonObj["commits"].toArray();
QString changes;
for (const QJsonValue& commitValue : commits) {
QJsonObject commitObj = commitValue.toObject();
QString message = commitObj["commit"].toObject()["message"].toString();
// Remove texts after first line break, if any, to make it cleaner
int newlineIndex = message.indexOf('\n');
if (newlineIndex != -1) {
message = message.left(newlineIndex);
}
if (!changes.isEmpty()) {
changes += "<br>";
}
changes += "&nbsp;&nbsp;&nbsp;&nbsp;• " + message;
}
// Update the text field with the changelog
QTextBrowser* textField = findChild<QTextBrowser*>();
if (textField) {
QRegularExpression re("\\(\\#(\\d+)\\)");
QString newChanges;
int lastIndex = 0;
QRegularExpressionMatchIterator i = re.globalMatch(changes);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
newChanges += changes.mid(lastIndex, match.capturedStart() - lastIndex);
QString num = match.captured(1);
newChanges +=
QString(
"(<a "
"href=\"https://github.com/shadps4-emu/shadPS4/pull/%1\">#%1</a>)")
.arg(num);
lastIndex = match.capturedEnd();
}
newChanges += changes.mid(lastIndex);
changes = newChanges;
textField->setOpenExternalLinks(true);
textField->setHtml("<h2>" + tr("Changes") + ":</h2>" + changes);
}
compareReply->deleteLater();
});
}
void CheckUpdate::DownloadUpdate(const QString& url) {
QProgressBar* progressBar = new QProgressBar(this);
progressBar->setRange(0, 100);
progressBar->setTextVisible(true);
progressBar->setValue(0);
layout()->addWidget(progressBar);
QNetworkRequest request(url);
QNetworkReply* reply = networkManager->get(request);
connect(reply, &QNetworkReply::downloadProgress, this,
[progressBar](qint64 bytesReceived, qint64 bytesTotal) {
if (bytesTotal > 0) {
int percentage = static_cast<int>((bytesReceived * 100) / bytesTotal);
progressBar->setValue(percentage);
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply, progressBar, url]() {
progressBar->setValue(100);
if (reply->error() != QNetworkReply::NoError) {
QMessageBox::warning(this, tr("Error"),
tr("Network error occurred while trying to access the URL") +
":\n" + url + "\n" + reply->errorString());
reply->deleteLater();
progressBar->deleteLater();
return;
}
QString userPath;
Common::FS::PathToQString(userPath, Common::FS::GetUserPath(Common::FS::PathType::UserDir));
#ifdef Q_OS_WIN
QString tempDownloadPath =
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) +
"/Temp/temp_download_update";
#else
QString tempDownloadPath = userPath + "/temp_download_update";
#endif
QDir dir(tempDownloadPath);
if (!dir.exists()) {
dir.mkpath(".");
}
QString downloadPath = tempDownloadPath + "/temp_download_update.zip";
QFile file(downloadPath);
if (file.open(QIODevice::WriteOnly)) {
file.write(reply->readAll());
file.close();
QMessageBox::information(this, tr("Download Complete"),
tr("The update has been downloaded, press OK to install."));
Install();
} else {
QMessageBox::warning(
this, tr("Error"),
QString(tr("Failed to save the update file at") + ":\n" + downloadPath));
}
reply->deleteLater();
progressBar->deleteLater();
});
}
void CheckUpdate::Install() {
QString userPath;
Common::FS::PathToQString(userPath, Common::FS::GetUserPath(Common::FS::PathType::UserDir));
QString rootPath;
Common::FS::PathToQString(rootPath, std::filesystem::current_path());
QString tempDirPath = userPath + "/temp_download_update";
QString startingUpdate = tr("Starting Update...");
QString binaryStartingUpdate;
for (QChar c : startingUpdate) {
binaryStartingUpdate.append(QString::number(c.unicode(), 2).rightJustified(16, '0'));
}
QString scriptContent;
QString scriptFileName;
QStringList arguments;
QString processCommand;
#ifdef Q_OS_WIN
// On windows, overwrite tempDirPath with AppData/Roaming/shadps4/Temp folder
// due to PowerShell Expand-Archive not being able to handle correctly
// paths in square brackets (ie: ./[shadps4])
tempDirPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) +
"/Temp/temp_download_update";
// Windows Batch Script
scriptFileName = tempDirPath + "/update.ps1";
scriptContent = QStringLiteral(
"Set-ExecutionPolicy Bypass -Scope Process -Force\n"
"$binaryStartingUpdate = '%1'\n"
"$chars = @()\n"
"for ($i = 0; $i -lt $binaryStartingUpdate.Length; $i += 16) {\n"
" $chars += [char]([convert]::ToInt32($binaryStartingUpdate.Substring($i, 16), 2))\n"
"}\n"
"$startingUpdate = -join $chars\n"
"Write-Output $startingUpdate\n"
"Expand-Archive -Path '%2\\temp_download_update.zip' -DestinationPath '%2' -Force\n"
"Start-Sleep -Seconds 3\n"
"Copy-Item -Recurse -Force '%2\\*' '%3\\'\n"
"Start-Sleep -Seconds 2\n"
"Remove-Item -Force -LiteralPath '%3\\update.ps1'\n"
"Remove-Item -Force -LiteralPath '%3\\temp_download_update.zip'\n"
"Remove-Item -Recurse -Force '%2'\n"
"Start-Process -FilePath '%3\\shadps4.exe' "
"-WorkingDirectory ([WildcardPattern]::Escape('%3'))\n");
arguments << "-ExecutionPolicy"
<< "Bypass"
<< "-File" << scriptFileName;
processCommand = "powershell.exe";
#elif defined(Q_OS_LINUX)
// Linux Shell Script
scriptFileName = tempDirPath + "/update.sh";
scriptContent = QStringLiteral(
"#!/bin/bash\n"
"check_unzip() {\n"
" if ! command -v unzip &> /dev/null && ! command -v 7z &> /dev/null; then\n"
" echo \"Neither 'unzip' nor '7z' is installed.\"\n"
" read -p \"Would you like to install 'unzip'? (y/n): \" response\n"
" if [[ \"$response\" == \"y\" || \"$response\" == \"Y\" ]]; then\n"
" if [[ -f /etc/os-release ]]; then\n"
" . /etc/os-release\n"
" case \"$ID\" in\n"
" ubuntu|debian)\n"
" sudo apt-get install unzip -y\n"
" ;;\n"
" fedora|redhat)\n"
" sudo dnf install unzip -y\n"
" ;;\n"
" *)\n"
" echo \"Unsupported distribution for automatic installation.\"\n"
" exit 1\n"
" ;;\n"
" esac\n"
" else\n"
" echo \"Could not identify the distribution.\"\n"
" exit 1\n"
" fi\n"
" else\n"
" echo \"At least one of 'unzip' or '7z' is required to continue. The process "
"will be terminated.\"\n"
" exit 1\n"
" fi\n"
" fi\n"
"}\n"
"extract_file() {\n"
" if command -v unzip &> /dev/null; then\n"
" unzip -o \"%2/temp_download_update.zip\" -d \"%2/\"\n"
" elif command -v 7z &> /dev/null; then\n"
" 7z x \"%2/temp_download_update.zip\" -o\"%2/\" -y\n"
" else\n"
" echo \"No suitable extraction tool found.\"\n"
" exit 1\n"
" fi\n"
"}\n"
"main() {\n"
" check_unzip\n"
" echo \"%1\"\n"
" sleep 2\n"
" extract_file\n"
" sleep 2\n"
" if pgrep -f \"Shadps4-qt.AppImage\" > /dev/null; then\n"
" pkill -f \"Shadps4-qt.AppImage\"\n"
" sleep 2\n"
" fi\n"
" cp -r \"%2/\"* \"%3/\"\n"
" sleep 2\n"
" rm \"%3/update.sh\"\n"
" rm \"%3/temp_download_update.zip\"\n"
" chmod +x \"%3/Shadps4-qt.AppImage\"\n"
" rm -r \"%2\"\n"
" cd \"%3\" && ./Shadps4-qt.AppImage\n"
"}\n"
"main\n");
arguments << scriptFileName;
processCommand = "bash";
#elif defined(Q_OS_MAC)
// macOS Shell Script
scriptFileName = tempDirPath + "/update.sh";
scriptContent = QStringLiteral(
"#!/bin/bash\n"
"check_tools() {\n"
" if ! command -v unzip &> /dev/null && ! command -v tar &> /dev/null; then\n"
" echo \"Neither 'unzip' nor 'tar' is installed.\"\n"
" read -p \"Would you like to install 'unzip'? (y/n): \" response\n"
" if [[ \"$response\" == \"y\" || \"$response\" == \"Y\" ]]; then\n"
" echo \"Please install 'unzip' using Homebrew or another package manager.\"\n"
" exit 1\n"
" else\n"
" echo \"At least one of 'unzip' or 'tar' is required to continue. The process "
"will be terminated.\"\n"
" exit 1\n"
" fi\n"
" fi\n"
"}\n"
"check_tools\n"
"echo \"%1\"\n"
"sleep 2\n"
"unzip -o \"%2/temp_download_update.zip\" -d \"%2/\"\n"
"sleep 2\n"
"tar -xzf \"%2/shadps4-macos-qt.tar.gz\" -C \"%3\"\n"
"sleep 2\n"
"rm \"%3/update.sh\"\n"
"chmod +x \"%3/shadps4.app/Contents/MacOS/shadps4\"\n"
"open \"%3/shadps4.app\"\n"
"rm -r \"%2\"\n");
arguments << scriptFileName;
processCommand = "bash";
#else
QMessageBox::warning(this, tr("Error"), "Unsupported operating system.");
return;
#endif
QFile scriptFile(scriptFileName);
if (scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&scriptFile);
scriptFile.write("\xEF\xBB\xBF");
#ifdef Q_OS_WIN
out << scriptContent.arg(binaryStartingUpdate).arg(tempDirPath).arg(rootPath);
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
out << scriptContent.arg(startingUpdate).arg(tempDirPath).arg(rootPath);
#endif
scriptFile.close();
// Make the script executable on Unix-like systems
#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
scriptFile.setPermissions(QFileDevice::ExeOwner | QFileDevice::ReadOwner |
QFileDevice::WriteOwner);
#endif
QProcess::startDetached(processCommand, arguments);
exit(EXIT_SUCCESS);
} else {
QMessageBox::warning(
this, tr("Error"),
QString(tr("Failed to create the update script file") + ":\n" + scriptFileName));
}
}

View File

@ -1,43 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef CHECKUPDATE_H
#define CHECKUPDATE_H
#include <QCheckBox>
#include <QDialog>
#include <QNetworkAccessManager>
#include <QPushButton>
#include "gui_settings.h"
class CheckUpdate : public QDialog {
Q_OBJECT
public:
explicit CheckUpdate(std::shared_ptr<gui_settings> gui_settings, const bool showMessage,
QWidget* parent = nullptr);
~CheckUpdate();
private slots:
void CheckForUpdates(const bool showMessage);
void DownloadUpdate(const QString& url);
void Install();
private:
void setupUI(const QString& downloadUrl, const QString& latestDate, const QString& latestRev,
const QString& currentDate, const QString& currentRev);
void requestChangelog(const QString& currentRev, const QString& latestRev,
const QString& downloadUrl, const QString& latestDate,
const QString& currentDate);
QCheckBox* autoUpdateCheckBox;
QPushButton* yesButton;
QPushButton* noButton;
QString updateDownloadUrl;
QNetworkAccessManager* networkManager;
std::shared_ptr<gui_settings> m_gui_settings;
};
#endif // CHECKUPDATE_H

View File

@ -1,216 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QFileInfo>
#include <QMessageBox>
#include <QProgressDialog>
#include <QTimer>
#include "common/path_util.h"
#include "compatibility_info.h"
CompatibilityInfoClass::CompatibilityInfoClass()
: m_network_manager(new QNetworkAccessManager(this)) {
QStringList file_paths;
std::filesystem::path compatibility_file_path =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / "compatibility_data.json";
Common::FS::PathToQString(m_compatibility_filename, compatibility_file_path);
};
CompatibilityInfoClass::~CompatibilityInfoClass() = default;
void CompatibilityInfoClass::UpdateCompatibilityDatabase(QWidget* parent, bool forced) {
if (!forced && LoadCompatibilityFile())
return;
QUrl url("https://github.com/shadps4-compatibility/shadps4-game-compatibility/releases/latest/"
"download/compatibility_data.json");
QNetworkRequest request(url);
QNetworkReply* reply = m_network_manager->get(request);
QProgressDialog dialog(tr("Fetching compatibility data, please wait"), tr("Cancel"), 0, 100,
parent);
dialog.setWindowTitle(tr("Loading..."));
dialog.setWindowModality(Qt::WindowModal);
dialog.setMinimumDuration(0);
dialog.setValue(0);
connect(reply, &QNetworkReply::downloadProgress,
[&dialog](qint64 bytesReceived, qint64 bytesTotal) {
if (bytesTotal > 0) {
dialog.setMaximum(bytesTotal);
dialog.setValue(bytesReceived);
}
});
connect(&dialog, &QProgressDialog::canceled, reply, &QNetworkReply::abort);
QEventLoop loop;
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() != QNetworkReply::NoError) {
reply->deleteLater();
QMessageBox::critical(parent, tr("Error"),
tr("Unable to update compatibility data! Try again later."));
// Try loading compatibility_file.json again
if (!forced)
LoadCompatibilityFile();
return;
}
QFile compatibility_file(m_compatibility_filename);
if (!compatibility_file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
QMessageBox::critical(parent, tr("Error"),
tr("Unable to open compatibility_data.json for writing."));
reply->deleteLater();
return;
}
// Writes the received data to the file.
QByteArray json_data = reply->readAll();
compatibility_file.write(json_data);
compatibility_file.close();
reply->deleteLater();
LoadCompatibilityFile();
}
CompatibilityEntry CompatibilityInfoClass::GetCompatibilityInfo(const std::string& serial) {
QString title_id = QString::fromStdString(serial);
if (m_compatibility_database.contains(title_id)) {
QJsonObject compatibility_obj = m_compatibility_database[title_id].toObject();
// Set current_os automatically
QString current_os;
#ifdef Q_OS_WIN
current_os = "os-windows";
#elif defined(Q_OS_MAC)
current_os = "os-macOS";
#elif defined(Q_OS_LINUX)
current_os = "os-linux";
#else
current_os = "os-unknown";
#endif
// Check if the game is compatible with the current operating system
if (compatibility_obj.contains(current_os)) {
QJsonObject compatibility_entry_obj = compatibility_obj[current_os].toObject();
CompatibilityEntry compatibility_entry{
LabelToCompatStatus.at(compatibility_entry_obj["status"].toString()),
compatibility_entry_obj["version"].toString(),
QDateTime::fromString(compatibility_entry_obj["last_tested"].toString(),
Qt::ISODate),
compatibility_entry_obj["url"].toString(),
compatibility_entry_obj["issue_number"].toString()};
return compatibility_entry;
} else {
// If there is no entry for the current operating system, return "Unknown"
return CompatibilityEntry{CompatibilityStatus::Unknown, "",
QDateTime::currentDateTime(), "", 0};
}
}
// If title not found, return "Unknown"
return CompatibilityEntry{CompatibilityStatus::Unknown, "", QDateTime::currentDateTime(), "",
0};
}
bool CompatibilityInfoClass::LoadCompatibilityFile() {
// Returns true if compatibility is loaded succescfully
QFileInfo check_file(m_compatibility_filename);
const auto modified_delta = QDateTime::currentDateTime() - check_file.lastModified();
if (!check_file.exists() || !check_file.isFile() ||
std::chrono::duration_cast<std::chrono::minutes>(modified_delta).count() > 60) {
return false;
}
QFile compatibility_file(m_compatibility_filename);
if (!compatibility_file.open(QIODevice::ReadOnly)) {
compatibility_file.close();
return false;
}
QByteArray json_data = compatibility_file.readAll();
compatibility_file.close();
QJsonDocument json_doc = QJsonDocument::fromJson(json_data);
if (json_doc.isEmpty() || json_doc.isNull()) {
return false;
}
m_compatibility_database = json_doc.object();
return true;
}
void CompatibilityInfoClass::ExtractCompatibilityInfo(QByteArray response) {
QJsonDocument json_doc(QJsonDocument::fromJson(response));
if (json_doc.isNull()) {
return;
}
QJsonArray json_arr;
json_arr = json_doc.array();
for (const auto& issue_ref : std::as_const(json_arr)) {
QJsonObject issue_obj = issue_ref.toObject();
QString title_id;
QRegularExpression title_id_regex("CUSA[0-9]{5}");
QRegularExpressionMatch title_id_match =
title_id_regex.match(issue_obj["title"].toString());
QString current_os = "os-unknown";
QString compatibility_status = "status-unknown";
if (issue_obj.contains("labels") && title_id_match.hasMatch()) {
title_id = title_id_match.captured(0);
const QJsonArray& label_array = issue_obj["labels"].toArray();
for (const auto& elem : label_array) {
QString label = elem.toObject()["name"].toString();
if (LabelToOSType.contains(label)) {
current_os = label;
continue;
}
if (LabelToCompatStatus.contains(label)) {
compatibility_status = label;
continue;
}
}
// QJson does not support editing nested objects directly..
QJsonObject compatibility_obj = m_compatibility_database[title_id].toObject();
QJsonObject compatibility_data{
{{"status", compatibility_status},
{"last_tested", issue_obj["updated_at"]},
{"version", issue_obj["milestone"].isNull()
? "unknown"
: issue_obj["milestone"].toObject()["title"].toString()},
{"url", issue_obj["html_url"]},
{"issue_number", issue_obj["number"]}}};
compatibility_obj[current_os] = compatibility_data;
m_compatibility_database[title_id] = compatibility_obj;
}
}
return;
}
const QString CompatibilityInfoClass::GetCompatStatusString(const CompatibilityStatus status) {
switch (status) {
case CompatibilityStatus::Unknown:
return tr("Unknown");
case CompatibilityStatus::Nothing:
return tr("Nothing");
case CompatibilityStatus::Boots:
return tr("Boots");
case CompatibilityStatus::Menus:
return tr("Menus");
case CompatibilityStatus::Ingame:
return tr("Ingame");
case CompatibilityStatus::Playable:
return tr("Playable");
default:
return tr("Unknown");
}
}

View File

@ -1,88 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrent>
#include <QtNetwork>
#include "common/config.h"
#include "core/file_format/psf.h"
enum class CompatibilityStatus {
Unknown,
Nothing,
Boots,
Menus,
Ingame,
Playable,
};
// Prioritize different compatibility reports based on user's platform
enum class OSType {
#ifdef Q_OS_WIN
Win32 = 0,
Unknown,
Linux,
macOS,
#elif defined(Q_OS_LINUX)
Linux = 0,
Unknown,
Win32,
macOS,
#elif defined(Q_OS_MAC)
macOS = 0,
Unknown,
Linux,
Win32,
#endif
// Fake enum to allow for iteration
Last
};
struct CompatibilityEntry {
CompatibilityStatus status;
QString version;
QDateTime last_tested;
QString url;
QString issue_number;
};
class CompatibilityInfoClass : public QObject {
Q_OBJECT
public:
// Please think of a better alternative
inline static const std::unordered_map<QString, CompatibilityStatus> LabelToCompatStatus = {
{QStringLiteral("status-unknown"), CompatibilityStatus::Unknown},
{QStringLiteral("status-nothing"), CompatibilityStatus::Nothing},
{QStringLiteral("status-boots"), CompatibilityStatus::Boots},
{QStringLiteral("status-menus"), CompatibilityStatus::Menus},
{QStringLiteral("status-ingame"), CompatibilityStatus::Ingame},
{QStringLiteral("status-playable"), CompatibilityStatus::Playable}};
inline static const std::unordered_map<QString, OSType> LabelToOSType = {
{QStringLiteral("os-linux"), OSType::Linux},
{QStringLiteral("os-macOS"), OSType::macOS},
{QStringLiteral("os-windows"), OSType::Win32},
};
inline static const std::unordered_map<OSType, QString> OSTypeToString = {
{OSType::Linux, QStringLiteral("os-linux")},
{OSType::macOS, QStringLiteral("os-macOS")},
{OSType::Win32, QStringLiteral("os-windows")},
{OSType::Unknown, QStringLiteral("os-unknown")}};
CompatibilityInfoClass();
~CompatibilityInfoClass();
void UpdateCompatibilityDatabase(QWidget* parent = nullptr, bool forced = false);
bool LoadCompatibilityFile();
CompatibilityEntry GetCompatibilityInfo(const std::string& serial);
const QString GetCompatStatusString(const CompatibilityStatus status);
void ExtractCompatibilityInfo(QByteArray response);
private:
QNetworkAccessManager* m_network_manager;
QString m_compatibility_filename;
QJsonObject m_compatibility_database;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,87 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <SDL3/SDL.h>
#include <SDL3/SDL_gamepad.h>
#include "game_info.h"
#include "sdl_event_wrapper.h"
namespace Ui {
class ControlSettings;
}
class ControlSettings : public QDialog {
Q_OBJECT
public:
explicit ControlSettings(std::shared_ptr<GameInfoClass> game_info_get, bool GameRunning,
std::string GameRunningSerial, QWidget* parent = nullptr);
~ControlSettings();
signals:
void PushGamepadEvent();
void AxisChanged();
private Q_SLOTS:
void SaveControllerConfig(bool CloseOnSave);
void SetDefault();
void UpdateLightbarColor();
void CheckMapping(QPushButton*& button);
void StartTimer(QPushButton*& button, bool isButton);
void ConnectAxisInputs(QPushButton*& button);
void ActiveControllerChanged(int value);
private:
std::unique_ptr<Ui::ControlSettings> ui;
std::shared_ptr<GameInfoClass> m_game_info;
bool eventFilter(QObject* obj, QEvent* event) override;
void AddBoxItems();
void SetUIValuestoMappings();
void GetGameTitle();
void CheckGamePad();
void processSDLEvents(int Type, int Input, int Value);
void pollSDLEvents();
void SetMapping(QString input);
void DisableMappingButtons();
void EnableMappingButtons();
void Cleanup();
// use QMap instead of QSet to maintain order of inserted strings
QMap<int, QString> pressedButtons;
QList<QPushButton*> ButtonsList;
QList<QPushButton*> AxisList;
std::string RunningGameSerial;
bool GameRunning;
bool L2Pressed = false;
bool R2Pressed = false;
bool EnableButtonMapping = false;
bool EnableAxisMapping = false;
bool MappingCompleted = false;
QString mapping;
int MappingTimer;
int gamepad_count;
QTimer* timer;
QPushButton* MappingButton;
SDL_Gamepad* gamepad = nullptr;
SDL_JoystickID* gamepads;
SdlEventWrapper::Wrapper* RemapWrapper;
QFuture<void> Polling;
const std::vector<std::string> ControllerInputs = {
"cross", "circle", "square", "triangle", "l1",
"r1", "l2", "r2", "l3",
"r3", "options", "pad_up",
"pad_down",
"pad_left", "pad_right", "axis_left_x", "axis_left_y", "axis_right_x",
"axis_right_y", "back"};
protected:
void closeEvent(QCloseEvent* event) override {
Cleanup();
}
};

File diff suppressed because it is too large Load Diff

View File

@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "elf_viewer.h"
ElfViewer::ElfViewer(std::shared_ptr<gui_settings> gui_settings, QWidget* parent)
: QTableWidget(parent), m_gui_settings(std::move(gui_settings)) {
list = gui_settings::Var2List(m_gui_settings->GetValue(gui::gen_elfDirs));
for (const auto& str : list) {
dir_list.append(str);
}
CheckElfFolders();
this->setShowGrid(false);
this->setEditTriggers(QAbstractItemView::NoEditTriggers);
this->setSelectionBehavior(QAbstractItemView::SelectRows);
this->setSelectionMode(QAbstractItemView::SingleSelection);
this->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
this->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
this->verticalScrollBar()->installEventFilter(this);
this->verticalScrollBar()->setSingleStep(20);
this->horizontalScrollBar()->setSingleStep(20);
this->verticalHeader()->setVisible(false);
this->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
this->horizontalHeader()->setHighlightSections(false);
this->horizontalHeader()->setSortIndicatorShown(true);
this->horizontalHeader()->setStretchLastSection(true);
this->setContextMenuPolicy(Qt::CustomContextMenu);
this->setColumnCount(2);
this->setColumnWidth(0, 250);
this->setColumnWidth(1, 400);
this->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
this->setStyleSheet("QTableWidget { background-color: #D3D3D3; }");
OpenElfFiles();
QStringList headers;
headers << "Name"
<< "Path";
this->setHorizontalHeaderLabels(headers);
this->horizontalHeader()->setSortIndicatorShown(true);
this->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
}
void ElfViewer::OpenElfFolder() {
QString folderPath =
QFileDialog::getExistingDirectory(this, tr("Open Folder"), QDir::homePath());
if (!dir_list.contains(folderPath)) {
dir_list.append(folderPath);
QDir directory(folderPath);
QFileInfoList fileInfoList = directory.entryInfoList(QDir::Files);
for (const QFileInfo& fileInfo : fileInfoList) {
QString file_ext = fileInfo.suffix();
if (fileInfo.isFile() && (file_ext == "bin" || file_ext == "elf")) {
m_elf_list.append(fileInfo.absoluteFilePath());
}
}
std::ranges::sort(m_elf_list);
OpenElfFiles();
list.clear();
for (auto dir : dir_list) {
list.push_back(dir);
}
m_gui_settings->SetValue(gui::gen_elfDirs, gui_settings::List2Var(list));
} else {
// qDebug() << "Folder selection canceled.";
}
}
void ElfViewer::CheckElfFolders() {
m_elf_list.clear();
for (const QString& dir : dir_list) {
QDir directory(dir);
QFileInfoList fileInfoList = directory.entryInfoList(QDir::Files);
for (const QFileInfo& fileInfo : fileInfoList) {
QString file_ext = fileInfo.suffix();
if (fileInfo.isFile() && (file_ext == "bin" || file_ext == "elf")) {
m_elf_list.append(fileInfo.absoluteFilePath());
}
}
}
std::sort(m_elf_list.begin(), m_elf_list.end());
}
void ElfViewer::OpenElfFiles() {
this->clearContents();
this->setRowCount(m_elf_list.size());
for (int i = 0; auto elf : m_elf_list) {
QTableWidgetItem* item = new QTableWidgetItem();
QFileInfo fileInfo(m_elf_list[i]);
QString fileName = fileInfo.baseName();
SetTableItem(this, i, 0, fileName);
item = new QTableWidgetItem();
SetTableItem(this, i, 1, m_elf_list[i]);
i++;
}
this->resizeColumnsToContents();
}

View File

@ -1,53 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QFileDialog>
#include "core/loader/elf.h"
#include "game_list_frame.h"
class ElfViewer : public QTableWidget {
Q_OBJECT
public:
explicit ElfViewer(std::shared_ptr<gui_settings> gui_settings, QWidget* parent = nullptr);
QStringList m_elf_list;
private:
void CheckElfFolders();
void OpenElfFiles();
Core::Loader::Elf m_elf_file;
QStringList dir_list;
QStringList elf_headers_list;
QList<QString> list;
std::shared_ptr<gui_settings> m_gui_settings;
void SetTableItem(QTableWidget* game_list, int row, int column, QString itemStr) {
QTableWidgetItem* item = new QTableWidgetItem();
QWidget* widget = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(widget);
QLabel* label = new QLabel(itemStr, widget);
label->setStyleSheet("color: white; font-size: 15px; font-weight: bold;");
// Create shadow effect
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect();
shadowEffect->setBlurRadius(5); // Set the blur radius of the shadow
shadowEffect->setColor(QColor(0, 0, 0, 160)); // Set the color and opacity of the shadow
shadowEffect->setOffset(2, 2); // Set the offset of the shadow
label->setGraphicsEffect(shadowEffect); // Apply shadow effect to the QLabel
layout->addWidget(label);
if (column != 8 && column != 1)
layout->setAlignment(Qt::AlignCenter);
widget->setLayout(layout);
game_list->setItem(row, column, item);
game_list->setCellWidget(row, column, widget);
}
public slots:
void OpenElfFolder();
};

View File

@ -1,297 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/path_util.h"
#include "game_grid_frame.h"
#include "qt_gui/compatibility_info.h"
GameGridFrame::GameGridFrame(std::shared_ptr<gui_settings> gui_settings,
std::shared_ptr<GameInfoClass> game_info_get,
std::shared_ptr<CompatibilityInfoClass> compat_info_get,
QWidget* parent)
: QTableWidget(parent), m_gui_settings(std::move(gui_settings)), m_game_info(game_info_get),
m_compat_info(compat_info_get) {
icon_size = m_gui_settings->GetValue(gui::gg_icon_size).toInt();
windowWidth = parent->width();
this->setShowGrid(false);
this->setEditTriggers(QAbstractItemView::NoEditTriggers);
this->setSelectionBehavior(QAbstractItemView::SelectItems);
this->setSelectionMode(QAbstractItemView::SingleSelection);
this->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
this->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
this->verticalScrollBar()->installEventFilter(this);
this->verticalScrollBar()->setSingleStep(20);
this->horizontalScrollBar()->setSingleStep(20);
this->horizontalHeader()->setVisible(false);
this->verticalHeader()->setVisible(false);
this->setContextMenuPolicy(Qt::CustomContextMenu);
PopulateGameGrid(m_game_info->m_games, false);
connect(this, &QTableWidget::currentCellChanged, this, &GameGridFrame::onCurrentCellChanged);
connect(this->verticalScrollBar(), &QScrollBar::valueChanged, this,
&GameGridFrame::RefreshGridBackgroundImage);
connect(this->horizontalScrollBar(), &QScrollBar::valueChanged, this,
&GameGridFrame::RefreshGridBackgroundImage);
connect(this, &QTableWidget::customContextMenuRequested, this, [=, this](const QPoint& pos) {
m_gui_context_menus.RequestGameMenu(pos, m_game_info->m_games, m_compat_info,
m_gui_settings, this, false);
PopulateGameGrid(m_game_info->m_games, false);
});
}
void GameGridFrame::onCurrentCellChanged(int currentRow, int currentColumn, int previousRow,
int previousColumn) {
// Early exit for invalid indices
if (currentRow < 0 || currentColumn < 0) {
cellClicked = false;
validCellSelected = false;
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
crtRow = currentRow;
crtColumn = currentColumn;
columnCnt = this->columnCount();
// Prevent integer overflow
if (columnCnt <= 0 || crtRow > (std::numeric_limits<int>::max() / columnCnt)) {
cellClicked = false;
validCellSelected = false;
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
auto itemID = (crtRow * columnCnt) + currentColumn;
if (itemID < 0 || itemID > m_game_info->m_games.count() - 1) {
cellClicked = false;
validCellSelected = false;
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
cellClicked = true;
validCellSelected = true;
SetGridBackgroundImage(crtRow, crtColumn);
auto snd0Path = QString::fromStdString(m_game_info->m_games[itemID].snd0_path.string());
PlayBackgroundMusic(snd0Path);
}
void GameGridFrame::PlayBackgroundMusic(QString path) {
if (path.isEmpty() || !m_gui_settings->GetValue(gui::gl_playBackgroundMusic).toBool()) {
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
BackgroundMusicPlayer::getInstance().playMusic(path);
}
void GameGridFrame::PopulateGameGrid(QVector<GameInfo> m_games_search, bool fromSearch) {
this->crtRow = -1;
this->crtColumn = -1;
QVector<GameInfo> m_games_;
this->clearContents();
if (fromSearch) {
SortByFavorite(&m_games_search);
m_games_ = m_games_search;
} else {
SortByFavorite(&(m_game_info->m_games));
m_games_ = m_game_info->m_games;
}
m_games_shared = std::make_shared<QVector<GameInfo>>(m_games_);
icon_size =
m_gui_settings->GetValue(gui::gg_icon_size).toInt(); // update icon size for resize event.
int gamesPerRow = windowWidth / (icon_size + 20); // 2 x cell widget border size.
int row = 0;
int gameCounter = 0;
int rowCount = m_games_.size() / gamesPerRow;
if (m_games_.size() % gamesPerRow != 0) {
rowCount += 1; // Add an extra row for the remainder
}
int column = 0;
this->setColumnCount(gamesPerRow);
this->setRowCount(rowCount);
for (int i = 0; i < m_games_.size(); i++) {
QWidget* widget = new QWidget();
QVBoxLayout* layout = new QVBoxLayout();
QWidget* image_container = new QWidget();
image_container->setFixedSize(icon_size, icon_size);
QLabel* image_label = new QLabel(image_container);
QImage icon = m_games_[gameCounter].icon.scaled(
QSize(icon_size, icon_size), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
image_label->setFixedSize(icon.width(), icon.height());
image_label->setPixmap(QPixmap::fromImage(icon));
image_label->move(0, 0);
SetFavoriteIcon(image_container, m_games_, gameCounter);
SetGameConfigIcon(image_container, m_games_, gameCounter);
QLabel* name_label = new QLabel(QString::fromStdString(m_games_[gameCounter].serial));
name_label->setAlignment(Qt::AlignHCenter);
layout->addWidget(image_container);
layout->addWidget(name_label);
// Resizing of font-size.
float fontSize = (m_gui_settings->GetValue(gui::gg_icon_size).toInt() / 5.5f);
QString styleSheet =
QString("color: white; font-weight: bold; font-size: %1px;").arg(fontSize);
name_label->setStyleSheet(styleSheet);
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect();
shadowEffect->setBlurRadius(5); // Set the blur radius of the shadow
shadowEffect->setColor(QColor(0, 0, 0, 160)); // Set the color and opacity of the shadow
shadowEffect->setOffset(2, 2); // Set the offset of the shadow
name_label->setGraphicsEffect(shadowEffect);
widget->setLayout(layout);
QString tooltipText = QString::fromStdString(m_games_[gameCounter].name + " (" +
m_games_[gameCounter].version + ", " +
m_games_[gameCounter].region + ")");
widget->setToolTip(tooltipText);
QString tooltipStyle = QString("QToolTip {"
"background-color: #ffffff;"
"color: #000000;"
"border: 1px solid #000000;"
"padding: 2px;"
"font-size: 12px; }");
widget->setStyleSheet(tooltipStyle);
this->setCellWidget(row, column, widget);
column++;
if (column == gamesPerRow) {
column = 0;
row++;
}
gameCounter++;
if (gameCounter >= m_games_.size()) {
break;
}
}
m_games_.clear();
this->resizeRowsToContents();
this->resizeColumnsToContents();
}
void GameGridFrame::SetGridBackgroundImage(int row, int column) {
int itemID = (row * this->columnCount()) + column;
QWidget* item = this->cellWidget(row, column);
if (!item) {
// handle case where no item was clicked
return;
}
// If background images are hidden, clear the background image
if (!m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()) {
backgroundImage = QImage();
m_last_opacity = -1; // Reset opacity tracking when disabled
m_current_game_path.clear(); // Reset current game path
RefreshGridBackgroundImage();
return;
}
const auto& game = (*m_games_shared)[itemID];
const int opacity = m_gui_settings->GetValue(gui::gl_backgroundImageOpacity).toInt();
// Recompute if opacity changed or we switched to a different game
if (opacity != m_last_opacity || game.pic_path != m_current_game_path) {
QImage original_image(QString::fromStdString(game.pic_path.string()));
if (!original_image.isNull()) {
backgroundImage = m_game_list_utils.ChangeImageOpacity(
original_image, original_image.rect(), opacity / 100.0f);
m_last_opacity = opacity;
m_current_game_path = game.pic_path;
}
}
RefreshGridBackgroundImage();
}
void GameGridFrame::RefreshGridBackgroundImage() {
QPalette palette;
if (!backgroundImage.isNull() &&
m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()) {
QSize widgetSize = size();
QPixmap scaledPixmap =
QPixmap::fromImage(backgroundImage)
.scaled(widgetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
int x = (widgetSize.width() - scaledPixmap.width()) / 2;
int y = (widgetSize.height() - scaledPixmap.height()) / 2;
QPixmap finalPixmap(widgetSize);
finalPixmap.fill(Qt::transparent);
QPainter painter(&finalPixmap);
painter.drawPixmap(x, y, scaledPixmap);
palette.setBrush(QPalette::Base, QBrush(finalPixmap));
}
QColor transparentColor = QColor(135, 206, 235, 40);
palette.setColor(QPalette::Highlight, transparentColor);
this->setPalette(palette);
}
void GameGridFrame::resizeEvent(QResizeEvent* event) {
QTableWidget::resizeEvent(event);
RefreshGridBackgroundImage();
}
bool GameGridFrame::IsValidCellSelected() {
return validCellSelected;
}
void GameGridFrame::SetFavoriteIcon(QWidget* parentWidget, QVector<GameInfo> m_games_,
int gameCounter) {
QString serialStr = QString::fromStdString(m_games_[gameCounter].serial);
QList<QString> list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list));
bool isFavorite = list.contains(serialStr);
QLabel* label = new QLabel(parentWidget);
label->setPixmap(QPixmap(":images/favorite_icon.png")
.scaled(icon_size / 3.8, icon_size / 3.8, Qt::KeepAspectRatio,
Qt::SmoothTransformation));
label->move(icon_size - icon_size / 4, 2);
label->raise();
label->setVisible(isFavorite);
label->setObjectName("favoriteIcon");
}
void GameGridFrame::SetGameConfigIcon(QWidget* parentWidget, QVector<GameInfo> m_games_,
int gameCounter) {
std::string serialStr = m_games_[gameCounter].serial;
bool hasGameConfig = std::filesystem::exists(
Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (serialStr + ".toml"));
QLabel* label = new QLabel(parentWidget);
label->setPixmap(QPixmap(":images/game_settings.png")
.scaled(icon_size / 3.8, icon_size / 3.8, Qt::KeepAspectRatio,
Qt::SmoothTransformation));
label->move(2, 2);
label->raise();
label->setVisible(hasGameConfig);
label->setObjectName("gameConfigIcon");
}
void GameGridFrame::SortByFavorite(QVector<GameInfo>* game_list) {
std::sort(game_list->begin(), game_list->end(), [this](const GameInfo& a, const GameInfo& b) {
return this->CompareWithFavorite(a, b);
});
}
bool GameGridFrame::CompareWithFavorite(GameInfo a, GameInfo b) {
std::string serial_a = a.serial;
std::string serial_b = b.serial;
QString serialStr_a = QString::fromStdString(a.serial);
QString serialStr_b = QString::fromStdString(b.serial);
QList<QString> list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list));
bool isFavorite_a = list.contains(serialStr_a);
bool isFavorite_b = list.contains(serialStr_b);
if (isFavorite_a != isFavorite_b) {
return isFavorite_a;
} else {
std::string name_a = a.name, name_b = b.name;
std::transform(name_a.begin(), name_a.end(), name_a.begin(), ::tolower);
std::transform(name_b.begin(), name_b.end(), name_b.begin(), ::tolower);
return name_a < name_b;
}
}

View File

@ -1,61 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QPainter>
#include <QScrollBar>
#include "background_music_player.h"
#include "common/config.h"
#include "game_info.h"
#include "game_list_utils.h"
#include "gui_context_menus.h"
#include "gui_settings.h"
#include "qt_gui/compatibility_info.h"
class GameGridFrame : public QTableWidget {
Q_OBJECT
Q_SIGNALS:
void GameGridFrameClosed();
public Q_SLOTS:
void SetGridBackgroundImage(int row, int column);
void RefreshGridBackgroundImage();
void resizeEvent(QResizeEvent* event);
void PlayBackgroundMusic(QString path);
void onCurrentCellChanged(int currentRow, int currentColumn, int previousRow,
int previousColumn);
private:
QImage backgroundImage;
GameListUtils m_game_list_utils;
GuiContextMenus m_gui_context_menus;
std::shared_ptr<GameInfoClass> m_game_info;
std::shared_ptr<CompatibilityInfoClass> m_compat_info;
std::shared_ptr<QVector<GameInfo>> m_games_shared;
bool validCellSelected = false;
int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation
std::filesystem::path m_current_game_path; // Track current game path to detect changes
std::shared_ptr<gui_settings> m_gui_settings;
void SetFavoriteIcon(QWidget* parentWidget, QVector<GameInfo> m_games_, int gameCounter);
void SetGameConfigIcon(QWidget* parentWidget, QVector<GameInfo> m_games_, int gameCounter);
bool CompareWithFavorite(GameInfo a, GameInfo b);
public:
explicit GameGridFrame(std::shared_ptr<gui_settings> gui_settings,
std::shared_ptr<GameInfoClass> game_info_get,
std::shared_ptr<CompatibilityInfoClass> compat_info_get,
QWidget* parent = nullptr);
void PopulateGameGrid(QVector<GameInfo> m_games, bool fromSearch);
bool IsValidCellSelected();
void SortByFavorite(QVector<GameInfo>* game_list);
bool cellClicked = false;
int icon_size;
int windowWidth;
int crtRow;
int crtColumn;
int columnCnt;
};

View File

@ -1,73 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QProgressDialog>
#include "common/path_util.h"
#include "compatibility_info.h"
#include "game_info.h"
// Maximum depth to search for games in subdirectories
const int max_recursion_depth = 5;
void ScanDirectoryRecursively(const QString& dir, QStringList& filePaths, int current_depth = 0) {
// Stop recursion if we've reached the maximum depth
if (current_depth >= max_recursion_depth) {
return;
}
QDir directory(dir);
QFileInfoList entries = directory.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const auto& entry : entries) {
if (entry.fileName().endsWith("-UPDATE") || entry.fileName().endsWith("-patch")) {
continue;
}
// Check if this directory contains a PS4 game (has sce_sys/param.sfo)
if (QFile::exists(entry.filePath() + "/sce_sys/param.sfo")) {
filePaths.append(entry.absoluteFilePath());
} else {
// If not a game directory, recursively scan it with increased depth
ScanDirectoryRecursively(entry.absoluteFilePath(), filePaths, current_depth + 1);
}
}
}
GameInfoClass::GameInfoClass() = default;
GameInfoClass::~GameInfoClass() = default;
void GameInfoClass::GetGameInfo(QWidget* parent) {
QStringList filePaths;
for (const auto& installLoc : Config::getGameInstallDirs()) {
QString installDir;
Common::FS::PathToQString(installDir, installLoc);
ScanDirectoryRecursively(installDir, filePaths, 0);
}
m_games = QtConcurrent::mapped(filePaths, [&](const QString& path) {
return readGameInfo(Common::FS::PathFromQString(path));
}).results();
// used to retrieve values after performing a search
m_games_backup = m_games;
// Progress bar, please be patient :)
QProgressDialog dialog(tr("Loading game list, please wait :3"), tr("Cancel"), 0, 0, parent);
dialog.setWindowTitle(tr("Loading..."));
QFutureWatcher<void> futureWatcher;
GameListUtils game_util;
bool finished = false;
futureWatcher.setFuture(QtConcurrent::map(m_games, game_util.GetFolderSize));
connect(&futureWatcher, &QFutureWatcher<void>::finished, [&]() {
dialog.reset();
std::sort(m_games.begin(), m_games.end(), CompareStrings);
});
connect(&dialog, &QProgressDialog::canceled, &futureWatcher, &QFutureWatcher<void>::cancel);
dialog.setRange(0, m_games.size());
connect(&futureWatcher, &QFutureWatcher<void>::progressValueChanged, &dialog,
&QProgressDialog::setValue);
dialog.exec();
}

View File

@ -1,100 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QFutureWatcher>
#include <QtConcurrent>
#include "common/config.h"
#include "core/file_format/psf.h"
#include "game_list_utils.h"
class GameInfoClass : public QObject {
Q_OBJECT
public:
GameInfoClass();
~GameInfoClass();
void GetGameInfo(QWidget* parent = nullptr);
QVector<GameInfo> m_games;
QVector<GameInfo> m_games_backup;
static void SceUpdateChecker(const std::string sceItem, std::filesystem::path& gameItem,
std::filesystem::path& update_folder,
std::filesystem::path& patch_folder,
std::filesystem::path& game_folder) {
if (std::filesystem::exists(update_folder / "sce_sys" / sceItem)) {
gameItem = update_folder / "sce_sys" / sceItem;
} else if (std::filesystem::exists(patch_folder / "sce_sys" / sceItem)) {
gameItem = patch_folder / "sce_sys" / sceItem;
} else {
gameItem = game_folder / "sce_sys" / sceItem;
}
}
static bool CompareStrings(GameInfo& a, GameInfo& b) {
std::string name_a = a.name, name_b = b.name;
std::transform(name_a.begin(), name_a.end(), name_a.begin(), ::tolower);
std::transform(name_b.begin(), name_b.end(), name_b.begin(), ::tolower);
return name_a < name_b;
}
static GameInfo readGameInfo(const std::filesystem::path& filePath) {
GameInfo game;
game.path = filePath;
std::filesystem::path param_sfo_path;
std::filesystem::path game_update_path = filePath;
game_update_path += "-UPDATE";
std::filesystem::path game_patch_path = filePath;
game_patch_path += "-patch";
SceUpdateChecker("param.sfo", param_sfo_path, game_update_path, game_patch_path, game.path);
PSF psf;
if (psf.Open(param_sfo_path)) {
SceUpdateChecker("icon0.png", game.icon_path, game_update_path, game_patch_path,
game.path);
QString iconpath;
Common::FS::PathToQString(iconpath, game.icon_path);
game.icon = QImage(iconpath);
SceUpdateChecker("pic1.png", game.pic_path, game_update_path, game_patch_path,
game.path);
SceUpdateChecker("snd0.at9", game.snd0_path, game_update_path, game_patch_path,
game.path);
if (const auto title = psf.GetString("TITLE"); title.has_value()) {
game.name = *title;
}
if (const auto title_id = psf.GetString("TITLE_ID"); title_id.has_value()) {
game.serial = *title_id;
}
if (const auto content_id = psf.GetString("CONTENT_ID");
content_id.has_value() && !content_id->empty()) {
game.region = GameListUtils::GetRegion(content_id->at(0)).toStdString();
}
if (const auto fw_int_opt = psf.GetInteger("SYSTEM_VER"); fw_int_opt.has_value()) {
auto fw_int = *fw_int_opt;
if (fw_int == 0) {
game.fw = "0.00";
} else {
QString fw = QString::number(fw_int, 16);
QString fw_ = fw.length() > 7
? QString::number(fw_int, 16).left(3).insert(2, '.')
: fw.left(3).insert(1, '.');
game.fw = fw_.toStdString();
}
}
if (auto app_ver = psf.GetString("APP_VER"); app_ver.has_value()) {
game.version = *app_ver;
}
if (const auto play_time = psf.GetString("PLAY_TIME"); play_time.has_value()) {
game.play_time = *play_time;
}
if (const auto save_dir = psf.GetString("INSTALL_DIR_SAVEDATA"); save_dir.has_value()) {
game.save_dir = *save_dir;
} else {
game.save_dir = game.serial;
}
}
return game;
}
};

View File

@ -1,134 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QVBoxLayout>
#include "game_install_dialog.h"
GameInstallDialog::GameInstallDialog() : m_gamesDirectory(nullptr) {
auto layout = new QVBoxLayout(this);
layout->addWidget(SetupGamesDirectory());
layout->addWidget(SetupAddonsDirectory());
layout->addStretch();
layout->addWidget(SetupDialogActions());
setWindowTitle(tr("shadPS4 - Choose directory"));
setWindowIcon(QIcon(":images/shadps4.ico"));
}
GameInstallDialog::~GameInstallDialog() {}
void GameInstallDialog::BrowseGamesDirectory() {
auto path = QFileDialog::getExistingDirectory(this, tr("Directory to install games"));
if (!path.isEmpty()) {
m_gamesDirectory->setText(QDir::toNativeSeparators(path));
}
}
void GameInstallDialog::BrowseAddonsDirectory() {
auto path = QFileDialog::getExistingDirectory(this, tr("Directory to install DLC"));
if (!path.isEmpty()) {
m_addonsDirectory->setText(QDir::toNativeSeparators(path));
}
}
QWidget* GameInstallDialog::SetupGamesDirectory() {
auto group = new QGroupBox(tr("Directory to install games"));
auto layout = new QHBoxLayout(group);
// Input.
m_gamesDirectory = new QLineEdit();
QString install_dir;
std::filesystem::path install_path =
Config::getGameInstallDirs().empty() ? "" : Config::getGameInstallDirs().front();
Common::FS::PathToQString(install_dir, install_path);
m_gamesDirectory->setText(install_dir);
m_gamesDirectory->setMinimumWidth(400);
layout->addWidget(m_gamesDirectory);
// Browse button.
auto browse = new QPushButton(tr("Browse"));
connect(browse, &QPushButton::clicked, this, &GameInstallDialog::BrowseGamesDirectory);
layout->addWidget(browse);
return group;
}
QWidget* GameInstallDialog::SetupAddonsDirectory() {
auto group = new QGroupBox(tr("Directory to install DLC"));
auto layout = new QHBoxLayout(group);
// Input.
m_addonsDirectory = new QLineEdit();
QString install_dir;
Common::FS::PathToQString(install_dir, Config::getAddonInstallDir());
m_addonsDirectory->setText(install_dir);
m_addonsDirectory->setMinimumWidth(400);
layout->addWidget(m_addonsDirectory);
// Browse button.
auto browse = new QPushButton(tr("Browse"));
connect(browse, &QPushButton::clicked, this, &GameInstallDialog::BrowseAddonsDirectory);
layout->addWidget(browse);
return group;
}
QWidget* GameInstallDialog::SetupDialogActions() {
auto actions = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(actions, &QDialogButtonBox::accepted, this, &GameInstallDialog::Save);
connect(actions, &QDialogButtonBox::rejected, this, &GameInstallDialog::reject);
return actions;
}
void GameInstallDialog::Save() {
// Check games directory.
auto gamesDirectory = m_gamesDirectory->text();
auto addonsDirectory = m_addonsDirectory->text();
if (gamesDirectory.isEmpty() || !QDir(gamesDirectory).exists() ||
!QDir::isAbsolutePath(gamesDirectory)) {
QMessageBox::critical(this, tr("Error"),
"The value for location to install games is not valid.");
return;
}
if (addonsDirectory.isEmpty() || !QDir::isAbsolutePath(addonsDirectory)) {
QMessageBox::critical(this, tr("Error"),
"The value for location to install DLC is not valid.");
return;
}
QDir addonsDir(addonsDirectory);
if (!addonsDir.exists()) {
if (!addonsDir.mkpath(".")) {
QMessageBox::critical(this, tr("Error"),
"The DLC install location could not be created.");
return;
}
}
Config::addGameInstallDir(Common::FS::PathFromQString(gamesDirectory));
Config::setAddonInstallDir(Common::FS::PathFromQString(addonsDirectory));
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
Config::save(config_dir / "config.toml");
accept();
}

View File

@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include "common/config.h"
#include "common/path_util.h"
class QLineEdit;
class GameInstallDialog final : public QDialog {
Q_OBJECT
public:
GameInstallDialog();
~GameInstallDialog();
private slots:
void BrowseGamesDirectory();
void BrowseAddonsDirectory();
private:
QWidget* SetupGamesDirectory();
QWidget* SetupAddonsDirectory();
QWidget* SetupDialogActions();
void Save();
private:
QLineEdit* m_gamesDirectory;
QLineEdit* m_addonsDirectory;
};

View File

@ -1,514 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QToolTip>
#include "common/config.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#include "common/string_util.h"
#include "game_list_frame.h"
#include "game_list_utils.h"
GameListFrame::GameListFrame(std::shared_ptr<gui_settings> gui_settings,
std::shared_ptr<GameInfoClass> game_info_get,
std::shared_ptr<CompatibilityInfoClass> compat_info_get,
QWidget* parent)
: QTableWidget(parent), m_gui_settings(std::move(gui_settings)), m_game_info(game_info_get),
m_compat_info(compat_info_get) {
icon_size = m_gui_settings->GetValue(gui::gl_icon_size).toInt();
last_favorite = "";
this->setShowGrid(false);
this->setEditTriggers(QAbstractItemView::NoEditTriggers);
this->setSelectionBehavior(QAbstractItemView::SelectRows);
this->setSelectionMode(QAbstractItemView::SingleSelection);
this->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
this->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
this->verticalScrollBar()->installEventFilter(this);
this->verticalScrollBar()->setSingleStep(20);
this->horizontalScrollBar()->setSingleStep(20);
this->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
this->verticalHeader()->setVisible(false);
this->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
this->horizontalHeader()->setHighlightSections(false);
this->horizontalHeader()->setSortIndicatorShown(true);
this->setContextMenuPolicy(Qt::CustomContextMenu);
this->setColumnCount(11);
this->setColumnWidth(1, 300); // Name
this->setColumnWidth(2, 140); // Compatibility
this->setColumnWidth(3, 120); // Serial
this->setColumnWidth(4, 90); // Region
this->setColumnWidth(5, 90); // Firmware
this->setColumnWidth(6, 90); // Size
this->setColumnWidth(7, 90); // Version
this->setColumnWidth(8, 120); // Play Time
this->setColumnWidth(10, 90); // Favorite
QStringList headers;
headers << tr("Icon") << tr("Name") << tr("Compatibility") << tr("Serial") << tr("Region")
<< tr("Firmware") << tr("Size") << tr("Version") << tr("Play Time") << tr("Path")
<< tr("Favorite");
this->setHorizontalHeaderLabels(headers);
this->horizontalHeader()->setSortIndicatorShown(true);
this->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
this->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Fixed);
this->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Fixed);
this->horizontalHeader()->setSectionResizeMode(9, QHeaderView::Stretch);
this->horizontalHeader()->setSectionResizeMode(10, QHeaderView::Fixed);
PopulateGameList();
connect(this, &QTableWidget::currentCellChanged, this, &GameListFrame::onCurrentCellChanged);
connect(this->verticalScrollBar(), &QScrollBar::valueChanged, this,
&GameListFrame::RefreshListBackgroundImage);
connect(this->horizontalScrollBar(), &QScrollBar::valueChanged, this,
&GameListFrame::RefreshListBackgroundImage);
this->horizontalHeader()->setSortIndicatorShown(true);
this->horizontalHeader()->setSectionsClickable(true);
QObject::connect(
this->horizontalHeader(), &QHeaderView::sectionClicked, this, [this](int columnIndex) {
if (ListSortedAsc) {
SortNameDescending(columnIndex);
this->horizontalHeader()->setSortIndicator(columnIndex, Qt::DescendingOrder);
ListSortedAsc = false;
sortColumn = columnIndex;
} else {
SortNameAscending(columnIndex);
this->horizontalHeader()->setSortIndicator(columnIndex, Qt::AscendingOrder);
ListSortedAsc = true;
sortColumn = columnIndex;
}
this->clearContents();
PopulateGameList(false);
});
connect(this, &QTableWidget::customContextMenuRequested, this, [=, this](const QPoint& pos) {
int changedFavorite = m_gui_context_menus.RequestGameMenu(
pos, m_game_info->m_games, m_compat_info, m_gui_settings, this, true);
PopulateGameList(false);
});
connect(this, &QTableWidget::cellClicked, this, [=, this](int row, int column) {
if (column == 2 && m_game_info->m_games[row].compatibility.issue_number != "") {
auto url_issues =
"https://github.com/shadps4-compatibility/shadps4-game-compatibility/issues/";
QDesktopServices::openUrl(
QUrl(url_issues + m_game_info->m_games[row].compatibility.issue_number));
} else if (column == 10) {
last_favorite = m_game_info->m_games[row].serial;
QString serialStr = QString::fromStdString(last_favorite);
QList<QString> list =
gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list));
bool isFavorite = list.contains(serialStr);
if (isFavorite) {
list.removeOne(serialStr);
} else {
list.append(serialStr);
}
m_gui_settings->SetValue(gui::favorites_list, gui_settings::List2Var(list));
PopulateGameList(false);
}
});
}
void GameListFrame::onCurrentCellChanged(int currentRow, int currentColumn, int previousRow,
int previousColumn) {
QTableWidgetItem* item = this->item(currentRow, currentColumn);
if (!item) {
return;
}
m_current_item = item; // Store current item
SetListBackgroundImage(item);
PlayBackgroundMusic(item);
}
void GameListFrame::PlayBackgroundMusic(QTableWidgetItem* item) {
if (!item || !m_gui_settings->GetValue(gui::gl_playBackgroundMusic).toBool()) {
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
QString snd0path;
Common::FS::PathToQString(snd0path, m_game_info->m_games[item->row()].snd0_path);
BackgroundMusicPlayer::getInstance().playMusic(snd0path);
}
void GameListFrame::PopulateGameList(bool isInitialPopulation) {
this->m_current_item = nullptr;
// Do not show status column if it is not enabled
this->setColumnHidden(2, !Config::getCompatibilityEnabled());
this->setColumnHidden(6, !Config::GetLoadGameSizeEnabled());
this->setRowCount(m_game_info->m_games.size());
ResizeIcons(icon_size);
ApplyLastSorting(isInitialPopulation);
for (int i = 0; i < m_game_info->m_games.size(); i++) {
SetTableItem(i, 1, QString::fromStdString(m_game_info->m_games[i].name));
if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) /
(m_game_info->m_games[i].serial + ".toml"))) {
QTableWidgetItem* name_item = item(i, 1);
name_item->setIcon(QIcon(":images/game_settings.png"));
}
SetTableItem(i, 3, QString::fromStdString(m_game_info->m_games[i].serial));
SetRegionFlag(i, 4, QString::fromStdString(m_game_info->m_games[i].region));
SetTableItem(i, 5, QString::fromStdString(m_game_info->m_games[i].fw));
SetTableItem(i, 6, QString::fromStdString(m_game_info->m_games[i].size));
SetTableItem(i, 7, QString::fromStdString(m_game_info->m_games[i].version));
SetFavoriteIcon(i, 10);
if (m_game_info->m_games[i].serial == last_favorite && !isInitialPopulation) {
this->setCurrentCell(i, 10);
}
m_game_info->m_games[i].compatibility =
m_compat_info->GetCompatibilityInfo(m_game_info->m_games[i].serial);
SetCompatibilityItem(i, 2, m_game_info->m_games[i].compatibility);
QString playTime = GetPlayTime(m_game_info->m_games[i].serial);
if (playTime.isEmpty()) {
m_game_info->m_games[i].play_time = "0:00:00";
SetTableItem(i, 8, tr("Never Played"));
} else {
QStringList timeParts = playTime.split(':');
int hours = timeParts[0].toInt();
int minutes = timeParts[1].toInt();
int seconds = timeParts[2].toInt();
QString formattedPlayTime;
if (hours > 0) {
formattedPlayTime += QString("%1").arg(hours) + tr("h");
}
if (minutes > 0) {
formattedPlayTime += QString("%1").arg(minutes) + tr("m");
}
formattedPlayTime = formattedPlayTime.trimmed();
m_game_info->m_games[i].play_time = playTime.toStdString();
if (formattedPlayTime.isEmpty()) {
SetTableItem(i, 8, QString("%1").arg(seconds) + tr("s"));
} else {
SetTableItem(i, 8, formattedPlayTime);
}
}
QString path;
Common::FS::PathToQString(path, m_game_info->m_games[i].path);
SetTableItem(i, 9, path);
}
}
void GameListFrame::SetListBackgroundImage(QTableWidgetItem* item) {
if (!item) {
// handle case where no item was clicked
return;
}
// If background images are hidden, clear the background image
if (!m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()) {
backgroundImage = QImage();
m_last_opacity = -1; // Reset opacity tracking when disabled
m_current_game_path.clear(); // Reset current game path
RefreshListBackgroundImage();
return;
}
const auto& game = m_game_info->m_games[item->row()];
const int opacity = m_gui_settings->GetValue(gui::gl_backgroundImageOpacity).toInt();
// Recompute if opacity changed or we switched to a different game
if (opacity != m_last_opacity || game.pic_path != m_current_game_path) {
auto image_path = game.pic_path.u8string();
QImage original_image(QString::fromStdString({image_path.begin(), image_path.end()}));
if (!original_image.isNull()) {
backgroundImage = m_game_list_utils.ChangeImageOpacity(
original_image, original_image.rect(), opacity / 100.0f);
m_last_opacity = opacity;
m_current_game_path = game.pic_path;
}
}
RefreshListBackgroundImage();
}
void GameListFrame::RefreshListBackgroundImage() {
QPalette palette;
if (!backgroundImage.isNull() &&
m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()) {
QSize widgetSize = size();
QPixmap scaledPixmap =
QPixmap::fromImage(backgroundImage)
.scaled(widgetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
int x = (widgetSize.width() - scaledPixmap.width()) / 2;
int y = (widgetSize.height() - scaledPixmap.height()) / 2;
QPixmap finalPixmap(widgetSize);
finalPixmap.fill(Qt::transparent);
QPainter painter(&finalPixmap);
painter.drawPixmap(x, y, scaledPixmap);
palette.setBrush(QPalette::Base, QBrush(finalPixmap));
}
QColor transparentColor = QColor(135, 206, 235, 40);
palette.setColor(QPalette::Highlight, transparentColor);
this->setPalette(palette);
}
void GameListFrame::resizeEvent(QResizeEvent* event) {
QTableWidget::resizeEvent(event);
RefreshListBackgroundImage();
}
bool GameListFrame::CompareWithFavorite(GameInfo a, GameInfo b, int columnIndex, bool ascending) {
std::string serial_a = a.serial;
std::string serial_b = b.serial;
QString serialStr_a = QString::fromStdString(a.serial);
QString serialStr_b = QString::fromStdString(b.serial);
QList<QString> list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list));
bool isFavorite_a = list.contains(serialStr_a);
bool isFavorite_b = list.contains(serialStr_b);
if (isFavorite_a != isFavorite_b) {
return isFavorite_a;
} else if (ascending) {
return CompareStringsAscending(a, b, columnIndex);
} else {
return CompareStringsDescending(a, b, columnIndex);
}
}
void GameListFrame::SortNameAscending(int columnIndex) {
std::sort(m_game_info->m_games.begin(), m_game_info->m_games.end(),
[this, columnIndex](const GameInfo& a, const GameInfo& b) {
return this->CompareWithFavorite(a, b, columnIndex, true);
});
}
void GameListFrame::SortNameDescending(int columnIndex) {
std::sort(m_game_info->m_games.begin(), m_game_info->m_games.end(),
[this, columnIndex](const GameInfo& a, const GameInfo& b) {
return this->CompareWithFavorite(a, b, columnIndex, false);
});
}
void GameListFrame::ApplyLastSorting(bool isInitialPopulation) {
if (isInitialPopulation) {
SortNameAscending(1); // Column 1 = Name
ResizeIcons(icon_size);
} else if (ListSortedAsc) {
SortNameAscending(sortColumn);
ResizeIcons(icon_size);
} else {
SortNameDescending(sortColumn);
ResizeIcons(icon_size);
}
}
void GameListFrame::ResizeIcons(int iconSize) {
for (int index = 0; auto& game : m_game_info->m_games) {
QImage scaledPixmap = game.icon.scaled(QSize(iconSize, iconSize), Qt::KeepAspectRatio,
Qt::SmoothTransformation);
QTableWidgetItem* iconItem = new QTableWidgetItem();
this->verticalHeader()->resizeSection(index, scaledPixmap.height());
this->horizontalHeader()->resizeSection(0, scaledPixmap.width());
iconItem->setData(Qt::DecorationRole, scaledPixmap);
this->setItem(index, 0, iconItem);
index++;
}
this->horizontalHeader()->setSectionResizeMode(8, QHeaderView::ResizeToContents);
}
void GameListFrame::SetCompatibilityItem(int row, int column, CompatibilityEntry entry) {
QTableWidgetItem* item = new QTableWidgetItem();
QWidget* widget = new QWidget(this);
QGridLayout* layout = new QGridLayout(widget);
widget->setStyleSheet("QToolTip {background-color: black; color: white;}");
QColor color;
QString status_explanation;
switch (entry.status) {
case CompatibilityStatus::Unknown:
color = QStringLiteral("#000000");
status_explanation = tr("Compatibility is untested");
break;
case CompatibilityStatus::Nothing:
color = QStringLiteral("#212121");
status_explanation = tr("Game does not initialize properly / crashes the emulator");
break;
case CompatibilityStatus::Boots:
color = QStringLiteral("#828282");
status_explanation = tr("Game boots, but only displays a blank screen");
break;
case CompatibilityStatus::Menus:
color = QStringLiteral("#FF0000");
status_explanation = tr("Game displays an image but does not go past the menu");
break;
case CompatibilityStatus::Ingame:
color = QStringLiteral("#F2D624");
status_explanation = tr("Game has game-breaking glitches or unplayable performance");
break;
case CompatibilityStatus::Playable:
color = QStringLiteral("#47D35C");
status_explanation =
tr("Game can be completed with playable performance and no major glitches");
break;
}
QString tooltip_string;
if (entry.status == CompatibilityStatus::Unknown) {
tooltip_string = status_explanation;
} else {
tooltip_string =
"<p> <i>" + tr("Click to see details on github") + "</i>" + "<br>" +
tr("Last updated") +
QString(": %1 (%2)").arg(entry.last_tested.toString("yyyy-MM-dd"), entry.version) +
"<br>" + status_explanation + "</p>";
}
QPixmap circle_pixmap(16, 16);
circle_pixmap.fill(Qt::transparent);
QPainter painter(&circle_pixmap);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(color);
painter.setBrush(color);
painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 6.0, 6.0);
QLabel* dotLabel = new QLabel("", widget);
dotLabel->setPixmap(circle_pixmap);
QLabel* label = new QLabel(m_compat_info->GetCompatStatusString(entry.status), widget);
this->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
label->setStyleSheet("color: white; font-size: 16px; font-weight: bold;");
// Create shadow effect
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect();
shadowEffect->setBlurRadius(5); // Set the blur radius of the shadow
shadowEffect->setColor(QColor(0, 0, 0, 160)); // Set the color and opacity of the shadow
shadowEffect->setOffset(2, 2); // Set the offset of the shadow
label->setGraphicsEffect(shadowEffect); // Apply shadow effect to the QLabel
layout->addWidget(dotLabel, 0, 0, -1, 1);
layout->addWidget(label, 0, 1, 1, 1);
layout->setAlignment(Qt::AlignLeft);
widget->setLayout(layout);
widget->setToolTip(tooltip_string);
this->setItem(row, column, item);
this->setCellWidget(row, column, widget);
return;
}
void GameListFrame::SetTableItem(int row, int column, QString itemStr) {
QTableWidgetItem* item = new QTableWidgetItem();
QWidget* widget = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(widget);
QLabel* label = new QLabel(itemStr, widget);
label->setStyleSheet("color: white; font-size: 16px; font-weight: bold;");
// Create shadow effect
QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect();
shadowEffect->setBlurRadius(5); // Set the blur radius of the shadow
shadowEffect->setColor(QColor(0, 0, 0, 160)); // Set the color and opacity of the shadow
shadowEffect->setOffset(2, 2); // Set the offset of the shadow
label->setGraphicsEffect(shadowEffect); // Apply shadow effect to the QLabel
layout->addWidget(label);
if (column != 8 && column != 1)
layout->setAlignment(Qt::AlignCenter);
widget->setLayout(layout);
this->setItem(row, column, item);
this->setCellWidget(row, column, widget);
}
void GameListFrame::SetRegionFlag(int row, int column, QString itemStr) {
QTableWidgetItem* item = new QTableWidgetItem();
QImage scaledPixmap;
if (itemStr == "Japan") {
scaledPixmap = QImage(":images/flag_jp.png");
} else if (itemStr == "Europe") {
scaledPixmap = QImage(":images/flag_eu.png");
} else if (itemStr == "USA") {
scaledPixmap = QImage(":images/flag_us.png");
} else if (itemStr == "Asia") {
scaledPixmap = QImage(":images/flag_china.png");
} else if (itemStr == "World") {
scaledPixmap = QImage(":images/flag_world.png");
} else {
scaledPixmap = QImage(":images/flag_unk.png");
}
QWidget* widget = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(widget);
QLabel* label = new QLabel(widget);
label->setPixmap(QPixmap::fromImage(scaledPixmap));
layout->setAlignment(Qt::AlignCenter);
layout->addWidget(label);
widget->setLayout(layout);
this->setItem(row, column, item);
this->setCellWidget(row, column, widget);
}
void GameListFrame::SetFavoriteIcon(int row, int column) {
QString serialStr = QString::fromStdString(m_game_info->m_games[row].serial);
QList<QString> list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list));
bool isFavorite = list.contains(serialStr);
QTableWidgetItem* item = new QTableWidgetItem();
QImage scaledPixmap = QImage(":images/favorite_icon.png");
scaledPixmap = scaledPixmap.scaledToHeight(this->columnWidth(column) / 2.5);
scaledPixmap = scaledPixmap.scaledToWidth(this->columnWidth(column) / 2.5);
QWidget* widget = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(widget);
QLabel* label = new QLabel(widget);
label->setPixmap(QPixmap::fromImage(scaledPixmap));
label->setObjectName("favoriteIcon");
label->setVisible(isFavorite);
layout->setAlignment(Qt::AlignCenter);
layout->addWidget(label);
widget->setLayout(layout);
this->setItem(row, column, item);
this->setCellWidget(row, column, widget);
if (column > 0) {
this->horizontalHeader()->setSectionResizeMode(column - 1, QHeaderView::Stretch);
}
}
QString GameListFrame::GetPlayTime(const std::string& serial) {
QString playTime;
const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
QString filePath = QString::fromStdString((user_dir / "play_time.txt").string());
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return playTime;
}
while (!file.atEnd()) {
QByteArray line = file.readLine();
QString lineStr = QString::fromUtf8(line).trimmed();
QStringList parts = lineStr.split(' ');
if (parts.size() >= 2) {
QString fileSerial = parts[0];
QString time = parts[1];
if (fileSerial == QString::fromStdString(serial)) {
playTime = time;
break;
}
}
}
file.close();
return playTime;
}
QTableWidgetItem* GameListFrame::GetCurrentItem() {
return m_current_item;
}

View File

@ -1,139 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm> // std::transform
#include <cctype> // std::tolower
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPainter>
#include <QScrollBar>
#include "background_music_player.h"
#include "compatibility_info.h"
#include "game_info.h"
#include "game_list_utils.h"
#include "gui_context_menus.h"
#include "gui_settings.h"
class GameListFrame : public QTableWidget {
Q_OBJECT
public:
explicit GameListFrame(std::shared_ptr<gui_settings> gui_settings,
std::shared_ptr<GameInfoClass> game_info_get,
std::shared_ptr<CompatibilityInfoClass> compat_info_get,
QWidget* parent = nullptr);
Q_SIGNALS:
void GameListFrameClosed();
public Q_SLOTS:
void SetListBackgroundImage(QTableWidgetItem* item);
void RefreshListBackgroundImage();
void resizeEvent(QResizeEvent* event);
void SortNameAscending(int columnIndex);
void SortNameDescending(int columnIndex);
void PlayBackgroundMusic(QTableWidgetItem* item);
void onCurrentCellChanged(int currentRow, int currentColumn, int previousRow,
int previousColumn);
private:
void SetTableItem(int row, int column, QString itemStr);
void SetRegionFlag(int row, int column, QString itemStr);
void SetFavoriteIcon(int row, int column);
void SetCompatibilityItem(int row, int column, CompatibilityEntry entry);
QString GetPlayTime(const std::string& serial);
QList<QAction*> m_columnActs;
GameInfoClass* game_inf_get = nullptr;
bool ListSortedAsc = true;
int sortColumn = 1;
QTableWidgetItem* m_current_item = nullptr;
int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation
std::filesystem::path m_current_game_path; // Track current game path to detect changes
std::shared_ptr<gui_settings> m_gui_settings;
public:
void PopulateGameList(bool isInitialPopulation = true);
void ResizeIcons(int iconSize);
void ApplyLastSorting(bool isInitialPopulation);
QTableWidgetItem* GetCurrentItem();
QImage backgroundImage;
GameListUtils m_game_list_utils;
GuiContextMenus m_gui_context_menus;
std::shared_ptr<GameInfoClass> m_game_info;
std::shared_ptr<CompatibilityInfoClass> m_compat_info;
int icon_size;
std::string last_favorite;
static float parseAsFloat(const std::string& str, const int& offset) {
return std::stof(str.substr(0, str.size() - offset));
}
static float parseSizeMB(const std::string& size) {
float num = parseAsFloat(size, 3);
return (size[size.size() - 2] == 'G') ? num * 1024 : num;
}
static bool CompareStringsAscending(GameInfo a, GameInfo b, int columnIndex) {
switch (columnIndex) {
case 1: {
std::string name_a = a.name, name_b = b.name;
std::transform(name_a.begin(), name_a.end(), name_a.begin(), ::tolower);
std::transform(name_b.begin(), name_b.end(), name_b.begin(), ::tolower);
return name_a < name_b;
}
case 2:
return a.compatibility.status < b.compatibility.status;
case 3:
return a.serial.substr(4) < b.serial.substr(4);
case 4:
return a.region < b.region;
case 5:
return parseAsFloat(a.fw, 0) < parseAsFloat(b.fw, 0);
case 6:
return parseSizeMB(b.size) < parseSizeMB(a.size);
case 7:
return a.version < b.version;
case 8:
return a.play_time < b.play_time;
case 9:
return a.path < b.path;
default:
return false;
}
}
static bool CompareStringsDescending(GameInfo a, GameInfo b, int columnIndex) {
switch (columnIndex) {
case 1: {
std::string name_a = a.name, name_b = b.name;
std::transform(name_a.begin(), name_a.end(), name_a.begin(), ::tolower);
std::transform(name_b.begin(), name_b.end(), name_b.begin(), ::tolower);
return name_a > name_b;
}
case 2:
return a.compatibility.status > b.compatibility.status;
case 3:
return a.serial.substr(4) > b.serial.substr(4);
case 4:
return a.region > b.region;
case 5:
return parseAsFloat(a.fw, 0) > parseAsFloat(b.fw, 0);
case 6:
return parseSizeMB(b.size) > parseSizeMB(a.size);
case 7:
return a.version > b.version;
case 8:
return a.play_time > b.play_time;
case 9:
return a.path > b.path;
default:
return false;
}
}
bool CompareWithFavorite(GameInfo a, GameInfo b, int columnIndex, bool ascending);
};

View File

@ -1,235 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <unordered_map>
#include <QDir>
#include <QDirIterator>
#include <QImage>
#include <QString>
#include "common/path_util.h"
#include "compatibility_info.h"
struct GameInfo {
std::filesystem::path path; // root path of game directory
// (normally directory that contains eboot.bin)
std::filesystem::path icon_path; // path of icon0.png
std::filesystem::path pic_path; // path of pic1.png
std::filesystem::path snd0_path; // path of snd0.at9
QImage icon;
std::string size;
// variables extracted from param.sfo
std::string name = "Unknown";
std::string serial = "Unknown";
std::string version = "Unknown";
std::string region = "Unknown";
std::string fw = "Unknown";
std::string save_dir = "Unknown";
std::string play_time = "Unknown";
CompatibilityEntry compatibility = CompatibilityEntry{CompatibilityStatus::Unknown};
};
class GameListUtils : public QObject {
Q_OBJECT
public:
static QString FormatSize(qint64 size) {
static const QStringList suffixes = {tr("B"), tr("KB"), tr("MB"), tr("GB"), tr("TB")};
int suffixIndex = 0;
double gameSize = static_cast<double>(size);
while (gameSize >= 1024 && suffixIndex < suffixes.size() - 1) {
gameSize /= 1024;
++suffixIndex;
}
// Format the size with a specified precision
QString sizeString;
if (gameSize < 10.0) {
sizeString = QString::number(gameSize, 'f', 2);
} else if (gameSize < 100.0) {
sizeString = QString::number(gameSize, 'f', 1);
} else {
sizeString = QString::number(gameSize, 'f', 0);
}
return sizeString + " " + suffixes[suffixIndex];
}
static void GetFolderSize(GameInfo& game) {
QString dirPath;
Common::FS::PathToQString(dirPath, game.path);
QDir dir(dirPath);
QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories);
qint64 total = 0;
if (!Config::GetLoadGameSizeEnabled()) {
game.size = FormatSize(0).toStdString();
return;
}
// Cache path
QDir cacheDir =
QDir(Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game.serial);
if (!cacheDir.exists()) {
cacheDir.mkpath(".");
}
QFile size_cache_file(cacheDir.absoluteFilePath("size_cache.txt"));
QFileInfo cacheInfo(size_cache_file);
QFileInfo dirInfo(dirPath);
// Check if cache file exists and is valid
if (size_cache_file.exists() && cacheInfo.lastModified() >= dirInfo.lastModified()) {
if (size_cache_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&size_cache_file);
QString cachedSize = in.readLine();
size_cache_file.close();
if (!cachedSize.isEmpty()) {
game.size = cachedSize.toStdString();
return;
}
}
}
// Cache is invalid or does not exist; calculate size
while (it.hasNext()) {
it.next();
total += it.fileInfo().size();
}
game.size = FormatSize(total).toStdString();
// Save new cache
if (size_cache_file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&size_cache_file);
out << QString::fromStdString(game.size) << "\n";
size_cache_file.close();
}
}
static QString GetRegion(char region) {
switch (region) {
case 'U':
return "USA";
case 'E':
return "Europe";
case 'J':
return "Japan";
case 'H':
return "Asia";
case 'I':
return "World";
default:
return "Unknown";
}
}
static QString GetAppType(int type) {
switch (type) {
case 0:
return "Not Specified";
case 1:
return "FULL APP";
case 2:
return "UPGRADABLE";
case 3:
return "DEMO";
case 4:
return "FREEMIUM";
default:
return "Unknown";
}
}
QImage BlurImage(const QImage& image, const QRect& rect, int radius) {
int tab[] = {14, 10, 8, 6, 5, 5, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2};
int alpha = (radius < 1) ? 16 : (radius > 17) ? 1 : tab[radius - 1];
QImage result = image.convertToFormat(QImage::Format_ARGB32);
int r1 = rect.top();
int r2 = rect.bottom();
int c1 = rect.left();
int c2 = rect.right();
int bpl = result.bytesPerLine();
int rgba[4];
unsigned char* p;
int i1 = 0;
int i2 = 3;
for (int col = c1; col <= c2; col++) {
p = result.scanLine(r1) + col * 4;
for (int i = i1; i <= i2; i++)
rgba[i] = p[i] << 4;
p += bpl;
for (int j = r1; j < r2; j++, p += bpl)
for (int i = i1; i <= i2; i++)
p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4;
}
for (int row = r1; row <= r2; row++) {
p = result.scanLine(row) + c1 * 4;
for (int i = i1; i <= i2; i++)
rgba[i] = p[i] << 4;
p += 4;
for (int j = c1; j < c2; j++, p += 4)
for (int i = i1; i <= i2; i++)
p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4;
}
for (int col = c1; col <= c2; col++) {
p = result.scanLine(r2) + col * 4;
for (int i = i1; i <= i2; i++)
rgba[i] = p[i] << 4;
p -= bpl;
for (int j = r1; j < r2; j++, p -= bpl)
for (int i = i1; i <= i2; i++)
p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4;
}
for (int row = r1; row <= r2; row++) {
p = result.scanLine(row) + c2 * 4;
for (int i = i1; i <= i2; i++)
rgba[i] = p[i] << 4;
p -= 4;
for (int j = c1; j < c2; j++, p -= 4)
for (int i = i1; i <= i2; i++)
p[i] = (rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4;
}
return result;
}
// Opacity is a float between 0 and 1
static QImage ChangeImageOpacity(const QImage& image, const QRect& rect, float opacity) {
// Convert to ARGB32 format to ensure alpha channel support
QImage result = image.convertToFormat(QImage::Format_ARGB32);
// Ensure opacity is between 0 and 1
opacity = std::clamp(opacity, 0.0f, 1.0f);
// Convert opacity to integer alpha value (0-255)
int alpha = static_cast<int>(opacity * 255);
// Process only the specified rectangle area
for (int y = rect.top(); y <= rect.bottom(); ++y) {
QRgb* line = reinterpret_cast<QRgb*>(result.scanLine(y));
for (int x = rect.left(); x <= rect.right(); ++x) {
// Get current pixel
QRgb pixel = line[x];
// Keep RGB values, but modify alpha while preserving relative transparency
int newAlpha = (qAlpha(pixel) * alpha) / 255;
line[x] = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), newAlpha);
}
}
return result;
}
};

View File

@ -1,755 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QClipboard>
#include <QDesktopServices>
#include <QMenu>
#include <QMessageBox>
#include <QTreeWidgetItem>
#include <qt_gui/background_music_player.h>
#include "cheats_patches.h"
#include "common/config.h"
#include "common/path_util.h"
#include "common/scm_rev.h"
#include "compatibility_info.h"
#include "game_info.h"
#include "gui_settings.h"
#include "settings_dialog.h"
#include "trophy_viewer.h"
#ifdef Q_OS_WIN
#include <ShlObj.h>
#include <Windows.h>
#include <objbase.h>
#include <shlguid.h>
#include <shobjidl.h>
#include <wrl/client.h>
#endif
class GuiContextMenus : public QObject {
Q_OBJECT
public:
int RequestGameMenu(const QPoint& pos, QVector<GameInfo>& m_games,
std::shared_ptr<CompatibilityInfoClass> m_compat_info,
std::shared_ptr<gui_settings> settings, QTableWidget* widget, bool isList) {
QPoint global_pos = widget->viewport()->mapToGlobal(pos);
std::shared_ptr<gui_settings> m_gui_settings = std::move(settings);
int itemID = 0;
int changedFavorite = 0;
if (isList) {
itemID = widget->currentRow();
} else {
itemID = widget->currentRow() * widget->columnCount() + widget->currentColumn();
}
// Do not show the menu if no item is selected
if (itemID < 0 || itemID >= m_games.size()) {
return changedFavorite;
}
// Setup menu.
QMenu menu(widget);
// "Open Folder..." submenu
QMenu* openFolderMenu = new QMenu(tr("Open Folder..."), widget);
QAction* openGameFolder = new QAction(tr("Open Game Folder"), widget);
QAction* openUpdateFolder = new QAction(tr("Open Update Folder"), widget);
QAction* openSaveDataFolder = new QAction(tr("Open Save Data Folder"), widget);
QAction* openLogFolder = new QAction(tr("Open Log Folder"), widget);
openFolderMenu->addAction(openGameFolder);
openFolderMenu->addAction(openUpdateFolder);
openFolderMenu->addAction(openSaveDataFolder);
openFolderMenu->addAction(openLogFolder);
menu.addMenu(openFolderMenu);
QMenu* gameConfigMenu = new QMenu(tr("Game-specific Settings..."), widget);
QAction gameConfigConfigure(tr("Configure Game-specific Settings"), widget);
QAction gameConfigCreate(tr("Create Game-specific Settings from Global Settings"), widget);
QAction gameConfigDelete(tr("Delete Game-specific Settings"), widget);
if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) /
(m_games[itemID].serial + ".toml"))) {
gameConfigMenu->addAction(&gameConfigConfigure);
} else {
gameConfigMenu->addAction(&gameConfigCreate);
}
if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) /
(m_games[itemID].serial + ".toml")))
gameConfigMenu->addAction(&gameConfigDelete);
menu.addMenu(gameConfigMenu);
QString serialStr = QString::fromStdString(m_games[itemID].serial);
QList<QString> list = gui_settings::Var2List(m_gui_settings->GetValue(gui::favorites_list));
bool isFavorite = list.contains(serialStr);
QAction* toggleFavorite;
if (isFavorite) {
toggleFavorite = new QAction(tr("Remove from Favorites"), widget);
} else {
toggleFavorite = new QAction(tr("Add to Favorites"), widget);
}
QAction createShortcut(tr("Create Shortcut"), widget);
QAction openCheats(tr("Cheats / Patches"), widget);
QAction openSfoViewer(tr("SFO Viewer"), widget);
QAction openTrophyViewer(tr("Trophy Viewer"), widget);
menu.addAction(toggleFavorite);
menu.addAction(&createShortcut);
menu.addAction(&openCheats);
menu.addAction(&openSfoViewer);
menu.addAction(&openTrophyViewer);
// "Copy" submenu.
QMenu* copyMenu = new QMenu(tr("Copy info..."), widget);
QAction* copyName = new QAction(tr("Copy Name"), widget);
QAction* copySerial = new QAction(tr("Copy Serial"), widget);
QAction* copyVersion = new QAction(tr("Copy Version"), widget);
QAction* copySize = new QAction(tr("Copy Size"), widget);
QAction* copyNameAll = new QAction(tr("Copy All"), widget);
copyMenu->addAction(copyName);
copyMenu->addAction(copySerial);
copyMenu->addAction(copyVersion);
if (Config::GetLoadGameSizeEnabled()) {
copyMenu->addAction(copySize);
}
copyMenu->addAction(copyNameAll);
menu.addMenu(copyMenu);
// "Delete..." submenu.
QMenu* deleteMenu = new QMenu(tr("Delete..."), widget);
QAction* deleteGame = new QAction(tr("Delete Game"), widget);
QAction* deleteUpdate = new QAction(tr("Delete Update"), widget);
QAction* deleteSaveData = new QAction(tr("Delete Save Data"), widget);
QAction* deleteDLC = new QAction(tr("Delete DLC"), widget);
QAction* deleteTrophy = new QAction(tr("Delete Trophy"), widget);
deleteMenu->addAction(deleteGame);
deleteMenu->addAction(deleteUpdate);
deleteMenu->addAction(deleteSaveData);
deleteMenu->addAction(deleteDLC);
deleteMenu->addAction(deleteTrophy);
menu.addMenu(deleteMenu);
// Compatibility submenu.
QMenu* compatibilityMenu = new QMenu(tr("Compatibility..."), widget);
QAction* updateCompatibility = new QAction(tr("Update Database"), widget);
QAction* viewCompatibilityReport = new QAction(tr("View Report"), widget);
QAction* submitCompatibilityReport = new QAction(tr("Submit a Report"), widget);
compatibilityMenu->addAction(updateCompatibility);
compatibilityMenu->addAction(viewCompatibilityReport);
if (Common::g_is_release) {
compatibilityMenu->addAction(submitCompatibilityReport);
}
menu.addMenu(compatibilityMenu);
compatibilityMenu->setEnabled(Config::getCompatibilityEnabled());
viewCompatibilityReport->setEnabled(m_games[itemID].compatibility.status !=
CompatibilityStatus::Unknown);
// Show menu.
auto selected = menu.exec(global_pos);
if (!selected) {
return changedFavorite;
}
if (selected == openGameFolder) {
QString folderPath;
Common::FS::PathToQString(folderPath, m_games[itemID].path);
QDesktopServices::openUrl(QUrl::fromLocalFile(folderPath));
}
if (selected == openUpdateFolder) {
QString open_update_path;
Common::FS::PathToQString(open_update_path, m_games[itemID].path);
open_update_path += "-UPDATE";
if (std::filesystem::exists(Common::FS::PathFromQString(open_update_path))) {
QDesktopServices::openUrl(QUrl::fromLocalFile(open_update_path));
} else {
Common::FS::PathToQString(open_update_path, m_games[itemID].path);
open_update_path += "-patch";
if (std::filesystem::exists(Common::FS::PathFromQString(open_update_path))) {
QDesktopServices::openUrl(QUrl::fromLocalFile(open_update_path));
} else {
QMessageBox::critical(nullptr, tr("Error"),
QString(tr("This game has no update folder to open!")));
}
}
}
if (selected == openSaveDataFolder) {
QString saveDataPath;
Common::FS::PathToQString(saveDataPath,
Config::GetSaveDataPath() / "1" / m_games[itemID].save_dir);
QDir(saveDataPath).mkpath(saveDataPath);
QDesktopServices::openUrl(QUrl::fromLocalFile(saveDataPath));
}
if (selected == openLogFolder) {
QString logPath;
Common::FS::PathToQString(logPath,
Common::FS::GetUserPath(Common::FS::PathType::LogDir));
if (!Config::getSeparateLogFilesEnabled()) {
QDesktopServices::openUrl(QUrl::fromLocalFile(logPath));
} else {
QString fileName = QString::fromStdString(m_games[itemID].serial) + ".log";
QString filePath = logPath + "/" + fileName;
QStringList arguments;
if (QFile::exists(filePath)) {
#ifdef Q_OS_WIN
arguments << "/select," << filePath.replace("/", "\\");
QProcess::startDetached("explorer", arguments);
#elif defined(Q_OS_MAC)
arguments << "-R" << filePath;
QProcess::startDetached("open", arguments);
#elif defined(Q_OS_LINUX)
QStringList arguments;
arguments << "--select" << filePath;
if (!QProcess::startDetached("nautilus", arguments)) {
// Failed to open Nautilus to select file
arguments.clear();
arguments << logPath;
if (!QProcess::startDetached("xdg-open", arguments)) {
// Failed to open directory on Linux
}
}
#else
QDesktopServices::openUrl(QUrl::fromLocalFile(logPath));
#endif
} else {
QMessageBox msgBox;
msgBox.setIcon(QMessageBox::Information);
msgBox.setText(tr("No log file found for this game!"));
QPushButton* okButton = msgBox.addButton(QMessageBox::Ok);
QPushButton* openFolderButton =
msgBox.addButton(tr("Open Log Folder"), QMessageBox::ActionRole);
msgBox.exec();
if (msgBox.clickedButton() == openFolderButton) {
QDesktopServices::openUrl(QUrl::fromLocalFile(logPath));
}
}
}
}
if (selected == &openSfoViewer) {
PSF psf;
QString gameName = QString::fromStdString(m_games[itemID].name);
std::filesystem::path game_folder_path = m_games[itemID].path;
std::filesystem::path game_update_path = game_folder_path;
game_update_path += "-UPDATE";
if (std::filesystem::exists(game_update_path)) {
game_folder_path = game_update_path;
} else {
game_update_path = game_folder_path;
game_update_path += "-patch";
if (std::filesystem::exists(game_update_path)) {
game_folder_path = game_update_path;
}
}
if (psf.Open(game_folder_path / "sce_sys" / "param.sfo")) {
int rows = psf.GetEntries().size();
QTableWidget* tableWidget = new QTableWidget(rows, 2);
tableWidget->setAttribute(Qt::WA_DeleteOnClose);
connect(widget->parent(), &QWidget::destroyed, tableWidget,
[tableWidget]() { tableWidget->deleteLater(); });
tableWidget->verticalHeader()->setVisible(false); // Hide vertical header
int row = 0;
for (const auto& entry : psf.GetEntries()) {
QTableWidgetItem* keyItem =
new QTableWidgetItem(QString::fromStdString(entry.key));
QTableWidgetItem* valueItem;
switch (entry.param_fmt) {
case PSFEntryFmt::Binary: {
const auto bin = psf.GetBinary(entry.key);
if (!bin.has_value()) {
valueItem = new QTableWidgetItem(QString("Unknown"));
} else {
std::string text;
text.reserve(bin->size() * 2);
for (const auto& c : *bin) {
static constexpr char hex[] = "0123456789ABCDEF";
text.push_back(hex[c >> 4 & 0xF]);
text.push_back(hex[c & 0xF]);
}
valueItem = new QTableWidgetItem(QString::fromStdString(text));
}
} break;
case PSFEntryFmt::Text: {
auto text = psf.GetString(entry.key);
if (!text.has_value()) {
valueItem = new QTableWidgetItem(QString("Unknown"));
} else {
valueItem =
new QTableWidgetItem(QString::fromStdString(std::string{*text}));
}
} break;
case PSFEntryFmt::Integer: {
auto integer = psf.GetInteger(entry.key);
if (!integer.has_value()) {
valueItem = new QTableWidgetItem(QString("Unknown"));
} else {
valueItem =
new QTableWidgetItem(QString("0x") + QString::number(*integer, 16));
}
} break;
}
tableWidget->setItem(row, 0, keyItem);
tableWidget->setItem(row, 1, valueItem);
keyItem->setFlags(keyItem->flags() & ~Qt::ItemIsEditable);
valueItem->setFlags(valueItem->flags() & ~Qt::ItemIsEditable);
row++;
}
tableWidget->resizeColumnsToContents();
tableWidget->resizeRowsToContents();
int width = tableWidget->horizontalHeader()->sectionSize(0) +
tableWidget->horizontalHeader()->sectionSize(1) + 2;
int height = (rows + 1) * (tableWidget->rowHeight(0));
tableWidget->setFixedSize(width, height);
tableWidget->sortItems(0, Qt::AscendingOrder);
tableWidget->horizontalHeader()->setVisible(false);
tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
tableWidget->setWindowTitle(tr("SFO Viewer for ") + gameName);
tableWidget->show();
}
}
if (selected == toggleFavorite) {
if (isFavorite) {
list.removeOne(serialStr);
} else {
list.append(serialStr);
}
m_gui_settings->SetValue(gui::favorites_list, gui_settings::List2Var(list));
changedFavorite = 1;
}
if (selected == &openCheats) {
QString gameName = QString::fromStdString(m_games[itemID].name);
QString gameSerial = QString::fromStdString(m_games[itemID].serial);
QString gameVersion = QString::fromStdString(m_games[itemID].version);
QString gameSize = QString::fromStdString(m_games[itemID].size);
QString iconPath;
Common::FS::PathToQString(iconPath, m_games[itemID].icon_path);
QPixmap gameImage(iconPath);
CheatsPatches* cheatsPatches =
new CheatsPatches(gameName, gameSerial, gameVersion, gameSize, gameImage);
cheatsPatches->show();
connect(widget->parent(), &QWidget::destroyed, cheatsPatches,
[cheatsPatches]() { cheatsPatches->deleteLater(); });
}
if (selected == &openTrophyViewer) {
QString trophyPath, gameTrpPath;
Common::FS::PathToQString(trophyPath, m_games[itemID].serial);
Common::FS::PathToQString(gameTrpPath, m_games[itemID].path);
auto game_update_path = Common::FS::PathFromQString(gameTrpPath);
game_update_path += "-UPDATE";
if (std::filesystem::exists(game_update_path)) {
Common::FS::PathToQString(gameTrpPath, game_update_path);
} else {
game_update_path = Common::FS::PathFromQString(gameTrpPath);
game_update_path += "-patch";
if (std::filesystem::exists(game_update_path)) {
Common::FS::PathToQString(gameTrpPath, game_update_path);
}
}
// Array with all games and their trophy information
QVector<TrophyGameInfo> allTrophyGames;
for (const auto& game : m_games) {
TrophyGameInfo gameInfo;
gameInfo.name = QString::fromStdString(game.name);
Common::FS::PathToQString(gameInfo.trophyPath, game.serial);
Common::FS::PathToQString(gameInfo.gameTrpPath, game.path);
auto update_path = Common::FS::PathFromQString(gameInfo.gameTrpPath);
update_path += "-UPDATE";
if (std::filesystem::exists(update_path)) {
Common::FS::PathToQString(gameInfo.gameTrpPath, update_path);
} else {
update_path = Common::FS::PathFromQString(gameInfo.gameTrpPath);
update_path += "-patch";
if (std::filesystem::exists(update_path)) {
Common::FS::PathToQString(gameInfo.gameTrpPath, update_path);
}
}
allTrophyGames.append(gameInfo);
}
QString gameName = QString::fromStdString(m_games[itemID].name);
TrophyViewer* trophyViewer =
new TrophyViewer(m_gui_settings, trophyPath, gameTrpPath, gameName, allTrophyGames);
trophyViewer->show();
connect(widget->parent(), &QWidget::destroyed, trophyViewer,
[trophyViewer]() { trophyViewer->deleteLater(); });
}
if (selected == &gameConfigConfigure || selected == &gameConfigCreate) {
auto settingsWindow = new SettingsDialog(m_gui_settings, m_compat_info, widget, false,
true, serialStr.toStdString());
settingsWindow->exec();
}
if (selected == &gameConfigDelete) {
if (QMessageBox::Yes == QMessageBox::question(widget, tr("Confirm deletion"),
tr("Delete game-specific settings?"),
QMessageBox::Yes | QMessageBox::No)) {
std::filesystem::remove(
Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) /
(m_games[itemID].serial + ".toml"));
}
}
if (selected == &createShortcut) {
QString targetPath;
Common::FS::PathToQString(targetPath, m_games[itemID].path);
QString ebootPath = targetPath + "/eboot.bin";
// Get the full path to the icon
QString iconPath;
Common::FS::PathToQString(iconPath, m_games[itemID].icon_path);
QFileInfo iconFileInfo(iconPath);
QString icoPath = iconFileInfo.absolutePath() + "/" + iconFileInfo.baseName() + ".ico";
// Path to shortcut/link
QString linkPath;
// Path to the shadps4.exe executable
QString exePath;
#ifdef Q_OS_WIN
linkPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) + "/" +
QString::fromStdString(m_games[itemID].name)
.remove(QRegularExpression("[\\\\/:*?\"<>|]")) +
".lnk";
exePath = QCoreApplication::applicationFilePath().replace("\\", "/");
#else
linkPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) + "/" +
QString::fromStdString(m_games[itemID].name)
.remove(QRegularExpression("[\\\\/:*?\"<>|]")) +
".desktop";
#endif
// Convert the icon to .ico if necessary
if (iconFileInfo.suffix().toLower() == "png") {
// Convert icon from PNG to ICO
if (convertPngToIco(iconPath, icoPath)) {
#ifdef Q_OS_WIN
if (createShortcutWin(linkPath, ebootPath, icoPath, exePath)) {
#else
if (createShortcutLinux(linkPath, m_games[itemID].name, ebootPath, iconPath)) {
#endif
QMessageBox::information(
nullptr, tr("Shortcut creation"),
QString(tr("Shortcut created successfully!") + "\n%1").arg(linkPath));
} else {
QMessageBox::critical(
nullptr, tr("Error"),
QString(tr("Error creating shortcut!") + "\n%1").arg(linkPath));
}
} else {
QMessageBox::critical(nullptr, tr("Error"), tr("Failed to convert icon."));
}
} else {
// If the icon is already in ICO format, we just create the shortcut
#ifdef Q_OS_WIN
if (createShortcutWin(linkPath, ebootPath, iconPath, exePath)) {
#else
if (createShortcutLinux(linkPath, m_games[itemID].name, ebootPath, iconPath)) {
#endif
QMessageBox::information(
nullptr, tr("Shortcut creation"),
QString(tr("Shortcut created successfully!") + "\n%1").arg(linkPath));
} else {
QMessageBox::critical(
nullptr, tr("Error"),
QString(tr("Error creating shortcut!") + "\n%1").arg(linkPath));
}
}
}
// Handle the "Copy" actions
if (selected == copyName) {
QClipboard* clipboard = QGuiApplication::clipboard();
clipboard->setText(QString::fromStdString(m_games[itemID].name));
}
if (selected == copySerial) {
QClipboard* clipboard = QGuiApplication::clipboard();
clipboard->setText(QString::fromStdString(m_games[itemID].serial));
}
if (selected == copyVersion) {
QClipboard* clipboard = QGuiApplication::clipboard();
clipboard->setText(QString::fromStdString(m_games[itemID].version));
}
if (selected == copySize) {
QClipboard* clipboard = QGuiApplication::clipboard();
clipboard->setText(QString::fromStdString(m_games[itemID].size));
}
if (selected == copyNameAll) {
QString GameSizeEnabled;
if (Config::GetLoadGameSizeEnabled()) {
GameSizeEnabled = " | Size:" + QString::fromStdString(m_games[itemID].size);
}
QClipboard* clipboard = QGuiApplication::clipboard();
QString combinedText = QString("Name:%1 | Serial:%2 | Version:%3%4")
.arg(QString::fromStdString(m_games[itemID].name))
.arg(QString::fromStdString(m_games[itemID].serial))
.arg(QString::fromStdString(m_games[itemID].version))
.arg(GameSizeEnabled);
clipboard->setText(combinedText);
}
if (selected == deleteGame || selected == deleteUpdate || selected == deleteDLC ||
selected == deleteSaveData || selected == deleteTrophy) {
bool error = false;
QString folder_path, game_update_path, dlc_path, save_data_path, trophy_data_path;
Common::FS::PathToQString(folder_path, m_games[itemID].path);
game_update_path = folder_path + "-UPDATE";
if (!std::filesystem::exists(Common::FS::PathFromQString(game_update_path))) {
game_update_path = folder_path + "-patch";
}
Common::FS::PathToQString(
dlc_path, Config::getAddonInstallDir() /
Common::FS::PathFromQString(folder_path).parent_path().filename());
Common::FS::PathToQString(save_data_path,
Config::GetSaveDataPath() / "1" / m_games[itemID].save_dir);
Common::FS::PathToQString(trophy_data_path,
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) /
m_games[itemID].serial / "TrophyFiles");
QString message_type;
if (selected == deleteGame) {
BackgroundMusicPlayer::getInstance().stopMusic();
message_type = tr("Game");
} else if (selected == deleteUpdate) {
if (!std::filesystem::exists(Common::FS::PathFromQString(game_update_path))) {
QMessageBox::critical(nullptr, tr("Error"),
QString(tr("This game has no update to delete!")));
error = true;
} else {
folder_path = game_update_path;
message_type = tr("Update");
}
} else if (selected == deleteDLC) {
if (!std::filesystem::exists(Common::FS::PathFromQString(dlc_path))) {
QMessageBox::critical(nullptr, tr("Error"),
QString(tr("This game has no DLC to delete!")));
error = true;
} else {
folder_path = dlc_path;
message_type = tr("DLC");
}
} else if (selected == deleteSaveData) {
if (!std::filesystem::exists(Common::FS::PathFromQString(save_data_path))) {
QMessageBox::critical(nullptr, tr("Error"),
QString(tr("This game has no save data to delete!")));
error = true;
} else {
folder_path = save_data_path;
message_type = tr("Save Data");
}
} else if (selected == deleteTrophy) {
if (!std::filesystem::exists(Common::FS::PathFromQString(trophy_data_path))) {
QMessageBox::critical(
nullptr, tr("Error"),
QString(tr("This game has no saved trophies to delete!")));
error = true;
} else {
folder_path = trophy_data_path;
message_type = tr("Trophy");
}
}
if (!error) {
QString gameName = QString::fromStdString(m_games[itemID].name);
QDir dir(folder_path);
QMessageBox::StandardButton reply = QMessageBox::question(
nullptr, QString(tr("Delete %1")).arg(message_type),
QString(tr("Are you sure you want to delete %1's %2 directory?"))
.arg(gameName, message_type),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
dir.removeRecursively();
if (selected == deleteGame) {
widget->removeRow(itemID);
m_games.removeAt(itemID);
}
}
}
}
if (selected == updateCompatibility) {
m_compat_info->UpdateCompatibilityDatabase(widget, true);
}
if (selected == viewCompatibilityReport) {
if (m_games[itemID].compatibility.issue_number != "") {
auto url_issues =
"https://github.com/shadps4-compatibility/shadps4-game-compatibility/issues/";
QDesktopServices::openUrl(
QUrl(url_issues + m_games[itemID].compatibility.issue_number));
}
}
if (selected == submitCompatibilityReport) {
if (m_games[itemID].compatibility.issue_number == "") {
QUrl url = QUrl("https://github.com/shadps4-compatibility/"
"shadps4-game-compatibility/issues/new");
QUrlQuery query;
query.addQueryItem("template", QString("game_compatibility.yml"));
query.addQueryItem(
"title", QString("%1 - %2").arg(QString::fromStdString(m_games[itemID].serial),
QString::fromStdString(m_games[itemID].name)));
query.addQueryItem("game-name", QString::fromStdString(m_games[itemID].name));
query.addQueryItem("game-serial", QString::fromStdString(m_games[itemID].serial));
query.addQueryItem("game-version", QString::fromStdString(m_games[itemID].version));
query.addQueryItem("emulator-version", QString(Common::g_version));
url.setQuery(query);
QDesktopServices::openUrl(url);
} else {
auto url_issues =
"https://github.com/shadps4-compatibility/shadps4-game-compatibility/issues/";
QDesktopServices::openUrl(
QUrl(url_issues + m_games[itemID].compatibility.issue_number));
}
}
return changedFavorite;
}
int GetRowIndex(QTreeWidget* treeWidget, QTreeWidgetItem* item) {
int row = 0;
for (int i = 0; i < treeWidget->topLevelItemCount(); i++) { // check top level/parent items
QTreeWidgetItem* currentItem = treeWidget->topLevelItem(i);
if (currentItem == item) {
return row;
}
row++;
if (currentItem->childCount() > 0) { // check child items
for (int j = 0; j < currentItem->childCount(); j++) {
QTreeWidgetItem* childItem = currentItem->child(j);
if (childItem == item) {
return row;
}
row++;
}
}
}
return -1;
}
private:
bool convertPngToIco(const QString& pngFilePath, const QString& icoFilePath) {
// Load the PNG image
QImage image(pngFilePath);
if (image.isNull()) {
return false;
}
// Scale the image to the default icon size (256x256 pixels)
QImage scaledImage =
image.scaled(QSize(256, 256), Qt::KeepAspectRatio, Qt::SmoothTransformation);
// Convert the image to QPixmap
QPixmap pixmap = QPixmap::fromImage(scaledImage);
// Save the pixmap as an ICO file
if (pixmap.save(icoFilePath, "ICO")) {
return true;
} else {
return false;
}
}
#ifdef Q_OS_WIN
bool createShortcutWin(const QString& linkPath, const QString& targetPath,
const QString& iconPath, const QString& exePath) {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
// Create the ShellLink object
Microsoft::WRL::ComPtr<IShellLink> pShellLink;
HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&pShellLink));
if (SUCCEEDED(hres)) {
// Defines the path to the program executable
pShellLink->SetPath((LPCWSTR)exePath.utf16());
// Sets the home directory ("Start in")
pShellLink->SetWorkingDirectory((LPCWSTR)QFileInfo(exePath).absolutePath().utf16());
// Set arguments, eboot.bin file location
QString arguments = QString("-g \"%1\"").arg(targetPath);
pShellLink->SetArguments((LPCWSTR)arguments.utf16());
// Set the icon for the shortcut
pShellLink->SetIconLocation((LPCWSTR)iconPath.utf16(), 0);
// Save the shortcut
Microsoft::WRL::ComPtr<IPersistFile> pPersistFile;
hres = pShellLink.As(&pPersistFile);
if (SUCCEEDED(hres)) {
hres = pPersistFile->Save((LPCWSTR)linkPath.utf16(), TRUE);
}
}
CoUninitialize();
return SUCCEEDED(hres);
}
#else
bool createShortcutLinux(const QString& linkPath, const std::string& name,
const QString& targetPath, const QString& iconPath) {
QFile shortcutFile(linkPath);
if (!shortcutFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(nullptr, "Error",
QString("Error creating shortcut!\n %1").arg(linkPath));
return false;
}
QTextStream out(&shortcutFile);
out << "[Desktop Entry]\n";
out << "Version=1.0\n";
out << "Name=" << QString::fromStdString(name) << "\n";
out << "Exec=" << QCoreApplication::applicationFilePath() << " \"" << targetPath << "\"\n";
out << "Icon=" << iconPath << "\n";
out << "Terminal=false\n";
out << "Type=Application\n";
shortcutFile.close();
return true;
}
#endif
};

View File

@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "gui_settings.h"
gui_settings::gui_settings(QObject* parent) : settings(parent) {
m_settings = std::make_unique<QSettings>(ComputeSettingsDir() + "qt_ui.ini",
QSettings::Format::IniFormat, parent);
}

View File

@ -1,58 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QWindow>
#include "settings.h"
namespace gui {
// categories
const QString general_settings = "general_settings";
const QString main_window = "main_window";
const QString game_list = "game_list";
const QString game_grid = "game_grid";
const QString favorites = "favorites";
// general
const gui_value gen_checkForUpdates = gui_value(general_settings, "checkForUpdates", false);
const gui_value gen_showChangeLog = gui_value(general_settings, "showChangeLog", false);
const gui_value gen_updateChannel = gui_value(general_settings, "updateChannel", "Release");
const gui_value gen_recentFiles =
gui_value(main_window, "recentFiles", QVariant::fromValue(QList<QString>()));
const gui_value gen_guiLanguage = gui_value(general_settings, "guiLanguage", "en_US");
const gui_value gen_elfDirs =
gui_value(main_window, "elfDirs", QVariant::fromValue(QList<QString>()));
const gui_value gen_theme = gui_value(general_settings, "theme", 0);
// main window settings
const gui_value mw_geometry = gui_value(main_window, "geometry", QByteArray());
const gui_value mw_showLabelsUnderIcons = gui_value(main_window, "showLabelsUnderIcons", true);
// game list settings
const gui_value gl_mode = gui_value(game_list, "tableMode", 0);
const gui_value gl_icon_size = gui_value(game_list, "icon_size", 36);
const gui_value gl_slider_pos = gui_value(game_list, "slider_pos", 0);
const gui_value gl_showBackgroundImage = gui_value(game_list, "showBackgroundImage", true);
const gui_value gl_backgroundImageOpacity = gui_value(game_list, "backgroundImageOpacity", 50);
const gui_value gl_playBackgroundMusic = gui_value(game_list, "playBackgroundMusic", true);
const gui_value gl_backgroundMusicVolume = gui_value(game_list, "backgroundMusicVolume", 50);
const gui_value gl_VolumeSlider = gui_value(game_list, "volumeSlider", 100);
// game grid settings
const gui_value gg_icon_size = gui_value(game_grid, "icon_size", 69);
const gui_value gg_slider_pos = gui_value(game_grid, "slider_pos", 0);
// favorites list
const gui_value favorites_list =
gui_value(favorites, "favoritesList", QVariant::fromValue(QList<QString>()));
} // namespace gui
class gui_settings : public settings {
Q_OBJECT
public:
explicit gui_settings(QObject* parent = nullptr);
~gui_settings() override = default;
};

View File

@ -1,930 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <QKeyEvent>
#include <QMessageBox>
#include <QtConcurrent/qtconcurrentrun.h>
#include <SDL3/SDL.h>
#include "common/config.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#include "hotkeys.h"
#include "input/controller.h"
#include "input/input_handler.h"
#include "sdl_event_wrapper.h"
#include "ui_hotkeys.h"
Hotkeys::Hotkeys(bool isGameRunning, QWidget* parent)
: QDialog(parent), GameRunning(isGameRunning), ui(new Ui::Hotkeys) {
ui->setupUi(this);
if (!GameRunning) {
SDL_InitSubSystem(SDL_INIT_GAMEPAD);
SDL_InitSubSystem(SDL_INIT_EVENTS);
} else {
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
}
LoadHotkeys();
CheckGamePad();
installEventFilter(this);
PadButtonsList = {ui->fpsButtonPad, ui->quitButtonPad, ui->fullscreenButtonPad,
ui->pauseButtonPad, ui->reloadButtonPad};
KBButtonsList = {ui->fpsButtonKB, ui->quitButtonKB, ui->fullscreenButtonKB,
ui->pauseButtonKB, ui->reloadButtonKB, ui->renderdocButton,
ui->mouseJoystickButton, ui->mouseGyroButton};
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton* button) {
if (button == ui->buttonBox->button(QDialogButtonBox::Save)) {
SaveHotkeys(true);
} else if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) {
SaveHotkeys(false);
} else if (button == ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)) {
SetDefault();
} else if (button == ui->buttonBox->button(QDialogButtonBox::Cancel)) {
QWidget::close();
}
});
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QWidget::close);
ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save"));
ui->buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Apply"));
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(tr("Restore Defaults"));
for (auto& button : PadButtonsList) {
connect(button, &QPushButton::clicked, this,
[this, &button]() { StartTimer(button, true); });
}
for (auto& button : KBButtonsList) {
connect(button, &QPushButton::clicked, this,
[this, &button]() { StartTimer(button, false); });
}
SdlEventWrapper::Wrapper::wrapperActive = true;
QObject::connect(SdlEventWrapper::Wrapper::GetInstance(), &SdlEventWrapper::Wrapper::SDLEvent,
this, &Hotkeys::processSDLEvents);
if (!GameRunning) {
Polling = QtConcurrent::run(&Hotkeys::pollSDLEvents, this);
}
}
void Hotkeys::DisableMappingButtons() {
for (auto& i : PadButtonsList) {
i->setEnabled(false);
}
for (auto& i : KBButtonsList) {
i->setEnabled(false);
}
ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
}
void Hotkeys::EnableMappingButtons() {
for (auto& i : PadButtonsList) {
i->setEnabled(true);
}
for (auto& i : KBButtonsList) {
i->setEnabled(true);
}
ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(true);
ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
}
void Hotkeys::SetDefault() {
PadButtonsList = {ui->fpsButtonPad, ui->quitButtonPad, ui->fullscreenButtonPad,
ui->pauseButtonPad, ui->reloadButtonPad};
KBButtonsList = {ui->fpsButtonKB, ui->quitButtonKB, ui->fullscreenButtonKB,
ui->pauseButtonKB, ui->reloadButtonKB, ui->renderdocButton,
ui->mouseJoystickButton, ui->mouseGyroButton};
ui->fpsButtonPad->setText("unmapped");
ui->quitButtonPad->setText("unmapped");
ui->fullscreenButtonPad->setText("unmapped");
ui->pauseButtonPad->setText("unmapped");
ui->reloadButtonPad->setText("unmapped");
ui->fpsButtonKB->setText("f10");
ui->quitButtonKB->setText("lctrl, lshift, end");
ui->fullscreenButtonKB->setText("f11");
ui->pauseButtonKB->setText("f9");
ui->reloadButtonKB->setText("f8");
ui->renderdocButton->setText("f12");
ui->mouseJoystickButton->setText("f7");
ui->mouseGyroButton->setText("f6");
}
void Hotkeys::SaveHotkeys(bool CloseOnSave) {
std::vector<std::string> lines, inputs;
auto add_mapping = [&](const QString& buttonText, const std::string& output_name) {
if (buttonText.toStdString() != "unmapped") {
lines.push_back(output_name + " = " + buttonText.toStdString());
inputs.push_back(buttonText.toStdString());
}
};
lines.push_back("# Anything put here will be loaded for all games,");
lines.push_back("# alongside the game's config or default.ini depending on your preference.");
lines.push_back("");
add_mapping(ui->fullscreenButtonPad->text(), "hotkey_fullscreen");
add_mapping(ui->fullscreenButtonKB->text(), "hotkey_fullscreen");
lines.push_back("");
add_mapping(ui->pauseButtonPad->text(), "hotkey_pause");
add_mapping(ui->pauseButtonKB->text(), "hotkey_pause");
lines.push_back("");
add_mapping(ui->fpsButtonPad->text(), "hotkey_show_fps");
add_mapping(ui->fpsButtonKB->text(), "hotkey_show_fps");
lines.push_back("");
add_mapping(ui->quitButtonPad->text(), "hotkey_quit");
add_mapping(ui->quitButtonKB->text(), "hotkey_quit");
lines.push_back("");
add_mapping(ui->reloadButtonPad->text(), "hotkey_reload_inputs");
add_mapping(ui->reloadButtonKB->text(), "hotkey_reload_inputs");
lines.push_back("");
add_mapping(ui->renderdocButton->text(), "hotkey_renderdoc_capture");
add_mapping(ui->mouseJoystickButton->text(), "hotkey_toggle_mouse_to_joystick");
add_mapping(ui->mouseGyroButton->text(), "hotkey_toggle_mouse_to_gyro");
auto hotkey_file = Config::GetFoolproofInputConfigFile("global");
std::fstream file(hotkey_file);
int lineCount = 0;
std::string line;
while (std::getline(file, line)) {
lineCount++;
std::size_t comment_pos = line.find('#');
if (comment_pos != std::string::npos) {
if (!line.contains("Anything put here will be loaded for all games") &&
!line.contains("alongside the game's config or default.ini depending on your"))
lines.push_back(line);
continue;
}
std::size_t equal_pos = line.find('=');
if (equal_pos == std::string::npos) {
lines.push_back(line);
continue;
}
if (!line.contains("hotkey")) {
lines.push_back(line);
}
}
file.close();
// Prevent duplicate inputs that break the input engine
bool duplicateFound = false;
QSet<QString> duplicateMappings;
for (auto it = inputs.begin(); it != inputs.end(); ++it) {
if (std::find(it + 1, inputs.end(), *it) != inputs.end()) {
duplicateFound = true;
duplicateMappings.insert(QString::fromStdString(*it));
}
}
if (duplicateFound) {
QStringList duplicatesList;
for (const QString& mapping : duplicateMappings) {
for (auto& button : PadButtonsList) {
if (button->text() == mapping)
duplicatesList.append(button->objectName() + " - " + mapping);
}
for (auto& button : KBButtonsList) {
if (button->text() == mapping)
duplicatesList.append(button->objectName() + " - " + mapping);
}
}
QMessageBox::information(
this, tr("Unable to Save"),
// clang-format off
QString(tr("Cannot bind any unique input more than once. Duplicate inputs mapped to the following buttons:\n\n%1").arg(duplicatesList.join("\n"))));
// clang-format on
return;
}
std::vector<std::string> save;
bool CurrentLineEmpty = false, LastLineEmpty = false;
for (auto const& line : lines) {
LastLineEmpty = CurrentLineEmpty ? true : false;
CurrentLineEmpty = line.empty() ? true : false;
if (!CurrentLineEmpty || !LastLineEmpty)
save.push_back(line);
}
std::ofstream output_file(hotkey_file);
for (auto const& line : save) {
output_file << line << '\n';
}
output_file.close();
// this also parses global hotkeys
if (GameRunning)
Input::ParseInputConfig("default");
if (CloseOnSave)
QWidget::close();
}
void Hotkeys::LoadHotkeys() {
auto hotkey_file = Config::GetFoolproofInputConfigFile("global");
std::ifstream file(hotkey_file);
int lineCount = 0;
std::string line = "";
while (std::getline(file, line)) {
lineCount++;
std::size_t equal_pos = line.find('=');
if (equal_pos == std::string::npos)
continue;
std::string output_string = line.substr(0, equal_pos);
std::string input_string = line.substr(equal_pos + 2);
bool controllerInputDetected = false;
for (const std::string& input : ControllerInputs) {
// Needed to avoid detecting backspace while detecting back
if (input_string.contains(input) && !input_string.contains("backspace")) {
controllerInputDetected = true;
break;
}
}
if (output_string.contains("hotkey_fullscreen")) {
controllerInputDetected
? ui->fullscreenButtonPad->setText(QString::fromStdString(input_string))
: ui->fullscreenButtonKB->setText(QString::fromStdString(input_string));
} else if (output_string.contains("hotkey_show_fps")) {
controllerInputDetected
? ui->fpsButtonPad->setText(QString::fromStdString(input_string))
: ui->fpsButtonKB->setText(QString::fromStdString(input_string));
} else if (output_string.contains("hotkey_pause")) {
controllerInputDetected
? ui->pauseButtonPad->setText(QString::fromStdString(input_string))
: ui->pauseButtonKB->setText(QString::fromStdString(input_string));
} else if (output_string.contains("hotkey_quit")) {
controllerInputDetected
? ui->quitButtonPad->setText(QString::fromStdString(input_string))
: ui->quitButtonKB->setText(QString::fromStdString(input_string));
} else if (output_string.contains("hotkey_reload_inputs")) {
controllerInputDetected
? ui->reloadButtonPad->setText(QString::fromStdString(input_string))
: ui->reloadButtonKB->setText(QString::fromStdString(input_string));
} else if (output_string.contains("hotkey_renderdoc_capture")) {
ui->renderdocButton->setText(QString::fromStdString(input_string));
} else if (output_string.contains("hotkey_toggle_mouse_to_joystick")) {
ui->mouseJoystickButton->setText(QString::fromStdString(input_string));
} else if (output_string.contains("hotkey_toggle_mouse_to_gyro")) {
ui->mouseGyroButton->setText(QString::fromStdString(input_string));
}
}
file.close();
}
void Hotkeys::CheckGamePad() {
if (h_gamepad) {
SDL_CloseGamepad(h_gamepad);
h_gamepad = nullptr;
}
h_gamepads = SDL_GetGamepads(&gamepad_count);
if (!h_gamepads) {
LOG_ERROR(Input, "Cannot get gamepad list: {}", SDL_GetError());
return;
}
int defaultIndex = GamepadSelect::GetIndexfromGUID(h_gamepads, gamepad_count,
Config::getDefaultControllerID());
int activeIndex = GamepadSelect::GetIndexfromGUID(h_gamepads, gamepad_count,
GamepadSelect::GetSelectedGamepad());
if (!GameRunning) {
if (activeIndex != -1) {
h_gamepad = SDL_OpenGamepad(h_gamepads[activeIndex]);
} else if (defaultIndex != -1) {
h_gamepad = SDL_OpenGamepad(h_gamepads[defaultIndex]);
} else {
LOG_INFO(Input, "Got {} gamepads. Opening the first one.", gamepad_count);
h_gamepad = SDL_OpenGamepad(h_gamepads[0]);
}
if (!h_gamepad) {
LOG_ERROR(Input, "Failed to open gamepad: {}", SDL_GetError());
}
}
}
void Hotkeys::StartTimer(QPushButton*& button, bool isButton) {
MappingTimer = 3;
MappingCompleted = false;
mapping = button->text();
DisableMappingButtons();
isButton ? EnablePadMapping = true : EnableKBMapping = true;
button->setText(tr("Waiting for inputs") + " [" + QString::number(MappingTimer) + "]");
timer = new QTimer(this);
MappingButton = button;
timer->start(1000);
connect(timer, &QTimer::timeout, this, [this]() { CheckMapping(MappingButton); });
}
void Hotkeys::CheckMapping(QPushButton*& button) {
MappingTimer -= 1;
button->setText(tr("Waiting for inputs") + " [" + QString::number(MappingTimer) + "]");
if (pressedButtons.size() > 0) {
QStringList keyStrings;
for (QString& buttonAction : pressedButtons) {
keyStrings << buttonAction;
}
QString combo = keyStrings.join(", ");
SetMapping(combo);
MappingButton->setText(combo);
pressedButtons.clear();
}
if (MappingCompleted || MappingTimer <= 0) {
button->setText(mapping);
EnablePadMapping = false;
EnableKBMapping = false;
L2Pressed = false;
R2Pressed = false;
EnableMappingButtons();
timer->stop();
}
}
void Hotkeys::SetMapping(QString input) {
mapping = input;
MappingCompleted = true;
}
// use QT events instead of SDL for KB since SDL doesn't allow background KB events
bool Hotkeys::eventFilter(QObject* obj, QEvent* event) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Escape) {
if (EnableKBMapping || EnablePadMapping) {
SetMapping("unmapped");
CheckMapping(MappingButton);
}
return true;
} else {
if (EnableKBMapping) {
if (pressedButtons.size() >= 3) {
return true;
}
switch (keyEvent->key()) {
// modifiers
case Qt::Key_Shift:
if (keyEvent->nativeScanCode() == LSHIFT_KEY) {
pressedButtons.insert(1, "lshift");
} else {
pressedButtons.insert(2, "rshift");
}
break;
case Qt::Key_Alt:
if (keyEvent->nativeScanCode() == LALT_KEY) {
pressedButtons.insert(3, "lalt");
} else {
pressedButtons.insert(4, "ralt");
}
break;
case Qt::Key_Control:
if (keyEvent->nativeScanCode() == LCTRL_KEY) {
pressedButtons.insert(5, "lctrl");
} else {
pressedButtons.insert(6, "rctrl");
}
break;
case Qt::Key_Meta:
#ifdef _WIN32
pressedButtons.insert(7, "lwin");
#else
pressedButtons.insert(7, "lmeta");
#endif
break;
// f keys
case Qt::Key_F1:
pressedButtons.insert(8, "f1");
break;
case Qt::Key_F2:
pressedButtons.insert(9, "f2");
break;
case Qt::Key_F3:
pressedButtons.insert(10, "f3");
break;
case Qt::Key_F4:
pressedButtons.insert(11, "f4");
break;
case Qt::Key_F5:
pressedButtons.insert(12, "f5");
break;
case Qt::Key_F6:
pressedButtons.insert(13, "f6");
break;
case Qt::Key_F7:
pressedButtons.insert(14, "f7");
break;
case Qt::Key_F8:
pressedButtons.insert(15, "f8");
break;
case Qt::Key_F9:
pressedButtons.insert(16, "f9");
break;
case Qt::Key_F10:
pressedButtons.insert(17, "f10");
break;
case Qt::Key_F11:
pressedButtons.insert(18, "f11");
break;
case Qt::Key_F12:
pressedButtons.insert(19, "f12");
break;
// alphanumeric
case Qt::Key_A:
pressedButtons.insert(20, "a");
break;
case Qt::Key_B:
pressedButtons.insert(21, "b");
break;
case Qt::Key_C:
pressedButtons.insert(22, "c");
break;
case Qt::Key_D:
pressedButtons.insert(23, "d");
break;
case Qt::Key_E:
pressedButtons.insert(24, "e");
break;
case Qt::Key_F:
pressedButtons.insert(25, "f");
break;
case Qt::Key_G:
pressedButtons.insert(26, "g");
break;
case Qt::Key_H:
pressedButtons.insert(27, "h");
break;
case Qt::Key_I:
pressedButtons.insert(28, "i");
break;
case Qt::Key_J:
pressedButtons.insert(29, "j");
break;
case Qt::Key_K:
pressedButtons.insert(30, "k");
break;
case Qt::Key_L:
pressedButtons.insert(31, "l");
break;
case Qt::Key_M:
pressedButtons.insert(32, "m");
break;
case Qt::Key_N:
pressedButtons.insert(33, "n");
break;
case Qt::Key_O:
pressedButtons.insert(34, "o");
break;
case Qt::Key_P:
pressedButtons.insert(35, "p");
break;
case Qt::Key_Q:
pressedButtons.insert(36, "q");
break;
case Qt::Key_R:
pressedButtons.insert(37, "r");
break;
case Qt::Key_S:
pressedButtons.insert(38, "s");
break;
case Qt::Key_T:
pressedButtons.insert(39, "t");
break;
case Qt::Key_U:
pressedButtons.insert(40, "u");
break;
case Qt::Key_V:
pressedButtons.insert(41, "v");
break;
case Qt::Key_W:
pressedButtons.insert(42, "w");
break;
case Qt::Key_X:
pressedButtons.insert(43, "x");
break;
case Qt::Key_Y:
pressedButtons.insert(44, "y");
break;
case Qt::Key_Z:
pressedButtons.insert(45, "z");
break;
case Qt::Key_0:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(46, "kp0")
: pressedButtons.insert(56, "0");
break;
case Qt::Key_1:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(47, "kp1")
: pressedButtons.insert(57, "1");
break;
case Qt::Key_2:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(48, "kp2")
: pressedButtons.insert(58, "2");
break;
case Qt::Key_3:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(49, "kp3")
: pressedButtons.insert(59, "3");
break;
case Qt::Key_4:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(50, "kp4")
: pressedButtons.insert(60, "4");
break;
case Qt::Key_5:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(51, "kp5")
: pressedButtons.insert(61, "5");
break;
case Qt::Key_6:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(52, "kp6")
: pressedButtons.insert(62, "6");
break;
case Qt::Key_7:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(53, "kp7")
: pressedButtons.insert(63, "7");
break;
case Qt::Key_8:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(54, "kp8")
: pressedButtons.insert(64, "8");
break;
case Qt::Key_9:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(55, "kp9")
: pressedButtons.insert(65, "9");
break;
// symbols
case Qt::Key_Asterisk:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(66, "kpasterisk")
: pressedButtons.insert(74, "asterisk");
break;
case Qt::Key_Minus:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(67, "kpminus")
: pressedButtons.insert(75, "minus");
break;
case Qt::Key_Equal:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(68, "kpequals")
: pressedButtons.insert(76, "equals");
break;
case Qt::Key_Plus:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(69, "kpplus")
: pressedButtons.insert(77, "plus");
break;
case Qt::Key_Slash:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(70, "kpslash")
: pressedButtons.insert(78, "slash");
break;
case Qt::Key_Comma:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(71, "kpcomma")
: pressedButtons.insert(79, "comma");
break;
case Qt::Key_Period:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(72, "kpperiod")
: pressedButtons.insert(80, "period");
break;
case Qt::Key_Enter:
QApplication::keyboardModifiers() & Qt::KeypadModifier
? pressedButtons.insert(73, "kpenter")
: pressedButtons.insert(81, "enter");
break;
case Qt::Key_QuoteLeft:
pressedButtons.insert(82, "grave");
break;
case Qt::Key_AsciiTilde:
pressedButtons.insert(83, "tilde");
break;
case Qt::Key_Exclam:
pressedButtons.insert(84, "exclamation");
break;
case Qt::Key_At:
pressedButtons.insert(85, "at");
break;
case Qt::Key_NumberSign:
pressedButtons.insert(86, "hash");
break;
case Qt::Key_Dollar:
pressedButtons.insert(87, "dollar");
break;
case Qt::Key_Percent:
pressedButtons.insert(88, "percent");
break;
case Qt::Key_AsciiCircum:
pressedButtons.insert(89, "caret");
break;
case Qt::Key_Ampersand:
pressedButtons.insert(90, "ampersand");
break;
case Qt::Key_ParenLeft:
pressedButtons.insert(91, "lparen");
break;
case Qt::Key_ParenRight:
pressedButtons.insert(92, "rparen");
break;
case Qt::Key_BracketLeft:
pressedButtons.insert(93, "lbracket");
break;
case Qt::Key_BracketRight:
pressedButtons.insert(94, "rbracket");
break;
case Qt::Key_BraceLeft:
pressedButtons.insert(95, "lbrace");
break;
case Qt::Key_BraceRight:
pressedButtons.insert(96, "rbrace");
break;
case Qt::Key_Underscore:
pressedButtons.insert(97, "underscore");
break;
case Qt::Key_Backslash:
pressedButtons.insert(98, "backslash");
break;
case Qt::Key_Bar:
pressedButtons.insert(99, "pipe");
break;
case Qt::Key_Semicolon:
pressedButtons.insert(100, "semicolon");
break;
case Qt::Key_Colon:
pressedButtons.insert(101, "colon");
break;
case Qt::Key_Apostrophe:
pressedButtons.insert(102, "apostrophe");
break;
case Qt::Key_QuoteDbl:
pressedButtons.insert(103, "quote");
break;
case Qt::Key_Less:
pressedButtons.insert(104, "less");
break;
case Qt::Key_Greater:
pressedButtons.insert(105, "greater");
break;
case Qt::Key_Question:
pressedButtons.insert(106, "question");
break;
// special keys
case Qt::Key_Print:
pressedButtons.insert(107, "printscreen");
break;
case Qt::Key_ScrollLock:
pressedButtons.insert(108, "scrolllock");
break;
case Qt::Key_Pause:
pressedButtons.insert(109, "pausebreak");
break;
case Qt::Key_Backspace:
pressedButtons.insert(110, "backspace");
break;
case Qt::Key_Insert:
pressedButtons.insert(111, "insert");
break;
case Qt::Key_Delete:
pressedButtons.insert(112, "delete");
break;
case Qt::Key_Home:
pressedButtons.insert(113, "home");
break;
case Qt::Key_End:
pressedButtons.insert(114, "end");
break;
case Qt::Key_PageUp:
pressedButtons.insert(115, "pgup");
break;
case Qt::Key_PageDown:
pressedButtons.insert(116, "pgdown");
break;
case Qt::Key_Tab:
pressedButtons.insert(117, "tab");
break;
case Qt::Key_CapsLock:
pressedButtons.insert(118, "capslock");
break;
case Qt::Key_Return:
pressedButtons.insert(119, "enter");
break;
case Qt::Key_Space:
pressedButtons.insert(120, "space");
break;
case Qt::Key_Up:
pressedButtons.insert(121, "up");
break;
case Qt::Key_Down:
pressedButtons.insert(122, "down");
break;
case Qt::Key_Left:
pressedButtons.insert(123, "left");
break;
case Qt::Key_Right:
pressedButtons.insert(124, "right");
break;
default:
break;
}
return true;
}
}
}
if (event->type() == QEvent::KeyPress && EnableKBMapping) {
CheckMapping(MappingButton);
return true;
}
return QDialog::eventFilter(obj, event);
}
void Hotkeys::processSDLEvents(int Type, int Input, int Value) {
if (EnablePadMapping) {
if (pressedButtons.size() >= 3) {
return;
}
if (Type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
switch (Input) {
case SDL_GAMEPAD_BUTTON_SOUTH:
pressedButtons.insert(5, "cross");
break;
case SDL_GAMEPAD_BUTTON_EAST:
pressedButtons.insert(6, "circle");
break;
case SDL_GAMEPAD_BUTTON_NORTH:
pressedButtons.insert(7, "triangle");
break;
case SDL_GAMEPAD_BUTTON_WEST:
pressedButtons.insert(8, "square");
break;
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
pressedButtons.insert(3, "l1");
break;
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
pressedButtons.insert(4, "r1");
break;
case SDL_GAMEPAD_BUTTON_LEFT_STICK:
pressedButtons.insert(9, "l3");
break;
case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
pressedButtons.insert(10, "r3");
break;
case SDL_GAMEPAD_BUTTON_DPAD_UP:
pressedButtons.insert(13, "pad_up");
break;
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
pressedButtons.insert(14, "pad_down");
break;
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
pressedButtons.insert(15, "pad_left");
break;
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
pressedButtons.insert(16, "pad_right");
break;
case SDL_GAMEPAD_BUTTON_BACK:
pressedButtons.insert(11, "back");
break;
case SDL_GAMEPAD_BUTTON_START:
pressedButtons.insert(12, "options");
break;
default:
break;
}
}
if (Type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
// SDL trigger axis values range from 0 to 32000, set mapping on half movement
// Set zone for trigger release signal arbitrarily at 5000
switch (Input) {
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
if (Value > 16000) {
pressedButtons.insert(1, "l2");
L2Pressed = true;
} else if (Value < 5000) {
if (L2Pressed && !R2Pressed)
CheckMapping(MappingButton);
}
break;
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
if (Value > 16000) {
pressedButtons.insert(2, "r2");
R2Pressed = true;
} else if (Value < 5000) {
if (R2Pressed && !L2Pressed)
CheckMapping(MappingButton);
}
break;
default:
break;
}
}
if (Type == SDL_EVENT_GAMEPAD_BUTTON_UP)
CheckMapping(MappingButton);
}
if (Type == SDL_EVENT_GAMEPAD_ADDED || SDL_EVENT_GAMEPAD_REMOVED) {
CheckGamePad();
}
}
void Hotkeys::pollSDLEvents() {
SDL_Event event;
while (SdlEventWrapper::Wrapper::wrapperActive) {
if (!SDL_WaitEvent(&event)) {
return;
}
if (event.type == SDL_EVENT_QUIT) {
return;
}
SdlEventWrapper::Wrapper::GetInstance()->Wrapper::ProcessEvent(&event);
}
}
void Hotkeys::Cleanup() {
SdlEventWrapper::Wrapper::wrapperActive = false;
if (h_gamepad) {
SDL_CloseGamepad(h_gamepad);
h_gamepad = nullptr;
}
if (h_gamepads)
SDL_free(h_gamepads);
if (!GameRunning) {
SDL_Event quitLoop{};
quitLoop.type = SDL_EVENT_QUIT;
SDL_PushEvent(&quitLoop);
Polling.waitForFinished();
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
SDL_QuitSubSystem(SDL_INIT_EVENTS);
SDL_Quit();
} else {
if (!Config::getBackgroundControllerInput()) {
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
}
}
}
Hotkeys::~Hotkeys() {}

View File

@ -1,88 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QDialog>
#include <QFuture>
#include <QTimer>
#include <SDL3/SDL_gamepad.h>
#ifdef _WIN32
#define LCTRL_KEY 29
#define LALT_KEY 56
#define LSHIFT_KEY 42
#else
#define LCTRL_KEY 37
#define LALT_KEY 64
#define LSHIFT_KEY 50
#endif
namespace Ui {
class Hotkeys;
}
class Hotkeys : public QDialog {
Q_OBJECT
public:
explicit Hotkeys(bool GameRunning, QWidget* parent = nullptr);
~Hotkeys();
private Q_SLOTS:
void processSDLEvents(int Type, int Input, int Value);
void StartTimer(QPushButton*& button, bool isPad);
void SaveHotkeys(bool CloseOnSave);
void SetDefault();
private:
bool eventFilter(QObject* obj, QEvent* event) override;
void CheckMapping(QPushButton*& button);
void DisableMappingButtons();
void EnableMappingButtons();
void LoadHotkeys();
void pollSDLEvents();
void CheckGamePad();
void SetMapping(QString input);
void Cleanup();
bool GameRunning;
bool EnablePadMapping = false;
bool EnableKBMapping = false;
bool MappingCompleted = false;
bool L2Pressed = false;
bool R2Pressed = false;
int MappingTimer;
int gamepad_count;
QString mapping;
QTimer* timer;
QPushButton* MappingButton;
SDL_Gamepad* h_gamepad = nullptr;
SDL_JoystickID* h_gamepads;
// use QMap instead of QSet to maintain order of inserted strings
QMap<int, QString> pressedButtons;
QList<QPushButton*> PadButtonsList;
QList<QPushButton*> KBButtonsList;
QFuture<void> Polling;
Ui::Hotkeys* ui;
const std::vector<std::string> ControllerInputs = {
"cross", "circle", "square", "triangle", "l1",
"r1", "l2", "r2", "l3",
"r3", "options", "pad_up",
"pad_down",
"pad_left", "pad_right", "axis_left_x", "axis_left_y", "axis_right_x",
"axis_right_y", "back"};
protected:
void closeEvent(QCloseEvent* event) override {
Cleanup();
}
void accept() override {
// Blank override to prevent quitting when save button pressed
}
};

View File

@ -1,549 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later -->
<ui version="4.0">
<class>Hotkeys</class>
<widget class="QDialog" name="Hotkeys">
<property name="windowModality">
<enum>Qt::WindowModality::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>987</width>
<height>549</height>
</rect>
</property>
<property name="windowTitle">
<string>Customize Hotkeys</string>
</property>
<widget class="QScrollArea" name="scrollArea">
<property name="geometry">
<rect>
<x>3</x>
<y>2</y>
<width>981</width>
<height>541</height>
</rect>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>979</width>
<height>539</height>
</rect>
</property>
<widget class="QWidget" name="verticalLayoutWidget_3">
<property name="geometry">
<rect>
<x>3</x>
<y>7</y>
<width>971</width>
<height>532</height>
</rect>
</property>
<layout class="QVBoxLayout" name="HotkeysLayout">
<item>
<widget class="QLabel" name="controllerLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>19</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Controller Hotkeys</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutPad1">
<item>
<widget class="QGroupBox" name="fpsGroupboxPad">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Show FPS Counter</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="fpsButtonPad">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="quitGroupboxKB_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Quit Emulation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QPushButton" name="quitButtonPad">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="reloadMappingsBoxPad">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Reload Button Mappings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QPushButton" name="reloadButtonPad">
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutPad2">
<item>
<widget class="QGroupBox" name="fullscreenGroupboxPad">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Toggle Fullscreen</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QPushButton" name="fullscreenButtonPad">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="pauseGroupboxPad">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Toggle Pause</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QPushButton" name="pauseButtonPad">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="keyboardLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>19</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Keyboard Hotkeys</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutKB1">
<item>
<widget class="QGroupBox" name="fpsGroupboxKB">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Show FPS Counter</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QPushButton" name="fpsButtonKB">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="quitGroupboxKB">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Quit Emulation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QPushButton" name="quitButtonKB">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="reloadMappingsBoxKB">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Reload Button Mappings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<widget class="QPushButton" name="reloadButtonKB">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutKB2">
<item>
<widget class="QGroupBox" name="fullscreenGroupboxKB">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Toggle Fullscreen</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QPushButton" name="fullscreenButtonKB">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="pauseGroupboxKB">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Toggle Pause</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QPushButton" name="pauseButtonKB">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="renderdocGroupbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Renderdoc Capture (for debugging only)</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_12">
<item>
<widget class="QPushButton" name="renderdocButton">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="mouseJoystickGroupbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Toggle Mouse to Joystick Emulation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_13">
<item>
<widget class="QPushButton" name="mouseJoystickButton">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="mouseGyroGroupbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Toggle Mouse to Gyro Emulation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_14">
<item>
<widget class="QPushButton" name="mouseGyroButton">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>unmapped</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="tipLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Tip: Up to three simultaneous inputs can be assigned for each hotkey</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::RestoreDefaults|QDialogButtonBox::StandardButton::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Hotkeys</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Hotkeys</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,239 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "kbm_config_dialog.h"
#include "kbm_help_dialog.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include "common/config.h"
#include "common/path_util.h"
#include "game_info.h"
#include "sdl_window.h"
#include <QCheckBox>
#include <QCloseEvent>
#include <QComboBox>
#include <QFile>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPushButton>
#include <QTextStream>
#include <QVBoxLayout>
QString previous_game = "default";
bool isHelpOpen = false;
HelpDialog* helpDialog;
EditorDialog::EditorDialog(QWidget* parent) : QDialog(parent) {
setWindowTitle(tr("Edit Keyboard + Mouse and Controller input bindings"));
resize(600, 400);
// Create the editor widget
editor = new QPlainTextEdit(this);
editorFont.setPointSize(10); // Set default text size
editor->setFont(editorFont); // Apply font to the editor
// Create the game selection combo box
gameComboBox = new QComboBox(this);
gameComboBox->addItem("default"); // Add default option
// Load all installed games
loadInstalledGames();
QCheckBox* unifiedInputCheckBox = new QCheckBox(tr("Use Per-Game configs"), this);
unifiedInputCheckBox->setChecked(!Config::GetUseUnifiedInputConfig());
// Connect checkbox signal
connect(unifiedInputCheckBox, &QCheckBox::toggled, this, [](bool checked) {
Config::SetUseUnifiedInputConfig(!checked);
Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml");
});
// Create Save, Cancel, and Help buttons
QPushButton* saveButton = new QPushButton("Save", this);
QPushButton* cancelButton = new QPushButton("Cancel", this);
QPushButton* helpButton = new QPushButton("Help", this);
QPushButton* defaultButton = new QPushButton("Default", this);
// Layout for the game selection and buttons
QHBoxLayout* topLayout = new QHBoxLayout();
topLayout->addWidget(unifiedInputCheckBox);
topLayout->addWidget(gameComboBox);
topLayout->addStretch();
topLayout->addWidget(saveButton);
topLayout->addWidget(cancelButton);
topLayout->addWidget(defaultButton);
topLayout->addWidget(helpButton);
// Main layout with editor and buttons
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addLayout(topLayout);
layout->addWidget(editor);
// Load the default config file content into the editor
loadFile(gameComboBox->currentText());
// Connect button and combo box signals
connect(saveButton, &QPushButton::clicked, this, &EditorDialog::onSaveClicked);
connect(cancelButton, &QPushButton::clicked, this, &EditorDialog::onCancelClicked);
connect(helpButton, &QPushButton::clicked, this, &EditorDialog::onHelpClicked);
connect(defaultButton, &QPushButton::clicked, this, &EditorDialog::onResetToDefaultClicked);
connect(gameComboBox, &QComboBox::currentTextChanged, this,
&EditorDialog::onGameSelectionChanged);
}
void EditorDialog::loadFile(QString game) {
const auto config_file = Config::GetFoolproofInputConfigFile(game.toStdString());
QFile file(config_file);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&file);
editor->setPlainText(in.readAll());
originalConfig = editor->toPlainText();
file.close();
} else {
QMessageBox::warning(this, tr("Error"), tr("Could not open the file for reading"));
}
}
void EditorDialog::saveFile(QString game) {
const auto config_file = Config::GetFoolproofInputConfigFile(game.toStdString());
QFile file(config_file);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);
out << editor->toPlainText();
file.close();
} else {
QMessageBox::warning(this, tr("Error"), tr("Could not open the file for writing"));
}
}
// Override the close event to show the save confirmation dialog only if changes were made
void EditorDialog::closeEvent(QCloseEvent* event) {
if (isHelpOpen) {
helpDialog->close();
isHelpOpen = false;
// at this point I might have to add this flag and the help dialog to the class itself
}
if (hasUnsavedChanges()) {
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, tr("Save Changes"), tr("Do you want to save changes?"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (reply == QMessageBox::Yes) {
saveFile(gameComboBox->currentText());
event->accept(); // Close the dialog
} else if (reply == QMessageBox::No) {
event->accept(); // Close the dialog without saving
} else {
event->ignore(); // Cancel the close event
}
} else {
event->accept(); // No changes, close the dialog without prompting
}
}
void EditorDialog::keyPressEvent(QKeyEvent* event) {
if (event->key() == Qt::Key_Escape) {
if (isHelpOpen) {
helpDialog->close();
isHelpOpen = false;
}
close(); // Trigger the close action, same as pressing the close button
} else {
QDialog::keyPressEvent(event); // Call the base class implementation for other keys
}
}
void EditorDialog::onSaveClicked() {
if (isHelpOpen) {
helpDialog->close();
isHelpOpen = false;
}
saveFile(gameComboBox->currentText());
reject(); // Close the dialog
}
void EditorDialog::onCancelClicked() {
if (isHelpOpen) {
helpDialog->close();
isHelpOpen = false;
}
reject(); // Close the dialog
}
void EditorDialog::onHelpClicked() {
if (!isHelpOpen) {
helpDialog = new HelpDialog(&isHelpOpen, this);
helpDialog->setWindowTitle(tr("Help"));
helpDialog->setAttribute(Qt::WA_DeleteOnClose); // Clean up on close
// Get the position and size of the Config window
QRect configGeometry = this->geometry();
int helpX = configGeometry.x() + configGeometry.width() + 10; // 10 pixels offset
int helpY = configGeometry.y();
// Move the Help dialog to the right side of the Config window
helpDialog->move(helpX, helpY);
helpDialog->show();
isHelpOpen = true;
} else {
helpDialog->close();
isHelpOpen = false;
}
}
void EditorDialog::onResetToDefaultClicked() {
bool default_default = gameComboBox->currentText() == "default";
QString prompt =
default_default
? tr("Do you want to reset your custom default config to the original default config?")
: tr("Do you want to reset this config to your custom default config?");
QMessageBox::StandardButton reply = QMessageBox::question(this, tr("Reset to Default"), prompt,
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
if (default_default) {
const auto default_file = Config::GetFoolproofInputConfigFile("default");
std::filesystem::remove(default_file);
}
const auto config_file = Config::GetFoolproofInputConfigFile("default");
QFile file(config_file);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&file);
editor->setPlainText(in.readAll());
file.close();
} else {
QMessageBox::warning(this, tr("Error"), tr("Could not open the file for reading"));
}
// saveFile(gameComboBox->currentText());
}
}
bool EditorDialog::hasUnsavedChanges() {
// Compare the current content with the original content to check if there are unsaved changes
return editor->toPlainText() != originalConfig;
}
void EditorDialog::loadInstalledGames() {
previous_game = "default";
QStringList filePaths;
for (const auto& installLoc : Config::getGameInstallDirs()) {
QString installDir;
Common::FS::PathToQString(installDir, installLoc);
QDir parentFolder(installDir);
QFileInfoList fileList = parentFolder.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const auto& fileInfo : fileList) {
if (fileInfo.isDir() && (!fileInfo.filePath().endsWith("-UPDATE") ||
!fileInfo.filePath().endsWith("-patch"))) {
gameComboBox->addItem(fileInfo.fileName()); // Add game name to combo box
}
}
}
}
void EditorDialog::onGameSelectionChanged(const QString& game) {
saveFile(previous_game);
loadFile(gameComboBox->currentText()); // Reload file based on the selected game
previous_game = gameComboBox->currentText();
}

View File

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QComboBox>
#include <QDialog>
#include <QPlainTextEdit>
#include "string"
class EditorDialog : public QDialog {
Q_OBJECT // Necessary for using Qt's meta-object system (signals/slots)
public : explicit EditorDialog(QWidget* parent = nullptr); // Constructor
protected:
void closeEvent(QCloseEvent* event) override; // Override close event
void keyPressEvent(QKeyEvent* event) override;
private:
QPlainTextEdit* editor; // Editor widget for the config file
QFont editorFont; // To handle the text size
QString originalConfig; // Starting config string
std::string gameId;
QComboBox* gameComboBox; // Combo box for selecting game configurations
void loadFile(QString game); // Function to load the config file
void saveFile(QString game); // Function to save the config file
void loadInstalledGames(); // Helper to populate gameComboBox
bool hasUnsavedChanges(); // Checks for unsaved changes
private slots:
void onSaveClicked(); // Save button slot
void onCancelClicked(); // Slot for handling cancel button
void onHelpClicked(); // Slot for handling help button
void onResetToDefaultClicked();
void onGameSelectionChanged(const QString& game); // Slot for game selection changes
};

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QDialog>
#include "game_info.h"
// macros > declaring constants
// also, we were only using one counterpart
#ifdef _WIN32
#define LCTRL_KEY 29
#define LALT_KEY 56
#define LSHIFT_KEY 42
#else
#define LCTRL_KEY 37
#define LALT_KEY 64
#define LSHIFT_KEY 50
#endif
namespace Ui {
class KBMSettings;
}
class KBMSettings : public QDialog {
Q_OBJECT
public:
explicit KBMSettings(std::shared_ptr<GameInfoClass> game_info_get, bool GameRunning,
std::string GameRunningSerial, QWidget* parent = nullptr);
~KBMSettings();
signals:
void PushKBMEvent();
private Q_SLOTS:
void SaveKBMConfig(bool CloseOnSave);
void SetDefault();
void CheckMapping(QPushButton*& button);
void StartTimer(QPushButton*& button);
void onHelpClicked();
private:
std::unique_ptr<Ui::KBMSettings> ui;
std::shared_ptr<GameInfoClass> m_game_info;
bool eventFilter(QObject* obj, QEvent* event) override;
void ButtonConnects();
void SetUIValuestoMappings(std::string config_id);
void GetGameTitle();
void DisableMappingButtons();
void EnableMappingButtons();
void SetMapping(QString input);
void Cleanup();
std::string RunningGameSerial;
QMap<int, QString> pressedKeys;
bool GameRunning;
bool EnableMapping = false;
bool MappingCompleted = false;
bool HelpWindowOpen = false;
QString mapping;
int MappingTimer;
QTimer* timer;
QPushButton* MappingButton;
QList<QPushButton*> ButtonsList;
std::string config_id;
const std::vector<std::string> ControllerInputs = {
"cross", "circle", "square", "triangle", "l1",
"r1", "l2", "r2", "l3",
"r3", "options", "pad_up",
"pad_down",
"pad_left", "pad_right", "axis_left_x", "axis_left_y", "axis_right_x",
"axis_right_y", "back"};
};

File diff suppressed because it is too large Load Diff

View File

@ -1,306 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "kbm_help_dialog.h"
#include <QApplication>
#include <QDialog>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QWidget>
ExpandableSection::ExpandableSection(const QString& title, const QString& content,
QWidget* parent = nullptr)
: QWidget(parent) {
QVBoxLayout* layout = new QVBoxLayout(this);
// Button to toggle visibility of content
toggleButton = new QPushButton(title);
layout->addWidget(toggleButton);
// QTextBrowser for content (initially hidden)
contentBrowser = new QTextBrowser();
contentBrowser->setPlainText(content);
contentBrowser->setVisible(false);
// Remove scrollbars from QTextBrowser
contentBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
contentBrowser->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// Set size policy to allow vertical stretching only
contentBrowser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
// Calculate and set initial height based on content
updateContentHeight();
layout->addWidget(contentBrowser);
// Connect button click to toggle visibility
connect(toggleButton, &QPushButton::clicked, [this]() {
contentBrowser->setVisible(!contentBrowser->isVisible());
if (contentBrowser->isVisible()) {
updateContentHeight(); // Update height when expanding
}
emit expandedChanged(); // Notify for layout adjustments
});
// Connect to update height if content changes
connect(contentBrowser->document(), &QTextDocument::contentsChanged, this,
&ExpandableSection::updateContentHeight);
// Minimal layout settings for spacing
layout->setSpacing(2);
layout->setContentsMargins(0, 0, 0, 0);
}
void HelpDialog::closeEvent(QCloseEvent* event) {
*help_open_ptr = false;
close();
}
void HelpDialog::reject() {
*help_open_ptr = false;
close();
}
HelpDialog::HelpDialog(bool* open_flag, QWidget* parent) : QDialog(parent) {
help_open_ptr = open_flag;
// Main layout for the help dialog
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Container widget for the scroll area
QWidget* containerWidget = new QWidget;
QVBoxLayout* containerLayout = new QVBoxLayout(containerWidget);
// Add expandable sections to container layout
auto* quickstartSection = new ExpandableSection(tr("Quickstart"), quickstart());
auto* syntaxSection = new ExpandableSection(tr("Syntax"), syntax());
auto* bindingsSection = new ExpandableSection(tr("Keybindings"), bindings());
auto* specialSection = new ExpandableSection(tr("Special Bindings"), special());
auto* faqSection = new ExpandableSection(tr("FAQ"), faq());
containerLayout->addWidget(quickstartSection);
containerLayout->addWidget(syntaxSection);
containerLayout->addWidget(bindingsSection);
containerLayout->addWidget(specialSection);
containerLayout->addWidget(faqSection);
containerLayout->addStretch(1);
// Scroll area wrapping the container
QScrollArea* scrollArea = new QScrollArea;
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
scrollArea->setWidgetResizable(true);
scrollArea->setWidget(containerWidget);
// Add the scroll area to the main dialog layout
mainLayout->addWidget(scrollArea);
setLayout(mainLayout);
// Minimum size for the dialog
setMinimumSize(500, 400);
// Re-adjust dialog layout when any section expands/collapses
connect(quickstartSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
connect(faqSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
connect(syntaxSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
connect(specialSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
connect(bindingsSection, &ExpandableSection::expandedChanged, this, &HelpDialog::adjustSize);
}
// Helper functions that store the text contents for each tab inb the HelpDialog menu
QString HelpDialog::quickstart() {
return R"(
In this section, you will find information about the project and its features, as well as help setting up your ideal setup.
To view the config file's syntax, check out the Syntax tab, for keybind names, visit Normal Keybinds and Special Bindings, and if you are here to view emulator-wide keybinds, you can find it in the FAQ section.
)";
}
QString HelpDialog::syntax() {
return R"(
Below is the file format for mouse, keyboard, and controller inputs:
Rules:
- You can bind up to 3 inputs to one button.
- Adding '#' at the beginning of a line creates a comment.
- Extra whitespace doesn't affect input.
<output>=<input>; is just as valid as <output> = <input>;
- ';' at the end of a line is also optional.
Syntax (aka how a line can look like):
#Comment line
<controller_button> = <input>, <input>, <input>;
<controller_button> = <input>, <input>;
<controller_button> = <input>;
Examples:
#Interact
cross = e;
#Heavy attack (in BB)
r2 = leftbutton, lshift;
#Move forward
axis_left_y_minus = w;)";
}
QString HelpDialog::bindings() {
return R"(
The following names should be interpreted without the '' around them. For inputs that have left and right versions, only the left one is shown, but the right version also works.
(Example: 'lshift', 'rshift')
Keyboard:
Alphabet:
'a', 'b', ..., 'z'
Numbers:
'0', '1', ..., '9'
F keys:
'f1', 'f2', ..., 'f12'
Keypad:
'kp0', 'kp1', ..., 'kp9',
'kpperiod', 'kpcomma', 'kpslash', 'kpasterisk', 'kpminus', 'kpplus', 'kpequals', 'kpenter'
Symbols:
(See below)
Special keys:
'escape' (text editor only), 'printscreen', 'scrolllock', 'pausebreak',
'backspace', 'insert', 'delete', 'home', 'end', 'pgup', 'pgdown', 'tab',
'capslock', 'enter', 'space'
Arrow keys:
'up', 'down', 'left', 'right'
Modifier keys:
'lctrl', 'lshift', 'lalt', 'lwin' = 'lmeta' (same input, different names, so if you are not on Windows and don't like calling this the Windows key, there is an alternative)
Mouse:
'leftbutton', 'rightbutton', 'middlebutton', 'sidebuttonforward',
'sidebuttonback'
The following wheel inputs cannot be bound to axis input, only button:
'mousewheelup', 'mousewheeldown', 'mousewheelleft',
'mousewheelright'
Controller:
The touchpad currently can't be rebound to anything else, but you can bind buttons to it.
If you have a controller that has different names for buttons, it will still work. Just look up the equivalent names for that controller.
The same left-right rule still applies here.
Buttons:
'triangle', 'circle', 'cross', 'square', 'l1', 'l3',
'options', touchpad', 'up', 'down', 'left', 'right'
Input-only:
'lpaddle_low', 'lpaddle_high', 'back' = 'share' (Xbox and PS4 names)
Output-only:
'touchpad_left', 'touchpad_center', 'touchpad_right'
Axes if you bind them to a button input:
'axis_left_x_plus', 'axis_left_x_minus', 'axis_left_y_plus', 'axis_left_y_minus',
'axis_right_x_plus', ..., 'axis_right_y_minus',
'l2'
Axes if you bind them to another axis input:
'axis_left_x', 'axis_left_y', 'axis_right_x', 'axis_right_y',
'l2'
Symbols (expanded):
` 'grave'
~ 'tilde'
! 'exclamation'
@ 'at'
# 'hash'
$ 'dollar'
% 'percent'
^ 'caret'
& 'ampersand'
* 'asterisk'
( 'lparen'
- 'minus'
_ 'underscore'
= 'equals'
+ 'plus'
[ 'lbracket'
{ 'lbrace'
\ 'backslash'
| 'pipe'
; 'semicolon'
: 'colon'
' 'apostrophe'
" 'quote'
, 'comma'
< 'less'
. 'period'
> 'greater'
/ 'slash'
? 'question')";
}
QString HelpDialog::special() {
return R"(
There are some extra bindings you can put in the config file that don't correspond to a controller input but something else.
You can find these here, with comments, examples, and suggestions for most of them.
Emulator hotkeys:
These are regarded as normal bindings, but they are put in a special config named global.ini by default, which is a config that is always loaded alongside the main config (If you want to, you can use this to set some common bindings that'll be in effect for every game, then put only the game specific bindings in their respective files). This doesn't mean you can't add them to the normal configs, you can absolutely make game specific emulator hokeys as well.
'hotkey_pause', 'hotkey_fullscreen', 'hotkey_show_fps',
'hotkey_quit', 'hotkey_reload_inputs', 'hotkey_toggle_mouse_to_joystick',
'hotkey_toggle_mouse_to_gyro', 'hotkey_renderdoc_capture'
'leftjoystick_halfmode' and 'rightjoystick_halfmode' = <key>;
These are a pair of input modifiers that change the way keyboard button-bound axes work. By default, those push the joystick to the max in their respective direction, but if their respective 'joystick_halfmode' modifier value is true, they only push it... halfway. With this, you can change from run to walk in games like Bloodborne.
'mouse_to_joystick' = 'none', 'left' or 'right';
This binds the mouse movement to either joystick. If it receives a value that is not 'left' or 'right', it defaults to 'none'.
'mouse_movement_params' = float, float, float;
(If you don't know what a float is, it is a data type that stores decimal numbers.)
Default values: 0.5, 1, 0.125
Let's break each parameter down:
1st: 'mouse_deadzone_offset' should have a value between 0 and 1 (it gets clamped to that range anyway), with 0 being no offset and 1 being pushing the joystick to the max in the direction the mouse moved.
This controls the minimum distance the joystick gets moved when moving the mouse. If set to 0, it will emulate raw mouse input, which doesn't work very well due to deadzones preventing input if the movement is not large enough.
2nd: 'mouse_speed' is just a standard multiplier to the mouse input speed.
If you input a negative number, the axis directions get reversed. Keep in mind that the offset can still push it back to positive if it's big enough.
3rd: 'mouse_speed_offset' should be in the 0 to 1 range, with 0 being no offset and 1 being offsetting to the max possible value.
Let's set 'mouse_deadzone_offset' to 0.5, and 'mouse_speed_offset' to 0: This means that if we move the mouse very slowly, it still inputs a half-strength joystick input, and if we increase the speed, it would stay that way until we move faster than half the max speed. If we instead set this to 0.25, we now only need to move the mouse faster than the 0.5-0.25=0.25=quarter of the max speed, to get an increase in joystick speed. If we set it to 0.5, then even moving the mouse at 1 pixel per frame will result in a faster-than-minimum speed.
'key_toggle' = <key>, <key_to_toggle>;
This assigns a key to another key, and if pressed, toggles that key's virtual value. If it's on, then it doesn't matter if the key is pressed or not, the input handler will treat it as if it's pressed.
Let's say we want to be able to toggle 'l1' with 't'. You can then bind 'l1' to a key you won't use, like 'kpenter'. Then bind 't' to toggle that. You will end up with this:
l1 = kpenter;
key_toggle = t, kpenter;
'analog_deadzone' = <device>, <value>, <value>;
This sets the deadzone range for various inputs. The first value is the minimum deadzone while the second is the maximum. Values go from 1 to 127 (no deadzone to max deadzone).
If you only want a minimum or maximum deadzone, set the other value to 1 or 127 respectively.
Valid devices: 'leftjoystick', 'rightjoystick', 'l2', 'r2'
'mouse_gyro_roll_mode':
Controls whether moving the mouse sideways causes a panning or a rolling motion while mouse-to-gyro emulation is active.)";
}
QString HelpDialog::faq() {
return R"(
Q: What are the emulator-wide keybinds?
A: By default, they're the following:
-F12: Triggers Renderdoc capture
-F11: Toggles fullscreen
-F10: Toggles FPS counter
-Ctrl+F10: Open the debug menu
-F9: Pauses the emulator if the debug menu is open
-F8: Reparses the config file while in-game
-F7: Toggles mouse capture and mouse input
-F6: Toggles mouse-to-gyro emulation
More info on these can be found in the Special keybinds tab.
Q: How do I switch between mouse and controller joystick input? Why is it even required?
A: Pressing F7 toggles between mouse and controller joystick input. It is required because the program polls the mouse input, which means it checks mouse movement every frame. If it didn't move, the code would manually set the emulator's virtual controller to 0 (to the center), even if other input devices would update it.
Q: What in the world is a 'grave' key?
A: (`). It represents one of the many symbols you can bind to a key. You can find the various symbols and their names in the Bindings tab.
Q: What happens if I accidentally make a typo in the config?
A: The code recognises the line as wrong and skips it, so the rest of the file will get parsed, but that line in question will be treated like a comment line. You can find these lines in the log if you search for 'input_handler'.
Q: I want to bind <input> to <output>, but your code doesn't support <input>!
A: Some keys are intentionally omitted, but if you read the bindings through, and you're sure it is not there and isn't one of the intentionally disabled ones, open an issue on https://github.com/shadps4-emu/shadPS4.
Q: What does default.ini do?
A: If you're using per-game configs, it's the base from which all new games generate their config file. If you use the unified config, then default.ini is used for every game directly instead.
Q: What does the use Per-game Config checkbox do?
A: It controls whether the config is loaded from CUSAXXXXX.ini for a game or from default.ini. This way, if you only want to manage one set of bindings, you can do so, but if you want to use a different setup for every game, that's possible as well.)";
}

View File

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QApplication>
#include <QDialog>
#include <QGroupBox>
#include <QLabel>
#include <QPropertyAnimation>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <QWidget>
class ExpandableSection : public QWidget {
Q_OBJECT
public:
explicit ExpandableSection(const QString& title, const QString& content, QWidget* parent);
signals:
void expandedChanged(); // Signal to indicate layout size change
private:
QPushButton* toggleButton;
QTextBrowser* contentBrowser; // Changed from QLabel to QTextBrowser
QPropertyAnimation* animation;
int contentHeight;
void updateContentHeight() {
int contentHeight = contentBrowser->document()->size().height();
contentBrowser->setMinimumHeight(contentHeight + 5);
contentBrowser->setMaximumHeight(contentHeight + 50);
}
};
class HelpDialog : public QDialog {
Q_OBJECT
public:
explicit HelpDialog(bool* open_flag = nullptr, QWidget* parent = nullptr);
protected:
void closeEvent(QCloseEvent* event) override;
void reject() override;
private:
bool* help_open_ptr;
QString quickstart();
QString faq();
QString syntax();
QString bindings();
QString special();
};

View File

@ -1,414 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "log_presets_dialog.h"
#include <algorithm>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QItemSelection>
#include <QItemSelectionModel>
#include <QPushButton>
#include <QSet>
#include <QSignalBlocker>
#include <QStyleOptionViewItem>
#include <QTableWidget>
#include <QVBoxLayout>
namespace {
constexpr const char* kPresetsGroup = "logger_presets";
constexpr const char* kPresetsKey = "entries";
inline QString MakeEntry(const QString& comment, const QString& filter) {
return comment + "\t" + filter;
}
inline void SplitEntry(const QString& entry, QString& comment, QString& filter) {
const int idx = entry.indexOf('\t');
if (idx < 0) {
comment = entry;
filter = QString();
} else {
comment = entry.left(idx);
filter = entry.mid(idx + 1);
}
}
} // namespace
LogPresetsDialog::LogPresetsDialog(std::shared_ptr<gui_settings> gui_settings, QWidget* parent)
: QDialog(parent), m_gui_settings(std::move(gui_settings)) {
setWindowTitle(tr("Log Filter Presets"));
resize(640, 360);
auto* root = new QVBoxLayout(this);
m_table = new QTableWidget(this);
m_table->setColumnCount(3);
m_table->horizontalHeader()->setSectionsClickable(true);
m_table->horizontalHeader()->setHighlightSections(false);
QStringList headers;
headers << QString() << tr("Comment") << tr("Filter");
m_table->setHorizontalHeaderLabels(headers);
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeMode::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeMode::Interactive);
m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeMode::Stretch);
m_table->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection);
m_table->setEditTriggers(QAbstractItemView::EditTrigger::DoubleClicked |
QAbstractItemView::EditTrigger::SelectedClicked |
QAbstractItemView::EditTrigger::EditKeyPressed);
connect(m_table, &QTableWidget::cellDoubleClicked, this,
[this](int /*row*/, int /*col*/) { LoadSelected(); });
connect(m_table, &QTableWidget::itemChanged, this, [this](QTableWidgetItem* item) {
if (m_updating_checks)
return;
if (item && item->column() == 0) {
m_updating_checks = true;
const int row = item->row();
auto* sm = m_table->selectionModel();
const auto idx = m_table->model()->index(row, 0);
const bool check = (item->checkState() == Qt::Checked);
sm->select(idx, (check ? QItemSelectionModel::Select : QItemSelectionModel::Deselect) |
QItemSelectionModel::Rows);
m_updating_checks = false;
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
}
});
connect(m_table->selectionModel(), &QItemSelectionModel::selectionChanged, this,
[this](const QItemSelection& selected, const QItemSelection& deselected) {
if (m_updating_checks)
return;
m_updating_checks = true;
QSet<int> selRows;
for (const auto& idx : selected.indexes())
selRows.insert(idx.row());
QSet<int> deselRows;
for (const auto& idx : deselected.indexes())
deselRows.insert(idx.row());
for (int r : selRows) {
auto* it = m_table->item(r, 0);
if (!it) {
it = new QTableWidgetItem();
it->setFlags((QTableWidgetItem().flags() | Qt::ItemIsUserCheckable) &
~Qt::ItemIsEditable);
m_table->setItem(r, 0, it);
}
it->setCheckState(Qt::Checked);
}
for (int r : deselRows) {
auto* it = m_table->item(r, 0);
if (it)
it->setCheckState(Qt::Unchecked);
}
m_updating_checks = false;
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
});
connect(m_table->horizontalHeader(), &QHeaderView::sectionClicked, this, [this](int section) {
if (section != 0)
return;
const auto next = (m_header_checkbox && m_header_checkbox->checkState() == Qt::Checked)
? Qt::Unchecked
: Qt::Checked;
if (m_header_checkbox)
m_header_checkbox->setCheckState(next);
SetAllCheckStates(next);
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
});
m_header_checkbox = new QCheckBox(m_table->horizontalHeader());
m_header_checkbox->setTristate(true);
m_header_checkbox->setText(QString());
m_header_checkbox->setFocusPolicy(Qt::NoFocus);
PositionHeaderCheckbox();
QObject::connect(m_table->horizontalHeader(), &QHeaderView::geometriesChanged, this,
[this]() { PositionHeaderCheckbox(); });
QObject::connect(m_table->horizontalHeader(), &QHeaderView::sectionResized, this,
[this](int, int, int) { PositionHeaderCheckbox(); });
QObject::connect(m_header_checkbox, &QCheckBox::clicked, this, [this](bool checked) {
SetAllCheckStates(checked ? Qt::Checked : Qt::Unchecked);
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
});
root->addWidget(m_table);
auto* buttons_layout = new QHBoxLayout();
m_add_btn = new QPushButton("+", this);
m_remove_btn = new QPushButton("-", this);
m_load_btn = new QPushButton(tr("Load"), this);
m_close_btn = new QPushButton(tr("Close"), this);
m_add_btn->setToolTip(tr("Add a new preset after the selected row"));
m_remove_btn->setToolTip(tr("Remove selected presets"));
m_load_btn->setToolTip(tr("Load the selected preset"));
buttons_layout->addWidget(m_add_btn);
buttons_layout->addWidget(m_remove_btn);
buttons_layout->addStretch();
buttons_layout->addWidget(m_load_btn);
buttons_layout->addWidget(m_close_btn);
root->addLayout(buttons_layout);
m_add_btn->setAutoDefault(false);
m_remove_btn->setAutoDefault(false);
m_load_btn->setAutoDefault(false);
m_close_btn->setAutoDefault(false);
m_add_btn->setDefault(false);
m_remove_btn->setDefault(false);
m_load_btn->setDefault(false);
m_close_btn->setDefault(false);
connect(m_add_btn, &QPushButton::clicked, this, [this]() { AddAfterSelection(); });
connect(m_remove_btn, &QPushButton::clicked, this, [this]() { RemoveSelected(); });
connect(m_load_btn, &QPushButton::clicked, this, [this]() { LoadSelected(); });
connect(m_close_btn, &QPushButton::clicked, this, [this]() { reject(); });
LoadFromSettings();
m_updating_checks = true;
m_table->clearSelection();
m_table->setCurrentItem(nullptr);
m_updating_checks = false;
m_table->setFocus(Qt::OtherFocusReason);
UpdateLoadButtonEnabled();
}
void LogPresetsDialog::accept() {
SaveToSettings();
QDialog::accept();
}
void LogPresetsDialog::reject() {
SaveToSettings();
QDialog::reject();
}
void LogPresetsDialog::LoadFromSettings() {
if (!m_gui_settings)
return;
const auto var = m_gui_settings->GetValue(kPresetsGroup, kPresetsKey, QVariant());
QList<QString> list;
if (var.isValid()) {
list = m_gui_settings->Var2List(var);
}
PopulateFromList(list);
}
void LogPresetsDialog::SaveToSettings() {
if (!m_gui_settings)
return;
const auto list = SerializeTable();
m_gui_settings->SetValue(kPresetsGroup, kPresetsKey, m_gui_settings->List2Var(list));
}
QList<QString> LogPresetsDialog::SerializeTable() const {
QList<QString> list;
const int rows = m_table->rowCount();
for (int r = 0; r < rows; ++r) {
const auto* comment = m_table->item(r, 1);
const auto* filter = m_table->item(r, 2);
const QString c = comment ? comment->text() : QString();
const QString f = filter ? filter->text() : QString();
if (!c.isEmpty() || !f.isEmpty()) {
list.push_back(MakeEntry(c, f));
}
}
return list;
}
void LogPresetsDialog::PopulateFromList(const QList<QString>& list) {
m_table->setRowCount(0);
for (const auto& entry : list) {
QString c, f;
SplitEntry(entry, c, f);
const int row = m_table->rowCount();
m_table->insertRow(row);
auto* chk = new QTableWidgetItem();
chk->setFlags((chk->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsEditable);
chk->setCheckState(Qt::Unchecked);
m_table->setItem(row, 0, chk);
m_table->setItem(row, 1, new QTableWidgetItem(c));
m_table->setItem(row, 2, new QTableWidgetItem(f));
}
m_updating_checks = true;
m_table->clearSelection();
m_table->setCurrentItem(nullptr);
m_updating_checks = false;
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
}
void LogPresetsDialog::AddAfterSelection() {
int insert_row = m_table->rowCount();
const auto selected = m_table->selectionModel()->selectedRows();
if (!selected.isEmpty()) {
// place after the last selected row
insert_row = selected.last().row() + 1;
}
m_table->insertRow(insert_row);
auto* chk = new QTableWidgetItem();
chk->setFlags((chk->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsEditable);
chk->setCheckState(Qt::Unchecked);
m_table->setItem(insert_row, 0, chk);
m_table->setItem(insert_row, 1, new QTableWidgetItem(""));
m_table->setItem(insert_row, 2, new QTableWidgetItem(""));
UpdateHeaderCheckState();
m_table->setCurrentCell(insert_row, 1);
m_table->editItem(m_table->item(insert_row, 1));
UpdateLoadButtonEnabled();
}
void LogPresetsDialog::RemoveSelected() {
// Prefer checked rows; fall back to selected rows if none checked
QList<int> to_remove = GetCheckedRows();
if (to_remove.isEmpty()) {
const auto selected = m_table->selectionModel()->selectedRows();
for (const auto& idx : selected)
to_remove.push_back(idx.row());
}
if (to_remove.isEmpty())
return;
std::sort(to_remove.begin(), to_remove.end(), [](int a, int b) { return a > b; });
for (int row : to_remove)
m_table->removeRow(row);
UpdateHeaderCheckState();
UpdateLoadButtonEnabled();
}
void LogPresetsDialog::LoadSelected() {
QList<int> rows = GetCheckedRows();
if (rows.isEmpty()) {
const auto selected = m_table->selectionModel()->selectedRows();
if (selected.isEmpty())
return;
rows.push_back(selected.first().row());
}
const int row = rows.first();
const auto* item = m_table->item(row, 2);
if (item) {
emit PresetChosen(item->text());
accept();
}
}
void LogPresetsDialog::UpdateHeaderCheckState() {
const int rows = m_table->rowCount();
if (rows == 0) {
if (m_header_checkbox)
m_header_checkbox->setCheckState(Qt::Unchecked);
return;
}
int checked = 0;
for (int r = 0; r < rows; ++r) {
auto* it = m_table->item(r, 0);
if (it && it->checkState() == Qt::Checked)
++checked;
}
const auto state = (checked == 0) ? Qt::Unchecked
: (checked == rows) ? Qt::Checked
: Qt::PartiallyChecked;
if (m_header_checkbox)
m_header_checkbox->setCheckState(state);
}
void LogPresetsDialog::SetAllCheckStates(Qt::CheckState state) {
const QSignalBlocker blocker(m_table);
m_updating_checks = true;
const int rows = m_table->rowCount();
auto* sm = m_table->selectionModel();
for (int r = 0; r < rows; ++r) {
auto* it = m_table->item(r, 0);
if (!it) {
it = new QTableWidgetItem();
it->setFlags((QTableWidgetItem().flags() | Qt::ItemIsUserCheckable) &
~Qt::ItemIsEditable);
m_table->setItem(r, 0, it);
}
it->setCheckState(state);
const auto idx = m_table->model()->index(r, 0);
sm->select(idx, ((state == Qt::Checked) ? QItemSelectionModel::Select
: QItemSelectionModel::Deselect) |
QItemSelectionModel::Rows);
}
m_updating_checks = false;
}
QList<int> LogPresetsDialog::GetCheckedRows() const {
QList<int> rows;
const int count = m_table->rowCount();
for (int r = 0; r < count; ++r) {
const auto* it = m_table->item(r, 0);
if (it && it->checkState() == Qt::Checked)
rows.push_back(r);
}
return rows;
}
void LogPresetsDialog::UpdateLoadButtonEnabled() {
if (!m_table || !m_table->selectionModel())
return;
const int count = m_table->selectionModel()->selectedRows().size();
if (m_load_btn) {
const bool showLoad = (count == 1);
m_load_btn->setVisible(showLoad);
m_load_btn->setEnabled(showLoad);
}
if (m_remove_btn) {
const bool showRemove = (count >= 1);
m_remove_btn->setVisible(showRemove);
m_remove_btn->setEnabled(showRemove);
}
}
void LogPresetsDialog::PositionHeaderCheckbox() {
if (!m_header_checkbox)
return;
auto* hdr = m_table->horizontalHeader();
const int section = 0;
const int x = hdr->sectionViewportPosition(section);
const int w = hdr->sectionSize(section);
const int h = hdr->height();
const QSize sz = m_header_checkbox->sizeHint();
// Try to align horizontally with cell checkbox indicator for this column
int left_in_section = 0;
if (m_table->model()->rowCount() > 0) {
QStyleOptionViewItem opt;
opt.initFrom(m_table);
opt.features = QStyleOptionViewItem::HasCheckIndicator;
// Use a representative cell rect width (section width) and a typical row height
const int cell_h = m_table->rowHeight(0) > 0 ? m_table->rowHeight(0) : sz.height();
opt.rect = QRect(0, 0, w, cell_h);
const QRect ind =
m_table->style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt, m_table);
left_in_section = ind.left();
// Guard against styles that return empty rects
if (ind.isEmpty()) {
left_in_section =
m_table->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, m_table);
}
} else {
left_in_section =
m_table->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, m_table);
}
int cx = x + left_in_section;
// Center vertically in header
const int cy = (h - sz.height()) / 2;
// Ensure checkbox fully visible within the section bounds
if (cx + sz.width() > x + w)
cx = x + std::max(0, w - sz.width());
m_header_checkbox->setGeometry(QRect(QPoint(cx, cy), sz));
m_header_checkbox->show();
}

View File

@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <QPointer>
class QTableWidget;
class QPushButton;
class QTableWidgetItem;
class QCheckBox;
#include "gui_settings.h"
class LogPresetsDialog : public QDialog {
Q_OBJECT
public:
explicit LogPresetsDialog(std::shared_ptr<gui_settings> gui_settings,
QWidget* parent = nullptr);
~LogPresetsDialog() override = default;
signals:
void PresetChosen(const QString& filter);
protected:
void accept() override;
void reject() override;
private:
void LoadFromSettings();
void SaveToSettings();
void AddAfterSelection();
void RemoveSelected();
void LoadSelected();
void UpdateHeaderCheckState();
void SetAllCheckStates(Qt::CheckState state);
QList<int> GetCheckedRows() const;
void UpdateLoadButtonEnabled();
void PositionHeaderCheckbox();
QList<QString> SerializeTable() const;
void PopulateFromList(const QList<QString>& list);
private:
std::shared_ptr<gui_settings> m_gui_settings;
QTableWidget* m_table = nullptr;
QCheckBox* m_header_checkbox = nullptr;
QPushButton* m_add_btn = nullptr;
QPushButton* m_remove_btn = nullptr;
QPushButton* m_load_btn = nullptr;
QPushButton* m_close_btn = nullptr;
bool m_updating_checks = false;
};

View File

@ -1,264 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "iostream"
#include "system_error"
#include "unordered_map"
#include "common/config.h"
#include "common/logging/backend.h"
#include "common/memory_patcher.h"
#include "core/debugger.h"
#include "core/file_sys/fs.h"
#include "emulator.h"
#include "game_install_dialog.h"
#include "main_window.h"
#ifdef _WIN32
#include <windows.h>
#endif
// Custom message handler to ignore Qt logs
void customMessageHandler(QtMsgType, const QMessageLogContext&, const QString&) {}
int main(int argc, char* argv[]) {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
QApplication a(argc, argv);
QApplication::setDesktopFileName("net.shadps4.shadPS4");
// Load configurations and initialize Qt application
const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
Config::load(user_dir / "config.toml");
bool has_command_line_argument = argc > 1;
bool show_gui = false, has_game_argument = false;
std::string game_path;
std::vector<std::string> game_args{};
std::optional<std::filesystem::path> game_folder;
bool waitForDebugger = false;
std::optional<int> waitPid;
// Map of argument strings to lambda functions
std::unordered_map<std::string, std::function<void(int&)>> arg_map = {
{"-h",
[&](int&) {
std::cout
<< "Usage: shadps4 [options]\n"
"Options:\n"
" No arguments: Opens the GUI.\n"
" -g, --game <path|ID> Specify <eboot.bin or elf path> or "
"<game ID (CUSAXXXXX)> to launch\n"
" -- ... Parameters passed to the game ELF. "
"Needs to be at the end of the line, and everything after \"--\" is a "
"game argument.\n"
" -p, --patch <patch_file> Apply specified patch file\n"
" -i, --ignore-game-patch Disable automatic loading of game patch\n"
" -s, --show-gui Show the GUI\n"
" -f, --fullscreen <true|false> Specify window initial fullscreen "
"state. Does not overwrite the config file.\n"
" --add-game-folder <folder> Adds a new game folder to the config.\n"
" --log-append Append log output to file instead of "
"overwriting it.\n"
" --override-root <folder> Override the game root folder. Default is the "
"parent of game path\n"
" --wait-for-debugger Wait for debugger to attach\n"
" --wait-for-pid <pid> Wait for process with specified PID to stop\n"
" --config-clean Run the emulator with the default config "
"values, ignores the config file(s) entirely.\n"
" --config-global Run the emulator with the base config file "
"only, ignores game specific configs.\n"
" -h, --help Display this help message\n";
exit(0);
}},
{"--help", [&](int& i) { arg_map["-h"](i); }}, // Redirect --help to -h
{"-s", [&](int&) { show_gui = true; }},
{"--show-gui", [&](int& i) { arg_map["-s"](i); }},
{"-g",
[&](int& i) {
if (i + 1 < argc) {
game_path = argv[++i];
has_game_argument = true;
} else {
std::cerr << "Error: Missing argument for -g/--game\n";
exit(1);
}
}},
{"--game", [&](int& i) { arg_map["-g"](i); }},
{"-p",
[&](int& i) {
if (i + 1 < argc) {
MemoryPatcher::patchFile = argv[++i];
} else {
std::cerr << "Error: Missing argument for -p\n";
exit(1);
}
}},
{"--patch", [&](int& i) { arg_map["-p"](i); }},
{"-i", [&](int&) { Core::FileSys::MntPoints::ignore_game_patches = true; }},
{"--ignore-game-patch", [&](int& i) { arg_map["-i"](i); }},
{"-f",
[&](int& i) {
if (++i >= argc) {
std::cerr
<< "Error: Invalid argument for -f/--fullscreen. Use 'true' or 'false'.\n";
exit(1);
}
std::string f_param(argv[i]);
bool is_fullscreen;
if (f_param == "true") {
is_fullscreen = true;
} else if (f_param == "false") {
is_fullscreen = false;
} else {
std::cerr
<< "Error: Invalid argument for -f/--fullscreen. Use 'true' or 'false'.\n";
exit(1);
}
// Set fullscreen mode without saving it to config file
Config::setIsFullscreen(is_fullscreen);
}},
{"--fullscreen", [&](int& i) { arg_map["-f"](i); }},
{"--add-game-folder",
[&](int& i) {
if (++i >= argc) {
std::cerr << "Error: Missing argument for --add-game-folder\n";
exit(1);
}
std::string config_dir(argv[i]);
std::filesystem::path config_path = std::filesystem::path(config_dir);
std::error_code discard;
if (!std::filesystem::is_directory(config_path, discard)) {
std::cerr << "Error: Directory does not exist: " << config_path << "\n";
exit(1);
}
Config::addGameInstallDir(config_path);
Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml");
std::cout << "Game folder successfully saved.\n";
exit(0);
}},
{"--log-append", [&](int& i) { Common::Log::SetAppend(); }},
{"--config-clean", [&](int& i) { Config::setConfigMode(Config::ConfigMode::Clean); }},
{"--config-global", [&](int& i) { Config::setConfigMode(Config::ConfigMode::Global); }},
{"--override-root",
[&](int& i) {
if (++i >= argc) {
std::cerr << "Error: Missing argument for --override-root\n";
exit(1);
}
std::string folder_str{argv[i]};
std::filesystem::path folder{folder_str};
if (!std::filesystem::exists(folder) || !std::filesystem::is_directory(folder)) {
std::cerr << "Error: Folder does not exist: " << folder_str << "\n";
exit(1);
}
game_folder = folder;
}},
{"--wait-for-debugger", [&](int& i) { waitForDebugger = true; }},
{"--wait-for-pid", [&](int& i) {
if (++i >= argc) {
std::cerr << "Error: Missing argument for --wait-for-pid\n";
exit(1);
}
waitPid = std::stoi(argv[i]);
}}};
// Parse command-line arguments using the map
for (int i = 1; i < argc; ++i) {
std::string cur_arg = argv[i];
auto it = arg_map.find(cur_arg);
if (it != arg_map.end()) {
it->second(i); // Call the associated lambda function
} else if (i == argc - 1 && !has_game_argument) {
// Assume the last argument is the game file if not specified via -g/--game
game_path = argv[i];
has_game_argument = true;
} else if (std::string(argv[i]) == "--") {
if (i + 1 == argc) {
std::cerr << "Warning: -- is set, but no game arguments are added!\n";
break;
}
for (int j = i + 1; j < argc; j++) {
game_args.push_back(argv[j]);
}
break;
} else if (i + 1 < argc && std::string(argv[i + 1]) == "--") {
if (!has_game_argument) {
game_path = argv[i];
has_game_argument = true;
}
} else {
std::cerr << "Unknown argument: " << cur_arg << ", see --help for info.\n";
return 1;
}
}
// If no game directories are set and no command line argument, prompt for it
if (Config::getGameInstallDirsEnabled().empty() && !has_command_line_argument) {
GameInstallDialog dlg;
dlg.exec();
}
// Ignore Qt logs
qInstallMessageHandler(customMessageHandler);
// Initialize the main window
MainWindow* m_main_window = new MainWindow(nullptr);
if ((has_command_line_argument && show_gui) || !has_command_line_argument) {
m_main_window->Init();
}
if (has_command_line_argument && !has_game_argument) {
std::cerr << "Error: Please provide a game path or ID.\n";
exit(1);
}
if (waitPid.has_value()) {
Core::Debugger::WaitForPid(waitPid.value());
}
Core::Emulator* emulator = Common::Singleton<Core::Emulator>::Instance();
emulator->executableName = argv[0];
emulator->waitForDebuggerBeforeRun = waitForDebugger;
// Process game path or ID if provided
if (has_game_argument) {
std::filesystem::path game_file_path(game_path);
// Check if the provided path is a valid file
if (!std::filesystem::exists(game_file_path)) {
// If not a file, treat it as a game ID and search in install directories recursively
bool game_found = false;
const int max_depth = 5;
for (const auto& install_dir : Config::getGameInstallDirs()) {
if (auto found_path = Common::FS::FindGameByID(install_dir, game_path, max_depth)) {
game_file_path = *found_path;
game_found = true;
break;
}
}
if (!game_found) {
std::cerr << "Error: Game ID or file path not found: " << game_path << std::endl;
return 1;
}
}
// Run the emulator with the resolved game path
emulator->Run(game_file_path.string(), game_args, game_folder);
if (!show_gui) {
return 0; // Exit after running the emulator without showing the GUI
}
}
// Show the main window and run the Qt application
m_main_window->show();
return a.exec();
}

File diff suppressed because it is too large Load Diff

View File

@ -1,124 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QActionGroup>
#include <QDragEnterEvent>
#include <QProcess>
#include <QTranslator>
#include "background_music_player.h"
#include "common/config.h"
#include "common/path_util.h"
#include "compatibility_info.h"
#include "core/file_format/psf.h"
#include "core/file_sys/fs.h"
#include "elf_viewer.h"
#include "emulator.h"
#include "game_grid_frame.h"
#include "game_info.h"
#include "game_list_frame.h"
#include "game_list_utils.h"
#include "gui_settings.h"
#include "main_window_themes.h"
#include "main_window_ui.h"
class GameListFrame;
class MainWindow : public QMainWindow {
Q_OBJECT
signals:
void WindowResized(QResizeEvent* event);
public:
explicit MainWindow(QWidget* parent = nullptr);
~MainWindow();
bool Init();
void InstallDirectory();
void StartGame();
void PauseGame();
bool showLabels;
private Q_SLOTS:
void ConfigureGuiFromSettings();
void SaveWindowState();
void SearchGameTable(const QString& text);
void ShowGameList();
void RefreshGameTable();
void HandleResize(QResizeEvent* event);
void OnLanguageChanged(const QString& locale);
void toggleLabelsUnderIcons();
private:
Ui_MainWindow* ui;
void AddUiWidgets();
void UpdateToolbarLabels();
void UpdateToolbarButtons();
QWidget* createButtonWithLabel(QPushButton* button, const QString& labelText, bool showLabel);
void CreateActions();
void toggleFullscreen();
void CreateRecentGameActions();
void CreateDockWindows();
void LoadGameLists();
#ifdef ENABLE_UPDATER
void CheckUpdateMain(bool checkSave);
#endif
void CreateConnects();
void SetLastUsedTheme();
void SetLastIconSizeBullet();
void SetUiIcons(bool isWhite);
void BootGame();
void AddRecentFiles(QString filePath);
void LoadTranslation();
void PlayBackgroundMusic();
QIcon RecolorIcon(const QIcon& icon, bool isWhite);
void StartEmulator(std::filesystem::path);
bool isIconBlack = false;
bool isTableList = true;
bool isGameRunning = false;
bool isWhite = false;
bool is_paused = false;
std::string runningGameSerial = "";
QActionGroup* m_icon_size_act_group = nullptr;
QActionGroup* m_list_mode_act_group = nullptr;
QActionGroup* m_theme_act_group = nullptr;
QActionGroup* m_recent_files_group = nullptr;
// Dockable widget frames
WindowThemes m_window_themes;
GameListUtils m_game_list_utils;
QScopedPointer<QDockWidget> m_dock_widget;
// Game Lists
QScopedPointer<GameListFrame> m_game_list_frame;
QScopedPointer<GameGridFrame> m_game_grid_frame;
QScopedPointer<ElfViewer> m_elf_viewer;
// Status Bar.
QScopedPointer<QStatusBar> statusBar;
PSF psf;
std::shared_ptr<GameInfoClass> m_game_info = std::make_shared<GameInfoClass>();
std::shared_ptr<CompatibilityInfoClass> m_compat_info =
std::make_shared<CompatibilityInfoClass>();
QTranslator* translator;
std::shared_ptr<gui_settings> m_gui_settings;
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event1) override {
if (event1->mimeData()->hasUrls()) {
event1->acceptProposedAction();
}
}
void resizeEvent(QResizeEvent* event) override;
std::filesystem::path last_install_dir = "";
bool delete_file_on_install = false;
bool use_for_all_queued = false;
};

View File

@ -1,197 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "main_window_themes.h"
void WindowThemes::SetWindowTheme(Theme theme, QLineEdit* mw_searchbar) {
QPalette themePalette;
qApp->setStyleSheet("");
switch (theme) {
case Theme::Dark:
mw_searchbar->setStyleSheet(
"QLineEdit {"
"background-color: #1e1e1e; color: #ffffff; border: 1px solid #ffffff; "
"border-radius: 4px; padding: 5px; }"
"QLineEdit:focus {"
"border: 1px solid #2A82DA; }");
themePalette.setColor(QPalette::Window, QColor(50, 50, 50));
themePalette.setColor(QPalette::WindowText, Qt::white);
themePalette.setColor(QPalette::Base, QColor(20, 20, 20));
themePalette.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
themePalette.setColor(QPalette::ToolTipBase, QColor(20, 20, 20));
themePalette.setColor(QPalette::ToolTipText, Qt::white);
themePalette.setColor(QPalette::Text, Qt::white);
themePalette.setColor(QPalette::Button, QColor(53, 53, 53));
themePalette.setColor(QPalette::ButtonText, Qt::white);
themePalette.setColor(QPalette::BrightText, Qt::red);
themePalette.setColor(QPalette::Link, QColor(42, 130, 218));
themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
themePalette.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(themePalette);
break;
case Theme::Light:
mw_searchbar->setStyleSheet(
"QLineEdit {"
"background-color: #ffffff; color: #000000; border: 1px solid #000000; "
"border-radius: 4px; padding: 5px; }"
"QLineEdit:focus {"
"border: 1px solid #2A82DA; }");
themePalette.setColor(QPalette::Window, QColor(240, 240, 240)); // Light gray
themePalette.setColor(QPalette::WindowText, Qt::black); // Black
themePalette.setColor(QPalette::Base, QColor(230, 230, 230, 80)); // Grayish
themePalette.setColor(QPalette::ToolTipBase, QColor(230, 230, 230, 80)); // Grayish
themePalette.setColor(QPalette::ToolTipText, Qt::black); // Black
themePalette.setColor(QPalette::Text, Qt::black); // Black
themePalette.setColor(QPalette::Button, QColor(240, 240, 240)); // Light gray
themePalette.setColor(QPalette::ButtonText, Qt::black); // Black
themePalette.setColor(QPalette::BrightText, Qt::red); // Red
themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Blue
themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Blue
themePalette.setColor(QPalette::HighlightedText, Qt::white); // White
qApp->setPalette(themePalette);
break;
case Theme::Green:
mw_searchbar->setStyleSheet(
"QLineEdit {"
"background-color: #192819; color: #ffffff; border: 1px solid #ffffff; "
"border-radius: 4px; padding: 5px; }"
"QLineEdit:focus {"
"border: 1px solid #2A82DA; }");
themePalette.setColor(QPalette::Window, QColor(53, 69, 53)); // Dark green background
themePalette.setColor(QPalette::WindowText, Qt::white); // White text
themePalette.setColor(QPalette::Base, QColor(25, 40, 25)); // Darker green base
themePalette.setColor(QPalette::AlternateBase,
QColor(53, 69, 53)); // Dark green alternate base
themePalette.setColor(QPalette::ToolTipBase,
QColor(25, 40, 25)); // White tooltip background
themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text
themePalette.setColor(QPalette::Text, Qt::white); // White text
themePalette.setColor(QPalette::Button, QColor(53, 69, 53)); // Dark green button
themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text
themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts
themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links
themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight
themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text
qApp->setPalette(themePalette);
break;
case Theme::Blue:
mw_searchbar->setStyleSheet(
"QLineEdit {"
"background-color: #14283c; color: #ffffff; border: 1px solid #ffffff; "
"border-radius: 4px; padding: 5px; }"
"QLineEdit:focus {"
"border: 1px solid #2A82DA; }");
themePalette.setColor(QPalette::Window, QColor(40, 60, 90)); // Dark blue background
themePalette.setColor(QPalette::WindowText, Qt::white); // White text
themePalette.setColor(QPalette::Base, QColor(20, 40, 60)); // Darker blue base
themePalette.setColor(QPalette::AlternateBase,
QColor(40, 60, 90)); // Dark blue alternate base
themePalette.setColor(QPalette::ToolTipBase,
QColor(20, 40, 60)); // White tooltip background
themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text
themePalette.setColor(QPalette::Text, Qt::white); // White text
themePalette.setColor(QPalette::Button, QColor(40, 60, 90)); // Dark blue button
themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text
themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts
themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links
themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight
themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text
qApp->setPalette(themePalette);
break;
case Theme::Violet:
mw_searchbar->setStyleSheet(
"QLineEdit {"
"background-color: #501e5a; color: #ffffff; border: 1px solid #ffffff; "
"border-radius: 4px; padding: 5px; }"
"QLineEdit:focus {"
"border: 1px solid #2A82DA; }");
themePalette.setColor(QPalette::Window, QColor(100, 50, 120)); // Violet background
themePalette.setColor(QPalette::WindowText, Qt::white); // White text
themePalette.setColor(QPalette::Base, QColor(80, 30, 90)); // Darker violet base
themePalette.setColor(QPalette::AlternateBase,
QColor(100, 50, 120)); // Violet alternate base
themePalette.setColor(QPalette::ToolTipBase,
QColor(80, 30, 90)); // White tooltip background
themePalette.setColor(QPalette::ToolTipText, Qt::white); // White tooltip text
themePalette.setColor(QPalette::Text, Qt::white); // White text
themePalette.setColor(QPalette::Button, QColor(100, 50, 120)); // Violet button
themePalette.setColor(QPalette::ButtonText, Qt::white); // White button text
themePalette.setColor(QPalette::BrightText, Qt::red); // Bright red text for alerts
themePalette.setColor(QPalette::Link, QColor(42, 130, 218)); // Light blue links
themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); // Light blue highlight
themePalette.setColor(QPalette::HighlightedText, Qt::black); // Black highlighted text
qApp->setPalette(themePalette);
break;
case Theme::Gruvbox:
mw_searchbar->setStyleSheet(
"QLineEdit {"
"background-color: #1d2021; color: #f9f5d7; border: 1px solid #f9f5d7; "
"border-radius: 4px; padding: 5px; }"
"QLineEdit:focus {"
"border: 1px solid #83A598; }");
themePalette.setColor(QPalette::Window, QColor(29, 32, 33));
themePalette.setColor(QPalette::WindowText, QColor(249, 245, 215));
themePalette.setColor(QPalette::Base, QColor(29, 32, 33));
themePalette.setColor(QPalette::AlternateBase, QColor(50, 48, 47));
themePalette.setColor(QPalette::ToolTipBase, QColor(29, 32, 33));
themePalette.setColor(QPalette::ToolTipText, QColor(249, 245, 215));
themePalette.setColor(QPalette::Text, QColor(249, 245, 215));
themePalette.setColor(QPalette::Button, QColor(40, 40, 40));
themePalette.setColor(QPalette::ButtonText, QColor(249, 245, 215));
themePalette.setColor(QPalette::BrightText, QColor(251, 73, 52));
themePalette.setColor(QPalette::Link, QColor(131, 165, 152));
themePalette.setColor(QPalette::Highlight, QColor(131, 165, 152));
themePalette.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(themePalette);
break;
case Theme::TokyoNight:
mw_searchbar->setStyleSheet(
"QLineEdit {"
"background-color: #1a1b26; color: #9d7cd8; border: 1px solid #9d7cd8; "
"border-radius: 4px; padding: 5px; }"
"QLineEdit:focus {"
"border: 1px solid #7aa2f7; }");
themePalette.setColor(QPalette::Window, QColor(31, 35, 53));
themePalette.setColor(QPalette::WindowText, QColor(192, 202, 245));
themePalette.setColor(QPalette::Base, QColor(25, 28, 39));
themePalette.setColor(QPalette::AlternateBase, QColor(36, 40, 59));
themePalette.setColor(QPalette::ToolTipBase, QColor(25, 28, 39));
themePalette.setColor(QPalette::ToolTipText, QColor(192, 202, 245));
themePalette.setColor(QPalette::Text, QColor(192, 202, 245));
themePalette.setColor(QPalette::Button, QColor(30, 30, 41));
themePalette.setColor(QPalette::ButtonText, QColor(192, 202, 245));
themePalette.setColor(QPalette::BrightText, QColor(197, 59, 83));
themePalette.setColor(QPalette::Link, QColor(79, 214, 190));
themePalette.setColor(QPalette::Highlight, QColor(79, 214, 190));
themePalette.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(themePalette);
break;
case Theme::Oled:
mw_searchbar->setStyleSheet("QLineEdit:focus {"
"border: 1px solid #2A82DA; }");
themePalette.setColor(QPalette::Window, Qt::black);
themePalette.setColor(QPalette::WindowText, Qt::white);
themePalette.setColor(QPalette::Base, Qt::black);
themePalette.setColor(QPalette::AlternateBase, Qt::black);
themePalette.setColor(QPalette::ToolTipBase, Qt::black);
themePalette.setColor(QPalette::ToolTipText, Qt::white);
themePalette.setColor(QPalette::Text, Qt::white);
themePalette.setColor(QPalette::Button, QColor(5, 5, 5));
themePalette.setColor(QPalette::ButtonText, Qt::white);
themePalette.setColor(QPalette::BrightText, Qt::red);
themePalette.setColor(QPalette::Link, QColor(42, 130, 218));
themePalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
themePalette.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(themePalette);
qApp->setStyleSheet("QLineEdit {"
"background-color: #000000; color: #ffffff; border: 1px solid #a0a0a0; "
"border-radius: 4px; padding: 5px; }"
"QCheckBox::indicator:unchecked {"
"border: 1px solid #808080; border-radius: 4px; }");
break;
}
}

View File

@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QApplication>
#include <QLineEdit>
#include <QWidget>
enum class Theme : int { Dark, Light, Green, Blue, Violet, Gruvbox, TokyoNight, Oled };
class WindowThemes : public QObject {
Q_OBJECT
public Q_SLOTS:
void SetWindowTheme(Theme theme, QLineEdit* mw_searchbar);
};

View File

@ -1,427 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QMenuBar>
#include <QPushButton>
#include <QToolBar>
class Ui_MainWindow {
public:
QAction* bootGameAct;
QAction* addElfFolderAct;
QAction* shadFolderAct;
QAction* exitAct;
QAction* showGameListAct;
QAction* refreshGameListAct;
QAction* setIconSizeTinyAct;
QAction* setIconSizeSmallAct;
QAction* setIconSizeMediumAct;
QAction* setIconSizeLargeAct;
QAction* toggleLabelsAct;
QAction* setlistModeListAct;
QAction* setlistModeGridAct;
QAction* setlistElfAct;
QAction* gameInstallPathAct;
QAction* downloadCheatsPatchesAct;
QAction* dumpGameListAct;
QAction* trophyViewerAct;
#ifdef ENABLE_UPDATER
QAction* updaterAct;
#endif
QAction* aboutAct;
QAction* configureAct;
QAction* configureHotkeys;
QAction* setThemeDark;
QAction* setThemeLight;
QAction* setThemeGreen;
QAction* setThemeBlue;
QAction* setThemeViolet;
QAction* setThemeGruvbox;
QAction* setThemeTokyoNight;
QAction* setThemeOled;
QWidget* centralWidget;
QLineEdit* mw_searchbar;
QPushButton* playButton;
QPushButton* pauseButton;
QPushButton* stopButton;
QPushButton* refreshButton;
QPushButton* settingsButton;
QPushButton* controllerButton;
QPushButton* keyboardButton;
QPushButton* fullscreenButton;
QPushButton* restartButton;
QWidget* sizeSliderContainer;
QHBoxLayout* sizeSliderContainer_layout;
QSlider* sizeSlider;
QMenuBar* menuBar;
QMenu* menuFile;
QMenu* menuRecent;
QMenu* menuView;
QMenu* menuGame_List_Icons;
QMenu* menuGame_List_Mode;
QMenu* menuSettings;
QMenu* menuUtils;
QMenu* menuThemes;
QMenu* menuHelp;
QToolBar* toolBar;
void setupUi(QMainWindow* MainWindow) {
if (MainWindow->objectName().isEmpty())
MainWindow->setObjectName("MainWindow");
// MainWindow->resize(1280, 720);
QIcon icon;
icon.addFile(QString::fromUtf8(":images/shadps4.ico"), QSize(), QIcon::Normal, QIcon::Off);
MainWindow->setWindowIcon(icon);
QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
sizePolicy.setHorizontalStretch(0);
sizePolicy.setVerticalStretch(0);
sizePolicy.setHeightForWidth(MainWindow->sizePolicy().hasHeightForWidth());
MainWindow->setSizePolicy(sizePolicy);
MainWindow->setMinimumSize(QSize(4, 0));
MainWindow->setAutoFillBackground(false);
MainWindow->setAnimated(true);
MainWindow->setDockNestingEnabled(true);
MainWindow->setDockOptions(QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks |
QMainWindow::AnimatedDocks | QMainWindow::GroupedDragging);
bootGameAct = new QAction(MainWindow);
bootGameAct->setObjectName("bootGameAct");
bootGameAct->setIcon(QIcon(":images/play_icon.png"));
addElfFolderAct = new QAction(MainWindow);
addElfFolderAct->setObjectName("addElfFolderAct");
addElfFolderAct->setIcon(QIcon(":images/folder_icon.png"));
shadFolderAct = new QAction(MainWindow);
shadFolderAct->setObjectName("shadFolderAct");
shadFolderAct->setIcon(QIcon(":images/folder_icon.png"));
exitAct = new QAction(MainWindow);
exitAct->setObjectName("exitAct");
exitAct->setIcon(QIcon(":images/exit_icon.png"));
showGameListAct = new QAction(MainWindow);
showGameListAct->setObjectName("showGameListAct");
showGameListAct->setCheckable(true);
refreshGameListAct = new QAction(MainWindow);
refreshGameListAct->setObjectName("refreshGameListAct");
refreshGameListAct->setIcon(QIcon(":images/refreshlist_icon.png"));
toggleLabelsAct = new QAction(MainWindow);
toggleLabelsAct->setObjectName("toggleLabelsAct");
toggleLabelsAct->setCheckable(true);
setIconSizeTinyAct = new QAction(MainWindow);
setIconSizeTinyAct->setObjectName("setIconSizeTinyAct");
setIconSizeTinyAct->setCheckable(true);
setIconSizeSmallAct = new QAction(MainWindow);
setIconSizeSmallAct->setObjectName("setIconSizeSmallAct");
setIconSizeSmallAct->setCheckable(true);
setIconSizeMediumAct = new QAction(MainWindow);
setIconSizeMediumAct->setObjectName("setIconSizeMediumAct");
setIconSizeMediumAct->setCheckable(true);
setIconSizeLargeAct = new QAction(MainWindow);
setIconSizeLargeAct->setObjectName("setIconSizeLargeAct");
setIconSizeLargeAct->setCheckable(true);
setlistModeListAct = new QAction(MainWindow);
setlistModeListAct->setObjectName("setlistModeListAct");
setlistModeListAct->setIcon(QIcon(":images/list_icon.png"));
setlistModeListAct->setCheckable(true);
setlistModeGridAct = new QAction(MainWindow);
setlistModeGridAct->setObjectName("setlistModeGridAct");
setlistModeGridAct->setIcon(QIcon(":images/grid_icon.png"));
setlistModeGridAct->setCheckable(true);
setlistElfAct = new QAction(MainWindow);
setlistElfAct->setObjectName("setlistElfAct");
setlistElfAct->setCheckable(true);
gameInstallPathAct = new QAction(MainWindow);
gameInstallPathAct->setObjectName("gameInstallPathAct");
gameInstallPathAct->setIcon(QIcon(":images/folder_icon.png"));
downloadCheatsPatchesAct = new QAction(MainWindow);
downloadCheatsPatchesAct->setObjectName("downloadCheatsPatchesAct");
downloadCheatsPatchesAct->setIcon(QIcon(":images/update_icon.png"));
dumpGameListAct = new QAction(MainWindow);
dumpGameListAct->setObjectName("dumpGameList");
dumpGameListAct->setIcon(QIcon(":images/dump_icon.png"));
trophyViewerAct = new QAction(MainWindow);
trophyViewerAct->setObjectName("trophyViewer");
trophyViewerAct->setIcon(QIcon(":images/trophy_icon.png"));
#ifdef ENABLE_UPDATER
updaterAct = new QAction(MainWindow);
updaterAct->setObjectName("updaterAct");
updaterAct->setIcon(QIcon(":images/update_icon.png"));
#endif
aboutAct = new QAction(MainWindow);
aboutAct->setObjectName("aboutAct");
aboutAct->setIcon(QIcon(":images/about_icon.png"));
configureAct = new QAction(MainWindow);
configureAct->setObjectName("configureAct");
configureAct->setIcon(QIcon(":images/settings_icon.png"));
configureHotkeys = new QAction(MainWindow);
configureHotkeys->setObjectName("configureHotkeys");
configureHotkeys->setIcon(QIcon(":images/hotkey.png"));
setThemeDark = new QAction(MainWindow);
setThemeDark->setObjectName("setThemeDark");
setThemeDark->setCheckable(true);
setThemeDark->setChecked(true);
setThemeLight = new QAction(MainWindow);
setThemeLight->setObjectName("setThemeLight");
setThemeLight->setCheckable(true);
setThemeGreen = new QAction(MainWindow);
setThemeGreen->setObjectName("setThemeGreen");
setThemeGreen->setCheckable(true);
setThemeBlue = new QAction(MainWindow);
setThemeBlue->setObjectName("setThemeBlue");
setThemeBlue->setCheckable(true);
setThemeViolet = new QAction(MainWindow);
setThemeViolet->setObjectName("setThemeViolet");
setThemeViolet->setCheckable(true);
setThemeGruvbox = new QAction(MainWindow);
setThemeGruvbox->setObjectName("setThemeGruvbox");
setThemeGruvbox->setCheckable(true);
setThemeTokyoNight = new QAction(MainWindow);
setThemeTokyoNight->setObjectName("setThemeTokyoNight");
setThemeTokyoNight->setCheckable(true);
setThemeOled = new QAction(MainWindow);
setThemeOled->setObjectName("setThemeOled");
setThemeOled->setCheckable(true);
centralWidget = new QWidget(MainWindow);
centralWidget->setObjectName("centralWidget");
sizePolicy.setHeightForWidth(centralWidget->sizePolicy().hasHeightForWidth());
centralWidget->setSizePolicy(sizePolicy);
mw_searchbar = new QLineEdit(centralWidget);
mw_searchbar->setObjectName("mw_searchbar");
mw_searchbar->setGeometry(QRect(250, 10, 130, 31));
mw_searchbar->setMaximumWidth(250);
QFont font;
font.setPointSize(10);
font.setBold(false);
mw_searchbar->setFont(font);
mw_searchbar->setFocusPolicy(Qt::ClickFocus);
mw_searchbar->setFrame(false);
mw_searchbar->setClearButtonEnabled(false);
playButton = new QPushButton(centralWidget);
playButton->setFlat(true);
playButton->setIcon(QIcon(":images/play_icon.png"));
playButton->setIconSize(QSize(40, 40));
pauseButton = new QPushButton(centralWidget);
pauseButton->setFlat(true);
pauseButton->setIcon(QIcon(":images/pause_icon.png"));
pauseButton->setIconSize(QSize(40, 40));
stopButton = new QPushButton(centralWidget);
stopButton->setFlat(true);
stopButton->setIcon(QIcon(":images/stop_icon.png"));
stopButton->setIconSize(QSize(40, 40));
refreshButton = new QPushButton(centralWidget);
refreshButton->setFlat(true);
refreshButton->setIcon(QIcon(":images/refreshlist_icon.png"));
refreshButton->setIconSize(QSize(40, 40));
fullscreenButton = new QPushButton(centralWidget);
fullscreenButton->setFlat(true);
fullscreenButton->setIcon(QIcon(":images/fullscreen_icon.png"));
fullscreenButton->setIconSize(QSize(38, 38));
settingsButton = new QPushButton(centralWidget);
settingsButton->setFlat(true);
settingsButton->setIcon(QIcon(":images/settings_icon.png"));
settingsButton->setIconSize(QSize(40, 40));
controllerButton = new QPushButton(centralWidget);
controllerButton->setFlat(true);
controllerButton->setIcon(QIcon(":images/controller_icon.png"));
controllerButton->setIconSize(QSize(55, 48));
keyboardButton = new QPushButton(centralWidget);
keyboardButton->setFlat(true);
keyboardButton->setIcon(QIcon(":images/keyboard_icon.png"));
keyboardButton->setIconSize(QSize(50, 50));
restartButton = new QPushButton(centralWidget);
restartButton->setFlat(true);
restartButton->setIcon(QIcon(":images/restart_game_icon.png"));
restartButton->setIconSize(QSize(40, 40));
sizeSliderContainer = new QWidget(centralWidget);
sizeSliderContainer->setObjectName("sizeSliderContainer");
sizeSliderContainer->setGeometry(QRect(280, 10, 181, 31));
QSizePolicy sizePolicy1(QSizePolicy::Fixed, QSizePolicy::Expanding);
sizePolicy1.setHorizontalStretch(0);
sizePolicy1.setVerticalStretch(0);
sizePolicy1.setHeightForWidth(sizeSliderContainer->sizePolicy().hasHeightForWidth());
sizeSliderContainer->setSizePolicy(sizePolicy1);
sizeSliderContainer_layout = new QHBoxLayout(sizeSliderContainer);
sizeSliderContainer_layout->setSpacing(0);
sizeSliderContainer_layout->setContentsMargins(11, 11, 11, 11);
sizeSliderContainer_layout->setObjectName("sizeSliderContainer_layout");
sizeSliderContainer_layout->setContentsMargins(14, 0, 14, 0);
sizeSlider = new QSlider(sizeSliderContainer);
sizeSlider->setObjectName("sizeSlider");
QSizePolicy sizePolicy2(QSizePolicy::Expanding, QSizePolicy::Preferred);
sizePolicy2.setHorizontalStretch(0);
sizePolicy2.setVerticalStretch(0);
sizePolicy2.setHeightForWidth(sizeSlider->sizePolicy().hasHeightForWidth());
sizeSlider->setSizePolicy(sizePolicy2);
sizeSlider->setFocusPolicy(Qt::ClickFocus);
sizeSlider->setAutoFillBackground(false);
sizeSlider->setOrientation(Qt::Horizontal);
sizeSlider->setTickPosition(QSlider::NoTicks);
sizeSlider->setMinimum(0);
sizeSlider->setMaximum(220);
sizeSliderContainer_layout->addWidget(sizeSlider);
MainWindow->setCentralWidget(centralWidget);
menuBar = new QMenuBar(MainWindow);
menuBar->setObjectName("menuBar");
menuBar->setGeometry(QRect(0, 0, 1058, 22));
menuBar->setContextMenuPolicy(Qt::PreventContextMenu);
menuFile = new QMenu(menuBar);
menuFile->setObjectName("menuFile");
menuRecent = new QMenu(menuFile);
menuRecent->setObjectName("menuRecent");
menuView = new QMenu(menuBar);
menuView->setObjectName("menuView");
menuGame_List_Icons = new QMenu(menuView);
menuGame_List_Icons->setObjectName("menuGame_List_Icons");
menuGame_List_Icons->setIcon(QIcon(":images/iconsize_icon.png"));
menuGame_List_Mode = new QMenu(menuView);
menuGame_List_Mode->setObjectName("menuGame_List_Mode");
menuGame_List_Mode->setIcon(QIcon(":images/list_mode_icon.png"));
menuSettings = new QMenu(menuBar);
menuSettings->setObjectName("menuSettings");
menuUtils = new QMenu(menuSettings);
menuUtils->setObjectName("menuUtils");
menuUtils->setIcon(QIcon(":images/utils_icon.png"));
menuThemes = new QMenu(menuView);
menuThemes->setObjectName("menuThemes");
menuThemes->setIcon(QIcon(":images/themes_icon.png"));
menuHelp = new QMenu(menuBar);
menuHelp->setObjectName("menuHelp");
MainWindow->setMenuBar(menuBar);
toolBar = new QToolBar(MainWindow);
toolBar->setObjectName("toolBar");
MainWindow->addToolBar(Qt::TopToolBarArea, toolBar);
menuBar->addAction(menuFile->menuAction());
menuBar->addAction(menuView->menuAction());
menuBar->addAction(menuSettings->menuAction());
menuBar->addAction(menuHelp->menuAction());
menuFile->addAction(bootGameAct);
menuFile->addSeparator();
menuFile->addAction(addElfFolderAct);
menuFile->addAction(shadFolderAct);
menuFile->addSeparator();
menuFile->addAction(menuRecent->menuAction());
menuFile->addSeparator();
menuFile->addAction(exitAct);
menuView->addAction(showGameListAct);
menuView->addSeparator();
menuView->addAction(refreshGameListAct);
menuView->addAction(menuGame_List_Mode->menuAction());
menuView->addAction(menuGame_List_Icons->menuAction());
menuView->addAction(toggleLabelsAct);
menuView->addAction(menuThemes->menuAction());
menuThemes->addAction(setThemeDark);
menuThemes->addAction(setThemeLight);
menuThemes->addAction(setThemeGreen);
menuThemes->addAction(setThemeBlue);
menuThemes->addAction(setThemeViolet);
menuThemes->addAction(setThemeGruvbox);
menuThemes->addAction(setThemeTokyoNight);
menuThemes->addAction(setThemeOled);
menuGame_List_Icons->addAction(setIconSizeTinyAct);
menuGame_List_Icons->addAction(setIconSizeSmallAct);
menuGame_List_Icons->addAction(setIconSizeMediumAct);
menuGame_List_Icons->addAction(setIconSizeLargeAct);
menuGame_List_Mode->addAction(setlistModeListAct);
menuGame_List_Mode->addAction(setlistModeGridAct);
menuGame_List_Mode->addAction(setlistElfAct);
menuSettings->addAction(configureAct);
menuSettings->addAction(gameInstallPathAct);
menuSettings->addAction(configureHotkeys);
menuSettings->addAction(menuUtils->menuAction());
menuUtils->addAction(downloadCheatsPatchesAct);
menuUtils->addAction(dumpGameListAct);
menuUtils->addAction(trophyViewerAct);
#ifdef ENABLE_UPDATER
menuHelp->addAction(updaterAct);
#endif
menuHelp->addAction(aboutAct);
retranslateUi(MainWindow);
QMetaObject::connectSlotsByName(MainWindow);
} // setupUi
void retranslateUi(QMainWindow* MainWindow) {
MainWindow->setWindowTitle(QCoreApplication::translate("MainWindow", "shadPS4", nullptr));
addElfFolderAct->setText(
QCoreApplication::translate("MainWindow", "Open/Add Elf Folder", nullptr));
bootGameAct->setText(QCoreApplication::translate("MainWindow", "Boot Game", nullptr));
#ifdef ENABLE_UPDATER
updaterAct->setText(
QCoreApplication::translate("MainWindow", "Check for Updates", nullptr));
#endif
aboutAct->setText(QCoreApplication::translate("MainWindow", "About shadPS4", nullptr));
configureAct->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr));
configureHotkeys->setText(
QCoreApplication::translate("MainWindow", "Customize Hotkeys", nullptr));
#if QT_CONFIG(tooltip)
#endif // QT_CONFIG(tooltip)
menuRecent->setTitle(QCoreApplication::translate("MainWindow", "Recent Games", nullptr));
shadFolderAct->setText(
QCoreApplication::translate("MainWindow", "Open shadPS4 Folder", nullptr));
exitAct->setText(QCoreApplication::translate("MainWindow", "Exit", nullptr));
#if QT_CONFIG(tooltip)
exitAct->setToolTip(QCoreApplication::translate("MainWindow", "Exit shadPS4", nullptr));
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(statustip)
exitAct->setStatusTip(
QCoreApplication::translate("MainWindow", "Exit the application.", nullptr));
#endif // QT_CONFIG(statustip)
showGameListAct->setText(
QCoreApplication::translate("MainWindow", "Show Game List", nullptr));
refreshGameListAct->setText(
QCoreApplication::translate("MainWindow", "Game List Refresh", nullptr));
setIconSizeTinyAct->setText(QCoreApplication::translate("MainWindow", "Tiny", nullptr));
setIconSizeSmallAct->setText(QCoreApplication::translate("MainWindow", "Small", nullptr));
setIconSizeMediumAct->setText(QCoreApplication::translate("MainWindow", "Medium", nullptr));
setIconSizeLargeAct->setText(QCoreApplication::translate("MainWindow", "Large", nullptr));
setlistModeListAct->setText(
QCoreApplication::translate("MainWindow", "List View", nullptr));
setlistModeGridAct->setText(
QCoreApplication::translate("MainWindow", "Grid View", nullptr));
setlistElfAct->setText(QCoreApplication::translate("MainWindow", "Elf Viewer", nullptr));
gameInstallPathAct->setText(
QCoreApplication::translate("MainWindow", "Game Install Directory", nullptr));
downloadCheatsPatchesAct->setText(
QCoreApplication::translate("MainWindow", "Download Cheats/Patches", nullptr));
dumpGameListAct->setText(
QCoreApplication::translate("MainWindow", "Dump Game List", nullptr));
trophyViewerAct->setText(
QCoreApplication::translate("MainWindow", "Trophy Viewer", nullptr));
mw_searchbar->setPlaceholderText(
QCoreApplication::translate("MainWindow", "Search...", nullptr));
menuFile->setTitle(QCoreApplication::translate("MainWindow", "File", nullptr));
menuView->setTitle(QCoreApplication::translate("MainWindow", "View", nullptr));
menuGame_List_Icons->setTitle(
QCoreApplication::translate("MainWindow", "Game List Icons", nullptr));
menuGame_List_Mode->setTitle(
QCoreApplication::translate("MainWindow", "Game List Mode", nullptr));
menuSettings->setTitle(QCoreApplication::translate("MainWindow", "Settings", nullptr));
menuUtils->setTitle(QCoreApplication::translate("MainWindow", "Utils", nullptr));
menuThemes->setTitle(QCoreApplication::translate("MainWindow", "Themes", nullptr));
menuHelp->setTitle(QCoreApplication::translate("MainWindow", "Help", nullptr));
setThemeDark->setText(QCoreApplication::translate("MainWindow", "Dark", nullptr));
setThemeLight->setText(QCoreApplication::translate("MainWindow", "Light", nullptr));
setThemeGreen->setText(QCoreApplication::translate("MainWindow", "Green", nullptr));
setThemeBlue->setText(QCoreApplication::translate("MainWindow", "Blue", nullptr));
setThemeViolet->setText(QCoreApplication::translate("MainWindow", "Violet", nullptr));
setThemeGruvbox->setText("Gruvbox");
setThemeTokyoNight->setText("Tokyo Night");
setThemeOled->setText("OLED");
toolBar->setWindowTitle(QCoreApplication::translate("MainWindow", "toolBar", nullptr));
toggleLabelsAct->setText(
QCoreApplication::translate("MainWindow", "Show Labels Under Icons"));
} // retranslateUi
};
namespace Ui {
class MainWindow : public Ui_MainWindow {};
} // namespace Ui

View File

@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "sdl_event_wrapper.h"
using namespace SdlEventWrapper;
Wrapper* Wrapper::WrapperInstance = nullptr;
bool Wrapper::wrapperActive = false;
Wrapper::Wrapper(QObject* parent) : QObject(parent) {}
Wrapper* Wrapper::GetInstance() {
if (WrapperInstance == nullptr) {
WrapperInstance = new Wrapper();
}
return WrapperInstance;
}
bool Wrapper::ProcessEvent(SDL_Event* event) {
switch (event->type) {
case SDL_EVENT_WINDOW_RESTORED:
return false;
case SDL_EVENT_WINDOW_EXPOSED:
return false;
case SDL_EVENT_GAMEPAD_ADDED:
emit SDLEvent(SDL_EVENT_GAMEPAD_ADDED, 0, 0);
return false;
case SDL_EVENT_GAMEPAD_REMOVED:
emit SDLEvent(SDL_EVENT_GAMEPAD_REMOVED, 0, 0);
return false;
case SDL_EVENT_QUIT:
emit SDLEvent(SDL_EVENT_QUIT, 0, 0);
return true;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
emit SDLEvent(SDL_EVENT_GAMEPAD_BUTTON_DOWN, event->gbutton.button, 0);
return true;
case SDL_EVENT_GAMEPAD_BUTTON_UP:
emit SDLEvent(SDL_EVENT_GAMEPAD_BUTTON_UP, event->gbutton.button, 0);
return true;
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
emit SDLEvent(SDL_EVENT_GAMEPAD_AXIS_MOTION, event->gaxis.axis, event->gaxis.value);
return true;
case SDL_EVENT_AUDIO_DEVICE_ADDED:
if (event->adevice.recording == 0)
emit audioDeviceChanged(true);
return true;
case SDL_EVENT_AUDIO_DEVICE_REMOVED:
if (event->adevice.recording == 0)
emit audioDeviceChanged(false);
return true;
// block all other SDL events while wrapper is active
default:
return true;
}
}
Wrapper::~Wrapper() {}

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <SDL3/SDL_events.h>
namespace SdlEventWrapper {
class Wrapper : public QObject {
Q_OBJECT
public:
explicit Wrapper(QObject* parent = nullptr);
~Wrapper();
bool ProcessEvent(SDL_Event* event);
static Wrapper* GetInstance();
static bool wrapperActive;
static Wrapper* WrapperInstance;
signals:
void SDLEvent(int Type, int Input, int Value);
void audioDeviceChanged(bool isAdd);
};
} // namespace SdlEventWrapper

View File

@ -1,91 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <common/path_util.h>
#include "settings.h"
settings::settings(QObject* parent) : QObject(parent), m_settings_dir(ComputeSettingsDir()) {}
settings::~settings() {
sync();
}
void settings::sync() {
if (m_settings) {
m_settings->sync();
}
}
QString settings::GetSettingsDir() const {
return m_settings_dir.absolutePath();
}
QString settings::ComputeSettingsDir() {
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
return QString::fromStdString(config_dir.string() + "/");
}
void settings::RemoveValue(const QString& key, const QString& name, bool sync) const {
if (m_settings) {
m_settings->beginGroup(key);
m_settings->remove(name);
m_settings->endGroup();
if (sync) {
m_settings->sync();
}
}
}
void settings::RemoveValue(const gui_value& entry, bool sync) const {
RemoveValue(entry.key, entry.name, sync);
}
QVariant settings::GetValue(const QString& key, const QString& name, const QVariant& def) const {
return m_settings ? m_settings->value(key + "/" + name, def) : def;
}
QVariant settings::GetValue(const gui_value& entry) const {
return GetValue(entry.key, entry.name, entry.def);
}
void settings::SetValue(const gui_value& entry, const QVariant& value, bool sync) const {
SetValue(entry.key, entry.name, value, sync);
}
void settings::SetValue(const QString& key, const QVariant& value, bool sync) const {
if (m_settings) {
m_settings->setValue(key, value);
if (sync) {
m_settings->sync();
}
}
}
void settings::SetValue(const QString& key, const QString& name, const QVariant& value,
bool sync) const {
if (m_settings) {
m_settings->beginGroup(key);
m_settings->setValue(name, value);
m_settings->endGroup();
if (sync) {
m_settings->sync();
}
}
}
QVariant settings::List2Var(const QList<QString>& list) {
QByteArray ba;
QDataStream stream(&ba, QIODevice::WriteOnly);
stream << list;
return QVariant(ba);
}
QList<QString> settings::Var2List(const QVariant& var) {
QList<QString> list;
QByteArray ba = var.toByteArray();
QDataStream stream(&ba, QIODevice::ReadOnly);
stream >> list;
return list;
}

View File

@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDir>
#include <QSettings>
#include <QString>
#include <QVariant>
struct gui_value {
QString key;
QString name;
QVariant def;
gui_value() {}
gui_value(const QString& k, const QString& n, const QVariant& d) : key(k), name(n), def(d) {}
bool operator==(const gui_value& rhs) const noexcept {
return key == rhs.key && name == rhs.name && def == rhs.def;
}
};
class settings : public QObject {
Q_OBJECT
public:
explicit settings(QObject* parent = nullptr);
~settings();
void sync();
QString GetSettingsDir() const;
QVariant GetValue(const QString& key, const QString& name, const QVariant& def) const;
QVariant GetValue(const gui_value& entry) const;
static QVariant List2Var(const QList<QString>& list);
static QList<QString> Var2List(const QVariant& var);
public Q_SLOTS:
/** Remove entry */
void RemoveValue(const QString& key, const QString& name, bool sync = true) const;
void RemoveValue(const gui_value& entry, bool sync = true) const;
/** Write value to entry */
void SetValue(const gui_value& entry, const QVariant& value, bool sync = true) const;
void SetValue(const QString& key, const QVariant& value, bool sync = true) const;
void SetValue(const QString& key, const QString& name, const QVariant& value,
bool sync = true) const;
protected:
static QString ComputeSettingsDir();
std::unique_ptr<QSettings> m_settings;
QDir m_settings_dir;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <span>
#include <QDialog>
#include <QGroupBox>
#include <QPushButton>
#include "common/config.h"
#include "common/path_util.h"
#include "gui_settings.h"
#include "qt_gui/compatibility_info.h"
namespace Ui {
class SettingsDialog;
}
class SettingsDialog : public QDialog {
Q_OBJECT
public:
explicit SettingsDialog(std::shared_ptr<gui_settings> gui_settings,
std::shared_ptr<CompatibilityInfoClass> m_compat_info,
QWidget* parent = nullptr, bool is_game_running = false,
bool is_game_specific = false, std::string gsc_serial = "");
~SettingsDialog();
bool eventFilter(QObject* obj, QEvent* event) override;
void updateNoteTextEdit(const QString& groupName);
int exec() override;
signals:
void LanguageChanged(const QString& locale);
void CompatibilityChanged();
void BackgroundOpacityChanged(int opacity);
private:
void LoadValuesFromConfig();
void UpdateSettings(bool game_specific = false);
void SyncRealTimeWidgetstoConfig();
void InitializeEmulatorLanguages();
void OnLanguageChanged(int index);
void OnCursorStateChanged(s16 index);
void closeEvent(QCloseEvent* event) override;
void setDefaultValues();
void VolumeSliderChange(int value);
void onAudioDeviceChange(bool isAdd);
void pollSDLevents();
std::unique_ptr<Ui::SettingsDialog> ui;
std::map<std::string, int> languages;
QString defaultTextEdit;
int initialHeight;
std::string gs_serial;
bool is_game_running = false;
bool is_game_specific = false;
bool is_game_saving = false;
std::shared_ptr<gui_settings> m_gui_settings;
QFuture<void> Polling;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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