From 84ac2326cd80066d47e0a518aa07d4795be0c400 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 27 Oct 2022 12:39:52 +0300 Subject: [PATCH] Initial game list viewer --- .gitignore | 1 + shadPS4/gui/GameListViewer.cpp | 179 ++++++++++++++++++++++++++++++++ shadPS4/gui/GameListViewer.h | 117 +++++++++++++++++++++ shadPS4/gui/shadps4gui.cpp | 6 ++ shadPS4/gui/shadps4gui.h | 2 + shadPS4/shadPS4.vcxproj | 4 + shadPS4/shadPS4.vcxproj.filters | 6 ++ 7 files changed, 315 insertions(+) create mode 100644 shadPS4/gui/GameListViewer.cpp create mode 100644 shadPS4/gui/GameListViewer.h diff --git a/.gitignore b/.gitignore index 8a30d258..bf96999d 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,4 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +/shadPS4/game/* diff --git a/shadPS4/gui/GameListViewer.cpp b/shadPS4/gui/GameListViewer.cpp new file mode 100644 index 00000000..ed34a116 --- /dev/null +++ b/shadPS4/gui/GameListViewer.cpp @@ -0,0 +1,179 @@ +#include "GameListViewer.h" +#include +#include +#include +#include +#include +#include +#include + +GameListViewer::GameListViewer(QWidget* parent) + : QWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout; + QHBoxLayout* search_layout = new QHBoxLayout; + proxyModel = new QSortFilterProxyModel; + search_games = new QLineEdit; + + tree_view = new QTreeView; + item_model = new QStandardItemModel(tree_view); + proxyModel->setSourceModel(item_model); + tree_view->setModel(proxyModel); + + tree_view->setAlternatingRowColors(true); + tree_view->setSelectionMode(QHeaderView::SingleSelection); + tree_view->setSelectionBehavior(QHeaderView::SelectRows); + tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel); + tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); + tree_view->setSortingEnabled(true); + tree_view->setEditTriggers(QHeaderView::NoEditTriggers); + tree_view->setUniformRowHeights(true); + tree_view->setContextMenuPolicy(Qt::CustomContextMenu); + + item_model->insertColumns(0, 7); + item_model->setHeaderData(0, Qt::Horizontal, "Icon"); + item_model->setHeaderData(1, Qt::Horizontal, "Name"); + item_model->setHeaderData(2, Qt::Horizontal, "Serial"); + item_model->setHeaderData(3, Qt::Horizontal, "FW"); + item_model->setHeaderData(4, Qt::Horizontal, "App Version"); + item_model->setHeaderData(5, Qt::Horizontal, "Category"); + item_model->setHeaderData(6, Qt::Horizontal, "Path"); + + connect(tree_view, &QTreeView::activated, this, &GameListViewer::ValidateEntry); + connect(search_games, &QLineEdit::textChanged, this, &GameListViewer::searchGame); + connect(&watcher, &QFileSystemWatcher::directoryChanged, this, &GameListViewer::RefreshGameDirectory); + + // We must register all custom types with the Qt Automoc system so that we are able to use it + // with signals/slots. In this case, QList falls under the umbrells of custom types. + qRegisterMetaType>("QList"); + + layout->setContentsMargins(0, 0, 0, 0); + QSpacerItem* item = new QSpacerItem(100, 1, QSizePolicy::Expanding, QSizePolicy::Fixed); + search_layout->setContentsMargins(0, 5, 0, 0); + search_layout->addSpacerItem(item); + search_layout->addWidget(search_games); + layout->addLayout(search_layout); + layout->addWidget(tree_view); + setLayout(layout); +} + +GameListViewer::~GameListViewer() +{ + emit ShouldCancelWorker(); +} +void GameListViewer::searchGame(QString searchText) +{ + proxyModel->setFilterKeyColumn(1); //filter Name column only + QString strPattern = searchText; + //TODO QRegExp regExp(strPattern, Qt::CaseInsensitive); + //TODO proxyModel->setFilterRegExp(regExp); +} +void GameListViewer::AddEntry(const QList& entry_items) { + item_model->invisibleRootItem()->appendRow(entry_items); +} + +void GameListViewer::ValidateEntry(const QModelIndex& item) { + // We don't care about the individual QStandardItem that was selected, but its row. +} + +void GameListViewer::DonePopulating() { + tree_view->setEnabled(true); + tree_view->resizeColumnToContents(1);//resize tittle to fit the column +} + +void GameListViewer::PopulateAsync() { + QDir game_folder(game_path); + if (!game_folder.exists()) + { + //game directory doesn't exist + return; + } + + tree_view->setEnabled(false); + // Delete any rows that might already exist if we're repopulating + item_model->removeRows(0, item_model->rowCount()); + + emit ShouldCancelWorker(); + + auto watch_dirs = watcher.directories(); + if (!watch_dirs.isEmpty()) { + watcher.removePaths(watch_dirs); + } + UpdateWatcherList(game_path.toStdString()); + GameListWorker* worker = new GameListWorker(game_path); + + connect(worker, &GameListWorker::EntryReady, this, &GameListViewer::AddEntry, Qt::QueuedConnection); + connect(worker, &GameListWorker::Finished, this, &GameListViewer::DonePopulating, Qt::QueuedConnection); + // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel without delay. + connect(this, &GameListViewer::ShouldCancelWorker, worker, &GameListWorker::Cancel, Qt::DirectConnection); + + QThreadPool::globalInstance()->start(worker); + current_worker = std::move(worker); +} + +void GameListViewer::RefreshGameDirectory() { + QDir game_folder(game_path); + bool empty = game_folder.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::DirsFirst).count() == 0; + if (!empty && current_worker != nullptr) { + //Change detected in the games directory. Reloading game list + PopulateAsync(); + } +} + +/** +* Adds the game list folder to the QFileSystemWatcher to check for updates. +* +* The file watcher will fire off an update to the game list when a change is detected in the game +* list folder. +* +* Notice: This method is run on the UI thread because QFileSystemWatcher is not thread safe and +* this function is fast enough to not stall the UI thread. If performance is an issue, it should +* be moved to another thread and properly locked to prevent concurrency issues. +* +* @param dir folder to check for changes in +*/ +void GameListViewer::UpdateWatcherList(const std::string& dir) { + /*watcher.addPath(QString::fromStdString(dir)); + QDir parent_folder(QString::fromStdString(dir)); + QFileInfoList fList = parent_folder.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::DirsFirst); + foreach(QFileInfo item, fList) + { + UpdateWatcherList(item.absoluteFilePath().toStdString()); + }*/ +} + +void GameListWorker::AddEntriesToGameList(const std::string& dir_path) { + QDir parent_folder(QString::fromStdString(dir_path)); + QFileInfoList fList = parent_folder.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::DirsFirst); + foreach(QFileInfo item, fList) + { + //TODO PSF psf; + //TODO if (!psf.open(item.absoluteFilePath().toStdString() + "/PARAM.SFO")) + //TODO continue;//if we can't open param.sfo go to the next entry + + //TODO std::string test = psf.get_string("TITLE_ID"); + QString iconpath(item.absoluteFilePath() + "/ICON0.PNG"); + + emit EntryReady({ + new GameIconItem(iconpath), + new GameListItem("TODO"/*QString::fromStdString(psf.get_string("TITLE"))*/), + new GameListItem("TODO"/*QString::fromStdString(psf.get_string("TITLE_ID"))*/), + new GameListItem("TODO"/*QString::fromStdString(psf.get_string("SYSTEM_VER"))*/), + new GameListItem("TODO"/*QString::fromStdString(psf.get_string("APP_VER"))*/), + new GameListItem("TODO"/*QString::fromStdString(psf.get_string("CATEGORY"))*/), + new GameListItem(item.fileName()) + }); + + } +} + +void GameListWorker::run() { + stop_processing = false; + AddEntriesToGameList(dir_path.toStdString()); + emit Finished(); +} + +void GameListWorker::Cancel() { + this->disconnect(); + stop_processing = true; +} \ No newline at end of file diff --git a/shadPS4/gui/GameListViewer.h b/shadPS4/gui/GameListViewer.h new file mode 100644 index 00000000..95d2dab8 --- /dev/null +++ b/shadPS4/gui/GameListViewer.h @@ -0,0 +1,117 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class GameListWorker; + +class GameListViewer : public QWidget +{ + Q_OBJECT + +public: + explicit GameListViewer(QWidget* parent = nullptr); + ~GameListViewer(); + void PopulateAsync(); + void SetGamePath(QString game_path) + { + this->game_path = game_path; + } +signals: + void ShouldCancelWorker(); + +private: + void AddEntry(const QList& entry_items); + void ValidateEntry(const QModelIndex& item); + void DonePopulating(); + void UpdateWatcherList(const std::string& path); + + QTreeView* tree_view = nullptr; + QStandardItemModel* item_model = nullptr; + GameListWorker* current_worker = nullptr; + QLineEdit* search_games = nullptr; + QSortFilterProxyModel* proxyModel = nullptr; + QFileSystemWatcher watcher; + QString game_path; +public: + void RefreshGameDirectory(); + +public slots: + void searchGame(QString searchText); +}; +class GameListItem : public QStandardItem { + +public: + GameListItem() : QStandardItem() {} + GameListItem(const QString& string) : QStandardItem(string) {} + virtual ~GameListItem() override {} +}; + +/** +* A specialization of GameListItem for icons +* If no icon found then create an empty one +*/ +class GameIconItem : public GameListItem +{ +public: + GameIconItem() : GameListItem() {} + GameIconItem(const QString& pix_path) + : GameListItem() { + + QPixmap icon(pix_path); + if (icon.isNull()) + { + QPixmap emptyicon(80, 44); + emptyicon.fill(Qt::transparent); + setData(emptyicon.scaled(80, 44, Qt::KeepAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); + } + else + { + setData(icon.scaled(80, 44, Qt::KeepAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); + } + } +}; + +/** +* Asynchronous worker object for populating the game list. +* Communicates with other threads through Qt's signal/slot system. +*/ +class GameListWorker : public QObject, public QRunnable { + Q_OBJECT + +public: + GameListWorker(QString dir_path) + : QObject(), QRunnable(), dir_path(dir_path) {} + +public slots: + /// Starts the processing of directory tree information. + void run() override; + /// Tells the worker that it should no longer continue processing. Thread-safe. + void Cancel(); + +signals: + /** + * The `EntryReady` signal is emitted once an entry has been prepared and is ready + * to be added to the game list. + * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. + */ + void EntryReady(QList entry_items); + void Finished(); + +private: + QString dir_path; + std::atomic_bool stop_processing; + + void AddEntriesToGameList(const std::string& dir_path); +}; diff --git a/shadPS4/gui/shadps4gui.cpp b/shadPS4/gui/shadps4gui.cpp index ba50e27b..c7933610 100644 --- a/shadPS4/gui/shadps4gui.cpp +++ b/shadPS4/gui/shadps4gui.cpp @@ -1,9 +1,15 @@ #include "shadps4gui.h" +#include #include shadps4gui::shadps4gui(QWidget *parent) : QMainWindow(parent) { ui.setupUi(this); + game_list = new GameListViewer(); + game_list->SetGamePath(QDir::currentPath() + "/game/"); + ui.horizontalLayout->addWidget(game_list); + show(); + game_list->PopulateAsync(); } shadps4gui::~shadps4gui() diff --git a/shadPS4/gui/shadps4gui.h b/shadPS4/gui/shadps4gui.h index 637f5e78..bd9d722e 100644 --- a/shadPS4/gui/shadps4gui.h +++ b/shadPS4/gui/shadps4gui.h @@ -2,6 +2,7 @@ #include #include "ui_shadps4gui.h" +#include "GameListViewer.h" class shadps4gui : public QMainWindow { @@ -16,4 +17,5 @@ public slots: private: Ui::shadps4guiClass ui; + GameListViewer* game_list; }; diff --git a/shadPS4/shadPS4.vcxproj b/shadPS4/shadPS4.vcxproj index 3b0475aa..08b10862 100644 --- a/shadPS4/shadPS4.vcxproj +++ b/shadPS4/shadPS4.vcxproj @@ -11,6 +11,7 @@ + @@ -20,6 +21,9 @@ + + + {F005E4D9-1FBE-40B3-9FBD-35CEC59081CD} QtVS_v304 diff --git a/shadPS4/shadPS4.vcxproj.filters b/shadPS4/shadPS4.vcxproj.filters index 776371b6..8c5d400f 100644 --- a/shadPS4/shadPS4.vcxproj.filters +++ b/shadPS4/shadPS4.vcxproj.filters @@ -32,6 +32,9 @@ gui + + gui + @@ -42,5 +45,8 @@ gui + + gui + \ No newline at end of file