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 }