mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-24 04:46:56 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
|
||||
find_package(Qt6 COMPONENTS Concurrent Network Widgets REQUIRED)
|
||||
|
||||
set(CMAKE_AUTOUIC_SEARCH_PATHS forms)
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
file(GLOB launcher_src
|
||||
"include/*.h"
|
||||
"src/*.cpp"
|
||||
)
|
||||
|
||||
file(GLOB launcher_forms
|
||||
"forms/*.ui"
|
||||
)
|
||||
|
||||
add_executable(launcher WIN32
|
||||
${launcher_src}
|
||||
${launcher_forms}
|
||||
)
|
||||
|
||||
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets)
|
||||
if(WIN32)
|
||||
target_link_libraries(launcher setupapi)
|
||||
endif()
|
||||
if (CLANG AND NOT KYTY_CLANG_CL)
|
||||
target_link_libraries(launcher pthread)
|
||||
endif()
|
||||
target_include_directories(launcher PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
set_target_properties(launcher PROPERTIES AUTOUIC ON AUTOMOC ON AUTORCC ON)
|
||||
|
||||
if(WIN32 AND NOT KYTY_CLANG_CL)
|
||||
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
|
||||
endif()
|
||||
|
||||
add_dependencies(launcher KytyGitVersion)
|
||||
add_dependencies(launcher kyty_emulator)
|
||||
|
||||
if(KYTY_CLANG_CL)
|
||||
add_custom_command(TARGET launcher POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_PDB_FILE:kyty_emulator> $<TARGET_FILE_DIR:launcher>/$<TARGET_PDB_FILE_NAME:kyty_emulator>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE_DIR:kyty_emulator>/${KYTY_EMULATOR_MAP_NAME} $<TARGET_FILE_DIR:launcher>/${KYTY_EMULATOR_MAP_NAME}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${KYTY_THIRD_PARTY_DIR}/winpthread/bin/libwinpthread-1.dll" $<TARGET_FILE_DIR:launcher>/libwinpthread-1.dll
|
||||
)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
find_program(QT_WINDEPLOYQT NAMES windeployqt PATHS "${Qt6_DIR}/../../../bin")
|
||||
if(NOT QT_WINDEPLOYQT)
|
||||
message(FATAL_ERROR "Could not find windeployqt")
|
||||
endif()
|
||||
add_custom_command(TARGET launcher POST_BUILD
|
||||
COMMAND ${QT_WINDEPLOYQT} $<TARGET_FILE:launcher>
|
||||
--no-compiler-runtime
|
||||
--no-system-d3d-compiler
|
||||
--no-system-dxc-compiler
|
||||
--no-opengl-sw
|
||||
--no-translations
|
||||
COMMENT "Deploying Qt runtime and TLS plugins"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
set(launcher_name "launcher")
|
||||
|
||||
set_target_properties(launcher PROPERTIES OUTPUT_NAME ${launcher_name})
|
||||
|
||||
install(TARGETS launcher DESTINATION .)
|
||||
|
||||
if(WIN32)
|
||||
install(CODE "
|
||||
execute_process(
|
||||
COMMAND ${QT_WINDEPLOYQT} \"\${CMAKE_INSTALL_PREFIX}/${launcher_name}.exe\"
|
||||
--no-compiler-runtime
|
||||
--no-system-d3d-compiler
|
||||
--no-system-dxc-compiler
|
||||
--no-opengl-sw
|
||||
--no-translations
|
||||
)
|
||||
")
|
||||
endif()
|
||||
|
||||
|
||||
set(tidy_dirs "${CMAKE_SOURCE_DIR}/launcher/include")
|
||||
set(iwyu_maps "${CMAKE_SOURCE_DIR}/launcher/utils/qt6_16.imp")
|
||||
|
||||
get_property(inc_headers TARGET launcher PROPERTY INCLUDE_DIRECTORIES)
|
||||
|
||||
list(APPEND inc_headers
|
||||
${CMAKE_SOURCE_DIR}/launcher
|
||||
${CMAKE_BINARY_DIR}/launcher/launcher_autogen/include
|
||||
${Qt6Widgets_INCLUDE_DIRS}
|
||||
${Qt6Core_INCLUDE_DIRS}
|
||||
${Qt6Gui_INCLUDE_DIRS}
|
||||
#${Qt6_DIR}/../../../include
|
||||
#${Qt6_DIR}/../../../include/QtWidgets
|
||||
#${Qt6_DIR}/../../../include/QtCore
|
||||
#${Qt6_DIR}/../../../include/QtGui
|
||||
)
|
||||
|
||||
include_what_you_use_with_mappings(launcher "${inc_headers}" "${iwyu_maps}")
|
||||
|
||||
clang_tidy_check(launcher "" "${tidy_dirs}" "${inc_headers}")
|
||||
@@ -0,0 +1,338 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ConfigurationEditDialog</class>
|
||||
<widget class="QDialog" name="ConfigurationEditDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>560</width>
|
||||
<height>420</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QFrame" name="configuaration_groupbox">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_6">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QFrame" name="frame_4">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="checkBox_vulkan_validation">
|
||||
<property name="toolTip">
|
||||
<string>Enable Vulkan validation layers</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Vulkan validation</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_shader_validation">
|
||||
<property name="toolTip">
|
||||
<string>Validate SPIR-V binary</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Shader validation</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_cmd_dump">
|
||||
<property name="toolTip">
|
||||
<string>Dump command buffers</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Command buffer dump</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="checkBox_renderdoc_capture">
|
||||
<property name="toolTip">
|
||||
<string>Enable RenderDoc capture</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>RenderDoc capture</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_ngg_rectlist_draw">
|
||||
<property name="toolTip">
|
||||
<string>Use the NGG 4-vertex path for rect-list DrawIndexAuto primitive 7</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use NGG rect-list draw</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
<string>Screen resolution:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="comboBox_screen_resolution">
|
||||
<property name="toolTip">
|
||||
<string>Window resolution</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_vblank_frequency">
|
||||
<property name="text">
|
||||
<string>Vblank frequency:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_vblank_frequency">
|
||||
<property name="toolTip">
|
||||
<string>Virtual display refresh rate used for frame pacing</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>30</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>360</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>60</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_20">
|
||||
<property name="text">
|
||||
<string>Shader optimization type:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="comboBox_shader_optimization_type">
|
||||
<property name="toolTip">
|
||||
<string>Optimize shaders for code size or performance</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_21">
|
||||
<property name="text">
|
||||
<string>Shader log direction:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="comboBox_shader_log_direction">
|
||||
<property name="toolTip">
|
||||
<string>Dump shaders to file or console window. If enabled may decrease emulator performance</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_22">
|
||||
<property name="text">
|
||||
<string>Shader log folder:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
|
||||
<property name="toolTip">
|
||||
<string>Specify directory to dump shaders</string>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_24">
|
||||
<property name="text">
|
||||
<string>Command buffer dump folder:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
|
||||
<property name="toolTip">
|
||||
<string>Specify directory to dump command buffers</string>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_25">
|
||||
<property name="text">
|
||||
<string>Printf direction:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="comboBox_printf_direction">
|
||||
<property name="toolTip">
|
||||
<string>Print logs to file or console window. If enabled may decrease emulator performance</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_26">
|
||||
<property name="text">
|
||||
<string>Printf output file:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
|
||||
<property name="toolTip">
|
||||
<string>Specify file to dump logs</string>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="label_27">
|
||||
<property name="text">
|
||||
<string>Profiler direction:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QComboBox" name="comboBox_profiler_direction">
|
||||
<property name="toolTip">
|
||||
<string>Enable or disable profiler. If enabled may decrease emulator performance</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="ok_button">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancel_button">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="clear_button">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Clear</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>MandatoryLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>mandatoryLineEdit.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>cancel_button</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>ConfigurationEditDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>496</x>
|
||||
<y>65</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>280</x>
|
||||
<y>180</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,223 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ConfigurationListWidget</class>
|
||||
<widget class="QWidget" name="ConfigurationListWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>292</width>
|
||||
<height>268</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Configurations</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="leftMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QToolButton" name="global_settings_button">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>36</width>
|
||||
<height>36</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Edit global settings</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="edit_button">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>36</width>
|
||||
<height>36</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Edit configuration</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="delete_button">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>36</width>
|
||||
<height>36</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Delete configuration</p></body></html></string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search_line_edit">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>220</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>320</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Search game name or serial</string>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Search name or serial</string>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="GameListTreeWidget" name="cfgs_list">
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Game</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Serial</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Firmware Version</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Status</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Comments</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>GameListTreeWidget</class>
|
||||
<extends>QTreeWidget</extends>
|
||||
<header>gameListTreeWidget.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainDialog</class>
|
||||
<widget class="QDialog" name="MainDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>428</width>
|
||||
<height>454</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Kyty Launcher</string>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="ConfigurationListWidget" name="widget" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_settings_file">
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_Interpreter">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_Version">
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ConfigurationListWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>configurationListWidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<buttongroups>
|
||||
<buttongroup name="buttonGroup_Console"/>
|
||||
</buttongroups>
|
||||
</ui>
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef LAUNCHER_INCLUDE_COMMON_H_
|
||||
#define LAUNCHER_INCLUDE_COMMON_H_
|
||||
|
||||
// IWYU pragma: begin_exports
|
||||
#include <QArgument>
|
||||
#include <QObject>
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
// IWYU pragma: end_exports
|
||||
|
||||
#define KYTY_QT_CLASS_NO_COPY(name) \
|
||||
public: \
|
||||
name(const name&) = delete; /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
name& operator=(const name&) = delete; /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
name(name&&) noexcept = delete; /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
name& operator=(name&&) noexcept = delete; /* NOLINT(bugprone-macro-parentheses) */
|
||||
|
||||
#endif /* LAUNCHER_INCLUDE_COMMON_H_ */
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef COMPATIBILITY_DATABASE_H
|
||||
#define COMPATIBILITY_DATABASE_H
|
||||
|
||||
#include "common.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
struct CompatibilityEntry {
|
||||
Configuration::GameStatus status = Configuration::GameStatus::Unknown;
|
||||
QString comment;
|
||||
};
|
||||
|
||||
class CompatibilityDatabase: public QObject {
|
||||
Q_OBJECT
|
||||
KYTY_QT_CLASS_NO_COPY(CompatibilityDatabase);
|
||||
|
||||
public:
|
||||
explicit CompatibilityDatabase(bool local, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool IsLocal() const { return m_local; }
|
||||
[[nodiscard]] const CompatibilityEntry* Find(const QString& title_id) const;
|
||||
|
||||
void Load();
|
||||
void SetStatus(const QString& title_id, Configuration::GameStatus status);
|
||||
void SetComment(const QString& title_id, const QString& comment);
|
||||
|
||||
signals:
|
||||
void Updated();
|
||||
|
||||
private:
|
||||
void Save() const;
|
||||
|
||||
QMap<QString, CompatibilityEntry> m_entries;
|
||||
bool m_local = false;
|
||||
};
|
||||
|
||||
#endif // COMPATIBILITY_DATABASE_H
|
||||
@@ -0,0 +1,178 @@
|
||||
#ifndef LAUNCHER_INCLUDE_CONFIGURATION_H_
|
||||
#define LAUNCHER_INCLUDE_CONFIGURATION_H_
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QMetaEnum>
|
||||
#include <QMetaType>
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
|
||||
#define KYTY_CFG_SET(n) s->setValue(#n, QVariant::fromValue(n).toString());
|
||||
#define KYTY_CFG_GET(n) n = s->value(#n).value<decltype(n)>();
|
||||
|
||||
template <class T>
|
||||
inline QStringList EnumToList() {
|
||||
QStringList ret;
|
||||
auto me = QMetaEnum::fromType<T>();
|
||||
int count = me.keyCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
auto key = QString(me.key(i));
|
||||
ret << (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit()
|
||||
? key.remove('R').toLower()
|
||||
: key);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
T TextToEnum(const QString& text) {
|
||||
auto me = QMetaEnum::fromType<T>();
|
||||
return static_cast<T>(me.keyToValue(
|
||||
((text.size() > 1 && text.at(0).isDigit()) ? 'R' + text.toUpper() : text).toUtf8().data()));
|
||||
}
|
||||
|
||||
template <class T>
|
||||
QString EnumToText(T value) {
|
||||
auto me = QMetaEnum::fromType<T>();
|
||||
auto key = QString(me.valueToKey(static_cast<int>(value)));
|
||||
return (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit() ? key.remove('R').toLower()
|
||||
: key);
|
||||
}
|
||||
|
||||
class Configuration: public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Resolution {
|
||||
R1280X720,
|
||||
R1920X1080,
|
||||
};
|
||||
Q_ENUM(Resolution)
|
||||
|
||||
enum class ShaderOptimizationType { None, Size, Performance };
|
||||
Q_ENUM(ShaderOptimizationType)
|
||||
|
||||
enum class ShaderLogDirection { Silent, Console, File };
|
||||
Q_ENUM(ShaderLogDirection)
|
||||
|
||||
enum class ProfilerDirection { None, Network };
|
||||
Q_ENUM(ProfilerDirection)
|
||||
|
||||
enum class LogDirection { Silent, Console, File };
|
||||
Q_ENUM(LogDirection)
|
||||
|
||||
enum class GameStatus { Unknown, InGame, Logo, DoesntBoot, MainMenu };
|
||||
Q_ENUM(GameStatus)
|
||||
|
||||
Configuration() = default;
|
||||
|
||||
QString name;
|
||||
QString title_id; /* Serial / title id from sce_sys/param.json */
|
||||
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
|
||||
QString basedir; /* Game base directory */
|
||||
QString game_path; /* Launcher-unique game path */
|
||||
bool custom_settings = false;
|
||||
GameStatus game_status = GameStatus::Unknown;
|
||||
QString game_comment;
|
||||
|
||||
Resolution screen_resolution = Resolution::R1280X720;
|
||||
int vblank_frequency = 60;
|
||||
bool vulkan_validation_enabled = true;
|
||||
bool shader_validation_enabled = true;
|
||||
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::Performance;
|
||||
ShaderLogDirection shader_log_direction = ShaderLogDirection::Silent;
|
||||
QString shader_log_folder = "_Shaders";
|
||||
bool command_buffer_dump_enabled = false;
|
||||
QString command_buffer_dump_folder = "_Buffers";
|
||||
LogDirection printf_direction = LogDirection::Silent;
|
||||
QString printf_output_file = "_kyty.txt";
|
||||
ProfilerDirection profiler_direction = ProfilerDirection::None;
|
||||
bool renderdoc_enabled = false;
|
||||
bool ngg_rectlist_draw_enabled = true;
|
||||
|
||||
QString elf = QStringLiteral("eboot.bin");
|
||||
|
||||
void CopyEmulatorSettingsFrom(const Configuration& other) {
|
||||
screen_resolution = other.screen_resolution;
|
||||
vblank_frequency = other.vblank_frequency;
|
||||
vulkan_validation_enabled = other.vulkan_validation_enabled;
|
||||
shader_validation_enabled = other.shader_validation_enabled;
|
||||
shader_optimization_type = other.shader_optimization_type;
|
||||
shader_log_direction = other.shader_log_direction;
|
||||
shader_log_folder = other.shader_log_folder;
|
||||
command_buffer_dump_enabled = other.command_buffer_dump_enabled;
|
||||
command_buffer_dump_folder = other.command_buffer_dump_folder;
|
||||
printf_direction = other.printf_direction;
|
||||
printf_output_file = other.printf_output_file;
|
||||
profiler_direction = other.profiler_direction;
|
||||
renderdoc_enabled = other.renderdoc_enabled;
|
||||
ngg_rectlist_draw_enabled = other.ngg_rectlist_draw_enabled;
|
||||
}
|
||||
|
||||
void CopyFrom(const Configuration& other) {
|
||||
name = other.name;
|
||||
title_id = other.title_id;
|
||||
firmwareVer = other.firmwareVer;
|
||||
basedir = other.basedir;
|
||||
game_path = other.game_path;
|
||||
custom_settings = other.custom_settings;
|
||||
game_status = other.game_status;
|
||||
game_comment = other.game_comment;
|
||||
CopyEmulatorSettingsFrom(other);
|
||||
elf = other.elf;
|
||||
}
|
||||
|
||||
void WriteSettings(QSettings* s) const {
|
||||
KYTY_CFG_SET(name);
|
||||
KYTY_CFG_SET(basedir);
|
||||
KYTY_CFG_SET(game_path);
|
||||
KYTY_CFG_SET(custom_settings);
|
||||
KYTY_CFG_SET(screen_resolution);
|
||||
KYTY_CFG_SET(vblank_frequency);
|
||||
KYTY_CFG_SET(vulkan_validation_enabled);
|
||||
KYTY_CFG_SET(shader_validation_enabled);
|
||||
KYTY_CFG_SET(shader_optimization_type);
|
||||
KYTY_CFG_SET(shader_log_direction);
|
||||
KYTY_CFG_SET(shader_log_folder);
|
||||
KYTY_CFG_SET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_SET(command_buffer_dump_folder);
|
||||
KYTY_CFG_SET(printf_direction);
|
||||
KYTY_CFG_SET(printf_output_file);
|
||||
KYTY_CFG_SET(profiler_direction);
|
||||
KYTY_CFG_SET(renderdoc_enabled);
|
||||
KYTY_CFG_SET(ngg_rectlist_draw_enabled);
|
||||
KYTY_CFG_SET(elf);
|
||||
}
|
||||
|
||||
void ReadSettings(QSettings* s) {
|
||||
KYTY_CFG_GET(name);
|
||||
KYTY_CFG_GET(basedir);
|
||||
KYTY_CFG_GET(game_path);
|
||||
KYTY_CFG_GET(custom_settings);
|
||||
KYTY_CFG_GET(screen_resolution);
|
||||
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
|
||||
KYTY_CFG_GET(vulkan_validation_enabled);
|
||||
KYTY_CFG_GET(shader_validation_enabled);
|
||||
KYTY_CFG_GET(shader_optimization_type);
|
||||
KYTY_CFG_GET(shader_log_direction);
|
||||
KYTY_CFG_GET(shader_log_folder);
|
||||
KYTY_CFG_GET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_GET(command_buffer_dump_folder);
|
||||
KYTY_CFG_GET(printf_direction);
|
||||
KYTY_CFG_GET(printf_output_file);
|
||||
KYTY_CFG_GET(profiler_direction);
|
||||
KYTY_CFG_GET(renderdoc_enabled);
|
||||
ngg_rectlist_draw_enabled =
|
||||
s->value("ngg_rectlist_draw_enabled", ngg_rectlist_draw_enabled).toBool();
|
||||
elf = s->value("elf", elf).toString();
|
||||
}
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Configuration*)
|
||||
|
||||
#endif /* LAUNCHER_INCLUDE_CONFIGURATION_H_ */
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef CONFIGURATION_EDIT_DIALOG_H
|
||||
#define CONFIGURATION_EDIT_DIALOG_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
class QByteArray;
|
||||
class QGroupBox;
|
||||
class QListWidget;
|
||||
class QMoveEvent;
|
||||
class QToolButton;
|
||||
class QSettings;
|
||||
class QWidget;
|
||||
|
||||
class Configuration;
|
||||
namespace Ui {
|
||||
class ConfigurationEditDialog;
|
||||
} // namespace Ui
|
||||
|
||||
class ConfigurationEditDialog: public QDialog {
|
||||
Q_OBJECT
|
||||
KYTY_QT_CLASS_NO_COPY(ConfigurationEditDialog);
|
||||
|
||||
public:
|
||||
explicit ConfigurationEditDialog(Configuration& info, QWidget* parent = nullptr);
|
||||
~ConfigurationEditDialog() override;
|
||||
|
||||
static void WriteSettings(QSettings& s);
|
||||
static void ReadSettings(QSettings& s);
|
||||
|
||||
void SetTitle(const QString& str);
|
||||
void SetGameDirectories(const QStringList& dirs);
|
||||
[[nodiscard]] QStringList GetGameDirectories() const;
|
||||
|
||||
private:
|
||||
Ui::ConfigurationEditDialog* m_ui = nullptr;
|
||||
Configuration& m_info;
|
||||
QGroupBox* m_game_dirs_group = nullptr;
|
||||
QListWidget* m_game_dirs_list = nullptr;
|
||||
QToolButton* m_remove_game_dir_button = nullptr;
|
||||
bool m_show_game_dirs = false;
|
||||
|
||||
protected:
|
||||
void Init(const Configuration& info);
|
||||
void InitGameDirectories();
|
||||
void AddGameDirectoryItem(const QString& dir);
|
||||
|
||||
void moveEvent(QMoveEvent* event) override;
|
||||
|
||||
static QByteArray g_last_geometry;
|
||||
|
||||
/*slots:*/
|
||||
|
||||
void update_info();
|
||||
void adjust_size();
|
||||
void save();
|
||||
void clear();
|
||||
void add_game_directory();
|
||||
void remove_selected_game_directories();
|
||||
void update_game_directory_buttons();
|
||||
};
|
||||
|
||||
#endif // CONFIGURATION_EDIT_DIALOG_H
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef CONFIGURATION_ITEM_H
|
||||
#define CONFIGURATION_ITEM_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QTreeWidget;
|
||||
class QWidget;
|
||||
|
||||
class Configuration;
|
||||
class ConfigurationItem: public QTreeWidgetItem {
|
||||
public:
|
||||
explicit ConfigurationItem(std::unique_ptr<Configuration> info, QTreeWidget* parent);
|
||||
~ConfigurationItem() override;
|
||||
|
||||
void Update();
|
||||
bool operator<(const QTreeWidgetItem& other) const override;
|
||||
|
||||
KYTY_QT_CLASS_NO_COPY(ConfigurationItem);
|
||||
|
||||
Configuration& GetInfo() { return *m_info; }
|
||||
[[nodiscard]] const Configuration& GetInfo() const { return *m_info; }
|
||||
// void SetInfo(Configuration* info);
|
||||
void SetRunning(bool state);
|
||||
void SetCompatibilityEditable(bool editable);
|
||||
[[nodiscard]] bool IsRunning() const { return m_running; }
|
||||
QComboBox* GetStatusCombo() { return m_status_combo; }
|
||||
QLineEdit* GetCommentEdit() { return m_comment_edit; }
|
||||
|
||||
private:
|
||||
void UpdateIcon();
|
||||
void UpdateStatusIndicator();
|
||||
|
||||
std::unique_ptr<Configuration> m_info;
|
||||
bool m_running = false;
|
||||
QComboBox* m_status_combo = nullptr;
|
||||
QLabel* m_status_indicator = nullptr;
|
||||
QWidget* m_status_widget = nullptr;
|
||||
QLineEdit* m_comment_edit = nullptr;
|
||||
};
|
||||
|
||||
#endif // CONFIGURATION_ITEM_H
|
||||
@@ -0,0 +1,86 @@
|
||||
#ifndef CONFIGURATION_LIST_WIDGET_H
|
||||
#define CONFIGURATION_LIST_WIDGET_H
|
||||
|
||||
#include "common.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QWidget>
|
||||
|
||||
class ConfigurationItem;
|
||||
class CompatibilityDatabase;
|
||||
class QTreeWidgetItem;
|
||||
class QPoint;
|
||||
class MainDialog;
|
||||
|
||||
namespace Ui {
|
||||
class ConfigurationListWidget;
|
||||
} // namespace Ui
|
||||
|
||||
class ConfigurationListWidget: public QWidget {
|
||||
Q_OBJECT
|
||||
KYTY_QT_CLASS_NO_COPY(ConfigurationListWidget);
|
||||
|
||||
public:
|
||||
explicit ConfigurationListWidget(QWidget* parent = nullptr);
|
||||
~ConfigurationListWidget() override;
|
||||
|
||||
void SetRunEnabled(bool flag) { m_run_enabled = flag; }
|
||||
[[nodiscard]] bool IsRunEnabled() const { return m_run_enabled; }
|
||||
|
||||
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
|
||||
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
|
||||
|
||||
void SetMainDialog(MainDialog* main_dialog) { m_main_dialog = main_dialog; }
|
||||
MainDialog* GetMainDialog() { return m_main_dialog; }
|
||||
|
||||
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
|
||||
[[nodiscard]] const QStringList& GetGameDirectories() const { return m_game_dirs; }
|
||||
[[nodiscard]] bool CanViewSelectedTrophies() const;
|
||||
|
||||
bool EnsureGameDirectory();
|
||||
void ScanGameDirectory();
|
||||
void ViewTrophies();
|
||||
|
||||
signals:
|
||||
|
||||
void Run();
|
||||
void Select();
|
||||
|
||||
public slots:
|
||||
void WriteSettings();
|
||||
void ReadSettings();
|
||||
|
||||
protected slots:
|
||||
|
||||
void edit_configuration();
|
||||
void delete_configuartion();
|
||||
void edit_global_settings();
|
||||
void run_configuration();
|
||||
void list_currentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
|
||||
void list_itemDoubleClicked(QTreeWidgetItem* witem, int column);
|
||||
void show_context_menu(const QPoint& pos);
|
||||
void open_game_folder();
|
||||
void remove_save_data();
|
||||
void filter_configurations(const QString& text);
|
||||
|
||||
private:
|
||||
void ClearCustomSettings(ConfigurationItem* item);
|
||||
void SelectItem(QTreeWidgetItem* witem);
|
||||
void ApplyCompatibility();
|
||||
[[nodiscard]] bool HasValidGameDirectory() const;
|
||||
|
||||
ConfigurationItem* m_selected_item = nullptr;
|
||||
bool m_run_enabled = true;
|
||||
Ui::ConfigurationListWidget* m_ui = nullptr;
|
||||
MainDialog* m_main_dialog = nullptr;
|
||||
QString m_settings_file;
|
||||
QStringList m_game_dirs;
|
||||
Configuration m_global_info;
|
||||
QMap<QString, Configuration*> m_custom_infos;
|
||||
CompatibilityDatabase* m_compatibility = nullptr;
|
||||
};
|
||||
|
||||
#endif // CONFIGURATION_LIST_WIDGET_H
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef GAME_LIST_TREE_WIDGET_H
|
||||
#define GAME_LIST_TREE_WIDGET_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QPaintEvent>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
#include <QPoint>
|
||||
#include <QResizeEvent>
|
||||
#include <QString>
|
||||
#include <QTreeWidget>
|
||||
|
||||
class GameListTreeWidget: public QTreeWidget {
|
||||
public:
|
||||
explicit GameListTreeWidget(QWidget* parent = nullptr): QTreeWidget(parent) {
|
||||
setAutoFillBackground(false);
|
||||
viewport()->setAutoFillBackground(false);
|
||||
}
|
||||
|
||||
void SetBackgroundImage(const QString& path) {
|
||||
if (m_path == path) {
|
||||
return;
|
||||
}
|
||||
m_path = path;
|
||||
m_source = QPixmap();
|
||||
if (!path.isEmpty()) {
|
||||
m_source.load(path);
|
||||
}
|
||||
UpdateScaledBackground();
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override {
|
||||
QTreeWidget::resizeEvent(event);
|
||||
UpdateScaledBackground();
|
||||
}
|
||||
|
||||
void paintEvent(QPaintEvent* event) override {
|
||||
{
|
||||
QPainter painter(viewport());
|
||||
if (m_scaled.isNull()) {
|
||||
painter.fillRect(event->rect(), palette().brush(QPalette::Base));
|
||||
} else {
|
||||
painter.drawPixmap(m_scaled_pos, m_scaled);
|
||||
painter.fillRect(event->rect(), QColor(0, 0, 0, 115));
|
||||
}
|
||||
}
|
||||
|
||||
QTreeWidget::paintEvent(event);
|
||||
}
|
||||
|
||||
private:
|
||||
void UpdateScaledBackground() {
|
||||
m_scaled = QPixmap();
|
||||
if (m_source.isNull() || viewport()->size().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_scaled = m_source.scaled(viewport()->size(), Qt::KeepAspectRatioByExpanding,
|
||||
Qt::FastTransformation);
|
||||
m_scaled_pos = QPoint((viewport()->width() - m_scaled.width()) / 2,
|
||||
(viewport()->height() - m_scaled.height()) / 2);
|
||||
}
|
||||
|
||||
QString m_path;
|
||||
QPixmap m_source;
|
||||
QPixmap m_scaled;
|
||||
QPoint m_scaled_pos;
|
||||
};
|
||||
|
||||
#endif // GAME_LIST_TREE_WIDGET_H
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef MAIN_DIALOG_H
|
||||
#define MAIN_DIALOG_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
class QWidget;
|
||||
class QProcess;
|
||||
class MainDialogPrivate;
|
||||
class QSettings;
|
||||
class QResizeEvent;
|
||||
|
||||
class Configuration;
|
||||
class MainDialog: public QDialog {
|
||||
Q_OBJECT
|
||||
KYTY_QT_CLASS_NO_COPY(MainDialog);
|
||||
|
||||
signals:
|
||||
void Start();
|
||||
void Quit();
|
||||
void Resize();
|
||||
|
||||
public:
|
||||
explicit MainDialog(QWidget* parent = nullptr);
|
||||
~MainDialog() override = default;
|
||||
|
||||
void RunInterpreter(QProcess* process, const Configuration& info);
|
||||
|
||||
static void WriteSettings(QSettings& s);
|
||||
static void ReadSettings(QSettings& s);
|
||||
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
private:
|
||||
MainDialogPrivate* m_p = nullptr;
|
||||
};
|
||||
|
||||
#endif // MAIN_DIALOG_H
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef MANDATORY_LINEEDIT_H
|
||||
#define MANDATORY_LINEEDIT_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QString>
|
||||
|
||||
class QPaintEvent;
|
||||
class QWidget;
|
||||
|
||||
constexpr char MANDATORY_AND_EMPTY[] = "mandatory_and_empty";
|
||||
|
||||
class MandatoryLineEdit: public QLineEdit {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MandatoryLineEdit(QWidget* parent = nullptr);
|
||||
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
static bool FindEmpty(QObject* p);
|
||||
|
||||
protected slots:
|
||||
|
||||
void changed(const QString& text);
|
||||
};
|
||||
|
||||
#endif // MANDATORY_LINEEDIT_H
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef TROPHY_VIEWER_DIALOG_H
|
||||
#define TROPHY_VIEWER_DIALOG_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QTabWidget;
|
||||
class QWidget;
|
||||
|
||||
class Configuration;
|
||||
class TrophyViewerDialog: public QDialog {
|
||||
KYTY_QT_CLASS_NO_COPY(TrophyViewerDialog);
|
||||
|
||||
public:
|
||||
explicit TrophyViewerDialog(QWidget* parent = nullptr);
|
||||
|
||||
static bool HasTrophyData(const Configuration* info);
|
||||
static void ShowForGame(const Configuration* info, QWidget* parent);
|
||||
|
||||
private:
|
||||
bool LoadGame(const Configuration& info, QString& error);
|
||||
|
||||
QTabWidget* m_tabs = nullptr;
|
||||
};
|
||||
|
||||
#endif // TROPHY_VIEWER_DIALOG_H
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "compatibilityDatabase.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QFutureWatcher>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSaveFile>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char FILE_NAME[] = "compatibility_db.json";
|
||||
constexpr char URL[] =
|
||||
"https://github.com/Nmzik/KytyPS5/releases/download/compat-db/compatibility_db.json";
|
||||
constexpr int RETRY_COUNT = 3;
|
||||
constexpr int RETRY_DELAY_MS = 750;
|
||||
|
||||
struct LoadResult {
|
||||
QMap<QString, CompatibilityEntry> entries;
|
||||
QString error;
|
||||
};
|
||||
|
||||
QString TitleKey(const QString& title_id) {
|
||||
return title_id.trimmed().toUpper();
|
||||
}
|
||||
|
||||
Configuration::GameStatus StatusFromText(const QString& text) {
|
||||
const auto value = text.trimmed();
|
||||
if (value == QStringLiteral("InGame") || value == QStringLiteral("In game")) {
|
||||
return Configuration::GameStatus::InGame;
|
||||
}
|
||||
if (value == QStringLiteral("MainMenu") || value == QStringLiteral("Main menu")) {
|
||||
return Configuration::GameStatus::MainMenu;
|
||||
}
|
||||
if (value == QStringLiteral("Logo")) {
|
||||
return Configuration::GameStatus::Logo;
|
||||
}
|
||||
if (value == QStringLiteral("DoesntBoot") || value == QStringLiteral("Doesn't boot")) {
|
||||
return Configuration::GameStatus::DoesntBoot;
|
||||
}
|
||||
return Configuration::GameStatus::Unknown;
|
||||
}
|
||||
|
||||
QString StatusToText(Configuration::GameStatus status) {
|
||||
switch (status) {
|
||||
case Configuration::GameStatus::InGame: return QStringLiteral("InGame");
|
||||
case Configuration::GameStatus::MainMenu: return QStringLiteral("MainMenu");
|
||||
case Configuration::GameStatus::Logo: return QStringLiteral("Logo");
|
||||
case Configuration::GameStatus::DoesntBoot: return QStringLiteral("DoesntBoot");
|
||||
case Configuration::GameStatus::Unknown: return QStringLiteral("Unknown");
|
||||
}
|
||||
return QStringLiteral("Unknown");
|
||||
}
|
||||
|
||||
LoadResult Parse(const QByteArray& data) {
|
||||
LoadResult ret;
|
||||
QJsonParseError parse_error;
|
||||
const auto doc = QJsonDocument::fromJson(data, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
ret.error = QStringLiteral("Invalid compatibility JSON: %1").arg(parse_error.errorString());
|
||||
return ret;
|
||||
}
|
||||
|
||||
const auto root = doc.object();
|
||||
for (auto it = root.constBegin(); it != root.constEnd(); ++it) {
|
||||
const auto title_id = TitleKey(it.key());
|
||||
const auto object = it.value().toObject();
|
||||
if (title_id.isEmpty() || object.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ret.entries.insert(title_id,
|
||||
{StatusFromText(object.value(QStringLiteral("status")).toString()),
|
||||
object.value(QStringLiteral("comment")).toString()});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
LoadResult DownloadOnce() {
|
||||
QNetworkAccessManager manager;
|
||||
QNetworkRequest request(QUrl(QString::fromLatin1(URL)));
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setRawHeader("User-Agent", "Kyty-Launcher");
|
||||
|
||||
auto* reply = manager.get(request);
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&timeout, &QTimer::timeout, reply, &QNetworkReply::abort);
|
||||
timeout.start(15000);
|
||||
loop.exec();
|
||||
|
||||
if (!timeout.isActive()) {
|
||||
return {{}, QStringLiteral("Compatibility download timed out")};
|
||||
}
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
return {{}, reply->errorString()};
|
||||
}
|
||||
return Parse(reply->readAll());
|
||||
}
|
||||
|
||||
LoadResult Download() {
|
||||
auto result = DownloadOnce();
|
||||
for (int retry = 1; retry <= RETRY_COUNT && !result.error.isEmpty(); retry++) {
|
||||
QThread::msleep(static_cast<unsigned long>(RETRY_DELAY_MS * retry));
|
||||
result = DownloadOnce();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CompatibilityDatabase::CompatibilityDatabase(bool local, QObject* parent)
|
||||
: QObject(parent), m_local(local) {}
|
||||
|
||||
const CompatibilityEntry* CompatibilityDatabase::Find(const QString& title_id) const {
|
||||
const auto entry = m_entries.constFind(TitleKey(title_id));
|
||||
return entry != m_entries.constEnd() ? &entry.value() : nullptr;
|
||||
}
|
||||
|
||||
void CompatibilityDatabase::Load() {
|
||||
if (m_local) {
|
||||
QFile file(QDir(".").absoluteFilePath(FILE_NAME));
|
||||
if (!file.exists()) {
|
||||
emit Updated();
|
||||
return;
|
||||
}
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "Could not open compatibility database:" << file.errorString();
|
||||
return;
|
||||
}
|
||||
auto result = Parse(file.readAll());
|
||||
if (!result.error.isEmpty()) {
|
||||
qWarning() << result.error;
|
||||
return;
|
||||
}
|
||||
m_entries = std::move(result.entries);
|
||||
emit Updated();
|
||||
return;
|
||||
}
|
||||
|
||||
auto* watcher = new QFutureWatcher<LoadResult>(this);
|
||||
connect(watcher, &QFutureWatcher<LoadResult>::finished, this, [this, watcher]() {
|
||||
const auto result = watcher->result();
|
||||
watcher->deleteLater();
|
||||
if (!result.error.isEmpty()) {
|
||||
qWarning() << "Could not refresh compatibility database:" << result.error;
|
||||
return;
|
||||
}
|
||||
m_entries = result.entries;
|
||||
emit Updated();
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run(Download));
|
||||
}
|
||||
|
||||
void CompatibilityDatabase::SetStatus(const QString& title_id, Configuration::GameStatus status) {
|
||||
const auto key = TitleKey(title_id);
|
||||
if (!m_local || key.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
m_entries[key].status = status;
|
||||
Save();
|
||||
}
|
||||
|
||||
void CompatibilityDatabase::SetComment(const QString& title_id, const QString& comment) {
|
||||
const auto key = TitleKey(title_id);
|
||||
if (!m_local || key.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
m_entries[key].comment = comment;
|
||||
Save();
|
||||
}
|
||||
|
||||
void CompatibilityDatabase::Save() const {
|
||||
QJsonObject root;
|
||||
for (auto it = m_entries.constBegin(); it != m_entries.constEnd(); ++it) {
|
||||
root.insert(it.key(),
|
||||
QJsonObject {{QStringLiteral("status"), StatusToText(it.value().status)},
|
||||
{QStringLiteral("comment"), it.value().comment}});
|
||||
}
|
||||
|
||||
QSaveFile file(QDir(".").absoluteFilePath(FILE_NAME));
|
||||
const auto data = QJsonDocument(root).toJson(QJsonDocument::Indented);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) {
|
||||
qWarning() << "Could not save compatibility database:" << file.errorString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
#include "configurationEditDialog.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#include "mandatoryLineEdit.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLayout>
|
||||
#include <QListView>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSpinBox>
|
||||
#include <QStyle>
|
||||
#include <QToolButton>
|
||||
#include <QTreeView>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtAlgorithms>
|
||||
|
||||
#include "ui_configuration_edit_dialog.h"
|
||||
|
||||
constexpr char SETTINGS_CFG_DIALOG[] = "ConfigurationEditDialog";
|
||||
constexpr char SETTINGS_CFG_LAST_GEOMETRY[] = "geometry";
|
||||
constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 560;
|
||||
|
||||
static QString NormalizeGameDirectory(const QString& dir) {
|
||||
const auto trimmed = dir.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return QDir::cleanPath(QDir(trimmed).absolutePath());
|
||||
}
|
||||
|
||||
static QString GameDirectoryKey(const QString& dir) {
|
||||
const auto normalized = NormalizeGameDirectory(dir);
|
||||
if (normalized.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto canonical = QFileInfo(normalized).canonicalFilePath();
|
||||
if (canonical.isEmpty()) {
|
||||
canonical = normalized;
|
||||
}
|
||||
canonical = QDir::cleanPath(canonical);
|
||||
|
||||
#ifdef __linux__
|
||||
return canonical;
|
||||
#else
|
||||
return canonical.toCaseFolded();
|
||||
#endif
|
||||
}
|
||||
|
||||
ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* parent)
|
||||
: QDialog(parent, Qt::WindowCloseButtonHint), m_ui(new Ui::ConfigurationEditDialog),
|
||||
m_info(info) {
|
||||
m_ui->setupUi(this);
|
||||
InitGameDirectories();
|
||||
|
||||
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
|
||||
connect(m_ui->clear_button, &QPushButton::clicked, this, &ConfigurationEditDialog::clear);
|
||||
connect(m_ui->comboBox_shader_log_direction, &QComboBox::currentTextChanged, this,
|
||||
[this](const QString& text) {
|
||||
auto log = TextToEnum<Configuration::ShaderLogDirection>(text);
|
||||
m_ui->lineEdit_shader_log_folder->setEnabled(
|
||||
log == Configuration::ShaderLogDirection::File);
|
||||
});
|
||||
connect(m_ui->checkBox_cmd_dump, &QCheckBox::toggled, this,
|
||||
[this](bool flag) { m_ui->lineEdit_cmd_dump_folder->setEnabled(flag); });
|
||||
connect(m_ui->comboBox_printf_direction, &QComboBox::currentTextChanged, this,
|
||||
[this](const QString& text) {
|
||||
auto log = TextToEnum<Configuration::LogDirection>(text);
|
||||
m_ui->lineEdit_printf_file->setEnabled(log == Configuration::LogDirection::File);
|
||||
});
|
||||
|
||||
layout()->setSizeConstraint(QLayout::SetFixedSize);
|
||||
|
||||
restoreGeometry(g_last_geometry);
|
||||
|
||||
Init(info);
|
||||
}
|
||||
|
||||
QByteArray ConfigurationEditDialog::g_last_geometry;
|
||||
|
||||
ConfigurationEditDialog::~ConfigurationEditDialog() {
|
||||
delete m_ui;
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::WriteSettings(QSettings& s) {
|
||||
s.beginGroup(SETTINGS_CFG_DIALOG);
|
||||
|
||||
if (!g_last_geometry.isEmpty()) {
|
||||
s.setValue(SETTINGS_CFG_LAST_GEOMETRY, g_last_geometry);
|
||||
}
|
||||
|
||||
s.endGroup();
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::ReadSettings(QSettings& s) {
|
||||
s.beginGroup(SETTINGS_CFG_DIALOG);
|
||||
|
||||
g_last_geometry = s.value(SETTINGS_CFG_LAST_GEOMETRY, g_last_geometry).toByteArray();
|
||||
|
||||
s.endGroup();
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static void ListInit(QComboBox* combo, T value) {
|
||||
combo->clear();
|
||||
combo->addItems(EnumToList<T>());
|
||||
combo->setCurrentText(EnumToText(value));
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution);
|
||||
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
|
||||
m_ui->checkBox_shader_validation->setChecked(info.shader_validation_enabled);
|
||||
m_ui->checkBox_vulkan_validation->setChecked(info.vulkan_validation_enabled);
|
||||
m_ui->checkBox_renderdoc_capture->setChecked(info.renderdoc_enabled);
|
||||
m_ui->checkBox_ngg_rectlist_draw->setChecked(info.ngg_rectlist_draw_enabled);
|
||||
ListInit(m_ui->comboBox_shader_optimization_type, info.shader_optimization_type);
|
||||
ListInit(m_ui->comboBox_shader_log_direction, info.shader_log_direction);
|
||||
m_ui->lineEdit_shader_log_folder->setText(info.shader_log_folder);
|
||||
m_ui->lineEdit_shader_log_folder->setEnabled(info.shader_log_direction ==
|
||||
Configuration::ShaderLogDirection::File);
|
||||
m_ui->checkBox_cmd_dump->setChecked(info.command_buffer_dump_enabled);
|
||||
m_ui->lineEdit_cmd_dump_folder->setText(info.command_buffer_dump_folder);
|
||||
m_ui->lineEdit_cmd_dump_folder->setEnabled(info.command_buffer_dump_enabled);
|
||||
ListInit(m_ui->comboBox_printf_direction, info.printf_direction);
|
||||
m_ui->lineEdit_printf_file->setText(info.printf_output_file);
|
||||
m_ui->lineEdit_printf_file->setEnabled(info.printf_direction ==
|
||||
Configuration::LogDirection::File);
|
||||
ListInit(m_ui->comboBox_profiler_direction, info.profiler_direction);
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::InitGameDirectories() {
|
||||
m_game_dirs_group = new QGroupBox(tr("Game folders"), this);
|
||||
|
||||
auto* group_layout = new QVBoxLayout(m_game_dirs_group);
|
||||
group_layout->setContentsMargins(8, 8, 8, 8);
|
||||
group_layout->setSpacing(6);
|
||||
|
||||
m_game_dirs_list = new QListWidget(m_game_dirs_group);
|
||||
m_game_dirs_list->setMinimumHeight(120);
|
||||
m_game_dirs_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
group_layout->addWidget(m_game_dirs_list);
|
||||
|
||||
auto* button_layout = new QHBoxLayout;
|
||||
button_layout->setContentsMargins(0, 0, 0, 0);
|
||||
button_layout->setSpacing(4);
|
||||
|
||||
auto* add_button = new QToolButton(m_game_dirs_group);
|
||||
add_button->setIcon(style()->standardIcon(QStyle::SP_DirOpenIcon));
|
||||
add_button->setToolTip(tr("Add game folder"));
|
||||
add_button->setAutoRaise(true);
|
||||
button_layout->addWidget(add_button);
|
||||
|
||||
m_remove_game_dir_button = new QToolButton(m_game_dirs_group);
|
||||
m_remove_game_dir_button->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton));
|
||||
m_remove_game_dir_button->setToolTip(tr("Remove selected game folders"));
|
||||
m_remove_game_dir_button->setAutoRaise(true);
|
||||
button_layout->addWidget(m_remove_game_dir_button);
|
||||
|
||||
button_layout->addStretch(1);
|
||||
group_layout->addLayout(button_layout);
|
||||
|
||||
m_ui->gridLayout->addWidget(m_game_dirs_group, 1, 0, 1, 1);
|
||||
m_game_dirs_group->setVisible(false);
|
||||
|
||||
connect(add_button, &QToolButton::clicked, this, &ConfigurationEditDialog::add_game_directory);
|
||||
connect(m_remove_game_dir_button, &QToolButton::clicked, this,
|
||||
&ConfigurationEditDialog::remove_selected_game_directories);
|
||||
connect(m_game_dirs_list, &QListWidget::itemSelectionChanged, this,
|
||||
&ConfigurationEditDialog::update_game_directory_buttons);
|
||||
|
||||
update_game_directory_buttons();
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::SetTitle(const QString& str) {
|
||||
setWindowTitle(str);
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) {
|
||||
m_show_game_dirs = true;
|
||||
m_game_dirs_list->clear();
|
||||
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
|
||||
|
||||
for (const auto& dir: dirs) {
|
||||
AddGameDirectoryItem(dir);
|
||||
}
|
||||
|
||||
m_game_dirs_group->setVisible(true);
|
||||
update_game_directory_buttons();
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
QStringList ConfigurationEditDialog::GetGameDirectories() const {
|
||||
QStringList dirs;
|
||||
if (!m_show_game_dirs) {
|
||||
return dirs;
|
||||
}
|
||||
|
||||
for (int index = 0; index < m_game_dirs_list->count(); index++) {
|
||||
auto* item = m_game_dirs_list->item(index);
|
||||
dirs.append(item->text());
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::AddGameDirectoryItem(const QString& dir) {
|
||||
const auto normalized = NormalizeGameDirectory(dir);
|
||||
const auto key = GameDirectoryKey(normalized);
|
||||
if (normalized.isEmpty() || key.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int index = 0; index < m_game_dirs_list->count(); index++) {
|
||||
auto* item = m_game_dirs_list->item(index);
|
||||
if (GameDirectoryKey(item->text()) == key) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto* item = new QListWidgetItem(normalized, m_game_dirs_list);
|
||||
item->setToolTip(normalized);
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::moveEvent(QMoveEvent* event) {
|
||||
QDialog::moveEvent(event);
|
||||
g_last_geometry = saveGeometry();
|
||||
}
|
||||
|
||||
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
|
||||
info.screen_resolution =
|
||||
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
|
||||
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
|
||||
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
|
||||
info.shader_validation_enabled = ui.checkBox_shader_validation->isChecked();
|
||||
info.renderdoc_enabled = ui.checkBox_renderdoc_capture->isChecked();
|
||||
info.ngg_rectlist_draw_enabled = ui.checkBox_ngg_rectlist_draw->isChecked();
|
||||
info.shader_optimization_type = TextToEnum<Configuration::ShaderOptimizationType>(
|
||||
ui.comboBox_shader_optimization_type->currentText());
|
||||
info.shader_log_direction = TextToEnum<Configuration::ShaderLogDirection>(
|
||||
ui.comboBox_shader_log_direction->currentText());
|
||||
info.shader_log_folder = ui.lineEdit_shader_log_folder->text();
|
||||
info.command_buffer_dump_enabled = ui.checkBox_cmd_dump->isChecked();
|
||||
info.command_buffer_dump_folder = ui.lineEdit_cmd_dump_folder->text();
|
||||
info.printf_direction =
|
||||
TextToEnum<Configuration::LogDirection>(ui.comboBox_printf_direction->currentText());
|
||||
info.printf_output_file = ui.lineEdit_printf_file->text();
|
||||
info.profiler_direction =
|
||||
TextToEnum<Configuration::ProfilerDirection>(ui.comboBox_profiler_direction->currentText());
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::update_info() {
|
||||
UpdateInfo(m_info, *m_ui);
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::adjust_size() {
|
||||
this->adjustSize();
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::save() {
|
||||
if (MandatoryLineEdit::FindEmpty(this)) {
|
||||
QMessageBox::critical(this, tr("Save failed"), tr("Please fill all mandatory fields"));
|
||||
return;
|
||||
}
|
||||
|
||||
update_info();
|
||||
|
||||
emit accept();
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::clear() {
|
||||
Configuration default_info;
|
||||
Init(default_info);
|
||||
|
||||
if (m_show_game_dirs) {
|
||||
m_game_dirs_list->clear();
|
||||
update_game_directory_buttons();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::add_game_directory() {
|
||||
QString start_dir = QDir::homePath();
|
||||
if (m_game_dirs_list->count() > 0) {
|
||||
start_dir = m_game_dirs_list->item(m_game_dirs_list->count() - 1)->text();
|
||||
}
|
||||
|
||||
QFileDialog dialog(this, tr("Select game folders"), start_dir);
|
||||
dialog.setFileMode(QFileDialog::Directory);
|
||||
dialog.setOption(QFileDialog::ShowDirsOnly, true);
|
||||
dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
dialog.setLabelText(QFileDialog::Accept, tr("Add"));
|
||||
|
||||
auto* list_view = dialog.findChild<QListView*>(QStringLiteral("listView"));
|
||||
if (list_view != nullptr) {
|
||||
list_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
auto* tree_view = dialog.findChild<QTreeView*>(QStringLiteral("treeView"));
|
||||
if (tree_view != nullptr) {
|
||||
tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& dir: dialog.selectedFiles()) {
|
||||
AddGameDirectoryItem(dir);
|
||||
}
|
||||
|
||||
update_game_directory_buttons();
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::remove_selected_game_directories() {
|
||||
qDeleteAll(m_game_dirs_list->selectedItems());
|
||||
update_game_directory_buttons();
|
||||
}
|
||||
|
||||
void ConfigurationEditDialog::update_game_directory_buttons() {
|
||||
m_remove_game_dir_button->setEnabled(!m_game_dirs_list->selectedItems().isEmpty());
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
#include "configurationItem.h"
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QSize>
|
||||
#include <QStringList>
|
||||
#include <QStyle>
|
||||
#include <QTreeWidget>
|
||||
#include <QVersionNumber>
|
||||
#include <QWheelEvent>
|
||||
#include <QWidget>
|
||||
|
||||
namespace {
|
||||
|
||||
enum Column {
|
||||
NameColumn,
|
||||
SerialColumn,
|
||||
FirmwareVersionColumn,
|
||||
PathColumn,
|
||||
StatusColumn,
|
||||
CommentsColumn,
|
||||
};
|
||||
|
||||
class NoWheelComboBox: public QComboBox {
|
||||
public:
|
||||
explicit NoWheelComboBox(QWidget* parent = nullptr): QComboBox(parent) {}
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent* event) override { event->ignore(); }
|
||||
};
|
||||
|
||||
QString GetStatusText(Configuration::GameStatus status) {
|
||||
switch (status) {
|
||||
case Configuration::GameStatus::Unknown: return QStringLiteral("Unknown");
|
||||
case Configuration::GameStatus::MainMenu: return QStringLiteral("Main menu");
|
||||
case Configuration::GameStatus::InGame: return QStringLiteral("In game");
|
||||
case Configuration::GameStatus::Logo: return QStringLiteral("Logo");
|
||||
case Configuration::GameStatus::DoesntBoot: return QStringLiteral("Doesn't boot");
|
||||
}
|
||||
|
||||
return QStringLiteral("Unknown");
|
||||
}
|
||||
|
||||
QString GetStatusColor(Configuration::GameStatus status) {
|
||||
switch (status) {
|
||||
case Configuration::GameStatus::InGame: return QStringLiteral("#2fb344");
|
||||
case Configuration::GameStatus::MainMenu: return QStringLiteral("#2f80ed");
|
||||
case Configuration::GameStatus::Logo: return QStringLiteral("#f2c94c");
|
||||
case Configuration::GameStatus::DoesntBoot: return QStringLiteral("#e55353");
|
||||
case Configuration::GameStatus::Unknown: return QStringLiteral("#8a8a8a");
|
||||
}
|
||||
|
||||
return QStringLiteral("#8a8a8a");
|
||||
}
|
||||
|
||||
void AddStatus(QComboBox* combo, Configuration::GameStatus status) {
|
||||
combo->addItem(GetStatusText(status), static_cast<int>(status));
|
||||
}
|
||||
|
||||
void SetStatus(QComboBox* combo, Configuration::GameStatus status) {
|
||||
const int index = combo->findData(static_cast<int>(status));
|
||||
combo->setCurrentIndex(index >= 0 ? index : 0);
|
||||
}
|
||||
|
||||
QIcon StandardIcon(QStyle::StandardPixmap icon) {
|
||||
return QApplication::style()->standardIcon(icon);
|
||||
}
|
||||
|
||||
QString GetPathText(const Configuration& info) {
|
||||
return !info.game_path.isEmpty() ? info.game_path : info.basedir;
|
||||
}
|
||||
|
||||
QString GetDisplayText(const Configuration& info) {
|
||||
QStringList lines({info.name});
|
||||
|
||||
if (!info.title_id.isEmpty()) {
|
||||
lines.append(QStringLiteral("Serial ID: %1").arg(info.title_id));
|
||||
}
|
||||
if (!info.firmwareVer.isEmpty()) {
|
||||
lines.append(QStringLiteral("Firmware version: %1").arg(info.firmwareVer));
|
||||
}
|
||||
|
||||
const auto path = GetPathText(info);
|
||||
if (!path.isEmpty()) {
|
||||
lines.append(QStringLiteral("Path: %1").arg(path));
|
||||
}
|
||||
if (!info.game_comment.isEmpty()) {
|
||||
lines.append(QStringLiteral("Comment: %1").arg(info.game_comment));
|
||||
}
|
||||
|
||||
return lines.join(QLatin1Char('\n'));
|
||||
}
|
||||
|
||||
QString GetSortText(const Configuration& info) {
|
||||
return QStringLiteral("%1\n%2").arg(info.name.toCaseFolded(), info.game_path.toCaseFolded());
|
||||
}
|
||||
|
||||
void MakeTransparent(QWidget* widget) {
|
||||
widget->setAutoFillBackground(false);
|
||||
widget->setAttribute(Qt::WA_NoSystemBackground);
|
||||
widget->setAttribute(Qt::WA_TranslucentBackground);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeWidget* parent)
|
||||
: QTreeWidgetItem(parent), m_info(std::move(info)) {
|
||||
setSizeHint(NameColumn, QSize(0, 72));
|
||||
|
||||
m_status_widget = new QWidget(parent);
|
||||
MakeTransparent(m_status_widget);
|
||||
auto* layout = new QHBoxLayout(m_status_widget);
|
||||
layout->setContentsMargins(4, 0, 4, 0);
|
||||
layout->setSpacing(6);
|
||||
|
||||
m_status_indicator = new QLabel(m_status_widget);
|
||||
m_status_indicator->setFixedSize(QSize(12, 12));
|
||||
layout->addWidget(m_status_indicator);
|
||||
|
||||
m_status_combo = new NoWheelComboBox(m_status_widget);
|
||||
MakeTransparent(m_status_combo);
|
||||
AddStatus(m_status_combo, Configuration::GameStatus::Unknown);
|
||||
AddStatus(m_status_combo, Configuration::GameStatus::MainMenu);
|
||||
AddStatus(m_status_combo, Configuration::GameStatus::InGame);
|
||||
AddStatus(m_status_combo, Configuration::GameStatus::Logo);
|
||||
AddStatus(m_status_combo, Configuration::GameStatus::DoesntBoot);
|
||||
m_status_combo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
m_status_combo->setFixedWidth(125);
|
||||
m_status_combo->setStyleSheet(QStringLiteral(
|
||||
"QComboBox { background: transparent; border: 0; color: #eef4ff; padding-left: 2px; }"
|
||||
"QComboBox:focus { background: rgba(255,255,255,35); border-radius: 3px; }"
|
||||
"QComboBox::drop-down { border: 0; }"
|
||||
"QComboBox QAbstractItemView { background: #181c24; color: #eef4ff; "
|
||||
"selection-background-color: #267bd8; }"));
|
||||
layout->addWidget(m_status_combo);
|
||||
layout->addStretch(1);
|
||||
|
||||
parent->setItemWidget(this, StatusColumn, m_status_widget);
|
||||
|
||||
m_comment_edit = new QLineEdit(parent);
|
||||
MakeTransparent(m_comment_edit);
|
||||
m_comment_edit->setClearButtonEnabled(true);
|
||||
m_comment_edit->setFrame(false);
|
||||
m_comment_edit->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: transparent; border: 0; color: #eef4ff; padding-left: 2px; "
|
||||
"selection-background-color: #267bd8; }"
|
||||
"QLineEdit:focus { background: rgba(255,255,255,35); "
|
||||
"border: 1px solid rgba(255,255,255,90); border-radius: 3px; }"));
|
||||
parent->setItemWidget(this, CommentsColumn, m_comment_edit);
|
||||
|
||||
Update();
|
||||
SetRunning(false);
|
||||
}
|
||||
|
||||
ConfigurationItem::~ConfigurationItem() = default;
|
||||
|
||||
void ConfigurationItem::Update() {
|
||||
const auto display_text = GetDisplayText(*m_info);
|
||||
const auto path = GetPathText(*m_info);
|
||||
|
||||
setText(NameColumn, m_info->name);
|
||||
setText(SerialColumn, m_info->title_id);
|
||||
setText(FirmwareVersionColumn,
|
||||
m_info->firmwareVer.isEmpty() ? QStringLiteral("\u2014") : m_info->firmwareVer);
|
||||
setText(PathColumn, path);
|
||||
setText(StatusColumn, {});
|
||||
setText(CommentsColumn, {});
|
||||
for (int column = NameColumn; column <= CommentsColumn; column++) {
|
||||
setToolTip(column, display_text);
|
||||
}
|
||||
SetStatus(m_status_combo, m_info->game_status);
|
||||
if (m_comment_edit->text() != m_info->game_comment) {
|
||||
m_comment_edit->setText(m_info->game_comment);
|
||||
}
|
||||
|
||||
UpdateIcon();
|
||||
UpdateStatusIndicator();
|
||||
}
|
||||
|
||||
bool ConfigurationItem::operator<(const QTreeWidgetItem& other) const {
|
||||
const auto* other_item = dynamic_cast<const ConfigurationItem*>(&other);
|
||||
if (other_item == nullptr) {
|
||||
return QTreeWidgetItem::operator<(other);
|
||||
}
|
||||
|
||||
const int column = treeWidget() != nullptr ? treeWidget()->sortColumn() : NameColumn;
|
||||
switch (column) {
|
||||
case NameColumn: return GetSortText(*m_info) < GetSortText(*other_item->m_info);
|
||||
case StatusColumn:
|
||||
return GetStatusText(m_info->game_status) <
|
||||
GetStatusText(other_item->m_info->game_status);
|
||||
case FirmwareVersionColumn: {
|
||||
const auto& version = m_info->firmwareVer;
|
||||
const auto& other_version = other_item->m_info->firmwareVer;
|
||||
if (version.isEmpty() || other_version.isEmpty()) {
|
||||
return version.isEmpty() && !other_version.isEmpty();
|
||||
}
|
||||
return QVersionNumber::compare(QVersionNumber::fromString(version),
|
||||
QVersionNumber::fromString(other_version)) < 0;
|
||||
}
|
||||
case CommentsColumn:
|
||||
return m_info->game_comment.toCaseFolded() <
|
||||
other_item->m_info->game_comment.toCaseFolded();
|
||||
default: return text(column).toCaseFolded() < other.text(column).toCaseFolded();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationItem::SetRunning(bool state) {
|
||||
m_running = state;
|
||||
|
||||
QFont f = font(NameColumn);
|
||||
f.setBold(state);
|
||||
for (int column = NameColumn; column <= CommentsColumn; column++) {
|
||||
setFont(column, f);
|
||||
}
|
||||
|
||||
UpdateIcon();
|
||||
}
|
||||
|
||||
void ConfigurationItem::SetCompatibilityEditable(bool editable) {
|
||||
m_status_combo->setEnabled(editable);
|
||||
m_comment_edit->setReadOnly(!editable);
|
||||
m_comment_edit->setClearButtonEnabled(editable);
|
||||
}
|
||||
|
||||
void ConfigurationItem::UpdateIcon() {
|
||||
const QString icon_file = QDir(m_info->basedir).filePath(QStringLiteral("sce_sys/icon0.png"));
|
||||
if (QFileInfo::exists(icon_file)) {
|
||||
setIcon(NameColumn, QIcon(icon_file));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_running) {
|
||||
setIcon(NameColumn, StandardIcon(QStyle::SP_MediaPlay));
|
||||
} else if (m_info->custom_settings) {
|
||||
setIcon(NameColumn, StandardIcon(QStyle::SP_FileIcon));
|
||||
} else {
|
||||
setIcon(NameColumn, StandardIcon(QStyle::SP_ComputerIcon));
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationItem::UpdateStatusIndicator() {
|
||||
m_status_indicator->setStyleSheet(
|
||||
QStringLiteral(
|
||||
"background-color: %1; border: 1px solid rgba(255, 255, 255, 90); border-radius: 6px;")
|
||||
.arg(GetStatusColor(m_info->game_status)));
|
||||
m_status_indicator->setToolTip(GetStatusText(m_info->game_status));
|
||||
}
|
||||
@@ -0,0 +1,908 @@
|
||||
#include "configurationListWidget.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "compatibilityDatabase.h"
|
||||
#include "configuration.h"
|
||||
#include "configurationEditDialog.h"
|
||||
#include "configurationItem.h"
|
||||
#include "gameListTreeWidget.h"
|
||||
#include "mainDialog.h"
|
||||
#include "trophyViewerDialog.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemView>
|
||||
#include <QAction>
|
||||
#include <QComboBox>
|
||||
#include <QCoreApplication>
|
||||
#include <QCursor>
|
||||
#include <QDesktopServices>
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
#include <QStyle>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QUrl>
|
||||
#include <QtCore>
|
||||
|
||||
#include "ui_configuration_list_widget.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
constexpr char CONF_FILE_NAME[] = "Kyty.ini";
|
||||
constexpr char CONF_ORG_NAME[] = "Kyty";
|
||||
constexpr char CONF_APP_NAME[] = "Kyty";
|
||||
constexpr char CONF_SECTION_NAME[] = "GameConfigurations";
|
||||
constexpr char CONF_LAUNCHER[] = "Launcher";
|
||||
constexpr char CONF_GAME_DIR[] = "game_dir";
|
||||
constexpr char CONF_GAME_DIRS[] = "game_dirs";
|
||||
constexpr char CONF_GLOBAL[] = "GlobalConfiguration";
|
||||
constexpr char SAVE_DATA_DIR[] = "_SaveData";
|
||||
|
||||
constexpr int GAME_NAME_COLUMN = 0;
|
||||
constexpr int GAME_SERIAL_COLUMN = 1;
|
||||
constexpr int GAME_FIRMWARE_VERSION_COLUMN = 2;
|
||||
constexpr int GAME_PATH_COLUMN = 3;
|
||||
constexpr int GAME_STATUS_COLUMN = 4;
|
||||
constexpr int GAME_COMMENT_COLUMN = 5;
|
||||
|
||||
static QString NormalizeGameDirectory(const QString& dir) {
|
||||
const auto trimmed = dir.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return QDir::cleanPath(QDir(trimmed).absolutePath());
|
||||
}
|
||||
|
||||
static QString PathKey(const QString& path) {
|
||||
auto normalized = NormalizeGameDirectory(path);
|
||||
if (normalized.isEmpty()) {
|
||||
normalized = QDir::cleanPath(path.trimmed());
|
||||
}
|
||||
if (normalized.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto canonical = QFileInfo(normalized).canonicalFilePath();
|
||||
if (canonical.isEmpty()) {
|
||||
canonical = normalized;
|
||||
}
|
||||
canonical = QDir::cleanPath(canonical);
|
||||
|
||||
#ifdef __linux__
|
||||
return canonical;
|
||||
#else
|
||||
return canonical.toCaseFolded();
|
||||
#endif
|
||||
}
|
||||
|
||||
static QStringList NormalizeGameDirectories(const QStringList& dirs) {
|
||||
QStringList dirs_ret;
|
||||
QSet<QString> seen;
|
||||
|
||||
for (const auto& dir: dirs) {
|
||||
const auto normalized = NormalizeGameDirectory(dir);
|
||||
const auto key = PathKey(normalized);
|
||||
if (normalized.isEmpty() || key.isEmpty() || seen.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.insert(key);
|
||||
dirs_ret.append(normalized);
|
||||
}
|
||||
|
||||
return dirs_ret;
|
||||
}
|
||||
|
||||
static QStringList SettingsStringList(const QVariant& value) {
|
||||
auto list = value.toStringList();
|
||||
if (!list.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
|
||||
const auto text = value.toString();
|
||||
return text.isEmpty() ? QStringList() : QStringList({text});
|
||||
}
|
||||
|
||||
static QString GetPic0Path(const Configuration& info) {
|
||||
if (info.basedir.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto path = QDir(info.basedir).filePath(QStringLiteral("sce_sys/pic0.png"));
|
||||
return QFileInfo::exists(path) ? QFileInfo(path).absoluteFilePath() : QString();
|
||||
}
|
||||
|
||||
static void ApplyGameListStyle(Ui::ConfigurationListWidget* ui) {
|
||||
ui->cfgs_list->setAlternatingRowColors(false);
|
||||
ui->cfgs_list->setAllColumnsShowFocus(true);
|
||||
ui->cfgs_list->setIndentation(0);
|
||||
ui->cfgs_list->setMouseTracking(false);
|
||||
ui->cfgs_list->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
ui->cfgs_list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
ui->cfgs_list->setTextElideMode(Qt::ElideMiddle);
|
||||
ui->cfgs_list->header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
ui->cfgs_list->header()->setHighlightSections(false);
|
||||
ui->cfgs_list->header()->setStretchLastSection(true);
|
||||
|
||||
ui->cfgs_list->setStyleSheet(QStringLiteral(
|
||||
"QTreeWidget { background: transparent; border: 1px solid rgba(255,255,255,42); "
|
||||
"color: #eef4ff; outline: 0; }"
|
||||
"QTreeWidget::item { background: rgba(8,10,16,92); "
|
||||
"border-bottom: 1px solid rgba(255,255,255,24); padding: 4px; }"
|
||||
"QTreeWidget::item:selected { background: rgba(32,118,210,150); color: #ffffff; }"
|
||||
"QHeaderView::section { background: rgba(22,25,31,235); color: #dce6f6; border: 0; "
|
||||
"border-right: 1px solid rgba(255,255,255,35); border-bottom: 1px solid "
|
||||
"rgba(255,255,255,45); "
|
||||
"padding: 5px 6px; }"
|
||||
"QScrollBar:vertical { background: rgba(12,14,18,150); width: 14px; margin: 0; }"
|
||||
"QScrollBar::handle:vertical { background: rgba(220,230,245,120); min-height: 36px; }"
|
||||
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }"));
|
||||
|
||||
ui->search_line_edit->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: rgba(12,14,18,210); border: 1px solid rgba(255,255,255,42); "
|
||||
"border-radius: 4px; color: #eef4ff; padding: 5px 8px; selection-background-color: "
|
||||
"#267bd8; }"
|
||||
"QLineEdit:focus { border-color: rgba(80,160,255,180); }"));
|
||||
ui->global_settings_button->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { background: rgba(255,255,255,18); border: 1px solid transparent; "
|
||||
"border-radius: 5px; padding: 3px; }"
|
||||
"QToolButton:hover { background: rgba(255,255,255,45); border-color: rgba(255,255,255,70); "
|
||||
"}"
|
||||
"QToolButton:disabled { background: transparent; }"));
|
||||
ui->edit_button->setStyleSheet(ui->global_settings_button->styleSheet());
|
||||
ui->delete_button->setStyleSheet(ui->global_settings_button->styleSheet());
|
||||
}
|
||||
|
||||
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
|
||||
const QString& title_id) {
|
||||
const auto path = QDir(root).filePath(
|
||||
QStringLiteral("%1/%2").arg(QString::fromLatin1(SAVE_DATA_DIR), title_id));
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto absolute = dir.absolutePath();
|
||||
auto canonical = QFileInfo(absolute).canonicalFilePath();
|
||||
if (canonical.isEmpty()) {
|
||||
canonical = absolute;
|
||||
}
|
||||
canonical = QDir::cleanPath(canonical);
|
||||
|
||||
if (!seen->contains(canonical)) {
|
||||
seen->insert(canonical);
|
||||
dirs->append(absolute);
|
||||
}
|
||||
}
|
||||
|
||||
static QStringList GetSaveDataDirs(const Configuration& info) {
|
||||
QStringList dirs;
|
||||
if (info.title_id.trimmed().isEmpty()) {
|
||||
return dirs;
|
||||
}
|
||||
|
||||
QSet<QString> seen;
|
||||
QStringList roots({QDir::currentPath(), QCoreApplication::applicationDirPath()});
|
||||
|
||||
QDir current_parent(QDir::currentPath());
|
||||
if (current_parent.cdUp()) {
|
||||
roots.append(current_parent.absolutePath());
|
||||
}
|
||||
|
||||
QDir app_parent(QCoreApplication::applicationDirPath());
|
||||
if (app_parent.cdUp()) {
|
||||
roots.append(app_parent.absolutePath());
|
||||
}
|
||||
|
||||
for (const auto& root: roots) {
|
||||
AddSaveDataDir(&dirs, &seen, root, info.title_id.trimmed());
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
: QWidget(parent), m_ui(new Ui::ConfigurationListWidget) {
|
||||
m_compatibility = new CompatibilityDatabase(
|
||||
QCoreApplication::arguments().contains(QStringLiteral("--local")), this);
|
||||
m_ui->setupUi(this);
|
||||
ApplyGameListStyle(m_ui);
|
||||
|
||||
m_ui->global_settings_button->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView));
|
||||
m_ui->global_settings_button->setToolTip(tr("Edit global settings and game folders"));
|
||||
m_ui->edit_button->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
|
||||
m_ui->delete_button->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton));
|
||||
|
||||
m_ui->delete_button->setEnabled(false);
|
||||
m_ui->edit_button->setEnabled(false);
|
||||
|
||||
m_ui->cfgs_list->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_ui->cfgs_list->setIconSize(QSize(48, 48));
|
||||
m_ui->cfgs_list->setRootIsDecorated(false);
|
||||
m_ui->cfgs_list->setUniformRowHeights(true);
|
||||
m_ui->cfgs_list->setSortingEnabled(true);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_NAME_COLUMN, 320);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_SERIAL_COLUMN, 110);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_FIRMWARE_VERSION_COLUMN, 150);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_PATH_COLUMN, 320);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_STATUS_COLUMN, 150);
|
||||
m_ui->cfgs_list->setColumnWidth(GAME_COMMENT_COLUMN, 240);
|
||||
|
||||
connect(m_ui->global_settings_button, &QToolButton::clicked, this,
|
||||
&ConfigurationListWidget::edit_global_settings);
|
||||
connect(m_ui->edit_button, &QToolButton::clicked, this,
|
||||
&ConfigurationListWidget::edit_configuration);
|
||||
connect(m_ui->delete_button, &QToolButton::clicked, this,
|
||||
&ConfigurationListWidget::delete_configuartion);
|
||||
connect(m_ui->cfgs_list, &QTreeWidget::currentItemChanged, this,
|
||||
&ConfigurationListWidget::list_currentItemChanged);
|
||||
connect(m_ui->cfgs_list, &QTreeWidget::itemDoubleClicked, this,
|
||||
&ConfigurationListWidget::list_itemDoubleClicked);
|
||||
connect(m_ui->cfgs_list, &QTreeWidget::customContextMenuRequested, this,
|
||||
&ConfigurationListWidget::show_context_menu);
|
||||
connect(m_ui->search_line_edit, &QLineEdit::textChanged, this,
|
||||
&ConfigurationListWidget::filter_configurations);
|
||||
connect(m_compatibility, &CompatibilityDatabase::Updated, this,
|
||||
&ConfigurationListWidget::ApplyCompatibility);
|
||||
|
||||
m_ui->cfgs_list->setDragDropMode(QAbstractItemView::NoDragDrop);
|
||||
|
||||
ReadSettings();
|
||||
if (m_compatibility->IsLocal()) {
|
||||
m_compatibility->Load();
|
||||
}
|
||||
ScanGameDirectory();
|
||||
if (!m_compatibility->IsLocal()) {
|
||||
m_compatibility->Load();
|
||||
}
|
||||
}
|
||||
|
||||
ConfigurationListWidget::~ConfigurationListWidget() {
|
||||
qDeleteAll(m_custom_infos);
|
||||
delete m_ui;
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::WriteSettings() {
|
||||
QFile file = QFile(QDir(".").absoluteFilePath(CONF_FILE_NAME));
|
||||
std::unique_ptr<QSettings> s;
|
||||
if (file.exists()) {
|
||||
s = std::make_unique<QSettings>(CONF_FILE_NAME, QSettings::IniFormat);
|
||||
} else {
|
||||
#ifdef __linux__
|
||||
s = std::make_unique<QSettings>(QSettings::IniFormat, QSettings::UserScope, CONF_ORG_NAME,
|
||||
CONF_APP_NAME);
|
||||
#else
|
||||
s = std::make_unique<QSettings>(QSettings::IniFormat, QSettings::SystemScope, CONF_ORG_NAME,
|
||||
CONF_APP_NAME);
|
||||
#endif
|
||||
}
|
||||
|
||||
MainDialog::WriteSettings(*s);
|
||||
ConfigurationEditDialog::WriteSettings(*s);
|
||||
|
||||
s->beginGroup(CONF_LAUNCHER);
|
||||
m_game_dirs = NormalizeGameDirectories(m_game_dirs);
|
||||
s->setValue(CONF_GAME_DIRS, m_game_dirs);
|
||||
s->remove(CONF_GAME_DIR);
|
||||
s->endGroup();
|
||||
|
||||
s->remove(CONF_GLOBAL);
|
||||
s->beginGroup(CONF_GLOBAL);
|
||||
m_global_info.WriteSettings(s.get());
|
||||
s->endGroup();
|
||||
|
||||
s->remove(CONF_SECTION_NAME);
|
||||
s->beginWriteArray(CONF_SECTION_NAME);
|
||||
int i = 0;
|
||||
for (auto it = m_custom_infos.constBegin(); it != m_custom_infos.constEnd(); ++it) {
|
||||
s->setArrayIndex(i++);
|
||||
it.value()->WriteSettings(s.get());
|
||||
}
|
||||
s->endArray();
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ReadSettings() {
|
||||
QFile file = QFile(QDir(".").absoluteFilePath(CONF_FILE_NAME));
|
||||
std::unique_ptr<QSettings> s;
|
||||
if (file.exists()) {
|
||||
s = std::make_unique<QSettings>(CONF_FILE_NAME, QSettings::IniFormat);
|
||||
} else {
|
||||
#ifdef __linux__
|
||||
s = std::make_unique<QSettings>(QSettings::IniFormat, QSettings::UserScope, CONF_ORG_NAME,
|
||||
CONF_APP_NAME);
|
||||
#else
|
||||
s = std::make_unique<QSettings>(QSettings::IniFormat, QSettings::SystemScope, CONF_ORG_NAME,
|
||||
CONF_APP_NAME);
|
||||
#endif
|
||||
}
|
||||
|
||||
m_settings_file = s->fileName();
|
||||
|
||||
MainDialog::ReadSettings(*s);
|
||||
ConfigurationEditDialog::ReadSettings(*s);
|
||||
|
||||
s->beginGroup(CONF_LAUNCHER);
|
||||
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIRS)));
|
||||
if (m_game_dirs.isEmpty()) {
|
||||
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIR)));
|
||||
}
|
||||
s->endGroup();
|
||||
|
||||
s->beginGroup(CONF_GLOBAL);
|
||||
if (!s->childKeys().isEmpty()) {
|
||||
m_global_info.ReadSettings(s.get());
|
||||
}
|
||||
s->endGroup();
|
||||
|
||||
qDeleteAll(m_custom_infos);
|
||||
m_custom_infos.clear();
|
||||
|
||||
int size = s->beginReadArray(CONF_SECTION_NAME);
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
s->setArrayIndex(i);
|
||||
auto* info = new Configuration;
|
||||
info->ReadSettings(s.get());
|
||||
info->custom_settings = true;
|
||||
if (!info->game_path.isEmpty()) {
|
||||
m_custom_infos.insert(info->game_path, info);
|
||||
} else {
|
||||
delete info;
|
||||
}
|
||||
}
|
||||
s->endArray();
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ApplyCompatibility() {
|
||||
const bool sorting_enabled = m_ui->cfgs_list->isSortingEnabled();
|
||||
const int sort_column = m_ui->cfgs_list->sortColumn();
|
||||
const auto sort_order = m_ui->cfgs_list->header()->sortIndicatorOrder();
|
||||
m_ui->cfgs_list->setSortingEnabled(false);
|
||||
|
||||
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); index++) {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
|
||||
const auto& title_id = item->GetInfo().title_id;
|
||||
const auto* entry = m_compatibility->Find(title_id);
|
||||
item->GetInfo().game_status =
|
||||
entry != nullptr ? entry->status : Configuration::GameStatus::Unknown;
|
||||
item->GetInfo().game_comment = entry != nullptr ? entry->comment : QString();
|
||||
|
||||
const QSignalBlocker blocker(item->GetStatusCombo());
|
||||
item->Update();
|
||||
item->SetCompatibilityEditable(m_compatibility->IsLocal() && !title_id.trimmed().isEmpty());
|
||||
}
|
||||
|
||||
m_ui->cfgs_list->setSortingEnabled(sorting_enabled);
|
||||
if (sorting_enabled) {
|
||||
m_ui->cfgs_list->sortItems(sort_column, sort_order);
|
||||
}
|
||||
}
|
||||
|
||||
static Configuration* CloneConfiguration(const Configuration& source) {
|
||||
auto* ret = new Configuration;
|
||||
ret->CopyFrom(source);
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct GameMetadata {
|
||||
QString title_name;
|
||||
QString title_id;
|
||||
QString firmwareVer;
|
||||
};
|
||||
|
||||
static QString GetJsonString(const QJsonObject& obj, const QString& key) {
|
||||
return obj.value(key).toString().trimmed();
|
||||
}
|
||||
|
||||
static QString GetLocalizedTitleName(const QJsonObject& root) {
|
||||
const auto localized = root.value(QStringLiteral("localizedParameters")).toObject();
|
||||
if (localized.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto default_language = GetJsonString(localized, QStringLiteral("defaultLanguage"));
|
||||
if (!default_language.isEmpty()) {
|
||||
const auto title = GetJsonString(localized.value(default_language).toObject(),
|
||||
QStringLiteral("titleName"));
|
||||
if (!title.isEmpty()) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
const auto english_title = GetJsonString(localized.value(QStringLiteral("en-US")).toObject(),
|
||||
QStringLiteral("titleName"));
|
||||
if (!english_title.isEmpty()) {
|
||||
return english_title;
|
||||
}
|
||||
|
||||
for (auto it = localized.constBegin(); it != localized.constEnd(); ++it) {
|
||||
const auto title = GetJsonString(it.value().toObject(), QStringLiteral("titleName"));
|
||||
if (!title.isEmpty()) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static QString GetFirmwareVersion(const QJsonObject& root) {
|
||||
const auto encoded = GetJsonString(root, QStringLiteral("requiredSystemSoftwareVersion"));
|
||||
static const QRegularExpression version_pattern(
|
||||
QStringLiteral("^0[xX]([0-9]{6})[0-9A-Fa-f]{10}$"));
|
||||
const auto match = version_pattern.match(encoded);
|
||||
if (!match.hasMatch()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto digits = match.captured(1);
|
||||
bool valid = false;
|
||||
const int major = digits.left(2).toInt(&valid, 10);
|
||||
if (!valid) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QString version = QStringLiteral("%1.%2").arg(major).arg(digits.mid(2, 2));
|
||||
const auto patch = digits.mid(4, 2);
|
||||
if (patch != QStringLiteral("00")) {
|
||||
version += QStringLiteral(".") + patch;
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
static GameMetadata GetGameMetadata(const QString& param_file, const QString& fallback) {
|
||||
GameMetadata ret;
|
||||
ret.title_name = fallback;
|
||||
|
||||
QFile param(param_file);
|
||||
if (!param.open(QIODevice::ReadOnly)) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
QJsonParseError parse_error;
|
||||
const auto doc = QJsonDocument::fromJson(param.readAll(), &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const auto root = doc.object();
|
||||
const auto title = GetLocalizedTitleName(root);
|
||||
if (!title.isEmpty()) {
|
||||
ret.title_name = title;
|
||||
}
|
||||
|
||||
ret.title_id = GetJsonString(root, QStringLiteral("titleId"));
|
||||
ret.firmwareVer = GetFirmwareVersion(root);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void SetGameFiles(Configuration& info, const QString& game_dir, const QString& game_path,
|
||||
const GameMetadata& metadata) {
|
||||
QDir game(game_dir);
|
||||
|
||||
info.game_path = game_path;
|
||||
info.basedir = game.absolutePath();
|
||||
info.name = metadata.title_name;
|
||||
info.title_id = metadata.title_id;
|
||||
info.firmwareVer = metadata.firmwareVer;
|
||||
|
||||
if (info.name.isEmpty()) {
|
||||
info.name = game.dirName();
|
||||
}
|
||||
if (info.elf.isEmpty()) {
|
||||
info.elf = QStringLiteral("eboot.bin");
|
||||
}
|
||||
}
|
||||
|
||||
static Configuration* FindCustomInfo(QMap<QString, Configuration*>* custom_infos,
|
||||
const QString& game_path, const QString& legacy_game_path) {
|
||||
auto custom = custom_infos->find(game_path);
|
||||
if (custom != custom_infos->end()) {
|
||||
return custom.value();
|
||||
}
|
||||
|
||||
custom = custom_infos->find(legacy_game_path);
|
||||
if (custom == custom_infos->end()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* info = custom.value();
|
||||
custom_infos->erase(custom);
|
||||
info->game_path = game_path;
|
||||
custom_infos->insert(game_path, info);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
bool ConfigurationListWidget::EnsureGameDirectory() {
|
||||
if (HasValidGameDirectory()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QMessageBox::information(this, tr("Game folders"),
|
||||
tr("Add at least one game folder in global settings."));
|
||||
edit_global_settings();
|
||||
|
||||
return HasValidGameDirectory();
|
||||
}
|
||||
|
||||
bool ConfigurationListWidget::HasValidGameDirectory() const {
|
||||
for (const auto& dir: m_game_dirs) {
|
||||
if (!dir.isEmpty() && QDir(dir).exists()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ScanGameDirectory() {
|
||||
m_selected_item = nullptr;
|
||||
m_ui->cfgs_list->clear();
|
||||
m_ui->cfgs_list->SetBackgroundImage({});
|
||||
m_ui->edit_button->setEnabled(false);
|
||||
m_ui->delete_button->setEnabled(false);
|
||||
|
||||
const QString eboot_name = QStringLiteral("eboot.bin");
|
||||
QSet<QString> found_games;
|
||||
|
||||
for (const auto& root_path: m_game_dirs) {
|
||||
QDir root(root_path);
|
||||
if (root_path.isEmpty() || !root.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QList<QDir> pending_dirs;
|
||||
const auto root_subdirs =
|
||||
root.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
||||
for (const auto& subdir: root_subdirs) {
|
||||
pending_dirs.append(QDir(subdir.absoluteFilePath()));
|
||||
}
|
||||
|
||||
while (!pending_dirs.isEmpty()) {
|
||||
QDir game_dir = pending_dirs.takeFirst();
|
||||
|
||||
if (game_dir.exists(eboot_name)) {
|
||||
const QString game_path = NormalizeGameDirectory(game_dir.absolutePath());
|
||||
const QString legacy_game_path = root.relativeFilePath(game_dir.absolutePath());
|
||||
const QString game_key = PathKey(game_path);
|
||||
if (game_key.isEmpty() || found_games.contains(game_key)) {
|
||||
continue;
|
||||
}
|
||||
found_games.insert(game_key);
|
||||
|
||||
auto metadata = GetGameMetadata(
|
||||
game_dir.filePath(QStringLiteral("sce_sys/param.json")), game_dir.dirName());
|
||||
|
||||
auto info = std::make_unique<Configuration>();
|
||||
auto* custom = FindCustomInfo(&m_custom_infos, game_path, legacy_game_path);
|
||||
if (custom != nullptr) {
|
||||
info->CopyFrom(*custom);
|
||||
info->custom_settings = true;
|
||||
} else {
|
||||
info->CopyEmulatorSettingsFrom(m_global_info);
|
||||
info->custom_settings = false;
|
||||
}
|
||||
|
||||
SetGameFiles(*info, game_dir.absolutePath(), game_path, metadata);
|
||||
const auto* compatibility = m_compatibility->Find(info->title_id);
|
||||
if (compatibility != nullptr) {
|
||||
info->game_status = compatibility->status;
|
||||
info->game_comment = compatibility->comment;
|
||||
}
|
||||
|
||||
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
|
||||
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
|
||||
!item->GetInfo().title_id.trimmed().isEmpty());
|
||||
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, this,
|
||||
[this, item](int /*index*/) {
|
||||
if (!m_compatibility->IsLocal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& title_id = item->GetInfo().title_id;
|
||||
if (title_id.trimmed().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
item->GetInfo().game_status = static_cast<Configuration::GameStatus>(
|
||||
item->GetStatusCombo()->currentData().toInt());
|
||||
item->Update();
|
||||
m_compatibility->SetStatus(title_id, item->GetInfo().game_status);
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
SelectItem(item);
|
||||
});
|
||||
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, this, [this, item]() {
|
||||
if (!m_compatibility->IsLocal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& title_id = item->GetInfo().title_id;
|
||||
if (title_id.trimmed().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
item->GetInfo().game_comment = item->GetCommentEdit()->text();
|
||||
item->Update();
|
||||
m_compatibility->SetComment(title_id, item->GetInfo().game_comment);
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
SelectItem(item);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto subdirs =
|
||||
game_dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
||||
for (const auto& subdir: subdirs) {
|
||||
pending_dirs.append(QDir(subdir.absoluteFilePath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_ui->cfgs_list->sortItems(GAME_NAME_COLUMN, Qt::AscendingOrder);
|
||||
filter_configurations(m_ui->search_line_edit->text());
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::edit_configuration() {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationEditDialog dlg(item->GetInfo(), this);
|
||||
dlg.SetTitle(tr("Edit game settings"));
|
||||
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
item->GetInfo().custom_settings = true;
|
||||
auto game_path = item->GetInfo().game_path;
|
||||
delete m_custom_infos.take(game_path);
|
||||
m_custom_infos.insert(game_path, CloneConfiguration(item->GetInfo()));
|
||||
WriteSettings();
|
||||
ScanGameDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::delete_configuartion() {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
|
||||
if (item == nullptr || !item->GetInfo().custom_settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear custom settings"),
|
||||
tr("Do you want to clear custom settings?"))) {
|
||||
ClearCustomSettings(item);
|
||||
WriteSettings();
|
||||
ScanGameDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::edit_global_settings() {
|
||||
Configuration info;
|
||||
info.CopyEmulatorSettingsFrom(m_global_info);
|
||||
info.name = tr("Global settings");
|
||||
|
||||
ConfigurationEditDialog dlg(info, this);
|
||||
dlg.SetTitle(tr("Global settings"));
|
||||
dlg.SetGameDirectories(m_game_dirs);
|
||||
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
m_global_info.CopyEmulatorSettingsFrom(info);
|
||||
m_game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
|
||||
WriteSettings();
|
||||
ScanGameDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ClearCustomSettings(ConfigurationItem* item) {
|
||||
delete m_custom_infos.take(item->GetInfo().game_path);
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::run_configuration() {
|
||||
emit Run();
|
||||
}
|
||||
|
||||
bool ConfigurationListWidget::CanViewSelectedTrophies() const {
|
||||
return m_selected_item != nullptr &&
|
||||
TrophyViewerDialog::HasTrophyData(&m_selected_item->GetInfo());
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ViewTrophies() {
|
||||
auto* current = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
|
||||
auto* item = current != nullptr ? current : m_selected_item;
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrophyViewerDialog::ShowForGame(&item->GetInfo(), this);
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::open_game_folder() {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QDir game_dir(item->GetInfo().basedir);
|
||||
if (!game_dir.exists()) {
|
||||
QMessageBox::warning(this, tr("Open game folder"), tr("Game folder does not exist."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QDesktopServices::openUrl(QUrl::fromLocalFile(game_dir.absolutePath()))) {
|
||||
QMessageBox::warning(this, tr("Open game folder"), tr("Could not open game folder."));
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::remove_save_data() {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto save_data_dirs = GetSaveDataDirs(item->GetInfo());
|
||||
if (save_data_dirs.isEmpty()) {
|
||||
QMessageBox::information(this, tr("Remove save data"),
|
||||
tr("No save data folder found for this game."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto title =
|
||||
!item->GetInfo().name.isEmpty() ? item->GetInfo().name : item->GetInfo().title_id;
|
||||
const auto text =
|
||||
tr("Remove save data for \"%1\"?\n\nThis will delete:\n%2\n\nThis cannot be undone.")
|
||||
.arg(title, save_data_dirs.join(QLatin1Char('\n')));
|
||||
|
||||
if (QMessageBox::Yes != QMessageBox::question(this, tr("Remove save data"), text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList failed_dirs;
|
||||
for (const auto& path: save_data_dirs) {
|
||||
QDir dir(path);
|
||||
if (dir.exists() && !dir.removeRecursively()) {
|
||||
failed_dirs.append(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed_dirs.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Remove save data"),
|
||||
tr("Could not remove:\n%1").arg(failed_dirs.join(QLatin1Char('\n'))));
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::filter_configurations(const QString& text) {
|
||||
const auto query = text.trimmed();
|
||||
const bool has_query = !query.isEmpty();
|
||||
bool selection_is_shown = false;
|
||||
|
||||
for (int item_index = 0; item_index < m_ui->cfgs_list->topLevelItemCount(); item_index++) {
|
||||
auto* item = m_ui->cfgs_list->topLevelItem(item_index);
|
||||
if (item == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool match = !has_query ||
|
||||
item->text(GAME_NAME_COLUMN).contains(query, Qt::CaseInsensitive) ||
|
||||
item->text(GAME_SERIAL_COLUMN).contains(query, Qt::CaseInsensitive);
|
||||
item->setHidden(!match);
|
||||
|
||||
if (match && item == m_selected_item) {
|
||||
selection_is_shown = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_selected_item != nullptr && !selection_is_shown) {
|
||||
m_ui->cfgs_list->clearSelection();
|
||||
m_ui->cfgs_list->setCurrentItem(nullptr);
|
||||
SelectItem(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::SelectItem(QTreeWidgetItem* witem) {
|
||||
auto* item = static_cast<ConfigurationItem*>(witem);
|
||||
if (item == nullptr) {
|
||||
m_selected_item = nullptr;
|
||||
m_ui->cfgs_list->SetBackgroundImage({});
|
||||
m_ui->edit_button->setEnabled(false);
|
||||
m_ui->delete_button->setEnabled(false);
|
||||
emit Select();
|
||||
return;
|
||||
}
|
||||
|
||||
m_ui->delete_button->setEnabled(!item->IsRunning() && item->GetInfo().custom_settings);
|
||||
m_ui->edit_button->setEnabled(!item->IsRunning());
|
||||
|
||||
m_selected_item = item;
|
||||
m_ui->cfgs_list->SetBackgroundImage(GetPic0Path(item->GetInfo()));
|
||||
|
||||
emit Select();
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::list_currentItemChanged(QTreeWidgetItem* current,
|
||||
QTreeWidgetItem* /*previous*/) {
|
||||
SelectItem(current);
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::list_itemDoubleClicked(QTreeWidgetItem* witem, int /*column*/) {
|
||||
SelectItem(witem);
|
||||
if (m_run_enabled) {
|
||||
emit Run();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->itemAt(pos));
|
||||
|
||||
if (item != nullptr) {
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
SelectItem(item);
|
||||
}
|
||||
|
||||
QMenu menu;
|
||||
const auto save_data_dirs = item != nullptr ? GetSaveDataDirs(item->GetInfo()) : QStringList();
|
||||
const bool has_trophy_data =
|
||||
item != nullptr && TrophyViewerDialog::HasTrophyData(&item->GetInfo());
|
||||
|
||||
QAction* action_run = menu.addAction(tr("Run"), this, SLOT(run_configuration()));
|
||||
QAction* action_open_folder =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_DirOpenIcon), tr("Open game folder"), this,
|
||||
SLOT(open_game_folder()));
|
||||
QAction* action_view_trophies = menu.addAction(
|
||||
style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("View trophies..."));
|
||||
connect(action_view_trophies, &QAction::triggered, this,
|
||||
&ConfigurationListWidget::ViewTrophies);
|
||||
QAction* action_remove_save_data =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
||||
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
||||
menu.addSeparator();
|
||||
QAction* action_edit =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game settings..."),
|
||||
this, SLOT(edit_configuration()));
|
||||
QAction* action_delete =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
||||
tr("Clear custom settings"), this, SLOT(delete_configuartion()));
|
||||
|
||||
if (item == nullptr) {
|
||||
menu.addSeparator();
|
||||
/*QAction *action_global = */ menu.addAction(
|
||||
style()->standardIcon(QStyle::SP_FileDialogDetailedView), tr("Global settings..."),
|
||||
this, SLOT(edit_global_settings()));
|
||||
}
|
||||
|
||||
if (item != nullptr) {
|
||||
action_run->setDisabled(item->IsRunning());
|
||||
action_open_folder->setDisabled(!QDir(item->GetInfo().basedir).exists());
|
||||
action_view_trophies->setDisabled(!has_trophy_data);
|
||||
action_remove_save_data->setDisabled(item->IsRunning() || save_data_dirs.isEmpty());
|
||||
action_edit->setDisabled(item->IsRunning());
|
||||
action_delete->setDisabled(item->IsRunning() || !item->GetInfo().custom_settings);
|
||||
} else {
|
||||
action_run->setDisabled(true);
|
||||
action_open_folder->setDisabled(true);
|
||||
action_view_trophies->setDisabled(true);
|
||||
action_remove_save_data->setDisabled(true);
|
||||
action_edit->setDisabled(true);
|
||||
action_delete->setDisabled(true);
|
||||
}
|
||||
|
||||
if (!m_run_enabled) {
|
||||
action_run->setDisabled(true);
|
||||
}
|
||||
|
||||
menu.exec(QCursor::pos());
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "mainDialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QArgument>
|
||||
#include <QObject>
|
||||
#include <QStyleFactory>
|
||||
|
||||
class QStyle;
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QApplication a(argc, argv);
|
||||
MainDialog w;
|
||||
|
||||
QStyle* s = QStyleFactory::create("fusion");
|
||||
QApplication::setStyle(s);
|
||||
|
||||
QObject::connect(&a, &QApplication::aboutToQuit, &w, &MainDialog::Quit);
|
||||
|
||||
w.emit Start();
|
||||
|
||||
w.show();
|
||||
|
||||
return QApplication::exec();
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
#include "mainDialog.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "configuration.h"
|
||||
#include "configurationItem.h"
|
||||
#include "configurationListWidget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QIODevice>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QRadioButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
#include <QTextStream>
|
||||
#include <QVariant>
|
||||
#include <QtCore>
|
||||
|
||||
#include "ui_main_dialog.h"
|
||||
|
||||
#ifndef __linux__
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#endif
|
||||
|
||||
// IWYU pragma: no_include <minwindef.h>
|
||||
// IWYU pragma: no_include <processthreadsapi.h>
|
||||
// IWYU pragma: no_include <winbase.h>
|
||||
|
||||
class QWidget;
|
||||
|
||||
#ifdef __linux__
|
||||
constexpr char EMULATOR_EXE[] = "kyty_emulator";
|
||||
#else
|
||||
constexpr char EMULATOR_EXE[] = "kyty_emulator.exe";
|
||||
#endif
|
||||
|
||||
#ifndef __linux__
|
||||
constexpr char CMD_EXE[] = "cmd.exe";
|
||||
#else
|
||||
constexpr char GNOME[] = "gnome-terminal";
|
||||
constexpr char XTERM[] = "xterm";
|
||||
constexpr char KYTY_BASH_FILE[] = "kyty_run.sh";
|
||||
#endif
|
||||
#ifndef __linux__
|
||||
constexpr DWORD CMD_X_CHARS = 175;
|
||||
constexpr DWORD CMD_Y_CHARS = 1000;
|
||||
#endif
|
||||
constexpr char SETTINGS_MAIN_DIALOG[] = "MainDialog";
|
||||
constexpr char SETTINGS_MAIN_LAST_GEOMETRY[] = "geometry";
|
||||
|
||||
class DetachableProcess: public QProcess {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit DetachableProcess(QObject* parent = nullptr): QProcess(parent) {}
|
||||
void Detach() {
|
||||
this->waitForStarted();
|
||||
setProcessState(QProcess::NotRunning);
|
||||
}
|
||||
};
|
||||
|
||||
class MainDialogPrivate: public QObject {
|
||||
Q_OBJECT
|
||||
KYTY_QT_CLASS_NO_COPY(MainDialogPrivate);
|
||||
|
||||
public:
|
||||
explicit MainDialogPrivate(QObject* parent = nullptr): QObject(parent) {}
|
||||
~MainDialogPrivate() override;
|
||||
|
||||
void Setup(MainDialog* main_dialog);
|
||||
|
||||
/*slots:*/
|
||||
|
||||
void Update();
|
||||
void FindInterpreter();
|
||||
void Run();
|
||||
|
||||
[[nodiscard]] const QString& GetInterpreter() const { return m_interpreter; }
|
||||
|
||||
static void WriteSettings(QSettings& s);
|
||||
static void ReadSettings(QSettings& s);
|
||||
|
||||
private:
|
||||
static QByteArray g_last_geometry;
|
||||
|
||||
Ui::MainDialog* m_ui = {nullptr};
|
||||
MainDialog* m_main_dialog = nullptr;
|
||||
QString m_interpreter;
|
||||
|
||||
/*DetachableProcess*/ QProcess m_process;
|
||||
|
||||
ConfigurationItem* m_running_item = nullptr;
|
||||
};
|
||||
|
||||
QByteArray MainDialogPrivate::g_last_geometry;
|
||||
|
||||
MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPrivate(this)) {
|
||||
m_p->Setup(this);
|
||||
}
|
||||
|
||||
MainDialogPrivate::~MainDialogPrivate() {
|
||||
delete m_ui;
|
||||
}
|
||||
|
||||
void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
||||
m_ui = new Ui::MainDialog;
|
||||
m_ui->setupUi(main_dialog);
|
||||
|
||||
m_main_dialog = main_dialog;
|
||||
m_ui->widget->SetMainDialog(main_dialog);
|
||||
|
||||
main_dialog->setWindowFlags(Qt::Dialog /*| Qt::MSWindowsFixedSizeDialogHint*/);
|
||||
|
||||
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Select, this, &MainDialogPrivate::Update);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Run, this, &MainDialogPrivate::Run);
|
||||
connect(main_dialog, &MainDialog::Resize, [this]() {
|
||||
g_last_geometry = m_main_dialog->saveGeometry();
|
||||
m_ui->widget->WriteSettings();
|
||||
});
|
||||
|
||||
connect(&m_process,
|
||||
static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
|
||||
[this](int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) {
|
||||
if (m_running_item != nullptr) {
|
||||
m_running_item->SetRunning(false);
|
||||
}
|
||||
Update();
|
||||
});
|
||||
|
||||
// connect(main_dialog, &MainDialog::Quit, [=]() { m_process.Detach(); });
|
||||
|
||||
m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile());
|
||||
|
||||
m_main_dialog->restoreGeometry(g_last_geometry);
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
void MainDialogPrivate::FindInterpreter() {
|
||||
QDir search_dir(QApplication::applicationDirPath());
|
||||
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
|
||||
|
||||
if (!QFile::exists(m_interpreter)) {
|
||||
search_dir.cdUp();
|
||||
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
|
||||
}
|
||||
|
||||
bool found = QFile::exists(m_interpreter);
|
||||
|
||||
if (found) {
|
||||
m_ui->label_Interpreter->setText(tr("Emulator: ") + m_interpreter);
|
||||
|
||||
QProcess test;
|
||||
test.setProgram(m_interpreter);
|
||||
test.start();
|
||||
test.waitForFinished();
|
||||
|
||||
auto output = QString(test.readAllStandardOutput());
|
||||
auto lines = output.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts);
|
||||
|
||||
if (lines.count() >= 2) {
|
||||
m_ui->label_Version->setText(
|
||||
tr("Version: ") + (lines.at(0).startsWith("exe_name") ? lines.at(1) : lines.at(0)));
|
||||
} else {
|
||||
found = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
QMessageBox::critical(m_main_dialog, tr("Error"), tr("Can't find emulator"));
|
||||
QApplication::quit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_ui->widget->EnsureGameDirectory()) {
|
||||
QApplication::quit();
|
||||
return;
|
||||
}
|
||||
|
||||
m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile());
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
static QString BoolArg(bool value) {
|
||||
return value ? QStringLiteral("true") : QStringLiteral("false");
|
||||
}
|
||||
|
||||
static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
QStringList args;
|
||||
auto r = EnumToText(info.screen_resolution).split('x');
|
||||
|
||||
if (r.size() != 2) {
|
||||
return {};
|
||||
}
|
||||
|
||||
args << "--screen-width" << r.at(0);
|
||||
args << "--screen-height" << r.at(1);
|
||||
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
|
||||
args << "--vulkan-validation" << BoolArg(info.vulkan_validation_enabled);
|
||||
args << "--shader-validation" << BoolArg(info.shader_validation_enabled);
|
||||
args << "--shader-optimization-type" << EnumToText(info.shader_optimization_type);
|
||||
args << "--shader-log-direction" << EnumToText(info.shader_log_direction);
|
||||
args << "--shader-log-folder" << info.shader_log_folder;
|
||||
args << "--command-buffer-dump" << BoolArg(info.command_buffer_dump_enabled);
|
||||
args << "--command-buffer-dump-folder" << info.command_buffer_dump_folder;
|
||||
args << "--printf-direction" << EnumToText(info.printf_direction);
|
||||
args << "--printf-output-file" << info.printf_output_file;
|
||||
args << "--profiler-direction" << EnumToText(info.profiler_direction);
|
||||
args << "--spirv-debug-printf" << "false";
|
||||
args << "--ngg-rectlist-draw" << BoolArg(info.ngg_rectlist_draw_enabled);
|
||||
if (info.renderdoc_enabled) {
|
||||
args << "--rd";
|
||||
}
|
||||
|
||||
QString game = info.basedir;
|
||||
if (!info.elf.isEmpty()) {
|
||||
game = QDir(info.basedir).filePath(info.elf);
|
||||
}
|
||||
args << "--game" << game;
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
static QString BashQuote(QString value) {
|
||||
value.replace('\'', "'\\''");
|
||||
return QStringLiteral("'") + value + QStringLiteral("'");
|
||||
}
|
||||
|
||||
static bool CreateBashScript(const QString& interpreter, const QStringList& args,
|
||||
const QString& file_name) {
|
||||
QFile file(file_name);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QTextStream s(&file);
|
||||
|
||||
s << "#!/bin/bash\n";
|
||||
s << BashQuote(interpreter);
|
||||
for (const auto& arg: args) {
|
||||
s << " " << BashQuote(arg);
|
||||
}
|
||||
s << "\n";
|
||||
s << "echo Press any key...\n";
|
||||
s << "read -n1\n";
|
||||
|
||||
file.close();
|
||||
|
||||
return file.setPermissions(file.permissions() | QFile::ExeUser | QFile::ExeOwner |
|
||||
QFile::ExeGroup);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
|
||||
const auto& interpreter = m_p->GetInterpreter();
|
||||
|
||||
QFileInfo f(interpreter);
|
||||
auto dir = f.absoluteDir();
|
||||
|
||||
auto args = CreateEmulatorArgs(info);
|
||||
if (args.isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid emulator configuration"));
|
||||
QApplication::quit();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
auto bash_file_name = dir.filePath(KYTY_BASH_FILE);
|
||||
if (!CreateBashScript(interpreter, args, bash_file_name)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Can't create file:\n") + bash_file_name);
|
||||
QApplication::quit();
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
process->setProgram(GNOME);
|
||||
process->setArguments({"--", "bash", "-c", bash_file_name});
|
||||
}
|
||||
#else
|
||||
{
|
||||
process->setProgram(CMD_EXE);
|
||||
QStringList process_args;
|
||||
process_args << QStringLiteral("/K") << interpreter;
|
||||
process_args += args;
|
||||
process->setArguments(process_args);
|
||||
}
|
||||
#endif
|
||||
process->setWorkingDirectory(dir.path());
|
||||
#ifndef __linux__
|
||||
process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments* args) {
|
||||
args->flags |= static_cast<uint32_t>(CREATE_NEW_CONSOLE);
|
||||
args->startupInfo->dwFlags &= ~static_cast<DWORD>(STARTF_USESTDHANDLES);
|
||||
args->startupInfo->dwFlags |= static_cast<DWORD>(STARTF_USECOUNTCHARS);
|
||||
args->startupInfo->dwXCountChars = CMD_X_CHARS;
|
||||
args->startupInfo->dwYCountChars = CMD_Y_CHARS;
|
||||
// args->startupInfo->dwFlags |= static_cast<DWORD>(STARTF_USEFILLATTRIBUTE);
|
||||
// args->startupInfo->dwFillAttribute =
|
||||
// static_cast<DWORD>(BACKGROUND_BLUE) | static_cast<DWORD>(FOREGROUND_RED) |
|
||||
// static_cast<DWORD>(FOREGROUND_INTENSITY);
|
||||
});
|
||||
#endif
|
||||
process->start();
|
||||
process->waitForFinished(100);
|
||||
}
|
||||
|
||||
void MainDialog::WriteSettings(QSettings& s) {
|
||||
MainDialogPrivate::WriteSettings(s);
|
||||
}
|
||||
|
||||
void MainDialog::ReadSettings(QSettings& s) {
|
||||
MainDialogPrivate::ReadSettings(s);
|
||||
}
|
||||
|
||||
void MainDialog::resizeEvent(QResizeEvent* event) {
|
||||
emit Resize();
|
||||
QDialog::resizeEvent(event);
|
||||
}
|
||||
|
||||
void MainDialogPrivate::WriteSettings(QSettings& s) {
|
||||
s.beginGroup(SETTINGS_MAIN_DIALOG);
|
||||
|
||||
if (!g_last_geometry.isEmpty()) {
|
||||
s.setValue(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry);
|
||||
}
|
||||
|
||||
s.endGroup();
|
||||
}
|
||||
|
||||
void MainDialogPrivate::ReadSettings(QSettings& s) {
|
||||
s.beginGroup(SETTINGS_MAIN_DIALOG);
|
||||
|
||||
g_last_geometry = s.value(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry).toByteArray();
|
||||
|
||||
s.endGroup();
|
||||
}
|
||||
|
||||
void MainDialogPrivate::Run() {
|
||||
m_running_item = m_ui->widget->GetSelectedItem();
|
||||
if (m_running_item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_running_item->SetRunning(true);
|
||||
|
||||
m_main_dialog->RunInterpreter(&m_process, m_running_item->GetInfo());
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
void MainDialogPrivate::Update() {
|
||||
const auto* item = m_ui->widget->GetSelectedItem();
|
||||
|
||||
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr);
|
||||
|
||||
if (run_enabled) {
|
||||
const auto& info = item->GetInfo();
|
||||
auto dir = info.basedir;
|
||||
run_enabled = !dir.isEmpty() && QDir(dir).exists();
|
||||
}
|
||||
|
||||
m_ui->widget->SetRunEnabled(run_enabled);
|
||||
}
|
||||
|
||||
#include "mainDialog.moc"
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "mandatoryLineEdit.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QLine>
|
||||
#include <QPainter>
|
||||
#include <QPen>
|
||||
#include <QVariant>
|
||||
#include <QtCore>
|
||||
|
||||
class QPaintEvent;
|
||||
class QWidget;
|
||||
|
||||
MandatoryLineEdit::MandatoryLineEdit(QWidget* parent): QLineEdit(parent) {
|
||||
this->setProperty(MANDATORY_AND_EMPTY, true);
|
||||
|
||||
connect(this, &QLineEdit::textChanged, this, &MandatoryLineEdit::changed);
|
||||
}
|
||||
|
||||
void MandatoryLineEdit::paintEvent(QPaintEvent* event) {
|
||||
QLineEdit::paintEvent(event);
|
||||
|
||||
if (this->property(MANDATORY_AND_EMPTY).toBool() && this->isEnabled()) {
|
||||
QPainter painter(this);
|
||||
QLine line(4, this->height() - 4, this->width() - 4, this->height() - 4);
|
||||
painter.setPen(QPen(QColor(255, 0, 0), 1.0, Qt::DotLine));
|
||||
painter.drawLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
bool MandatoryLineEdit::FindEmpty(QObject* p) {
|
||||
const QList<QLineEdit*>& ch_list = p->findChildren<QLineEdit*>();
|
||||
|
||||
foreach (QLineEdit* ch, ch_list) {
|
||||
if (ch->isVisible() && ch->property(MANDATORY_AND_EMPTY).toBool() && ch->isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MandatoryLineEdit::changed(const QString& text) {
|
||||
this->setProperty(MANDATORY_AND_EMPTY, text.isEmpty());
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
#include "trophyViewerDialog.h"
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QBrush>
|
||||
#include <QByteArray>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
#include <QLabel>
|
||||
#include <QMap>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVariant>
|
||||
#include <QWidget>
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr quint32 UCP_MAGIC = 0xb228c60a;
|
||||
constexpr quint32 UCP_VERSION = 1;
|
||||
constexpr int UCP_HEADER_LEN = 0x40;
|
||||
constexpr int UCP_TOC_SKIP = 0x20;
|
||||
constexpr int UCP_ENTRY_LEN = 0x40;
|
||||
constexpr int UCP_NAME_LEN = 0x20;
|
||||
|
||||
struct UcpEntry {
|
||||
QString name;
|
||||
quint64 offset = 0;
|
||||
quint64 size = 0;
|
||||
};
|
||||
|
||||
struct TrophyDefinition {
|
||||
QString id;
|
||||
QString grade;
|
||||
bool hidden = false;
|
||||
bool has_reward = false;
|
||||
};
|
||||
|
||||
struct TrophyText {
|
||||
QString name;
|
||||
QString detail;
|
||||
QString reward;
|
||||
};
|
||||
|
||||
struct TrophyRow {
|
||||
QString id;
|
||||
QString name;
|
||||
QString detail;
|
||||
QString grade;
|
||||
QString reward;
|
||||
bool hidden = false;
|
||||
bool has_reward = false;
|
||||
QPixmap icon;
|
||||
};
|
||||
|
||||
struct TrophySet {
|
||||
QString tab_title;
|
||||
QList<TrophyRow> trophies;
|
||||
};
|
||||
|
||||
static bool CanRead(const QByteArray& data, qsizetype offset, qsizetype size) {
|
||||
return offset >= 0 && size >= 0 && offset <= data.size() && size <= data.size() - offset;
|
||||
}
|
||||
|
||||
static quint32 ReadBe32(const QByteArray& data, qsizetype offset) {
|
||||
const auto* p = reinterpret_cast<const uchar*>(data.constData() + offset);
|
||||
return (static_cast<quint32>(p[0]) << 24u) | (static_cast<quint32>(p[1]) << 16u) |
|
||||
(static_cast<quint32>(p[2]) << 8u) | static_cast<quint32>(p[3]);
|
||||
}
|
||||
|
||||
static quint64 ReadBe64(const QByteArray& data, qsizetype offset) {
|
||||
return (static_cast<quint64>(ReadBe32(data, offset)) << 32u) |
|
||||
static_cast<quint64>(ReadBe32(data, offset + 4));
|
||||
}
|
||||
|
||||
static bool FitsQSizeType(quint64 value) {
|
||||
return value <= static_cast<quint64>(std::numeric_limits<qsizetype>::max());
|
||||
}
|
||||
|
||||
static QString ReadFixedString(const QByteArray& data, qsizetype offset, qsizetype max_size) {
|
||||
if (!CanRead(data, offset, max_size)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
qsizetype len = 0;
|
||||
while (len < max_size && data.at(offset + len) != '\0') {
|
||||
len++;
|
||||
}
|
||||
|
||||
return QString::fromLatin1(data.constData() + offset, len);
|
||||
}
|
||||
|
||||
static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files, QString& error) {
|
||||
QFile file(file_name);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
error = QObject::tr("Could not open %1").arg(QDir::toNativeSeparators(file_name));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray data = file.readAll();
|
||||
if (data.size() < UCP_HEADER_LEN) {
|
||||
error = QObject::tr("%1 is too small to be a trophy package.")
|
||||
.arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto magic = ReadBe32(data, 0x00);
|
||||
if (magic != UCP_MAGIC) {
|
||||
error = QObject::tr("%1 has an invalid trophy package magic.")
|
||||
.arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto version = ReadBe32(data, 0x04);
|
||||
if (version != UCP_VERSION) {
|
||||
error = QObject::tr("%1 uses unsupported trophy package version %2.")
|
||||
.arg(QFileInfo(file_name).fileName(), QString::number(version));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto declared_size = ReadBe64(data, 0x08);
|
||||
if (declared_size > static_cast<quint64>(data.size())) {
|
||||
error = QObject::tr("%1 is truncated.").arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto file_count = ReadBe32(data, 0x10);
|
||||
const auto toc_offset = static_cast<quint64>(ReadBe32(data, 0x14));
|
||||
const auto data_size = static_cast<quint64>(data.size());
|
||||
|
||||
const quint64 table_size = UCP_TOC_SKIP + static_cast<quint64>(file_count) * UCP_ENTRY_LEN;
|
||||
if (toc_offset > data_size || table_size > data_size - toc_offset ||
|
||||
!FitsQSizeType(toc_offset + table_size)) {
|
||||
error = QObject::tr("%1 has an invalid table of contents.")
|
||||
.arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (quint32 i = 0; i < file_count; i++) {
|
||||
const auto entry_offset = static_cast<qsizetype>(toc_offset + UCP_TOC_SKIP +
|
||||
static_cast<quint64>(i) * UCP_ENTRY_LEN);
|
||||
UcpEntry entry;
|
||||
entry.name = ReadFixedString(data, entry_offset, UCP_NAME_LEN).trimmed();
|
||||
entry.offset = ReadBe64(data, entry_offset + 0x20);
|
||||
entry.size = ReadBe64(data, entry_offset + 0x28);
|
||||
|
||||
if (entry.name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.offset > data_size || entry.size > data_size - entry.offset ||
|
||||
!FitsQSizeType(entry.offset) || !FitsQSizeType(entry.size)) {
|
||||
error = QObject::tr("%1 has an invalid entry for %2.")
|
||||
.arg(QFileInfo(file_name).fileName(), entry.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
files.insert(entry.name.toCaseFolded(), data.mid(static_cast<qsizetype>(entry.offset),
|
||||
static_cast<qsizetype>(entry.size)));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static const QByteArray* FindFile(const QMap<QString, QByteArray>& files, const QString& name) {
|
||||
const auto it = files.constFind(name.toCaseFolded());
|
||||
return it == files.constEnd() ? nullptr : &it.value();
|
||||
}
|
||||
|
||||
static const QByteArray* FindFirstTrophyMetadataFile(const QMap<QString, QByteArray>& files) {
|
||||
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
|
||||
if (it.key().startsWith(QStringLiteral("tropmeta_")) &&
|
||||
it.key().endsWith(QStringLiteral(".json"))) {
|
||||
return &it.value();
|
||||
}
|
||||
}
|
||||
|
||||
return FindFile(files, QStringLiteral("tropmeta.json"));
|
||||
}
|
||||
|
||||
static bool ReadJsonObject(const QByteArray& data, const QString& file_name, QJsonObject& object,
|
||||
QString& error) {
|
||||
QJsonParseError parse_error;
|
||||
const auto doc = QJsonDocument::fromJson(data, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
error = QObject::tr("Could not read %1: %2").arg(file_name, parse_error.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
object = doc.object();
|
||||
return true;
|
||||
}
|
||||
|
||||
static QString JsonString(const QJsonValue& value) {
|
||||
if (value.isString()) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
const double number = value.toDouble();
|
||||
if (number == static_cast<int>(number)) {
|
||||
return QString::number(static_cast<int>(number));
|
||||
}
|
||||
return QString::number(number);
|
||||
}
|
||||
if (value.isBool()) {
|
||||
return value.toBool() ? QStringLiteral("true") : QStringLiteral("false");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static QList<TrophyDefinition> ReadDefinitions(const QJsonObject& tropconf) {
|
||||
QList<TrophyDefinition> ret;
|
||||
|
||||
const auto trophies = tropconf.value(QStringLiteral("trophies")).toArray();
|
||||
for (const auto& value: trophies) {
|
||||
const auto obj = value.toObject();
|
||||
TrophyDefinition def;
|
||||
def.id = JsonString(obj.value(QStringLiteral("id"))).trimmed();
|
||||
def.grade = JsonString(obj.value(QStringLiteral("grade"))).trimmed();
|
||||
def.hidden = obj.value(QStringLiteral("hidden")).toBool(false);
|
||||
def.has_reward = obj.value(QStringLiteral("hasReward")).toBool(false);
|
||||
|
||||
if (!def.id.isEmpty()) {
|
||||
ret.append(def);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static QMap<QString, TrophyText> ReadMetadata(const QJsonObject& tropmeta) {
|
||||
QMap<QString, TrophyText> ret;
|
||||
|
||||
const auto metadata = tropmeta.value(QStringLiteral("metadata")).toObject();
|
||||
const auto trophies = metadata.value(QStringLiteral("trophyMetadata")).toArray();
|
||||
for (const auto& value: trophies) {
|
||||
const auto obj = value.toObject();
|
||||
const auto id = JsonString(obj.value(QStringLiteral("id"))).trimmed();
|
||||
if (id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TrophyText text;
|
||||
text.name = JsonString(obj.value(QStringLiteral("name"))).trimmed();
|
||||
text.detail = JsonString(obj.value(QStringLiteral("detail"))).trimmed();
|
||||
text.reward = JsonString(obj.value(QStringLiteral("reward"))).trimmed();
|
||||
ret.insert(id, text);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static QString GradeToText(const QString& grade) {
|
||||
if (grade == QStringLiteral("P")) {
|
||||
return QObject::tr("Platinum");
|
||||
}
|
||||
if (grade == QStringLiteral("G")) {
|
||||
return QObject::tr("Gold");
|
||||
}
|
||||
if (grade == QStringLiteral("S")) {
|
||||
return QObject::tr("Silver");
|
||||
}
|
||||
if (grade == QStringLiteral("B")) {
|
||||
return QObject::tr("Bronze");
|
||||
}
|
||||
|
||||
return grade;
|
||||
}
|
||||
|
||||
static QPixmap LoadTrophyIcon(const QMap<QString, QByteArray>& files, const QString& id) {
|
||||
QStringList names;
|
||||
names.append(QStringLiteral("trop%1.png").arg(id));
|
||||
|
||||
bool id_is_number = false;
|
||||
const int id_num = id.toInt(&id_is_number);
|
||||
if (id_is_number) {
|
||||
names.append(QStringLiteral("trop%1.png").arg(id_num, 4, 10, QLatin1Char('0')));
|
||||
}
|
||||
|
||||
for (const auto& name: names) {
|
||||
const auto* data = FindFile(files, name);
|
||||
if (data == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QPixmap pixmap;
|
||||
if (pixmap.loadFromData(*data)) {
|
||||
return pixmap;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static QString TrophyTabTitle(const QString& file_name) {
|
||||
static const QRegularExpression trophy_file_re(QStringLiteral("^trophy(\\d+)\\.ucp$"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
const auto name = QFileInfo(file_name).fileName();
|
||||
const auto match = trophy_file_re.match(name);
|
||||
if (match.hasMatch()) {
|
||||
return QObject::tr("Trophy %1").arg(match.captured(1));
|
||||
}
|
||||
|
||||
return QFileInfo(file_name).completeBaseName();
|
||||
}
|
||||
|
||||
static bool BuildTrophySet(const QString& ucp_file, TrophySet& set, QString& error) {
|
||||
QMap<QString, QByteArray> files;
|
||||
if (!ReadUcp(ucp_file, files, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* conf_data = FindFile(files, QStringLiteral("tropconf.json"));
|
||||
if (conf_data == nullptr) {
|
||||
error =
|
||||
QObject::tr("%1 does not contain tropconf.json.").arg(QFileInfo(ucp_file).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject tropconf;
|
||||
if (!ReadJsonObject(*conf_data, QStringLiteral("tropconf.json"), tropconf, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto default_language =
|
||||
JsonString(tropconf.value(QStringLiteral("defaultLanguage"))).trimmed();
|
||||
const QByteArray* meta_data = nullptr;
|
||||
if (!default_language.isEmpty()) {
|
||||
meta_data = FindFile(files, QStringLiteral("tropmeta_%1.json").arg(default_language));
|
||||
}
|
||||
if (meta_data == nullptr) {
|
||||
meta_data = FindFirstTrophyMetadataFile(files);
|
||||
}
|
||||
if (meta_data == nullptr) {
|
||||
error = QObject::tr("%1 does not contain readable trophy metadata.")
|
||||
.arg(QFileInfo(ucp_file).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject tropmeta;
|
||||
if (!ReadJsonObject(*meta_data, QStringLiteral("tropmeta.json"), tropmeta, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto definitions = ReadDefinitions(tropconf);
|
||||
if (definitions.isEmpty()) {
|
||||
error = QObject::tr("%1 does not define any trophies.").arg(QFileInfo(ucp_file).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto texts = ReadMetadata(tropmeta);
|
||||
|
||||
set.tab_title = TrophyTabTitle(ucp_file);
|
||||
for (const auto& def: definitions) {
|
||||
const auto text = texts.value(def.id);
|
||||
|
||||
TrophyRow row;
|
||||
row.id = def.id;
|
||||
row.grade = def.grade;
|
||||
row.hidden = def.hidden;
|
||||
row.has_reward = def.has_reward;
|
||||
row.reward = text.reward;
|
||||
row.name = text.name;
|
||||
row.detail = text.detail;
|
||||
row.icon = LoadTrophyIcon(files, def.id);
|
||||
|
||||
if (row.name.isEmpty()) {
|
||||
row.name =
|
||||
row.hidden ? QObject::tr("Hidden Trophy") : QObject::tr("Trophy %1").arg(def.id);
|
||||
}
|
||||
if (row.detail.isEmpty() && row.hidden) {
|
||||
row.detail = QObject::tr("This trophy is hidden.");
|
||||
}
|
||||
|
||||
set.trophies.append(row);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static QStringList FindTrophyFiles(const Configuration* info) {
|
||||
if (info == nullptr || info->basedir.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QDir trophy_dir(QDir(info->basedir).filePath(QStringLiteral("sce_sys/trophy2")));
|
||||
if (!trophy_dir.exists()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto files =
|
||||
trophy_dir.entryInfoList({QStringLiteral("Trophy*.ucp"), QStringLiteral("trophy*.ucp")},
|
||||
QDir::Files | QDir::NoSymLinks, QDir::Name | QDir::IgnoreCase);
|
||||
|
||||
QStringList trophy_files;
|
||||
QSet<QString> seen;
|
||||
for (const auto& file: files) {
|
||||
auto key = file.canonicalFilePath();
|
||||
if (key.isEmpty()) {
|
||||
key = file.absoluteFilePath();
|
||||
}
|
||||
key = QDir::cleanPath(key).toCaseFolded();
|
||||
|
||||
if (!seen.contains(key)) {
|
||||
seen.insert(key);
|
||||
trophy_files.append(file.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
return trophy_files;
|
||||
}
|
||||
|
||||
static void PrepareTable(QTableWidget* table) {
|
||||
table->setColumnCount(4);
|
||||
table->setHorizontalHeaderLabels({QObject::tr("Unlocked"), QObject::tr("Trophy"),
|
||||
QObject::tr("Name"), QObject::tr("Description")});
|
||||
table->setAlternatingRowColors(true);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setShowGrid(false);
|
||||
table->setWordWrap(true);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->verticalHeader()->setDefaultSectionSize(112);
|
||||
table->horizontalHeader()->setStretchLastSection(true);
|
||||
table->horizontalHeader()->setHighlightSections(false);
|
||||
table->setColumnWidth(0, 100);
|
||||
table->setColumnWidth(1, 132);
|
||||
table->setColumnWidth(2, 260);
|
||||
table->setColumnWidth(3, 480);
|
||||
}
|
||||
|
||||
static QTableWidgetItem* CreateItem(const QString& text) {
|
||||
auto* item = new QTableWidgetItem(text);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||
return item;
|
||||
}
|
||||
|
||||
static QString TrophyTooltip(const TrophyRow& row) {
|
||||
QStringList lines;
|
||||
lines.append(row.name);
|
||||
if (!row.detail.isEmpty()) {
|
||||
lines.append(row.detail);
|
||||
}
|
||||
|
||||
const auto grade = GradeToText(row.grade);
|
||||
if (!grade.isEmpty()) {
|
||||
lines.append(QObject::tr("Grade: %1").arg(grade));
|
||||
}
|
||||
if (row.hidden) {
|
||||
lines.append(QObject::tr("Hidden trophy"));
|
||||
}
|
||||
if (row.has_reward && !row.reward.isEmpty()) {
|
||||
lines.append(QObject::tr("Reward: %1").arg(row.reward));
|
||||
}
|
||||
|
||||
return lines.join(QLatin1Char('\n'));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TrophyViewerDialog::TrophyViewerDialog(QWidget* parent): QDialog(parent) {
|
||||
setWindowTitle(tr("Trophy Viewer"));
|
||||
resize(1000, 640);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
|
||||
m_tabs = new QTabWidget(this);
|
||||
layout->addWidget(m_tabs, 1);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
layout->addWidget(buttons);
|
||||
}
|
||||
|
||||
bool TrophyViewerDialog::HasTrophyData(const Configuration* info) {
|
||||
return !FindTrophyFiles(info).isEmpty();
|
||||
}
|
||||
|
||||
void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent) {
|
||||
if (info == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrophyViewerDialog dlg(parent);
|
||||
if (!info->name.isEmpty()) {
|
||||
dlg.setWindowTitle(tr("Trophy Viewer - %1").arg(info->name));
|
||||
}
|
||||
|
||||
QString error;
|
||||
if (!dlg.LoadGame(*info, error)) {
|
||||
QMessageBox::warning(parent, tr("Trophy Viewer"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
bool TrophyViewerDialog::LoadGame(const Configuration& info, QString& error) {
|
||||
const auto trophy_files = FindTrophyFiles(&info);
|
||||
if (trophy_files.isEmpty()) {
|
||||
error = tr("No trophy package found in sce_sys/trophy2.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList errors;
|
||||
for (const auto& file: trophy_files) {
|
||||
TrophySet set;
|
||||
QString set_error;
|
||||
if (!BuildTrophySet(file, set, set_error)) {
|
||||
errors.append(set_error);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* table = new QTableWidget(set.trophies.size(), 4, m_tabs);
|
||||
PrepareTable(table);
|
||||
|
||||
for (int row_index = 0; row_index < set.trophies.size(); row_index++) {
|
||||
const auto& row = set.trophies.at(row_index);
|
||||
table->setRowHeight(row_index, 112);
|
||||
|
||||
auto* status = CreateItem(tr("locked"));
|
||||
status->setTextAlignment(Qt::AlignCenter);
|
||||
if (row.hidden) {
|
||||
status->setForeground(QBrush(Qt::gray));
|
||||
}
|
||||
table->setItem(row_index, 0, status);
|
||||
|
||||
auto* icon_label = new QLabel(table);
|
||||
icon_label->setAlignment(Qt::AlignCenter);
|
||||
icon_label->setMinimumSize(QSize(108, 108));
|
||||
if (!row.icon.isNull()) {
|
||||
icon_label->setPixmap(
|
||||
row.icon.scaled(QSize(96, 96), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
table->setCellWidget(row_index, 1, icon_label);
|
||||
|
||||
auto* name = CreateItem(row.name);
|
||||
auto font = name->font();
|
||||
font.setBold(true);
|
||||
name->setFont(font);
|
||||
name->setToolTip(TrophyTooltip(row));
|
||||
table->setItem(row_index, 2, name);
|
||||
|
||||
auto* detail = CreateItem(row.detail);
|
||||
detail->setToolTip(TrophyTooltip(row));
|
||||
table->setItem(row_index, 3, detail);
|
||||
}
|
||||
|
||||
m_tabs->addTab(table, set.tab_title);
|
||||
}
|
||||
|
||||
if (m_tabs->count() == 0) {
|
||||
error = errors.isEmpty() ? tr("No readable trophy data found.")
|
||||
: errors.join(QLatin1Char('\n'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user