From 08f3639c80f188a3efd7ed1277b0bf8baaa33681 Mon Sep 17 00:00:00 2001 From: crueter Date: Sun, 9 Nov 2025 18:07:38 +0100 Subject: [PATCH] [desktop, fs] main_window separation; fix Ryujinx save data link issues (#2929) Some genius decided to put the entire MainWindow class into main.h and main.cpp, which is not only horrific practice but also completely destroys clangd beyond repair. Please, just don't do this. (this will probably merge conflict to hell and back) Also, fixes a bunch of issues with Ryujinx save data link: - Paths with spaces would cause mklink to fail - Add support for portable directories - Symlink detection was incorrect sometimes(????) - Some other stuff I'm forgetting Furthermore, when selecting "From Eden" and attempting to save in Ryujinx, Ryujinx would destroy the link for... some reason? So to get around this we just copy the Eden data to Ryujinx then treat it like a "From Ryujinx" op Signed-off-by: crueter Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2929 Reviewed-by: Lizzie Reviewed-by: CamilleLaVey --- CMakeLists.txt | 2 +- cpmfile.json | 2 +- src/common/CMakeLists.txt | 4 +- src/common/fs/ryujinx_compat.cpp | 19 +- src/common/fs/ryujinx_compat.h | 11 +- src/common/fs/symlink.cpp | 39 +- src/common/fs/symlink.h | 2 +- src/core/hle/service/acc/profile_manager.cpp | 57 +- src/core/hle/service/acc/profile_manager.h | 2 + src/qt_common/abstract/frontend.cpp | 11 +- src/qt_common/abstract/frontend.h | 4 + src/qt_common/config/qt_config.cpp | 26 + src/qt_common/config/shared_translation.h | 54 +- src/qt_common/config/uisettings.h | 2 + src/qt_common/qt_string_lookup.h | 6 + src/qt_common/util/content.h | 7 +- src/qt_common/util/fs.cpp | 76 +- src/qt_common/util/fs.h | 9 +- src/yuzu/CMakeLists.txt | 8 +- src/yuzu/applets/qt_amiibo_settings.cpp | 13 +- src/yuzu/applets/qt_amiibo_settings.h | 7 +- src/yuzu/applets/qt_controller.cpp | 10 +- src/yuzu/applets/qt_controller.h | 7 +- src/yuzu/applets/qt_error.cpp | 13 +- src/yuzu/applets/qt_error.h | 7 +- src/yuzu/applets/qt_profile_select.cpp | 10 +- src/yuzu/applets/qt_profile_select.h | 7 +- src/yuzu/applets/qt_software_keyboard.cpp | 25 +- src/yuzu/applets/qt_software_keyboard.h | 7 +- src/yuzu/applets/qt_web_browser.cpp | 15 +- src/yuzu/applets/qt_web_browser.h | 7 +- src/yuzu/bootmanager.cpp | 12 +- src/yuzu/bootmanager.h | 7 +- src/yuzu/data_dialog.cpp | 60 +- src/yuzu/data_dialog.ui | 25 - src/yuzu/deps_dialog.cpp | 4 +- src/yuzu/game_list.cpp | 10 +- src/yuzu/game_list.h | 8 +- src/yuzu/main.cpp | 4945 +----------------- src/yuzu/main_window.cpp | 4899 +++++++++++++++++ src/yuzu/{main.h => main_window.h} | 13 +- src/yuzu/migration_worker.cpp | 37 +- src/yuzu/migration_worker.h | 22 +- src/yuzu/multiplayer/direct_connect.cpp | 2 +- src/yuzu/multiplayer/host_room.cpp | 2 +- src/yuzu/multiplayer/lobby.cpp | 2 +- src/yuzu/ryujinx_dialog.cpp | 26 +- src/yuzu/user_data_migration.cpp | 61 +- src/yuzu/user_data_migration.h | 1 - src/yuzu/util/util.cpp | 50 + src/yuzu/util/util.h | 16 + 51 files changed, 5387 insertions(+), 5284 deletions(-) create mode 100644 src/yuzu/main_window.cpp rename src/yuzu/{main.h => main_window.h} (98%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ced1ff77a..8ae534444e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -586,7 +586,7 @@ else() find_package(zstd 1.5 REQUIRED MODULE) # wow - find_package(Boost 1.57.0 CONFIG REQUIRED OPTIONAL_COMPONENTS headers context system fiber) + find_package(Boost 1.57.0 CONFIG REQUIRED OPTIONAL_COMPONENTS headers context system fiber filesystem) if (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR ANDROID) find_package(gamemode 1.7 MODULE) diff --git a/cpmfile.json b/cpmfile.json index ee5fb89c15..cb0bfb4487 100644 --- a/cpmfile.json +++ b/cpmfile.json @@ -20,7 +20,7 @@ "hash": "4fb7f6fde92762305aad8754d7643cd918dd1f3f67e104e9ab385b18c73178d72a17321354eb203b790b6702f2cf6d725a5d6e2dfbc63b1e35f9eb59fb42ece9", "git_version": "1.89.0", "version": "1.57", - "find_args": "CONFIG", + "find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem", "patches": [ "0001-clang-cl.patch", "0002-use-marmasm.patch", diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 17d6a42f37..b07778be28 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -252,11 +252,13 @@ if(CXX_CLANG) endif() if (BOOST_NO_HEADERS) - target_link_libraries(common PUBLIC Boost::algorithm Boost::icl Boost::pool) + target_link_libraries(common PUBLIC Boost::algorithm Boost::icl Boost::pool Boost::filesystem) else() target_link_libraries(common PUBLIC Boost::headers) endif() +target_link_libraries(common PUBLIC Boost::filesystem) + if (lz4_ADDED) target_include_directories(common PRIVATE ${lz4_SOURCE_DIR}/lib) endif() diff --git a/src/common/fs/ryujinx_compat.cpp b/src/common/fs/ryujinx_compat.cpp index 610ba736a7..bb6a633723 100644 --- a/src/common/fs/ryujinx_compat.cpp +++ b/src/common/fs/ryujinx_compat.cpp @@ -14,16 +14,29 @@ namespace fs = std::filesystem; fs::path GetKvdbPath() { - return GetLegacyPath(EmuPath::RyujinxDir) / "bis" / "system" / "save" / "8000000000000000" / "0" + return GetKvdbPath(GetLegacyPath(EmuPath::RyujinxDir)); +} + +fs::path GetKvdbPath(const fs::path& path) { + return path / "bis" / "system" / "save" / "8000000000000000" / "0" / "imkvdb.arc"; } +fs::path GetRyuPathFromSavePath(const fs::path& path) { + // This is a horrible hack, but I cba to find something better + return path.parent_path().parent_path().parent_path().parent_path().parent_path(); +} + fs::path GetRyuSavePath(const u64 &save_id) { + return GetRyuSavePath(GetLegacyPath(EmuPath::RyujinxDir), save_id); +} + +std::filesystem::path GetRyuSavePath(const std::filesystem::path& path, const u64& save_id) { std::string hex = fmt::format("{:016x}", save_id); - // TODO: what's the difference between 0 and 1? - return GetLegacyPath(EmuPath::RyujinxDir) / "bis" / "user" / "save" / hex / "0"; + // TODO: what's the difference between 0 and 1? + return path / "bis" / "user" / "save" / hex / "0"; } IMENReadResult ReadKvdb(const fs::path &path, std::vector &imens) diff --git a/src/common/fs/ryujinx_compat.h b/src/common/fs/ryujinx_compat.h index 6273754074..d243337791 100644 --- a/src/common/fs/ryujinx_compat.h +++ b/src/common/fs/ryujinx_compat.h @@ -7,16 +7,17 @@ #include #include -namespace fs = std::filesystem; - namespace Common::FS { constexpr const char IMEN_MAGIC[4] = {0x49, 0x4d, 0x45, 0x4e}; constexpr const char IMKV_MAGIC[4] = {0x49, 0x4d, 0x4b, 0x56}; constexpr const u8 IMEN_SIZE = 0x8c; -fs::path GetKvdbPath(); -fs::path GetRyuSavePath(const u64 &program_id); +std::filesystem::path GetKvdbPath(); +std::filesystem::path GetKvdbPath(const std::filesystem::path &path); +std::filesystem::path GetRyuPathFromSavePath(const std::filesystem::path &path); +std::filesystem::path GetRyuSavePath(const u64 &save_id); +std::filesystem::path GetRyuSavePath(const std::filesystem::path &path, const u64 &save_id); enum class IMENReadResult { Nonexistent, // ryujinx not found @@ -35,6 +36,6 @@ struct IMEN static_assert(sizeof(IMEN) == 0x10, "IMEN has incorrect size."); -IMENReadResult ReadKvdb(const fs::path &path, std::vector &imens); +IMENReadResult ReadKvdb(const std::filesystem::path &path, std::vector &imens); } // namespace Common::FS diff --git a/src/common/fs/symlink.cpp b/src/common/fs/symlink.cpp index 9c9fe3d50b..01be5e180a 100644 --- a/src/common/fs/symlink.cpp +++ b/src/common/fs/symlink.cpp @@ -4,10 +4,12 @@ #include "symlink.h" #ifdef _WIN32 -#include #include +#include #endif +#include + namespace fs = std::filesystem; // The sole purpose of this file is to treat symlinks like symlinks on POSIX, @@ -15,29 +17,40 @@ namespace fs = std::filesystem; // This is because, for some inexplicable reason, Microsoft has locked symbolic // links behind a "security policy", whereas directory junctions--functionally identical // for directories, by the way--are not. Why? I don't know. +// And no, they do NOT provide a standard API for this (at least to my knowledge). +// CreateSymbolicLink, even when EXPLICITLY TOLD to create a junction, still fails +// because of their security policy. +// I don't know what kind of drugs the Windows developers have been on since NT started. + +// Microsoft still has not implemented any of this in their std::filesystem implemenation, +// which ALSO means that it DOES NOT FOLLOW ANY DIRECTORY JUNCTIONS... AT ALL. +// Nor does any of their command line utilities or APIs. So you're quite literally +// on your own. namespace Common::FS { -bool CreateSymlink(const fs::path &from, const fs::path &to) +bool CreateSymlink(fs::path from, fs::path to) { -#ifdef _WIN32 - const std::string command = fmt::format("mklink /J {} {}", to.string(), from.string()); - return system(command.c_str()) == 0; -#else + from.make_preferred(); + to.make_preferred(); + std::error_code ec; fs::create_directory_symlink(from, to, ec); - return !ec; +#ifdef _WIN32 + if (ec) { + const std::string command = fmt::format("mklink /J \"{}\" \"{}\"", + to.string(), + from.string()); + return system(command.c_str()) == 0; + } #endif + + return !ec; } bool IsSymlink(const fs::path &path) { -#ifdef _WIN32 - auto attributes = GetFileAttributesW(path.wstring().c_str()); - return attributes & FILE_ATTRIBUTE_REPARSE_POINT; -#else - return fs::is_symlink(path); -#endif + return boost::filesystem::is_symlink(boost::filesystem::path{path}); } } // namespace Common::FS diff --git a/src/common/fs/symlink.h b/src/common/fs/symlink.h index 3bfff95ebf..175196185e 100644 --- a/src/common/fs/symlink.h +++ b/src/common/fs/symlink.h @@ -6,7 +6,7 @@ #include namespace Common::FS { -bool CreateSymlink(const std::filesystem::path &from, const std::filesystem::path &to); +bool CreateSymlink(std::filesystem::path from, std::filesystem::path to); bool IsSymlink(const std::filesystem::path &path); } // namespace Common::FS diff --git a/src/core/hle/service/acc/profile_manager.cpp b/src/core/hle/service/acc/profile_manager.cpp index c0cc2986a7..b610792271 100644 --- a/src/core/hle/service/acc/profile_manager.cpp +++ b/src/core/hle/service/acc/profile_manager.cpp @@ -7,8 +7,6 @@ #include #include #include -#include -#include #include #include @@ -18,11 +16,11 @@ #include "common/fs/fs.h" #include "common/fs/fs_types.h" #include "common/fs/path_util.h" +#include "common/fs/symlink.h" #include "common/settings.h" #include "common/string_util.h" #include "core/file_sys/savedata_factory.h" #include "core/hle/service/acc/profile_manager.h" -#include namespace Service::Account { @@ -492,6 +490,32 @@ void ProfileManager::ResetUserSaveFile() ParseUserSaveFile(); } +std::vector ProfileManager::FindExistingProfileUUIDs() +{ + std::vector uuids; + for (const ProfileInfo& p : profiles) { + auto uuid = p.user_uuid; + if (!uuid.IsInvalid()) { + uuids.emplace_back(uuid); + } + } + + return uuids; +} + +std::vector ProfileManager::FindExistingProfileStrings() +{ + std::vector uuids = FindExistingProfileUUIDs(); + std::vector uuid_strings; + + for (const UUID &uuid : uuids) { + auto user_id = uuid.AsU128(); + uuid_strings.emplace_back(fmt::format("{:016X}{:016X}", user_id[1], user_id[0])); + } + + return uuid_strings; +} + std::vector ProfileManager::FindGoodProfiles() { namespace fs = std::filesystem; @@ -501,31 +525,17 @@ std::vector ProfileManager::FindGoodProfiles() const auto path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) / "user/save/0000000000000000"; - // some exceptions because certain games just LOVE TO CAUSE ISSUES - static constexpr const std::array EXCEPTION_UUIDS - = {"5755CC2A545A87128500000000000000", "00000000000000000000000000000000"}; + // some exceptions, e.g. the "system" profile + static constexpr const std::array EXCEPTION_UUIDS + = {"00000000000000000000000000000000"}; for (const char *const uuid : EXCEPTION_UUIDS) { if (fs::exists(path / uuid)) good_uuids.emplace_back(uuid); } - for (const ProfileInfo& p : profiles) { - std::string uuid_string = [p]() -> std::string { - auto uuid = p.user_uuid; - - // "ignore" invalid uuids - if (uuid.IsInvalid()) { - return "0"; - } - - auto user_id = uuid.AsU128(); - - return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]); - }(); - - if (uuid_string != "0") good_uuids.emplace_back(uuid_string); - } + auto existing = FindExistingProfileStrings(); + good_uuids.insert(good_uuids.end(), existing.begin(), existing.end()); return good_uuids; } @@ -562,7 +572,8 @@ std::vector ProfileManager::FindOrphanedProfiles() override = true; // if there are any regular files (NOT directories) there, do NOT delete it :p - if (file.is_regular_file()) + // Also: check for symlinks + if (file.is_regular_file() || Common::FS::IsSymlink(file.path())) return false; } } catch (const fs::filesystem_error& e) { diff --git a/src/core/hle/service/acc/profile_manager.h b/src/core/hle/service/acc/profile_manager.h index 9c91fcde41..4948118b92 100644 --- a/src/core/hle/service/acc/profile_manager.h +++ b/src/core/hle/service/acc/profile_manager.h @@ -105,6 +105,8 @@ public: void ResetUserSaveFile(); + std::vector FindExistingProfileUUIDs(); + std::vector FindExistingProfileStrings(); std::vector FindGoodProfiles(); std::vector FindOrphanedProfiles(); diff --git a/src/qt_common/abstract/frontend.cpp b/src/qt_common/abstract/frontend.cpp index 2ed76f8ea9..a0ce943538 100644 --- a/src/qt_common/abstract/frontend.cpp +++ b/src/qt_common/abstract/frontend.cpp @@ -28,7 +28,7 @@ const QString GetOpenFileName(const QString &title, Options options) { #ifdef YUZU_QT_WIDGETS - return QFileDialog::getOpenFileName((QWidget *) rootObject, title, dir, filter, selectedFilter, options); + return QFileDialog::getOpenFileName(rootObject, title, dir, filter, selectedFilter, options); #endif } @@ -39,7 +39,14 @@ const QString GetSaveFileName(const QString &title, Options options) { #ifdef YUZU_QT_WIDGETS - return QFileDialog::getSaveFileName((QWidget *) rootObject, title, dir, filter, selectedFilter, options); + return QFileDialog::getSaveFileName(rootObject, title, dir, filter, selectedFilter, options); +#endif +} + +const QString GetExistingDirectory(const QString& caption, const QString& dir, + Options options) { +#ifdef YUZU_QT_WIDGETS + return QFileDialog::getExistingDirectory(rootObject, caption, dir, options); #endif } diff --git a/src/qt_common/abstract/frontend.h b/src/qt_common/abstract/frontend.h index b496b37cbe..0ef9ea97e4 100644 --- a/src/qt_common/abstract/frontend.h +++ b/src/qt_common/abstract/frontend.h @@ -135,5 +135,9 @@ const QString GetSaveFileName(const QString &title, QString *selectedFilter = nullptr, Options options = Options()); +const QString GetExistingDirectory(const QString &caption = QString(), + const QString &dir = QString(), + Options options = Option::ShowDirsOnly); + } // namespace QtCommon::Frontend #endif // FRONTEND_H diff --git a/src/qt_common/config/qt_config.cpp b/src/qt_common/config/qt_config.cpp index f787873cf6..65bf488c5c 100644 --- a/src/qt_common/config/qt_config.cpp +++ b/src/qt_common/config/qt_config.cpp @@ -294,6 +294,17 @@ void QtConfig::ReadUIGamelistValues() { } EndArray(); + const int linked_size = BeginArray("ryujinx_linked"); + for (int i = 0; i < linked_size; ++i) { + SetArrayIndex(i); + + QDir ryu_dir = QString::fromStdString(ReadStringSetting("ryujinx_path")); + u64 program_id = ReadUnsignedIntegerSetting("program_id"); + + UISettings::values.ryujinx_link_paths.insert(program_id, ryu_dir); + } + EndArray(); + EndGroup(); } @@ -499,6 +510,21 @@ void QtConfig::SaveUIGamelistValues() { } EndArray(); // favorites + BeginArray(std::string("ryujinx_linked")); + int i = 0; + QMapIterator iter(UISettings::values.ryujinx_link_paths); + + while (iter.hasNext()) { + iter.next(); + + SetArrayIndex(i); + WriteIntegerSetting("program_id", iter.key()); + WriteStringSetting("ryujinx_path", iter.value().absolutePath().toStdString()); + ++i; + } + + EndArray(); // ryujinx + EndGroup(); } diff --git a/src/qt_common/config/shared_translation.h b/src/qt_common/config/shared_translation.h index 1ac68de3cc..8cc54644af 100644 --- a/src/qt_common/config/shared_translation.h +++ b/src/qt_common/config/shared_translation.h @@ -28,54 +28,54 @@ std::unique_ptr InitializeTranslations(QObject *parent); std::unique_ptr ComboboxEnumeration(QObject* parent); static const std::map anti_aliasing_texts_map = { - {Settings::AntiAliasing::None, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "None"))}, - {Settings::AntiAliasing::Fxaa, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FXAA"))}, - {Settings::AntiAliasing::Smaa, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "SMAA"))}, + {Settings::AntiAliasing::None, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "None"))}, + {Settings::AntiAliasing::Fxaa, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FXAA"))}, + {Settings::AntiAliasing::Smaa, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SMAA"))}, }; static const std::map scaling_filter_texts_map = { {Settings::ScalingFilter::NearestNeighbor, - QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Nearest"))}, + QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Nearest"))}, {Settings::ScalingFilter::Bilinear, - QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Bilinear"))}, - {Settings::ScalingFilter::Bicubic, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Bicubic"))}, - {Settings::ScalingFilter::ZeroTangent, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Zero-Tangent"))}, - {Settings::ScalingFilter::BSpline, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "B-Spline"))}, - {Settings::ScalingFilter::Mitchell, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Mitchell"))}, + QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Bilinear"))}, + {Settings::ScalingFilter::Bicubic, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Bicubic"))}, + {Settings::ScalingFilter::ZeroTangent, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Zero-Tangent"))}, + {Settings::ScalingFilter::BSpline, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "B-Spline"))}, + {Settings::ScalingFilter::Mitchell, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Mitchell"))}, {Settings::ScalingFilter::Spline1, - QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Spline-1"))}, + QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Spline-1"))}, {Settings::ScalingFilter::Gaussian, - QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Gaussian"))}, + QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Gaussian"))}, {Settings::ScalingFilter::Lanczos, - QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Lanczos"))}, + QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Lanczos"))}, {Settings::ScalingFilter::ScaleForce, - QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "ScaleForce"))}, - {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FSR"))}, - {Settings::ScalingFilter::Area, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Area"))}, - {Settings::ScalingFilter::Mmpx, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "MMPX"))}, + QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "ScaleForce"))}, + {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR"))}, + {Settings::ScalingFilter::Area, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Area"))}, + {Settings::ScalingFilter::Mmpx, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "MMPX"))}, }; static const std::map use_docked_mode_texts_map = { - {Settings::ConsoleMode::Docked, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Docked"))}, - {Settings::ConsoleMode::Handheld, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Handheld"))}, + {Settings::ConsoleMode::Docked, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Docked"))}, + {Settings::ConsoleMode::Handheld, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Handheld"))}, }; static const std::map gpu_accuracy_texts_map = { - {Settings::GpuAccuracy::Normal, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Normal"))}, - {Settings::GpuAccuracy::High, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "High"))}, - {Settings::GpuAccuracy::Extreme, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Extreme"))}, + {Settings::GpuAccuracy::Normal, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Normal"))}, + {Settings::GpuAccuracy::High, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "High"))}, + {Settings::GpuAccuracy::Extreme, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Extreme"))}, }; static const std::map renderer_backend_texts_map = { - {Settings::RendererBackend::Vulkan, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Vulkan"))}, - {Settings::RendererBackend::OpenGL, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "OpenGL"))}, - {Settings::RendererBackend::Null, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "Null"))}, + {Settings::RendererBackend::Vulkan, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Vulkan"))}, + {Settings::RendererBackend::OpenGL, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "OpenGL"))}, + {Settings::RendererBackend::Null, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Null"))}, }; static const std::map shader_backend_texts_map = { - {Settings::ShaderBackend::Glsl, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "GLSL"))}, - {Settings::ShaderBackend::Glasm, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "GLASM"))}, - {Settings::ShaderBackend::SpirV, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "SPIRV"))}, + {Settings::ShaderBackend::Glsl, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "GLSL"))}, + {Settings::ShaderBackend::Glasm, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "GLASM"))}, + {Settings::ShaderBackend::SpirV, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SPIRV"))}, }; } // namespace ConfigurationShared diff --git a/src/qt_common/config/uisettings.h b/src/qt_common/config/uisettings.h index 042dad16df..5ca1a362f7 100644 --- a/src/qt_common/config/uisettings.h +++ b/src/qt_common/config/uisettings.h @@ -14,6 +14,7 @@ #include #include #include +#include #include "common/common_types.h" #include "common/settings.h" #include "common/settings_enums.h" @@ -201,6 +202,7 @@ struct Values { Setting cache_game_list{linkage, true, "cache_game_list", Category::UiGameList}; Setting favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList}; QVector favorited_ids; + QMap ryujinx_link_paths; // Compatibility List Setting show_compat{linkage, true, "show_compat", Category::UiGameList}; diff --git a/src/qt_common/qt_string_lookup.h b/src/qt_common/qt_string_lookup.h index 42d71e6f9d..b33bd044c2 100644 --- a/src/qt_common/qt_string_lookup.h +++ b/src/qt_common/qt_string_lookup.h @@ -8,6 +8,12 @@ #include "frozen/map.h" #include "frozen/string.h" +/// Small helper to look up enums. +/// res = the result code +/// base = the base matching value in the StringKey table +#define LOOKUP_ENUM(res, base) StringLookup::Lookup( \ + static_cast((int) res + (int) StringLookup::base)) + namespace QtCommon::StringLookup { Q_NAMESPACE diff --git a/src/qt_common/util/content.h b/src/qt_common/util/content.h index 4102737fb1..6e8642083f 100644 --- a/src/qt_common/util/content.h +++ b/src/qt_common/util/content.h @@ -25,8 +25,7 @@ enum class FirmwareInstallResult { inline const QString GetFirmwareInstallResultString(FirmwareInstallResult result) { - return QtCommon::StringLookup::Lookup(static_cast( - (int) result + (int) QtCommon::StringLookup::FwInstallSuccess)); + return LOOKUP_ENUM(result, FwInstallSuccess); } /** @@ -36,9 +35,7 @@ inline const QString GetFirmwareInstallResultString(FirmwareInstallResult result */ inline const QString GetKeyInstallResultString(FirmwareManager::KeyInstallResult result) { - // this can probably be made into a common function of sorts - return QtCommon::StringLookup::Lookup(static_cast( - (int) result + (int) QtCommon::StringLookup::KeyInstallSuccess)); + return LOOKUP_ENUM(result, KeyInstallSuccess); } void InstallFirmware(const QString &location, bool recursive); diff --git a/src/qt_common/util/fs.cpp b/src/qt_common/util/fs.cpp index da59a4e675..dd105849aa 100644 --- a/src/qt_common/util/fs.cpp +++ b/src/qt_common/util/fs.cpp @@ -1,11 +1,11 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include #include -#include "fs.h" #include "common/fs/ryujinx_compat.h" #include "common/fs/symlink.h" -#include "frontend_common/data_manager.h" +#include "fs.h" #include "qt_common/abstract/frontend.h" #include "qt_common/qt_string_lookup.h" @@ -56,6 +56,9 @@ bool CheckUnlink(const fs::path &eden_dir, const fs::path &ryu_dir) orig = eden_dir; } + linked.make_preferred(); + orig.make_preferred(); + // first cleanup the symlink/junction, try { // NB: do NOT use remove_all, as Windows treats this as a remove_all to the target, @@ -84,47 +87,64 @@ bool CheckUnlink(const fs::path &eden_dir, const fs::path &ryu_dir) return true; } -u64 GetRyujinxSaveID(const u64 &program_id) +const fs::path GetRyujinxSavePath(const fs::path &path_hint, const u64 &program_id) { - auto path = Common::FS::GetKvdbPath(); + auto ryu_path = path_hint; + + auto kvdb_path = Common::FS::GetKvdbPath(ryu_path); + + if (!fs::exists(kvdb_path)) { + using namespace QtCommon::Frontend; + auto res = Warning( + tr("Could not find Ryujinx installation"), + tr("Could not find a valid Ryujinx installation. This may typically occur if you are " + "using Ryujinx in portable mode.\n\nWould you like to manually select a portable " + "folder to use?"), StandardButton::Yes | StandardButton::No); + + if (res == StandardButton::Yes) { + auto selected_path = GetExistingDirectory(tr("Ryujinx Portable Location"), QDir::homePath()).toStdString(); + if (selected_path.empty()) + return fs::path{}; + + ryu_path = selected_path; + + // In case the user selects the actual ryujinx installation dir INSTEAD OF + // the portable dir + if (fs::exists(ryu_path / "portable")) { + ryu_path = ryu_path / "portable"; + } + + kvdb_path = Common::FS::GetKvdbPath(ryu_path); + + if (!fs::exists(kvdb_path)) { + QtCommon::Frontend::Critical( + tr("Not a valid Ryujinx directory"), + tr("The specified directory does not contain valid Ryujinx data.")); + return fs::path{}; + } + } else { + return fs::path{}; + } + } + std::vector imens; - Common::FS::IMENReadResult res = Common::FS::ReadKvdb(path, imens); + Common::FS::IMENReadResult res = Common::FS::ReadKvdb(kvdb_path, imens); if (res == Common::FS::IMENReadResult::Success) { - // TODO: this can probably be done with std::find_if but I'm lazy for (const Common::FS::IMEN &imen : imens) { if (imen.title_id == program_id) - return imen.save_id; + return Common::FS::GetRyuSavePath(ryu_path, imen.save_id); } QtCommon::Frontend::Critical( tr("Could not find Ryujinx save data"), StringLookup::Lookup(StringLookup::RyujinxNoSaveId).arg(program_id, 0, 16)); } else { - // TODO: make this long thing a function or something - QString caption = StringLookup::Lookup( - static_cast((int) res + (int) StringLookup::KvdbNonexistent)); + QString caption = LOOKUP_ENUM(res, KvdbNonexistent); QtCommon::Frontend::Critical(tr("Could not find Ryujinx save data"), caption); } - return -1; -} - -std::optional > GetEmuPaths( - const u64 program_id, const u64 save_id, const std::string &user_id) -{ - fs::path ryu_dir = Common::FS::GetRyuSavePath(save_id); - - if (user_id.empty()) - return std::nullopt; - - std::string hex_program = fmt::format("{:016X}", program_id); - fs::path eden_dir - = FrontendCommon::DataManager::GetDataDir(FrontendCommon::DataManager::DataDir::Saves, - user_id) - / hex_program; - - return std::make_pair(eden_dir, ryu_dir); + return fs::path{}; } } // namespace QtCommon::FS diff --git a/src/qt_common/util/fs.h b/src/qt_common/util/fs.h index ee8859a2e5..41669e8019 100644 --- a/src/qt_common/util/fs.h +++ b/src/qt_common/util/fs.h @@ -10,13 +10,10 @@ namespace QtCommon::FS { void LinkRyujinx(std::filesystem::path &from, std::filesystem::path &to); -u64 GetRyujinxSaveID(const u64 &program_id); - -/// @brief {eden, ryu} -std::optional> GetEmuPaths( - const u64 program_id, const u64 save_id, const std::string &user_id); +const std::filesystem::path GetRyujinxSavePath(const std::filesystem::path &path_hint, const u64 &program_id); /// returns FALSE if the dirs are NOT linked -bool CheckUnlink(const std::filesystem::path &eden_dir, const std::filesystem::path &ryu_dir); +bool CheckUnlink(const std::filesystem::path& eden_dir, + const std::filesystem::path& ryu_dir); } // namespace QtCommon::FS diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 04db8bdc94..79453e4570 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -12,6 +12,8 @@ if (YUZU_USE_BUNDLED_QT AND PLATFORM_LINUX) set(CMAKE_BUILD_RPATH "${CMAKE_BINARY_DIR}/bin/lib/") endif() +find_package(Qt6 REQUIRED COMPONENTS Widgets) + add_executable(yuzu Info.plist about_dialog.cpp @@ -169,9 +171,9 @@ add_executable(yuzu loading_screen.cpp loading_screen.h loading_screen.ui + main.cpp - main.h - main.ui + multiplayer/chat_room.cpp multiplayer/chat_room.h multiplayer/chat_room.ui @@ -235,6 +237,7 @@ add_executable(yuzu data_dialog.h data_dialog.cpp data_dialog.ui data_widget.ui ryujinx_dialog.h ryujinx_dialog.cpp ryujinx_dialog.ui + main_window.h main_window.cpp ) set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden") @@ -441,6 +444,7 @@ endif() if (YUZU_ROOM) target_link_libraries(yuzu PRIVATE yuzu-room) +target_link_libraries(yuzu PRIVATE Qt6::Widgets) endif() create_target_directory_groups(yuzu) diff --git a/src/yuzu/applets/qt_amiibo_settings.cpp b/src/yuzu/applets/qt_amiibo_settings.cpp index ca9c5cfbdc..932f306bc3 100644 --- a/src/yuzu/applets/qt_amiibo_settings.cpp +++ b/src/yuzu/applets/qt_amiibo_settings.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -18,7 +21,7 @@ #include "web_service/web_backend.h" #endif #include "yuzu/applets/qt_amiibo_settings.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" QtAmiiboSettingsDialog::QtAmiiboSettingsDialog(QWidget* parent, Core::Frontend::CabinetParameters parameters_, @@ -244,12 +247,12 @@ void QtAmiiboSettingsDialog::SetSettingsDescription() { } } -QtAmiiboSettings::QtAmiiboSettings(GMainWindow& parent) { +QtAmiiboSettings::QtAmiiboSettings(MainWindow& parent) { connect(this, &QtAmiiboSettings::MainWindowShowAmiiboSettings, &parent, - &GMainWindow::AmiiboSettingsShowDialog, Qt::QueuedConnection); + &MainWindow::AmiiboSettingsShowDialog, Qt::QueuedConnection); connect(this, &QtAmiiboSettings::MainWindowRequestExit, &parent, - &GMainWindow::AmiiboSettingsRequestExit, Qt::QueuedConnection); - connect(&parent, &GMainWindow::AmiiboSettingsFinished, this, + &MainWindow::AmiiboSettingsRequestExit, Qt::QueuedConnection); + connect(&parent, &MainWindow::AmiiboSettingsFinished, this, &QtAmiiboSettings::MainWindowFinished, Qt::QueuedConnection); } diff --git a/src/yuzu/applets/qt_amiibo_settings.h b/src/yuzu/applets/qt_amiibo_settings.h index 3833cf6f2a..24bf9681e9 100644 --- a/src/yuzu/applets/qt_amiibo_settings.h +++ b/src/yuzu/applets/qt_amiibo_settings.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -8,7 +11,7 @@ #include #include "core/frontend/applets/cabinet.h" -class GMainWindow; +class MainWindow; class QCheckBox; class QComboBox; class QDialogButtonBox; @@ -65,7 +68,7 @@ class QtAmiiboSettings final : public QObject, public Core::Frontend::CabinetApp Q_OBJECT public: - explicit QtAmiiboSettings(GMainWindow& parent); + explicit QtAmiiboSettings(MainWindow& parent); ~QtAmiiboSettings() override; void Close() const override; diff --git a/src/yuzu/applets/qt_controller.cpp b/src/yuzu/applets/qt_controller.cpp index 7977d75ca0..aa210caf77 100644 --- a/src/yuzu/applets/qt_controller.cpp +++ b/src/yuzu/applets/qt_controller.cpp @@ -25,7 +25,7 @@ #include "yuzu/configuration/configure_motion_touch.h" #include "yuzu/configuration/configure_vibration.h" #include "yuzu/configuration/input_profiles.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #include "yuzu/util/controller_navigation.h" namespace { @@ -753,12 +753,12 @@ void QtControllerSelectorDialog::DisableUnsupportedPlayers() { } } -QtControllerSelector::QtControllerSelector(GMainWindow& parent) { +QtControllerSelector::QtControllerSelector(MainWindow& parent) { connect(this, &QtControllerSelector::MainWindowReconfigureControllers, &parent, - &GMainWindow::ControllerSelectorReconfigureControllers, Qt::QueuedConnection); + &MainWindow::ControllerSelectorReconfigureControllers, Qt::QueuedConnection); connect(this, &QtControllerSelector::MainWindowRequestExit, &parent, - &GMainWindow::ControllerSelectorRequestExit, Qt::QueuedConnection); - connect(&parent, &GMainWindow::ControllerSelectorReconfigureFinished, this, + &MainWindow::ControllerSelectorRequestExit, Qt::QueuedConnection); + connect(&parent, &MainWindow::ControllerSelectorReconfigureFinished, this, &QtControllerSelector::MainWindowReconfigureFinished, Qt::QueuedConnection); } diff --git a/src/yuzu/applets/qt_controller.h b/src/yuzu/applets/qt_controller.h index e5372495d7..8bd2db2f11 100644 --- a/src/yuzu/applets/qt_controller.h +++ b/src/yuzu/applets/qt_controller.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -8,7 +11,7 @@ #include #include "core/frontend/applets/controller.h" -class GMainWindow; +class MainWindow; class QCheckBox; class QComboBox; class QDialogButtonBox; @@ -163,7 +166,7 @@ class QtControllerSelector final : public QObject, public Core::Frontend::Contro Q_OBJECT public: - explicit QtControllerSelector(GMainWindow& parent); + explicit QtControllerSelector(MainWindow& parent); ~QtControllerSelector() override; void Close() const override; diff --git a/src/yuzu/applets/qt_error.cpp b/src/yuzu/applets/qt_error.cpp index ad35f41260..b0872ca270 100644 --- a/src/yuzu/applets/qt_error.cpp +++ b/src/yuzu/applets/qt_error.cpp @@ -1,16 +1,19 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "yuzu/applets/qt_error.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" -QtErrorDisplay::QtErrorDisplay(GMainWindow& parent) { +QtErrorDisplay::QtErrorDisplay(MainWindow& parent) { connect(this, &QtErrorDisplay::MainWindowDisplayError, &parent, - &GMainWindow::ErrorDisplayDisplayError, Qt::QueuedConnection); + &MainWindow::ErrorDisplayDisplayError, Qt::QueuedConnection); connect(this, &QtErrorDisplay::MainWindowRequestExit, &parent, - &GMainWindow::ErrorDisplayRequestExit, Qt::QueuedConnection); - connect(&parent, &GMainWindow::ErrorDisplayFinished, this, + &MainWindow::ErrorDisplayRequestExit, Qt::QueuedConnection); + connect(&parent, &MainWindow::ErrorDisplayFinished, this, &QtErrorDisplay::MainWindowFinishedError, Qt::DirectConnection); } diff --git a/src/yuzu/applets/qt_error.h b/src/yuzu/applets/qt_error.h index 957f170ade..8fed1558b9 100644 --- a/src/yuzu/applets/qt_error.h +++ b/src/yuzu/applets/qt_error.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -7,13 +10,13 @@ #include "core/frontend/applets/error.h" -class GMainWindow; +class MainWindow; class QtErrorDisplay final : public QObject, public Core::Frontend::ErrorApplet { Q_OBJECT public: - explicit QtErrorDisplay(GMainWindow& parent); + explicit QtErrorDisplay(MainWindow& parent); ~QtErrorDisplay() override; void Close() const override; diff --git a/src/yuzu/applets/qt_profile_select.cpp b/src/yuzu/applets/qt_profile_select.cpp index 8f28827191..ace6be4d3e 100644 --- a/src/yuzu/applets/qt_profile_select.cpp +++ b/src/yuzu/applets/qt_profile_select.cpp @@ -20,7 +20,7 @@ #include "core/core.h" #include "core/hle/service/acc/profile_manager.h" #include "yuzu/applets/qt_profile_select.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #include "yuzu/util/controller_navigation.h" namespace { @@ -230,12 +230,12 @@ void QtProfileSelectionDialog::SetDialogPurpose( } } -QtProfileSelector::QtProfileSelector(GMainWindow& parent) { +QtProfileSelector::QtProfileSelector(MainWindow& parent) { connect(this, &QtProfileSelector::MainWindowSelectProfile, &parent, - &GMainWindow::ProfileSelectorSelectProfile, Qt::QueuedConnection); + &MainWindow::ProfileSelectorSelectProfile, Qt::QueuedConnection); connect(this, &QtProfileSelector::MainWindowRequestExit, &parent, - &GMainWindow::ProfileSelectorRequestExit, Qt::QueuedConnection); - connect(&parent, &GMainWindow::ProfileSelectorFinishedSelection, this, + &MainWindow::ProfileSelectorRequestExit, Qt::QueuedConnection); + connect(&parent, &MainWindow::ProfileSelectorFinishedSelection, this, &QtProfileSelector::MainWindowFinishedSelection, Qt::DirectConnection); } diff --git a/src/yuzu/applets/qt_profile_select.h b/src/yuzu/applets/qt_profile_select.h index 607f1777cd..23903b626e 100644 --- a/src/yuzu/applets/qt_profile_select.h +++ b/src/yuzu/applets/qt_profile_select.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -9,7 +12,7 @@ #include "core/frontend/applets/profile_select.h" class ControllerNavigation; -class GMainWindow; +class MainWindow; class QDialogButtonBox; class QGraphicsScene; class QLabel; @@ -69,7 +72,7 @@ class QtProfileSelector final : public QObject, public Core::Frontend::ProfileSe Q_OBJECT public: - explicit QtProfileSelector(GMainWindow& parent); + explicit QtProfileSelector(MainWindow& parent); ~QtProfileSelector() override; void Close() const override; diff --git a/src/yuzu/applets/qt_software_keyboard.cpp b/src/yuzu/applets/qt_software_keyboard.cpp index 2749e6ed31..47b6629077 100644 --- a/src/yuzu/applets/qt_software_keyboard.cpp +++ b/src/yuzu/applets/qt_software_keyboard.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -15,7 +18,7 @@ #include "hid_core/hid_types.h" #include "ui_qt_software_keyboard.h" #include "yuzu/applets/qt_software_keyboard.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #include "yuzu/util/overlay_dialog.h" namespace { @@ -1541,24 +1544,24 @@ void QtSoftwareKeyboardDialog::InputThread() { } } -QtSoftwareKeyboard::QtSoftwareKeyboard(GMainWindow& main_window) { +QtSoftwareKeyboard::QtSoftwareKeyboard(MainWindow& main_window) { connect(this, &QtSoftwareKeyboard::MainWindowInitializeKeyboard, &main_window, - &GMainWindow::SoftwareKeyboardInitialize, Qt::QueuedConnection); + &MainWindow::SoftwareKeyboardInitialize, Qt::QueuedConnection); connect(this, &QtSoftwareKeyboard::MainWindowShowNormalKeyboard, &main_window, - &GMainWindow::SoftwareKeyboardShowNormal, Qt::QueuedConnection); + &MainWindow::SoftwareKeyboardShowNormal, Qt::QueuedConnection); connect(this, &QtSoftwareKeyboard::MainWindowShowTextCheckDialog, &main_window, - &GMainWindow::SoftwareKeyboardShowTextCheck, Qt::QueuedConnection); + &MainWindow::SoftwareKeyboardShowTextCheck, Qt::QueuedConnection); connect(this, &QtSoftwareKeyboard::MainWindowShowInlineKeyboard, &main_window, - &GMainWindow::SoftwareKeyboardShowInline, Qt::QueuedConnection); + &MainWindow::SoftwareKeyboardShowInline, Qt::QueuedConnection); connect(this, &QtSoftwareKeyboard::MainWindowHideInlineKeyboard, &main_window, - &GMainWindow::SoftwareKeyboardHideInline, Qt::QueuedConnection); + &MainWindow::SoftwareKeyboardHideInline, Qt::QueuedConnection); connect(this, &QtSoftwareKeyboard::MainWindowInlineTextChanged, &main_window, - &GMainWindow::SoftwareKeyboardInlineTextChanged, Qt::QueuedConnection); + &MainWindow::SoftwareKeyboardInlineTextChanged, Qt::QueuedConnection); connect(this, &QtSoftwareKeyboard::MainWindowExitKeyboard, &main_window, - &GMainWindow::SoftwareKeyboardExit, Qt::QueuedConnection); - connect(&main_window, &GMainWindow::SoftwareKeyboardSubmitNormalText, this, + &MainWindow::SoftwareKeyboardExit, Qt::QueuedConnection); + connect(&main_window, &MainWindow::SoftwareKeyboardSubmitNormalText, this, &QtSoftwareKeyboard::SubmitNormalText, Qt::QueuedConnection); - connect(&main_window, &GMainWindow::SoftwareKeyboardSubmitInlineText, this, + connect(&main_window, &MainWindow::SoftwareKeyboardSubmitInlineText, this, &QtSoftwareKeyboard::SubmitInlineText, Qt::QueuedConnection); } diff --git a/src/yuzu/applets/qt_software_keyboard.h b/src/yuzu/applets/qt_software_keyboard.h index 7e2fdf09ea..4c03df10fc 100644 --- a/src/yuzu/applets/qt_software_keyboard.h +++ b/src/yuzu/applets/qt_software_keyboard.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -27,7 +30,7 @@ namespace Ui { class QtSoftwareKeyboardDialog; } -class GMainWindow; +class MainWindow; class QtSoftwareKeyboardDialog final : public QDialog { Q_OBJECT @@ -230,7 +233,7 @@ class QtSoftwareKeyboard final : public QObject, public Core::Frontend::Software Q_OBJECT public: - explicit QtSoftwareKeyboard(GMainWindow& parent); + explicit QtSoftwareKeyboard(MainWindow& parent); ~QtSoftwareKeyboard() override; void Close() const override { diff --git a/src/yuzu/applets/qt_web_browser.cpp b/src/yuzu/applets/qt_web_browser.cpp index a287ea16df..cab8ef190e 100644 --- a/src/yuzu/applets/qt_web_browser.cpp +++ b/src/yuzu/applets/qt_web_browser.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -18,7 +21,7 @@ #endif #include "yuzu/applets/qt_web_browser.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #ifdef YUZU_USE_QT_WEB_ENGINE @@ -391,14 +394,14 @@ void QtNXWebEngineView::FocusFirstLinkElement() { #endif -QtWebBrowser::QtWebBrowser(GMainWindow& main_window) { +QtWebBrowser::QtWebBrowser(MainWindow& main_window) { connect(this, &QtWebBrowser::MainWindowOpenWebPage, &main_window, - &GMainWindow::WebBrowserOpenWebPage, Qt::QueuedConnection); + &MainWindow::WebBrowserOpenWebPage, Qt::QueuedConnection); connect(this, &QtWebBrowser::MainWindowRequestExit, &main_window, - &GMainWindow::WebBrowserRequestExit, Qt::QueuedConnection); - connect(&main_window, &GMainWindow::WebBrowserExtractOfflineRomFS, this, + &MainWindow::WebBrowserRequestExit, Qt::QueuedConnection); + connect(&main_window, &MainWindow::WebBrowserExtractOfflineRomFS, this, &QtWebBrowser::MainWindowExtractOfflineRomFS, Qt::QueuedConnection); - connect(&main_window, &GMainWindow::WebBrowserClosed, this, + connect(&main_window, &MainWindow::WebBrowserClosed, this, &QtWebBrowser::MainWindowWebBrowserClosed, Qt::QueuedConnection); } diff --git a/src/yuzu/applets/qt_web_browser.h b/src/yuzu/applets/qt_web_browser.h index e8a0b6931b..70b6d8e050 100644 --- a/src/yuzu/applets/qt_web_browser.h +++ b/src/yuzu/applets/qt_web_browser.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -14,7 +17,7 @@ #include "core/frontend/applets/web_browser.h" -class GMainWindow; +class MainWindow; class InputInterpreter; class UrlRequestInterceptor; @@ -193,7 +196,7 @@ class QtWebBrowser final : public QObject, public Core::Frontend::WebBrowserAppl Q_OBJECT public: - explicit QtWebBrowser(GMainWindow& parent); + explicit QtWebBrowser(MainWindow& parent); ~QtWebBrowser() override; void Close() const override; diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 8ecce2831e..a5d69821bd 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -57,7 +57,7 @@ #include "video_core/rasterizer_interface.h" #include "video_core/renderer_base.h" #include "yuzu/bootmanager.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #include "qt_common/qt_common.h" class QObject; @@ -272,7 +272,7 @@ struct NullRenderWidget : public RenderWidget { explicit NullRenderWidget(GRenderWindow* parent) : RenderWidget(parent) {} }; -GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_, +GRenderWindow::GRenderWindow(MainWindow* parent, EmuThread* emu_thread_, std::shared_ptr input_subsystem_, Core::System& system_) : QWidget(parent), @@ -290,11 +290,11 @@ GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_, strict_context_required = QGuiApplication::platformName() == QStringLiteral("wayland") || QGuiApplication::platformName() == QStringLiteral("wayland-egl"); - connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete); - connect(this, &GRenderWindow::ExecuteProgramSignal, parent, &GMainWindow::OnExecuteProgram, + connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &MainWindow::OnLoadComplete); + connect(this, &GRenderWindow::ExecuteProgramSignal, parent, &MainWindow::OnExecuteProgram, Qt::QueuedConnection); - connect(this, &GRenderWindow::ExitSignal, parent, &GMainWindow::OnExit, Qt::QueuedConnection); - connect(this, &GRenderWindow::TasPlaybackStateChanged, parent, &GMainWindow::OnTasStateChanged); + connect(this, &GRenderWindow::ExitSignal, parent, &MainWindow::OnExit, Qt::QueuedConnection); + connect(this, &GRenderWindow::TasPlaybackStateChanged, parent, &MainWindow::OnTasStateChanged); mouse_constrain_timer.setInterval(default_mouse_constrain_timeout); connect(&mouse_constrain_timer, &QTimer::timeout, this, &GRenderWindow::ConstrainMouse); diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index ae12b34818..83d364df4b 100644 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2014 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -29,7 +32,7 @@ #include "common/thread.h" #include "core/frontend/emu_window.h" -class GMainWindow; +class MainWindow; class QCamera; class QCameraImageCapture; class QCloseEvent; @@ -146,7 +149,7 @@ class GRenderWindow : public QWidget, public Core::Frontend::EmuWindow { Q_OBJECT public: - explicit GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_, + explicit GRenderWindow(MainWindow* parent, EmuThread* emu_thread_, std::shared_ptr input_subsystem_, Core::System& system_); ~GRenderWindow() override; diff --git a/src/yuzu/data_dialog.cpp b/src/yuzu/data_dialog.cpp index 147c985f83..4aaa12ce41 100644 --- a/src/yuzu/data_dialog.cpp +++ b/src/yuzu/data_dialog.cpp @@ -2,12 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "data_dialog.h" -#include "core/hle/service/acc/profile_manager.h" #include "frontend_common/data_manager.h" -#include "qt_common/qt_common.h" #include "qt_common/util/content.h" #include "qt_common/qt_string_lookup.h" #include "ui_data_dialog.h" +#include "util/util.h" #include #include @@ -26,17 +25,18 @@ DataDialog::DataDialog(QWidget *parent) ui->setupUi(this); // TODO: Should we make this a single widget that pulls data from a model? -#define WIDGET(name) \ +#define WIDGET(label, name) \ ui->page->addWidget(new DataWidget(FrontendCommon::DataManager::DataDir::name, \ QtCommon::StringLookup::name##Tooltip, \ QStringLiteral(#name), \ - this)); + this)); \ + ui->labels->addItem(label); - WIDGET(Shaders) - WIDGET(UserNand) - WIDGET(SysNand) - WIDGET(Mods) - WIDGET(Saves) + WIDGET(tr("Shaders"), Shaders) + WIDGET(tr("UserNAND"), UserNand) + WIDGET(tr("SysNAND"), SysNand) + WIDGET(tr("Mods"), Mods) + WIDGET(tr("Saves"), Saves) #undef WIDGET @@ -82,7 +82,7 @@ void DataWidget::clear() { std::string user_id{}; if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { - user_id = selectProfile(); + user_id = GetProfileIDString(); } QtCommon::Content::ClearDataDir(m_dir, user_id); scan(); @@ -92,7 +92,7 @@ void DataWidget::open() { std::string user_id{}; if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { - user_id = selectProfile(); + user_id = GetProfileIDString(); } QDesktopServices::openUrl(QUrl::fromLocalFile( QString::fromStdString(FrontendCommon::DataManager::GetDataDirString(m_dir, user_id)))); @@ -102,7 +102,7 @@ void DataWidget::upload() { std::string user_id{}; if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { - user_id = selectProfile(); + user_id = GetProfileIDString(); } QtCommon::Content::ExportDataDir(m_dir, user_id, m_exportName); } @@ -111,7 +111,7 @@ void DataWidget::download() { std::string user_id{}; if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { - user_id = selectProfile(); + user_id = GetProfileIDString(); } QtCommon::Content::ImportDataDir(m_dir, user_id, std::bind(&DataWidget::scan, this)); } @@ -131,37 +131,3 @@ void DataWidget::scan() { watcher->setFuture( QtConcurrent::run([this]() { return FrontendCommon::DataManager::DataDirSize(m_dir); })); } - -std::string DataWidget::selectProfile() -{ - const auto select_profile = [this] { - const Core::Frontend::ProfileSelectParameters parameters{ - .mode = Service::AM::Frontend::UiMode::UserSelector, - .invalid_uid_list = {}, - .display_options = {}, - .purpose = Service::AM::Frontend::UserSelectionPurpose::General, - }; - QtProfileSelectionDialog dialog(*QtCommon::system, this, parameters); - dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint - | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::WindowModal); - - if (dialog.exec() == QDialog::Rejected) { - return -1; - } - - return dialog.GetIndex(); - }; - - const auto index = select_profile(); - if (index == -1) { - return ""; - } - - const auto uuid = QtCommon::system->GetProfileManager().GetUser(static_cast(index)); - ASSERT(uuid); - - const auto user_id = uuid->AsU128(); - - return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]); -} diff --git a/src/yuzu/data_dialog.ui b/src/yuzu/data_dialog.ui index 2c735f3bc2..02e674d856 100644 --- a/src/yuzu/data_dialog.ui +++ b/src/yuzu/data_dialog.ui @@ -36,31 +36,6 @@ 0 - - - Shaders - - - - - UserNAND - - - - - SysNAND - - - - - Mods - - - - - Saves - - diff --git a/src/yuzu/deps_dialog.cpp b/src/yuzu/deps_dialog.cpp index 4cef05e054..ffbd0a0e74 100644 --- a/src/yuzu/deps_dialog.cpp +++ b/src/yuzu/deps_dialog.cpp @@ -18,7 +18,7 @@ DepsDialog::DepsDialog(QWidget* parent) { ui->setupUi(this); - constexpr size_t rows = Common::dep_hashes.size(); + constexpr int rows = (int) Common::dep_hashes.size(); ui->tableDeps->setRowCount(rows); QStringList labels; @@ -29,7 +29,7 @@ DepsDialog::DepsDialog(QWidget* parent) ui->tableDeps->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeMode::Fixed); ui->tableDeps->horizontalHeader()->setMinimumSectionSize(200); - for (size_t i = 0; i < rows; ++i) { + for (int i = 0; i < rows; ++i) { const std::string name = Common::dep_names.at(i); const std::string sha = Common::dep_hashes.at(i); const std::string url = Common::dep_urls.at(i); diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 89bbb6078f..0c59f4cc5e 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -23,7 +23,7 @@ #include "yuzu/compatibility_list.h" #include "yuzu/game_list_p.h" #include "yuzu/game_list_worker.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #include "yuzu/util/controller_navigation.h" #include #include @@ -314,7 +314,7 @@ void GameList::OnFilterCloseClicked() { GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_, PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_, - GMainWindow* parent) + MainWindow* parent) : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, play_time_manager{play_time_manager_}, system{system_} { watcher = new QFileSystemWatcher(this); @@ -347,7 +347,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time); item_model->setSortRole(GameListItemPath::SortRole); - connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons); + connect(main_window, &MainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons); connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); connect(tree_view, &QTreeView::expanded, this, &GameList::OnItemExpanded); @@ -943,8 +943,8 @@ void GameList::RemoveFavorite(u64 program_id) { } } -GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { - connect(parent, &GMainWindow::UpdateThemedIcons, this, +GameListPlaceholder::GameListPlaceholder(MainWindow* parent) : QWidget{parent} { + connect(parent, &MainWindow::UpdateThemedIcons, this, &GameListPlaceholder::onUpdateThemedIcons); layout = new QVBoxLayout; diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 0b3ac7fcde..1115eadaca 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -33,7 +33,7 @@ class ControllerNavigation; class GameListWorker; class GameListSearchField; class GameListDir; -class GMainWindow; +class MainWindow; enum class AmLaunchType; enum class StartGameType; @@ -69,7 +69,7 @@ public: explicit GameList(std::shared_ptr vfs_, FileSys::ManualContentProvider* provider_, PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_, - GMainWindow* parent = nullptr); + MainWindow* parent = nullptr); ~GameList() override; QString GetLastFilterResultItem() const; @@ -153,7 +153,7 @@ private: std::shared_ptr vfs; FileSys::ManualContentProvider* provider; GameListSearchField* search_field; - GMainWindow* main_window = nullptr; + MainWindow* main_window = nullptr; QVBoxLayout* layout = nullptr; QTreeView* tree_view = nullptr; QStandardItemModel* item_model = nullptr; @@ -171,7 +171,7 @@ private: class GameListPlaceholder : public QWidget { Q_OBJECT public: - explicit GameListPlaceholder(GMainWindow* parent = nullptr); + explicit GameListPlaceholder(MainWindow* parent = nullptr); ~GameListPlaceholder(); signals: diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index dd19b1b826..bcf5932e73 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1,376 +1,24 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -#include "core/hle/service/am/applet_manager.h" -#include "core/loader/nca.h" -#include "core/tools/renderdoc.h" -#include "frontend_common/firmware_manager.h" -#include "qt_common/qt_common.h" -#include "qt_common/abstract/frontend.h" -#include "qt_common/util/content.h" -#include "qt_common/util/game.h" -#include "qt_common/util/meta.h" -#include "qt_common/util/path.h" -#include "qt_common/util/fs.h" -#include -#include -#include -#include +#include +#include "startup_checks.h" -#include "set_play_time_dialog.h" - -#ifdef __APPLE__ -#include // for chdir -#endif -#ifdef __unix__ -#include -#include -#include "qt_common/gui_settings.h" -#endif -#ifdef __linux__ -#include "common/linux/gamemode.h" -#endif - -#include - -// VFS includes must be before glad as they will conflict with Windows file api, which uses defines. -#include "applets/qt_amiibo_settings.h" -#include "applets/qt_controller.h" -#include "applets/qt_error.h" -#include "applets/qt_profile_select.h" -#include "applets/qt_software_keyboard.h" -#include "applets/qt_web_browser.h" -#include "common/settings_enums.h" -#include "configuration/configure_input.h" -#include "configuration/configure_per_game.h" -#include "configuration/configure_tas.h" -#include "core/file_sys/romfs_factory.h" -#include "core/frontend/applets/cabinet.h" -#include "core/frontend/applets/controller.h" -#include "core/frontend/applets/general.h" -#include "core/frontend/applets/mii_edit.h" -#include "core/frontend/applets/software_keyboard.h" -#include "core/hle/service/am/frontend/applets.h" -#include "frontend_common/content_manager.h" -#include "hid_core/hid_core.h" -#include "hid_core/frontend/emulated_controller.h" -#include "yuzu/multiplayer/state.h" -#include "yuzu/util/controller_navigation.h" - -#ifdef ENABLE_UPDATE_CHECKER -#include "frontend_common/update_checker.h" -#endif - -#ifdef YUZU_ROOM +#if YUZU_ROOM #include "dedicated_room/yuzu_room.h" +#include +#include #endif -// These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows -// defines. -static FileSys::VirtualDir VfsFilesystemCreateDirectoryWrapper(const std::string& path, FileSys::OpenMode mode) { - return QtCommon::vfs->CreateDirectory(path, mode); -} - -static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::VirtualDir& dir, - const std::string& path) { - return dir->CreateFile(path); -} - -#include -#include - -#define QT_NO_OPENGL -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#ifdef HAVE_SDL2 -#include // For SDL ScreenSaver functions -#endif - -#include -#include "common/detached_tasks.h" -#include "common/fs/fs.h" -#include "common/fs/path_util.h" -#include "common/fs/ryujinx_compat.h" -#include "common/literals.h" -#include "common/logging/backend.h" -#include "common/logging/log.h" -#include "common/memory_detect.h" -#include "common/scm_rev.h" -#include "common/scope_exit.h" -#ifdef _WIN32 -#include "core/core_timing.h" -#include "common/windows/timer_resolution.h" -#endif -#ifdef ARCHITECTURE_x86_64 -#include "common/x64/cpu_detect.h" -#endif -#include "common/settings.h" -#include "core/core.h" -#include "core/crypto/key_manager.h" -#include "core/file_sys/card_image.h" -#include "core/file_sys/common_funcs.h" -#include "core/file_sys/content_archive.h" -#include "core/file_sys/control_metadata.h" -#include "core/file_sys/patch_manager.h" -#include "core/file_sys/registered_cache.h" -#include "core/file_sys/romfs.h" -#include "core/file_sys/savedata_factory.h" -#include "core/file_sys/submission_package.h" -#include "core/hle/kernel/k_process.h" -#include "core/hle/service/acc/profile_manager.h" -#include "core/hle/service/am/am.h" -#include "core/hle/service/filesystem/filesystem.h" -#include "core/hle/service/sm/sm.h" -#include "core/loader/loader.h" -#include "core/perf_stats.h" -#include "frontend_common/config.h" -#include "input_common/drivers/tas_input.h" -#include "input_common/drivers/virtual_amiibo.h" -#include "input_common/main.h" -#include "ui_main.h" -#include "yuzu/util/overlay_dialog.h" -#include "video_core/gpu.h" -#include "video_core/renderer_base.h" -#include "video_core/shader_notify.h" -#include "yuzu/about_dialog.h" -#include "yuzu/bootmanager.h" -#include "yuzu/compatibility_list.h" -#include "yuzu/configuration/configure_dialog.h" -#include "yuzu/configuration/configure_input_per_game.h" -#include "qt_common/config/qt_config.h" -#include "yuzu/debugger/console.h" -#include "yuzu/debugger/controller.h" -#include "yuzu/data_dialog.h" -#include "yuzu/deps_dialog.h" -#include "yuzu/ryujinx_dialog.h" -#include "qt_common/discord/discord.h" -#include "yuzu/game_list.h" -#include "yuzu/game_list_p.h" -#include "yuzu/install_dialog.h" -#include "yuzu/loading_screen.h" -#include "yuzu/main.h" -#include "frontend_common/play_time_manager.h" -#include "yuzu/startup_checks.h" -#include "qt_common/config/uisettings.h" -#include "yuzu/util/clickable_label.h" -#include "yuzu/vk_device_info.h" - -#ifdef _WIN32 -#include -#include -#include -#ifdef _MSC_VER -#pragma comment(lib, "Dwmapi.lib") -#endif - -static inline void ApplyWindowsTitleBarDarkMode(HWND hwnd, bool enabled) { - if (!hwnd) - return; - BOOL val = enabled ? TRUE : FALSE; - // 20 = Win11/21H2+ - if (SUCCEEDED(DwmSetWindowAttribute(hwnd, 20, &val, sizeof(val)))) - return; - // 19 = pre-21H2 - DwmSetWindowAttribute(hwnd, 19, &val, sizeof(val)); -} - -static inline void ApplyDarkToTopLevel(QWidget* w, bool on) { - if (!w || !w->isWindow()) - return; - ApplyWindowsTitleBarDarkMode(reinterpret_cast(w->winId()), on); -} - -namespace { -struct TitlebarFilter final : QObject { - bool dark; - explicit TitlebarFilter(bool is_dark) : QObject(qApp), dark(is_dark) {} - - void setDark(bool is_dark) { - dark = is_dark; - } - - void onFocusChanged(QWidget*, QWidget* now) { - if (now) - ApplyDarkToTopLevel(now->window(), dark); - } - - bool eventFilter(QObject* obj, QEvent* ev) override { - if (auto* w = qobject_cast(obj)) { - switch (ev->type()) { - case QEvent::WinIdChange: - case QEvent::Show: - case QEvent::ShowToParent: - case QEvent::Polish: - case QEvent::WindowStateChange: - case QEvent::ZOrderChange: - ApplyDarkToTopLevel(w, dark); - break; - default: - break; - } - } - return QObject::eventFilter(obj, ev); - } -}; - -static TitlebarFilter* g_filter = nullptr; -static QMetaObject::Connection g_focusConn; - -} // namespace - -static void ApplyGlobalDarkTitlebar(bool is_dark) { - if (!g_filter) { - g_filter = new TitlebarFilter(is_dark); - qApp->installEventFilter(g_filter); - g_focusConn = QObject::connect(qApp, &QApplication::focusChanged, g_filter, - &TitlebarFilter::onFocusChanged); - } else { - g_filter->setDark(is_dark); - } - for (QWidget* w : QApplication::topLevelWidgets()) - ApplyDarkToTopLevel(w, is_dark); -} - -static void RemoveTitlebarFilter() { - if (!g_filter) - return; - qApp->removeEventFilter(g_filter); - QObject::disconnect(g_focusConn); - g_filter->deleteLater(); - g_filter = nullptr; -} - -#endif - -#ifdef YUZU_CRASH_DUMPS -#include "yuzu/breakpad.h" -#endif - -using namespace Common::Literals; - -#ifdef USE_DISCORD_PRESENCE -#include "qt_common/discord/discord_impl.h" -#endif - -#ifdef QT_STATICPLUGIN -Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); -#endif - -#ifdef _WIN32 -#include -extern "C" { -// tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable -// graphics -__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; -__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; -} +#ifdef __unix__ +#include "qt_common/gui_settings.h" #endif -constexpr int default_mouse_hide_timeout = 2500; -constexpr int default_input_update_timeout = 1; - -constexpr size_t CopyBufferSize = 1_MiB; - -/** - * "Callouts" are one-time instructional messages shown to the user. In the config settings, there - * is a bitfield "callout_flags" options, used to track if a message has already been shown to the - * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. - */ -enum class CalloutFlag : uint32_t { - DRDDeprecation = 0x2, -}; - -const int GMainWindow::max_recent_files_item; - -static void RemoveCachedContents() { - const auto cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir); - const auto offline_fonts = cache_dir / "fonts"; - const auto offline_manual = cache_dir / "offline_web_applet_manual"; - const auto offline_legal_information = cache_dir / "offline_web_applet_legal_information"; - const auto offline_system_data = cache_dir / "offline_web_applet_system_data"; - - Common::FS::RemoveDirRecursively(offline_fonts); - Common::FS::RemoveDirRecursively(offline_manual); - Common::FS::RemoveDirRecursively(offline_legal_information); - Common::FS::RemoveDirRecursively(offline_system_data); -} - -static void LogRuntimes() { -#ifdef _MSC_VER - // It is possible that the name of the dll will change. - // vcruntime140.dll is for 2015 and onwards - static constexpr char runtime_dll_name[] = "vcruntime140.dll"; - UINT sz = GetFileVersionInfoSizeA(runtime_dll_name, nullptr); - bool runtime_version_inspection_worked = false; - if (sz > 0) { - std::vector buf(sz); - if (GetFileVersionInfoA(runtime_dll_name, 0, sz, buf.data())) { - VS_FIXEDFILEINFO* pvi; - sz = sizeof(VS_FIXEDFILEINFO); - if (VerQueryValueA(buf.data(), "\\", reinterpret_cast(&pvi), &sz)) { - if (pvi->dwSignature == VS_FFI_SIGNATURE) { - runtime_version_inspection_worked = true; - LOG_INFO(Frontend, "MSVC Compiler: {} Runtime: {}.{}.{}.{}", _MSC_VER, - pvi->dwProductVersionMS >> 16, pvi->dwProductVersionMS & 0xFFFF, - pvi->dwProductVersionLS >> 16, pvi->dwProductVersionLS & 0xFFFF); - } - } - } - } - if (!runtime_version_inspection_worked) { - LOG_INFO(Frontend, "Unable to inspect {}", runtime_dll_name); - } -#endif - LOG_INFO(Frontend, "Qt Compile: {} Runtime: {}", QT_VERSION_STR, qVersion()); -} +#include "main_window.h" -static QString PrettyProductName() { #ifdef _WIN32 - // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 - // With that notation change they changed the registry key used to denote the current version - QSettings windows_registry( - QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), - QSettings::NativeFormat); - const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); - if (release_id == QStringLiteral("2009")) { - const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); - const QString display_version = - windows_registry.value(QStringLiteral("DisplayVersion")).toString(); - const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); - u32 version = 10; - if (current_build >= 22000) { - version = 11; - } - return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") - .arg(QString::number(version), display_version, QString::number(current_build), - QString::number(ubr)); - } -#endif - return QSysInfo::prettyProductName(); -} +#include -#ifdef _WIN32 static void OverrideWindowsFont() { // Qt5 chooses these fonts on Windows and they have fairly ugly alphanumeric/cyrillic characters // Asking to use "MS Shell Dlg 2" gives better other chars while leaving the Chinese Characters. @@ -382,4577 +30,6 @@ static void OverrideWindowsFont() { } #endif -#ifndef _WIN32 -// TODO(crueter): carboxyl does this, is it needed in qml? -inline static bool isDarkMode() { -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - const auto scheme = QGuiApplication::styleHints()->colorScheme(); - return scheme == Qt::ColorScheme::Dark; -#else - const QPalette defaultPalette; - const auto text = defaultPalette.color(QPalette::WindowText); - const auto window = defaultPalette.color(QPalette::Window); - return text.lightness() > window.lightness(); -#endif // QT_VERSION -} -#endif // _WIN32 - -GMainWindow::GMainWindow(bool has_broken_vulkan) - : ui{std::make_unique()}, - input_subsystem{std::make_shared()}, user_data_migrator{this} { - QtCommon::Init(this); - - Common::FS::CreateEdenPaths(); - this->config = std::make_unique(); - - if (user_data_migrator.migrated) { - // Sort-of hack whereby we only move the old dir if it's a subfolder of the user dir -#define MIGRATE_DIR(type) \ - std::string type##path = Common::FS::GetEdenPathString(Common::FS::EdenPath::type##Dir); \ - if (type##path.starts_with(user_data_migrator.selected_emu.get_user_dir())) { \ - boost::replace_all(type##path, user_data_migrator.selected_emu.lower_name().toStdString(), "eden"); \ - Common::FS::SetEdenPath(Common::FS::EdenPath::type##Dir, type##path); \ - } - - MIGRATE_DIR(NAND) - MIGRATE_DIR(SDMC) - MIGRATE_DIR(Dump) - MIGRATE_DIR(Load) - -#undef MIGRATE_DIR - } - -#ifdef __unix__ - SetupSigInterrupts(); -#endif - -#ifdef __linux__ - SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); -#endif - - UISettings::RestoreWindowState(config); - - QtCommon::system->Initialize(); - - Common::Log::Initialize(); - Common::Log::Start(); - - LoadTranslation(); - - setAcceptDrops(true); - ui->setupUi(this); - statusBar()->hide(); - - startup_icon_theme = QIcon::themeName(); - // fallback can only be set once, colorful theme icons are okay on both light/dark - QIcon::setFallbackThemeName(QStringLiteral("colorful")); - QIcon::setFallbackSearchPaths(QStringList(QStringLiteral(":/icons"))); - - default_theme_paths = QIcon::themeSearchPaths(); - UpdateUITheme(); - - SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); - discord_rpc->Update(); - - play_time_manager = std::make_unique(); - - Network::Init(); - - QtCommon::Meta::RegisterMetaTypes(); - - InitializeWidgets(); - InitializeDebugWidgets(); - InitializeRecentFileMenuActions(); - InitializeHotkeys(); - - SetDefaultUIGeometry(); - RestoreUIState(); - - ConnectMenuEvents(); - ConnectWidgetEvents(); - - QtCommon::system->HIDCore().ReloadInputDevices(); - controller_dialog->refreshConfiguration(); - - const auto branch_name = std::string(Common::g_scm_branch); - const auto description = std::string(Common::g_scm_desc); - const auto build_id = std::string(Common::g_build_id); - - const auto yuzu_build = fmt::format("Eden Development Build | {}-{}", branch_name, description); - const auto override_build = - fmt::format(fmt::runtime(std::string(Common::g_title_bar_format_idle)), build_id); - const auto yuzu_build_version = override_build.empty() ? yuzu_build : override_build; - const auto processor_count = std::thread::hardware_concurrency(); - - LOG_INFO(Frontend, "Eden Version: {}", yuzu_build_version); - LogRuntimes(); -#ifdef ARCHITECTURE_x86_64 - const auto& caps = Common::GetCPUCaps(); - std::string cpu_string = caps.cpu_string; - if (caps.avx || caps.avx2 || caps.avx512f) { - cpu_string += " | AVX"; - if (caps.avx512f) { - cpu_string += "512"; - } else if (caps.avx2) { - cpu_string += '2'; - } - if (caps.fma || caps.fma4) { - cpu_string += " | FMA"; - } - } - LOG_INFO(Frontend, "Host CPU: {}", cpu_string); - if (std::optional processor_core = Common::GetProcessorCount()) { - LOG_INFO(Frontend, "Host CPU Cores: {}", *processor_core); - } -#endif - LOG_INFO(Frontend, "Host CPU Threads: {}", processor_count); - LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); - LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", - Common::GetMemInfo().TotalPhysicalMemory / f64{1_GiB}); - LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", Common::GetMemInfo().TotalSwapMemory / f64{1_GiB}); -#ifdef _WIN32 - LOG_INFO(Frontend, "Host Timer Resolution: {:.4f} ms", - std::chrono::duration_cast>( - Common::Windows::SetCurrentTimerResolutionToMaximum()) - .count()); - QtCommon::system->CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution()); -#endif - UpdateWindowTitle(); - - show(); - -#ifdef ENABLE_UPDATE_CHECKER - if (UISettings::values.check_for_updates) { - update_future = QtConcurrent::run([]() -> QString { - const bool is_prerelease = ((strstr(Common::g_build_version, "pre-alpha") != NULL) || - (strstr(Common::g_build_version, "alpha") != NULL) || - (strstr(Common::g_build_version, "beta") != NULL) || - (strstr(Common::g_build_version, "rc") != NULL)); - const std::optional latest_release_tag = - UpdateChecker::GetLatestRelease(is_prerelease); - if (latest_release_tag && latest_release_tag.value() != Common::g_build_version) { - return QString::fromStdString(latest_release_tag.value()); - } - return QString{}; - }); - update_watcher.connect(&update_watcher, &QFutureWatcher::finished, this, - &GMainWindow::OnEmulatorUpdateAvailable); - update_watcher.setFuture(update_future); - } -#endif - - QtCommon::system->SetContentProvider(std::make_unique()); - QtCommon::system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, - QtCommon::provider.get()); - QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); - - // Remove cached contents generated during the previous session - RemoveCachedContents(); - - // Gen keys if necessary - OnCheckFirmwareDecryption(); - -#ifdef __unix__ - OnCheckGraphicsBackend(); -#endif - - // Check for orphaned profiles and reset profile data if necessary - QtCommon::Content::FixProfiles(); - - game_list->LoadCompatibilityList(); - // force reload on first load to ensure add-ons get updated - game_list->PopulateAsync(UISettings::values.game_dirs); - - // make sure menubar has the arrow cursor instead of inheriting from this - ui->menubar->setCursor(QCursor()); - statusBar()->setCursor(QCursor()); - - mouse_hide_timer.setInterval(default_mouse_hide_timeout); - connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); - connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor); - - update_input_timer.setInterval(default_input_update_timeout); - connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers); - update_input_timer.start(); - - MigrateConfigFiles(); - - if (has_broken_vulkan) { - UISettings::values.has_broken_vulkan = true; - - QMessageBox::warning(this, tr("Broken Vulkan Installation Detected"), - tr("Vulkan initialization failed during boot.

Click " - "here for instructions to fix the issue.")); - -#ifdef HAS_OPENGL - Settings::values.renderer_backend = Settings::RendererBackend::OpenGL; -#else - Settings::values.renderer_backend = Settings::RendererBackend::Null; -#endif - - UpdateAPIText(); - renderer_status_button->setDisabled(true); - renderer_status_button->setChecked(false); - } else { - VkDeviceInfo::PopulateRecords(vk_device_records, this->window()->windowHandle()); - } - -#if defined(HAVE_SDL2) && !defined(_WIN32) - SDL_InitSubSystem(SDL_INIT_VIDEO); - - // Set a screensaver inhibition reason string. Currently passed to DBus by SDL and visible to - // the user through their desktop environment. - //: TRANSLATORS: This string is shown to the user to explain why yuzu needs to prevent the - //: computer from sleeping - QByteArray wakelock_reason = tr("Running a game").toUtf8(); - SDL_SetHint(SDL_HINT_SCREENSAVER_INHIBIT_ACTIVITY_NAME, wakelock_reason.data()); - - // SDL disables the screen saver by default, and setting the hint - // SDL_HINT_VIDEO_ALLOW_SCREENSAVER doesn't seem to work, so we just enable the screen saver - // for now. - SDL_EnableScreenSaver(); -#endif - - SetupPrepareForSleep(); - - // Some moron added a race condition to the status bar - // so now we have to make this completely unnecessary call - // to prevent the UI from blowing up. - UpdateUITheme(); - - QStringList args = QApplication::arguments(); - - if (args.size() < 2) { - return; - } - - QString game_path; - bool should_launch_qlaunch = false; - bool should_launch_setup = false; - bool has_gamepath = false; - bool is_fullscreen = false; - - for (int i = 1; i < args.size(); ++i) { - // Preserves drag/drop functionality - if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { - game_path = args[1]; - has_gamepath = true; - break; - } - - // Launch game in fullscreen mode - if (args[i] == QStringLiteral("-f")) { - is_fullscreen = true; - continue; - } - - // Launch game with a specific user - if (args[i] == QStringLiteral("-u")) { - if (i >= args.size() - 1) { - continue; - } - - if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { - continue; - } - - int user_arg_idx = ++i; - bool argument_ok; - std::size_t selected_user = args[user_arg_idx].toUInt(&argument_ok); - - if (!argument_ok) { - // try to look it up by username, only finds the first username that matches. - const std::string user_arg_str = args[user_arg_idx].toStdString(); - const auto user_idx = QtCommon::system->GetProfileManager().GetUserIndex(user_arg_str); - - if (user_idx == std::nullopt) { - LOG_ERROR(Frontend, "Invalid user argument"); - continue; - } - - selected_user = user_idx.value(); - } - - if (!QtCommon::system->GetProfileManager().UserExistsIndex(selected_user)) { - LOG_ERROR(Frontend, "Selected user doesn't exist"); - continue; - } - - Settings::values.current_user = static_cast(selected_user); - - user_flag_cmd_line = true; - continue; - } - - // Launch game at path - if (args[i] == QStringLiteral("-g")) { - if (i >= args.size() - 1) { - continue; - } - - if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { - continue; - } - - game_path = args[++i]; - has_gamepath = true; - } - - if (args[i] == QStringLiteral("-qlaunch")) - should_launch_qlaunch = true; - if (args[i] == QStringLiteral("-setup")) - should_launch_setup = true; - } - - // Override fullscreen setting if gamepath or argument is provided - if (has_gamepath || is_fullscreen) { - ui->action_Fullscreen->setChecked(is_fullscreen); - } - - if (should_launch_setup) { - OnInitialSetup(); - } else { - if (!game_path.isEmpty()) { - BootGame(game_path, ApplicationAppletParameters()); - } else { - if (should_launch_qlaunch) { - OnHomeMenu(); - } - } - } -} - -GMainWindow::~GMainWindow() { - // will get automatically deleted otherwise - if (render_window->parent() == nullptr) { - delete render_window; - } - -#ifdef __unix__ - ::close(sig_interrupt_fds[0]); - ::close(sig_interrupt_fds[1]); -#endif -} - -void GMainWindow::AmiiboSettingsShowDialog(const Core::Frontend::CabinetParameters& parameters, - std::shared_ptr nfp_device) { - cabinet_applet = - new QtAmiiboSettingsDialog(this, parameters, input_subsystem.get(), nfp_device); - SCOPE_EXIT { - cabinet_applet->deleteLater(); - cabinet_applet = nullptr; - }; - - cabinet_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint | - Qt::WindowTitleHint | Qt::WindowSystemMenuHint); - cabinet_applet->setWindowModality(Qt::WindowModal); - - if (cabinet_applet->exec() == QDialog::Rejected) { - emit AmiiboSettingsFinished(false, {}); - return; - } - - emit AmiiboSettingsFinished(true, cabinet_applet->GetName()); -} - -void GMainWindow::AmiiboSettingsRequestExit() { - if (cabinet_applet) { - cabinet_applet->reject(); - } -} - -void GMainWindow::ControllerSelectorReconfigureControllers( - const Core::Frontend::ControllerParameters& parameters) { - controller_applet = - new QtControllerSelectorDialog(this, parameters, input_subsystem.get(), *QtCommon::system); - SCOPE_EXIT { - controller_applet->deleteLater(); - controller_applet = nullptr; - }; - - controller_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | - Qt::WindowStaysOnTopHint | Qt::WindowTitleHint | - Qt::WindowSystemMenuHint); - controller_applet->setWindowModality(Qt::WindowModal); - bool is_success = controller_applet->exec() != QDialog::Rejected; - - // Don't forget to apply settings. - QtCommon::system->HIDCore().DisableAllControllerConfiguration(); - QtCommon::system->ApplySettings(); - config->SaveAllValues(); - - UpdateStatusButtons(); - - emit ControllerSelectorReconfigureFinished(is_success); -} - -void GMainWindow::ControllerSelectorRequestExit() { - if (controller_applet) { - controller_applet->reject(); - } -} - -void GMainWindow::ProfileSelectorSelectProfile( - const Core::Frontend::ProfileSelectParameters& parameters) { - profile_select_applet = new QtProfileSelectionDialog(*QtCommon::system, this, parameters); - SCOPE_EXIT { - profile_select_applet->deleteLater(); - profile_select_applet = nullptr; - }; - - profile_select_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | - Qt::WindowStaysOnTopHint | Qt::WindowTitleHint | - Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); - profile_select_applet->setWindowModality(Qt::WindowModal); - if (profile_select_applet->exec() == QDialog::Rejected) { - emit ProfileSelectorFinishedSelection(std::nullopt); - return; - } - - const auto uuid = QtCommon::system->GetProfileManager().GetUser( - static_cast(profile_select_applet->GetIndex())); - if (!uuid.has_value()) { - emit ProfileSelectorFinishedSelection(std::nullopt); - return; - } - - emit ProfileSelectorFinishedSelection(uuid); -} - -void GMainWindow::ProfileSelectorRequestExit() { - if (profile_select_applet) { - profile_select_applet->reject(); - } -} - -void GMainWindow::SoftwareKeyboardInitialize( - bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters) { - if (software_keyboard) { - LOG_ERROR(Frontend, "The software keyboard is already initialized!"); - return; - } - - software_keyboard = new QtSoftwareKeyboardDialog(render_window, *QtCommon::system, is_inline, - std::move(initialize_parameters)); - - if (is_inline) { - connect( - software_keyboard, &QtSoftwareKeyboardDialog::SubmitInlineText, this, - [this](Service::AM::Frontend::SwkbdReplyType reply_type, std::u16string submitted_text, - s32 cursor_position) { - emit SoftwareKeyboardSubmitInlineText(reply_type, submitted_text, cursor_position); - }, - Qt::QueuedConnection); - } else { - connect( - software_keyboard, &QtSoftwareKeyboardDialog::SubmitNormalText, this, - [this](Service::AM::Frontend::SwkbdResult result, std::u16string submitted_text, - bool confirmed) { - emit SoftwareKeyboardSubmitNormalText(result, submitted_text, confirmed); - }, - Qt::QueuedConnection); - } -} - -void GMainWindow::SoftwareKeyboardShowNormal() { - if (!software_keyboard) { - LOG_ERROR(Frontend, "The software keyboard is not initialized!"); - return; - } - - const auto& layout = render_window->GetFramebufferLayout(); - - const auto x = layout.screen.left; - const auto y = layout.screen.top; - const auto w = layout.screen.GetWidth(); - const auto h = layout.screen.GetHeight(); - const auto scale_ratio = devicePixelRatioF(); - - software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y) / scale_ratio), - QSize(w, h) / scale_ratio); -} - -void GMainWindow::SoftwareKeyboardShowTextCheck( - Service::AM::Frontend::SwkbdTextCheckResult text_check_result, - std::u16string text_check_message) { - if (!software_keyboard) { - LOG_ERROR(Frontend, "The software keyboard is not initialized!"); - return; - } - - software_keyboard->ShowTextCheckDialog(text_check_result, text_check_message); -} - -void GMainWindow::SoftwareKeyboardShowInline( - Core::Frontend::InlineAppearParameters appear_parameters) { - if (!software_keyboard) { - LOG_ERROR(Frontend, "The software keyboard is not initialized!"); - return; - } - - const auto& layout = render_window->GetFramebufferLayout(); - - const auto x = - static_cast(layout.screen.left + (0.5f * layout.screen.GetWidth() * - ((2.0f * appear_parameters.key_top_translate_x) + - (1.0f - appear_parameters.key_top_scale_x)))); - const auto y = - static_cast(layout.screen.top + (layout.screen.GetHeight() * - ((2.0f * appear_parameters.key_top_translate_y) + - (1.0f - appear_parameters.key_top_scale_y)))); - const auto w = static_cast(layout.screen.GetWidth() * appear_parameters.key_top_scale_x); - const auto h = static_cast(layout.screen.GetHeight() * appear_parameters.key_top_scale_y); - const auto scale_ratio = devicePixelRatioF(); - - software_keyboard->ShowInlineKeyboard(std::move(appear_parameters), - render_window->mapToGlobal(QPoint(x, y) / scale_ratio), - QSize(w, h) / scale_ratio); -} - -void GMainWindow::SoftwareKeyboardHideInline() { - if (!software_keyboard) { - LOG_ERROR(Frontend, "The software keyboard is not initialized!"); - return; - } - - software_keyboard->HideInlineKeyboard(); -} - -void GMainWindow::SoftwareKeyboardInlineTextChanged( - Core::Frontend::InlineTextParameters text_parameters) { - if (!software_keyboard) { - LOG_ERROR(Frontend, "The software keyboard is not initialized!"); - return; - } - - software_keyboard->InlineTextChanged(std::move(text_parameters)); -} - -void GMainWindow::SoftwareKeyboardExit() { - if (!software_keyboard) { - return; - } - - software_keyboard->ExitKeyboard(); - - software_keyboard = nullptr; -} - -void GMainWindow::WebBrowserOpenWebPage(const std::string& main_url, - const std::string& additional_args, bool is_local) { -#ifdef YUZU_USE_QT_WEB_ENGINE - - // Raw input breaks with the web applet, Disable web applets if enabled - if (Settings::values.disable_web_applet || Settings::values.enable_raw_input) { - emit WebBrowserClosed(Service::AM::Frontend::WebExitReason::WindowClosed, - "http://localhost/"); - return; - } - - web_applet = new QtNXWebEngineView(this, *QtCommon::system, input_subsystem.get()); - - ui->action_Pause->setEnabled(false); - ui->action_Restart->setEnabled(false); - ui->action_Stop->setEnabled(false); - - { - QProgressDialog loading_progress(this); - loading_progress.setLabelText(tr("Loading Web Applet...")); - loading_progress.setRange(0, 3); - loading_progress.setValue(0); - - if (is_local && !Common::FS::Exists(main_url)) { - loading_progress.show(); - - auto future = QtConcurrent::run([this] { emit WebBrowserExtractOfflineRomFS(); }); - - while (!future.isFinished()) { - QCoreApplication::processEvents(); - - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - } - - loading_progress.setValue(1); - - if (is_local) { - web_applet->LoadLocalWebPage(main_url, additional_args); - } else { - web_applet->LoadExternalWebPage(main_url, additional_args); - } - - if (render_window->IsLoadingComplete()) { - render_window->hide(); - } - - const auto& layout = render_window->GetFramebufferLayout(); - const auto scale_ratio = devicePixelRatioF(); - web_applet->resize(layout.screen.GetWidth() / scale_ratio, - layout.screen.GetHeight() / scale_ratio); - web_applet->move(layout.screen.left / scale_ratio, - (layout.screen.top / scale_ratio) + menuBar()->height()); - web_applet->setZoomFactor(static_cast(layout.screen.GetWidth() / scale_ratio) / - static_cast(Layout::ScreenUndocked::Width)); - - web_applet->setFocus(); - web_applet->show(); - - loading_progress.setValue(2); - - QCoreApplication::processEvents(); - - loading_progress.setValue(3); - } - - bool exit_check = false; - - // TODO (Morph): Remove this - QAction* exit_action = new QAction(tr("Disable Web Applet"), this); - connect(exit_action, &QAction::triggered, this, [this] { - const auto result = QMessageBox::warning( - this, tr("Disable Web Applet"), - tr("Disabling the web applet can lead to undefined behavior and should only be used " - "with Super Mario 3D All-Stars. Are you sure you want to disable the web " - "applet?\n(This can be re-enabled in the Debug settings.)"), - QMessageBox::Yes | QMessageBox::No); - if (result == QMessageBox::Yes) { - Settings::values.disable_web_applet = true; - web_applet->SetFinished(true); - } - }); - ui->menubar->addAction(exit_action); - - while (!web_applet->IsFinished()) { - QCoreApplication::processEvents(); - - if (!exit_check) { - web_applet->page()->runJavaScript( - QStringLiteral("end_applet;"), [&](const QVariant& variant) { - exit_check = false; - if (variant.toBool()) { - web_applet->SetFinished(true); - web_applet->SetExitReason( - Service::AM::Frontend::WebExitReason::EndButtonPressed); - } - }); - - exit_check = true; - } - - if (web_applet->GetCurrentURL().contains(QStringLiteral("localhost"))) { - if (!web_applet->IsFinished()) { - web_applet->SetFinished(true); - web_applet->SetExitReason(Service::AM::Frontend::WebExitReason::CallbackURL); - } - - web_applet->SetLastURL(web_applet->GetCurrentURL().toStdString()); - } - - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - const auto exit_reason = web_applet->GetExitReason(); - const auto last_url = web_applet->GetLastURL(); - - web_applet->hide(); - - render_window->setFocus(); - - if (render_window->IsLoadingComplete()) { - render_window->show(); - } - - ui->action_Pause->setEnabled(true); - ui->action_Restart->setEnabled(true); - ui->action_Stop->setEnabled(true); - - ui->menubar->removeAction(exit_action); - - QCoreApplication::processEvents(); - - emit WebBrowserClosed(exit_reason, last_url); - -#else - - // Utilize the same fallback as the default web browser applet. - emit WebBrowserClosed(Service::AM::Frontend::WebExitReason::WindowClosed, "http://localhost/"); - -#endif -} - -void GMainWindow::WebBrowserRequestExit() { -#ifdef YUZU_USE_QT_WEB_ENGINE - if (web_applet) { - web_applet->SetExitReason(Service::AM::Frontend::WebExitReason::ExitRequested); - web_applet->SetFinished(true); - } -#endif -} - -void GMainWindow::InitializeWidgets() { -#ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING - ui->action_Report_Compatibility->setVisible(true); -#endif - render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *QtCommon::system); - render_window->hide(); - - game_list = new GameList(QtCommon::vfs, QtCommon::provider.get(), *play_time_manager, *QtCommon::system, this); - ui->horizontalLayout->addWidget(game_list); - - game_list_placeholder = new GameListPlaceholder(this); - ui->horizontalLayout->addWidget(game_list_placeholder); - game_list_placeholder->setVisible(false); - - loading_screen = new LoadingScreen(this); - loading_screen->hide(); - ui->horizontalLayout->addWidget(loading_screen); - connect(loading_screen, &LoadingScreen::Hidden, this, [&] { - loading_screen->Clear(); - if (emulation_running) { - render_window->show(); - render_window->setFocus(); - } - }); - - multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room, - ui->action_Show_Room, *QtCommon::system); - multiplayer_state->setVisible(false); - - // Create status bar - message_label = new QLabel(); - // Configured separately for left alignment - message_label->setFrameStyle(QFrame::NoFrame); - message_label->setContentsMargins(4, 0, 4, 0); - message_label->setAlignment(Qt::AlignLeft); - statusBar()->addPermanentWidget(message_label, 1); - - shader_building_label = new QLabel(); - shader_building_label->setToolTip(tr("The amount of shaders currently being built")); - res_scale_label = new QLabel(); - res_scale_label->setToolTip(tr("The current selected resolution scaling multiplier.")); - emu_speed_label = new QLabel(); - emu_speed_label->setToolTip( - tr("Current emulation speed. Values higher or lower than 100% " - "indicate emulation is running faster or slower than a Switch.")); - game_fps_label = new QLabel(); - game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " - "This will vary from game to game and scene to scene.")); - emu_frametime_label = new QLabel(); - emu_frametime_label->setToolTip( - tr("Time taken to emulate a Switch frame, not counting framelimiting or v-sync. For " - "full-speed emulation this should be at most 16.67 ms.")); - - for (auto& label : {shader_building_label, res_scale_label, emu_speed_label, game_fps_label, - emu_frametime_label}) { - label->setVisible(false); - label->setFrameStyle(QFrame::NoFrame); - label->setContentsMargins(4, 0, 4, 0); - statusBar()->addPermanentWidget(label); - } - - firmware_label = new QLabel(); - firmware_label->setObjectName(QStringLiteral("FirmwareLabel")); - firmware_label->setVisible(false); - firmware_label->setContentsMargins(4, 0, 4, 0); - firmware_label->setFocusPolicy(Qt::NoFocus); - statusBar()->addPermanentWidget(firmware_label); - - statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); - statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); - - tas_label = new QLabel(); - tas_label->setObjectName(QStringLiteral("TASlabel")); - tas_label->setFocusPolicy(Qt::NoFocus); - statusBar()->insertPermanentWidget(0, tas_label); - - volume_popup = new QWidget(this); - volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); - volume_popup->setLayout(new QVBoxLayout()); - volume_popup->setMinimumWidth(200); - - volume_slider = new QSlider(Qt::Horizontal); - volume_slider->setObjectName(QStringLiteral("volume_slider")); - volume_slider->setMaximum(200); - volume_slider->setPageStep(5); - volume_popup->layout()->addWidget(volume_slider); - - volume_button = new VolumeButton(); - volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); - volume_button->setFocusPolicy(Qt::NoFocus); - volume_button->setCheckable(true); - UpdateVolumeUI(); - connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { - Settings::values.audio_muted = false; - const auto volume = static_cast(percentage); - Settings::values.volume.SetValue(volume); - UpdateVolumeUI(); - }); - connect(volume_button, &QPushButton::clicked, this, [&] { - UpdateVolumeUI(); - volume_popup->setVisible(!volume_popup->isVisible()); - QRect rect = volume_button->geometry(); - QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); - bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); - volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); - }); - volume_button->setContextMenuPolicy(Qt::CustomContextMenu); - connect(volume_button, &QPushButton::customContextMenuRequested, - [this](const QPoint& menu_location) { - QMenu context_menu; - context_menu.addAction( - Settings::values.audio_muted ? tr("Unmute") : tr("Mute"), [this] { - Settings::values.audio_muted = !Settings::values.audio_muted; - UpdateVolumeUI(); - }); - - context_menu.addAction(tr("Reset Volume"), [this] { - Settings::values.volume.SetValue(100); - UpdateVolumeUI(); - }); - - context_menu.exec(volume_button->mapToGlobal(menu_location)); - volume_button->repaint(); - }); - connect(volume_button, &VolumeButton::VolumeChanged, this, &GMainWindow::UpdateVolumeUI); - - statusBar()->insertPermanentWidget(0, volume_button); - - // setup AA button - aa_status_button = new QPushButton(); - aa_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); - aa_status_button->setFocusPolicy(Qt::NoFocus); - connect(aa_status_button, &QPushButton::clicked, [&] { - auto aa_mode = Settings::values.anti_aliasing.GetValue(); - aa_mode = static_cast(static_cast(aa_mode) + 1); - if (aa_mode == Settings::AntiAliasing::MaxEnum) { - aa_mode = Settings::AntiAliasing::None; - } - Settings::values.anti_aliasing.SetValue(aa_mode); - aa_status_button->setChecked(true); - UpdateAAText(); - }); - UpdateAAText(); - aa_status_button->setCheckable(true); - aa_status_button->setChecked(true); - aa_status_button->setContextMenuPolicy(Qt::CustomContextMenu); - connect(aa_status_button, &QPushButton::customContextMenuRequested, - [this](const QPoint& menu_location) { - QMenu context_menu; - for (auto const& aa_text_pair : ConfigurationShared::anti_aliasing_texts_map) { - context_menu.addAction(aa_text_pair.second, [this, aa_text_pair] { - Settings::values.anti_aliasing.SetValue(aa_text_pair.first); - UpdateAAText(); - }); - } - context_menu.exec(aa_status_button->mapToGlobal(menu_location)); - aa_status_button->repaint(); - }); - statusBar()->insertPermanentWidget(0, aa_status_button); - - // Setup Filter button - filter_status_button = new QPushButton(); - filter_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); - filter_status_button->setFocusPolicy(Qt::NoFocus); - connect(filter_status_button, &QPushButton::clicked, this, - &GMainWindow::OnToggleAdaptingFilter); - UpdateFilterText(); - filter_status_button->setCheckable(true); - filter_status_button->setChecked(true); - filter_status_button->setContextMenuPolicy(Qt::CustomContextMenu); - connect(filter_status_button, &QPushButton::customContextMenuRequested, - [this](const QPoint& menu_location) { - QMenu context_menu; - for (auto const& filter_text_pair : ConfigurationShared::scaling_filter_texts_map) { - context_menu.addAction(filter_text_pair.second, [this, filter_text_pair] { - Settings::values.scaling_filter.SetValue(filter_text_pair.first); - UpdateFilterText(); - }); - } - context_menu.exec(filter_status_button->mapToGlobal(menu_location)); - filter_status_button->repaint(); - }); - statusBar()->insertPermanentWidget(0, filter_status_button); - - // Setup Dock button - dock_status_button = new QPushButton(); - dock_status_button->setObjectName(QStringLiteral("DockingStatusBarButton")); - dock_status_button->setFocusPolicy(Qt::NoFocus); - connect(dock_status_button, &QPushButton::clicked, this, &GMainWindow::OnToggleDockedMode); - dock_status_button->setCheckable(true); - UpdateDockedButton(); - dock_status_button->setContextMenuPolicy(Qt::CustomContextMenu); - connect(dock_status_button, &QPushButton::customContextMenuRequested, - [this](const QPoint& menu_location) { - QMenu context_menu; - - for (auto const& pair : ConfigurationShared::use_docked_mode_texts_map) { - context_menu.addAction(pair.second, [this, &pair] { - if (pair.first != Settings::values.use_docked_mode.GetValue()) { - OnToggleDockedMode(); - } - }); - } - context_menu.exec(dock_status_button->mapToGlobal(menu_location)); - dock_status_button->repaint(); - }); - statusBar()->insertPermanentWidget(0, dock_status_button); - - // Setup GPU Accuracy button - gpu_accuracy_button = new QPushButton(); - gpu_accuracy_button->setObjectName(QStringLiteral("GPUStatusBarButton")); - gpu_accuracy_button->setCheckable(true); - gpu_accuracy_button->setFocusPolicy(Qt::NoFocus); - connect(gpu_accuracy_button, &QPushButton::clicked, this, &GMainWindow::OnToggleGpuAccuracy); - UpdateGPUAccuracyButton(); - gpu_accuracy_button->setContextMenuPolicy(Qt::CustomContextMenu); - connect(gpu_accuracy_button, &QPushButton::customContextMenuRequested, - [this](const QPoint& menu_location) { - QMenu context_menu; - - for (auto const& gpu_accuracy_pair : ConfigurationShared::gpu_accuracy_texts_map) { - if (gpu_accuracy_pair.first == Settings::GpuAccuracy::Extreme) { - continue; - } - context_menu.addAction(gpu_accuracy_pair.second, [this, gpu_accuracy_pair] { - Settings::values.gpu_accuracy.SetValue(gpu_accuracy_pair.first); - UpdateGPUAccuracyButton(); - }); - } - context_menu.exec(gpu_accuracy_button->mapToGlobal(menu_location)); - gpu_accuracy_button->repaint(); - }); - statusBar()->insertPermanentWidget(0, gpu_accuracy_button); - - // Setup Renderer API button - renderer_status_button = new QPushButton(); - renderer_status_button->setObjectName(QStringLiteral("RendererStatusBarButton")); - renderer_status_button->setCheckable(true); - renderer_status_button->setFocusPolicy(Qt::NoFocus); - connect(renderer_status_button, &QPushButton::clicked, this, &GMainWindow::OnToggleGraphicsAPI); - UpdateAPIText(); - renderer_status_button->setCheckable(true); - renderer_status_button->setChecked(Settings::values.renderer_backend.GetValue() == - Settings::RendererBackend::Vulkan); - renderer_status_button->setContextMenuPolicy(Qt::CustomContextMenu); - connect(renderer_status_button, &QPushButton::customContextMenuRequested, - [this](const QPoint& menu_location) { - QMenu context_menu; - - for (auto const& renderer_backend_pair : - ConfigurationShared::renderer_backend_texts_map) { - if (renderer_backend_pair.first == Settings::RendererBackend::Null) { - continue; - } - context_menu.addAction( - renderer_backend_pair.second, [this, renderer_backend_pair] { - Settings::values.renderer_backend.SetValue(renderer_backend_pair.first); - UpdateAPIText(); - }); - } - context_menu.exec(renderer_status_button->mapToGlobal(menu_location)); - renderer_status_button->repaint(); - }); - statusBar()->insertPermanentWidget(0, renderer_status_button); - - // Setup Refresh Button - refresh_button = new QPushButton(); - refresh_button->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); - refresh_button->setObjectName(QStringLiteral("RefreshButton")); - refresh_button->setFocusPolicy(Qt::NoFocus); - connect(refresh_button, &QPushButton::clicked, this, &GMainWindow::OnGameListRefresh); - - statusBar()->insertPermanentWidget(0, refresh_button); - - statusBar()->setVisible(true); - setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); -} - -void GMainWindow::InitializeDebugWidgets() { - QMenu* debug_menu = ui->menu_View_Debugging; - - controller_dialog = new ControllerDialog(QtCommon::system->HIDCore(), input_subsystem, this); - controller_dialog->hide(); - debug_menu->addAction(controller_dialog->toggleViewAction()); -} - -void GMainWindow::InitializeRecentFileMenuActions() { - for (int i = 0; i < max_recent_files_item; ++i) { - actions_recent_files[i] = new QAction(this); - actions_recent_files[i]->setVisible(false); - connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile); - - ui->menu_recent_files->addAction(actions_recent_files[i]); - } - ui->menu_recent_files->addSeparator(); - QAction* action_clear_recent_files = new QAction(this); - action_clear_recent_files->setText(tr("&Clear Recent Files")); - connect(action_clear_recent_files, &QAction::triggered, this, [this] { - UISettings::values.recent_files.clear(); - UpdateRecentFiles(); - }); - ui->menu_recent_files->addAction(action_clear_recent_files); - - UpdateRecentFiles(); -} - -void GMainWindow::LinkActionShortcut(QAction* action, const QString& action_name, - const bool tas_allowed) { - static const auto main_window = std::string("Main Window"); - action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name.toStdString())); - action->setShortcutContext( - hotkey_registry.GetShortcutContext(main_window, action_name.toStdString())); - action->setAutoRepeat(false); - - this->addAction(action); - - auto* controller = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); - const auto* controller_hotkey = - hotkey_registry.GetControllerHotkey(main_window, action_name.toStdString(), controller); - connect( - controller_hotkey, &ControllerShortcut::Activated, this, - [action, tas_allowed, this] { - auto [tas_status, current_tas_frame, total_tas_frames] = - input_subsystem->GetTas()->GetStatus(); - if (tas_allowed || tas_status == InputCommon::TasInput::TasState::Stopped) { - action->trigger(); - } - }, - Qt::QueuedConnection); -} - -void GMainWindow::InitializeHotkeys() { - hotkey_registry.LoadHotkeys(); - - LinkActionShortcut(ui->action_Load_File, QStringLiteral("Load File")); - LinkActionShortcut(ui->action_Load_Amiibo, QStringLiteral("Load/Remove Amiibo")); - LinkActionShortcut(ui->action_Exit, QStringLiteral("Exit Eden")); - LinkActionShortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); - LinkActionShortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); - LinkActionShortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); - LinkActionShortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); - LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); - LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen")); - LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); - LinkActionShortcut(ui->action_TAS_Start, QStringLiteral("TAS Start/Stop"), true); - LinkActionShortcut(ui->action_TAS_Record, QStringLiteral("TAS Record"), true); - LinkActionShortcut(ui->action_TAS_Reset, QStringLiteral("TAS Reset"), true); - LinkActionShortcut(ui->action_View_Lobby, - QStringLiteral("Multiplayer Browse Public Game Lobby")); - LinkActionShortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); - LinkActionShortcut(ui->action_Connect_To_Room, - QStringLiteral("Multiplayer Direct Connect to Room")); - LinkActionShortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); - LinkActionShortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); - LinkActionShortcut(ui->action_Configure, QStringLiteral("Configure")); - LinkActionShortcut(ui->action_Configure_Current_Game, QStringLiteral("Configure Current Game")); - - static const QString main_window = QStringLiteral("Main Window"); - const auto connect_shortcut = [&](const QString& action_name, const Fn& function) { - const auto* hotkey = - hotkey_registry.GetHotkey(main_window.toStdString(), action_name.toStdString(), this); - auto* controller = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); - const auto* controller_hotkey = hotkey_registry.GetControllerHotkey( - main_window.toStdString(), action_name.toStdString(), controller); - connect(hotkey, &QShortcut::activated, this, function); - connect(controller_hotkey, &ControllerShortcut::Activated, this, function, - Qt::QueuedConnection); - }; - - connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { - if (emulation_running && ui->action_Fullscreen->isChecked()) { - ui->action_Fullscreen->setChecked(false); - ToggleFullscreen(); - } - }); - connect_shortcut(QStringLiteral("Change Adapting Filter"), - &GMainWindow::OnToggleAdaptingFilter); - connect_shortcut(QStringLiteral("Change Docked Mode"), &GMainWindow::OnToggleDockedMode); - connect_shortcut(QStringLiteral("Change GPU Accuracy"), &GMainWindow::OnToggleGpuAccuracy); - connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); - connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume); - connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume); - connect_shortcut(QStringLiteral("Toggle Framerate Limit"), [] { - Settings::values.use_speed_limit.SetValue(!Settings::values.use_speed_limit.GetValue()); - }); - connect_shortcut(QStringLiteral("Toggle Renderdoc Capture"), [] { - if (Settings::values.enable_renderdoc_hotkey) { - QtCommon::system->GetRenderdocAPI().ToggleCapture(); - } - }); - connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] { - Settings::values.mouse_panning = !Settings::values.mouse_panning; - if (Settings::values.mouse_panning) { - render_window->installEventFilter(render_window); - render_window->setAttribute(Qt::WA_Hover, true); - } - }); -} - -void GMainWindow::SetDefaultUIGeometry() { - // geometry: 53% of the window contents are in the upper screen half, 47% in the lower half - const QRect screenRect = QGuiApplication::primaryScreen()->geometry(); - - const int w = screenRect.width() * 2 / 3; - const int h = screenRect.height() * 2 / 3; - const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; - const int y = (screenRect.y() + screenRect.height()) / 2 - h * 53 / 100; - - setGeometry(x, y, w, h); -} - -void GMainWindow::RestoreUIState() { - setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); - restoreGeometry(UISettings::values.geometry); - // Work-around because the games list isn't supposed to be full screen - if (isFullScreen()) { - showNormal(); - } - restoreState(UISettings::values.state); - render_window->setWindowFlags(render_window->windowFlags() & ~Qt::FramelessWindowHint); - render_window->restoreGeometry(UISettings::values.renderwindow_geometry); - - game_list->LoadInterfaceLayout(); - - ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); - ToggleWindowMode(); - - ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); - - ui->action_Display_Dock_Widget_Headers->setChecked( - UISettings::values.display_titlebar.GetValue()); - - ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); - game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); - - ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); - statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); - Debugger::ToggleConsole(); -} - -void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { - if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && - state != Qt::ApplicationActive) { - LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); - } - if (!emulation_running) { - return; - } - if (UISettings::values.pause_when_in_background) { - if (emu_thread->IsRunning() && - (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { - auto_paused = true; - OnPauseGame(); - } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { - auto_paused = false; - OnStartGame(); - } - } - if (UISettings::values.mute_when_in_background) { - if (!Settings::values.audio_muted && - (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { - Settings::values.audio_muted = true; - auto_muted = true; - } else if (auto_muted && state == Qt::ApplicationActive) { - Settings::values.audio_muted = false; - auto_muted = false; - } - UpdateVolumeUI(); - } -} - -void GMainWindow::ConnectWidgetEvents() { - connect(game_list, &GameList::BootGame, this, &GMainWindow::BootGameFromList); - connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); - connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); - connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); - connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this, [this](u64 program_id) { - QtCommon::Path::OpenShaderCache(program_id, this); - }); - connect(game_list, &GameList::RemoveInstalledEntryRequested, this, - &GMainWindow::OnGameListRemoveInstalledEntry); - connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile); - connect(game_list, &GameList::RemovePlayTimeRequested, this, - &GMainWindow::OnGameListRemovePlayTimeData); - connect(game_list, &GameList::SetPlayTimeRequested, this, - &GMainWindow::OnGameListSetPlayTime); - connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); - connect(game_list, &GameList::VerifyIntegrityRequested, this, - &GMainWindow::OnGameListVerifyIntegrity); - connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); - connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, - &GMainWindow::OnGameListNavigateToGamedbEntry); - connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut); - connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); - connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, - &GMainWindow::OnGameListAddDirectory); - connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); - connect(game_list, &GameList::PopulatingCompleted, - [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); - connect(game_list, &GameList::SaveConfig, this, &GMainWindow::OnSaveConfig); - - connect(game_list, &GameList::OpenPerGameGeneralRequested, this, - &GMainWindow::OnGameListOpenPerGameProperties); - connect(game_list, &GameList::LinkToRyujinxRequested, this, - &GMainWindow::OnLinkToRyujinx); - - connect(this, &GMainWindow::UpdateInstallProgress, this, - &GMainWindow::IncrementInstallProgress); - - connect(this, &GMainWindow::EmulationStarting, render_window, - &GRenderWindow::OnEmulationStarting); - connect(this, &GMainWindow::EmulationStopping, render_window, - &GRenderWindow::OnEmulationStopping); - - // Software Keyboard Applet - connect(this, &GMainWindow::EmulationStarting, this, &GMainWindow::SoftwareKeyboardExit); - connect(this, &GMainWindow::EmulationStopping, this, &GMainWindow::SoftwareKeyboardExit); - - connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); - - connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, - &MultiplayerState::UpdateThemedIcons); -} - -void GMainWindow::ConnectMenuEvents() { - const auto connect_menu = [&](QAction* action, const Fn& event_fn) { - connect(action, &QAction::triggered, this, event_fn); - // Add actions to this window so that hiding menus in fullscreen won't disable them - addAction(action); - // Add actions to the render window so that they work outside of single window mode - render_window->addAction(action); - }; - - // File - connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); - connect_menu(ui->action_Load_Folder, &GMainWindow::OnMenuLoadFolder); - connect_menu(ui->action_Install_File_NAND, &GMainWindow::OnMenuInstallToNAND); - connect_menu(ui->action_Exit, &QMainWindow::close); - connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); - - // Emulation - connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame); - connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); - connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); - connect_menu(ui->action_Open_Mods_Page, &GMainWindow::OnOpenModsPage); - connect_menu(ui->action_Open_Quickstart_Guide, &GMainWindow::OnOpenQuickstartGuide); - connect_menu(ui->action_Open_FAQ, &GMainWindow::OnOpenFAQ); - connect_menu(ui->action_Restart, &GMainWindow::OnRestartGame); - connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); - connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame); - - // View - connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen); - connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode); - connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); - connect_menu(ui->action_Show_Status_Bar, &GMainWindow::OnToggleStatusBar); - - connect_menu(ui->action_Reset_Window_Size_720, &GMainWindow::ResetWindowSize720); - connect_menu(ui->action_Reset_Window_Size_900, &GMainWindow::ResetWindowSize900); - connect_menu(ui->action_Reset_Window_Size_1080, &GMainWindow::ResetWindowSize1080); - ui->menu_Reset_Window_Size->addActions({ui->action_Reset_Window_Size_720, - ui->action_Reset_Window_Size_900, - ui->action_Reset_Window_Size_1080}); - - // Multiplayer - connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, - &MultiplayerState::OnViewLobby); - connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, - &MultiplayerState::OnCreateRoom); - connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, - &MultiplayerState::OnCloseRoom); - connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, - &MultiplayerState::OnDirectConnectToRoom); - connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, - &MultiplayerState::OnOpenNetworkRoom); - connect(multiplayer_state, &MultiplayerState::SaveConfig, this, &GMainWindow::OnSaveConfig); - - // Tools - connect_menu(ui->action_Load_Album, &GMainWindow::OnAlbum); - connect_menu(ui->action_Load_Cabinet_Nickname_Owner, - [this]() { OnCabinet(Service::NFP::CabinetMode::StartNicknameAndOwnerSettings); }); - connect_menu(ui->action_Load_Cabinet_Eraser, - [this]() { OnCabinet(Service::NFP::CabinetMode::StartGameDataEraser); }); - connect_menu(ui->action_Load_Cabinet_Restorer, - [this]() { OnCabinet(Service::NFP::CabinetMode::StartRestorer); }); - connect_menu(ui->action_Load_Cabinet_Formatter, - [this]() { OnCabinet(Service::NFP::CabinetMode::StartFormatter); }); - connect_menu(ui->action_Load_Mii_Edit, &GMainWindow::OnMiiEdit); - connect_menu(ui->action_Open_Controller_Menu, &GMainWindow::OnOpenControllerMenu); - connect_menu(ui->action_Load_Home_Menu, &GMainWindow::OnHomeMenu); - connect_menu(ui->action_Open_Setup, &GMainWindow::OnInitialSetup); - connect_menu(ui->action_Desktop, &GMainWindow::OnCreateHomeMenuDesktopShortcut); - connect_menu(ui->action_Application_Menu, - &GMainWindow::OnCreateHomeMenuApplicationMenuShortcut); - connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); - - // TAS - connect_menu(ui->action_TAS_Start, &GMainWindow::OnTasStartStop); - connect_menu(ui->action_TAS_Record, &GMainWindow::OnTasRecord); - connect_menu(ui->action_TAS_Reset, &GMainWindow::OnTasReset); - connect_menu(ui->action_Configure_Tas, &GMainWindow::OnConfigureTas); - - // Help - connect_menu(ui->action_Root_Data_Folder, &GMainWindow::OnOpenRootDataFolder); - connect_menu(ui->action_NAND_Folder, &GMainWindow::OnOpenNANDFolder); - connect_menu(ui->action_SDMC_Folder, &GMainWindow::OnOpenSDMCFolder); - connect_menu(ui->action_Mod_Folder, &GMainWindow::OnOpenModFolder); - connect_menu(ui->action_Log_Folder, &GMainWindow::OnOpenLogFolder); - - connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents); - connect_menu(ui->action_Firmware_From_Folder, &GMainWindow::OnInstallFirmware); - connect_menu(ui->action_Firmware_From_ZIP, &GMainWindow::OnInstallFirmwareFromZIP); - connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys); - connect_menu(ui->action_About, &GMainWindow::OnAbout); - connect_menu(ui->action_Eden_Dependencies, &GMainWindow::OnEdenDependencies); - connect_menu(ui->action_Data_Manager, &GMainWindow::OnDataDialog); -} - -void GMainWindow::UpdateMenuState() { - const bool is_paused = emu_thread == nullptr || !emu_thread->IsRunning(); - const bool is_firmware_available = CheckFirmwarePresence(); - - const std::array running_actions{ - ui->action_Stop, - ui->action_Restart, - ui->action_Configure_Current_Game, - ui->action_Report_Compatibility, - ui->action_Load_Amiibo, - ui->action_Pause, - }; - - const std::array applet_actions{ui->action_Load_Album, - ui->action_Load_Cabinet_Nickname_Owner, - ui->action_Load_Cabinet_Eraser, - ui->action_Load_Cabinet_Restorer, - ui->action_Load_Cabinet_Formatter, - ui->action_Load_Mii_Edit, - ui->action_Load_Home_Menu, - ui->action_Open_Controller_Menu}; - - for (QAction* action : running_actions) { - action->setEnabled(emulation_running); - } - - ui->action_Firmware_From_Folder->setEnabled(!emulation_running); - ui->action_Firmware_From_ZIP->setEnabled(!emulation_running); - ui->action_Install_Keys->setEnabled(!emulation_running); - - for (QAction* action : applet_actions) { - action->setEnabled(is_firmware_available && !emulation_running); - } - - ui->action_Capture_Screenshot->setEnabled(emulation_running && !is_paused); - - if (emulation_running && is_paused) { - ui->action_Pause->setText(tr("&Continue")); - } else { - ui->action_Pause->setText(tr("&Pause")); - } - - multiplayer_state->UpdateNotificationStatus(); -} - -void GMainWindow::SetupPrepareForSleep() { -#ifdef __unix__ - if (auto bus = QDBusConnection::systemBus(); bus.isConnected()) { - // See https://github.com/ConsoleKit2/ConsoleKit2/issues/150 -#ifdef __linux__ - const auto dbus_logind_service = QStringLiteral("org.freedesktop.login1"); - const auto dbus_logind_path = QStringLiteral("/org/freedesktop/login1"); - const auto dbus_logind_manager_if = QStringLiteral("org.freedesktop.login1.Manager"); - //const auto dbus_logind_session_if = QStringLiteral("org.freedesktop.login1.Session"); -#else - const auto dbus_logind_service = QStringLiteral("org.freedesktop.ConsoleKit"); - const auto dbus_logind_path = QStringLiteral("/org/freedesktop/ConsoleKit/Manager"); - const auto dbus_logind_manager_if = QStringLiteral("org.freedesktop.ConsoleKit.Manager"); - //const auto dbus_logind_session_if = QStringLiteral("org.freedesktop.ConsoleKit.Session"); -#endif - const bool success = bus.connect( - dbus_logind_service, dbus_logind_path, - dbus_logind_manager_if, QStringLiteral("PrepareForSleep"), - QStringLiteral("b"), this, SLOT(OnPrepareForSleep(bool))); - if (!success) - LOG_WARNING(Frontend, "Couldn't register PrepareForSleep signal"); - } else { - LOG_WARNING(Frontend, "QDBusConnection system bus is not connected"); - } -#endif // __unix__ -} - -void GMainWindow::OnPrepareForSleep(bool prepare_sleep) { - if (emu_thread == nullptr) { - return; - } - - if (prepare_sleep) { - if (emu_thread->IsRunning()) { - auto_paused = true; - OnPauseGame(); - } - } else { - if (!emu_thread->IsRunning() && auto_paused) { - auto_paused = false; - OnStartGame(); - } - } -} - -#ifdef __unix__ -std::array GMainWindow::sig_interrupt_fds{0, 0, 0}; - -void GMainWindow::SetupSigInterrupts() { - if (sig_interrupt_fds[2] == 1) { - return; - } - socketpair(AF_UNIX, SOCK_STREAM, 0, sig_interrupt_fds.data()); - sig_interrupt_fds[2] = 1; - - struct sigaction sa; - sa.sa_handler = &GMainWindow::HandleSigInterrupt; - sigemptyset(&sa.sa_mask); - sa.sa_flags = SA_RESETHAND; - sigaction(SIGINT, &sa, nullptr); - sigaction(SIGTERM, &sa, nullptr); - - sig_interrupt_notifier = new QSocketNotifier(sig_interrupt_fds[1], QSocketNotifier::Read, this); - connect(sig_interrupt_notifier, &QSocketNotifier::activated, this, - &GMainWindow::OnSigInterruptNotifierActivated); - connect(this, &GMainWindow::SigInterrupt, this, &GMainWindow::close); -} - -void GMainWindow::HandleSigInterrupt(int sig) { - if (sig == SIGINT) { - _exit(1); - } - - // Calling into Qt directly from a signal handler is not safe, - // so wake up a QSocketNotifier with this hacky write call instead. - char a = 1; - int ret = write(sig_interrupt_fds[0], &a, sizeof(a)); - (void)ret; -} - -void GMainWindow::OnSigInterruptNotifierActivated() { - sig_interrupt_notifier->setEnabled(false); - - char a; - int ret = read(sig_interrupt_fds[1], &a, sizeof(a)); - (void)ret; - - sig_interrupt_notifier->setEnabled(true); - - emit SigInterrupt(); -} -#endif // __unix__ - -void GMainWindow::PreventOSSleep() { -#ifdef _WIN32 - SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); -#elif defined(HAVE_SDL2) - SDL_DisableScreenSaver(); -#endif -} - -void GMainWindow::AllowOSSleep() { -#ifdef _WIN32 - SetThreadExecutionState(ES_CONTINUOUS); -#elif defined(HAVE_SDL2) - SDL_EnableScreenSaver(); -#endif -} - -bool GMainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletParameters params) { - // Shutdown previous session if the emu thread is still active... - if (emu_thread != nullptr) { - ShutdownGame(); - } - - if (!render_window->InitRenderTarget()) { - return false; - } - - QtCommon::system->SetFilesystem(QtCommon::vfs); - - if (params.launch_type == Service::AM::LaunchType::FrontendInitiated) { - QtCommon::system->GetUserChannel().clear(); - } - - QtCommon::system->SetFrontendAppletSet({ - std::make_unique(*this), // Amiibo Settings - (UISettings::values.controller_applet_disabled.GetValue() == true) - ? nullptr - : std::make_unique(*this), // Controller Selector - std::make_unique(*this), // Error Display - nullptr, // Mii Editor - nullptr, // Parental Controls - nullptr, // Photo Viewer - std::make_unique(*this), // Profile Selector - std::make_unique(*this), // Software Keyboard - std::make_unique(*this), // Web Browser - nullptr, // Net Connect - }); - - /** firmware check */ - - if (!QtCommon::Content::CheckGameFirmware(params.program_id, this)) { - return false; - } - - /** Exec */ - const Core::SystemResultStatus result{ - QtCommon::system->Load(*render_window, filename.toStdString(), params)}; - - const auto drd_callout = (UISettings::values.callout_flags.GetValue() & - static_cast(CalloutFlag::DRDDeprecation)) == 0; - - if (result == Core::SystemResultStatus::Success && - QtCommon::system->GetAppLoader().GetFileType() == Loader::FileType::DeconstructedRomDirectory && - drd_callout) { - UISettings::values.callout_flags = UISettings::values.callout_flags.GetValue() | - static_cast(CalloutFlag::DRDDeprecation); - QMessageBox::warning( - this, tr("Warning: Outdated Game Format"), - tr("You are using the deconstructed ROM directory format for this game, which is an " - "outdated format that has been superseded by others such as NCA, NAX, XCI, or " - "NSP. Deconstructed ROM directories lack icons, metadata, and update " - "support.

For an explanation of the various Switch formats Eden supports, check " - "out our " - "wiki. This message will not be shown again.")); - } - - if (result != Core::SystemResultStatus::Success) { - switch (result) { - case Core::SystemResultStatus::ErrorGetLoader: - LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); - QMessageBox::critical(this, tr("Error while loading ROM!"), - tr("The ROM format is not supported.")); - break; - case Core::SystemResultStatus::ErrorVideoCore: - QMessageBox::critical( - this, tr("An error occurred initializing the video core."), - tr("Eden has encountered an error while running the video core. " - "This is usually caused by outdated GPU drivers, including integrated ones. " - "Please see the log for more details. " - "For more information on accessing the log, please see the following page: " - "" - "How to Upload the Log File. ")); - break; - default: - if (result > Core::SystemResultStatus::ErrorLoader) { - const u16 loader_id = static_cast(Core::SystemResultStatus::ErrorLoader); - const u16 error_id = static_cast(result) - loader_id; - const std::string error_code = fmt::format("({:04X}-{:04X})", loader_id, error_id); - LOG_CRITICAL(Frontend, "Failed to load ROM! {}", error_code); - - const auto title = - tr("Error while loading ROM! %1", "%1 signifies a numeric error code.") - .arg(QString::fromStdString(error_code)); - const auto description = - tr("%1
Please redump your files or ask on Discord/Revolt for help.", - "%1 signifies an error string.") - .arg(QString::fromStdString( - GetResultStatusString(static_cast(error_id)))); - - QMessageBox::critical(this, title, description); - } else { - QMessageBox::critical( - this, tr("Error while loading ROM!"), - tr("An unknown error occurred. Please see the log for more details.")); - } - break; - } - return false; - } - current_game_path = filename; - - return true; -} - -bool GMainWindow::SelectAndSetCurrentUser( - const Core::Frontend::ProfileSelectParameters& parameters) { - QtProfileSelectionDialog dialog(*QtCommon::system, this, parameters); - dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | - Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::WindowModal); - - if (dialog.exec() == QDialog::Rejected) { - return false; - } - - Settings::values.current_user = dialog.GetIndex(); - return true; -} - -void GMainWindow::ConfigureFilesystemProvider(const std::string& filepath) { - // Ensure all NCAs are registered before launching the game - const auto file = QtCommon::vfs->OpenFile(filepath, FileSys::OpenMode::Read); - if (!file) { - return; - } - - auto loader = Loader::GetLoader(*QtCommon::system, file); - if (!loader) { - return; - } - - const auto file_type = loader->GetFileType(); - if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { - return; - } - - u64 program_id = 0; - const auto res2 = loader->ReadProgramId(program_id); - if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { - QtCommon::provider->AddEntry(FileSys::TitleType::Application, - FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id, - file); - } else if (res2 == Loader::ResultStatus::Success && - (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { - const auto nsp = file_type == Loader::FileType::NSP - ? std::make_shared(file) - : FileSys::XCI{file}.GetSecurePartitionNSP(); - for (const auto& title : nsp->GetNCAs()) { - for (const auto& entry : title.second) { - QtCommon::provider->AddEntry(entry.first.first, entry.first.second, title.first, - entry.second->GetBaseFile()); - } - } - } -} - -void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletParameters params, - StartGameType type) { - LOG_INFO(Frontend, "Eden starting..."); - - if (params.program_id == 0 || - params.program_id > static_cast(Service::AM::AppletProgramId::MaxProgramId)) { - StoreRecentFile(filename); // Put the filename on top of the list - } - - // Save configurations - UpdateUISettings(); - game_list->SaveInterfaceLayout(); - config->SaveAllValues(); - - u64 title_id{0}; - - last_filename_booted = filename; - - ConfigureFilesystemProvider(filename.toStdString()); - const auto v_file = Core::GetGameFileFromPath(QtCommon::vfs, filename.toUtf8().constData()); - const auto loader = Loader::GetLoader(*QtCommon::system, v_file, params.program_id, params.program_index); - - if (loader != nullptr && loader->ReadProgramId(title_id) == Loader::ResultStatus::Success && - type == StartGameType::Normal) { - // Load per game settings - const auto file_path = - std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())}; - const auto config_file_name = title_id == 0 - ? Common::FS::PathToUTF8String(file_path.filename()) - : fmt::format("{:016X}", title_id); - QtConfig per_game_config(config_file_name, Config::ConfigType::PerGameConfig); - QtCommon::system->HIDCore().ReloadInputDevices(); - QtCommon::system->ApplySettings(); - } - - Settings::LogSettings(); - - if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) { - const Core::Frontend::ProfileSelectParameters parameters{ - .mode = Service::AM::Frontend::UiMode::UserSelector, - .invalid_uid_list = {}, - .display_options = {}, - .purpose = Service::AM::Frontend::UserSelectionPurpose::General, - }; - if (SelectAndSetCurrentUser(parameters) == false) { - return; - } - } - - // If the user specifies -u (successfully) on the cmd line, don't prompt for a user on first - // game startup only. If the user stops emulation and starts a new one, go back to the expected - // behavior of asking. - user_flag_cmd_line = false; - - if (!LoadROM(filename, params)) { - return; - } - - QtCommon::system->SetShuttingDown(false); - game_list->setDisabled(true); - - // Create and start the emulation thread - emu_thread = std::make_unique(*QtCommon::system); - emit EmulationStarting(emu_thread.get()); - emu_thread->start(); - - // Register an ExecuteProgram callback such that Core can execute a sub-program - QtCommon::system->RegisterExecuteProgramCallback( - [this](std::size_t program_index_) { render_window->ExecuteProgram(program_index_); }); - - QtCommon::system->RegisterExitCallback([this] { - emu_thread->ForceStop(); - render_window->Exit(); - }); - - connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); - connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); - - connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, - &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); - - // Update the GUI - UpdateStatusButtons(); - if (ui->action_Single_Window_Mode->isChecked()) { - game_list->hide(); - game_list_placeholder->hide(); - } - status_bar_update_timer.start(500); - renderer_status_button->setDisabled(true); - refresh_button->setDisabled(true); - - if (UISettings::values.hide_mouse || Settings::values.mouse_panning) { - render_window->installEventFilter(render_window); - render_window->setAttribute(Qt::WA_Hover, true); - } - - if (UISettings::values.hide_mouse) { - mouse_hide_timer.start(); - } - - render_window->InitializeCamera(); - - std::string title_name; - std::string title_version; - const auto res = QtCommon::system->GetGameName(title_name); - - const auto metadata = [title_id] { - const FileSys::PatchManager pm(title_id, QtCommon::system->GetFileSystemController(), - QtCommon::system->GetContentProvider()); - return pm.GetControlMetadata(); - }(); - if (metadata.first != nullptr) { - title_version = metadata.first->GetVersionString(); - title_name = metadata.first->GetApplicationName(); - } - if (res != Loader::ResultStatus::Success || title_name.empty()) { - title_name = Common::FS::PathToUTF8String( - std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())} - .filename()); - } - const bool is_64bit = QtCommon::system->Kernel().ApplicationProcess()->Is64Bit(); - const auto instruction_set_suffix = is_64bit ? tr("(64-bit)") : tr("(32-bit)"); - title_name = tr("%1 %2", "%1 is the title name. %2 indicates if the title is 64-bit or 32-bit") - .arg(QString::fromStdString(title_name), instruction_set_suffix) - .toStdString(); - LOG_INFO(Frontend, "Booting game: {:016X} | {} | {}", title_id, title_name, title_version); - const auto gpu_vendor = QtCommon::system->GPU().Renderer().GetDeviceVendor(); - UpdateWindowTitle(title_name, title_version, gpu_vendor); - - loading_screen->Prepare(QtCommon::system->GetAppLoader()); - loading_screen->show(); - - emulation_running = true; - if (ui->action_Fullscreen->isChecked()) { - ShowFullscreen(); - } - OnStartGame(); -} - -void GMainWindow::BootGameFromList(const QString& filename, StartGameType with_config) { - BootGame(filename, ApplicationAppletParameters(), with_config); -} - -bool GMainWindow::OnShutdownBegin() { - if (!emulation_running) { - return false; - } - - if (ui->action_Fullscreen->isChecked()) { - HideFullscreen(); - } - - AllowOSSleep(); - - // Disable unlimited frame rate - Settings::values.use_speed_limit.SetValue(true); - - if (QtCommon::system->IsShuttingDown()) { - return false; - } - - QtCommon::system->SetShuttingDown(true); - discord_rpc->Pause(); - - RequestGameExit(); - emu_thread->disconnect(); - emu_thread->SetRunning(true); - - emit EmulationStopping(); - - int shutdown_time = 1000; - - if (QtCommon::system->DebuggerEnabled()) { - shutdown_time = 0; - } else if (QtCommon::system->GetExitLocked()) { - shutdown_time = 5000; - } - - shutdown_timer.setSingleShot(true); - shutdown_timer.start(shutdown_time); - connect(&shutdown_timer, &QTimer::timeout, this, &GMainWindow::OnEmulationStopTimeExpired); - connect(emu_thread.get(), &QThread::finished, this, &GMainWindow::OnEmulationStopped); - - // Disable everything to prevent anything from being triggered here - ui->action_Pause->setEnabled(false); - ui->action_Restart->setEnabled(false); - ui->action_Stop->setEnabled(false); - - return true; -} - -void GMainWindow::OnShutdownBeginDialog() { - shutdown_dialog = new OverlayDialog(this, *QtCommon::system, QString{}, tr("Closing software..."), - QString{}, QString{}, Qt::AlignHCenter | Qt::AlignVCenter); - shutdown_dialog->open(); -} - -void GMainWindow::OnEmulationStopTimeExpired() { - if (emu_thread) { - emu_thread->ForceStop(); - } -} - -void GMainWindow::OnEmulationStopped() { - shutdown_timer.stop(); - if (emu_thread) { - emu_thread->disconnect(); - emu_thread->wait(); - emu_thread.reset(); - } - - if (shutdown_dialog) { - shutdown_dialog->deleteLater(); - shutdown_dialog = nullptr; - } - - emulation_running = false; - - discord_rpc->Update(); - -#ifdef __linux__ - Common::Linux::StopGamemode(); -#endif - - // The emulation is stopped, so closing the window or not does not matter anymore - disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); - - // Update the GUI - UpdateMenuState(); - - render_window->hide(); - loading_screen->hide(); - loading_screen->Clear(); - if (game_list->IsEmpty()) { - game_list_placeholder->show(); - } else { - game_list->show(); - } - game_list->SetFilterFocus(); - tas_label->clear(); - input_subsystem->GetTas()->Stop(); - OnTasStateChanged(); - render_window->FinalizeCamera(); - - QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::None); - - // Enable all controllers - QtCommon::system->HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All}); - - render_window->removeEventFilter(render_window); - render_window->setAttribute(Qt::WA_Hover, false); - - UpdateWindowTitle(); - - // Disable status bar updates - status_bar_update_timer.stop(); - shader_building_label->setVisible(false); - res_scale_label->setVisible(false); - emu_speed_label->setVisible(false); - game_fps_label->setVisible(false); - emu_frametime_label->setVisible(false); - renderer_status_button->setEnabled(!UISettings::values.has_broken_vulkan); - refresh_button->setEnabled(true); - - if (!firmware_label->text().isEmpty()) { - firmware_label->setVisible(true); - } - - current_game_path.clear(); - - // When closing the game, destroy the GLWindow to clear the context after the game is closed - render_window->ReleaseRenderTarget(); - - // Enable game list - game_list->setEnabled(true); - - Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn()); - QtCommon::system->HIDCore().ReloadInputDevices(); - UpdateStatusButtons(); -} - -void GMainWindow::ShutdownGame() { - if (!emulation_running) { - return; - } - - // TODO(crueter): make this common as well (frontend_common?) - play_time_manager->Stop(); - OnShutdownBegin(); - OnEmulationStopTimeExpired(); - OnEmulationStopped(); -} - -void GMainWindow::StoreRecentFile(const QString& filename) { - UISettings::values.recent_files.prepend(filename); - UISettings::values.recent_files.removeDuplicates(); - while (UISettings::values.recent_files.size() > max_recent_files_item) { - UISettings::values.recent_files.removeLast(); - } - - UpdateRecentFiles(); -} - -void GMainWindow::UpdateRecentFiles() { - const int num_recent_files = - (std::min)(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); - - for (int i = 0; i < num_recent_files; i++) { - const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( - QFileInfo(UISettings::values.recent_files[i]).fileName()); - actions_recent_files[i]->setText(text); - actions_recent_files[i]->setData(UISettings::values.recent_files[i]); - actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); - actions_recent_files[i]->setVisible(true); - } - - for (int j = num_recent_files; j < max_recent_files_item; ++j) { - actions_recent_files[j]->setVisible(false); - } - - // Enable the recent files menu if the list isn't empty - ui->menu_recent_files->setEnabled(num_recent_files != 0); -} - -void GMainWindow::OnGameListLoadFile(QString game_path, u64 program_id) { - auto params = ApplicationAppletParameters(); - params.program_id = program_id; - - BootGame(game_path, params); -} - -// TODO(crueter): Common profile selector -void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target, - const std::string& game_path) { - std::filesystem::path path; - QString open_target; - - const auto [user_save_size, device_save_size] = [&game_path, &program_id] { - const FileSys::PatchManager pm{program_id, QtCommon::system->GetFileSystemController(), - QtCommon::system->GetContentProvider()}; - const auto control = pm.GetControlMetadata().first; - if (control != nullptr) { - return std::make_pair(control->GetDefaultNormalSaveSize(), - control->GetDeviceSaveDataSize()); - } else { - const auto file = Core::GetGameFileFromPath(QtCommon::vfs, game_path); - const auto loader = Loader::GetLoader(*QtCommon::system, file); - - FileSys::NACP nacp{}; - loader->ReadControlData(nacp); - return std::make_pair(nacp.GetDefaultNormalSaveSize(), nacp.GetDeviceSaveDataSize()); - } - }(); - - const bool has_user_save{user_save_size > 0}; - const bool has_device_save{device_save_size > 0}; - - ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?"); - - switch (target) { - case GameListOpenTarget::SaveData: { - open_target = tr("Save Data"); - const auto nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir); - auto vfs_nand_dir = - QtCommon::vfs->OpenDirectory(Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read); - - if (has_user_save) { - // User save data - const auto select_profile = [this] { - const Core::Frontend::ProfileSelectParameters parameters{ - .mode = Service::AM::Frontend::UiMode::UserSelector, - .invalid_uid_list = {}, - .display_options = {}, - .purpose = Service::AM::Frontend::UserSelectionPurpose::General, - }; - QtProfileSelectionDialog dialog(*QtCommon::system, this, parameters); - dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | - Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::WindowModal); - - if (dialog.exec() == QDialog::Rejected) { - return -1; - } - - return dialog.GetIndex(); - }; - - const auto index = select_profile(); - if (index == -1) { - return; - } - - const auto user_id = - QtCommon::system->GetProfileManager().GetUser(static_cast(index)); - ASSERT(user_id); - - const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( - {}, vfs_nand_dir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, - program_id, user_id->AsU128(), 0); - - path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path); - } else { - // Device save data - const auto device_save_data_path = FileSys::SaveDataFactory::GetFullPath( - {}, vfs_nand_dir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, - program_id, {}, 0); - - path = Common::FS::ConcatPathSafe(nand_dir, device_save_data_path); - } - - if (!Common::FS::CreateDirs(path)) { - LOG_ERROR(Frontend, "Unable to create the directories for save data"); - } - - break; - } - case GameListOpenTarget::ModData: { - open_target = tr("Mod Data"); - path = Common::FS::GetEdenPath(Common::FS::EdenPath::LoadDir) / - fmt::format("{:016X}", program_id); - break; - } - default: - UNIMPLEMENTED(); - break; - } - - const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(path)); - const QDir dir(qpath); - if (!dir.exists()) { - QMessageBox::warning(this, tr("Error Opening %1 Folder").arg(open_target), - tr("Folder does not exist!")); - return; - } - LOG_INFO(Frontend, "Opening {} path for program_id={:016x}", open_target.toStdString(), - program_id); - QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); -} - -static bool RomFSRawCopy(size_t total_size, size_t& read_size, QProgressDialog& dialog, - const FileSys::VirtualDir& src, const FileSys::VirtualDir& dest, - bool full) { - if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) - return false; - if (dialog.wasCanceled()) - return false; - - std::vector buffer(CopyBufferSize); - auto last_timestamp = std::chrono::steady_clock::now(); - - const auto QtRawCopy = [&](const FileSys::VirtualFile& src_file, - const FileSys::VirtualFile& dest_file) { - if (src_file == nullptr || dest_file == nullptr) { - return false; - } - if (!dest_file->Resize(src_file->GetSize())) { - return false; - } - - for (std::size_t i = 0; i < src_file->GetSize(); i += buffer.size()) { - if (dialog.wasCanceled()) { - dest_file->Resize(0); - return false; - } - - using namespace std::literals::chrono_literals; - const auto new_timestamp = std::chrono::steady_clock::now(); - - if ((new_timestamp - last_timestamp) > 33ms) { - last_timestamp = new_timestamp; - dialog.setValue( - static_cast((std::min)(read_size, total_size) * 100 / total_size)); - QCoreApplication::processEvents(); - } - - const auto read = src_file->Read(buffer.data(), buffer.size(), i); - dest_file->Write(buffer.data(), read, i); - - read_size += read; - } - - return true; - }; - - if (full) { - for (const auto& file : src->GetFiles()) { - const auto out = VfsDirectoryCreateFileWrapper(dest, file->GetName()); - if (!QtRawCopy(file, out)) - return false; - } - } - - for (const auto& dir : src->GetSubdirectories()) { - const auto out = dest->CreateSubdirectory(dir->GetName()); - if (!RomFSRawCopy(total_size, read_size, dialog, dir, out, full)) - return false; - } - - return true; -} - -// TODO(crueter): All this can be transfered to qt_common -// Aldoe I need to decide re: message boxes for QML -// translations_common? strings_common? qt_strings? who knows -void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, QtCommon::Game::InstalledEntryType type) { - const QString entry_question = [type] { - switch (type) { - case QtCommon::Game::InstalledEntryType::Game: - return tr("Remove Installed Game Contents?"); - case QtCommon::Game::InstalledEntryType::Update: - return tr("Remove Installed Game Update?"); - case QtCommon::Game::InstalledEntryType::AddOnContent: - return tr("Remove Installed Game DLC?"); - default: - return QStringLiteral("Remove Installed Game ?"); - } - }(); - - if (!question(this, tr("Remove Entry"), entry_question, QMessageBox::Yes | QMessageBox::No, - QMessageBox::No)) { - return; - } - - // TODO(crueter): move this to QtCommon (populate async?) - switch (type) { - case QtCommon::Game::InstalledEntryType::Game: - QtCommon::Game::RemoveBaseContent(program_id, type); - [[fallthrough]]; - case QtCommon::Game::InstalledEntryType::Update: - QtCommon::Game::RemoveUpdateContent(program_id, type); - if (type != QtCommon::Game::InstalledEntryType::Game) { - break; - } - [[fallthrough]]; - case QtCommon::Game::InstalledEntryType::AddOnContent: - QtCommon::Game::RemoveAddOnContent(program_id, type); - break; - } - Common::FS::RemoveDirRecursively(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / - "game_list"); - game_list->PopulateAsync(UISettings::values.game_dirs); -} - -void GMainWindow::OnGameListRemoveFile(u64 program_id, QtCommon::Game::GameListRemoveTarget target, - const std::string& game_path) { - const QString question = [target] { - switch (target) { - case QtCommon::Game::GameListRemoveTarget::GlShaderCache: - return tr("Delete OpenGL Transferable Shader Cache?"); - case QtCommon::Game::GameListRemoveTarget::VkShaderCache: - return tr("Delete Vulkan Transferable Shader Cache?"); - case QtCommon::Game::GameListRemoveTarget::AllShaderCache: - return tr("Delete All Transferable Shader Caches?"); - case QtCommon::Game::GameListRemoveTarget::CustomConfiguration: - return tr("Remove Custom Game Configuration?"); - case QtCommon::Game::GameListRemoveTarget::CacheStorage: - return tr("Remove Cache Storage?"); - default: - return QString{}; - } - }(); - - if (!GMainWindow::question(this, tr("Remove File"), question, - QMessageBox::Yes | QMessageBox::No, QMessageBox::No)) { - return; - } - - switch (target) { - case QtCommon::Game::GameListRemoveTarget::VkShaderCache: - QtCommon::Game::RemoveVulkanDriverPipelineCache(program_id); - [[fallthrough]]; - case QtCommon::Game::GameListRemoveTarget::GlShaderCache: - QtCommon::Game::RemoveTransferableShaderCache(program_id, target); - break; - case QtCommon::Game::GameListRemoveTarget::AllShaderCache: - QtCommon::Game::RemoveAllTransferableShaderCaches(program_id); - break; - case QtCommon::Game::GameListRemoveTarget::CustomConfiguration: - QtCommon::Game::RemoveCustomConfiguration(program_id, game_path); - break; - case QtCommon::Game::GameListRemoveTarget::CacheStorage: - QtCommon::Game::RemoveCacheStorage(program_id); - break; - } -} - -void GMainWindow::OnGameListSetPlayTime(u64 program_id) { - const u64 current_play_time = play_time_manager->GetPlayTime(program_id); - - SetPlayTimeDialog dialog(this, current_play_time); - - if (dialog.exec() == QDialog::Accepted) { - const u64 total_seconds = dialog.GetTotalSeconds(); - play_time_manager->SetPlayTime(program_id, total_seconds); - game_list->PopulateAsync(UISettings::values.game_dirs); - } -} - - -void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) { - if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No) != QMessageBox::Yes) { - return; - } - - play_time_manager->ResetProgramPlayTime(program_id); - game_list->PopulateAsync(UISettings::values.game_dirs); -} - -void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_path, - DumpRomFSTarget target) { - const auto failed = [this] { - QMessageBox::warning(this, tr("RomFS Extraction Failed!"), - tr("There was an error copying the RomFS files or the user " - "cancelled the operation.")); - }; - - const auto loader = - Loader::GetLoader(*QtCommon::system, QtCommon::vfs->OpenFile(game_path, FileSys::OpenMode::Read)); - if (loader == nullptr) { - failed(); - return; - } - - FileSys::VirtualFile packed_update_raw{}; - loader->ReadUpdateRaw(packed_update_raw); - - const auto& installed = QtCommon::system->GetContentProvider(); - - u64 title_id{}; - u8 raw_type{}; - if (!SelectRomFSDumpTarget(installed, program_id, &title_id, &raw_type)) { - failed(); - return; - } - - const auto type = static_cast(raw_type); - const auto base_nca = installed.GetEntry(title_id, type); - if (!base_nca) { - failed(); - return; - } - - const FileSys::NCA update_nca{packed_update_raw, nullptr}; - if (type != FileSys::ContentRecordType::Program || - update_nca.GetStatus() != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS || - update_nca.GetTitleId() != FileSys::GetUpdateTitleID(title_id)) { - packed_update_raw = {}; - } - - const auto base_romfs = base_nca->GetRomFS(); - const auto dump_dir = - target == DumpRomFSTarget::Normal - ? Common::FS::GetEdenPath(Common::FS::EdenPath::DumpDir) - : Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir) / "atmosphere" / "contents"; - const auto romfs_dir = fmt::format("{:016X}/romfs", title_id); - - const auto path = Common::FS::PathToUTF8String(dump_dir / romfs_dir); - - const FileSys::PatchManager pm{title_id, QtCommon::system->GetFileSystemController(), installed}; - auto romfs = pm.PatchRomFS(base_nca.get(), base_romfs, type, packed_update_raw, false); - - const auto out = VfsFilesystemCreateDirectoryWrapper(path, FileSys::OpenMode::ReadWrite); - - if (out == nullptr) { - failed(); - QtCommon::vfs->DeleteDirectory(path); - return; - } - - bool ok = false; - const QStringList selections{tr("Full"), tr("Skeleton")}; - const auto res = QInputDialog::getItem( - this, tr("Select RomFS Dump Mode"), - tr("Please select the how you would like the RomFS dumped.
Full will copy all of the " - "files into the new directory while
skeleton will only create the directory " - "structure."), - selections, 0, false, &ok); - if (!ok) { - failed(); - QtCommon::vfs->DeleteDirectory(path); - return; - } - - const auto extracted = FileSys::ExtractRomFS(romfs); - if (extracted == nullptr) { - failed(); - return; - } - - const auto full = res == selections.constFirst(); - - // The expected required space is the size of the RomFS + 1 GiB - const auto minimum_free_space = romfs->GetSize() + 0x40000000; - - if (full && Common::FS::GetFreeSpaceSize(path) < minimum_free_space) { - QMessageBox::warning(this, tr("RomFS Extraction Failed!"), - tr("There is not enough free space at %1 to extract the RomFS. Please " - "free up space or select a different dump directory at " - "Emulation > Configure > System > Filesystem > Dump Root") - .arg(QString::fromStdString(path))); - return; - } - - QProgressDialog progress(tr("Extracting RomFS..."), tr("Cancel"), 0, 100, this); - progress.setWindowModality(Qt::WindowModal); - progress.setMinimumDuration(100); - progress.setAutoClose(false); - progress.setAutoReset(false); - - size_t read_size = 0; - - if (RomFSRawCopy(romfs->GetSize(), read_size, progress, extracted, out, full)) { - progress.close(); - QMessageBox::information(this, tr("RomFS Extraction Succeeded!"), - tr("The operation completed successfully.")); - QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(path))); - } else { - progress.close(); - failed(); - QtCommon::vfs->DeleteDirectory(path); - } -} - -// END -void GMainWindow::OnGameListVerifyIntegrity(const std::string& game_path) { - QtCommon::Content::VerifyGameContents(game_path); -} - -void GMainWindow::OnGameListCopyTID(u64 program_id) { - QClipboard* clipboard = QGuiApplication::clipboard(); - clipboard->setText(QString::fromStdString(fmt::format("{:016X}", program_id))); -} - -void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, - const CompatibilityList& compatibility_list) { - const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); - - QString directory; - if (it != compatibility_list.end()) { - directory = it->second.second; - } - - QDesktopServices::openUrl( - QUrl(QStringLiteral("https://eden-emulator.github.io/game/") + directory)); -} - -void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, - const QtCommon::Game::ShortcutTarget target) { - // Create shortcut - std::string arguments = fmt::format("-g \"{:s}\"", game_path); - - QtCommon::Game::CreateShortcut(game_path, program_id, "", target, arguments, true); -} - -void GMainWindow::OnGameListOpenDirectory(const QString& directory) { - // TODO(crueter): QtCommon - std::filesystem::path fs_path; - if (directory == QStringLiteral("SDMC")) { - fs_path = - Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir) / "Nintendo/Contents/registered"; - } else if (directory == QStringLiteral("UserNAND")) { - fs_path = - Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) / "user/Contents/registered"; - } else if (directory == QStringLiteral("SysNAND")) { - fs_path = - Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) / "system/Contents/registered"; - } else { - fs_path = directory.toStdString(); - } - - const auto qt_path = QString::fromStdString(Common::FS::PathToUTF8String(fs_path)); - - if (!Common::FS::IsDir(fs_path)) { - QMessageBox::critical(this, tr("Error Opening %1").arg(qt_path), - tr("Folder does not exist!")); - return; - } - - QDesktopServices::openUrl(QUrl::fromLocalFile(qt_path)); -} - -void GMainWindow::OnGameListAddDirectory() { - const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); - if (dir_path.isEmpty()) { - return; - } - - UISettings::GameDir game_dir{dir_path.toStdString(), false, true}; - if (!UISettings::values.game_dirs.contains(game_dir)) { - UISettings::values.game_dirs.append(game_dir); - game_list->PopulateAsync(UISettings::values.game_dirs); - } else { - LOG_WARNING(Frontend, "Selected directory is already in the game list"); - } - - OnSaveConfig(); -} - -void GMainWindow::OnGameListShowList(bool show) { - if (emulation_running && ui->action_Single_Window_Mode->isChecked()) - return; - game_list->setVisible(show); - game_list_placeholder->setVisible(!show); -}; - -void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { - u64 title_id{}; - const auto v_file = Core::GetGameFileFromPath(QtCommon::vfs, file); - const auto loader = Loader::GetLoader(*QtCommon::system, v_file); - - if (loader == nullptr || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { - QMessageBox::information(this, tr("Properties"), - tr("The game properties could not be loaded.")); - return; - } - - OpenPerGameConfiguration(title_id, file); -} - -std::string GMainWindow::GetProfileID() -{ - const auto select_profile = [this] { - const Core::Frontend::ProfileSelectParameters parameters{ - .mode = Service::AM::Frontend::UiMode::UserSelector, - .invalid_uid_list = {}, - .display_options = {}, - .purpose = Service::AM::Frontend::UserSelectionPurpose::General, - }; - QtProfileSelectionDialog dialog(*QtCommon::system, this, parameters); - dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint - | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::WindowModal); - - if (dialog.exec() == QDialog::Rejected) { - return -1; - } - - return dialog.GetIndex(); - }; - - const auto index = select_profile(); - if (index == -1) { - return ""; - } - - const auto uuid = QtCommon::system->GetProfileManager().GetUser(static_cast(index)); - ASSERT(uuid); - - const auto user_id = uuid->AsU128(); - - return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]); -} - -void GMainWindow::OnLinkToRyujinx(const u64& program_id) -{ - u64 save_id = QtCommon::FS::GetRyujinxSaveID(program_id); - if (save_id == (u64) -1) - return; - - const std::string user_id = GetProfileID(); - - auto paths = QtCommon::FS::GetEmuPaths(program_id, save_id, user_id); - if (!paths) - return; - - auto eden_dir = paths.value().first; - auto ryu_dir = paths.value().second; - - if (!QtCommon::FS::CheckUnlink(eden_dir, ryu_dir)) { - RyujinxDialog dialog(eden_dir, ryu_dir, this); - dialog.exec(); - } -} - -void GMainWindow::OnMenuLoadFile() { - if (is_load_file_select_active) { - return; - } - - is_load_file_select_active = true; - const QString extensions = - QStringLiteral("*.") - .append(GameList::supported_file_extensions.join(QStringLiteral(" *."))) - .append(QStringLiteral(" main")); - const QString file_filter = tr("Switch Executable (%1);;All Files (*.*)", - "%1 is an identifier for the Switch executable file extensions.") - .arg(extensions); - const QString filename = QFileDialog::getOpenFileName( - this, tr("Load File"), QString::fromStdString(UISettings::values.roms_path), file_filter); - is_load_file_select_active = false; - - if (filename.isEmpty()) { - return; - } - - UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); - BootGame(filename, ApplicationAppletParameters()); -} - -void GMainWindow::OnMenuLoadFolder() { - const QString dir_path = - QFileDialog::getExistingDirectory(this, tr("Open Extracted ROM Directory")); - - if (dir_path.isNull()) { - return; - } - - const QDir dir{dir_path}; - const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); - if (matching_main.size() == 1) { - BootGame(dir.path() + QDir::separator() + matching_main[0], ApplicationAppletParameters()); - } else { - QMessageBox::warning(this, tr("Invalid Directory Selected"), - tr("The directory you have selected does not contain a 'main' file.")); - } -} - -void GMainWindow::IncrementInstallProgress() { - install_progress->setValue(install_progress->value() + 1); -} - -void GMainWindow::OnMenuInstallToNAND() { - const QString file_filter = - tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive " - "(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge " - "Image (*.xci)"); - - QStringList filenames = QFileDialog::getOpenFileNames( - this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path), - file_filter); - - if (filenames.isEmpty()) { - return; - } - - InstallDialog installDialog(this, filenames); - if (installDialog.exec() == QDialog::Rejected) { - return; - } - - const QStringList files = installDialog.GetFiles(); - - if (files.isEmpty()) { - return; - } - - // Save folder location of the first selected file - UISettings::values.roms_path = QFileInfo(filenames[0]).path().toStdString(); - - int remaining = filenames.size(); - - // This would only overflow above 2^51 bytes (2.252 PB) - int total_size = 0; - for (const QString& file : files) { - total_size += static_cast(QFile(file).size() / CopyBufferSize); - } - if (total_size < 0) { - LOG_CRITICAL(Frontend, "Attempting to install too many files, aborting."); - return; - } - - QStringList new_files{}; // Newly installed files that do not yet exist in the NAND - QStringList overwritten_files{}; // Files that overwrote those existing in the NAND - QStringList failed_files{}; // Files that failed to install due to errors - bool detected_base_install{}; // Whether a base game was attempted to be installed - - ui->action_Install_File_NAND->setEnabled(false); - - install_progress = new QProgressDialog(QString{}, tr("Cancel"), 0, total_size, this); - install_progress->setWindowFlags(windowFlags() & ~Qt::WindowMaximizeButtonHint); - install_progress->setAttribute(Qt::WA_DeleteOnClose, true); - install_progress->setFixedWidth(installDialog.GetMinimumWidth() + 40); - install_progress->show(); - - for (const QString& file : files) { - install_progress->setWindowTitle(tr("%n file(s) remaining", "", remaining)); - install_progress->setLabelText( - tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName())); - - QFuture future; - ContentManager::InstallResult result; - - if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { - const auto progress_callback = [this](size_t size, size_t progress) { - emit UpdateInstallProgress(); - if (install_progress->wasCanceled()) { - return true; - } - return false; - }; - future = QtConcurrent::run([&file, progress_callback] { - return ContentManager::InstallNSP(*QtCommon::system, *QtCommon::vfs, file.toStdString(), - progress_callback); - }); - - while (!future.isFinished()) { - QCoreApplication::processEvents(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - result = future.result(); - - } else { - result = InstallNCA(file); - } - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - switch (result) { - case ContentManager::InstallResult::Success: - new_files.append(QFileInfo(file).fileName()); - break; - case ContentManager::InstallResult::Overwrite: - overwritten_files.append(QFileInfo(file).fileName()); - break; - case ContentManager::InstallResult::Failure: - failed_files.append(QFileInfo(file).fileName()); - break; - case ContentManager::InstallResult::BaseInstallAttempted: - failed_files.append(QFileInfo(file).fileName()); - detected_base_install = true; - break; - } - - --remaining; - } - - install_progress->close(); - - if (detected_base_install) { - QMessageBox::warning( - this, tr("Install Results"), - tr("To avoid possible conflicts, we discourage users from installing base games to the " - "NAND.\nPlease, only use this feature to install updates and DLC.")); - } - - const QString install_results = - (new_files.isEmpty() ? QString{} - : tr("%n file(s) were newly installed\n", "", new_files.size())) + - (overwritten_files.isEmpty() - ? QString{} - : tr("%n file(s) were overwritten\n", "", overwritten_files.size())) + - (failed_files.isEmpty() ? QString{} - : tr("%n file(s) failed to install\n", "", failed_files.size())); - - QMessageBox::information(this, tr("Install Results"), install_results); - Common::FS::RemoveDirRecursively(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / - "game_list"); - game_list->PopulateAsync(UISettings::values.game_dirs); - ui->action_Install_File_NAND->setEnabled(true); -} - -ContentManager::InstallResult GMainWindow::InstallNCA(const QString& filename) { - const QStringList tt_options{tr("System Application"), - tr("System Archive"), - tr("System Application Update"), - tr("Firmware Package (Type A)"), - tr("Firmware Package (Type B)"), - tr("Game"), - tr("Game Update"), - tr("Game DLC"), - tr("Delta Title")}; - bool ok; - const auto item = QInputDialog::getItem( - this, tr("Select NCA Install Type..."), - tr("Please select the type of title you would like to install this NCA as:\n(In " - "most instances, the default 'Game' is fine.)"), - tt_options, 5, false, &ok); - - auto index = tt_options.indexOf(item); - if (!ok || index == -1) { - QMessageBox::warning(this, tr("Failed to Install"), - tr("The title type you selected for the NCA is invalid.")); - return ContentManager::InstallResult::Failure; - } - - // If index is equal to or past Game, add the jump in TitleType. - if (index >= 5) { - index += static_cast(FileSys::TitleType::Application) - - static_cast(FileSys::TitleType::FirmwarePackageB); - } - - const bool is_application = index >= static_cast(FileSys::TitleType::Application); - const auto& fs_controller = QtCommon::system->GetFileSystemController(); - auto* registered_cache = is_application ? fs_controller.GetUserNANDContents() - : fs_controller.GetSystemNANDContents(); - - const auto progress_callback = [this](size_t size, size_t progress) { - emit UpdateInstallProgress(); - if (install_progress->wasCanceled()) { - return true; - } - return false; - }; - return ContentManager::InstallNCA(*QtCommon::vfs, filename.toStdString(), *registered_cache, - static_cast(index), progress_callback); -} - -void GMainWindow::OnMenuRecentFile() { - QAction* action = qobject_cast(sender()); - assert(action); - - const QString filename = action->data().toString(); - if (QFileInfo::exists(filename)) { - BootGame(filename, ApplicationAppletParameters()); - } else { - // Display an error message and remove the file from the list. - QMessageBox::information(this, tr("File not found"), - tr("File \"%1\" not found").arg(filename)); - - UISettings::values.recent_files.removeOne(filename); - UpdateRecentFiles(); - } -} - -void GMainWindow::OnStartGame() { - PreventOSSleep(); - - emu_thread->SetRunning(true); - - UpdateMenuState(); - OnTasStateChanged(); - - play_time_manager->SetProgramId(QtCommon::system->GetApplicationProcessProgramID()); - play_time_manager->Start(); - - discord_rpc->Update(); - -#ifdef __linux__ - Common::Linux::StartGamemode(); -#endif -} - -void GMainWindow::OnRestartGame() { - if (!QtCommon::system->IsPoweredOn()) { - return; - } - - if (ConfirmShutdownGame()) { - // Make a copy since ShutdownGame edits game_path - const auto current_game = QString(current_game_path); - ShutdownGame(); - BootGame(current_game, ApplicationAppletParameters()); - } -} - -void GMainWindow::OnPauseGame() { - emu_thread->SetRunning(false); - play_time_manager->Stop(); - UpdateMenuState(); - AllowOSSleep(); - -#ifdef __linux__ - Common::Linux::StopGamemode(); -#endif -} - -void GMainWindow::OnPauseContinueGame() { - if (emulation_running) { - if (emu_thread->IsRunning()) { - OnPauseGame(); - } else { - OnStartGame(); - } - } -} - -void GMainWindow::OnStopGame() { - if (ConfirmShutdownGame()) { - play_time_manager->Stop(); - // Update game list to show new play time - game_list->PopulateAsync(UISettings::values.game_dirs); - if (OnShutdownBegin()) { - OnShutdownBeginDialog(); - } else { - OnEmulationStopped(); - } - } -} - -bool GMainWindow::ConfirmShutdownGame() { - if (UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Always) { - if (QtCommon::system->GetExitLocked()) { - if (!ConfirmForceLockedExit()) { - return false; - } - } else { - if (!ConfirmChangeGame()) { - return false; - } - } - } else { - if (UISettings::values.confirm_before_stopping.GetValue() == - ConfirmStop::Ask_Based_On_Game && - QtCommon::system->GetExitLocked()) { - if (!ConfirmForceLockedExit()) { - return false; - } - } - } - return true; -} - -void GMainWindow::OnLoadComplete() { - loading_screen->OnLoadComplete(); -} - -void GMainWindow::OnExecuteProgram(std::size_t program_index) { - ShutdownGame(); - - auto params = ApplicationAppletParameters(); - params.program_index = static_cast(program_index); - params.launch_type = Service::AM::LaunchType::ApplicationInitiated; - BootGame(last_filename_booted, params); -} - -void GMainWindow::OnExit() { - ShutdownGame(); -} - -void GMainWindow::OnSaveConfig() { - QtCommon::system->ApplySettings(); - config->SaveAllValues(); -} - -void GMainWindow::ErrorDisplayDisplayError(QString error_code, QString error_text) { - error_applet = new OverlayDialog(render_window, *QtCommon::system, error_code, error_text, QString{}, - tr("OK"), Qt::AlignLeft | Qt::AlignVCenter); - SCOPE_EXIT { - error_applet->deleteLater(); - error_applet = nullptr; - }; - error_applet->exec(); - - emit ErrorDisplayFinished(); -} - -void GMainWindow::ErrorDisplayRequestExit() { - if (error_applet) { - error_applet->reject(); - } -} - -void GMainWindow::OnMenuReportCompatibility() { -#if defined(ARCHITECTURE_x86_64) && !defined(__APPLE__) - const auto& caps = Common::GetCPUCaps(); - const bool has_fma = caps.fma || caps.fma4; - const auto processor_count = std::thread::hardware_concurrency(); - const bool has_4threads = processor_count == 0 || processor_count >= 4; - const bool has_8gb_ram = Common::GetMemInfo().TotalPhysicalMemory >= 8_GiB; - const bool has_broken_vulkan = UISettings::values.has_broken_vulkan; - - if (!has_fma || !has_4threads || !has_8gb_ram || has_broken_vulkan) { - QMessageBox::critical(this, tr("Hardware requirements not met"), - tr("Your system does not meet the recommended hardware requirements. " - "Compatibility reporting has been disabled.")); - return; - } - - if (!Settings::values.eden_token.GetValue().empty() && - !Settings::values.eden_username.GetValue().empty()) { - } else { - QMessageBox::critical( - this, tr("Missing yuzu Account"), - tr("In order to submit a game compatibility test case, you must set up your web token " - "and " - "username.

To link your eden account, go to Emulation > Configuration " - "> " - "Web.")); - } -#else - QMessageBox::critical(this, tr("Hardware requirements not met"), - tr("Your system does not meet the recommended hardware requirements. " - "Compatibility reporting has been disabled.")); -#endif -} - -void GMainWindow::OpenURL(const QUrl& url) { - const bool open = QDesktopServices::openUrl(url); - if (!open) { - QMessageBox::warning(this, tr("Error opening URL"), - tr("Unable to open the URL \"%1\".").arg(url.toString())); - } -} - -void GMainWindow::OnOpenModsPage() { - OpenURL(QUrl(QStringLiteral("https://github.com/eden-emulator/yuzu-mod-archive"))); -} - -void GMainWindow::OnOpenQuickstartGuide() { - OpenURL(QUrl(QStringLiteral("https://yuzu-mirror.github.io/help/quickstart/"))); -} - -void GMainWindow::OnOpenFAQ() { - OpenURL(QUrl(QStringLiteral("https://yuzu-mirror.github.io/help"))); -} - -void GMainWindow::ToggleFullscreen() { - if (!emulation_running) { - return; - } - if (ui->action_Fullscreen->isChecked()) { - ShowFullscreen(); - } else { - HideFullscreen(); - } -} - -// We're going to return the screen that the given window has the most pixels on -static QScreen* GuessCurrentScreen(QWidget* window) { - const QList screens = QGuiApplication::screens(); - return *std::max_element( - screens.cbegin(), screens.cend(), [window](const QScreen* left, const QScreen* right) { - const QSize left_size = left->geometry().intersected(window->geometry()).size(); - const QSize right_size = right->geometry().intersected(window->geometry()).size(); - return (left_size.height() * left_size.width()) < - (right_size.height() * right_size.width()); - }); -} - -bool GMainWindow::UsingExclusiveFullscreen() { - return Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive || - QGuiApplication::platformName() == QStringLiteral("wayland") || - QGuiApplication::platformName() == QStringLiteral("wayland-egl"); -} - -void GMainWindow::ShowFullscreen() { - const auto show_fullscreen = [this](QWidget* window) { - if (UsingExclusiveFullscreen()) { - window->showFullScreen(); - return; - } - window->hide(); - window->setWindowFlags(window->windowFlags() | Qt::FramelessWindowHint); - const auto screen_geometry = GuessCurrentScreen(window)->geometry(); - window->setGeometry(screen_geometry.x(), screen_geometry.y(), screen_geometry.width(), - screen_geometry.height() + 1); - window->raise(); - window->showNormal(); - }; - - if (ui->action_Single_Window_Mode->isChecked()) { - UISettings::values.geometry = saveGeometry(); - - ui->menubar->hide(); - statusBar()->hide(); - - show_fullscreen(this); - } else { - UISettings::values.renderwindow_geometry = render_window->saveGeometry(); - show_fullscreen(render_window); - } -} - -void GMainWindow::HideFullscreen() { - if (ui->action_Single_Window_Mode->isChecked()) { - if (UsingExclusiveFullscreen()) { - showNormal(); - restoreGeometry(UISettings::values.geometry); - } else { - hide(); - setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); - restoreGeometry(UISettings::values.geometry); - raise(); - show(); - } - - statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); - ui->menubar->show(); - } else { - if (UsingExclusiveFullscreen()) { - render_window->showNormal(); - render_window->restoreGeometry(UISettings::values.renderwindow_geometry); - } else { - render_window->hide(); - render_window->setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); - render_window->restoreGeometry(UISettings::values.renderwindow_geometry); - render_window->raise(); - render_window->show(); - } - } -} - -void GMainWindow::ToggleWindowMode() { - if (ui->action_Single_Window_Mode->isChecked()) { - // Render in the main window... - render_window->BackupGeometry(); - ui->horizontalLayout->addWidget(render_window); - render_window->setFocusPolicy(Qt::StrongFocus); - if (emulation_running) { - render_window->setVisible(true); - render_window->setFocus(); - game_list->hide(); - } - - } else { - // Render in a separate window... - ui->horizontalLayout->removeWidget(render_window); - render_window->setParent(nullptr); - render_window->setFocusPolicy(Qt::NoFocus); - if (emulation_running) { - render_window->setVisible(true); - render_window->RestoreGeometry(); - game_list->show(); - } - } -} - -void GMainWindow::ResetWindowSize(u32 width, u32 height) { - const auto aspect_ratio = Layout::EmulationAspectRatio(Settings::values.aspect_ratio.GetValue(), float(height) / width); - if (!ui->action_Single_Window_Mode->isChecked()) { - render_window->resize(height / aspect_ratio, height); - } else { - const bool show_status_bar = ui->action_Show_Status_Bar->isChecked(); - const auto status_bar_height = show_status_bar ? statusBar()->height() : 0; - resize(height / aspect_ratio, height + menuBar()->height() + status_bar_height); - } -} - -void GMainWindow::ResetWindowSize720() { - ResetWindowSize(Layout::ScreenUndocked::Width, Layout::ScreenUndocked::Height); -} - -void GMainWindow::ResetWindowSize900() { - ResetWindowSize(1600U, 900U); -} - -void GMainWindow::ResetWindowSize1080() { - ResetWindowSize(Layout::ScreenDocked::Width, Layout::ScreenDocked::Height); -} - -void GMainWindow::OnConfigure() { - const auto old_theme = UISettings::values.theme; - const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); - const auto old_language_index = Settings::values.language_index.GetValue(); -#ifdef __linux__ - const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); -#endif -#ifdef __unix__ - const bool old_force_x11 = Settings::values.gui_force_x11.GetValue(); -#endif - - Settings::SetConfiguringGlobal(true); - ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), - vk_device_records, *QtCommon::system, - !multiplayer_state->IsHostingPublicRoom()); - connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this, - &GMainWindow::OnLanguageChanged); - - const auto result = configure_dialog.exec(); - if (result != QDialog::Accepted && !UISettings::values.configuration_applied && - !UISettings::values.reset_to_defaults) { - // Runs if the user hit Cancel or closed the window, and did not ever press the Apply button - // or `Reset to Defaults` button - return; - } else if (result == QDialog::Accepted) { - // Only apply new changes if user hit Okay - // This is here to avoid applying changes if the user hit Apply, made some changes, then hit - // Cancel - configure_dialog.ApplyConfiguration(); - } else if (UISettings::values.reset_to_defaults) { - LOG_INFO(Frontend, "Resetting all settings to defaults"); - if (!Common::FS::RemoveFile(config->GetConfigFilePath())) { - LOG_WARNING(Frontend, "Failed to remove configuration file"); - } - if (!Common::FS::RemoveDirContentsRecursively( - Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir) / "custom")) { - LOG_WARNING(Frontend, "Failed to remove custom configuration files"); - } - if (!Common::FS::RemoveDirRecursively( - Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list")) { - LOG_WARNING(Frontend, "Failed to remove game metadata cache files"); - } - - // Explicitly save the game directories, since reinitializing config does not explicitly do - // so. - QVector old_game_dirs = std::move(UISettings::values.game_dirs); - QVector old_favorited_ids = std::move(UISettings::values.favorited_ids); - - Settings::values.disabled_addons.clear(); - - config = std::make_unique(); - UISettings::values.reset_to_defaults = false; - - UISettings::values.game_dirs = std::move(old_game_dirs); - UISettings::values.favorited_ids = std::move(old_favorited_ids); - - InitializeRecentFileMenuActions(); - - SetDefaultUIGeometry(); - RestoreUIState(); - } - InitializeHotkeys(); - - if (UISettings::values.theme != old_theme) { - UpdateUITheme(); - } - if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { - SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); - } -#ifdef __linux__ - if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { - SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); - } -#endif -#ifdef __unix__ - if (Settings::values.gui_force_x11.GetValue() != old_force_x11) { - GraphicsBackend::SetForceX11(Settings::values.gui_force_x11.GetValue()); - } -#endif - - if (!multiplayer_state->IsHostingPublicRoom()) { - multiplayer_state->UpdateCredentials(); - } - - emit UpdateThemedIcons(); - - const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); - if (reload || Settings::values.language_index.GetValue() != old_language_index) { - game_list->PopulateAsync(UISettings::values.game_dirs); - } - - UISettings::values.configuration_applied = false; - - config->SaveAllValues(); - - if ((UISettings::values.hide_mouse || Settings::values.mouse_panning) && emulation_running) { - render_window->installEventFilter(render_window); - render_window->setAttribute(Qt::WA_Hover, true); - } else { - render_window->removeEventFilter(render_window); - render_window->setAttribute(Qt::WA_Hover, false); - } - - if (UISettings::values.hide_mouse) { - mouse_hide_timer.start(); - } - - // Restart camera config - if (emulation_running) { - render_window->FinalizeCamera(); - render_window->InitializeCamera(); - } - - if (!UISettings::values.has_broken_vulkan) { - renderer_status_button->setEnabled(!emulation_running); - } - - UpdateStatusButtons(); - controller_dialog->refreshConfiguration(); - QtCommon::system->ApplySettings(); -} - -void GMainWindow::OnConfigureTas() { - ConfigureTasDialog dialog(this); - const auto result = dialog.exec(); - - if (result != QDialog::Accepted && !UISettings::values.configuration_applied) { - Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn()); - return; - } else if (result == QDialog::Accepted) { - dialog.ApplyConfiguration(); - OnSaveConfig(); - } -} - -void GMainWindow::OnTasStartStop() { - if (!emulation_running) { - return; - } - - // Disable system buttons to prevent TAS from executing a hotkey - auto* controller = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); - controller->ResetSystemButtons(); - - input_subsystem->GetTas()->StartStop(); - OnTasStateChanged(); -} - -void GMainWindow::OnTasRecord() { - if (!emulation_running) { - return; - } - if (is_tas_recording_dialog_active) { - return; - } - - // Disable system buttons to prevent TAS from recording a hotkey - auto* controller = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); - controller->ResetSystemButtons(); - - const bool is_recording = input_subsystem->GetTas()->Record(); - if (!is_recording) { - is_tas_recording_dialog_active = true; - - bool answer = question(this, tr("TAS Recording"), tr("Overwrite file of player 1?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - - input_subsystem->GetTas()->SaveRecording(answer); - is_tas_recording_dialog_active = false; - } - OnTasStateChanged(); -} - -void GMainWindow::OnTasReset() { - input_subsystem->GetTas()->Reset(); -} - -void GMainWindow::OnToggleDockedMode() { - const bool is_docked = Settings::IsDockedMode(); - auto* player_1 = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); - auto* handheld = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld); - - if (!is_docked && handheld->IsConnected()) { - QMessageBox::warning(this, tr("Invalid config detected"), - tr("Handheld controller can't be used on docked mode. Pro " - "controller will be selected.")); - handheld->Disconnect(); - player_1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); - player_1->Connect(); - controller_dialog->refreshConfiguration(); - } - - Settings::values.use_docked_mode.SetValue(is_docked ? Settings::ConsoleMode::Handheld - : Settings::ConsoleMode::Docked); - UpdateDockedButton(); - OnDockedModeChanged(is_docked, !is_docked, *QtCommon::system); -} - -void GMainWindow::OnToggleGpuAccuracy() { - switch (Settings::values.gpu_accuracy.GetValue()) { - case Settings::GpuAccuracy::High: { - Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::Normal); - break; - } - case Settings::GpuAccuracy::Normal: - case Settings::GpuAccuracy::Extreme: - default: { - Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::High); - break; - } - } - - QtCommon::system->ApplySettings(); - UpdateGPUAccuracyButton(); -} - -void GMainWindow::OnMute() { - Settings::values.audio_muted = !Settings::values.audio_muted; - UpdateVolumeUI(); -} - -void GMainWindow::OnDecreaseVolume() { - Settings::values.audio_muted = false; - const auto current_volume = static_cast(Settings::values.volume.GetValue()); - int step = 5; - if (current_volume <= 30) { - step = 2; - } - if (current_volume <= 6) { - step = 1; - } - Settings::values.volume.SetValue((std::max)(current_volume - step, 0)); - UpdateVolumeUI(); -} - -void GMainWindow::OnIncreaseVolume() { - Settings::values.audio_muted = false; - const auto current_volume = static_cast(Settings::values.volume.GetValue()); - int step = 5; - if (current_volume < 30) { - step = 2; - } - if (current_volume < 6) { - step = 1; - } - Settings::values.volume.SetValue(current_volume + step); - UpdateVolumeUI(); -} - -void GMainWindow::OnToggleAdaptingFilter() { - auto filter = Settings::values.scaling_filter.GetValue(); - filter = static_cast(static_cast(filter) + 1); - if (filter == Settings::ScalingFilter::MaxEnum) { - filter = Settings::ScalingFilter::NearestNeighbor; - } - Settings::values.scaling_filter.SetValue(filter); - filter_status_button->setChecked(true); - UpdateFilterText(); -} - -void GMainWindow::OnToggleGraphicsAPI() { - auto api = Settings::values.renderer_backend.GetValue(); - if (api != Settings::RendererBackend::Vulkan) { - api = Settings::RendererBackend::Vulkan; - } else { -#ifdef HAS_OPENGL - api = Settings::RendererBackend::OpenGL; -#else - api = Settings::RendererBackend::Null; -#endif - } - Settings::values.renderer_backend.SetValue(api); - renderer_status_button->setChecked(api == Settings::RendererBackend::Vulkan); - UpdateAPIText(); -} - -void GMainWindow::OnConfigurePerGame() { - const u64 title_id = QtCommon::system->GetApplicationProcessProgramID(); - OpenPerGameConfiguration(title_id, current_game_path.toStdString()); -} - -void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file_name) { - const auto v_file = Core::GetGameFileFromPath(QtCommon::vfs, file_name); - - Settings::SetConfiguringGlobal(false); - ConfigurePerGame dialog(this, title_id, file_name, vk_device_records, *QtCommon::system); - dialog.LoadFromFile(v_file); - const auto result = dialog.exec(); - - if (result != QDialog::Accepted && !UISettings::values.configuration_applied) { - Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn()); - return; - } else if (result == QDialog::Accepted) { - dialog.ApplyConfiguration(); - } - - const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); - if (reload) { - game_list->PopulateAsync(UISettings::values.game_dirs); - } - - // Do not cause the global config to write local settings into the config file - const bool is_powered_on = QtCommon::system->IsPoweredOn(); - Settings::RestoreGlobalState(is_powered_on); - QtCommon::system->HIDCore().ReloadInputDevices(); - - UISettings::values.configuration_applied = false; - - if (!is_powered_on) { - config->SaveAllValues(); - } -} - -void GMainWindow::OnLoadAmiibo() { - if (emu_thread == nullptr || !emu_thread->IsRunning()) { - return; - } - if (is_amiibo_file_select_active) { - return; - } - - auto* virtual_amiibo = input_subsystem->GetVirtualAmiibo(); - - // Remove amiibo if one is connected - if (virtual_amiibo->GetCurrentState() == InputCommon::VirtualAmiibo::State::TagNearby) { - virtual_amiibo->CloseAmiibo(); - QMessageBox::warning(this, tr("Amiibo"), tr("The current amiibo has been removed")); - return; - } - - if (virtual_amiibo->GetCurrentState() != InputCommon::VirtualAmiibo::State::WaitingForAmiibo) { - QMessageBox::warning(this, tr("Error"), tr("The current game is not looking for amiibos")); - return; - } - - is_amiibo_file_select_active = true; - const QString extensions{QStringLiteral("*.bin")}; - const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); - const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); - is_amiibo_file_select_active = false; - - if (filename.isEmpty()) { - return; - } - - LoadAmiibo(filename); -} - -// TODO(crueter): does this need to be ported to QML? -bool GMainWindow::question(QWidget* parent, const QString& title, const QString& text, - QMessageBox::StandardButtons buttons, - QMessageBox::StandardButton defaultButton) { - QMessageBox* box_dialog = new QMessageBox(parent); - box_dialog->setWindowTitle(title); - box_dialog->setText(text); - box_dialog->setStandardButtons(buttons); - box_dialog->setDefaultButton(defaultButton); - - ControllerNavigation* controller_navigation = - new ControllerNavigation(QtCommon::system->HIDCore(), box_dialog); - connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, - [box_dialog](Qt::Key key) { - QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); - QCoreApplication::postEvent(box_dialog, event); - }); - int res = box_dialog->exec(); - - controller_navigation->UnloadController(); - return res == QMessageBox::Yes; -} - -void GMainWindow::LoadAmiibo(const QString& filename) { - auto* virtual_amiibo = input_subsystem->GetVirtualAmiibo(); - const QString title = tr("Error loading Amiibo data"); - // Remove amiibo if one is connected - if (virtual_amiibo->GetCurrentState() == InputCommon::VirtualAmiibo::State::TagNearby) { - virtual_amiibo->CloseAmiibo(); - QMessageBox::warning(this, tr("Amiibo"), tr("The current amiibo has been removed")); - return; - } - - switch (virtual_amiibo->LoadAmiibo(filename.toStdString())) { - case InputCommon::VirtualAmiibo::Info::NotAnAmiibo: - QMessageBox::warning(this, title, tr("The selected file is not a valid amiibo")); - break; - case InputCommon::VirtualAmiibo::Info::UnableToLoad: - QMessageBox::warning(this, title, tr("The selected file is already on use")); - break; - case InputCommon::VirtualAmiibo::Info::WrongDeviceState: - QMessageBox::warning(this, title, tr("The current game is not looking for amiibos")); - break; - case InputCommon::VirtualAmiibo::Info::Unknown: - QMessageBox::warning(this, title, tr("An unknown error occurred")); - break; - default: - break; - } -} - -void GMainWindow::OnOpenRootDataFolder() { - QtCommon::Game::OpenRootDataFolder(); -} - -void GMainWindow::OnOpenNANDFolder() -{ - QtCommon::Game::OpenNANDFolder(); -} - -void GMainWindow::OnOpenSDMCFolder() -{ - QtCommon::Game::OpenSDMCFolder(); -} - -void GMainWindow::OnOpenModFolder() -{ - QtCommon::Game::OpenModFolder(); -} - -void GMainWindow::OnOpenLogFolder() -{ - QtCommon::Game::OpenLogFolder(); -} - -void GMainWindow::OnVerifyInstalledContents() { - QtCommon::Content::VerifyInstalledContents(); -} - -void GMainWindow::InstallFirmware(const QString& location, bool recursive) { - QtCommon::Content::InstallFirmware(location, recursive); - OnCheckFirmwareDecryption(); -} - -void GMainWindow::OnInstallFirmware() { - // Don't do this while emulation is running, that'd probably be a bad idea. - if (emu_thread != nullptr && emu_thread->IsRunning()) { - return; - } - - // Check for installed keys, error out, suggest restart? - if (!ContentManager::AreKeysPresent()) { - QMessageBox::information( - this, tr("Keys not installed"), - tr("Install decryption keys and restart Eden before attempting to install firmware.")); - return; - } - - const QString firmware_source_location = QFileDialog::getExistingDirectory( - this, tr("Select Dumped Firmware Source Location"), {}, QFileDialog::ShowDirsOnly); - if (firmware_source_location.isEmpty()) { - return; - } - - InstallFirmware(firmware_source_location); -} - -void GMainWindow::OnInstallFirmwareFromZIP() { - // Don't do this while emulation is running, that'd probably be a bad idea. - if (emu_thread != nullptr && emu_thread->IsRunning()) { - return; - } - - // Check for installed keys, error out, suggest restart? - if (!ContentManager::AreKeysPresent()) { - QMessageBox::information( - this, tr("Keys not installed"), - tr("Install decryption keys and restart Eden before attempting to install firmware.")); - return; - } - - const QString firmware_zip_location = QFileDialog::getOpenFileName( - this, tr("Select Dumped Firmware ZIP"), {}, tr("Zipped Archives (*.zip)")); - if (firmware_zip_location.isEmpty()) { - return; - } - - const QString qCacheDir = QtCommon::Content::UnzipFirmwareToTmp(firmware_zip_location); - - // In this case, it has to be done recursively, since sometimes people - // will pack it into a subdirectory after dumping - if (!qCacheDir.isEmpty()) { - InstallFirmware(qCacheDir, true); - std::error_code ec; - std::filesystem::remove_all(std::filesystem::temp_directory_path() / "eden" / "firmware", ec); - - if (ec) { - QMessageBox::warning(this, tr("Firmware cleanup failed"), - tr("Failed to clean up extracted firmware cache.\n" - "Check write permissions in the system temp directory and try " - "again.\nOS reported error: %1") - .arg(QString::fromStdString(ec.message()))); - } - } -} - -void GMainWindow::OnInstallDecryptionKeys() { - // Don't do this while emulation is running. - if (emu_thread != nullptr && emu_thread->IsRunning()) { - return; - } - - QtCommon::Content::InstallKeys(); - - game_list->PopulateAsync(UISettings::values.game_dirs); - OnCheckFirmwareDecryption(); -} - -void GMainWindow::OnAbout() { - AboutDialog aboutDialog(this); - aboutDialog.exec(); -} - -void GMainWindow::OnEdenDependencies() { - DepsDialog depsDialog(this); - depsDialog.exec(); -} - -void GMainWindow::OnDataDialog() { - DataDialog dataDialog(this); - dataDialog.exec(); - - // refresh stuff in case it was cleared - OnGameListRefresh(); - -} - -void GMainWindow::OnToggleFilterBar() { - game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); - if (ui->action_Show_Filter_Bar->isChecked()) { - game_list->SetFilterFocus(); - } else { - game_list->ClearFilter(); - } -} - -void GMainWindow::OnToggleStatusBar() { - statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); -} - -void GMainWindow::OnGameListRefresh() -{ - // Resets metadata cache and reloads - QtCommon::Game::ResetMetadata(false); - game_list->RefreshGameDirectory(); - SetFirmwareVersion(); -} - -void GMainWindow::OnAlbum() { - constexpr u64 AlbumId = static_cast(Service::AM::AppletProgramId::PhotoViewer); - auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); - if (!bis_system) { - QMessageBox::warning(this, tr("No firmware available"), - tr("Please install firmware to use the Album applet.")); - return; - } - - auto album_nca = bis_system->GetEntry(AlbumId, FileSys::ContentRecordType::Program); - if (!album_nca) { - QMessageBox::warning(this, tr("Album Applet"), - tr("Album applet is not available. Please reinstall firmware.")); - return; - } - - QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::PhotoViewer); - - const auto filename = QString::fromStdString(album_nca->GetFullPath()); - UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); - BootGame(filename, LibraryAppletParameters(AlbumId, Service::AM::AppletId::PhotoViewer)); -} - -void GMainWindow::OnCabinet(Service::NFP::CabinetMode mode) { - constexpr u64 CabinetId = static_cast(Service::AM::AppletProgramId::Cabinet); - auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); - if (!bis_system) { - QMessageBox::warning(this, tr("No firmware available"), - tr("Please install firmware to use the Cabinet applet.")); - return; - } - - auto cabinet_nca = bis_system->GetEntry(CabinetId, FileSys::ContentRecordType::Program); - if (!cabinet_nca) { - QMessageBox::warning(this, tr("Cabinet Applet"), - tr("Cabinet applet is not available. Please reinstall firmware.")); - return; - } - - QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::Cabinet); - QtCommon::system->GetFrontendAppletHolder().SetCabinetMode(mode); - - const auto filename = QString::fromStdString(cabinet_nca->GetFullPath()); - UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); - BootGame(filename, LibraryAppletParameters(CabinetId, Service::AM::AppletId::Cabinet)); -} - -void GMainWindow::OnMiiEdit() { - constexpr u64 MiiEditId = static_cast(Service::AM::AppletProgramId::MiiEdit); - auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); - if (!bis_system) { - QMessageBox::warning(this, tr("No firmware available"), - tr("Please install firmware to use the Mii editor.")); - return; - } - - auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program); - if (!mii_applet_nca) { - QMessageBox::warning(this, tr("Mii Edit Applet"), - tr("Mii editor is not available. Please reinstall firmware.")); - return; - } - - QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::MiiEdit); - - const auto filename = QString::fromStdString((mii_applet_nca->GetFullPath())); - UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); - BootGame(filename, LibraryAppletParameters(MiiEditId, Service::AM::AppletId::MiiEdit)); -} - -void GMainWindow::OnOpenControllerMenu() { - constexpr u64 ControllerAppletId = static_cast(Service::AM::AppletProgramId::Controller); - auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); - if (!bis_system) { - QMessageBox::warning(this, tr("No firmware available"), - tr("Please install firmware to use the Controller Menu.")); - return; - } - - auto controller_applet_nca = - bis_system->GetEntry(ControllerAppletId, FileSys::ContentRecordType::Program); - if (!controller_applet_nca) { - QMessageBox::warning(this, tr("Controller Applet"), - tr("Controller Menu is not available. Please reinstall firmware.")); - return; - } - - QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::Controller); - - const auto filename = QString::fromStdString((controller_applet_nca->GetFullPath())); - UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); - BootGame(filename, - LibraryAppletParameters(ControllerAppletId, Service::AM::AppletId::Controller)); -} - -void GMainWindow::OnHomeMenu() { - auto result = FirmwareManager::VerifyFirmware(*QtCommon::system.get()); - - switch (result) { - case FirmwareManager::ErrorFirmwareMissing: - QMessageBox::warning(this, tr("No firmware available"), - tr("Please install firmware to use the Home Menu.")); - return; - case FirmwareManager::ErrorFirmwareCorrupted: - QMessageBox::warning(this, tr("Firmware Corrupted"), - tr(FirmwareManager::GetFirmwareCheckString(result))); - return; - case FirmwareManager::ErrorFirmwareTooNew: { - if (!UISettings::values.show_fw_warning.GetValue()) break; - - QMessageBox box(QMessageBox::Warning, - tr("Firmware Too New"), - tr(FirmwareManager::GetFirmwareCheckString(result)) + tr("\nContinue anyways?"), - QMessageBox::Yes | QMessageBox::No, - this); - - QCheckBox *checkbox = new QCheckBox(tr("Don't show again")); - box.setCheckBox(checkbox); - - int button = box.exec(); - if (checkbox->isChecked()) { - UISettings::values.show_fw_warning.SetValue(false); - } - - if (button == static_cast(QMessageBox::No)) return; - break; - } default: - break; - } - - constexpr u64 QLaunchId = static_cast(Service::AM::AppletProgramId::QLaunch); - auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); - - auto qlaunch_applet_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program); - if (!qlaunch_applet_nca) { - QMessageBox::warning(this, tr("Home Menu Applet"), - tr("Home Menu is not available. Please reinstall firmware.")); - return; - } - - QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::QLaunch); - - const auto filename = QString::fromStdString((qlaunch_applet_nca->GetFullPath())); - UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); - BootGame(filename, LibraryAppletParameters(QLaunchId, Service::AM::AppletId::QLaunch)); -} - -void GMainWindow::OnInitialSetup() { - constexpr u64 Starter = static_cast(Service::AM::AppletProgramId::Starter); - auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); - if (!bis_system) { - QMessageBox::warning(this, tr("No firmware available"), - tr("Please install firmware to use Starter.")); - return; - } - - auto qlaunch_nca = bis_system->GetEntry(Starter, FileSys::ContentRecordType::Program); - if (!qlaunch_nca) { - QMessageBox::warning(this, tr("Starter Applet"), - tr("Starter is not available. Please reinstall firmware.")); - return; - } - - QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::Starter); - - const auto filename = QString::fromStdString((qlaunch_nca->GetFullPath())); - UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); - BootGame(filename, LibraryAppletParameters(Starter, Service::AM::AppletId::Starter)); -} - -void GMainWindow::OnCreateHomeMenuDesktopShortcut() { - QtCommon::Game::CreateHomeMenuShortcut(QtCommon::Game::ShortcutTarget::Desktop); -} - -void GMainWindow::OnCreateHomeMenuApplicationMenuShortcut() { - QtCommon::Game::CreateHomeMenuShortcut(QtCommon::Game::ShortcutTarget::Applications); -} - -void GMainWindow::OnCaptureScreenshot() { - if (emu_thread == nullptr || !emu_thread->IsRunning()) { - return; - } - - const u64 title_id = QtCommon::system->GetApplicationProcessProgramID(); - const auto screenshot_path = - QString::fromStdString(Common::FS::GetEdenPathString(Common::FS::EdenPath::ScreenshotsDir)); - const auto date = - QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss-zzz")); - QString filename = QStringLiteral("%1/%2_%3.png") - .arg(screenshot_path) - .arg(title_id, 16, 16, QLatin1Char{'0'}) - .arg(date); - - if (!Common::FS::CreateDir(screenshot_path.toStdString())) { - return; - } - -#ifdef _WIN32 - if (UISettings::values.enable_screenshot_save_as) { - OnPauseGame(); - filename = QFileDialog::getSaveFileName(this, tr("Capture Screenshot"), filename, - tr("PNG Image (*.png)")); - OnStartGame(); - if (filename.isEmpty()) { - return; - } - } -#endif - render_window->CaptureScreenshot(filename); -} - -// TODO: Written 2020-10-01: Remove per-game config migration code when it is irrelevant -void GMainWindow::MigrateConfigFiles() { - const auto config_dir_fs_path = Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir); - const QDir config_dir = - QDir(QString::fromStdString(Common::FS::PathToUTF8String(config_dir_fs_path))); - const QStringList config_dir_list = config_dir.entryList(QStringList(QStringLiteral("*.ini"))); - - if (!Common::FS::CreateDirs(config_dir_fs_path / "custom")) { - LOG_ERROR(Frontend, "Failed to create new config file directory"); - } - - for (auto it = config_dir_list.constBegin(); it != config_dir_list.constEnd(); ++it) { - const auto filename = it->toStdString(); - if (filename.find_first_not_of("0123456789abcdefACBDEF", 0) < 16) { - continue; - } - const auto origin = config_dir_fs_path / filename; - const auto destination = config_dir_fs_path / "custom" / filename; - LOG_INFO(Frontend, "Migrating config file from {} to {}", origin.string(), - destination.string()); - if (!Common::FS::RenameFile(origin, destination)) { - // Delete the old config file if one already exists in the new location. - Common::FS::RemoveFile(origin); - } - } -} - -#ifdef ENABLE_UPDATE_CHECKER -void GMainWindow::OnEmulatorUpdateAvailable() { - QString version_string = update_future.result(); - if (version_string.isEmpty()) - return; - - QMessageBox update_prompt(this); - update_prompt.setWindowTitle(tr("Update Available")); - update_prompt.setIcon(QMessageBox::Information); - update_prompt.addButton(QMessageBox::Yes); - update_prompt.addButton(QMessageBox::Ignore); - update_prompt.setText( - tr("Download the %1 update?").arg(version_string)); - update_prompt.exec(); - if (update_prompt.button(QMessageBox::Yes) == update_prompt.clickedButton()) { - auto const full_url = fmt::format("{}/{}/releases/tag/", - std::string{Common::g_build_auto_update_website}, - std::string{Common::g_build_auto_update_repo} - ); - QDesktopServices::openUrl(QUrl(QString::fromStdString(full_url) + version_string)); - } -} -#endif - -void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_view title_version, std::string_view gpu_vendor) { - static const std::string build_id = std::string{Common::g_build_id}; - static const std::string yuzu_title = fmt::format("{} | {} | {}", - std::string{Common::g_build_name}, - std::string{Common::g_build_version}, - std::string{Common::g_compiler_id} - ); - - const auto override_title = - fmt::format(fmt::runtime(std::string(Common::g_title_bar_format_idle)), build_id); - const auto window_title = override_title.empty() ? yuzu_title : override_title; - - if (title_name.empty()) { - setWindowTitle(QString::fromStdString(window_title)); - } else { - const auto run_title = [window_title, title_name, title_version, gpu_vendor]() { - if (title_version.empty()) { - return fmt::format("{} | {} | {}", window_title, title_name, gpu_vendor); - } - return fmt::format("{} | {} | {} | {}", window_title, title_name, title_version, - gpu_vendor); - }(); - setWindowTitle(QString::fromStdString(run_title)); - } -} - -std::string GMainWindow::CreateTASFramesString( - std::array frames) const { - std::string string = ""; - size_t maxPlayerIndex = 0; - for (size_t i = 0; i < frames.size(); i++) { - if (frames[i] != 0) { - if (maxPlayerIndex != 0) - string += ", "; - while (maxPlayerIndex++ != i) - string += "0, "; - string += std::to_string(frames[i]); - } - } - return string; -} - -QString GMainWindow::GetTasStateDescription() const { - auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus(); - std::string tas_frames_string = CreateTASFramesString(total_tas_frames); - switch (tas_status) { - case InputCommon::TasInput::TasState::Running: - return tr("TAS state: Running %1/%2") - .arg(current_tas_frame) - .arg(QString::fromStdString(tas_frames_string)); - case InputCommon::TasInput::TasState::Recording: - return tr("TAS state: Recording %1").arg(total_tas_frames[0]); - case InputCommon::TasInput::TasState::Stopped: - return tr("TAS state: Idle %1/%2") - .arg(current_tas_frame) - .arg(QString::fromStdString(tas_frames_string)); - default: - return tr("TAS State: Invalid"); - } -} - -void GMainWindow::OnTasStateChanged() { - bool is_running = false; - bool is_recording = false; - if (emulation_running) { - const InputCommon::TasInput::TasState tas_status = - std::get<0>(input_subsystem->GetTas()->GetStatus()); - is_running = tas_status == InputCommon::TasInput::TasState::Running; - is_recording = tas_status == InputCommon::TasInput::TasState::Recording; - } - - ui->action_TAS_Start->setText(is_running ? tr("&Stop Running") : tr("&Start")); - ui->action_TAS_Record->setText(is_recording ? tr("Stop R&ecording") : tr("R&ecord")); - - ui->action_TAS_Start->setEnabled(emulation_running); - ui->action_TAS_Record->setEnabled(emulation_running); - ui->action_TAS_Reset->setEnabled(emulation_running); -} - -void GMainWindow::UpdateStatusBar() { - if (emu_thread == nullptr || !QtCommon::system->IsPoweredOn()) { - status_bar_update_timer.stop(); - return; - } - - if (Settings::values.tas_enable) { - tas_label->setText(GetTasStateDescription()); - } else { - tas_label->clear(); - } - - auto results = QtCommon::system->GetAndResetPerfStats(); - auto& shader_notify = QtCommon::system->GPU().ShaderNotify(); - const int shaders_building = shader_notify.ShadersBuilding(); - - if (shaders_building > 0) { - shader_building_label->setText(tr("Building: %n shader(s)", "", shaders_building)); - shader_building_label->setVisible(true); - } else { - shader_building_label->setVisible(false); - } - - const auto res_info = Settings::values.resolution_info; - const auto res_scale = res_info.up_factor; - res_scale_label->setText( - tr("Scale: %1x", "%1 is the resolution scaling factor").arg(res_scale)); - - if (Settings::values.use_speed_limit.GetValue()) { - emu_speed_label->setText(tr("Speed: %1% / %2%") - .arg(results.emulation_speed * 100.0, 0, 'f', 0) - .arg(Settings::values.speed_limit.GetValue())); - } else { - emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); - } - - game_fps_label->setText( - tr("Game: %1 FPS").arg(std::round(results.average_game_fps), 0, 'f', 0) + - tr(Settings::values.use_speed_limit ? "" : " (Unlocked)")); - - emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); - - res_scale_label->setVisible(true); - emu_speed_label->setVisible(!Settings::values.use_multi_core.GetValue()); - game_fps_label->setVisible(true); - emu_frametime_label->setVisible(true); - firmware_label->setVisible(false); -} - -void GMainWindow::UpdateGPUAccuracyButton() { - const auto gpu_accuracy = Settings::values.gpu_accuracy.GetValue(); - const auto gpu_accuracy_text = - ConfigurationShared::gpu_accuracy_texts_map.find(gpu_accuracy)->second; - gpu_accuracy_button->setText(gpu_accuracy_text.toUpper()); - gpu_accuracy_button->setChecked(gpu_accuracy != Settings::GpuAccuracy::Normal); -} - -void GMainWindow::UpdateDockedButton() { - const auto console_mode = Settings::values.use_docked_mode.GetValue(); - dock_status_button->setChecked(Settings::IsDockedMode()); - dock_status_button->setText( - ConfigurationShared::use_docked_mode_texts_map.find(console_mode)->second.toUpper()); -} - -void GMainWindow::UpdateAPIText() { - const auto api = Settings::values.renderer_backend.GetValue(); - const auto renderer_status_text = - ConfigurationShared::renderer_backend_texts_map.find(api)->second; - renderer_status_button->setText( - api == Settings::RendererBackend::OpenGL - ? tr("%1 %2").arg(renderer_status_text.toUpper(), - ConfigurationShared::shader_backend_texts_map - .find(Settings::values.shader_backend.GetValue()) - ->second) - : renderer_status_text.toUpper()); -} - -void GMainWindow::UpdateFilterText() { - const auto filter = Settings::values.scaling_filter.GetValue(); - const auto filter_text = ConfigurationShared::scaling_filter_texts_map.find(filter)->second; - filter_status_button->setText(filter == Settings::ScalingFilter::Fsr ? tr("FSR") - : filter_text.toUpper()); -} - -void GMainWindow::UpdateAAText() { - const auto aa_mode = Settings::values.anti_aliasing.GetValue(); - const auto aa_text = ConfigurationShared::anti_aliasing_texts_map.find(aa_mode)->second; - aa_status_button->setText(aa_mode == Settings::AntiAliasing::None - ? QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "NO AA")) - : aa_text.toUpper()); -} - -void GMainWindow::UpdateVolumeUI() { - const auto volume_value = static_cast(Settings::values.volume.GetValue()); - volume_slider->setValue(volume_value); - if (Settings::values.audio_muted) { - volume_button->setChecked(false); - volume_button->setText(tr("VOLUME: MUTE")); - } else { - volume_button->setChecked(true); - volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); - } -} - -void GMainWindow::UpdateStatusButtons() { - renderer_status_button->setChecked(Settings::values.renderer_backend.GetValue() == - Settings::RendererBackend::Vulkan); - UpdateAPIText(); - UpdateGPUAccuracyButton(); - UpdateDockedButton(); - UpdateFilterText(); - UpdateAAText(); - UpdateVolumeUI(); -} - -void GMainWindow::UpdateUISettings() { - if (!ui->action_Fullscreen->isChecked()) { - UISettings::values.geometry = saveGeometry(); - UISettings::values.renderwindow_geometry = render_window->saveGeometry(); - } - UISettings::values.state = saveState(); - UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); - UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); - UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); - UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); - UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); - UISettings::values.first_start = false; -} - -void GMainWindow::UpdateInputDrivers() { - if (!input_subsystem) { - return; - } - input_subsystem->PumpEvents(); -} - -void GMainWindow::HideMouseCursor() { - if (emu_thread == nullptr && UISettings::values.hide_mouse) { - mouse_hide_timer.stop(); - ShowMouseCursor(); - return; - } - render_window->setCursor(QCursor(Qt::BlankCursor)); -} - -void GMainWindow::ShowMouseCursor() { - render_window->unsetCursor(); - if (emu_thread != nullptr && UISettings::values.hide_mouse) { - mouse_hide_timer.start(); - } -} - -void GMainWindow::OnMouseActivity() { - if (!Settings::values.mouse_panning) { - ShowMouseCursor(); - } -} - -void GMainWindow::OnCheckFirmwareDecryption() { - if (!ContentManager::AreKeysPresent()) { - QMessageBox::warning(this, tr("Derivation Components Missing"), - tr("Encryption keys are missing.")); - } - SetFirmwareVersion(); - UpdateMenuState(); -} - -#ifdef __unix__ -void GMainWindow::OnCheckGraphicsBackend() { - const QString platformName = QGuiApplication::platformName(); - const QByteArray qtPlatform = qgetenv("QT_QPA_PLATFORM"); - - if (platformName == QStringLiteral("xcb") || qtPlatform == "xcb") - return; - - const bool isWayland = platformName.startsWith(QStringLiteral("wayland"), Qt::CaseInsensitive) || qtPlatform.startsWith("wayland"); - if (!isWayland) - return; - - const bool currently_hidden = Settings::values.gui_hide_backend_warning.GetValue(); - if (currently_hidden) - return; - - QMessageBox msgbox(this); - msgbox.setWindowTitle(tr("Wayland Detected!")); - msgbox.setText(tr("Wayland is known to have significant performance issues and mysterious bugs.\n" - "It's recommended to use X11 instead.\n\n" - "Would you like to force it for future launches?")); - msgbox.setIcon(QMessageBox::Warning); - - QPushButton* okButton = msgbox.addButton(tr("Use X11"), QMessageBox::AcceptRole); - msgbox.addButton(tr("Continue with Wayland"), QMessageBox::RejectRole); - msgbox.setDefaultButton(okButton); - - QCheckBox* cb = new QCheckBox(tr("Don't show again"), &msgbox); - cb->setChecked(currently_hidden); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - const bool hide = cb->isChecked(); - if (hide != currently_hidden) { - Settings::values.gui_hide_backend_warning.SetValue(hide); - } - - if (msgbox.clickedButton() == okButton) { - Settings::values.gui_force_x11.SetValue(true); - GraphicsBackend::SetForceX11(true); - QMessageBox::information(this, - tr("Restart Required"), - tr("Restart Eden to apply the X11 backend.")); - } -} -#endif - -bool GMainWindow::CheckFirmwarePresence() { - return FirmwareManager::CheckFirmwarePresence(*QtCommon::system.get()); -} - -void GMainWindow::SetFirmwareVersion() { - const auto pair = FirmwareManager::GetFirmwareVersion(*QtCommon::system.get()); - const auto firmware_data = pair.first; - const auto result = pair.second; - - if (result.IsError() || !CheckFirmwarePresence()) { - LOG_INFO(Frontend, "Installed firmware: No firmware available"); - ui->menu_Applets->setEnabled(false); - ui->menu_Create_Shortcuts->setEnabled(false); - firmware_label->setVisible(false); - return; - } - - firmware_label->setVisible(true); - ui->menu_Applets->setEnabled(true); - ui->menu_Create_Shortcuts->setEnabled(true); - - const std::string display_version(firmware_data.display_version.data()); - const std::string display_title(firmware_data.display_title.data()); - - LOG_INFO(Frontend, "Installed firmware: {}", display_version); - - firmware_label->setText(QString::fromStdString(display_version)); - firmware_label->setToolTip(QString::fromStdString(display_title)); -} - -bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed, u64 program_id, - u64* selected_title_id, u8* selected_content_record_type) { - using ContentInfo = std::tuple; - boost::container::flat_set available_title_ids; - - const auto RetrieveEntries = [&](FileSys::TitleType title_type, - FileSys::ContentRecordType record_type) { - const auto entries = installed.ListEntriesFilter(title_type, record_type); - for (const auto& entry : entries) { - if (FileSys::GetBaseTitleID(entry.title_id) == program_id && - installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success) { - available_title_ids.insert({entry.title_id, title_type, record_type}); - } - } - }; - - RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::Program); - RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::HtmlDocument); - RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::LegalInformation); - RetrieveEntries(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); - - if (available_title_ids.empty()) { - return false; - } - - size_t title_index = 0; - - if (available_title_ids.size() > 1) { - QStringList list; - for (auto& [title_id, title_type, record_type] : available_title_ids) { - const auto hex_title_id = QString::fromStdString(fmt::format("{:X}", title_id)); - if (record_type == FileSys::ContentRecordType::Program) { - list.push_back(QStringLiteral("Program [%1]").arg(hex_title_id)); - } else if (record_type == FileSys::ContentRecordType::HtmlDocument) { - list.push_back(QStringLiteral("HTML document [%1]").arg(hex_title_id)); - } else if (record_type == FileSys::ContentRecordType::LegalInformation) { - list.push_back(QStringLiteral("Legal information [%1]").arg(hex_title_id)); - } else { - list.push_back( - QStringLiteral("DLC %1 [%2]").arg(title_id & 0x7FF).arg(hex_title_id)); - } - } - - bool ok; - const auto res = QInputDialog::getItem( - this, tr("Select RomFS Dump Target"), - tr("Please select which RomFS you would like to dump."), list, 0, false, &ok); - if (!ok) { - return false; - } - - title_index = list.indexOf(res); - } - - const auto& [title_id, title_type, record_type] = *available_title_ids.nth(title_index); - *selected_title_id = title_id; - *selected_content_record_type = static_cast(record_type); - return true; -} - -bool GMainWindow::ConfirmClose() { - if (emu_thread == nullptr || - UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Never) { - return true; - } - if (!QtCommon::system->GetExitLocked() && - UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Based_On_Game) { - return true; - } - const auto text = tr("Are you sure you want to close Eden?"); - return question(this, tr("Eden"), text); -} - -void GMainWindow::closeEvent(QCloseEvent* event) { - if (!ConfirmClose()) { - event->ignore(); - return; - } - - UpdateUISettings(); - game_list->SaveInterfaceLayout(); - UISettings::SaveWindowState(); - hotkey_registry.SaveHotkeys(); - - // Unload controllers early - controller_dialog->UnloadController(); - game_list->UnloadController(); - - // Shutdown session if the emu thread is active... - if (emu_thread != nullptr) { - ShutdownGame(); - } - - render_window->close(); - multiplayer_state->Close(); - QtCommon::system->HIDCore().UnloadInputDevices(); - Network::Shutdown(); - - QWidget::closeEvent(event); -} - -static bool IsSingleFileDropEvent(const QMimeData* mime) { - return mime->hasUrls() && mime->urls().length() == 1; -} - -void GMainWindow::AcceptDropEvent(QDropEvent* event) { - if (IsSingleFileDropEvent(event->mimeData())) { - event->setDropAction(Qt::DropAction::LinkAction); - event->accept(); - } -} - -bool GMainWindow::DropAction(QDropEvent* event) { - if (!IsSingleFileDropEvent(event->mimeData())) { - return false; - } - - const QMimeData* mime_data = event->mimeData(); - const QString& filename = mime_data->urls().at(0).toLocalFile(); - - if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { - // Amiibo - LoadAmiibo(filename); - } else { - // Game - if (ConfirmChangeGame()) { - BootGame(filename, ApplicationAppletParameters()); - } - } - return true; -} - -void GMainWindow::dropEvent(QDropEvent* event) { - DropAction(event); -} - -void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { - AcceptDropEvent(event); -} - -void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { - AcceptDropEvent(event); -} - -bool GMainWindow::ConfirmChangeGame() { - if (emu_thread == nullptr) - return true; - - // Use custom question to link controller navigation - return question( - this, tr("Eden"), - tr("Are you sure you want to stop the emulation? Any unsaved progress will be lost."), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); -} - -bool GMainWindow::ConfirmForceLockedExit() { - if (emu_thread == nullptr) { - return true; - } - const auto text = tr("The currently running application has requested Eden to not exit.\n\n" - "Would you like to bypass this and exit anyway?"); - - return question(this, tr("Eden"), text); -} - -void GMainWindow::RequestGameExit() { - if (!QtCommon::system->IsPoweredOn()) { - return; - } - - QtCommon::system->SetExitRequested(true); - QtCommon::system->GetAppletManager().RequestExit(); -} - -void GMainWindow::filterBarSetChecked(bool state) { - ui->action_Show_Filter_Bar->setChecked(state); - emit(OnToggleFilterBar()); -} - -static void AdjustLinkColor() { - QPalette new_pal(qApp->palette()); - if (UISettings::IsDarkTheme()) { - new_pal.setColor(QPalette::Link, QColor(0, 190, 255, 255)); - } else { - new_pal.setColor(QPalette::Link, QColor(0, 140, 200, 255)); - } - if (qApp->palette().color(QPalette::Link) != new_pal.color(QPalette::Link)) { - qApp->setPalette(new_pal); - } -} - -void GMainWindow::UpdateUITheme() { - const QString default_theme = QString::fromUtf8( - UISettings::themes[static_cast(UISettings::default_theme)].second); - QString current_theme = QString::fromStdString(UISettings::values.theme); - - if (current_theme.isEmpty()) { - current_theme = default_theme; - } - -#ifdef _WIN32 - QIcon::setThemeName(current_theme); - AdjustLinkColor(); -#else - if (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")) { - QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme - : startup_icon_theme); - QIcon::setThemeSearchPaths(QStringList(default_theme_paths)); - if (isDarkMode()) { - current_theme = QStringLiteral("default_dark"); - } - } else { - QIcon::setThemeName(current_theme); - QIcon::setThemeSearchPaths(QStringList(QStringLiteral(":/icons"))); - AdjustLinkColor(); - } -#endif - - if (current_theme != default_theme) { - QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; - QFile f(theme_uri); - if (!f.open(QFile::ReadOnly | QFile::Text)) { - LOG_ERROR(Frontend, "Unable to open style \"{}\", fallback to the default theme", - UISettings::values.theme); - current_theme = default_theme; - } - } - - QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; - QFile f(theme_uri); - if (f.open(QFile::ReadOnly | QFile::Text)) { - QTextStream ts(&f); - qApp->setStyleSheet(ts.readAll()); - setStyleSheet(ts.readAll()); - } else { - LOG_ERROR(Frontend, "Unable to set style \"{}\", stylesheet file not found", - UISettings::values.theme); - qApp->setStyleSheet({}); - setStyleSheet({}); - } - -#ifdef _WIN32 - RemoveTitlebarFilter(); - ApplyGlobalDarkTitlebar(UISettings::IsDarkTheme()); -#endif -} - -void GMainWindow::LoadTranslation() { - bool loaded; - - if (UISettings::values.language.GetValue().empty()) { - // If the selected language is empty, use system locale - loaded = translator.load(QLocale(), {}, {}, QStringLiteral(":/languages/")); - } else { - // Otherwise load from the specified file - loaded = translator.load(QString::fromStdString(UISettings::values.language.GetValue()), - QStringLiteral(":/languages/")); - } - - if (loaded) { - qApp->installTranslator(&translator); - } else { - UISettings::values.language = std::string("en"); - } -} - -void GMainWindow::OnLanguageChanged(const QString& locale) { - if (UISettings::values.language.GetValue() != std::string("en")) { - qApp->removeTranslator(&translator); - } - - UISettings::values.language = locale.toStdString(); - LoadTranslation(); - ui->retranslateUi(this); - multiplayer_state->retranslateUi(); - UpdateWindowTitle(); -} - -void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { -#ifdef USE_DISCORD_PRESENCE - if (state) { - discord_rpc = std::make_unique(*QtCommon::system); - } else { - discord_rpc = std::make_unique(); - } -#else - discord_rpc = std::make_unique(); -#endif - discord_rpc->Update(); -} - -#ifdef __linux__ -void GMainWindow::SetGamemodeEnabled(bool state) { - if (emulation_running) { - Common::Linux::SetGamemodeState(state); - } -} -#endif - -void GMainWindow::changeEvent(QEvent* event) { -#ifdef __unix__ - // PaletteChange event appears to only reach so far into the GUI, explicitly asking to - // UpdateUITheme is a decent work around - if (event->type() == QEvent::PaletteChange) { - const QPalette test_palette(qApp->palette()); - const QString current_theme = QString::fromStdString(UISettings::values.theme); - // Keeping eye on QPalette::Window to avoid looping. QPalette::Text might be useful too - static QColor last_window_color; - const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window); - if (last_window_color != window_color && (current_theme == QStringLiteral("default") || - current_theme == QStringLiteral("colorful"))) { - UpdateUITheme(); - } - last_window_color = window_color; - } -#endif // __unix__ - QWidget::changeEvent(event); -} - -Service::AM::FrontendAppletParameters GMainWindow::ApplicationAppletParameters() { - return Service::AM::FrontendAppletParameters{ - .applet_id = Service::AM::AppletId::Application, - .applet_type = Service::AM::AppletType::Application, - }; -} - -Service::AM::FrontendAppletParameters GMainWindow::LibraryAppletParameters( - u64 program_id, Service::AM::AppletId applet_id) { - return Service::AM::FrontendAppletParameters{ - .program_id = program_id, - .applet_id = applet_id, - .applet_type = Service::AM::AppletType::LibraryApplet, - }; -} - -void VolumeButton::wheelEvent(QWheelEvent* event) { - - int num_degrees = event->angleDelta().y() / 8; - int num_steps = (num_degrees / 15) * scroll_multiplier; - // Stated in QT docs: Most mouse types work in steps of 15 degrees, in which case the delta - // value is a multiple of 120; i.e., 120 units * 1/8 = 15 degrees. - - if (num_steps > 0) { - Settings::values.volume.SetValue( - (std::min)(200, Settings::values.volume.GetValue() + num_steps)); - } else { - Settings::values.volume.SetValue( - (std::max)(0, Settings::values.volume.GetValue() + num_steps)); - } - - scroll_multiplier = (std::min)(MaxMultiplier, scroll_multiplier * 2); - scroll_timer.start(100); // reset the multiplier if no scroll event occurs within 100 ms - - emit VolumeChanged(); - event->accept(); -} - -void VolumeButton::ResetMultiplier() { - scroll_multiplier = 1; -} - -#ifdef main -#undef main -#endif - static void SetHighDPIAttributes() { #ifdef _WIN32 // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. @@ -5094,12 +171,12 @@ int main(int argc, char* argv[]) { // generating shaders setlocale(LC_ALL, "C"); - GMainWindow main_window{has_broken_vulkan}; + MainWindow main_window{has_broken_vulkan}; // After settings have been loaded by GMainWindow, apply the filter main_window.show(); app.connect(&app, &QGuiApplication::applicationStateChanged, &main_window, - &GMainWindow::OnAppFocusStateChanged); + &MainWindow::OnAppFocusStateChanged); int result = app.exec(); detached_tasks.WaitForAllTasks(); diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp new file mode 100644 index 0000000000..b26baf7202 --- /dev/null +++ b/src/yuzu/main_window.cpp @@ -0,0 +1,4899 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/fs/ryujinx_compat.h" +#include "main_window.h" +#include "network/network.h" +#include "qt_common/discord/discord.h" +#include "ui_main.h" + +// Other Yuzu stuff // +#include "debugger/controller.h" +#include "debugger/console.h" + +#include "about_dialog.h" +#include "data_dialog.h" +#include "deps_dialog.h" +#include "install_dialog.h" + +#include "bootmanager.h" +#include "game_list.h" +#include "loading_screen.h" +#include "ryujinx_dialog.h" +#include "set_play_time_dialog.h" +#include "util/util.h" +#include "vk_device_info.h" + +#include "applets/qt_amiibo_settings.h" +#include "applets/qt_controller.h" +#include "applets/qt_profile_select.h" +#include "applets/qt_software_keyboard.h" +#include "applets/qt_error.h" +#include "applets/qt_web_browser.h" + +#include "configuration/configure_dialog.h" +#include "configuration/configure_input.h" +#include "configuration/configure_per_game.h" +#include "configuration/configure_tas.h" + +#include "util/clickable_label.h" +#include "util/overlay_dialog.h" +#include "util/controller_navigation.h" + +#include "multiplayer/state.h" + +// Qt Stuff // +#define QT_NO_OPENGL +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Qt Common // +#include "qt_common/config/uisettings.h" +#include "qt_common/config/shared_translation.h" + +#include "qt_common/qt_common.h" + +#include "qt_common/util/path.h" +#include "qt_common/util/meta.h" +#include "qt_common/util/content.h" +#include "qt_common/util/fs.h" + +// These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows +// defines. +static FileSys::VirtualDir VfsFilesystemCreateDirectoryWrapper(const std::string& path, FileSys::OpenMode mode) { + return QtCommon::vfs->CreateDirectory(path, mode); +} + +static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::VirtualDir& dir, + const std::string& path) { + return dir->CreateFile(path); +} + +// Frontend // +#include "frontend_common/play_time_manager.h" + +#ifdef ENABLE_UPDATE_CHECKER +#include "frontend_common/update_checker.h" +#endif + +// Common // +#include "common/fs/fs.h" +#include "common/logging/backend.h" +#include "common/memory_detect.h" +#include "common/scm_rev.h" +#include "common/scope_exit.h" +#include "common/string_util.h" + +#ifdef ARCHITECTURE_x86_64 +#include "common/x64/cpu_detect.h" +#endif + +// Core // +#include "core/frontend/applets/software_keyboard.h" +#include "core/frontend/applets/mii_edit.h" +#include "core/frontend/applets/general.h" + +#include "core/hle/service/acc/profile_manager.h" +#include "core/hle/service/am/applet_manager.h" +#include "core/hle/service/am/frontend/applet_web_browser_types.h" +#include "core/hle/kernel/k_process.h" + +#include "core/file_sys/card_image.h" +#include "core/file_sys/romfs.h" +#include "core/file_sys/savedata_factory.h" + +#include "core/tools/renderdoc.h" + +#include "core/perf_stats.h" + +// Input // +#include "hid_core/hid_core.h" +#include "hid_core/frontend/emulated_controller.h" +#include "input_common/drivers/virtual_amiibo.h" + +// Video Core // +#include "video_core/gpu.h" +#include "video_core/renderer_base.h" +#include "video_core/shader_notify.h" + +#ifdef HAVE_SDL2 +#include +#endif + +#include + +// Platform stuff // +#include + +#ifdef __APPLE__ +#include // for chdir +#endif + +#ifdef __unix__ + +#include +#include +#include "qt_common/gui_settings.h" +#include + +#endif + +#ifdef __linux__ +#include "common/linux/gamemode.h" +#endif + +#ifdef _WIN32 +#include "core/core_timing.h" +#include "common/windows/timer_resolution.h" + +#include +#include +#include +#include +#ifdef _MSC_VER +#pragma comment(lib, "Dwmapi.lib") +#endif + +static inline void ApplyWindowsTitleBarDarkMode(HWND hwnd, bool enabled) { + if (!hwnd) + return; + BOOL val = enabled ? TRUE : FALSE; + // 20 = Win11/21H2+ + if (SUCCEEDED(DwmSetWindowAttribute(hwnd, 20, &val, sizeof(val)))) + return; + // 19 = pre-21H2 + DwmSetWindowAttribute(hwnd, 19, &val, sizeof(val)); +} + +static inline void ApplyDarkToTopLevel(QWidget* w, bool on) { + if (!w || !w->isWindow()) + return; + ApplyWindowsTitleBarDarkMode(reinterpret_cast(w->winId()), on); +} + +namespace { +struct TitlebarFilter final : QObject { + bool dark; + explicit TitlebarFilter(bool is_dark) : QObject(qApp), dark(is_dark) {} + + void setDark(bool is_dark) { + dark = is_dark; + } + + void onFocusChanged(QWidget*, QWidget* now) { + if (now) + ApplyDarkToTopLevel(now->window(), dark); + } + + bool eventFilter(QObject* obj, QEvent* ev) override { + if (auto* w = qobject_cast(obj)) { + switch (ev->type()) { + case QEvent::WinIdChange: + case QEvent::Show: + case QEvent::ShowToParent: + case QEvent::Polish: + case QEvent::WindowStateChange: + case QEvent::ZOrderChange: + ApplyDarkToTopLevel(w, dark); + break; + default: + break; + } + } + return QObject::eventFilter(obj, ev); + } +}; + +static TitlebarFilter* g_filter = nullptr; +static QMetaObject::Connection g_focusConn; + +} // namespace + +static void ApplyGlobalDarkTitlebar(bool is_dark) { + if (!g_filter) { + g_filter = new TitlebarFilter(is_dark); + qApp->installEventFilter(g_filter); + g_focusConn = QObject::connect(qApp, &QApplication::focusChanged, g_filter, + &TitlebarFilter::onFocusChanged); + } else { + g_filter->setDark(is_dark); + } + for (QWidget* w : QApplication::topLevelWidgets()) + ApplyDarkToTopLevel(w, is_dark); +} + +static void RemoveTitlebarFilter() { + if (!g_filter) + return; + qApp->removeEventFilter(g_filter); + QObject::disconnect(g_focusConn); + g_filter->deleteLater(); + g_filter = nullptr; +} + +#endif + +#ifdef YUZU_CRASH_DUMPS +#include "yuzu/breakpad.h" +#endif + +using namespace Common::Literals; + +#include "qt_common/discord/discord.h" + +#ifdef USE_DISCORD_PRESENCE +#include "qt_common/discord/discord_impl.h" +#endif + +#ifdef QT_STATICPLUGIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); +#endif + +#ifdef _WIN32 +#include +extern "C" { +// tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable +// graphics +__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +} +#endif + +constexpr int default_mouse_hide_timeout = 2500; +constexpr int default_input_update_timeout = 1; + +constexpr size_t CopyBufferSize = 1_MiB; + +/** + * "Callouts" are one-time instructional messages shown to the user. In the config settings, there + * is a bitfield "callout_flags" options, used to track if a message has already been shown to the + * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. + */ +enum class CalloutFlag : uint32_t { + DRDDeprecation = 0x2, +}; + +const int MainWindow::max_recent_files_item; + +static void RemoveCachedContents() { + const auto cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir); + const auto offline_fonts = cache_dir / "fonts"; + const auto offline_manual = cache_dir / "offline_web_applet_manual"; + const auto offline_legal_information = cache_dir / "offline_web_applet_legal_information"; + const auto offline_system_data = cache_dir / "offline_web_applet_system_data"; + + Common::FS::RemoveDirRecursively(offline_fonts); + Common::FS::RemoveDirRecursively(offline_manual); + Common::FS::RemoveDirRecursively(offline_legal_information); + Common::FS::RemoveDirRecursively(offline_system_data); +} + +static void LogRuntimes() { +#ifdef _MSC_VER + // It is possible that the name of the dll will change. + // vcruntime140.dll is for 2015 and onwards + static constexpr char runtime_dll_name[] = "vcruntime140.dll"; + UINT sz = GetFileVersionInfoSizeA(runtime_dll_name, nullptr); + bool runtime_version_inspection_worked = false; + if (sz > 0) { + std::vector buf(sz); + if (GetFileVersionInfoA(runtime_dll_name, 0, sz, buf.data())) { + VS_FIXEDFILEINFO* pvi; + sz = sizeof(VS_FIXEDFILEINFO); + if (VerQueryValueA(buf.data(), "\\", reinterpret_cast(&pvi), &sz)) { + if (pvi->dwSignature == VS_FFI_SIGNATURE) { + runtime_version_inspection_worked = true; + LOG_INFO(Frontend, "MSVC Compiler: {} Runtime: {}.{}.{}.{}", _MSC_VER, + pvi->dwProductVersionMS >> 16, pvi->dwProductVersionMS & 0xFFFF, + pvi->dwProductVersionLS >> 16, pvi->dwProductVersionLS & 0xFFFF); + } + } + } + } + if (!runtime_version_inspection_worked) { + LOG_INFO(Frontend, "Unable to inspect {}", runtime_dll_name); + } +#endif + LOG_INFO(Frontend, "Qt Compile: {} Runtime: {}", QT_VERSION_STR, qVersion()); +} + +static QString PrettyProductName() { +#ifdef _WIN32 + // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 + // With that notation change they changed the registry key used to denote the current version + QSettings windows_registry( + QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), + QSettings::NativeFormat); + const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); + if (release_id == QStringLiteral("2009")) { + const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); + const QString display_version = + windows_registry.value(QStringLiteral("DisplayVersion")).toString(); + const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); + u32 version = 10; + if (current_build >= 22000) { + version = 11; + } + return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") + .arg(QString::number(version), display_version, QString::number(current_build), + QString::number(ubr)); + } +#endif + return QSysInfo::prettyProductName(); +} + +#ifndef _WIN32 +// TODO(crueter): carboxyl does this, is it needed in qml? +inline static bool isDarkMode() { +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + const auto scheme = QGuiApplication::styleHints()->colorScheme(); + return scheme == Qt::ColorScheme::Dark; +#else + const QPalette defaultPalette; + const auto text = defaultPalette.color(QPalette::WindowText); + const auto window = defaultPalette.color(QPalette::Window); + return text.lightness() > window.lightness(); +#endif // QT_VERSION +} +#endif // _WIN32 + +MainWindow::MainWindow(bool has_broken_vulkan) + : ui{std::make_unique()}, + input_subsystem{std::make_shared()}, user_data_migrator{this} { + QtCommon::Init(this); + + Common::FS::CreateEdenPaths(); + this->config = std::make_unique(); + + if (user_data_migrator.migrated) { + // Sort-of hack whereby we only move the old dir if it's a subfolder of the user dir + + using namespace Common::FS; + + static constexpr const std::array paths = {EdenPath::NANDDir, + EdenPath::SDMCDir, + EdenPath::DumpDir, + EdenPath::LoadDir}; + + for (const EdenPath& path : paths) { + std::string str_path = Common::FS::GetEdenPathString(path); + if (str_path.starts_with(user_data_migrator.selected_emu.get_user_dir())) { + boost::replace_all(str_path, + user_data_migrator.selected_emu.lower_name().toStdString(), + "eden"); + Common::FS::SetEdenPath(path, str_path); + } + } + } + +#ifdef __unix__ + SetupSigInterrupts(); +#endif + +#ifdef __linux__ + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); +#endif + + UISettings::RestoreWindowState(config); + + QtCommon::system->Initialize(); + + Common::Log::Initialize(); + Common::Log::Start(); + + LoadTranslation(); + + setAcceptDrops(true); + ui->setupUi(this); + statusBar()->hide(); + + startup_icon_theme = QIcon::themeName(); + // fallback can only be set once, colorful theme icons are okay on both light/dark + QIcon::setFallbackThemeName(QStringLiteral("colorful")); + QIcon::setFallbackSearchPaths(QStringList(QStringLiteral(":/icons"))); + + default_theme_paths = QIcon::themeSearchPaths(); + UpdateUITheme(); + + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + discord_rpc->Update(); + + play_time_manager = std::make_unique(); + + Network::Init(); + + QtCommon::Meta::RegisterMetaTypes(); + + InitializeWidgets(); + InitializeDebugWidgets(); + InitializeRecentFileMenuActions(); + InitializeHotkeys(); + + SetDefaultUIGeometry(); + RestoreUIState(); + + ConnectMenuEvents(); + ConnectWidgetEvents(); + + QtCommon::system->HIDCore().ReloadInputDevices(); + controller_dialog->refreshConfiguration(); + + const auto branch_name = std::string(Common::g_scm_branch); + const auto description = std::string(Common::g_scm_desc); + const auto build_id = std::string(Common::g_build_id); + + const auto yuzu_build = fmt::format("Eden Development Build | {}-{}", branch_name, description); + const auto override_build = + fmt::format(fmt::runtime(std::string(Common::g_title_bar_format_idle)), build_id); + const auto yuzu_build_version = override_build.empty() ? yuzu_build : override_build; + const auto processor_count = std::thread::hardware_concurrency(); + + LOG_INFO(Frontend, "Eden Version: {}", yuzu_build_version); + LogRuntimes(); +#ifdef ARCHITECTURE_x86_64 + const auto& caps = Common::GetCPUCaps(); + std::string cpu_string = caps.cpu_string; + if (caps.avx || caps.avx2 || caps.avx512f) { + cpu_string += " | AVX"; + if (caps.avx512f) { + cpu_string += "512"; + } else if (caps.avx2) { + cpu_string += '2'; + } + if (caps.fma || caps.fma4) { + cpu_string += " | FMA"; + } + } + LOG_INFO(Frontend, "Host CPU: {}", cpu_string); + if (std::optional processor_core = Common::GetProcessorCount()) { + LOG_INFO(Frontend, "Host CPU Cores: {}", *processor_core); + } +#endif + LOG_INFO(Frontend, "Host CPU Threads: {}", processor_count); + LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); + LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", + Common::GetMemInfo().TotalPhysicalMemory / f64{1_GiB}); + LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", Common::GetMemInfo().TotalSwapMemory / f64{1_GiB}); +#ifdef _WIN32 + LOG_INFO(Frontend, "Host Timer Resolution: {:.4f} ms", + std::chrono::duration_cast>( + Common::Windows::SetCurrentTimerResolutionToMaximum()) + .count()); + QtCommon::system->CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution()); +#endif + UpdateWindowTitle(); + + show(); + +#ifdef ENABLE_UPDATE_CHECKER + if (UISettings::values.check_for_updates) { + update_future = QtConcurrent::run([]() -> QString { + const bool is_prerelease = ((strstr(Common::g_build_version, "pre-alpha") != NULL) || + (strstr(Common::g_build_version, "alpha") != NULL) || + (strstr(Common::g_build_version, "beta") != NULL) || + (strstr(Common::g_build_version, "rc") != NULL)); + const std::optional latest_release_tag = + UpdateChecker::GetLatestRelease(is_prerelease); + if (latest_release_tag && latest_release_tag.value() != Common::g_build_version) { + return QString::fromStdString(latest_release_tag.value()); + } + return QString{}; + }); + update_watcher.connect(&update_watcher, &QFutureWatcher::finished, this, + &MainWindow::OnEmulatorUpdateAvailable); + update_watcher.setFuture(update_future); + } +#endif + + QtCommon::system->SetContentProvider(std::make_unique()); + QtCommon::system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, + QtCommon::provider.get()); + QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); + + // Remove cached contents generated during the previous session + RemoveCachedContents(); + + // Gen keys if necessary + OnCheckFirmwareDecryption(); + +#ifdef __unix__ + OnCheckGraphicsBackend(); +#endif + + // Check for orphaned profiles and reset profile data if necessary + QtCommon::Content::FixProfiles(); + + game_list->LoadCompatibilityList(); + game_list->PopulateAsync(UISettings::values.game_dirs); + + // make sure menubar has the arrow cursor instead of inheriting from this + ui->menubar->setCursor(QCursor()); + statusBar()->setCursor(QCursor()); + + mouse_hide_timer.setInterval(default_mouse_hide_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &MainWindow::HideMouseCursor); + connect(ui->menubar, &QMenuBar::hovered, this, &MainWindow::ShowMouseCursor); + + update_input_timer.setInterval(default_input_update_timeout); + connect(&update_input_timer, &QTimer::timeout, this, &MainWindow::UpdateInputDrivers); + update_input_timer.start(); + + MigrateConfigFiles(); + + if (has_broken_vulkan) { + UISettings::values.has_broken_vulkan = true; + + QMessageBox::warning(this, tr("Broken Vulkan Installation Detected"), + tr("Vulkan initialization failed during boot.

Click " + "here for instructions to fix the issue.")); + +#ifdef HAS_OPENGL + Settings::values.renderer_backend = Settings::RendererBackend::OpenGL; +#else + Settings::values.renderer_backend = Settings::RendererBackend::Null; +#endif + + UpdateAPIText(); + renderer_status_button->setDisabled(true); + renderer_status_button->setChecked(false); + } else { + VkDeviceInfo::PopulateRecords(vk_device_records, this->window()->windowHandle()); + } + +#if defined(HAVE_SDL2) && !defined(_WIN32) + SDL_InitSubSystem(SDL_INIT_VIDEO); + + // Set a screensaver inhibition reason string. Currently passed to DBus by SDL and visible to + // the user through their desktop environment. + //: TRANSLATORS: This string is shown to the user to explain why yuzu needs to prevent the + //: computer from sleeping + QByteArray wakelock_reason = tr("Running a game").toUtf8(); + SDL_SetHint(SDL_HINT_SCREENSAVER_INHIBIT_ACTIVITY_NAME, wakelock_reason.data()); + + // SDL disables the screen saver by default, and setting the hint + // SDL_HINT_VIDEO_ALLOW_SCREENSAVER doesn't seem to work, so we just enable the screen saver + // for now. + SDL_EnableScreenSaver(); +#endif + + SetupPrepareForSleep(); + + // Some moron added a race condition to the status bar + // so now we have to make this completely unnecessary call + // to prevent the UI from blowing up. + UpdateUITheme(); + + QStringList args = QApplication::arguments(); + + if (args.size() < 2) { + return; + } + + QString game_path; + bool should_launch_qlaunch = false; + bool should_launch_setup = false; + bool has_gamepath = false; + bool is_fullscreen = false; + + for (int i = 1; i < args.size(); ++i) { + // Preserves drag/drop functionality + if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { + game_path = args[1]; + has_gamepath = true; + break; + } + + // Launch game in fullscreen mode + if (args[i] == QStringLiteral("-f")) { + is_fullscreen = true; + continue; + } + + // Launch game with a specific user + if (args[i] == QStringLiteral("-u")) { + if (i >= args.size() - 1) { + continue; + } + + if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { + continue; + } + + int user_arg_idx = ++i; + bool argument_ok; + std::size_t selected_user = args[user_arg_idx].toUInt(&argument_ok); + + if (!argument_ok) { + // try to look it up by username, only finds the first username that matches. + const std::string user_arg_str = args[user_arg_idx].toStdString(); + const auto user_idx = QtCommon::system->GetProfileManager().GetUserIndex(user_arg_str); + + if (user_idx == std::nullopt) { + LOG_ERROR(Frontend, "Invalid user argument"); + continue; + } + + selected_user = user_idx.value(); + } + + if (!QtCommon::system->GetProfileManager().UserExistsIndex(selected_user)) { + LOG_ERROR(Frontend, "Selected user doesn't exist"); + continue; + } + + Settings::values.current_user = static_cast(selected_user); + + user_flag_cmd_line = true; + continue; + } + + // Launch game at path + if (args[i] == QStringLiteral("-g")) { + if (i >= args.size() - 1) { + continue; + } + + if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { + continue; + } + + game_path = args[++i]; + has_gamepath = true; + } + + if (args[i] == QStringLiteral("-qlaunch")) + should_launch_qlaunch = true; + if (args[i] == QStringLiteral("-setup")) + should_launch_setup = true; + } + + // Override fullscreen setting if gamepath or argument is provided + if (has_gamepath || is_fullscreen) { + ui->action_Fullscreen->setChecked(is_fullscreen); + } + + if (should_launch_setup) { + OnInitialSetup(); + } else { + if (!game_path.isEmpty()) { + BootGame(game_path, ApplicationAppletParameters()); + } else { + if (should_launch_qlaunch) { + OnHomeMenu(); + } + } + } +} + +MainWindow::~MainWindow() { + // will get automatically deleted otherwise + if (render_window->parent() == nullptr) { + delete render_window; + } + +#ifdef __unix__ + ::close(sig_interrupt_fds[0]); + ::close(sig_interrupt_fds[1]); +#endif +} + +void MainWindow::AmiiboSettingsShowDialog(const Core::Frontend::CabinetParameters& parameters, + std::shared_ptr nfp_device) { + cabinet_applet = + new QtAmiiboSettingsDialog(this, parameters, input_subsystem.get(), nfp_device); + SCOPE_EXIT { + cabinet_applet->deleteLater(); + cabinet_applet = nullptr; + }; + + cabinet_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint | + Qt::WindowTitleHint | Qt::WindowSystemMenuHint); + cabinet_applet->setWindowModality(Qt::WindowModal); + + if (cabinet_applet->exec() == QDialog::Rejected) { + emit AmiiboSettingsFinished(false, {}); + return; + } + + emit AmiiboSettingsFinished(true, cabinet_applet->GetName()); +} + +void MainWindow::AmiiboSettingsRequestExit() { + if (cabinet_applet) { + cabinet_applet->reject(); + } +} + +void MainWindow::ControllerSelectorReconfigureControllers( + const Core::Frontend::ControllerParameters& parameters) { + controller_applet = + new QtControllerSelectorDialog(this, parameters, input_subsystem.get(), *QtCommon::system); + SCOPE_EXIT { + controller_applet->deleteLater(); + controller_applet = nullptr; + }; + + controller_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | + Qt::WindowStaysOnTopHint | Qt::WindowTitleHint | + Qt::WindowSystemMenuHint); + controller_applet->setWindowModality(Qt::WindowModal); + bool is_success = controller_applet->exec() != QDialog::Rejected; + + // Don't forget to apply settings. + QtCommon::system->HIDCore().DisableAllControllerConfiguration(); + QtCommon::system->ApplySettings(); + config->SaveAllValues(); + + UpdateStatusButtons(); + + emit ControllerSelectorReconfigureFinished(is_success); +} + +void MainWindow::ControllerSelectorRequestExit() { + if (controller_applet) { + controller_applet->reject(); + } +} + +void MainWindow::ProfileSelectorSelectProfile( + const Core::Frontend::ProfileSelectParameters& parameters) { + profile_select_applet = new QtProfileSelectionDialog(*QtCommon::system, this, parameters); + SCOPE_EXIT { + profile_select_applet->deleteLater(); + profile_select_applet = nullptr; + }; + + profile_select_applet->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | + Qt::WindowStaysOnTopHint | Qt::WindowTitleHint | + Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); + profile_select_applet->setWindowModality(Qt::WindowModal); + if (profile_select_applet->exec() == QDialog::Rejected) { + emit ProfileSelectorFinishedSelection(std::nullopt); + return; + } + + const auto uuid = QtCommon::system->GetProfileManager().GetUser( + static_cast(profile_select_applet->GetIndex())); + if (!uuid.has_value()) { + emit ProfileSelectorFinishedSelection(std::nullopt); + return; + } + + emit ProfileSelectorFinishedSelection(uuid); +} + +void MainWindow::ProfileSelectorRequestExit() { + if (profile_select_applet) { + profile_select_applet->reject(); + } +} + +void MainWindow::SoftwareKeyboardInitialize( + bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters) { + if (software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is already initialized!"); + return; + } + + software_keyboard = new QtSoftwareKeyboardDialog(render_window, *QtCommon::system, is_inline, + std::move(initialize_parameters)); + + if (is_inline) { + connect( + software_keyboard, &QtSoftwareKeyboardDialog::SubmitInlineText, this, + [this](Service::AM::Frontend::SwkbdReplyType reply_type, std::u16string submitted_text, + s32 cursor_position) { + emit SoftwareKeyboardSubmitInlineText(reply_type, submitted_text, cursor_position); + }, + Qt::QueuedConnection); + } else { + connect( + software_keyboard, &QtSoftwareKeyboardDialog::SubmitNormalText, this, + [this](Service::AM::Frontend::SwkbdResult result, std::u16string submitted_text, + bool confirmed) { + emit SoftwareKeyboardSubmitNormalText(result, submitted_text, confirmed); + }, + Qt::QueuedConnection); + } +} + +void MainWindow::SoftwareKeyboardShowNormal() { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + const auto& layout = render_window->GetFramebufferLayout(); + + const auto x = layout.screen.left; + const auto y = layout.screen.top; + const auto w = layout.screen.GetWidth(); + const auto h = layout.screen.GetHeight(); + const auto scale_ratio = devicePixelRatioF(); + + software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y) / scale_ratio), + QSize(w, h) / scale_ratio); +} + +void MainWindow::SoftwareKeyboardShowTextCheck( + Service::AM::Frontend::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + software_keyboard->ShowTextCheckDialog(text_check_result, text_check_message); +} + +void MainWindow::SoftwareKeyboardShowInline( + Core::Frontend::InlineAppearParameters appear_parameters) { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + const auto& layout = render_window->GetFramebufferLayout(); + + const auto x = + static_cast(layout.screen.left + (0.5f * layout.screen.GetWidth() * + ((2.0f * appear_parameters.key_top_translate_x) + + (1.0f - appear_parameters.key_top_scale_x)))); + const auto y = + static_cast(layout.screen.top + (layout.screen.GetHeight() * + ((2.0f * appear_parameters.key_top_translate_y) + + (1.0f - appear_parameters.key_top_scale_y)))); + const auto w = static_cast(layout.screen.GetWidth() * appear_parameters.key_top_scale_x); + const auto h = static_cast(layout.screen.GetHeight() * appear_parameters.key_top_scale_y); + const auto scale_ratio = devicePixelRatioF(); + + software_keyboard->ShowInlineKeyboard(std::move(appear_parameters), + render_window->mapToGlobal(QPoint(x, y) / scale_ratio), + QSize(w, h) / scale_ratio); +} + +void MainWindow::SoftwareKeyboardHideInline() { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + software_keyboard->HideInlineKeyboard(); +} + +void MainWindow::SoftwareKeyboardInlineTextChanged( + Core::Frontend::InlineTextParameters text_parameters) { + if (!software_keyboard) { + LOG_ERROR(Frontend, "The software keyboard is not initialized!"); + return; + } + + software_keyboard->InlineTextChanged(std::move(text_parameters)); +} + +void MainWindow::SoftwareKeyboardExit() { + if (!software_keyboard) { + return; + } + + software_keyboard->ExitKeyboard(); + + software_keyboard = nullptr; +} + +void MainWindow::WebBrowserOpenWebPage(const std::string& main_url, + const std::string& additional_args, bool is_local) { +#ifdef YUZU_USE_QT_WEB_ENGINE + + // Raw input breaks with the web applet, Disable web applets if enabled + if (Settings::values.disable_web_applet || Settings::values.enable_raw_input) { + emit WebBrowserClosed(Service::AM::Frontend::WebExitReason::WindowClosed, + "http://localhost/"); + return; + } + + web_applet = new QtNXWebEngineView(this, *QtCommon::system, input_subsystem.get()); + + ui->action_Pause->setEnabled(false); + ui->action_Restart->setEnabled(false); + ui->action_Stop->setEnabled(false); + + { + QProgressDialog loading_progress(this); + loading_progress.setLabelText(tr("Loading Web Applet...")); + loading_progress.setRange(0, 3); + loading_progress.setValue(0); + + if (is_local && !Common::FS::Exists(main_url)) { + loading_progress.show(); + + auto future = QtConcurrent::run([this] { emit WebBrowserExtractOfflineRomFS(); }); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + loading_progress.setValue(1); + + if (is_local) { + web_applet->LoadLocalWebPage(main_url, additional_args); + } else { + web_applet->LoadExternalWebPage(main_url, additional_args); + } + + if (render_window->IsLoadingComplete()) { + render_window->hide(); + } + + const auto& layout = render_window->GetFramebufferLayout(); + const auto scale_ratio = devicePixelRatioF(); + web_applet->resize(layout.screen.GetWidth() / scale_ratio, + layout.screen.GetHeight() / scale_ratio); + web_applet->move(layout.screen.left / scale_ratio, + (layout.screen.top / scale_ratio) + menuBar()->height()); + web_applet->setZoomFactor(static_cast(layout.screen.GetWidth() / scale_ratio) / + static_cast(Layout::ScreenUndocked::Width)); + + web_applet->setFocus(); + web_applet->show(); + + loading_progress.setValue(2); + + QCoreApplication::processEvents(); + + loading_progress.setValue(3); + } + + bool exit_check = false; + + // TODO (Morph): Remove this + QAction* exit_action = new QAction(tr("Disable Web Applet"), this); + connect(exit_action, &QAction::triggered, this, [this] { + const auto result = QMessageBox::warning( + this, tr("Disable Web Applet"), + tr("Disabling the web applet can lead to undefined behavior and should only be used " + "with Super Mario 3D All-Stars. Are you sure you want to disable the web " + "applet?\n(This can be re-enabled in the Debug settings.)"), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) { + Settings::values.disable_web_applet = true; + web_applet->SetFinished(true); + } + }); + ui->menubar->addAction(exit_action); + + while (!web_applet->IsFinished()) { + QCoreApplication::processEvents(); + + if (!exit_check) { + web_applet->page()->runJavaScript( + QStringLiteral("end_applet;"), [&](const QVariant& variant) { + exit_check = false; + if (variant.toBool()) { + web_applet->SetFinished(true); + web_applet->SetExitReason( + Service::AM::Frontend::WebExitReason::EndButtonPressed); + } + }); + + exit_check = true; + } + + if (web_applet->GetCurrentURL().contains(QStringLiteral("localhost"))) { + if (!web_applet->IsFinished()) { + web_applet->SetFinished(true); + web_applet->SetExitReason(Service::AM::Frontend::WebExitReason::CallbackURL); + } + + web_applet->SetLastURL(web_applet->GetCurrentURL().toStdString()); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + const auto exit_reason = web_applet->GetExitReason(); + const auto last_url = web_applet->GetLastURL(); + + web_applet->hide(); + + render_window->setFocus(); + + if (render_window->IsLoadingComplete()) { + render_window->show(); + } + + ui->action_Pause->setEnabled(true); + ui->action_Restart->setEnabled(true); + ui->action_Stop->setEnabled(true); + + ui->menubar->removeAction(exit_action); + + QCoreApplication::processEvents(); + + emit WebBrowserClosed(exit_reason, last_url); + +#else + + // Utilize the same fallback as the default web browser applet. + emit WebBrowserClosed(Service::AM::Frontend::WebExitReason::WindowClosed, "http://localhost/"); + +#endif +} + +void MainWindow::WebBrowserRequestExit() { +#ifdef YUZU_USE_QT_WEB_ENGINE + if (web_applet) { + web_applet->SetExitReason(Service::AM::Frontend::WebExitReason::ExitRequested); + web_applet->SetFinished(true); + } +#endif +} + +void MainWindow::InitializeWidgets() { +#ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING + ui->action_Report_Compatibility->setVisible(true); +#endif + render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *QtCommon::system); + render_window->hide(); + + game_list = new GameList(QtCommon::vfs, QtCommon::provider.get(), *play_time_manager, *QtCommon::system, this); + ui->horizontalLayout->addWidget(game_list); + + game_list_placeholder = new GameListPlaceholder(this); + ui->horizontalLayout->addWidget(game_list_placeholder); + game_list_placeholder->setVisible(false); + + loading_screen = new LoadingScreen(this); + loading_screen->hide(); + ui->horizontalLayout->addWidget(loading_screen); + connect(loading_screen, &LoadingScreen::Hidden, this, [&] { + loading_screen->Clear(); + if (emulation_running) { + render_window->show(); + render_window->setFocus(); + } + }); + + multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room, + ui->action_Show_Room, *QtCommon::system); + multiplayer_state->setVisible(false); + + // Create status bar + message_label = new QLabel(); + // Configured separately for left alignment + message_label->setFrameStyle(QFrame::NoFrame); + message_label->setContentsMargins(4, 0, 4, 0); + message_label->setAlignment(Qt::AlignLeft); + statusBar()->addPermanentWidget(message_label, 1); + + shader_building_label = new QLabel(); + shader_building_label->setToolTip(tr("The amount of shaders currently being built")); + res_scale_label = new QLabel(); + res_scale_label->setToolTip(tr("The current selected resolution scaling multiplier.")); + emu_speed_label = new QLabel(); + emu_speed_label->setToolTip( + tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a Switch.")); + game_fps_label = new QLabel(); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label = new QLabel(); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a Switch frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + for (auto& label : {shader_building_label, res_scale_label, emu_speed_label, game_fps_label, + emu_frametime_label}) { + label->setVisible(false); + label->setFrameStyle(QFrame::NoFrame); + label->setContentsMargins(4, 0, 4, 0); + statusBar()->addPermanentWidget(label); + } + + firmware_label = new QLabel(); + firmware_label->setObjectName(QStringLiteral("FirmwareLabel")); + firmware_label->setVisible(false); + firmware_label->setContentsMargins(4, 0, 4, 0); + firmware_label->setFocusPolicy(Qt::NoFocus); + statusBar()->addPermanentWidget(firmware_label); + + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); + + tas_label = new QLabel(); + tas_label->setObjectName(QStringLiteral("TASlabel")); + tas_label->setFocusPolicy(Qt::NoFocus); + statusBar()->insertPermanentWidget(0, tas_label); + + volume_popup = new QWidget(this); + volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); + volume_popup->setLayout(new QVBoxLayout()); + volume_popup->setMinimumWidth(200); + + volume_slider = new QSlider(Qt::Horizontal); + volume_slider->setObjectName(QStringLiteral("volume_slider")); + volume_slider->setMaximum(200); + volume_slider->setPageStep(5); + volume_popup->layout()->addWidget(volume_slider); + + volume_button = new VolumeButton(); + volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + volume_button->setFocusPolicy(Qt::NoFocus); + volume_button->setCheckable(true); + UpdateVolumeUI(); + connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { + Settings::values.audio_muted = false; + const auto volume = static_cast(percentage); + Settings::values.volume.SetValue(volume); + UpdateVolumeUI(); + }); + connect(volume_button, &QPushButton::clicked, this, [&] { + UpdateVolumeUI(); + volume_popup->setVisible(!volume_popup->isVisible()); + QRect rect = volume_button->geometry(); + QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); + bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); + volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); + }); + volume_button->setContextMenuPolicy(Qt::CustomContextMenu); + connect(volume_button, &QPushButton::customContextMenuRequested, + [this](const QPoint& menu_location) { + QMenu context_menu; + context_menu.addAction( + Settings::values.audio_muted ? tr("Unmute") : tr("Mute"), [this] { + Settings::values.audio_muted = !Settings::values.audio_muted; + UpdateVolumeUI(); + }); + + context_menu.addAction(tr("Reset Volume"), [this] { + Settings::values.volume.SetValue(100); + UpdateVolumeUI(); + }); + + context_menu.exec(volume_button->mapToGlobal(menu_location)); + volume_button->repaint(); + }); + connect(volume_button, &VolumeButton::VolumeChanged, this, &MainWindow::UpdateVolumeUI); + + statusBar()->insertPermanentWidget(0, volume_button); + + // setup AA button + aa_status_button = new QPushButton(); + aa_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + aa_status_button->setFocusPolicy(Qt::NoFocus); + connect(aa_status_button, &QPushButton::clicked, [&] { + auto aa_mode = Settings::values.anti_aliasing.GetValue(); + aa_mode = static_cast(static_cast(aa_mode) + 1); + if (aa_mode == Settings::AntiAliasing::MaxEnum) { + aa_mode = Settings::AntiAliasing::None; + } + Settings::values.anti_aliasing.SetValue(aa_mode); + aa_status_button->setChecked(true); + UpdateAAText(); + }); + UpdateAAText(); + aa_status_button->setCheckable(true); + aa_status_button->setChecked(true); + aa_status_button->setContextMenuPolicy(Qt::CustomContextMenu); + connect(aa_status_button, &QPushButton::customContextMenuRequested, + [this](const QPoint& menu_location) { + QMenu context_menu; + for (auto const& aa_text_pair : ConfigurationShared::anti_aliasing_texts_map) { + context_menu.addAction(aa_text_pair.second, [this, aa_text_pair] { + Settings::values.anti_aliasing.SetValue(aa_text_pair.first); + UpdateAAText(); + }); + } + context_menu.exec(aa_status_button->mapToGlobal(menu_location)); + aa_status_button->repaint(); + }); + statusBar()->insertPermanentWidget(0, aa_status_button); + + // Setup Filter button + filter_status_button = new QPushButton(); + filter_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + filter_status_button->setFocusPolicy(Qt::NoFocus); + connect(filter_status_button, &QPushButton::clicked, this, + &MainWindow::OnToggleAdaptingFilter); + UpdateFilterText(); + filter_status_button->setCheckable(true); + filter_status_button->setChecked(true); + filter_status_button->setContextMenuPolicy(Qt::CustomContextMenu); + connect(filter_status_button, &QPushButton::customContextMenuRequested, + [this](const QPoint& menu_location) { + QMenu context_menu; + for (auto const& filter_text_pair : ConfigurationShared::scaling_filter_texts_map) { + context_menu.addAction(filter_text_pair.second, [this, filter_text_pair] { + Settings::values.scaling_filter.SetValue(filter_text_pair.first); + UpdateFilterText(); + }); + } + context_menu.exec(filter_status_button->mapToGlobal(menu_location)); + filter_status_button->repaint(); + }); + statusBar()->insertPermanentWidget(0, filter_status_button); + + // Setup Dock button + dock_status_button = new QPushButton(); + dock_status_button->setObjectName(QStringLiteral("DockingStatusBarButton")); + dock_status_button->setFocusPolicy(Qt::NoFocus); + connect(dock_status_button, &QPushButton::clicked, this, &MainWindow::OnToggleDockedMode); + dock_status_button->setCheckable(true); + UpdateDockedButton(); + dock_status_button->setContextMenuPolicy(Qt::CustomContextMenu); + connect(dock_status_button, &QPushButton::customContextMenuRequested, + [this](const QPoint& menu_location) { + QMenu context_menu; + + for (auto const& pair : ConfigurationShared::use_docked_mode_texts_map) { + context_menu.addAction(pair.second, [this, &pair] { + if (pair.first != Settings::values.use_docked_mode.GetValue()) { + OnToggleDockedMode(); + } + }); + } + context_menu.exec(dock_status_button->mapToGlobal(menu_location)); + dock_status_button->repaint(); + }); + statusBar()->insertPermanentWidget(0, dock_status_button); + + // Setup GPU Accuracy button + gpu_accuracy_button = new QPushButton(); + gpu_accuracy_button->setObjectName(QStringLiteral("GPUStatusBarButton")); + gpu_accuracy_button->setCheckable(true); + gpu_accuracy_button->setFocusPolicy(Qt::NoFocus); + connect(gpu_accuracy_button, &QPushButton::clicked, this, &MainWindow::OnToggleGpuAccuracy); + UpdateGPUAccuracyButton(); + gpu_accuracy_button->setContextMenuPolicy(Qt::CustomContextMenu); + connect(gpu_accuracy_button, &QPushButton::customContextMenuRequested, + [this](const QPoint& menu_location) { + QMenu context_menu; + + for (auto const& gpu_accuracy_pair : ConfigurationShared::gpu_accuracy_texts_map) { + if (gpu_accuracy_pair.first == Settings::GpuAccuracy::Extreme) { + continue; + } + context_menu.addAction(gpu_accuracy_pair.second, [this, gpu_accuracy_pair] { + Settings::values.gpu_accuracy.SetValue(gpu_accuracy_pair.first); + UpdateGPUAccuracyButton(); + }); + } + context_menu.exec(gpu_accuracy_button->mapToGlobal(menu_location)); + gpu_accuracy_button->repaint(); + }); + statusBar()->insertPermanentWidget(0, gpu_accuracy_button); + + // Setup Renderer API button + renderer_status_button = new QPushButton(); + renderer_status_button->setObjectName(QStringLiteral("RendererStatusBarButton")); + renderer_status_button->setCheckable(true); + renderer_status_button->setFocusPolicy(Qt::NoFocus); + connect(renderer_status_button, &QPushButton::clicked, this, &MainWindow::OnToggleGraphicsAPI); + UpdateAPIText(); + renderer_status_button->setCheckable(true); + renderer_status_button->setChecked(Settings::values.renderer_backend.GetValue() == + Settings::RendererBackend::Vulkan); + renderer_status_button->setContextMenuPolicy(Qt::CustomContextMenu); + connect(renderer_status_button, &QPushButton::customContextMenuRequested, + [this](const QPoint& menu_location) { + QMenu context_menu; + + for (auto const& renderer_backend_pair : + ConfigurationShared::renderer_backend_texts_map) { + if (renderer_backend_pair.first == Settings::RendererBackend::Null) { + continue; + } + context_menu.addAction( + renderer_backend_pair.second, [this, renderer_backend_pair] { + Settings::values.renderer_backend.SetValue(renderer_backend_pair.first); + UpdateAPIText(); + }); + } + context_menu.exec(renderer_status_button->mapToGlobal(menu_location)); + renderer_status_button->repaint(); + }); + statusBar()->insertPermanentWidget(0, renderer_status_button); + + // Setup Refresh Button + refresh_button = new QPushButton(); + refresh_button->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); + refresh_button->setObjectName(QStringLiteral("RefreshButton")); + refresh_button->setFocusPolicy(Qt::NoFocus); + connect(refresh_button, &QPushButton::clicked, this, &MainWindow::OnGameListRefresh); + + statusBar()->insertPermanentWidget(0, refresh_button); + + statusBar()->setVisible(true); + setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); +} + +void MainWindow::InitializeDebugWidgets() { + QMenu* debug_menu = ui->menu_View_Debugging; + + controller_dialog = new ControllerDialog(QtCommon::system->HIDCore(), input_subsystem, this); + controller_dialog->hide(); + debug_menu->addAction(controller_dialog->toggleViewAction()); +} + +void MainWindow::InitializeRecentFileMenuActions() { + for (int i = 0; i < max_recent_files_item; ++i) { + actions_recent_files[i] = new QAction(this); + actions_recent_files[i]->setVisible(false); + connect(actions_recent_files[i], &QAction::triggered, this, &MainWindow::OnMenuRecentFile); + + ui->menu_recent_files->addAction(actions_recent_files[i]); + } + ui->menu_recent_files->addSeparator(); + QAction* action_clear_recent_files = new QAction(this); + action_clear_recent_files->setText(tr("&Clear Recent Files")); + connect(action_clear_recent_files, &QAction::triggered, this, [this] { + UISettings::values.recent_files.clear(); + UpdateRecentFiles(); + }); + ui->menu_recent_files->addAction(action_clear_recent_files); + + UpdateRecentFiles(); +} + +void MainWindow::LinkActionShortcut(QAction* action, const QString& action_name, + const bool tas_allowed) { + static const auto main_window = std::string("Main Window"); + action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name.toStdString())); + action->setShortcutContext( + hotkey_registry.GetShortcutContext(main_window, action_name.toStdString())); + action->setAutoRepeat(false); + + this->addAction(action); + + auto* controller = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); + const auto* controller_hotkey = + hotkey_registry.GetControllerHotkey(main_window, action_name.toStdString(), controller); + connect( + controller_hotkey, &ControllerShortcut::Activated, this, + [action, tas_allowed, this] { + auto [tas_status, current_tas_frame, total_tas_frames] = + input_subsystem->GetTas()->GetStatus(); + if (tas_allowed || tas_status == InputCommon::TasInput::TasState::Stopped) { + action->trigger(); + } + }, + Qt::QueuedConnection); +} + +void MainWindow::InitializeHotkeys() { + hotkey_registry.LoadHotkeys(); + + LinkActionShortcut(ui->action_Load_File, QStringLiteral("Load File")); + LinkActionShortcut(ui->action_Load_Amiibo, QStringLiteral("Load/Remove Amiibo")); + LinkActionShortcut(ui->action_Exit, QStringLiteral("Exit Eden")); + LinkActionShortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); + LinkActionShortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); + LinkActionShortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); + LinkActionShortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); + LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); + LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen")); + LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); + LinkActionShortcut(ui->action_TAS_Start, QStringLiteral("TAS Start/Stop"), true); + LinkActionShortcut(ui->action_TAS_Record, QStringLiteral("TAS Record"), true); + LinkActionShortcut(ui->action_TAS_Reset, QStringLiteral("TAS Reset"), true); + LinkActionShortcut(ui->action_View_Lobby, + QStringLiteral("Multiplayer Browse Public Game Lobby")); + LinkActionShortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); + LinkActionShortcut(ui->action_Connect_To_Room, + QStringLiteral("Multiplayer Direct Connect to Room")); + LinkActionShortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); + LinkActionShortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); + LinkActionShortcut(ui->action_Configure, QStringLiteral("Configure")); + LinkActionShortcut(ui->action_Configure_Current_Game, QStringLiteral("Configure Current Game")); + + static const QString main_window = QStringLiteral("Main Window"); + const auto connect_shortcut = [&](const QString& action_name, const Fn& function) { + const auto* hotkey = + hotkey_registry.GetHotkey(main_window.toStdString(), action_name.toStdString(), this); + auto* controller = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); + const auto* controller_hotkey = hotkey_registry.GetControllerHotkey( + main_window.toStdString(), action_name.toStdString(), controller); + connect(hotkey, &QShortcut::activated, this, function); + connect(controller_hotkey, &ControllerShortcut::Activated, this, function, + Qt::QueuedConnection); + }; + + connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { + if (emulation_running && ui->action_Fullscreen->isChecked()) { + ui->action_Fullscreen->setChecked(false); + ToggleFullscreen(); + } + }); + connect_shortcut(QStringLiteral("Change Adapting Filter"), + &MainWindow::OnToggleAdaptingFilter); + connect_shortcut(QStringLiteral("Change Docked Mode"), &MainWindow::OnToggleDockedMode); + connect_shortcut(QStringLiteral("Change GPU Accuracy"), &MainWindow::OnToggleGpuAccuracy); + connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &MainWindow::OnMute); + connect_shortcut(QStringLiteral("Audio Volume Down"), &MainWindow::OnDecreaseVolume); + connect_shortcut(QStringLiteral("Audio Volume Up"), &MainWindow::OnIncreaseVolume); + connect_shortcut(QStringLiteral("Toggle Framerate Limit"), [] { + Settings::values.use_speed_limit.SetValue(!Settings::values.use_speed_limit.GetValue()); + }); + connect_shortcut(QStringLiteral("Toggle Renderdoc Capture"), [] { + if (Settings::values.enable_renderdoc_hotkey) { + QtCommon::system->GetRenderdocAPI().ToggleCapture(); + } + }); + connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] { + Settings::values.mouse_panning = !Settings::values.mouse_panning; + if (Settings::values.mouse_panning) { + render_window->installEventFilter(render_window); + render_window->setAttribute(Qt::WA_Hover, true); + } + }); +} + +void MainWindow::SetDefaultUIGeometry() { + // geometry: 53% of the window contents are in the upper screen half, 47% in the lower half + const QRect screenRect = QGuiApplication::primaryScreen()->geometry(); + + const int w = screenRect.width() * 2 / 3; + const int h = screenRect.height() * 2 / 3; + const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; + const int y = (screenRect.y() + screenRect.height()) / 2 - h * 53 / 100; + + setGeometry(x, y, w, h); +} + +void MainWindow::RestoreUIState() { + setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); + restoreGeometry(UISettings::values.geometry); + // Work-around because the games list isn't supposed to be full screen + if (isFullScreen()) { + showNormal(); + } + restoreState(UISettings::values.state); + render_window->setWindowFlags(render_window->windowFlags() & ~Qt::FramelessWindowHint); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + + game_list->LoadInterfaceLayout(); + + ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); + ToggleWindowMode(); + + ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); + + ui->action_Display_Dock_Widget_Headers->setChecked( + UISettings::values.display_titlebar.GetValue()); + + ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + + ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + Debugger::ToggleConsole(); +} + +void MainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { + if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && + state != Qt::ApplicationActive) { + LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); + } + if (!emulation_running) { + return; + } + if (UISettings::values.pause_when_in_background) { + if (emu_thread->IsRunning() && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + auto_paused = true; + OnPauseGame(); + } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { + auto_paused = false; + OnStartGame(); + } + } + if (UISettings::values.mute_when_in_background) { + if (!Settings::values.audio_muted && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + Settings::values.audio_muted = true; + auto_muted = true; + } else if (auto_muted && state == Qt::ApplicationActive) { + Settings::values.audio_muted = false; + auto_muted = false; + } + UpdateVolumeUI(); + } +} + +void MainWindow::ConnectWidgetEvents() { + connect(game_list, &GameList::BootGame, this, &MainWindow::BootGameFromList); + connect(game_list, &GameList::GameChosen, this, &MainWindow::OnGameListLoadFile); + connect(game_list, &GameList::OpenDirectory, this, &MainWindow::OnGameListOpenDirectory); + connect(game_list, &GameList::OpenFolderRequested, this, &MainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this, [this](u64 program_id) { + QtCommon::Path::OpenShaderCache(program_id, this); + }); + connect(game_list, &GameList::RemoveInstalledEntryRequested, this, + &MainWindow::OnGameListRemoveInstalledEntry); + connect(game_list, &GameList::RemoveFileRequested, this, &MainWindow::OnGameListRemoveFile); + connect(game_list, &GameList::RemovePlayTimeRequested, this, + &MainWindow::OnGameListRemovePlayTimeData); + connect(game_list, &GameList::SetPlayTimeRequested, this, + &MainWindow::OnGameListSetPlayTime); + connect(game_list, &GameList::DumpRomFSRequested, this, &MainWindow::OnGameListDumpRomFS); + connect(game_list, &GameList::VerifyIntegrityRequested, this, + &MainWindow::OnGameListVerifyIntegrity); + connect(game_list, &GameList::CopyTIDRequested, this, &MainWindow::OnGameListCopyTID); + connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, + &MainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::CreateShortcut, this, &MainWindow::OnGameListCreateShortcut); + connect(game_list, &GameList::AddDirectory, this, &MainWindow::OnGameListAddDirectory); + connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, + &MainWindow::OnGameListAddDirectory); + connect(game_list, &GameList::ShowList, this, &MainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); + connect(game_list, &GameList::SaveConfig, this, &MainWindow::OnSaveConfig); + + connect(game_list, &GameList::OpenPerGameGeneralRequested, this, + &MainWindow::OnGameListOpenPerGameProperties); + connect(game_list, &GameList::LinkToRyujinxRequested, this, + &MainWindow::OnLinkToRyujinx); + + connect(this, &MainWindow::UpdateInstallProgress, this, + &MainWindow::IncrementInstallProgress); + + connect(this, &MainWindow::EmulationStarting, render_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &MainWindow::EmulationStopping, render_window, + &GRenderWindow::OnEmulationStopping); + + // Software Keyboard Applet + connect(this, &MainWindow::EmulationStarting, this, &MainWindow::SoftwareKeyboardExit); + connect(this, &MainWindow::EmulationStopping, this, &MainWindow::SoftwareKeyboardExit); + + connect(&status_bar_update_timer, &QTimer::timeout, this, &MainWindow::UpdateStatusBar); + + connect(this, &MainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); +} + +void MainWindow::ConnectMenuEvents() { + const auto connect_menu = [&](QAction* action, const Fn& event_fn) { + connect(action, &QAction::triggered, this, event_fn); + // Add actions to this window so that hiding menus in fullscreen won't disable them + addAction(action); + // Add actions to the render window so that they work outside of single window mode + render_window->addAction(action); + }; + + // File + connect_menu(ui->action_Load_File, &MainWindow::OnMenuLoadFile); + connect_menu(ui->action_Load_Folder, &MainWindow::OnMenuLoadFolder); + connect_menu(ui->action_Install_File_NAND, &MainWindow::OnMenuInstallToNAND); + connect_menu(ui->action_Exit, &QMainWindow::close); + connect_menu(ui->action_Load_Amiibo, &MainWindow::OnLoadAmiibo); + + // Emulation + connect_menu(ui->action_Pause, &MainWindow::OnPauseContinueGame); + connect_menu(ui->action_Stop, &MainWindow::OnStopGame); + connect_menu(ui->action_Report_Compatibility, &MainWindow::OnMenuReportCompatibility); + connect_menu(ui->action_Open_Mods_Page, &MainWindow::OnOpenModsPage); + connect_menu(ui->action_Open_Quickstart_Guide, &MainWindow::OnOpenQuickstartGuide); + connect_menu(ui->action_Open_FAQ, &MainWindow::OnOpenFAQ); + connect_menu(ui->action_Restart, &MainWindow::OnRestartGame); + connect_menu(ui->action_Configure, &MainWindow::OnConfigure); + connect_menu(ui->action_Configure_Current_Game, &MainWindow::OnConfigurePerGame); + + // View + connect_menu(ui->action_Fullscreen, &MainWindow::ToggleFullscreen); + connect_menu(ui->action_Single_Window_Mode, &MainWindow::ToggleWindowMode); + connect_menu(ui->action_Show_Filter_Bar, &MainWindow::OnToggleFilterBar); + connect_menu(ui->action_Show_Status_Bar, &MainWindow::OnToggleStatusBar); + + connect_menu(ui->action_Reset_Window_Size_720, &MainWindow::ResetWindowSize720); + connect_menu(ui->action_Reset_Window_Size_900, &MainWindow::ResetWindowSize900); + connect_menu(ui->action_Reset_Window_Size_1080, &MainWindow::ResetWindowSize1080); + ui->menu_Reset_Window_Size->addActions({ui->action_Reset_Window_Size_720, + ui->action_Reset_Window_Size_900, + ui->action_Reset_Window_Size_1080}); + + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + connect(multiplayer_state, &MultiplayerState::SaveConfig, this, &MainWindow::OnSaveConfig); + + // Tools + connect_menu(ui->action_Load_Album, &MainWindow::OnAlbum); + connect_menu(ui->action_Load_Cabinet_Nickname_Owner, + [this]() { OnCabinet(Service::NFP::CabinetMode::StartNicknameAndOwnerSettings); }); + connect_menu(ui->action_Load_Cabinet_Eraser, + [this]() { OnCabinet(Service::NFP::CabinetMode::StartGameDataEraser); }); + connect_menu(ui->action_Load_Cabinet_Restorer, + [this]() { OnCabinet(Service::NFP::CabinetMode::StartRestorer); }); + connect_menu(ui->action_Load_Cabinet_Formatter, + [this]() { OnCabinet(Service::NFP::CabinetMode::StartFormatter); }); + connect_menu(ui->action_Load_Mii_Edit, &MainWindow::OnMiiEdit); + connect_menu(ui->action_Open_Controller_Menu, &MainWindow::OnOpenControllerMenu); + connect_menu(ui->action_Load_Home_Menu, &MainWindow::OnHomeMenu); + connect_menu(ui->action_Open_Setup, &MainWindow::OnInitialSetup); + connect_menu(ui->action_Desktop, &MainWindow::OnCreateHomeMenuDesktopShortcut); + connect_menu(ui->action_Application_Menu, + &MainWindow::OnCreateHomeMenuApplicationMenuShortcut); + connect_menu(ui->action_Capture_Screenshot, &MainWindow::OnCaptureScreenshot); + + // TAS + connect_menu(ui->action_TAS_Start, &MainWindow::OnTasStartStop); + connect_menu(ui->action_TAS_Record, &MainWindow::OnTasRecord); + connect_menu(ui->action_TAS_Reset, &MainWindow::OnTasReset); + connect_menu(ui->action_Configure_Tas, &MainWindow::OnConfigureTas); + + // Help + connect_menu(ui->action_Root_Data_Folder, &MainWindow::OnOpenRootDataFolder); + connect_menu(ui->action_NAND_Folder, &MainWindow::OnOpenNANDFolder); + connect_menu(ui->action_SDMC_Folder, &MainWindow::OnOpenSDMCFolder); + connect_menu(ui->action_Mod_Folder, &MainWindow::OnOpenModFolder); + connect_menu(ui->action_Log_Folder, &MainWindow::OnOpenLogFolder); + + connect_menu(ui->action_Verify_installed_contents, &MainWindow::OnVerifyInstalledContents); + connect_menu(ui->action_Firmware_From_Folder, &MainWindow::OnInstallFirmware); + connect_menu(ui->action_Firmware_From_ZIP, &MainWindow::OnInstallFirmwareFromZIP); + connect_menu(ui->action_Install_Keys, &MainWindow::OnInstallDecryptionKeys); + connect_menu(ui->action_About, &MainWindow::OnAbout); + connect_menu(ui->action_Eden_Dependencies, &MainWindow::OnEdenDependencies); + connect_menu(ui->action_Data_Manager, &MainWindow::OnDataDialog); +} + +void MainWindow::UpdateMenuState() { + const bool is_paused = emu_thread == nullptr || !emu_thread->IsRunning(); + const bool is_firmware_available = CheckFirmwarePresence(); + + const std::array running_actions{ + ui->action_Stop, + ui->action_Restart, + ui->action_Configure_Current_Game, + ui->action_Report_Compatibility, + ui->action_Load_Amiibo, + ui->action_Pause, + }; + + const std::array applet_actions{ui->action_Load_Album, + ui->action_Load_Cabinet_Nickname_Owner, + ui->action_Load_Cabinet_Eraser, + ui->action_Load_Cabinet_Restorer, + ui->action_Load_Cabinet_Formatter, + ui->action_Load_Mii_Edit, + ui->action_Load_Home_Menu, + ui->action_Open_Controller_Menu}; + + for (QAction* action : running_actions) { + action->setEnabled(emulation_running); + } + + ui->action_Firmware_From_Folder->setEnabled(!emulation_running); + ui->action_Firmware_From_ZIP->setEnabled(!emulation_running); + ui->action_Install_Keys->setEnabled(!emulation_running); + + for (QAction* action : applet_actions) { + action->setEnabled(is_firmware_available && !emulation_running); + } + + ui->action_Capture_Screenshot->setEnabled(emulation_running && !is_paused); + + if (emulation_running && is_paused) { + ui->action_Pause->setText(tr("&Continue")); + } else { + ui->action_Pause->setText(tr("&Pause")); + } + + multiplayer_state->UpdateNotificationStatus(); +} + +void MainWindow::SetupPrepareForSleep() { +#ifdef __unix__ + if (auto bus = QDBusConnection::systemBus(); bus.isConnected()) { + // See https://github.com/ConsoleKit2/ConsoleKit2/issues/150 +#ifdef __linux__ + const auto dbus_logind_service = QStringLiteral("org.freedesktop.login1"); + const auto dbus_logind_path = QStringLiteral("/org/freedesktop/login1"); + const auto dbus_logind_manager_if = QStringLiteral("org.freedesktop.login1.Manager"); + //const auto dbus_logind_session_if = QStringLiteral("org.freedesktop.login1.Session"); +#else + const auto dbus_logind_service = QStringLiteral("org.freedesktop.ConsoleKit"); + const auto dbus_logind_path = QStringLiteral("/org/freedesktop/ConsoleKit/Manager"); + const auto dbus_logind_manager_if = QStringLiteral("org.freedesktop.ConsoleKit.Manager"); + //const auto dbus_logind_session_if = QStringLiteral("org.freedesktop.ConsoleKit.Session"); +#endif + const bool success = bus.connect( + dbus_logind_service, dbus_logind_path, + dbus_logind_manager_if, QStringLiteral("PrepareForSleep"), + QStringLiteral("b"), this, SLOT(OnPrepareForSleep(bool))); + if (!success) + LOG_WARNING(Frontend, "Couldn't register PrepareForSleep signal"); + } else { + LOG_WARNING(Frontend, "QDBusConnection system bus is not connected"); + } +#endif // __unix__ +} + +void MainWindow::OnPrepareForSleep(bool prepare_sleep) { + if (emu_thread == nullptr) { + return; + } + + if (prepare_sleep) { + if (emu_thread->IsRunning()) { + auto_paused = true; + OnPauseGame(); + } + } else { + if (!emu_thread->IsRunning() && auto_paused) { + auto_paused = false; + OnStartGame(); + } + } +} + +#ifdef __unix__ +std::array MainWindow::sig_interrupt_fds{0, 0, 0}; + +void MainWindow::SetupSigInterrupts() { + if (sig_interrupt_fds[2] == 1) { + return; + } + socketpair(AF_UNIX, SOCK_STREAM, 0, sig_interrupt_fds.data()); + sig_interrupt_fds[2] = 1; + + struct sigaction sa; + sa.sa_handler = &MainWindow::HandleSigInterrupt; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESETHAND; + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + sig_interrupt_notifier = new QSocketNotifier(sig_interrupt_fds[1], QSocketNotifier::Read, this); + connect(sig_interrupt_notifier, &QSocketNotifier::activated, this, + &MainWindow::OnSigInterruptNotifierActivated); + connect(this, &MainWindow::SigInterrupt, this, &MainWindow::close); +} + +void MainWindow::HandleSigInterrupt(int sig) { + if (sig == SIGINT) { + _exit(1); + } + + // Calling into Qt directly from a signal handler is not safe, + // so wake up a QSocketNotifier with this hacky write call instead. + char a = 1; + int ret = write(sig_interrupt_fds[0], &a, sizeof(a)); + (void)ret; +} + +void MainWindow::OnSigInterruptNotifierActivated() { + sig_interrupt_notifier->setEnabled(false); + + char a; + int ret = read(sig_interrupt_fds[1], &a, sizeof(a)); + (void)ret; + + sig_interrupt_notifier->setEnabled(true); + + emit SigInterrupt(); +} +#endif // __unix__ + +void MainWindow::PreventOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(HAVE_SDL2) + SDL_DisableScreenSaver(); +#endif +} + +void MainWindow::AllowOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(HAVE_SDL2) + SDL_EnableScreenSaver(); +#endif +} + +bool MainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletParameters params) { + // Shutdown previous session if the emu thread is still active... + if (emu_thread != nullptr) { + ShutdownGame(); + } + + if (!render_window->InitRenderTarget()) { + return false; + } + + QtCommon::system->SetFilesystem(QtCommon::vfs); + + if (params.launch_type == Service::AM::LaunchType::FrontendInitiated) { + QtCommon::system->GetUserChannel().clear(); + } + + QtCommon::system->SetFrontendAppletSet({ + std::make_unique(*this), // Amiibo Settings + (UISettings::values.controller_applet_disabled.GetValue() == true) + ? nullptr + : std::make_unique(*this), // Controller Selector + std::make_unique(*this), // Error Display + nullptr, // Mii Editor + nullptr, // Parental Controls + nullptr, // Photo Viewer + std::make_unique(*this), // Profile Selector + std::make_unique(*this), // Software Keyboard + std::make_unique(*this), // Web Browser + nullptr, // Net Connect + }); + + /** firmware check */ + + if (!QtCommon::Content::CheckGameFirmware(params.program_id, this)) { + return false; + } + + /** Exec */ + const Core::SystemResultStatus result{ + QtCommon::system->Load(*render_window, filename.toStdString(), params)}; + + const auto drd_callout = (UISettings::values.callout_flags.GetValue() & + static_cast(CalloutFlag::DRDDeprecation)) == 0; + + if (result == Core::SystemResultStatus::Success && + QtCommon::system->GetAppLoader().GetFileType() == Loader::FileType::DeconstructedRomDirectory && + drd_callout) { + UISettings::values.callout_flags = UISettings::values.callout_flags.GetValue() | + static_cast(CalloutFlag::DRDDeprecation); + QMessageBox::warning( + this, tr("Warning: Outdated Game Format"), + tr("You are using the deconstructed ROM directory format for this game, which is an " + "outdated format that has been superseded by others such as NCA, NAX, XCI, or " + "NSP. Deconstructed ROM directories lack icons, metadata, and update " + "support.

For an explanation of the various Switch formats Eden supports, check " + "out our " + "wiki. This message will not be shown again.")); + } + + if (result != Core::SystemResultStatus::Success) { + switch (result) { + case Core::SystemResultStatus::ErrorGetLoader: + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); + QMessageBox::critical(this, tr("Error while loading ROM!"), + tr("The ROM format is not supported.")); + break; + case Core::SystemResultStatus::ErrorVideoCore: + QMessageBox::critical( + this, tr("An error occurred initializing the video core."), + tr("Eden has encountered an error while running the video core. " + "This is usually caused by outdated GPU drivers, including integrated ones. " + "Please see the log for more details. " + "For more information on accessing the log, please see the following page: " + "" + "How to Upload the Log File. ")); + break; + default: + if (result > Core::SystemResultStatus::ErrorLoader) { + const u16 loader_id = static_cast(Core::SystemResultStatus::ErrorLoader); + const u16 error_id = static_cast(result) - loader_id; + const std::string error_code = fmt::format("({:04X}-{:04X})", loader_id, error_id); + LOG_CRITICAL(Frontend, "Failed to load ROM! {}", error_code); + + const auto title = + tr("Error while loading ROM! %1", "%1 signifies a numeric error code.") + .arg(QString::fromStdString(error_code)); + const auto description = + tr("%1
Please redump your files or ask on Discord/Revolt for help.", + "%1 signifies an error string.") + .arg(QString::fromStdString( + GetResultStatusString(static_cast(error_id)))); + + QMessageBox::critical(this, title, description); + } else { + QMessageBox::critical( + this, tr("Error while loading ROM!"), + tr("An unknown error occurred. Please see the log for more details.")); + } + break; + } + return false; + } + current_game_path = filename; + + return true; +} + +bool MainWindow::SelectAndSetCurrentUser( + const Core::Frontend::ProfileSelectParameters& parameters) { + QtProfileSelectionDialog dialog(*QtCommon::system, this, parameters); + dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); + dialog.setWindowModality(Qt::WindowModal); + + if (dialog.exec() == QDialog::Rejected) { + return false; + } + + Settings::values.current_user = dialog.GetIndex(); + return true; +} + +void MainWindow::ConfigureFilesystemProvider(const std::string& filepath) { + // Ensure all NCAs are registered before launching the game + const auto file = QtCommon::vfs->OpenFile(filepath, FileSys::OpenMode::Read); + if (!file) { + return; + } + + auto loader = Loader::GetLoader(*QtCommon::system, file); + if (!loader) { + return; + } + + const auto file_type = loader->GetFileType(); + if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { + return; + } + + u64 program_id = 0; + const auto res2 = loader->ReadProgramId(program_id); + if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { + QtCommon::provider->AddEntry(FileSys::TitleType::Application, + FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id, + file); + } else if (res2 == Loader::ResultStatus::Success && + (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { + const auto nsp = file_type == Loader::FileType::NSP + ? std::make_shared(file) + : FileSys::XCI{file}.GetSecurePartitionNSP(); + for (const auto& title : nsp->GetNCAs()) { + for (const auto& entry : title.second) { + QtCommon::provider->AddEntry(entry.first.first, entry.first.second, title.first, + entry.second->GetBaseFile()); + } + } + } +} + +void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletParameters params, + StartGameType type) { + LOG_INFO(Frontend, "Eden starting..."); + + if (params.program_id == 0 || + params.program_id > static_cast(Service::AM::AppletProgramId::MaxProgramId)) { + StoreRecentFile(filename); // Put the filename on top of the list + } + + // Save configurations + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + config->SaveAllValues(); + + u64 title_id{0}; + + last_filename_booted = filename; + + ConfigureFilesystemProvider(filename.toStdString()); + const auto v_file = Core::GetGameFileFromPath(QtCommon::vfs, filename.toUtf8().constData()); + const auto loader = Loader::GetLoader(*QtCommon::system, v_file, params.program_id, params.program_index); + + if (loader != nullptr && loader->ReadProgramId(title_id) == Loader::ResultStatus::Success && + type == StartGameType::Normal) { + // Load per game settings + const auto file_path = + std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())}; + const auto config_file_name = title_id == 0 + ? Common::FS::PathToUTF8String(file_path.filename()) + : fmt::format("{:016X}", title_id); + QtConfig per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + QtCommon::system->HIDCore().ReloadInputDevices(); + QtCommon::system->ApplySettings(); + } + + Settings::LogSettings(); + + if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) { + const Core::Frontend::ProfileSelectParameters parameters{ + .mode = Service::AM::Frontend::UiMode::UserSelector, + .invalid_uid_list = {}, + .display_options = {}, + .purpose = Service::AM::Frontend::UserSelectionPurpose::General, + }; + if (SelectAndSetCurrentUser(parameters) == false) { + return; + } + } + + // If the user specifies -u (successfully) on the cmd line, don't prompt for a user on first + // game startup only. If the user stops emulation and starts a new one, go back to the expected + // behavior of asking. + user_flag_cmd_line = false; + + if (!LoadROM(filename, params)) { + return; + } + + QtCommon::system->SetShuttingDown(false); + game_list->setDisabled(true); + + // Create and start the emulation thread + emu_thread = std::make_unique(*QtCommon::system); + emit EmulationStarting(emu_thread.get()); + emu_thread->start(); + + // Register an ExecuteProgram callback such that Core can execute a sub-program + QtCommon::system->RegisterExecuteProgramCallback( + [this](std::size_t program_index_) { render_window->ExecuteProgram(program_index_); }); + + QtCommon::system->RegisterExitCallback([this] { + emu_thread->ForceStop(); + render_window->Exit(); + }); + + connect(render_window, &GRenderWindow::Closed, this, &MainWindow::OnStopGame); + connect(render_window, &GRenderWindow::MouseActivity, this, &MainWindow::OnMouseActivity); + + connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, + &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + + // Update the GUI + UpdateStatusButtons(); + if (ui->action_Single_Window_Mode->isChecked()) { + game_list->hide(); + game_list_placeholder->hide(); + } + status_bar_update_timer.start(500); + renderer_status_button->setDisabled(true); + refresh_button->setDisabled(true); + + if (UISettings::values.hide_mouse || Settings::values.mouse_panning) { + render_window->installEventFilter(render_window); + render_window->setAttribute(Qt::WA_Hover, true); + } + + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } + + render_window->InitializeCamera(); + + std::string title_name; + std::string title_version; + const auto res = QtCommon::system->GetGameName(title_name); + + const auto metadata = [title_id] { + const FileSys::PatchManager pm(title_id, QtCommon::system->GetFileSystemController(), + QtCommon::system->GetContentProvider()); + return pm.GetControlMetadata(); + }(); + if (metadata.first != nullptr) { + title_version = metadata.first->GetVersionString(); + title_name = metadata.first->GetApplicationName(); + } + if (res != Loader::ResultStatus::Success || title_name.empty()) { + title_name = Common::FS::PathToUTF8String( + std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())} + .filename()); + } + const bool is_64bit = QtCommon::system->Kernel().ApplicationProcess()->Is64Bit(); + const auto instruction_set_suffix = is_64bit ? tr("(64-bit)") : tr("(32-bit)"); + title_name = tr("%1 %2", "%1 is the title name. %2 indicates if the title is 64-bit or 32-bit") + .arg(QString::fromStdString(title_name), instruction_set_suffix) + .toStdString(); + LOG_INFO(Frontend, "Booting game: {:016X} | {} | {}", title_id, title_name, title_version); + const auto gpu_vendor = QtCommon::system->GPU().Renderer().GetDeviceVendor(); + UpdateWindowTitle(title_name, title_version, gpu_vendor); + + loading_screen->Prepare(QtCommon::system->GetAppLoader()); + loading_screen->show(); + + emulation_running = true; + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } + OnStartGame(); +} + +void MainWindow::BootGameFromList(const QString& filename, StartGameType with_config) { + BootGame(filename, ApplicationAppletParameters(), with_config); +} + +bool MainWindow::OnShutdownBegin() { + if (!emulation_running) { + return false; + } + + if (ui->action_Fullscreen->isChecked()) { + HideFullscreen(); + } + + AllowOSSleep(); + + // Disable unlimited frame rate + Settings::values.use_speed_limit.SetValue(true); + + if (QtCommon::system->IsShuttingDown()) { + return false; + } + + QtCommon::system->SetShuttingDown(true); + discord_rpc->Pause(); + + RequestGameExit(); + emu_thread->disconnect(); + emu_thread->SetRunning(true); + + emit EmulationStopping(); + + int shutdown_time = 1000; + + if (QtCommon::system->DebuggerEnabled()) { + shutdown_time = 0; + } else if (QtCommon::system->GetExitLocked()) { + shutdown_time = 5000; + } + + shutdown_timer.setSingleShot(true); + shutdown_timer.start(shutdown_time); + connect(&shutdown_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeExpired); + connect(emu_thread.get(), &QThread::finished, this, &MainWindow::OnEmulationStopped); + + // Disable everything to prevent anything from being triggered here + ui->action_Pause->setEnabled(false); + ui->action_Restart->setEnabled(false); + ui->action_Stop->setEnabled(false); + + return true; +} + +void MainWindow::OnShutdownBeginDialog() { + shutdown_dialog = new OverlayDialog(this, *QtCommon::system, QString{}, tr("Closing software..."), + QString{}, QString{}, Qt::AlignHCenter | Qt::AlignVCenter); + shutdown_dialog->open(); +} + +void MainWindow::OnEmulationStopTimeExpired() { + if (emu_thread) { + emu_thread->ForceStop(); + } +} + +void MainWindow::OnEmulationStopped() { + shutdown_timer.stop(); + if (emu_thread) { + emu_thread->disconnect(); + emu_thread->wait(); + emu_thread.reset(); + } + + if (shutdown_dialog) { + shutdown_dialog->deleteLater(); + shutdown_dialog = nullptr; + } + + emulation_running = false; + + discord_rpc->Update(); + +#ifdef __linux__ + Common::Linux::StopGamemode(); +#endif + + // The emulation is stopped, so closing the window or not does not matter anymore + disconnect(render_window, &GRenderWindow::Closed, this, &MainWindow::OnStopGame); + + // Update the GUI + UpdateMenuState(); + + render_window->hide(); + loading_screen->hide(); + loading_screen->Clear(); + if (game_list->IsEmpty()) { + game_list_placeholder->show(); + } else { + game_list->show(); + } + game_list->SetFilterFocus(); + tas_label->clear(); + input_subsystem->GetTas()->Stop(); + OnTasStateChanged(); + render_window->FinalizeCamera(); + + QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::None); + + // Enable all controllers + QtCommon::system->HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All}); + + render_window->removeEventFilter(render_window); + render_window->setAttribute(Qt::WA_Hover, false); + + UpdateWindowTitle(); + + // Disable status bar updates + status_bar_update_timer.stop(); + shader_building_label->setVisible(false); + res_scale_label->setVisible(false); + emu_speed_label->setVisible(false); + game_fps_label->setVisible(false); + emu_frametime_label->setVisible(false); + renderer_status_button->setEnabled(!UISettings::values.has_broken_vulkan); + refresh_button->setEnabled(true); + + if (!firmware_label->text().isEmpty()) { + firmware_label->setVisible(true); + } + + current_game_path.clear(); + + // When closing the game, destroy the GLWindow to clear the context after the game is closed + render_window->ReleaseRenderTarget(); + + // Enable game list + game_list->setEnabled(true); + + Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn()); + QtCommon::system->HIDCore().ReloadInputDevices(); + UpdateStatusButtons(); +} + +void MainWindow::ShutdownGame() { + if (!emulation_running) { + return; + } + + // TODO(crueter): make this common as well (frontend_common?) + play_time_manager->Stop(); + OnShutdownBegin(); + OnEmulationStopTimeExpired(); + OnEmulationStopped(); +} + +void MainWindow::StoreRecentFile(const QString& filename) { + UISettings::values.recent_files.prepend(filename); + UISettings::values.recent_files.removeDuplicates(); + while (UISettings::values.recent_files.size() > max_recent_files_item) { + UISettings::values.recent_files.removeLast(); + } + + UpdateRecentFiles(); +} + +void MainWindow::UpdateRecentFiles() { + const int num_recent_files = + (std::min)(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); + + for (int i = 0; i < num_recent_files; i++) { + const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( + QFileInfo(UISettings::values.recent_files[i]).fileName()); + actions_recent_files[i]->setText(text); + actions_recent_files[i]->setData(UISettings::values.recent_files[i]); + actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); + actions_recent_files[i]->setVisible(true); + } + + for (int j = num_recent_files; j < max_recent_files_item; ++j) { + actions_recent_files[j]->setVisible(false); + } + + // Enable the recent files menu if the list isn't empty + ui->menu_recent_files->setEnabled(num_recent_files != 0); +} + +void MainWindow::OnGameListLoadFile(QString game_path, u64 program_id) { + auto params = ApplicationAppletParameters(); + params.program_id = program_id; + + BootGame(game_path, params); +} + +// TODO(crueter): Common profile selector +void MainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target, + const std::string& game_path) { + std::filesystem::path path; + QString open_target; + + const auto [user_save_size, device_save_size] = [&game_path, &program_id] { + const FileSys::PatchManager pm{program_id, QtCommon::system->GetFileSystemController(), + QtCommon::system->GetContentProvider()}; + const auto control = pm.GetControlMetadata().first; + if (control != nullptr) { + return std::make_pair(control->GetDefaultNormalSaveSize(), + control->GetDeviceSaveDataSize()); + } else { + const auto file = Core::GetGameFileFromPath(QtCommon::vfs, game_path); + const auto loader = Loader::GetLoader(*QtCommon::system, file); + + FileSys::NACP nacp{}; + loader->ReadControlData(nacp); + return std::make_pair(nacp.GetDefaultNormalSaveSize(), nacp.GetDeviceSaveDataSize()); + } + }(); + + const bool has_user_save{user_save_size > 0}; + const bool has_device_save{device_save_size > 0}; + + ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?"); + + switch (target) { + case GameListOpenTarget::SaveData: { + open_target = tr("Save Data"); + const auto nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir); + auto vfs_nand_dir = + QtCommon::vfs->OpenDirectory(Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read); + + if (has_user_save) { + // User save data + const auto user_id = GetProfileID(); + assert(user_id); + + const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( + {}, vfs_nand_dir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, + program_id, user_id->AsU128(), 0); + + path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path); + } else { + // Device save data + const auto device_save_data_path = FileSys::SaveDataFactory::GetFullPath( + {}, vfs_nand_dir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, + program_id, {}, 0); + + path = Common::FS::ConcatPathSafe(nand_dir, device_save_data_path); + } + + if (!Common::FS::CreateDirs(path)) { + LOG_ERROR(Frontend, "Unable to create the directories for save data"); + } + + break; + } + case GameListOpenTarget::ModData: { + open_target = tr("Mod Data"); + path = Common::FS::GetEdenPath(Common::FS::EdenPath::LoadDir) / + fmt::format("{:016X}", program_id); + break; + } + default: + UNIMPLEMENTED(); + break; + } + + const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(path)); + const QDir dir(qpath); + if (!dir.exists()) { + QMessageBox::warning(this, tr("Error Opening %1 Folder").arg(open_target), + tr("Folder does not exist!")); + return; + } + LOG_INFO(Frontend, "Opening {} path for program_id={:016x}", open_target.toStdString(), + program_id); + QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); +} + +static bool RomFSRawCopy(size_t total_size, size_t& read_size, QProgressDialog& dialog, + const FileSys::VirtualDir& src, const FileSys::VirtualDir& dest, + bool full) { + if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) + return false; + if (dialog.wasCanceled()) + return false; + + std::vector buffer(CopyBufferSize); + auto last_timestamp = std::chrono::steady_clock::now(); + + const auto QtRawCopy = [&](const FileSys::VirtualFile& src_file, + const FileSys::VirtualFile& dest_file) { + if (src_file == nullptr || dest_file == nullptr) { + return false; + } + if (!dest_file->Resize(src_file->GetSize())) { + return false; + } + + for (std::size_t i = 0; i < src_file->GetSize(); i += buffer.size()) { + if (dialog.wasCanceled()) { + dest_file->Resize(0); + return false; + } + + using namespace std::literals::chrono_literals; + const auto new_timestamp = std::chrono::steady_clock::now(); + + if ((new_timestamp - last_timestamp) > 33ms) { + last_timestamp = new_timestamp; + dialog.setValue( + static_cast((std::min)(read_size, total_size) * 100 / total_size)); + QCoreApplication::processEvents(); + } + + const auto read = src_file->Read(buffer.data(), buffer.size(), i); + dest_file->Write(buffer.data(), read, i); + + read_size += read; + } + + return true; + }; + + if (full) { + for (const auto& file : src->GetFiles()) { + const auto out = VfsDirectoryCreateFileWrapper(dest, file->GetName()); + if (!QtRawCopy(file, out)) + return false; + } + } + + for (const auto& dir : src->GetSubdirectories()) { + const auto out = dest->CreateSubdirectory(dir->GetName()); + if (!RomFSRawCopy(total_size, read_size, dialog, dir, out, full)) + return false; + } + + return true; +} + +// TODO(crueter): All this can be transfered to qt_common +// Aldoe I need to decide re: message boxes for QML +// translations_common? strings_common? qt_strings? who knows +void MainWindow::OnGameListRemoveInstalledEntry(u64 program_id, QtCommon::Game::InstalledEntryType type) { + const QString entry_question = [type] { + switch (type) { + case QtCommon::Game::InstalledEntryType::Game: + return tr("Remove Installed Game Contents?"); + case QtCommon::Game::InstalledEntryType::Update: + return tr("Remove Installed Game Update?"); + case QtCommon::Game::InstalledEntryType::AddOnContent: + return tr("Remove Installed Game DLC?"); + default: + return QStringLiteral("Remove Installed Game ?"); + } + }(); + + if (!question(this, tr("Remove Entry"), entry_question, QMessageBox::Yes | QMessageBox::No, + QMessageBox::No)) { + return; + } + + // TODO(crueter): move this to QtCommon (populate async?) + switch (type) { + case QtCommon::Game::InstalledEntryType::Game: + QtCommon::Game::RemoveBaseContent(program_id, type); + [[fallthrough]]; + case QtCommon::Game::InstalledEntryType::Update: + QtCommon::Game::RemoveUpdateContent(program_id, type); + if (type != QtCommon::Game::InstalledEntryType::Game) { + break; + } + [[fallthrough]]; + case QtCommon::Game::InstalledEntryType::AddOnContent: + QtCommon::Game::RemoveAddOnContent(program_id, type); + break; + } + Common::FS::RemoveDirRecursively(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / + "game_list"); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + +void MainWindow::OnGameListRemoveFile(u64 program_id, QtCommon::Game::GameListRemoveTarget target, + const std::string& game_path) { + const QString question = [target] { + switch (target) { + case QtCommon::Game::GameListRemoveTarget::GlShaderCache: + return tr("Delete OpenGL Transferable Shader Cache?"); + case QtCommon::Game::GameListRemoveTarget::VkShaderCache: + return tr("Delete Vulkan Transferable Shader Cache?"); + case QtCommon::Game::GameListRemoveTarget::AllShaderCache: + return tr("Delete All Transferable Shader Caches?"); + case QtCommon::Game::GameListRemoveTarget::CustomConfiguration: + return tr("Remove Custom Game Configuration?"); + case QtCommon::Game::GameListRemoveTarget::CacheStorage: + return tr("Remove Cache Storage?"); + default: + return QString{}; + } + }(); + + if (!MainWindow::question(this, tr("Remove File"), question, + QMessageBox::Yes | QMessageBox::No, QMessageBox::No)) { + return; + } + + switch (target) { + case QtCommon::Game::GameListRemoveTarget::VkShaderCache: + QtCommon::Game::RemoveVulkanDriverPipelineCache(program_id); + [[fallthrough]]; + case QtCommon::Game::GameListRemoveTarget::GlShaderCache: + QtCommon::Game::RemoveTransferableShaderCache(program_id, target); + break; + case QtCommon::Game::GameListRemoveTarget::AllShaderCache: + QtCommon::Game::RemoveAllTransferableShaderCaches(program_id); + break; + case QtCommon::Game::GameListRemoveTarget::CustomConfiguration: + QtCommon::Game::RemoveCustomConfiguration(program_id, game_path); + break; + case QtCommon::Game::GameListRemoveTarget::CacheStorage: + QtCommon::Game::RemoveCacheStorage(program_id); + break; + } +} + +void MainWindow::OnGameListSetPlayTime(u64 program_id) { + const u64 current_play_time = play_time_manager->GetPlayTime(program_id); + + SetPlayTimeDialog dialog(this, current_play_time); + + if (dialog.exec() == QDialog::Accepted) { + const u64 total_seconds = dialog.GetTotalSeconds(); + play_time_manager->SetPlayTime(program_id, total_seconds); + game_list->PopulateAsync(UISettings::values.game_dirs); + } +} + + +void MainWindow::OnGameListRemovePlayTimeData(u64 program_id) { + if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) != QMessageBox::Yes) { + return; + } + + play_time_manager->ResetProgramPlayTime(program_id); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + +void MainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_path, + DumpRomFSTarget target) { + const auto failed = [this] { + QMessageBox::warning(this, tr("RomFS Extraction Failed!"), + tr("There was an error copying the RomFS files or the user " + "cancelled the operation.")); + }; + + const auto loader = + Loader::GetLoader(*QtCommon::system, QtCommon::vfs->OpenFile(game_path, FileSys::OpenMode::Read)); + if (loader == nullptr) { + failed(); + return; + } + + FileSys::VirtualFile packed_update_raw{}; + loader->ReadUpdateRaw(packed_update_raw); + + const auto& installed = QtCommon::system->GetContentProvider(); + + u64 title_id{}; + u8 raw_type{}; + if (!SelectRomFSDumpTarget(installed, program_id, &title_id, &raw_type)) { + failed(); + return; + } + + const auto type = static_cast(raw_type); + const auto base_nca = installed.GetEntry(title_id, type); + if (!base_nca) { + failed(); + return; + } + + const FileSys::NCA update_nca{packed_update_raw, nullptr}; + if (type != FileSys::ContentRecordType::Program || + update_nca.GetStatus() != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS || + update_nca.GetTitleId() != FileSys::GetUpdateTitleID(title_id)) { + packed_update_raw = {}; + } + + const auto base_romfs = base_nca->GetRomFS(); + const auto dump_dir = + target == DumpRomFSTarget::Normal + ? Common::FS::GetEdenPath(Common::FS::EdenPath::DumpDir) + : Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir) / "atmosphere" / "contents"; + const auto romfs_dir = fmt::format("{:016X}/romfs", title_id); + + const auto path = Common::FS::PathToUTF8String(dump_dir / romfs_dir); + + const FileSys::PatchManager pm{title_id, QtCommon::system->GetFileSystemController(), installed}; + auto romfs = pm.PatchRomFS(base_nca.get(), base_romfs, type, packed_update_raw, false); + + const auto out = VfsFilesystemCreateDirectoryWrapper(path, FileSys::OpenMode::ReadWrite); + + if (out == nullptr) { + failed(); + QtCommon::vfs->DeleteDirectory(path); + return; + } + + bool ok = false; + const QStringList selections{tr("Full"), tr("Skeleton")}; + const auto res = QInputDialog::getItem( + this, tr("Select RomFS Dump Mode"), + tr("Please select the how you would like the RomFS dumped.
Full will copy all of the " + "files into the new directory while
skeleton will only create the directory " + "structure."), + selections, 0, false, &ok); + if (!ok) { + failed(); + QtCommon::vfs->DeleteDirectory(path); + return; + } + + const auto extracted = FileSys::ExtractRomFS(romfs); + if (extracted == nullptr) { + failed(); + return; + } + + const auto full = res == selections.constFirst(); + + // The expected required space is the size of the RomFS + 1 GiB + const auto minimum_free_space = romfs->GetSize() + 0x40000000; + + if (full && Common::FS::GetFreeSpaceSize(path) < minimum_free_space) { + QMessageBox::warning(this, tr("RomFS Extraction Failed!"), + tr("There is not enough free space at %1 to extract the RomFS. Please " + "free up space or select a different dump directory at " + "Emulation > Configure > System > Filesystem > Dump Root") + .arg(QString::fromStdString(path))); + return; + } + + QProgressDialog progress(tr("Extracting RomFS..."), tr("Cancel"), 0, 100, this); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(100); + progress.setAutoClose(false); + progress.setAutoReset(false); + + size_t read_size = 0; + + if (RomFSRawCopy(romfs->GetSize(), read_size, progress, extracted, out, full)) { + progress.close(); + QMessageBox::information(this, tr("RomFS Extraction Succeeded!"), + tr("The operation completed successfully.")); + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(path))); + } else { + progress.close(); + failed(); + QtCommon::vfs->DeleteDirectory(path); + } +} + +// END +void MainWindow::OnGameListVerifyIntegrity(const std::string& game_path) { + QtCommon::Content::VerifyGameContents(game_path); +} + +void MainWindow::OnGameListCopyTID(u64 program_id) { + QClipboard* clipboard = QGuiApplication::clipboard(); + clipboard->setText(QString::fromStdString(fmt::format("{:016X}", program_id))); +} + +void MainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list) { + const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + QString directory; + if (it != compatibility_list.end()) { + directory = it->second.second; + } + + QDesktopServices::openUrl( + QUrl(QStringLiteral("https://eden-emulator.github.io/game/") + directory)); +} + +void MainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, + const QtCommon::Game::ShortcutTarget target) { + // Create shortcut + std::string arguments = fmt::format("-g \"{:s}\"", game_path); + + QtCommon::Game::CreateShortcut(game_path, program_id, "", target, arguments, true); +} + +void MainWindow::OnGameListOpenDirectory(const QString& directory) { + // TODO(crueter): QtCommon + std::filesystem::path fs_path; + if (directory == QStringLiteral("SDMC")) { + fs_path = + Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir) / "Nintendo/Contents/registered"; + } else if (directory == QStringLiteral("UserNAND")) { + fs_path = + Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) / "user/Contents/registered"; + } else if (directory == QStringLiteral("SysNAND")) { + fs_path = + Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) / "system/Contents/registered"; + } else { + fs_path = directory.toStdString(); + } + + const auto qt_path = QString::fromStdString(Common::FS::PathToUTF8String(fs_path)); + + if (!Common::FS::IsDir(fs_path)) { + QMessageBox::critical(this, tr("Error Opening %1").arg(qt_path), + tr("Folder does not exist!")); + return; + } + + QDesktopServices::openUrl(QUrl::fromLocalFile(qt_path)); +} + +void MainWindow::OnGameListAddDirectory() { + const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (dir_path.isEmpty()) { + return; + } + + UISettings::GameDir game_dir{dir_path.toStdString(), false, true}; + if (!UISettings::values.game_dirs.contains(game_dir)) { + UISettings::values.game_dirs.append(game_dir); + game_list->PopulateAsync(UISettings::values.game_dirs); + } else { + LOG_WARNING(Frontend, "Selected directory is already in the game list"); + } + + OnSaveConfig(); +} + +void MainWindow::OnGameListShowList(bool show) { + if (emulation_running && ui->action_Single_Window_Mode->isChecked()) + return; + game_list->setVisible(show); + game_list_placeholder->setVisible(!show); +}; + +void MainWindow::OnGameListOpenPerGameProperties(const std::string& file) { + u64 title_id{}; + const auto v_file = Core::GetGameFileFromPath(QtCommon::vfs, file); + const auto loader = Loader::GetLoader(*QtCommon::system, v_file); + + if (loader == nullptr || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { + QMessageBox::information(this, tr("Properties"), + tr("The game properties could not be loaded.")); + return; + } + + OpenPerGameConfiguration(title_id, file); +} + +void MainWindow::OnLinkToRyujinx(const u64& program_id) +{ + namespace fs = std::filesystem; + + fs::path ryu_dir; + + // find an existing Ryujinx linked path in config.ini; if it exists, use it as a "hint" + // If it's not defined in config.ini, use default + const fs::path existing_path = + UISettings::values.ryujinx_link_paths + .value(program_id, QDir(Common::FS::GetLegacyPath(Common::FS::RyujinxDir))) + .filesystemAbsolutePath(); + + // this function also prompts the user to manually specify a portable location + ryu_dir = QtCommon::FS::GetRyujinxSavePath(existing_path, program_id); + + if (ryu_dir.empty()) return; + + const std::string user_id = GetProfileIDString(); + if (user_id.empty()) return; + + const std::string hex_program = fmt::format("{:016X}", program_id); + + const fs::path eden_dir = FrontendCommon::DataManager::GetDataDir( + FrontendCommon::DataManager::DataDir::Saves, user_id) / + hex_program; + + + // CheckUnlink basically just checks to see if one or both are linked, and prompts the user to + // unlink if this is the case. + // If it returns false, neither dir is linked so it's fine to continue + if (!QtCommon::FS::CheckUnlink(eden_dir, ryu_dir)) { + RyujinxDialog dialog(eden_dir, ryu_dir, this); + if (dialog.exec() == QDialog::Accepted) { + UISettings::values.ryujinx_link_paths.insert( + program_id, + QString::fromStdString(Common::FS::GetRyuPathFromSavePath(ryu_dir).string())); + } + } else { + UISettings::values.ryujinx_link_paths.remove(program_id); + } + + config->SaveAllValues(); +} + +void MainWindow::OnMenuLoadFile() { + if (is_load_file_select_active) { + return; + } + + is_load_file_select_active = true; + const QString extensions = + QStringLiteral("*.") + .append(GameList::supported_file_extensions.join(QStringLiteral(" *."))) + .append(QStringLiteral(" main")); + const QString file_filter = tr("Switch Executable (%1);;All Files (*.*)", + "%1 is an identifier for the Switch executable file extensions.") + .arg(extensions); + const QString filename = QFileDialog::getOpenFileName( + this, tr("Load File"), QString::fromStdString(UISettings::values.roms_path), file_filter); + is_load_file_select_active = false; + + if (filename.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); + BootGame(filename, ApplicationAppletParameters()); +} + +void MainWindow::OnMenuLoadFolder() { + const QString dir_path = + QFileDialog::getExistingDirectory(this, tr("Open Extracted ROM Directory")); + + if (dir_path.isNull()) { + return; + } + + const QDir dir{dir_path}; + const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); + if (matching_main.size() == 1) { + BootGame(dir.path() + QDir::separator() + matching_main[0], ApplicationAppletParameters()); + } else { + QMessageBox::warning(this, tr("Invalid Directory Selected"), + tr("The directory you have selected does not contain a 'main' file.")); + } +} + +void MainWindow::IncrementInstallProgress() { + install_progress->setValue(install_progress->value() + 1); +} + +void MainWindow::OnMenuInstallToNAND() { + const QString file_filter = + tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive " + "(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge " + "Image (*.xci)"); + + QStringList filenames = QFileDialog::getOpenFileNames( + this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path), + file_filter); + + if (filenames.isEmpty()) { + return; + } + + InstallDialog installDialog(this, filenames); + if (installDialog.exec() == QDialog::Rejected) { + return; + } + + const QStringList files = installDialog.GetFiles(); + + if (files.isEmpty()) { + return; + } + + // Save folder location of the first selected file + UISettings::values.roms_path = QFileInfo(filenames[0]).path().toStdString(); + + int remaining = filenames.size(); + + // This would only overflow above 2^51 bytes (2.252 PB) + int total_size = 0; + for (const QString& file : files) { + total_size += static_cast(QFile(file).size() / CopyBufferSize); + } + if (total_size < 0) { + LOG_CRITICAL(Frontend, "Attempting to install too many files, aborting."); + return; + } + + QStringList new_files{}; // Newly installed files that do not yet exist in the NAND + QStringList overwritten_files{}; // Files that overwrote those existing in the NAND + QStringList failed_files{}; // Files that failed to install due to errors + bool detected_base_install{}; // Whether a base game was attempted to be installed + + ui->action_Install_File_NAND->setEnabled(false); + + install_progress = new QProgressDialog(QString{}, tr("Cancel"), 0, total_size, this); + install_progress->setWindowFlags(windowFlags() & ~Qt::WindowMaximizeButtonHint); + install_progress->setAttribute(Qt::WA_DeleteOnClose, true); + install_progress->setFixedWidth(installDialog.GetMinimumWidth() + 40); + install_progress->show(); + + for (const QString& file : files) { + install_progress->setWindowTitle(tr("%n file(s) remaining", "", remaining)); + install_progress->setLabelText( + tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName())); + + QFuture future; + ContentManager::InstallResult result; + + if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { + const auto progress_callback = [this](size_t size, size_t progress) { + emit UpdateInstallProgress(); + if (install_progress->wasCanceled()) { + return true; + } + return false; + }; + future = QtConcurrent::run([&file, progress_callback] { + return ContentManager::InstallNSP(*QtCommon::system, *QtCommon::vfs, file.toStdString(), + progress_callback); + }); + + while (!future.isFinished()) { + QCoreApplication::processEvents(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + result = future.result(); + + } else { + result = InstallNCA(file); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + switch (result) { + case ContentManager::InstallResult::Success: + new_files.append(QFileInfo(file).fileName()); + break; + case ContentManager::InstallResult::Overwrite: + overwritten_files.append(QFileInfo(file).fileName()); + break; + case ContentManager::InstallResult::Failure: + failed_files.append(QFileInfo(file).fileName()); + break; + case ContentManager::InstallResult::BaseInstallAttempted: + failed_files.append(QFileInfo(file).fileName()); + detected_base_install = true; + break; + } + + --remaining; + } + + install_progress->close(); + + if (detected_base_install) { + QMessageBox::warning( + this, tr("Install Results"), + tr("To avoid possible conflicts, we discourage users from installing base games to the " + "NAND.\nPlease, only use this feature to install updates and DLC.")); + } + + const QString install_results = + (new_files.isEmpty() ? QString{} + : tr("%n file(s) were newly installed\n", "", new_files.size())) + + (overwritten_files.isEmpty() + ? QString{} + : tr("%n file(s) were overwritten\n", "", overwritten_files.size())) + + (failed_files.isEmpty() ? QString{} + : tr("%n file(s) failed to install\n", "", failed_files.size())); + + QMessageBox::information(this, tr("Install Results"), install_results); + Common::FS::RemoveDirRecursively(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / + "game_list"); + game_list->PopulateAsync(UISettings::values.game_dirs); + ui->action_Install_File_NAND->setEnabled(true); +} + +ContentManager::InstallResult MainWindow::InstallNCA(const QString& filename) { + const QStringList tt_options{tr("System Application"), + tr("System Archive"), + tr("System Application Update"), + tr("Firmware Package (Type A)"), + tr("Firmware Package (Type B)"), + tr("Game"), + tr("Game Update"), + tr("Game DLC"), + tr("Delta Title")}; + bool ok; + const auto item = QInputDialog::getItem( + this, tr("Select NCA Install Type..."), + tr("Please select the type of title you would like to install this NCA as:\n(In " + "most instances, the default 'Game' is fine.)"), + tt_options, 5, false, &ok); + + auto index = tt_options.indexOf(item); + if (!ok || index == -1) { + QMessageBox::warning(this, tr("Failed to Install"), + tr("The title type you selected for the NCA is invalid.")); + return ContentManager::InstallResult::Failure; + } + + // If index is equal to or past Game, add the jump in TitleType. + if (index >= 5) { + index += static_cast(FileSys::TitleType::Application) - + static_cast(FileSys::TitleType::FirmwarePackageB); + } + + const bool is_application = index >= static_cast(FileSys::TitleType::Application); + const auto& fs_controller = QtCommon::system->GetFileSystemController(); + auto* registered_cache = is_application ? fs_controller.GetUserNANDContents() + : fs_controller.GetSystemNANDContents(); + + const auto progress_callback = [this](size_t size, size_t progress) { + emit UpdateInstallProgress(); + if (install_progress->wasCanceled()) { + return true; + } + return false; + }; + return ContentManager::InstallNCA(*QtCommon::vfs, filename.toStdString(), *registered_cache, + static_cast(index), progress_callback); +} + +void MainWindow::OnMenuRecentFile() { + QAction* action = qobject_cast(sender()); + assert(action); + + const QString filename = action->data().toString(); + if (QFileInfo::exists(filename)) { + BootGame(filename, ApplicationAppletParameters()); + } else { + // Display an error message and remove the file from the list. + QMessageBox::information(this, tr("File not found"), + tr("File \"%1\" not found").arg(filename)); + + UISettings::values.recent_files.removeOne(filename); + UpdateRecentFiles(); + } +} + +void MainWindow::OnStartGame() { + PreventOSSleep(); + + emu_thread->SetRunning(true); + + UpdateMenuState(); + OnTasStateChanged(); + + play_time_manager->SetProgramId(QtCommon::system->GetApplicationProcessProgramID()); + play_time_manager->Start(); + + discord_rpc->Update(); + +#ifdef __linux__ + Common::Linux::StartGamemode(); +#endif +} + +void MainWindow::OnRestartGame() { + if (!QtCommon::system->IsPoweredOn()) { + return; + } + + if (ConfirmShutdownGame()) { + // Make a copy since ShutdownGame edits game_path + const auto current_game = QString(current_game_path); + ShutdownGame(); + BootGame(current_game, ApplicationAppletParameters()); + } +} + +void MainWindow::OnPauseGame() { + emu_thread->SetRunning(false); + play_time_manager->Stop(); + UpdateMenuState(); + AllowOSSleep(); + +#ifdef __linux__ + Common::Linux::StopGamemode(); +#endif +} + +void MainWindow::OnPauseContinueGame() { + if (emulation_running) { + if (emu_thread->IsRunning()) { + OnPauseGame(); + } else { + OnStartGame(); + } + } +} + +void MainWindow::OnStopGame() { + if (ConfirmShutdownGame()) { + play_time_manager->Stop(); + // Update game list to show new play time + game_list->PopulateAsync(UISettings::values.game_dirs); + if (OnShutdownBegin()) { + OnShutdownBeginDialog(); + } else { + OnEmulationStopped(); + } + } +} + +bool MainWindow::ConfirmShutdownGame() { + if (UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Always) { + if (QtCommon::system->GetExitLocked()) { + if (!ConfirmForceLockedExit()) { + return false; + } + } else { + if (!ConfirmChangeGame()) { + return false; + } + } + } else { + if (UISettings::values.confirm_before_stopping.GetValue() == + ConfirmStop::Ask_Based_On_Game && + QtCommon::system->GetExitLocked()) { + if (!ConfirmForceLockedExit()) { + return false; + } + } + } + return true; +} + +void MainWindow::OnLoadComplete() { + loading_screen->OnLoadComplete(); +} + +void MainWindow::OnExecuteProgram(std::size_t program_index) { + ShutdownGame(); + + auto params = ApplicationAppletParameters(); + params.program_index = static_cast(program_index); + params.launch_type = Service::AM::LaunchType::ApplicationInitiated; + BootGame(last_filename_booted, params); +} + +void MainWindow::OnExit() { + ShutdownGame(); +} + +void MainWindow::OnSaveConfig() { + QtCommon::system->ApplySettings(); + config->SaveAllValues(); +} + +void MainWindow::ErrorDisplayDisplayError(QString error_code, QString error_text) { + error_applet = new OverlayDialog(render_window, *QtCommon::system, error_code, error_text, QString{}, + tr("OK"), Qt::AlignLeft | Qt::AlignVCenter); + SCOPE_EXIT { + error_applet->deleteLater(); + error_applet = nullptr; + }; + error_applet->exec(); + + emit ErrorDisplayFinished(); +} + +void MainWindow::ErrorDisplayRequestExit() { + if (error_applet) { + error_applet->reject(); + } +} + +void MainWindow::OnMenuReportCompatibility() { +#if defined(ARCHITECTURE_x86_64) && !defined(__APPLE__) + const auto& caps = Common::GetCPUCaps(); + const bool has_fma = caps.fma || caps.fma4; + const auto processor_count = std::thread::hardware_concurrency(); + const bool has_4threads = processor_count == 0 || processor_count >= 4; + const bool has_8gb_ram = Common::GetMemInfo().TotalPhysicalMemory >= 8_GiB; + const bool has_broken_vulkan = UISettings::values.has_broken_vulkan; + + if (!has_fma || !has_4threads || !has_8gb_ram || has_broken_vulkan) { + QMessageBox::critical(this, tr("Hardware requirements not met"), + tr("Your system does not meet the recommended hardware requirements. " + "Compatibility reporting has been disabled.")); + return; + } + + if (!Settings::values.eden_token.GetValue().empty() && + !Settings::values.eden_username.GetValue().empty()) { + } else { + QMessageBox::critical( + this, tr("Missing yuzu Account"), + tr("In order to submit a game compatibility test case, you must set up your web token " + "and " + "username.

To link your eden account, go to Emulation > Configuration " + "> " + "Web.")); + } +#else + QMessageBox::critical(this, tr("Hardware requirements not met"), + tr("Your system does not meet the recommended hardware requirements. " + "Compatibility reporting has been disabled.")); +#endif +} + +void MainWindow::OpenURL(const QUrl& url) { + const bool open = QDesktopServices::openUrl(url); + if (!open) { + QMessageBox::warning(this, tr("Error opening URL"), + tr("Unable to open the URL \"%1\".").arg(url.toString())); + } +} + +void MainWindow::OnOpenModsPage() { + OpenURL(QUrl(QStringLiteral("https://github.com/eden-emulator/yuzu-mod-archive"))); +} + +void MainWindow::OnOpenQuickstartGuide() { + OpenURL(QUrl(QStringLiteral("https://yuzu-mirror.github.io/help/quickstart/"))); +} + +void MainWindow::OnOpenFAQ() { + OpenURL(QUrl(QStringLiteral("https://yuzu-mirror.github.io/help"))); +} + +void MainWindow::ToggleFullscreen() { + if (!emulation_running) { + return; + } + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } else { + HideFullscreen(); + } +} + +// We're going to return the screen that the given window has the most pixels on +static QScreen* GuessCurrentScreen(QWidget* window) { + const QList screens = QGuiApplication::screens(); + return *std::max_element( + screens.cbegin(), screens.cend(), [window](const QScreen* left, const QScreen* right) { + const QSize left_size = left->geometry().intersected(window->geometry()).size(); + const QSize right_size = right->geometry().intersected(window->geometry()).size(); + return (left_size.height() * left_size.width()) < + (right_size.height() * right_size.width()); + }); +} + +bool MainWindow::UsingExclusiveFullscreen() { + return Settings::values.fullscreen_mode.GetValue() == Settings::FullscreenMode::Exclusive || + QGuiApplication::platformName() == QStringLiteral("wayland") || + QGuiApplication::platformName() == QStringLiteral("wayland-egl"); +} + +void MainWindow::ShowFullscreen() { + const auto show_fullscreen = [this](QWidget* window) { + if (UsingExclusiveFullscreen()) { + window->showFullScreen(); + return; + } + window->hide(); + window->setWindowFlags(window->windowFlags() | Qt::FramelessWindowHint); + const auto screen_geometry = GuessCurrentScreen(window)->geometry(); + window->setGeometry(screen_geometry.x(), screen_geometry.y(), screen_geometry.width(), + screen_geometry.height() + 1); + window->raise(); + window->showNormal(); + }; + + if (ui->action_Single_Window_Mode->isChecked()) { + UISettings::values.geometry = saveGeometry(); + + ui->menubar->hide(); + statusBar()->hide(); + + show_fullscreen(this); + } else { + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + show_fullscreen(render_window); + } +} + +void MainWindow::HideFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + if (UsingExclusiveFullscreen()) { + showNormal(); + restoreGeometry(UISettings::values.geometry); + } else { + hide(); + setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); + restoreGeometry(UISettings::values.geometry); + raise(); + show(); + } + + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + ui->menubar->show(); + } else { + if (UsingExclusiveFullscreen()) { + render_window->showNormal(); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + } else { + render_window->hide(); + render_window->setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + render_window->raise(); + render_window->show(); + } + } +} + +void MainWindow::ToggleWindowMode() { + if (ui->action_Single_Window_Mode->isChecked()) { + // Render in the main window... + render_window->BackupGeometry(); + ui->horizontalLayout->addWidget(render_window); + render_window->setFocusPolicy(Qt::StrongFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->setFocus(); + game_list->hide(); + } + + } else { + // Render in a separate window... + ui->horizontalLayout->removeWidget(render_window); + render_window->setParent(nullptr); + render_window->setFocusPolicy(Qt::NoFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->RestoreGeometry(); + game_list->show(); + } + } +} + +void MainWindow::ResetWindowSize(u32 width, u32 height) { + const auto aspect_ratio = Layout::EmulationAspectRatio(Settings::values.aspect_ratio.GetValue(), float(height) / width); + if (!ui->action_Single_Window_Mode->isChecked()) { + render_window->resize(height / aspect_ratio, height); + } else { + const bool show_status_bar = ui->action_Show_Status_Bar->isChecked(); + const auto status_bar_height = show_status_bar ? statusBar()->height() : 0; + resize(height / aspect_ratio, height + menuBar()->height() + status_bar_height); + } +} + +void MainWindow::ResetWindowSize720() { + ResetWindowSize(Layout::ScreenUndocked::Width, Layout::ScreenUndocked::Height); +} + +void MainWindow::ResetWindowSize900() { + ResetWindowSize(1600U, 900U); +} + +void MainWindow::ResetWindowSize1080() { + ResetWindowSize(Layout::ScreenDocked::Width, Layout::ScreenDocked::Height); +} + +void MainWindow::OnConfigure() { + const auto old_theme = UISettings::values.theme; + const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); + const auto old_language_index = Settings::values.language_index.GetValue(); +#ifdef __linux__ + const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); +#endif +#ifdef __unix__ + const bool old_force_x11 = Settings::values.gui_force_x11.GetValue(); +#endif + + Settings::SetConfiguringGlobal(true); + ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), + vk_device_records, *QtCommon::system, + !multiplayer_state->IsHostingPublicRoom()); + connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this, + &MainWindow::OnLanguageChanged); + + const auto result = configure_dialog.exec(); + if (result != QDialog::Accepted && !UISettings::values.configuration_applied && + !UISettings::values.reset_to_defaults) { + // Runs if the user hit Cancel or closed the window, and did not ever press the Apply button + // or `Reset to Defaults` button + return; + } else if (result == QDialog::Accepted) { + // Only apply new changes if user hit Okay + // This is here to avoid applying changes if the user hit Apply, made some changes, then hit + // Cancel + configure_dialog.ApplyConfiguration(); + } else if (UISettings::values.reset_to_defaults) { + LOG_INFO(Frontend, "Resetting all settings to defaults"); + if (!Common::FS::RemoveFile(config->GetConfigFilePath())) { + LOG_WARNING(Frontend, "Failed to remove configuration file"); + } + if (!Common::FS::RemoveDirContentsRecursively( + Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir) / "custom")) { + LOG_WARNING(Frontend, "Failed to remove custom configuration files"); + } + if (!Common::FS::RemoveDirRecursively( + Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list")) { + LOG_WARNING(Frontend, "Failed to remove game metadata cache files"); + } + + // Explicitly save the game directories, since reinitializing config does not explicitly do + // so. + QVector old_game_dirs = std::move(UISettings::values.game_dirs); + QVector old_favorited_ids = std::move(UISettings::values.favorited_ids); + + Settings::values.disabled_addons.clear(); + + config = std::make_unique(); + UISettings::values.reset_to_defaults = false; + + UISettings::values.game_dirs = std::move(old_game_dirs); + UISettings::values.favorited_ids = std::move(old_favorited_ids); + + InitializeRecentFileMenuActions(); + + SetDefaultUIGeometry(); + RestoreUIState(); + } + InitializeHotkeys(); + + if (UISettings::values.theme != old_theme) { + UpdateUITheme(); + } + if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + } +#ifdef __linux__ + if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); + } +#endif +#ifdef __unix__ + if (Settings::values.gui_force_x11.GetValue() != old_force_x11) { + GraphicsBackend::SetForceX11(Settings::values.gui_force_x11.GetValue()); + } +#endif + + if (!multiplayer_state->IsHostingPublicRoom()) { + multiplayer_state->UpdateCredentials(); + } + + emit UpdateThemedIcons(); + + const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); + if (reload || Settings::values.language_index.GetValue() != old_language_index) { + game_list->PopulateAsync(UISettings::values.game_dirs); + } + + UISettings::values.configuration_applied = false; + + config->SaveAllValues(); + + if ((UISettings::values.hide_mouse || Settings::values.mouse_panning) && emulation_running) { + render_window->installEventFilter(render_window); + render_window->setAttribute(Qt::WA_Hover, true); + } else { + render_window->removeEventFilter(render_window); + render_window->setAttribute(Qt::WA_Hover, false); + } + + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } + + // Restart camera config + if (emulation_running) { + render_window->FinalizeCamera(); + render_window->InitializeCamera(); + } + + if (!UISettings::values.has_broken_vulkan) { + renderer_status_button->setEnabled(!emulation_running); + } + + UpdateStatusButtons(); + controller_dialog->refreshConfiguration(); + QtCommon::system->ApplySettings(); +} + +void MainWindow::OnConfigureTas() { + ConfigureTasDialog dialog(this); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted && !UISettings::values.configuration_applied) { + Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + OnSaveConfig(); + } +} + +void MainWindow::OnTasStartStop() { + if (!emulation_running) { + return; + } + + // Disable system buttons to prevent TAS from executing a hotkey + auto* controller = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); + controller->ResetSystemButtons(); + + input_subsystem->GetTas()->StartStop(); + OnTasStateChanged(); +} + +void MainWindow::OnTasRecord() { + if (!emulation_running) { + return; + } + if (is_tas_recording_dialog_active) { + return; + } + + // Disable system buttons to prevent TAS from recording a hotkey + auto* controller = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); + controller->ResetSystemButtons(); + + const bool is_recording = input_subsystem->GetTas()->Record(); + if (!is_recording) { + is_tas_recording_dialog_active = true; + + bool answer = question(this, tr("TAS Recording"), tr("Overwrite file of player 1?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + input_subsystem->GetTas()->SaveRecording(answer); + is_tas_recording_dialog_active = false; + } + OnTasStateChanged(); +} + +void MainWindow::OnTasReset() { + input_subsystem->GetTas()->Reset(); +} + +void MainWindow::OnToggleDockedMode() { + const bool is_docked = Settings::IsDockedMode(); + auto* player_1 = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); + auto* handheld = QtCommon::system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld); + + if (!is_docked && handheld->IsConnected()) { + QMessageBox::warning(this, tr("Invalid config detected"), + tr("Handheld controller can't be used on docked mode. Pro " + "controller will be selected.")); + handheld->Disconnect(); + player_1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); + player_1->Connect(); + controller_dialog->refreshConfiguration(); + } + + Settings::values.use_docked_mode.SetValue(is_docked ? Settings::ConsoleMode::Handheld + : Settings::ConsoleMode::Docked); + UpdateDockedButton(); + OnDockedModeChanged(is_docked, !is_docked, *QtCommon::system); +} + +void MainWindow::OnToggleGpuAccuracy() { + switch (Settings::values.gpu_accuracy.GetValue()) { + case Settings::GpuAccuracy::High: { + Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::Normal); + break; + } + case Settings::GpuAccuracy::Normal: + case Settings::GpuAccuracy::Extreme: + default: { + Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::High); + break; + } + } + + QtCommon::system->ApplySettings(); + UpdateGPUAccuracyButton(); +} + +void MainWindow::OnMute() { + Settings::values.audio_muted = !Settings::values.audio_muted; + UpdateVolumeUI(); +} + +void MainWindow::OnDecreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = static_cast(Settings::values.volume.GetValue()); + int step = 5; + if (current_volume <= 30) { + step = 2; + } + if (current_volume <= 6) { + step = 1; + } + Settings::values.volume.SetValue((std::max)(current_volume - step, 0)); + UpdateVolumeUI(); +} + +void MainWindow::OnIncreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = static_cast(Settings::values.volume.GetValue()); + int step = 5; + if (current_volume < 30) { + step = 2; + } + if (current_volume < 6) { + step = 1; + } + Settings::values.volume.SetValue(current_volume + step); + UpdateVolumeUI(); +} + +void MainWindow::OnToggleAdaptingFilter() { + auto filter = Settings::values.scaling_filter.GetValue(); + filter = static_cast(static_cast(filter) + 1); + if (filter == Settings::ScalingFilter::MaxEnum) { + filter = Settings::ScalingFilter::NearestNeighbor; + } + Settings::values.scaling_filter.SetValue(filter); + filter_status_button->setChecked(true); + UpdateFilterText(); +} + +void MainWindow::OnToggleGraphicsAPI() { + auto api = Settings::values.renderer_backend.GetValue(); + if (api != Settings::RendererBackend::Vulkan) { + api = Settings::RendererBackend::Vulkan; + } else { +#ifdef HAS_OPENGL + api = Settings::RendererBackend::OpenGL; +#else + api = Settings::RendererBackend::Null; +#endif + } + Settings::values.renderer_backend.SetValue(api); + renderer_status_button->setChecked(api == Settings::RendererBackend::Vulkan); + UpdateAPIText(); +} + +void MainWindow::OnConfigurePerGame() { + const u64 title_id = QtCommon::system->GetApplicationProcessProgramID(); + OpenPerGameConfiguration(title_id, current_game_path.toStdString()); +} + +void MainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file_name) { + const auto v_file = Core::GetGameFileFromPath(QtCommon::vfs, file_name); + + Settings::SetConfiguringGlobal(false); + ConfigurePerGame dialog(this, title_id, file_name, vk_device_records, *QtCommon::system); + dialog.LoadFromFile(v_file); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted && !UISettings::values.configuration_applied) { + Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } + + const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); + if (reload) { + game_list->PopulateAsync(UISettings::values.game_dirs); + } + + // Do not cause the global config to write local settings into the config file + const bool is_powered_on = QtCommon::system->IsPoweredOn(); + Settings::RestoreGlobalState(is_powered_on); + QtCommon::system->HIDCore().ReloadInputDevices(); + + UISettings::values.configuration_applied = false; + + if (!is_powered_on) { + config->SaveAllValues(); + } +} + +void MainWindow::OnLoadAmiibo() { + if (emu_thread == nullptr || !emu_thread->IsRunning()) { + return; + } + if (is_amiibo_file_select_active) { + return; + } + + auto* virtual_amiibo = input_subsystem->GetVirtualAmiibo(); + + // Remove amiibo if one is connected + if (virtual_amiibo->GetCurrentState() == InputCommon::VirtualAmiibo::State::TagNearby) { + virtual_amiibo->CloseAmiibo(); + QMessageBox::warning(this, tr("Amiibo"), tr("The current amiibo has been removed")); + return; + } + + if (virtual_amiibo->GetCurrentState() != InputCommon::VirtualAmiibo::State::WaitingForAmiibo) { + QMessageBox::warning(this, tr("Error"), tr("The current game is not looking for amiibos")); + return; + } + + is_amiibo_file_select_active = true; + const QString extensions{QStringLiteral("*.bin")}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); + is_amiibo_file_select_active = false; + + if (filename.isEmpty()) { + return; + } + + LoadAmiibo(filename); +} + +// TODO(crueter): does this need to be ported to QML? +bool MainWindow::question(QWidget* parent, const QString& title, const QString& text, + QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) { + QMessageBox* box_dialog = new QMessageBox(parent); + box_dialog->setWindowTitle(title); + box_dialog->setText(text); + box_dialog->setStandardButtons(buttons); + box_dialog->setDefaultButton(defaultButton); + + ControllerNavigation* controller_navigation = + new ControllerNavigation(QtCommon::system->HIDCore(), box_dialog); + connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, + [box_dialog](Qt::Key key) { + QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); + QCoreApplication::postEvent(box_dialog, event); + }); + int res = box_dialog->exec(); + + controller_navigation->UnloadController(); + return res == QMessageBox::Yes; +} + +void MainWindow::LoadAmiibo(const QString& filename) { + auto* virtual_amiibo = input_subsystem->GetVirtualAmiibo(); + const QString title = tr("Error loading Amiibo data"); + // Remove amiibo if one is connected + if (virtual_amiibo->GetCurrentState() == InputCommon::VirtualAmiibo::State::TagNearby) { + virtual_amiibo->CloseAmiibo(); + QMessageBox::warning(this, tr("Amiibo"), tr("The current amiibo has been removed")); + return; + } + + switch (virtual_amiibo->LoadAmiibo(filename.toStdString())) { + case InputCommon::VirtualAmiibo::Info::NotAnAmiibo: + QMessageBox::warning(this, title, tr("The selected file is not a valid amiibo")); + break; + case InputCommon::VirtualAmiibo::Info::UnableToLoad: + QMessageBox::warning(this, title, tr("The selected file is already on use")); + break; + case InputCommon::VirtualAmiibo::Info::WrongDeviceState: + QMessageBox::warning(this, title, tr("The current game is not looking for amiibos")); + break; + case InputCommon::VirtualAmiibo::Info::Unknown: + QMessageBox::warning(this, title, tr("An unknown error occurred")); + break; + default: + break; + } +} + +void MainWindow::OnOpenRootDataFolder() { + QtCommon::Game::OpenRootDataFolder(); +} + +void MainWindow::OnOpenNANDFolder() +{ + QtCommon::Game::OpenNANDFolder(); +} + +void MainWindow::OnOpenSDMCFolder() +{ + QtCommon::Game::OpenSDMCFolder(); +} + +void MainWindow::OnOpenModFolder() +{ + QtCommon::Game::OpenModFolder(); +} + +void MainWindow::OnOpenLogFolder() +{ + QtCommon::Game::OpenLogFolder(); +} + +void MainWindow::OnVerifyInstalledContents() { + QtCommon::Content::VerifyInstalledContents(); +} + +void MainWindow::InstallFirmware(const QString& location, bool recursive) { + QtCommon::Content::InstallFirmware(location, recursive); + OnCheckFirmwareDecryption(); +} + +void MainWindow::OnInstallFirmware() { + // Don't do this while emulation is running, that'd probably be a bad idea. + if (emu_thread != nullptr && emu_thread->IsRunning()) { + return; + } + + // Check for installed keys, error out, suggest restart? + if (!ContentManager::AreKeysPresent()) { + QMessageBox::information( + this, tr("Keys not installed"), + tr("Install decryption keys and restart Eden before attempting to install firmware.")); + return; + } + + const QString firmware_source_location = QFileDialog::getExistingDirectory( + this, tr("Select Dumped Firmware Source Location"), {}, QFileDialog::ShowDirsOnly); + if (firmware_source_location.isEmpty()) { + return; + } + + InstallFirmware(firmware_source_location); +} + +void MainWindow::OnInstallFirmwareFromZIP() { + // Don't do this while emulation is running, that'd probably be a bad idea. + if (emu_thread != nullptr && emu_thread->IsRunning()) { + return; + } + + // Check for installed keys, error out, suggest restart? + if (!ContentManager::AreKeysPresent()) { + QMessageBox::information( + this, tr("Keys not installed"), + tr("Install decryption keys and restart Eden before attempting to install firmware.")); + return; + } + + const QString firmware_zip_location = QFileDialog::getOpenFileName( + this, tr("Select Dumped Firmware ZIP"), {}, tr("Zipped Archives (*.zip)")); + if (firmware_zip_location.isEmpty()) { + return; + } + + const QString qCacheDir = QtCommon::Content::UnzipFirmwareToTmp(firmware_zip_location); + + // In this case, it has to be done recursively, since sometimes people + // will pack it into a subdirectory after dumping + if (!qCacheDir.isEmpty()) { + InstallFirmware(qCacheDir, true); + std::error_code ec; + std::filesystem::remove_all(std::filesystem::temp_directory_path() / "eden" / "firmware", ec); + + if (ec) { + QMessageBox::warning(this, tr("Firmware cleanup failed"), + tr("Failed to clean up extracted firmware cache.\n" + "Check write permissions in the system temp directory and try " + "again.\nOS reported error: %1") + .arg(QString::fromStdString(ec.message()))); + } + } +} + +void MainWindow::OnInstallDecryptionKeys() { + // Don't do this while emulation is running. + if (emu_thread != nullptr && emu_thread->IsRunning()) { + return; + } + + QtCommon::Content::InstallKeys(); + + game_list->PopulateAsync(UISettings::values.game_dirs); + OnCheckFirmwareDecryption(); +} + +void MainWindow::OnAbout() { + AboutDialog aboutDialog(this); + aboutDialog.exec(); +} + +void MainWindow::OnEdenDependencies() { + DepsDialog depsDialog(this); + depsDialog.exec(); +} + +void MainWindow::OnDataDialog() { + DataDialog dataDialog(this); + dataDialog.exec(); + + // refresh stuff in case it was cleared + OnGameListRefresh(); + +} + +void MainWindow::OnToggleFilterBar() { + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + if (ui->action_Show_Filter_Bar->isChecked()) { + game_list->SetFilterFocus(); + } else { + game_list->ClearFilter(); + } +} + +void MainWindow::OnToggleStatusBar() { + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); +} + +void MainWindow::OnGameListRefresh() +{ + // Resets metadata cache and reloads + QtCommon::Game::ResetMetadata(false); + game_list->RefreshGameDirectory(); + SetFirmwareVersion(); +} + +void MainWindow::OnAlbum() { + constexpr u64 AlbumId = static_cast(Service::AM::AppletProgramId::PhotoViewer); + auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install firmware to use the Album applet.")); + return; + } + + auto album_nca = bis_system->GetEntry(AlbumId, FileSys::ContentRecordType::Program); + if (!album_nca) { + QMessageBox::warning(this, tr("Album Applet"), + tr("Album applet is not available. Please reinstall firmware.")); + return; + } + + QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::PhotoViewer); + + const auto filename = QString::fromStdString(album_nca->GetFullPath()); + UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); + BootGame(filename, LibraryAppletParameters(AlbumId, Service::AM::AppletId::PhotoViewer)); +} + +void MainWindow::OnCabinet(Service::NFP::CabinetMode mode) { + constexpr u64 CabinetId = static_cast(Service::AM::AppletProgramId::Cabinet); + auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install firmware to use the Cabinet applet.")); + return; + } + + auto cabinet_nca = bis_system->GetEntry(CabinetId, FileSys::ContentRecordType::Program); + if (!cabinet_nca) { + QMessageBox::warning(this, tr("Cabinet Applet"), + tr("Cabinet applet is not available. Please reinstall firmware.")); + return; + } + + QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::Cabinet); + QtCommon::system->GetFrontendAppletHolder().SetCabinetMode(mode); + + const auto filename = QString::fromStdString(cabinet_nca->GetFullPath()); + UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); + BootGame(filename, LibraryAppletParameters(CabinetId, Service::AM::AppletId::Cabinet)); +} + +void MainWindow::OnMiiEdit() { + constexpr u64 MiiEditId = static_cast(Service::AM::AppletProgramId::MiiEdit); + auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install firmware to use the Mii editor.")); + return; + } + + auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program); + if (!mii_applet_nca) { + QMessageBox::warning(this, tr("Mii Edit Applet"), + tr("Mii editor is not available. Please reinstall firmware.")); + return; + } + + QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::MiiEdit); + + const auto filename = QString::fromStdString((mii_applet_nca->GetFullPath())); + UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); + BootGame(filename, LibraryAppletParameters(MiiEditId, Service::AM::AppletId::MiiEdit)); +} + +void MainWindow::OnOpenControllerMenu() { + constexpr u64 ControllerAppletId = static_cast(Service::AM::AppletProgramId::Controller); + auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install firmware to use the Controller Menu.")); + return; + } + + auto controller_applet_nca = + bis_system->GetEntry(ControllerAppletId, FileSys::ContentRecordType::Program); + if (!controller_applet_nca) { + QMessageBox::warning(this, tr("Controller Applet"), + tr("Controller Menu is not available. Please reinstall firmware.")); + return; + } + + QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::Controller); + + const auto filename = QString::fromStdString((controller_applet_nca->GetFullPath())); + UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); + BootGame(filename, + LibraryAppletParameters(ControllerAppletId, Service::AM::AppletId::Controller)); +} + +void MainWindow::OnHomeMenu() { + auto result = FirmwareManager::VerifyFirmware(*QtCommon::system.get()); + + switch (result) { + case FirmwareManager::ErrorFirmwareMissing: + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install firmware to use the Home Menu.")); + return; + case FirmwareManager::ErrorFirmwareCorrupted: + QMessageBox::warning(this, tr("Firmware Corrupted"), + tr(FirmwareManager::GetFirmwareCheckString(result))); + return; + case FirmwareManager::ErrorFirmwareTooNew: { + if (!UISettings::values.show_fw_warning.GetValue()) break; + + QMessageBox box(QMessageBox::Warning, + tr("Firmware Too New"), + tr(FirmwareManager::GetFirmwareCheckString(result)) + tr("\nContinue anyways?"), + QMessageBox::Yes | QMessageBox::No, + this); + + QCheckBox *checkbox = new QCheckBox(tr("Don't show again")); + box.setCheckBox(checkbox); + + int button = box.exec(); + if (checkbox->isChecked()) { + UISettings::values.show_fw_warning.SetValue(false); + } + + if (button == static_cast(QMessageBox::No)) return; + break; + } default: + break; + } + + constexpr u64 QLaunchId = static_cast(Service::AM::AppletProgramId::QLaunch); + auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); + + auto qlaunch_applet_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program); + if (!qlaunch_applet_nca) { + QMessageBox::warning(this, tr("Home Menu Applet"), + tr("Home Menu is not available. Please reinstall firmware.")); + return; + } + + QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::QLaunch); + + const auto filename = QString::fromStdString((qlaunch_applet_nca->GetFullPath())); + UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); + BootGame(filename, LibraryAppletParameters(QLaunchId, Service::AM::AppletId::QLaunch)); +} + +void MainWindow::OnInitialSetup() { + constexpr u64 Starter = static_cast(Service::AM::AppletProgramId::Starter); + auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); + if (!bis_system) { + QMessageBox::warning(this, tr("No firmware available"), + tr("Please install firmware to use Starter.")); + return; + } + + auto qlaunch_nca = bis_system->GetEntry(Starter, FileSys::ContentRecordType::Program); + if (!qlaunch_nca) { + QMessageBox::warning(this, tr("Starter Applet"), + tr("Starter is not available. Please reinstall firmware.")); + return; + } + + QtCommon::system->GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::Starter); + + const auto filename = QString::fromStdString((qlaunch_nca->GetFullPath())); + UISettings::values.roms_path = QFileInfo(filename).path().toStdString(); + BootGame(filename, LibraryAppletParameters(Starter, Service::AM::AppletId::Starter)); +} + +void MainWindow::OnCreateHomeMenuDesktopShortcut() { + QtCommon::Game::CreateHomeMenuShortcut(QtCommon::Game::ShortcutTarget::Desktop); +} + +void MainWindow::OnCreateHomeMenuApplicationMenuShortcut() { + QtCommon::Game::CreateHomeMenuShortcut(QtCommon::Game::ShortcutTarget::Applications); +} + +void MainWindow::OnCaptureScreenshot() { + if (emu_thread == nullptr || !emu_thread->IsRunning()) { + return; + } + + const u64 title_id = QtCommon::system->GetApplicationProcessProgramID(); + const auto screenshot_path = + QString::fromStdString(Common::FS::GetEdenPathString(Common::FS::EdenPath::ScreenshotsDir)); + const auto date = + QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss-zzz")); + QString filename = QStringLiteral("%1/%2_%3.png") + .arg(screenshot_path) + .arg(title_id, 16, 16, QLatin1Char{'0'}) + .arg(date); + + if (!Common::FS::CreateDir(screenshot_path.toStdString())) { + return; + } + +#ifdef _WIN32 + if (UISettings::values.enable_screenshot_save_as) { + OnPauseGame(); + filename = QFileDialog::getSaveFileName(this, tr("Capture Screenshot"), filename, + tr("PNG Image (*.png)")); + OnStartGame(); + if (filename.isEmpty()) { + return; + } + } +#endif + render_window->CaptureScreenshot(filename); +} + +// TODO: Written 2020-10-01: Remove per-game config migration code when it is irrelevant +void MainWindow::MigrateConfigFiles() { + const auto config_dir_fs_path = Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir); + const QDir config_dir = + QDir(QString::fromStdString(Common::FS::PathToUTF8String(config_dir_fs_path))); + const QStringList config_dir_list = config_dir.entryList(QStringList(QStringLiteral("*.ini"))); + + if (!Common::FS::CreateDirs(config_dir_fs_path / "custom")) { + LOG_ERROR(Frontend, "Failed to create new config file directory"); + } + + for (auto it = config_dir_list.constBegin(); it != config_dir_list.constEnd(); ++it) { + const auto filename = it->toStdString(); + if (filename.find_first_not_of("0123456789abcdefACBDEF", 0) < 16) { + continue; + } + const auto origin = config_dir_fs_path / filename; + const auto destination = config_dir_fs_path / "custom" / filename; + LOG_INFO(Frontend, "Migrating config file from {} to {}", origin.string(), + destination.string()); + if (!Common::FS::RenameFile(origin, destination)) { + // Delete the old config file if one already exists in the new location. + Common::FS::RemoveFile(origin); + } + } +} + +#ifdef ENABLE_UPDATE_CHECKER +void MainWindow::OnEmulatorUpdateAvailable() { + QString version_string = update_future.result(); + if (version_string.isEmpty()) + return; + + QMessageBox update_prompt(this); + update_prompt.setWindowTitle(tr("Update Available")); + update_prompt.setIcon(QMessageBox::Information); + update_prompt.addButton(QMessageBox::Yes); + update_prompt.addButton(QMessageBox::Ignore); + update_prompt.setText( + tr("Download the %1 update?").arg(version_string)); + update_prompt.exec(); + if (update_prompt.button(QMessageBox::Yes) == update_prompt.clickedButton()) { + auto const full_url = fmt::format("{}/{}/releases/tag/", + std::string{Common::g_build_auto_update_website}, + std::string{Common::g_build_auto_update_repo} + ); + QDesktopServices::openUrl(QUrl(QString::fromStdString(full_url) + version_string)); + } +} +#endif + +void MainWindow::UpdateWindowTitle(std::string_view title_name, std::string_view title_version, std::string_view gpu_vendor) { + static const std::string build_id = std::string{Common::g_build_id}; + static const std::string yuzu_title = fmt::format("{} | {} | {}", + std::string{Common::g_build_name}, + std::string{Common::g_build_version}, + std::string{Common::g_compiler_id} + ); + + const auto override_title = + fmt::format(fmt::runtime(std::string(Common::g_title_bar_format_idle)), build_id); + const auto window_title = override_title.empty() ? yuzu_title : override_title; + + if (title_name.empty()) { + setWindowTitle(QString::fromStdString(window_title)); + } else { + const auto run_title = [window_title, title_name, title_version, gpu_vendor]() { + if (title_version.empty()) { + return fmt::format("{} | {} | {}", window_title, title_name, gpu_vendor); + } + return fmt::format("{} | {} | {} | {}", window_title, title_name, title_version, + gpu_vendor); + }(); + setWindowTitle(QString::fromStdString(run_title)); + } +} + +std::string MainWindow::CreateTASFramesString( + std::array frames) const { + std::string string = ""; + size_t maxPlayerIndex = 0; + for (size_t i = 0; i < frames.size(); i++) { + if (frames[i] != 0) { + if (maxPlayerIndex != 0) + string += ", "; + while (maxPlayerIndex++ != i) + string += "0, "; + string += std::to_string(frames[i]); + } + } + return string; +} + +QString MainWindow::GetTasStateDescription() const { + auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus(); + std::string tas_frames_string = CreateTASFramesString(total_tas_frames); + switch (tas_status) { + case InputCommon::TasInput::TasState::Running: + return tr("TAS state: Running %1/%2") + .arg(current_tas_frame) + .arg(QString::fromStdString(tas_frames_string)); + case InputCommon::TasInput::TasState::Recording: + return tr("TAS state: Recording %1").arg(total_tas_frames[0]); + case InputCommon::TasInput::TasState::Stopped: + return tr("TAS state: Idle %1/%2") + .arg(current_tas_frame) + .arg(QString::fromStdString(tas_frames_string)); + default: + return tr("TAS State: Invalid"); + } +} + +void MainWindow::OnTasStateChanged() { + bool is_running = false; + bool is_recording = false; + if (emulation_running) { + const InputCommon::TasInput::TasState tas_status = + std::get<0>(input_subsystem->GetTas()->GetStatus()); + is_running = tas_status == InputCommon::TasInput::TasState::Running; + is_recording = tas_status == InputCommon::TasInput::TasState::Recording; + } + + ui->action_TAS_Start->setText(is_running ? tr("&Stop Running") : tr("&Start")); + ui->action_TAS_Record->setText(is_recording ? tr("Stop R&ecording") : tr("R&ecord")); + + ui->action_TAS_Start->setEnabled(emulation_running); + ui->action_TAS_Record->setEnabled(emulation_running); + ui->action_TAS_Reset->setEnabled(emulation_running); +} + +void MainWindow::UpdateStatusBar() { + if (emu_thread == nullptr || !QtCommon::system->IsPoweredOn()) { + status_bar_update_timer.stop(); + return; + } + + if (Settings::values.tas_enable) { + tas_label->setText(GetTasStateDescription()); + } else { + tas_label->clear(); + } + + auto results = QtCommon::system->GetAndResetPerfStats(); + auto& shader_notify = QtCommon::system->GPU().ShaderNotify(); + const int shaders_building = shader_notify.ShadersBuilding(); + + if (shaders_building > 0) { + shader_building_label->setText(tr("Building: %n shader(s)", "", shaders_building)); + shader_building_label->setVisible(true); + } else { + shader_building_label->setVisible(false); + } + + const auto res_info = Settings::values.resolution_info; + const auto res_scale = res_info.up_factor; + res_scale_label->setText( + tr("Scale: %1x", "%1 is the resolution scaling factor").arg(res_scale)); + + if (Settings::values.use_speed_limit.GetValue()) { + emu_speed_label->setText(tr("Speed: %1% / %2%") + .arg(results.emulation_speed * 100.0, 0, 'f', 0) + .arg(Settings::values.speed_limit.GetValue())); + } else { + emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); + } + + game_fps_label->setText( + tr("Game: %1 FPS").arg(std::round(results.average_game_fps), 0, 'f', 0) + + tr(Settings::values.use_speed_limit ? "" : " (Unlocked)")); + + emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + + res_scale_label->setVisible(true); + emu_speed_label->setVisible(!Settings::values.use_multi_core.GetValue()); + game_fps_label->setVisible(true); + emu_frametime_label->setVisible(true); + firmware_label->setVisible(false); +} + +void MainWindow::UpdateGPUAccuracyButton() { + const auto gpu_accuracy = Settings::values.gpu_accuracy.GetValue(); + const auto gpu_accuracy_text = + ConfigurationShared::gpu_accuracy_texts_map.find(gpu_accuracy)->second; + gpu_accuracy_button->setText(gpu_accuracy_text.toUpper()); + gpu_accuracy_button->setChecked(gpu_accuracy != Settings::GpuAccuracy::Normal); +} + +void MainWindow::UpdateDockedButton() { + const auto console_mode = Settings::values.use_docked_mode.GetValue(); + dock_status_button->setChecked(Settings::IsDockedMode()); + dock_status_button->setText( + ConfigurationShared::use_docked_mode_texts_map.find(console_mode)->second.toUpper()); +} + +void MainWindow::UpdateAPIText() { + const auto api = Settings::values.renderer_backend.GetValue(); + const auto renderer_status_text = + ConfigurationShared::renderer_backend_texts_map.find(api)->second; + renderer_status_button->setText( + api == Settings::RendererBackend::OpenGL + ? tr("%1 %2").arg(renderer_status_text.toUpper(), + ConfigurationShared::shader_backend_texts_map + .find(Settings::values.shader_backend.GetValue()) + ->second) + : renderer_status_text.toUpper()); +} + +void MainWindow::UpdateFilterText() { + const auto filter = Settings::values.scaling_filter.GetValue(); + const auto filter_text = ConfigurationShared::scaling_filter_texts_map.find(filter)->second; + filter_status_button->setText(filter == Settings::ScalingFilter::Fsr ? tr("FSR") + : filter_text.toUpper()); +} + +void MainWindow::UpdateAAText() { + const auto aa_mode = Settings::values.anti_aliasing.GetValue(); + const auto aa_text = ConfigurationShared::anti_aliasing_texts_map.find(aa_mode)->second; + aa_status_button->setText(aa_mode == Settings::AntiAliasing::None + ? QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "NO AA")) + : aa_text.toUpper()); +} + +void MainWindow::UpdateVolumeUI() { + const auto volume_value = static_cast(Settings::values.volume.GetValue()); + volume_slider->setValue(volume_value); + if (Settings::values.audio_muted) { + volume_button->setChecked(false); + volume_button->setText(tr("VOLUME: MUTE")); + } else { + volume_button->setChecked(true); + volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); + } +} + +void MainWindow::UpdateStatusButtons() { + renderer_status_button->setChecked(Settings::values.renderer_backend.GetValue() == + Settings::RendererBackend::Vulkan); + UpdateAPIText(); + UpdateGPUAccuracyButton(); + UpdateDockedButton(); + UpdateFilterText(); + UpdateAAText(); + UpdateVolumeUI(); +} + +void MainWindow::UpdateUISettings() { + if (!ui->action_Fullscreen->isChecked()) { + UISettings::values.geometry = saveGeometry(); + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + } + UISettings::values.state = saveState(); + UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); + UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); + UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); + UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); + UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); + UISettings::values.first_start = false; +} + +void MainWindow::UpdateInputDrivers() { + if (!input_subsystem) { + return; + } + input_subsystem->PumpEvents(); +} + +void MainWindow::HideMouseCursor() { + if (emu_thread == nullptr && UISettings::values.hide_mouse) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + render_window->setCursor(QCursor(Qt::BlankCursor)); +} + +void MainWindow::ShowMouseCursor() { + render_window->unsetCursor(); + if (emu_thread != nullptr && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void MainWindow::OnMouseActivity() { + if (!Settings::values.mouse_panning) { + ShowMouseCursor(); + } +} + +void MainWindow::OnCheckFirmwareDecryption() { + if (!ContentManager::AreKeysPresent()) { + QMessageBox::warning(this, tr("Derivation Components Missing"), + tr("Encryption keys are missing.")); + } + SetFirmwareVersion(); + UpdateMenuState(); +} + +#ifdef __unix__ +void MainWindow::OnCheckGraphicsBackend() { + const QString platformName = QGuiApplication::platformName(); + const QByteArray qtPlatform = qgetenv("QT_QPA_PLATFORM"); + + if (platformName == QStringLiteral("xcb") || qtPlatform == "xcb") + return; + + const bool isWayland = platformName.startsWith(QStringLiteral("wayland"), Qt::CaseInsensitive) || qtPlatform.startsWith("wayland"); + if (!isWayland) + return; + + const bool currently_hidden = Settings::values.gui_hide_backend_warning.GetValue(); + if (currently_hidden) + return; + + QMessageBox msgbox(this); + msgbox.setWindowTitle(tr("Wayland Detected!")); + msgbox.setText(tr("Wayland is known to have significant performance issues and mysterious bugs.\n" + "It's recommended to use X11 instead.\n\n" + "Would you like to force it for future launches?")); + msgbox.setIcon(QMessageBox::Warning); + + QPushButton* okButton = msgbox.addButton(tr("Use X11"), QMessageBox::AcceptRole); + msgbox.addButton(tr("Continue with Wayland"), QMessageBox::RejectRole); + msgbox.setDefaultButton(okButton); + + QCheckBox* cb = new QCheckBox(tr("Don't show again"), &msgbox); + cb->setChecked(currently_hidden); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + const bool hide = cb->isChecked(); + if (hide != currently_hidden) { + Settings::values.gui_hide_backend_warning.SetValue(hide); + } + + if (msgbox.clickedButton() == okButton) { + Settings::values.gui_force_x11.SetValue(true); + GraphicsBackend::SetForceX11(true); + QMessageBox::information(this, + tr("Restart Required"), + tr("Restart Eden to apply the X11 backend.")); + } +} +#endif + +bool MainWindow::CheckFirmwarePresence() { + return FirmwareManager::CheckFirmwarePresence(*QtCommon::system.get()); +} + +void MainWindow::SetFirmwareVersion() { + const auto pair = FirmwareManager::GetFirmwareVersion(*QtCommon::system.get()); + const auto firmware_data = pair.first; + const auto result = pair.second; + + if (result.IsError() || !CheckFirmwarePresence()) { + LOG_INFO(Frontend, "Installed firmware: No firmware available"); + ui->menu_Applets->setEnabled(false); + ui->menu_Create_Shortcuts->setEnabled(false); + firmware_label->setVisible(false); + return; + } + + firmware_label->setVisible(true); + ui->menu_Applets->setEnabled(true); + ui->menu_Create_Shortcuts->setEnabled(true); + + const std::string display_version(firmware_data.display_version.data()); + const std::string display_title(firmware_data.display_title.data()); + + LOG_INFO(Frontend, "Installed firmware: {}", display_version); + + firmware_label->setText(QString::fromStdString(display_version)); + firmware_label->setToolTip(QString::fromStdString(display_title)); +} + +bool MainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed, u64 program_id, + u64* selected_title_id, u8* selected_content_record_type) { + using ContentInfo = std::tuple; + boost::container::flat_set available_title_ids; + + const auto RetrieveEntries = [&](FileSys::TitleType title_type, + FileSys::ContentRecordType record_type) { + const auto entries = installed.ListEntriesFilter(title_type, record_type); + for (const auto& entry : entries) { + if (FileSys::GetBaseTitleID(entry.title_id) == program_id && + installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success) { + available_title_ids.insert({entry.title_id, title_type, record_type}); + } + } + }; + + RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::Program); + RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::HtmlDocument); + RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::LegalInformation); + RetrieveEntries(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); + + if (available_title_ids.empty()) { + return false; + } + + size_t title_index = 0; + + if (available_title_ids.size() > 1) { + QStringList list; + for (auto& [title_id, title_type, record_type] : available_title_ids) { + const auto hex_title_id = QString::fromStdString(fmt::format("{:X}", title_id)); + if (record_type == FileSys::ContentRecordType::Program) { + list.push_back(QStringLiteral("Program [%1]").arg(hex_title_id)); + } else if (record_type == FileSys::ContentRecordType::HtmlDocument) { + list.push_back(QStringLiteral("HTML document [%1]").arg(hex_title_id)); + } else if (record_type == FileSys::ContentRecordType::LegalInformation) { + list.push_back(QStringLiteral("Legal information [%1]").arg(hex_title_id)); + } else { + list.push_back( + QStringLiteral("DLC %1 [%2]").arg(title_id & 0x7FF).arg(hex_title_id)); + } + } + + bool ok; + const auto res = QInputDialog::getItem( + this, tr("Select RomFS Dump Target"), + tr("Please select which RomFS you would like to dump."), list, 0, false, &ok); + if (!ok) { + return false; + } + + title_index = list.indexOf(res); + } + + const auto& [title_id, title_type, record_type] = *available_title_ids.nth(title_index); + *selected_title_id = title_id; + *selected_content_record_type = static_cast(record_type); + return true; +} + +bool MainWindow::ConfirmClose() { + if (emu_thread == nullptr || + UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Never) { + return true; + } + if (!QtCommon::system->GetExitLocked() && + UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Based_On_Game) { + return true; + } + const auto text = tr("Are you sure you want to close Eden?"); + return question(this, tr("Eden"), text); +} + +void MainWindow::closeEvent(QCloseEvent* event) { + if (!ConfirmClose()) { + event->ignore(); + return; + } + + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + UISettings::SaveWindowState(); + hotkey_registry.SaveHotkeys(); + + // Unload controllers early + controller_dialog->UnloadController(); + game_list->UnloadController(); + + // Shutdown session if the emu thread is active... + if (emu_thread != nullptr) { + ShutdownGame(); + } + + render_window->close(); + multiplayer_state->Close(); + QtCommon::system->HIDCore().UnloadInputDevices(); + Network::Shutdown(); + + QWidget::closeEvent(event); +} + +static bool IsSingleFileDropEvent(const QMimeData* mime) { + return mime->hasUrls() && mime->urls().length() == 1; +} + +void MainWindow::AcceptDropEvent(QDropEvent* event) { + if (IsSingleFileDropEvent(event->mimeData())) { + event->setDropAction(Qt::DropAction::LinkAction); + event->accept(); + } +} + +bool MainWindow::DropAction(QDropEvent* event) { + if (!IsSingleFileDropEvent(event->mimeData())) { + return false; + } + + const QMimeData* mime_data = event->mimeData(); + const QString& filename = mime_data->urls().at(0).toLocalFile(); + + if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { + // Amiibo + LoadAmiibo(filename); + } else { + // Game + if (ConfirmChangeGame()) { + BootGame(filename, ApplicationAppletParameters()); + } + } + return true; +} + +void MainWindow::dropEvent(QDropEvent* event) { + DropAction(event); +} + +void MainWindow::dragEnterEvent(QDragEnterEvent* event) { + AcceptDropEvent(event); +} + +void MainWindow::dragMoveEvent(QDragMoveEvent* event) { + AcceptDropEvent(event); +} + +bool MainWindow::ConfirmChangeGame() { + if (emu_thread == nullptr) + return true; + + // Use custom question to link controller navigation + return question( + this, tr("Eden"), + tr("Are you sure you want to stop the emulation? Any unsaved progress will be lost."), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); +} + +bool MainWindow::ConfirmForceLockedExit() { + if (emu_thread == nullptr) { + return true; + } + const auto text = tr("The currently running application has requested Eden to not exit.\n\n" + "Would you like to bypass this and exit anyway?"); + + return question(this, tr("Eden"), text); +} + +void MainWindow::RequestGameExit() { + if (!QtCommon::system->IsPoweredOn()) { + return; + } + + QtCommon::system->SetExitRequested(true); + QtCommon::system->GetAppletManager().RequestExit(); +} + +void MainWindow::filterBarSetChecked(bool state) { + ui->action_Show_Filter_Bar->setChecked(state); + emit(OnToggleFilterBar()); +} + +static void AdjustLinkColor() { + QPalette new_pal(qApp->palette()); + if (UISettings::IsDarkTheme()) { + new_pal.setColor(QPalette::Link, QColor(0, 190, 255, 255)); + } else { + new_pal.setColor(QPalette::Link, QColor(0, 140, 200, 255)); + } + if (qApp->palette().color(QPalette::Link) != new_pal.color(QPalette::Link)) { + qApp->setPalette(new_pal); + } +} + +void MainWindow::UpdateUITheme() { + const QString default_theme = QString::fromUtf8( + UISettings::themes[static_cast(UISettings::default_theme)].second); + QString current_theme = QString::fromStdString(UISettings::values.theme); + + if (current_theme.isEmpty()) { + current_theme = default_theme; + } + +#ifdef _WIN32 + QIcon::setThemeName(current_theme); + AdjustLinkColor(); +#else + if (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")) { + QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme + : startup_icon_theme); + QIcon::setThemeSearchPaths(QStringList(default_theme_paths)); + if (isDarkMode()) { + current_theme = QStringLiteral("default_dark"); + } + } else { + QIcon::setThemeName(current_theme); + QIcon::setThemeSearchPaths(QStringList(QStringLiteral(":/icons"))); + AdjustLinkColor(); + } +#endif + + if (current_theme != default_theme) { + QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; + QFile f(theme_uri); + if (!f.open(QFile::ReadOnly | QFile::Text)) { + LOG_ERROR(Frontend, "Unable to open style \"{}\", fallback to the default theme", + UISettings::values.theme); + current_theme = default_theme; + } + } + + QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)}; + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, "Unable to set style \"{}\", stylesheet file not found", + UISettings::values.theme); + qApp->setStyleSheet({}); + setStyleSheet({}); + } + +#ifdef _WIN32 + RemoveTitlebarFilter(); + ApplyGlobalDarkTitlebar(UISettings::IsDarkTheme()); +#endif +} + +void MainWindow::LoadTranslation() { + bool loaded; + + if (UISettings::values.language.GetValue().empty()) { + // If the selected language is empty, use system locale + loaded = translator.load(QLocale(), {}, {}, QStringLiteral(":/languages/")); + } else { + // Otherwise load from the specified file + loaded = translator.load(QString::fromStdString(UISettings::values.language.GetValue()), + QStringLiteral(":/languages/")); + } + + if (loaded) { + qApp->installTranslator(&translator); + } else { + UISettings::values.language = std::string("en"); + } +} + +void MainWindow::OnLanguageChanged(const QString& locale) { + if (UISettings::values.language.GetValue() != std::string("en")) { + qApp->removeTranslator(&translator); + } + + UISettings::values.language = locale.toStdString(); + LoadTranslation(); + ui->retranslateUi(this); + multiplayer_state->retranslateUi(); + UpdateWindowTitle(); +} + +void MainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { +#ifdef USE_DISCORD_PRESENCE + if (state) { + discord_rpc = std::make_unique(*QtCommon::system); + } else { + discord_rpc = std::make_unique(); + } +#else + discord_rpc = std::make_unique(); +#endif + discord_rpc->Update(); +} + +#ifdef __linux__ +void MainWindow::SetGamemodeEnabled(bool state) { + if (emulation_running) { + Common::Linux::SetGamemodeState(state); + } +} +#endif + +void MainWindow::changeEvent(QEvent* event) { +#ifdef __unix__ + // PaletteChange event appears to only reach so far into the GUI, explicitly asking to + // UpdateUITheme is a decent work around + if (event->type() == QEvent::PaletteChange) { + const QPalette test_palette(qApp->palette()); + const QString current_theme = QString::fromStdString(UISettings::values.theme); + // Keeping eye on QPalette::Window to avoid looping. QPalette::Text might be useful too + static QColor last_window_color; + const QColor window_color = test_palette.color(QPalette::Active, QPalette::Window); + if (last_window_color != window_color && (current_theme == QStringLiteral("default") || + current_theme == QStringLiteral("colorful"))) { + UpdateUITheme(); + } + last_window_color = window_color; + } +#endif // __unix__ + QWidget::changeEvent(event); +} + +Service::AM::FrontendAppletParameters MainWindow::ApplicationAppletParameters() { + return Service::AM::FrontendAppletParameters{ + .applet_id = Service::AM::AppletId::Application, + .applet_type = Service::AM::AppletType::Application, + }; +} + +Service::AM::FrontendAppletParameters MainWindow::LibraryAppletParameters( + u64 program_id, Service::AM::AppletId applet_id) { + return Service::AM::FrontendAppletParameters{ + .program_id = program_id, + .applet_id = applet_id, + .applet_type = Service::AM::AppletType::LibraryApplet, + }; +} + +void VolumeButton::wheelEvent(QWheelEvent* event) { + + int num_degrees = event->angleDelta().y() / 8; + int num_steps = (num_degrees / 15) * scroll_multiplier; + // Stated in QT docs: Most mouse types work in steps of 15 degrees, in which case the delta + // value is a multiple of 120; i.e., 120 units * 1/8 = 15 degrees. + + if (num_steps > 0) { + Settings::values.volume.SetValue( + (std::min)(200, Settings::values.volume.GetValue() + num_steps)); + } else { + Settings::values.volume.SetValue( + (std::max)(0, Settings::values.volume.GetValue() + num_steps)); + } + + scroll_multiplier = (std::min)(MaxMultiplier, scroll_multiplier * 2); + scroll_timer.start(100); // reset the multiplier if no scroll event occurs within 100 ms + + emit VolumeChanged(); + event->accept(); +} + +void VolumeButton::ResetMultiplier() { + scroll_multiplier = 1; +} + +#ifdef main +#undef main +#endif diff --git a/src/yuzu/main.h b/src/yuzu/main_window.h similarity index 98% rename from src/yuzu/main.h rename to src/yuzu/main_window.h index e33612694c..2ba92d476c 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main_window.h @@ -29,7 +29,7 @@ #include #include #include -#include +#include #endif #ifdef ENABLE_UPDATE_CHECKER @@ -38,7 +38,6 @@ #endif class QtConfig; -class ClickableLabel; class EmuThread; class GameList; class GImageInfo; @@ -154,7 +153,7 @@ private: constexpr static int MaxMultiplier = 8; }; -class GMainWindow : public QMainWindow { +class MainWindow : public QMainWindow { Q_OBJECT /// Max number of recently loaded items to keep track of @@ -163,8 +162,8 @@ class GMainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); - explicit GMainWindow(bool has_broken_vulkan); - ~GMainWindow() override; + explicit MainWindow(bool has_broken_vulkan); + ~MainWindow() override; bool DropAction(QDropEvent* event); void AcceptDropEvent(QDropEvent* event); @@ -467,11 +466,9 @@ private: */ bool question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = - QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), + QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); - std::string GetProfileID(); - std::unique_ptr ui; std::unique_ptr discord_rpc; diff --git a/src/yuzu/migration_worker.cpp b/src/yuzu/migration_worker.cpp index 50b6d1ebd4..0149db4149 100644 --- a/src/yuzu/migration_worker.cpp +++ b/src/yuzu/migration_worker.cpp @@ -11,11 +11,11 @@ #include "common/fs/path_util.h" -MigrationWorker::MigrationWorker(const Emulator selected_legacy_emu_, +MigrationWorker::MigrationWorker(const Emulator selected_emu_, const bool clear_shader_cache_, const MigrationStrategy strategy_) : QObject() - , selected_legacy_emu(selected_legacy_emu_) + , selected_emu(selected_emu_) , clear_shader_cache(clear_shader_cache_) , strategy(strategy_) {} @@ -25,15 +25,20 @@ void MigrationWorker::process() namespace fs = std::filesystem; constexpr auto copy_options = fs::copy_options::update_existing | fs::copy_options::recursive; - const fs::path legacy_user_dir = selected_legacy_emu.get_user_dir(); - const fs::path legacy_config_dir = selected_legacy_emu.get_config_dir(); - const fs::path legacy_cache_dir = selected_legacy_emu.get_cache_dir(); + const fs::path legacy_user_dir = selected_emu.get_user_dir(); + const fs::path legacy_config_dir = selected_emu.get_config_dir(); + const fs::path legacy_cache_dir = selected_emu.get_cache_dir(); // TODO(crueter): Make these constexpr since they're defaulted - const fs::path eden_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::EdenDir); - const fs::path config_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir); - const fs::path cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir); - const fs::path shader_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir); + fs::path eden_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::EdenDir); + fs::path config_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir); + fs::path cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir); + fs::path shader_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir); + + eden_dir.make_preferred(); + config_dir.make_preferred(); + cache_dir.make_preferred(); + shader_dir.make_preferred(); try { fs::remove_all(eden_dir); @@ -55,8 +60,8 @@ void MigrationWorker::process() std::exit(-1); } -// Windows doesn't need any more links, because cache and config -// are already children of the root directory + // Windows doesn't need any more links, because cache and config + // are already children of the root directory #ifndef WIN32 if (fs::is_directory(legacy_config_dir)) { Common::FS::CreateSymlink(legacy_config_dir, config_dir); @@ -69,7 +74,7 @@ void MigrationWorker::process() success_text.append(tr("\n\nNote that your configuration and data will be shared with %1.\n" "If this is not desirable, delete the following files:\n%2\n%3\n%4") - .arg(selected_legacy_emu.name(), + .arg(selected_emu.name(), QString::fromStdString(eden_dir.string()), QString::fromStdString(config_dir.string()), QString::fromStdString(cache_dir.string()))); @@ -79,8 +84,8 @@ void MigrationWorker::process() // Rename directories if deletion is requested (achieves the same result) fs::rename(legacy_user_dir, eden_dir); -// Windows doesn't need any more renames, because cache and config -// are already children of the root directory + // Windows doesn't need any more renames, because cache and config + // are already children of the root directory #ifndef WIN32 if (fs::is_directory(legacy_config_dir)) { fs::rename(legacy_config_dir, config_dir); @@ -96,8 +101,8 @@ void MigrationWorker::process() // Default behavior: copy fs::copy(legacy_user_dir, eden_dir, copy_options); -// Windows doesn't need any more copies, because cache and config -// are already children of the root directory + // Windows doesn't need any more copies, because cache and config + // are already children of the root directory #ifndef WIN32 if (fs::is_directory(legacy_config_dir)) { fs::copy(legacy_config_dir, config_dir, copy_options); diff --git a/src/yuzu/migration_worker.h b/src/yuzu/migration_worker.h index 2ff6e8c481..2db05570ea 100644 --- a/src/yuzu/migration_worker.h +++ b/src/yuzu/migration_worker.h @@ -7,14 +7,12 @@ #include #include "common/fs/path_util.h" -using namespace Common::FS; - typedef struct Emulator { const char *m_name; - EmuPath e_user_dir; - EmuPath e_config_dir; - EmuPath e_cache_dir; + Common::FS::EmuPath e_user_dir; + Common::FS::EmuPath e_config_dir; + Common::FS::EmuPath e_cache_dir; const std::string get_user_dir() const { return Common::FS::GetLegacyPath(e_user_dir).string(); @@ -35,11 +33,13 @@ typedef struct Emulator { } } Emulator; +#define STRUCT_EMU(name, enumName) Emulator{name, Common::FS::enumName##Dir, Common::FS::enumName##ConfigDir, Common::FS::enumName##CacheDir} + static constexpr std::array legacy_emus = { - Emulator{QT_TR_NOOP("Citron"), CitronDir, CitronConfigDir, CitronCacheDir}, - Emulator{QT_TR_NOOP("Sudachi"), SudachiDir, SudachiConfigDir, SudachiCacheDir}, - Emulator{QT_TR_NOOP("Suyu"), SuyuDir, SuyuConfigDir, SuyuCacheDir}, - Emulator{QT_TR_NOOP("Yuzu"), YuzuDir, YuzuConfigDir, YuzuCacheDir}, + STRUCT_EMU(QT_TR_NOOP("Citron"), Citron), + STRUCT_EMU(QT_TR_NOOP("Sudachi"), Sudachi), + STRUCT_EMU(QT_TR_NOOP("Suyu"), Suyu), + STRUCT_EMU(QT_TR_NOOP("Yuzu"), Yuzu), }; class MigrationWorker : public QObject @@ -52,7 +52,7 @@ public: Link, }; - MigrationWorker(const Emulator selected_legacy_emu, + MigrationWorker(const Emulator selected_emu, const bool clear_shader_cache, const MigrationStrategy strategy); @@ -64,7 +64,7 @@ signals: void error(const QString &error_message); private: - Emulator selected_legacy_emu; + Emulator selected_emu; bool clear_shader_cache; MigrationStrategy strategy; QString success_text = tr("Data was migrated successfully."); diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp index 992e9a1ee6..486ab28213 100644 --- a/src/yuzu/multiplayer/direct_connect.cpp +++ b/src/yuzu/multiplayer/direct_connect.cpp @@ -15,7 +15,7 @@ #include "core/internal_network/network_interface.h" #include "network/network.h" #include "ui_direct_connect.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #include "yuzu/multiplayer/client_room.h" #include "yuzu/multiplayer/direct_connect.h" #include "yuzu/multiplayer/message.h" diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp index 5c74c4b87e..8b811d8878 100644 --- a/src/yuzu/multiplayer/host_room.cpp +++ b/src/yuzu/multiplayer/host_room.cpp @@ -20,7 +20,7 @@ #include "network/announce_multiplayer_session.h" #include "ui_host_room.h" #include "yuzu/game_list_p.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #include "yuzu/multiplayer/host_room.h" #include "yuzu/multiplayer/message.h" #include "yuzu/multiplayer/state.h" diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp index 3bf2825098..fab9a56b2c 100644 --- a/src/yuzu/multiplayer/lobby.cpp +++ b/src/yuzu/multiplayer/lobby.cpp @@ -15,7 +15,7 @@ #include "network/network.h" #include "ui_lobby.h" #include "yuzu/game_list_p.h" -#include "yuzu/main.h" +#include "yuzu/main_window.h" #include "yuzu/multiplayer/client_room.h" #include "yuzu/multiplayer/lobby.h" #include "yuzu/multiplayer/lobby_p.h" diff --git a/src/yuzu/ryujinx_dialog.cpp b/src/yuzu/ryujinx_dialog.cpp index db10c06d93..563714f6be 100644 --- a/src/yuzu/ryujinx_dialog.cpp +++ b/src/yuzu/ryujinx_dialog.cpp @@ -1,25 +1,25 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include "qt_common/abstract/frontend.h" #include "ryujinx_dialog.h" #include "qt_common/util/fs.h" #include "ui_ryujinx_dialog.h" #include -namespace fs = std::filesystem; - RyujinxDialog::RyujinxDialog(std::filesystem::path eden_path, std::filesystem::path ryu_path, QWidget *parent) : QDialog(parent) - , ui(new Ui::RyujinxDialog) - , m_eden(eden_path.make_preferred()) - , m_ryu(ryu_path.make_preferred()) + , ui(new Ui::RyujinxDialog) + , m_eden(eden_path.make_preferred()) + , m_ryu(ryu_path.make_preferred()) { ui->setupUi(this); connect(ui->eden, &QPushButton::clicked, this, &RyujinxDialog::fromEden); connect(ui->ryujinx, &QPushButton::clicked, this, &RyujinxDialog::fromRyujinx); + connect(ui->cancel, &QPushButton::clicked, this, &RyujinxDialog::reject); } RyujinxDialog::~RyujinxDialog() @@ -30,7 +30,21 @@ RyujinxDialog::~RyujinxDialog() void RyujinxDialog::fromEden() { accept(); - QtCommon::FS::LinkRyujinx(m_eden, m_ryu); + + // Workaround: Ryujinx deletes and re-creates its directory structure??? + // So we just copy Eden's data to Ryujinx and then link the other way + namespace fs = std::filesystem; + try { + fs::remove_all(m_ryu); + fs::create_directories(m_ryu); + fs::copy(m_eden, m_ryu, fs::copy_options::recursive); + } catch (std::exception &e) { + QtCommon::Frontend::Critical(tr("Failed to link save data"), + tr("OS returned error: %1").arg(QString::fromStdString(e.what()))); + } + + // ?ploo + QtCommon::FS::LinkRyujinx(m_ryu, m_eden); } void RyujinxDialog::fromRyujinx() diff --git a/src/yuzu/user_data_migration.cpp b/src/yuzu/user_data_migration.cpp index a0243ec882..bc31c99e30 100644 --- a/src/yuzu/user_data_migration.cpp +++ b/src/yuzu/user_data_migration.cpp @@ -23,8 +23,6 @@ #include #include -namespace fs = std::filesystem; - UserDataMigrator::UserDataMigrator(QMainWindow *main_window) { // NOTE: Logging is not initialized yet, do not produce logs here. @@ -32,7 +30,7 @@ UserDataMigrator::UserDataMigrator(QMainWindow *main_window) // Check migration if config directory does not exist // TODO: ProfileManager messes with us a bit here, and force-creates the /nand/system/save/8000000000000010/su/avators/profiles.dat // file. Find a way to reorder operations and have it create after this guy runs. - if (!fs::is_directory(Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir))) { + if (!std::filesystem::is_directory(Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir))) { ShowMigrationPrompt(main_window); } } @@ -40,23 +38,7 @@ UserDataMigrator::UserDataMigrator(QMainWindow *main_window) void UserDataMigrator::ShowMigrationPrompt(QMainWindow *main_window) { namespace fs = std::filesystem; - - // define strings here for easy access - - QString prompt_prefix_text = QtCommon::StringLookup::Lookup( - QtCommon::StringLookup::MigrationPromptPrefix); - QString migration_prompt_message = QtCommon::StringLookup::Lookup( - QtCommon::StringLookup::MigrationPrompt); - QString clear_shader_tooltip = QtCommon::StringLookup::Lookup( - QtCommon::StringLookup::MigrationTooltipClearShader); - QString keep_old_data_tooltip = QtCommon::StringLookup::Lookup( - QtCommon::StringLookup::MigrationTooltipKeepOld); - QString clear_old_data_tooltip = QtCommon::StringLookup::Lookup( - QtCommon::StringLookup::MigrationTooltipClearOld); - QString link_old_dir_tooltip = QtCommon::StringLookup::Lookup( - QtCommon::StringLookup::MigrationTooltipLinkOld); - - // actual migration code + using namespace QtCommon::StringLookup; MigrationDialog migration_prompt; migration_prompt.setWindowTitle(QObject::tr("Migration")); @@ -69,11 +51,11 @@ void UserDataMigrator::ShowMigrationPrompt(QMainWindow *main_window) #define BUTTON(clazz, name, text, tooltip, checkState) \ clazz *name = new clazz(&migration_prompt); \ name->setText(text); \ - name->setToolTip(tooltip); \ + name->setToolTip(Lookup(tooltip)); \ name->setChecked(checkState); \ migration_prompt.addBox(name); - BUTTON(QCheckBox, clear_shaders, QObject::tr("Clear Shader Cache"), clear_shader_tooltip, true) + BUTTON(QCheckBox, clear_shaders, QObject::tr("Clear Shader Cache"), MigrationTooltipClearShader, true) u32 id = 0; @@ -81,9 +63,9 @@ void UserDataMigrator::ShowMigrationPrompt(QMainWindow *main_window) BUTTON(QRadioButton, name, text, tooltip, checkState) \ group->addButton(name, ++id); - RADIO(keep_old, QObject::tr("Keep Old Data"), keep_old_data_tooltip, true) - RADIO(clear_old, QObject::tr("Clear Old Data"), clear_old_data_tooltip, false) - RADIO(link_old, QObject::tr("Link Old Directory"), link_old_dir_tooltip, false) + RADIO(keep_old, QObject::tr("Keep Old Data"), MigrationTooltipKeepOld, true) + RADIO(clear_old, QObject::tr("Clear Old Data"), MigrationTooltipClearOld, false) + RADIO(link_old, QObject::tr("Link Old Directory"), MigrationTooltipLinkOld, false) #undef RADIO #undef BUTTON @@ -101,7 +83,7 @@ void UserDataMigrator::ShowMigrationPrompt(QMainWindow *main_window) // makes my life easier qRegisterMetaType(); - QString prompt_text = prompt_prefix_text; + QString prompt_text = Lookup(MigrationPromptPrefix); // natural language processing is a nightmare for (const Emulator &emu : found) { @@ -114,7 +96,7 @@ void UserDataMigrator::ShowMigrationPrompt(QMainWindow *main_window) } prompt_text.append(QObject::tr("\n\n")); - prompt_text = prompt_text % QStringLiteral("\n\n") % migration_prompt_message; + prompt_text = prompt_text % QStringLiteral("\n\n") % Lookup(MigrationPrompt); migration_prompt.setText(prompt_text); migration_prompt.addButton(QObject::tr("No"), true); @@ -127,24 +109,12 @@ void UserDataMigrator::ShowMigrationPrompt(QMainWindow *main_window) return ShowMigrationCancelledMessage(main_window); } - MigrationWorker::MigrationStrategy strategy; - - switch (group->checkedId()) { - default: - [[fallthrough]]; - case 1: - strategy = MigrationWorker::MigrationStrategy::Copy; - break; - case 2: - strategy = MigrationWorker::MigrationStrategy::Move; - break; - case 3: - strategy = MigrationWorker::MigrationStrategy::Link; - break; - } + MigrationWorker::MigrationStrategy strategy = static_cast( + group->checkedId()); + + selected_emu = button->property("emulator").value(); MigrateUserData(main_window, - button->property("emulator").value(), clear_shaders->isChecked(), strategy); } @@ -161,12 +131,9 @@ void UserDataMigrator::ShowMigrationCancelledMessage(QMainWindow *main_window) } void UserDataMigrator::MigrateUserData(QMainWindow *main_window, - const Emulator selected_legacy_emu, const bool clear_shader_cache, const MigrationWorker::MigrationStrategy strategy) { - selected_emu = selected_legacy_emu; - // Create a dialog to let the user know it's migrating QProgressDialog *progress = new QProgressDialog(main_window); progress->setWindowTitle(QObject::tr("Migrating")); @@ -176,7 +143,7 @@ void UserDataMigrator::MigrateUserData(QMainWindow *main_window, progress->setWindowModality(Qt::WindowModality::ApplicationModal); QThread *thread = new QThread(main_window); - MigrationWorker *worker = new MigrationWorker(selected_legacy_emu, clear_shader_cache, strategy); + MigrationWorker *worker = new MigrationWorker(selected_emu, clear_shader_cache, strategy); worker->moveToThread(thread); thread->connect(thread, &QThread::started, worker, &MigrationWorker::process); diff --git a/src/yuzu/user_data_migration.h b/src/yuzu/user_data_migration.h index a18dd27ddb..1cfeda3bca 100644 --- a/src/yuzu/user_data_migration.h +++ b/src/yuzu/user_data_migration.h @@ -22,7 +22,6 @@ private: void ShowMigrationPrompt(QMainWindow* main_window); void ShowMigrationCancelledMessage(QMainWindow* main_window); void MigrateUserData(QMainWindow* main_window, - const Emulator selected_legacy_emu, const bool clear_shader_cache, const MigrationWorker::MigrationStrategy strategy); }; diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index 844da5c401..0c035e5058 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -8,7 +8,11 @@ #include #include +#include "applets/qt_profile_select.h" #include "common/logging/log.h" +#include "core/frontend/applets/profile_select.h" +#include "core/hle/service/acc/profile_manager.h" +#include "qt_common/qt_common.h" #include "yuzu/util/util.h" #ifdef _WIN32 @@ -153,3 +157,49 @@ bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image) return false; #endif } +const std::optional GetProfileID() { + // if there's only a single profile, the user probably wants to use that... right? + const auto& profiles = QtCommon::system->GetProfileManager().FindExistingProfileUUIDs(); + if (profiles.size() == 1) { + return profiles[0]; + } + + const auto select_profile = [] { + const Core::Frontend::ProfileSelectParameters parameters{ + .mode = Service::AM::Frontend::UiMode::UserSelector, + .invalid_uid_list = {}, + .display_options = {}, + .purpose = Service::AM::Frontend::UserSelectionPurpose::General, + }; + QtProfileSelectionDialog dialog(*QtCommon::system, QtCommon::rootObject, parameters); + dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); + dialog.setWindowModality(Qt::WindowModal); + + if (dialog.exec() == QDialog::Rejected) { + return -1; + } + + return dialog.GetIndex(); + }; + + const auto index = select_profile(); + if (index == -1) { + return std::nullopt; + } + + const auto uuid = + QtCommon::system->GetProfileManager().GetUser(static_cast(index)); + ASSERT(uuid); + + return uuid; +} +std::string GetProfileIDString() { + const auto uuid = GetProfileID(); + if (!uuid) + return ""; + + auto user_id = uuid->AsU128(); + + return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]); +} diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 4094cf6c2b..7b482aa11d 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2015 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -6,6 +9,7 @@ #include #include #include +#include "common/uuid.h" /// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. [[nodiscard]] QFont GetMonospaceFont(); @@ -27,3 +31,15 @@ * @return bool If the operation succeeded */ [[nodiscard]] bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image); + +/** + * Prompt the user for a profile ID. If there is only one valid profile, returns that profile. + * @return The selected profile, or an std::nullopt if none were selected + */ +const std::optional GetProfileID(); + +/** + * Prompt the user for a profile ID. If there is only one valid profile, returns that profile. + * @return A string representation of the selected profile, or an empty string if none were seleeced + */ +std::string GetProfileIDString();