diff options
| -rw-r--r-- | launcher/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | launcher/minecraft/mod/ModFolderModel.cpp | 424 | ||||
| -rw-r--r-- | launcher/minecraft/mod/ModFolderModel.h | 93 | ||||
| -rw-r--r-- | launcher/minecraft/mod/ResourceFolderModel.cpp | 336 | ||||
| -rw-r--r-- | launcher/minecraft/mod/ResourceFolderModel.h | 274 | ||||
| -rw-r--r-- | launcher/minecraft/mod/tasks/BasicFolderLoadTask.h | 44 | ||||
| -rw-r--r-- | launcher/minecraft/mod/tasks/LocalModParseTask.cpp | 16 | ||||
| -rw-r--r-- | launcher/minecraft/mod/tasks/LocalModParseTask.h | 15 | ||||
| -rw-r--r-- | launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 8 | ||||
| -rw-r--r-- | launcher/minecraft/mod/tasks/ModFolderLoadTask.h | 12 | ||||
| -rw-r--r-- | launcher/ui/pages/instance/ExternalResourcesPage.cpp | 8 |
11 files changed, 778 insertions, 455 deletions
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 70997e51..d1e0befd 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -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 diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index d4c5e819..597f9807 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -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))); } -void ModFolderModel::startWatching() +Task* ModFolderModel::createUpdateTask() { - if(is_watching) - return; + 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() +{ // 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 index_dir = indexDir(); - auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); - m_first_folder_load = false; - - m_update = task->result(); + auto update_results = static_cast<ModFolderLoadTask*>(m_current_update_task.get())->result(); - QThreadPool *threadPool = QThreadPool::globalInstance(); - connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); - - threadPool->start(task); - return true; -} + auto& new_mods = update_results->mods; -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)); -} -void ModFolderModel::disableInteraction(bool disabled) -{ - if (interaction_disabled == disabled) { - return; - } - interaction_disabled = disabled; - if(size()) { - emit dataChanged(index(0), index(size() - 1)); - } + parse_task->deleteLater(); + m_active_parse_tasks.remove(ticket); } -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; - } - - // 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); - - 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; - } + QList<Mod::Ptr> mods; - 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; - } + for (auto res : m_resources) + mods.append(static_cast<Mod*>(res.get())); - 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; -} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 3d6efac3..a90457d5 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -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); - void startWatching(); - void stopWatching(); - bool isValid(); - QDir& dir() - { - return m_dir; - } - - QDir indexDir() - { - return { QString("%1/.index").arg(dir().absolutePath()) }; - } + void startWatching(); + void stopWatching(); - 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; }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp new file mode 100644 index 00000000..4867a8c2 --- /dev/null +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -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 |
