/*
* cmodlistview_moc.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "cmodlistview_moc.h"
#include "ui_cmodlistview_moc.h"
#include "imageviewer_moc.h"
#include "../mainwindow_moc.h"
#include %1: ";
QString entryEnd = "
%1: "; // shared prefix QString redPrefix = "
%1: "; // shared prefix QString lineTemplate = prefix + "%2
"; QString urlTemplate = prefix + "%3"; QString textTemplate = prefix + "%2
"; QString listTemplate = "%1: %2
"; QString noteTemplate = "%1
"; QString incompatibleString = redPrefix + tr("Mod is incompatible") + ""; QString supportedVersions = redPrefix + "%2 %3 %4"; QString result; result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg(tr("Mod name"))); result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version"))); result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); if(mod.getValue("localSizeBytes").isValid()) result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("Size"))); if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("downloadSize").isValid()) result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("downloadSize").toDouble())), lineTemplate.arg(tr("Download size"))); result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors"))); if(mod.getValue("licenseURL").isValid()) result += urlTemplate.arg(tr("License")).arg(mod.getValue("licenseURL").toString()).arg(mod.getValue("licenseName").toString()); if(mod.getValue("contact").isValid()) result += urlTemplate.arg(tr("Contact")).arg(mod.getValue("contact").toString()).arg(mod.getValue("contact").toString()); //compatibility info if(!mod.isCompatible()) { auto compatibilityInfo = mod.getValue("compatibility").toMap(); auto minStr = compatibilityInfo.value("min").toString(); auto maxStr = compatibilityInfo.value("max").toString(); result += incompatibleString.arg(tr("Compatibility")); if(minStr == maxStr) result += supportedVersions.arg(tr("Required VCMI version"), minStr, "", ""); else { if(minStr.isEmpty() || maxStr.isEmpty()) { if(minStr.isEmpty()) result += supportedVersions.arg(tr("Supported VCMI version"), maxStr, ", ", "please upgrade mod"); else result += supportedVersions.arg(tr("Required VCMI version"), minStr, " ", "or above"); } else result += supportedVersions.arg(tr("Supported VCMI versions"), minStr, " - ", maxStr); } } QStringList supportedLanguages; QVariant baseLanguageVariant = mod.getBaseValue("language"); QString baseLanguageID = baseLanguageVariant.isValid() ? baseLanguageVariant.toString() : "english"; bool needToShowSupportedLanguages = false; for(const auto & language : Languages::getLanguageList()) { if (!language.hasTranslation) continue; QString languageID = QString::fromStdString(language.identifier); if (languageID != baseLanguageID && !mod.getValue(languageID).isValid()) continue; if (languageID != baseLanguageID) needToShowSupportedLanguages = true; supportedLanguages += QApplication::translate("Language", language.nameEnglish.c_str()); } if(needToShowSupportedLanguages) result += replaceIfNotEmpty(supportedLanguages, lineTemplate.arg(tr("Languages"))); result += replaceIfNotEmpty(getModNames(mod.getDependencies()), lineTemplate.arg(tr("Required mods"))); result += replaceIfNotEmpty(getModNames(mod.getConflicts()), lineTemplate.arg(tr("Conflicting mods"))); result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg(tr("Description"))); result += ""; // to get some empty space QString unknownDeps = tr("This mod can not be installed or enabled because the following dependencies are not present"); QString blockingMods = tr("This mod can not be enabled because the following mods are incompatible with it"); QString hasActiveDependentMods = tr("This mod cannot be disabled because it is required by the following mods"); QString hasDependentMods = tr("This mod cannot be uninstalled or updated because it is required by the following mods"); QString thisIsSubmod = tr("This is a submod and it cannot be installed or uninstalled separately from its parent mod"); QString notes; notes += replaceIfNotEmpty(getModNames(findInvalidDependencies(mod.getName())), listTemplate.arg(unknownDeps)); notes += replaceIfNotEmpty(getModNames(findBlockingMods(mod.getName())), listTemplate.arg(blockingMods)); if(mod.isEnabled()) notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), true)), listTemplate.arg(hasActiveDependentMods)); if(mod.isInstalled()) notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), false)), listTemplate.arg(hasDependentMods)); if(mod.isSubmod()) notes += noteTemplate.arg(thisIsSubmod); if(notes.size()) result += textTemplate.arg(tr("Notes")).arg(notes); return result; } void CModListView::disableModInfo() { ui->disableButton->setVisible(false); ui->enableButton->setVisible(false); ui->installButton->setVisible(false); ui->uninstallButton->setVisible(false); ui->updateButton->setVisible(false); } void CModListView::dataChanged(const QModelIndex & topleft, const QModelIndex & bottomRight) { selectMod(ui->allModsView->currentIndex()); } void CModListView::selectMod(const QModelIndex & index) { if(!index.isValid()) { disableModInfo(); } else { auto mod = modModel->getMod(index.data(ModRoles::ModNameRole).toString()); ui->modInfoBrowser->setHtml(genModInfoText(mod)); ui->changelogBrowser->setHtml(genChangelogText(mod)); bool hasInvalidDeps = !findInvalidDependencies(index.data(ModRoles::ModNameRole).toString()).empty(); bool hasBlockingMods = !findBlockingMods(index.data(ModRoles::ModNameRole).toString()).empty(); bool hasDependentMods = !findDependentMods(index.data(ModRoles::ModNameRole).toString(), true).empty(); ui->disableButton->setVisible(mod.isEnabled()); ui->enableButton->setVisible(mod.isDisabled()); ui->installButton->setVisible(mod.isAvailable() && !mod.isSubmod()); ui->uninstallButton->setVisible(mod.isInstalled() && !mod.isSubmod()); ui->updateButton->setVisible(mod.isUpdateable()); // Block buttons if action is not allowed at this time // TODO: automate handling of some of these cases instead of forcing player // to resolve all conflicts manually. ui->disableButton->setEnabled(!hasDependentMods && !mod.isEssential()); ui->enableButton->setEnabled(!hasBlockingMods && !hasInvalidDeps); ui->installButton->setEnabled(!hasInvalidDeps); ui->uninstallButton->setEnabled(!hasDependentMods && !mod.isEssential()); ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods); loadScreenshots(); } } void CModListView::modSelected(const QModelIndex & current, const QModelIndex &) { selectMod(current); } void CModListView::on_allModsView_activated(const QModelIndex & index) { selectMod(index); loadScreenshots(); } void CModListView::on_lineEdit_textChanged(const QString & arg1) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto baseStr = QRegularExpression::wildcardToRegularExpression(arg1, QRegularExpression::UnanchoredWildcardConversion); #else auto baseStr = QRegularExpression::wildcardToRegularExpression(arg1); //Hack due to lack QRegularExpression::UnanchoredWildcardConversion in Qt5 baseStr.chop(3); baseStr.remove(0,5); #endif QRegularExpression regExp{baseStr, QRegularExpression::CaseInsensitiveOption}; filterModel->setFilterRegularExpression(regExp); } void CModListView::on_comboBox_currentIndexChanged(int index) { switch(index) { case 0: filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::MASK_NONE); break; case 1: filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::INSTALLED); break; case 2: filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::INSTALLED); break; case 3: filterModel->setTypeFilter(ModStatus::UPDATEABLE, ModStatus::UPDATEABLE); break; case 4: filterModel->setTypeFilter(ModStatus::ENABLED | ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED); break; case 5: filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED); break; } } QStringList CModListView::findInvalidDependencies(QString mod) { QStringList ret; for(QString requrement : modModel->getRequirements(mod)) { if(!modModel->hasMod(requrement)) ret += requrement; } return ret; } QStringList CModListView::findBlockingMods(QString modUnderTest) { QStringList ret; auto required = modModel->getRequirements(modUnderTest); for(QString name : modModel->getModList()) { auto mod = modModel->getMod(name); if(mod.isEnabled()) { // one of enabled mods have requirement (or this mod) marked as conflict for(auto conflict : mod.getConflicts()) { if(required.contains(conflict)) ret.push_back(name); } } } return ret; } QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled) { QStringList ret; for(QString modName : modModel->getModList()) { auto current = modModel->getMod(modName); if(!current.isInstalled() || !current.isVisible()) continue; if(current.getDependencies().contains(mod, Qt::CaseInsensitive)) { if(!(current.isDisabled() && excludeDisabled)) ret += modName; } } return ret; } void CModListView::on_enableButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); enableModByName(modName); checkManagerErrors(); } void CModListView::enableModByName(QString modName) { assert(findBlockingMods(modName).empty()); assert(findInvalidDependencies(modName).empty()); for(auto & name : modModel->getRequirements(modName)) { if(modModel->getMod(name).isDisabled()) manager->enableMod(name); } emit modsChanged(); } void CModListView::on_disableButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); disableModByName(modName); checkManagerErrors(); } void CModListView::disableModByName(QString modName) { if(modModel->hasMod(modName) && modModel->getMod(modName).isEnabled()) manager->disableMod(modName); emit modsChanged(); } void CModListView::on_updateButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); assert(findInvalidDependencies(modName).empty()); for(auto & name : modModel->getRequirements(modName)) { auto mod = modModel->getMod(name); // update required mod, install missing (can be new dependency) if(mod.isUpdateable() || !mod.isInstalled()) downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble())); } } void CModListView::on_uninstallButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); // NOTE: perhaps add "manually installed" flag and uninstall those dependencies that don't have it? if(modModel->hasMod(modName) && modModel->getMod(modName).isInstalled()) { if(modModel->getMod(modName).isEnabled()) manager->disableMod(modName); manager->uninstallMod(modName); } emit modsChanged(); checkManagerErrors(); } void CModListView::on_installButton_clicked() { QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); assert(findInvalidDependencies(modName).empty()); for(auto & name : modModel->getRequirements(modName)) { auto mod = modModel->getMod(name); if(!mod.isInstalled()) downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble())); } } void CModListView::downloadFile(QString file, QString url, QString description, qint64 size) { if(!dlManager) { dlManager = new CDownloadManager(); ui->progressWidget->setVisible(true); connect(dlManager, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(downloadProgress(qint64,qint64))); connect(dlManager, SIGNAL(finished(QStringList,QStringList,QStringList)), this, SLOT(downloadFinished(QStringList,QStringList,QStringList))); connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)), this, SLOT(extractionProgress(qint64,qint64))); connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); QString progressBarFormat = tr("Downloading %s%. %p% (%v MB out of %m MB) finished"); progressBarFormat.replace("%s%", description); ui->progressBar->setFormat(progressBarFormat); } dlManager->downloadFile(QUrl(url), file, size); } void CModListView::downloadProgress(qint64 current, qint64 max) { // display progress, in megabytes ui->progressBar->setVisible(true); ui->progressBar->setMaximum(max / (1024 * 1024)); ui->progressBar->setValue(current / (1024 * 1024)); } void CModListView::extractionProgress(qint64 current, qint64 max) { // display progress, in extracted files ui->progressBar->setVisible(true); ui->progressBar->setMaximum(max); ui->progressBar->setValue(current); } void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors) { QString title = tr("Download failed"); QString firstLine = tr("Unable to download all files.\n\nEncountered errors:\n\n"); QString lastLine = tr("\n\nInstall successfully downloaded?"); bool doInstallFiles = false; // if all files were d/loaded there should be no errors. And on failure there must be an error assert(failedFiles.empty() == errors.empty()); if(savedFiles.empty()) { // no successfully downloaded mods QMessageBox::warning(this, title, firstLine + errors.join("\n"), QMessageBox::Ok, QMessageBox::Ok); } else if(!failedFiles.empty()) { // some mods were not downloaded int result = QMessageBox::warning (this, title, firstLine + errors.join("\n") + lastLine, QMessageBox::Yes | QMessageBox::No, QMessageBox::No ); if(result == QMessageBox::Yes) doInstallFiles = true; } else { // everything OK doInstallFiles = true; } dlManager->deleteLater(); dlManager = nullptr; ui->progressBar->setMaximum(0); ui->progressBar->setValue(0); if(doInstallFiles) installFiles(savedFiles); hideProgressBar(); emit modsChanged(); } void CModListView::hideProgressBar() { if(dlManager == nullptr) // it was not recreated meanwhile { ui->progressWidget->setVisible(false); ui->progressBar->setMaximum(0); ui->progressBar->setValue(0); } } void CModListView::installFiles(QStringList files) { QStringList mods; QStringList maps; QStringList images; QVector