From f3e9c3ec8c328c5500c268a6936e9629fe9ba2f7 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 31 Dec 2019 16:17:17 +1000 Subject: [PATCH] Add initial work on Qt frontend --- .github/workflows/linux-build.yml | 4 +- CMakeLists.txt | 18 +- dep/msvc/vsprops/QtCompile.props | 116 +++++ dep/msvc/vsprops/QtCompile.targets | 10 + dep/msvc/vsprops/QtCompile.xml | 11 + duckstation.sln | 14 + src/CMakeLists.txt | 7 +- src/common/cd_image_cue.cpp | 2 +- src/core/game_list.cpp | 45 +- src/core/game_list.h | 16 +- src/duckstation-qt/CMakeLists.txt | 32 ++ src/duckstation-qt/consolesettingswidget.cpp | 8 + src/duckstation-qt/consolesettingswidget.h | 17 + src/duckstation-qt/consolesettingswidget.ui | 114 +++++ src/duckstation-qt/duckstation-qt.vcxproj | 426 +++++++++++++++++ .../duckstation-qt.vcxproj.filters | 40 ++ src/duckstation-qt/gamelistsettingswidget.cpp | 230 ++++++++++ src/duckstation-qt/gamelistsettingswidget.h | 35 ++ src/duckstation-qt/gamelistsettingswidget.ui | 150 ++++++ src/duckstation-qt/gamelistwidget.cpp | 164 +++++++ src/duckstation-qt/gamelistwidget.h | 36 ++ src/duckstation-qt/main.cpp | 28 ++ src/duckstation-qt/mainwindow.cpp | 207 +++++++++ src/duckstation-qt/mainwindow.h | 57 +++ src/duckstation-qt/mainwindow.ui | 368 +++++++++++++++ src/duckstation-qt/opengldisplaywindow.cpp | 430 ++++++++++++++++++ src/duckstation-qt/opengldisplaywindow.h | 75 +++ src/duckstation-qt/qthostinterface.cpp | 282 ++++++++++++ src/duckstation-qt/qthostinterface.h | 89 ++++ src/duckstation-qt/qtsettingsinterface.cpp | 142 ++++++ src/duckstation-qt/qtsettingsinterface.h | 31 ++ src/duckstation-qt/qtutils.cpp | 23 + src/duckstation-qt/qtutils.h | 12 + src/duckstation-qt/resources/icons.qrc | 29 ++ .../icons/applications-development.png | Bin 0 -> 2174 bytes .../resources/icons/applications-internet.png | Bin 0 -> 2464 bytes .../resources/icons/applications-other.png | Bin 0 -> 1490 bytes .../resources/icons/applications-system.png | Bin 0 -> 2544 bytes .../resources/icons/audio-card.png | Bin 0 -> 1985 bytes .../resources/icons/document-open.png | Bin 0 -> 1550 bytes .../resources/icons/document-save.png | Bin 0 -> 1971 bytes .../resources/icons/drive-optical.png | Bin 0 -> 1338 bytes .../resources/icons/drive-removable-media.png | Bin 0 -> 964 bytes src/duckstation-qt/resources/icons/duck.png | Bin 0 -> 14021 bytes .../resources/icons/duck_128.png | Bin 0 -> 8478 bytes .../resources/icons/duck_64.png | Bin 0 -> 3896 bytes .../resources/icons/edit-find.png | Bin 0 -> 1636 bytes .../resources/icons/folder-open.png | Bin 0 -> 1184 bytes .../resources/icons/input-gaming.png | Bin 0 -> 1470 bytes .../resources/icons/list-add.png | Bin 0 -> 601 bytes .../resources/icons/list-remove.png | Bin 0 -> 317 bytes .../resources/icons/media-flash.png | Bin 0 -> 1323 bytes .../resources/icons/media-optical.png | Bin 0 -> 2288 bytes .../resources/icons/media-playback-pause.png | Bin 0 -> 481 bytes .../resources/icons/media-playback-start.png | Bin 0 -> 1028 bytes .../resources/icons/system-search.png | Bin 0 -> 2215 bytes .../resources/icons/system-shutdown.png | Bin 0 -> 1055 bytes .../icons/utilities-system-monitor.png | Bin 0 -> 1886 bytes .../resources/icons/video-display.png | Bin 0 -> 1596 bytes .../resources/icons/view-fullscreen.png | Bin 0 -> 1256 bytes .../resources/icons/view-refresh.png | Bin 0 -> 2024 bytes src/duckstation-qt/settingsdialog.cpp | 42 ++ src/duckstation-qt/settingsdialog.h | 44 ++ src/duckstation-qt/settingsdialog.ui | 150 ++++++ 64 files changed, 3490 insertions(+), 14 deletions(-) create mode 100644 dep/msvc/vsprops/QtCompile.props create mode 100644 dep/msvc/vsprops/QtCompile.targets create mode 100644 dep/msvc/vsprops/QtCompile.xml create mode 100644 src/duckstation-qt/CMakeLists.txt create mode 100644 src/duckstation-qt/consolesettingswidget.cpp create mode 100644 src/duckstation-qt/consolesettingswidget.h create mode 100644 src/duckstation-qt/consolesettingswidget.ui create mode 100644 src/duckstation-qt/duckstation-qt.vcxproj create mode 100644 src/duckstation-qt/duckstation-qt.vcxproj.filters create mode 100644 src/duckstation-qt/gamelistsettingswidget.cpp create mode 100644 src/duckstation-qt/gamelistsettingswidget.h create mode 100644 src/duckstation-qt/gamelistsettingswidget.ui create mode 100644 src/duckstation-qt/gamelistwidget.cpp create mode 100644 src/duckstation-qt/gamelistwidget.h create mode 100644 src/duckstation-qt/main.cpp create mode 100644 src/duckstation-qt/mainwindow.cpp create mode 100644 src/duckstation-qt/mainwindow.h create mode 100644 src/duckstation-qt/mainwindow.ui create mode 100644 src/duckstation-qt/opengldisplaywindow.cpp create mode 100644 src/duckstation-qt/opengldisplaywindow.h create mode 100644 src/duckstation-qt/qthostinterface.cpp create mode 100644 src/duckstation-qt/qthostinterface.h create mode 100644 src/duckstation-qt/qtsettingsinterface.cpp create mode 100644 src/duckstation-qt/qtsettingsinterface.h create mode 100644 src/duckstation-qt/qtutils.cpp create mode 100644 src/duckstation-qt/qtutils.h create mode 100644 src/duckstation-qt/resources/icons.qrc create mode 100644 src/duckstation-qt/resources/icons/applications-development.png create mode 100644 src/duckstation-qt/resources/icons/applications-internet.png create mode 100644 src/duckstation-qt/resources/icons/applications-other.png create mode 100644 src/duckstation-qt/resources/icons/applications-system.png create mode 100644 src/duckstation-qt/resources/icons/audio-card.png create mode 100644 src/duckstation-qt/resources/icons/document-open.png create mode 100644 src/duckstation-qt/resources/icons/document-save.png create mode 100644 src/duckstation-qt/resources/icons/drive-optical.png create mode 100644 src/duckstation-qt/resources/icons/drive-removable-media.png create mode 100644 src/duckstation-qt/resources/icons/duck.png create mode 100644 src/duckstation-qt/resources/icons/duck_128.png create mode 100644 src/duckstation-qt/resources/icons/duck_64.png create mode 100644 src/duckstation-qt/resources/icons/edit-find.png create mode 100644 src/duckstation-qt/resources/icons/folder-open.png create mode 100644 src/duckstation-qt/resources/icons/input-gaming.png create mode 100644 src/duckstation-qt/resources/icons/list-add.png create mode 100644 src/duckstation-qt/resources/icons/list-remove.png create mode 100644 src/duckstation-qt/resources/icons/media-flash.png create mode 100644 src/duckstation-qt/resources/icons/media-optical.png create mode 100644 src/duckstation-qt/resources/icons/media-playback-pause.png create mode 100644 src/duckstation-qt/resources/icons/media-playback-start.png create mode 100644 src/duckstation-qt/resources/icons/system-search.png create mode 100644 src/duckstation-qt/resources/icons/system-shutdown.png create mode 100644 src/duckstation-qt/resources/icons/utilities-system-monitor.png create mode 100644 src/duckstation-qt/resources/icons/video-display.png create mode 100644 src/duckstation-qt/resources/icons/view-fullscreen.png create mode 100644 src/duckstation-qt/resources/icons/view-refresh.png create mode 100644 src/duckstation-qt/settingsdialog.cpp create mode 100644 src/duckstation-qt/settingsdialog.h create mode 100644 src/duckstation-qt/settingsdialog.ui diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 3c4690f28..34d0226a5 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -27,7 +27,7 @@ jobs: run: | mkdir build-debug cd build-debug - cmake -DCMAKE_BUILD_TYPE=Debug .. + cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_QT_FRONTEND=OFF .. make - name: Compile release build @@ -35,6 +35,6 @@ jobs: run: | mkdir build-release cd build-release - cmake -DCMAKE_BUILD_TYPE=Release .. + cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_QT_FRONTEND=OFF .. make diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b87e6975..3cc7b09ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,11 @@ project(duckstation C CXX) # Pull in modules. set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules/") +if(NOT ANDROID) + option(BUILD_SDL_FRONTEND "Build the SDL frontend" ON) + option(BUILD_QT_FRONTEND "Build the Qt frontend" ON) +endif() + # Common include/library directories on Windows. if(WIN32) @@ -30,8 +35,15 @@ endif() # Required libraries. if(NOT ANDROID) - find_package(SDL2 REQUIRED) -else() + if(BUILD_SDL_FRONTEND) + find_package(SDL2 REQUIRED) + endif() + if(BUILD_QT_FRONTEND) + find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) + endif() +endif() + +if(ANDROID) find_package(EGL REQUIRED) endif() @@ -117,4 +129,4 @@ add_subdirectory(src) if(ANDROID) add_subdirectory(android/app/src/cpp) -endif() \ No newline at end of file +endif() diff --git a/dep/msvc/vsprops/QtCompile.props b/dep/msvc/vsprops/QtCompile.props new file mode 100644 index 000000000..c2744d381 --- /dev/null +++ b/dep/msvc/vsprops/QtCompile.props @@ -0,0 +1,116 @@ + + + + $(SolutionDir)dep\msvc\qt5-x64\ + $(QTDIRDefault) + $(QTDIR)\ + false + true + $(QTDIR)include\ + $(QTDIR)lib\ + $(QTDIR)bin\ + $(QTDIR)plugins\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(QtToolOutDir)moc_ + d + $(QtDebugSuffix) + QtPlugins + + + + QT_NO_DEBUG;%(PreprocessorDefinitions) + $(ProjectDir);%(AdditionalIncludeDirectories) + $(QtToolOutDir);%(AdditionalIncludeDirectories) + $(QtIncludeDir);%(AdditionalIncludeDirectories) + + + $(QtLibDir);%(AdditionalLibraryDirectories) + qtmain$(QtLibSuffix).lib;Qt5Core$(QtLibSuffix).lib;Qt5Gui$(QtLibSuffix).lib;Qt5Widgets$(QtLibSuffix).lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -DQT_NO_DEBUG -DNDEBUG $(MocDefines) + + "-I$(QtIncludeDir)" "-I$(SolutionDir)src" -I. + + + + + + + + + + + + + + + + + + + + QtResource + + + QtUi + + + QtMoc + + + diff --git a/dep/msvc/vsprops/QtCompile.targets b/dep/msvc/vsprops/QtCompile.targets new file mode 100644 index 000000000..9ab1522a8 --- /dev/null +++ b/dep/msvc/vsprops/QtCompile.targets @@ -0,0 +1,10 @@ + + + + QtResourceClean;QtUiClean;QtMocClean;$(CleanDependsOn) + + + \ No newline at end of file diff --git a/dep/msvc/vsprops/QtCompile.xml b/dep/msvc/vsprops/QtCompile.xml new file mode 100644 index 000000000..976c2d2b8 --- /dev/null +++ b/dep/msvc/vsprops/QtCompile.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/duckstation.sln b/duckstation.sln index 04c9cd0c2..887207f59 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -27,6 +27,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "simpleini", "dep\simpleini\ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "tinyxml2", "dep\tinyxml2\tinyxml2.vcxproj", "{933118A9-68C5-47B4-B151-B03C93961623}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "duckstation-qt", "src\duckstation-qt\duckstation-qt.vcxproj", "{28F14272-0EC4-41BB-849F-182ADB81AF70}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -215,6 +217,18 @@ Global {933118A9-68C5-47B4-B151-B03C93961623}.ReleaseLTCG|x64.Build.0 = Release|x64 {933118A9-68C5-47B4-B151-B03C93961623}.ReleaseLTCG|x86.ActiveCfg = Release|Win32 {933118A9-68C5-47B4-B151-B03C93961623}.ReleaseLTCG|x86.Build.0 = Release|Win32 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.Debug|x64.ActiveCfg = Debug|x64 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.Debug|x86.ActiveCfg = Debug|Win32 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.Debug|x86.Build.0 = Debug|Win32 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.DebugFast|x64.ActiveCfg = DebugFast|x64 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.DebugFast|x86.ActiveCfg = DebugFast|Win32 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.DebugFast|x86.Build.0 = DebugFast|Win32 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.Release|x64.ActiveCfg = Release|x64 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.Release|x86.ActiveCfg = Release|Win32 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.Release|x86.Build.0 = Release|Win32 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32 + {28F14272-0EC4-41BB-849F-182ADB81AF70}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4c372b8d3..66cea8d2e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,11 @@ add_subdirectory(common) add_subdirectory(core) -if(NOT ANDROID) +if(BUILD_SDL_FRONTEND) add_subdirectory(duckstation) endif() + +if(BUILD_QT_FRONTEND) + add_subdirectory(duckstation-qt) +endif() + diff --git a/src/common/cd_image_cue.cpp b/src/common/cd_image_cue.cpp index 464ee21f6..1c536a412 100644 --- a/src/common/cd_image_cue.cpp +++ b/src/common/cd_image_cue.cpp @@ -35,7 +35,7 @@ static std::string GetPathDirectory(const char* path) const char* backslash_ptr = std::strrchr(path, '\\'); const char* slash_ptr; if (forwardslash_ptr && backslash_ptr) - slash_ptr = std::min(forwardslash_ptr, backslash_ptr); + slash_ptr = std::max(forwardslash_ptr, backslash_ptr); else if (backslash_ptr) slash_ptr = backslash_ptr; else if (forwardslash_ptr) diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 17c5ea016..9d2fb1e1e 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -4,6 +4,7 @@ #include "bios.h" #include "common/cd_image.h" #include "common/iso_reader.h" +#include "settings.h" #include #include #include @@ -194,11 +195,6 @@ std::optional GameList::GetRegionForPath(const char* image_path) return GetRegionForImage(cdi.get()); } -void GameList::AddDirectory(const char* path, bool recursive) -{ - ScanDirectory(path, recursive); -} - bool GameList::IsExeFileName(const char* path) { const char* extension = std::strrchr(path, '.'); @@ -404,6 +400,40 @@ private: GameList::DatabaseMap& m_database; }; +void GameList::AddDirectory(std::string path, bool recursive) +{ + auto iter = std::find_if(m_search_directories.begin(), m_search_directories.end(), + [&path](const DirectoryEntry& de) { return de.path == path; }); + if (iter != m_search_directories.end()) + { + iter->recursive = recursive; + return; + } + + m_search_directories.push_back({path, recursive}); +} + +void GameList::SetDirectoriesFromSettings(SettingsInterface& si) +{ + m_search_directories.clear(); + + std::vector dirs = si.GetStringList("GameList", "Paths"); + for (std::string& dir : dirs) + m_search_directories.push_back({std::move(dir), false}); + + dirs = si.GetStringList("GameList", "RecursivePaths"); + for (std::string& dir : dirs) + m_search_directories.push_back({std::move(dir), true}); +} + +void GameList::RescanAllDirectories() +{ + m_entries.clear(); + + for (const DirectoryEntry& de : m_search_directories) + ScanDirectory(de.path.c_str(), de.recursive); +} + bool GameList::ParseRedumpDatabase(const char* redump_dat_path) { tinyxml2::XMLDocument doc; @@ -427,3 +457,8 @@ bool GameList::ParseRedumpDatabase(const char* redump_dat_path) Log_InfoPrintf("Loaded %zu entries from Redump.org database '%s'", m_database.size(), redump_dat_path); return true; } + +void GameList::ClearDatabase() +{ + m_database.clear(); +} diff --git a/src/core/game_list.h b/src/core/game_list.h index 6f797cb8b..6a2b3eede 100644 --- a/src/core/game_list.h +++ b/src/core/game_list.h @@ -8,6 +8,8 @@ class CDImage; +class SettingsInterface; + class GameList { public: @@ -54,18 +56,28 @@ public: const EntryList& GetEntries() const { return m_entries; } const u32 GetEntryCount() const { return static_cast(m_entries.size()); } - void AddDirectory(const char* path, bool recursive); + void AddDirectory(std::string path, bool recursive); + void SetDirectoriesFromSettings(SettingsInterface& si); + void RescanAllDirectories(); bool ParseRedumpDatabase(const char* redump_dat_path); + void ClearDatabase(); private: + struct DirectoryEntry + { + std::string path; + bool recursive; + }; + static bool IsExeFileName(const char* path); static bool GetExeListEntry(const char* path, GameListEntry* entry); bool GetGameListEntry(const char* path, GameListEntry* entry); - void ScanDirectory(const char* path, bool recursive); DatabaseMap m_database; EntryList m_entries; + + std::vector m_search_directories; }; diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt new file mode 100644 index 000000000..eec9bf139 --- /dev/null +++ b/src/duckstation-qt/CMakeLists.txt @@ -0,0 +1,32 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +add_executable(duckstation-qt + resources/icons.qrc + consolesettingswidget.cpp + consolesettingswidget.h + consolesettingswidget.ui + gamelistsettingswidget.cpp + gamelistsettingswidget.h + gamelistsettingswidget.ui + gamelistwidget.cpp + gamelistwidget.h + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui + opengldisplaywindow.cpp + opengldisplaywindow.h + qthostinterface.cpp + qthostinterface.h + qtsettingsinterface.cpp + qtsettingsinterface.h + qtutils.cpp + qtutils.h + settingsdialog.cpp + settingsdialog.h + settingsdialog.ui +) + +target_link_libraries(duckstation-qt PRIVATE YBaseLib core common imgui glad Qt5::Core Qt5::Gui Qt5::Widgets) diff --git a/src/duckstation-qt/consolesettingswidget.cpp b/src/duckstation-qt/consolesettingswidget.cpp new file mode 100644 index 000000000..93462083a --- /dev/null +++ b/src/duckstation-qt/consolesettingswidget.cpp @@ -0,0 +1,8 @@ +#include "consolesettingswidget.h" + +ConsoleSettingsWidget::ConsoleSettingsWidget(QWidget* parent /*= nullptr*/) : QWidget(parent) +{ + m_ui.setupUi(this); +} + +ConsoleSettingsWidget::~ConsoleSettingsWidget() = default; diff --git a/src/duckstation-qt/consolesettingswidget.h b/src/duckstation-qt/consolesettingswidget.h new file mode 100644 index 000000000..2ec8bb05b --- /dev/null +++ b/src/duckstation-qt/consolesettingswidget.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "ui_consolesettingswidget.h" + +class ConsoleSettingsWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ConsoleSettingsWidget(QWidget* parent = nullptr); + ~ConsoleSettingsWidget(); + +private: + Ui::ConsoleSettingsWidget m_ui; +}; diff --git a/src/duckstation-qt/consolesettingswidget.ui b/src/duckstation-qt/consolesettingswidget.ui new file mode 100644 index 000000000..cfe94d238 --- /dev/null +++ b/src/duckstation-qt/consolesettingswidget.ui @@ -0,0 +1,114 @@ + + + ConsoleSettingsWidget + + + + 0 + 0 + 502 + 308 + + + + Form + + + + 0 + + + 0 + + + 0 + + + + + + + Region: + + + + + + + + Auto-Detect + + + + + NTSC-U (US) + + + + + NTSC-J (Japan) + + + + + PAL (Europe, Australia) + + + + + + + + BIOS Path: + + + + + + + + + + + + ... + + + + + + + + + Enable TTY Output + + + + + + + Fast Boot + + + + + + + Enable Speed Limiter + + + + + + + Pause On Start + + + + + + + + + + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj new file mode 100644 index 000000000..3f88d915b --- /dev/null +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -0,0 +1,426 @@ + + + + + DebugFast + Win32 + + + DebugFast + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseLTCG + Win32 + + + ReleaseLTCG + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + {43540154-9e1e-409c-834f-b84be5621388} + + + {bb08260f-6fbc-46af-8924-090ee71360c6} + + + {b56ce698-7300-4fa5-9609-942f1d05c5a2} + + + {ee054e08-3799-4a59-a422-18259c105ffd} + + + {868b98c8-65a1-494b-8346-250a73a48c0a} + + + + + Document + + + Document + + + Document + + + Document + + + + + Document + + + + + + + + + + + + + + {28F14272-0EC4-41BB-849F-182ADB81AF70} + Win32Proj + duckstation-qt + 10.0 + + + + Application + true + v142 + NotSet + + + Application + true + v142 + NotSet + + + Application + true + v142 + NotSet + + + Application + true + v142 + NotSet + + + Application + false + v142 + true + NotSet + + + Application + false + v142 + true + NotSet + + + Application + false + v142 + true + NotSet + + + Application + false + v142 + true + NotSet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)bin\$(Platform)\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + true + $(SolutionDir)bin\$(Platform)\ + + + true + $(SolutionDir)bin\$(Platform)\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + true + $(SolutionDir)bin\$(Platform)\ + + + false + $(SolutionDir)bin\$(Platform)\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + + + false + $(SolutionDir)bin\$(Platform)\ + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + false + $(SolutionDir)bin\$(Platform)\ + + + $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\ + $(ProjectName)-$(Platform)-$(Configuration) + false + $(SolutionDir)bin\$(Platform)\ + + + + + + Level4 + Disabled + _CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) + true + false + stdcpp17 + + + Console + true + $(SolutionDir)dep\msvc\lib32-debug;$(SolutionDir)dep\msvc\qt5-x86\lib;%(AdditionalLibraryDirectories) + Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;%(AdditionalDependencies) + + + + + + + Level4 + Disabled + _CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) + true + false + stdcpp17 + + + Console + true + $(SolutionDir)dep\msvc\lib64-debug;$(SolutionDir)dep\msvc\qt5-x64\lib;%(AdditionalLibraryDirectories) + Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;%(AdditionalDependencies) + + + + + + + Level4 + Disabled + _ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) + Default + true + false + stdcpp17 + false + + + Console + true + $(SolutionDir)dep\msvc\lib32-debug;$(SolutionDir)dep\msvc\qt5-x86\lib;%(AdditionalLibraryDirectories) + Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;%(AdditionalDependencies) + + + + + + + Level4 + Disabled + _ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) + Default + true + false + stdcpp17 + false + + + Console + true + $(SolutionDir)dep\msvc\lib64-debug;$(SolutionDir)dep\msvc\qt5-x64\lib;%(AdditionalLibraryDirectories) + Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;%(AdditionalDependencies) + + + + + Level4 + + + MaxSpeed + true + _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) + true + false + stdcpp17 + + + Console + true + true + true + $(SolutionDir)dep\msvc\lib32;$(SolutionDir)dep\msvc\qt5-x86\lib;%(AdditionalLibraryDirectories) + Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;%(AdditionalDependencies) + + + + + Level4 + + + MaxSpeed + true + _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) + true + true + stdcpp17 + true + + + Console + true + true + true + $(SolutionDir)dep\msvc\lib32;$(SolutionDir)dep\msvc\qt5-x86\lib;%(AdditionalLibraryDirectories) + Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + Level4 + + + MaxSpeed + true + _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) + true + false + stdcpp17 + + + Console + true + true + true + $(SolutionDir)dep\msvc\lib64;$(SolutionDir)dep\msvc\qt5-x64\lib;%(AdditionalLibraryDirectories) + Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;%(AdditionalDependencies) + + + + + Level4 + + + MaxSpeed + true + _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) + true + true + stdcpp17 + true + + + Console + true + true + true + $(SolutionDir)dep\msvc\lib64;$(SolutionDir)dep\msvc\qt5-x64\lib;%(AdditionalLibraryDirectories) + Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + \ No newline at end of file diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters new file mode 100644 index 000000000..c10cb7a9f --- /dev/null +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + resources + + + + + {3b2587ae-ce3b-4eb5-ada2-237e853620cf} + + + \ No newline at end of file diff --git a/src/duckstation-qt/gamelistsettingswidget.cpp b/src/duckstation-qt/gamelistsettingswidget.cpp new file mode 100644 index 000000000..ec47b9028 --- /dev/null +++ b/src/duckstation-qt/gamelistsettingswidget.cpp @@ -0,0 +1,230 @@ +#include "gamelistsettingswidget.h" +#include "qthostinterface.h" +#include "qtutils.h" +#include +#include +#include +#include +#include +#include + +class GameListSearchDirectoriesModel : public QAbstractTableModel +{ +public: + GameListSearchDirectoriesModel(QSettings& settings) : m_settings(settings) {} + + ~GameListSearchDirectoriesModel() = default; + + int columnCount(const QModelIndex& parent) const override + { + if (parent.isValid()) + return 0; + + return 2; + } + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override + { + if (orientation != Qt::Horizontal || role != Qt::DisplayRole) + return {}; + + if (section == 0) + return tr("Path"); + else + return tr("Recursive"); + } + + int rowCount(const QModelIndex& parent) const override + { + if (parent.isValid()) + return 0; + + return static_cast(m_entries.size()); + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) + return {}; + + const int row = index.row(); + const int column = index.column(); + if (row < 0 || row >= static_cast(m_entries.size())) + return {}; + + const Entry& entry = m_entries[row]; + if (role == Qt::CheckStateRole) + { + if (column == 1) + return entry.recursive ? Qt::Checked : Qt::Unchecked; + } + else if (role == Qt::DisplayRole) + { + if (column == 0) + return entry.path; + } + + return {}; + } + + void addEntry(const QString& path, bool recursive) + { + if (std::find_if(m_entries.begin(), m_entries.end(), [path](const Entry& e) { return e.path == path; }) != + m_entries.end()) + { + return; + } + + beginInsertRows(QModelIndex(), static_cast(m_entries.size()), static_cast(m_entries.size() + 1)); + m_entries.push_back({path, recursive}); + endInsertRows(); + } + + void removeEntry(int row) + { + if (row < 0 || row >= static_cast(m_entries.size())) + return; + + beginRemoveRows(QModelIndex(), row, row); + m_entries.erase(m_entries.begin() + row); + endRemoveRows(); + } + + void loadFromSettings() + { + QStringList path_list = m_settings.value(QStringLiteral("GameList/Paths")).toStringList(); + for (QString& entry : path_list) + m_entries.push_back({std::move(entry), false}); + + path_list = m_settings.value(QStringLiteral("GameList/RecursivePaths")).toStringList(); + for (QString& entry : path_list) + m_entries.push_back({std::move(entry), true}); + } + + void saveToSettings() + { + QStringList paths; + QStringList recursive_paths; + + for (const Entry& entry : m_entries) + { + if (entry.recursive) + recursive_paths.push_back(entry.path); + else + paths.push_back(entry.path); + } + + if (paths.empty()) + m_settings.remove(QStringLiteral("GameList/Paths")); + else + m_settings.setValue(QStringLiteral("GameList/Paths"), paths); + + if (recursive_paths.empty()) + m_settings.remove(QStringLiteral("GameList/RecursivePaths")); + else + m_settings.setValue(QStringLiteral("GameList/RecursivePaths"), recursive_paths); + } + +private: + struct Entry + { + QString path; + bool recursive; + }; + + QSettings& m_settings; + std::vector m_entries; +}; + +GameListSettingsWidget::GameListSettingsWidget(QtHostInterface* host_interface, QWidget* parent /* = nullptr */) + : QWidget(parent), m_host_interface(host_interface) +{ + m_ui.setupUi(this); + + QSettings& qsettings = host_interface->getQSettings(); + + m_search_directories_model = new GameListSearchDirectoriesModel(qsettings); + m_search_directories_model->loadFromSettings(); + m_ui.redumpDatabasePath->setText(qsettings.value("GameList/RedumpDatabasePath").toString()); + m_ui.searchDirectoryList->setModel(m_search_directories_model); + m_ui.searchDirectoryList->setSelectionMode(QAbstractItemView::SingleSelection); + m_ui.searchDirectoryList->setSelectionBehavior(QAbstractItemView::SelectRows); + m_ui.searchDirectoryList->setAlternatingRowColors(true); + m_ui.searchDirectoryList->setShowGrid(false); + m_ui.searchDirectoryList->verticalHeader()->hide(); + m_ui.searchDirectoryList->setCurrentIndex({}); + + connect(m_ui.addSearchDirectoryButton, &QToolButton::pressed, this, + &GameListSettingsWidget::onAddSearchDirectoryButtonPressed); + connect(m_ui.removeSearchDirectoryButton, &QToolButton::pressed, this, + &GameListSettingsWidget::onRemoveSearchDirectoryButtonPressed); + connect(m_ui.refreshGameListButton, &QToolButton::pressed, this, + &GameListSettingsWidget::onRefreshGameListButtonPressed); + connect(m_ui.browseRedumpPath, &QToolButton::pressed, this, &GameListSettingsWidget::onBrowseRedumpPathButtonPressed); + connect(m_ui.downloadRedumpDatabase, &QToolButton::pressed, this, + &GameListSettingsWidget::onDownloadRedumpDatabaseButtonPressed); +} + +GameListSettingsWidget::~GameListSettingsWidget() = default; + +void GameListSettingsWidget::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + + QtUtils::ResizeColumnsForTableView(m_ui.searchDirectoryList, {-1, 100}); +} + +void GameListSettingsWidget::onAddSearchDirectoryButtonPressed() +{ + QString dir = QFileDialog::getExistingDirectory(this, tr("Select Search Directory")); + if (dir.isEmpty()) + return; + + QMessageBox::StandardButton selection = + QMessageBox::question(this, tr("Scan Recursively?"), + tr("Would you like to scan the directory \"%1\" recursively?\n\nScanning recursively takes " + "more time, but will identify files in subdirectories.") + .arg(dir), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (selection == QMessageBox::Cancel) + return; + + const bool recursive = (selection == QMessageBox::Yes); + m_search_directories_model->addEntry(dir, recursive); + m_search_directories_model->saveToSettings(); + m_host_interface->refreshGameList(false); +} + +void GameListSettingsWidget::onRemoveSearchDirectoryButtonPressed() +{ + QModelIndexList selection = m_ui.searchDirectoryList->selectionModel()->selectedIndexes(); + if (selection.size() < 1) + return; + + const int row = selection[0].row(); + m_search_directories_model->removeEntry(row); + m_search_directories_model->saveToSettings(); + m_host_interface->refreshGameList(false); +} + +void GameListSettingsWidget::onRefreshGameListButtonPressed() +{ + m_host_interface->refreshGameList(true); +} + +void GameListSettingsWidget::onBrowseRedumpPathButtonPressed() +{ + QString filename = QFileDialog::getOpenFileName(this, tr("Select Redump Database File"), QString(), + tr("Redump Database Files (*.dat)")); + if (filename.isEmpty()) + return; + + m_ui.redumpDatabasePath->setText(filename); + m_host_interface->getQSettings().setValue("GameList/RedumpDatabasePath", filename); + m_host_interface->updateGameListDatabase(true); +} + +void GameListSettingsWidget::onDownloadRedumpDatabaseButtonPressed() +{ + QMessageBox::information(this, tr("TODO"), tr("TODO")); +} diff --git a/src/duckstation-qt/gamelistsettingswidget.h b/src/duckstation-qt/gamelistsettingswidget.h new file mode 100644 index 000000000..ab7a20f2d --- /dev/null +++ b/src/duckstation-qt/gamelistsettingswidget.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include "ui_gamelistsettingswidget.h" + +class QtHostInterface; + +class GameListSearchDirectoriesModel; + +class GameListSettingsWidget : public QWidget +{ + Q_OBJECT + +public: + GameListSettingsWidget(QtHostInterface* host_interface, QWidget* parent = nullptr); + ~GameListSettingsWidget(); + +private Q_SLOTS: + void onAddSearchDirectoryButtonPressed(); + void onRemoveSearchDirectoryButtonPressed(); + void onRefreshGameListButtonPressed(); + void onBrowseRedumpPathButtonPressed(); + void onDownloadRedumpDatabaseButtonPressed(); + +protected: + void resizeEvent(QResizeEvent* event); + +private: + QtHostInterface* m_host_interface; + + Ui::GameListSettingsWidget m_ui; + + GameListSearchDirectoriesModel* m_search_directories_model = nullptr; +}; diff --git a/src/duckstation-qt/gamelistsettingswidget.ui b/src/duckstation-qt/gamelistsettingswidget.ui new file mode 100644 index 000000000..fe52a8969 --- /dev/null +++ b/src/duckstation-qt/gamelistsettingswidget.ui @@ -0,0 +1,150 @@ + + + GameListSettingsWidget + + + + 0 + 0 + 527 + 376 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Search Directories + + + + + + + + + + + + Add + + + + :/icons/list-add.png:/icons/list-add.png + + + Qt::ToolButtonTextBesideIcon + + + + + + + Remove + + + + :/icons/list-remove.png:/icons/list-remove.png + + + Qt::ToolButtonTextBesideIcon + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh + + + + :/icons/view-refresh.png:/icons/view-refresh.png + + + Qt::ToolButtonTextBesideIcon + + + + + + + + + Redump Database Path + + + + + + + + + + + + Browse... + + + + :/icons/system-search.png:/icons/system-search.png + + + Qt::ToolButtonTextBesideIcon + + + false + + + + + + + Download... + + + + :/icons/applications-internet.png:/icons/applications-internet.png + + + + + + + + + + + + + + diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp new file mode 100644 index 000000000..9d7c0abfc --- /dev/null +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -0,0 +1,164 @@ +#include "gamelistwidget.h" +#include "core/settings.h" +#include "qthostinterface.h" +#include "qtutils.h" +#include + +class GameListModel : public QAbstractTableModel +{ +public: + enum Column : int + { + // Column_Icon, + Column_Code, + Column_Title, + Column_Region, + Column_Size, + + Column_Count + }; + + GameListModel(GameList* game_list, QObject* parent = nullptr) + : QAbstractTableModel(parent), m_game_list(game_list), m_size(static_cast(m_game_list->GetEntryCount())) + { + } + ~GameListModel() = default; + + int rowCount(const QModelIndex& parent = QModelIndex()) const override + { + if (parent.isValid()) + return 0; + + return static_cast(m_game_list->GetEntryCount()); + } + + int columnCount(const QModelIndex& parent = QModelIndex()) const override + { + if (parent.isValid()) + return 0; + + return Column_Count; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) + return {}; + + if (role != Qt::DisplayRole) + return {}; + + const int row = index.row(); + if (row < 0 || row >= static_cast(m_game_list->GetEntryCount())) + return {}; + + const GameList::GameListEntry& ge = m_game_list->GetEntries()[row]; + switch (index.column()) + { + case Column_Code: + return QString::fromStdString(ge.code); + + case Column_Title: + return QString::fromStdString(ge.title); + + case Column_Region: + return QString(Settings::GetConsoleRegionName(ge.region)); + + case Column_Size: + return QString("%1 MB").arg(static_cast(ge.total_size) / 1048576.0, 0, 'f', 2); + + default: + return {}; + } + } + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override + { + if (orientation != Qt::Horizontal || role != Qt::DisplayRole) + return {}; + + switch (section) + { + case Column_Code: + return "Code"; + + case Column_Title: + return "Title"; + + case Column_Region: + return "Region"; + + case Column_Size: + return "Size"; + + default: + return {}; + } + } + + void refresh() + { + if (m_size > 0) + { + beginRemoveRows(QModelIndex(), 0, m_size - 1); + endRemoveRows(); + } + + m_size = static_cast(m_game_list->GetEntryCount()); + beginInsertRows(QModelIndex(), 0, m_size); + endInsertRows(); + } + +private: + GameList* m_game_list; + int m_size; +}; + +GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QStackedWidget(parent) {} + +GameListWidget::~GameListWidget() = default; + +void GameListWidget::initialize(QtHostInterface* host_interface) +{ + m_host_interface = host_interface; + m_game_list = host_interface->getGameList(); + + connect(m_host_interface, &QtHostInterface::gameListRefreshed, this, &GameListWidget::onGameListRefreshed); + + m_table_model = new GameListModel(m_game_list, this); + m_table_view = new QTableView(this); + m_table_view->setModel(m_table_model); + m_table_view->setSelectionMode(QAbstractItemView::SingleSelection); + m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table_view->setAlternatingRowColors(true); + m_table_view->setShowGrid(false); + m_table_view->setCurrentIndex({}); + m_table_view->verticalHeader()->hide(); + m_table_view->resizeColumnsToContents(); + + connect(m_table_view, &QTableView::doubleClicked, this, &GameListWidget::onTableViewItemDoubleClicked); + + insertWidget(0, m_table_view); + setCurrentIndex(0); +} + +void GameListWidget::onGameListRefreshed() +{ + m_table_model->refresh(); +} + +void GameListWidget::onTableViewItemDoubleClicked(const QModelIndex& index) +{ + if (!index.isValid() || index.row() >= static_cast(m_game_list->GetEntryCount())) + return; + + const GameList::GameListEntry& entry = m_game_list->GetEntries().at(index.row()); + emit bootEntryRequested(entry); +} + +void GameListWidget::resizeEvent(QResizeEvent* event) +{ + QStackedWidget::resizeEvent(event); + + QtUtils::ResizeColumnsForTableView(m_table_view, {100, -1, 100, 100}); +} diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h new file mode 100644 index 000000000..864643a44 --- /dev/null +++ b/src/duckstation-qt/gamelistwidget.h @@ -0,0 +1,36 @@ +#pragma once +#include "core/game_list.h" +#include +#include + +class GameListModel; + +class QtHostInterface; + +class GameListWidget : public QStackedWidget +{ + Q_OBJECT + +public: + GameListWidget(QWidget* parent = nullptr); + ~GameListWidget(); + + void initialize(QtHostInterface* host_interface); + +Q_SIGNALS: + void bootEntryRequested(const GameList::GameListEntry& entry); + +private Q_SLOTS: + void onGameListRefreshed(); + void onTableViewItemDoubleClicked(const QModelIndex& index); + +protected: + void resizeEvent(QResizeEvent* event); + +private: + QtHostInterface* m_host_interface = nullptr; + GameList* m_game_list = nullptr; + + GameListModel* m_table_model = nullptr; + QTableView* m_table_view = nullptr; +}; diff --git a/src/duckstation-qt/main.cpp b/src/duckstation-qt/main.cpp new file mode 100644 index 000000000..de0189f1b --- /dev/null +++ b/src/duckstation-qt/main.cpp @@ -0,0 +1,28 @@ +#include "YBaseLib/Log.h" +#include "mainwindow.h" +#include "qthostinterface.h" +#include +#include + +static void InitLogging() +{ + // set log flags + // g_pLog->SetConsoleOutputParams(true); + g_pLog->SetConsoleOutputParams(true, nullptr, LOGLEVEL_PROFILE); + g_pLog->SetFilterLevel(LOGLEVEL_PROFILE); + // g_pLog->SetDebugOutputParams(true); +} + +int main(int argc, char* argv[]) +{ + InitLogging(); + + QApplication app(argc, argv); + + std::unique_ptr host_interface = std::make_unique(); + + std::unique_ptr window = std::make_unique(host_interface.get()); + window->show(); + + return app.exec(); +} diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp new file mode 100644 index 000000000..b7d718f60 --- /dev/null +++ b/src/duckstation-qt/mainwindow.cpp @@ -0,0 +1,207 @@ +#include "mainwindow.h" +#include "core/game_list.h" +#include "core/settings.h" +#include "gamelistwidget.h" +#include "qthostinterface.h" +#include "qtsettingsinterface.h" +#include "settingsdialog.h" +#include + +static constexpr char DISC_IMAGE_FILTER[] = + "All File Types (*.bin *.img *.cue *.exe *.psexe);;Single-Track Raw Images (*.bin *.img);;Cue Sheets " + "(*.cue);;PlayStation Executables (*.exe *.psexe)"; + +MainWindow::MainWindow(QtHostInterface* host_interface) : QMainWindow(nullptr), m_host_interface(host_interface) +{ + m_ui.setupUi(this); + setupAdditionalUi(); + connectSignals(); + + // force a scan of the game list + m_host_interface->refreshGameList(); + + resize(750, 690); +} + +MainWindow::~MainWindow() +{ + m_host_interface->destroyDisplayWidget(); +} + +void MainWindow::onEmulationStarting() +{ + switchToEmulationView(); + updateEmulationActions(true, false); + + // we need the surface visible.. + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); +} + +void MainWindow::onEmulationStarted() +{ + updateEmulationActions(false, true); + m_emulation_running = true; +} + +void MainWindow::onEmulationStopped() +{ + updateEmulationActions(false, false); + switchToGameListView(); + m_emulation_running = false; +} + +void MainWindow::onEmulationPaused(bool paused) +{ + m_ui.actionPause->setChecked(paused); +} + +void MainWindow::onStartDiscActionTriggered() +{ + QString filename = + QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr); + if (filename.isEmpty()) + return; + + m_host_interface->bootSystem(std::move(filename), QString()); +} + +void MainWindow::onChangeDiscActionTriggered() +{ + QMenu menu(tr("Change Disc..."), this); + QAction* from_file = menu.addAction(tr("From File...")); + QAction* from_game_list = menu.addAction(tr("From Game List")); + + QAction* selected = menu.exec(QCursor::pos()); + if (selected == from_file) + { + QString filename = + QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr); + if (filename.isEmpty()) + return; + + m_host_interface->changeDisc(filename); + } + else if (selected == from_game_list) + { + m_host_interface->pauseSystem(true); + switchToGameListView(); + } +} + +void MainWindow::onStartBiosActionTriggered() +{ + m_host_interface->bootSystem(QString(), QString()); +} + +void MainWindow::onOpenDirectoryActionTriggered() {} + +void MainWindow::onExitActionTriggered() {} + +void MainWindow::onFullscreenActionToggled(bool fullscreen) {} + +void MainWindow::onGitHubRepositoryActionTriggered() {} + +void MainWindow::onIssueTrackerActionTriggered() {} + +void MainWindow::onAboutActionTriggered() {} + +void MainWindow::setupAdditionalUi() +{ + m_game_list_widget = new GameListWidget(m_ui.mainContainer); + m_game_list_widget->initialize(m_host_interface); + m_ui.mainContainer->insertWidget(0, m_game_list_widget); + + QWidget* display_widget = m_host_interface->createDisplayWidget(m_ui.mainContainer); + m_ui.mainContainer->insertWidget(1, display_widget); + + m_ui.mainContainer->setCurrentIndex(0); +} + +void MainWindow::updateEmulationActions(bool starting, bool running) +{ + m_ui.actionStartDisc->setDisabled(starting || running); + m_ui.actionStartBios->setDisabled(starting || running); + m_ui.actionOpenDirectory->setDisabled(starting || running); + m_ui.actionPowerOff->setDisabled(starting || running); + + m_ui.actionPowerOff->setDisabled(starting || !running); + m_ui.actionReset->setDisabled(starting || !running); + m_ui.actionPause->setDisabled(starting || !running); + m_ui.actionChangeDisc->setDisabled(starting || !running); + + m_ui.actionLoadState->setDisabled(starting); + m_ui.actionSaveState->setDisabled(starting); + + m_ui.actionFullscreen->setDisabled(starting || !running); +} + +void MainWindow::switchToGameListView() +{ + m_ui.mainContainer->setCurrentIndex(0); +} + +void MainWindow::switchToEmulationView() +{ + m_ui.mainContainer->setCurrentIndex(1); +} + +void MainWindow::connectSignals() +{ + updateEmulationActions(false, false); + onEmulationPaused(false); + + connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered); + connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBiosActionTriggered); + connect(m_ui.actionChangeDisc, &QAction::triggered, this, &MainWindow::onChangeDiscActionTriggered); + connect(m_ui.actionOpenDirectory, &QAction::triggered, this, &MainWindow::onOpenDirectoryActionTriggered); + connect(m_ui.actionPowerOff, &QAction::triggered, m_host_interface, &QtHostInterface::powerOffSystem); + connect(m_ui.actionReset, &QAction::triggered, m_host_interface, &QtHostInterface::resetSystem); + connect(m_ui.actionPause, &QAction::toggled, m_host_interface, &QtHostInterface::pauseSystem); + connect(m_ui.actionExit, &QAction::triggered, this, &MainWindow::onExitActionTriggered); + connect(m_ui.actionFullscreen, &QAction::toggled, this, &MainWindow::onFullscreenActionToggled); + connect(m_ui.actionSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::Count); }); + connect(m_ui.actionGameListSettings, &QAction::triggered, + [this]() { doSettings(SettingsDialog::Category::GameListSettings); }); + connect(m_ui.actionCPUSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::CPUSettings); }); + connect(m_ui.actionGPUSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::GPUSettings); }); + connect(m_ui.actionAudioSettings, &QAction::triggered, + [this]() { doSettings(SettingsDialog::Category::AudioSettings); }); + connect(m_ui.actionGitHubRepository, &QAction::triggered, this, &MainWindow::onGitHubRepositoryActionTriggered); + connect(m_ui.actionIssueTracker, &QAction::triggered, this, &MainWindow::onIssueTrackerActionTriggered); + connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); + + connect(m_host_interface, &QtHostInterface::emulationStarting, this, &MainWindow::onEmulationStarting); + connect(m_host_interface, &QtHostInterface::emulationStarted, this, &MainWindow::onEmulationStarted); + connect(m_host_interface, &QtHostInterface::emulationStopped, this, &MainWindow::onEmulationStopped); + connect(m_host_interface, &QtHostInterface::emulationPaused, this, &MainWindow::onEmulationPaused); + + connect(m_game_list_widget, &GameListWidget::bootEntryRequested, [this](const GameList::GameListEntry& entry) { + // if we're not running, boot the system, otherwise swap discs + QString path = QString::fromStdString(entry.path); + if (!m_emulation_running) + { + m_host_interface->bootSystem(path, QString()); + } + else + { + m_host_interface->changeDisc(path); + m_host_interface->pauseSystem(false); + switchToEmulationView(); + } + }); +} + +void MainWindow::doSettings(SettingsDialog::Category category) +{ + if (!m_settings_dialog) + m_settings_dialog = new SettingsDialog(m_host_interface, this); + + if (!m_settings_dialog->isVisible()) + { + m_settings_dialog->setModal(false); + m_settings_dialog->show(); + } + + if (category != SettingsDialog::Category::Count) + m_settings_dialog->setCategory(category); +} \ No newline at end of file diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h new file mode 100644 index 000000000..c0c7dd8d2 --- /dev/null +++ b/src/duckstation-qt/mainwindow.h @@ -0,0 +1,57 @@ +#pragma once +#include +#include +#include +#include + +#include "settingsdialog.h" +#include "ui_mainwindow.h" + +class GameList; +class GameListWidget; +class QtHostInterface; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QtHostInterface* host_interface); + ~MainWindow(); + +public Q_SLOTS: + void onEmulationStarting(); + void onEmulationStarted(); + void onEmulationStopped(); + void onEmulationPaused(bool paused); + + void onStartDiscActionTriggered(); + void onChangeDiscActionTriggered(); + void onStartBiosActionTriggered(); + void onOpenDirectoryActionTriggered(); + void onExitActionTriggered(); + void onFullscreenActionToggled(bool fullscreen); + void onGitHubRepositoryActionTriggered(); + void onIssueTrackerActionTriggered(); + void onAboutActionTriggered(); + +private: + void createGameList(); + void setupAdditionalUi(); + void connectSignals(); + void updateEmulationActions(bool starting, bool running); + void switchToGameListView(); + void switchToEmulationView(); + void doSettings(SettingsDialog::Category category = SettingsDialog::Category::Count); + + Ui::MainWindow m_ui; + + QtHostInterface* m_host_interface = nullptr; + + std::unique_ptr m_game_list; + GameListWidget* m_game_list_widget = nullptr; + + SettingsDialog* m_settings_dialog = nullptr; + + bool m_emulation_running = false; +}; diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui new file mode 100644 index 000000000..70c5b5438 --- /dev/null +++ b/src/duckstation-qt/mainwindow.ui @@ -0,0 +1,368 @@ + + + MainWindow + + + + 0 + 0 + 754 + 600 + + + + DuckStation + + + + :/icons/duck.png:/icons/duck.png + + + + 0 + + + + + + + + 0 + 0 + 754 + 21 + + + + + System + + + + + + + + + + + + + + + + + S&ettings + + + + Renderer + + + + + + + + CPU Execution Mode + + + + + + + + + + + + + + + + + + + + + + &Help + + + + + + + + + + + + + toolBar + + + + 32 + 32 + + + + Qt::ToolButtonTextUnderIcon + + + TopToolBarArea + + + false + + + + + + + + + + + + + + + + + + + + :/icons/drive-optical.png:/icons/drive-optical.png + + + Start &Disc... + + + + + + :/icons/drive-removable-media.png:/icons/drive-removable-media.png + + + Start &BIOS + + + + + + :/icons/system-shutdown.png:/icons/system-shutdown.png + + + Power &Off + + + + + + :/icons/view-refresh.png:/icons/view-refresh.png + + + &Reset + + + + + true + + + + :/icons/media-playback-pause.png:/icons/media-playback-pause.png + + + &Pause + + + + + + :/icons/document-open.png:/icons/document-open.png + + + &Load State + + + + + + :/icons/document-save.png:/icons/document-save.png + + + &Save State + + + + + E&xit + + + + + + :/icons/utilities-system-monitor.png:/icons/utilities-system-monitor.png + + + &Console Settings... + + + + + + :/icons/input-gaming.png:/icons/input-gaming.png + + + &Port Settings... + + + + + + :/icons/applications-other.png:/icons/applications-other.png + + + &CPU Settings... + + + + + + :/icons/video-display.png:/icons/video-display.png + + + &GPU Settings... + + + + + + :/icons/view-fullscreen.png:/icons/view-fullscreen.png + + + Fullscreen + + + + + true + + + Hardware (D3D11) + + + + + true + + + Hardware (OpenGL) + + + + + true + + + Software + + + + + true + + + Interpreter (Slowest) + + + + + true + + + Cached Interpreter (Slower) + + + + + true + + + Recompiler (Fastest) + + + + + Resolution Scale + + + + + &GitHub Repository... + + + + + &Issue Tracker... + + + + + &About... + + + + + + :/icons/media-optical.png:/icons/media-optical.png + + + Change Disc... + + + + + + :/icons/audio-card.png:/icons/audio-card.png + + + Audio Settings... + + + + + + :/icons/folder-open.png:/icons/folder-open.png + + + Game List Settings... + + + + + + :/icons/edit-find.png:/icons/edit-find.png + + + Open Directory... + + + + + + :/icons/applications-system.png:/icons/applications-system.png + + + &Settings... + + + + + + + + diff --git a/src/duckstation-qt/opengldisplaywindow.cpp b/src/duckstation-qt/opengldisplaywindow.cpp new file mode 100644 index 000000000..ba1c72402 --- /dev/null +++ b/src/duckstation-qt/opengldisplaywindow.cpp @@ -0,0 +1,430 @@ +#include "opengldisplaywindow.h" +#include "YBaseLib/Assert.h" +#include "YBaseLib/Log.h" +#include +#include +Log_SetChannel(OpenGLDisplayWindow); + +static thread_local QOpenGLContext* s_thread_gl_context; + +static void* GetProcAddressCallback(const char* name) +{ + QOpenGLContext* ctx = s_thread_gl_context; + if (!ctx) + return nullptr; + + return (void*)ctx->getProcAddress(name); +} + +class OpenGLHostDisplayTexture : public HostDisplayTexture +{ +public: + OpenGLHostDisplayTexture(GLuint id, u32 width, u32 height) : m_id(id), m_width(width), m_height(height) {} + ~OpenGLHostDisplayTexture() override { glDeleteTextures(1, &m_id); } + + void* GetHandle() const override { return reinterpret_cast(static_cast(m_id)); } + u32 GetWidth() const override { return m_width; } + u32 GetHeight() const override { return m_height; } + + GLuint GetGLID() const { return m_id; } + + static std::unique_ptr Create(u32 width, u32 height, const void* initial_data, + u32 initial_data_stride) + { + GLuint id; + glGenTextures(1, &id); + + GLint old_texture_binding = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture_binding); + + // TODO: Set pack width + Assert(!initial_data || initial_data_stride == (width * sizeof(u32))); + + glBindTexture(GL_TEXTURE_2D, id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, initial_data); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glBindTexture(GL_TEXTURE_2D, id); + return std::make_unique(id, width, height); + } + +private: + GLuint m_id; + u32 m_width; + u32 m_height; +}; + +OpenGLDisplayWindow::OpenGLDisplayWindow(QWindow* parent) : QWindow(parent) +{ + setSurfaceType(QWindow::OpenGLSurface); +} + +OpenGLDisplayWindow::~OpenGLDisplayWindow() = default; + +HostDisplay::RenderAPI OpenGLDisplayWindow::GetRenderAPI() const +{ + return HostDisplay::RenderAPI::OpenGL; +} + +void* OpenGLDisplayWindow::GetRenderDevice() const +{ + return nullptr; +} + +void* OpenGLDisplayWindow::GetRenderContext() const +{ + return m_gl_context; +} + +void* OpenGLDisplayWindow::GetRenderWindow() const +{ + return const_cast(static_cast(this)); +} + +void OpenGLDisplayWindow::ChangeRenderWindow(void* new_window) +{ + Panic("Not implemented"); +} + +std::unique_ptr OpenGLDisplayWindow::CreateTexture(u32 width, u32 height, const void* data, + u32 data_stride, bool dynamic) +{ + return OpenGLHostDisplayTexture::Create(width, height, data, data_stride); +} + +void OpenGLDisplayWindow::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, + const void* data, u32 data_stride) +{ + OpenGLHostDisplayTexture* tex = static_cast(texture); + Assert(data_stride == (width * sizeof(u32))); + + GLint old_texture_binding = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture_binding); + + glBindTexture(GL_TEXTURE_2D, tex->GetGLID()); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data); + + glBindTexture(GL_TEXTURE_2D, old_texture_binding); +} + +void OpenGLDisplayWindow::SetDisplayTexture(void* texture, s32 offset_x, s32 offset_y, s32 width, s32 height, + u32 texture_width, u32 texture_height, float aspect_ratio) +{ + m_display_texture_id = static_cast(reinterpret_cast(texture)); + m_display_offset_x = offset_x; + m_display_offset_y = offset_y; + m_display_width = width; + m_display_height = height; + m_display_texture_width = texture_width; + m_display_texture_height = texture_height; + m_display_aspect_ratio = aspect_ratio; + m_display_texture_changed = true; +} + +void OpenGLDisplayWindow::SetDisplayLinearFiltering(bool enabled) +{ + m_display_linear_filtering = enabled; +} + +void OpenGLDisplayWindow::SetDisplayTopMargin(int height) +{ + m_display_top_margin = height; +} + +void OpenGLDisplayWindow::SetVSync(bool enabled) +{ + // Window framebuffer has to be bound to call SetSwapInterval. + GLint current_fbo = 0; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤t_fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + // SDL_GL_SetSwapInterval(enabled ? 1 : 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, current_fbo); +} + +std::tuple OpenGLDisplayWindow::GetWindowSize() const +{ + const QSize s = size(); + return std::make_tuple(static_cast(s.width()), static_cast(s.height())); +} + +void OpenGLDisplayWindow::WindowResized() {} + +const char* OpenGLDisplayWindow::GetGLSLVersionString() const +{ + return m_is_gles ? "#version 300 es" : "#version 130\n"; +} + +std::string OpenGLDisplayWindow::GetGLSLVersionHeader() const +{ + std::string header = GetGLSLVersionString(); + header += "\n\n"; + if (m_is_gles) + { + header += "precision highp float;\n"; + header += "precision highp int;\n\n"; + } + + return header; +} + +static void APIENTRY GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, + const GLchar* message, const void* userParam) +{ + switch (severity) + { + case GL_DEBUG_SEVERITY_HIGH_KHR: + Log_ErrorPrintf(message); + break; + case GL_DEBUG_SEVERITY_MEDIUM_KHR: + Log_WarningPrint(message); + break; + case GL_DEBUG_SEVERITY_LOW_KHR: + Log_InfoPrintf(message); + break; + case GL_DEBUG_SEVERITY_NOTIFICATION: + // Log_DebugPrint(message); + break; + } +} + +bool OpenGLDisplayWindow::createGLContext(QThread* worker_thread) +{ + m_gl_context = new QOpenGLContext(); + + // Prefer a desktop OpenGL context where possible. If we can't get this, try OpenGL ES. + static constexpr std::array, 11> desktop_versions_to_try = { + {{4, 6}, {4, 5}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0}, {3, 3}, {3, 2}, {3, 1}, {3, 0}}}; + static constexpr std::array, 4> es_versions_to_try = {{{3, 2}, {3, 1}, {3, 0}}}; + + QSurfaceFormat surface_format = requestedFormat(); + surface_format.setSwapBehavior(QSurfaceFormat::DoubleBuffer); + surface_format.setSwapInterval(0); + surface_format.setRenderableType(QSurfaceFormat::OpenGL); + surface_format.setProfile(QSurfaceFormat::CoreProfile); + +#ifdef _DEBUG + surface_format.setOption(QSurfaceFormat::DebugContext); +#endif + + for (const auto [major, minor] : desktop_versions_to_try) + { + surface_format.setVersion(major, minor); + m_gl_context->setFormat(surface_format); + if (m_gl_context->create()) + { + Log_InfoPrintf("Got a desktop OpenGL %d.%d context", major, minor); + break; + } + } + + if (!m_gl_context) + { + // try es + surface_format.setRenderableType(QSurfaceFormat::OpenGLES); + surface_format.setProfile(QSurfaceFormat::NoProfile); +#ifdef _DEBUG + surface_format.setOption(QSurfaceFormat::DebugContext, false); +#endif + + for (const auto [major, minor] : es_versions_to_try) + { + surface_format.setVersion(major, minor); + m_gl_context->setFormat(surface_format); + if (m_gl_context->create()) + { + Log_InfoPrintf("Got a OpenGL ES %d.%d context", major, minor); + m_is_gles = true; + break; + } + } + } + + if (!m_gl_context->isValid()) + { + Log_ErrorPrintf("Failed to create any GL context"); + delete m_gl_context; + m_gl_context = nullptr; + return false; + } + + if (!m_gl_context->makeCurrent(this)) + { + Log_ErrorPrintf("Failed to make GL context current on UI thread"); + delete m_gl_context; + m_gl_context = nullptr; + return false; + } + + m_gl_context->doneCurrent(); + m_gl_context->moveToThread(worker_thread); + return true; +} + +bool OpenGLDisplayWindow::initializeGLContext() +{ + if (!m_gl_context->makeCurrent(this)) + return false; + + s_thread_gl_context = m_gl_context; + + // Load GLAD. + const auto load_result = + m_is_gles ? gladLoadGLES2Loader(GetProcAddressCallback) : gladLoadGLLoader(GetProcAddressCallback); + if (!load_result) + { + Log_ErrorPrintf("Failed to load GL functions"); + return false; + } + +#if 1 + if (GLAD_GL_KHR_debug) + { + glad_glDebugMessageCallbackKHR(GLDebugCallback, nullptr); + glEnable(GL_DEBUG_OUTPUT); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + } +#endif + + if (!CreateImGuiContext() || !CreateGLResources()) + { + s_thread_gl_context = nullptr; + m_gl_context->doneCurrent(); + return false; + } + + return true; +} + +void OpenGLDisplayWindow::destroyGLContext() +{ + Assert(m_gl_context && s_thread_gl_context == m_gl_context); + s_thread_gl_context = nullptr; + + if (m_display_vao != 0) + glDeleteVertexArrays(1, &m_display_vao); + if (m_display_linear_sampler != 0) + glDeleteSamplers(1, &m_display_linear_sampler); + if (m_display_nearest_sampler != 0) + glDeleteSamplers(1, &m_display_nearest_sampler); + + m_display_program.Destroy(); + + m_gl_context->doneCurrent(); + delete m_gl_context; + m_gl_context = nullptr; +} + +bool OpenGLDisplayWindow::CreateImGuiContext() +{ + return true; +} + +bool OpenGLDisplayWindow::CreateGLResources() +{ + static constexpr char fullscreen_quad_vertex_shader[] = R"( +uniform vec4 u_src_rect; +out vec2 v_tex0; + +void main() +{ + vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); + v_tex0 = u_src_rect.xy + pos * u_src_rect.zw; + gl_Position = vec4(pos * vec2(2.0f, -2.0f) + vec2(-1.0f, 1.0f), 0.0f, 1.0f); +} +)"; + + static constexpr char display_fragment_shader[] = R"( +uniform sampler2D samp0; + +in vec2 v_tex0; +out vec4 o_col0; + +void main() +{ + o_col0 = texture(samp0, v_tex0); +} +)"; + + if (!m_display_program.Compile(GetGLSLVersionHeader() + fullscreen_quad_vertex_shader, + GetGLSLVersionHeader() + display_fragment_shader)) + { + Log_ErrorPrintf("Failed to compile display shaders"); + return false; + } + + if (!m_is_gles) + m_display_program.BindFragData(0, "o_col0"); + + if (!m_display_program.Link()) + { + Log_ErrorPrintf("Failed to link display program"); + return false; + } + + m_display_program.Bind(); + m_display_program.RegisterUniform("u_src_rect"); + m_display_program.RegisterUniform("samp0"); + m_display_program.Uniform1i(1, 0); + + glGenVertexArrays(1, &m_display_vao); + + // samplers + glGenSamplers(1, &m_display_nearest_sampler); + glSamplerParameteri(m_display_nearest_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glSamplerParameteri(m_display_nearest_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glGenSamplers(1, &m_display_linear_sampler); + glSamplerParameteri(m_display_linear_sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glSamplerParameteri(m_display_linear_sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + return true; +} + +void OpenGLDisplayWindow::Render() +{ + glDisable(GL_SCISSOR_TEST); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + RenderDisplay(); + + // ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + m_gl_context->makeCurrent(this); + m_gl_context->swapBuffers(this); + + // ImGui_ImplSDL2_NewFrame(m_window); + // ImGui_ImplOpenGL3_NewFrame(); + + GL::Program::ResetLastProgram(); +} + +void OpenGLDisplayWindow::RenderDisplay() +{ + if (!m_display_texture_id) + return; + + // - 20 for main menu padding + const QSize window_size = size(); + const auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect( + window_size.width(), std::max(window_size.height() - m_display_top_margin, 1), m_display_aspect_ratio); + + glViewport(vp_left, window_size.height() - (m_display_top_margin + vp_top) - vp_height, vp_width, vp_height); + glDisable(GL_BLEND); + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); + glDisable(GL_SCISSOR_TEST); + glDepthMask(GL_FALSE); + m_display_program.Bind(); + m_display_program.Uniform4f(0, static_cast(m_display_offset_x) / static_cast(m_display_texture_width), + static_cast(m_display_offset_y) / static_cast(m_display_texture_height), + static_cast(m_display_width) / static_cast(m_display_texture_width), + static_cast(m_display_height) / static_cast(m_display_texture_height)); + glBindTexture(GL_TEXTURE_2D, m_display_texture_id); + glBindSampler(0, m_display_linear_filtering ? m_display_linear_sampler : m_display_nearest_sampler); + glBindVertexArray(m_display_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + glBindSampler(0, 0); +} diff --git a/src/duckstation-qt/opengldisplaywindow.h b/src/duckstation-qt/opengldisplaywindow.h new file mode 100644 index 000000000..0749fb2a3 --- /dev/null +++ b/src/duckstation-qt/opengldisplaywindow.h @@ -0,0 +1,75 @@ +#pragma once +#include + +#include +#include +#include "common/gl/program.h" +#include "common/gl/texture.h" +#include "core/host_display.h" +#include +#include + +class OpenGLDisplayWindow final : public QWindow, public HostDisplay +{ + Q_OBJECT + +public: + explicit OpenGLDisplayWindow(QWindow* parent); + ~OpenGLDisplayWindow(); + + bool createGLContext(QThread* worker_thread); + bool initializeGLContext(); + void destroyGLContext(); + + RenderAPI GetRenderAPI() const override; + void* GetRenderDevice() const override; + void* GetRenderContext() const override; + void* GetRenderWindow() const override; + + void ChangeRenderWindow(void* new_window) override; + + std::unique_ptr CreateTexture(u32 width, u32 height, const void* data, u32 data_stride, + bool dynamic) override; + void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data, + u32 data_stride) override; + + void SetDisplayTexture(void* texture, s32 offset_x, s32 offset_y, s32 width, s32 height, u32 texture_width, + u32 texture_height, float aspect_ratio) override; + void SetDisplayLinearFiltering(bool enabled) override; + void SetDisplayTopMargin(int height) override; + + void SetVSync(bool enabled) override; + + std::tuple GetWindowSize() const override; + void WindowResized() override; + +private: + const char* GetGLSLVersionString() const; + std::string GetGLSLVersionHeader() const; + + bool CreateImGuiContext(); + bool CreateGLResources(); + + void Render(); + void RenderDisplay(); + + QOpenGLContext* m_gl_context = nullptr; + + GL::Program m_display_program; + GLuint m_display_vao = 0; + GLuint m_display_texture_id = 0; + s32 m_display_offset_x = 0; + s32 m_display_offset_y = 0; + s32 m_display_width = 0; + s32 m_display_height = 0; + u32 m_display_texture_width = 0; + u32 m_display_texture_height = 0; + int m_display_top_margin = 0; + float m_display_aspect_ratio = 1.0f; + GLuint m_display_nearest_sampler = 0; + GLuint m_display_linear_sampler = 0; + + bool m_is_gles = false; + bool m_display_texture_changed = false; + bool m_display_linear_filtering = false; +}; diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp new file mode 100644 index 000000000..c07913da7 --- /dev/null +++ b/src/duckstation-qt/qthostinterface.cpp @@ -0,0 +1,282 @@ +#include "qthostinterface.h" +#include "YBaseLib/Log.h" +#include "common/null_audio_stream.h" +#include "core/game_list.h" +#include "core/gpu.h" +#include "core/system.h" +#include "qtsettingsinterface.h" +#include +#include +#include +Log_SetChannel(QtHostInterface); + +QtHostInterface::QtHostInterface(QObject* parent) + : QObject(parent), m_qsettings("duckstation-qt.ini", QSettings::IniFormat) +{ + checkSettings(); + createGameList(); + createThread(); +} + +QtHostInterface::~QtHostInterface() +{ + Assert(!m_opengl_display_window); + stopThread(); +} + +void QtHostInterface::ReportError(const char* message) +{ + // QMessageBox::critical(nullptr, tr("DuckStation Error"), message, QMessageBox::Ok); +} + +void QtHostInterface::ReportMessage(const char* message) +{ + // QMessageBox::information(nullptr, tr("DuckStation Information"), message, QMessageBox::Ok); +} + +void QtHostInterface::setDefaultSettings() +{ + QtSettingsInterface si(m_qsettings); + m_settings.SetDefaults(); + m_settings.Save(si); + m_qsettings.sync(); +} + +void QtHostInterface::applySettings() +{ + QtSettingsInterface si(m_qsettings); + m_settings.Load(si); +} + +void QtHostInterface::checkSettings() +{ + const QSettings::Status settings_status = m_qsettings.status(); + if (settings_status != QSettings::NoError) + m_qsettings.clear(); + + const QString settings_version_key = QStringLiteral("General/SettingsVersion"); + const int expected_version = 1; + const QVariant settings_version_var = m_qsettings.value(settings_version_key); + bool settings_version_okay; + int settings_version = settings_version_var.toInt(&settings_version_okay); + if (!settings_version_okay) + settings_version = 0; + if (settings_version != expected_version) + { + Log_WarningPrintf("Settings version %d does not match expected version %d, resetting", settings_version, + expected_version); + m_qsettings.clear(); + m_qsettings.setValue(settings_version_key, expected_version); + setDefaultSettings(); + } +} + +void QtHostInterface::createGameList() +{ + m_game_list = std::make_unique(); + updateGameListDatabase(false); + refreshGameList(false); +} + +void QtHostInterface::updateGameListDatabase(bool refresh_list /*= true*/) +{ + m_game_list->ClearDatabase(); + + const QString redump_dat_path = m_qsettings.value("GameList/RedumpDatabasePath").toString(); + if (!redump_dat_path.isEmpty()) + m_game_list->ParseRedumpDatabase(redump_dat_path.toStdString().c_str()); + + if (refresh_list) + refreshGameList(true); +} + +void QtHostInterface::refreshGameList(bool invalidate_cache /*= false*/) +{ + QtSettingsInterface si(m_qsettings); + m_game_list->SetDirectoriesFromSettings(si); + m_game_list->RescanAllDirectories(); + emit gameListRefreshed(); +} + +QWidget* QtHostInterface::createDisplayWidget(QWidget* parent) +{ + m_opengl_display_window = new OpenGLDisplayWindow(nullptr); + m_display.release(); + m_display = std::unique_ptr(static_cast(m_opengl_display_window)); + return QWidget::createWindowContainer(m_opengl_display_window, parent); +} + +void QtHostInterface::destroyDisplayWidget() +{ + m_display.release(); + delete m_opengl_display_window; + m_opengl_display_window = nullptr; +} + +void QtHostInterface::bootSystem(QString initial_filename, QString initial_save_state_filename) +{ + emit emulationStarting(); + + if (!m_opengl_display_window->createGLContext(m_worker_thread)) + { + emit emulationStopped(); + return; + } + + QMetaObject::invokeMethod(this, "doBootSystem", Qt::QueuedConnection, Q_ARG(QString, initial_filename), + Q_ARG(QString, initial_save_state_filename)); +} + +void QtHostInterface::powerOffSystem() +{ + if (!isOnWorkerThread()) + { + QMetaObject::invokeMethod(this, "doPowerOffSystem", Qt::QueuedConnection); + return; + } + + if (!m_system) + { + Log_ErrorPrintf("powerOffSystem() called without system"); + return; + } + + m_system.reset(); + m_opengl_display_window->destroyGLContext(); + + emit emulationStopped(); +} + +void QtHostInterface::resetSystem() +{ + if (!isOnWorkerThread()) + { + QMetaObject::invokeMethod(this, "resetSystem", Qt::QueuedConnection); + return; + } + + if (!m_system) + { + Log_ErrorPrintf("resetSystem() called without system"); + return; + } + + HostInterface::ResetSystem(); +} + +void QtHostInterface::pauseSystem(bool paused) +{ + if (!isOnWorkerThread()) + { + QMetaObject::invokeMethod(this, "pauseSystem", Qt::QueuedConnection, Q_ARG(bool, paused)); + return; + } + + m_paused = paused; + emit emulationPaused(paused); +} + +void QtHostInterface::changeDisc(QString new_disc_filename) {} + +void QtHostInterface::doBootSystem(QString initial_filename, QString initial_save_state_filename) +{ + if (!m_opengl_display_window->initializeGLContext()) + { + emit emulationStopped(); + return; + } + + m_audio_stream = NullAudioStream::Create(); + m_audio_stream->Reconfigure(); + + std::string initial_filename_str = initial_filename.toStdString(); + std::string initial_save_state_filename_str = initial_save_state_filename.toStdString(); + if (!CreateSystem() || + !BootSystem(initial_filename_str.empty() ? nullptr : initial_filename_str.c_str(), + initial_save_state_filename_str.empty() ? nullptr : initial_save_state_filename_str.c_str())) + { + m_opengl_display_window->destroyGLContext(); + emit emulationStopped(); + return; + } + + emit emulationStarted(); +} + +void QtHostInterface::createThread() +{ + m_original_thread = QThread::currentThread(); + m_worker_thread = new Thread(this); + m_worker_thread->start(); + moveToThread(m_worker_thread); +} + +void QtHostInterface::stopThread() +{ + Assert(!isOnWorkerThread()); + + QMetaObject::invokeMethod(this, "doStopThread", Qt::QueuedConnection); + m_worker_thread->wait(); +} + +void QtHostInterface::doStopThread() +{ + m_shutdown_flag.store(true); +} + +void QtHostInterface::threadEntryPoint() +{ + while (!m_shutdown_flag.load()) + { + if (!m_system) + { + // wait until we have a system before running + QCoreApplication::processEvents(QEventLoop::AllEvents, 1000); + continue; + } + + // execute the system, polling events inbetween frames + // simulate the system if not paused + if (m_system && !m_paused) + m_system->RunFrame(); + + // rendering + { + // DrawImGui(); + + if (m_system) + m_system->GetGPU()->ResetGraphicsAPIState(); + + // ImGui::Render(); + m_display->Render(); + + // ImGui::NewFrame(); + + if (m_system) + { + m_system->GetGPU()->RestoreGraphicsAPIState(); + + if (m_speed_limiter_enabled) + Throttle(); + } + + UpdatePerformanceCounters(); + } + + QCoreApplication::processEvents(QEventLoop::AllEvents, m_paused ? 16 : 0); + } + + m_system.reset(); + + // move back to UI thread + moveToThread(m_original_thread); +} + +QtHostInterface::Thread::Thread(QtHostInterface* parent) : QThread(parent), m_parent(parent) {} + +QtHostInterface::Thread::~Thread() = default; + +void QtHostInterface::Thread::run() +{ + m_parent->threadEntryPoint(); +} diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h new file mode 100644 index 000000000..37ebfe80c --- /dev/null +++ b/src/duckstation-qt/qthostinterface.h @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include +#include +#include +#include "core/host_interface.h" +#include "opengldisplaywindow.h" + +class QWidget; + +class GameList; + +class QtHostInterface : public QObject, private HostInterface +{ + Q_OBJECT + +public: + explicit QtHostInterface(QObject* parent = nullptr); + ~QtHostInterface(); + + void ReportError(const char* message) override; + void ReportMessage(const char* message) override; + + const QSettings& getQSettings() const { return m_qsettings; } + QSettings& getQSettings() { return m_qsettings; } + void setDefaultSettings(); + void applySettings(); + + const GameList* getGameList() const { return m_game_list.get(); } + GameList* getGameList() { return m_game_list.get(); } + void updateGameListDatabase(bool refresh_list = true); + void refreshGameList(bool invalidate_cache = false); + + bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; } + + QWidget* createDisplayWidget(QWidget* parent); + void destroyDisplayWidget(); + + void bootSystem(QString initial_filename, QString initial_save_state_filename); + +Q_SIGNALS: + void emulationStarting(); + void emulationStarted(); + void emulationStopped(); + void emulationPaused(bool paused); + void gameListRefreshed(); + +public Q_SLOTS: + void powerOffSystem(); + void resetSystem(); + void pauseSystem(bool paused); + void changeDisc(QString new_disc_filename); + +private Q_SLOTS: + void doBootSystem(QString initial_filename, QString initial_save_state_filename); + void doStopThread(); + +private: + class Thread : public QThread + { + public: + Thread(QtHostInterface* parent); + ~Thread(); + + protected: + void run() override; + + private: + QtHostInterface* m_parent; + }; + + void checkSettings(); + void createGameList(); + void createThread(); + void stopThread(); + void threadEntryPoint(); + + QSettings m_qsettings; + + std::unique_ptr m_game_list; + + OpenGLDisplayWindow* m_opengl_display_window = nullptr; + QThread* m_original_thread = nullptr; + Thread* m_worker_thread = nullptr; + + std::atomic_bool m_shutdown_flag{ false }; +}; + diff --git a/src/duckstation-qt/qtsettingsinterface.cpp b/src/duckstation-qt/qtsettingsinterface.cpp new file mode 100644 index 000000000..a9843da63 --- /dev/null +++ b/src/duckstation-qt/qtsettingsinterface.cpp @@ -0,0 +1,142 @@ +#include "qtsettingsinterface.h" +#include +#include + +static QString GetFullKey(const char* section, const char* key) +{ + return QStringLiteral("%1/%2").arg(section, key); +} + +QtSettingsInterface::QtSettingsInterface(QSettings& settings) : m_settings(settings) {} + +QtSettingsInterface::~QtSettingsInterface() = default; + +int QtSettingsInterface::GetIntValue(const char* section, const char* key, int default_value /*= 0*/) +{ + QVariant value = m_settings.value(GetFullKey(section, key)); + if (!value.isValid()) + return default_value; + + bool converted_value_okay; + int converted_value = value.toInt(&converted_value_okay); + if (!converted_value_okay) + return default_value; + else + return converted_value; +} + +float QtSettingsInterface::GetFloatValue(const char* section, const char* key, float default_value /*= 0.0f*/) +{ + QVariant value = m_settings.value(GetFullKey(section, key)); + if (!value.isValid()) + return default_value; + + bool converted_value_okay; + float converted_value = value.toFloat(&converted_value_okay); + if (!converted_value_okay) + return default_value; + else + return converted_value; +} + +bool QtSettingsInterface::GetBoolValue(const char* section, const char* key, bool default_value /*= false*/) +{ + QVariant value = m_settings.value(GetFullKey(section, key)); + return value.isValid() ? value.toBool() : default_value; +} + +std::string QtSettingsInterface::GetStringValue(const char* section, const char* key, + const char* default_value /*= ""*/) +{ + QVariant value = m_settings.value(GetFullKey(section, key)); + return value.isValid() ? value.toString().toStdString() : std::string(default_value); +} + +void QtSettingsInterface::SetIntValue(const char* section, const char* key, int value) +{ + m_settings.setValue(GetFullKey(section, key), QVariant(value)); +} + +void QtSettingsInterface::SetFloatValue(const char* section, const char* key, float value) +{ + m_settings.setValue(GetFullKey(section, key), QVariant(value)); +} + +void QtSettingsInterface::SetBoolValue(const char* section, const char* key, bool value) +{ + m_settings.setValue(GetFullKey(section, key), QVariant(value)); +} + +void QtSettingsInterface::SetStringValue(const char* section, const char* key, const char* value) +{ + m_settings.setValue(GetFullKey(section, key), QVariant(value)); +} + +std::vector QtSettingsInterface::GetStringList(const char* section, const char* key) +{ + QVariant value = m_settings.value(GetFullKey(section, key)); + if (value.type() == QVariant::String) + return { value.toString().toStdString() }; + else if (value.type() != QVariant::StringList) + return {}; + + QStringList value_sl = value.toStringList(); + std::vector results; + results.reserve(static_cast(value_sl.size())); + std::transform(value_sl.begin(), value_sl.end(), std::back_inserter(results), + [](const QString& str) { return str.toStdString(); }); + return results; +} + +void QtSettingsInterface::SetStringList(const char* section, const char* key, + const std::vector& items) +{ + QString full_key = GetFullKey(section, key); + if (items.empty()) + { + m_settings.remove(full_key); + return; + } + + QStringList sl; + sl.reserve(static_cast(items.size())); + std::transform(items.begin(), items.end(), std::back_inserter(sl), [](const std::string_view& sv) { + return QString::fromLocal8Bit(sv.data(), static_cast(sv.size())); + }); + m_settings.setValue(full_key, sl); +} + +bool QtSettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item) +{ + QString full_key = GetFullKey(section, key); + QVariant var = m_settings.value(full_key); + QStringList sl = var.toStringList(); + if (sl.removeAll(item) == 0) + return false; + + if (sl.isEmpty()) + m_settings.remove(full_key); + else + m_settings.setValue(full_key, sl); + return true; +} + +bool QtSettingsInterface::AddToStringList(const char* section, const char* key, const char* item) +{ + QString full_key = GetFullKey(section, key); + QVariant var = m_settings.value(full_key); + + QStringList sl = (var.type() == QVariant::StringList) ? var.toStringList() : QStringList(); + QString qitem(item); + if (sl.contains(qitem)) + return false; + + sl.push_back(qitem); + m_settings.setValue(full_key, sl); + return true; +} + +void QtSettingsInterface::DeleteValue(const char* section, const char* key) +{ + m_settings.remove(GetFullKey(section, key)); +} diff --git a/src/duckstation-qt/qtsettingsinterface.h b/src/duckstation-qt/qtsettingsinterface.h new file mode 100644 index 000000000..15851e334 --- /dev/null +++ b/src/duckstation-qt/qtsettingsinterface.h @@ -0,0 +1,31 @@ +#pragma once +#include "core/settings.h" + +class QSettings; + +class QtSettingsInterface : public SettingsInterface +{ +public: + QtSettingsInterface(QSettings& settings); + ~QtSettingsInterface(); + + int GetIntValue(const char* section, const char* key, int default_value = 0) override; + float GetFloatValue(const char* section, const char* key, float default_value = 0.0f) override; + bool GetBoolValue(const char* section, const char* key, bool default_value = false) override; + std::string GetStringValue(const char* section, const char* key, const char* default_value = "") override; + + void SetIntValue(const char* section, const char* key, int value) override; + void SetFloatValue(const char* section, const char* key, float value) override; + void SetBoolValue(const char* section, const char* key, bool value) override; + void SetStringValue(const char* section, const char* key, const char* value) override; + + std::vector GetStringList(const char* section, const char* key) override; + void SetStringList(const char* section, const char* key, const std::vector& items) override; + bool RemoveFromStringList(const char* section, const char* key, const char* item) override; + bool AddToStringList(const char* section, const char* key, const char* item) override; + + void DeleteValue(const char* section, const char* key) override; + +private: + QSettings& m_settings; +}; \ No newline at end of file diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp new file mode 100644 index 000000000..a1295d784 --- /dev/null +++ b/src/duckstation-qt/qtutils.cpp @@ -0,0 +1,23 @@ +#include "qtutils.h" +#include +#include + +namespace QtUtils { + +void ResizeColumnsForTableView(QTableView* view, const std::initializer_list& widths) +{ + const int total_width = + std::accumulate(widths.begin(), widths.end(), 0, [](int a, int b) { return a + std::max(b, 0); }); + + const int flex_width = std::max(view->width() - total_width - 2, 1); + + int column_index = 0; + for (const int spec_width : widths) + { + const int width = spec_width < 0 ? flex_width : spec_width; + view->setColumnWidth(column_index, width); + column_index++; + } +} + +} // namespace QtUtils \ No newline at end of file diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h new file mode 100644 index 000000000..ea2b6f85a --- /dev/null +++ b/src/duckstation-qt/qtutils.h @@ -0,0 +1,12 @@ +#pragma once +#include + +class QTableView; + +namespace QtUtils { + +/// Resizes columns of the table view to at the specified widths. A width of -1 will stretch the column to use the +/// remaining space. +void ResizeColumnsForTableView(QTableView* view, const std::initializer_list& widths); + +} // namespace QtUtils \ No newline at end of file diff --git a/src/duckstation-qt/resources/icons.qrc b/src/duckstation-qt/resources/icons.qrc new file mode 100644 index 000000000..f461499bf --- /dev/null +++ b/src/duckstation-qt/resources/icons.qrc @@ -0,0 +1,29 @@ + + + icons/applications-internet.png + icons/system-search.png + icons/list-add.png + icons/list-remove.png + icons/duck.png + icons/edit-find.png + icons/folder-open.png + icons/applications-development.png + icons/applications-other.png + icons/applications-system.png + icons/audio-card.png + icons/document-open.png + icons/document-save.png + icons/drive-optical.png + icons/drive-removable-media.png + icons/input-gaming.png + icons/media-flash.png + icons/media-optical.png + icons/media-playback-pause.png + icons/media-playback-start.png + icons/system-shutdown.png + icons/utilities-system-monitor.png + icons/video-display.png + icons/view-fullscreen.png + icons/view-refresh.png + + diff --git a/src/duckstation-qt/resources/icons/applications-development.png b/src/duckstation-qt/resources/icons/applications-development.png new file mode 100644 index 0000000000000000000000000000000000000000..bc88a5c56a5b9cdbc647e0824b55d4bdc6c42238 GIT binary patch literal 2174 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 AFaQ7m literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/applications-internet.png b/src/duckstation-qt/resources/icons/applications-internet.png new file mode 100644 index 0000000000000000000000000000000000000000..096e8489584adb762d2bdb7ef6e6719e32d19c44 GIT binary patch literal 2464 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 literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/applications-system.png b/src/duckstation-qt/resources/icons/applications-system.png new file mode 100644 index 0000000000000000000000000000000000000000..565f406dd147d585f47374e20544996b3542dbd5 GIT binary patch literal 2544 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 Tt0X-P00000NkvXXu0mjfKQhdA literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/document-open.png b/src/duckstation-qt/resources/icons/document-open.png new file mode 100644 index 0000000000000000000000000000000000000000..f35f2583540678b7a544d9175245096082f302af GIT binary patch literal 1550 zcmV+p2J!icP)x2@7Ze~uNfga3vbZE9GEM?G2QWwx$l|aG!6EBtvokxp zvoq5l)m6n|XM1OQW{kx~$|IHPv)|{b_kF9YL=;7Qme9AeD9aOny9T9{E~r#qPhztg z1VQi60-DXH_~GhP{O0)=sMqT=*LlCEvE6RV7hihev3LLe?mJnMeJ2Ql_)r2aUc4x- z|MMTLudh!Kdhc=0;hZDSbBr;>am=?)-y@FSdHfr{d*ijY=4x z#ajD7d4czy8~^!0D5VNF1HAo*w{gyntm#@D5z98bLK&mQk*(<3S$g;o^$tIcdPgRwf3{;e(~#PqA2nc0*o<9nxnsE`-LIWI_Ot zrfI5Ltv0Pzs}Qgs0`EOaDW3kpYGL@zx85Affik@J1VO+9-#vrY8b7MCEa2F&V@ape zQHzTU7-Pl+7-Nd;--l9)bB~@IBP)}Swzbv(vjwcJt!0fyLvC$t6&?VJ^Sw_xYW(+C zE@Q2mARk3gsj#wgw)l1$#Qt3{KR<6)S6B7s<|fxZbXWb-Uf>Myt+`pZqpoJF&>Y{II?i%M@>vM)Z2C$1h*G_FyqEqR`aq z^EG1(=HHSdwSW&k>9aG)0YA+55~dE8Cd5Hi4>eh45Q5hJbdg!sTGQ!l({8uvXO4O; zAa@=iKp=)DO2yAA{-C(M;+!YWEJ>P!7aJ2E@ZP7@YRJOE0%{#M{1PU2C?%#?pSjsFq{cBwEqGCcF;J4-ewqsol5o@miPjnwhSaIhPc7D#%!{#V zm}9=Et@Ah!S?(rIq!_B*-c4QZ{iFcrT-@vRXl-k13jw{@kme3C++}VZ#(GTd$n9Q+ zdoe*#YK;%X#G!vjSX>TF~;q=T9oZhNsyA4M$u1m`iarZ z2*F@5=(E+W64v0x=762S!BcM5{=|$VEjvd5-9bi{+sfaLzqfVQT1kL&ZU6$O=SZ!i zJ1}I%jais=AZJ8kY+Opg=1zhXauAI?&Bk&dj$^v9<@lW;b5%_>`2WbpmJD4ag&X#0LE9SR|WHW;{8|{n}zcHXy18Hr;{SQ*qRW~Y&L_Hl@;3^WNZ&SA8l+8 zpWjniZku$p?>%g6?AXi<{@iRfbreN8?0HTCpF4T-_k!I_OG@E@Fg9FcT}rK8@wz!z{kKk5Jyqu zMHEFen@tG>fa2i3{j}kS#Q=E}MGi)uWSPyKrkN=j13{7l$sh8kRr(E5NL&1 z>!1#!rDOlGGqnS){h%GE){$|>KiZj6XZnZJPIa)3EiIJ}(L#$lXqBKHAfN#Qq>Cn- z&Ayl2_ujp?e}sKa0!SU{^vs-j_v_vB+jGu6cOU$pMOFv+#AIiFq4h=gm^qo0he(dDAnKv}nMC^z~ z(Np9LKIv4PWTLG6z5b!=v%P7w7n6c*34cds7n=9Ua))@1PNF=hvvaAV^ z!+xC6Yi?xu;&uiG^8{)v7s{fNXJ34oFH|1G7!$5nN-5v>pLbpN-aUKv6ek1N<2zbg zr=<@5p|$?mrcIlcc6N3mr3^PT#*lZ5+_vRuqy#U}Xl)o7^{GjxkanD%J9lD?8L!qe z&pdNycX#(2z!M(}Q1tcb`Ar{;h(R%b#flaD;gue~`Q%F%xy`mELI^xxp|wF9Lo8z9 z6g*~cd}_klFMY9@hq_kN-QE2Fumdm?_8Q;G?0e*2(Q7t*b=mE=r!C8(V|ESSzkd-= zzu3dlg`cIoEQ%!swj~jO=WDbvXl*b8wryFBlmcVm^~3LTZs0uM`0`v*sT6H(ZOuZ6 zrMq_R+ILxi3v&OeRjbJ7^Z35c%8o`dgD$%d{*&8oX(N*z#VrIF491Lw#VrQB@YfT# z#enwa8UnAtk)wV5{98-WD!_3ZmM>qKJC_@qt9kXUV@$0oM;SvlUu57Thxg77bLM=WY`#cSZHh7*{{H4M9(!;x=~Nsk zC8MLGG&eU>QBkpe-MV!(mjw_)Jg{-&M&Y_{sgKqgr4$d}-NCd}j91?}K{{DRcBDwo z^-y{&q_x!*%)G9WeFxv++jqA!^SVlmF$f_r#^C!t%a$#RMk0~Tmjqa{WQi?=xUapv zoqRrzZQB@QP)ZR50V?qM!RGly@j2OdhMQ(jBN`biq?L&%H?`F8^1*lLTrh)PE z!qX$A#C2UdIywNhY~Q|JUIdU%r`If6w5T=`iBKpMkWvx^0iNekEEXvii)a<_=mR&C z%MNpP;3MYGo<_VZ!mV@bdF}09>Z@biaqEn5z2|uZL4a-Bc%DZxnPkqKIWrF*KD^>0 z03pQJR<2x0E|&u!2m*{T;U>lywAMr{!;dz%bNu9K2D3TV-ZGQk_s@~b4)b7VGoI(+ z`#!$!69fTLN|aJ)t#MtK#fukXS=N>b0PEJRtF5lCUe(ahfa5qQrO4;=0F;-Phs8)K zg;I*ja>)I7TwnnTagQ*8Z8 z8&j(iM59sS@i@t3l4LT8=Xp4egOm~}C6P!3r4$PnF0{4Q_rrxcAcVNPy}cdRb*Zhb zC7aDMGBSb?f*=UOwq;o)lSyFg{NJ2T^ShrkP*H9pr9^9u)_N>ov<}~qQj$ugsIIQ2 zzrUZ3jt=(j-Fpx4<5D5|Ofs2dWMqW1XV0RwM#{1Duq-PysI?CBq12Dr;QRgr&&6Uf zg!s=rXN)1A&*L}_T5G0EnS$2(hR_2*%=0{K+YVnGe*u-AC8*2IkDn{)__!Lx;nJhw6?b5c^)k-Effj`nwy((9EYZ+ zCUUtPsZ^?T)QKKi3?iBCBB+Ir34qW0OT(K@o!k3U4Q?i+IRo}002ovPDHLk FV1jL*$BzI2 literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/drive-optical.png b/src/duckstation-qt/resources/icons/drive-optical.png new file mode 100644 index 0000000000000000000000000000000000000000..bf2e8c89fbc45b0d26a699b5a26b1542c4ac8575 GIT binary patch literal 1338 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 b8(g%xI>pKS%`5C&8bDcIL#|%NEc|}}aB&`t literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/duck_128.png b/src/duckstation-qt/resources/icons/duck_128.png new file mode 100644 index 0000000000000000000000000000000000000000..bc6655bbdba35e6e34a2c1ab63af7ac55511de77 GIT binary patch literal 8478 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 literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/list-add.png b/src/duckstation-qt/resources/icons/list-add.png new file mode 100644 index 0000000000000000000000000000000000000000..2acdd8f514c199a37b7b14f53081467f88ef7bff GIT binary patch literal 601 zcmV-f0;c_mP)i&hw@usRe0Hlp%^VXF!>2}-I+fVC+BCh2Xu(H}= zWwkR-i17&Q1@?r1slIQmGmt?bz+``iz=_N4nTEMJnWp6PdlDi4$xDysihSSRJ^#HD zfm0WkcL*Ezw|WjD;wX`Y@`OQ$i> zk~|wHA%P^W-+NY=qm+O%@!?CCIP&DpgbAGaz)s%w=F2r6?z` z-r3~+=g{XtaVheZEby-7%X^w2a;hK35HfD6NN^I8P7V;(0mQ@Z@*Wtm@8FfUzLwkFPvfh|u~3@f)V nN>6CzLo>o}j6Z`3{#X10^GpxssPJ~100000NkvXXu0mjfzW5Zm literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/list-remove.png b/src/duckstation-qt/resources/icons/list-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..c5524f7284e4ecb40cbee1e3d3a449bd58a0e4a8 GIT binary patch literal 317 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 KMNUMnLSTX_UsMzT literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/media-playback-pause.png b/src/duckstation-qt/resources/icons/media-playback-pause.png new file mode 100644 index 0000000000000000000000000000000000000000..1e9f4d535795bb2d2afccec95b0e5ad1cafcab8b GIT binary patch literal 481 zcmV<70UrK|P)1uoCLv!^$R=$F_1r5`T=3!q@j(2$AuqraX_xA% zur~90u(eQZ+1WWY@6j;t!{lLx4+3-M%=zErBA6#YbD{eIe2HYv#zzR$&##|-K39Bv z_vz^)m^6X5Z3XcD?Fa6D-vLngg6o$rXxmo1-7bo`lq^<25}VCeZf-XJbTLe_z}3~& z@$hNdv!C`%({$3sFv$X|)hYl4h2x$M>%UzLlPs`YE_FB&mt{B{l1{o9CRt#ySO_3U zk!K%-@tmkmYD&L!k2h|?`&qr5sP7xZ=ftonHdi7-XGuA%#I@#!z2szeJ`sja1QT1 zAsh~`D*C=xER!tIbzQi>UdUVqSwILgqwBg5%Oneo<0vyj5@t1$4r^l^N5wM90!`D% ztm5^XS10>s6;0D9mbsJ%{|C0)ZJXzLey*FQY3loa7sDhA07yDJuFkpe7-r6#Ip^{R Xfk+A`Fy8dj00000NkvXXu0mjfy?@sl literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/media-playback-start.png b/src/duckstation-qt/resources/icons/media-playback-start.png new file mode 100644 index 0000000000000000000000000000000000000000..66f32d89b59aed6480c4b3682ad57055828498ac GIT binary patch literal 1028 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 literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/system-shutdown.png b/src/duckstation-qt/resources/icons/system-shutdown.png new file mode 100644 index 0000000000000000000000000000000000000000..36acd46bd33d2e7a0db774c1b35863dd52db7949 GIT binary patch literal 1055 zcmV+)1mOFLP)t( zIkOA=-;E6ZIXF1T#^do{2qE7or521aWR&B?LWCGfY0Vh(>+9?5kNf-k&jEmwlam=C z#P3^MTlqqvV8>#y_tN%_bB_D_ds!}*>x{9Dy}iAb?cJ18e%#*P&abSj*o{U5oO6#n zaGlhQG5$3ci(z$j)h2}G%jNP%06+KuN~sTpLcxA~e1uX8gb>8zapZD2*tR`t{jq|^ z7)UAc{QL~gIfM{+dU`^kP_WO>&p!kJwAL7+^R8mSrK4 zNMLDcDYV$l%?&Ke3i%jgCIj%UQ2+qoI1XlKXG8OhF>ubu0u0}q3LqYjBa_KQ0w|?I zJ`*LOCr$`~QYw@K0FcRKu(q}qm@~#8nM_9Jjn*2J(k}z>#K%QoVPOHAo11|-&N=4i z=OcYQfT$=;1&~gsLpjiHxBc>q0(iG70N!#S>afl}f19YB0utF$PL0 zY}F2>jTQF!*p)DvGR literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/utilities-system-monitor.png b/src/duckstation-qt/resources/icons/utilities-system-monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..b62959e4f3d7871c986f24c6f112153561912ea8 GIT binary patch literal 1886 zcmV-k2ch_hP)-U0|+Dp3WA0}6@ieFRyc7|mpHW@J5HSS{_yVJ zJ2Qued%e4^od}^Y(&(O4AIEWY$i^<`y(gKDE}g8M^PBd1 z*U9K*rQiPM*KYwQLucKyVLS5y-m+t7r**DU;pAUve_)o`z*kQfIH{eHM)(1dq)$dsb8~Mp?@G? zdD)>Pf=Pv|Ch_*^fmffz-z#1f^&amPF^1V0kBGtdcybfs9D;T0b^#&`^a;Z|GAu4R zTpFEq9-3P30X%plS$m%dwCRh(%rK3Qv2R13-RfwEoy%IY`LfAQ;XJ}iVd?CQP#M*3DiPwdvLaY~@ zcGt0kR^*vqu5)Ll(N&|x=zTFY}!JB^}L>6u2XM%3WY4Cz6_I3m*_8t1P01wgZHpw z7!K?W`RGPWwc%+;j&@{eMV4mU5=C(ue+Lp^0zrMk--vm%2bz)6uCy@Kn888Aj^RA3 z%~ez(S8Q?n{wmKrU*L&d1+t|YvonGK17%OPpk(rbY6kB->P=FHttX1%USjFiSs)0R zjGn8$o33%VW<*uAH~x4$(^K9d84zzE|z3jAa0KFzk}xr0M|ZZc$aD9_dD z2OQd8=IApqr;iso^3)J!Gp0M^UOh{p&paB0B0?b-(3cItBsu=^T!mZnZ5l0j;kkf) zPxR4hd9K`0_U-bVd8wb#p+5e0^*&QGEz~QDPm7X*_gL@n*3w$@X&OERgtNES_|e-H zkPwjoF#*N|m>^)^lTdG34nEn(?Zp;1Z{@f;)8h1zK|Z)r=v=BX{5=N1yR!;;bGbAv{bKxGWR3r9J6uCKPX*6^EbIPK+%AYT1S*~PJ zJc>g_5OIhoNC2WBn%*JiAs(Pxsgh|hVh|G`#?WdT7VcWcwtH?b3Z5MEiy3N-kdL5TB)%4!}l9-{(S zR1Kn<-lTpbKQAd{`k;uJH0mfEG=ElL-luo1B74cZ0WIwlei$8p5c z^3n#yItr*N%~q4x##kF;W80x)Yq2)QIoJ6xj$`6>M4{N1A{$!GCdLT4d_FNMwzQ*& zLLrYa-7=jCGab6K>%C7}XxrOPTdZ?Qi@ia(<v$v^N>)f3Fm?-LotrvvG7}Jhzj1hygiNHq*2WZsl3=Iu&^ysmU^$Hw5e3;3} zN$T|mjYfn1{y~l%JB|^_T4Y--n0ArB4)p}F8qJ} zoT{p|*3C{|pZ>vx3xE0}u()3T4A2jhfE-)2YL8*~B&Ni`Do_KQZ1G~Za6kUht;y;5 Y4{P?sxg-E8IRF3v07*qoM6N<$g5g<-#{d8T literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/video-display.png b/src/duckstation-qt/resources/icons/video-display.png new file mode 100644 index 0000000000000000000000000000000000000000..b95ea5d366ca056831cf478afa1806543c4c983f GIT binary patch literal 1596 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 + +SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent /* = nullptr */) + : QDialog(parent), m_host_interface(host_interface) +{ + m_ui.setupUi(this); + + m_console_settings = new ConsoleSettingsWidget(m_ui.settingsContainer); + m_game_list_settings = new GameListSettingsWidget(host_interface, m_ui.settingsContainer); + m_cpu_settings = new QWidget(m_ui.settingsContainer); + m_gpu_settings = new QWidget(m_ui.settingsContainer); + m_audio_settings = new QWidget(m_ui.settingsContainer); + + m_ui.settingsContainer->insertWidget(0, m_console_settings); + m_ui.settingsContainer->insertWidget(1, m_game_list_settings); + m_ui.settingsContainer->insertWidget(2, m_cpu_settings); + m_ui.settingsContainer->insertWidget(3, m_gpu_settings); + m_ui.settingsContainer->insertWidget(4, m_audio_settings); + + m_ui.settingsCategory->setCurrentRow(0); + m_ui.settingsContainer->setCurrentIndex(0); + connect(m_ui.settingsCategory, &QListWidget::currentRowChanged, this, &SettingsDialog::onCategoryCurrentRowChanged); +} + +SettingsDialog::~SettingsDialog() = default; + +void SettingsDialog::setCategory(Category category) +{ + if (category >= Category::Count) + return; + + m_ui.settingsCategory->setCurrentRow(static_cast(category)); +} + +void SettingsDialog::onCategoryCurrentRowChanged(int row) +{ + m_ui.settingsContainer->setCurrentIndex(row); +} diff --git a/src/duckstation-qt/settingsdialog.h b/src/duckstation-qt/settingsdialog.h new file mode 100644 index 000000000..f006768cb --- /dev/null +++ b/src/duckstation-qt/settingsdialog.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include "ui_settingsdialog.h" + +class QtHostInterface; + +class ConsoleSettingsWidget; + +class SettingsDialog : public QDialog +{ + Q_OBJECT + +public: + enum class Category + { + ConsoleSettings, + GameListSettings, + CPUSettings, + GPUSettings, + AudioSettings, + Count + }; + + explicit SettingsDialog(QtHostInterface* host_interface, QWidget* parent = nullptr); + ~SettingsDialog(); + +public Q_SLOTS: + void setCategory(Category category); + +private Q_SLOTS: + void onCategoryCurrentRowChanged(int row); + +private: + Ui::SettingsDialog m_ui; + + QtHostInterface* m_host_interface; + + ConsoleSettingsWidget* m_console_settings = nullptr; + QWidget* m_game_list_settings = nullptr; + QWidget* m_cpu_settings = nullptr; + QWidget* m_gpu_settings = nullptr; + QWidget* m_audio_settings = nullptr; +}; diff --git a/src/duckstation-qt/settingsdialog.ui b/src/duckstation-qt/settingsdialog.ui new file mode 100644 index 000000000..2e33176a7 --- /dev/null +++ b/src/duckstation-qt/settingsdialog.ui @@ -0,0 +1,150 @@ + + + SettingsDialog + + + Qt::WindowModal + + + + 0 + 0 + 637 + 405 + + + + DuckStation Settings + + + + :/icons/applications-system.png:/icons/applications-system.png + + + + + + 10 + + + + + + 160 + 16777215 + + + + + 32 + 32 + + + + + Console Settings + + + + :/icons/utilities-system-monitor.png:/icons/utilities-system-monitor.png + + + + + Game List Settings + + + + :/icons/folder-open.png:/icons/folder-open.png + + + + + CPU Settings + + + + :/icons/applications-other.png:/icons/applications-other.png + + + + + GPU Settings + + + + :/icons/video-display.png:/icons/video-display.png + + + + + Audio Settings + + + + :/icons/audio-card.png:/icons/audio-card.png + + + + + + + + 0 + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + + + + + + + + + + + buttonBox + accepted() + SettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +