shadPS4/src/qt_gui/gui_context_menus.h
2024-12-28 11:49:44 +01:00

536 lines
22 KiB
C++

// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QClipboard>
#include <QDesktopServices>
#include <QMenu>
#include <QMessageBox>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#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 <ShlObj.h>
#include <Windows.h>
#include <objbase.h>
#include <shlguid.h>
#include <shobjidl.h>
#include <wrl/client.h>
#endif
#include "common/path_util.h"
class GuiContextMenus : public QObject {
Q_OBJECT
public:
void RequestGameMenu(const QPoint& pos, QVector<GameInfo> m_games,
std::shared_ptr<CompatibilityInfoClass> 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<void(std::filesystem::path, int, int)> 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<IShellLink> 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<IPersistFile> 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
};