// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include #include #include #include #include #include "cheats_patches.h" #include "common/config.h" #include "common/version.h" #include "compatibility_info.h" #include "game_info.h" #include "trophy_viewer.h" #ifdef Q_OS_WIN #include #include #include #include #include #include #endif #include "common/path_util.h" class GuiContextMenus : public QObject { Q_OBJECT public: void RequestGameMenu(const QPoint& pos, QVector m_games, std::shared_ptr m_compat_info, QTableWidget* widget, bool isList) { QPoint global_pos = widget->viewport()->mapToGlobal(pos); int itemID = 0; if (isList) { itemID = widget->currentRow(); } else { itemID = widget->currentRow() * widget->columnCount() + widget->currentColumn(); } // Do not show the menu if an item is selected if (itemID == -1) { return; } // Setup menu. QMenu menu(widget); // "Open Folder..." submenu QMenu* openFolderMenu = new QMenu(tr("Open Folder..."), widget); QAction* openGameFolder = new QAction(tr("Open Game Folder"), widget); QAction* openSaveDataFolder = new QAction(tr("Open Save Data Folder"), widget); QAction* openLogFolder = new QAction(tr("Open Log Folder"), widget); openFolderMenu->addAction(openGameFolder); openFolderMenu->addAction(openSaveDataFolder); openFolderMenu->addAction(openLogFolder); menu.addMenu(openFolderMenu); QAction createShortcut(tr("Create Shortcut"), widget); QAction openCheats(tr("Cheats / Patches"), widget); QAction openSfoViewer(tr("SFO Viewer"), widget); QAction openTrophyViewer(tr("Trophy Viewer"), widget); menu.addAction(&createShortcut); menu.addAction(&openCheats); menu.addAction(&openSfoViewer); menu.addAction(&openTrophyViewer); // "Copy" submenu. QMenu* copyMenu = new QMenu(tr("Copy info..."), widget); QAction* copyName = new QAction(tr("Copy Name"), widget); QAction* copySerial = new QAction(tr("Copy Serial"), widget); QAction* copyNameAll = new QAction(tr("Copy All"), widget); copyMenu->addAction(copyName); copyMenu->addAction(copySerial); copyMenu->addAction(copyNameAll); menu.addMenu(copyMenu); // "Delete..." submenu. QMenu* deleteMenu = new QMenu(tr("Delete..."), widget); QAction* deleteGame = new QAction(tr("Delete Game"), widget); QAction* deleteUpdate = new QAction(tr("Delete Update"), widget); QAction* deleteDLC = new QAction(tr("Delete DLC"), widget); deleteMenu->addAction(deleteGame); deleteMenu->addAction(deleteUpdate); deleteMenu->addAction(deleteDLC); menu.addMenu(deleteMenu); // Compatibility submenu. QMenu* compatibilityMenu = new QMenu(tr("Compatibility..."), widget); QAction* updateCompatibility = new QAction(tr("Update database"), widget); QAction* viewCompatibilityReport = new QAction(tr("View report"), widget); QAction* submitCompatibilityReport = new QAction(tr("Submit a report"), widget); compatibilityMenu->addAction(updateCompatibility); compatibilityMenu->addAction(viewCompatibilityReport); compatibilityMenu->addAction(submitCompatibilityReport); menu.addMenu(compatibilityMenu); compatibilityMenu->setEnabled(Config::getCompatibilityEnabled()); viewCompatibilityReport->setEnabled(!m_games[itemID].compatibility.url.isEmpty()); // Show menu. auto selected = menu.exec(global_pos); if (!selected) { return; } if (selected == openGameFolder) { QString folderPath; Common::FS::PathToQString(folderPath, m_games[itemID].path); QDesktopServices::openUrl(QUrl::fromLocalFile(folderPath)); } if (selected == openSaveDataFolder) { QString userPath; Common::FS::PathToQString(userPath, Common::FS::GetUserPath(Common::FS::PathType::UserDir)); QString saveDataPath = userPath + "/savedata/1/" + QString::fromStdString(m_games[itemID].serial); QDir(saveDataPath).mkpath(saveDataPath); QDesktopServices::openUrl(QUrl::fromLocalFile(saveDataPath)); } if (selected == openLogFolder) { QString userPath; Common::FS::PathToQString(userPath, Common::FS::GetUserPath(Common::FS::PathType::UserDir)); QDesktopServices::openUrl(QUrl::fromLocalFile(userPath + "/log")); } if (selected == &openSfoViewer) { PSF psf; std::filesystem::path game_folder_path = m_games[itemID].path; std::filesystem::path game_update_path = game_folder_path; game_update_path += "UPDATE"; if (std::filesystem::exists(game_update_path)) { game_folder_path = game_update_path; } if (psf.Open(game_folder_path / "sce_sys" / "param.sfo")) { int rows = psf.GetEntries().size(); QTableWidget* tableWidget = new QTableWidget(rows, 2); tableWidget->setAttribute(Qt::WA_DeleteOnClose); connect(widget->parent(), &QWidget::destroyed, tableWidget, [tableWidget]() { tableWidget->deleteLater(); }); tableWidget->verticalHeader()->setVisible(false); // Hide vertical header int row = 0; for (const auto& entry : psf.GetEntries()) { QTableWidgetItem* keyItem = new QTableWidgetItem(QString::fromStdString(entry.key)); QTableWidgetItem* valueItem; switch (entry.param_fmt) { case PSFEntryFmt::Binary: { const auto bin = psf.GetBinary(entry.key); if (!bin.has_value()) { valueItem = new QTableWidgetItem(QString("Unknown")); } else { std::string text; text.reserve(bin->size() * 2); for (const auto& c : *bin) { static constexpr char hex[] = "0123456789ABCDEF"; text.push_back(hex[c >> 4 & 0xF]); text.push_back(hex[c & 0xF]); } valueItem = new QTableWidgetItem(QString::fromStdString(text)); } } break; case PSFEntryFmt::Text: { auto text = psf.GetString(entry.key); if (!text.has_value()) { valueItem = new QTableWidgetItem(QString("Unknown")); } else { valueItem = new QTableWidgetItem(QString::fromStdString(std::string{*text})); } } break; case PSFEntryFmt::Integer: { auto integer = psf.GetInteger(entry.key); if (!integer.has_value()) { valueItem = new QTableWidgetItem(QString("Unknown")); } else { valueItem = new QTableWidgetItem(QString("0x") + QString::number(*integer, 16)); } } break; } tableWidget->setItem(row, 0, keyItem); tableWidget->setItem(row, 1, valueItem); keyItem->setFlags(keyItem->flags() & ~Qt::ItemIsEditable); valueItem->setFlags(valueItem->flags() & ~Qt::ItemIsEditable); row++; } tableWidget->resizeColumnsToContents(); tableWidget->resizeRowsToContents(); int width = tableWidget->horizontalHeader()->sectionSize(0) + tableWidget->horizontalHeader()->sectionSize(1) + 2; int height = (rows + 1) * (tableWidget->rowHeight(0)); tableWidget->setFixedSize(width, height); tableWidget->sortItems(0, Qt::AscendingOrder); tableWidget->horizontalHeader()->setVisible(false); tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed); tableWidget->setWindowTitle(tr("SFO Viewer")); tableWidget->show(); } } if (selected == &openCheats) { QString gameName = QString::fromStdString(m_games[itemID].name); QString gameSerial = QString::fromStdString(m_games[itemID].serial); QString gameVersion = QString::fromStdString(m_games[itemID].version); QString gameSize = QString::fromStdString(m_games[itemID].size); QString iconPath; Common::FS::PathToQString(iconPath, m_games[itemID].icon_path); QPixmap gameImage(iconPath); CheatsPatches* cheatsPatches = new CheatsPatches(gameName, gameSerial, gameVersion, gameSize, gameImage); cheatsPatches->show(); connect(widget->parent(), &QWidget::destroyed, cheatsPatches, [cheatsPatches]() { cheatsPatches->deleteLater(); }); } if (selected == &openTrophyViewer) { QString trophyPath, gameTrpPath; Common::FS::PathToQString(trophyPath, m_games[itemID].serial); Common::FS::PathToQString(gameTrpPath, m_games[itemID].path); TrophyViewer* trophyViewer = new TrophyViewer(trophyPath, gameTrpPath); trophyViewer->show(); connect(widget->parent(), &QWidget::destroyed, trophyViewer, [trophyViewer]() { trophyViewer->deleteLater(); }); } if (selected == &createShortcut) { QString targetPath; Common::FS::PathToQString(targetPath, m_games[itemID].path); QString ebootPath = targetPath + "/eboot.bin"; // Get the full path to the icon QString iconPath; Common::FS::PathToQString(iconPath, m_games[itemID].icon_path); QFileInfo iconFileInfo(iconPath); QString icoPath = iconFileInfo.absolutePath() + "/" + iconFileInfo.baseName() + ".ico"; // Path to shortcut/link QString linkPath; // Path to the shadps4.exe executable QString exePath; #ifdef Q_OS_WIN linkPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) + "/" + QString::fromStdString(m_games[itemID].name) .remove(QRegularExpression("[\\\\/:*?\"<>|]")) + ".lnk"; exePath = QCoreApplication::applicationFilePath().replace("\\", "/"); #else linkPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) + "/" + QString::fromStdString(m_games[itemID].name) .remove(QRegularExpression("[\\\\/:*?\"<>|]")) + ".desktop"; #endif // Convert the icon to .ico if necessary if (iconFileInfo.suffix().toLower() == "png") { // Convert icon from PNG to ICO if (convertPngToIco(iconPath, icoPath)) { #ifdef Q_OS_WIN if (createShortcutWin(linkPath, ebootPath, icoPath, exePath)) { #else if (createShortcutLinux(linkPath, ebootPath, iconPath)) { #endif QMessageBox::information( nullptr, tr("Shortcut creation"), QString(tr("Shortcut created successfully!") + "\n%1").arg(linkPath)); } else { QMessageBox::critical( nullptr, tr("Error"), QString(tr("Error creating shortcut!") + "\n%1").arg(linkPath)); } } else { QMessageBox::critical(nullptr, tr("Error"), tr("Failed to convert icon.")); } } else { // If the icon is already in ICO format, we just create the shortcut #ifdef Q_OS_WIN if (createShortcutWin(linkPath, ebootPath, iconPath, exePath)) { #else if (createShortcutLinux(linkPath, ebootPath, iconPath)) { #endif QMessageBox::information( nullptr, tr("Shortcut creation"), QString(tr("Shortcut created successfully!") + "\n%1").arg(linkPath)); } else { QMessageBox::critical( nullptr, tr("Error"), QString(tr("Error creating shortcut!") + "\n%1").arg(linkPath)); } } } // Handle the "Copy" actions if (selected == copyName) { QClipboard* clipboard = QGuiApplication::clipboard(); clipboard->setText(QString::fromStdString(m_games[itemID].name)); } if (selected == copySerial) { QClipboard* clipboard = QGuiApplication::clipboard(); clipboard->setText(QString::fromStdString(m_games[itemID].serial)); } if (selected == copyNameAll) { QClipboard* clipboard = QGuiApplication::clipboard(); QString combinedText = QString("Name:%1 | Serial:%2 | Version:%3 | Size:%4") .arg(QString::fromStdString(m_games[itemID].name)) .arg(QString::fromStdString(m_games[itemID].serial)) .arg(QString::fromStdString(m_games[itemID].version)) .arg(QString::fromStdString(m_games[itemID].size)); clipboard->setText(combinedText); } if (selected == deleteGame || selected == deleteUpdate || selected == deleteDLC) { bool error = false; QString folder_path, game_update_path, dlc_path; Common::FS::PathToQString(folder_path, m_games[itemID].path); game_update_path = folder_path + "-UPDATE"; Common::FS::PathToQString( dlc_path, Config::getAddonInstallDir() / Common::FS::PathFromQString(folder_path).parent_path().filename()); QString message_type = tr("Game"); if (selected == deleteUpdate) { if (!std::filesystem::exists(Common::FS::PathFromQString(game_update_path))) { QMessageBox::critical(nullptr, tr("Error"), QString(tr("This game has no update to delete!"))); error = true; } else { folder_path = game_update_path; message_type = tr("Update"); } } else if (selected == deleteDLC) { if (!std::filesystem::exists(Common::FS::PathFromQString(dlc_path))) { QMessageBox::critical(nullptr, tr("Error"), QString(tr("This game has no DLC to delete!"))); error = true; } else { folder_path = dlc_path; message_type = tr("DLC"); } } if (!error) { QString gameName = QString::fromStdString(m_games[itemID].name); QDir dir(folder_path); QMessageBox::StandardButton reply = QMessageBox::question( nullptr, QString(tr("Delete %1")).arg(message_type), QString(tr("Are you sure you want to delete %1's %2 directory?")) .arg(gameName, message_type), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { dir.removeRecursively(); widget->removeRow(itemID); } } } if (selected == updateCompatibility) { m_compat_info->UpdateCompatibilityDatabase(widget, true); } if (selected == viewCompatibilityReport) { if (!m_games[itemID].compatibility.url.isEmpty()) QDesktopServices::openUrl(QUrl(m_games[itemID].compatibility.url)); } if (selected == submitCompatibilityReport) { QUrl url = QUrl("https://github.com/shadps4-emu/shadps4-game-compatibility/issues/new"); QUrlQuery query; query.addQueryItem("template", QString("game_compatibility.yml")); query.addQueryItem( "title", QString("%1 - %2").arg(QString::fromStdString(m_games[itemID].serial), QString::fromStdString(m_games[itemID].name))); query.addQueryItem("game-name", QString::fromStdString(m_games[itemID].name)); query.addQueryItem("game-code", QString::fromStdString(m_games[itemID].serial)); query.addQueryItem("game-version", QString::fromStdString(m_games[itemID].version)); query.addQueryItem("emulator-version", QString(Common::VERSION)); url.setQuery(query); QDesktopServices::openUrl(url); } } int GetRowIndex(QTreeWidget* treeWidget, QTreeWidgetItem* item) { int row = 0; for (int i = 0; i < treeWidget->topLevelItemCount(); i++) { // check top level/parent items QTreeWidgetItem* currentItem = treeWidget->topLevelItem(i); if (currentItem == item) { return row; } row++; if (currentItem->childCount() > 0) { // check child items for (int j = 0; j < currentItem->childCount(); j++) { QTreeWidgetItem* childItem = currentItem->child(j); if (childItem == item) { return row; } row++; } } } return -1; } void RequestGameMenuPKGViewer( const QPoint& pos, QStringList m_pkg_app_list, QTreeWidget* treeWidget, std::function InstallDragDropPkg) { QPoint global_pos = treeWidget->viewport()->mapToGlobal(pos); // context menu position QTreeWidgetItem* currentItem = treeWidget->currentItem(); // current clicked item int itemIndex = GetRowIndex(treeWidget, currentItem); // row QMenu menu(treeWidget); QAction installPackage(tr("Install PKG"), treeWidget); menu.addAction(&installPackage); auto selected = menu.exec(global_pos); if (!selected) { return; } if (selected == &installPackage) { QStringList pkg_app_ = m_pkg_app_list[itemIndex].split(";;"); std::filesystem::path path = Common::FS::PathFromQString(pkg_app_[9]); InstallDragDropPkg(path, 1, 1); } } private: bool convertPngToIco(const QString& pngFilePath, const QString& icoFilePath) { // Load the PNG image QImage image(pngFilePath); if (image.isNull()) { return false; } // Scale the image to the default icon size (256x256 pixels) QImage scaledImage = image.scaled(QSize(256, 256), Qt::KeepAspectRatio, Qt::SmoothTransformation); // Convert the image to QPixmap QPixmap pixmap = QPixmap::fromImage(scaledImage); // Save the pixmap as an ICO file if (pixmap.save(icoFilePath, "ICO")) { return true; } else { return false; } } #ifdef Q_OS_WIN bool createShortcutWin(const QString& linkPath, const QString& targetPath, const QString& iconPath, const QString& exePath) { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); // Create the ShellLink object Microsoft::WRL::ComPtr pShellLink; HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink)); if (SUCCEEDED(hres)) { // Defines the path to the program executable pShellLink->SetPath((LPCWSTR)exePath.utf16()); // Sets the home directory ("Start in") pShellLink->SetWorkingDirectory((LPCWSTR)QFileInfo(exePath).absolutePath().utf16()); // Set arguments, eboot.bin file location QString arguments = QString("-g \"%1\"").arg(targetPath); pShellLink->SetArguments((LPCWSTR)arguments.utf16()); // Set the icon for the shortcut pShellLink->SetIconLocation((LPCWSTR)iconPath.utf16(), 0); // Save the shortcut Microsoft::WRL::ComPtr pPersistFile; hres = pShellLink.As(&pPersistFile); if (SUCCEEDED(hres)) { hres = pPersistFile->Save((LPCWSTR)linkPath.utf16(), TRUE); } } CoUninitialize(); return SUCCEEDED(hres); } #else bool createShortcutLinux(const QString& linkPath, const QString& targetPath, const QString& iconPath) { QFile shortcutFile(linkPath); if (!shortcutFile.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::critical(nullptr, "Error", QString("Error creating shortcut!\n %1").arg(linkPath)); return false; } QTextStream out(&shortcutFile); out << "[Desktop Entry]\n"; out << "Version=1.0\n"; out << "Name=" << QFileInfo(linkPath).baseName() << "\n"; out << "Exec=" << QCoreApplication::applicationFilePath() << " \"" << targetPath << "\"\n"; out << "Icon=" << iconPath << "\n"; out << "Terminal=false\n"; out << "Type=Application\n"; shortcutFile.close(); return true; } #endif };