mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2024-12-28 02:26:07 +00:00
Initial game list viewer
This commit is contained in:
parent
4ff04549c9
commit
84ac2326cd
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -396,3 +396,4 @@ FodyWeavers.xsd
|
|||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
/shadPS4/game/*
|
||||
|
|
179
shadPS4/gui/GameListViewer.cpp
Normal file
179
shadPS4/gui/GameListViewer.cpp
Normal 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;
|
||||
}
|
117
shadPS4/gui/GameListViewer.h
Normal file
117
shadPS4/gui/GameListViewer.h
Normal 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);
|
||||
};
|
|
@ -1,9 +1,15 @@
|
|||
#include "shadps4gui.h"
|
||||
#include <QDir>
|
||||
#include <QMessageBox>
|
||||
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()
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <QMainWindow>
|
||||
#include "ui_shadps4gui.h"
|
||||
#include "GameListViewer.h"
|
||||
|
||||
class shadps4gui : public QMainWindow
|
||||
{
|
||||
|
@ -16,4 +17,5 @@ public slots:
|
|||
|
||||
private:
|
||||
Ui::shadps4guiClass ui;
|
||||
GameListViewer* game_list;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="gui\GameListViewer.cpp" />
|
||||
<ClCompile Include="gui\shadps4gui.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
</ItemGroup>
|
||||
|
@ -20,6 +21,9 @@
|
|||
<ItemGroup>
|
||||
<QtUic Include="gui\shadps4gui.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="gui\GameListViewer.h" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{F005E4D9-1FBE-40B3-9FBD-35CEC59081CD}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
|
|
|
@ -32,6 +32,9 @@
|
|||
<ClCompile Include="gui\shadps4gui.cpp">
|
||||
<Filter>gui</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gui\GameListViewer.cpp">
|
||||
<Filter>gui</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="gui\shadps4gui.ui">
|
||||
|
@ -42,5 +45,8 @@
|
|||
<QtMoc Include="gui\shadps4gui.h">
|
||||
<Filter>gui</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="gui\GameListViewer.h">
|
||||
<Filter>gui</Filter>
|
||||
</QtMoc>
|
||||
</ItemGroup>
|
||||
</Project>
|
Loading…
Reference in a new issue