File indexing completed on 2024-11-24 04:53:15

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include "ThreadingMsgListModel.h"
0024 #include <algorithm>
0025 #include <QBuffer>
0026 #include <QDebug>
0027 #include "Imap/Tasks/SortTask.h"
0028 #include "Imap/Tasks/ThreadTask.h"
0029 #include "ItemRoles.h"
0030 #include "MailboxTree.h"
0031 #include "MsgListModel.h"
0032 
0033 namespace {
0034     /** @short Preallocate a bit more space in the hashmaps for future new arrivals */
0035     const int headroomForNewmessages = 1000;
0036 }
0037 
0038 namespace {
0039 using Imap::Mailbox::ThreadNodeInfo;
0040 
0041 #if 0
0042 QByteArray dumpThreadNodeInfo(const QHash<uint,ThreadNodeInfo> &mapping, const uint nodeId, const uint offset)
0043 {
0044     QByteArray res;
0045     QByteArray prefix(offset, ' ');
0046     QTextStream ss(&res);
0047     Q_ASSERT(mapping.contains(nodeId));
0048     const ThreadNodeInfo &node = mapping[nodeId];
0049     ss << prefix << "ThreadNodeInfo intId " << node.internalId << " UID " << node.uid << " ptr " << node.ptr <<
0050           " parentIntId " << node.parent << "\n";
0051     Q_FOREACH(const uint childId, node.children) {
0052         ss << dumpThreadNodeInfo(mapping, childId, offset + 1);
0053     }
0054     return res;
0055 }
0056 #endif
0057 
0058 #if 0
0059 void dumpThreading(const QVector<Imap::Responses::ThreadingNode>& thr, const uint offset)
0060 {
0061     QByteArray prefix(offset, ' ');
0062     for (const auto& x: thr) {
0063         std::cerr << prefix.data() << "ThreadingNode " << x.num << "\n";
0064         dumpThreading(x.children, offset + 1);
0065     }
0066 }
0067 #endif
0068 
0069 }
0070 
0071 namespace Imap
0072 {
0073 namespace Mailbox
0074 {
0075 
0076 ThreadingMsgListModel::ThreadingMsgListModel(QObject *parent):
0077     QAbstractProxyModel(parent), threadingHelperLastId(0), modelResetInProgress(false), threadingInFlight(false),
0078     m_shallBeThreading(false), m_filteredBySearch(false), m_sortTask(0), m_sortReverse(false), m_currentSortingCriteria(SORT_NONE),
0079     m_searchValidity(RESULT_INVALIDATED)
0080 {
0081     m_delayedPrune = new QTimer(this);
0082     m_delayedPrune->setSingleShot(true);
0083     m_delayedPrune->setInterval(0);
0084     connect(m_delayedPrune, &QTimer::timeout, this, &ThreadingMsgListModel::delayedPrune);
0085 }
0086 
0087 void ThreadingMsgListModel::setSourceModel(QAbstractItemModel *sourceModel)
0088 {
0089     beginResetModel();
0090     threading.clear();
0091     ptrToInternal.clear();
0092     unknownUids.clear();
0093     threadedRootIds.clear();
0094     m_currentSortResult.clear();
0095     m_searchValidity = RESULT_INVALIDATED;
0096 
0097     if (this->sourceModel()) {
0098         // there's already something, so take care to disconnect all signals
0099         this->sourceModel()->disconnect(this);
0100     }
0101 
0102     endResetModel();
0103 
0104     if (!sourceModel)
0105         return;
0106 
0107     Imap::Mailbox::MsgListModel *msgList = qobject_cast<Imap::Mailbox::MsgListModel *>(sourceModel);
0108     Q_ASSERT(msgList);
0109     QAbstractProxyModel::setSourceModel(msgList);
0110 
0111     // FIXME: will need to be expanded when Model supports more signals...
0112     connect(sourceModel, &QAbstractItemModel::modelReset, this, &ThreadingMsgListModel::resetMe);
0113     connect(sourceModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &QAbstractItemModel::layoutAboutToBeChanged);
0114     connect(sourceModel, &QAbstractItemModel::layoutChanged, this, &QAbstractItemModel::layoutChanged);
0115     connect(sourceModel, &QAbstractItemModel::dataChanged, this, &ThreadingMsgListModel::handleDataChanged);
0116     connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ThreadingMsgListModel::handleRowsAboutToBeRemoved);
0117     connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &ThreadingMsgListModel::handleRowsRemoved);
0118     connect(sourceModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &ThreadingMsgListModel::handleRowsAboutToBeInserted);
0119     connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &ThreadingMsgListModel::handleRowsInserted);
0120     resetMe();
0121 }
0122 
0123 QVariant ThreadingMsgListModel::headerData(int section, Qt::Orientation orientation, int role) const
0124 {
0125     if (sourceModel()) {
0126         return sourceModel()->headerData(section, orientation, role);
0127     } else {
0128         return QVariant();
0129     }
0130 }
0131 
0132 void ThreadingMsgListModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
0133 {
0134     // We don't support updates which concern multiple rows at this time.
0135     // Doing that would very likely require a completely different codepath due to threading...
0136     Q_ASSERT(topLeft.parent() == bottomRight.parent());
0137     Q_ASSERT(topLeft.row() == bottomRight.row());
0138     QModelIndex translated = mapFromSource(topLeft);
0139 
0140     emit dataChanged(translated, translated.sibling(translated.row(), bottomRight.column()));
0141 
0142     // We provide funny data like "does this thread contain unread messages?". Now the original signal might mean that flags of a
0143     // nested message have changed. In order to always be consistent, we have to find the thread root and emit dataChanged() on that
0144     // as well.
0145     QModelIndex rootCandidate = translated;
0146     while (rootCandidate.parent().isValid()) {
0147         rootCandidate = rootCandidate.parent();
0148     }
0149     if (rootCandidate != translated) {
0150         // We're really an embedded message
0151         emit dataChanged(rootCandidate, rootCandidate.sibling(rootCandidate.row(), bottomRight.column()));
0152     }
0153 
0154     auto message = dynamic_cast<TreeItemMessage*>(static_cast<TreeItemMessage*>(topLeft.internalPointer()));
0155     Q_ASSERT(message);
0156     if (message->uid() == 0) {
0157         // UID is not yet known.
0158         // This is a legal situation, for example when an unsolicited FETCH FLAGS arrives and there's no UID in there.
0159         return;
0160     }
0161 
0162     QSet<TreeItem*>::iterator persistent = unknownUids.find(message);
0163     if (persistent != unknownUids.end()) {
0164         // The message wasn't fully synced before, and now it is
0165         persistent = unknownUids.erase(persistent);
0166         if (unknownUids.isEmpty()) {
0167             wantThreading();
0168         }
0169     }
0170 }
0171 
0172 QModelIndex ThreadingMsgListModel::index(int row, int column, const QModelIndex &parent) const
0173 {
0174     Q_ASSERT(!parent.isValid() || parent.model() == this);
0175 
0176     if (threading.isEmpty()) {
0177         // mapping not available yet
0178         return QModelIndex();
0179     }
0180 
0181     if (row < 0 || column < 0 || column >= MsgListModel::COLUMN_COUNT)
0182         return QModelIndex();
0183 
0184     if (parent.isValid() && parent.column() != 0) {
0185         // only the first column should have children
0186         return QModelIndex();
0187     }
0188 
0189     uint parentId = parent.isValid() ? parent.internalId() : 0;
0190 
0191     QHash<uint,ThreadNodeInfo>::const_iterator it = threading.constFind(parentId);
0192     Q_ASSERT(it != threading.constEnd());
0193 
0194     if (it->children.size() <= row)
0195         return QModelIndex();
0196 
0197     return createIndex(row, column, it->children[row]);
0198 }
0199 
0200 QModelIndex ThreadingMsgListModel::parent(const QModelIndex &index) const
0201 {
0202     if (! index.isValid() || index.model() != this)
0203         return QModelIndex();
0204 
0205     if (threading.isEmpty())
0206         return QModelIndex();
0207 
0208     if (index.row() < 0 || index.column() < 0 || index.column() >= MsgListModel::COLUMN_COUNT)
0209         return QModelIndex();
0210 
0211     QHash<uint,ThreadNodeInfo>::const_iterator node = threading.constFind(index.internalId());
0212     if (node == threading.constEnd())
0213         return QModelIndex();
0214 
0215     QHash<uint,ThreadNodeInfo>::const_iterator parentNode = threading.constFind(node->parent);
0216     Q_ASSERT(parentNode != threading.constEnd());
0217     Q_ASSERT(parentNode->internalId == node->parent);
0218 
0219     if (parentNode->internalId == 0)
0220         return QModelIndex();
0221 
0222     return createIndex(parentNode->offset, 0, parentNode->internalId);
0223 }
0224 
0225 bool ThreadingMsgListModel::hasChildren(const QModelIndex &parent) const
0226 {
0227     if (parent.isValid() && parent.column() != 0)
0228         return false;
0229 
0230     return ! threading.isEmpty() && ! threading.value(parent.internalId()).children.isEmpty();
0231 }
0232 
0233 int ThreadingMsgListModel::rowCount(const QModelIndex &parent) const
0234 {
0235     if (threading.isEmpty())
0236         return 0;
0237 
0238     if (parent.isValid() && parent.column() != 0)
0239         return 0;
0240 
0241     return threading.value(parent.internalId()).children.size();
0242 }
0243 
0244 int ThreadingMsgListModel::columnCount(const QModelIndex &parent) const
0245 {
0246     if (parent.isValid() && parent.column() != 0)
0247         return 0;
0248 
0249     return MsgListModel::COLUMN_COUNT;
0250 }
0251 
0252 QModelIndex ThreadingMsgListModel::mapToSource(const QModelIndex &proxyIndex) const
0253 {
0254     if (!proxyIndex.isValid() || !proxyIndex.internalId())
0255         return QModelIndex();
0256 
0257     if (threading.isEmpty())
0258         return QModelIndex();
0259 
0260     Imap::Mailbox::MsgListModel *msgList = qobject_cast<Imap::Mailbox::MsgListModel *>(sourceModel());
0261     Q_ASSERT(msgList);
0262 
0263     QHash<uint,ThreadNodeInfo>::const_iterator node = threading.constFind(proxyIndex.internalId());
0264     if (node == threading.constEnd())
0265         return QModelIndex();
0266 
0267     if (node->ptr) {
0268         return msgList->createIndex(node->ptr->row(), proxyIndex.column(), node->ptr);
0269     } else {
0270         // it's a fake message
0271         return QModelIndex();
0272     }
0273 }
0274 
0275 QModelIndex ThreadingMsgListModel::mapFromSource(const QModelIndex &sourceIndex) const
0276 {
0277     if (!sourceIndex.isValid())
0278         return QModelIndex();
0279 
0280     Q_ASSERT(sourceIndex.model() == sourceModel());
0281 
0282     QHash<void *,uint>::const_iterator it = ptrToInternal.constFind(sourceIndex.internalPointer());
0283     if (it == ptrToInternal.constEnd())
0284         return QModelIndex();
0285 
0286     const uint internalId = *it;
0287 
0288     QHash<uint,ThreadNodeInfo>::const_iterator node = threading.constFind(internalId);
0289     if (node == threading.constEnd()) {
0290         // The filtering criteria say that this index shall not be visible
0291         return QModelIndex();
0292     }
0293     Q_ASSERT(node != threading.constEnd());
0294 
0295     return createIndex(node->offset, sourceIndex.column(), internalId);
0296 }
0297 
0298 QVariant ThreadingMsgListModel::data(const QModelIndex &proxyIndex, int role) const
0299 {
0300     if (! proxyIndex.isValid() || proxyIndex.model() != this)
0301         return QVariant();
0302 
0303     QHash<uint,ThreadNodeInfo>::const_iterator it = threading.constFind(proxyIndex.internalId());
0304     Q_ASSERT(it != threading.constEnd());
0305 
0306     if (it->ptr) {
0307         // It's a real item which exists in the underlying model
0308         switch (role) {
0309         case RoleThreadRootWithUnreadMessages:
0310             if (proxyIndex.parent().isValid()) {
0311                 // We don't support this kind of questions for other messages than the roots of the threads.
0312                 // Other components, like the QML bindings, are however happy to request that, so let's just return
0313                 // a reasonable result instead of whinning about callers requesting useless stuff.
0314                 return false;
0315             } else {
0316                 return threadContainsUnreadMessages(it->internalId);
0317             }
0318         case RoleThreadAggregatedFlags:
0319             return threadAggregatedFlags(it->internalId);
0320         default:
0321             return QAbstractProxyModel::data(proxyIndex, role);
0322         }
0323     }
0324 
0325     switch (role) {
0326     case Qt::DisplayRole:
0327         if (proxyIndex.column() == 0)
0328             return tr("[Message is missing]");
0329         break;
0330     case Qt::ToolTipRole:
0331         return tr("This thread refers to an extra message, but that message is not present in the "
0332                   "selected mailbox, or is missing from the current search context.");
0333     }
0334     return QVariant();
0335 }
0336 
0337 Qt::ItemFlags ThreadingMsgListModel::flags(const QModelIndex &index) const
0338 {
0339     if (! index.isValid() || index.model() != this)
0340         return Qt::NoItemFlags;
0341 
0342     QHash<uint,ThreadNodeInfo>::const_iterator it = threading.constFind(index.internalId());
0343     Q_ASSERT(it != threading.constEnd());
0344     if (it->ptr && it->uid)
0345         return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled;
0346 
0347     return Qt::NoItemFlags;
0348 
0349 }
0350 
0351 void ThreadingMsgListModel::handleRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
0352 {
0353     Q_ASSERT(!parent.isValid());
0354 
0355     for (int i = start; i <= end; ++i) {
0356         QModelIndex index = sourceModel()->index(i, 0, parent);
0357         Q_ASSERT(index.isValid());
0358         QModelIndex translated = mapFromSource(index);
0359 
0360         unknownUids.remove(static_cast<TreeItem*>(index.internalPointer()));
0361 
0362         if (!translated.isValid()) {
0363             // The index being removed wasn't visible in our mapping anyway
0364             continue;
0365         }
0366 
0367         Q_ASSERT(translated.isValid());
0368         QHash<uint,ThreadNodeInfo>::iterator it = threading.find(translated.internalId());
0369         Q_ASSERT(it != threading.end());
0370         it->uid = 0;
0371         it->ptr = 0;
0372     }
0373 }
0374 
0375 void ThreadingMsgListModel::handleRowsRemoved(const QModelIndex &parent, int start, int end)
0376 {
0377     Q_ASSERT(!parent.isValid());
0378     Q_UNUSED(start);
0379     Q_UNUSED(end);
0380     if (!m_delayedPrune->isActive())
0381         m_delayedPrune->start();
0382 }
0383 
0384 void ThreadingMsgListModel::delayedPrune()
0385 {
0386     emit layoutAboutToBeChanged();
0387     updatePersistentIndexesPhase1();
0388     pruneTree();
0389     updatePersistentIndexesPhase2();
0390     emit layoutChanged();
0391 }
0392 
0393 void ThreadingMsgListModel::handleRowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
0394 {
0395     Q_ASSERT(!parent.isValid());
0396 
0397     int myStart = threading[0].children.size();
0398     int myEnd = myStart + (end - start);
0399     beginInsertRows(QModelIndex(), myStart, myEnd);
0400 }
0401 
0402 void ThreadingMsgListModel::handleRowsInserted(const QModelIndex &parent, int start, int end)
0403 {
0404     Q_ASSERT(!parent.isValid());
0405 
0406     for (int i = start; i <= end; ++i) {
0407         QModelIndex index = sourceModel()->index(i, 0);
0408         uint uid = index.data(RoleMessageUid).toUInt();
0409         ThreadNodeInfo node;
0410         node.internalId = ++threadingHelperLastId;
0411         node.uid = uid;
0412         node.ptr = static_cast<TreeItem *>(index.internalPointer());
0413         node.offset = threading[0].children.size();
0414         threading[node.internalId] = node;
0415         threading[0].children << node.internalId;
0416         ptrToInternal[node.ptr] = node.internalId;
0417         if (!node.uid) {
0418             unknownUids << static_cast<TreeItem*>(index.internalPointer());
0419         } else {
0420             threadedRootIds.append(node.internalId);
0421         }
0422     }
0423     endInsertRows();
0424 
0425     if (!m_sortTask || !m_sortTask->isPersistent()) {
0426         m_currentSortResult.clear();
0427         if (m_searchValidity == RESULT_FRESH)
0428             m_searchValidity = RESULT_INVALIDATED;
0429     }
0430 
0431     if (m_shallBeThreading)
0432         wantThreading();
0433 }
0434 
0435 void ThreadingMsgListModel::resetMe()
0436 {
0437     // Prevent possible recursion here
0438     if (modelResetInProgress)
0439         return;
0440 
0441     beginResetModel();
0442     modelResetInProgress = true;
0443     threading.clear();
0444     ptrToInternal.clear();
0445     unknownUids.clear();
0446     threadedRootIds.clear();
0447     m_currentSortResult.clear();
0448     m_searchValidity = RESULT_INVALIDATED;
0449     endResetModel();
0450     updateNoThreading();
0451     modelResetInProgress = false;
0452     // Refresh the sorting/searching/threading preferences.
0453     // This is important, otherwise we won't track threading and/or the sort direction after e.g. changing a mailbox.
0454     wantThreading();
0455 }
0456 
0457 void ThreadingMsgListModel::updateNoThreading()
0458 {
0459     threadingHelperLastId = 0;
0460 
0461     if (!sourceModel()) {
0462         // Maybe we got reset because the parent model is no longer here...
0463         if (! threading.isEmpty()) {
0464             beginRemoveRows(QModelIndex(), 0, rowCount() - 1);
0465             threading.clear();
0466             ptrToInternal.clear();
0467             endRemoveRows();
0468         }
0469         unknownUids.clear();
0470         return;
0471     }
0472 
0473     emit layoutAboutToBeChanged();
0474     updatePersistentIndexesPhase1();
0475     threading.clear();
0476     ptrToInternal.clear();
0477     unknownUids.clear();
0478     threadedRootIds.clear();
0479 
0480     int upstreamMessages = sourceModel()->rowCount();
0481     QList<uint> allIds;
0482     QHash<uint,ThreadNodeInfo> newThreading;
0483     QHash<void *,uint> newPtrToInternal;
0484 
0485     if (upstreamMessages) {
0486         // Prefer the direct pointer access instead of going through the MVC API -- similar to how applyThreading() works.
0487         // This improves the speed of the testSortingPerformance benchmark by 18%.
0488         QModelIndex firstMessageIndex = sourceModel()->index(0, 0);
0489         Q_ASSERT(firstMessageIndex.isValid());
0490         const Model *realModel = 0;
0491         TreeItem *firstMessagePtr = Model::realTreeItem(firstMessageIndex, &realModel);
0492         Q_ASSERT(firstMessagePtr);
0493         // If the next asserts fails, it means that the implementation of MsgListModel has changed and uses its own pointers
0494         Q_ASSERT(firstMessagePtr == firstMessageIndex.internalPointer());
0495         TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(firstMessagePtr->parent());
0496         Q_ASSERT(list);
0497 
0498         newThreading.reserve(upstreamMessages + headroomForNewmessages);
0499         newPtrToInternal.reserve(upstreamMessages + headroomForNewmessages);
0500 
0501         for (int i = 0; i < upstreamMessages; ++i) {
0502             TreeItemMessage *ptr = static_cast<TreeItemMessage*>(list->m_children[i]);
0503             Q_ASSERT(ptr);
0504             ThreadNodeInfo node;
0505             node.internalId = i + 1;
0506             node.uid = ptr->uid();
0507             node.ptr = ptr;
0508             node.offset = i;
0509             newThreading[node.internalId] = node;
0510             allIds.append(node.internalId);
0511             newPtrToInternal[node.ptr] = node.internalId;
0512             if (!node.uid) {
0513                 unknownUids << ptr;
0514             }
0515         }
0516     }
0517 
0518     if (newThreading.size()) {
0519         threading = newThreading;
0520         ptrToInternal = newPtrToInternal;
0521         threading[ 0 ].children = allIds;
0522         threading[ 0 ].ptr = 0;
0523         threadingHelperLastId = newThreading.size();
0524         threadedRootIds = threading[0].children;
0525     }
0526     updatePersistentIndexesPhase2();
0527     emit layoutChanged();
0528 }
0529 
0530 void ThreadingMsgListModel::wantThreading(const SkipSortSearch skipSortSearch)
0531 {
0532     if (!sourceModel() || !sourceModel()->rowCount() || !m_shallBeThreading) {
0533         updateNoThreading();
0534         if (skipSortSearch == AUTO_SORT_SEARCH) {
0535             searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder);
0536         }
0537         return;
0538     }
0539 
0540     if (threadingInFlight) {
0541         // Imagine the following scenario:
0542         // <<< "* 3 EXISTS"
0543         // Message 2 has unknown UID
0544         // >>> "y4 UID FETCH 66:* (FLAGS)"
0545         // >>> "y5 UID THREAD REFS utf-8 ALL"
0546         // <<< "* 3 FETCH (UID 66 FLAGS ())"
0547         // Got UID for seq# 3
0548         // ThreadingMsgListModel::wantThreading: THREAD contains info about UID 1 (or higher), mailbox has 66
0549         //    *** this is the interesting part ***
0550         // <<< "y4 OK fetch"
0551         // <<< "* THREAD (1)(2)(66)"
0552         // <<< "y5 OK thread"
0553         // >>> "y6 UID THREAD REFS utf-8 ALL"
0554         //
0555         // See, at the indicated (***) place, we already have an in-flight THREAD request and receive UID for newly arrived
0556         // message.  We certainly don't want to ask for threading once again; it's better to wait a bit and only ask when the
0557         // to-be-received THREAD does not contain all required UIDs.
0558         if (skipSortSearch == AUTO_SORT_SEARCH) {
0559             searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder);
0560         }
0561         return;
0562     }
0563 
0564     const Imap::Mailbox::Model *realModel;
0565     QModelIndex someMessage = sourceModel()->index(0,0);
0566     QModelIndex realIndex;
0567     Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex);
0568     QModelIndex mailbox = realIndex.parent().parent();
0569     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList*>(static_cast<TreeItem*>(realIndex.parent().internalPointer()));
0570     Q_ASSERT(list);
0571 
0572     // Something has happened and we want to process the THREAD response
0573     QVector<Imap::Responses::ThreadingNode> mapping = realModel->cache()->messageThreading(mailbox.data(RoleMailboxName).toString());
0574 
0575     // Find the UID of the last message in the mailbox
0576     QString scope;
0577     uint highestUidInMailbox;
0578     if (m_filteredBySearch) {
0579         scope = QLatin1String("search");
0580         if (m_searchValidity != RESULT_FRESH) {
0581             if (m_searchValidity != RESULT_ASKED) {
0582                 if (skipSortSearch == AUTO_SORT_SEARCH) {
0583                     searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder);
0584                 }
0585             } else {
0586                 logTrace(QStringLiteral("Seems like the server answered in reverse order to our SEARCH/THREAD question. It's okay, threading should be tried later."));
0587             }
0588             return;
0589         }
0590         // Required due to incremental updates
0591         highestUidInMailbox = *std::max_element(m_currentSortResult.constBegin(), m_currentSortResult.constEnd());
0592     } else {
0593         scope = QLatin1String("mailbox");
0594         highestUidInMailbox = findHighestUidInMailbox(list);
0595     }
0596     uint highestUidInThreadingLowerBound = findHighEnoughNumber(mapping, highestUidInMailbox);
0597     logTrace(QStringLiteral("ThreadingMsgListModel::wantThreading: THREAD contains info about UID %1 (or higher), %2 has %3")
0598              .arg(QString::number(highestUidInThreadingLowerBound), scope, QString::number(highestUidInMailbox)));
0599 
0600     if (highestUidInThreadingLowerBound >= highestUidInMailbox) {
0601         // There's no point asking for data at this point, we shall just apply threading
0602         applyThreading(mapping);
0603     } else {
0604         // There's apparently at least one known UID whose threading info we do not know; that means that we have to ask the
0605         // server here.
0606         auto roughlyLastKnown = const_cast<Model*>(realModel)->findMessageOrNextOneByUid(list, highestUidInThreadingLowerBound);
0607         if (list->m_children.end() - roughlyLastKnown >= 50 || roughlyLastKnown == list->m_children.begin()) {
0608             askForThreading();
0609         } else {
0610             askForThreading(static_cast<TreeItemMessage*>(*roughlyLastKnown)->uid() + 1);
0611         }
0612     }
0613 }
0614 
0615 uint ThreadingMsgListModel::findHighestUidInMailbox(TreeItemMsgList *list)
0616 {
0617     uint highestUidInMailbox = 0;
0618     for (int i = sourceModel()->rowCount() - 1; i > -1 && !highestUidInMailbox; --i) {
0619         highestUidInMailbox = dynamic_cast<TreeItemMessage*>(list->m_children[i])->uid();
0620     }
0621     return highestUidInMailbox;
0622 }
0623 
0624 uint ThreadingMsgListModel::findHighEnoughNumber(const QVector<Responses::ThreadingNode> &mapping, uint marker)
0625 {
0626     if (mapping.isEmpty())
0627         return 0;
0628 
0629     // Find the highest UID for which we have the threading info
0630     uint highestUidInThreadingLowerBound = 0;
0631 
0632     // If the threading already contains everything we need, we could have a higher chance of finding the high enough UID at the
0633     // end of the list.  On the other hand, in case when the cached THREAD response does not cintain everything we need, we're out
0634     // of luck, we have absolutely no guarantee about relative greatness of parent/child/siblings in the tree.
0635     // Searching backward could lead to faster lookups, but we cannot avoid a full lookup in the bad case.
0636     for (int i = mapping.size() - 1; i >= 0; --i) {
0637         if (highestUidInThreadingLowerBound < mapping[i].num) {
0638             highestUidInThreadingLowerBound = mapping[i].num;
0639 
0640             if (highestUidInThreadingLowerBound >= marker) {
0641                 // There's no point going further, we already know that we shall ask for threading
0642                 return highestUidInThreadingLowerBound;
0643             }
0644         }
0645 
0646         // OK, we have to consult our children
0647         highestUidInThreadingLowerBound = qMax(highestUidInThreadingLowerBound, findHighEnoughNumber(mapping[i].children, marker));
0648         if (highestUidInThreadingLowerBound >= marker) {
0649             return highestUidInThreadingLowerBound;
0650         }
0651     }
0652     return highestUidInThreadingLowerBound;
0653 }
0654 
0655 void ThreadingMsgListModel::askForThreading(const uint firstUnknownUid)
0656 {
0657     Q_ASSERT(m_shallBeThreading);
0658     Q_ASSERT(sourceModel());
0659     Q_ASSERT(sourceModel()->rowCount());
0660 
0661     const Imap::Mailbox::Model *realModel = nullptr;
0662     QModelIndex someMessage = sourceModel()->index(0,0);
0663     QModelIndex realIndex;
0664     Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex);
0665     Q_ASSERT(realModel);
0666     QModelIndex mailboxIndex = realIndex.parent().parent();
0667 
0668     if (realModel->capabilities().contains(QStringLiteral("THREAD=REFS"))) {
0669         requestedAlgorithm = "REFS";
0670     } else if (realModel->capabilities().contains(QStringLiteral("THREAD=REFERENCES"))) {
0671         requestedAlgorithm = "REFERENCES";
0672     } else if (realModel->capabilities().contains(QStringLiteral("THREAD=ORDEREDSUBJECT"))) {
0673         requestedAlgorithm = "ORDEREDSUBJECT";
0674     }
0675 
0676     if (! requestedAlgorithm.isEmpty()) {
0677         threadingInFlight = true;
0678         if (firstUnknownUid && realModel->capabilities().contains(QStringLiteral("INCTHREAD"))) {
0679             auto threadTask = realModel->m_taskFactory->
0680                     createIncrementalThreadTask(const_cast<Model *>(realModel), mailboxIndex, requestedAlgorithm,
0681                                                                     QStringList() << QStringLiteral("INTHREAD") << QString::fromUtf8(requestedAlgorithm) << QStringLiteral("UID") <<
0682                                                                         QString::fromUtf8(Sequence::startingAt(firstUnknownUid).toByteArray()));
0683             connect(threadTask, &ThreadTask::incrementalThreadingAvailable,
0684                     this, &ThreadingMsgListModel::slotIncrementalThreadingAvailable);
0685             connect(threadTask, &ImapTask::failed, this, &ThreadingMsgListModel::slotIncrementalThreadingFailed);
0686         } else {
0687             auto searchConditions = m_filteredBySearch ? m_currentSearchConditions : QStringList() << QStringLiteral("ALL");
0688             realModel->m_taskFactory->createThreadTask(const_cast<Model *>(realModel), mailboxIndex,
0689                                                        requestedAlgorithm, searchConditions);
0690             connect(realModel, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable);
0691             connect(realModel, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed);
0692         }
0693     }
0694 }
0695 
0696 /** @short Gather all UIDs present in the mapping and push them into the "uids" vector */
0697 static void gatherAllUidsFromThreadNode(Imap::Uids &uids, const QVector<Responses::ThreadingNode> &list)
0698 {
0699     for (QVector<Responses::ThreadingNode>::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) {
0700         uids.push_back(it->num);
0701         gatherAllUidsFromThreadNode(uids, it->children);
0702     }
0703 }
0704 
0705 void ThreadingMsgListModel::slotIncrementalThreadingAvailable(const Responses::ESearch::IncrementalThreadingData_t &data)
0706 {
0707     // Preparation: get through to the real model
0708     const Imap::Mailbox::Model *realModel;
0709     QModelIndex someMessage = sourceModel()->index(0,0);
0710     Q_ASSERT(someMessage.isValid());
0711     QModelIndex realIndex;
0712     Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex);
0713     QModelIndex mailboxIndex = realIndex.parent().parent();
0714     Q_ASSERT(mailboxIndex.isValid());
0715 
0716     // First phase: remove all messages mentioned in the incremental responses from their original placement
0717     Imap::Uids affectedUids;
0718     for (Responses::ESearch::IncrementalThreadingData_t::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) {
0719         gatherAllUidsFromThreadNode(affectedUids, it->thread);
0720     }
0721     std::sort(affectedUids.begin(), affectedUids.end());
0722     QList<TreeItemMessage*> affectedMessages = const_cast<Model*>(realModel)->
0723             findMessagesByUids(static_cast<TreeItemMailbox*>(mailboxIndex.internalPointer()), affectedUids);
0724     QHash<uint,void *> uidToPtrCache;
0725 
0726 
0727     emit layoutAboutToBeChanged();
0728     updatePersistentIndexesPhase1();
0729     for (QList<TreeItemMessage*>::const_iterator it = affectedMessages.constBegin(); it != affectedMessages.constEnd(); ++it) {
0730         QHash<void *,uint>::const_iterator ptrMappingIt = ptrToInternal.constFind(*it);
0731         Q_ASSERT(ptrMappingIt != ptrToInternal.constEnd());
0732         QHash<uint,ThreadNodeInfo>::iterator threadIt = threading.find(*ptrMappingIt);
0733         Q_ASSERT(threadIt != threading.end());
0734         uidToPtrCache[(*it)->uid()] = threadIt->ptr;
0735         threadIt->ptr = 0;
0736     }
0737     pruneTree();
0738     updatePersistentIndexesPhase2();
0739     emit layoutChanged();
0740 
0741     // Second phase: for each message whose UID is returned by the server, update the threading data
0742     QSet<uint> usedNodes;
0743     emit layoutAboutToBeChanged();
0744     updatePersistentIndexesPhase1();
0745     for (Responses::ESearch::IncrementalThreadingData_t::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) {
0746         registerThreading(it->thread, 0, uidToPtrCache, usedNodes);
0747         int actualOffset = threading[0].children.size() - 1;
0748         int expectedOffsetOfPrevious = threading[0].children.indexOf(it->previousThreadRoot);
0749         if (actualOffset == expectedOffsetOfPrevious + 1) {
0750             // it's on the correct position, yay!
0751         } else {
0752             // move the new subthread to a correct place
0753             threading[0].children.insert(expectedOffsetOfPrevious + 1, threading[0].children.takeLast());
0754             // push the rest (including the new arrival) forward
0755             for (int i = expectedOffsetOfPrevious + 1; i < threading[0].children.size(); ++i) {
0756                 threading[threading[0].children[i]].offset = i;
0757             }
0758         }
0759     }
0760     updatePersistentIndexesPhase2();
0761     emit layoutChanged();
0762 }
0763 
0764 void ThreadingMsgListModel::slotIncrementalThreadingFailed()
0765 {
0766 }
0767 
0768 bool ThreadingMsgListModel::shouldIgnoreThisThreadingResponse(const QModelIndex &mailbox, const QByteArray &algorithm,
0769         const QStringList &searchCriteria, const Model **realModel)
0770 {
0771     QModelIndex someMessage = sourceModel()->index(0,0);
0772     if (!someMessage.isValid())
0773         return true;
0774     const Model *model;
0775     QModelIndex realIndex;
0776     Imap::Mailbox::Model::realTreeItem(someMessage, &model, &realIndex);
0777     QModelIndex mailboxIndex = realIndex.parent().parent();
0778     if (mailboxIndex != mailbox) {
0779         // this is for another mailbox
0780         return true;
0781     }
0782 
0783     if (algorithm != requestedAlgorithm) {
0784         logTrace(QStringLiteral("Weird, asked for threading via %1 but got %2 instead -- ignoring")
0785                  .arg(QString::fromUtf8(requestedAlgorithm), QString::fromUtf8(algorithm)));
0786         return true;
0787     }
0788 
0789     if (realModel)
0790         *realModel = model;
0791     return false;
0792 }
0793 
0794 void ThreadingMsgListModel::slotThreadingFailed(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria)
0795 {
0796     // Better safe than sorry -- prevent infinite waiting to the maximal possible extent
0797     threadingInFlight = false;
0798 
0799     if (shouldIgnoreThisThreadingResponse(mailbox, algorithm, searchCriteria))
0800         return;
0801 
0802     auto model = qobject_cast<Model*>(sender());
0803     Q_ASSERT(model);
0804     disconnect(model, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable);
0805     disconnect(model, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed);
0806 
0807     updateNoThreading();
0808 }
0809 
0810 void ThreadingMsgListModel::slotThreadingAvailable(const QModelIndex &mailbox, const QByteArray &algorithm,
0811         const QStringList &searchCriteria,
0812         const QVector<Imap::Responses::ThreadingNode> &mapping)
0813 {
0814     // Better safe than sorry -- prevent infinite waiting to the maximal possible extent
0815     threadingInFlight = false;
0816 
0817     const Model *model = 0;
0818     if (shouldIgnoreThisThreadingResponse(mailbox, algorithm, searchCriteria, &model))
0819         return;
0820 
0821     Q_ASSERT(model);
0822     disconnect(model, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable);
0823     disconnect(model, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed);
0824 
0825     model->cache()->setMessageThreading(mailbox.data(RoleMailboxName).toString(), mapping);
0826 
0827     // Indirect processing here -- the wantThreading() will check that the received response really contains everything we need
0828     // and if it does, simply applyThreading() that.  If there's something missing, it will ask for the threading again.
0829     if (m_shallBeThreading)
0830         wantThreading();
0831 }
0832 
0833 void ThreadingMsgListModel::slotSortingAvailable(const Imap::Uids &uids)
0834 {
0835     if (!m_sortTask->isPersistent()) {
0836         disconnect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable);
0837         disconnect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed);
0838         disconnect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate);
0839 
0840         m_sortTask = 0;
0841     }
0842 
0843     m_currentSortResult = uids;
0844     if (m_searchValidity == RESULT_ASKED)
0845         m_searchValidity = RESULT_FRESH;
0846     wantThreading();
0847 }
0848 
0849 void ThreadingMsgListModel::slotSortingFailed()
0850 {
0851     disconnect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable);
0852     disconnect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed);
0853     disconnect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate);
0854 
0855     m_sortTask = 0;
0856     m_sortReverse = false;
0857     calculateNullSort();
0858     applySort();
0859     emit sortingFailed();
0860 }
0861 
0862 void ThreadingMsgListModel::slotSortingIncrementalUpdate(const Responses::ESearch::IncrementalContextData_t &updates)
0863 {
0864     for (Responses::ESearch::IncrementalContextData_t::const_iterator it = updates.constBegin(); it != updates.constEnd(); ++it) {
0865         switch (it->modification) {
0866         case Responses::ESearch::ContextIncrementalItem::ADDTO:
0867             for (int i = 0; i < it->uids.size(); ++i)  {
0868                 int offset;
0869                 if (it->offset == 0) {
0870                     // FIXME: use mailbox order later on
0871                     offset = 0;
0872                 } else {
0873                     // IMAP uses one-based indexing, we use zero-based offsets
0874                     offset = it->offset + i - 1;
0875                 }
0876                 if (offset < 0 || offset > m_currentSortResult.size()) {
0877                     throw MailboxException("ESEARCH: ADDTO out of bounds");
0878                 }
0879                 m_currentSortResult.insert(offset, it->uids[i]);
0880             }
0881             break;
0882 
0883         case Responses::ESearch::ContextIncrementalItem::REMOVEFROM:
0884             for (int i = 0; i < it->uids.size(); ++i)  {
0885                 if (it->offset == 0) {
0886                     // When the offset is not given, we have to find it ourselves
0887                     auto item = std::find(m_currentSortResult.begin(), m_currentSortResult.end(), it->uids[i]);
0888                     if (item == m_currentSortResult.end()) {
0889                         throw MailboxException("ESEARCH: there's no such UID");
0890                     }
0891                     m_currentSortResult.erase(item);
0892                 } else {
0893                     // We're given an offset, so let's make sure it is a correct one
0894                     int offset = it->offset + i - 1;
0895                     if (offset < 0 || offset >= m_currentSortResult.size()) {
0896                         throw MailboxException("ESEARCH: REMOVEFROM out of bounds");
0897                     }
0898                     if (m_currentSortResult[offset] != it->uids[i]) {
0899                         throw MailboxException("ESEARCH: REMOVEFROM UID mismatch");
0900                     }
0901                     m_currentSortResult.remove(offset);
0902                 }
0903             }
0904             break;
0905         }
0906     }
0907     m_searchValidity = RESULT_FRESH;
0908     wantThreading();
0909 }
0910 
0911 /** @short Store UIDs of the thread roots as the "current search order" */
0912 void ThreadingMsgListModel::calculateNullSort()
0913 {
0914     m_currentSortResult.clear();
0915     m_currentSortResult.reserve(threadedRootIds.size() + headroomForNewmessages);
0916     Q_FOREACH(const uint internalId, threadedRootIds) {
0917         QHash<uint,ThreadNodeInfo>::const_iterator it = threading.constFind(internalId);
0918         if (it == threading.constEnd())
0919             continue;
0920         if (it->uid)
0921             m_currentSortResult.append(it->uid);
0922     }
0923     m_searchValidity = RESULT_FRESH;
0924 }
0925 
0926 void ThreadingMsgListModel::applyThreading(const QVector<Imap::Responses::ThreadingNode> &mapping)
0927 {
0928     if (! unknownUids.isEmpty()) {
0929         // Some messages have UID zero, which means that they weren't loaded yet. Too bad.
0930         logTrace(QStringLiteral("%1 messages have 0 UID").arg(unknownUids.size()));
0931         return;
0932     }
0933 
0934     emit layoutAboutToBeChanged();
0935 
0936     updatePersistentIndexesPhase1();
0937 
0938     threading.clear();
0939     ptrToInternal.clear();
0940     // Default-construct the root node
0941     threading[ 0 ].ptr = 0;
0942 
0943     // At first, initialize threading nodes for all messages which are right now available in the mailbox.
0944     // We risk that we will have to delete some of them later on, but this is likely better than doing a lookup
0945     // for each UID individually (remember, the THREAD response might contain UIDs in crazy order).
0946     int upstreamMessages = sourceModel()->rowCount();
0947     QHash<uint,void *> uidToPtrCache;
0948     QSet<uint> usedNodes;
0949     uidToPtrCache.reserve(upstreamMessages + headroomForNewmessages);
0950     threading.reserve(upstreamMessages + headroomForNewmessages);
0951     ptrToInternal.reserve(upstreamMessages + headroomForNewmessages);
0952 
0953     if (upstreamMessages) {
0954         // Work with pointers instead going through the MVC API for performance.
0955         // This matters (at least that's what by benchmarks said).
0956         QModelIndex firstMessageIndex = sourceModel()->index(0, 0);
0957         Q_ASSERT(firstMessageIndex.isValid());
0958         const Model *realModel = 0;
0959         TreeItem *firstMessagePtr = Model::realTreeItem(firstMessageIndex, &realModel);
0960         Q_ASSERT(firstMessagePtr);
0961         // If the next asserts fails, it means that the implementation of MsgListModel has changed and uses its own pointers
0962         Q_ASSERT(firstMessagePtr == firstMessageIndex.internalPointer());
0963         TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(firstMessagePtr->parent());
0964         Q_ASSERT(list);
0965         for (int i = 0; i < upstreamMessages; ++i) {
0966             ThreadNodeInfo node;
0967             node.uid = dynamic_cast<TreeItemMessage *>(list->m_children[i])->uid();
0968             if (! node.uid) {
0969                 throw UnknownMessageIndex("Encountered a message with zero UID when threading. This is a bug in Trojita, sorry.");
0970             }
0971 
0972             node.internalId = i + 1;
0973             node.ptr = list->m_children[i];
0974             uidToPtrCache[node.uid] = node.ptr;
0975             threadingHelperLastId = node.internalId;
0976             // We're creating a new node here
0977             Q_ASSERT(!threading.contains(node.internalId));
0978             threading[ node.internalId ] = node;
0979             ptrToInternal[ node.ptr ] = node.internalId;
0980         }
0981     }
0982 
0983     // Mark the root node as always present
0984     usedNodes.insert(0);
0985 
0986     // Set up parents and find the list of all used nodes
0987     registerThreading(mapping, 0, uidToPtrCache, usedNodes);
0988 
0989     // Now remove all messages which were not referenced in the THREAD response from our mapping
0990     QHash<uint,ThreadNodeInfo>::iterator it = threading.begin();
0991     while (it != threading.end()) {
0992         if (usedNodes.contains(it.key())) {
0993             // this message should be shown
0994             ++it;
0995         } else {
0996             // this message is not included in the list of messages actually to be shown
0997             ptrToInternal.remove(it->ptr);
0998             it = threading.erase(it);
0999         }
1000     }
1001     pruneTree();
1002     updatePersistentIndexesPhase2();
1003     if (rowCount())
1004         threadedRootIds = threading[0].children;
1005     emit layoutChanged();
1006 
1007     // If the sorting was active before, we shall reactivate it now
1008     searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder);
1009 }
1010 
1011 void ThreadingMsgListModel::registerThreading(const QVector<Imap::Responses::ThreadingNode> &mapping, uint parentId, const QHash<uint,void *> &uidToPtr, QSet<uint> &usedNodes)
1012 {
1013     Q_FOREACH(const Imap::Responses::ThreadingNode &node, mapping) {
1014         uint nodeId;
1015         QHash<uint,void *>::const_iterator ptrIt;
1016         if (node.num == 0 ||
1017                 (ptrIt = uidToPtr.find(node.num)) == uidToPtr.constEnd()) {
1018             // Either this is an empty node, or the THREAD response references a UID which is no longer in the mailbox.
1019             // This is a valid scenario; it can happen e.g. when reusing data from cache, or when a message got
1020             // expunged after the untagged THREAD was received, but before the tagged OK.
1021             // We cannot just ignore this node, though, because it might have some children which we would otherwise
1022             // simply hide.
1023             // The ptrIt which is initialized by the condition is used in the else branch.
1024             ThreadNodeInfo fake;
1025             fake.internalId = ++threadingHelperLastId;
1026             fake.parent = parentId;
1027             Q_ASSERT(threading.contains(parentId));
1028             // The child will be registered to the list of parent's children after the if/else branch
1029             threading[ fake.internalId ] = fake;
1030             nodeId = fake.internalId;
1031         } else {
1032             QHash<void *,uint>::const_iterator nodeIt = ptrToInternal.constFind(*ptrIt);
1033             // The following assert would fail if there was a node with a valid UID, but not in our ptrToInternal mapping.
1034             // That is however non-issue, as we pre-create nodes for all messages beforehand.
1035             Q_ASSERT(nodeIt != ptrToInternal.constEnd());
1036             nodeId = *nodeIt;
1037             // This is needed for the incremental stuff
1038             threading[nodeId].ptr = static_cast<TreeItem*>(*ptrIt);
1039         }
1040         threading[nodeId].offset = threading[parentId].children.size();
1041         threading[ parentId ].children.append(nodeId);
1042         threading[ nodeId ].parent = parentId;
1043         usedNodes.insert(nodeId);
1044         registerThreading(node.children, nodeId, uidToPtr, usedNodes);
1045     }
1046 }
1047 
1048 /** @short Gather a list of persistent indexes which we have to transform after out layout change */
1049 void ThreadingMsgListModel::updatePersistentIndexesPhase1()
1050 {
1051     oldPersistentIndexes = persistentIndexList();
1052     oldPtrs.clear();
1053     Q_FOREACH(const QModelIndex &idx, oldPersistentIndexes) {
1054         // the index could get invalidated by the pruneTree() or something else manipulating our threading
1055         bool isOk = idx.isValid() && threading.contains(idx.internalId());
1056         if (!isOk) {
1057             oldPtrs << 0;
1058             continue;
1059         }
1060         QModelIndex translated = mapToSource(idx);
1061         if (!translated.isValid()) {
1062             // another stale item
1063             oldPtrs << 0;
1064             continue;
1065         }
1066         oldPtrs << translated.internalPointer();
1067     }
1068 }
1069 
1070 /** @short Update the gathered persistent indexes after our change in the layout */
1071 void ThreadingMsgListModel::updatePersistentIndexesPhase2()
1072 {
1073     Q_ASSERT(oldPersistentIndexes.size() == oldPtrs.size());
1074     QList<QModelIndex> updatedIndexes;
1075     for (int i = 0; i < oldPersistentIndexes.size(); ++i) {
1076         QHash<void *,uint>::const_iterator ptrIt = ptrToInternal.constFind(oldPtrs[i]);
1077         if (ptrIt == ptrToInternal.constEnd()) {
1078             // That message is no longer there
1079             updatedIndexes.append(QModelIndex());
1080             continue;
1081         }
1082         QHash<uint,ThreadNodeInfo>::const_iterator it = threading.constFind(*ptrIt);
1083         if (it == threading.constEnd()) {
1084             // Filtering doesn't accept this index, let's declare it dead
1085             updatedIndexes.append(QModelIndex());
1086         } else {
1087             updatedIndexes.append(createIndex(it->offset, oldPersistentIndexes[i].column(), it->internalId));
1088         }
1089     }
1090     Q_ASSERT(oldPersistentIndexes.size() == updatedIndexes.size());
1091     changePersistentIndexList(oldPersistentIndexes, updatedIndexes);
1092     oldPersistentIndexes.clear();
1093     oldPtrs.clear();
1094 }
1095 
1096 void ThreadingMsgListModel::pruneTree()
1097 {
1098     // Our mapping (threading) is completely unsorted, which means that we simply don't have any way of walking the tree from
1099     // the top. Instead, we got to work with a random walk, processing nodes in an unspecified order.  If we iterated on the QHash
1100     // directly, we'd hit an issue with iterator ordering (basically, we want to be able to say "hey, I don't care at which point
1101     // of the iteration I'm right now, the next node to process should be that one, and then we should resume with the rest").
1102     QList<uint> pending = threading.keys();
1103 
1104     // These are the parents whose children will have to be renumbered later on
1105     QSet<uint> parentsForRenumbering;
1106 
1107     for (QList<uint>::iterator id = pending.begin(); id != pending.end(); /* nothing */) {
1108         // Convert to the hashmap
1109         // The "it" iterator point to the current node in the threading mapping
1110         QHash<uint, ThreadNodeInfo>::iterator it = threading.find(*id);
1111         if (it == threading.end()) {
1112             // We've already seen this node, that's due to promoting
1113             ++id;
1114             continue;
1115         }
1116 
1117         if (it->internalId == 0) {
1118             // A special root item; we should not delete that one :)
1119             ++id;
1120             continue;
1121         }
1122         if (it->ptr) {
1123             // regular and valid message -> skip
1124             ++id;
1125         } else {
1126             // a fake one
1127 
1128             // each node has a parent
1129             QHash<uint, ThreadNodeInfo>::iterator parent = threading.find(it->parent);
1130             Q_ASSERT(parent != threading.end());
1131 
1132             // and the node itself has to be found in its parent's children
1133             QList<uint>::iterator childIt = std::find(parent->children.begin(), parent->children.end(), it->internalId);
1134             Q_ASSERT(childIt != parent->children.end());
1135             // The offset of this child might no longer be correct, though -- we're postponing the actual deletion until later
1136 
1137             if (it->children.isEmpty()) {
1138                 // This is a leaf node, so we can just remove it
1139                 childIt = parent->children.erase(childIt);
1140                 // We do not perform the renumbering immediately, that would lead to an O(n^2) performance when deleting nodes.
1141                 parentsForRenumbering.insert(it->parent);
1142                 parentsForRenumbering.remove(it->internalId);
1143 
1144                 if (it->parent == 0) {
1145                     threadedRootIds.removeOne(it->internalId);
1146                 }
1147                 threading.erase(it);
1148                 ++id;
1149 
1150             } else {
1151                 // This node has some children, so we can't just delete it. Instead of that, we promote its first child
1152                 // to replace this node.
1153                 QHash<uint, ThreadNodeInfo>::iterator replaceWith = threading.find(it->children.first());
1154                 Q_ASSERT(replaceWith != threading.end());
1155 
1156                 // The offsets will, again, be updated later on
1157                 parentsForRenumbering.insert(it->parent);
1158                 parentsForRenumbering.insert(replaceWith.key());
1159                 parentsForRenumbering.remove(it->internalId);
1160 
1161                 // Replace the node
1162                 *childIt = it->children.first();
1163                 replaceWith->parent = parent->internalId;
1164 
1165                 // Now merge the lists of children
1166                 it->children.removeFirst();
1167                 replaceWith->children = replaceWith->children + it->children;
1168 
1169                 // Fix parent information of all children of the replacement node
1170                 for (int i = 0; i < replaceWith->children.size(); ++i) {
1171                     QHash<uint, ThreadNodeInfo>::iterator sibling = threading.find(replaceWith->children[i]);
1172                     Q_ASSERT(sibling != threading.end());
1173                     sibling->parent = replaceWith.key();
1174                 }
1175 
1176                 if (parent->internalId == 0) {
1177                     // Update the list of all thread roots
1178                     QList<uint>::iterator rootIt = std::find(threadedRootIds.begin(), threadedRootIds.end(), it->internalId);
1179                     if (rootIt != threadedRootIds.end())
1180                         *rootIt = replaceWith->internalId;
1181                 }
1182 
1183                 // Now that all references are gone, remove the original node
1184                 threading.erase(it);
1185 
1186                 if (!replaceWith->ptr) {
1187                     // If the just-promoted item is also a fake one, we'll have to visit it as well. This assignment is safe,
1188                     // because we've already processed the current item and are completely done with it. The worst which can
1189                     // happen is that we'll visit the same node twice, which is reasonably acceptable.
1190                     *id = replaceWith.key();
1191                 }
1192             }
1193         }
1194     }
1195 
1196     // Now fix the sequential numbering of all siblings of deleted children
1197     Q_FOREACH(const auto parentId, parentsForRenumbering) {
1198         auto parentIt = threading.constFind(parentId);
1199         Q_ASSERT(parentIt != threading.constEnd());
1200         int offset = 0;
1201         for (auto childNumber = parentIt->children.constBegin(); childNumber != parentIt->children.constEnd(); ++childNumber, ++offset) {
1202             auto childIt = threading.find(*childNumber);
1203             Q_ASSERT(childIt != threading.end());
1204             childIt->offset = offset;
1205         }
1206     }
1207 }
1208 
1209 QStringList ThreadingMsgListModel::supportedCapabilities()
1210 {
1211     return QStringList() << QStringLiteral("THREAD=REFS") << QStringLiteral("THREAD=REFERENCES") << QStringLiteral("THREAD=ORDEREDSUBJECT");
1212 }
1213 
1214 QStringList ThreadingMsgListModel::mimeTypes() const
1215 {
1216     return sourceModel() ? sourceModel()->mimeTypes() : QStringList();
1217 }
1218 
1219 QMimeData *ThreadingMsgListModel::mimeData(const QModelIndexList &indexes) const
1220 {
1221     if (! sourceModel())
1222         return 0;
1223 
1224     QModelIndexList translated;
1225     Q_FOREACH(const QModelIndex &idx, indexes) {
1226         translated << mapToSource(idx);
1227     }
1228     return sourceModel()->mimeData(translated);
1229 }
1230 
1231 template<typename T>
1232 bool threadForeachCallback(std::function<T(const TreeItemMessage &)> callback, const TreeItemMessage &message)
1233 {
1234     callback(message);
1235     return false;
1236 }
1237 template<>
1238 bool threadForeachCallback<const bool>(std::function<const bool(const TreeItemMessage &)> callback, const TreeItemMessage &message)
1239 {
1240     return callback(message);
1241 }
1242 
1243 /** @short Execute the provided function once for each message
1244 
1245 Returns immediately if the provided function returns `true`.
1246 */
1247 template<typename T>
1248 void ThreadingMsgListModel::threadForeach(const uint &root, std::function<T(const TreeItemMessage &)> callback) const
1249 {
1250     QList<uint> queue;
1251     queue.append(root);
1252     while (! queue.isEmpty()) {
1253         uint current = queue.takeFirst();
1254         QHash<uint,ThreadNodeInfo>::const_iterator it = threading.constFind(current);
1255         Q_ASSERT(it != threading.constEnd());
1256         if (it->ptr) {
1257             // Because of the delayed delete via pruneTree, we can hit a null pointer here
1258             TreeItemMessage *message = dynamic_cast<TreeItemMessage *>(it->ptr);
1259             Q_ASSERT(message);
1260             if (threadForeachCallback(callback, *message))
1261                 return;
1262         }
1263         queue.append(it->children);
1264     }
1265 }
1266 
1267 bool ThreadingMsgListModel::threadContainsUnreadMessages(const uint root) const
1268 {
1269     // FIXME: cache the value somewhere...
1270     bool containsUnreadMessages = false;
1271     threadForeach<bool>(root, [&containsUnreadMessages](const TreeItemMessage &message) -> bool {
1272         return containsUnreadMessages = ! message.isMarkedAsRead();
1273     });
1274     return containsUnreadMessages;
1275 }
1276 
1277 QStringList ThreadingMsgListModel::threadAggregatedFlags(const uint root) const
1278 {
1279     // FIXME: cache the value somewhere...
1280     QStringList aggregatedFlags;
1281     threadForeach<void>(root, [&aggregatedFlags](const TreeItemMessage &message) {
1282         aggregatedFlags += message.m_flags;
1283     });
1284     aggregatedFlags.removeDuplicates();
1285     return aggregatedFlags;
1286 }
1287 
1288 /** @short Pass a debugging message to the real Model, if possible
1289 
1290 If we don't know what the real model is, just dump it through the qDebug(); that's better than nothing.
1291 */
1292 void ThreadingMsgListModel::logTrace(const QString &message)
1293 {
1294     if (!sourceModel()) {
1295         qDebug() << message;
1296         return;
1297     }
1298     QModelIndex idx = sourceModel()->index(0, 0);
1299     if (!idx.isValid()) {
1300         qDebug() << message;
1301         return;
1302     }
1303 
1304     // Got to find out the real model and also translate the index to one belonging to a real Model
1305     Q_ASSERT(idx.model());
1306     const Model *realModel;
1307     QModelIndex realIndex;
1308     Model::realTreeItem(idx, &realModel, &realIndex);
1309     Q_ASSERT(realModel);
1310     QModelIndex mailboxIndex = const_cast<Model *>(realModel)->findMailboxForItems(QModelIndexList() << realIndex);
1311     const_cast<Model *>(realModel)->logTrace(mailboxIndex, Common::LOG_OTHER,
1312             QStringLiteral("ThreadingMsgListModel for %1").arg(mailboxIndex.data(RoleMailboxName).toString()), message);
1313 }
1314 
1315 void ThreadingMsgListModel::setUserWantsThreading(bool enable)
1316 {
1317     m_shallBeThreading = enable;
1318     if (m_shallBeThreading) {
1319         wantThreading();
1320     } else {
1321         updateNoThreading();
1322     }
1323 }
1324 
1325 bool ThreadingMsgListModel::setUserSearchingSortingPreference(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order)
1326 {
1327     auto changedSearch = (searchConditions != m_currentSearchConditions);
1328     if (!m_shallBeThreading) {
1329         updateNoThreading();
1330     }
1331     auto succeed = searchSortPreferenceImplementation(searchConditions, criterium, order);
1332     if (m_shallBeThreading && changedSearch) {
1333         // Asking for threading regardless of `threadingInFlight` because
1334         // THREAD-ing had to be redone in the context of this search
1335         logTrace(QStringLiteral("Current threading invalidated by changed search"));
1336         askForThreading(0);
1337     }
1338     return succeed;
1339 }
1340 
1341 /** @short The workhorse behind setUserSearchingSortingPreference() */
1342 bool ThreadingMsgListModel::searchSortPreferenceImplementation(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order)
1343 {
1344     Q_ASSERT(sourceModel());
1345     m_sortReverse = order == Qt::DescendingOrder;
1346     if (!sourceModel()->rowCount()) {
1347         return false;
1348     }
1349 
1350     const Model *realModel;
1351     QModelIndex someMessage = sourceModel()->index(0,0);
1352     QModelIndex realIndex;
1353     Model::realTreeItem(someMessage, &realModel, &realIndex);
1354     QModelIndex mailboxIndex = realIndex.parent().parent();
1355 
1356     bool hasDisplaySort = false;
1357     bool hasSort = false;
1358     if (realModel->capabilities().contains(QStringLiteral("SORT=DISPLAY"))) {
1359         hasDisplaySort = true;
1360         hasSort = true;
1361     } else if (realModel->capabilities().contains(QStringLiteral("SORT"))) {
1362         // just the regular sort
1363         hasSort = true;
1364     }
1365 
1366     QStringList sortOptions;
1367     switch (criterium) {
1368     case SORT_ARRIVAL:
1369         sortOptions << QStringLiteral("ARRIVAL");
1370         break;
1371     case SORT_CC:
1372         sortOptions << QStringLiteral("CC");
1373         break;
1374     case SORT_DATE:
1375         sortOptions << QStringLiteral("DATE");
1376         break;
1377     case SORT_FROM:
1378         sortOptions << (hasDisplaySort ? QStringLiteral("DISPLAYFROM") : QStringLiteral("FROM"));
1379         break;
1380     case SORT_SIZE:
1381         sortOptions << QStringLiteral("SIZE");
1382         break;
1383     case SORT_SUBJECT:
1384         sortOptions << QStringLiteral("SUBJECT");
1385         break;
1386     case SORT_TO:
1387         sortOptions << (hasDisplaySort ? QStringLiteral("DISPLAYTO") : QStringLiteral("TO"));
1388         break;
1389     case SORT_NONE:
1390         if (m_sortTask && m_sortTask->isPersistent() &&
1391                 (m_currentSearchConditions != searchConditions || m_currentSortingCriteria != criterium)) {
1392             // Any change shall result in us killing that sort task
1393             m_sortTask->cancelSortingUpdates();
1394         }
1395 
1396         m_currentSortingCriteria = criterium;
1397 
1398         if (searchConditions.isEmpty()) {
1399             // This operation is special, it will immediately restore the original shape of the mailbox
1400             m_currentSearchConditions = searchConditions;
1401             m_filteredBySearch = false;
1402             calculateNullSort();
1403             applySort();
1404             return true;
1405         } else if (searchConditions != m_currentSearchConditions || m_searchValidity != RESULT_FRESH) {
1406             // We have to update our search conditions
1407             m_sortTask = realModel->m_taskFactory->createSortTask(const_cast<Model *>(realModel), mailboxIndex, searchConditions,
1408                                                                   QStringList());
1409             connect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable);
1410             connect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed);
1411             connect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate);
1412             m_currentSearchConditions = searchConditions;
1413             m_filteredBySearch = true;
1414             m_searchValidity = RESULT_ASKED;
1415         } else {
1416             // A result of SEARCH has just arrived
1417             Q_ASSERT(m_searchValidity == RESULT_FRESH);
1418             applySort();
1419         }
1420 
1421         return true;
1422     }
1423 
1424     if (!hasSort) {
1425         // sorting is completely unsupported
1426         return false;
1427     }
1428 
1429     Q_ASSERT(!sortOptions.isEmpty());
1430 
1431     if (m_currentSortingCriteria == criterium && m_currentSearchConditions == searchConditions &&
1432             m_searchValidity != RESULT_INVALIDATED) {
1433         applySort();
1434     } else {
1435         m_currentSearchConditions = searchConditions;
1436         m_filteredBySearch = ! searchConditions.isEmpty();
1437         m_currentSortingCriteria = criterium;
1438         calculateNullSort();
1439         applySort();
1440 
1441         if (m_sortTask && m_sortTask->isPersistent())
1442             m_sortTask->cancelSortingUpdates();
1443 
1444         m_sortTask = realModel->m_taskFactory->createSortTask(const_cast<Model *>(realModel), mailboxIndex, searchConditions, sortOptions);
1445         connect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable);
1446         connect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed);
1447         connect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate);
1448         m_searchValidity = RESULT_ASKED;
1449     }
1450 
1451     return true;
1452 }
1453 
1454 void ThreadingMsgListModel::applySort()
1455 {
1456     if (!sourceModel()->rowCount()) {
1457         // empty mailbox is a corner case and it's already sorted anyway
1458         return;
1459     }
1460 
1461     const Imap::Mailbox::Model *realModel;
1462     QModelIndex someMessage = sourceModel()->index(0,0);
1463     QModelIndex realIndex;
1464     Model::realTreeItem(someMessage, &realModel, &realIndex);
1465     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox*>(static_cast<TreeItem*>(realIndex.parent().parent().internalPointer()));
1466     Q_ASSERT(mailbox);
1467 
1468     emit layoutAboutToBeChanged();
1469     updatePersistentIndexesPhase1();
1470     QSet<uint> newlyUnreachable(threading[0].children.begin(), threading[0].children.end());
1471     threading[0].children.clear();
1472     threading[0].children.reserve(m_currentSortResult.size() + headroomForNewmessages);
1473 
1474     QSet<uint> allRootIds(threadedRootIds.begin(), threadedRootIds.end());
1475 
1476     for (int i = 0; i < m_currentSortResult.size(); ++i) {
1477         int offset = m_sortReverse ? m_currentSortResult.size() - 1 - i : i;
1478         QList<TreeItemMessage *> messages = const_cast<Model*>(realModel)
1479                 ->findMessagesByUids(mailbox, Imap::Uids() << m_currentSortResult[offset]);
1480         if (messages.isEmpty()) {
1481             // wrong UID, weird
1482             continue;
1483         }
1484         Q_ASSERT(messages.size() == 1);
1485         QHash<void *,uint>::const_iterator it = ptrToInternal.constFind(messages.front());
1486         // else applyThreading() taking care of it
1487         if (!threadingInFlight)
1488             Q_ASSERT(it != ptrToInternal.constEnd());
1489         if (!allRootIds.contains(*it)) {
1490             // not a thread root, so don't show it
1491             continue;
1492         }
1493         threading[*it].offset = threading[0].children.size();
1494         threading[0].children.append(*it);
1495     }
1496 
1497     // Now remove everything which is no longer reachable from the root of the thread mapping
1498     // Start working on the top-level orphans
1499     Q_FOREACH(const uint uid, threading[0].children) {
1500         newlyUnreachable.remove(uid);
1501     }
1502     std::vector<uint> queue(newlyUnreachable.constBegin(), newlyUnreachable.constEnd());
1503     for (std::vector<uint>::size_type i = 0; i < queue.size(); ++i) {
1504         QHash<uint,ThreadNodeInfo>::iterator threadingIt = threading.find(queue[i]);
1505         Q_ASSERT(threadingIt != threading.end());
1506         queue.insert(queue.end(), threadingIt->children.constBegin(), threadingIt->children.constEnd());
1507         threading.erase(threadingIt);
1508     }
1509 
1510     updatePersistentIndexesPhase2();
1511     emit layoutChanged();
1512 }
1513 
1514 QStringList ThreadingMsgListModel::currentSearchCondition() const
1515 {
1516     return m_currentSearchConditions;
1517 }
1518 
1519 ThreadingMsgListModel::SortCriterium ThreadingMsgListModel::currentSortCriterium() const
1520 {
1521     return m_currentSortingCriteria;
1522 }
1523 
1524 Qt::SortOrder ThreadingMsgListModel::currentSortOrder() const
1525 {
1526     return m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder;
1527 }
1528 
1529 QModelIndex ThreadingMsgListModel::sibling(int row, int column, const QModelIndex &idx) const
1530 {
1531     return index(row, column, idx.parent());
1532 }
1533 
1534 }
1535 }