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>
337 lines
9.3 KiB
C++
337 lines
9.3 KiB
C++
#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));
|
|
}
|