Initial game list viewer
This commit is contained in:
parent
4ff04549c9
commit
84ac2326cd
|
@ -396,3 +396,4 @@ FodyWeavers.xsd
|
||||||
|
|
||||||
# JetBrains Rider
|
# JetBrains Rider
|
||||||
*.sln.iml
|
*.sln.iml
|
||||||
|
/shadPS4/game/*
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
};
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue