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 000000000..bc88a5c56 Binary files /dev/null and b/src/duckstation-qt/resources/icons/applications-development.png differ 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 000000000..096e84895 Binary files /dev/null and b/src/duckstation-qt/resources/icons/applications-internet.png differ diff --git a/src/duckstation-qt/resources/icons/applications-other.png b/src/duckstation-qt/resources/icons/applications-other.png new file mode 100644 index 000000000..1990dbb82 Binary files /dev/null and b/src/duckstation-qt/resources/icons/applications-other.png differ 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 000000000..565f406dd Binary files /dev/null and b/src/duckstation-qt/resources/icons/applications-system.png differ diff --git a/src/duckstation-qt/resources/icons/audio-card.png b/src/duckstation-qt/resources/icons/audio-card.png new file mode 100644 index 000000000..5b15dd6d2 Binary files /dev/null and b/src/duckstation-qt/resources/icons/audio-card.png differ 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 000000000..f35f25835 Binary files /dev/null and b/src/duckstation-qt/resources/icons/document-open.png differ diff --git a/src/duckstation-qt/resources/icons/document-save.png b/src/duckstation-qt/resources/icons/document-save.png new file mode 100644 index 000000000..db5c52b76 Binary files /dev/null and b/src/duckstation-qt/resources/icons/document-save.png differ 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 000000000..bf2e8c89f Binary files /dev/null and b/src/duckstation-qt/resources/icons/drive-optical.png differ diff --git a/src/duckstation-qt/resources/icons/drive-removable-media.png b/src/duckstation-qt/resources/icons/drive-removable-media.png new file mode 100644 index 000000000..2d2890935 Binary files /dev/null and b/src/duckstation-qt/resources/icons/drive-removable-media.png differ diff --git a/src/duckstation-qt/resources/icons/duck.png b/src/duckstation-qt/resources/icons/duck.png new file mode 100644 index 000000000..c78869e43 Binary files /dev/null and b/src/duckstation-qt/resources/icons/duck.png differ 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 000000000..bc6655bbd Binary files /dev/null and b/src/duckstation-qt/resources/icons/duck_128.png differ diff --git a/src/duckstation-qt/resources/icons/duck_64.png b/src/duckstation-qt/resources/icons/duck_64.png new file mode 100644 index 000000000..82b5aaa5d Binary files /dev/null and b/src/duckstation-qt/resources/icons/duck_64.png differ diff --git a/src/duckstation-qt/resources/icons/edit-find.png b/src/duckstation-qt/resources/icons/edit-find.png new file mode 100644 index 000000000..5594785d1 Binary files /dev/null and b/src/duckstation-qt/resources/icons/edit-find.png differ diff --git a/src/duckstation-qt/resources/icons/folder-open.png b/src/duckstation-qt/resources/icons/folder-open.png new file mode 100644 index 000000000..901816c8e Binary files /dev/null and b/src/duckstation-qt/resources/icons/folder-open.png differ diff --git a/src/duckstation-qt/resources/icons/input-gaming.png b/src/duckstation-qt/resources/icons/input-gaming.png new file mode 100644 index 000000000..26e2a9827 Binary files /dev/null and b/src/duckstation-qt/resources/icons/input-gaming.png differ 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 000000000..2acdd8f51 Binary files /dev/null and b/src/duckstation-qt/resources/icons/list-add.png differ 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 000000000..c5524f728 Binary files /dev/null and b/src/duckstation-qt/resources/icons/list-remove.png differ diff --git a/src/duckstation-qt/resources/icons/media-flash.png b/src/duckstation-qt/resources/icons/media-flash.png new file mode 100644 index 000000000..7540f3fd2 Binary files /dev/null and b/src/duckstation-qt/resources/icons/media-flash.png differ diff --git a/src/duckstation-qt/resources/icons/media-optical.png b/src/duckstation-qt/resources/icons/media-optical.png new file mode 100644 index 000000000..5853a754e Binary files /dev/null and b/src/duckstation-qt/resources/icons/media-optical.png differ 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 000000000..1e9f4d535 Binary files /dev/null and b/src/duckstation-qt/resources/icons/media-playback-pause.png differ 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 000000000..66f32d89b Binary files /dev/null and b/src/duckstation-qt/resources/icons/media-playback-start.png differ diff --git a/src/duckstation-qt/resources/icons/system-search.png b/src/duckstation-qt/resources/icons/system-search.png new file mode 100644 index 000000000..950d792af Binary files /dev/null and b/src/duckstation-qt/resources/icons/system-search.png differ 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 000000000..36acd46bd Binary files /dev/null and b/src/duckstation-qt/resources/icons/system-shutdown.png differ 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 000000000..b62959e4f Binary files /dev/null and b/src/duckstation-qt/resources/icons/utilities-system-monitor.png differ 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 000000000..b95ea5d36 Binary files /dev/null and b/src/duckstation-qt/resources/icons/video-display.png differ diff --git a/src/duckstation-qt/resources/icons/view-fullscreen.png b/src/duckstation-qt/resources/icons/view-fullscreen.png new file mode 100644 index 000000000..00e6b83cc Binary files /dev/null and b/src/duckstation-qt/resources/icons/view-fullscreen.png differ diff --git a/src/duckstation-qt/resources/icons/view-refresh.png b/src/duckstation-qt/resources/icons/view-refresh.png new file mode 100644 index 000000000..606ea9eba Binary files /dev/null and b/src/duckstation-qt/resources/icons/view-refresh.png differ diff --git a/src/duckstation-qt/settingsdialog.cpp b/src/duckstation-qt/settingsdialog.cpp new file mode 100644 index 000000000..3f219bb34 --- /dev/null +++ b/src/duckstation-qt/settingsdialog.cpp @@ -0,0 +1,42 @@ +#include "settingsdialog.h" +#include "consolesettingswidget.h" +#include "gamelistsettingswidget.h" +#include "qthostinterface.h" +#include + +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 + + + + +