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/theme.h"
10 
11 #include <QApplication>
12 #include <QDataStream>
13 #include <QIcon>
14 #include <QPixmap>
15 #include <QStandardPaths>
16 
17 #include "messagelist_debug.h"
18 #include <KLocalizedString>
19 
20 using namespace MessageList::Core;
21 
22 //
23 // Theme versioning
24 //
25 // The themes simply have a DWORD version number attached.
26 // The earliest version we're able to load is 0x1013.
27 //
28 // Theme revision history:
29 //
30 //  Version Date introduced Description
31 // --------------------------------------------------------------------------------------------------------------
32 //  0x1013  08.11.2008      Initial theme version, introduced when this piece of code has been moved into trunk.
33 //  0x1014  12.11.2008      Added runtime column data: width and column visibility
34 //  0x1015  03.03.2009      Added icon size
35 //  0x1016  08.03.2009      Added support for sorting by New/Unread status
36 //  0x1017  16.08.2009      Added support for column icon
37 //  0x1018  17.01.2010      Added support for annotation icon
38 //  0x1019  13.07.2010      Added support for invitation icon
39 //
40 static const int gThemeCurrentVersion = 0x1019; // increase if you add new fields or change the meaning of some
41 // you don't need to change the values below, but you might want to add new ones
42 static const int gThemeMinimumSupportedVersion = 0x1013;
43 static const int gThemeMinimumVersionWithColumnRuntimeData = 0x1014;
44 static const int gThemeMinimumVersionWithIconSizeField = 0x1015;
45 static const int gThemeMinimumVersionWithSortingByUnreadStatusAllowed = 0x1016;
46 static const int gThemeMinimumVersionWithColumnIcon = 0x1017;
47 static const int gThemeMinimumVersionWithAnnotationIcon = 0x1018;
48 static const int gThemeMinimumVersionWithInvitationIcon = 0x1019;
49 
50 // the default icon size
51 static const int gThemeDefaultIconSize = 16;
52 
ContentItem(Type type)53 Theme::ContentItem::ContentItem(Type type)
54     : mType(type)
55     , mFlags(0)
56 {
57 }
58 
ContentItem(const ContentItem & src)59 Theme::ContentItem::ContentItem(const ContentItem &src)
60     : mType(src.mType)
61     , mFlags(src.mFlags)
62     , mCustomColor(src.mCustomColor)
63 {
64 }
65 
type() const66 Theme::ContentItem::Type Theme::ContentItem::type() const
67 {
68     return mType;
69 }
70 
canBeDisabled() const71 bool Theme::ContentItem::canBeDisabled() const
72 {
73     return (static_cast<int>(mType) & CanBeDisabled) != 0;
74 }
75 
canUseCustomColor() const76 bool Theme::ContentItem::canUseCustomColor() const
77 {
78     return (static_cast<int>(mType) & CanUseCustomColor) != 0;
79 }
80 
displaysText() const81 bool Theme::ContentItem::displaysText() const
82 {
83     return (static_cast<int>(mType) & DisplaysText) != 0;
84 }
85 
displaysLongText() const86 bool Theme::ContentItem::displaysLongText() const
87 {
88     return (static_cast<int>(mType) & LongText) != 0;
89 }
90 
isIcon() const91 bool Theme::ContentItem::isIcon() const
92 {
93     return (static_cast<int>(mType) & IsIcon) != 0;
94 }
95 
isClickable() const96 bool Theme::ContentItem::isClickable() const
97 {
98     return (static_cast<int>(mType) & IsClickable) != 0;
99 }
100 
isSpacer() const101 bool Theme::ContentItem::isSpacer() const
102 {
103     return (static_cast<int>(mType) & IsSpacer) != 0;
104 }
105 
description(Type type)106 QString Theme::ContentItem::description(Type type)
107 {
108     switch (type) {
109     case Subject:
110         return i18nc("Description of Type Subject", "Subject");
111         break;
112     case Date:
113         return i18nc("Description of Type Date", "Date");
114         break;
115     case SenderOrReceiver:
116         return i18n("Sender/Receiver");
117         break;
118     case Sender:
119         return i18nc("Description of Type Sender", "Sender");
120         break;
121     case Receiver:
122         return i18nc("Description of Type Receiver", "Receiver");
123         break;
124     case Size:
125         return i18nc("Description of Type Size", "Size");
126         break;
127     case ReadStateIcon:
128         return i18n("Unread/Read Icon");
129         break;
130     case AttachmentStateIcon:
131         return i18n("Attachment Icon");
132         break;
133     case RepliedStateIcon:
134         return i18n("Replied/Forwarded Icon");
135         break;
136     case CombinedReadRepliedStateIcon:
137         return i18n("Combined New/Unread/Read/Replied/Forwarded Icon");
138         break;
139     case ActionItemStateIcon:
140         return i18n("Action Item Icon");
141         break;
142     case ImportantStateIcon:
143         return i18n("Important Icon");
144         break;
145     case GroupHeaderLabel:
146         return i18n("Group Header Label");
147         break;
148     case SpamHamStateIcon:
149         return i18n("Spam/Ham Icon");
150         break;
151     case WatchedIgnoredStateIcon:
152         return i18n("Watched/Ignored Icon");
153         break;
154     case ExpandedStateIcon:
155         return i18n("Group Header Expand/Collapse Icon");
156         break;
157     case EncryptionStateIcon:
158         return i18n("Encryption State Icon");
159         break;
160     case SignatureStateIcon:
161         return i18n("Signature State Icon");
162         break;
163     case VerticalLine:
164         return i18n("Vertical Separation Line");
165         break;
166     case HorizontalSpacer:
167         return i18n("Horizontal Spacer");
168         break;
169     case MostRecentDate:
170         return i18n("Max Date");
171         break;
172     case TagList:
173         return i18n("Message Tags");
174         break;
175     case AnnotationIcon:
176         return i18n("Note Icon");
177     case InvitationIcon:
178         return i18n("Invitation Icon");
179     case Folder:
180         return i18nc("Description of Type Folder", "Folder");
181     default:
182         return i18nc("Description for an Unknown Type", "Unknown");
183         break;
184     }
185 }
186 
useCustomColor() const187 bool Theme::ContentItem::useCustomColor() const
188 {
189     return mFlags & UseCustomColor;
190 }
191 
setUseCustomColor(bool useCustomColor)192 void Theme::ContentItem::setUseCustomColor(bool useCustomColor)
193 {
194     if (useCustomColor) {
195         mFlags |= UseCustomColor;
196     } else {
197         mFlags &= ~UseCustomColor;
198     }
199 }
200 
isBold() const201 bool Theme::ContentItem::isBold() const
202 {
203     return mFlags & IsBold;
204 }
205 
setBold(bool isBold)206 void Theme::ContentItem::setBold(bool isBold)
207 {
208     if (isBold) {
209         mFlags |= IsBold;
210     } else {
211         mFlags &= ~IsBold;
212     }
213 }
214 
isItalic() const215 bool Theme::ContentItem::isItalic() const
216 {
217     return mFlags & IsItalic;
218 }
219 
setItalic(bool isItalic)220 void Theme::ContentItem::setItalic(bool isItalic)
221 {
222     if (isItalic) {
223         mFlags |= IsItalic;
224     } else {
225         mFlags &= ~IsItalic;
226     }
227 }
228 
hideWhenDisabled() const229 bool Theme::ContentItem::hideWhenDisabled() const
230 {
231     return mFlags & HideWhenDisabled;
232 }
233 
setHideWhenDisabled(bool hideWhenDisabled)234 void Theme::ContentItem::setHideWhenDisabled(bool hideWhenDisabled)
235 {
236     if (hideWhenDisabled) {
237         mFlags |= HideWhenDisabled;
238     } else {
239         mFlags &= ~HideWhenDisabled;
240     }
241 }
242 
softenByBlendingWhenDisabled() const243 bool Theme::ContentItem::softenByBlendingWhenDisabled() const
244 {
245     return mFlags & SoftenByBlendingWhenDisabled;
246 }
247 
setSoftenByBlendingWhenDisabled(bool softenByBlendingWhenDisabled)248 void Theme::ContentItem::setSoftenByBlendingWhenDisabled(bool softenByBlendingWhenDisabled)
249 {
250     if (softenByBlendingWhenDisabled) {
251         mFlags |= SoftenByBlendingWhenDisabled;
252     } else {
253         mFlags &= ~SoftenByBlendingWhenDisabled;
254     }
255 }
256 
softenByBlending() const257 bool Theme::ContentItem::softenByBlending() const
258 {
259     return mFlags & SoftenByBlending;
260 }
261 
setSoftenByBlending(bool softenByBlending)262 void Theme::ContentItem::setSoftenByBlending(bool softenByBlending)
263 {
264     if (softenByBlending) {
265         mFlags |= SoftenByBlending;
266     } else {
267         mFlags &= ~SoftenByBlending;
268     }
269 }
270 
customColor() const271 const QColor &Theme::ContentItem::customColor() const
272 {
273     return mCustomColor;
274 }
275 
setCustomColor(const QColor & clr)276 void Theme::ContentItem::setCustomColor(const QColor &clr)
277 {
278     mCustomColor = clr;
279 }
280 
applicableToMessageItems(Type type)281 bool Theme::ContentItem::applicableToMessageItems(Type type)
282 {
283     return static_cast<int>(type) & ApplicableToMessageItems;
284 }
285 
applicableToGroupHeaderItems(Type type)286 bool Theme::ContentItem::applicableToGroupHeaderItems(Type type)
287 {
288     return static_cast<int>(type) & ApplicableToGroupHeaderItems;
289 }
290 
save(QDataStream & stream) const291 void Theme::ContentItem::save(QDataStream &stream) const
292 {
293     stream << (int)mType;
294     stream << mFlags;
295     stream << mCustomColor;
296 }
297 
load(QDataStream & stream,int)298 bool Theme::ContentItem::load(QDataStream &stream, int /*themeVersion*/)
299 {
300     int val;
301 
302     stream >> val;
303     mType = static_cast<Type>(val);
304     switch (mType) {
305     case Subject:
306     case Date:
307     case SenderOrReceiver:
308     case Sender:
309     case Receiver:
310     case Size:
311     case ReadStateIcon:
312     case AttachmentStateIcon:
313     case RepliedStateIcon:
314     case GroupHeaderLabel:
315     case ActionItemStateIcon:
316     case ImportantStateIcon:
317     case SpamHamStateIcon:
318     case WatchedIgnoredStateIcon:
319     case ExpandedStateIcon:
320     case EncryptionStateIcon:
321     case SignatureStateIcon:
322     case VerticalLine:
323     case HorizontalSpacer:
324     case MostRecentDate:
325     case CombinedReadRepliedStateIcon:
326     case TagList:
327     case AnnotationIcon:
328     case InvitationIcon:
329     case Folder:
330         // ok
331         break;
332     default:
333         qCDebug(MESSAGELIST_LOG) << "Invalid content item type";
334         return false; // b0rken
335         break;
336     }
337 
338     stream >> mFlags;
339     stream >> mCustomColor;
340     if (mFlags & UseCustomColor) {
341         if (!mCustomColor.isValid()) {
342             mFlags &= ~UseCustomColor;
343         }
344     }
345     return true;
346 }
347 
Row()348 Theme::Row::Row()
349 {
350 }
351 
Row(const Row & src)352 Theme::Row::Row(const Row &src)
353 {
354     for (const auto ci : std::as_const(src.mLeftItems)) {
355         addLeftItem(new ContentItem(*ci));
356     }
357 
358     for (const auto ci : std::as_const(src.mRightItems)) {
359         addRightItem(new ContentItem(*ci));
360     }
361 }
362 
~Row()363 Theme::Row::~Row()
364 {
365     removeAllLeftItems();
366     removeAllRightItems();
367 }
368 
removeAllLeftItems()369 void Theme::Row::removeAllLeftItems()
370 {
371     while (!mLeftItems.isEmpty()) {
372         delete mLeftItems.takeFirst();
373     }
374 }
375 
addLeftItem(Theme::ContentItem * item)376 void Theme::Row::addLeftItem(Theme::ContentItem *item)
377 {
378     mLeftItems.append(item);
379 }
380 
removeAllRightItems()381 void Theme::Row::removeAllRightItems()
382 {
383     while (!mRightItems.isEmpty()) {
384         delete mRightItems.takeFirst();
385     }
386 }
387 
addRightItem(Theme::ContentItem * item)388 void Theme::Row::addRightItem(Theme::ContentItem *item)
389 {
390     mRightItems.append(item);
391 }
392 
insertLeftItem(int idx,ContentItem * item)393 void Theme::Row::insertLeftItem(int idx, ContentItem *item)
394 {
395     if (idx >= mLeftItems.count()) {
396         mLeftItems.append(item);
397         return;
398     }
399     mLeftItems.insert(idx, item);
400 }
401 
removeLeftItem(Theme::ContentItem * item)402 void Theme::Row::removeLeftItem(Theme::ContentItem *item)
403 {
404     mLeftItems.removeAll(item);
405 }
406 
rightItems() const407 const QList<Theme::ContentItem *> &Theme::Row::rightItems() const
408 {
409     return mRightItems;
410 }
411 
insertRightItem(int idx,ContentItem * item)412 void Theme::Row::insertRightItem(int idx, ContentItem *item)
413 {
414     if (idx >= mRightItems.count()) {
415         mRightItems.append(item);
416         return;
417     }
418     mRightItems.insert(idx, item);
419 }
420 
removeRightItem(Theme::ContentItem * item)421 void Theme::Row::removeRightItem(Theme::ContentItem *item)
422 {
423     mRightItems.removeAll(item);
424 }
425 
containsTextItems() const426 bool Theme::Row::containsTextItems() const
427 {
428     for (const auto ci : std::as_const(mLeftItems)) {
429         if (ci->displaysText()) {
430             return true;
431         }
432     }
433     for (const auto ci : std::as_const(mRightItems)) {
434         if (ci->displaysText()) {
435             return true;
436         }
437     }
438     return false;
439 }
440 
save(QDataStream & stream) const441 void Theme::Row::save(QDataStream &stream) const
442 {
443     stream << (int)mLeftItems.count();
444 
445     int cnt = mLeftItems.count();
446 
447     for (int i = 0; i < cnt; ++i) {
448         ContentItem *ci = mLeftItems.at(i);
449         ci->save(stream);
450     }
451 
452     stream << (int)mRightItems.count();
453 
454     cnt = mRightItems.count();
455 
456     for (int i = 0; i < cnt; ++i) {
457         ContentItem *ci = mRightItems.at(i);
458         ci->save(stream);
459     }
460 }
461 
LoadContentItem(int val,QDataStream & stream,int themeVersion,bool leftItem)462 bool Theme::Row::LoadContentItem(int val, QDataStream &stream, int themeVersion, bool leftItem)
463 {
464     if ((val < 0) || (val > 50)) {
465         return false; // senseless
466     }
467 
468     // FIXME: Remove code duplication here
469 
470     for (int i = 0; i < val; ++i) {
471         auto ci = new ContentItem(ContentItem::Subject); // dummy type
472         if (!ci->load(stream, themeVersion)) {
473             qCDebug(MESSAGELIST_LOG) << "Left content item loading failed";
474             delete ci;
475             return false;
476         }
477         if (leftItem) {
478             addLeftItem(ci);
479         } else {
480             addRightItem(ci);
481         }
482 
483         // Add the annotation item next to the attachment icon, so that users upgrading from old
484         // versions don't manually need to set this.
485         // Don't do this for the stand-alone attachment column.
486         if (ci->type() == ContentItem::AttachmentStateIcon && themeVersion < gThemeMinimumVersionWithAnnotationIcon && val > 1) {
487             qCDebug(MESSAGELIST_LOG) << "Old theme version detected, adding annotation item next to attachment icon.";
488             auto annotationItem = new ContentItem(ContentItem::AnnotationIcon);
489             annotationItem->setHideWhenDisabled(true);
490             if (leftItem) {
491                 addLeftItem(annotationItem);
492             } else {
493                 addRightItem(annotationItem);
494             }
495         }
496 
497         // Same as above, for the invitation icon
498         if (ci->type() == ContentItem::AttachmentStateIcon && themeVersion < gThemeMinimumVersionWithInvitationIcon && val > 1) {
499             qCDebug(MESSAGELIST_LOG) << "Old theme version detected, adding invitation item next to attachment icon.";
500             auto invitationItem = new ContentItem(ContentItem::InvitationIcon);
501             invitationItem->setHideWhenDisabled(true);
502             if (leftItem) {
503                 addLeftItem(invitationItem);
504             } else {
505                 addRightItem(invitationItem);
506             }
507         }
508     }
509     return true;
510 }
511 
leftItems() const512 const QList<Theme::ContentItem *> &Theme::Row::leftItems() const
513 {
514     return mLeftItems;
515 }
516 
load(QDataStream & stream,int themeVersion)517 bool Theme::Row::load(QDataStream &stream, int themeVersion)
518 {
519     removeAllLeftItems();
520     removeAllRightItems();
521 
522     int val;
523 
524     // left item count
525 
526     stream >> val;
527     if (!LoadContentItem(val, stream, themeVersion, true)) {
528         return false;
529     }
530 
531     // right item count
532 
533     stream >> val;
534 
535     if (!LoadContentItem(val, stream, themeVersion, false)) {
536         return false;
537     }
538 
539     return true;
540 }
541 
SharedRuntimeData(bool currentlyVisible,double currentWidth)542 Theme::Column::SharedRuntimeData::SharedRuntimeData(bool currentlyVisible, double currentWidth)
543     : mReferences(0)
544     , mCurrentlyVisible(currentlyVisible)
545     , mCurrentWidth(currentWidth)
546 {
547 }
548 
~SharedRuntimeData()549 Theme::Column::SharedRuntimeData::~SharedRuntimeData()
550 {
551 }
552 
addReference()553 void Theme::Column::SharedRuntimeData::addReference()
554 {
555     mReferences++;
556 }
557 
deleteReference()558 bool Theme::Column::SharedRuntimeData::deleteReference()
559 {
560     mReferences--;
561     Q_ASSERT(mReferences >= 0);
562     return mReferences > 0;
563 }
564 
referenceCount() const565 int Theme::Column::SharedRuntimeData::referenceCount() const
566 {
567     return mReferences;
568 }
569 
currentlyVisible() const570 bool Theme::Column::SharedRuntimeData::currentlyVisible() const
571 {
572     return mCurrentlyVisible;
573 }
574 
setCurrentlyVisible(bool visible)575 void Theme::Column::SharedRuntimeData::setCurrentlyVisible(bool visible)
576 {
577     mCurrentlyVisible = visible;
578 }
579 
currentWidth() const580 double Theme::Column::SharedRuntimeData::currentWidth() const
581 {
582     return mCurrentWidth;
583 }
584 
setCurrentWidth(double currentWidth)585 void Theme::Column::SharedRuntimeData::setCurrentWidth(double currentWidth)
586 {
587     mCurrentWidth = currentWidth;
588 }
589 
save(QDataStream & stream) const590 void Theme::Column::SharedRuntimeData::save(QDataStream &stream) const
591 {
592     stream << mCurrentlyVisible;
593     stream << mCurrentWidth;
594 }
595 
load(QDataStream & stream,int)596 bool Theme::Column::SharedRuntimeData::load(QDataStream &stream, int /* themeVersion */)
597 {
598     stream >> mCurrentlyVisible;
599     stream >> mCurrentWidth;
600     if (mCurrentWidth > 10000) {
601         qCDebug(MESSAGELIST_LOG) << "Theme has insane column width " << mCurrentWidth << " chopping to 100";
602         mCurrentWidth = 100; // avoid really insane values
603     }
604     return mCurrentWidth >= -1;
605 }
606 
Column()607 Theme::Column::Column()
608     : mVisibleByDefault(true)
609     , mIsSenderOrReceiver(false)
610     , mMessageSorting(SortOrder::NoMessageSorting)
611 {
612     mSharedRuntimeData = new SharedRuntimeData(true, -1);
613     mSharedRuntimeData->addReference();
614 }
615 
Column(const Column & src)616 Theme::Column::Column(const Column &src)
617 {
618     mLabel = src.mLabel;
619     mPixmapName = src.mPixmapName;
620     mVisibleByDefault = src.mVisibleByDefault;
621     mIsSenderOrReceiver = src.mIsSenderOrReceiver;
622     mMessageSorting = src.mMessageSorting;
623 
624     mSharedRuntimeData = src.mSharedRuntimeData;
625     mSharedRuntimeData->addReference();
626     for (const auto row : std::as_const(src.mMessageRows)) {
627         addMessageRow(new Row(*row));
628     }
629 
630     for (const auto row : std::as_const(src.mGroupHeaderRows)) {
631         addGroupHeaderRow(new Row(*row));
632     }
633 }
634 
~Column()635 Theme::Column::~Column()
636 {
637     removeAllMessageRows();
638     removeAllGroupHeaderRows();
639     if (!(mSharedRuntimeData->deleteReference())) {
640         delete mSharedRuntimeData;
641     }
642 }
643 
label() const644 const QString &Theme::Column::label() const
645 {
646     return mLabel;
647 }
648 
setLabel(const QString & label)649 void Theme::Column::setLabel(const QString &label)
650 {
651     mLabel = label;
652 }
653 
pixmapName() const654 const QString &Theme::Column::pixmapName() const
655 {
656     return mPixmapName;
657 }
658 
setPixmapName(const QString & pixmapName)659 void Theme::Column::setPixmapName(const QString &pixmapName)
660 {
661     mPixmapName = pixmapName;
662 }
663 
isSenderOrReceiver() const664 bool Theme::Column::isSenderOrReceiver() const
665 {
666     return mIsSenderOrReceiver;
667 }
668 
setIsSenderOrReceiver(bool sor)669 void Theme::Column::setIsSenderOrReceiver(bool sor)
670 {
671     mIsSenderOrReceiver = sor;
672 }
673 
visibleByDefault() const674 bool Theme::Column::visibleByDefault() const
675 {
676     return mVisibleByDefault;
677 }
678 
setVisibleByDefault(bool vbd)679 void Theme::Column::setVisibleByDefault(bool vbd)
680 {
681     mVisibleByDefault = vbd;
682 }
683 
detach()684 void Theme::Column::detach()
685 {
686     if (mSharedRuntimeData->referenceCount() < 2) {
687         return; // nothing to detach
688     }
689     mSharedRuntimeData->deleteReference();
690 
691     mSharedRuntimeData = new SharedRuntimeData(mVisibleByDefault, -1);
692     mSharedRuntimeData->addReference();
693 }
694 
messageSorting() const695 SortOrder::MessageSorting Theme::Column::messageSorting() const
696 {
697     return mMessageSorting;
698 }
699 
setMessageSorting(SortOrder::MessageSorting ms)700 void Theme::Column::setMessageSorting(SortOrder::MessageSorting ms)
701 {
702     mMessageSorting = ms;
703 }
704 
currentlyVisible() const705 bool Theme::Column::currentlyVisible() const
706 {
707     return mSharedRuntimeData->currentlyVisible();
708 }
709 
setCurrentlyVisible(bool currentlyVisible)710 void Theme::Column::setCurrentlyVisible(bool currentlyVisible)
711 {
712     mSharedRuntimeData->setCurrentlyVisible(currentlyVisible);
713 }
714 
currentWidth() const715 double Theme::Column::currentWidth() const
716 {
717     return mSharedRuntimeData->currentWidth();
718 }
719 
setCurrentWidth(double currentWidth)720 void Theme::Column::setCurrentWidth(double currentWidth)
721 {
722     mSharedRuntimeData->setCurrentWidth(currentWidth);
723 }
724 
messageRows() const725 const QList<Theme::Row *> &Theme::Column::messageRows() const
726 {
727     return mMessageRows;
728 }
729 
removeAllMessageRows()730 void Theme::Column::removeAllMessageRows()
731 {
732     while (!mMessageRows.isEmpty()) {
733         delete mMessageRows.takeFirst();
734     }
735 }
736 
addMessageRow(Theme::Row * row)737 void Theme::Column::addMessageRow(Theme::Row *row)
738 {
739     mMessageRows.append(row);
740 }
741 
removeAllGroupHeaderRows()742 void Theme::Column::removeAllGroupHeaderRows()
743 {
744     while (!mGroupHeaderRows.isEmpty()) {
745         delete mGroupHeaderRows.takeFirst();
746     }
747 }
748 
addGroupHeaderRow(Theme::Row * row)749 void Theme::Column::addGroupHeaderRow(Theme::Row *row)
750 {
751     mGroupHeaderRows.append(row);
752 }
753 
insertMessageRow(int idx,Row * row)754 void Theme::Column::insertMessageRow(int idx, Row *row)
755 {
756     if (idx >= mMessageRows.count()) {
757         mMessageRows.append(row);
758         return;
759     }
760     mMessageRows.insert(idx, row);
761 }
762 
removeMessageRow(Theme::Row * row)763 void Theme::Column::removeMessageRow(Theme::Row *row)
764 {
765     mMessageRows.removeAll(row);
766 }
767 
groupHeaderRows() const768 const QList<Theme::Row *> &Theme::Column::groupHeaderRows() const
769 {
770     return mGroupHeaderRows;
771 }
772 
insertGroupHeaderRow(int idx,Row * row)773 void Theme::Column::insertGroupHeaderRow(int idx, Row *row)
774 {
775     if (idx >= mGroupHeaderRows.count()) {
776         mGroupHeaderRows.append(row);
777         return;
778     }
779     mGroupHeaderRows.insert(idx, row);
780 }
781 
removeGroupHeaderRow(Theme::Row * row)782 void Theme::Column::removeGroupHeaderRow(Theme::Row *row)
783 {
784     mGroupHeaderRows.removeAll(row);
785 }
786 
containsTextItems() const787 bool Theme::Column::containsTextItems() const
788 {
789     for (const auto row : std::as_const(mMessageRows)) {
790         if (row->containsTextItems()) {
791             return true;
792         }
793     }
794     for (const auto row : std::as_const(mGroupHeaderRows)) {
795         if (row->containsTextItems()) {
796             return true;
797         }
798     }
799     return false;
800 }
801 
save(QDataStream & stream) const802 void Theme::Column::save(QDataStream &stream) const
803 {
804     stream << mLabel;
805     stream << mPixmapName;
806     stream << mVisibleByDefault;
807     stream << mIsSenderOrReceiver;
808     stream << static_cast<int>(mMessageSorting);
809 
810     stream << static_cast<int>(mGroupHeaderRows.count());
811 
812     int cnt = mGroupHeaderRows.count();
813 
814     for (int i = 0; i < cnt; ++i) {
815         Row *row = mGroupHeaderRows.at(i);
816         row->save(stream);
817     }
818 
819     cnt = mMessageRows.count();
820     stream << static_cast<int>(cnt);
821 
822     for (int i = 0; i < cnt; ++i) {
823         Row *row = mMessageRows.at(i);
824         row->save(stream);
825     }
826 
827     // added in version 0x1014
828     mSharedRuntimeData->save(stream);
829 }
830 
load(QDataStream & stream,int themeVersion)831 bool Theme::Column::load(QDataStream &stream, int themeVersion)
832 {
833     removeAllGroupHeaderRows();
834     removeAllMessageRows();
835 
836     stream >> mLabel;
837 
838     if (themeVersion >= gThemeMinimumVersionWithColumnIcon) {
839         stream >> mPixmapName;
840     }
841 
842     stream >> mVisibleByDefault;
843     stream >> mIsSenderOrReceiver;
844 
845     int val;
846 
847     stream >> val;
848     mMessageSorting = static_cast<SortOrder::MessageSorting>(val);
849     if (!SortOrder::isValidMessageSorting(mMessageSorting)) {
850         qCDebug(MESSAGELIST_LOG) << "Invalid message sorting";
851         return false;
852     }
853 
854     if (themeVersion < gThemeMinimumVersionWithSortingByUnreadStatusAllowed) {
855         // The default "Classic" theme "Unread" column had sorting disabled here.
856         // We want to be nice to the existing users and automatically set
857         // the new sorting method for this column (so they don't have to make the
858         // complex steps to set it by themselves).
859         // This piece of code isn't strictly required: it's just a niceness :)
860         if ((mMessageSorting == SortOrder::NoMessageSorting) && (mLabel == i18n("Unread"))) {
861             mMessageSorting = SortOrder::SortMessagesByUnreadStatus;
862         }
863     }
864 
865     // group header row count
866     stream >> val;
867 
868     if ((val < 0) || (val > 50)) {
869         qCDebug(MESSAGELIST_LOG) << "Invalid group header row count";
870         return false; // senseless
871     }
872 
873     for (int i = 0; i < val; ++i) {
874         Row *row = new Row();
875         if (!row->load(stream, themeVersion)) {
876             qCDebug(MESSAGELIST_LOG) << "Group header row loading failed";
877             delete row;
878             return false;
879         }
880         addGroupHeaderRow(row);
881     }
882 
883     // message row count
884     stream >> val;
885 
886     if ((val < 0) || (val > 50)) {
887         qCDebug(MESSAGELIST_LOG) << "Invalid message row count";
888         return false; // senseless
889     }
890 
891     for (int i = 0; i < val; ++i) {
892         Row *row = new Row();
893         if (!row->load(stream, themeVersion)) {
894             qCDebug(MESSAGELIST_LOG) << "Message row loading failed";
895             delete row;
896             return false;
897         }
898         addMessageRow(row);
899     }
900 
901     if (themeVersion >= gThemeMinimumVersionWithColumnRuntimeData) {
902         // starting with version 0x1014 we have runtime data too
903         if (!mSharedRuntimeData->load(stream, themeVersion)) {
904             qCDebug(MESSAGELIST_LOG) << "Shared runtime data loading failed";
905             return false;
906         }
907     } else {
908         // assume default shared data
909         mSharedRuntimeData->setCurrentlyVisible(mVisibleByDefault);
910         mSharedRuntimeData->setCurrentWidth(-1);
911     }
912 
913     return true;
914 }
915 
Theme()916 Theme::Theme()
917     : OptionSet()
918 {
919     mGroupHeaderBackgroundMode = AutoColor;
920     mViewHeaderPolicy = ShowHeaderAlways;
921     mIconSize = gThemeDefaultIconSize;
922     mGroupHeaderBackgroundStyle = StyledJoinedRect;
923 }
924 
Theme(const QString & name,const QString & description,bool readOnly)925 Theme::Theme(const QString &name, const QString &description, bool readOnly)
926     : OptionSet(name, description, readOnly)
927 {
928     mGroupHeaderBackgroundMode = AutoColor;
929     mGroupHeaderBackgroundStyle = StyledJoinedRect;
930     mViewHeaderPolicy = ShowHeaderAlways;
931     mIconSize = gThemeDefaultIconSize;
932 }
933 
Theme(const Theme & src)934 Theme::Theme(const Theme &src)
935     : OptionSet(src)
936 {
937     mGroupHeaderBackgroundMode = src.mGroupHeaderBackgroundMode;
938     mGroupHeaderBackgroundColor = src.mGroupHeaderBackgroundColor;
939     mGroupHeaderBackgroundStyle = src.mGroupHeaderBackgroundStyle;
940     mViewHeaderPolicy = src.mViewHeaderPolicy;
941     mIconSize = src.mIconSize;
942     for (const auto col : std::as_const(src.mColumns)) {
943         addColumn(new Column(*col));
944     }
945 }
946 
~Theme()947 Theme::~Theme()
948 {
949     clearPixmapCache();
950     removeAllColumns();
951 }
952 
detach()953 void Theme::detach()
954 {
955     for (const auto col : std::as_const(mColumns)) {
956         col->detach();
957     }
958 }
959 
resetColumnState()960 void Theme::resetColumnState()
961 {
962     for (const auto col : std::as_const(mColumns)) {
963         col->setCurrentlyVisible(col->visibleByDefault());
964         col->setCurrentWidth(-1);
965     }
966 }
967 
resetColumnSizes()968 void Theme::resetColumnSizes()
969 {
970     for (const auto col : std::as_const(mColumns)) {
971         col->setCurrentWidth(-1);
972     }
973 }
974 
columns() const975 const QList<Theme::Column *> &Theme::columns() const
976 {
977     return mColumns;
978 }
979 
column(int idx) const980 Theme::Column *Theme::column(int idx) const
981 {
982     return mColumns.count() > idx ? mColumns.at(idx) : nullptr;
983 }
984 
removeAllColumns()985 void Theme::removeAllColumns()
986 {
987     while (!mColumns.isEmpty()) {
988         delete mColumns.takeFirst();
989     }
990 }
991 
addColumn(Theme::Column * column)992 void Theme::addColumn(Theme::Column *column)
993 {
994     mColumns.append(column);
995 }
996 
insertColumn(int idx,Column * column)997 void Theme::insertColumn(int idx, Column *column)
998 {
999     if (idx >= mColumns.count()) {
1000         mColumns.append(column);
1001         return;
1002     }
1003     mColumns.insert(idx, column);
1004 }
1005 
removeColumn(Theme::Column * col)1006 void Theme::removeColumn(Theme::Column *col)
1007 {
1008     mColumns.removeAll(col);
1009 }
1010 
groupHeaderBackgroundMode() const1011 Theme::GroupHeaderBackgroundMode Theme::groupHeaderBackgroundMode() const
1012 {
1013     return mGroupHeaderBackgroundMode;
1014 }
1015 
moveColumn(int idx,int newPosition)1016 void Theme::moveColumn(int idx, int newPosition)
1017 {
1018     if ((newPosition >= mColumns.count()) || newPosition < 0) {
1019         return;
1020     }
1021     mColumns.move(idx, newPosition);
1022 }
1023 
setGroupHeaderBackgroundMode(GroupHeaderBackgroundMode bm)1024 void Theme::setGroupHeaderBackgroundMode(GroupHeaderBackgroundMode bm)
1025 {
1026     mGroupHeaderBackgroundMode = bm;
1027     if ((bm == CustomColor) && !mGroupHeaderBackgroundColor.isValid()) {
1028         mGroupHeaderBackgroundColor = QColor(127, 127, 127); // something neutral
1029     }
1030 }
1031 
groupHeaderBackgroundColor() const1032 const QColor &Theme::groupHeaderBackgroundColor() const
1033 {
1034     return mGroupHeaderBackgroundColor;
1035 }
1036 
setGroupHeaderBackgroundColor(const QColor & clr)1037 void Theme::setGroupHeaderBackgroundColor(const QColor &clr)
1038 {
1039     mGroupHeaderBackgroundColor = clr;
1040 }
1041 
groupHeaderBackgroundStyle() const1042 Theme::GroupHeaderBackgroundStyle Theme::groupHeaderBackgroundStyle() const
1043 {
1044     return mGroupHeaderBackgroundStyle;
1045 }
1046 
setGroupHeaderBackgroundStyle(Theme::GroupHeaderBackgroundStyle groupHeaderBackgroundStyle)1047 void Theme::setGroupHeaderBackgroundStyle(Theme::GroupHeaderBackgroundStyle groupHeaderBackgroundStyle)
1048 {
1049     mGroupHeaderBackgroundStyle = groupHeaderBackgroundStyle;
1050 }
1051 
enumerateViewHeaderPolicyOptions()1052 QVector<QPair<QString, int>> Theme::enumerateViewHeaderPolicyOptions()
1053 {
1054     return {{i18n("Never Show"), NeverShowHeader}, {i18n("Always Show"), ShowHeaderAlways}};
1055 }
1056 
enumerateGroupHeaderBackgroundStyles()1057 QVector<QPair<QString, int>> Theme::enumerateGroupHeaderBackgroundStyles()
1058 {
1059     return {{i18n("Plain Rectangles"), PlainRect},
1060             {i18n("Plain Joined Rectangle"), PlainJoinedRect},
1061             {i18n("Rounded Rectangles"), RoundedRect},
1062             {i18n("Rounded Joined Rectangle"), RoundedJoinedRect},
1063             {i18n("Gradient Rectangles"), GradientRect},
1064             {i18n("Gradient Joined Rectangle"), GradientJoinedRect},
1065             {i18n("Styled Rectangles"), StyledRect},
1066             {i18n("Styled Joined Rectangles"), StyledJoinedRect}};
1067 }
1068 
viewHeaderPolicy() const1069 Theme::ViewHeaderPolicy Theme::viewHeaderPolicy() const
1070 {
1071     return mViewHeaderPolicy;
1072 }
1073 
setViewHeaderPolicy(Theme::ViewHeaderPolicy vhp)1074 void Theme::setViewHeaderPolicy(Theme::ViewHeaderPolicy vhp)
1075 {
1076     mViewHeaderPolicy = vhp;
1077 }
1078 
iconSize() const1079 int Theme::iconSize() const
1080 {
1081     return mIconSize;
1082 }
1083 
setIconSize(int iconSize)1084 void Theme::setIconSize(int iconSize)
1085 {
1086     if (mIconSize != iconSize) {
1087         clearPixmapCache();
1088 
1089         mIconSize = iconSize;
1090         if ((mIconSize < 8) || (mIconSize > 64)) {
1091             mIconSize = gThemeDefaultIconSize;
1092         }
1093     }
1094 }
1095 
load(QDataStream & stream)1096 bool Theme::load(QDataStream &stream)
1097 {
1098     removeAllColumns();
1099 
1100     int themeVersion;
1101 
1102     stream >> themeVersion;
1103 
1104     // We support themes starting at version gThemeMinimumSupportedVersion (0x1013 actually)
1105 
1106     if ((themeVersion > gThemeCurrentVersion) || (themeVersion < gThemeMinimumSupportedVersion)) {
1107         qCDebug(MESSAGELIST_LOG) << "Invalid theme version";
1108         return false; // b0rken (invalid version)
1109     }
1110 
1111     int val;
1112 
1113     stream >> val;
1114     mGroupHeaderBackgroundMode = static_cast<GroupHeaderBackgroundMode>(val);
1115     switch (mGroupHeaderBackgroundMode) {
1116     case Transparent:
1117     case AutoColor:
1118     case CustomColor:
1119         // ok
1120         break;
1121     default:
1122         qCDebug(MESSAGELIST_LOG) << "Invalid theme group header background mode";
1123         return false; // b0rken
1124     }
1125 
1126     stream >> mGroupHeaderBackgroundColor;
1127 
1128     stream >> val;
1129     mGroupHeaderBackgroundStyle = static_cast<GroupHeaderBackgroundStyle>(val);
1130     switch (mGroupHeaderBackgroundStyle) {
1131     case PlainRect:
1132     case PlainJoinedRect:
1133     case RoundedRect:
1134     case RoundedJoinedRect:
1135     case GradientRect:
1136     case GradientJoinedRect:
1137     case StyledRect:
1138     case StyledJoinedRect:
1139         // ok
1140         break;
1141     default:
1142         qCDebug(MESSAGELIST_LOG) << "Invalid theme group header background style";
1143         return false; // b0rken
1144     }
1145 
1146     stream >> val;
1147     mViewHeaderPolicy = (ViewHeaderPolicy)val;
1148     switch (mViewHeaderPolicy) {
1149     case ShowHeaderAlways:
1150     case NeverShowHeader:
1151         // ok
1152         break;
1153     default:
1154         qCDebug(MESSAGELIST_LOG) << "Invalid theme view header policy";
1155         return false; // b0rken
1156     }
1157 
1158     if (themeVersion >= gThemeMinimumVersionWithIconSizeField) {
1159         // icon size parameter
1160         stream >> mIconSize;
1161         if ((mIconSize < 8) || (mIconSize > 64)) {
1162             mIconSize = gThemeDefaultIconSize; // limit insane values
1163         }
1164     } else {
1165         mIconSize = gThemeDefaultIconSize;
1166     }
1167 
1168     // column count
1169     stream >> val;
1170     if (val < 1 || val > 50) {
1171         return false; // plain b0rken ( negative, zero or more than 50 columns )
1172     }
1173 
1174     for (int i = 0; i < val; ++i) {
1175         auto col = new Column();
1176         if (!col->load(stream, themeVersion)) {
1177             qCDebug(MESSAGELIST_LOG) << "Column loading failed";
1178             delete col;
1179             return false;
1180         }
1181         addColumn(col);
1182     }
1183 
1184     return true;
1185 }
1186 
save(QDataStream & stream) const1187 void Theme::save(QDataStream &stream) const
1188 {
1189     stream << (int)gThemeCurrentVersion;
1190 
1191     stream << (int)mGroupHeaderBackgroundMode;
1192     stream << mGroupHeaderBackgroundColor;
1193     stream << (int)mGroupHeaderBackgroundStyle;
1194     stream << (int)mViewHeaderPolicy;
1195     stream << mIconSize;
1196 
1197     const int cnt = mColumns.count();
1198     stream << (int)cnt;
1199 
1200     for (int i = 0; i < cnt; ++i) {
1201         Column *col = mColumns.at(i);
1202         col->save(stream);
1203     }
1204 }
1205 
clearPixmapCache() const1206 void Theme::clearPixmapCache() const
1207 {
1208     qDeleteAll(mPixmaps);
1209     mPixmaps.clear();
1210 }
1211 
populatePixmapCache() const1212 void Theme::populatePixmapCache() const
1213 {
1214     clearPixmapCache();
1215 
1216     mPixmaps.reserve(_IconCount);
1217     // WARNING: The order of those icons must be in sync with order of the
1218     // corresponding enum values in ThemeIcon!
1219     mPixmaps << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-unread-new")).pixmap(mIconSize, mIconSize))
1220              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-unread")).pixmap(mIconSize, mIconSize))
1221              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-read")).pixmap(mIconSize, mIconSize))
1222              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-deleted")).pixmap(mIconSize, mIconSize))
1223              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-replied")).pixmap(mIconSize, mIconSize))
1224              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-forwarded-replied")).pixmap(mIconSize, mIconSize))
1225              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-queued")).pixmap(mIconSize, mIconSize)) // mail-queue ?
1226              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-task")).pixmap(mIconSize, mIconSize))
1227              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-sent")).pixmap(mIconSize, mIconSize))
1228              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-forwarded")).pixmap(mIconSize, mIconSize))
1229              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-important")).pixmap(mIconSize, mIconSize)) // "flag"
1230              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-thread-watch")).pixmap(mIconSize, mIconSize))
1231              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-thread-ignored")).pixmap(mIconSize, mIconSize))
1232              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-junk")).pixmap(mIconSize, mIconSize))
1233              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-mark-notjunk")).pixmap(mIconSize, mIconSize))
1234              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-signed-verified")).pixmap(mIconSize, mIconSize))
1235              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-signed-part")).pixmap(mIconSize, mIconSize))
1236              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-signed")).pixmap(mIconSize, mIconSize))
1237              << new QPixmap(QIcon::fromTheme(QStringLiteral("text-plain")).pixmap(mIconSize, mIconSize))
1238              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-encrypted-full")).pixmap(mIconSize, mIconSize))
1239              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-encrypted-part")).pixmap(mIconSize, mIconSize))
1240              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-encrypted")).pixmap(mIconSize, mIconSize))
1241              << new QPixmap(QIcon::fromTheme(QStringLiteral("text-plain")).pixmap(mIconSize, mIconSize))
1242              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-attachment")).pixmap(mIconSize, mIconSize))
1243              << new QPixmap(QIcon::fromTheme(QStringLiteral("view-pim-notes")).pixmap(mIconSize, mIconSize))
1244              << new QPixmap(QIcon::fromTheme(QStringLiteral("mail-invitation")).pixmap(mIconSize, mIconSize))
1245              << ((QApplication::isRightToLeft()) ? new QPixmap(QIcon::fromTheme(QStringLiteral("arrow-left")).pixmap(mIconSize, mIconSize))
1246                                                  : new QPixmap(QIcon::fromTheme(QStringLiteral("arrow-right")).pixmap(mIconSize, mIconSize)))
1247              << new QPixmap(QIcon::fromTheme(QStringLiteral("arrow-down")).pixmap(mIconSize, mIconSize))
1248              << new QPixmap(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("messagelist/pics/mail-vertical-separator-line.png")))
1249              << new QPixmap(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("messagelist/pics/mail-horizontal-space.png")));
1250 }
1251