summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2020-04-17 07:06:38 -0400
committerGitHub <noreply@github.com>2020-04-17 07:06:38 -0400
commit4621c271642906d13fffaed83e1033da09abe20d (patch)
treeb0b00df4afd0e8a1ce477e438e6cf52a5a5823cf /src
parent502e752b0af4e7a6800a7504ddbefbeef99778e4 (diff)
parent0faa5d69953f41deafba33cf36317127cbea8a8a (diff)
Merge pull request #1052 from isanae/filetree-crash-fixes
File tree crashes
Diffstat (limited to 'src')
-rw-r--r--src/filetree.cpp1
-rw-r--r--src/filetreeitem.cpp3
-rw-r--r--src/filetreemodel.cpp154
-rw-r--r--src/filetreemodel.h23
4 files changed, 158 insertions, 23 deletions
diff --git a/src/filetree.cpp b/src/filetree.cpp
index 6d96ddaa..2d92c954 100644
--- a/src/filetree.cpp
+++ b/src/filetree.cpp
@@ -145,7 +145,6 @@ FileTree::FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree)
connect(
m_tree, &QTreeView::activated,
[&](auto&& index){ onItemActivated(index); });
-
}
FileTreeModel* FileTree::model()
diff --git a/src/filetreeitem.cpp b/src/filetreeitem.cpp
index e5425573..64d902ee 100644
--- a/src/filetreeitem.cpp
+++ b/src/filetreeitem.cpp
@@ -366,8 +366,7 @@ void FileTreeItem::unload()
return;
}
- m_loaded = false;
- m_children.clear();
+ clear();
}
bool FileTreeItem::areChildrenVisible() const
diff --git a/src/filetreemodel.cpp b/src/filetreemodel.cpp
index 1a5433c5..a9cbeefe 100644
--- a/src/filetreemodel.cpp
+++ b/src/filetreemodel.cpp
@@ -5,6 +5,7 @@
#include "shared/directoryentry.h"
#include "shared/fileentry.h"
#include <log.h>
+#include <moassert.h>
using namespace MOBase;
using namespace MOShared;
@@ -12,10 +13,46 @@ using namespace MOShared;
// in mainwindow.cpp
QString UnmanagedModName();
-
#define trace(f)
+// about queueRemoveItem(), queueSortItems() and the `forFetching` parameter
+//
+// update() can be called when refreshing the tree or expanding a node;
+// there are certain operations that cannot be done during node expansion,
+// namely removing or moving items
+//
+// 1) removing items
+// beginRemoveRows()/endRemoveRows() clears the internal list of visible
+// items in a QTreeView, which is repopulated during the next layout
+//
+// doing this _during_ layout (which happens during node expansion) crashes
+// in Qt because it assumes the list of visible items doesn't change; that
+// is, fetchMore() cannot _remove_ items from the tree, it can only _add_
+// items
+//
+// 2) moving items
+// when a QSortFilterProxyModel is used between a tree and the FileTreeModel,
+// it maintains a mapping of indices between the source and proxy models;
+// calling layoutAboutToBeChanged()/layoutChanged() clears that mapping
+//
+// the only time this is called in FileTreeModel is when sorting items,
+// which can happen during layout because since it calls fetchMore()
+//
+// doing this _during_ layout (which happens during node expansion) crashes
+// in Qt because the QSortFilterProxyModel assumes the mapping doesn't
+// change; that is, fetchMore() cannot _move_ items in the tree, it can
+// only _add_ items
+//
+//
+// therefore, these two operations are queued in a 1ms timer which is processed
+// immediately after update()
+//
+// note that not all instances of removing items are currently getting queued
+// (such as when removeDisappearingFiles()) because they cannot happen while
+// fetching
+
+
// tracks a contiguous range in the model to avoid calling begin*Rows(), etc.
// for every single item that's added/removed
//
@@ -55,13 +92,21 @@ public:
return m_current;
}
+ // manually set this range
+ //
+ void set(int first, int last)
+ {
+ m_first = first;
+ m_current = last;
+ }
+
// adds the given items to this range
//
void add(FileTreeItem::Children toAdd)
{
if (m_first == -1) {
// nothing to add
- Q_ASSERT(toAdd.empty());
+ MO_ASSERT(toAdd.empty());
return;
}
@@ -69,7 +114,7 @@ public:
const auto parentIndex = m_model->indexFromItem(m_parentItem);
// make sure the number of items is the same as the size of this range
- Q_ASSERT(static_cast<int>(toAdd.size()) == (last - m_first + 1));
+ MO_ASSERT(static_cast<int>(toAdd.size()) == (last - m_first + 1));
trace(log::debug("Range::add() {} to {}", m_first, last));
@@ -121,6 +166,14 @@ public:
return m_parentItem.children().begin() + m_current + 1;
}
+ static void removeChildren(FileTreeModel* model, FileTreeItem& parentItem)
+ {
+ Range r(model, parentItem);
+ r.set(0, static_cast<int>(parentItem.children().size()));
+ r.remove();
+ parentItem.clear();
+ }
+
private:
FileTreeModel* m_model;
FileTreeItem& m_parentItem;
@@ -147,6 +200,9 @@ FileTreeModel::FileTreeModel(OrganizerCore& core, QObject* parent) :
{
m_root->setExpanded(true);
+ connect(&m_removeTimer, &QTimer::timeout, [&]{ removeItems(); });
+ connect(&m_sortTimer, &QTimer::timeout, [&]{ sortItems(); });
+
connect(&m_iconPendingTimer, &QTimer::timeout, [&]{ updatePendingIcons(); });
}
@@ -155,7 +211,7 @@ void FileTreeModel::refresh()
TimeThis tt("FileTreeModel::refresh()");
m_fullyLoaded = false;
- update(*m_root, *m_core.directoryStructure(), L"");
+ update(*m_root, *m_core.directoryStructure(), L"", false);
}
void FileTreeModel::clear()
@@ -254,8 +310,14 @@ int FileTreeModel::columnCount(const QModelIndex&) const
bool FileTreeModel::hasChildren(const QModelIndex& parent) const
{
+ if (!m_enabled) {
+ return false;
+ }
+
if (auto* item=itemFromIndex(parent)) {
- return item->hasChildren();
+ if (parent.column() <= 0) {
+ return item->hasChildren();
+ }
}
return false;
@@ -292,8 +354,7 @@ void FileTreeModel::fetchMore(const QModelIndex& parent)
}
const auto parentPath = item->dataRelativeParentPath();
-
- update(*item, *parentEntry, parentPath.toStdWString());
+ update(*item, *parentEntry, parentPath.toStdWString(), true);
}
QVariant FileTreeModel::data(const QModelIndex& index, int role) const
@@ -385,7 +446,8 @@ Qt::ItemFlags FileTreeModel::flags(const QModelIndex& index) const
void FileTreeModel::sortItem(FileTreeItem& item, bool force)
{
- emit layoutAboutToBeChanged();
+ emit layoutAboutToBeChanged({}, QAbstractItemModel::VerticalSortHint);
+
const auto oldList = persistentIndexList();
std::vector<std::pair<FileTreeItem*, int>> oldItems;
@@ -435,7 +497,7 @@ FileTreeItem* FileTreeModel::itemFromIndex(const QModelIndex& index) const
if (index.row() < 0 || index.row() >= parentItem->children().size()) {
log::error(
- "FileeTreeModel::itemFromIndex(): row {} is out of range for {}",
+ "FileTreeModel::itemFromIndex(): row {} is out of range for {}",
index.row(), parentItem->debugName());
return nullptr;
@@ -465,7 +527,7 @@ QModelIndex FileTreeModel::indexFromItem(FileTreeItem& item, int col) const
void FileTreeModel::update(
FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
- const std::wstring& parentPath)
+ const std::wstring& parentPath, bool forFetching)
{
trace(log::debug("updating {}", parentItem.debugName()));
@@ -482,7 +544,7 @@ void FileTreeModel::update(
bool added = false;
- if (updateDirectories(parentItem, path, parentEntry)) {
+ if (updateDirectories(parentItem, path, parentEntry, forFetching)) {
added = true;
}
@@ -491,26 +553,31 @@ void FileTreeModel::update(
}
if (added) {
- parentItem.sort(m_sort.column, m_sort.order, true);
+ // see comment at the top of this file
+ if (forFetching)
+ queueSortItem(&parentItem);
+ else
+ sortItem(parentItem, true);
}
}
bool FileTreeModel::updateDirectories(
FileTreeItem& parentItem, const std::wstring& parentPath,
- const MOShared::DirectoryEntry& parentEntry)
+ const MOShared::DirectoryEntry& parentEntry, bool forFetching)
{
// removeDisappearingDirectories() will add directories that are in the
// tree and still on the filesystem to this set; addNewDirectories() will
// use this to figure out if a directory is new or not
std::unordered_set<std::wstring_view> seen;
- removeDisappearingDirectories(parentItem, parentEntry, parentPath, seen);
+ removeDisappearingDirectories(parentItem, parentEntry, parentPath, seen, forFetching);
return addNewDirectories(parentItem, parentEntry, parentPath, seen);
}
void FileTreeModel::removeDisappearingDirectories(
FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
- const std::wstring& parentPath, std::unordered_set<std::wstring_view>& seen)
+ const std::wstring& parentPath, std::unordered_set<std::wstring_view>& seen,
+ bool forFetching)
{
auto& children = parentItem.children();
auto itor = children.begin();
@@ -541,7 +608,7 @@ void FileTreeModel::removeDisappearingDirectories(
if (item->areChildrenVisible()) {
// the item is currently expanded, update it
- update(*item, *d, parentPath);
+ update(*item, *d, parentPath, forFetching);
}
if (shouldShowFolder(*d, item.get())) {
@@ -549,9 +616,20 @@ void FileTreeModel::removeDisappearingDirectories(
if (!item->areChildrenVisible() && item->isLoaded()) {
if (!d->isEmpty()) {
// the item is loaded (previously expanded but now collapsed) and
- // has children, mark it as unloaded so it updates when next
+ // has children, mark it as unloaded so it updates when next
// expanded
item->setLoaded(false);
+
+ if (!item->children().empty()) {
+ // if the item had children, remove them from the tree
+
+ // see comment at the top of this file
+ if (forFetching) {
+ queueRemoveItem(item.get());
+ } else {
+ Range::removeChildren(this, *item);
+ }
+ }
}
}
} else {
@@ -766,6 +844,48 @@ bool FileTreeModel::addNewFiles(
return added;
}
+void FileTreeModel::queueRemoveItem(FileTreeItem* item)
+{
+ trace(log::debug("queuing {} for removal", item->debugName()));
+
+ m_removeItems.push_back(item);
+ m_removeTimer.start(1);
+}
+
+void FileTreeModel::removeItems()
+{
+ // see comment at the top of this file
+ log::debug("remove item timer: removing {} items", m_removeItems.size());
+
+ auto copy = std::move(m_removeItems);
+ m_removeItems.clear();
+ m_removeTimer.stop();
+
+ for (auto&& f : copy) {
+ Range::removeChildren(this, *f);
+ }
+}
+
+void FileTreeModel::queueSortItem(FileTreeItem* item)
+{
+ m_sortItems.push_back(item);
+ m_sortTimer.start(1);
+}
+
+void FileTreeModel::sortItems()
+{
+ // see comment at the top of this file
+ log::debug("sort item timer: sorting {} items", m_sortItems.size());
+
+ auto copy = std::move(m_sortItems);
+ m_sortItems.clear();
+ m_sortTimer.stop();
+
+ for (auto&& f : copy) {
+ sortItem(*f, true);
+ }
+}
+
FileTreeItem::Ptr FileTreeModel::createDirectoryItem(
FileTreeItem& parentItem, const std::wstring& parentPath,
const DirectoryEntry& d)
diff --git a/src/filetreemodel.h b/src/filetreemodel.h
index 5bfb75aa..8d3af5f7 100644
--- a/src/filetreemodel.h
+++ b/src/filetreemodel.h
@@ -93,6 +93,13 @@ private:
SortInfo m_sort;
bool m_fullyLoaded;
+ // see top of filetreemodel.cpp
+ std::vector<FileTreeItem*> m_removeItems;
+ QTimer m_removeTimer;
+ std::vector<FileTreeItem*> m_sortItems;
+ QTimer m_sortTimer;
+
+
bool showConflictsOnly() const
{
return (m_flags & ConflictsOnly);
@@ -101,18 +108,28 @@ private:
bool showArchives() const;
+ // for `forFetching`, see top of filetreemodel.cpp
void update(
FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
- const std::wstring& parentPath);
+ const std::wstring& parentPath, bool forFetching);
+
+ void queueRemoveItem(FileTreeItem* item);
+ void removeItems();
+ void queueSortItem(FileTreeItem* item);
+ void sortItems();
+
+ // for `forFetching`, see top of filetreemodel.cpp
bool updateDirectories(
FileTreeItem& parentItem, const std::wstring& path,
- const MOShared::DirectoryEntry& parentEntry);
+ const MOShared::DirectoryEntry& parentEntry, bool forFetching);
+ // for `forFetching`, see top of filetreemodel.cpp
void removeDisappearingDirectories(
FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
- const std::wstring& parentPath, std::unordered_set<std::wstring_view>& seen);
+ const std::wstring& parentPath, std::unordered_set<std::wstring_view>& seen,
+ bool forFetching);
bool addNewDirectories(
FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,