summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/CMakeLists.txt6
-rw-r--r--src/datatab.cpp3
-rw-r--r--src/filetree.cpp1091
-rw-r--r--src/filetree.h176
-rw-r--r--src/filetreeitem.cpp222
-rw-r--r--src/filetreeitem.h78
-rw-r--r--src/filetreemodel.cpp872
-rw-r--r--src/filetreemodel.h101
8 files changed, 1291 insertions, 1258 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 38b4fac6..5199954d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -146,6 +146,8 @@ SET(organizer_SRCS
filterlist.cpp
datatab.cpp
filetree.cpp
+ filetreemodel.cpp
+ filetreeitem.cpp
iconfetcher.cpp
shared/windows_error.cpp
@@ -273,6 +275,8 @@ SET(organizer_HDRS
filterlist.h
datatab.h
filetree.h
+ filetreeitem.h
+ filetreemodel.h
iconfetcher.h
shared/windows_error.h
@@ -407,6 +411,8 @@ set(mainwindow
datatab
iconfetcher
filetree
+ filetreeitem
+ filetreemodel
filterlist
mainwindow
statusbar
diff --git a/src/datatab.cpp b/src/datatab.cpp
index 7e60ca0f..95c4eca3 100644
--- a/src/datatab.cpp
+++ b/src/datatab.cpp
@@ -5,6 +5,7 @@
#include "directoryentry.h"
#include "messagedialog.h"
#include "filetree.h"
+#include "filetreemodel.h"
#include <log.h>
#include <report.h>
@@ -162,6 +163,6 @@ void DataTab::updateOptions()
flags |= FileTreeModel::Archives;
}
- m_filetree->setFlags(flags);
+ m_filetree->model()->setFlags(flags);
refreshDataTree();
}
diff --git a/src/filetree.cpp b/src/filetree.cpp
index b9741a12..5e5debc5 100644
--- a/src/filetree.cpp
+++ b/src/filetree.cpp
@@ -1,14 +1,12 @@
#include "filetree.h"
+#include "filetreemodel.h"
+#include "filetreeitem.h"
#include "organizercore.h"
-#include "modinfodialogfwd.h"
#include <log.h>
using namespace MOShared;
using namespace MOBase;
-// in mainwindow.cpp
-QString UnmanagedModName();
-
bool canPreviewFile(const PluginContainer& pc, const FileEntry& file)
{
@@ -118,1087 +116,6 @@ private:
};
-FileTreeItem::FileTreeItem()
- : m_flags(NoFlags), m_loaded(false)
-{
-}
-
-FileTreeItem::FileTreeItem(
- FileTreeItem* parent, int originID,
- std::wstring dataRelativeParentPath, std::wstring realPath, Flags flags,
- std::wstring file, std::wstring mod) :
- m_parent(parent), m_originID(originID),
- m_virtualParentPath(QString::fromStdWString(dataRelativeParentPath)),
- m_realPath(QString::fromStdWString(realPath)),
- m_flags(flags),
- m_file(QString::fromStdWString(file)),
- m_mod(QString::fromStdWString(mod)),
- m_loaded(false),
- m_expanded(false)
-{
-}
-
-void FileTreeItem::add(std::unique_ptr<FileTreeItem> child)
-{
- m_children.push_back(std::move(child));
-}
-
-void FileTreeItem::insert(std::unique_ptr<FileTreeItem> child, std::size_t at)
-{
- if (at > m_children.size()) {
- log::error(
- "{}: can't insert child {} at {}, out of range",
- debugName(), child->debugName(), at);
-
- return;
- }
-
- m_children.insert(m_children.begin() + at, std::move(child));
-}
-
-void FileTreeItem::remove(std::size_t i)
-{
- if (i >= m_children.size()) {
- log::error("{}: can't remove child at {}", debugName(), i);
- return;
- }
-
- m_children.erase(m_children.begin() + i);
-}
-
-const std::vector<std::unique_ptr<FileTreeItem>>& FileTreeItem::children() const
-{
- return m_children;
-}
-
-FileTreeItem* FileTreeItem::parent()
-{
- return m_parent;
-}
-
-int FileTreeItem::originID() const
-{
- return m_originID;
-}
-
-const QString& FileTreeItem::virtualParentPath() const
-{
- return m_virtualParentPath;
-}
-
-QString FileTreeItem::virtualPath() const
-{
- QString s = "Data\\";
-
- if (!m_virtualParentPath.isEmpty()) {
- s += m_virtualParentPath + "\\";
- }
-
- s += m_file;
-
- return s;
-}
-
-QString FileTreeItem::dataRelativeParentPath() const
-{
- return m_virtualParentPath;
-}
-
-QString FileTreeItem::dataRelativeFilePath() const
-{
- auto path = dataRelativeParentPath();
- if (!path.isEmpty()) {
- path += "\\";
- }
-
- return path += m_file;
-}
-
-const QString& FileTreeItem::realPath() const
-{
- return m_realPath;
-}
-
-const QString& FileTreeItem::filename() const
-{
- return m_file;
-}
-
-const QString& FileTreeItem::mod() const
-{
- return m_mod;
-}
-
-QFont FileTreeItem::font() const
-{
- QFont f;
-
- if (isFromArchive()) {
- f.setItalic(true);
- } else if (isHidden()) {
- f.setStrikeOut(true);
- }
-
- return f;
-}
-
-QFileIconProvider::IconType FileTreeItem::icon() const
-{
- if (m_flags & Directory) {
- return QFileIconProvider::Folder;
- } else {
- return QFileIconProvider::File;
- }
-}
-
-bool FileTreeItem::isDirectory() const
-{
- return (m_flags & Directory);
-}
-
-bool FileTreeItem::isFromArchive() const
-{
- return (m_flags & FromArchive);
-}
-
-bool FileTreeItem::isConflicted() const
-{
- return (m_flags & Conflicted);
-}
-
-bool FileTreeItem::isHidden() const
-{
- return m_file.endsWith(ModInfo::s_HiddenExt);
-}
-
-bool FileTreeItem::hasChildren() const
-{
- if (!isDirectory()) {
- return false;
- }
-
- if (isLoaded() && m_children.empty()) {
- return false;
- }
-
- return true;
-}
-
-void FileTreeItem::setLoaded(bool b)
-{
- m_loaded = b;
-}
-
-bool FileTreeItem::isLoaded() const
-{
- return m_loaded;
-}
-
-void FileTreeItem::unload()
-{
- if (!m_loaded) {
- return;
- }
-
- m_loaded = false;
- m_children.clear();
-}
-
-void FileTreeItem::setExpanded(bool b)
-{
- m_expanded = b;
-}
-
-bool FileTreeItem::isStrictlyExpanded() const
-{
- return m_expanded;
-}
-
-bool FileTreeItem::areChildrenVisible() const
-{
- if (m_expanded) {
- if (m_parent) {
- return m_parent->areChildrenVisible();
- } else {
- return true;
- }
- }
-
- return false;
-}
-
-QString FileTreeItem::debugName() const
-{
- return QString("%1(ld=%2,cs=%3)")
- .arg(virtualPath())
- .arg(m_loaded)
- .arg(m_children.size());
-}
-
-
-FileTreeModel::FileTreeModel(OrganizerCore& core, QObject* parent)
- : QAbstractItemModel(parent), m_core(core), m_flags(NoFlags)
-{
- connect(&m_iconPendingTimer, &QTimer::timeout, [&]{ updatePendingIcons(); });
-
- connect(
- this, &QAbstractItemModel::modelAboutToBeReset,
- [&]{ m_iconPending.clear(); });
-
- connect(
- this, &QAbstractItemModel::rowsAboutToBeRemoved,
- [&](auto&& parent, int first, int last){
- removePendingIcons(parent, first, last);
- });
-}
-
-void FileTreeModel::setFlags(Flags f)
-{
- m_flags = f;
-}
-
-bool FileTreeModel::showConflicts() const
-{
- return (m_flags & Conflicts);
-}
-
-bool FileTreeModel::showArchives() const
-{
- return (m_flags & Archives) && m_core.getArchiveParsing();
-}
-
-void FileTreeModel::refresh()
-{
- if (m_root.hasChildren()) {
- update(m_root, *m_core.directoryStructure(), L"");
- } else {
- beginResetModel();
- m_root = {nullptr, 0, L"", L"", FileTreeItem::Directory, L"", L"<root>"};
- m_root.setExpanded(true);
- fill(m_root, *m_core.directoryStructure(), L"");
- endResetModel();
- }
-}
-
-void FileTreeModel::ensureLoaded(FileTreeItem* item) const
-{
- if (!item) {
- log::error("ensureLoaded(): item is null");
- return;
- }
-
- if (item->isLoaded()) {
- return;
- }
-
- log::debug("{}: loading on demand", item->debugName());
-
- const auto path = item->dataRelativeFilePath();
- auto* dir = m_core.directoryStructure()->findSubDirectoryRecursive(
- path.toStdWString());
-
- if (!dir) {
- log::error("{}: directory '{}' not found", item->debugName(), path);
- return;
- }
-
- const_cast<FileTreeModel*>(this)
- ->fill(*item, *dir, item->dataRelativeParentPath().toStdWString());
-}
-
-void FileTreeModel::fill(
- FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
- const std::wstring& parentPath)
-{
- std::wstring path = parentPath;
-
- if (!parentEntry.isTopLevel()) {
- if (!path.empty()) {
- path += L"\\";
- }
-
- path += parentEntry.getName();
- }
-
- const auto flags = FillFlag::PruneDirectories;
-
- std::vector<DirectoryEntry*>::const_iterator begin, end;
- parentEntry.getSubDirectories(begin, end);
- fillDirectories(parentItem, path, begin, end, flags);
-
- fillFiles(parentItem, path, parentEntry.getFiles(), flags);
-
- parentItem.setLoaded(true);
-}
-
-void FileTreeModel::update(
- FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
- const std::wstring& parentPath)
-{
- log::debug("updating {}", parentItem.debugName());
-
- std::wstring path = parentPath;
-
- if (!parentEntry.isTopLevel()) {
- if (!path.empty()) {
- path += L"\\";
- }
-
- path += parentEntry.getName();
- }
-
- const auto flags = FillFlag::PruneDirectories;
-
- updateDirectories(parentItem, path, parentEntry, flags);
- updateFiles(parentItem, path, parentEntry, flags);
-}
-
-bool FileTreeModel::shouldShowFile(const FileEntry& file) const
-{
- if (showConflicts() && (file.getAlternatives().size() == 0)) {
- return false;
- }
-
- bool isArchive = false;
- int originID = file.getOrigin(isArchive);
- if (!showArchives() && isArchive) {
- return false;
- }
-
- return true;
-}
-
-bool FileTreeModel::hasFilesAnywhere(const DirectoryEntry& dir) const
-{
- bool foundFile = false;
-
- dir.forEachFile([&](auto&& f) {
- if (shouldShowFile(f)) {
- foundFile = true;
-
- // stop
- return false;
- }
-
- // continue
- return true;
- });
-
- if (foundFile) {
- return true;
- }
-
- std::vector<DirectoryEntry*>::const_iterator begin, end;
- dir.getSubDirectories(begin, end);
-
- for (auto itor=begin; itor!=end; ++itor) {
- if (hasFilesAnywhere(**itor)) {
- return true;
- }
- }
-
- return false;
-}
-
-void FileTreeModel::fillDirectories(
- FileTreeItem& parentItem, const std::wstring& path,
- DirectoryIterator begin, DirectoryIterator end, FillFlags flags)
-{
- for (auto itor=begin; itor!=end; ++itor) {
- const auto& dir = **itor;
-
- if (flags & FillFlag::PruneDirectories) {
- if (!hasFilesAnywhere(dir)) {
- continue;
- }
- }
-
- auto child = std::make_unique<FileTreeItem>(
- &parentItem, 0, path, L"", FileTreeItem::Directory, dir.getName(), L"");
-
- if (dir.isEmpty()) {
- child->setLoaded(true);
- }
-
- parentItem.add(std::move(child));
- }
-}
-
-void FileTreeModel::fillFiles(
- FileTreeItem& parentItem, const std::wstring& path,
- const std::vector<FileEntry::Ptr>& files, FillFlags)
-{
- for (auto&& file : files) {
- if (!shouldShowFile(*file)) {
- continue;
- }
-
- bool isArchive = false;
- int originID = file->getOrigin(isArchive);
-
- FileTreeItem::Flags flags = FileTreeItem::NoFlags;
-
- if (isArchive) {
- flags |= FileTreeItem::FromArchive;
- }
-
- if (!file->getAlternatives().empty()) {
- flags |= FileTreeItem::Conflicted;
- }
-
- parentItem.add(std::make_unique<FileTreeItem>(
- &parentItem, originID, path, file->getFullPath(), flags, file->getName(),
- makeModName(*file, originID)));
- }
-}
-
-void FileTreeModel::updateDirectories(
- FileTreeItem& parentItem, const std::wstring& path,
- const MOShared::DirectoryEntry& parentEntry, FillFlags flags)
-{
- log::debug(
- "updating directories in {} from {}",
- parentItem.debugName(), (path.empty() ? L"\\" : path));
-
- int row = 0;
- std::vector<FileTreeItem*> remove;
- std::set<std::wstring> seen;
-
- for (auto&& item : parentItem.children()) {
- if (!item->isDirectory()) {
- break;
- }
-
- const auto name = item->filename().toStdWString();
-
- if (auto d=parentEntry.findSubDirectory(name)) {
- // directory still exists
- seen.insert(name);
-
- if (item->areChildrenVisible()) {
- log::debug("{} still exists and is expanded", item->debugName());
-
- // node is expanded
- update(*item, *d, path);
-
- if (flags & FillFlag::PruneDirectories) {
- if (item->children().empty()) {
- log::debug("{} is now empty, will prune", item->debugName());
- remove.push_back(item.get());
- }
- }
- } else {
- if ((flags & FillFlag::PruneDirectories) && !hasFilesAnywhere(*d)) {
- log::debug("{} still exists but is empty; pruning", item->debugName());
- remove.push_back(item.get());
- } else if (item->isLoaded()) {
- log::debug(
- "{} still exists, is loaded, but is not expanded; unloading",
- item->debugName());
-
- // node is not expanded, unload
-
- bool mustEnd = false;
-
- if (!item->children().empty()) {
- const auto itemIndex = indexFromItem(item.get(), row, 0);
- const int first = 0;
- const int last = static_cast<int>(item->children().size());
-
- beginRemoveRows(itemIndex, first, last);
- mustEnd = true;
- }
-
- item->unload();
-
- if (mustEnd) {
- endRemoveRows();
- }
-
- if (d->isEmpty()) {
- item->setLoaded(true);
- }
- }
- }
- } else {
- // directory is gone
- log::debug("{} is gone, removing", item->debugName());
- remove.push_back(item.get());
- }
-
- ++row;
- }
-
- if (!remove.empty()) {
- log::debug("{}: removing disappearing items", parentItem.debugName());
-
- for (auto* toRemove : remove) {
- const auto& cs = parentItem.children();
-
- for (std::size_t i=0; i<cs.size(); ++i) {
- if (cs[i].get() == toRemove) {
- const auto itemIndex = indexFromItem(
- toRemove, static_cast<int>(i), 0);
-
- const auto parentIndex = parent(itemIndex);
- const int first = static_cast<int>(i);
- const int last = static_cast<int>(i);
-
- beginRemoveRows(parentIndex, first, last);
- parentItem.remove(i);
- endRemoveRows();
-
- break;
- }
- }
- }
- }
-
-
- std::vector<DirectoryEntry*>::const_iterator begin, end;
- parentEntry.getSubDirectories(begin, end);
-
- std::size_t insertPos = 0;
- for (auto itor=begin; itor!=end; ++itor) {
- const auto& dir = **itor;
-
- if (!seen.contains(dir.getName())) {
- log::debug(
- "{}: new directory {}",
- parentItem.debugName(), QString::fromStdWString(dir.getName()));
-
- if (flags & FillFlag::PruneDirectories) {
- if (!hasFilesAnywhere(dir)) {
- log::debug("has no files and pruning is set, skipping");
- continue;
- }
- }
-
- auto child = std::make_unique<FileTreeItem>(
- &parentItem, 0, path, L"", FileTreeItem::Directory, dir.getName(), L"");
-
- if (dir.isEmpty()) {
- child->setLoaded(true);
- }
-
- QModelIndex parentIndex;
-
- if (parentItem.parent()) {
- const auto& cs = parentItem.parent()->children();
-
- for (std::size_t i=0; i<cs.size(); ++i) {
- if (cs[i].get() == &parentItem) {
- parentIndex = indexFromItem(&parentItem, static_cast<int>(i), 0);
- break;
- }
- }
- }
-
- const auto first = static_cast<int>(insertPos);
- const auto last = static_cast<int>(insertPos);
-
- log::debug(
- "{}: inserting {} at {}",
- parentItem.debugName(), child->debugName(), insertPos);
-
- beginInsertRows(parentIndex, first, last);
- parentItem.insert(std::move(child), insertPos);
- endInsertRows();
- }
-
- ++insertPos;
- }
-}
-
-void FileTreeModel::updateFiles(
- FileTreeItem& parentItem, const std::wstring& path,
- const MOShared::DirectoryEntry& parentEntry, FillFlags)
-{
- log::debug(
- "updating files in {} from {}",
- parentItem.debugName(), (path.empty() ? L"\\" : path));
-
- std::set<std::wstring> seen;
- std::vector<FileTreeItem*> remove;
-
- for (auto&& item : parentItem.children()) {
- if (item->isDirectory()) {
- continue;
- }
-
- const auto name = item->filename().toStdWString();
-
- if (auto f=parentEntry.findFile(name)) {
- if (shouldShowFile(*f)) {
- // file still exists
- log::debug("{} still exists", item->debugName());
- seen.insert(name);
- continue;
- }
- }
-
- log::debug("{} is gone", item->debugName());
-
- remove.push_back(item.get());
- }
-
-
- if (!remove.empty()) {
- log::debug("{}: removing disappearing items", parentItem.debugName());
-
- for (auto* toRemove : remove) {
- const auto& cs = parentItem.children();
-
- for (std::size_t i=0; i<cs.size(); ++i) {
- if (cs[i].get() == toRemove) {
- const auto itemIndex = indexFromItem(
- toRemove, static_cast<int>(i), 0);
-
- const auto parentIndex = parent(itemIndex);
- const int first = static_cast<int>(i);
- const int last = static_cast<int>(i);
-
- beginRemoveRows(parentIndex, first, last);
- parentItem.remove(i);
- endRemoveRows();
-
- break;
- }
- }
- }
- }
-
- std::size_t firstFile = 0;
- for (std::size_t i=0; i<parentItem.children().size(); ++i) {
- if (!parentItem.children()[i]->isDirectory()) {
- break;
- }
-
- ++firstFile;
- }
-
- log::debug("{}: first file index is {}", parentItem.debugName(), firstFile);
- std::size_t insertPos = firstFile;
-
- for (auto&& file : parentEntry.getFiles()) {
- if (shouldShowFile(*file)) {
- if (!seen.contains(file->getName())) {
- log::debug(
- "{}: new file {}",
- parentItem.debugName(), QString::fromStdWString(file->getName()));
-
- bool isArchive = false;
- int originID = file->getOrigin(isArchive);
-
- FileTreeItem::Flags flags = FileTreeItem::NoFlags;
-
- if (isArchive) {
- flags |= FileTreeItem::FromArchive;
- }
-
- if (!file->getAlternatives().empty()) {
- flags |= FileTreeItem::Conflicted;
- }
-
- auto child = std::make_unique<FileTreeItem>(
- &parentItem, originID, path, file->getFullPath(), flags, file->getName(),
- makeModName(*file, originID));
-
- log::debug(
- "{}: inserting {} at {}",
- parentItem.debugName(), child->debugName(), insertPos);
-
- QModelIndex parentIndex;
-
- if (parentItem.parent()) {
- const auto& cs = parentItem.parent()->children();
-
- for (std::size_t i=0; i<cs.size(); ++i) {
- if (cs[i].get() == &parentItem) {
- parentIndex = indexFromItem(&parentItem, static_cast<int>(i), 0);
- break;
- }
- }
- }
-
- const auto first = static_cast<int>(insertPos);
- const auto last = static_cast<int>(insertPos);
-
- beginInsertRows(parentIndex, first, last);
- parentItem.insert(std::move(child), insertPos);
- endInsertRows();
- }
-
- ++insertPos;
- }
- }
-}
-
-std::wstring FileTreeModel::makeModName(const FileEntry& file, int originID) const
-{
- static const std::wstring Unmanaged = UnmanagedModName().toStdWString();
-
- const auto origin = m_core.directoryStructure()->getOriginByID(originID);
-
- if (origin.getID() == 0) {
- return Unmanaged;
- }
-
- std::wstring name = origin.getName();
-
- const auto& archive = file.getArchive();
- if (!archive.first.empty()) {
- name += L" (" + archive.first + L")";
- }
-
- return name;
-}
-
-FileTreeItem* FileTreeModel::itemFromIndex(const QModelIndex& index) const
-{
- auto* data = index.internalPointer();
- if (!data) {
- return nullptr;
- }
-
- auto* item = static_cast<FileTreeItem*>(data);
- if (!item->debugName().isEmpty()) {
- return item;
- }
-
- return nullptr;
-}
-
-QModelIndex FileTreeModel::indexFromItem(
- FileTreeItem* item, int row, int col) const
-{
- return createIndex(row, col, item);
-}
-
-QModelIndex FileTreeModel::index(
- int row, int col, const QModelIndex& parentIndex) const
-{
- FileTreeItem* parent = nullptr;
-
- if (!parentIndex.isValid()) {
- parent = &m_root;
- } else {
- parent = itemFromIndex(parentIndex);
- }
-
- if (!parent) {
- log::error("FileTreeModel::index(): parent is null");
- return {};
- }
-
- ensureLoaded(parent);
-
- if (static_cast<std::size_t>(row) >= parent->children().size()) {
- // don't warn if the tree hasn't been refreshed yet
- if (!m_root.children().empty()) {
- log::error(
- "FileTreeModel::index(): row {} is out of range for {}",
- row, parent->debugName());
- }
-
- return {};
- }
-
- if (col >= columnCount({})) {
- log::error(
- "FileTreeModel::index(): col {} is out of range for {}",
- col, parent->debugName());
-
- return {};
- }
-
- auto* item = parent->children()[static_cast<std::size_t>(row)].get();
- return indexFromItem(item, row, col);
-}
-
-QModelIndex FileTreeModel::parent(const QModelIndex& index) const
-{
- if (!index.isValid()) {
- return {};
- }
-
- auto* item = itemFromIndex(index);
- if (!item) {
- return {};
- }
-
- auto* parent = item->parent();
- if (!parent) {
- return {};
- }
-
- ensureLoaded(parent);
-
- int row = 0;
- for (auto&& child : parent->children()) {
- if (child.get() == item) {
- return createIndex(row, 0, parent);
- }
-
- ++row;
- }
-
- log::error(
- "FileTreeModel::parent(): item {} has no child {}",
- parent->debugName(), item->debugName());
-
- return {};
-}
-
-int FileTreeModel::rowCount(const QModelIndex& parent) const
-{
- FileTreeItem* item = nullptr;
-
- if (!parent.isValid()) {
- item = &m_root;
- } else {
- item = itemFromIndex(parent);
- }
-
- if (!item) {
- return 0;
- }
-
- ensureLoaded(item);
- return static_cast<int>(item->children().size());
-}
-
-int FileTreeModel::columnCount(const QModelIndex&) const
-{
- return 2;
-}
-
-bool FileTreeModel::hasChildren(const QModelIndex& parent) const
-{
- const FileTreeItem* item = nullptr;
-
- if (!parent.isValid()) {
- item = &m_root;
- } else {
- item = itemFromIndex(parent);
- }
-
- if (!item) {
- return false;
- }
-
- return item->hasChildren();
-}
-
-QVariant FileTreeModel::data(const QModelIndex& index, int role) const
-{
- switch (role)
- {
- case Qt::DisplayRole:
- {
- if (auto* item=itemFromIndex(index)) {
- if (index.column() == 0) {
- return item->filename();
- } else if (index.column() == 1) {
- return item->mod();
- }
- }
-
- break;
- }
-
- case Qt::FontRole:
- {
- if (auto* item=itemFromIndex(index)) {
- return item->font();
- }
-
- break;
- }
-
- case Qt::ToolTipRole:
- {
- if (auto* item=itemFromIndex(index)) {
- return makeTooltip(*item);
- }
-
- return {};
- }
-
- case Qt::ForegroundRole:
- {
- if (index.column() == 1) {
- if (auto* item=itemFromIndex(index)) {
- if (item->isConflicted()) {
- return QBrush(Qt::red);
- }
- }
- }
-
- break;
- }
-
- case Qt::DecorationRole:
- {
- if (index.column() == 0) {
- if (auto* item=itemFromIndex(index)) {
- return makeIcon(*item, index);
- }
- }
-
- break;
- }
- }
-
- return {};
-}
-
-QString FileTreeModel::makeTooltip(const FileTreeItem& item) const
-{
- if (item.isDirectory()) {
- return {};
- }
-
- auto nowrap = [&](auto&& s) {
- return "<p style=\"white-space: pre; margin: 0; padding: 0;\">" + s + "</p>";
- };
-
- auto line = [&](auto&& caption, auto&& value) {
- if (value.isEmpty()) {
- return nowrap("<b>" + caption + ":</b>\n");
- } else {
- return nowrap("<b>" + caption + ":</b> " + value.toHtmlEscaped()) + "\n";
- }
- };
-
- static const QString ListStart =
- "<ul style=\""
- "margin-left: 20px; "
- "margin-top: 0; "
- "margin-bottom: 0; "
- "padding: 0; "
- "-qt-list-indent: 0;"
- "\">";
-
- static const QString ListEnd = "</ul>";
-
-
- QString s =
- line(tr("Virtual path"), item.virtualPath()) +
- line(tr("Real path"), item.realPath()) +
- line(tr("From"), item.mod());
-
-
- const auto file = m_core.directoryStructure()->searchFile(
- item.dataRelativeFilePath().toStdWString(), nullptr);
-
- if (file) {
- const auto alternatives = file->getAlternatives();
- QStringList list;
-
- for (auto&& alt : file->getAlternatives()) {
- const auto& origin = m_core.directoryStructure()->getOriginByID(alt.first);
- list.push_back(QString::fromStdWString(origin.getName()));
- }
-
- if (list.size() == 1) {
- s += line(tr("Also in"), list[0]);
- } else if (list.size() >= 2) {
- s += line(tr("Also in"), QString()) + ListStart;
-
- for (auto&& alt : list) {
- s += "<li>" + alt +"</li>";
- }
-
- s += ListEnd;
- }
- }
-
- return s;
-}
-
-QVariant FileTreeModel::makeIcon(
- const FileTreeItem& item, const QModelIndex& index) const
-{
- if (item.isDirectory()) {
- return m_iconFetcher.genericDirectoryIcon();
- }
-
- auto v = m_iconFetcher.icon(item.realPath());
- if (!v.isNull()) {
- return v;
- }
-
- m_iconPending.push_back(index);
- m_iconPendingTimer.start(std::chrono::milliseconds(1));
-
- return m_iconFetcher.genericFileIcon();
-}
-
-void FileTreeModel::updatePendingIcons()
-{
- std::vector<QModelIndex> v(std::move(m_iconPending));
- m_iconPending.clear();
-
- for (auto&& index : v) {
- emit dataChanged(index, index, {Qt::DecorationRole});
- }
-
- if (m_iconPending.empty()) {
- m_iconPendingTimer.stop();
- }
-}
-
-void FileTreeModel::removePendingIcons(
- const QModelIndex& parent, int first, int last)
-{
- auto itor = m_iconPending.begin();
-
- while (itor != m_iconPending.end()) {
- if (itor->parent() == parent) {
- if (itor->row() >= first && itor->row() <= last) {
- if (auto* item=itemFromIndex(*itor)) {
- log::debug("removing pending icon {}", item->debugName());
- } else {
- log::debug("removing pending icon (can't get item)");
- }
-
- itor = m_iconPending.erase(itor);
- continue;
- }
- }
-
- ++itor;
- }
-}
-
-QVariant FileTreeModel::headerData(int i, Qt::Orientation ori, int role) const
-{
- if (role == Qt::DisplayRole) {
- if (i == 0) {
- return tr("File");
- } else if (i == 1) {
- return tr("Mod");
- }
- }
-
- return {};
-}
-
-Qt::ItemFlags FileTreeModel::flags(const QModelIndex& index) const
-{
- auto f = QAbstractItemModel::flags(index);
-
- if (auto* item=itemFromIndex(index)) {
- if (!item->hasChildren()) {
- f |= Qt::ItemNeverHasChildren;
- }
- }
-
- return f;
-}
-
-
FileTree::FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree)
: m_core(core), m_plugins(pc), m_tree(tree), m_model(new FileTreeModel(core))
{
@@ -1217,9 +134,9 @@ FileTree::FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree)
[&](auto&& index){ onExpandedChanged(index, false); });
}
-void FileTree::setFlags(FileTreeModel::Flags flags)
+FileTreeModel* FileTree::model()
{
- m_model->setFlags(flags);
+ return m_model;
}
void FileTree::refresh()
diff --git a/src/filetree.h b/src/filetree.h
index f92f10f2..77d5012c 100644
--- a/src/filetree.h
+++ b/src/filetree.h
@@ -1,179 +1,15 @@
#ifndef MODORGANIZER_FILETREE_INCLUDED
#define MODORGANIZER_FILETREE_INCLUDED
-#include "directoryentry.h"
-#include "iconfetcher.h"
-#include "modinfodialogfwd.h"
#include "modinfo.h"
-#include <QAbstractItemModel>
+#include "modinfodialogfwd.h"
+
+namespace MOShared { class FileEntry; }
class OrganizerCore;
class PluginContainer;
-
-class FileTreeItem
-{
-public:
- enum Flag
- {
- NoFlags = 0x00,
- Directory = 0x01,
- FromArchive = 0x02,
- Conflicted = 0x04
- };
-
- Q_DECLARE_FLAGS(Flags, Flag);
-
- FileTreeItem();
- FileTreeItem(
- FileTreeItem* parent, int originID,
- std::wstring dataRelativeParentPath, std::wstring realPath, Flags flags,
- std::wstring file, std::wstring mod);
-
- FileTreeItem(const FileTreeItem&) = delete;
- FileTreeItem& operator=(const FileTreeItem&) = delete;
- FileTreeItem(FileTreeItem&&) = default;
- FileTreeItem& operator=(FileTreeItem&&) = default;
-
- void add(std::unique_ptr<FileTreeItem> child);
- void insert(std::unique_ptr<FileTreeItem> child, std::size_t at);
- void remove(std::size_t i);
- const std::vector<std::unique_ptr<FileTreeItem>>& children() const;
-
- FileTreeItem* parent();
- int originID() const;
- const QString& virtualParentPath() const;
- QString virtualPath() const;
- const QString& filename() const;
- const QString& mod() const;
- QFont font() const;
-
- const QString& realPath() const;
- QString dataRelativeParentPath() const;
- QString dataRelativeFilePath() const;
-
- QFileIconProvider::IconType icon() const;
-
- bool isDirectory() const;
- bool isFromArchive() const;
- bool isConflicted() const;
- bool isHidden() const;
- bool hasChildren() const;
-
- void setLoaded(bool b);
- bool isLoaded() const;
- void unload();
-
- void setExpanded(bool b);
- bool isStrictlyExpanded() const;
- bool areChildrenVisible() const;
-
- QString debugName() const;
-
-private:
- FileTreeItem* m_parent;
- int m_originID;
- QString m_virtualParentPath;
- QString m_realPath;
- Flags m_flags;
- QString m_file;
- QString m_mod;
- bool m_loaded;
- bool m_expanded;
- std::vector<std::unique_ptr<FileTreeItem>> m_children;
-};
-
-
-class FileTreeModel : public QAbstractItemModel
-{
- Q_OBJECT;
-
-public:
- enum Flag
- {
- NoFlags = 0x00,
- Conflicts = 0x01,
- Archives = 0x02
- };
-
- Q_DECLARE_FLAGS(Flags, Flag);
-
- FileTreeModel(OrganizerCore& core, QObject* parent=nullptr);
-
- void setFlags(Flags f);
- void refresh();
-
- QModelIndex index(int row, int col, const QModelIndex& parent={}) const override;
- QModelIndex parent(const QModelIndex& index) const override;
- int rowCount(const QModelIndex& parent={}) const override;
- int columnCount(const QModelIndex& parent={}) const override;
- bool hasChildren(const QModelIndex& parent={}) const override;
- QVariant data(const QModelIndex& index, int role=Qt::DisplayRole) const override;
- QVariant headerData(int i, Qt::Orientation ori, int role=Qt::DisplayRole) const override;
- Qt::ItemFlags flags(const QModelIndex& index) const override;
- FileTreeItem* itemFromIndex(const QModelIndex& index) const;
-
-private:
- enum class FillFlag
- {
- None = 0x00,
- PruneDirectories = 0x01
- };
-
- Q_DECLARE_FLAGS(FillFlags, FillFlag);
-
- using DirectoryIterator = std::vector<MOShared::DirectoryEntry*>::const_iterator;
- OrganizerCore& m_core;
- mutable FileTreeItem m_root;
- Flags m_flags;
- mutable IconFetcher m_iconFetcher;
- mutable std::vector<QModelIndex> m_iconPending;
- mutable QTimer m_iconPendingTimer;
-
- bool showConflicts() const;
- bool showArchives() const;
-
- void fill(
- FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
- const std::wstring& parentPath);
-
- void fillDirectories(
- FileTreeItem& parentItem, const std::wstring& path,
- DirectoryIterator begin, DirectoryIterator end, FillFlags flags);
-
- void fillFiles(
- FileTreeItem& parentItem, const std::wstring& path,
- const std::vector<MOShared::FileEntry::Ptr>& files, FillFlags flags);
-
-
- void update(
- FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
- const std::wstring& parentPath);
-
- void updateDirectories(
- FileTreeItem& parentItem, const std::wstring& path,
- const MOShared::DirectoryEntry& parentEntry, FillFlags flags);
-
- void updateFiles(
- FileTreeItem& parentItem, const std::wstring& path,
- const MOShared::DirectoryEntry& parentEntry, FillFlags flags);
-
- std::wstring makeModName(const MOShared::FileEntry& file, int originID) const;
-
- void ensureLoaded(FileTreeItem* item) const;
- void updatePendingIcons();
- void removePendingIcons(const QModelIndex& parent, int first, int last);
-
- bool shouldShowFile(const MOShared::FileEntry& file) const;
- bool hasFilesAnywhere(const MOShared::DirectoryEntry& dir) const;
- QString makeTooltip(const FileTreeItem& item) const;
- QVariant makeIcon(const FileTreeItem& item, const QModelIndex& index) const;
-
- QModelIndex indexFromItem(FileTreeItem* item, int row, int col) const;
-};
-
-Q_DECLARE_OPERATORS_FOR_FLAGS(FileTreeModel::Flags);
-Q_DECLARE_OPERATORS_FOR_FLAGS(FileTreeItem::Flags);
-
+class FileTreeModel;
+class FileTreeItem;
class FileTree : public QObject
{
@@ -182,7 +18,7 @@ class FileTree : public QObject
public:
FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree);
- void setFlags(FileTreeModel::Flags flags);
+ FileTreeModel* model();
void refresh();
void open();
diff --git a/src/filetreeitem.cpp b/src/filetreeitem.cpp
new file mode 100644
index 00000000..38eb5ec2
--- /dev/null
+++ b/src/filetreeitem.cpp
@@ -0,0 +1,222 @@
+#include "filetreeitem.h"
+#include "modinfo.h"
+#include <log.h>
+
+using namespace MOBase;
+
+FileTreeItem::FileTreeItem()
+ : m_flags(NoFlags), m_loaded(false)
+{
+}
+
+FileTreeItem::FileTreeItem(
+ FileTreeItem* parent, int originID,
+ std::wstring dataRelativeParentPath, std::wstring realPath, Flags flags,
+ std::wstring file, std::wstring mod) :
+ m_parent(parent), m_originID(originID),
+ m_virtualParentPath(QString::fromStdWString(dataRelativeParentPath)),
+ m_realPath(QString::fromStdWString(realPath)),
+ m_flags(flags),
+ m_file(QString::fromStdWString(file)),
+ m_mod(QString::fromStdWString(mod)),
+ m_loaded(false),
+ m_expanded(false)
+{
+}
+
+void FileTreeItem::add(std::unique_ptr<FileTreeItem> child)
+{
+ m_children.push_back(std::move(child));
+}
+
+void FileTreeItem::insert(std::unique_ptr<FileTreeItem> child, std::size_t at)
+{
+ if (at > m_children.size()) {
+ log::error(
+ "{}: can't insert child {} at {}, out of range",
+ debugName(), child->debugName(), at);
+
+ return;
+ }
+
+ m_children.insert(m_children.begin() + at, std::move(child));
+}
+
+void FileTreeItem::remove(std::size_t i)
+{
+ if (i >= m_children.size()) {
+ log::error("{}: can't remove child at {}", debugName(), i);
+ return;
+ }
+
+ m_children.erase(m_children.begin() + i);
+}
+
+const std::vector<std::unique_ptr<FileTreeItem>>& FileTreeItem::children() const
+{
+ return m_children;
+}
+
+FileTreeItem* FileTreeItem::parent()
+{
+ return m_parent;
+}
+
+int FileTreeItem::originID() const
+{
+ return m_originID;
+}
+
+const QString& FileTreeItem::virtualParentPath() const
+{
+ return m_virtualParentPath;
+}
+
+QString FileTreeItem::virtualPath() const
+{
+ QString s = "Data\\";
+
+ if (!m_virtualParentPath.isEmpty()) {
+ s += m_virtualParentPath + "\\";
+ }
+
+ s += m_file;
+
+ return s;
+}
+
+QString FileTreeItem::dataRelativeParentPath() const
+{
+ return m_virtualParentPath;
+}
+
+QString FileTreeItem::dataRelativeFilePath() const
+{
+ auto path = dataRelativeParentPath();
+ if (!path.isEmpty()) {
+ path += "\\";
+ }
+
+ return path += m_file;
+}
+
+const QString& FileTreeItem::realPath() const
+{
+ return m_realPath;
+}
+
+const QString& FileTreeItem::filename() const
+{
+ return m_file;
+}
+
+const QString& FileTreeItem::mod() const
+{
+ return m_mod;
+}
+
+QFont FileTreeItem::font() const
+{
+ QFont f;
+
+ if (isFromArchive()) {
+ f.setItalic(true);
+ } else if (isHidden()) {
+ f.setStrikeOut(true);
+ }
+
+ return f;
+}
+
+QFileIconProvider::IconType FileTreeItem::icon() const
+{
+ if (m_flags & Directory) {
+ return QFileIconProvider::Folder;
+ } else {
+ return QFileIconProvider::File;
+ }
+}
+
+bool FileTreeItem::isDirectory() const
+{
+ return (m_flags & Directory);
+}
+
+bool FileTreeItem::isFromArchive() const
+{
+ return (m_flags & FromArchive);
+}
+
+bool FileTreeItem::isConflicted() const
+{
+ return (m_flags & Conflicted);
+}
+
+bool FileTreeItem::isHidden() const
+{
+ return m_file.endsWith(ModInfo::s_HiddenExt);
+}
+
+bool FileTreeItem::hasChildren() const
+{
+ if (!isDirectory()) {
+ return false;
+ }
+
+ if (isLoaded() && m_children.empty()) {
+ return false;
+ }
+
+ return true;
+}
+
+void FileTreeItem::setLoaded(bool b)
+{
+ m_loaded = b;
+}
+
+bool FileTreeItem::isLoaded() const
+{
+ return m_loaded;
+}
+
+void FileTreeItem::unload()
+{
+ if (!m_loaded) {
+ return;
+ }
+
+ m_loaded = false;
+ m_children.clear();
+}
+
+void FileTreeItem::setExpanded(bool b)
+{
+ m_expanded = b;
+}
+
+bool FileTreeItem::isStrictlyExpanded() const
+{
+ return m_expanded;
+}
+
+bool FileTreeItem::areChildrenVisible() const
+{
+ if (m_expanded) {
+ if (m_parent) {
+ return m_parent->areChildrenVisible();
+ } else {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+QString FileTreeItem::debugName() const
+{
+ return QString("%1(ld=%2,cs=%3)")
+ .arg(virtualPath())
+ .arg(m_loaded)
+ .arg(m_children.size());
+}
diff --git a/src/filetreeitem.h b/src/filetreeitem.h
new file mode 100644
index 00000000..516319ac
--- /dev/null
+++ b/src/filetreeitem.h
@@ -0,0 +1,78 @@
+#ifndef MODORGANIZER_FILETREEITEM_INCLUDED
+#define MODORGANIZER_FILETREEITEM_INCLUDED
+
+#include <QFileIconProvider>
+
+class FileTreeItem
+{
+public:
+ enum Flag
+ {
+ NoFlags = 0x00,
+ Directory = 0x01,
+ FromArchive = 0x02,
+ Conflicted = 0x04
+ };
+
+ Q_DECLARE_FLAGS(Flags, Flag);
+
+ FileTreeItem();
+ FileTreeItem(
+ FileTreeItem* parent, int originID,
+ std::wstring dataRelativeParentPath, std::wstring realPath, Flags flags,
+ std::wstring file, std::wstring mod);
+
+ FileTreeItem(const FileTreeItem&) = delete;
+ FileTreeItem& operator=(const FileTreeItem&) = delete;
+ FileTreeItem(FileTreeItem&&) = default;
+ FileTreeItem& operator=(FileTreeItem&&) = default;
+
+ void add(std::unique_ptr<FileTreeItem> child);
+ void insert(std::unique_ptr<FileTreeItem> child, std::size_t at);
+ void remove(std::size_t i);
+ const std::vector<std::unique_ptr<FileTreeItem>>& children() const;
+
+ FileTreeItem* parent();
+ int originID() const;
+ const QString& virtualParentPath() const;
+ QString virtualPath() const;
+ const QString& filename() const;
+ const QString& mod() const;
+ QFont font() const;
+
+ const QString& realPath() const;
+ QString dataRelativeParentPath() const;
+ QString dataRelativeFilePath() const;
+
+ QFileIconProvider::IconType icon() const;
+
+ bool isDirectory() const;
+ bool isFromArchive() const;
+ bool isConflicted() const;
+ bool isHidden() const;
+ bool hasChildren() const;
+
+ void setLoaded(bool b);
+ bool isLoaded() const;
+ void unload();
+
+ void setExpanded(bool b);
+ bool isStrictlyExpanded() const;
+ bool areChildrenVisible() const;
+
+ QString debugName() const;
+
+private:
+ FileTreeItem* m_parent;
+ int m_originID;
+ QString m_virtualParentPath;
+ QString m_realPath;
+ Flags m_flags;
+ QString m_file;
+ QString m_mod;
+ bool m_loaded;
+ bool m_expanded;
+ std::vector<std::unique_ptr<FileTreeItem>> m_children;
+};
+
+#endif // MODORGANIZER_FILETREEITEM_INCLUDED
diff --git a/src/filetreemodel.cpp b/src/filetreemodel.cpp
new file mode 100644
index 00000000..aa9d52e3
--- /dev/null
+++ b/src/filetreemodel.cpp
@@ -0,0 +1,872 @@
+#include "filetreemodel.h"
+#include "organizercore.h"
+#include <log.h>
+
+using namespace MOBase;
+using namespace MOShared;
+
+// in mainwindow.cpp
+QString UnmanagedModName();
+
+
+FileTreeModel::FileTreeModel(OrganizerCore& core, QObject* parent)
+ : QAbstractItemModel(parent), m_core(core), m_flags(NoFlags)
+{
+ connect(&m_iconPendingTimer, &QTimer::timeout, [&]{ updatePendingIcons(); });
+
+ connect(
+ this, &QAbstractItemModel::modelAboutToBeReset,
+ [&]{ m_iconPending.clear(); });
+
+ connect(
+ this, &QAbstractItemModel::rowsAboutToBeRemoved,
+ [&](auto&& parent, int first, int last){
+ removePendingIcons(parent, first, last);
+ });
+}
+
+void FileTreeModel::setFlags(Flags f)
+{
+ m_flags = f;
+}
+
+bool FileTreeModel::showConflicts() const
+{
+ return (m_flags & Conflicts);
+}
+
+bool FileTreeModel::showArchives() const
+{
+ return (m_flags & Archives) && m_core.getArchiveParsing();
+}
+
+void FileTreeModel::refresh()
+{
+ if (m_root.hasChildren()) {
+ update(m_root, *m_core.directoryStructure(), L"");
+ } else {
+ beginResetModel();
+ m_root = {nullptr, 0, L"", L"", FileTreeItem::Directory, L"", L"<root>"};
+ m_root.setExpanded(true);
+ fill(m_root, *m_core.directoryStructure(), L"");
+ endResetModel();
+ }
+}
+
+void FileTreeModel::ensureLoaded(FileTreeItem* item) const
+{
+ if (!item) {
+ log::error("ensureLoaded(): item is null");
+ return;
+ }
+
+ if (item->isLoaded()) {
+ return;
+ }
+
+ log::debug("{}: loading on demand", item->debugName());
+
+ const auto path = item->dataRelativeFilePath();
+ auto* dir = m_core.directoryStructure()->findSubDirectoryRecursive(
+ path.toStdWString());
+
+ if (!dir) {
+ log::error("{}: directory '{}' not found", item->debugName(), path);
+ return;
+ }
+
+ const_cast<FileTreeModel*>(this)
+ ->fill(*item, *dir, item->dataRelativeParentPath().toStdWString());
+}
+
+void FileTreeModel::fill(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath)
+{
+ std::wstring path = parentPath;
+
+ if (!parentEntry.isTopLevel()) {
+ if (!path.empty()) {
+ path += L"\\";
+ }
+
+ path += parentEntry.getName();
+ }
+
+ const auto flags = FillFlag::PruneDirectories;
+
+ std::vector<DirectoryEntry*>::const_iterator begin, end;
+ parentEntry.getSubDirectories(begin, end);
+ fillDirectories(parentItem, path, begin, end, flags);
+
+ fillFiles(parentItem, path, parentEntry.getFiles(), flags);
+
+ parentItem.setLoaded(true);
+}
+
+void FileTreeModel::update(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath)
+{
+ log::debug("updating {}", parentItem.debugName());
+
+ std::wstring path = parentPath;
+
+ if (!parentEntry.isTopLevel()) {
+ if (!path.empty()) {
+ path += L"\\";
+ }
+
+ path += parentEntry.getName();
+ }
+
+ const auto flags = FillFlag::PruneDirectories;
+
+ updateDirectories(parentItem, path, parentEntry, flags);
+ updateFiles(parentItem, path, parentEntry, flags);
+}
+
+bool FileTreeModel::shouldShowFile(const FileEntry& file) const
+{
+ if (showConflicts() && (file.getAlternatives().size() == 0)) {
+ return false;
+ }
+
+ bool isArchive = false;
+ int originID = file.getOrigin(isArchive);
+ if (!showArchives() && isArchive) {
+ return false;
+ }
+
+ return true;
+}
+
+bool FileTreeModel::hasFilesAnywhere(const DirectoryEntry& dir) const
+{
+ bool foundFile = false;
+
+ dir.forEachFile([&](auto&& f) {
+ if (shouldShowFile(f)) {
+ foundFile = true;
+
+ // stop
+ return false;
+ }
+
+ // continue
+ return true;
+ });
+
+ if (foundFile) {
+ return true;
+ }
+
+ std::vector<DirectoryEntry*>::const_iterator begin, end;
+ dir.getSubDirectories(begin, end);
+
+ for (auto itor=begin; itor!=end; ++itor) {
+ if (hasFilesAnywhere(**itor)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void FileTreeModel::fillDirectories(
+ FileTreeItem& parentItem, const std::wstring& path,
+ DirectoryIterator begin, DirectoryIterator end, FillFlags flags)
+{
+ for (auto itor=begin; itor!=end; ++itor) {
+ const auto& dir = **itor;
+
+ if (flags & FillFlag::PruneDirectories) {
+ if (!hasFilesAnywhere(dir)) {
+ continue;
+ }
+ }
+
+ auto child = std::make_unique<FileTreeItem>(
+ &parentItem, 0, path, L"", FileTreeItem::Directory, dir.getName(), L"");
+
+ if (dir.isEmpty()) {
+ child->setLoaded(true);
+ }
+
+ parentItem.add(std::move(child));
+ }
+}
+
+void FileTreeModel::fillFiles(
+ FileTreeItem& parentItem, const std::wstring& path,
+ const std::vector<FileEntry::Ptr>& files, FillFlags)
+{
+ for (auto&& file : files) {
+ if (!shouldShowFile(*file)) {
+ continue;
+ }
+
+ bool isArchive = false;
+ int originID = file->getOrigin(isArchive);
+
+ FileTreeItem::Flags flags = FileTreeItem::NoFlags;
+
+ if (isArchive) {
+ flags |= FileTreeItem::FromArchive;
+ }
+
+ if (!file->getAlternatives().empty()) {
+ flags |= FileTreeItem::Conflicted;
+ }
+
+ parentItem.add(std::make_unique<FileTreeItem>(
+ &parentItem, originID, path, file->getFullPath(), flags, file->getName(),
+ makeModName(*file, originID)));
+ }
+}
+
+void FileTreeModel::updateDirectories(
+ FileTreeItem& parentItem, const std::wstring& path,
+ const MOShared::DirectoryEntry& parentEntry, FillFlags flags)
+{
+ log::debug(
+ "updating directories in {} from {}",
+ parentItem.debugName(), (path.empty() ? L"\\" : path));
+
+ int row = 0;
+ std::vector<FileTreeItem*> remove;
+ std::set<std::wstring> seen;
+
+ for (auto&& item : parentItem.children()) {
+ if (!item->isDirectory()) {
+ break;
+ }
+
+ const auto name = item->filename().toStdWString();
+
+ if (auto d=parentEntry.findSubDirectory(name)) {
+ // directory still exists
+ seen.insert(name);
+
+ if (item->areChildrenVisible()) {
+ log::debug("{} still exists and is expanded", item->debugName());
+
+ // node is expanded
+ update(*item, *d, path);
+
+ if (flags & FillFlag::PruneDirectories) {
+ if (item->children().empty()) {
+ log::debug("{} is now empty, will prune", item->debugName());
+ remove.push_back(item.get());
+ }
+ }
+ } else {
+ if ((flags & FillFlag::PruneDirectories) && !hasFilesAnywhere(*d)) {
+ log::debug("{} still exists but is empty; pruning", item->debugName());
+ remove.push_back(item.get());
+ } else if (item->isLoaded()) {
+ log::debug(
+ "{} still exists, is loaded, but is not expanded; unloading",
+ item->debugName());
+
+ // node is not expanded, unload
+
+ bool mustEnd = false;
+
+ if (!item->children().empty()) {
+ const auto itemIndex = indexFromItem(item.get(), row, 0);
+ const int first = 0;
+ const int last = static_cast<int>(item->children().size());
+
+ beginRemoveRows(itemIndex, first, last);
+ mustEnd = true;
+ }
+
+ item->unload();
+
+ if (mustEnd) {
+ endRemoveRows();
+ }
+
+ if (d->isEmpty()) {
+ item->setLoaded(true);
+ }
+ }
+ }
+ } else {
+ // directory is gone
+ log::debug("{} is gone, removing", item->debugName());
+ remove.push_back(item.get());
+ }
+
+ ++row;
+ }
+
+ if (!remove.empty()) {
+ log::debug("{}: removing disappearing items", parentItem.debugName());
+
+ for (auto* toRemove : remove) {
+ const auto& cs = parentItem.children();
+
+ for (std::size_t i=0; i<cs.size(); ++i) {
+ if (cs[i].get() == toRemove) {
+ const auto itemIndex = indexFromItem(
+ toRemove, static_cast<int>(i), 0);
+
+ const auto parentIndex = parent(itemIndex);
+ const int first = static_cast<int>(i);
+ const int last = static_cast<int>(i);
+
+ beginRemoveRows(parentIndex, first, last);
+ parentItem.remove(i);
+ endRemoveRows();
+
+ break;
+ }
+ }
+ }
+ }
+
+
+ std::vector<DirectoryEntry*>::const_iterator begin, end;
+ parentEntry.getSubDirectories(begin, end);
+
+ std::size_t insertPos = 0;
+ for (auto itor=begin; itor!=end; ++itor) {
+ const auto& dir = **itor;
+
+ if (!seen.contains(dir.getName())) {
+ log::debug(
+ "{}: new directory {}",
+ parentItem.debugName(), QString::fromStdWString(dir.getName()));
+
+ if (flags & FillFlag::PruneDirectories) {
+ if (!hasFilesAnywhere(dir)) {
+ log::debug("has no files and pruning is set, skipping");
+ continue;
+ }
+ }
+
+ auto child = std::make_unique<FileTreeItem>(
+ &parentItem, 0, path, L"", FileTreeItem::Directory, dir.getName(), L"");
+
+ if (dir.isEmpty()) {
+ child->setLoaded(true);
+ }
+
+ QModelIndex parentIndex;
+
+ if (parentItem.parent()) {
+ const auto& cs = parentItem.parent()->children();
+
+ for (std::size_t i=0; i<cs.size(); ++i) {
+ if (cs[i].get() == &parentItem) {
+ parentIndex = indexFromItem(&parentItem, static_cast<int>(i), 0);
+ break;
+ }
+ }
+ }
+
+ const auto first = static_cast<int>(insertPos);
+ const auto last = static_cast<int>(insertPos);
+
+ log::debug(
+ "{}: inserting {} at {}",
+ parentItem.debugName(), child->debugName(), insertPos);
+
+ beginInsertRows(parentIndex, first, last);
+ parentItem.insert(std::move(child), insertPos);
+ endInsertRows();
+ }
+
+ ++insertPos;
+ }
+}
+
+void FileTreeModel::updateFiles(
+ FileTreeItem& parentItem, const std::wstring& path,
+ const MOShared::DirectoryEntry& parentEntry, FillFlags)
+{
+ log::debug(
+ "updating files in {} from {}",
+ parentItem.debugName(), (path.empty() ? L"\\" : path));
+
+ std::set<std::wstring> seen;
+ std::vector<FileTreeItem*> remove;
+
+ for (auto&& item : parentItem.children()) {
+ if (item->isDirectory()) {
+ continue;
+ }
+
+ const auto name = item->filename().toStdWString();
+
+ if (auto f=parentEntry.findFile(name)) {
+ if (shouldShowFile(*f)) {
+ // file still exists
+ log::debug("{} still exists", item->debugName());
+ seen.insert(name);
+ continue;
+ }
+ }
+
+ log::debug("{} is gone", item->debugName());
+
+ remove.push_back(item.get());
+ }
+
+
+ if (!remove.empty()) {
+ log::debug("{}: removing disappearing items", parentItem.debugName());
+
+ for (auto* toRemove : remove) {
+ const auto& cs = parentItem.children();
+
+ for (std::size_t i=0; i<cs.size(); ++i) {
+ if (cs[i].get() == toRemove) {
+ const auto itemIndex = indexFromItem(
+ toRemove, static_cast<int>(i), 0);
+
+ const auto parentIndex = parent(itemIndex);
+ const int first = static_cast<int>(i);
+ const int last = static_cast<int>(i);
+
+ beginRemoveRows(parentIndex, first, last);
+ parentItem.remove(i);
+ endRemoveRows();
+
+ break;
+ }
+ }
+ }
+ }
+
+ std::size_t firstFile = 0;
+ for (std::size_t i=0; i<parentItem.children().size(); ++i) {
+ if (!parentItem.children()[i]->isDirectory()) {
+ break;
+ }
+
+ ++firstFile;
+ }
+
+ log::debug("{}: first file index is {}", parentItem.debugName(), firstFile);
+ std::size_t insertPos = firstFile;
+
+ for (auto&& file : parentEntry.getFiles()) {
+ if (shouldShowFile(*file)) {
+ if (!seen.contains(file->getName())) {
+ log::debug(
+ "{}: new file {}",
+ parentItem.debugName(), QString::fromStdWString(file->getName()));
+
+ bool isArchive = false;
+ int originID = file->getOrigin(isArchive);
+
+ FileTreeItem::Flags flags = FileTreeItem::NoFlags;
+
+ if (isArchive) {
+ flags |= FileTreeItem::FromArchive;
+ }
+
+ if (!file->getAlternatives().empty()) {
+ flags |= FileTreeItem::Conflicted;
+ }
+
+ auto child = std::make_unique<FileTreeItem>(
+ &parentItem, originID, path, file->getFullPath(), flags, file->getName(),
+ makeModName(*file, originID));
+
+ log::debug(
+ "{}: inserting {} at {}",
+ parentItem.debugName(), child->debugName(), insertPos);
+
+ QModelIndex parentIndex;
+
+ if (parentItem.parent()) {
+ const auto& cs = parentItem.parent()->children();
+
+ for (std::size_t i=0; i<cs.size(); ++i) {
+ if (cs[i].get() == &parentItem) {
+ parentIndex = indexFromItem(&parentItem, static_cast<int>(i), 0);
+ break;
+ }
+ }
+ }
+
+ const auto first = static_cast<int>(insertPos);
+ const auto last = static_cast<int>(insertPos);
+
+ beginInsertRows(parentIndex, first, last);
+ parentItem.insert(std::move(child), insertPos);
+ endInsertRows();
+ }
+
+ ++insertPos;
+ }
+ }
+}
+
+std::wstring FileTreeModel::makeModName(const FileEntry& file, int originID) const
+{
+ static const std::wstring Unmanaged = UnmanagedModName().toStdWString();
+
+ const auto origin = m_core.directoryStructure()->getOriginByID(originID);
+
+ if (origin.getID() == 0) {
+ return Unmanaged;
+ }
+
+ std::wstring name = origin.getName();
+
+ const auto& archive = file.getArchive();
+ if (!archive.first.empty()) {
+ name += L" (" + archive.first + L")";
+ }
+
+ return name;
+}
+
+FileTreeItem* FileTreeModel::itemFromIndex(const QModelIndex& index) const
+{
+ auto* data = index.internalPointer();
+ if (!data) {
+ return nullptr;
+ }
+
+ auto* item = static_cast<FileTreeItem*>(data);
+ if (!item->debugName().isEmpty()) {
+ return item;
+ }
+
+ return nullptr;
+}
+
+QModelIndex FileTreeModel::indexFromItem(
+ FileTreeItem* item, int row, int col) const
+{
+ return createIndex(row, col, item);
+}
+
+QModelIndex FileTreeModel::index(
+ int row, int col, const QModelIndex& parentIndex) const
+{
+ FileTreeItem* parent = nullptr;
+
+ if (!parentIndex.isValid()) {
+ parent = &m_root;
+ } else {
+ parent = itemFromIndex(parentIndex);
+ }
+
+ if (!parent) {
+ log::error("FileTreeModel::index(): parent is null");
+ return {};
+ }
+
+ ensureLoaded(parent);
+
+ if (static_cast<std::size_t>(row) >= parent->children().size()) {
+ // don't warn if the tree hasn't been refreshed yet
+ if (!m_root.children().empty()) {
+ log::error(
+ "FileTreeModel::index(): row {} is out of range for {}",
+ row, parent->debugName());
+ }
+
+ return {};
+ }
+
+ if (col >= columnCount({})) {
+ log::error(
+ "FileTreeModel::index(): col {} is out of range for {}",
+ col, parent->debugName());
+
+ return {};
+ }
+
+ auto* item = parent->children()[static_cast<std::size_t>(row)].get();
+ return indexFromItem(item, row, col);
+}
+
+QModelIndex FileTreeModel::parent(const QModelIndex& index) const
+{
+ if (!index.isValid()) {
+ return {};
+ }
+
+ auto* item = itemFromIndex(index);
+ if (!item) {
+ return {};
+ }
+
+ auto* parent = item->parent();
+ if (!parent) {
+ return {};
+ }
+
+ ensureLoaded(parent);
+
+ int row = 0;
+ for (auto&& child : parent->children()) {
+ if (child.get() == item) {
+ return createIndex(row, 0, parent);
+ }
+
+ ++row;
+ }
+
+ log::error(
+ "FileTreeModel::parent(): item {} has no child {}",
+ parent->debugName(), item->debugName());
+
+ return {};
+}
+
+int FileTreeModel::rowCount(const QModelIndex& parent) const
+{
+ FileTreeItem* item = nullptr;
+
+ if (!parent.isValid()) {
+ item = &m_root;
+ } else {
+ item = itemFromIndex(parent);
+ }
+
+ if (!item) {
+ return 0;
+ }
+
+ ensureLoaded(item);
+ return static_cast<int>(item->children().size());
+}
+
+int FileTreeModel::columnCount(const QModelIndex&) const
+{
+ return 2;
+}
+
+bool FileTreeModel::hasChildren(const QModelIndex& parent) const
+{
+ const FileTreeItem* item = nullptr;
+
+ if (!parent.isValid()) {
+ item = &m_root;
+ } else {
+ item = itemFromIndex(parent);
+ }
+
+ if (!item) {
+ return false;
+ }
+
+ return item->hasChildren();
+}
+
+QVariant FileTreeModel::data(const QModelIndex& index, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ {
+ if (auto* item=itemFromIndex(index)) {
+ if (index.column() == 0) {
+ return item->filename();
+ } else if (index.column() == 1) {
+ return item->mod();
+ }
+ }
+
+ break;
+ }
+
+ case Qt::FontRole:
+ {
+ if (auto* item=itemFromIndex(index)) {
+ return item->font();
+ }
+
+ break;
+ }
+
+ case Qt::ToolTipRole:
+ {
+ if (auto* item=itemFromIndex(index)) {
+ return makeTooltip(*item);
+ }
+
+ return {};
+ }
+
+ case Qt::ForegroundRole:
+ {
+ if (index.column() == 1) {
+ if (auto* item=itemFromIndex(index)) {
+ if (item->isConflicted()) {
+ return QBrush(Qt::red);
+ }
+ }
+ }
+
+ break;
+ }
+
+ case Qt::DecorationRole:
+ {
+ if (index.column() == 0) {
+ if (auto* item=itemFromIndex(index)) {
+ return makeIcon(*item, index);
+ }
+ }
+
+ break;
+ }
+ }
+
+ return {};
+}
+
+QString FileTreeModel::makeTooltip(const FileTreeItem& item) const
+{
+ if (item.isDirectory()) {
+ return {};
+ }
+
+ auto nowrap = [&](auto&& s) {
+ return "<p style=\"white-space: pre; margin: 0; padding: 0;\">" + s + "</p>";
+ };
+
+ auto line = [&](auto&& caption, auto&& value) {
+ if (value.isEmpty()) {
+ return nowrap("<b>" + caption + ":</b>\n");
+ } else {
+ return nowrap("<b>" + caption + ":</b> " + value.toHtmlEscaped()) + "\n";
+ }
+ };
+
+ static const QString ListStart =
+ "<ul style=\""
+ "margin-left: 20px; "
+ "margin-top: 0; "
+ "margin-bottom: 0; "
+ "padding: 0; "
+ "-qt-list-indent: 0;"
+ "\">";
+
+ static const QString ListEnd = "</ul>";
+
+
+ QString s =
+ line(tr("Virtual path"), item.virtualPath()) +
+ line(tr("Real path"), item.realPath()) +
+ line(tr("From"), item.mod());
+
+
+ const auto file = m_core.directoryStructure()->searchFile(
+ item.dataRelativeFilePath().toStdWString(), nullptr);
+
+ if (file) {
+ const auto alternatives = file->getAlternatives();
+ QStringList list;
+
+ for (auto&& alt : file->getAlternatives()) {
+ const auto& origin = m_core.directoryStructure()->getOriginByID(alt.first);
+ list.push_back(QString::fromStdWString(origin.getName()));
+ }
+
+ if (list.size() == 1) {
+ s += line(tr("Also in"), list[0]);
+ } else if (list.size() >= 2) {
+ s += line(tr("Also in"), QString()) + ListStart;
+
+ for (auto&& alt : list) {
+ s += "<li>" + alt +"</li>";
+ }
+
+ s += ListEnd;
+ }
+ }
+
+ return s;
+}
+
+QVariant FileTreeModel::makeIcon(
+ const FileTreeItem& item, const QModelIndex& index) const
+{
+ if (item.isDirectory()) {
+ return m_iconFetcher.genericDirectoryIcon();
+ }
+
+ auto v = m_iconFetcher.icon(item.realPath());
+ if (!v.isNull()) {
+ return v;
+ }
+
+ m_iconPending.push_back(index);
+ m_iconPendingTimer.start(std::chrono::milliseconds(1));
+
+ return m_iconFetcher.genericFileIcon();
+}
+
+void FileTreeModel::updatePendingIcons()
+{
+ std::vector<QModelIndex> v(std::move(m_iconPending));
+ m_iconPending.clear();
+
+ for (auto&& index : v) {
+ emit dataChanged(index, index, {Qt::DecorationRole});
+ }
+
+ if (m_iconPending.empty()) {
+ m_iconPendingTimer.stop();
+ }
+}
+
+void FileTreeModel::removePendingIcons(
+ const QModelIndex& parent, int first, int last)
+{
+ auto itor = m_iconPending.begin();
+
+ while (itor != m_iconPending.end()) {
+ if (itor->parent() == parent) {
+ if (itor->row() >= first && itor->row() <= last) {
+ if (auto* item=itemFromIndex(*itor)) {
+ log::debug("removing pending icon {}", item->debugName());
+ } else {
+ log::debug("removing pending icon (can't get item)");
+ }
+
+ itor = m_iconPending.erase(itor);
+ continue;
+ }
+ }
+
+ ++itor;
+ }
+}
+
+QVariant FileTreeModel::headerData(int i, Qt::Orientation ori, int role) const
+{
+ if (role == Qt::DisplayRole) {
+ if (i == 0) {
+ return tr("File");
+ } else if (i == 1) {
+ return tr("Mod");
+ }
+ }
+
+ return {};
+}
+
+Qt::ItemFlags FileTreeModel::flags(const QModelIndex& index) const
+{
+ auto f = QAbstractItemModel::flags(index);
+
+ if (auto* item=itemFromIndex(index)) {
+ if (!item->hasChildren()) {
+ f |= Qt::ItemNeverHasChildren;
+ }
+ }
+
+ return f;
+}
diff --git a/src/filetreemodel.h b/src/filetreemodel.h
new file mode 100644
index 00000000..0cfe19c7
--- /dev/null
+++ b/src/filetreemodel.h
@@ -0,0 +1,101 @@
+#ifndef MODORGANIZER_FILETREEMODEL_INCLUDED
+#define MODORGANIZER_FILETREEMODEL_INCLUDED
+
+#include "filetreeitem.h"
+#include "iconfetcher.h"
+#include "directoryentry.h"
+
+class OrganizerCore;
+
+class FileTreeModel : public QAbstractItemModel
+{
+ Q_OBJECT;
+
+public:
+ enum Flag
+ {
+ NoFlags = 0x00,
+ Conflicts = 0x01,
+ Archives = 0x02
+ };
+
+ Q_DECLARE_FLAGS(Flags, Flag);
+
+ FileTreeModel(OrganizerCore& core, QObject* parent=nullptr);
+
+ void setFlags(Flags f);
+ void refresh();
+
+ QModelIndex index(int row, int col, const QModelIndex& parent={}) const override;
+ QModelIndex parent(const QModelIndex& index) const override;
+ int rowCount(const QModelIndex& parent={}) const override;
+ int columnCount(const QModelIndex& parent={}) const override;
+ bool hasChildren(const QModelIndex& parent={}) const override;
+ QVariant data(const QModelIndex& index, int role=Qt::DisplayRole) const override;
+ QVariant headerData(int i, Qt::Orientation ori, int role=Qt::DisplayRole) const override;
+ Qt::ItemFlags flags(const QModelIndex& index) const override;
+ FileTreeItem* itemFromIndex(const QModelIndex& index) const;
+
+private:
+ enum class FillFlag
+ {
+ None = 0x00,
+ PruneDirectories = 0x01
+ };
+
+ Q_DECLARE_FLAGS(FillFlags, FillFlag);
+
+ using DirectoryIterator = std::vector<MOShared::DirectoryEntry*>::const_iterator;
+ OrganizerCore& m_core;
+ mutable FileTreeItem m_root;
+ Flags m_flags;
+ mutable IconFetcher m_iconFetcher;
+ mutable std::vector<QModelIndex> m_iconPending;
+ mutable QTimer m_iconPendingTimer;
+
+ bool showConflicts() const;
+ bool showArchives() const;
+
+ void fill(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath);
+
+ void fillDirectories(
+ FileTreeItem& parentItem, const std::wstring& path,
+ DirectoryIterator begin, DirectoryIterator end, FillFlags flags);
+
+ void fillFiles(
+ FileTreeItem& parentItem, const std::wstring& path,
+ const std::vector<MOShared::FileEntry::Ptr>& files, FillFlags flags);
+
+
+ void update(
+ FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
+ const std::wstring& parentPath);
+
+ void updateDirectories(
+ FileTreeItem& parentItem, const std::wstring& path,
+ const MOShared::DirectoryEntry& parentEntry, FillFlags flags);
+
+ void updateFiles(
+ FileTreeItem& parentItem, const std::wstring& path,
+ const MOShared::DirectoryEntry& parentEntry, FillFlags flags);
+
+ std::wstring makeModName(const MOShared::FileEntry& file, int originID) const;
+
+ void ensureLoaded(FileTreeItem* item) const;
+ void updatePendingIcons();
+ void removePendingIcons(const QModelIndex& parent, int first, int last);
+
+ bool shouldShowFile(const MOShared::FileEntry& file) const;
+ bool hasFilesAnywhere(const MOShared::DirectoryEntry& dir) const;
+ QString makeTooltip(const FileTreeItem& item) const;
+ QVariant makeIcon(const FileTreeItem& item, const QModelIndex& index) const;
+
+ QModelIndex indexFromItem(FileTreeItem* item, int row, int col) const;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(FileTreeModel::Flags);
+Q_DECLARE_OPERATORS_FOR_FLAGS(FileTreeItem::Flags);
+
+#endif // MODORGANIZER_FILETREEMODEL_INCLUDED