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