/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "fomodinstallerdialog.h" #include "ui_fomodinstallerdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #endif #include #include #include #include #include #include #include #include #include "fomodscreenshotdialog.h" #include "xmlreader.h" using namespace MOBase; bool ControlsAscending(QAbstractButton* LHS, QAbstractButton* RHS) { return LHS->text() < RHS->text(); } bool ControlsDescending(QAbstractButton* LHS, QAbstractButton* RHS) { return LHS->text() > RHS->text(); } bool PagesAscending(QGroupBox* LHS, QGroupBox* RHS) { return LHS->title() < RHS->title(); } bool PagesDescending(QGroupBox* LHS, QGroupBox* RHS) { return LHS->title() > RHS->title(); } FomodInstallerDialog::FomodInstallerDialog( InstallerFomod* installer, const GuessedValue& modName, const QString& fomodPath, const QString& fomodDirName, const std::function& fileCheck, QWidget* parent) : QDialog(parent), ui(new Ui::FomodInstallerDialog), m_Installer(installer), m_ModName(modName), m_ModID(-1), m_FomodPath(fomodPath), m_FomodDirName(fomodDirName), m_Manual(false), m_FileCheck(fileCheck), m_FileSystemItemSequence() { ui->setupUi(this); setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint); setWindowTitle(modName); updateNameEdit(); ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); } FomodInstallerDialog::~FomodInstallerDialog() { delete ui; } bool FomodInstallerDialog::hasOptions() { return ui->stepsStack->count() > 0; } void FomodInstallerDialog::transformToSmallInstall() { ui->descriptionText->setVisible(false); ui->screenshotLabel->setVisible(false); ui->stepsStack->setVisible(false); adjustSize(); } void FomodInstallerDialog::updateNameEdit() { ui->nameCombo->clear(); for (auto iter = m_ModName.variants().begin(); iter != m_ModName.variants().end(); ++iter) { ui->nameCombo->addItem(*iter); } ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(m_ModName)); } int FomodInstallerDialog::bomOffset(const QByteArray& buffer) { static const unsigned char BOM_UTF8[] = {0xEF, 0xBB, 0xBF}; static const unsigned char BOM_UTF16BE[] = {0xFE, 0xFF}; static const unsigned char BOM_UTF16LE[] = {0xFF, 0xFE}; auto startsWithBytes = [&buffer](const unsigned char* bytes, qsizetype size) { return buffer.startsWith( QByteArrayView(reinterpret_cast(bytes), size)); }; if (startsWithBytes(BOM_UTF8, sizeof(BOM_UTF8))) return 3; if (startsWithBytes(BOM_UTF16BE, sizeof(BOM_UTF16BE)) || startsWithBytes(BOM_UTF16LE, sizeof(BOM_UTF16LE))) return 2; return 0; } struct XmlParseError : std::runtime_error { XmlParseError(const QString& message) : std::runtime_error(qUtf8Printable(message)) {} }; static QString normalizeArchivePath(QString path) { path.replace("\\", "/"); path = QDir::cleanPath(path); return path == "." ? QString() : path; } static QString joinArchivePath(const QString& base, const QString& child) { if (base.isEmpty()) { return normalizeArchivePath(child); } return normalizeArchivePath(QDir(base).filePath(child)); } static QString tempArchivePath(const QString& archivePath) { return QDir(QDir::tempPath()).filePath(normalizeArchivePath(archivePath)); } static QString fomodFilePath(const QString& fomodPath, const QString& fomodDirName, const QString& fileName) { const QString directory = tempArchivePath(joinArchivePath(fomodPath, fomodDirName)); const QDir dir(directory); for (const QString& candidate : dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { if (candidate.compare(fileName, Qt::CaseInsensitive) == 0) { return dir.filePath(candidate); } } return dir.filePath(fileName); } QByteArray skipXmlHeader(QIODevice& file) { static const unsigned char UTF16LE_BOM[] = {0xFF, 0xFE}; static const unsigned char UTF16BE_BOM[] = {0xFE, 0xFF}; static const unsigned char UTF8_BOM[] = {0xEF, 0xBB, 0xBF}; static const unsigned char UTF16LE[] = {0x3C, 0x00, 0x3F, 0x00}; static const unsigned char UTF16BE[] = {0x00, 0x3C, 0x00, 0x3F}; static const unsigned char UTF8[] = {0x3C, 0x3F, 0x78, 0x6D}; auto startsWithBytes = [](const QByteArray& bytes, const unsigned char* prefix, qsizetype size) { return bytes.startsWith( QByteArrayView(reinterpret_cast(prefix), size)); }; file.seek(0); QByteArray rawBytes = file.read(4); QTextStream stream(&file); int bom = 0; if (startsWithBytes(rawBytes, UTF16LE_BOM, sizeof(UTF16LE_BOM))) { stream.setEncoding(QStringConverter::Encoding::Utf16LE); bom = 2; } else if (startsWithBytes(rawBytes, UTF16BE_BOM, sizeof(UTF16BE_BOM))) { stream.setEncoding(QStringConverter::Encoding::Utf16BE); bom = 2; } else if (startsWithBytes(rawBytes, UTF8_BOM, sizeof(UTF8_BOM))) { stream.setEncoding(QStringConverter::Encoding::Utf8); bom = 3; } else if (startsWithBytes(rawBytes, UTF16LE, sizeof(UTF16LE))) { stream.setEncoding(QStringConverter::Encoding::Utf16LE); } else if (startsWithBytes(rawBytes, UTF16BE, sizeof(UTF16BE))) { stream.setEncoding(QStringConverter::Encoding::Utf16BE); } else if (startsWithBytes(rawBytes, UTF8, sizeof(UTF8))) { stream.setEncoding(QStringConverter::Encoding::Utf8); } // otherwise maybe the textstream knows the encoding? stream.seek(bom); QString header = stream.readLine(); if (!header.startsWith(" encodings{ QStringConverter::Encoding::Utf16, QStringConverter::Encoding::Utf8, QStringConverter::Encoding::Latin1}; bool success = false; std::string errorMessage; try { XmlReader reader(&file); (this->*callback)(reader); success = true; } catch (const XmlParseError& e) { log::warn("The {} in this file is incorrectly encoded ({}). Applying heuristics...", file.fileName(), e.what()); } if (!success) { // nmm's xml parser is less strict than the one from qt and allows files with // wrong encoding in the header. Being strict here would be bad user experience // this works around bad headers. QByteArray headerlessData = skipXmlHeader(file); // try parsing the file with several encodings to support broken files for (auto encoding : encodings) { log::debug("Trying encoding {} for {}... ", encoding, file.fileName()); try { QStringEncoder encoder(encoding); XmlReader reader( encoder.encode(QString("") .arg(encoder.name())) + headerlessData); (this->*callback)(reader); log::debug("Interpreting {} as {}.", file.fileName(), encoding); success = true; break; } catch (const XmlParseError& e) { log::debug("Not {}: {}.", encoding, e.what()); } } if (!success) { reportError( tr("Failed to parse %1. See console for details.").arg(file.fileName())); } file.close(); } } void FomodInstallerDialog::readInfoXml() { QFile file(fomodFilePath(m_FomodPath, m_FomodDirName, "info.xml")); // We don't need a info.xml file, so we just return if we cannot open it: if (!file.open(QIODevice::ReadOnly)) { return; } readXml(file, &FomodInstallerDialog::parseInfo); } void FomodInstallerDialog::readModuleConfigXml() { QFile file(fomodFilePath(m_FomodPath, m_FomodDirName, "ModuleConfig.xml")); if (!file.open(QIODevice::ReadOnly)) { throw Exception(tr("%1 missing.").arg(file.fileName())); } readXml(file, &FomodInstallerDialog::parseModuleConfig); } void FomodInstallerDialog::initData(IOrganizer* moInfo) { m_MoInfo = moInfo; // parse provided package information readInfoXml(); QString screenshotPath = tempArchivePath(joinArchivePath( joinArchivePath(m_FomodPath, m_FomodDirName), "screenshot.png")); if (!QImage(screenshotPath).isNull()) { ui->screenshotLabel->setScalableResource(screenshotPath); ui->screenshotExpand->setVisible(false); } readModuleConfigXml(); } QString FomodInstallerDialog::getName() const { return ui->nameCombo->currentText(); } QString FomodInstallerDialog::getVersion() const { return ui->versionLabel->text(); } int FomodInstallerDialog::getModID() const { return m_ModID; } QString FomodInstallerDialog::getURL() const { return m_URL; } void FomodInstallerDialog::applyPriority(Leaves& leaves, IFileTree const* tree, int priority) { for (auto entry : *tree) { if (entry->isDir()) { applyPriority(leaves, entry->astree().get(), priority); } else { leaves.insert({entry.get(), {priority, entry->path()}}); } } } void FomodInstallerDialog::copyLeaf(std::shared_ptr sourceEntry, std::shared_ptr destinationTree, QString destinationPath, IFileTree::OverwritesType& overwrites, Leaves& leaves, int pri) { // TODO: applyPriority(leaves, sourceEntry->parent().get(), pri); if (destinationPath.isEmpty() || destinationPath.endsWith("/") || destinationPath.endsWith("\\")) { destinationPath += sourceEntry->name(); } auto oldEntry = destinationTree->find(destinationPath); if (oldEntry != nullptr) { overwrites[oldEntry] = sourceEntry; } destinationTree->copy(sourceEntry, destinationPath, IFileTree::InsertPolicy::REPLACE); } bool FomodInstallerDialog::copyFileIterator(std::shared_ptr sourceTree, std::shared_ptr destinationTree, const FileDescriptor* descriptor, Leaves& leaves, IFileTree::OverwritesType& overwrites) { QString source = (m_FomodPath.length() != 0) ? QDir(m_FomodPath).filePath(descriptor->m_Source) : descriptor->m_Source; int pri = descriptor->m_Priority; QString destination = descriptor->m_Destination; if (descriptor->m_IsFolder) { std::shared_ptr sourceNode = sourceTree->findDirectory(source); if (sourceNode == nullptr) { log::error("Folder '{}' not found.", source); return false; } // Apply the priority on the source tree: applyPriority(leaves, sourceNode.get(), pri); // addDirectory will create the directory if it does not exist: std::shared_ptr targetNode = destinationTree->addDirectory(destination); // Note (Holt59): Before, the directories were processed before the files, and the // files were processed in reverse order. The directories before files was mandatory // since both were stored differently, but I have no idea why the files were // processed in reverse order and it does not make sense since there cannot be two // identical file in a tree. Also, the files were copied but the directories were // moved, I am pretty sure this made no sense. for (auto e : *sourceNode) { targetNode->copy(e, "", IFileTree::InsertPolicy::MERGE); } } else { std::shared_ptr sourceEntry = sourceTree->find(source); if (sourceEntry == nullptr) { log::error("File '{}' not found.", source); return false; } copyLeaf(sourceEntry, destinationTree, destination, overwrites, leaves, pri); } return true; } std::pair FomodInstallerDialog::testCondition(int maxIndex, const ValueCondition* valCondition) const { return testCondition(maxIndex, valCondition->m_Name, valCondition->m_Value); } std::pair FomodInstallerDialog::testCondition(int maxIndex, const ConditionFlag* conditionFlag) const { return testCondition(maxIndex, conditionFlag->m_Name, conditionFlag->m_Value); } std::pair FomodInstallerDialog::testCondition(int maxIndex, const SubCondition* condition) const { ConditionOperator op = condition->m_Operator; for (const Condition* cond : condition->m_Conditions) { std::pair conditionMatches = cond->test(maxIndex, this); if (!conditionMatches.first) qWarning() << conditionMatches.second; if (op == OP_OR && conditionMatches.first) { return std::make_pair( true, tr("At least one condition was successful in an 'OR' clause!")); } if (op == OP_AND && !conditionMatches.first) { return conditionMatches; } } // If we get through here, everything matched (AND) or nothing matched (OR) if (op == OP_AND) return std::make_pair( true, tr("All conditions were successful in an 'AND' clause!")); else return std::make_pair( false, tr("No conditions were successful in an 'OR' clause!")); } QString FomodInstallerDialog::toString(IPluginList::PluginStates state) { if (state.testFlag(IPluginList::STATE_MISSING)) return "Missing"; if (state.testFlag(IPluginList::STATE_INACTIVE)) return "Inactive"; if (state.testFlag(IPluginList::STATE_ACTIVE)) return "Active"; throw Exception(tr("invalid plugin state %1").arg(static_cast(state))); } std::pair FomodInstallerDialog::testCondition(int, const FileCondition* condition) const { static const std::map trPluginStates = { {"Missing", tr("Missing")}, {"Inactive", tr("Inactive")}, {"Active", tr("Active")}}; QString result = toString(m_FileCheck(condition->m_File)); if (result == condition->m_State) return std::make_pair( true, tr("Success: The file '%1' was marked %2.") .arg(condition->m_File) .arg(trPluginStates.at(condition->m_State).toLower())); else return std::make_pair( false, tr("Missing requirement: The file '%1' should be %2, but was %3!") .arg(condition->m_File) .arg(trPluginStates.at(condition->m_State).toLower()) .arg(trPluginStates.at(result).toLower())); } namespace FOMOD { class Version { public: explicit Version(QString const& v); friend bool operator<=(Version const&, Version const&); private: std::array m_version; }; Version::Version(QString const& v) { std::istringstream parser(v.toStdString()); m_version.fill(0); parser >> m_version[0]; for (int idx = 1; idx < 4; idx++) { parser.get(); // Skip period parser >> m_version[idx]; } } bool operator<=(Version const& lhs, Version const& rhs) { return lhs.m_version <= rhs.m_version; } } // namespace FOMOD std::pair FomodInstallerDialog::testCondition(int, const VersionCondition* condition) const { QString version; MOBase::IPluginGame const* game = m_MoInfo->managedGame(); QString typeName; switch (condition->m_Type) { case VersionCondition::v_Game: { version = game->gameVersion(); typeName = game->gameName(); } break; case VersionCondition::v_FOMM: // We should use m_MoInfo->appVersion() but then we wouldn't be able to // install anything as MO is at 0.3.11 at the time of writing. version = "0.13.21"; typeName = "FOMM (FOMOD syntax)"; break; case VersionCondition::v_FOSE: { auto extender = m_MoInfo->gameFeatures()->gameFeature(); if (extender != nullptr) { version = extender->getExtenderVersion(); typeName = extender->BinaryName(); } else { version = "not installed"; typeName = "the script extender"; } } break; } if (FOMOD::Version(condition->m_RequiredVersion) <= FOMOD::Version(version)) return std::make_pair( true, tr("Success: The required version of %1 is %2, and was detected as %3.") .arg(typeName) .arg(condition->m_RequiredVersion) .arg(version)); else return std::make_pair(false, tr("Missing requirement: The required version " "of %1 is %2, but was detected as %3.") .arg(typeName) .arg(condition->m_RequiredVersion) .arg(version)); } bool FomodInstallerDialog::displayMissingFilesDialog( std::vector missingFiles) { QMessageBox dialog(parentWidget()); dialog.setIcon(QMessageBox::Icon::Warning); dialog.setWindowTitle(tr("Missing files or folders")); dialog.addButton(tr("Install anyway"), QMessageBox::AcceptRole); dialog.addButton(tr("Cancel"), QMessageBox::RejectRole); QString text = tr("The following files or folders were not found in the archive. " "This is likely due to an incorrect FOMOD installer. " "This mod may not work properly."); text.append("\n\n"); for (auto* fileDescriptor : missingFiles) { QString temp = fileDescriptor->m_IsFolder ? tr("Folder '%1'.") : tr("File '%1'."); text.append("- " + temp.arg(fileDescriptor->m_Source) + "\n"); } dialog.setTextFormat(Qt::MarkdownText); dialog.setText(text); return dialog.exec() == QMessageBox::AcceptRole; } IPluginInstaller::EInstallResult FomodInstallerDialog::updateTree(std::shared_ptr& tree) { FileDescriptorList descriptorList; // enable all required files for (FileDescriptor* file : m_RequiredFiles) { descriptorList.push_back(file); } // enable all conditional file installs (files programatically selected by conditions // instead of a user selection. usually dependencies) for (ConditionalInstall& cond : m_ConditionalInstalls) { SubCondition* condition = &cond.m_Condition; std::pair result = condition->test(ui->stepsStack->count(), this); if (result.first) { for (FileDescriptor* file : cond.m_Files) { descriptorList.push_back(file); } } } // enable all user-enabled choices for (int i = 0; i < ui->stepsStack->count(); ++i) { if (testVisible(i)) { QList choices = ui->stepsStack->widget(i)->findChildren("choice"); for (QAbstractButton* choice : choices) { if (choice->isChecked()) { QVariantList fileList = choice->property("files").toList(); for (QVariant fileVariant : fileList) { descriptorList.push_back(fileVariant.value()); } } } } } std::stable_sort(descriptorList.begin(), descriptorList.end(), byPriority); IFileTree::OverwritesType overwrites; Leaves leaves; std::shared_ptr newTree = tree->createOrphanTree(); std::vector failures; const QStringList ignoreMissingFolder = m_MoInfo ->persistent(m_Installer->name(), "ignored_missing_files", QStringList{"no folder"}) .toStringList(); for (const FileDescriptor* file : descriptorList) { if (!copyFileIterator(tree, newTree, file, leaves, overwrites)) { if (!ignoreMissingFolder.contains(file->m_Source, FileNameComparator::CaseSensitivity)) { failures.push_back(file); } } } if (!failures.empty()) { if (!displayMissingFilesDialog(failures)) { return IPluginInstaller::RESULT_CANCELED; } } for (auto overwrite : overwrites) { if (leaves[overwrite.first.get()].priority == leaves[overwrite.second.get()].priority) { qWarning() << "Overriding " << leaves[overwrite.first.get()].path << " with " << leaves[overwrite.second.get()].path << " which has the same priority"; } } // Update the tree: tree = newTree; return IPluginInstaller::RESULT_SUCCESS; } void FomodInstallerDialog::highlightControl(QAbstractButton* button) { QVariant screenshotName = button->property("screenshot"); if (screenshotName.isValid()) { QString screenshotFileName = screenshotName.toString(); if (!screenshotFileName.isEmpty()) { QString temp = tempArchivePath(joinArchivePath(m_FomodPath, screenshotFileName)); ui->screenshotLabel->setScalableResource(temp); ui->screenshotExpand->setVisible(true); } else { ui->screenshotLabel->setScalableResource(QString()); ui->screenshotExpand->setVisible(false); } } ui->descriptionText->setText(button->property("description").toString()); } bool FomodInstallerDialog::eventFilter(QObject* object, QEvent* event) { QAbstractButton* button = qobject_cast(object); if ((button != nullptr) && (event->type() == QEvent::HoverEnter)) { highlightControl(button); } return QDialog::eventFilter(object, event); } QString FomodInstallerDialog::readContent(QXmlStreamReader& reader) { if (reader.readNext() == XmlReader::Characters) { return reader.text().toString(); } else { return QString(); } } void FomodInstallerDialog::parseInfo(XmlReader& reader) { while (!reader.atEnd()) { switch (reader.readNext()) { case QXmlStreamReader::StartElement: { if (reader.name().toString() == "Name") { m_ModName.update(readContent(reader), GUESS_META); updateNameEdit(); } else if (reader.name().toString() == "Author") { ui->authorLabel->setText(readContent(reader)); } else if (reader.name().toString() == "Version") { ui->versionLabel->setText(readContent(reader)); } else if (reader.name().toString() == "Id") { m_ModID = readContent(reader).toInt(); } else if (reader.name().toString() == "Website") { m_URL = readContent(reader); ui->websiteLabel->setText(tr("Link").arg(m_URL)); ui->websiteLabel->setToolTip(m_URL); } } break; default: { } break; } } if (reader.hasError()) { throw XmlParseError( QString("%1 in line %2").arg(reader.errorString()).arg(reader.lineNumber())); } } FomodInstallerDialog::ItemOrder FomodInstallerDialog::getItemOrder(const QString& orderString) { if (orderString == "Ascending") { return ORDER_ASCENDING; } else if (orderString == "Descending") { return ORDER_DESCENDING; } else if (orderString == "Explicit") { return ORDER_EXPLICIT; } else { throw Exception(tr("unsupported order type %1").arg(orderString)); } } FomodInstallerDialog::GroupType FomodInstallerDialog::getGroupType(const QString& typeString) { if (typeString == "SelectAtLeastOne") { return TYPE_SELECTATLEASTONE; } else if (typeString == "SelectAtMostOne") { return TYPE_SELECTATMOSTONE; } else if (typeString == "SelectExactlyOne") { return TYPE_SELECTEXACTLYONE; } else if (typeString == "SelectAny") { return TYPE_SELECTANY; } else if (typeString == "SelectAll") { return TYPE_SELECTALL; } else { throw Exception(tr("unsupported group type %1").arg(typeString)); } } FomodInstallerDialog::PluginType FomodInstallerDialog::getPluginType(const QString& typeString) { if (typeString == "Required") { return FomodInstallerDialog::TYPE_REQUIRED; } else if (typeString == "Optional") { return FomodInstallerDialog::TYPE_OPTIONAL; } else if (typeString == "Recommended") { return FomodInstallerDialog::TYPE_RECOMMENDED; } else if (typeString == "NotUsable") { return FomodInstallerDialog::TYPE_NOTUSABLE; } else if (typeString == "CouldBeUsable") { return FomodInstallerDialog::TYPE_COULDBEUSABLE; } else { qCritical("invalid plugin type %s", qUtf8Printable(typeString)); return FomodInstallerDialog::TYPE_OPTIONAL; } } void FomodInstallerDialog::readFileList(XmlReader& reader, FileDescriptorList& fileList) { QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "folder" || reader.name().toString() == "file") { QXmlStreamAttributes attributes = reader.attributes(); // This is a horrendous hack. It doesn't make sense to specify an empty source // folder name, as it would require you to copy everything including the fomod // directory. However, people have been known to write entries like in order to achieve an option that does nothing. Are // groups and buttons that hard? An empty source file is very probably a serious // error but given people do the above, I'm assuming that they probably assume // will work the same, so I'm not // differentiating. Similarly, I'm not checking for the destination if the source // is blank. Why'd you want to copy the fomod directory on an install? if (attributes.value("source").isEmpty()) { log::debug("Ignoring {} entry with empty source.", reader.name().toString()); } else { FileDescriptor* file = new FileDescriptor(this); file->m_Source = normalizeArchivePath(attributes.value("source").toString()); file->m_Destination = attributes.hasAttribute("destination") ? normalizeArchivePath(attributes.value("destination").toString()) : file->m_Source; file->m_Priority = attributes.hasAttribute("priority") ? attributes.value("priority").toString().toInt() : 0; file->m_FileSystemItemSequence = ++m_FileSystemItemSequence; file->m_IsFolder = reader.name().toString() == "folder"; file->m_InstallIfUsable = attributes.hasAttribute("installIfUsable") ? (attributes.value("installIfUsable").compare((QString) "true") == 0) : false; file->m_AlwaysInstall = attributes.hasAttribute("alwaysInstall") ? (attributes.value("alwaysInstall").compare((QString) "true") == 0) : false; fileList.push_back(file); } reader.finishedElement(); } else { reader.unexpected(); } } } void FomodInstallerDialog::readDependencyPattern(XmlReader& reader, DependencyPattern& pattern) { // sequence // dependency // type QString self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "dependencies") { readCompositeDependency(reader, pattern.condition); } else if (reader.name().toString() == "type") { pattern.type = getPluginType(reader.attributes().value("name").toString()); reader.finishedElement(); } else { reader.unexpected(); } } } void FomodInstallerDialog::readDependencyPatternList(XmlReader& reader, DependencyPatternList& patterns) { QString self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "pattern") { DependencyPattern pattern; readDependencyPattern(reader, pattern); patterns.push_back(pattern); } else { reader.unexpected(); } } } void FomodInstallerDialog::readDependencyPluginType(XmlReader& reader, PluginTypeInfo& info) { // sequence // defaultType // patterns QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "defaultType") { info.m_DefaultType = getPluginType(reader.attributes().value("name").toString()); reader.finishedElement(); } else if (reader.name().toString() == "patterns") { readDependencyPatternList(reader, info.m_DependencyPatterns); } else { reader.unexpected(); } } } void FomodInstallerDialog::readPluginType(XmlReader& reader, Plugin& plugin) { // Have a choice here of precisely one of 'type' or 'dependencytype', so this is // not strictly necessary plugin.m_PluginTypeInfo.m_DefaultType = TYPE_OPTIONAL; QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "type") { plugin.m_PluginTypeInfo.m_DefaultType = getPluginType(reader.attributes().value("name").toString()); reader.finishedElement(); } else if (reader.name().toString() == "dependencyType") { readDependencyPluginType(reader, plugin.m_PluginTypeInfo); } else { reader.unexpected(); } } } void FomodInstallerDialog::readConditionFlagList(XmlReader& reader, ConditionFlagList& condflags) { QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "flag") { QString name = reader.attributes().value("name").toString(); QString content = reader.getText().trimmed(); condflags.push_back(ConditionFlag(name, content)); } else { reader.unexpected(); } } } bool FomodInstallerDialog::byPriority(const FileDescriptor* LHS, const FileDescriptor* RHS) { return LHS->m_Priority == RHS->m_Priority ? LHS->m_FileSystemItemSequence < RHS->m_FileSystemItemSequence : LHS->m_Priority < RHS->m_Priority; } FomodInstallerDialog::Plugin FomodInstallerDialog::readPlugin(XmlReader& reader) { Plugin result; result.m_Name = reader.attributes().value("name").toString(); QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "description") { result.m_Description = reader.getText().trimmed(); } else if (reader.name().toString() == "image") { result.m_ImagePath = reader.attributes().value("path").toString(); reader.finishedElement(); } else if (reader.name().toString() == "files") { readFileList(reader, result.m_Files); } else if (reader.name().toString() == "conditionFlags") { readConditionFlagList(reader, result.m_ConditionFlags); } else if (reader.name().toString() == "typeDescriptor") { readPluginType(reader, result); } else { reader.unexpected(); } } // I (TRT) am not quite sure why this sort is done here. It is done again // when the files have been selected before installing them, which seems // a more appropriate place. std::sort(result.m_Files.begin(), result.m_Files.end(), byPriority); return result; } FomodInstallerDialog::PluginType FomodInstallerDialog::getPluginDependencyType(int page, const PluginTypeInfo& info) const { if (info.m_DependencyPatterns.size() != 0) { for (const DependencyPattern& pattern : info.m_DependencyPatterns) { if (testCondition(page, &pattern.condition).first) { return pattern.type; } } } return info.m_DefaultType; } void FomodInstallerDialog::readPluginList(XmlReader& reader, QString const& groupName, GroupType& groupType, QLayout* layout) { ItemOrder pluginOrder = reader.attributes().hasAttribute("order") ? getItemOrder(reader.attributes().value("order").toString()) : ORDER_ASCENDING; // Read in all the plugins so we can check if the author is using "atmost" or // "exactly", and correct as appropriate std::vector plugins; QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "plugin") { plugins.push_back(readPlugin(reader)); } else { reader.unexpected(); } } std::vector controls; // This is somewhat of a hack. If the author has specified only 1 plugin and the // group type is SELECTATLEASTONE or SELECTEXACTLYONE, then that plugin has to // be selected. A note: This doesn't check for if somebody has defined a single // plugin group with one of the above types, and then made the plugin unselectable. // They deserve what they get. // Similarly, if they've specfied SELECTATMOSTONE, we might as well give them // a checkbox if (plugins.size() == 1) { switch (groupType) { case TYPE_SELECTATLEASTONE: { qWarning() << "Plugin " << plugins[0].m_Name << " is the only plugin specified in group " << groupName << " which requires selection of at least one plugin"; groupType = TYPE_SELECTALL; } break; case TYPE_SELECTEXACTLYONE: { qWarning() << "Plugin " << plugins[0].m_Name << " is the only plugin specified in group " << groupName << " which requires selection of exactly one plugin"; groupType = TYPE_SELECTALL; } break; case TYPE_SELECTATMOSTONE: { qWarning() << "Plugin " << plugins[0].m_Name << " is the only plugin specified in group " << groupName << " which permits selection of at most one plugin"; groupType = TYPE_SELECTANY; } break; } } for (Plugin const& plugin : plugins) { QAbstractButton* newControl = nullptr; switch (groupType) { case TYPE_SELECTATLEASTONE: case TYPE_SELECTANY: { newControl = new QCheckBox(plugin.m_Name); } break; case TYPE_SELECTATMOSTONE: case TYPE_SELECTEXACTLYONE: { newControl = new QRadioButton(plugin.m_Name); } break; case TYPE_SELECTALL: { newControl = new QCheckBox(plugin.m_Name); newControl->setChecked(true); newControl->setEnabled(false); newControl->setToolTip(tr("All components in this group are required")); } break; } newControl->setObjectName("choice"); newControl->setAttribute(Qt::WA_Hover); QVariant type(QVariant::fromValue(plugin.m_PluginTypeInfo)); newControl->setProperty("plugintypeinfo", type); newControl->setProperty("screenshot", plugin.m_ImagePath); newControl->setProperty("description", plugin.m_Description); QVariantList fileList; // This looks horrible... for (FileDescriptor* const& descriptor : plugin.m_Files) { fileList.append(QVariant::fromValue(descriptor)); } newControl->setProperty("files", fileList); QVariantList conditionFlags; for (ConditionFlag const& conditionFlag : plugin.m_ConditionFlags) { if (!conditionFlag.m_Name.isEmpty()) { conditionFlags.append(QVariant::fromValue(conditionFlag)); } } newControl->setProperty("conditionFlags", conditionFlags); newControl->installEventFilter(this); // We need somehow to check the 'toggled' signal. how do I do that // void QAbstractButton::clicked ( bool checked ) [signal] connect(newControl, SIGNAL(clicked()), this, SLOT(widgetButtonClicked())); controls.push_back(newControl); } if (pluginOrder == ORDER_ASCENDING) { std::sort(controls.begin(), controls.end(), ControlsAscending); } else if (pluginOrder == ORDER_DESCENDING) { std::sort(controls.begin(), controls.end(), ControlsDescending); } for (QAbstractButton* const control : controls) { layout->addWidget(control); } if (groupType == TYPE_SELECTATMOSTONE) { QRadioButton* newButton = new QRadioButton(tr("None")); newButton->setObjectName("none"); layout->addWidget(newButton); } } void FomodInstallerDialog::readGroup(XmlReader& reader, QLayout* layout) { QString name = reader.attributes().value("name").toString(); GroupType type = getGroupType(reader.attributes().value("type").toString()); QGroupBox* groupBox = new QGroupBox(name); QVBoxLayout* groupLayout = new QVBoxLayout; QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "plugins") { readPluginList(reader, name, type, groupLayout); } else { reader.unexpected(); } } groupLayout->setProperty("groupType", QVariant::fromValue(type)); groupLayout->setObjectName("grouplayout"); groupBox->setLayout(groupLayout); if (type == TYPE_SELECTATLEASTONE) { QLabel* label = new QLabel(tr("Select one or more of these options:")); layout->addWidget(label); } layout->addWidget(groupBox); } void FomodInstallerDialog::readGroupList(XmlReader& reader, QLayout* layout) { QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "group") { readGroup(reader, layout); } else { reader.unexpected(); } } } QGroupBox* FomodInstallerDialog::readInstallStep(XmlReader& reader) { QString name = reader.attributes().value("name").toString(); QGroupBox* page = new QGroupBox(name); QVBoxLayout* pageLayout = new QVBoxLayout; QScrollArea* scrollArea = new QScrollArea; QFrame* scrolledArea = new QFrame; QVBoxLayout* scrollLayout = new QVBoxLayout; SubCondition subcondition; // sequence: // visible (optional) // optionalFileGroups QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "visible") { readCompositeDependency(reader, subcondition); } else if (reader.name().toString() == "optionalFileGroups") { readGroupList(reader, scrollLayout); } else { reader.unexpected(); } } if (subcondition.m_Conditions.size() != 0) { // FIXME Is this actually OK? I'm storing a pointer in the property? // Also AFAICS this is subject to memory leaks page->setProperty("conditional", QVariant::fromValue(subcondition)); } scrolledArea->setLayout(scrollLayout); scrollArea->setWidget(scrolledArea); scrollArea->setWidgetResizable(true); pageLayout->addWidget(scrollArea); page->setLayout(pageLayout); return page; } void FomodInstallerDialog::readStepList(XmlReader& reader) { ItemOrder stepOrder = reader.attributes().hasAttribute("order") ? getItemOrder(reader.attributes().value("order").toString()) : ORDER_ASCENDING; std::vector pages; // sequence installStep (1 or more) QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "installStep") { pages.push_back(readInstallStep(reader)); } else { reader.unexpected(); } } if (stepOrder == ORDER_ASCENDING) { std::sort(pages.begin(), pages.end(), PagesAscending); } else if (stepOrder == ORDER_DESCENDING) { std::sort(pages.begin(), pages.end(), PagesDescending); } for (std::vector::const_iterator iter = pages.begin(); iter != pages.end(); ++iter) { ui->stepsStack->addWidget(*iter); } } void FomodInstallerDialog::readCompositeDependency(XmlReader& reader, SubCondition& conditional) { conditional.m_Operator = OP_AND; if (reader.attributes().hasAttribute("operator")) { auto opString = reader.attributes().value("operator").toString(); if (opString == "Or") { conditional.m_Operator = OP_OR; } else if (opString != "And") { qWarning() << "Expected 'and' or 'or' at line " << reader.lineNumber() << ", got " << opString; } // OP_AND is the default, set at the beginning of the function } QString const self = reader.name().toString(); while (reader.getNextElement(self)) { auto elString = reader.name().toString(); if (elString == "fileDependency") { conditional.m_Conditions.push_back( new FileCondition(reader.attributes().value("file").toString(), reader.attributes().value("state").toString())); reader.finishedElement(); } else if (elString == "flagDependency") { conditional.m_Conditions.push_back( new ValueCondition(reader.attributes().value("flag").toString(), reader.attributes().value("value").toString())); reader.finishedElement(); } else if (elString == "gameDependency") { conditional.m_Conditions.push_back(new VersionCondition( VersionCondition::v_Game, reader.attributes().value("version").toString())); reader.finishedElement(); } else if (elString == "fommDependency") { conditional.m_Conditions.push_back(new VersionCondition( VersionCondition::v_FOMM, reader.attributes().value("version").toString())); reader.finishedElement(); } else if (elString == "foseDependency") { conditional.m_Conditions.push_back(new VersionCondition( VersionCondition::v_FOSE, reader.attributes().value("version").toString())); reader.finishedElement(); } else if (elString == "dependencies") { SubCondition* nested = new SubCondition(); readCompositeDependency(reader, *nested); conditional.m_Conditions.push_back(nested); } else { reader.unexpected(); } } if (conditional.m_Conditions.size() == 0) { qWarning() << "Empty conditional found at line " << reader.lineNumber(); } } FomodInstallerDialog::ConditionalInstall FomodInstallerDialog::readConditionalInstallPattern(XmlReader& reader) { ConditionalInstall result; result.m_Condition.m_Operator = OP_AND; QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "dependencies") { readCompositeDependency(reader, result.m_Condition); } else if (reader.name().toString() == "files") { readFileList(reader, result.m_Files); } else { reader.unexpected(); } } return result; } void FomodInstallerDialog::readConditionalFilePatternList(XmlReader& reader) { QString const self(reader.name().toString()); while (reader.getNextElement(self)) { if (reader.name().toString() == "pattern") { m_ConditionalInstalls.push_back(readConditionalInstallPattern(reader)); } else { reader.unexpected(); } } } void FomodInstallerDialog::readConditionalFileInstallList(XmlReader& reader) { QString const self(reader.name().toString()); // Technically there should be only one but it's easier to write like this while (reader.getNextElement(self)) { if (reader.name().toString() == "patterns") { readConditionalFilePatternList(reader); } else { reader.unexpected(); } } } void FomodInstallerDialog::readModuleConfiguration(XmlReader& reader) { // sequence: // modulename // optional - moduleImage // optional - moduleDependencies // optional - requiredInstallFiles // optional - installSteps // optional - conditionalFileInstalls QString const self(reader.name().toString()); while (reader.getNextElement(self)) { auto elString = reader.name().toString(); if (elString == "moduleName") { QString title = reader.getText(); qDebug() << "module name : " << title; } else if (elString == "moduleImage") { // do something useful with the attributes of this reader.finishedElement(); } else if (elString == "moduleDependencies") { SubCondition condition; readCompositeDependency(reader, condition); std::pair result = testCondition(-1, &condition); if (!result.first) { // TODO Better messages? throw Exception(result.second); } } else if (elString == "requiredInstallFiles") { readFileList(reader, m_RequiredFiles); } else if (elString == "installSteps") { readStepList(reader); } else if (elString == "conditionalFileInstalls") { readConditionalFileInstallList(reader); } else { reader.unexpected(); } } } void FomodInstallerDialog::parseModuleConfig(XmlReader& reader) { if (reader.readNext() != XmlReader::StartDocument) { throw XmlParseError( QString("Expected document start at line %1").arg(reader.lineNumber())); } processXmlTag(reader, "config", &FomodInstallerDialog::readModuleConfiguration); if (reader.readNext() != XmlReader::EndDocument) { throw XmlParseError( QString("Expected document end at line %1").arg(reader.lineNumber())); } if (reader.hasError()) { throw XmlParseError( QString("%1 in line %2").arg(reader.errorString()).arg(reader.lineNumber())); } // Find the first visible page int index = 0; while (index < ui->stepsStack->count()) { if (testVisible(index)) { ui->stepsStack->setCurrentIndex(index); displayCurrentPage(); activateCurrentPage(); break; } ++index; } // No pages are visible? Go to a small install if (index >= ui->stepsStack->count()) { transformToSmallInstall(); } } void FomodInstallerDialog::processXmlTag(XmlReader& reader, char const* tag, TagProcessor func) { if (reader.readNext() == XmlReader::StartElement && reader.name().toString() == tag) { (this->*func)(reader); } else if (!reader.hasError()) { reader.raiseError( QString("Expected %1, got %2").arg(tag).arg(reader.name().toString())); } } void FomodInstallerDialog::on_manualBtn_clicked() { m_Manual = true; this->reject(); } void FomodInstallerDialog::on_cancelBtn_clicked() { this->reject(); } void FomodInstallerDialog::on_websiteLabel_linkActivated(const QString& link) { #ifdef _WIN32 ::ShellExecuteW(nullptr, L"open", ToWString(link).c_str(), nullptr, nullptr, SW_SHOWNORMAL); #else QDesktopServices::openUrl(QUrl(link)); #endif } void FomodInstallerDialog::activateCurrentPage() { QList choices = ui->stepsStack->currentWidget()->findChildren("choice"); if (choices.count() > 0) { highlightControl(choices.at(0)); } m_PageVisible.push_back(true); updateNextbtnText(); } std::pair FomodInstallerDialog::testCondition(int maxIndex, const QString& flag, const QString& value) const { // FIXME Review this and see if we can store the visible and evaluated variables for // each page and cache like that. This would make me happier (if no one else) about // the results of doing 'previous' and changing a flag to 'unset'. // iterate through all enabled condition flags on all activated controls on // all visible pages if one of them matches the condition, taking the most // recent setting. for (int i = maxIndex - 1; i >= 0; --i) { if (testVisible(i)) { QWidget* page = ui->stepsStack->widget(i); QList choices = page->findChildren("choice"); for (QAbstractButton const* choice : choices) { if (choice->isChecked()) { QVariant temp = choice->property("conditionFlags"); if (temp.isValid()) { QVariantList conditionFlags = temp.toList(); for (QVariant const& variant : conditionFlags) { ConditionFlag condition = variant.value(); if (condition.m_Name == flag) { if (condition.m_Value == value) return std::make_pair(true, tr("The flag '%1' matched '%2'") .arg(condition.m_Name) .arg(condition.m_Value)); else return std::make_pair(false, tr("The flag '%1' did not match '%2'") .arg(condition.m_Name) .arg(condition.m_Value)); } } } } } } } if (value.isEmpty()) return std::make_pair(true, tr("The condition was not matched and is empty.")); return std::make_pair(false, tr("The value exists but was not matched.")); } bool FomodInstallerDialog::testVisible(int pageIndex) const { if (pageIndex < static_cast(m_PageVisible.size())) { return m_PageVisible[pageIndex]; } if (pageIndex >= ui->stepsStack->count()) { return false; } QWidget* page = ui->stepsStack->widget(pageIndex); QVariant subcond = page->property("conditional"); if (subcond.isValid()) { SubCondition subc = subcond.value(); return testCondition(pageIndex, &subc).first; } return true; } bool FomodInstallerDialog::nextPage() { int oldIndex = ui->stepsStack->currentIndex(); int index = oldIndex + 1; // find the next "visible" install step while (index < ui->stepsStack->count()) { if (testVisible(index)) { ui->stepsStack->setCurrentIndex(index); ui->stepsStack->currentWidget()->setProperty("previous", oldIndex); return true; } m_PageVisible.push_back(false); ++index; } // no more visible pages -> install qWarning("Got to install after pressing next!"); return false; } void FomodInstallerDialog::widgetButtonClicked() { // A button has been clicked. At the moment we do nothing with this // beyond checking the next button state updateNextbtnText(); } void FomodInstallerDialog::updateNextbtnText() { // First we see if we can actually allow the 'next' button. Specifically, this // is a test to ensure that you have selected at least one item in a //'select at least one' box. int const page = ui->stepsStack->currentIndex(); QStringList groups_requiring_selection; for (QVBoxLayout const* const layout : ui->stepsStack->widget(page)->findChildren("grouplayout")) { GroupType const groupType(layout->property("groupType").value()); if (groupType == TYPE_SELECTATLEASTONE) { // Check at least one of this group is ticked bool checked = false; for (int i = 0; i != layout->count(); ++i) { if (QLayoutItem* item = layout->itemAt(i)) { QAbstractButton* const choice = dynamic_cast(item->widget()); if (choice != nullptr) { if (choice->objectName() == "choice" && choice->isChecked()) { checked = true; break; } } } } if (!checked) { QString group = dynamic_cast(layout->parentWidget())->title(); qDebug() << "Group " << group << " needs a selection"; groups_requiring_selection.append(group); } } } if (groups_requiring_selection.size() != 0) { ui->nextBtn->setText(tr("Disabled")); ui->nextBtn->setEnabled(false); ui->nextBtn->setToolTip(tr("This button is disabled because the following group(s) " "need a selection: ") + groups_requiring_selection.join(", ")); return; } // OK, clear up any warnings ui->nextBtn->setToolTip(""); // Display 'next' or 'install' as appropriate for the next button. // note this can change depending on what buttons you click here. auto old_PageVisible = m_PageVisible; ON_BLOCK_EXIT([&]() { m_PageVisible = old_PageVisible; }); bool isLast = true; for (int index = page + 1; index != ui->stepsStack->count(); ++index) { if (testVisible(index)) { isLast = false; break; } m_PageVisible.push_back(false); } ui->nextBtn->setEnabled(true); ui->nextBtn->setText(isLast ? tr("Install") : tr("Next")); } void FomodInstallerDialog::displayCurrentPage() { // Iterate over all buttons and set the tool tips as appropriate int const page = ui->stepsStack->currentIndex(); for (QVBoxLayout* layout : ui->stepsStack->widget(page)->findChildren("grouplayout")) { // Create a list of buttons, as in order to attempt to keep users existing choices // intact, we may need to cycle over this twice QList controls; QAbstractButton* none_button(nullptr); for (int i = 0; i != layout->count(); ++i) { if (QLayoutItem* const item = layout->itemAt(i)) { QAbstractButton* const choice = dynamic_cast(item->widget()); if (choice != nullptr) { if (choice->objectName() == "choice") { controls.push_back(choice); } else if (choice->objectName() == "none") { none_button = choice; } } } } // FIXME If we are displaying this for the 2nd time, we should do two passes, // as currently if you have decided against a recommended option, gone back, // and then gone forward, your selection will be lost. // For tick boxes it requires a bit of thought, because the first time we come // in here, all tick boxes are clear, which is a valid condition. For radio // buttons, that's not a valid condition so we can override. But we should // possibly override anyway if the plugin types have changed since last time. GroupType groupType(layout->property("groupType").value()); if (groupType != TYPE_SELECTALL) { bool const mustSelectOne = groupType == TYPE_SELECTEXACTLYONE || groupType == TYPE_SELECTATLEASTONE; bool maySelectMore = true; QAbstractButton* first_optional = nullptr; QAbstractButton* first_couldbe = nullptr; for (QAbstractButton* const control : controls) { PluginTypeInfo const info = control->property("plugintypeinfo").value(); PluginType const type = getPluginDependencyType(page, info); control->setEnabled(true); switch (type) { case TYPE_REQUIRED: { if ((groupType == TYPE_SELECTEXACTLYONE) || (groupType == TYPE_SELECTATMOSTONE)) { // This only makes sense if the option may be disabled through // conditions, so that if the conditions are met, this option is // forced, otherwise the user can pick. // This means that in this case the option is forced, and no user // selection should be possible for (QAbstractButton* groupControl : controls) { groupControl->setEnabled(false); } } else { control->setEnabled(false); } control->setChecked(true); control->setToolTip(tr("This component is required")); } break; case TYPE_RECOMMENDED: { if (maySelectMore || !mustSelectOne) { control->setChecked(true); } control->setToolTip(tr("It is recommended you enable this component")); } break; case TYPE_OPTIONAL: { if (first_optional == nullptr) { first_optional = control; } control->setToolTip(tr("Optional component")); } break; case TYPE_NOTUSABLE: { control->setChecked(false); control->setEnabled(false); control->setToolTip(tr("This component is not usable in combination with " "other installed plugins")); } break; case TYPE_COULDBEUSABLE: { if (first_couldbe == nullptr) { first_couldbe = control; } control->setCheckable(true); control->setIcon(QIcon(":/new/guiresources/warning_16")); control->setToolTip(tr("You may be experiencing instability in combination " "with other installed plugins")); } break; } if (control->isChecked()) { maySelectMore = false; } } if (maySelectMore) { if (none_button != nullptr) { none_button->setChecked(true); } else if (mustSelectOne) { if (first_optional != nullptr) { first_optional->setChecked(true); } else if (first_couldbe != nullptr) { qWarning("User should select at least one plugin but the only ones " "available could cause instability"); first_couldbe->setChecked(true); } else { // FIXME Should this generate an error qWarning("User should select at least one plugin but none are available"); controls[0]->setChecked(true); } } } } } } void FomodInstallerDialog::on_nextBtn_clicked() { if (ui->stepsStack->currentIndex() == ui->stepsStack->count() - 1) { this->accept(); } else { if (nextPage()) { ui->prevBtn->setEnabled(true); displayCurrentPage(); activateCurrentPage(); } else { this->accept(); } } } void FomodInstallerDialog::on_prevBtn_clicked() { // FIXME this will go wrong if the first page isn't visible if (ui->stepsStack->currentIndex() != 0) { int previousIndex = 0; QVariant temp = ui->stepsStack->currentWidget()->property("previous"); if (temp.isValid()) { previousIndex = temp.toInt(); } else { previousIndex = ui->stepsStack->currentIndex() - 1; } ui->stepsStack->setCurrentIndex(previousIndex); m_PageVisible.resize(previousIndex); ui->nextBtn->setText(tr("Next")); } if (ui->stepsStack->currentIndex() == 0) { ui->prevBtn->setEnabled(false); } activateCurrentPage(); } void FomodInstallerDialog::on_screenshotExpand_clicked() { std::vector> carouselImages; int carouselIndex = -1; for (auto choice : ui->stepsStack->currentWidget()->findChildren("choice")) { QString screenshotFileName = choice->property("screenshot").toString(); // If a choice has no screenshot, it should not be displayed in the screenshot // dialog nor marked as the active carouselIndex if (screenshotFileName.isEmpty()) { continue; } QString temp = tempArchivePath(joinArchivePath(m_FomodPath, screenshotFileName)); carouselImages.push_back(std::pair(choice->text(), temp)); // Focus the screenshot carousel on the user's selected choice (or the first if // there are multiple) if (carouselIndex == -1 && choice->isChecked()) { carouselIndex = ((int)carouselImages.size()) - 1; } } // Focus the screenshot carousel on the first screenshot if the user has not selected // a choice with a screenshot (or any choice at all) carouselIndex = (carouselIndex < 0) ? 0 : carouselIndex; QDialog* dialog = new FomodScreenshotDialog(this, carouselImages, carouselIndex); dialog->show(); }