From ec62d8e97334d3b5a30cea00858e7035468f3609 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 9 Aug 2022 01:58:22 -0300 Subject: 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 --- launcher/minecraft/mod/ModFolderModel.cpp | 424 ++++++------------------------ 1 file changed, 74 insertions(+), 350 deletions(-) (limited to 'launcher/minecraft/mod/ModFolderModel.cpp') 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(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 currentSet(currentList.begin(), currentList.end()); - auto & newMods = m_update->mods; - auto newList = newMods.keys(); - QSet newSet(newList.begin(), newList.end()); + auto current_list = m_resources_index.keys(); + QSet current_set(current_list.begin(), current_list.end()); + + auto new_list = new_mods.keys(); + QSet new_set(new_list.begin(), new_list.end()); #else - QSet currentSet = modsIndex.keys().toSet(); - auto& newMods = m_update->mods; - QSet newSet = newMods.keys().toSet(); + QSet current_set(m_resources_index.keys().toSet()); + QSet new_set(new_mods.keys().toSet()); #endif - // see if the kept mods changed in some way - { - QSet 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 removed = currentSet; - QList removedRows; - removed.subtract(newSet); - for(auto & removedMod: removed) { - removedRows.append(modsIndex[removedMod]); - } - std::sort(removedRows.begin(), removedRows.end(), std::greater()); - 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 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(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 { - QList selected_mods; + QList 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 { - 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 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(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; -} -- cgit