diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /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')
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><a href="#">Link</a></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"><image name></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><a href="#">Link</a></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 'OR' clause!</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="fomodinstallerdialog.cpp" line="405"/> + <source>All conditions were successful in an 'AND' clause!</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="fomodinstallerdialog.cpp" line="408"/> + <source>No conditions were successful in an 'OR' 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 '%1' was marked %2.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="fomodinstallerdialog.cpp" line="438"/> + <source>Missing requirement: The file '%1' 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 '%1'.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="fomodinstallerdialog.cpp" line="536"/> + <source>File '%1'.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="fomodinstallerdialog.cpp" line="680"/> + <source><a href="%1">Link</a></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 '%1' matched '%2'</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="fomodinstallerdialog.cpp" line="1386"/> + <source>The flag '%1' did not match '%2'</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 Binary files differnew file mode 100644 index 0000000..95c8149 --- /dev/null +++ b/libs/installer_fomod/src/resources/CloseButtonIcon.png diff --git a/libs/installer_fomod/src/resources/LeftButtonIcon.png b/libs/installer_fomod/src/resources/LeftButtonIcon.png Binary files differnew file mode 100644 index 0000000..181ca52 --- /dev/null +++ b/libs/installer_fomod/src/resources/LeftButtonIcon.png diff --git a/libs/installer_fomod/src/resources/RightButtonIcon.png b/libs/installer_fomod/src/resources/RightButtonIcon.png Binary files differnew file mode 100644 index 0000000..bd8d5b7 --- /dev/null +++ b/libs/installer_fomod/src/resources/RightButtonIcon.png 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 |
