aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_manual/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/installer_manual/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_manual/src')
-rw-r--r--libs/installer_manual/src/CMakeLists.txt17
-rw-r--r--libs/installer_manual/src/archivetree.cpp486
-rw-r--r--libs/installer_manual/src/archivetree.h178
-rw-r--r--libs/installer_manual/src/installdialog.cpp215
-rw-r--r--libs/installer_manual/src/installdialog.h124
-rw-r--r--libs/installer_manual/src/installdialog.ui162
-rw-r--r--libs/installer_manual/src/installer_manual_en.ts164
-rw-r--r--libs/installer_manual/src/installermanual.cpp136
-rw-r--r--libs/installer_manual/src/installermanual.h72
9 files changed, 1554 insertions, 0 deletions
diff --git a/libs/installer_manual/src/CMakeLists.txt b/libs/installer_manual/src/CMakeLists.txt
new file mode 100644
index 0000000..2254977
--- /dev/null
+++ b/libs/installer_manual/src/CMakeLists.txt
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 3.16)
+
+file(GLOB installer_manual_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_manual SHARED ${installer_manual_SOURCES})
+mo2_configure_plugin(installer_manual NO_SOURCES WARNINGS 4)
+target_include_directories(installer_manual PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+)
+target_link_libraries(installer_manual PRIVATE mo2::uibase)
+mo2_install_plugin(installer_manual)
diff --git a/libs/installer_manual/src/archivetree.cpp b/libs/installer_manual/src/archivetree.cpp
new file mode 100644
index 0000000..ad9319e
--- /dev/null
+++ b/libs/installer_manual/src/archivetree.cpp
@@ -0,0 +1,486 @@
+/*
+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 "archivetree.h"
+
+#include <QDebug>
+#include <QDragMoveEvent>
+#include <QMessageBox>
+
+#include <uibase/ifiletree.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+
+using namespace MOBase;
+
+// Implementation details for the ArchiveTree widget:
+//
+// The ArchiveTreeWidget presents to the user the underlying IFileTree, but in order
+// to increase performance, the tree is populated dynamically when required. Populating
+// the tree is currently required:
+// 1) when a branch of the tree widget is expanded,
+// 2) when an item is moved to a tree,
+// 3) when a directory is created,
+// 4) when a directory is "set as data root".
+//
+// Case 1 is handled automatically in the setExpanded method of ArchiveTreeWidget. Cases
+// 2 and 3 could be dealt with differently, but populating the tree before inserting an
+// item makes everything else easier (not that populating the widget is different from
+// populating the IFileTree which is done automatically). Case 4 is handled manually in
+// setDataRoot.
+//
+// Another specificity of the implementation is the treeCheckStateChanged() signal
+// emitted by the ArchiveTreeWidget. This signal is used to avoid having to connect to
+// the itemChanged() signal or overriding the dataChanged() method which are called much
+// more often than those. The treeCheckStateChanged() signal is send only for the item
+// that has actually been changed by the user. While the interface is automatically
+// updated by Qt, we need to update the underlying tree manually. This is done by doing
+// the following things:
+// 1) When an item is unchecked:
+// - We detach the corresponding entry from its parent, and recursively detach the
+// empty
+// parents (or the ones that become empty).
+// - If the entry is a directory and the item has been populated, we recursively
+// detach
+// all the child entries for all the child items that have been populated (no
+// need to do it for non-populated items)>
+// 2) When an item is checked, we do the same process but we re-attach parents and
+// re-insert
+// children.
+//
+// Detaching or re-attaching parents is also done when a directory is created (if the
+// directory is created in an empty directory, we need to re-attach), or when an item is
+// moved (if the directory the item comes from is now empty or if the target directory
+// was empty).
+//
+
+ArchiveTreeWidgetItem::ArchiveTreeWidgetItem(QString dataName)
+ : QTreeWidgetItem(QStringList(dataName)), m_Entry(nullptr)
+{
+ setFlags(flags() & ~Qt::ItemIsUserCheckable);
+ setExpanded(true);
+ m_Populated = true;
+}
+
+ArchiveTreeWidgetItem::ArchiveTreeWidgetItem(
+ std::shared_ptr<MOBase::FileTreeEntry> entry)
+ : QTreeWidgetItem(QStringList(entry->name())), m_Entry(entry)
+{
+ if (entry->isDir()) {
+ setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator);
+ setFlags(flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate);
+ } else {
+ setFlags(flags() | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren);
+ }
+ setCheckState(0, Qt::Checked);
+ setToolTip(0, entry->path());
+}
+
+void ArchiveTreeWidgetItem::setData(int column, int role, const QVariant& value)
+{
+ ArchiveTreeWidget* tree = static_cast<ArchiveTreeWidget*>(treeWidget());
+ if (tree != nullptr && tree->m_Emitter == nullptr) {
+ tree->m_Emitter = this;
+ }
+ QTreeWidgetItem::setData(column, role, value);
+ if (tree != nullptr && tree->m_Emitter == this) {
+ tree->m_Emitter = nullptr;
+ if (role == Qt::CheckStateRole) {
+ tree->onTreeCheckStateChanged(this);
+ }
+ }
+}
+
+void ArchiveTreeWidgetItem::populate(bool force)
+{
+
+ // Only populates once:
+ if (isPopulated() && !force) {
+ return;
+ }
+
+ // Should never happen:
+ if (entry()->isFile()) {
+ return;
+ }
+
+ // We go in reverse of the tree because we want to insert the original
+ // entries at the beginning (the item can only contains children if a
+ // directory has been created under it or if entries has been moved under
+ // it):
+ for (auto& entry : *entry()->astree()) {
+ auto newItem = new ArchiveTreeWidgetItem(entry);
+ newItem->setCheckState(0, flags().testFlag(Qt::ItemIsUserCheckable) ? checkState(0)
+ : Qt::Checked);
+ addChild(newItem);
+ }
+
+ // If the item is unchecked, we need to clear it because it has not been cleared
+ // before:
+ if (flags().testFlag(Qt::ItemIsUserCheckable) && checkState(0) == Qt::Unchecked) {
+ entry()->astree()->clear();
+ }
+
+ m_Populated = true;
+}
+
+ArchiveTreeWidget::ArchiveTreeWidget(QWidget* parent) : QTreeWidget(parent)
+{
+ setAutoExpandDelay(1000);
+ setDragDropOverwriteMode(true);
+ connect(this, &ArchiveTreeWidget::itemExpanded, this,
+ &ArchiveTreeWidget::populateItem);
+}
+
+void ArchiveTreeWidget::setup(QString dataFolderName)
+{
+ m_ViewRoot = new ArchiveTreeWidgetItem("<" + dataFolderName + ">");
+ m_DataRoot = nullptr;
+ addTopLevelItem(m_ViewRoot);
+}
+
+void ArchiveTreeWidget::populateItem(QTreeWidgetItem* item)
+{
+ static_cast<ArchiveTreeWidgetItem*>(item)->populate();
+}
+
+void ArchiveTreeWidget::setDataRoot(ArchiveTreeWidgetItem* const root)
+{
+ if (root != m_DataRoot) {
+ if (m_DataRoot != nullptr) {
+ m_DataRoot->addChildren(m_ViewRoot->takeChildren());
+ }
+
+ // Force populate:
+ root->populate();
+
+ m_DataRoot = root;
+ m_ViewRoot->setEntry(m_DataRoot->entry());
+ m_ViewRoot->addChildren(m_DataRoot->takeChildren());
+ m_ViewRoot->setExpanded(true);
+ }
+
+ emit treeChanged();
+}
+
+void ArchiveTreeWidget::detachParents(ArchiveTreeWidgetItem* item)
+{
+ auto entry = item->entry();
+ auto parent = entry->parent();
+ entry->detach();
+ while (parent != nullptr && parent->empty()) {
+ auto tmp = parent->parent();
+ parent->detach();
+ parent = tmp;
+ }
+}
+
+void ArchiveTreeWidget::attachParents(ArchiveTreeWidgetItem* item)
+{
+ while (item->parent() != nullptr) {
+ auto parent = static_cast<ArchiveTreeWidgetItem*>(item->parent());
+ auto parentEntry = parent->entry();
+ if (parentEntry != nullptr) {
+ parentEntry->astree()->insert(item->entry());
+ }
+ item = parent;
+ }
+}
+
+void ArchiveTreeWidget::recursiveInsert(ArchiveTreeWidgetItem* item)
+{
+ if (item->isPopulated()) {
+ auto tree = item->entry()->astree();
+ for (int i = 0; i < item->childCount(); ++i) {
+ auto child = static_cast<ArchiveTreeWidgetItem*>(item->child(i));
+ tree->insert(child->entry());
+ if (child->entry()->isDir()) {
+ recursiveInsert(child);
+ }
+ }
+ }
+}
+
+void ArchiveTreeWidget::recursiveDetach(ArchiveTreeWidgetItem* item)
+{
+ if (item->isPopulated()) {
+ for (int i = 0; i < item->childCount(); ++i) {
+ auto child = static_cast<ArchiveTreeWidgetItem*>(item->child(i));
+ if (child->entry()->isDir()) {
+ recursiveDetach(child);
+ }
+ }
+ item->entry()->astree()->clear();
+ }
+}
+
+ArchiveTreeWidgetItem* ArchiveTreeWidget::addDirectory(ArchiveTreeWidgetItem* item,
+ QString name)
+{
+ auto tree = item->entry()->astree();
+ auto* newItem = new ArchiveTreeWidgetItem(tree->addDirectory(name));
+
+ // find the insert position
+ auto it = std::find_if(tree->begin(), tree->end(), [name](auto&& entry) {
+ return entry->compare(name) == 0;
+ });
+ int index = it - tree->begin();
+ MOBase::log::debug("insert at: {}", index);
+ item->insertChild(index, newItem);
+
+ newItem->setCheckState(0, Qt::Checked);
+ attachParents(item);
+ emit treeChanged();
+
+ return newItem;
+}
+
+void ArchiveTreeWidget::moveItem(ArchiveTreeWidgetItem* source,
+ ArchiveTreeWidgetItem* target)
+{
+ // just insert the source in the target.
+ auto tree = target->entry()->astree();
+
+ detachParents(source);
+
+ // check if an entry exists with the same name, we check
+ // in the tree widget to find unchecked items
+ for (int i = 0; i < target->childCount(); ++i) {
+ auto* child = target->child(i);
+ if (child->entry()->compare(source->entry()->name()) == 0) {
+ // remove existing file and force check existing directory
+ if (child->entry()->isFile()) {
+ target->removeChild(child);
+ } else {
+ child->setCheckState(0, Qt::Checked);
+ }
+ break;
+ }
+ }
+
+ tree->insert(source->entry(), IFileTree::InsertPolicy::MERGE);
+
+ attachParents(target);
+
+ emit treeChanged();
+}
+
+void ArchiveTreeWidget::onTreeCheckStateChanged(ArchiveTreeWidgetItem* item)
+{
+
+ auto entry = item->entry();
+
+ // If the entry is a directory, we need to either detach or re-attach all the
+ // children. It is not possible to only detach the directory because if the
+ // user uncheck a directory and then check a file under it, the other files would
+ // still be attached.
+ //
+ // The two recursive methods only go down to the expanded (based on isPopulated()
+ // tree, for two reasons:
+ // 1. If a tree item has not been populated, then detaching an entry from its parent
+ // will
+ // delete it since there would be no remaining shared pointers.
+ // 2. If the tree has not been populated yet, all the entries under it are still
+ // attached,
+ // so there is no need to process them differently. Detaching a non-expanded item
+ // can be done by simply detaching the tree, no need to detach all the children.
+ if (entry->isDir()) {
+ if (item->checkState(0) == Qt::Checked && item->isPopulated()) {
+ recursiveInsert(item);
+ } else if (item->checkState(0) == Qt::Unchecked && item->isPopulated()) {
+ recursiveDetach(item);
+ }
+ }
+
+ // Unchecked: we go up the parent chain removing all trees that are now empty:
+ if (item->checkState(0) == Qt::Unchecked) {
+ detachParents(item);
+ }
+ // Otherwize, we need to-reattach the parent:
+ else {
+ attachParents(item);
+ }
+
+ emit treeChanged();
+}
+
+bool ArchiveTreeWidget::testMovePossible(ArchiveTreeWidgetItem* source,
+ ArchiveTreeWidgetItem* target)
+{
+ if (target == nullptr || source == nullptr) {
+ return false;
+ }
+
+ if (target->flags().testFlag(Qt::ItemNeverHasChildren)) {
+ return false;
+ }
+
+ if (source == target || source->parent() == target) {
+ return false;
+ }
+
+ return true;
+}
+
+void ArchiveTreeWidget::dragEnterEvent(QDragEnterEvent* event)
+{
+ QTreeWidgetItem* source = this->currentItem();
+ if ((source == nullptr) || (source->parent() == nullptr)) {
+ // can't change top level
+ event->ignore();
+ return;
+ } else {
+ QTreeWidget::dragEnterEvent(event);
+ }
+}
+
+void ArchiveTreeWidget::dragMoveEvent(QDragMoveEvent* event)
+{
+ if (!testMovePossible(
+ static_cast<ArchiveTreeWidgetItem*>(currentItem()),
+ static_cast<ArchiveTreeWidgetItem*>(itemAt(event->position().toPoint())))) {
+ event->ignore();
+ } else {
+ QTreeWidget::dragMoveEvent(event);
+ }
+}
+
+static bool isAncestor(const QTreeWidgetItem* ancestor, const QTreeWidgetItem* item)
+{
+ QTreeWidgetItem* iter = item->parent();
+ while (iter != nullptr) {
+ if (iter == ancestor) {
+ return true;
+ }
+ iter = iter->parent();
+ }
+ return false;
+}
+
+void ArchiveTreeWidget::refreshItem(ArchiveTreeWidgetItem* item)
+{
+ if (!item->isPopulated() || item->flags().testFlag(Qt::ItemNeverHasChildren)) {
+ return;
+ }
+
+ // at this point, all child items are checked for we only remember the ones
+ // that were expanded to re-expand them
+ std::map<QString, bool, MOBase::FileNameComparator> expanded;
+ while (item->childCount() > 0) {
+ auto* child = item->child(0);
+ expanded[child->entry()->name()] = child->isExpanded();
+ item->removeChild(child);
+ }
+
+ item->populate(true);
+
+ for (int i = 0; i < item->childCount(); ++i) {
+ auto* child = item->child(i);
+ if (expanded[child->entry()->name()]) {
+ child->setExpanded(true);
+ }
+ }
+}
+
+void ArchiveTreeWidget::dropEvent(QDropEvent* event)
+{
+ event->ignore();
+
+ // target widget (should be a directory)
+ auto* target =
+ static_cast<ArchiveTreeWidgetItem*>(itemAt(event->position().toPoint()));
+
+ // this should not really happen because it is prevent by dragMoveEvent
+ if (target->flags().testFlag(Qt::ItemNeverHasChildren)) {
+
+ // this should really not happen, how should a file get to the top level?
+ if (target->parent() == nullptr) {
+ return;
+ }
+
+ target = target->parent();
+ }
+
+ // populate target if required
+ target->populate();
+
+ auto sourceItems = this->selectedItems();
+
+ // check the selected items - we do not want to move only
+ // some items so we check everything first and then move
+ for (auto* source : sourceItems) {
+
+ auto* aSource = static_cast<ArchiveTreeWidgetItem*>(source);
+
+ // do not allow element to be dropped into one of its
+ // own child
+ if (isAncestor(source, target)) {
+ event->accept();
+ QMessageBox::warning(parentWidget(), tr("Cannot drop"),
+ tr("Cannot drop '%1' into one of its subfolder.")
+ .arg(aSource->entry()->name()));
+ return;
+ }
+
+ auto sourceEntry = aSource->entry();
+ auto targetEntry = target->entry()->astree()->find(sourceEntry->name());
+ if (targetEntry && targetEntry->fileType() != sourceEntry->fileType()) {
+ event->accept();
+ QMessageBox::warning(parentWidget(), tr("Cannot drop"),
+ targetEntry->isFile()
+ ? tr("A file '%1' already exists in folder '%2'.")
+ .arg(sourceEntry->name())
+ .arg(target->entry()->name())
+ : tr("A folder '%1' already exists in folder '%2'.")
+ .arg(sourceEntry->name())
+ .arg(target->entry()->name()));
+ return;
+ }
+ }
+
+ for (auto* source : sourceItems) {
+
+ auto* aSource = static_cast<ArchiveTreeWidgetItem*>(source);
+
+ // this only check dropping an item on itself or dropping an item in
+ // its parent so it is ok, it just does not do anything
+ if (source->parent() == nullptr || !testMovePossible(aSource, target)) {
+ continue;
+ }
+
+ // force expand item that are going to be merged
+ for (int i = 0; i < target->childCount(); ++i) {
+ auto* child = target->child(i);
+ if (child->entry()->compare(aSource->entry()->name()) == 0 &&
+ !child->flags().testFlag(Qt::ItemNeverHasChildren)) {
+ child->setExpanded(true);
+ }
+ }
+
+ // remove the source from its parent
+ source->parent()->removeChild(source);
+
+ // actually perform the move on the underlying tree model
+ moveItem(aSource, target);
+ }
+
+ // refresh the target item - this assumes that itemMoved is called synchronously
+ // and perform the FileTree changes
+ refreshItem(target);
+}
diff --git a/libs/installer_manual/src/archivetree.h b/libs/installer_manual/src/archivetree.h
new file mode 100644
index 0000000..156966c
--- /dev/null
+++ b/libs/installer_manual/src/archivetree.h
@@ -0,0 +1,178 @@
+/*
+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/>.
+*/
+
+#ifndef ARCHIVETREE_H
+#define ARCHIVETREE_H
+
+#include <QTreeWidget>
+
+#include <uibase/ifiletree.h>
+
+class ArchiveTreeWidget;
+
+// custom tree widget that holds a shared pointer to the file tree entry
+// they represent
+//
+class ArchiveTreeWidgetItem : public QTreeWidgetItem
+{
+public:
+ ArchiveTreeWidgetItem(QString dataName);
+ ArchiveTreeWidgetItem(std::shared_ptr<MOBase::FileTreeEntry> entry);
+
+public:
+ // populate this tree widget item if it has not been populated yet
+ // or if force is true
+ //
+ void populate(bool force = false);
+
+ // check if this item has already been populated
+ //
+ bool isPopulated() const { return m_Populated; }
+
+ // replace the entry corresponding to this item
+ //
+ void setEntry(std::shared_ptr<MOBase::FileTreeEntry> entry) { m_Entry = entry; }
+
+ // retrieve the entry corresponding to this item
+ //
+ std::shared_ptr<MOBase::FileTreeEntry> entry() const { return m_Entry; }
+
+ // overriden method to avoid propagating dataChanged events
+ //
+ void setData(int column, int role, const QVariant& value) override;
+
+ ArchiveTreeWidgetItem* parent() const
+ {
+ return static_cast<ArchiveTreeWidgetItem*>(QTreeWidgetItem::parent());
+ }
+
+ ArchiveTreeWidgetItem* child(int index) const
+ {
+ return static_cast<ArchiveTreeWidgetItem*>(QTreeWidgetItem::child(index));
+ }
+
+protected:
+ std::shared_ptr<MOBase::FileTreeEntry> m_Entry;
+ bool m_Populated = false;
+
+ friend class ArchiveTreeWidget;
+};
+
+// Qt tree widget used to display the content of an archive in the manual installation
+// dialog
+class ArchiveTreeWidget : public QTreeWidget
+{
+
+ Q_OBJECT
+
+public:
+ explicit ArchiveTreeWidget(QWidget* parent = 0);
+ void setup(QString dataFolderName);
+
+public:
+ // set the data root widget
+ //
+ void setDataRoot(ArchiveTreeWidgetItem* const root);
+
+ // create a directory under the given tree item, without
+ // performing any check
+ //
+ ArchiveTreeWidgetItem* addDirectory(ArchiveTreeWidgetItem* treeItem, QString name);
+
+ // return the root of the tree (the item corresponding to <data>)
+ //
+ ArchiveTreeWidgetItem* root() const { return m_ViewRoot; }
+
+signals:
+
+ // emitted when the tree has been modified
+ //
+ void treeChanged();
+
+public slots:
+
+protected:
+ // detach the entry of this item from its parent, and recursively detach
+ // all of its parent if they become
+ //
+ void detachParents(ArchiveTreeWidgetItem* item);
+
+ // re-attach the entry of this item to its parent, and recursively attach
+ // all of its parent if they were empty (and thus detached)
+ //
+ void attachParents(ArchiveTreeWidgetItem* item);
+
+ // recursively re-insert all the entries below the given item in their
+ // corresponding parents
+ //
+ // this method does not recurse in items that have not been populated yet
+ //
+ void recursiveInsert(ArchiveTreeWidgetItem* item);
+
+ // recursively detach all the entries below the given item from their
+ // corresponding parents
+ //
+ // this method does not recurse in items that have not been populated yet
+ //
+ void recursiveDetach(ArchiveTreeWidgetItem* item);
+
+ // slot that trigger the given item to be populated if it has not already
+ // been
+ //
+ void populateItem(QTreeWidgetItem* item);
+
+ // move the source under the target
+ //
+ void moveItem(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target);
+
+ // called when the state of the item changed - unlike the standard QTreeWidget,
+ // this is only called for the actual item, not its parent/children
+ //
+ void onTreeCheckStateChanged(ArchiveTreeWidgetItem* item);
+
+ void dragEnterEvent(QDragEnterEvent* event) override;
+ void dragMoveEvent(QDragMoveEvent* event) override;
+ void dropEvent(QDropEvent* event) override;
+
+private:
+ bool testMovePossible(ArchiveTreeWidgetItem* source, ArchiveTreeWidgetItem* target);
+
+ // refresh the given item (after a drop)
+ //
+ void refreshItem(ArchiveTreeWidgetItem* item);
+
+ // the widget item that emitted the dataChanged event
+ ArchiveTreeWidgetItem* m_Emitter = nullptr;
+
+ // IMPORTANT: if you intend to work on this and understand this, read the detailed
+ // explanation at the beginning of the archivetree.cpp file
+ //
+ // - the data root is the real widget of the current data, this widget
+ // is not the real root that is added to the tree
+ // - the view root is the actual tree in the widget (should be const but cannot be
+ // since
+ // the parent tree cannot be consstructed in the member initializer list)
+ //
+ ArchiveTreeWidgetItem* m_DataRoot;
+ ArchiveTreeWidgetItem* m_ViewRoot;
+
+ friend class ArchiveTreeWidgetItem;
+};
+
+#endif // ARCHIVETREE_H
diff --git a/libs/installer_manual/src/installdialog.cpp b/libs/installer_manual/src/installdialog.cpp
new file mode 100644
index 0000000..a6e96b4
--- /dev/null
+++ b/libs/installer_manual/src/installdialog.cpp
@@ -0,0 +1,215 @@
+/*
+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 "installdialog.h"
+#include "ui_installdialog.h"
+
+#include <QCompleter>
+#include <QInputDialog>
+#include <QMenu>
+#include <QMessageBox>
+#include <QMetaType>
+
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <uibase/utility.h>
+
+using namespace MOBase;
+
+InstallDialog::InstallDialog(
+ std::shared_ptr<IFileTree> tree, const GuessedValue<QString>& modName,
+ std::shared_ptr<const MOBase::ModDataChecker> modDataChecker,
+ const QString& dataName, QWidget* parent)
+ : TutorableDialog("InstallDialog", parent), ui(new Ui::InstallDialog),
+ m_Checker(modDataChecker), m_DataFolderName(dataName)
+{
+
+ ui->setupUi(this);
+
+ for (auto iter = modName.variants().begin(); iter != modName.variants().end();
+ ++iter) {
+ ui->nameCombo->addItem(*iter);
+ }
+
+ ui->nameCombo->setCurrentIndex(ui->nameCombo->findText(modName));
+ ui->nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive);
+
+ m_ProblemLabel = ui->problemLabel;
+
+ m_Tree = ui->treeContent;
+ m_TreeRoot = new ArchiveTreeWidgetItem(tree);
+ m_Tree->setup(m_DataFolderName);
+ connect(m_Tree, &ArchiveTreeWidget::treeChanged, [this] {
+ updateProblems();
+ });
+
+ m_Tree->setDataRoot(m_TreeRoot);
+}
+
+InstallDialog::~InstallDialog()
+{
+ delete ui;
+}
+
+QString InstallDialog::getModName() const
+{
+ return ui->nameCombo->currentText();
+}
+
+/**
+ * @brief Retrieve the user-modified directory structure.
+ *
+ * @return the new tree represented by this dialog, which can be a new
+ * tree or a subtree of the original tree.
+ **/
+std::shared_ptr<MOBase::IFileTree> InstallDialog::getModifiedTree() const
+{
+ return m_Tree->root()->entry()->astree();
+}
+
+bool InstallDialog::testForProblem()
+{
+ if (!m_Checker) {
+ return true;
+ }
+ return m_Checker->dataLooksValid(m_Tree->root()->entry()->astree()) ==
+ ModDataChecker::CheckReturn::VALID;
+}
+
+void InstallDialog::updateProblems()
+{
+ if (!m_Checker) {
+ m_Tree->setStyleSheet("QTreeWidget { border: none; }");
+ m_ProblemLabel->setText(
+ tr("Cannot check the content of <%1>.").arg(m_DataFolderName));
+ m_ProblemLabel->setToolTip(tr("The plugin for the current game does not provide a "
+ "way to check the content of <%1>.")
+ .arg(m_DataFolderName));
+ m_ProblemLabel->setStyleSheet("color: darkYellow;");
+ } else if (testForProblem()) {
+ m_Tree->setStyleSheet(
+ "QTreeWidget { border: 1px solid darkGreen; border-radius: 2px; }");
+ m_ProblemLabel->setText(
+ tr("The content of <%1> looks valid.").arg(m_DataFolderName));
+ m_ProblemLabel->setToolTip(
+ tr("The content of <%1> seems valid for the current game.")
+ .arg(m_DataFolderName));
+ m_ProblemLabel->setStyleSheet("color: darkGreen;");
+ } else {
+ m_Tree->setStyleSheet("QTreeWidget { border: 1px solid red; border-radius: 2px; }");
+ m_ProblemLabel->setText(
+ tr("The content of <%1> does not look valid.").arg(m_DataFolderName));
+ m_ProblemLabel->setToolTip(
+ tr("The content of <%1> is probably not valid for the current game.")
+ .arg(m_DataFolderName));
+ m_ProblemLabel->setStyleSheet("color: red;");
+ }
+}
+
+void InstallDialog::createDirectoryUnder(ArchiveTreeWidgetItem* item)
+{
+ // Should never happen if we customize the context menu depending
+ // on the item:
+ if (!item->entry()->isDir()) {
+ reportError(tr("Cannot create directory under a file."));
+ return;
+ }
+
+ // Retrieve the directory:
+ auto fileTree = item->entry()->astree();
+
+ bool ok = false;
+ QString result = QInputDialog::getText(this, tr("Enter a directory name"), tr("Name"),
+ QLineEdit::Normal, QString(), &ok);
+ result = result.trimmed();
+
+ if (ok && !result.isEmpty()) {
+
+ // If a file with this name already exists:
+ if (fileTree->exists(result)) {
+ reportError(tr("A directory or file with that name already exists."));
+ return;
+ }
+
+ item->setExpanded(true);
+ auto* newItem = m_Tree->addDirectory(item, result);
+ m_Tree->scrollToItem(newItem);
+ }
+}
+
+void InstallDialog::on_treeContent_customContextMenuRequested(QPoint pos)
+{
+ ArchiveTreeWidgetItem* selectedItem =
+ static_cast<ArchiveTreeWidgetItem*>(m_Tree->itemAt(pos));
+ if (selectedItem == nullptr) {
+ return;
+ }
+
+ QMenu menu;
+
+ if (selectedItem != m_Tree->root() && selectedItem->entry()->isDir()) {
+ menu.addAction(tr("Set as <%1> directory").arg(m_DataFolderName),
+ [this, selectedItem]() {
+ m_Tree->setDataRoot(selectedItem);
+ });
+ }
+
+ if (m_Tree->root()->entry() != m_TreeRoot->entry()) {
+ menu.addAction(tr("Unset <%1> directory").arg(m_DataFolderName), [this]() {
+ m_Tree->setDataRoot(m_TreeRoot);
+ });
+ }
+
+ // Add a separator if not empty:
+ if (!menu.isEmpty()) {
+ menu.addSeparator();
+ }
+
+ if (selectedItem->entry()->isDir()) {
+ menu.addAction(tr("Create directory..."), [this, selectedItem]() {
+ createDirectoryUnder(selectedItem);
+ });
+ } else {
+ menu.addAction(tr("&Open"), [this, selectedItem]() {
+ emit openFile(selectedItem->entry().get());
+ });
+ }
+ menu.exec(m_Tree->mapToGlobal(pos));
+}
+
+void InstallDialog::on_okButton_clicked()
+{
+ if (!testForProblem()) {
+ if (QMessageBox::question(
+ this, tr("Continue?"),
+ tr("This mod was probably NOT set up correctly, most likely it will NOT "
+ "work. "
+ "You should first correct the directory layout using the content-tree."),
+ QMessageBox::Ignore | QMessageBox::Cancel,
+ QMessageBox::Cancel) == QMessageBox::Cancel) {
+ return;
+ }
+ }
+ this->accept();
+}
+
+void InstallDialog::on_cancelButton_clicked()
+{
+ this->reject();
+}
diff --git a/libs/installer_manual/src/installdialog.h b/libs/installer_manual/src/installdialog.h
new file mode 100644
index 0000000..96e4085
--- /dev/null
+++ b/libs/installer_manual/src/installdialog.h
@@ -0,0 +1,124 @@
+/*
+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/>.
+*/
+
+#ifndef INSTALLDIALOG_H
+#define INSTALLDIALOG_H
+
+#include <QDialog>
+#include <QProgressDialog>
+#include <QTreeWidgetItem>
+#include <QUuid>
+
+#include <uibase/game_features/moddatachecker.h>
+#include <uibase/guessedvalue.h>
+#include <uibase/ifiletree.h>
+#include <uibase/iplugingame.h>
+#include <uibase/tutorabledialog.h>
+
+#include "archivetree.h"
+
+namespace Ui
+{
+class InstallDialog;
+}
+
+/**
+ * a dialog presented to manually define how a mod is to be installed. It provides
+ * a tree view of the file contents that can modified directly
+ **/
+class InstallDialog : public MOBase::TutorableDialog
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @brief Create a new install dialog for the given tree. The tree
+ * is "own" by the dialog, i.e., any change made by the user is immediately
+ * reflected to the given tree, except for the changes to the root.
+ *
+ * @param tree Tree structure describing the original archive structure.
+ * @param modName Name of the mod. The name can be modified through the dialog.
+ * @param modDataChecker The mod data checker to use to check.
+ * @param dataName The name of the data folder for the game.
+ * @param parent Parent widget.
+ **/
+ explicit InstallDialog(std::shared_ptr<MOBase::IFileTree> tree,
+ const MOBase::GuessedValue<QString>& modName,
+ std::shared_ptr<const MOBase::ModDataChecker> modDataChecker,
+ const QString& dataName, QWidget* parent = 0);
+ ~InstallDialog();
+
+ /**
+ * @brief retrieve the (modified) mod name
+ *
+ * @return updated mod name
+ **/
+ QString getModName() const;
+
+ /**
+ * @brief Retrieve the user-modified directory structure.
+ *
+ * @return the new tree represented by this dialog, which can be a new
+ * tree or a subtree of the original tree.
+ **/
+ std::shared_ptr<MOBase::IFileTree> getModifiedTree() const;
+
+signals:
+
+ /**
+ * @brief Signal emitted when user request the file corresponding
+ * to the given entry to be opened.
+ *
+ * @param entry Entry corresponding to the file to open.
+ */
+ void openFile(const MOBase::FileTreeEntry* entry);
+
+private:
+ bool testForProblem();
+ void updateProblems();
+ void createDirectoryUnder(ArchiveTreeWidgetItem* treeItem);
+
+private slots:
+
+ // Automatic slots that are directly bound to the UI:
+ void on_treeContent_customContextMenuRequested(QPoint pos);
+ void on_cancelButton_clicked();
+ void on_okButton_clicked();
+
+private:
+ Ui::InstallDialog* ui;
+
+ std::shared_ptr<const MOBase::ModDataChecker> m_Checker;
+
+ // Name of the "data" directory:
+ QString m_DataFolderName;
+
+ // the tree root is the initial root that will never change (should be const
+ // but cannot be since the parent tree cannot be constructed in the member
+ // initializer list)
+ //
+ // the tree root is not actually added to the tree, but is used to maintain
+ // the state of the tree and not lose entries when unsetting data root
+ //
+ ArchiveTreeWidget* m_Tree;
+ ArchiveTreeWidgetItem* m_TreeRoot;
+ QLabel* m_ProblemLabel;
+};
+
+#endif // INSTALLDIALOG_H
diff --git a/libs/installer_manual/src/installdialog.ui b/libs/installer_manual/src/installdialog.ui
new file mode 100644
index 0000000..49a7fdc
--- /dev/null
+++ b/libs/installer_manual/src/installdialog.ui
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>InstallDialog</class>
+ <widget class="QDialog" name="InstallDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>516</width>
+ <height>407</height>
+ </rect>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Install Mods</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="acceptDrops">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string/>
+ </property>
+ <property name="title">
+ <string/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="minimumSize">
+ <size>
+ <width>50</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="nameCombo">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Content</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="ArchiveTreeWidget" name="treeContent">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>Content of the archive. You can change the directory structure by using drag&amp;drop. Hint: Also try right clicking...</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This displays the content of the archive. &amp;lt;data&amp;gt; represents the base directory which will map to the game's data directory. You can change the base directory via the right-click context menu and you can move around files via drag&amp;amp;drop&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="locale">
+ <locale language="English" country="UnitedStates"/>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ <column>
+ <property name="text">
+ <string notr="true">1</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="problemLabel">
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Placeholder</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="okButton">
+ <property name="text">
+ <string>OK</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cancelButton">
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ArchiveTreeWidget</class>
+ <extends>QTreeWidget</extends>
+ <header>archivetree.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/installer_manual/src/installer_manual_en.ts b/libs/installer_manual/src/installer_manual_en.ts
new file mode 100644
index 0000000..3bc22c1
--- /dev/null
+++ b/libs/installer_manual/src/installer_manual_en.ts
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>ArchiveTreeWidget</name>
+ <message>
+ <location filename="archivetree.cpp" line="435"/>
+ <location filename="archivetree.cpp" line="445"/>
+ <source>Cannot drop</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="archivetree.cpp" line="436"/>
+ <source>Cannot drop &apos;%1&apos; into one of its subfolder.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="archivetree.cpp" line="447"/>
+ <source>A file &apos;%1&apos; already exists in folder &apos;%2&apos;.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="archivetree.cpp" line="450"/>
+ <source>A folder &apos;%1&apos; already exists in folder &apos;%2&apos;.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>InstallDialog</name>
+ <message>
+ <location filename="installdialog.ui" line="20"/>
+ <source>Install Mods</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.ui" line="46"/>
+ <location filename="installdialog.cpp" line="138"/>
+ <source>Name</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.ui" line="62"/>
+ <source>Content</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.ui" line="72"/>
+ <source>Content of the archive. You can change the directory structure by using drag&amp;drop. Hint: Also try right clicking...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.ui" line="75"/>
+ <source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This displays the content of the archive. &amp;lt;data&amp;gt; represents the base directory which will map to the game&apos;s data directory. You can change the base directory via the right-click context menu and you can move around files via drag&amp;amp;drop&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.ui" line="118"/>
+ <source>Placeholder</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.ui" line="138"/>
+ <source>OK</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.ui" line="145"/>
+ <source>Cancel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="138"/>
+ <source>Enter a directory name</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="100"/>
+ <source>Cannot check the content of &lt;%1&gt;.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="101"/>
+ <source>The plugin for the current game does not provide a way to check the content of &lt;%1&gt;.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="109"/>
+ <source>The content of &lt;%1&gt; looks valid.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="111"/>
+ <source>The content of &lt;%1&gt; seems valid for the current game.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="117"/>
+ <source>The content of &lt;%1&gt; does not look valid.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="119"/>
+ <source>The content of &lt;%1&gt; is probably not valid for the current game.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="130"/>
+ <source>Cannot create directory under a file.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="146"/>
+ <source>A directory or file with that name already exists.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="167"/>
+ <source>Set as &lt;%1&gt; directory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="174"/>
+ <source>Unset &lt;%1&gt; directory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="185"/>
+ <source>Create directory...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="189"/>
+ <source>&amp;Open</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="200"/>
+ <source>Continue?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installdialog.cpp" line="201"/>
+ <source>This mod was probably NOT set up correctly, most likely it will NOT work. You should first correct the directory layout using the content-tree.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>InstallerManual</name>
+ <message>
+ <location filename="installermanual.cpp" line="52"/>
+ <source>Manual Installer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installermanual.cpp" line="62"/>
+ <source>Fallback installer for mods that can be extracted but can&apos;t be handled by another installer</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/installer_manual/src/installermanual.cpp b/libs/installer_manual/src/installermanual.cpp
new file mode 100644
index 0000000..278b5f2
--- /dev/null
+++ b/libs/installer_manual/src/installermanual.cpp
@@ -0,0 +1,136 @@
+/*
+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 "installermanual.h"
+
+#include <QDialog>
+#include <QDesktopServices>
+#include <QUrl>
+#include <QtPlugin>
+
+#include <uibase/game_features/igamefeatures.h>
+#include <uibase/game_features/moddatachecker.h>
+#include <uibase/iinstallationmanager.h>
+#include <uibase/iplugingame.h>
+#include <uibase/utility.h>
+
+#include "installdialog.h"
+
+using namespace MOBase;
+
+InstallerManual::InstallerManual() : m_MOInfo(nullptr) {}
+
+bool InstallerManual::init(IOrganizer* moInfo)
+{
+ m_MOInfo = moInfo;
+ return true;
+}
+
+QString InstallerManual::name() const
+{
+ return "Manual Installer";
+}
+
+QString InstallerManual::localizedName() const
+{
+ return tr("Manual Installer");
+}
+
+QString InstallerManual::author() const
+{
+ return "Tannin, Holt59";
+}
+
+QString InstallerManual::description() const
+{
+ return tr("Fallback installer for mods that can be extracted but can't be handled by "
+ "another installer");
+}
+
+VersionInfo InstallerManual::version() const
+{
+ return VersionInfo(1, 0, 1, VersionInfo::RELEASE_FINAL);
+}
+
+QList<PluginSetting> InstallerManual::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+unsigned int InstallerManual::priority() const
+{
+ return 0;
+}
+
+bool InstallerManual::isManualInstaller() const
+{
+ return true;
+}
+
+bool InstallerManual::isArchiveSupported(std::shared_ptr<const MOBase::IFileTree>) const
+{
+ return true;
+}
+
+void InstallerManual::openFile(const FileTreeEntry* entry)
+{
+ QString tempName = manager()->extractFile(entry->shared_from_this());
+
+#ifdef _WIN32
+ SHELLEXECUTEINFOW execInfo;
+ memset(&execInfo, 0, sizeof(SHELLEXECUTEINFOW));
+ execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
+ execInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
+ execInfo.lpVerb = L"open";
+ std::wstring fileNameW = ToWString(tempName);
+ execInfo.lpFile = fileNameW.c_str();
+ execInfo.nShow = SW_SHOWNORMAL;
+ if (!::ShellExecuteExW(&execInfo)) {
+ qCritical("failed to spawn %s: %d", qUtf8Printable(tempName), ::GetLastError());
+ }
+#else
+ if (!QDesktopServices::openUrl(QUrl::fromLocalFile(tempName))) {
+ qCritical("failed to open %s", qUtf8Printable(tempName));
+ }
+#endif
+}
+
+IPluginInstaller::EInstallResult
+InstallerManual::install(GuessedValue<QString>& modName,
+ std::shared_ptr<MOBase::IFileTree>& tree, QString&, int&)
+{
+ qDebug("offering installation dialog");
+ InstallDialog dialog(
+ tree, modName, m_MOInfo->gameFeatures()->gameFeature<ModDataChecker>(),
+ m_MOInfo->managedGame()->dataDirectory().dirName().toLower(), parentWidget());
+ connect(&dialog, &InstallDialog::openFile, this, &InstallerManual::openFile);
+ if (dialog.exec() == QDialog::Accepted) {
+ modName.update(dialog.getModName(), GUESS_USER);
+
+ // TODO probably more complicated than necessary
+ tree = dialog.getModifiedTree();
+ return IPluginInstaller::RESULT_SUCCESS;
+ } else {
+ return IPluginInstaller::RESULT_CANCELED;
+ }
+}
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
+Q_EXPORT_PLUGIN2(installerManual, InstallerManual)
+#endif
diff --git a/libs/installer_manual/src/installermanual.h b/libs/installer_manual/src/installermanual.h
new file mode 100644
index 0000000..78bbd7f
--- /dev/null
+++ b/libs/installer_manual/src/installermanual.h
@@ -0,0 +1,72 @@
+/*
+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/>.
+*/
+
+#ifndef INSTALLERMANUAL_H
+#define INSTALLERMANUAL_H
+
+#include <uibase/imoinfo.h>
+#include <uibase/iplugininstallersimple.h>
+
+class InstallerManual : public MOBase::IPluginInstallerSimple
+{
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple)
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+ Q_PLUGIN_METADATA(IID "org.tannin.InstallerManual")
+#endif
+
+public:
+ InstallerManual();
+
+ 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;
+ virtual bool isManualInstaller() const;
+
+ virtual bool isArchiveSupported(std::shared_ptr<const MOBase::IFileTree> tree) const;
+ virtual EInstallResult install(MOBase::GuessedValue<QString>& modName,
+ std::shared_ptr<MOBase::IFileTree>& tree,
+ QString& version, int& modID);
+
+private:
+ bool
+ isSimpleArchiveTopLayer(const std::shared_ptr<const MOBase::IFileTree> tree) const;
+ std::shared_ptr<const MOBase::IFileTree>
+ getSimpleArchiveBase(const std::shared_ptr<const MOBase::IFileTree> tree) const;
+
+private slots:
+
+ /**
+ * @brief Opens a file from the archive in the (system-)default editor/viewer.
+ *
+ * @param entry Entry corresponding to the file to open.
+ */
+ void openFile(const MOBase::FileTreeEntry* entry);
+
+private:
+ const MOBase::IOrganizer* m_MOInfo;
+};
+
+#endif // INSTALLERMANUAL_H