// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include #include "common/io_file.h" #include "common/version.h" #include "core/file_format/pkg.h" #include "core/loader.h" #include "game_install_dialog.h" #include "gui_settings.h" #include "main_window.h" MainWindow::MainWindow(std::shared_ptr gui_settings, QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow), m_gui_settings(std::move(gui_settings)) { ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); } MainWindow::~MainWindow() { SaveWindowState(); } bool MainWindow::Init() { auto start = std::chrono::steady_clock::now(); AddUiWidgets(); CreateActions(); CreateDockWindows(); CreateConnects(); SetLastUsedTheme(); SetLastIconSizeBullet(); ConfigureGuiFromSettings(); LoadGameLists(); setMinimumSize(350, minimumSizeHint().height()); setWindowTitle(QString::fromStdString("shadPS4 v" + std::string(Common::VERSION))); show(); auto end = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(end - start); statusBar = new QStatusBar(this); m_main_window->setStatusBar(statusBar); // Update status bar int numGames = m_game_info->m_games.size(); QString statusMessage = "Games: " + QString::number(numGames) + " (" + QString::number(duration.count()) + "ms). Ready."; statusBar->showMessage(statusMessage); 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->setThemeLight); m_theme_act_group->addAction(ui->setThemeDark); 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->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() { m_main_window = new QMainWindow(); m_main_window->setContextMenuPolicy(Qt::PreventContextMenu); // resize window to last W and H QSize window_size = m_gui_settings->GetValue(gui::m_window_size).toSize(); m_main_window->resize(window_size.width(), window_size.height()); // Add the game table. m_dock_widget = new QDockWidget("Game List", m_main_window); m_game_list_frame = new GameListFrame(m_game_info, m_gui_settings, m_main_window); m_game_list_frame->setObjectName("gamelist"); m_game_grid_frame = new GameGridFrame(m_game_info, m_gui_settings, m_main_window); m_game_grid_frame->setObjectName("gamegridlist"); int table_mode = m_gui_settings->GetValue(gui::m_table_mode).toInt(); int slider_pos = 0; if (table_mode == 0) { // List m_game_grid_frame->hide(); m_dock_widget->setWidget(m_game_list_frame); slider_pos = m_gui_settings->GetValue(gui::m_slide_pos).toInt(); ui->sizeSlider->setSliderPosition(slider_pos); // set slider pos at start; isTableList = true; } else { // Grid m_game_list_frame->hide(); m_dock_widget->setWidget(m_game_grid_frame); slider_pos = m_gui_settings->GetValue(gui::m_slide_pos_grid).toInt(); ui->sizeSlider->setSliderPosition(slider_pos); // set slider pos at start; isTableList = false; } m_main_window->addDockWidget(Qt::LeftDockWidgetArea, m_dock_widget); m_main_window->setDockNestingEnabled(true); setCentralWidget(m_main_window); } void MainWindow::LoadGameLists() { // Get game info from game folders. m_game_info->GetGameInfo(); if (isTableList) { m_game_list_frame->PopulateGameList(); } else { m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); } } 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->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); m_gui_settings->SetValue(gui::m_icon_size, 36 + value); m_gui_settings->SetValue(gui::m_slide_pos, value); } else { m_game_grid_frame->icon_size = 69 + value; m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); m_gui_settings->SetValue(gui::m_icon_size_grid, 69 + value); m_gui_settings->SetValue(gui::m_slide_pos_grid, value); } }); connect(ui->setIconSizeTinyAct, &QAction::triggered, this, [this](int value) { if (isTableList) { m_game_list_frame->icon_size = 36; // 36 is the minimum icon size to use due to text disappearing. m_gui_settings->SetValue(gui::m_icon_size, 36); ui->sizeSlider->setValue(0); // icone_size - 36 m_gui_settings->SetValue(gui::m_slide_pos, 0); } else { m_gui_settings->SetValue(gui::m_icon_size_grid, 69); // nice :3 ui->sizeSlider->setValue(0); // icone_size - 36 m_gui_settings->SetValue(gui::m_slide_pos_grid, 0); } }); connect(ui->setIconSizeSmallAct, &QAction::triggered, this, [this](int value) { if (isTableList) { m_game_list_frame->icon_size = 64; m_gui_settings->SetValue(gui::m_icon_size, 64); ui->sizeSlider->setValue(28); m_gui_settings->SetValue(gui::m_slide_pos, 28); } else { m_gui_settings->SetValue(gui::m_icon_size_grid, 97); ui->sizeSlider->setValue(28); m_gui_settings->SetValue(gui::m_slide_pos_grid, 28); } }); connect(ui->setIconSizeMediumAct, &QAction::triggered, this, [this](int value) { if (isTableList) { m_game_list_frame->icon_size = 128; m_gui_settings->SetValue(gui::m_icon_size, 128); ui->sizeSlider->setValue(92); m_gui_settings->SetValue(gui::m_slide_pos, 92); } else { m_gui_settings->SetValue(gui::m_icon_size_grid, 160); ui->sizeSlider->setValue(92); m_gui_settings->SetValue(gui::m_slide_pos_grid, 92); } }); connect(ui->setIconSizeLargeAct, &QAction::triggered, this, [this](int value) { if (isTableList) { m_game_list_frame->icon_size = 256; m_gui_settings->SetValue(gui::m_icon_size, 256); ui->sizeSlider->setValue(220); m_gui_settings->SetValue(gui::m_slide_pos, 220); } else { m_gui_settings->SetValue(gui::m_icon_size_grid, 256); ui->sizeSlider->setValue(220); m_gui_settings->SetValue(gui::m_slide_pos_grid, 220); } }); connect(ui->setlistModeListAct, &QAction::triggered, m_dock_widget, [this]() { m_dock_widget->setWidget(m_game_list_frame); m_game_list_frame->show(); m_game_grid_frame->hide(); if (m_game_list_frame->item(0, 0) == nullptr) { m_game_list_frame->clearContents(); m_game_list_frame->PopulateGameList(); } isTableList = true; m_gui_settings->SetValue(gui::m_table_mode, 0); // save table mode int slider_pos = m_gui_settings->GetValue(gui::m_slide_pos).toInt(); ui->sizeSlider->setSliderPosition(slider_pos); }); connect(ui->setlistModeGridAct, &QAction::triggered, m_dock_widget, [this]() { m_dock_widget->setWidget(m_game_grid_frame); m_game_grid_frame->show(); m_game_list_frame->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; m_gui_settings->SetValue(gui::m_table_mode, 1); // save table mode int slider_pos_grid = m_gui_settings->GetValue(gui::m_slide_pos_grid).toInt(); ui->sizeSlider->setSliderPosition(slider_pos_grid); }); // Dump game list. connect(ui->dumpGameListAct, &QAction::triggered, this, [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) { 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(QString::fromStdString(game.path)); } }); // Package install. connect(ui->bootInstallPkgAct, &QAction::triggered, this, [this] { InstallPkg(); }); connect(ui->gameInstallPathAct, &QAction::triggered, this, [this] { InstallDirectory(); }); // Package Viewer. connect(ui->pkgViewerAct, &QAction::triggered, this, [this]() { PKGViewer* pkgViewer = new PKGViewer(m_game_info, m_gui_settings, [this](std::string file, int pkgNum, int nPkg) { this->InstallDragDropPkg(file, pkgNum, nPkg); }); pkgViewer->show(); }); // Themes connect(ui->setThemeLight, &QAction::triggered, &m_window_themes, [this]() { m_window_themes.SetWindowTheme(Theme::Light, ui->mw_searchbar); m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Light)); if (!isIconBlack) { SetUiIcons(true); isIconBlack = true; } }); connect(ui->setThemeDark, &QAction::triggered, &m_window_themes, [this]() { m_window_themes.SetWindowTheme(Theme::Dark, ui->mw_searchbar); m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Dark)); if (isIconBlack) { SetUiIcons(false); isIconBlack = false; } }); connect(ui->setThemeGreen, &QAction::triggered, &m_window_themes, [this]() { m_window_themes.SetWindowTheme(Theme::Green, ui->mw_searchbar); m_gui_settings->SetValue(gui::mw_themes, 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); m_gui_settings->SetValue(gui::mw_themes, 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); m_gui_settings->SetValue(gui::mw_themes, static_cast(Theme::Violet)); if (isIconBlack) { SetUiIcons(false); isIconBlack = false; } }); } 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::RefreshGameTable() { m_game_info->m_games.clear(); m_game_info->GetGameInfo(); 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 = "Games: " + QString::number(numGames) + ". Ready."; statusBar->showMessage(statusMessage); } void MainWindow::ConfigureGuiFromSettings() { // Restore GUI state if needed. We need to if they exist. if (!restoreGeometry(m_gui_settings->GetValue(gui::main_window_geometry).toByteArray())) { resize(QGuiApplication::primaryScreen()->availableSize() * 0.7); } m_main_window->restoreState(m_gui_settings->GetValue(gui::main_window_mwState).toByteArray()); ui->showGameListAct->setChecked( m_gui_settings->GetValue(gui::main_window_gamelist_visible).toBool()); if (isTableList) { ui->setlistModeListAct->setChecked(true); } else { ui->setlistModeGridAct->setChecked(true); } } void MainWindow::SaveWindowState() const { // Save gui settings m_gui_settings->SetValue(gui::main_window_geometry, saveGeometry()); m_gui_settings->SetValue(gui::main_window_windowState, saveState()); m_gui_settings->SetValue(gui::m_window_size, QSize(m_main_window->width(), m_main_window->height())); m_gui_settings->SetValue(gui::main_window_mwState, m_main_window->saveState()); } void MainWindow::InstallPkg() { QStringList fileNames = QFileDialog::getOpenFileNames( this, tr("Install PKG Files"), QDir::currentPath(), tr("PKG File (*.PKG)")); int nPkg = fileNames.size(); int pkgNum = 0; for (const QString& file : fileNames) { pkgNum++; MainWindow::InstallDragDropPkg(file.toStdString(), pkgNum, nPkg); } } void MainWindow::InstallDragDropPkg(std::string file, int pkgNum, int nPkg) { if (Loader::DetectFileType(file) == Loader::FileTypes::Pkg) { PKG pkg; pkg.Open(file); std::string failreason; const auto extract_path = std::filesystem::path( m_gui_settings->GetValue(gui::settings_install_dir).toString().toStdString()) / pkg.GetTitleID(); if (!pkg.Extract(file, extract_path, failreason)) { QMessageBox::critical(this, "PKG ERROR", QString::fromStdString(failreason), QMessageBox::Ok); } else { int nfiles = pkg.GetNumberOfFiles(); QList indices; for (int i = 0; i < nfiles; i++) { indices.append(i); } QProgressDialog dialog; dialog.setWindowTitle("PKG Extraction"); QString extractmsg = QString("Extracting PKG %1/%2").arg(pkgNum).arg(nPkg); dialog.setLabelText(extractmsg); // Create a QFutureWatcher and connect signals and slots. QFutureWatcher futureWatcher; QObject::connect(&futureWatcher, SIGNAL(finished()), &dialog, SLOT(reset())); QObject::connect(&dialog, SIGNAL(canceled()), &futureWatcher, SLOT(cancel())); QObject::connect(&futureWatcher, SIGNAL(progressRangeChanged(int, int)), &dialog, SLOT(setRange(int, int))); QObject::connect(&futureWatcher, SIGNAL(progressValueChanged(int)), &dialog, SLOT(setValue(int))); futureWatcher.setFuture(QtConcurrent::map( indices, std::bind(&PKG::ExtractFiles, pkg, std::placeholders::_1))); // Display the dialog and start the event loop. dialog.exec(); futureWatcher.waitForFinished(); auto path = m_gui_settings->GetValue(gui::settings_install_dir).toString(); if (pkgNum == nPkg) { QMessageBox::information(this, "Extraction Finished", "Game successfully installed at " + path, QMessageBox::Ok); // Refresh game table after extraction. RefreshGameTable(); } } } else { QMessageBox::critical(this, "PKG ERROR", "File doesn't appear to be a valid PKG file", QMessageBox::Ok); } } void MainWindow::InstallDirectory() { GameInstallDialog dlg(m_gui_settings); dlg.exec(); } void MainWindow::SetLastUsedTheme() { Theme lastTheme = static_cast(m_gui_settings->GetValue(gui::mw_themes).toInt()); 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 = m_gui_settings->GetValue(gui::m_icon_size).toInt(); 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)), QIcon::Normal)); 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); QIcon newIcon(pixmap); return newIcon; } void MainWindow::SetUiIcons(bool isWhite) { QIcon icon; icon = RecolorIcon(ui->bootInstallPkgAct->icon(), isWhite); ui->bootInstallPkgAct->setIcon(icon); icon = RecolorIcon(ui->exitAct->icon(), isWhite); ui->exitAct->setIcon(icon); icon = RecolorIcon(ui->setlistModeListAct->icon(), isWhite); ui->setlistModeListAct->setIcon(icon); icon = RecolorIcon(ui->setlistModeGridAct->icon(), isWhite); ui->setlistModeGridAct->setIcon(icon); icon = RecolorIcon(ui->gameInstallPathAct->icon(), isWhite); ui->gameInstallPathAct->setIcon(icon); icon = RecolorIcon(ui->menuThemes->icon(), isWhite); ui->menuThemes->setIcon(icon); icon = RecolorIcon(ui->menuGame_List_Icons->icon(), isWhite); ui->menuGame_List_Icons->setIcon(icon); icon = RecolorIcon(ui->playButton->icon(), isWhite); ui->playButton->setIcon(icon); icon = RecolorIcon(ui->pauseButton->icon(), isWhite); ui->pauseButton->setIcon(icon); icon = RecolorIcon(ui->stopButton->icon(), isWhite); ui->stopButton->setIcon(icon); icon = RecolorIcon(ui->settingsButton->icon(), isWhite); ui->settingsButton->setIcon(icon); icon = RecolorIcon(ui->controllerButton->icon(), isWhite); ui->controllerButton->setIcon(icon); icon = RecolorIcon(ui->refreshGameListAct->icon(), isWhite); ui->refreshGameListAct->setIcon(icon); icon = RecolorIcon(ui->menuGame_List_Mode->icon(), isWhite); ui->menuGame_List_Mode->setIcon(icon); icon = RecolorIcon(ui->pkgViewerAct->icon(), isWhite); ui->pkgViewerAct->setIcon(icon); } 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(); } }