diff options
| -rw-r--r-- | src/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | src/datatab.cpp | 3 | ||||
| -rw-r--r-- | src/filetree.cpp | 1091 | ||||
| -rw-r--r-- | src/filetree.h | 176 | ||||
| -rw-r--r-- | src/filetreeitem.cpp | 222 | ||||
| -rw-r--r-- | src/filetreeitem.h | 78 | ||||
| -rw-r--r-- | src/filetreemodel.cpp | 872 | ||||
| -rw-r--r-- | src/filetreemodel.h | 101 |
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 |
