Initial game list viewer

This commit is contained in:
georgemoralis 2022-10-27 12:39:52 +03:00
parent 4ff04549c9
commit 84ac2326cd
7 changed files with 315 additions and 0 deletions

1
.gitignore vendored
View File

@ -396,3 +396,4 @@ FodyWeavers.xsd
# JetBrains Rider # JetBrains Rider
*.sln.iml *.sln.iml
/shadPS4/game/*

View File

@ -0,0 +1,179 @@
#include "GameListViewer.h"
#include <QFileInfo>
#include <QHeaderView>
#include <QMenu>
#include <QThreadPool>
#include <QVBoxLayout>
#include <QDirIterator>
#include <QLineEdit>
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<QStandardItem*>>("QList<QStandardItem*>");
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<QStandardItem*>& 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;
}

View File

@ -0,0 +1,117 @@
#pragma once
#include <QFileSystemWatcher>
#include <QModelIndex>
#include <QSettings>
#include <QStandardItem>
#include <QStandardItemModel>
#include <QString>
#include <QTreeView>
#include <QWidget>
#include <atomic>
#include <QImage>
#include <QRunnable>
#include <QStandardItem>
#include <QString>
#include <QSortFilterProxyModel>
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<QStandardItem*>& 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<QStandardItem*> entry_items);
void Finished();
private:
QString dir_path;
std::atomic_bool stop_processing;
void AddEntriesToGameList(const std::string& dir_path);
};

View File

@ -1,9 +1,15 @@
#include "shadps4gui.h" #include "shadps4gui.h"
#include <QDir>
#include <QMessageBox> #include <QMessageBox>
shadps4gui::shadps4gui(QWidget *parent) shadps4gui::shadps4gui(QWidget *parent)
: QMainWindow(parent) : QMainWindow(parent)
{ {
ui.setupUi(this); ui.setupUi(this);
game_list = new GameListViewer();
game_list->SetGamePath(QDir::currentPath() + "/game/");
ui.horizontalLayout->addWidget(game_list);
show();
game_list->PopulateAsync();
} }
shadps4gui::~shadps4gui() shadps4gui::~shadps4gui()

View File

@ -2,6 +2,7 @@
#include <QMainWindow> #include <QMainWindow>
#include "ui_shadps4gui.h" #include "ui_shadps4gui.h"
#include "GameListViewer.h"
class shadps4gui : public QMainWindow class shadps4gui : public QMainWindow
{ {
@ -16,4 +17,5 @@ public slots:
private: private:
Ui::shadps4guiClass ui; Ui::shadps4guiClass ui;
GameListViewer* game_list;
}; };

View File

@ -11,6 +11,7 @@
</ProjectConfiguration> </ProjectConfiguration>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="gui\GameListViewer.cpp" />
<ClCompile Include="gui\shadps4gui.cpp" /> <ClCompile Include="gui\shadps4gui.cpp" />
<ClCompile Include="main.cpp" /> <ClCompile Include="main.cpp" />
</ItemGroup> </ItemGroup>
@ -20,6 +21,9 @@
<ItemGroup> <ItemGroup>
<QtUic Include="gui\shadps4gui.ui" /> <QtUic Include="gui\shadps4gui.ui" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<QtMoc Include="gui\GameListViewer.h" />
</ItemGroup>
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<ProjectGuid>{F005E4D9-1FBE-40B3-9FBD-35CEC59081CD}</ProjectGuid> <ProjectGuid>{F005E4D9-1FBE-40B3-9FBD-35CEC59081CD}</ProjectGuid>
<Keyword>QtVS_v304</Keyword> <Keyword>QtVS_v304</Keyword>

View File

@ -32,6 +32,9 @@
<ClCompile Include="gui\shadps4gui.cpp"> <ClCompile Include="gui\shadps4gui.cpp">
<Filter>gui</Filter> <Filter>gui</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="gui\GameListViewer.cpp">
<Filter>gui</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<QtUic Include="gui\shadps4gui.ui"> <QtUic Include="gui\shadps4gui.ui">
@ -42,5 +45,8 @@
<QtMoc Include="gui\shadps4gui.h"> <QtMoc Include="gui\shadps4gui.h">
<Filter>gui</Filter> <Filter>gui</Filter>
</QtMoc> </QtMoc>
<QtMoc Include="gui\GameListViewer.h">
<Filter>gui</Filter>
</QtMoc>
</ItemGroup> </ItemGroup>
</Project> </Project>