diff options
Diffstat (limited to 'libs/installer_fomod/src/fomodinstallerdialog.cpp')
| -rw-r--r-- | libs/installer_fomod/src/fomodinstallerdialog.cpp | 1701 |
1 files changed, 1701 insertions, 0 deletions
diff --git a/libs/installer_fomod/src/fomodinstallerdialog.cpp b/libs/installer_fomod/src/fomodinstallerdialog.cpp new file mode 100644 index 0000000..ef6c44f --- /dev/null +++ b/libs/installer_fomod/src/fomodinstallerdialog.cpp @@ -0,0 +1,1701 @@ +/* +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 <http://www.gnu.org/licenses/>. +*/ + +#include "fomodinstallerdialog.h" +#include "ui_fomodinstallerdialog.h" + +#include <array> +#include <sstream> +#include <utility> +#include <vector> + +#include <QCheckBox> +#include <QCompleter> +#include <QDebug> +#include <QDir> +#include <QFile> +#include <QImage> +#include <QDesktopServices> +#include <QRadioButton> +#include <QScrollArea> +#include <QUrl> +#include <QStringEncoder> + +#ifdef _WIN32 +#include <Shellapi.h> +#endif + +#include <uibase/game_features/igamefeatures.h> +#include <uibase/game_features/scriptextender.h> +#include <uibase/imoinfo.h> +#include <uibase/iplugingame.h> +#include <uibase/log.h> +#include <uibase/report.h> +#include <uibase/scopeguard.h> +#include <uibase/utility.h> + +#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<QString>& modName, + const QString& fomodPath, + const std::function<MOBase::IPluginList::PluginStates(const QString&)>& fileCheck, + QWidget* parent) + : QDialog(parent), ui(new Ui::FomodInstallerDialog), m_Installer(installer), + m_ModName(modName), m_ModID(-1), m_FomodPath(fomodPath), 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}; + + if (buffer.startsWith(reinterpret_cast<const char*>(BOM_UTF8))) + return 3; + if (buffer.startsWith(reinterpret_cast<const char*>(BOM_UTF16BE)) || + buffer.startsWith(reinterpret_cast<const char*>(BOM_UTF16LE))) + return 2; + + return 0; +} + +struct XmlParseError : std::runtime_error +{ + XmlParseError(const QString& message) : std::runtime_error(qUtf8Printable(message)) {} +}; + +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}; + + file.seek(0); + QByteArray rawBytes = file.read(4); + QTextStream stream(&file); + int bom = 0; + if (rawBytes.startsWith((const char*)UTF16LE_BOM)) { + stream.setEncoding(QStringConverter::Encoding::Utf16LE); + bom = 2; + } else if (rawBytes.startsWith((const char*)UTF16BE_BOM)) { + stream.setEncoding(QStringConverter::Encoding::Utf16BE); + bom = 2; + } else if (rawBytes.startsWith((const char*)UTF8_BOM)) { + stream.setEncoding(QStringConverter::Encoding::Utf8); + bom = 3; + } else if (rawBytes.startsWith(QByteArray((const char*)UTF16LE, 4))) { + stream.setEncoding(QStringConverter::Encoding::Utf16LE); + } else if (rawBytes.startsWith(QByteArray((const char*)UTF16BE, 4))) { + stream.setEncoding(QStringConverter::Encoding::Utf16BE); + } else if (rawBytes.startsWith(QByteArray((const char*)UTF8, 4))) { + stream.setEncoding(QStringConverter::Encoding::Utf8); + } // otherwise maybe the textstream knows the encoding? + + stream.seek(bom); + QString header = stream.readLine(); + if (!header.startsWith("<?")) { + // it was all for nothing, there is no header here... + stream.seek(bom); + } + // this seems to be necessary due to buffering in QTextStream + file.seek(stream.pos()); + return file.readAll(); +} + +void FomodInstallerDialog::readXml(QFile& file, + void (FomodInstallerDialog::*callback)(XmlReader&)) +{ + // List of encodings to try: + static const std::vector<QStringConverter::Encoding> 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("<?xml version=\"1.0\" encoding=\"%1\" ?>") + .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(QDir::tempPath() + "/" + m_FomodPath + "/fomod/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(QDir::tempPath() + "/" + m_FomodPath + "/fomod/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 = + QDir::tempPath() + "/" + m_FomodPath + "/fomod/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<FileTreeEntry> sourceEntry, + std::shared_ptr<IFileTree> 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<IFileTree> sourceTree, + std::shared_ptr<IFileTree> 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<IFileTree> 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<IFileTree> 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<FileTreeEntry> 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<bool, QString> +FomodInstallerDialog::testCondition(int maxIndex, + const ValueCondition* valCondition) const +{ + return testCondition(maxIndex, valCondition->m_Name, valCondition->m_Value); +} + +std::pair<bool, QString> +FomodInstallerDialog::testCondition(int maxIndex, + const ConditionFlag* conditionFlag) const +{ + return testCondition(maxIndex, conditionFlag->m_Name, conditionFlag->m_Value); +} + +std::pair<bool, QString> +FomodInstallerDialog::testCondition(int maxIndex, const SubCondition* condition) const +{ + ConditionOperator op = condition->m_Operator; + for (const Condition* cond : condition->m_Conditions) { + std::pair<bool, QString> conditionMatches = cond->test(maxIndex, this); + if (!conditionMatches.first) + qWarning() << conditionMatches.second; + if (op == OP_OR && conditionMatches.first) { + return std::make_pair<bool, QString>( + 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<bool, QString>( + true, tr("All conditions were successful in an 'AND' clause!")); + else + return std::make_pair<bool, QString>( + 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<int>(state))); +} + +std::pair<bool, QString> +FomodInstallerDialog::testCondition(int, const FileCondition* condition) const +{ + static const std::map<QString, QString> 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<bool, QString>( + 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<bool, QString>( + 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<int, 4> 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<bool, QString> +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<ScriptExtender>(); + 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<bool, QString>( + 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<bool, QString>(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<const FileDescriptor*> 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<IFileTree>& 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<bool, QString> 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<QAbstractButton*> choices = + ui->stepsStack->widget(i)->findChildren<QAbstractButton*>("choice"); + for (QAbstractButton* choice : choices) { + if (choice->isChecked()) { + QVariantList fileList = choice->property("files").toList(); + for (QVariant fileVariant : fileList) { + descriptorList.push_back(fileVariant.value<FileDescriptor*>()); + } + } + } + } + } + + std::stable_sort(descriptorList.begin(), descriptorList.end(), byPriority); + + IFileTree::OverwritesType overwrites; + Leaves leaves; + std::shared_ptr<IFileTree> newTree = tree->createOrphanTree(); + + std::vector<const FileDescriptor*> 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 = QDir::tempPath() + "/" + m_FomodPath + "/" + + QDir::fromNativeSeparators(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<QAbstractButton*>(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("<a href=\"%1\">Link</a>").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 <folder + // source="" destination=""/> 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 + // <file source="" destination=""/> 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 = attributes.value("source").toString(); + file->m_Destination = attributes.hasAttribute("destination") + ? 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<Plugin> 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<QAbstractButton*> 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<QGroupBox*> 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<QGroupBox*>::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<bool, QString> 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<QAbstractButton*> choices = + ui->stepsStack->currentWidget()->findChildren<QAbstractButton*>("choice"); + if (choices.count() > 0) { + highlightControl(choices.at(0)); + } + m_PageVisible.push_back(true); + updateNextbtnText(); +} + +std::pair<bool, QString> 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<QAbstractButton*> choices = page->findChildren<QAbstractButton*>("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<ConditionFlag>(); + 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<int>(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<SubCondition>(); + 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<QVBoxLayout*>("grouplayout")) { + GroupType const groupType(layout->property("groupType").value<GroupType>()); + 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<QAbstractButton*>(item->widget()); + if (choice != nullptr) { + if (choice->objectName() == "choice" && choice->isChecked()) { + checked = true; + break; + } + } + } + } + if (!checked) { + QString group = dynamic_cast<QGroupBox*>(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<QVBoxLayout*>("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<QAbstractButton*> 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<QAbstractButton*>(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<GroupType>()); + 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<PluginTypeInfo>(); + 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<std::pair<QString, QString>> carouselImages; + int carouselIndex = -1; + + for (auto choice : + ui->stepsStack->currentWidget()->findChildren<QAbstractButton*>("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 = QDir::tempPath() + "/" + m_FomodPath + "/" + + QDir::fromNativeSeparators(screenshotFileName); + carouselImages.push_back(std::pair<QString, QString>(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(); +} |
