diff --git a/src/qt_gui/discord_rpc_handler.cpp b/src/qt_gui/discord_rpc_handler.cpp index 05a32b8f6..1e340acd3 100644 --- a/src/qt_gui/discord_rpc_handler.cpp +++ b/src/qt_gui/discord_rpc_handler.cpp @@ -6,38 +6,38 @@ #include "src/qt_gui/discord_rpc_handler.h" namespace DiscordRPCHandler { + + void RPC::init() { + DiscordEventHandlers handlers{}; + std::string discordAppId = "1290207945476280360"; + Discord_Initialize(discordAppId.c_str(), &handlers, 1, nullptr); + startTimestamp = time(nullptr); + enabled = true; + } -void RPC::init() { - DiscordEventHandlers handlers{}; - std::string discordAppId = "1290207945476280360"; - Discord_Initialize(discordAppId.c_str(), &handlers, 1, nullptr); - startTimestamp = time(nullptr); - enabled = true; -} + void RPC::setStatusIdling() { + DiscordRichPresence rpc{}; + rpc.largeImageKey = "https://github.com/shadps4-emu/shadPS4/raw/main/.github/shadps4.png"; + rpc.largeImageText = "ShadPS4 is a PS4 emulator"; + rpc.startTimestamp = startTimestamp; + rpc.details = "Idle"; -void RPC::setStatusIdling() { - DiscordRichPresence rpc{}; - rpc.largeImageKey = "https://github.com/shadps4-emu/shadPS4/raw/main/.github/shadps4.png"; - rpc.largeImageText = "ShadPS4 is a PS4 emulator"; - rpc.startTimestamp = startTimestamp; - rpc.details = "Idle"; + Discord_UpdatePresence(&rpc); + } - Discord_UpdatePresence(&rpc); -} + void RPC::setStatusPlaying(const std::string& game_name, const std::string& game_id) { + DiscordRichPresence rpc{}; -void RPC::setStatusPlaying(const std::string& game_name, const std::string& game_id) { - DiscordRichPresence rpc{}; + rpc.details = "Playing"; + rpc.state = game_name.c_str(); + std::string largeImageUrl = + "https://store.playstation.com/store/api/chihiro/00_09_000/titlecontainer/US/en/999/" + + game_id + "_00/image"; + rpc.largeImageKey = largeImageUrl.c_str(); + rpc.largeImageText = game_name.c_str(); + rpc.startTimestamp = startTimestamp; - rpc.details = "Playing"; - rpc.state = game_name.c_str(); - std::string largeImageUrl = - "https://store.playstation.com/store/api/chihiro/00_09_000/titlecontainer/US/en/999/" + - game_id + "_00/image"; - rpc.largeImageKey = largeImageUrl.c_str(); - rpc.largeImageText = game_name.c_str(); - rpc.startTimestamp = startTimestamp; - - Discord_UpdatePresence(&rpc); -} + Discord_UpdatePresence(&rpc); + } } // namespace DiscordRPCHandler diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index e69de29bb..ee9cacda1 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -0,0 +1,1053 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "about_dialog.h" +#include "cheats_patches.h" +#include "check_update.h" +#include "common/io_file.h" +#include "common/path_util.h" +#include "common/scm_rev.h" +#include "common/string_util.h" +#include "common/version.h" +#include "core/file_format/pkg.h" +#include "core/loader.h" +#include "game_install_dialog.h" +#include "main_window.h" +#include "settings_dialog.h" +#include "video_core/renderer_vulkan/vk_instance.h" + +MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) { + ui->setupUi(this); + installEventFilter(this); + setAttribute(Qt::WA_DeleteOnClose); +} + +MainWindow::~MainWindow() { + SaveWindowState(); + const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + Config::save(config_dir / "config.toml"); +} + +bool MainWindow::Init() { + auto start = std::chrono::steady_clock::now(); + // setup ui + AddUiWidgets(); + CreateActions(); + CreateRecentGameActions(); + ConfigureGuiFromSettings(); + LoadTranslation(); + CreateDockWindows(); + CreateConnects(); + SetLastUsedTheme(); + SetLastIconSizeBullet(); + GetPhysicalDevices(); + // show ui + setMinimumSize(350, minimumSizeHint().height()); + std::string window_title = ""; + if (Common::isRelease) { + window_title = fmt::format("shadPS4 v{}", Common::VERSION); + } else { + window_title = fmt::format("shadPS4 v{} {} {}", Common::VERSION, Common::g_scm_branch, + Common::g_scm_desc); + } + setWindowTitle(QString::fromStdString(window_title)); + this->show(); + // load game list + LoadGameLists(); + CheckUpdateMain(true); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + statusBar.reset(new QStatusBar); + this->setStatusBar(statusBar.data()); + // Update status bar + int numGames = m_game_info->m_games.size(); + QString statusMessage = + "Games: " + QString::number(numGames) + " (" + QString::number(duration.count()) + "ms)"; + statusBar->showMessage(statusMessage); + + //Initialize Discord RPC + discordRPC.init(); + discordRPC.setStatusIdling(); + + return true; +} + +void MainWindow::CreateActions() { + // create action group for icon size + m_icon_size_act_group = new QActionGroup(this); + m_icon_size_act_group->addAction(ui->setIconSizeTinyAct); + m_icon_size_act_group->addAction(ui->setIconSizeSmallAct); + m_icon_size_act_group->addAction(ui->setIconSizeMediumAct); + m_icon_size_act_group->addAction(ui->setIconSizeLargeAct); + + // create action group for list mode + m_list_mode_act_group = new QActionGroup(this); + m_list_mode_act_group->addAction(ui->setlistModeListAct); + m_list_mode_act_group->addAction(ui->setlistModeGridAct); + + // create action group for themes + m_theme_act_group = new QActionGroup(this); + m_theme_act_group->addAction(ui->setThemeDark); + m_theme_act_group->addAction(ui->setThemeLight); + m_theme_act_group->addAction(ui->setThemeGreen); + m_theme_act_group->addAction(ui->setThemeBlue); + m_theme_act_group->addAction(ui->setThemeViolet); +} + +void MainWindow::AddUiWidgets() { + // add toolbar widgets + QApplication::setStyle("Fusion"); + ui->toolBar->setObjectName("mw_toolbar"); + ui->toolBar->addWidget(ui->playButton); + ui->toolBar->addWidget(ui->pauseButton); + ui->toolBar->addWidget(ui->stopButton); + ui->toolBar->addWidget(ui->refreshButton); + ui->toolBar->addWidget(ui->settingsButton); + ui->toolBar->addWidget(ui->controllerButton); + QFrame* line = new QFrame(this); + line->setFrameShape(QFrame::StyledPanel); + line->setFrameShadow(QFrame::Sunken); + ui->toolBar->addWidget(line); + ui->toolBar->addWidget(ui->sizeSliderContainer); + ui->toolBar->addWidget(ui->mw_searchbar); +} + +void MainWindow::CreateDockWindows() { + // place holder widget is needed for good health they say :) + QWidget* phCentralWidget = new QWidget(this); + setCentralWidget(phCentralWidget); + + m_dock_widget.reset(new QDockWidget(tr("Game List"), this)); + m_game_list_frame.reset(new GameListFrame(m_game_info, this)); + m_game_list_frame->setObjectName("gamelist"); + m_game_grid_frame.reset(new GameGridFrame(m_game_info, this)); + m_game_grid_frame->setObjectName("gamegridlist"); + m_elf_viewer.reset(new ElfViewer(this)); + m_elf_viewer->setObjectName("elflist"); + + int table_mode = Config::getTableMode(); + int slider_pos = 0; + if (table_mode == 0) { // List + m_game_grid_frame->hide(); + m_elf_viewer->hide(); + m_game_list_frame->show(); + m_dock_widget->setWidget(m_game_list_frame.data()); + slider_pos = Config::getSliderPosition(); + ui->sizeSlider->setSliderPosition(slider_pos); // set slider pos at start; + isTableList = true; + } else if (table_mode == 1) { // Grid + m_game_list_frame->hide(); + m_elf_viewer->hide(); + m_game_grid_frame->show(); + m_dock_widget->setWidget(m_game_grid_frame.data()); + slider_pos = Config::getSliderPositionGrid(); + ui->sizeSlider->setSliderPosition(slider_pos); // set slider pos at start; + isTableList = false; + } else { + m_game_list_frame->hide(); + m_game_grid_frame->hide(); + m_elf_viewer->show(); + m_dock_widget->setWidget(m_elf_viewer.data()); + isTableList = false; + } + + m_dock_widget->setAllowedAreas(Qt::AllDockWidgetAreas); + m_dock_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_dock_widget->resize(this->width(), this->height()); + addDockWidget(Qt::LeftDockWidgetArea, m_dock_widget.data()); + this->setDockNestingEnabled(true); + + // handle resize like this for now, we deal with it when we add more docks + connect(this, &MainWindow::WindowResized, this, [&]() { + this->resizeDocks({m_dock_widget.data()}, {this->width()}, Qt::Orientation::Horizontal); + }); +} + +void MainWindow::LoadGameLists() { + // Get game info from game folders. + m_game_info->GetGameInfo(this); + if (isTableList) { + m_game_list_frame->PopulateGameList(); + } else { + m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); + } +} + +void MainWindow::CheckUpdateMain(bool checkSave) { + if (checkSave) { + if (!Config::autoUpdate()) { + return; + } + } + auto checkUpdate = new CheckUpdate(false); + checkUpdate->exec(); +} + +void MainWindow::GetPhysicalDevices() { + Vulkan::Instance instance(false, false); + auto physical_devices = instance.GetPhysicalDevices(); + for (const vk::PhysicalDevice physical_device : physical_devices) { + auto prop = physical_device.getProperties(); + QString name = QString::fromUtf8(prop.deviceName, -1); + if (prop.apiVersion < Vulkan::TargetVulkanApiVersion) { + name += tr(" * Unsupported Vulkan Version"); + } + m_physical_devices.push_back(name); + } +} + +void MainWindow::CreateConnects() { + connect(this, &MainWindow::WindowResized, this, &MainWindow::HandleResize); + connect(ui->mw_searchbar, &QLineEdit::textChanged, this, &MainWindow::SearchGameTable); + connect(ui->exitAct, &QAction::triggered, this, &QWidget::close); + connect(ui->refreshGameListAct, &QAction::triggered, this, &MainWindow::RefreshGameTable); + connect(ui->refreshButton, &QPushButton::clicked, this, &MainWindow::RefreshGameTable); + connect(ui->showGameListAct, &QAction::triggered, this, &MainWindow::ShowGameList); + connect(this, &MainWindow::ExtractionFinished, this, &MainWindow::RefreshGameTable); + + connect(ui->sizeSlider, &QSlider::valueChanged, this, [this](int value) { + if (isTableList) { + m_game_list_frame->icon_size = + 36 + value; // 36 is the minimum icon size to use due to text disappearing. + m_game_list_frame->ResizeIcons(36 + value); + Config::setIconSize(36 + value); + Config::setSliderPosition(value); + } else { + m_game_grid_frame->icon_size = 69 + value; + m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); + Config::setIconSizeGrid(69 + value); + Config::setSliderPositionGrid(value); + } + }); + + connect(ui->playButton, &QPushButton::clicked, this, &MainWindow::StartGame); + connect(m_game_grid_frame.get(), &QTableWidget::cellDoubleClicked, this, + &MainWindow::StartGame); + connect(m_game_list_frame.get(), &QTableWidget::cellDoubleClicked, this, + &MainWindow::StartGame); + + connect(ui->configureAct, &QAction::triggered, this, [this]() { + auto settingsDialog = new SettingsDialog(m_physical_devices, this); + + connect(settingsDialog, &SettingsDialog::LanguageChanged, this, + &MainWindow::OnLanguageChanged); + + settingsDialog->exec(); + }); + + connect(ui->settingsButton, &QPushButton::clicked, this, [this]() { + auto settingsDialog = new SettingsDialog(m_physical_devices, this); + + connect(settingsDialog, &SettingsDialog::LanguageChanged, this, + &MainWindow::OnLanguageChanged); + + settingsDialog->exec(); + }); + + connect(ui->updaterAct, &QAction::triggered, this, [this]() { + auto checkUpdate = new CheckUpdate(true); + checkUpdate->exec(); + }); + + connect(ui->aboutAct, &QAction::triggered, this, [this]() { + auto aboutDialog = new AboutDialog(this); + aboutDialog->exec(); + }); + + connect(ui->setIconSizeTinyAct, &QAction::triggered, this, [this]() { + if (isTableList) { + m_game_list_frame->icon_size = + 36; // 36 is the minimum icon size to use due to text disappearing. + ui->sizeSlider->setValue(0); // icone_size - 36 + Config::setIconSize(36); + Config::setSliderPosition(0); + } else { + ui->sizeSlider->setValue(0); // icone_size - 36 + Config::setIconSizeGrid(69); + Config::setSliderPositionGrid(0); + } + }); + + connect(ui->setIconSizeSmallAct, &QAction::triggered, this, [this]() { + if (isTableList) { + m_game_list_frame->icon_size = 64; + ui->sizeSlider->setValue(28); + Config::setIconSize(64); + Config::setSliderPosition(28); + } else { + ui->sizeSlider->setValue(28); + Config::setIconSizeGrid(97); + Config::setSliderPositionGrid(28); + } + }); + + connect(ui->setIconSizeMediumAct, &QAction::triggered, this, [this]() { + if (isTableList) { + m_game_list_frame->icon_size = 128; + ui->sizeSlider->setValue(92); + Config::setIconSize(128); + Config::setSliderPosition(92); + } else { + ui->sizeSlider->setValue(92); + Config::setIconSizeGrid(160); + Config::setSliderPositionGrid(91); + } + }); + + connect(ui->setIconSizeLargeAct, &QAction::triggered, this, [this]() { + if (isTableList) { + m_game_list_frame->icon_size = 256; + ui->sizeSlider->setValue(220); + Config::setIconSize(256); + Config::setSliderPosition(220); + } else { + ui->sizeSlider->setValue(220); + Config::setIconSizeGrid(256); + Config::setSliderPositionGrid(220); + } + }); + // List + connect(ui->setlistModeListAct, &QAction::triggered, m_dock_widget.data(), [this]() { + BackgroundMusicPlayer::getInstance().stopMusic(); + m_dock_widget->setWidget(m_game_list_frame.data()); + m_game_grid_frame->hide(); + m_elf_viewer->hide(); + m_game_list_frame->show(); + if (m_game_list_frame->item(0, 0) == nullptr) { + m_game_list_frame->clearContents(); + m_game_list_frame->PopulateGameList(); + } + isTableList = true; + Config::setTableMode(0); + int slider_pos = Config::getSliderPosition(); + ui->sizeSlider->setEnabled(true); + ui->sizeSlider->setSliderPosition(slider_pos); + }); + // Grid + connect(ui->setlistModeGridAct, &QAction::triggered, m_dock_widget.data(), [this]() { + BackgroundMusicPlayer::getInstance().stopMusic(); + m_dock_widget->setWidget(m_game_grid_frame.data()); + m_game_grid_frame->show(); + m_game_list_frame->hide(); + m_elf_viewer->hide(); + if (m_game_grid_frame->item(0, 0) == nullptr) { + m_game_grid_frame->clearContents(); + m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); + } + isTableList = false; + Config::setTableMode(1); + int slider_pos_grid = Config::getSliderPositionGrid(); + ui->sizeSlider->setEnabled(true); + ui->sizeSlider->setSliderPosition(slider_pos_grid); + }); + // Elf + connect(ui->setlistElfAct, &QAction::triggered, m_dock_widget.data(), [this]() { + BackgroundMusicPlayer::getInstance().stopMusic(); + m_dock_widget->setWidget(m_elf_viewer.data()); + m_game_grid_frame->hide(); + m_game_list_frame->hide(); + m_elf_viewer->show(); + isTableList = false; + ui->sizeSlider->setDisabled(true); + Config::setTableMode(2); + }); + + // Cheats/Patches Download. + connect(ui->downloadCheatsPatchesAct, &QAction::triggered, this, [this]() { + QDialog* panelDialog = new QDialog(this); + QVBoxLayout* layout = new QVBoxLayout(panelDialog); + QPushButton* downloadAllCheatsButton = + new QPushButton(tr("Download Cheats For All Installed Games"), panelDialog); + QPushButton* downloadAllPatchesButton = + new QPushButton(tr("Download Patches For All Games"), panelDialog); + + layout->addWidget(downloadAllCheatsButton); + layout->addWidget(downloadAllPatchesButton); + + panelDialog->setLayout(layout); + + connect(downloadAllCheatsButton, &QPushButton::clicked, this, [this, panelDialog]() { + QEventLoop eventLoop; + int pendingDownloads = 0; + + auto onDownloadFinished = [&]() { + if (--pendingDownloads <= 0) { + eventLoop.quit(); + } + }; + + for (const GameInfo& game : m_game_info->m_games) { + QString empty = ""; + QString gameSerial = QString::fromStdString(game.serial); + QString gameVersion = QString::fromStdString(game.version); + + CheatsPatches* cheatsPatches = + new CheatsPatches(empty, empty, empty, empty, empty, nullptr); + connect(cheatsPatches, &CheatsPatches::downloadFinished, onDownloadFinished); + + pendingDownloads += 3; + + cheatsPatches->downloadCheats("wolf2022", gameSerial, gameVersion, false); + cheatsPatches->downloadCheats("GoldHEN", gameSerial, gameVersion, false); + cheatsPatches->downloadCheats("shadPS4", gameSerial, gameVersion, false); + } + eventLoop.exec(); + + QMessageBox::information( + nullptr, tr("Download Complete"), + tr("You have downloaded cheats for all the games you have installed.")); + + panelDialog->accept(); + }); + connect(downloadAllPatchesButton, &QPushButton::clicked, [panelDialog]() { + QEventLoop eventLoop; + int pendingDownloads = 0; + + auto onDownloadFinished = [&]() { + if (--pendingDownloads <= 0) { + eventLoop.quit(); + } + }; + + QString empty = ""; + CheatsPatches* cheatsPatches = + new CheatsPatches(empty, empty, empty, empty, empty, nullptr); + connect(cheatsPatches, &CheatsPatches::downloadFinished, onDownloadFinished); + + pendingDownloads += 2; + + cheatsPatches->downloadPatches("GoldHEN", false); + cheatsPatches->downloadPatches("shadPS4", false); + + eventLoop.exec(); + QMessageBox::information( + nullptr, tr("Download Complete"), + QString(tr("Patches Downloaded Successfully!") + "\n" + + tr("All Patches available for all games have been downloaded."))); + cheatsPatches->createFilesJson("GoldHEN"); + cheatsPatches->createFilesJson("shadPS4"); + panelDialog->accept(); + }); + panelDialog->exec(); + }); + + // Dump game list. + connect(ui->dumpGameListAct, &QAction::triggered, this, [&] { + QString filePath = qApp->applicationDirPath().append("/GameList.txt"); + QFile file(filePath); + QTextStream out(&file); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + qDebug() << "Failed to open file for writing:" << file.errorString(); + return; + } + out << QString("%1 %2 %3 %4 %5\n") + .arg(" NAME", -50) + .arg(" ID", -10) + .arg("FW", -4) + .arg(" APP VERSION", -11) + .arg(" Path"); + for (const GameInfo& game : m_game_info->m_games) { + QString game_path; + Common::FS::PathToQString(game_path, game.path); + out << QString("%1 %2 %3 %4 %5\n") + .arg(QString::fromStdString(game.name), -50) + .arg(QString::fromStdString(game.serial), -10) + .arg(QString::fromStdString(game.fw), -4) + .arg(QString::fromStdString(game.version), -11) + .arg(game_path); + } + }); + + // Package install. + connect(ui->bootInstallPkgAct, &QAction::triggered, this, &MainWindow::InstallPkg); + connect(ui->bootGameAct, &QAction::triggered, this, &MainWindow::BootGame); + connect(ui->gameInstallPathAct, &QAction::triggered, this, &MainWindow::InstallDirectory); + + // elf viewer + connect(ui->addElfFolderAct, &QAction::triggered, m_elf_viewer.data(), + &ElfViewer::OpenElfFolder); + + // Package Viewer. + connect(ui->pkgViewerAct, &QAction::triggered, this, [this]() { + PKGViewer* pkgViewer = new PKGViewer( + m_game_info, this, [this](std::filesystem::path file, int pkgNum, int nPkg) { + this->InstallDragDropPkg(file, pkgNum, nPkg); + }); + pkgViewer->show(); + }); + + // Themes + connect(ui->setThemeDark, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Dark, ui->mw_searchbar); + Config::setMainWindowTheme(static_cast(Theme::Dark)); + if (isIconBlack) { + SetUiIcons(false); + isIconBlack = false; + } + }); + connect(ui->setThemeLight, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Light, ui->mw_searchbar); + Config::setMainWindowTheme(static_cast(Theme::Light)); + if (!isIconBlack) { + SetUiIcons(true); + isIconBlack = true; + } + }); + connect(ui->setThemeGreen, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Green, ui->mw_searchbar); + Config::setMainWindowTheme(static_cast(Theme::Green)); + if (isIconBlack) { + SetUiIcons(false); + isIconBlack = false; + } + }); + connect(ui->setThemeBlue, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Blue, ui->mw_searchbar); + Config::setMainWindowTheme(static_cast(Theme::Blue)); + if (isIconBlack) { + SetUiIcons(false); + isIconBlack = false; + } + }); + connect(ui->setThemeViolet, &QAction::triggered, &m_window_themes, [this]() { + m_window_themes.SetWindowTheme(Theme::Violet, ui->mw_searchbar); + Config::setMainWindowTheme(static_cast(Theme::Violet)); + if (isIconBlack) { + SetUiIcons(false); + isIconBlack = false; + } + }); +} + +void MainWindow::StartGame() { + isGameRunning = true; + BackgroundMusicPlayer::getInstance().stopMusic(); + QString gamePath = ""; + int table_mode = Config::getTableMode(); + if (table_mode == 0) { + if (m_game_list_frame->currentItem()) { + int itemID = m_game_list_frame->currentItem()->row(); + discordRPC.setStatusPlaying(m_game_info->m_games[itemID].name, + m_game_info->m_games[itemID].serial); + Common::FS::PathToQString(gamePath, m_game_info->m_games[itemID].path / "eboot.bin"); + } + } else if (table_mode == 1) { + if (m_game_grid_frame->cellClicked) { + int itemID = (m_game_grid_frame->crtRow * m_game_grid_frame->columnCnt) + + m_game_grid_frame->crtColumn; + discordRPC.setStatusPlaying(m_game_info->m_games[itemID].name, + m_game_info->m_games[itemID].serial); + Common::FS::PathToQString(gamePath, m_game_info->m_games[itemID].path / "eboot.bin"); + } + } else { + if (m_elf_viewer->currentItem()) { + int itemID = m_elf_viewer->currentItem()->row(); + discordRPC.setStatusPlaying(m_game_info->m_games[itemID].name, + m_game_info->m_games[itemID].serial); + gamePath = m_elf_viewer->m_elf_list[itemID]; + } + } + if (gamePath != "") { + AddRecentFiles(gamePath); + Core::Emulator emulator; + const auto path = Common::FS::PathFromQString(gamePath); + if (!std::filesystem::exists(path)) { + QMessageBox::critical(nullptr, tr("Run Game"), QString(tr("Eboot.bin file not found"))); + return; + } + emulator.Run(path); + } +} + +void MainWindow::SearchGameTable(const QString& text) { + if (isTableList) { + for (int row = 0; row < m_game_list_frame->rowCount(); row++) { + QString game_name = QString::fromStdString(m_game_info->m_games[row].name); + bool match = (game_name.contains(text, Qt::CaseInsensitive)); // Check only in column 1 + m_game_list_frame->setRowHidden(row, !match); + } + } else { + QVector filteredGames; + for (const auto& gameInfo : m_game_info->m_games) { + QString game_name = QString::fromStdString(gameInfo.name); + if (game_name.contains(text, Qt::CaseInsensitive)) { + filteredGames.push_back(gameInfo); + } + } + std::sort(filteredGames.begin(), filteredGames.end(), m_game_info->CompareStrings); + m_game_grid_frame->PopulateGameGrid(filteredGames, true); + } +} + +void MainWindow::ShowGameList() { + if (ui->showGameListAct->isChecked()) { + RefreshGameTable(); + } else { + m_game_grid_frame->clearContents(); + m_game_list_frame->clearContents(); + } +}; + +void MainWindow::RefreshGameTable() { + // m_game_info->m_games.clear(); + m_game_info->GetGameInfo(this); + m_game_list_frame->clearContents(); + m_game_list_frame->PopulateGameList(); + m_game_grid_frame->clearContents(); + m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); + statusBar->clearMessage(); + int numGames = m_game_info->m_games.size(); + QString statusMessage = tr("Games: ") + QString::number(numGames); + statusBar->showMessage(statusMessage); +} + +void MainWindow::ConfigureGuiFromSettings() { + setGeometry(Config::getMainWindowGeometryX(), Config::getMainWindowGeometryY(), + Config::getMainWindowGeometryW(), Config::getMainWindowGeometryH()); + + ui->showGameListAct->setChecked(true); + if (isTableList) { + ui->setlistModeListAct->setChecked(true); + } else { + ui->setlistModeGridAct->setChecked(true); + } + BackgroundMusicPlayer::getInstance().setVolume(Config::getBGMvolume()); +} + +void MainWindow::SaveWindowState() const { + Config::setMainWindowWidth(this->width()); + Config::setMainWindowHeight(this->height()); + Config::setMainWindowGeometry(this->geometry().x(), this->geometry().y(), + this->geometry().width(), this->geometry().height()); +} + +void MainWindow::InstallPkg() { + QFileDialog dialog; + dialog.setFileMode(QFileDialog::ExistingFiles); + dialog.setNameFilter(tr("PKG File (*.PKG *.pkg)")); + if (dialog.exec()) { + QStringList fileNames = dialog.selectedFiles(); + int nPkg = fileNames.size(); + int pkgNum = 0; + for (const QString& file : fileNames) { + ++pkgNum; + std::filesystem::path path = Common::FS::PathFromQString(file); + MainWindow::InstallDragDropPkg(path, pkgNum, nPkg); + } + } +} + +void MainWindow::BootGame() { + QFileDialog dialog; + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setNameFilter(tr("ELF files (*.bin *.elf *.oelf)")); + if (dialog.exec()) { + QStringList fileNames = dialog.selectedFiles(); + int nFiles = fileNames.size(); + + if (nFiles > 1) { + QMessageBox::critical(nullptr, tr("Game Boot"), + QString(tr("Only one file can be selected!"))); + } else { + std::filesystem::path path = Common::FS::PathFromQString(fileNames[0]); + Core::Emulator emulator; + if (!std::filesystem::exists(path)) { + QMessageBox::critical(nullptr, tr("Run Game"), + QString(tr("Eboot.bin file not found"))); + return; + } + emulator.Run(path); + } + } +} + +void MainWindow::InstallDragDropPkg(std::filesystem::path file, int pkgNum, int nPkg) { + if (Loader::DetectFileType(file) == Loader::FileTypes::Pkg) { + std::string failreason; + pkg = PKG(); + if (!pkg.Open(file, failreason)) { + QMessageBox::critical(this, tr("PKG ERROR"), QString::fromStdString(failreason)); + return; + } + auto extract_path = Config::getGameInstallDir() / pkg.GetTitleID(); + QString pkgType = QString::fromStdString(pkg.GetPkgFlags()); + QString gameDirPath; + Common::FS::PathToQString(gameDirPath, extract_path); + QDir game_dir(gameDirPath); + if (game_dir.exists()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("PKG Extraction")); + + if (!psf.Open(pkg.sfo)) { + QMessageBox::critical(this, tr("PKG ERROR"), + "Could not read SFO. Check log for details"); + return; + } + + std::string content_id; + if (auto value = psf.GetString("CONTENT_ID"); value.has_value()) { + content_id = std::string{*value}; + } else { + QMessageBox::critical(this, tr("PKG ERROR"), "PSF file there is no CONTENT_ID"); + return; + } + std::string entitlement_label = Common::SplitString(content_id, '-')[2]; + + auto addon_extract_path = Common::FS::GetUserPath(Common::FS::PathType::AddonsDir) / + pkg.GetTitleID() / entitlement_label; + QString addonDirPath; + Common::FS::PathToQString(addonDirPath, addon_extract_path); + QDir addon_dir(addonDirPath); + auto category = psf.GetString("CATEGORY"); + + if (pkgType.contains("PATCH")) { + QString pkg_app_version; + if (auto app_ver = psf.GetString("APP_VER"); app_ver.has_value()) { + pkg_app_version = QString::fromStdString(std::string{*app_ver}); + } else { + QMessageBox::critical(this, tr("PKG ERROR"), "PSF file there is no APP_VER"); + return; + } + psf.Open(extract_path / "sce_sys" / "param.sfo"); + QString game_app_version; + if (auto app_ver = psf.GetString("APP_VER"); app_ver.has_value()) { + game_app_version = QString::fromStdString(std::string{*app_ver}); + } else { + QMessageBox::critical(this, tr("PKG ERROR"), "PSF file there is no APP_VER"); + return; + } + double appD = game_app_version.toDouble(); + double pkgD = pkg_app_version.toDouble(); + if (pkgD == appD) { + msgBox.setText(QString(tr("Patch detected!") + "\n" + + tr("PKG and Game versions match: ") + pkg_app_version + + "\n" + tr("Would you like to overwrite?"))); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + } else if (pkgD < appD) { + msgBox.setText(QString(tr("Patch detected!") + "\n" + + tr("PKG Version %1 is older than installed version: ") + .arg(pkg_app_version) + + game_app_version + "\n" + + tr("Would you like to overwrite?"))); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + } else { + msgBox.setText(QString(tr("Patch detected!") + "\n" + + tr("Game is installed: ") + game_app_version + "\n" + + tr("Would you like to install Patch: ") + + pkg_app_version + " ?")); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + } + int result = msgBox.exec(); + if (result == QMessageBox::Yes) { + // Do nothing. + } else { + return; + } + } else if (category == "ac") { + if (!addon_dir.exists()) { + QMessageBox addonMsgBox; + addonMsgBox.setWindowTitle(tr("DLC Installation")); + addonMsgBox.setText(QString(tr("Would you like to install DLC: %1?")) + .arg(QString::fromStdString(entitlement_label))); + + addonMsgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + addonMsgBox.setDefaultButton(QMessageBox::No); + int result = addonMsgBox.exec(); + if (result == QMessageBox::Yes) { + extract_path = addon_extract_path; + } else { + return; + } + } else { + msgBox.setText(QString(tr("DLC already installed:") + "\n" + addonDirPath + + "\n\n" + tr("Would you like to overwrite?"))); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + int result = msgBox.exec(); + if (result == QMessageBox::Yes) { + extract_path = addon_extract_path; + } else { + return; + } + } + } else { + msgBox.setText(QString(tr("Game already installed") + "\n" + addonDirPath + "\n" + + tr("Would you like to overwrite?"))); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + int result = msgBox.exec(); + if (result == QMessageBox::Yes) { + // Do nothing. + } else { + return; + } + } + } else { + // Do nothing; + if (pkgType.contains("PATCH")) { + QMessageBox::information(this, tr("PKG Extraction"), + tr("PKG is a patch, please install the game first!")); + return; + } + // what else? + } + + if (!pkg.Extract(file, extract_path, failreason)) { + QMessageBox::critical(this, tr("PKG ERROR"), QString::fromStdString(failreason)); + } else { + int nfiles = pkg.GetNumberOfFiles(); + + if (nfiles > 0) { + QVector indices; + for (int i = 0; i < nfiles; i++) { + indices.append(i); + } + + QProgressDialog dialog; + dialog.setWindowTitle(tr("PKG Extraction")); + dialog.setWindowModality(Qt::WindowModal); + QString extractmsg = QString(tr("Extracting PKG %1/%2")).arg(pkgNum).arg(nPkg); + dialog.setLabelText(extractmsg); + dialog.setAutoClose(true); + dialog.setRange(0, nfiles); + + QFutureWatcher futureWatcher; + connect(&futureWatcher, &QFutureWatcher::finished, this, [=, this]() { + if (pkgNum == nPkg) { + QString path; + Common::FS::PathToQString(path, Config::getGameInstallDir()); + QMessageBox extractMsgBox(this); + extractMsgBox.setWindowTitle(tr("Extraction Finished")); + extractMsgBox.setText( + QString(tr("Game successfully installed at %1")).arg(path)); + extractMsgBox.addButton(QMessageBox::Ok); + extractMsgBox.setDefaultButton(QMessageBox::Ok); + connect(&extractMsgBox, &QMessageBox::buttonClicked, this, + [&](QAbstractButton* button) { + if (extractMsgBox.button(QMessageBox::Ok) == button) { + extractMsgBox.close(); + emit ExtractionFinished(); + } + }); + extractMsgBox.exec(); + } + }); + connect(&dialog, &QProgressDialog::canceled, [&]() { futureWatcher.cancel(); }); + connect(&futureWatcher, &QFutureWatcher::progressValueChanged, &dialog, + &QProgressDialog::setValue); + futureWatcher.setFuture( + QtConcurrent::map(indices, [&](int index) { pkg.ExtractFiles(index); })); + dialog.exec(); + } + } + } else { + QMessageBox::critical(this, tr("PKG ERROR"), + tr("File doesn't appear to be a valid PKG file")); + } +} + +void MainWindow::InstallDirectory() { + GameInstallDialog dlg; + dlg.exec(); + RefreshGameTable(); +} + +void MainWindow::SetLastUsedTheme() { + Theme lastTheme = static_cast(Config::getMainWindowTheme()); + m_window_themes.SetWindowTheme(lastTheme, ui->mw_searchbar); + + switch (lastTheme) { + case Theme::Light: + ui->setThemeLight->setChecked(true); + isIconBlack = true; + break; + case Theme::Dark: + ui->setThemeDark->setChecked(true); + isIconBlack = false; + SetUiIcons(false); + break; + case Theme::Green: + ui->setThemeGreen->setChecked(true); + isIconBlack = false; + SetUiIcons(false); + break; + case Theme::Blue: + ui->setThemeBlue->setChecked(true); + isIconBlack = false; + SetUiIcons(false); + break; + case Theme::Violet: + ui->setThemeViolet->setChecked(true); + isIconBlack = false; + SetUiIcons(false); + break; + } +} + +void MainWindow::SetLastIconSizeBullet() { + // set QAction bullet point if applicable + int lastSize = Config::getIconSize(); + switch (lastSize) { + case 36: + ui->setIconSizeTinyAct->setChecked(true); + break; + case 64: + ui->setIconSizeSmallAct->setChecked(true); + break; + case 128: + ui->setIconSizeMediumAct->setChecked(true); + break; + case 256: + ui->setIconSizeLargeAct->setChecked(true); + break; + } +} + +QIcon MainWindow::RecolorIcon(const QIcon& icon, bool isWhite) { + QPixmap pixmap(icon.pixmap(icon.actualSize(QSize(120, 120)))); + QColor clr(isWhite ? Qt::white : Qt::black); + QBitmap mask = pixmap.createMaskFromColor(clr, Qt::MaskOutColor); + pixmap.fill(QColor(isWhite ? Qt::black : Qt::white)); + pixmap.setMask(mask); + return QIcon(pixmap); +} + +void MainWindow::SetUiIcons(bool isWhite) { + ui->bootInstallPkgAct->setIcon(RecolorIcon(ui->bootInstallPkgAct->icon(), isWhite)); + ui->bootGameAct->setIcon(RecolorIcon(ui->bootGameAct->icon(), isWhite)); + ui->exitAct->setIcon(RecolorIcon(ui->exitAct->icon(), isWhite)); + ui->updaterAct->setIcon(RecolorIcon(ui->updaterAct->icon(), isWhite)); + ui->downloadCheatsPatchesAct->setIcon( + RecolorIcon(ui->downloadCheatsPatchesAct->icon(), isWhite)); + ui->dumpGameListAct->setIcon(RecolorIcon(ui->dumpGameListAct->icon(), isWhite)); + ui->aboutAct->setIcon(RecolorIcon(ui->aboutAct->icon(), isWhite)); + ui->setlistModeListAct->setIcon(RecolorIcon(ui->setlistModeListAct->icon(), isWhite)); + ui->setlistModeGridAct->setIcon(RecolorIcon(ui->setlistModeGridAct->icon(), isWhite)); + ui->gameInstallPathAct->setIcon(RecolorIcon(ui->gameInstallPathAct->icon(), isWhite)); + ui->menuThemes->setIcon(RecolorIcon(ui->menuThemes->icon(), isWhite)); + ui->menuGame_List_Icons->setIcon(RecolorIcon(ui->menuGame_List_Icons->icon(), isWhite)); + ui->playButton->setIcon(RecolorIcon(ui->playButton->icon(), isWhite)); + ui->pauseButton->setIcon(RecolorIcon(ui->pauseButton->icon(), isWhite)); + ui->stopButton->setIcon(RecolorIcon(ui->stopButton->icon(), isWhite)); + ui->refreshButton->setIcon(RecolorIcon(ui->refreshButton->icon(), isWhite)); + ui->settingsButton->setIcon(RecolorIcon(ui->settingsButton->icon(), isWhite)); + ui->controllerButton->setIcon(RecolorIcon(ui->controllerButton->icon(), isWhite)); + ui->refreshGameListAct->setIcon(RecolorIcon(ui->refreshGameListAct->icon(), isWhite)); + ui->menuGame_List_Mode->setIcon(RecolorIcon(ui->menuGame_List_Mode->icon(), isWhite)); + ui->pkgViewerAct->setIcon(RecolorIcon(ui->pkgViewerAct->icon(), isWhite)); + ui->configureAct->setIcon(RecolorIcon(ui->configureAct->icon(), isWhite)); + ui->addElfFolderAct->setIcon(RecolorIcon(ui->addElfFolderAct->icon(), isWhite)); +} + +void MainWindow::resizeEvent(QResizeEvent* event) { + emit WindowResized(event); + QMainWindow::resizeEvent(event); +} + +void MainWindow::HandleResize(QResizeEvent* event) { + if (isTableList) { + m_game_list_frame->RefreshListBackgroundImage(); + } else { + m_game_grid_frame->windowWidth = this->width(); + m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); + m_game_grid_frame->RefreshGridBackgroundImage(); + } +} + +void MainWindow::AddRecentFiles(QString filePath) { + std::vector vec = Config::getRecentFiles(); + if (!vec.empty()) { + if (filePath.toStdString() == vec.at(0)) { + return; + } + auto it = std::find(vec.begin(), vec.end(), filePath.toStdString()); + if (it != vec.end()) { + vec.erase(it); + } + } + vec.insert(vec.begin(), filePath.toStdString()); + if (vec.size() > 6) { + vec.pop_back(); + } + Config::setRecentFiles(vec); + const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + Config::save(config_dir / "config.toml"); + CreateRecentGameActions(); // Refresh the QActions. +} + +void MainWindow::CreateRecentGameActions() { + m_recent_files_group = new QActionGroup(this); + ui->menuRecent->clear(); + std::vector vec = Config::getRecentFiles(); + for (int i = 0; i < vec.size(); i++) { + QAction* recentFileAct = new QAction(this); + recentFileAct->setText(QString::fromStdString(vec.at(i))); + ui->menuRecent->addAction(recentFileAct); + m_recent_files_group->addAction(recentFileAct); + } + + connect(m_recent_files_group, &QActionGroup::triggered, this, [this](QAction* action) { + auto gamePath = Common::FS::PathFromQString(action->text()); + AddRecentFiles(action->text()); // Update the list. + Core::Emulator emulator; + if (!std::filesystem::exists(gamePath)) { + QMessageBox::critical(nullptr, tr("Run Game"), QString(tr("Eboot.bin file not found"))); + return; + } + emulator.Run(gamePath); + }); +} + +void MainWindow::LoadTranslation() { + auto language = QString::fromStdString(Config::getEmulatorLanguage()); + + const QString base_dir = QStringLiteral(":/translations"); + QString base_path = QStringLiteral("%1/%2.qm").arg(base_dir).arg(language); + + if (QFile::exists(base_path)) { + if (translator != nullptr) { + qApp->removeTranslator(translator); + } + + translator = new QTranslator(qApp); + if (!translator->load(base_path)) { + QMessageBox::warning( + nullptr, QStringLiteral("Translation Error"), + QStringLiteral("Failed to find load translation file for '%1':\n%2") + .arg(language) + .arg(base_path)); + delete translator; + } else { + qApp->installTranslator(translator); + ui->retranslateUi(this); + } + } +} + +void MainWindow::OnLanguageChanged(const std::string& locale) { + Config::setEmulatorLanguage(locale); + + LoadTranslation(); +} + +bool MainWindow::eventFilter(QObject* obj, QEvent* event) { + if (event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { + auto tblMode = Config::getTableMode(); + if (tblMode != 2 && (tblMode != 1 || m_game_grid_frame->IsValidCellSelected())) { + StartGame(); + return true; + } + } + } + return QMainWindow::eventFilter(obj, event); +} diff --git a/src/qt_gui/main_window.h b/src/qt_gui/main_window.h index e69de29bb..9c8975fa9 100644 --- a/src/qt_gui/main_window.h +++ b/src/qt_gui/main_window.h @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "background_music_player.h" +#include "common/config.h" +#include "common/path_util.h" +#include "core/file_format/psf.h" +#include "core/file_sys/fs.h" +#include "qt_gui/discord_rpc_handler.h" +#include "elf_viewer.h" +#include "emulator.h" +#include "game_grid_frame.h" +#include "game_info.h" +#include "game_list_frame.h" +#include "game_list_utils.h" +#include "main_window_themes.h" +#include "main_window_ui.h" +#include "pkg_viewer.h" + +class GameListFrame; + +class MainWindow : public QMainWindow { + Q_OBJECT +signals: + void WindowResized(QResizeEvent* event); + void ExtractionFinished(); + +public: + explicit MainWindow(QWidget* parent = nullptr); + ~MainWindow(); + bool Init(); + void InstallDragDropPkg(std::filesystem::path file, int pkgNum, int nPkg); + void InstallDirectory(); + void StartGame(); + +private Q_SLOTS: + void ConfigureGuiFromSettings(); + void SaveWindowState() const; + void SearchGameTable(const QString& text); + void ShowGameList(); + void RefreshGameTable(); + void HandleResize(QResizeEvent* event); + void OnLanguageChanged(const std::string& locale); + +private: + Ui_MainWindow* ui; + void AddUiWidgets(); + void CreateActions(); + void CreateRecentGameActions(); + void CreateDockWindows(); + void GetPhysicalDevices(); + void LoadGameLists(); + void CheckUpdateMain(bool checkSave); + void CreateConnects(); + void SetLastUsedTheme(); + void SetLastIconSizeBullet(); + void SetUiIcons(bool isWhite); + void InstallPkg(); + void BootGame(); + void AddRecentFiles(QString filePath); + void LoadTranslation(); + void PlayBackgroundMusic(); + QIcon RecolorIcon(const QIcon& icon, bool isWhite); + bool isIconBlack = false; + bool isTableList = true; + bool isGameRunning = false; + QActionGroup* m_icon_size_act_group = nullptr; + QActionGroup* m_list_mode_act_group = nullptr; + QActionGroup* m_theme_act_group = nullptr; + QActionGroup* m_recent_files_group = nullptr; + PKG pkg; + // Dockable widget frames + WindowThemes m_window_themes; + GameListUtils m_game_list_utils; + QScopedPointer m_dock_widget; + // Game Lists + QScopedPointer m_game_list_frame; + QScopedPointer m_game_grid_frame; + QScopedPointer m_elf_viewer; + // Status Bar. + QScopedPointer statusBar; + // Available GPU devices + std::vector m_physical_devices; + + PSF psf; + + std::shared_ptr m_game_info = std::make_shared(); + + QTranslator* translator; + + DiscordRPCHandler::RPC discordRPC; + + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + + void dragEnterEvent(QDragEnterEvent* event1) override { + if (event1->mimeData()->hasUrls()) { + event1->acceptProposedAction(); + } + } + + void dropEvent(QDropEvent* event1) override { + const QMimeData* mimeData = event1->mimeData(); + if (mimeData->hasUrls()) { + QList urlList = mimeData->urls(); + int pkgNum = 0; + int nPkg = urlList.size(); + for (const QUrl& url : urlList) { + pkgNum++; + std::filesystem::path path = Common::FS::PathFromQString(url.toLocalFile()); + InstallDragDropPkg(path, pkgNum, nPkg); + } + } + } + + void resizeEvent(QResizeEvent* event) override; +};