refactor: move general code from mod model to its own model

This aims to continue decoupling other types of resources (e.g. resource
packs, shader packs, etc) from mods, so that we don't have to
continuously watch our backs for changes to one of them affecting the
others.

To do so, this creates a more general list model for resources, based on
the mods one, that allows you to extend it with functionality for other
resources.

I had to do some template and preprocessor stuff to get around the
QObject limitation of not allowing templated classes, so that's sadge :c

On the other hand, I tried cleaning up most general-purpose code in the
mod model, and added some documentation, because it looks nice :D

Signed-off-by: flow <flowlnlnln@gmail.com>
This commit is contained in:
flow 2022-08-09 01:58:22 -03:00
parent 3225f514f6
commit ec62d8e973
No known key found for this signature in database
GPG Key ID: 8D0F221F0A59F469
11 changed files with 779 additions and 456 deletions

View File

@ -320,10 +320,13 @@ set(MINECRAFT_SOURCES
minecraft/mod/ModFolderModel.cpp
minecraft/mod/Resource.h
minecraft/mod/Resource.cpp
minecraft/mod/ResourceFolderModel.h
minecraft/mod/ResourceFolderModel.cpp
minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp
minecraft/mod/tasks/BasicFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.cpp
minecraft/mod/tasks/LocalModParseTask.h

View File

@ -49,226 +49,91 @@
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed)
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(dir), m_is_indexed(is_indexed)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher = new QFileSystemWatcher(this);
connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
}
Task* ModFolderModel::createUpdateTask()
{
auto index_dir = indexDir();
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load);
m_first_folder_load = false;
return task;
}
void ModFolderModel::startWatching()
{
if(is_watching)
return;
// Remove orphaned metadata next time
m_first_folder_load = true;
update();
// Watch the mods folder
is_watching = m_watcher->addPath(m_dir.absolutePath());
if (is_watching) {
qDebug() << "Started watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to start watching " << m_dir.absolutePath();
}
// Watch the mods index folder
is_watching = m_watcher->addPath(indexDir().absolutePath());
if (is_watching) {
qDebug() << "Started watching " << indexDir().absolutePath();
} else {
qDebug() << "Failed to start watching " << indexDir().absolutePath();
}
ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
}
void ModFolderModel::stopWatching()
{
if(!is_watching)
return;
is_watching = !m_watcher->removePath(m_dir.absolutePath());
if (!is_watching) {
qDebug() << "Stopped watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
}
is_watching = !m_watcher->removePath(indexDir().absolutePath());
if (!is_watching) {
qDebug() << "Stopped watching " << indexDir().absolutePath();
} else {
qDebug() << "Failed to stop watching " << indexDir().absolutePath();
}
ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
}
bool ModFolderModel::update()
void ModFolderModel::onUpdateSucceeded()
{
if (!isValid()) {
return false;
}
if(m_update) {
scheduled_update = true;
return true;
}
auto update_results = static_cast<ModFolderLoadTask*>(m_current_update_task.get())->result();
auto index_dir = indexDir();
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load);
m_first_folder_load = false;
auto& new_mods = update_results->mods;
m_update = task->result();
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
threadPool->start(task);
return true;
}
void ModFolderModel::finishUpdate()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto currentList = modsIndex.keys();
QSet<QString> currentSet(currentList.begin(), currentList.end());
auto & newMods = m_update->mods;
auto newList = newMods.keys();
QSet<QString> newSet(newList.begin(), newList.end());
auto current_list = m_resources_index.keys();
QSet<QString> current_set(current_list.begin(), current_list.end());
auto new_list = new_mods.keys();
QSet<QString> new_set(new_list.begin(), new_list.end());
#else
QSet<QString> currentSet = modsIndex.keys().toSet();
auto& newMods = m_update->mods;
QSet<QString> newSet = newMods.keys().toSet();
QSet<QString> current_set(m_resources_index.keys().toSet());
QSet<QString> new_set(new_mods.keys().toSet());
#endif
// see if the kept mods changed in some way
{
QSet<QString> kept = currentSet;
kept.intersect(newSet);
for(auto& keptMod : kept) {
auto newMod = newMods[keptMod];
auto row = modsIndex[keptMod];
auto currentMod = mods[row];
if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) {
// no significant change, ignore...
continue;
}
auto oldMod = mods[row];
if(oldMod->isResolving()) {
activeTickets.remove(oldMod->resolutionTicket());
}
mods[row] = newMod;
resolveMod(mods[row]);
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
}
// remove mods no longer present
{
QSet<QString> removed = currentSet;
QList<int> removedRows;
removed.subtract(newSet);
for(auto & removedMod: removed) {
removedRows.append(modsIndex[removedMod]);
}
std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) {
int removedIndex = *iter;
beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
auto removedIter = mods.begin() + removedIndex;
if((*removedIter)->isResolving()) {
activeTickets.remove((*removedIter)->resolutionTicket());
}
mods.erase(removedIter);
endRemoveRows();
}
}
// add new mods to the end
{
QSet<QString> added = newSet;
added.subtract(currentSet);
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (added.size() > 0) {
beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1);
for (auto& addedMod : added) {
mods.append(newMods[addedMod]);
resolveMod(mods.last());
}
endInsertRows();
}
}
// update index
{
modsIndex.clear();
int idx = 0;
for(auto mod: mods) {
modsIndex[mod->internal_id()] = idx;
idx++;
}
}
m_update.reset();
applyUpdates(current_set, new_set, new_mods);
update_results.reset();
m_current_update_task.reset();
emit updateFinished();
if(scheduled_update) {
scheduled_update = false;
if(m_scheduled_update) {
m_scheduled_update = false;
update();
}
}
void ModFolderModel::resolveMod(Mod::Ptr m)
Task* ModFolderModel::createParseTask(Resource const& resource)
{
if(!m->shouldResolve()) {
return;
}
auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo());
auto result = task->result();
result->id = m->internal_id();
activeTickets.insert(nextResolutionTicket, result);
m->setResolving(true, nextResolutionTicket);
nextResolutionTicket++;
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
threadPool->start(task);
return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
}
void ModFolderModel::finishModParse(int token)
void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
{
auto iter = activeTickets.find(token);
if(iter == activeTickets.end()) {
auto iter = m_active_parse_tasks.constFind(ticket);
if (iter == m_active_parse_tasks.constEnd())
return;
}
auto result = *iter;
activeTickets.remove(token);
int row = modsIndex[result->id];
auto mod = mods[row];
mod->finishResolvingWithDetails(result->details);
int row = m_resources_index[mod_id];
auto parse_task = *iter;
auto cast_task = static_cast<LocalModParseTask*>(parse_task.get());
Q_ASSERT(cast_task->token() == ticket);
auto resource = find(mod_id);
auto result = cast_task->result();
if (result && resource)
resource->finishResolvingWithDetails(result->details);
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
parse_task->deleteLater();
m_active_parse_tasks.remove(ticket);
}
void ModFolderModel::disableInteraction(bool disabled)
{
if (interaction_disabled == disabled) {
return;
}
interaction_disabled = disabled;
if(size()) {
emit dataChanged(index(0), index(size() - 1));
}
}
void ModFolderModel::directoryChanged(QString path)
{
update();
}
bool ModFolderModel::isValid()
{
@ -277,104 +142,28 @@ bool ModFolderModel::isValid()
auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>
{
QList<Mod::Ptr> selected_mods;
QList<Mod::Ptr> selected_resources;
for (auto i : indexes) {
if(i.column() != 0)
continue;
selected_mods.push_back(mods[i.row()]);
selected_resources.push_back(at(i.row()));
}
return selected_mods;
return selected_resources;
}
// FIXME: this does not take disabled mod (with extra .disable extension) into account...
bool ModFolderModel::installMod(const QString &filename)
auto ModFolderModel::allMods() -> QList<Mod::Ptr>
{
if(interaction_disabled) {
return false;
}
QList<Mod::Ptr> mods;
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
auto originalPath = FS::NormalizePath(filename);
QFileInfo fileinfo(originalPath);
for (auto res : m_resources)
mods.append(static_cast<Mod*>(res.get()));
if (!fileinfo.exists() || !fileinfo.isReadable())
{
qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
return false;
}
qDebug() << "installing: " << fileinfo.absoluteFilePath();
Mod installedMod(fileinfo);
if (!installedMod.valid())
{
qDebug() << originalPath << "is not a valid mod. Ignoring it.";
return false;
}
auto type = installedMod.type();
if (type == Mod::MOD_UNKNOWN)
{
qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it.";
return false;
}
auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
if(originalPath == newpath)
{
qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
return false;
}
if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
{
if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled")))
{
if(!QFile::remove(newpath))
{
// FIXME: report error in a user-visible way
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
return false;
}
qDebug() << newpath << "has been deleted.";
}
if (!QFile::copy(fileinfo.filePath(), newpath))
{
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
// FIXME: report error in a user-visible way
return false;
}
FS::updateTimestamp(newpath);
QFileInfo newpathInfo(newpath);
installedMod.repath(newpathInfo);
update();
return true;
}
else if (type == Mod::MOD_FOLDER)
{
QString from = fileinfo.filePath();
if(QFile::exists(newpath))
{
qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath;
return false;
}
if (!FS::copy(from, newpath)())
{
qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed.";
return false;
}
QFileInfo newpathInfo(newpath);
installedMod.repath(newpathInfo);
update();
return true;
}
return false;
return mods;
}
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{
for(auto mod : allMods()){
if(mod->fileinfo().fileName() == filename){
auto index_dir = indexDir();
@ -388,7 +177,7 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat
bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
{
if(interaction_disabled) {
if(!m_can_interact) {
return false;
}
@ -407,7 +196,7 @@ bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusActio
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
if(interaction_disabled) {
if(!m_can_interact) {
return false;
}
@ -419,7 +208,7 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
if(i.column() != 0) {
continue;
}
auto m = mods[i.row()];
auto m = at(i.row());
auto index_dir = indexDir();
m->destroy(index_dir);
}
@ -433,48 +222,45 @@ int ModFolderModel::columnCount(const QModelIndex &parent) const
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (!validateIndex(index))
return {};
int row = index.row();
int column = index.column();
if (row < 0 || row >= mods.size())
return QVariant();
switch (role)
{
case Qt::DisplayRole:
switch (column)
{
case NameColumn:
return mods[row]->name();
return m_resources[row]->name();
case VersionColumn: {
switch(mods[row]->type()) {
case Mod::MOD_FOLDER:
switch(m_resources[row]->type()) {
case ResourceType::FOLDER:
return tr("Folder");
case Mod::MOD_SINGLEFILE:
case ResourceType::SINGLEFILE:
return tr("File");
default:
break;
}
return mods[row]->version();
return at(row)->version();
}
case DateColumn:
return mods[row]->dateTimeChanged();
return m_resources[row]->dateTimeChanged();
default:
return QVariant();
}
case Qt::ToolTipRole:
return mods[row]->internal_id();
return m_resources[row]->internal_id();
case Qt::CheckStateRole:
switch (column)
{
case ActiveColumn:
return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return QVariant();
}
@ -499,11 +285,11 @@ bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, in
bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
{
if(row < 0 || row >= mods.size()) {
if(row < 0 || row >= m_resources.size()) {
return false;
}
auto &mod = mods[row];
auto mod = at(row);
bool desiredStatus;
switch(action) {
case Enable:
@ -528,12 +314,12 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio
return false;
}
auto newId = mod->internal_id();
if(modsIndex.contains(newId)) {
if(m_resources_index.contains(newId)) {
// NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled
// But is it necessary?
}
modsIndex.remove(oldId);
modsIndex[newId] = row;
m_resources_index.remove(oldId);
m_resources_index[newId] = row;
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
return true;
}
@ -577,65 +363,3 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return QVariant();
}
Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags;
if(interaction_disabled) {
flags &= ~Qt::ItemIsDropEnabled;
}
else
{
flags |= Qt::ItemIsDropEnabled;
if(index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
}
return flags;
}
Qt::DropActions ModFolderModel::supportedDropActions() const
{
// copy from outside, move from within and other mod lists
return Qt::CopyAction | Qt::MoveAction;
}
QStringList ModFolderModel::mimeTypes() const
{
QStringList types;
types << "text/uri-list";
return types;
}
bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
{
if (action == Qt::IgnoreAction)
{
return true;
}
// check if the action is supported
if (!data || !(action & supportedDropActions()))
{
return false;
}
// files dropped from outside?
if (data->hasUrls())
{
auto urls = data->urls();
for (auto url : urls)
{
// only local files may be dropped...
if (!url.isLocalFile())
{
continue;
}
// TODO: implement not only copy, but also move
// FIXME: handle errors here
installMod(url.toLocalFile());
}
return true;
}
return false;
}

View File

@ -44,6 +44,7 @@
#include <QAbstractListModel>
#include "Mod.h"
#include "ResourceFolderModel.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
@ -56,7 +57,7 @@ class QFileSystemWatcher;
* A legacy mod list.
* Backed by a folder.
*/
class ModFolderModel : public QAbstractListModel
class ModFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
@ -75,48 +76,18 @@ public:
};
ModFolderModel(const QString &dir, bool is_indexed = false);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::DropActions supportedDropActions() const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
/// flags, mostly to support drag&drop
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
QStringList mimeTypes() const override;
bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
int columnCount(const QModelIndex &parent) const override;
virtual int rowCount(const QModelIndex &) const override
{
return size();
}
[[nodiscard]] Task* createUpdateTask() override;
[[nodiscard]] Task* createParseTask(Resource const&) override;
virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
virtual int columnCount(const QModelIndex &parent) const override;
size_t size() const
{
return mods.size();
}
;
bool empty() const
{
return size() == 0;
}
Mod& operator[](size_t index)
{
return *mods[index];
}
const Mod& at(size_t index) const
{
return *mods.at(index);
}
/// Reloads the mod list and returns true if the list changed.
bool update();
/**
* Adds the given mod to the list at the given index - if the list supports custom ordering
*/
bool installMod(const QString& filename);
// Alias for old code, consider those deprecated and don't use in new code :gun:
bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
void disableInteraction(bool disabled) { ResourceFolderModel::enableInteraction(!disabled); }
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
@ -126,55 +97,27 @@ public:
/// Enable or disable listed mods
bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
bool isValid();
void startWatching();
void stopWatching();
bool isValid();
QDir& dir()
{
return m_dir;
}
QDir indexDir()
{
return { QString("%1/.index").arg(dir().absolutePath()) };
}
const QList<Mod::Ptr>& allMods()
{
return mods;
}
QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
auto allMods() -> QList<Mod::Ptr>;
public slots:
void disableInteraction(bool disabled);
RESOURCE_HELPERS(Mod)
private
slots:
void directoryChanged(QString path);
void finishUpdate();
void finishModParse(int token);
signals:
void updateFinished();
void onUpdateSucceeded() override;
void onParseSucceeded(int ticket, QString resource_id) override;
private:
void resolveMod(Mod::Ptr m);
bool setModStatus(int index, ModStatusAction action);
protected:
QFileSystemWatcher *m_watcher;
bool is_watching = false;
ModFolderLoadTask::ResultPtr m_update;
bool scheduled_update = false;
bool interaction_disabled = false;
QDir m_dir;
bool m_is_indexed;
bool m_first_folder_load = true;
QMap<QString, int> modsIndex;
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
int nextResolutionTicket = 0;
QList<Mod::Ptr> mods;
};

View File

@ -0,0 +1,336 @@
#include "ResourceFolderModel.h"
#include <QDebug>
#include <QMimeData>
#include <QThreadPool>
#include <QUrl>
#include "FileSystem.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "tasks/Task.h"
ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this)
{
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
}
bool ResourceFolderModel::startWatching(const QStringList paths)
{
if (m_is_watching)
return false;
update();
auto couldnt_be_watched = m_watcher.addPaths(paths);
for (auto path : paths) {
if (couldnt_be_watched.contains(path))
qDebug() << "Failed to start watching " << path;
else
qDebug() << "Started watching " << path;
}
m_is_watching = !m_is_watching;
return m_is_watching;
}
bool ResourceFolderModel::stopWatching(const QStringList paths)
{
if (!m_is_watching)
return false;
auto couldnt_be_stopped = m_watcher.removePaths(paths);
for (auto path : paths) {
if (couldnt_be_stopped.contains(path))
qDebug() << "Failed to stop watching " << path;
else
qDebug() << "Stopped watching " << path;
}
m_is_watching = !m_is_watching;
return !m_is_watching;
}
bool ResourceFolderModel::installResource(QString original_path)
{
if (!m_can_interact) {
return false;
}
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
original_path = FS::NormalizePath(original_path);
QFileInfo file_info(original_path);
if (!file_info.exists() || !file_info.isReadable()) {
qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path;
return false;
}
qDebug() << "Installing: " << file_info.absoluteFilePath();
Resource resource(file_info);
if (!resource.valid()) {
qWarning() << original_path << "is not a valid resource. Ignoring it.";
return false;
}
auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName()));
if (original_path == new_path) {
qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense...";
return false;
}
switch (resource.type()) {
case ResourceType::SINGLEFILE:
case ResourceType::ZIPFILE:
case ResourceType::LITEMOD: {
if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) {
if (!QFile::remove(new_path)) {
qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!";
return false;
}
qDebug() << new_path << "has been deleted.";
}
if (!QFile::copy(original_path, new_path)) {
qCritical() << "Copy from" << original_path << "to" << new_path << "has failed.";
return false;
}
FS::updateTimestamp(new_path);
QFileInfo new_path_file_info(new_path);
resource.setFile(new_path_file_info);
update();
return true;
}
case ResourceType::FOLDER: {
if (QFile::exists(new_path)) {
qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path;
return false;
}
if (!FS::copy(original_path, new_path)()) {
qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed.";
return false;
}
QFileInfo newpathInfo(new_path);
resource.setFile(newpathInfo);
update();
return true;
}
default:
break;
}
return false;
}
bool ResourceFolderModel::uninstallResource(QString file_name)
{
for (auto resource : m_resources) {
if (resource->fileinfo().fileName() == file_name)
return resource->destroy();
}
return false;
}
bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
{
if (!m_can_interact)
return false;
if (indexes.isEmpty())
return true;
for (auto i : indexes) {
if (i.column() != 0) {
continue;
}
auto resource = m_resources.at(i.row());
resource->destroy();
}
return true;
}
bool ResourceFolderModel::update()
{
// Already updating, so we schedule a future update and return.
if (m_current_update_task) {
m_scheduled_update = true;
return false;
}
m_current_update_task.reset(createUpdateTask());
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
auto* thread_pool = QThreadPool::globalInstance();
thread_pool->start(m_current_update_task.get());
return true;
}
void ResourceFolderModel::resolveResource(Resource::Ptr res)
{
if (!res->shouldResolve()) {
return;
}
auto task = createParseTask(*res);
m_ticket_mutex.lock();
int ticket = m_next_resolution_ticket;
m_next_resolution_ticket += 1;
m_ticket_mutex.unlock();
res->setResolving(true, ticket);
m_active_parse_tasks.insert(ticket, task);
connect(task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect(task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
auto* thread_pool = QThreadPool::globalInstance();
thread_pool->start(task);
}
void ResourceFolderModel::onUpdateSucceeded()
{
auto update_results = static_cast<BasicFolderLoadTask*>(m_current_update_task.get())->result();
auto& new_resources = update_results->resources;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto current_list = m_resources_index.keys();
QSet<QString> current_set(current_list.begin(), current_list.end());
auto new_list = new_resources.keys();
QSet<QString> new_set(new_list.begin(), new_list.end());
#else
QSet<QString> current_set(m_resources_index.keys().toSet());
QSet<QString> new_set(new_resources.keys().toSet());
#endif
applyUpdates(current_set, new_set, new_resources);
update_results.reset();
m_current_update_task->deleteLater();
m_current_update_task.reset();
emit updateFinished();
if (m_scheduled_update) {
m_scheduled_update = false;
update();
}
}
void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id)
{
auto iter = m_active_parse_tasks.constFind(ticket);
if (iter == m_active_parse_tasks.constEnd())
return;
(*iter)->deleteLater();
m_active_parse_tasks.remove(ticket);
int row = m_resources_index[resource_id];
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
Task* ResourceFolderModel::createUpdateTask()
{
return new BasicFolderLoadTask(m_dir);
}
void ResourceFolderModel::directoryChanged(QString path)
{
update();
}
Qt::DropActions ResourceFolderModel::supportedDropActions() const
{
// copy from outside, move from within and other resource lists
return Qt::CopyAction | Qt::MoveAction;
}
Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags;
if (!m_can_interact) {
flags &= ~Qt::ItemIsDropEnabled;
} else {
flags |= Qt::ItemIsDropEnabled;
if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
}
return flags;
}
QStringList ResourceFolderModel::mimeTypes() const
{
QStringList types;
types << "text/uri-list";
return types;
}
bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
{
if (action == Qt::IgnoreAction) {
return true;
}
// check if the action is supported
if (!data || !(action & supportedDropActions())) {
return false;
}
// files dropped from outside?
if (data->hasUrls()) {
auto urls = data->urls();
for (auto url : urls) {
// only local files may be dropped...
if (!url.isLocalFile()) {
continue;
}
// TODO: implement not only copy, but also move
// FIXME: handle errors here
installResource(url.toLocalFile());
}
return true;
}
return false;
}
bool ResourceFolderModel::validateIndex(const QModelIndex& index) const
{
if (!index.isValid())
return false;
size_t row = index.row();
if (row < 0 || row >= size())
return false;
return true;
}
void ResourceFolderModel::enableInteraction(bool enabled)
{
if (m_can_interact == enabled)
return;
m_can_interact = enabled;
if (size())
emit dataChanged(index(0), index(size() - 1));
}

View File

@ -0,0 +1,274 @@
#pragma once
#include <QAbstractListModel>
#include <QDir>
#include <QFileSystemWatcher>
#include <QMutex>
#include <QSet>
#include "Resource.h"
#include "tasks/Task.h"
class QRunnable;
/** A basic model for external resources.
*
* To implement one such model, you need to implement, at the very minimum:
* - columnCount: The number of columns in your model.
* - data: How the model data is displayed and accessed.
* - headerData: Display properties of the header.
*/
class ResourceFolderModel : public QAbstractListModel {
Q_OBJECT
public:
ResourceFolderModel(QDir, QObject* parent = nullptr);
/** Starts watching the paths for changes.
*
* Returns whether starting to watch all the paths was successful.
* If one or more fails, it returns false.
*/
bool startWatching(const QStringList paths);
/** Stops watching the paths for changes.
*
* Returns whether stopping to watch all the paths was successful.
* If one or more fails, it returns false.
*/
bool stopWatching(const QStringList paths);
/** Given a path in the system, install that resource, moving it to its place in the
* instance file hierarchy.
*
* Returns whether the installation was succcessful.
*/
virtual bool installResource(QString path);
/** Uninstall (i.e. remove all data about it) a resource, given its file name.
*
* Returns whether the removal was successful.
*/
virtual bool uninstallResource(QString file_name);
virtual bool deleteResources(const QModelIndexList&);
/** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */
virtual bool update();
/** Creates a new parse task, if needed, for 'res' and start it.*/
virtual void resolveResource(Resource::Ptr res);
[[nodiscard]] size_t size() const { return m_resources.size(); };
[[nodiscard]] bool empty() const { return size() == 0; }
[[nodiscard]] QDir const& dir() const { return m_dir; }
/* Qt behavior */
[[nodiscard]] int rowCount(const QModelIndex&) const override { return size(); }
[[nodiscard]] int columnCount(const QModelIndex&) const override = 0;
[[nodiscard]] Qt::DropActions supportedDropActions() const override;
/// flags, mostly to support drag&drop
[[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override;
[[nodiscard]] QStringList mimeTypes() const override;
bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override;
[[nodiscard]] bool validateIndex(const QModelIndex& index) const;
[[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override = 0;
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override { return false; };
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override = 0;
public slots:
void enableInteraction(bool enabled);
signals:
void updateFinished();
protected:
/** This creates a new update task to be executed by update().
*
* The task should load and parse all resources necessary, and provide a way of accessing such results.
*
* This Task is normally executed when opening a page, so it shouldn't contain much heavy work.
* If such work is needed, try using it in the Task create by createParseTask() instead!
*/
[[nodiscard]] virtual Task* createUpdateTask();
/** This creates a new parse task to be executed by onUpdateSucceeded().
*
* This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed
* in the background, so it slowly updates the UI as tasks get done.
*/
[[nodiscard]] virtual Task* createParseTask(Resource const&) { return nullptr; };
/** Standard implementation of the model update logic.
*
* It uses set operations to find differences between the current state and the updated state,
* to act only on those disparities.
*
* The implementation is at the end of this header.
*/
template <typename T>
void applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources);
protected slots:
void directoryChanged(QString);
/** Called when the update task is successful.
*
* This usually calls static_cast on the specific Task type returned by createUpdateTask,
* so care must be taken in such cases.
* TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro dissalows that).
*/
virtual void onUpdateSucceeded();
virtual void onUpdateFailed() {}
/** Called when the parse task with the given ticket is successful.
*
* This is just a simple reference implementation. You probably want to override it with your own logic in a subclass
* if the resource is complex and has more stuff to parse.
*/
virtual void onParseSucceeded(int ticket, QString resource_id);
virtual void onParseFailed(int ticket, QString resource_id) {}
protected:
bool m_can_interact = true;
QDir m_dir;
QFileSystemWatcher m_watcher;
bool m_is_watching = false;
Task::Ptr m_current_update_task = nullptr;
bool m_scheduled_update = false;
QList<Resource::Ptr> m_resources;
// Represents the relationship between a resource's internal ID and it's row position on the model.
QMap<QString, int> m_resources_index;
QMap<int, Task::Ptr> m_active_parse_tasks;
int m_next_resolution_ticket = 0;
QMutex m_ticket_mutex;
};
/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */
#define RESOURCE_HELPERS(T) \
[[nodiscard]] T* operator[](size_t index) \
{ \
return static_cast<T*>(m_resources[index].get()); \
} \
[[nodiscard]] T* at(size_t index) \
{ \
return static_cast<T*>(m_resources[index].get()); \
} \
[[nodiscard]] const T* at(size_t index) const \
{ \
return static_cast<const T*>(m_resources.at(index).get()); \
} \
[[nodiscard]] T* first() \
{ \
return static_cast<T*>(m_resources.first().get()); \
} \
[[nodiscard]] T* last() \
{ \
return static_cast<T*>(m_resources.last().get()); \
} \
[[nodiscard]] T* find(QString id) \
{ \
auto iter = std::find_if(m_resources.begin(), m_resources.end(), [&](Resource::Ptr r) { return r->internal_id() == id; }); \
if (iter == m_resources.end()) \
return nullptr; \
return static_cast<T*>((*iter).get()); \
}
/* Template definition to avoid some code duplication */
template <typename T>
void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources)
{
// see if the kept resources changed in some way
{
QSet<QString> kept_set = current_set;
kept_set.intersect(new_set);
for (auto& kept : kept_set) {
auto row = m_resources_index[kept];
auto new_resource = new_resources[kept];
auto current_resource = m_resources[row];
if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
// no significant change, ignore...
continue;
}
// If the resource is resolving, but something about it changed, we don't want to
// continue the resolving.
if (current_resource->isResolving()) {
m_active_parse_tasks.remove(current_resource->resolutionTicket());
}
m_resources[row] = new_resource;
resolveResource(new_resource);
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
}
// remove resources no longer present
{
QSet<QString> removed_set = current_set;
removed_set.subtract(new_set);
QList<int> removed_rows;
for (auto& removed : removed_set)
removed_rows.append(m_resources_index[removed]);
std::sort(removed_rows.begin(), removed_rows.end());
for (auto& removed_index : removed_rows) {
beginRemoveRows(QModelIndex(), removed_index, removed_index);
auto removed_it = m_resources.begin() + removed_index;
if ((*removed_it)->isResolving()) {
m_active_parse_tasks.remove((*removed_it)->resolutionTicket());
}
m_resources.erase(removed_it);
endRemoveRows();
}
}
// add new resources to the end
{
QSet<QString> added_set = new_set;
added_set.subtract(current_set);
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (added_set.size() > 0) {
beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1);
for (auto& added : added_set) {
auto res = new_resources[added];
m_resources.append(res);
resolveResource(res);
}
endInsertRows();
}
}
// update index
{
m_resources_index.clear();
int idx = 0;
for (auto mod : m_resources) {
m_resources_index[mod->internal_id()] = idx;
idx++;
}
}
}

View File

@ -0,0 +1,44 @@
#pragma once
#include <QDir>
#include <QMap>
#include <QObject>
#include <memory>
#include "minecraft/mod/Resource.h"
#include "tasks/Task.h"
/** Very simple task that just loads a folder's contents directly.
*/
class BasicFolderLoadTask : public Task
{
Q_OBJECT
public:
struct Result {
QMap<QString, Resource::Ptr> resources;
};
using ResultPtr = std::shared_ptr<Result>;
[[nodiscard]] ResultPtr result() const {
return m_result;
}
public:
BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {}
void executeTask() override
{
m_dir.refresh();
for (auto entry : m_dir.entryInfoList()) {
auto resource = new Resource(entry);
m_result->resources.insert(resource->internal_id(), resource);
}
emitSucceeded();
}
private:
QDir m_dir;
ResultPtr m_result;
};

View File

@ -338,13 +338,13 @@ std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
}
LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile):
LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile):
Task(nullptr, false),
m_token(token),
m_type(type),
m_modFile(modFile),
m_result(new Result())
{
}
{}
void LocalModParseTask::processAsZip()
{
@ -497,21 +497,21 @@ void LocalModParseTask::processAsLitemod()
zip.close();
}
void LocalModParseTask::run()
void LocalModParseTask::executeTask()
{
switch(m_type)
{
case Mod::MOD_ZIPFILE:
case ResourceType::ZIPFILE:
processAsZip();
break;
case Mod::MOD_FOLDER:
case ResourceType::FOLDER:
processAsFolder();
break;
case Mod::MOD_LITEMOD:
case ResourceType::LITEMOD:
processAsLitemod();
break;
default:
break;
}
emit finished(m_token);
emitSucceeded();
}

View File

@ -2,17 +2,17 @@
#include <QDebug>
#include <QObject>
#include <QRunnable>
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModDetails.h"
class LocalModParseTask : public QObject, public QRunnable
#include "tasks/Task.h"
class LocalModParseTask : public Task
{
Q_OBJECT
public:
struct Result {
QString id;
std::shared_ptr<ModDetails> details;
};
using ResultPtr = std::shared_ptr<Result>;
@ -20,11 +20,10 @@ public:
return m_result;
}
LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
void run();
LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
void executeTask() override;
signals:
void finished(int token);
[[nodiscard]] int token() const { return m_token; }
private:
void processAsZip();
@ -33,7 +32,7 @@ private:
private:
int m_token;
Mod::ModType m_type;
ResourceType m_type;
QFileInfo m_modFile;
ResultPtr m_result;
};

View File

@ -38,11 +38,11 @@
#include "minecraft/mod/MetadataHandler.h"
ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan)
: m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan)
: Task(nullptr, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
{}
void ModFolderLoadTask::run()
void ModFolderLoadTask::executeTask()
{
if (m_is_indexed) {
// Read metadata first
@ -96,7 +96,7 @@ void ModFolderLoadTask::run()
}
}
emit succeeded();
emitSucceeded();
}
void ModFolderLoadTask::getFromMetadata()

View File

@ -42,8 +42,9 @@
#include <QRunnable>
#include <memory>
#include "minecraft/mod/Mod.h"
#include "tasks/Task.h"
class ModFolderLoadTask : public QObject, public QRunnable
class ModFolderLoadTask : public Task
{
Q_OBJECT
public:
@ -56,16 +57,15 @@ public:
}
public:
ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan = false);
void run();
signals:
void succeeded();
ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false);
void executeTask() override;
private:
void getFromMetadata();
private:
QDir& m_mods_dir, m_index_dir;
QDir m_mods_dir, m_index_dir;
bool m_is_indexed;
bool m_clean_orphan;
ResultPtr m_result;

View File

@ -32,12 +32,12 @@ class SortProxy : public QSortFilterProxyModel {
const auto& mod = model->at(source_row);
if (filterRegularExpression().match(mod.name()).hasMatch())
if (filterRegularExpression().match(mod->name()).hasMatch())
return true;
if (filterRegularExpression().match(mod.description()).hasMatch())
if (filterRegularExpression().match(mod->description()).hasMatch())
return true;
for (auto& author : mod.authors()) {
for (auto& author : mod->authors()) {
if (filterRegularExpression().match(author).hasMatch()) {
return true;
}
@ -292,6 +292,6 @@ void ExternalResourcesPage::current(const QModelIndex& current, const QModelInde
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
Mod& m = m_model->operator[](row);
Mod& m = *m_model->operator[](row);
ui->frame->updateWithMod(m);
}