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>
2022-08-09 05:58:22 +01:00
|
|
|
#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());
|
2022-08-10 18:42:24 +01:00
|
|
|
if (!m_current_update_task)
|
|
|
|
return false;
|
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>
2022-08-09 05:58:22 +01:00
|
|
|
|
2022-08-10 18:42:24 +01:00
|
|
|
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
|
|
|
|
Qt::ConnectionType::QueuedConnection);
|
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>
2022-08-09 05:58:22 +01:00
|
|
|
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);
|
2022-08-10 18:42:24 +01:00
|
|
|
if (!task)
|
|
|
|
return;
|
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>
2022-08-09 05:58:22 +01:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2022-08-10 18:42:24 +01:00
|
|
|
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);
|
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>
2022-08-09 05:58:22 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-08-10 18:42:24 +01:00
|
|
|
QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
|
|
|
|
{
|
|
|
|
if (!validateIndex(index))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
int row = index.row();
|
|
|
|
int column = index.column();
|
|
|
|
|
|
|
|
switch (role) {
|
|
|
|
case Qt::DisplayRole:
|
|
|
|
switch (column) {
|
|
|
|
case NAME_COLUMN:
|
|
|
|
return m_resources[row]->name();
|
|
|
|
case DATE_COLUMN:
|
|
|
|
return m_resources[row]->dateTimeChanged();
|
|
|
|
default:
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
case Qt::ToolTipRole:
|
|
|
|
return m_resources[row]->internal_id();
|
|
|
|
default:
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
|
|
|
|
{
|
|
|
|
switch (role) {
|
|
|
|
case Qt::DisplayRole:
|
|
|
|
switch (section) {
|
|
|
|
case NAME_COLUMN:
|
|
|
|
return tr("Name");
|
|
|
|
case DATE_COLUMN:
|
|
|
|
return tr("Last modified");
|
|
|
|
default:
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
case Qt::ToolTipRole: {
|
|
|
|
switch (section) {
|
|
|
|
case NAME_COLUMN:
|
|
|
|
return tr("The name of the resource.");
|
|
|
|
case DATE_COLUMN:
|
|
|
|
return tr("The date and time this resource was last changed (or added).");
|
|
|
|
default:
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent)
|
|
|
|
{
|
|
|
|
return new ProxyModel(parent);
|
|
|
|
}
|
|
|
|
|
|
|
|
SortType ResourceFolderModel::columnToSortKey(size_t column) const
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_column_sort_keys.size() == columnCount());
|
|
|
|
return m_column_sort_keys.at(column);
|
|
|
|
}
|
|
|
|
|
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>
2022-08-09 05:58:22 +01:00
|
|
|
void ResourceFolderModel::enableInteraction(bool enabled)
|
|
|
|
{
|
|
|
|
if (m_can_interact == enabled)
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_can_interact = enabled;
|
|
|
|
if (size())
|
|
|
|
emit dataChanged(index(0), index(size() - 1));
|
|
|
|
}
|
2022-08-10 18:42:24 +01:00
|
|
|
|
|
|
|
/* Standard Proxy Model for createFilterProxyModel */
|
|
|
|
[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
|
|
|
|
{
|
|
|
|
auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
|
|
|
|
if (!model)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
const auto& resource = model->at(source_row);
|
|
|
|
|
|
|
|
return resource.applyFilter(filterRegularExpression());
|
|
|
|
}
|
|
|
|
|
|
|
|
[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const
|
|
|
|
{
|
|
|
|
auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
|
|
|
|
if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
|
|
|
|
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
|
|
|
}
|
|
|
|
|
|
|
|
// we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
|
|
|
|
// proceed.
|
|
|
|
|
|
|
|
auto column_sort_key = model->columnToSortKey(source_left.column());
|
|
|
|
auto const& resource_left = model->at(source_left.row());
|
|
|
|
auto const& resource_right = model->at(source_right.row());
|
|
|
|
|
|
|
|
auto compare_result = resource_left.compare(resource_right, column_sort_key);
|
|
|
|
if (compare_result.first == 0)
|
|
|
|
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
|
|
|
|
|
|
|
if (compare_result.second || sortOrder() != Qt::DescendingOrder)
|
|
|
|
return (compare_result.first < 0);
|
|
|
|
return (compare_result.first > 0);
|
|
|
|
}
|
|
|
|
|