aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_manual/src/archivetree.cpp
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/archivetree.cpp
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/archivetree.cpp')
-rw-r--r--libs/installer_manual/src/archivetree.cpp486
1 files changed, 486 insertions, 0 deletions
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);
+}