rpcsx/rpcs3/rpcs3qt/game_list_frame.cpp
Megamouse d2518caf2e GUI: Hotfix for Boot Recent (#2847)
* fix recent list
check for empty name
only change list enabled at aboutToShow
only AddRecentGame if Boot succeeded

* use stringpairlist instead of two stringlists
fix weird typo line

* fix travis
2017-06-08 19:59:07 +01:00

653 lines
19 KiB
C++

#include "game_list_frame.h"
#include "settings_dialog.h"
#include "table_item_delegate.h"
#include "Emu/Memory/Memory.h"
#include "Emu/System.h"
#include "Loader/PSF.h"
#include "Utilities/types.h"
#include <algorithm>
#include <memory>
#include <QDesktopServices>
#include <QDir>
#include <QHeaderView>
#include <QListView>
#include <QMenuBar>
#include <QMessageBox>
#include <QProcess>
#include <QTimer>
#include <QUrl>
static const std::string m_class_name = "GameViewer";
inline std::string sstr(const QString& _in) { return _in.toUtf8().toStdString(); }
// Auxiliary classes
class sortGameData
{
int sortColumn;
bool sortAscending;
public:
sortGameData(u32 column, bool ascending) : sortColumn(column), sortAscending(ascending) {}
bool operator()(const GameInfo& game1, const GameInfo& game2) const
{
// Note that the column index has to match the appropriate GameInfo member
switch (sortColumn - 1) // skip *icon* column
{
case 0: return sortAscending ? (game1.name < game2.name) : (game1.name > game2.name);
case 1: return sortAscending ? (game1.serial < game2.serial) : (game1.serial > game2.serial);
case 2: return sortAscending ? (game1.fw < game2.fw) : (game1.fw > game2.fw);
case 3: return sortAscending ? (game1.app_ver < game2.app_ver) : (game1.app_ver > game2.app_ver);
case 4: return sortAscending ? (game1.category < game2.category) : (game1.category > game2.category);
case 5: return sortAscending ? (game1.root < game2.root) : (game1.root > game2.root);
default: return false;
}
}
};
game_list_frame::game_list_frame(std::shared_ptr<gui_settings> settings, Render_Creator r_Creator, QWidget *parent)
: QDockWidget(tr("Game List"), parent), xgui_settings(settings), m_Render_Creator(r_Creator)
{
m_Icon_Size = GUI::gl_icon_size.at(m_gui_settings->GetValue(GUI::gl_iconSize).toString());
m_columns = columns_arr(m_Icon_Size);
gameList = new QTableWidget(this);
gameList->setShowGrid(false);
gameList->setItemDelegate(new table_item_delegate(this));
gameList->setSelectionBehavior(QAbstractItemView::SelectRows);
gameList->setSelectionMode(QAbstractItemView::SingleSelection);
gameList->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
gameList->verticalHeader()->setMinimumSectionSize(m_Icon_Size.height());
gameList->verticalHeader()->setMaximumSectionSize(m_Icon_Size.height());
gameList->verticalHeader()->setVisible(false);
gameList->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
gameList->setContextMenuPolicy(Qt::CustomContextMenu);
gameList->setColumnCount(7);
gameList->setHorizontalHeaderItem(0, new QTableWidgetItem(tr("Icon")));
gameList->setHorizontalHeaderItem(1, new QTableWidgetItem(tr("Name")));
gameList->setHorizontalHeaderItem(2, new QTableWidgetItem(tr("Serial")));
gameList->setHorizontalHeaderItem(3, new QTableWidgetItem(tr("FW")));
gameList->setHorizontalHeaderItem(4, new QTableWidgetItem(tr("App version")));
gameList->setHorizontalHeaderItem(5, new QTableWidgetItem(tr("Category")));
gameList->setHorizontalHeaderItem(6, new QTableWidgetItem(tr("Path")));
setWidget(gameList);
// Actions
showIconColAct = new QAction(tr("Show Icons"), this);
showNameColAct = new QAction(tr("Show Names"), this);
showSerialColAct = new QAction(tr("Show Serials"), this);
showFWColAct = new QAction(tr("Show FWs"), this);
showAppVersionColAct = new QAction(tr("Show App Versions"), this);
showCategoryColAct = new QAction(tr("Show Categories"), this);
showPathColAct = new QAction(tr("Show Paths"), this);
columnActs = { showIconColAct, showNameColAct, showSerialColAct, showFWColAct, showAppVersionColAct, showCategoryColAct, showPathColAct };
// Events
connect(gameList, &QTableWidget::customContextMenuRequested, this, &game_list_frame::ShowContextMenu);
connect(gameList->horizontalHeader(), &QHeaderView::customContextMenuRequested, [=](const QPoint& pos) {
QMenu* configure = new QMenu(this);
configure->addActions({ showIconColAct, showNameColAct, showSerialColAct, showFWColAct, showAppVersionColAct, showCategoryColAct, showPathColAct });
configure->exec(mapToGlobal(pos));
});
connect(gameList, &QTableWidget::doubleClicked, this, &game_list_frame::doubleClickedSlot);
connect(gameList->horizontalHeader(), &QHeaderView::sectionClicked, this, &game_list_frame::OnColClicked);
for (int col = 0; col < columnActs.count(); ++col)
{
columnActs[col]->setCheckable(true);
auto l_CallBack = [this, col](bool val) {
if (!val) // be sure to have at least one column left so you can call the context menu at all time
{
int c = 0;
for (int i = 0; i < columnActs.count(); ++i)
{
if (xgui_settings->GetGamelistColVisibility(i)) { if (++c > 1) { break; } }
}
if (c < 2)
{
columnActs[col]->setChecked(true); // re-enable the checkbox if we don't change the actual state
return;
}
}
gameList->setColumnHidden(col, !val); // Negate because it's a set col hidden and we have menu say show.
xgui_settings->SetGamelistColVisibility(col, val);
};
connect(columnActs[col], &QAction::triggered, l_CallBack);
}
// Init
Refresh(); // Data MUST be loaded so that first settings load will reset columns to correct width w/r to data.
LoadSettings();
}
void game_list_frame::LoadSettings()
{
QByteArray state = xgui_settings->GetValue(GUI::gl_state).toByteArray();
for (int col = 0; col < columnActs.count(); ++col)
{
bool vis = xgui_settings->GetGamelistColVisibility(col);
columnActs[col]->setChecked(vis);
gameList->setColumnHidden(col, !vis);
}
m_sortAscending = xgui_settings->GetValue(GUI::gl_sortAsc).toBool();
m_sortColumn = xgui_settings->GetValue(GUI::gl_sortCol).toInt();
m_categoryFilters = xgui_settings->GetGameListCategoryFilters();
if (state.isEmpty())
{ // If no settings exist, go to default.
gameList->verticalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents);
gameList->horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents);
gameList->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed);
}
else
{
gameList->horizontalHeader()->restoreState(state);
}
Refresh();
}
game_list_frame::~game_list_frame()
{
SaveSettings();
}
void game_list_frame::OnColClicked(int col)
{
if (col == m_sortColumn)
{
m_sortAscending ^= true;
}
else
{
m_sortAscending = true;
}
m_sortColumn = col;
xgui_settings->SetValue(GUI::gl_sortAsc, m_sortAscending);
xgui_settings->SetValue(GUI::gl_sortCol, col);
// Sort entries, update columns and refresh the panel
Refresh();
}
void game_list_frame::LoadGames()
{
m_games.clear();
for (const auto& entry : fs::dir(Emu.GetGameDir()))
{
if (entry.is_directory)
{
m_games.push_back(entry.name);
}
}
}
void game_list_frame::LoadPSF()
{
m_game_data.clear();
const std::string& game_path = Emu.GetGameDir();
for (u32 i = 0; i < m_games.size(); ++i)
{
const std::string& dir = game_path + m_games[i];
const std::string& sfb = dir + "/PS3_DISC.SFB";
const std::string& sfo = dir + (fs::is_file(sfb) ? "/PS3_GAME/PARAM.SFO" : "/PARAM.SFO");
const fs::file sfo_file(sfo);
if (!sfo_file)
{
continue;
}
const auto& psf = psf::load_object(sfo_file);
GameInfo game;
game.root = m_games[i];
game.serial = psf::get_string(psf, "TITLE_ID", "");
game.name = psf::get_string(psf, "TITLE", "unknown");
game.app_ver = psf::get_string(psf, "APP_VER", "unknown");
game.category = psf::get_string(psf, "CATEGORY", "unknown");
game.fw = psf::get_string(psf, "PS3_SYSTEM_VER", "unknown");
game.parental_lvl = psf::get_integer(psf, "PARENTAL_LEVEL");
game.resolution = psf::get_integer(psf, "RESOLUTION");
game.sound_format = psf::get_integer(psf, "SOUND_FORMAT");
if (game.category == "HG")
{
game.category = sstr(category::hdd_Game);
game.icon_path = dir + "/ICON0.PNG";
}
else if (game.category == "DG")
{
game.category = sstr(category::disc_Game);
game.icon_path = dir + "/PS3_GAME/ICON0.PNG";
}
else if (game.category == "HM")
{
game.category = sstr(category::home);
game.icon_path = dir + "/ICON0.PNG";
}
else if (game.category == "AV")
{
game.category = sstr(category::audio_Video);
game.icon_path = dir + "/ICON0.PNG";
}
else if (game.category == "GD")
{
game.category = sstr(category::game_Data);
game.icon_path = dir + "/ICON0.PNG";
}
else if (game.category == "unknown")
{
game.category = sstr(category::unknown);
}
m_game_data.push_back(game);
}
// Sort entries and update columns
std::sort(m_game_data.begin(), m_game_data.end(), sortGameData(m_sortColumn, m_sortAscending));
m_columns.Update(m_game_data);
}
void game_list_frame::ShowData()
{
m_columns.ShowData(gameList);
}
// Filter for Categories
void game_list_frame::FilterData()
{
for (int i = 0; i < gameList->rowCount(); ++i)
{
bool match = false;
for (auto filter : m_categoryFilters)
{
for (int j = 0; j < gameList->columnCount(); ++j)
{
if (gameList->horizontalHeaderItem(j)->text() == tr("Category") && gameList->item(i, j)->text().contains(filter))
{
match = true;
goto OutOfThis;
}
}
}
OutOfThis:
gameList->setRowHidden(i, !match);
}
}
void game_list_frame::Refresh()
{
int row = gameList->currentRow();
LoadGames();
LoadPSF();
ShowData();
FilterData();
gameList->selectRow(row);
}
void game_list_frame::ToggleCategoryFilter(QString category, bool show)
{
if (show) { m_categoryFilters.append(category); } else { m_categoryFilters.removeAll(category); }
Refresh();
}
void game_list_frame::SaveSettings()
{
for (int col = 0; col < columnActs.count(); ++col)
{
xgui_settings->SetGamelistColVisibility(col, columnActs[col]->isChecked());
}
xgui_settings->SetValue(GUI::gl_sortCol, m_sortColumn);
xgui_settings->SetValue(GUI::gl_sortAsc, m_sortAscending);
xgui_settings->SetValue(GUI::gl_state, gameList->horizontalHeader()->saveState());
}
static void open_dir(const std::string& spath)
{
fs::create_dir(spath);
QString path = qstr(spath);
QProcess* process = new QProcess();
#ifdef _WIN32
std::string command = "explorer";
std::replace(path.begin(), path.end(), '/', '\\');
process->start("explorer", QStringList() << path);
#elif __APPLE__
process->start("open", QStringList() << path);
#elif __linux__
process->start("xdg-open", QStringList() << path);
#endif
}
void game_list_frame::doubleClickedSlot(const QModelIndex& index)
{
int i = index.row();
QString category = qstr(m_game_data[i].category);
// Boot these categories
if (category == category::hdd_Game || category == category::disc_Game || category == category::audio_Video)
{
const std::string& path = Emu.GetGameDir() + m_game_data[i].root;
emit RequestIconPathSet(path);
Emu.Stop();
if (!Emu.BootGame(path))
{
LOG_ERROR(LOADER, "Failed to boot /dev_hdd0/game/%s", m_game_data[i].root);
}
else
{
LOG_SUCCESS(LOADER, "Boot from gamelist per doubleclick: done");
emit RequestAddRecentGame(q_string_pair(qstr(path), qstr("[" + m_game_data[i].serial + "] " + m_game_data[i].name)));
}
}
else
{
open_dir(Emu.GetGameDir() + m_game_data[i].root);
}
}
void game_list_frame::ShowContextMenu(const QPoint &pos) // this is a slot
{
int row = gameList->indexAt(pos).row();
if (row == -1)
{
return; // invalid
}
// for most widgets
QPoint globalPos = gameList->mapToGlobal(pos);
// for QAbstractScrollArea and derived classes you would use:
// QPoint globalPos = myWidget->viewport()->mapToGlobal(pos);
QMenu myMenu;
// Make Actions
QAction* boot = myMenu.addAction(tr("&Boot"));
QFont f = boot->font();
f.setBold(true);
boot->setFont(f);
QAction* configure = myMenu.addAction(tr("&Configure"));
myMenu.addSeparator();
QAction* removeGame = myMenu.addAction(tr("&Remove"));
QAction* removeConfig = myMenu.addAction(tr("&Remove Custom Configuration"));
myMenu.addSeparator();
QAction* openGameFolder = myMenu.addAction(tr("&Open Install Folder"));
QAction* openConfig = myMenu.addAction(tr("&Open Config Folder"));
myMenu.addSeparator();
QAction* checkCompat = myMenu.addAction(tr("&Check Game Compatibility"));
connect(boot, &QAction::triggered, [=]() {Boot(row); });
connect(configure, &QAction::triggered, [=](){
settings_dialog(xgui_settings, m_Render_Creator, this, &m_game_data[row]).exec();
});
connect(removeGame, &QAction::triggered, [=](){
if (QMessageBox::question(this, tr("Confirm Delete"), tr("Permanently delete files?")) == QMessageBox::Yes)
fs::remove_all(Emu.GetGameDir() + m_game_data[row].root);
Refresh();
});
connect(removeConfig, &QAction::triggered, [=]() {RemoveCustomConfiguration(row); });
connect(openGameFolder, &QAction::triggered, [=]() {open_dir(Emu.GetGameDir() + m_game_data[row].root); });
connect(openConfig, &QAction::triggered, [=]() {open_dir(fs::get_config_dir() + "data/" + m_game_data[row].serial); });
connect(checkCompat, &QAction::triggered, [=]() {
QString serial = qstr(m_game_data[row].serial);
QString link = "https://rpcs3.net/compatibility?g=" + serial;
QDesktopServices::openUrl(QUrl(link));
});
//Disable options depending on software category
QString category = qstr(m_game_data[row].category);
if (category == category::disc_Game)
{
removeGame->setEnabled(false);
}
else if (category == category::audio_Video)
{
configure->setEnabled(false);
removeConfig->setEnabled(false);
openConfig->setEnabled(false);
checkCompat->setEnabled(false);
}
else if (category == category::home || category == category::game_Data)
{
boot->setEnabled(false), f.setBold(false), boot->setFont(f);
configure->setEnabled(false);
removeConfig->setEnabled(false);
openConfig->setEnabled(false);
checkCompat->setEnabled(false);
}
myMenu.exec(globalPos);
}
void game_list_frame::Boot(int row)
{
const std::string& path = Emu.GetGameDir() + m_game_data[row].root;
emit RequestIconPathSet(path);
Emu.Stop();
if (!Emu.BootGame(path))
{
QMessageBox::warning(this, tr("Warning!"), tr("Failed to boot ") + qstr(m_game_data[row].root));
LOG_ERROR(LOADER, "Failed to boot /dev_hdd0/game/%s", m_game_data[row].root);
}
else
{
LOG_SUCCESS(LOADER, "Boot from gamelist per Boot: done");
emit RequestAddRecentGame(q_string_pair(qstr(path), qstr("[" + m_game_data[row].serial + "] " + m_game_data[row].name)));
}
}
void game_list_frame::RemoveCustomConfiguration(int row)
{
const std::string config_path = fs::get_config_dir() + "data/" + m_game_data[row].serial + "/config.yml";
if (fs::is_file(config_path))
{
if (QMessageBox::question(this, tr("Confirm Delete"), tr("Delete custom game configuration?")) == QMessageBox::Yes)
{
if (fs::remove_file(config_path))
{
LOG_SUCCESS(GENERAL, "Removed configuration file: %s", config_path);
}
else
{
QMessageBox::warning(this, tr("Warning!"), tr("Failed to delete configuration file!"));
LOG_FATAL(GENERAL, "Failed to delete configuration file: %s\nError: %s", config_path, fs::g_tls_error);
}
}
}
else
{
QMessageBox::warning(this, tr("Warning!"), tr("No custom configuration found!"));
LOG_ERROR(GENERAL, "Configuration file not found: %s", config_path);
}
}
void game_list_frame::ResizeIcons(QSize size)
{
m_columns.m_Icon_Size = size;
m_Icon_Size = size;
gameList->verticalHeader()->setMinimumSectionSize(m_Icon_Size.height());
gameList->verticalHeader()->setMaximumSectionSize(m_Icon_Size.height());
Refresh();
}
columns_arr::columns_arr(QSize icon_Size) : m_Icon_Size(icon_Size)
{
m_img_list = new QList<QImage*>();
m_columns.clear();
m_columns.emplace_back(0, 90, "Icon");
m_columns.emplace_back(1, 160, "Name");
m_columns.emplace_back(2, 85, "Serial");
m_columns.emplace_back(3, 55, "FW");
m_columns.emplace_back(4, 55, "App version");
m_columns.emplace_back(5, 75, "Category");
m_columns.emplace_back(6, 160, "Path");
m_col_icon = &m_columns[0];
m_col_name = &m_columns[1];
m_col_serial = &m_columns[2];
m_col_fw = &m_columns[3];
m_col_app_ver = &m_columns[4];
m_col_category = &m_columns[5];
m_col_path = &m_columns[6];
}
Column* columns_arr::GetColumnByPos(u32 pos)
{
std::vector<Column *> columns;
for (u32 pos = 0; pos<m_columns.size(); pos++)
{
for (u32 c = 0; c<m_columns.size(); ++c)
{
if (m_columns[c].pos != pos) continue;
columns.push_back(&m_columns[c]);
}
}
for (u32 c = 0; c<columns.size(); ++c)
{
if (!columns[c]->shown)
{
pos++;
continue;
}
if (columns[c]->pos != pos) continue;
return columns[c];
}
return NULL;
}
void columns_arr::Update(const std::vector<GameInfo>& game_data)
{
m_img_list->clear();
m_col_icon->data.clear();
m_col_name->data.clear();
m_col_serial->data.clear();
m_col_fw->data.clear();
m_col_app_ver->data.clear();
m_col_category->data.clear();
m_col_path->data.clear();
m_icon_indexes.clear();
if (m_columns.size() == 0) return;
for (const auto& game : game_data)
{
m_col_icon->data.push_back(game.icon_path);
m_col_name->data.push_back(game.name);
m_col_serial->data.push_back(game.serial);
m_col_fw->data.push_back(game.fw);
m_col_app_ver->data.push_back(game.app_ver);
m_col_category->data.push_back(game.category);
m_col_path->data.push_back(game.root);
}
int c = 0;
// load icons
for (const auto& path : m_col_icon->data)
{
QImage* img = new QImage(m_Icon_Size, QImage::Format_ARGB32);
if (!path.empty())
{
// Load image.
bool success = img->load(qstr(path));
if (success)
{
m_img_list->append(new QImage(img->scaled(m_Icon_Size, Qt::KeepAspectRatio, Qt::TransformationMode::SmoothTransformation)));
}
else {
// IIRC a load failure means blank image which is fine to have as a placeholder.
QString abspath = QDir(qstr(path)).absolutePath();
LOG_ERROR(HLE, "Could not load game icon image from path %s", sstr(abspath));
img->fill(QColor(0, 0, 0, 0));
m_img_list->append(img);
}
}
else
{
LOG_ERROR(HLE, "Could not load game icon image from empty path");
img->fill(QColor(0, 0, 0, 0));
m_img_list->append(img);
}
m_icon_indexes.push_back(c);
c++;
}
}
void columns_arr::ShowData(QTableWidget* table)
{
// Hack to delete everything without removing the headers.
table->setRowCount(0);
// Expect number of columns to be the same as number of icons.
table->setRowCount(m_img_list->length());
// Add icons.
for (int r = 0; r < m_img_list->length(); ++r)
{
QTableWidgetItem* iconItem = new QTableWidgetItem;
iconItem->setFlags(iconItem->flags() & ~Qt::ItemIsEditable);
iconItem->setData(Qt::DecorationRole, QPixmap::fromImage(*m_img_list->at(m_icon_indexes[r])));
table->setItem(r, 0, iconItem);
}
// Add the other data.
for (int c = 1; c < table->columnCount(); ++c)
{
Column* col = GetColumnByPos(c);
if (!col)
{
LOG_ERROR(HLE, "Columns loaded with error!");
return;
}
int numRows = col->data.size();
if (numRows != table->rowCount())
{
table->setRowCount(numRows);
LOG_WARNING(HLE, "Warning. Columns are of different size: number of icons %d number wanted: %d", m_img_list->length(), numRows);
}
for (int r = 0; r<col->data.size(); ++r)
{
QTableWidgetItem* curr = new QTableWidgetItem;
curr->setFlags(curr->flags() & ~Qt::ItemIsEditable);
QString text = qstr(col->data[r]);
curr->setText(text);
table->setItem(r, c, curr);
}
table->resizeRowsToContents();
table->resizeColumnToContents(0);
}
}
void game_list_frame::closeEvent(QCloseEvent *event)
{
QDockWidget::closeEvent(event);
emit game_list_frameClosed();
}