aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_fomod/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/installer_fomod/src
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/installer_fomod/src')
-rw-r--r--libs/installer_fomod/src/CMakeLists.txt18
-rw-r--r--libs/installer_fomod/src/fomodinstallerdialog.cpp1701
-rw-r--r--libs/installer_fomod/src/fomodinstallerdialog.h476
-rw-r--r--libs/installer_fomod/src/fomodinstallerdialog.ui269
-rw-r--r--libs/installer_fomod/src/fomodscreenshotdialog.cpp170
-rw-r--r--libs/installer_fomod/src/fomodscreenshotdialog.h35
-rw-r--r--libs/installer_fomod/src/fomodscreenshotdialog.ui475
-rw-r--r--libs/installer_fomod/src/installer_fomod_en.ts274
-rw-r--r--libs/installer_fomod/src/installerfomod.cpp301
-rw-r--r--libs/installer_fomod/src/installerfomod.h100
-rw-r--r--libs/installer_fomod/src/resources.qrc7
-rw-r--r--libs/installer_fomod/src/resources/CloseButtonIcon.pngbin0 -> 462 bytes
-rw-r--r--libs/installer_fomod/src/resources/LeftButtonIcon.pngbin0 -> 380 bytes
-rw-r--r--libs/installer_fomod/src/resources/RightButtonIcon.pngbin0 -> 386 bytes
-rw-r--r--libs/installer_fomod/src/scalelabel.cpp107
-rw-r--r--libs/installer_fomod/src/scalelabel.h31
-rw-r--r--libs/installer_fomod/src/xmlreader.cpp83
-rw-r--r--libs/installer_fomod/src/xmlreader.h40
18 files changed, 4087 insertions, 0 deletions
diff --git a/libs/installer_fomod/src/CMakeLists.txt b/libs/installer_fomod/src/CMakeLists.txt
new file mode 100644
index 0000000..cbb324d
--- /dev/null
+++ b/libs/installer_fomod/src/CMakeLists.txt
@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.16)
+
+file(GLOB INSTALLER_FOMOD_SOURCES
+ CONFIGURE_DEPENDS
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.ui
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc
+)
+
+add_library(installer_fomod SHARED ${INSTALLER_FOMOD_SOURCES})
+mo2_configure_plugin(installer_fomod WARNINGS OFF)
+target_include_directories(installer_fomod PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+)
+target_link_libraries(installer_fomod PRIVATE mo2::uibase Qt6::Widgets)
+mo2_install_plugin(installer_fomod)
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();
+}
diff --git a/libs/installer_fomod/src/fomodinstallerdialog.h b/libs/installer_fomod/src/fomodinstallerdialog.h
new file mode 100644
index 0000000..bd9f975
--- /dev/null
+++ b/libs/installer_fomod/src/fomodinstallerdialog.h
@@ -0,0 +1,476 @@
+/*
+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/>.
+*/
+
+#pragma once
+
+#include <QDialog>
+#include <QGroupBox>
+#include <QMetaType>
+#include <QObject>
+#include <QString>
+
+#include <functional>
+#include <vector>
+
+#include <uibase/guessedvalue.h>
+#include <uibase/ifiletree.h>
+#include <uibase/imoinfo.h>
+#include <uibase/iplugininstaller.h>
+#include <uibase/ipluginlist.h>
+
+#include "installerfomod.h"
+
+class QAbstractButton;
+class QXmlStreamReader;
+
+namespace Ui
+{
+class FomodInstallerDialog;
+}
+
+class ValueCondition;
+class ConditionFlag;
+class SubCondition;
+class FileCondition;
+class VersionCondition;
+
+class XmlReader;
+
+class IConditionTester
+{
+public:
+ virtual std::pair<bool, QString>
+ testCondition(int maxIndex, const ValueCondition* condition) const = 0;
+ virtual std::pair<bool, QString>
+ testCondition(int maxIndex, const ConditionFlag* condition) const = 0;
+ virtual std::pair<bool, QString>
+ testCondition(int maxIndex, const SubCondition* condition) const = 0;
+ virtual std::pair<bool, QString>
+ testCondition(int maxIndex, const FileCondition* condition) const = 0;
+ virtual std::pair<bool, QString>
+ testCondition(int maxIndex, const VersionCondition* condition) const = 0;
+};
+
+enum ConditionOperator
+{
+ OP_AND,
+ OP_OR
+};
+
+class Condition
+{
+public:
+ Condition() {}
+ virtual std::pair<bool, QString> test(int maxIndex,
+ const IConditionTester* tester) const = 0;
+
+private:
+ Condition& operator=(const Condition&) = delete;
+};
+
+class ConditionFlag : public Condition
+{
+public:
+ ConditionFlag() : Condition(), m_Name(), m_Value() {}
+ ConditionFlag(const QString& name, const QString& value)
+ : Condition(), m_Name(name), m_Value(value)
+ {}
+ virtual std::pair<bool, QString> test(int maxIndex,
+ const IConditionTester* tester) const
+ {
+ return tester->testCondition(maxIndex, this);
+ }
+ QString m_Name;
+ QString m_Value;
+};
+Q_DECLARE_METATYPE(ConditionFlag)
+
+class ValueCondition : public Condition
+{
+public:
+ ValueCondition() : Condition(), m_Name(), m_Value() {}
+ ValueCondition(const QString& name, const QString& value)
+ : Condition(), m_Name(name), m_Value(value)
+ {}
+ virtual std::pair<bool, QString> test(int maxIndex,
+ const IConditionTester* tester) const
+ {
+ return tester->testCondition(maxIndex, this);
+ }
+ QString m_Name;
+ QString m_Value;
+};
+Q_DECLARE_METATYPE(ValueCondition)
+
+class FileCondition : public Condition
+{
+public:
+ FileCondition() : Condition(), m_File(), m_State() {}
+ FileCondition(const QString& file, const QString& state)
+ : Condition(), m_File(file), m_State(state)
+ {}
+ virtual std::pair<bool, QString> test(int maxIndex,
+ const IConditionTester* tester) const
+ {
+ return tester->testCondition(maxIndex, this);
+ }
+ QString m_File;
+ QString m_State;
+};
+Q_DECLARE_METATYPE(FileCondition)
+
+class SubCondition : public Condition
+{
+public:
+ virtual std::pair<bool, QString> test(int maxIndex,
+ const IConditionTester* tester) const
+ {
+ return tester->testCondition(maxIndex, this);
+ }
+ ConditionOperator m_Operator;
+ std::vector<Condition*> m_Conditions;
+};
+Q_DECLARE_METATYPE(SubCondition)
+
+class VersionCondition : public Condition
+{
+public:
+ enum Type
+ {
+ v_Game,
+ v_FOMM,
+ v_FOSE
+ };
+ VersionCondition() : Condition(), m_Type(), m_RequiredVersion() {}
+ VersionCondition(Type type, const QString& requiredVersion)
+ : Condition(), m_Type(type), m_RequiredVersion(requiredVersion)
+ {}
+ virtual std::pair<bool, QString> test(int maxIndex,
+ const IConditionTester* tester) const
+ {
+ return tester->testCondition(maxIndex, this);
+ }
+ Type m_Type;
+ QString m_RequiredVersion;
+};
+Q_DECLARE_METATYPE(VersionCondition)
+
+class FileDescriptor : public QObject
+{
+ Q_OBJECT
+public:
+ FileDescriptor(QObject* parent)
+ : QObject(parent), m_Source(), m_Destination(), m_Priority(0), m_IsFolder(false),
+ m_AlwaysInstall(false), m_InstallIfUsable(false), m_FileSystemItemSequence(0)
+ {}
+
+ FileDescriptor(const FileDescriptor& reference)
+ : QObject(reference.parent()), m_Source(reference.m_Source),
+ m_Destination(reference.m_Destination), m_Priority(reference.m_Priority),
+ m_IsFolder(reference.m_IsFolder), m_AlwaysInstall(reference.m_AlwaysInstall),
+ m_InstallIfUsable(reference.m_InstallIfUsable),
+ m_FileSystemItemSequence(reference.m_FileSystemItemSequence)
+ {}
+
+ QString m_Source;
+ QString m_Destination;
+ int m_Priority;
+ bool m_IsFolder;
+ bool m_AlwaysInstall;
+ bool m_InstallIfUsable;
+ int m_FileSystemItemSequence;
+
+private:
+ FileDescriptor& operator=(const FileDescriptor&);
+};
+
+Q_DECLARE_METATYPE(FileDescriptor*)
+
+class FomodInstallerDialog : public QDialog, public IConditionTester
+{
+ Q_OBJECT
+
+public:
+ explicit FomodInstallerDialog(
+ InstallerFomod* installer, const MOBase::GuessedValue<QString>& modName,
+ const QString& fomodPath,
+ const std::function<MOBase::IPluginList::PluginStates(const QString&)>& fileCheck,
+ QWidget* parent = 0);
+ ~FomodInstallerDialog();
+
+ void initData(MOBase::IOrganizer* moInfo);
+
+ /**
+ * @return bool true if the user requested the manual dialog
+ **/
+ bool manualRequested() const { return m_Manual; }
+
+ /**
+ * @return the (user-modified) name to be used for the mod
+ **/
+ QString getName() const;
+
+ /**
+ * @return the version of the mod as specified in the fomod info.xml
+ */
+ QString getVersion() const;
+
+ /**
+ * @return the mod id as specified in the info.xml
+ */
+ int getModID() const;
+
+ /**
+ * @return the mod url as specified in the fomod file
+ */
+ QString getURL() const;
+
+ /**
+ * @brief Updated the archive tree from the dialog.
+ *
+ * @param tree The input archive tree.
+ **/
+ MOBase::IPluginInstaller::EInstallResult
+ updateTree(std::shared_ptr<MOBase::IFileTree>& tree);
+
+ bool hasOptions();
+
+ void transformToSmallInstall();
+
+protected:
+ virtual bool eventFilter(QObject* object, QEvent* event);
+
+private slots:
+
+ void on_cancelBtn_clicked();
+
+ void on_manualBtn_clicked();
+
+ void on_websiteLabel_linkActivated(const QString& link);
+
+ void on_nextBtn_clicked();
+
+ void on_prevBtn_clicked();
+
+ // detect signals for people playing with checkboxes/buttons
+ void widgetButtonClicked();
+
+ void on_screenshotExpand_clicked();
+
+private:
+ enum ItemOrder
+ {
+ ORDER_ASCENDING,
+ ORDER_DESCENDING,
+ ORDER_EXPLICIT
+ };
+
+ // So I can make GroupType and PluginTypeInfo into QVariants
+public:
+ enum GroupType
+ {
+ TYPE_SELECTATLEASTONE,
+ TYPE_SELECTATMOSTONE,
+ TYPE_SELECTEXACTLYONE,
+ TYPE_SELECTANY,
+ TYPE_SELECTALL
+ };
+
+ enum PluginType
+ {
+ TYPE_REQUIRED,
+ TYPE_RECOMMENDED,
+ TYPE_OPTIONAL,
+ TYPE_NOTUSABLE,
+ TYPE_COULDBEUSABLE
+ };
+
+ struct DependencyPattern
+ {
+ PluginType type;
+ SubCondition condition;
+ };
+
+ typedef std::vector<DependencyPattern> DependencyPatternList;
+
+ struct PluginTypeInfo
+ {
+ PluginType m_DefaultType;
+ DependencyPatternList m_DependencyPatterns;
+ };
+
+private:
+ typedef std::vector<FileDescriptor*> FileDescriptorList;
+ typedef std::vector<ConditionFlag> ConditionFlagList;
+
+ struct Plugin
+ {
+ QString m_Name;
+ QString m_Description;
+ QString m_ImagePath;
+ PluginTypeInfo m_PluginTypeInfo;
+ ConditionFlagList m_ConditionFlags;
+ FileDescriptorList m_Files;
+ };
+
+ struct ConditionalInstall
+ {
+ SubCondition m_Condition;
+ FileDescriptorList m_Files;
+ };
+
+ struct LeafInfo
+ {
+ int priority;
+ QString path;
+ };
+
+ using Leaves = std::map<const MOBase::FileTreeEntry*, LeafInfo>;
+
+private:
+ QString readContent(QXmlStreamReader& reader);
+
+ /**
+ * @brief Read XML from the given file, trying various encoding, and using
+ * the given callback on each try.
+ *
+ * @param file The file to read, must already be opened.
+ * @param callback The callback used for every encoding try.
+ */
+ void readXml(QFile& file, void (FomodInstallerDialog::*callback)(XmlReader&));
+
+ void readInfoXml();
+ void readModuleConfigXml();
+
+ void parseInfo(XmlReader& data);
+ void parseModuleConfig(XmlReader& data);
+
+ void updateNameEdit();
+
+ static int bomOffset(const QByteArray& buffer);
+ static ItemOrder getItemOrder(const QString& orderString);
+ static GroupType getGroupType(const QString& typeString);
+ static PluginType getPluginType(const QString& typeString);
+ static bool byPriority(const FileDescriptor* LHS, const FileDescriptor* RHS);
+
+ PluginType getPluginDependencyType(int page, PluginTypeInfo const& info) const;
+
+ typedef void (FomodInstallerDialog::*TagProcessor)(XmlReader& reader);
+ void processXmlTag(XmlReader& reader, char const* tag, TagProcessor func);
+
+ void readFileList(XmlReader& reader, FileDescriptorList& fileList);
+ void readDependencyPattern(XmlReader& reader, DependencyPattern& pattern);
+ void readDependencyPatternList(XmlReader& reader, DependencyPatternList& patterns);
+ void readDependencyPluginType(XmlReader& reader, PluginTypeInfo& info);
+ void readPluginType(XmlReader& reader, Plugin& plugin);
+ void readConditionFlagList(XmlReader& reader, ConditionFlagList& condflags);
+ FomodInstallerDialog::Plugin readPlugin(XmlReader& reader);
+ void readPluginList(XmlReader& reader, QString const& groupName, GroupType& groupType,
+ QLayout* layout);
+ void readGroup(XmlReader& reader, QLayout* layout);
+ void readGroupList(XmlReader& reader, QLayout* layout);
+ QGroupBox* readInstallStep(XmlReader& reader);
+ void readCompositeDependency(XmlReader& reader, SubCondition& conditional);
+ ConditionalInstall readConditionalInstallPattern(XmlReader& reader);
+ void readConditionalFilePatternList(XmlReader& reader);
+ void readConditionalFileInstallList(XmlReader& reader);
+ void readStepList(XmlReader& reader);
+ void readModuleConfiguration(XmlReader& reader);
+ void highlightControl(QAbstractButton* button);
+
+ std::pair<bool, QString> testCondition(int maxIndex, const QString& flag,
+ const QString& value) const;
+ virtual std::pair<bool, QString> testCondition(int maxIndex,
+ const ValueCondition* condition) const;
+ virtual std::pair<bool, QString> testCondition(int maxIndex,
+ const ConditionFlag* condition) const;
+ virtual std::pair<bool, QString> testCondition(int maxIndex,
+ const SubCondition* condition) const;
+ virtual std::pair<bool, QString> testCondition(int maxIndex,
+ const FileCondition* condition) const;
+ virtual std::pair<bool, QString>
+ testCondition(int maxIndex, const VersionCondition* condition) const;
+ bool testVisible(int pageIndex) const;
+ bool nextPage();
+ void activateCurrentPage();
+
+ void moveTree(std::shared_ptr<MOBase::IFileTree> target,
+ std::shared_ptr<MOBase::IFileTree> source,
+ MOBase::IFileTree::OverwritesType& overwrites);
+
+ void copyLeaf(std::shared_ptr<MOBase::FileTreeEntry> sourceEntry,
+ std::shared_ptr<MOBase::IFileTree> destinationTree,
+ QString destinationPath, MOBase::IFileTree::OverwritesType& overwrites,
+ Leaves& leaves, int pri);
+
+ bool copyFileIterator(std::shared_ptr<MOBase::IFileTree> sourceTree,
+ std::shared_ptr<MOBase::IFileTree> destinationTree,
+ const FileDescriptor* descriptor, Leaves& leaves,
+ MOBase::IFileTree::OverwritesType& overwrites);
+
+ static void applyPriority(Leaves& leaves, MOBase::IFileTree const* tree,
+ int priority);
+
+ /**
+ * @brief Display a dialog indicating to the user that some files were not found.
+ *
+ * @param missingFiles List of missing files.
+ *
+ * @return true if the user chose to continue with the installation, false otherwize.
+ */
+ bool displayMissingFilesDialog(std::vector<const FileDescriptor*> missingFiles);
+
+ static QString toString(MOBase::IPluginList::PluginStates state);
+
+ // Set the 'next' button to display 'next' or 'install'
+ void updateNextbtnText();
+
+ // Display the current page calculating all the button enables/disables
+ void displayCurrentPage();
+
+private:
+ Ui::FomodInstallerDialog* ui;
+
+ InstallerFomod* m_Installer;
+ MOBase::GuessedValue<QString> m_ModName;
+
+ int m_ModID;
+
+ QString m_FomodPath;
+ bool m_Manual;
+
+ FileDescriptorList m_RequiredFiles;
+ std::vector<ConditionalInstall> m_ConditionalInstalls;
+ std::vector<bool> m_PageVisible;
+
+ std::function<MOBase::IPluginList::PluginStates(const QString&)> m_FileCheck;
+
+ // Because NMM maintains the sequence from the xml when dealing with things with
+ // the same priority, we have to as well. This is moderately hacky.
+ int m_FileSystemItemSequence;
+
+ // So I can find out game info (I hope)
+ MOBase::IOrganizer* m_MoInfo;
+
+ // The web page in the fomod (if supplied)
+ QString m_URL;
+};
+
+Q_DECLARE_METATYPE(FomodInstallerDialog::GroupType)
+Q_DECLARE_METATYPE(FomodInstallerDialog::PluginTypeInfo)
diff --git a/libs/installer_fomod/src/fomodinstallerdialog.ui b/libs/installer_fomod/src/fomodinstallerdialog.ui
new file mode 100644
index 0000000..9003416
--- /dev/null
+++ b/libs/installer_fomod/src/fomodinstallerdialog.ui
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FomodInstallerDialog</class>
+ <widget class="QDialog" name="FomodInstallerDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>769</width>
+ <height>477</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>FOMOD Installer</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,2">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="nameCombo">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+ </property>
+ <property name="horizontalSpacing">
+ <number>15</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Author</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Version</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="versionLabel">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Website</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="websiteLabel">
+ <property name="text">
+ <string>&lt;a href=&quot;#&quot;&gt;Link&lt;/a&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="authorLabel">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QSplitter" name="splitter_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="childrenCollapsible">
+ <bool>false</bool>
+ </property>
+ <widget class="QSplitter" name="splitter">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="childrenCollapsible">
+ <bool>false</bool>
+ </property>
+ <widget class="QTextEdit" name="descriptionText">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ <widget class="ScaleLabel" name="screenshotLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>320</width>
+ <height>200</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <layout class="QVBoxLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="screenshotExpand">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="cursor">
+ <cursorShape>PointingHandCursor</cursorShape>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border:none; background:none</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <widget class="QStackedWidget" name="stepsStack">
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="currentIndex">
+ <number>-1</number>
+ </property>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QPushButton" name="manualBtn">
+ <property name="text">
+ <string>Manual</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="prevBtn">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Back</string>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nextBtn">
+ <property name="text">
+ <string>Next</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cancelBtn">
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ <property name="default">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ScaleLabel</class>
+ <extends>QLabel</extends>
+ <header>scalelabel.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/installer_fomod/src/fomodscreenshotdialog.cpp b/libs/installer_fomod/src/fomodscreenshotdialog.cpp
new file mode 100644
index 0000000..568faa4
--- /dev/null
+++ b/libs/installer_fomod/src/fomodscreenshotdialog.cpp
@@ -0,0 +1,170 @@
+/*
+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 "fomodscreenshotdialog.h"
+#include "ui_fomodscreenshotdialog.h"
+
+#include <QDirIterator>
+#include <QFrame>
+#include <QProxyStyle>
+#include <QPushButton>
+#include <QScreen>
+#include <QTableWidget>
+#include <QWindow>
+
+#include "scalelabel.h"
+
+constexpr int kScreenshotTileWidth = 100;
+constexpr int kScreenshotTileHeight = 80;
+constexpr int kScreenshotTileSpacing = 16;
+
+// Disables the native dotted selection border around focused elements. Unfortunately,
+// disabling it via stylesheets is broken in the latest version of Qt
+// https://stackoverflow.com/questions/9795791/removing-dotted-border-without-setting-nofocus-in-windows-pyqt
+class NoFocusProxyStyle : public QProxyStyle
+{
+public:
+ NoFocusProxyStyle(QStyle* baseStyle = nullptr) : QProxyStyle(baseStyle) {}
+
+ void drawPrimitive(PrimitiveElement element, const QStyleOption* option,
+ QPainter* painter, const QWidget* widget) const
+ {
+ if (element == QStyle::PE_FrameFocusRect) {
+ return;
+ }
+ QProxyStyle::drawPrimitive(element, option, painter, widget);
+ }
+};
+
+FomodScreenshotDialog::FomodScreenshotDialog(
+ QWidget* parent, std::vector<std::pair<QString, QString>> carouselImages,
+ int carouselIndex)
+ : QDialog(parent, Qt::FramelessWindowHint), ui(new Ui::FomodScreenshotDialog),
+ m_carouselImages(carouselImages)
+{
+ Q_INIT_RESOURCE(resources);
+
+ ui->setupUi(this);
+ setAttribute(Qt::WA_TranslucentBackground);
+
+ // Manually maximize the dialog since showMaximized() clips over the taskbar
+ QScreen* screen = this->screen();
+ QRect availableGeometry = screen->availableGeometry();
+ setFixedSize(availableGeometry.width(), availableGeometry.height());
+ move(availableGeometry.x(), availableGeometry.y());
+
+ QTableWidget* carouselList = ui->carouselList;
+ carouselList->setStyle(new NoFocusProxyStyle);
+
+ carouselList->setRowCount(1);
+ carouselList->setRowHeight(0, kScreenshotTileHeight);
+ carouselList->setColumnCount(0);
+ for (auto carouselImage : m_carouselImages) {
+ QFrame* container = new QFrame(carouselList);
+ QVBoxLayout* layout = new QVBoxLayout(container);
+ layout->setContentsMargins(0, 0, 0, 0);
+ container->setLayout(layout);
+
+ ScaleLabel* scaleLabel = new ScaleLabel(container);
+ scaleLabel->setScalableResource(carouselImage.second);
+ scaleLabel->setFixedSize(kScreenshotTileWidth, kScreenshotTileHeight);
+ scaleLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ scaleLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ scaleLabel->setCursor(Qt::PointingHandCursor);
+ scaleLabel->setStatic(true);
+
+ layout->addWidget(scaleLabel);
+ layout->setAlignment(scaleLabel, Qt::AlignLeft | Qt::AlignVCenter);
+
+ int column = carouselList->columnCount();
+ carouselList->setColumnCount(column + 1);
+ carouselList->setColumnWidth(
+ column, kScreenshotTileWidth +
+ (column + 1 == carouselImages.size() ? 0 : kScreenshotTileSpacing));
+
+ QTableWidgetItem* item = new QTableWidgetItem("");
+ carouselList->setItem(0, column, item);
+ carouselList->setCellWidget(0, column, container);
+ }
+
+ connect(carouselList, &QTableWidget::itemSelectionChanged, this,
+ &FomodScreenshotDialog::selectedScreenshotChanged);
+ ui->carouselList->selectColumn(carouselIndex);
+}
+
+FomodScreenshotDialog::~FomodScreenshotDialog()
+{
+ delete ui;
+}
+
+void FomodScreenshotDialog::on_closeButton_clicked()
+{
+ close();
+}
+
+void FomodScreenshotDialog::on_navigateLeft_clicked()
+{
+ int selectedColumn = getSelectedScreenshot();
+ if (selectedColumn == 0) {
+ return;
+ }
+
+ ui->carouselList->selectColumn(selectedColumn - 1);
+}
+
+void FomodScreenshotDialog::on_navigateRight_clicked()
+{
+ int selectedColumn = getSelectedScreenshot();
+ if (selectedColumn == m_carouselImages.size() - 1) {
+ return;
+ }
+
+ ui->carouselList->selectColumn(selectedColumn + 1);
+}
+
+void FomodScreenshotDialog::selectedScreenshotChanged()
+{
+ // In case the user ctrl+clicks current selection to result in an empty selection
+ if (ui->carouselList->selectedItems().isEmpty()) {
+ ui->carouselList->selectColumn(0);
+ return;
+ }
+
+ int selectedColumn = getSelectedScreenshot();
+
+ ui->imageTitleLabel->setText(m_carouselImages.at(selectedColumn).first);
+ ui->image->setScalableResource(m_carouselImages.at(selectedColumn).second);
+ ui->slideshowPosition->setText(QString("%1/%2").arg(
+ QString::number(selectedColumn + 1), QString::number(m_carouselImages.size())));
+
+ for (int column = 0; column < ui->carouselList->columnCount(); column++) {
+ QWidget* widget = ui->carouselList->cellWidget(0, column);
+ ScaleLabel* scaleLabel = widget->findChild<ScaleLabel*>();
+ if (column == selectedColumn) {
+ scaleLabel->setStyleSheet(scaleLabel->styleSheet() +
+ "QLabel { border:2px solid white; }");
+ } else {
+ scaleLabel->setStyleSheet(scaleLabel->styleSheet() + "QLabel { border:none; }");
+ }
+ }
+}
+
+int FomodScreenshotDialog::getSelectedScreenshot()
+{
+ return ui->carouselList->selectedItems().front()->column();
+}
diff --git a/libs/installer_fomod/src/fomodscreenshotdialog.h b/libs/installer_fomod/src/fomodscreenshotdialog.h
new file mode 100644
index 0000000..dfe2cfd
--- /dev/null
+++ b/libs/installer_fomod/src/fomodscreenshotdialog.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <QDialog>
+#include <QString>
+
+#include <utility>
+#include <vector>
+
+namespace Ui
+{
+class FomodScreenshotDialog;
+}
+
+class FomodScreenshotDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit FomodScreenshotDialog(
+ QWidget* parent, std::vector<std::pair<QString, QString>> carouselImages,
+ int carouselIndex);
+ ~FomodScreenshotDialog();
+
+private slots:
+ void on_closeButton_clicked();
+ void on_navigateLeft_clicked();
+ void on_navigateRight_clicked();
+
+private:
+ void selectedScreenshotChanged();
+ int getSelectedScreenshot();
+
+ Ui::FomodScreenshotDialog* ui;
+ std::vector<std::pair<QString, QString>> m_carouselImages;
+};
diff --git a/libs/installer_fomod/src/fomodscreenshotdialog.ui b/libs/installer_fomod/src/fomodscreenshotdialog.ui
new file mode 100644
index 0000000..ea1ac9a
--- /dev/null
+++ b/libs/installer_fomod/src/fomodscreenshotdialog.ui
@@ -0,0 +1,475 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FomodScreenshotDialog</class>
+ <widget class="QDialog" name="FomodScreenshotDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1020</width>
+ <height>693</height>
+ </rect>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">background:none;</string>
+ </property>
+ <layout class="QVBoxLayout" name="backgroundLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QFrame" name="backgroundFrame">
+ <property name="styleSheet">
+ <string notr="true">QFrame#backgroundFrame{background-color: rgba(0,0,0,85%);}</string>
+ </property>
+ <layout class="QVBoxLayout" name="mainLayout">
+ <item>
+ <layout class="QHBoxLayout" name="toolbarLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="slideshowPosition">
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ </font>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QLabel { color:white; }</string>
+ </property>
+ <property name="text">
+ <string notr="true">001/999</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="cursor">
+ <cursorShape>PointingHandCursor</cursorShape>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border:none;</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/CloseButtonIcon.png</normaloff>:/MO/gui/resources/CloseButtonIcon.png</iconset>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="navigateLeft">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>75</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="cursor">
+ <cursorShape>PointingHandCursor</cursorShape>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"> QPushButton{border:none; } QPushButton:hover{background-color:rgba(255,255,255,25%); }</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/LeftButtonIcon.png</normaloff>:/MO/gui/resources/LeftButtonIcon.png</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="ScaleLabel" name="image">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="navigateRight">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>75</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="cursor">
+ <cursorShape>PointingHandCursor</cursorShape>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"> QPushButton{border:none; } QPushButton:hover{background-color:rgba(255,255,255,25%); }</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/RightButtonIcon.png</normaloff>:/MO/gui/resources/RightButtonIcon.png</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="imageTitleLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>12</pointsize>
+ </font>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QLabel { color:white; }</string>
+ </property>
+ <property name="text">
+ <string notr="true">&lt;image name&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTableWidget" name="carouselList">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>100</height>
+ </size>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QTableWidget {
+ background-color:transparent;
+ outline:none;
+}
+
+QTableWidget::item {
+ selection-background-color:transparent;
+ background: none;
+}
+
+QScrollBar:horizontal {
+ border: none;
+ background-color:transparent;
+ height: 15px;
+ margin: 0px 0px 0px 0px;
+}
+QScrollBar::handle:horizontal {
+ border-radius: 2px;
+ background-color: rgba(255,255,255,85%);
+ min-width: 20px;
+}
+
+QScrollBar::add-line:horizontal {
+ width: 0px;
+ subcontrol-position: right;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::sub-line:horizontal {
+ width: 0px;
+ subcontrol-position: left;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
+ background: none;
+}</string>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectColumns</enum>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>100</width>
+ <height>80</height>
+ </size>
+ </property>
+ <property name="showGrid">
+ <bool>false</bool>
+ </property>
+ <property name="rowCount">
+ <number>1</number>
+ </property>
+ <attribute name="horizontalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="verticalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ <row/>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">New Column</string>
+ </property>
+ </column>
+ <item row="0" column="0">
+ <property name="text">
+ <string notr="true">sdf</string>
+ </property>
+ </item>
+ <item row="0" column="1">
+ <property name="text">
+ <string notr="true">ssdf</string>
+ </property>
+ </item>
+ <item row="0" column="2">
+ <property name="text">
+ <string notr="true">sf</string>
+ </property>
+ </item>
+ <item row="0" column="3">
+ <property name="text">
+ <string notr="true">sdf</string>
+ </property>
+ </item>
+ <item row="0" column="4">
+ <property name="text">
+ <string notr="true">sdf</string>
+ </property>
+ </item>
+ <item row="0" column="5">
+ <property name="text">
+ <string notr="true">dsfdf</string>
+ </property>
+ </item>
+ <item row="0" column="6">
+ <property name="text">
+ <string notr="true">sdfsfd</string>
+ </property>
+ </item>
+ <item row="0" column="7">
+ <property name="text">
+ <string notr="true">sdfsdf</string>
+ </property>
+ </item>
+ <item row="0" column="8">
+ <property name="text">
+ <string notr="true">sdfsdf</string>
+ </property>
+ </item>
+ <item row="0" column="9">
+ <property name="text">
+ <string notr="true">sdf</string>
+ </property>
+ </item>
+ <item row="0" column="10">
+ <property name="text">
+ <string notr="true">sdf</string>
+ </property>
+ </item>
+ <item row="0" column="11">
+ <property name="text">
+ <string notr="true">sdf</string>
+ </property>
+ </item>
+ <item row="0" column="12">
+ <property name="text">
+ <string notr="true">sdf</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ScaleLabel</class>
+ <extends>QLabel</extends>
+ <header>scalelabel.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources>
+ <include location="resources.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/libs/installer_fomod/src/installer_fomod_en.ts b/libs/installer_fomod/src/installer_fomod_en.ts
new file mode 100644
index 0000000..98c5d97
--- /dev/null
+++ b/libs/installer_fomod/src/installer_fomod_en.ts
@@ -0,0 +1,274 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>FomodInstallerDialog</name>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="14"/>
+ <source>FOMOD Installer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="25"/>
+ <source>Name</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="55"/>
+ <source>Author</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="62"/>
+ <source>Version</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="76"/>
+ <source>Website</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="83"/>
+ <source>&lt;a href=&quot;#&quot;&gt;Link&lt;/a&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="203"/>
+ <source>Manual</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="229"/>
+ <source>Back</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="239"/>
+ <location filename="fomodinstallerdialog.cpp" line="1508"/>
+ <location filename="fomodinstallerdialog.cpp" line="1653"/>
+ <source>Next</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.ui" line="246"/>
+ <location filename="fomodinstallerdialog.cpp" line="529"/>
+ <source>Cancel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="225"/>
+ <source>Failed to parse %1. See console for details.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="247"/>
+ <source>%1 missing.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="396"/>
+ <source>At least one condition was successful in an &apos;OR&apos; clause!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="405"/>
+ <source>All conditions were successful in an &apos;AND&apos; clause!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="408"/>
+ <source>No conditions were successful in an &apos;OR&apos; clause!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="419"/>
+ <source>invalid plugin state %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="426"/>
+ <source>Missing</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="427"/>
+ <source>Inactive</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="428"/>
+ <source>Active</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="433"/>
+ <source>Success: The file &apos;%1&apos; was marked %2.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="438"/>
+ <source>Missing requirement: The file &apos;%1&apos; should be %2, but was %3!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="508"/>
+ <source>Success: The required version of %1 is %2, and was detected as %3.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="514"/>
+ <source>Missing requirement: The required version of %1 is %2, but was detected as %3.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="527"/>
+ <source>Missing files or folders</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="528"/>
+ <source>Install anyway</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="531"/>
+ <source>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.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="536"/>
+ <source>Folder &apos;%1&apos;.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="536"/>
+ <source>File &apos;%1&apos;.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="680"/>
+ <source>&lt;a href=&quot;%1&quot;&gt;Link&lt;/a&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="704"/>
+ <source>unsupported order type %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="722"/>
+ <source>unsupported group type %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="997"/>
+ <source>All components in this group are required</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1037"/>
+ <source>None</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1065"/>
+ <source>Select one or more of these options:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1382"/>
+ <source>The flag &apos;%1&apos; matched &apos;%2&apos;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1386"/>
+ <source>The flag &apos;%1&apos; did not match &apos;%2&apos;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1397"/>
+ <source>The condition was not matched and is empty.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1398"/>
+ <source>The value exists but was not matched.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1479"/>
+ <source>Disabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1481"/>
+ <source>This button is disabled because the following group(s) need a selection: </source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1508"/>
+ <source>Install</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1570"/>
+ <source>This component is required</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1576"/>
+ <source>It is recommended you enable this component</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1582"/>
+ <source>Optional component</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1587"/>
+ <source>This component is not usable in combination with other installed plugins</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="fomodinstallerdialog.cpp" line="1596"/>
+ <source>You may be experiencing instability in combination with other installed plugins</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>InstallerFomod</name>
+ <message>
+ <location filename="installerfomod.cpp" line="38"/>
+ <source>Installer for xml based fomod archives.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installerfomod.cpp" line="48"/>
+ <source>Fomod Installer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installerfomod.cpp" line="251"/>
+ <source>Installation as fomod failed: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installerfomod.cpp" line="276"/>
+ <source>image formats not supported.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installerfomod.cpp" line="278"/>
+ <location filename="installerfomod.cpp" line="290"/>
+ <source>invalid problem key %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installerfomod.cpp" line="286"/>
+ <source>This indicates that files from dlls/imageformats are missing from your MO installation or outdated. Images in installers may not be displayed. Please re-install MO</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/installer_fomod/src/installerfomod.cpp b/libs/installer_fomod/src/installerfomod.cpp
new file mode 100644
index 0000000..3558748
--- /dev/null
+++ b/libs/installer_fomod/src/installerfomod.cpp
@@ -0,0 +1,301 @@
+#include "installerfomod.h"
+
+#include <QImageReader>
+#include <QStringList>
+#include <QtPlugin>
+
+#include <uibase/iinstallationmanager.h>
+#include <uibase/imodinterface.h>
+#include <uibase/imodlist.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <uibase/utility.h>
+
+#include "fomodinstallerdialog.h"
+
+using namespace MOBase;
+
+const unsigned int InstallerFomod::PROBLEM_IMAGETYPE_UNSUPPORTED;
+
+InstallerFomod::InstallerFomod() : m_MOInfo(nullptr) {}
+
+bool InstallerFomod::init(IOrganizer* moInfo)
+{
+ m_MOInfo = moInfo;
+ return true;
+}
+
+QString InstallerFomod::name() const
+{
+ return "Fomod Installer";
+}
+
+QString InstallerFomod::author() const
+{
+ return "Tannin & thosrtanner";
+}
+
+QString InstallerFomod::description() const
+{
+ return tr("Installer for xml based fomod archives.");
+}
+
+VersionInfo InstallerFomod::version() const
+{
+ return VersionInfo(1, 7, 0, VersionInfo::RELEASE_FINAL);
+}
+
+QString InstallerFomod::localizedName() const
+{
+ return tr("Fomod Installer");
+}
+
+bool InstallerFomod::allowAnyFile() const
+{
+ return m_MOInfo->pluginSetting(name(), "use_any_file").toBool();
+}
+
+bool InstallerFomod::checkDisabledMods() const
+{
+ return m_MOInfo->pluginSetting(name(), "see_disabled_mods").toBool();
+}
+
+QList<PluginSetting> InstallerFomod::settings() const
+{
+ QList<PluginSetting> result;
+ result.push_back(
+ PluginSetting("prefer", "prefer this over the NCC based plugin", QVariant(true)));
+ result.push_back(PluginSetting("use_any_file",
+ "allow dependencies on any file, not just esp/esm",
+ QVariant(false)));
+ result.push_back(PluginSetting("see_disabled_mods",
+ "treat disabled mods as inactive rather than missing",
+ QVariant(false)));
+ return result;
+}
+
+unsigned int InstallerFomod::priority() const
+{
+ return m_MOInfo->pluginSetting(name(), "prefer").toBool() ? 110 : 90;
+}
+
+bool InstallerFomod::isManualInstaller() const
+{
+ return false;
+}
+
+void InstallerFomod::onInstallationStart(QString const&, bool, IModInterface*)
+{
+ m_InstallerUsed = false;
+}
+
+void InstallerFomod::onInstallationEnd(EInstallResult result, IModInterface* newMod)
+{
+ if (result == EInstallResult::RESULT_SUCCESS && m_InstallerUsed &&
+ newMod->url().isEmpty()) {
+ newMod->setUrl(m_Url);
+ }
+}
+
+std::shared_ptr<const IFileTree>
+InstallerFomod::findFomodDirectory(std::shared_ptr<const IFileTree> tree) const
+{
+ auto entry = tree->find("fomod", FileTreeEntry::DIRECTORY);
+
+ if (entry != nullptr) {
+ return entry->astree();
+ }
+
+ if (tree->size() == 1 && tree->at(0)->isDir()) {
+ return findFomodDirectory(tree->at(0)->astree());
+ }
+ return nullptr;
+}
+
+bool InstallerFomod::isArchiveSupported(std::shared_ptr<const IFileTree> tree) const
+{
+ tree = findFomodDirectory(tree);
+ if (tree != nullptr) {
+ return tree->exists("ModuleConfig.xml", FileTreeEntry::FILE);
+ }
+ return false;
+}
+
+void InstallerFomod::appendImageFiles(
+ std::vector<std::shared_ptr<const FileTreeEntry>>& entries,
+ std::shared_ptr<const IFileTree> tree) const
+{
+ static std::set<QString, FileNameComparator> imageSuffixes{"png", "jpg", "jpeg",
+ "gif", "bmp"};
+ for (auto entry : *tree) {
+ if (entry->isDir()) {
+ appendImageFiles(entries, entry->astree());
+ } else if (imageSuffixes.count(entry->suffix()) > 0) {
+ entries.push_back(entry);
+ }
+ }
+}
+
+std::vector<std::shared_ptr<const FileTreeEntry>>
+InstallerFomod::buildFomodTree(std::shared_ptr<const IFileTree> tree) const
+{
+ std::vector<std::shared_ptr<const FileTreeEntry>> entries;
+
+ auto fomodTree = findFomodDirectory(tree);
+
+ for (auto entry : *fomodTree) {
+ if (entry->isFile() &&
+ (entry->compare("info.xml") == 0 || entry->compare("ModuleConfig.xml") == 0)) {
+ entries.push_back(entry);
+ }
+ }
+
+ appendImageFiles(entries, tree);
+
+ return entries;
+}
+
+IPluginList::PluginStates InstallerFomod::fileState(const QString& fileName) const
+{
+ QString ext = QFileInfo(fileName).suffix().toLower();
+ if ((ext == "esp") || (ext == "esm") || (ext == "esl")) {
+ IPluginList::PluginStates state = m_MOInfo->pluginList()->state(fileName);
+ if (state != IPluginList::STATE_MISSING) {
+ return state;
+ }
+ } else if (allowAnyFile()) {
+ QFileInfo info(fileName);
+ QString name = info.fileName();
+ QStringList files =
+ m_MOInfo->findFiles(info.dir().path(), [&, name](const QString& f) -> bool {
+ return name.compare(QFileInfo(f).fileName(),
+ FileNameComparator::CaseSensitivity) == 0;
+ });
+ // A note: The list of files produced is somewhat odd as it's the full path
+ // to the originating mod (or mods). However, all we care about is if it's
+ // there or not.
+ if (files.size() != 0) {
+ return IPluginList::STATE_ACTIVE;
+ }
+ } else {
+ log::warn("A dependency on non esp/esm/esl {} will always find it as missing.",
+ fileName);
+ return IPluginList::STATE_MISSING;
+ }
+
+ // If they are really desparate we look in the full mod list and try that
+ if (checkDisabledMods()) {
+ IModList* modList = m_MOInfo->modList();
+ QStringList list = modList->allMods();
+ for (QString mod : list) {
+ // Get mod state. if it's active we've already looked. If it's not valid,
+ // no point in looking.
+ IModList::ModStates state = modList->state(mod);
+ if ((state & IModList::STATE_ACTIVE) != 0 ||
+ (state & IModList::STATE_VALID) == 0) {
+ continue;
+ }
+ MOBase::IModInterface* modInfo = m_MOInfo->modList()->getMod(mod);
+ // Go see if the file is in the mod
+ QDir modpath(modInfo->absolutePath());
+ QFile file(modpath.absoluteFilePath(fileName));
+ if (file.exists()) {
+ return IPluginList::STATE_INACTIVE;
+ }
+ }
+ }
+ return IPluginList::STATE_MISSING;
+}
+
+IPluginInstaller::EInstallResult
+InstallerFomod::install(GuessedValue<QString>& modName,
+ std::shared_ptr<IFileTree>& tree, QString& version, int& modID)
+{
+ auto installerFiles = buildFomodTree(tree);
+ if (manager()->extractFiles(installerFiles).size() == installerFiles.size()) {
+ try {
+ std::shared_ptr<const IFileTree> fomodTree = findFomodDirectory(tree);
+
+ QString fomodPath = fomodTree->parent()->path();
+ FomodInstallerDialog dialog(
+ this, modName, fomodPath,
+ std::bind(&InstallerFomod::fileState, this, std::placeholders::_1));
+ dialog.initData(m_MOInfo);
+ if (!dialog.getVersion().isEmpty()) {
+ version = dialog.getVersion();
+ }
+ if (dialog.getModID() != -1) {
+ modID = dialog.getModID();
+ }
+
+ m_InstallerUsed = true;
+ m_Url = dialog.getURL();
+
+ if (!dialog.hasOptions()) {
+ dialog.transformToSmallInstall();
+ }
+
+ auto result = dialog.exec();
+ if (result == QDialog::Accepted) {
+ modName.update(dialog.getName(), GUESS_USER);
+ return dialog.updateTree(tree);
+ } else {
+ if (dialog.manualRequested()) {
+ modName.update(dialog.getName(), GUESS_USER);
+ return IPluginInstaller::RESULT_MANUALREQUESTED;
+ } else if (result == QDialog::Rejected) {
+ return IPluginInstaller::RESULT_CANCELED;
+ } else {
+ return IPluginInstaller::RESULT_FAILED;
+ }
+ }
+ } catch (const std::exception& e) {
+ reportError(tr("Installation as fomod failed: %1").arg(e.what()));
+ return IPluginInstaller::RESULT_FAILED;
+ }
+ }
+ return IPluginInstaller::RESULT_CANCELED;
+}
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
+Q_EXPORT_PLUGIN2(installerFomod, InstallerFomod)
+#endif
+
+std::vector<unsigned int> InstallerFomod::activeProblems() const
+{
+ std::vector<unsigned int> result;
+ QList<QByteArray> formats = QImageReader::supportedImageFormats();
+ if (!formats.contains("jpg")) {
+ result.push_back(PROBLEM_IMAGETYPE_UNSUPPORTED);
+ }
+ return result;
+}
+
+QString InstallerFomod::shortDescription(unsigned int key) const
+{
+ switch (key) {
+ case PROBLEM_IMAGETYPE_UNSUPPORTED:
+ return tr("image formats not supported.");
+ default:
+ throw Exception(tr("invalid problem key %1").arg(key));
+ }
+}
+
+QString InstallerFomod::fullDescription(unsigned int key) const
+{
+ switch (key) {
+ case PROBLEM_IMAGETYPE_UNSUPPORTED:
+ return tr("This indicates that files from dlls/imageformats are missing from your "
+ "MO installation or outdated. "
+ "Images in installers may not be displayed. Please re-install MO");
+ default:
+ throw Exception(tr("invalid problem key %1").arg(key));
+ }
+}
+
+bool InstallerFomod::hasGuidedFix(unsigned int) const
+{
+ return false;
+}
+
+void InstallerFomod::startGuidedFix(unsigned int) const {}
diff --git a/libs/installer_fomod/src/installerfomod.h b/libs/installer_fomod/src/installerfomod.h
new file mode 100644
index 0000000..1ba8ed8
--- /dev/null
+++ b/libs/installer_fomod/src/installerfomod.h
@@ -0,0 +1,100 @@
+#ifndef INSTALLERFOMOD_H
+#define INSTALLERFOMOD_H
+
+#include <uibase/iplugindiagnose.h>
+#include <uibase/iplugininstallersimple.h>
+#include <uibase/ipluginlist.h>
+
+class InstallerFomod : public MOBase::IPluginInstallerSimple,
+ public MOBase::IPluginDiagnose
+{
+
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple
+ MOBase::IPluginDiagnose)
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+ Q_PLUGIN_METADATA(IID "org.tannin.InstallerFomod")
+#endif
+
+public:
+ InstallerFomod();
+
+ virtual bool init(MOBase::IOrganizer* moInfo) override;
+ virtual QString name() const override;
+ virtual QString localizedName() const override;
+ virtual QString author() const override;
+ virtual QString description() const override;
+ virtual MOBase::VersionInfo version() const override;
+ virtual QList<MOBase::PluginSetting> settings() const override;
+
+ virtual unsigned int priority() const override;
+ virtual bool isManualInstaller() const override;
+
+ virtual bool
+ isArchiveSupported(std::shared_ptr<const MOBase::IFileTree> tree) const override;
+ virtual EInstallResult install(MOBase::GuessedValue<QString>& modName,
+ std::shared_ptr<MOBase::IFileTree>& tree,
+ QString& version, int& modID) override;
+
+ virtual void onInstallationStart(QString const& archive, bool reinstallation,
+ MOBase::IModInterface* currentMod) override;
+ virtual void onInstallationEnd(EInstallResult result,
+ MOBase::IModInterface* newMod) override;
+
+public: // IPluginDiagnose interface
+ virtual std::vector<unsigned int> activeProblems() const;
+ virtual QString shortDescription(unsigned int key) const;
+ virtual QString fullDescription(unsigned int key) const;
+ virtual bool hasGuidedFix(unsigned int key) const;
+ virtual void startGuidedFix(unsigned int key) const;
+
+private:
+ /**
+ * @brief Retrieve the tree entry corresponding to the fomod directory.
+ *
+ * @param tree Tree to look-up the directory in.
+ *
+ * @return the entry corresponding to the fomod directory in the tree, or a null
+ * pointer if the entry was not found.
+ */
+ std::shared_ptr<const MOBase::IFileTree>
+ findFomodDirectory(std::shared_ptr<const MOBase::IFileTree> tree) const;
+
+ /**
+ * @brief Build a list of entries that should be extracted sincce the FOMOD installer
+ * may require access to (currently the .xml files in the FOMOD directory and the
+ * pictures in the archive).
+ *
+ * @param tree Base tree of the archive.
+ *
+ * @return a list of file entries that need to be extracted.
+ */
+ std::vector<std::shared_ptr<const MOBase::FileTreeEntry>>
+ buildFomodTree(std::shared_ptr<const MOBase::IFileTree> tree) const;
+
+ /**
+ * @brief Recurse through the given tree and add all the images to the given vector.
+ *
+ * @param result Vector of entries to add the images.
+ * @param tree The tree to look files in.
+ */
+ void
+ appendImageFiles(std::vector<std::shared_ptr<const MOBase::FileTreeEntry>>& entries,
+ std::shared_ptr<const MOBase::IFileTree> tree) const;
+
+ MOBase::IPluginList::PluginStates fileState(const QString& fileName) const;
+
+private:
+ static const unsigned int PROBLEM_IMAGETYPE_UNSUPPORTED = 1;
+
+private:
+ MOBase::IOrganizer* m_MOInfo;
+
+ bool allowAnyFile() const;
+ bool checkDisabledMods() const;
+
+ bool m_InstallerUsed;
+ QString m_Url;
+};
+
+#endif // INSTALLERFOMOD_H
diff --git a/libs/installer_fomod/src/resources.qrc b/libs/installer_fomod/src/resources.qrc
new file mode 100644
index 0000000..19f88bf
--- /dev/null
+++ b/libs/installer_fomod/src/resources.qrc
@@ -0,0 +1,7 @@
+<RCC>
+ <qresource prefix="/MO/gui">
+ <file>resources/CloseButtonIcon.png</file>
+ <file>resources/LeftButtonIcon.png</file>
+ <file>resources/RightButtonIcon.png</file>
+ </qresource>
+</RCC>
diff --git a/libs/installer_fomod/src/resources/CloseButtonIcon.png b/libs/installer_fomod/src/resources/CloseButtonIcon.png
new file mode 100644
index 0000000..95c8149
--- /dev/null
+++ b/libs/installer_fomod/src/resources/CloseButtonIcon.png
Binary files differ
diff --git a/libs/installer_fomod/src/resources/LeftButtonIcon.png b/libs/installer_fomod/src/resources/LeftButtonIcon.png
new file mode 100644
index 0000000..181ca52
--- /dev/null
+++ b/libs/installer_fomod/src/resources/LeftButtonIcon.png
Binary files differ
diff --git a/libs/installer_fomod/src/resources/RightButtonIcon.png b/libs/installer_fomod/src/resources/RightButtonIcon.png
new file mode 100644
index 0000000..bd8d5b7
--- /dev/null
+++ b/libs/installer_fomod/src/resources/RightButtonIcon.png
Binary files differ
diff --git a/libs/installer_fomod/src/scalelabel.cpp b/libs/installer_fomod/src/scalelabel.cpp
new file mode 100644
index 0000000..e654bf5
--- /dev/null
+++ b/libs/installer_fomod/src/scalelabel.cpp
@@ -0,0 +1,107 @@
+#include "scalelabel.h"
+
+#include <QResizeEvent>
+
+static bool isResourceMovie(const QString& path)
+{
+ for (QByteArray format : QMovie::supportedFormats()) {
+ QString fileExtension = "." + QString::fromUtf8(format);
+ if (path.endsWith(fileExtension)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+ScaleLabel::ScaleLabel(QWidget* parent) : QLabel(parent) {}
+
+void ScaleLabel::setScalableResource(const QString& path)
+{
+ if (auto m = movie()) {
+ setMovie(nullptr);
+ delete m;
+ m_OriginalMovieSize = QSize();
+ }
+ if (!pixmap().isNull()) {
+ setPixmap(QPixmap());
+ m_UnscaledImage = QImage();
+ }
+
+ if (path.isEmpty()) {
+ return;
+ }
+
+ if (isResourceMovie(path)) {
+ setScalableMovie(path);
+ } else {
+ setScalableImage(path);
+ }
+}
+
+void ScaleLabel::setStatic(bool isStatic)
+{
+ m_isStatic = isStatic;
+
+ if (auto m = movie()) {
+ if (isStatic) {
+ m->stop();
+ } else {
+ m->start();
+ }
+ }
+}
+
+void ScaleLabel::setScalableMovie(const QString& path)
+{
+ QMovie* m = new QMovie(path);
+ if (!m->isValid()) {
+ qWarning(">%s< is an invalid movie. Reason: %s", qUtf8Printable(path),
+ m->lastErrorString().toStdString().c_str());
+ delete m;
+ return;
+ }
+
+ m->setParent(this);
+ setMovie(m);
+ m->start();
+ m->stop();
+ m_OriginalMovieSize = m->currentImage().size();
+
+ m->setScaledSize(m_OriginalMovieSize.scaled(size(), Qt::KeepAspectRatio));
+ if (!m_isStatic) {
+ m->start();
+ }
+}
+
+void ScaleLabel::setScalableImage(const QString& path)
+{
+ QImage image(path);
+ if (image.isNull()) {
+ qWarning(">%s< is a null image", qUtf8Printable(path));
+ } else {
+ m_UnscaledImage = image;
+ setPixmap(QPixmap::fromImage(image).scaled(size(), Qt::KeepAspectRatio));
+ }
+}
+
+void ScaleLabel::resizeEvent(QResizeEvent* event)
+{
+ if (auto m = movie()) {
+ m->stop();
+ m->setScaledSize(m_OriginalMovieSize.scaled(event->size(), Qt::KeepAspectRatio));
+ m->start();
+
+ // We can't just skip the start() above since that is what triggers the label to
+ // resize the movie The only way to resize the movie but keep it paused is to start
+ // and then re-stop it
+ if (m_isStatic) {
+ m->stop();
+ }
+ }
+ auto p = pixmap();
+ if (!p.isNull()) {
+ setPixmap(
+ QPixmap::fromImage(m_UnscaledImage).scaled(event->size(), Qt::KeepAspectRatio));
+ }
+}
diff --git a/libs/installer_fomod/src/scalelabel.h b/libs/installer_fomod/src/scalelabel.h
new file mode 100644
index 0000000..4e943c3
--- /dev/null
+++ b/libs/installer_fomod/src/scalelabel.h
@@ -0,0 +1,31 @@
+#ifndef SCALELABEL_H
+#define SCALELABEL_H
+
+#include <QImage>
+#include <QLabel>
+#include <QMovie>
+
+class ScaleLabel : public QLabel
+{
+ Q_OBJECT
+public:
+ explicit ScaleLabel(QWidget* parent = nullptr);
+
+ void setScalableResource(const QString& path);
+ void setStatic(bool isStatic);
+signals:
+
+public slots:
+protected:
+ virtual void resizeEvent(QResizeEvent* event);
+
+private:
+ void setScalableMovie(const QString& path);
+ void setScalableImage(const QString& path);
+
+ QImage m_UnscaledImage;
+ QSize m_OriginalMovieSize;
+ bool m_isStatic = false;
+};
+
+#endif // SCALELABEL_H
diff --git a/libs/installer_fomod/src/xmlreader.cpp b/libs/installer_fomod/src/xmlreader.cpp
new file mode 100644
index 0000000..7f53710
--- /dev/null
+++ b/libs/installer_fomod/src/xmlreader.cpp
@@ -0,0 +1,83 @@
+#include "xmlreader.h"
+
+#include <QDebug>
+
+#include <uibase/utility.h>
+
+using MOBase::Exception;
+
+bool XmlReader::getNextElement(QString const& start)
+{
+ while (!atEnd()) {
+ switch (readNext()) {
+ case EndElement:
+ if (name() != start) {
+ qWarning() << "Got end of " << name() << ", expected " << start << " at "
+ << lineNumber();
+ continue;
+ }
+ return false;
+
+ case StartElement:
+ return true;
+
+ case Invalid:
+ throw Exception("bad xml");
+
+ default:
+ qWarning() << "Unexpected token type " << tokenString() << " at " << lineNumber();
+ }
+ }
+ return false;
+}
+
+void XmlReader::unexpected()
+{
+ qWarning() << "Unexpected element " << name() << " near line " << lineNumber();
+ // Eat the contents
+ QString s = readElementText(IncludeChildElements);
+ // Print them out if in debugging mode
+ qDebug() << " contains " << s;
+}
+
+void XmlReader::finishedElement()
+{
+ QString const self = name().toString();
+ while (!atEnd()) {
+ switch (readNext()) {
+ case EndElement:
+ if (name() != self) {
+ qWarning() << "Got end element for " << name() << ", expected " << self
+ << " at " << lineNumber();
+ continue;
+ }
+ return;
+
+ case Invalid:
+ throw Exception("bad xml");
+ return;
+
+ case StartElement:
+ unexpected();
+ break;
+
+ default:
+ qWarning() << "Unexpected token type " << tokenString() << " at " << lineNumber();
+ }
+ }
+}
+
+QString XmlReader::getText()
+{
+ // This reads the text in an element, leaving you at the next element.
+ QString result;
+ while (QXmlStreamReader::readNext() == Comment || tokenType() == Characters) {
+ if (tokenType() == Characters) {
+ result += text();
+ }
+ }
+ if (tokenType() != EndElement) {
+ qWarning() << "Unexpected token type " << tokenString() << " at " << lineNumber();
+ }
+ return result;
+}
diff --git a/libs/installer_fomod/src/xmlreader.h b/libs/installer_fomod/src/xmlreader.h
new file mode 100644
index 0000000..974d3a1
--- /dev/null
+++ b/libs/installer_fomod/src/xmlreader.h
@@ -0,0 +1,40 @@
+#ifndef XMLREADER_H
+#define XMLREADER_H
+
+#include <QXmlStreamReader>
+
+class XmlReader : public QXmlStreamReader
+{
+public:
+ XmlReader(QIODevice* device) : QXmlStreamReader(device) {}
+
+ XmlReader(QByteArray array) : QXmlStreamReader(array) {}
+
+ /** Get the next token, ignoring comments and white space text */
+ TokenType readNext()
+ {
+ while (QXmlStreamReader::readNext() == Comment || isWhitespace()) {
+ continue;
+ }
+ return tokenType();
+ }
+
+ /** get the next element.
+ *
+ * \param start - the name of the current start element
+ *
+ * \returns false if no more elements
+ */
+ bool getNextElement(QString const& start);
+
+ /* Get the text associated with this token. */
+ QString getText();
+
+ /** Print a message if we get an unexpected tag */
+ void unexpected();
+
+ /** Read till the end of an element. Used for leaf nodes */
+ void finishedElement();
+};
+
+#endif // XMLREADER_H