1 /******************************************************************************
2  *
3  *  SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *
7  *******************************************************************************/
8 
9 #include "core/item.h"
10 #include "core/item_p.h"
11 #include "core/manager.h"
12 #include "core/model.h"
13 
14 #include <KIO/Global> // for KIO::filesize_t and related functions
15 #include <KMime/DateFormatter>
16 
17 #include <KLocalizedString>
18 
19 using namespace MessageList::Core;
20 
Item(Type type)21 Item::Item(Type type)
22     : d_ptr(new ItemPrivate(this))
23 {
24     d_ptr->mType = type;
25 }
26 
Item(Item::Type type,ItemPrivate * dd)27 Item::Item(Item::Type type, ItemPrivate *dd)
28     : d_ptr(dd)
29 {
30     d_ptr->mType = type;
31 }
32 
~Item()33 Item::~Item()
34 {
35     killAllChildItems();
36 
37     if (d_ptr->mParent) {
38         d_ptr->mParent->d_ptr->childItemDead(this);
39     }
40 
41     delete d_ptr;
42 }
43 
childItemStats(ChildItemStats & stats) const44 void Item::childItemStats(ChildItemStats &stats) const
45 {
46     Q_ASSERT(d_ptr->mChildItems);
47 
48     stats.mTotalChildCount += d_ptr->mChildItems->count();
49     for (const auto child : std::as_const(*d_ptr->mChildItems)) {
50         if (!child->status().isRead()) {
51             stats.mUnreadChildCount++;
52         }
53         if (child->d_ptr->mChildItems) {
54             child->childItemStats(stats);
55         }
56     }
57 }
58 
childItems() const59 QList<Item *> *Item::childItems() const
60 {
61     return d_ptr->mChildItems;
62 }
63 
childItem(int idx) const64 Item *Item::childItem(int idx) const
65 {
66     if (idx < 0) {
67         return nullptr;
68     }
69     if (!d_ptr->mChildItems) {
70         return nullptr;
71     }
72     if (d_ptr->mChildItems->count() <= idx) {
73         return nullptr;
74     }
75     return d_ptr->mChildItems->at(idx);
76 }
77 
firstChildItem() const78 Item *Item::firstChildItem() const
79 {
80     return d_ptr->mChildItems ? (d_ptr->mChildItems->count() > 0 ? d_ptr->mChildItems->at(0) : nullptr) : nullptr;
81 }
82 
itemBelowChild(Item * child)83 Item *Item::itemBelowChild(Item *child)
84 {
85     Q_ASSERT(d_ptr->mChildItems);
86 
87     int idx = indexOfChildItem(child);
88     Q_ASSERT(idx >= 0);
89 
90     idx++;
91 
92     if (idx < d_ptr->mChildItems->count()) {
93         return d_ptr->mChildItems->at(idx);
94     }
95 
96     if (!d_ptr->mParent) {
97         return nullptr;
98     }
99 
100     return d_ptr->mParent->itemBelowChild(this);
101 }
102 
itemBelow()103 Item *Item::itemBelow()
104 {
105     if (d_ptr->mChildItems) {
106         if (!d_ptr->mChildItems->isEmpty()) {
107             return d_ptr->mChildItems->at(0);
108         }
109     }
110 
111     if (!d_ptr->mParent) {
112         return nullptr;
113     }
114 
115     return d_ptr->mParent->itemBelowChild(this);
116 }
117 
deepestItem()118 Item *Item::deepestItem()
119 {
120     if (d_ptr->mChildItems) {
121         if (!d_ptr->mChildItems->isEmpty()) {
122             return d_ptr->mChildItems->at(d_ptr->mChildItems->count() - 1)->deepestItem();
123         }
124     }
125 
126     return this;
127 }
128 
itemAboveChild(Item * child)129 Item *Item::itemAboveChild(Item *child)
130 {
131     if (d_ptr->mChildItems) {
132         int idx = indexOfChildItem(child);
133         Q_ASSERT(idx >= 0);
134         idx--;
135 
136         if (idx >= 0) {
137             return d_ptr->mChildItems->at(idx);
138         }
139     }
140 
141     return this;
142 }
143 
itemAbove()144 Item *Item::itemAbove()
145 {
146     if (!d_ptr->mParent) {
147         return nullptr;
148     }
149 
150     Item *siblingAbove = d_ptr->mParent->itemAboveChild(this);
151     if (siblingAbove && siblingAbove != this && siblingAbove != d_ptr->mParent && siblingAbove->childItemCount() > 0) {
152         return siblingAbove->deepestItem();
153     }
154 
155     return d_ptr->mParent->itemAboveChild(this);
156 }
157 
childItemCount() const158 int Item::childItemCount() const
159 {
160     return d_ptr->mChildItems ? d_ptr->mChildItems->count() : 0;
161 }
162 
hasChildren() const163 bool Item::hasChildren() const
164 {
165     return childItemCount() > 0;
166 }
167 
indexOfChildItem(Item * child) const168 int Item::indexOfChildItem(Item *child) const
169 {
170     if (!d_ptr->mChildItems) {
171         return -1;
172     }
173     int idx = child->d_ptr->mThisItemIndexGuess;
174     if (idx < d_ptr->mChildItems->count() && d_ptr->mChildItems->at(idx) == child) {
175         return idx; // good guess
176     }
177 
178     // We had a guess but it's out-of-date. Let's use the old guess as our
179     // starting point and search in both directions from it. It's more likely we
180     // will find the new position by going from the old guess rather than scanning
181     // the list from the beginning. The worst case scenario is equal to not having
182     // any guess at all.
183     if (idx > 0 && idx < d_ptr->mChildItems->count()) {
184         const auto begin = d_ptr->mChildItems->cbegin();
185         const auto end = d_ptr->mChildItems->cend();
186         auto fwdIt = begin + idx;
187         auto bwdIt = fwdIt;
188 
189         idx = -1; // invalidate idx so it's -1 in case we fail to find the item
190         while (fwdIt != end || bwdIt != end) {
191             if (fwdIt != end) {
192                 if (++fwdIt != end && (*fwdIt) == child) {
193                     idx = std::distance(begin, fwdIt);
194                     break;
195                 }
196             }
197             if (bwdIt != end) { // sic!
198                 Q_ASSERT(bwdIt != begin);
199                 if ((*--bwdIt) == child) {
200                     idx = std::distance(begin, bwdIt);
201                     break;
202                 }
203                 if (bwdIt == begin) {
204                     // invalidate the iterator if we just checked the first item
205                     bwdIt = end;
206                 }
207             }
208         }
209     } else {
210         idx = d_ptr->mChildItems->indexOf(child);
211     }
212 
213     if (idx >= 0) {
214         Q_ASSERT(d_ptr->mChildItems->at(idx) == child); // make sure the above algorithm works
215         child->d_ptr->mThisItemIndexGuess = idx;
216     }
217     return idx;
218 }
219 
setIndexGuess(int index)220 void Item::setIndexGuess(int index)
221 {
222     d_ptr->mThisItemIndexGuess = index;
223 }
224 
topmostNonRoot()225 Item *Item::topmostNonRoot()
226 {
227     Q_ASSERT(d_ptr->mType != InvisibleRoot);
228 
229     if (!d_ptr->mParent) {
230         return this;
231     }
232 
233     if (d_ptr->mParent->type() == InvisibleRoot) {
234         return this;
235     }
236 
237     return d_ptr->mParent->topmostNonRoot();
238 }
239 
append_string(QString & buffer,const QString & append)240 static inline void append_string(QString &buffer, const QString &append)
241 {
242     if (!buffer.isEmpty()) {
243         buffer += QLatin1String(", ");
244     }
245     buffer += append;
246 }
247 
statusDescription() const248 QString Item::statusDescription() const
249 {
250     QString ret;
251     if (status().isRead()) {
252         append_string(ret, i18nc("Status of an item", "Read"));
253     } else {
254         append_string(ret, i18nc("Status of an item", "Unread"));
255     }
256 
257     if (status().hasAttachment()) {
258         append_string(ret, i18nc("Status of an item", "Has Attachment"));
259     }
260 
261     if (status().isToAct()) {
262         append_string(ret, i18nc("Status of an item", "Action Item"));
263     }
264 
265     if (status().isReplied()) {
266         append_string(ret, i18nc("Status of an item", "Replied"));
267     }
268 
269     if (status().isForwarded()) {
270         append_string(ret, i18nc("Status of an item", "Forwarded"));
271     }
272 
273     if (status().isSent()) {
274         append_string(ret, i18nc("Status of an item", "Sent"));
275     }
276 
277     if (status().isImportant()) {
278         append_string(ret, i18nc("Status of an item", "Important"));
279     }
280 
281     if (status().isSpam()) {
282         append_string(ret, i18nc("Status of an item", "Spam"));
283     }
284 
285     if (status().isHam()) {
286         append_string(ret, i18nc("Status of an item", "Ham"));
287     }
288 
289     if (status().isWatched()) {
290         append_string(ret, i18nc("Status of an item", "Watched"));
291     }
292 
293     if (status().isIgnored()) {
294         append_string(ret, i18nc("Status of an item", "Ignored"));
295     }
296 
297     return ret;
298 }
299 
formattedSize() const300 QString Item::formattedSize() const
301 {
302     return KIO::convertSize((KIO::filesize_t)size());
303 }
304 
formattedDate() const305 QString Item::formattedDate() const
306 {
307     if (static_cast<uint>(date()) == static_cast<uint>(-1)) {
308         return Manager::instance()->cachedLocalizedUnknownText();
309     } else {
310         return Manager::instance()->dateFormatter()->dateString(date());
311     }
312 }
313 
formattedMaxDate() const314 QString Item::formattedMaxDate() const
315 {
316     if (static_cast<uint>(maxDate()) == static_cast<uint>(-1)) {
317         return Manager::instance()->cachedLocalizedUnknownText();
318     } else {
319         return Manager::instance()->dateFormatter()->dateString(maxDate());
320     }
321 }
322 
recomputeMaxDate()323 bool Item::recomputeMaxDate()
324 {
325     time_t newMaxDate = d_ptr->mDate;
326 
327     if (d_ptr->mChildItems) {
328         for (auto child : std::as_const(*d_ptr->mChildItems)) {
329             if (child->d_ptr->mMaxDate > newMaxDate) {
330                 newMaxDate = child->d_ptr->mMaxDate;
331             }
332         }
333     }
334 
335     if (newMaxDate != d_ptr->mMaxDate) {
336         setMaxDate(newMaxDate);
337         return true;
338     }
339     return false;
340 }
341 
type() const342 Item::Type Item::type() const
343 {
344     return d_ptr->mType;
345 }
346 
initialExpandStatus() const347 Item::InitialExpandStatus Item::initialExpandStatus() const
348 {
349     return d_ptr->mInitialExpandStatus;
350 }
351 
setInitialExpandStatus(InitialExpandStatus initialExpandStatus)352 void Item::setInitialExpandStatus(InitialExpandStatus initialExpandStatus)
353 {
354     d_ptr->mInitialExpandStatus = initialExpandStatus;
355 }
356 
isViewable() const357 bool Item::isViewable() const
358 {
359     return d_ptr->mIsViewable;
360 }
361 
hasAncestor(const Item * it) const362 bool Item::hasAncestor(const Item *it) const
363 {
364     return d_ptr->mParent ? (d_ptr->mParent == it ? true : d_ptr->mParent->hasAncestor(it)) : false;
365 }
366 
setViewable(Model * model,bool bViewable)367 void Item::setViewable(Model *model, bool bViewable)
368 {
369     if (d_ptr->mIsViewable == bViewable) {
370         return;
371     }
372 
373     if (!d_ptr->mChildItems) {
374         d_ptr->mIsViewable = bViewable;
375         return;
376     }
377 
378     if (d_ptr->mChildItems->isEmpty()) {
379         d_ptr->mIsViewable = bViewable;
380         return;
381     }
382 
383     if (bViewable) {
384         if (model) {
385             // fake having no children, for a second
386             QList<Item *> *tmp = d_ptr->mChildItems;
387             d_ptr->mChildItems = nullptr;
388             // qDebug("BEGIN INSERT ROWS FOR PARENT %x: from %d to %d, (will) have %d children",this,0,tmp->count()-1,tmp->count());
389             model->beginInsertRows(model->index(this, 0), 0, tmp->count() - 1);
390             d_ptr->mChildItems = tmp;
391             d_ptr->mIsViewable = true;
392             model->endInsertRows();
393         } else {
394             d_ptr->mIsViewable = true;
395         }
396 
397         for (const auto child : std::as_const(*d_ptr->mChildItems)) {
398             child->setViewable(model, bViewable);
399         }
400     } else {
401         for (const auto child : std::as_const(*d_ptr->mChildItems)) {
402             child->setViewable(model, bViewable);
403         }
404 
405         // It seems that we can avoid removing child items here since the parent has been removed: this is a hack tough
406         // and should check if Qt4 still supports it in the next (hopefully largely fixed) release
407 
408         if (model) {
409             // fake having no children, for a second
410             model->beginRemoveRows(model->index(this, 0), 0, d_ptr->mChildItems->count() - 1);
411             QList<Item *> *tmp = d_ptr->mChildItems;
412             d_ptr->mChildItems = nullptr;
413             d_ptr->mIsViewable = false;
414             model->endRemoveRows();
415             d_ptr->mChildItems = tmp;
416         } else {
417             d_ptr->mIsViewable = false;
418         }
419     }
420 }
421 
killAllChildItems()422 void Item::killAllChildItems()
423 {
424     if (!d_ptr->mChildItems) {
425         return;
426     }
427 
428     while (!d_ptr->mChildItems->isEmpty()) {
429         delete d_ptr->mChildItems->first(); // this will call childDead() which will remove the child from the list
430     }
431 
432     delete d_ptr->mChildItems;
433     d_ptr->mChildItems = nullptr;
434 }
435 
parent() const436 Item *Item::parent() const
437 {
438     return d_ptr->mParent;
439 }
440 
setParent(Item * pParent)441 void Item::setParent(Item *pParent)
442 {
443     d_ptr->mParent = pParent;
444 }
445 
status() const446 const Akonadi::MessageStatus &Item::status() const
447 {
448     return d_ptr->mStatus;
449 }
450 
setStatus(Akonadi::MessageStatus status)451 void Item::setStatus(Akonadi::MessageStatus status)
452 {
453     d_ptr->mStatus = status;
454 }
455 
size() const456 size_t Item::size() const
457 {
458     return d_ptr->mSize;
459 }
460 
setSize(size_t size)461 void Item::setSize(size_t size)
462 {
463     d_ptr->mSize = size;
464 }
465 
date() const466 time_t Item::date() const
467 {
468     return d_ptr->mDate;
469 }
470 
setDate(time_t date)471 void Item::setDate(time_t date)
472 {
473     d_ptr->mDate = date;
474 }
475 
maxDate() const476 time_t Item::maxDate() const
477 {
478     return d_ptr->mMaxDate;
479 }
480 
setMaxDate(time_t date)481 void Item::setMaxDate(time_t date)
482 {
483     d_ptr->mMaxDate = date;
484 }
485 
sender() const486 const QString &Item::sender() const
487 {
488     return d_ptr->mSender;
489 }
490 
setSender(const QString & sender)491 void Item::setSender(const QString &sender)
492 {
493     d_ptr->mSender = sender;
494 }
495 
displaySender() const496 QString Item::displaySender() const
497 {
498     return sender();
499 }
500 
receiver() const501 const QString &Item::receiver() const
502 {
503     return d_ptr->mReceiver;
504 }
505 
setReceiver(const QString & receiver)506 void Item::setReceiver(const QString &receiver)
507 {
508     d_ptr->mReceiver = receiver;
509 }
510 
displayReceiver() const511 QString Item::displayReceiver() const
512 {
513     return receiver();
514 }
515 
senderOrReceiver() const516 const QString &Item::senderOrReceiver() const
517 {
518     return d_ptr->mUseReceiver ? d_ptr->mReceiver : d_ptr->mSender;
519 }
520 
displaySenderOrReceiver() const521 QString Item::displaySenderOrReceiver() const
522 {
523     return senderOrReceiver();
524 }
525 
useReceiver() const526 bool Item::useReceiver() const
527 {
528     return d_ptr->mUseReceiver;
529 }
530 
subject() const531 const QString &Item::subject() const
532 {
533     return d_ptr->mSubject;
534 }
535 
setSubject(const QString & subject)536 void Item::setSubject(const QString &subject)
537 {
538     d_ptr->mSubject = subject;
539 }
540 
folder() const541 const QString &Item::folder() const
542 {
543     return d_ptr->mFolder;
544 }
545 
setFolder(const QString & folder)546 void Item::setFolder(const QString &folder)
547 {
548     d_ptr->mFolder = folder;
549 }
550 
initialSetup(time_t date,size_t size,const QString & sender,const QString & receiver,bool useReceiver)551 void MessageList::Core::Item::initialSetup(time_t date, size_t size, const QString &sender, const QString &receiver, bool useReceiver)
552 {
553     d_ptr->mDate = date;
554     d_ptr->mMaxDate = date;
555     d_ptr->mSize = size;
556     d_ptr->mSender = sender;
557     d_ptr->mReceiver = receiver;
558     d_ptr->mUseReceiver = useReceiver;
559 }
560 
setItemId(qint64 id)561 void MessageList::Core::Item::setItemId(qint64 id)
562 {
563     d_ptr->mItemId = id;
564 }
565 
itemId() const566 qint64 MessageList::Core::Item::itemId() const
567 {
568     return d_ptr->mItemId;
569 }
570 
setParentCollectionId(qint64 id)571 void Item::setParentCollectionId(qint64 id)
572 {
573     d_ptr->mParentCollectionId = id;
574 }
575 
parentCollectionId() const576 qint64 Item::parentCollectionId() const
577 {
578     return d_ptr->mParentCollectionId;
579 }
580 
setSubjectAndStatus(const QString & subject,Akonadi::MessageStatus status)581 void MessageList::Core::Item::setSubjectAndStatus(const QString &subject, Akonadi::MessageStatus status)
582 {
583     d_ptr->mSubject = subject;
584     d_ptr->mStatus = status;
585 }
586 
587 // FIXME: Try to "cache item insertions" and call beginInsertRows() and endInsertRows() in a chunked fashion...
588 
rawAppendChildItem(Item * child)589 void Item::rawAppendChildItem(Item *child)
590 {
591     if (!d_ptr->mChildItems) {
592         d_ptr->mChildItems = new QList<Item *>();
593     }
594     d_ptr->mChildItems->append(child);
595 }
596 
appendChildItem(Model * model,Item * child)597 int Item::appendChildItem(Model *model, Item *child)
598 {
599     if (!d_ptr->mChildItems) {
600         d_ptr->mChildItems = new QList<Item *>();
601     }
602     const int idx = d_ptr->mChildItems->count();
603     if (d_ptr->mIsViewable) {
604         if (model) {
605             model->beginInsertRows(model->index(this, 0), idx, idx); // THIS IS EXTREMELY UGLY, BUT IT'S THE ONLY POSSIBLE WAY WITH QT4 AT THE TIME OF WRITING
606         }
607         d_ptr->mChildItems->append(child);
608         child->setIndexGuess(idx);
609         if (model) {
610             model->endInsertRows(); // THIS IS EXTREMELY UGLY, BUT IT'S THE ONLY POSSIBLE WAY WITH QT4 AT THE TIME OF WRITING
611         }
612         child->setViewable(model, true);
613     } else {
614         d_ptr->mChildItems->append(child);
615         child->setIndexGuess(idx);
616     }
617     return idx;
618 }
619 
dump(const QString & prefix)620 void Item::dump(const QString &prefix)
621 {
622     QString out = QStringLiteral("%1 %x VIEWABLE:%2").arg(prefix, d_ptr->mIsViewable ? QStringLiteral("yes") : QStringLiteral("no"));
623     qDebug(out.toUtf8().data(), this);
624 
625     QString nPrefix(prefix);
626     nPrefix += QLatin1String("  ");
627 
628     if (!d_ptr->mChildItems) {
629         return;
630     }
631 
632     for (const auto child : std::as_const(*d_ptr->mChildItems)) {
633         child->dump(nPrefix);
634     }
635 }
636 
takeChildItem(Model * model,Item * child)637 void Item::takeChildItem(Model *model, Item *child)
638 {
639     if (!d_ptr->mChildItems) {
640         return; // Ugh... not our child ?
641     }
642 
643     if (!d_ptr->mIsViewable) {
644         // qDebug("TAKING NON VIEWABLE CHILD ITEM %x",child);
645         // We can highly optimize this case
646         d_ptr->mChildItems->removeOne(child);
647 #if 0
648         // This *could* be done, but we optimize and avoid it.
649         if (d->mChildItems->isEmpty()) {
650             delete d->mChildItems;
651             d->mChildItems = 0;
652         }
653 #endif
654         child->setParent(nullptr);
655         return;
656     }
657 
658     const int idx = indexOfChildItem(child);
659     if (idx < 0) {
660         return; // Aaargh... not our child ?
661     }
662 
663     child->setViewable(model, false);
664     if (model) {
665         model->beginRemoveRows(model->index(this, 0), idx, idx);
666     }
667     child->setParent(nullptr);
668     d_ptr->mChildItems->removeAt(idx);
669     if (model) {
670         model->endRemoveRows();
671     }
672 
673 #if 0
674     // This *could* be done, but we optimize and avoid it.
675     if (d->mChildItems->isEmpty()) {
676         delete d->mChildItems;
677         d->mChildItems = 0;
678     }
679 #endif
680 }
681 
childItemDead(Item * child)682 void ItemPrivate::childItemDead(Item *child)
683 {
684     // mChildItems MUST be non zero here, if it's not then it's a bug in THIS FILE
685     mChildItems->removeOne(child); // since we always have ONE (if we not, it's a bug)
686 }
687