/* 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 . */ #include "archivetree.h" #include #include #include #include #include #include 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 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(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(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(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(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(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(currentItem()), static_cast(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 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(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(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(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); }