Messagelib

themeeditor.cpp
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 "utils/themeeditor.h"
10#include "core/groupheaderitem.h"
11#include "core/messageitem.h"
12#include "core/modelinvariantrowmapper.h"
13#include "utils/comboboxutils.h"
14#include <KLocalization>
15
16#include <Akonadi/MessageStatus>
17
18#include <KTextEdit>
19
20#include <QActionGroup>
21#include <QCheckBox>
22#include <QCursor>
23#include <QDrag>
24#include <QGridLayout>
25#include <QGroupBox>
26#include <QHeaderView>
27#include <QMimeData>
28#include <QMouseEvent>
29#include <QPaintEvent>
30#include <QPainter>
31#include <QPixmap>
32#include <QPushButton>
33#include <QStringList>
34
35#include <KIconLoader>
36#include <KLocalizedString>
37#include <QColorDialog>
38#include <QComboBox>
39#include <QLineEdit>
40#include <QMenu>
41#include <QSpinBox>
42
43#include <QDialogButtonBox>
44#include <QVBoxLayout>
45#include <ctime> // for time_t
46
47using namespace MessageList::Utils;
48using namespace MessageList::Core;
49
50static const char gThemeContentItemTypeDndMimeDataFormat[] = "application/x-kmail-messagelistview-theme-contentitem-type";
51
52ThemeColumnPropertiesDialog::ThemeColumnPropertiesDialog(QWidget *parent, Theme::Column *column, const QString &title)
53 : QDialog(parent)
54 , mColumn(column)
55{
56 auto mainLayout = new QVBoxLayout(this);
58 QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
59 okButton->setDefault(true);
62 setWindowTitle(title);
63
64 auto base = new QWidget(this);
65 mainLayout->addWidget(base);
66 mainLayout->addWidget(buttonBox);
67
68 auto g = new QGridLayout(base);
69
70 auto l = new QLabel(i18nc("@label:textbox Property name", "Name:"), base);
71 g->addWidget(l, 0, 0);
72
73 mNameEdit = new QLineEdit(base);
74 mNameEdit->setToolTip(i18nc("@info:tooltip", "The label that will be displayed in the column header."));
75 g->addWidget(mNameEdit, 0, 1);
76
77 l = new QLabel(i18nc("@label:textbox", "Header click sorts messages:"), base);
78 g->addWidget(l, 1, 0);
79
80 mMessageSortingCombo = new QComboBox(base);
81 mMessageSortingCombo->setToolTip(i18nc("@info:tooltip", "The sorting order that clicking on this column header will switch to."));
82 g->addWidget(mMessageSortingCombo, 1, 1);
83
84 mVisibleByDefaultCheck = new QCheckBox(i18nc("@option:check", "Visible by default"), base);
85 mVisibleByDefaultCheck->setToolTip(i18nc("@info:tooltip", "Check this if this column should be visible when the theme is selected."));
86 g->addWidget(mVisibleByDefaultCheck, 2, 1);
87
88 mIsSenderOrReceiverCheck = new QCheckBox(i18nc("@option:check", "Contains \"Sender or Receiver\" field"), base);
89 mIsSenderOrReceiverCheck->setToolTip(
90 i18nc("@info:tooltip", "Check this if this column label should be updated depending on the folder \"inbound\"/\"outbound\" type."));
91 g->addWidget(mIsSenderOrReceiverCheck, 3, 1);
92
93 g->setColumnStretch(1, 1);
94 g->setRowStretch(10, 1);
95
96 connect(okButton, &QPushButton::clicked, this, &ThemeColumnPropertiesDialog::slotOkButtonClicked);
97
98 // Display the current settings
99 mNameEdit->setText(mColumn->label());
100 mVisibleByDefaultCheck->setChecked(mColumn->visibleByDefault());
101 mIsSenderOrReceiverCheck->setChecked(mColumn->isSenderOrReceiver());
103 ComboBoxUtils::setIntegerOptionComboValue(mMessageSortingCombo, mColumn->messageSorting());
104}
105
106void ThemeColumnPropertiesDialog::slotOkButtonClicked()
107{
108 QString text = mNameEdit->text();
109 if (text.isEmpty()) {
110 text = i18n("Unnamed Column");
111 }
112 mColumn->setLabel(text);
113 mColumn->setVisibleByDefault(mVisibleByDefaultCheck->isChecked());
114 mColumn->setIsSenderOrReceiver(mIsSenderOrReceiverCheck->isChecked());
115 mColumn->setMessageSorting(
117
118 accept();
119}
120
121ThemeContentItemSourceLabel::ThemeContentItemSourceLabel(QWidget *parent, Theme::ContentItem::Type type)
122 : QLabel(parent)
123 , mType(type)
124{
125 setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
126}
127
128ThemeContentItemSourceLabel::~ThemeContentItemSourceLabel() = default;
129
130MessageList::Core::Theme::ContentItem::Type ThemeContentItemSourceLabel::type() const
131{
132 return mType;
133}
134
135void ThemeContentItemSourceLabel::mousePressEvent(QMouseEvent *e)
136{
137 if (e->button() == Qt::LeftButton) {
138 mMousePressPoint = e->pos();
139 }
140}
141
142void ThemeContentItemSourceLabel::mouseMoveEvent(QMouseEvent *e)
143{
144 if (e->buttons() & Qt::LeftButton) {
145 const QPoint diff = mMousePressPoint - e->pos();
146 if (diff.manhattanLength() > 4) {
147 startDrag();
148 }
149 }
150}
151
152void ThemeContentItemSourceLabel::startDrag()
153{
154 // QPixmap pix = QPixmap::grabWidget( this );
155 // QPixmap alpha( pix.width(), pix.height() );
156 // alpha.fill(0x0f0f0f0f);
157 // pix.setAlphaChannel( alpha ); // <-- this crashes... no alpha for dragged pixmap :(
158 auto data = new QMimeData();
159 QByteArray arry;
160 arry.resize(sizeof(Theme::ContentItem::Type));
161 *((Theme::ContentItem::Type *)arry.data()) = mType;
162 data->setData(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat), arry);
163 auto drag = new QDrag(this);
164 drag->setMimeData(data);
165 // drag->setPixmap( pix );
166 // drag->setHotSpot( mapFromGlobal( QCursor::pos() ) );
167 drag->exec(Qt::CopyAction, Qt::CopyAction);
168}
169
170ThemePreviewDelegate::ThemePreviewDelegate(QAbstractItemView *parent)
171 : ThemeDelegate(parent)
172{
173 mRowMapper = new ModelInvariantRowMapper();
174
175 mSampleGroupHeaderItem = new GroupHeaderItem(i18n("Message Group"));
176
177 mSampleGroupHeaderItem->setDate(time(nullptr));
178 mSampleGroupHeaderItem->setMaxDate(time(nullptr) + 31337);
179 mSampleGroupHeaderItem->setSubject(i18n("Very long subject very long subject very long subject very long subject very long subject very long"));
180
181 mSampleMessageItem = new FakeItem();
182
183 mSampleMessageItem->setDate(time(nullptr));
184 mSampleMessageItem->setSize(0x31337);
185 mSampleMessageItem->setMaxDate(time(nullptr) + 31337);
186 mSampleMessageItem->setSender(i18n("Sender"));
187 mSampleMessageItem->setReceiver(i18n("Receiver"));
188 mSampleMessageItem->setSubject(i18n("Very long subject very long subject very long subject very long subject very long subject very long"));
189 mSampleMessageItem->setFolder(i18n("Folder"));
190 mSampleMessageItem->setSignatureState(MessageItem::FullySigned);
191 mSampleMessageItem->setEncryptionState(MessageItem::FullyEncrypted);
192
194 list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 1"), QString()));
195 list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 2"), QString()));
196 list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 3"), QString()));
197 mSampleMessageItem->setFakeTags(list);
198
199 mRowMapper->createModelInvariantIndex(0, mSampleMessageItem);
200
201 mSampleGroupHeaderItem->rawAppendChildItem(mSampleMessageItem);
202 mSampleMessageItem->setParent(mSampleGroupHeaderItem);
203
205
206 stat.fromQInt32(0x7fffffff);
207 stat.setQueued(false);
208 stat.setSent(false);
209 stat.setSpam(true);
210 stat.setWatched(true);
211 stat.setHasInvitation();
212 // stat.setHasAttachment( false );
213
214 mSampleMessageItem->setStatus(stat);
215}
216
217ThemePreviewDelegate::~ThemePreviewDelegate()
218{
219 delete mSampleGroupHeaderItem;
220 // delete mSampleMessageItem; (deleted by the parent)
221 delete mRowMapper;
222}
223
224Item *ThemePreviewDelegate::itemFromIndex(const QModelIndex &index) const
225{
226 if (index.parent().isValid()) {
227 return mSampleMessageItem;
228 }
229
230 return mSampleGroupHeaderItem;
231}
232
233ThemePreviewWidget::ThemePreviewWidget(QWidget *parent)
234 : QTreeWidget(parent)
235 , mTheme(nullptr)
236{
237 mSelectedThemeContentItem = nullptr;
238 mSelectedThemeColumn = nullptr;
239 mFirstShow = true;
240 mReadOnly = false;
241
242 mDelegate = new ThemePreviewDelegate(this);
243 setItemDelegate(mDelegate);
244 setRootIsDecorated(false);
245 viewport()->setAcceptDrops(true);
246
247 header()->setContextMenuPolicy(Qt::CustomContextMenu);
248 connect(header(), &QWidget::customContextMenuRequested, this, &ThemePreviewWidget::slotHeaderContextMenuRequested);
249
250 mGroupHeaderSampleItem = new QTreeWidgetItem(this);
251 mGroupHeaderSampleItem->setText(0, QString());
252 mGroupHeaderSampleItem->setFlags(Qt::ItemIsEnabled);
253
254 auto m = new QTreeWidgetItem(mGroupHeaderSampleItem);
255 m->setText(0, QString());
256
257 mGroupHeaderSampleItem->setExpanded(true);
258 header()->setSectionsMovable(false);
259}
260
261void ThemePreviewWidget::changeEvent(QEvent *event)
262{
263 if (event->type() == QEvent::FontChange) {
264 mDelegate->generalFontChanged();
265 }
267}
268
269ThemePreviewWidget::~ThemePreviewWidget() = default;
270
271QSize ThemePreviewWidget::sizeHint() const
272{
273 return {350, 180};
274}
275
276void ThemePreviewWidget::setReadOnly(bool readOnly)
277{
278 mReadOnly = readOnly;
279}
280
281void ThemePreviewWidget::applyThemeColumnWidths()
282{
283 if (!mTheme) {
284 return;
285 }
286
287 const QList<Theme::Column *> &columns = mTheme->columns();
288
289 if (columns.isEmpty()) {
290 viewport()->update(); // trigger a repaint
291 return;
292 }
293
294 // Now we want to distribute the available width on all the columns.
295 // The algorithm used here is very similar to the one used in View::applyThemeColumns().
296 // It just takes care of ALL the columns instead of the visible ones.
297
299
300 // Gather size hints for all sections.
301 int idx = 0;
302 int totalVisibleWidthHint = 0;
304
305 for (it = columns.constBegin(); it != end; ++it) {
306 totalVisibleWidthHint += mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width();
307 idx++;
308 }
309
310 if (totalVisibleWidthHint < 16) {
311 totalVisibleWidthHint = 16; // be reasonable
312 }
313
314 // Now we can compute proportional widths.
315
316 idx = 0;
317
318 QList<int> realWidths;
319 realWidths.reserve(columns.count());
320 int totalVisibleWidth = 0;
321
322 end = columns.constEnd();
323 for (it = columns.constBegin(); it != end; ++it) {
324 int hintWidth = mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width();
325 int realWidth;
326 if ((*it)->containsTextItems()) {
327 // the column contains text items, it should get more space
328 realWidth = ((hintWidth * viewport()->width()) / totalVisibleWidthHint) - 2; // -2 is heuristic
329 if (realWidth < (hintWidth + 2)) {
330 realWidth = hintWidth + 2; // can't be less
331 }
332 } else {
333 // the column contains no text items, it should get just a little bit more than its sizeHint().
334 realWidth = hintWidth + 2;
335 }
336
337 realWidths.append(realWidth);
338 totalVisibleWidth += realWidth;
339
340 idx++;
341 }
342
343 idx = 0;
344
345 totalVisibleWidth += 4; // account for some view's border
346
347 if (totalVisibleWidth < viewport()->width()) {
348 // give the additional space to the text columns
349 // also give more space to the first ones and less space to the last ones
350 int available = viewport()->width() - totalVisibleWidth;
351
352 for (it = columns.begin(); it != columns.end(); ++it) {
353 if (((*it)->visibleByDefault() || (idx == 0)) && (*it)->containsTextItems()) {
354 // give more space to this column
355 available >>= 1; // eat half of the available space
356 realWidths[idx] += available; // and give it to this column
357 }
358
359 idx++;
360 }
361
362 // if any space is still available, give it to the first column
363 if (available) {
364 realWidths[0] += available;
365 }
366 }
367
368 idx = 0;
369
370 // We're ready.
371 // Assign widths. Hide the sections that are not visible by default, show the other ones.
372 for (it = columns.begin(); it != columns.end(); ++it) {
373 header()->resizeSection(idx, realWidths[idx]);
374 idx++;
375 }
376
377#if 0
378 if (mTheme->viewHeaderPolicy() == Theme::NeverShowHeader) {
379 header()->hide();
380 } else {
381 header()->show();
382 }
383#endif
384}
385
386void ThemePreviewWidget::setTheme(Theme *theme)
387{
388 bool themeChanged = theme != mTheme;
389
390 mSelectedThemeContentItem = nullptr;
391 mThemeSelectedContentItemRect = QRect();
392 mDropIndicatorPoint1 = QPoint();
393 mDropIndicatorPoint2 = QPoint();
394 mTheme = theme;
395 mDelegate->setTheme(theme);
396 if (!mTheme) {
397 return;
398 }
399 mGroupHeaderSampleItem->setExpanded(true);
400
401 const QList<Theme::Column *> &columns = mTheme->columns();
402
403 setColumnCount(columns.count());
404
405 QStringList headerLabels;
406 headerLabels.reserve(columns.count());
408 for (QList<Theme::Column *>::ConstIterator it = columns.constBegin(); it != end; ++it) {
409 QString label = (*it)->label();
410 if ((*it)->visibleByDefault()) {
411 label += QStringLiteral(" (%1)").arg(i18nc("Indicates whether or not a header label is visible", "Visible"));
412 }
413
414 headerLabels.append(label);
415 }
416
417 setHeaderLabels(headerLabels);
418
419 if (themeChanged) {
420 applyThemeColumnWidths();
421 }
422
423 viewport()->update(); // trigger a repaint
424}
425
426void ThemePreviewWidget::internalHandleDragEnterEvent(QDragEnterEvent *e)
427{
428 e->ignore();
429
430 if (!e->mimeData()) {
431 return;
432 }
433 if (!e->mimeData()->hasFormat(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat))) {
434 return;
435 }
436
437 e->accept();
438}
439
440void ThemePreviewWidget::showEvent(QShowEvent *e)
441{
443
444 if (mFirstShow) {
445 // Make sure we re-apply column widths the first time we're shown.
446 // The first "apply" call was made while the widget was still hidden and
447 // almost surely had wrong sizes.
448 applyThemeColumnWidths();
449 mFirstShow = false;
450 }
451}
452
453void ThemePreviewWidget::dragEnterEvent(QDragEnterEvent *e)
454{
455 internalHandleDragEnterEvent(e);
456
457 mThemeSelectedContentItemRect = QRect();
458
459 viewport()->update(); // trigger a repaint
460}
461
462void ThemePreviewWidget::internalHandleDragMoveEvent(QDragMoveEvent *e)
463{
464 e->ignore();
465
466 if (mReadOnly) {
467 return;
468 }
469
470 if (!e->mimeData()) {
471 return;
472 }
473 if (!e->mimeData()->hasFormat(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat))) {
474 return;
475 }
476
477 QByteArray arry = e->mimeData()->data(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat));
478
479 if (arry.size() != sizeof(Theme::ContentItem::Type)) {
480 return; // ugh
481 }
482
484 if (!computeContentItemInsertPosition(e->position().toPoint(), type)) {
485 return;
486 }
487
488 e->accept();
489}
490
491void ThemePreviewWidget::dragMoveEvent(QDragMoveEvent *e)
492{
493 if (mReadOnly) {
494 return;
495 }
496
497 internalHandleDragMoveEvent(e);
498
499 mThemeSelectedContentItemRect = QRect();
500
501 viewport()->update(); // trigger a repaint
502}
503
504void ThemePreviewWidget::dropEvent(QDropEvent *e)
505{
506 mDropIndicatorPoint1 = mDropIndicatorPoint2;
507
508 e->ignore();
509
510 if (mReadOnly) {
511 return;
512 }
513
514 if (!e->mimeData()) {
515 return;
516 }
517
518 if (!e->mimeData()->hasFormat(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat))) {
519 return;
520 }
521
522 QByteArray arry = e->mimeData()->data(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat));
523
524 if (arry.size() != sizeof(Theme::ContentItem::Type)) {
525 return; // ugh
526 }
527
529 if (!computeContentItemInsertPosition(e->position().toPoint(), type)) {
530 viewport()->update();
531 return;
532 }
533
534 Theme::Row *row = nullptr;
535
536 switch (mRowInsertPosition) {
537 case AboveRow:
538 row = new Theme::Row();
539 if (mDelegate->hitItem()->type() == Item::Message) {
540 const_cast<Theme::Column *>(mDelegate->hitColumn())->insertMessageRow(mDelegate->hitRowIndex(), row);
541 } else {
542 const_cast<Theme::Column *>(mDelegate->hitColumn())->insertGroupHeaderRow(mDelegate->hitRowIndex(), row);
543 }
544 break;
545 case InsideRow:
546 row = const_cast<Theme::Row *>(mDelegate->hitRow());
547 break;
548 case BelowRow:
549 row = new Theme::Row();
550 if (mDelegate->hitItem()->type() == Item::Message) {
551 const_cast<Theme::Column *>(mDelegate->hitColumn())->insertMessageRow(mDelegate->hitRowIndex() + 1, row);
552 } else {
553 const_cast<Theme::Column *>(mDelegate->hitColumn())->insertGroupHeaderRow(mDelegate->hitRowIndex() + 1, row);
554 }
555 break;
556 }
557
558 if (!row) {
559 return;
560 }
561
562 auto ci = new Theme::ContentItem(type);
563 if (ci->canBeDisabled()) {
564 if (ci->isClickable()) {
565 ci->setSoftenByBlendingWhenDisabled(true); // default to softened
566 } else {
567 ci->setHideWhenDisabled(true); // default to hidden
568 }
569 }
570
571 int idx;
572
573 switch (mItemInsertPosition) {
574 case OnLeftOfItem:
575 if (!mDelegate->hitContentItem()) {
576 // bleah
577 delete ci;
578 return;
579 }
580 idx = mDelegate->hitContentItemRight() ? row->rightItems().indexOf(const_cast<Theme::ContentItem *>(mDelegate->hitContentItem()))
581 : row->leftItems().indexOf(const_cast<Theme::ContentItem *>(mDelegate->hitContentItem()));
582 if (idx < 0) {
583 // bleah
584 delete ci;
585 return;
586 }
587 if (mDelegate->hitContentItemRight()) {
588 row->insertRightItem(idx + 1, ci);
589 } else {
590 row->insertLeftItem(idx, ci);
591 }
592 break;
593 case OnRightOfItem:
594 if (!mDelegate->hitContentItem()) {
595 // bleah
596 delete ci;
597 return;
598 }
599 idx = mDelegate->hitContentItemRight() ? row->rightItems().indexOf(const_cast<Theme::ContentItem *>(mDelegate->hitContentItem()))
600 : row->leftItems().indexOf(const_cast<Theme::ContentItem *>(mDelegate->hitContentItem()));
601 if (idx < 0) {
602 // bleah
603 delete ci;
604 return;
605 }
606 if (mDelegate->hitContentItemRight()) {
607 row->insertRightItem(idx, ci);
608 } else {
609 row->insertLeftItem(idx + 1, ci);
610 }
611 break;
612 case AsLastLeftItem:
613 row->addLeftItem(ci);
614 break;
615 case AsLastRightItem:
616 row->addRightItem(ci);
617 break;
618 case AsFirstLeftItem:
619 row->insertLeftItem(0, ci);
620 break;
621 case AsFirstRightItem:
622 row->insertRightItem(0, ci);
623 break;
624 default: // should never happen
625 row->addRightItem(ci);
626 break;
627 }
628
630
631 mThemeSelectedContentItemRect = QRect();
632 mDropIndicatorPoint1 = mDropIndicatorPoint2;
633 mSelectedThemeContentItem = nullptr;
634
635 setTheme(mTheme); // this will reset theme cache and trigger a global update
636}
637
638bool ThemePreviewWidget::computeContentItemInsertPosition(const QPoint &pos, Theme::ContentItem::Type type)
639{
640 mDropIndicatorPoint1 = mDropIndicatorPoint2; // this marks the position as invalid
641
642 if (!mDelegate->hitTest(pos, false)) {
643 return false;
644 }
645
646 if (!mDelegate->hitRow()) {
647 return false;
648 }
649
650 if (mDelegate->hitRowIsMessageRow()) {
652 return false;
653 }
654 } else {
656 return false;
657 }
658 }
659
660 QRect rowRect = mDelegate->hitRowRect();
661
662 if (pos.y() < rowRect.top() + 3) {
663 // above a row
664 mRowInsertPosition = AboveRow;
665 if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) {
666 mDropIndicatorPoint1 = rowRect.topLeft();
667 mItemInsertPosition = AsLastLeftItem;
668 } else {
669 mDropIndicatorPoint1 = rowRect.topRight();
670 mItemInsertPosition = AsLastRightItem;
671 }
672 mDropIndicatorPoint2 = QPoint(rowRect.left() + (rowRect.width() / 2), rowRect.top());
673 return true;
674 }
675
676 if (pos.y() > rowRect.bottom() - 3) {
677 // below a row
678 mRowInsertPosition = BelowRow;
679 if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) {
680 mDropIndicatorPoint1 = rowRect.bottomLeft();
681 mItemInsertPosition = AsLastLeftItem;
682 } else {
683 mDropIndicatorPoint1 = rowRect.bottomRight();
684 mItemInsertPosition = AsLastRightItem;
685 }
686 mDropIndicatorPoint2 = QPoint(rowRect.left() + (rowRect.width() / 2), rowRect.bottom());
687 return true;
688 }
689
690 mRowInsertPosition = InsideRow;
691
692 if (!mDelegate->hitContentItem()) {
693 // didn't hit anything... probably no items in the row
694 if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) {
695 mItemInsertPosition = AsLastLeftItem;
696 mDropIndicatorPoint1 = QPoint(rowRect.left(), rowRect.top());
697 mDropIndicatorPoint2 = QPoint(rowRect.left(), rowRect.bottom());
698 } else {
699 mItemInsertPosition = AsLastRightItem;
700 mDropIndicatorPoint1 = QPoint(rowRect.right(), rowRect.top());
701 mDropIndicatorPoint2 = QPoint(rowRect.right(), rowRect.bottom());
702 }
703 return true;
704 }
705
706 // hit something, maybe inexactly
707 QRect itemRect = mDelegate->hitContentItemRect();
708
709 if (!itemRect.contains(pos)) {
710 // inexact hit: outside an item
711 if (pos.x() > itemRect.right()) {
712 // right side of an item
713 if (mDelegate->hitRow()->rightItems().count() < 1) {
714 // between the last left item and the right side
715 if (pos.x() > (itemRect.right() + ((rowRect.right() - itemRect.right()) / 2))) {
716 // first/last right item
717 mItemInsertPosition = AsFirstRightItem;
718 mDropIndicatorPoint1 = rowRect.topRight();
719 mDropIndicatorPoint2 = rowRect.bottomRight();
720 }
721 return true;
722 }
723 // either there were some right items (so the theme delegate knows that the reported item is the closest)
724 // or there were no right items but the position is closest to the left item than the right row end
725 mItemInsertPosition = OnRightOfItem;
726 mDropIndicatorPoint1 = itemRect.topRight();
727 mDropIndicatorPoint2 = itemRect.bottomRight();
728 return true;
729 }
730
731 // left side of an item
732 if (mDelegate->hitRow()->leftItems().count() < 1) {
733 // between the left side and the leftmost right item
734 if (pos.x() < (itemRect.left() - ((itemRect.left() - rowRect.left()) / 2))) {
735 mItemInsertPosition = AsFirstLeftItem;
736 mDropIndicatorPoint1 = rowRect.topLeft();
737 mDropIndicatorPoint2 = rowRect.bottomLeft();
738 return true;
739 }
740 }
741 mItemInsertPosition = OnLeftOfItem;
742 mDropIndicatorPoint1 = itemRect.topLeft();
743 mDropIndicatorPoint2 = itemRect.bottomLeft();
744 return true;
745 }
746
747 // exact hit
748 if (pos.x() < (itemRect.left() + (itemRect.width() / 2))) {
749 // left side
750 mItemInsertPosition = OnLeftOfItem;
751 mDropIndicatorPoint1 = itemRect.topLeft();
752 mDropIndicatorPoint2 = itemRect.bottomLeft();
753 return true;
754 }
755
756 // right side
757 mItemInsertPosition = OnRightOfItem;
758 mDropIndicatorPoint1 = itemRect.topRight();
759 mDropIndicatorPoint2 = itemRect.bottomRight();
760 return true;
761}
762
763void ThemePreviewWidget::mouseMoveEvent(QMouseEvent *e)
764{
765 if (!(mSelectedThemeContentItem && (e->buttons() & Qt::LeftButton)) || mReadOnly) {
767 return;
768 }
769
770 if (mSelectedThemeContentItem != mDelegate->hitContentItem()) {
772 return; // ugh.. something weird happened
773 }
774
775 // starting a drag ?
776 const QPoint diff = e->pos() - mMouseDownPoint;
777 if (diff.manhattanLength() <= 4) {
779 return; // ugh.. something weird happened
780 }
781
782 // starting a drag
783 auto data = new QMimeData();
784 QByteArray arry;
785 arry.resize(sizeof(Theme::ContentItem::Type));
786 *((Theme::ContentItem::Type *)arry.data()) = mSelectedThemeContentItem->type();
787 data->setData(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat), arry);
788 auto drag = new QDrag(this);
789 drag->setMimeData(data);
790
791 // remove the Theme::ContentItem from the Theme
792 if (mDelegate->hitContentItemRight()) {
793 const_cast<Theme::Row *>(mDelegate->hitRow())->removeRightItem(mSelectedThemeContentItem);
794 } else {
795 const_cast<Theme::Row *>(mDelegate->hitRow())->removeLeftItem(mSelectedThemeContentItem);
796 }
797
798 delete mSelectedThemeContentItem;
799
800 if (mDelegate->hitRow()->rightItems().isEmpty() && mDelegate->hitRow()->leftItems().isEmpty()) {
801 if (mDelegate->hitItem()->type() == Item::Message) {
802 if (mDelegate->hitColumn()->messageRows().count() > 1) {
803 const_cast<Theme::Column *>(mDelegate->hitColumn())->removeMessageRow(const_cast<Theme::Row *>(mDelegate->hitRow()));
804 delete mDelegate->hitRow();
805 }
806 } else {
807 if (mDelegate->hitColumn()->groupHeaderRows().count() > 1) {
808 const_cast<Theme::Column *>(mDelegate->hitColumn())->removeGroupHeaderRow(const_cast<Theme::Row *>(mDelegate->hitRow()));
809 delete mDelegate->hitRow();
810 }
811 }
812 }
813
814 mSelectedThemeContentItem = nullptr;
815 mThemeSelectedContentItemRect = QRect();
816 mDropIndicatorPoint1 = mDropIndicatorPoint2;
817
818 setTheme(mTheme); // this will reset theme cache and trigger a global update
819
820 // and do drag
821 drag->exec(Qt::CopyAction, Qt::CopyAction);
822}
823
824void ThemePreviewWidget::mousePressEvent(QMouseEvent *e)
825{
826 if (mReadOnly) {
828 return;
829 }
830
831 mMouseDownPoint = e->pos();
832
833 if (mDelegate->hitTest(mMouseDownPoint)) {
834 mSelectedThemeContentItem = const_cast<Theme::ContentItem *>(mDelegate->hitContentItem());
835 mThemeSelectedContentItemRect = mSelectedThemeContentItem ? mDelegate->hitContentItemRect() : QRect();
836 } else {
837 mSelectedThemeContentItem = nullptr;
838 mThemeSelectedContentItemRect = QRect();
839 }
840
842 viewport()->update();
843
844 if (e->button() == Qt::RightButton) {
845 QMenu menu;
846
847 if (mSelectedThemeContentItem) {
848 menu.addSection(Theme::ContentItem::description(mSelectedThemeContentItem->type()));
849
850 if (mSelectedThemeContentItem->displaysText()) {
851 QAction *act = menu.addAction(i18nc("@action:inmenu soften the text color", "Soften"));
852 act->setCheckable(true);
853 act->setChecked(mSelectedThemeContentItem->softenByBlending());
854 connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotSoftenActionTriggered);
855
856 auto childmenu = new QMenu(&menu);
857
858 act = childmenu->addAction(i18nc("@action:inmenu Font setting", "Bold"));
859 act->setData(QVariant(static_cast<int>(Theme::ContentItem::IsBold)));
860 act->setCheckable(true);
861 act->setChecked(mSelectedThemeContentItem->isBold());
862 act = childmenu->addAction(i18nc("@action:inmenu Font setting", "Italic"));
863 act->setData(QVariant(static_cast<int>(Theme::ContentItem::IsItalic)));
864 act->setCheckable(true);
865 act->setChecked(mSelectedThemeContentItem->isItalic());
866
867 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotFontMenuTriggered);
868
869 menu.addMenu(childmenu)->setText(i18n("Font"));
870 }
871
872 if (mSelectedThemeContentItem->canUseCustomColor()) {
873 auto childmenu = new QMenu(&menu);
874
875 auto grp = new QActionGroup(childmenu);
876
877 QAction *act = childmenu->addAction(i18nc("@action:inmenu Foreground color setting", "Default"));
878 act->setData(QVariant(static_cast<int>(0)));
879 act->setCheckable(true);
880 act->setChecked(!mSelectedThemeContentItem->useCustomColor());
881 grp->addAction(act);
882 act = childmenu->addAction(i18nc("@action:inmenu Foreground color setting", "Custom..."));
883 act->setData(QVariant(static_cast<int>(Theme::ContentItem::UseCustomColor)));
884 act->setCheckable(true);
885 act->setChecked(mSelectedThemeContentItem->useCustomColor());
886 grp->addAction(act);
887
888 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotForegroundColorMenuTriggered);
889
890 menu.addMenu(childmenu)->setText(i18n("Foreground Color"));
891 }
892
893 if (mSelectedThemeContentItem->canBeDisabled()) {
894 auto childmenu = new QMenu(&menu);
895
896 auto grp = new QActionGroup(childmenu);
897
898 QAction *act =
899 childmenu->addAction(i18nc("Hide a mark if the mail does not have the attribute, e.g. Important mark on a non important mail", "Hide"));
900 act->setData(QVariant(static_cast<int>(Theme::ContentItem::HideWhenDisabled)));
901 act->setCheckable(true);
902 act->setChecked(mSelectedThemeContentItem->hideWhenDisabled());
903 grp->addAction(act);
904 act = childmenu->addAction(
905 i18nc("Keep a empty space in the list if the mail does not have the attribute, e.g. Important mark on a non important mail",
906 "Keep Empty Space"));
907 act->setData(QVariant(static_cast<int>(0)));
908 act->setCheckable(true);
909 act->setChecked(!(mSelectedThemeContentItem->softenByBlendingWhenDisabled() || mSelectedThemeContentItem->hideWhenDisabled()));
910 grp->addAction(act);
911 act = childmenu->addAction(
912 i18nc("Show the icon softened in the list if the mail does not have the attribute, e.g. Important mark on a non important mail",
913 "Keep Softened Icon"));
914 act->setData(QVariant(static_cast<int>(Theme::ContentItem::SoftenByBlendingWhenDisabled)));
915 act->setCheckable(true);
916 act->setChecked(mSelectedThemeContentItem->softenByBlendingWhenDisabled());
917 grp->addAction(act);
918
919 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotDisabledFlagsMenuTriggered);
920
921 menu.addMenu(childmenu)->setText(i18n("When Disabled"));
922 }
923 }
924
925 if (mDelegate->hitItem()) {
926 if (mDelegate->hitItem()->type() == Item::GroupHeader) {
927 menu.addSection(i18n("Group Header"));
928
929 // Background color (mode) submenu
930 auto childmenu = new QMenu(&menu);
931
932 auto grp = new QActionGroup(childmenu);
933
934 QAction *act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "None"));
935 act->setData(QVariant(static_cast<int>(Theme::Transparent)));
936 act->setCheckable(true);
937 act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::Transparent);
938 grp->addAction(act);
939 act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "Automatic"));
940 act->setData(QVariant(static_cast<int>(Theme::AutoColor)));
941 act->setCheckable(true);
942 act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::AutoColor);
943 grp->addAction(act);
944 act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "Custom..."));
945 act->setData(QVariant(static_cast<int>(Theme::CustomColor)));
946 act->setCheckable(true);
947 act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::CustomColor);
948 grp->addAction(act);
949
950 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotGroupHeaderBackgroundModeMenuTriggered);
951
952 menu.addMenu(childmenu)->setText(i18n("Background Color"));
953
954 // Background style submenu
955 childmenu = new QMenu(&menu);
956
957 grp = new QActionGroup(childmenu);
958 QList<QPair<QString, int>> styles = Theme::enumerateGroupHeaderBackgroundStyles();
959 QList<QPair<QString, int>>::ConstIterator end(styles.constEnd());
960
961 for (QList<QPair<QString, int>>::ConstIterator it = styles.constBegin(); it != end; ++it) {
962 act = childmenu->addAction((*it).first);
963 act->setData(QVariant((*it).second));
964 act->setCheckable(true);
965 act->setChecked(mTheme->groupHeaderBackgroundStyle() == static_cast<Theme::GroupHeaderBackgroundStyle>((*it).second));
966 grp->addAction(act);
967 }
968
969 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotGroupHeaderBackgroundStyleMenuTriggered);
970
971 act = menu.addMenu(childmenu);
972 act->setText(i18n("Background Style"));
973 if (mTheme->groupHeaderBackgroundMode() == Theme::Transparent) {
974 act->setEnabled(false);
975 }
976 }
977 }
978
979 if (menu.isEmpty()) {
980 return;
981 }
982
983 menu.exec(viewport()->mapToGlobal(e->pos()));
984 }
985}
986
987void ThemePreviewWidget::slotDisabledFlagsMenuTriggered(QAction *act)
988{
989 if (!mSelectedThemeContentItem) {
990 return;
991 }
992
993 bool ok;
994 const int flags = act->data().toInt(&ok);
995 if (!ok) {
996 return;
997 }
998
999 mSelectedThemeContentItem->setHideWhenDisabled(flags == Theme::ContentItem::HideWhenDisabled);
1000 mSelectedThemeContentItem->setSoftenByBlendingWhenDisabled(flags == Theme::ContentItem::SoftenByBlendingWhenDisabled);
1001
1002 setTheme(mTheme); // this will reset theme cache and trigger a global update
1003}
1004
1005void ThemePreviewWidget::slotForegroundColorMenuTriggered(QAction *act)
1006{
1007 if (!mSelectedThemeContentItem) {
1008 return;
1009 }
1010
1011 bool ok;
1012 const int flag = act->data().toInt(&ok);
1013 if (!ok) {
1014 return;
1015 }
1016
1017 if (flag == 0) {
1018 mSelectedThemeContentItem->setUseCustomColor(false);
1019 setTheme(mTheme); // this will reset theme cache and trigger a global update
1020 return;
1021 }
1022
1023 QColor clr;
1024 clr = QColorDialog::getColor(mSelectedThemeContentItem->customColor(), this);
1025 if (!clr.isValid()) {
1026 return;
1027 }
1028
1029 mSelectedThemeContentItem->setCustomColor(clr);
1030 mSelectedThemeContentItem->setUseCustomColor(true);
1031
1032 setTheme(mTheme); // this will reset theme cache and trigger a global update
1033}
1034
1035void ThemePreviewWidget::slotSoftenActionTriggered(bool)
1036{
1037 if (!mSelectedThemeContentItem) {
1038 return;
1039 }
1040
1041 mSelectedThemeContentItem->setSoftenByBlending(!mSelectedThemeContentItem->softenByBlending());
1042 setTheme(mTheme); // this will reset theme cache and trigger a global update
1043}
1044
1045void ThemePreviewWidget::slotFontMenuTriggered(QAction *act)
1046{
1047 if (!mSelectedThemeContentItem) {
1048 return;
1049 }
1050
1051 bool ok;
1052 const int flag = act->data().toInt(&ok);
1053 if (!ok) {
1054 return;
1055 }
1056
1057 if (flag == Theme::ContentItem::IsBold && mSelectedThemeContentItem->isBold() != act->isChecked()) {
1058 mSelectedThemeContentItem->setBold(act->isChecked());
1059 setTheme(mTheme);
1060 } else if (flag == Theme::ContentItem::IsItalic && mSelectedThemeContentItem->isItalic() != act->isChecked()) {
1061 mSelectedThemeContentItem->setItalic(act->isChecked());
1062 setTheme(mTheme);
1063 }
1064}
1065
1066void ThemePreviewWidget::slotGroupHeaderBackgroundModeMenuTriggered(QAction *act)
1067{
1068 bool ok;
1069 Theme::GroupHeaderBackgroundMode mode = static_cast<Theme::GroupHeaderBackgroundMode>(act->data().toInt(&ok));
1070 if (!ok) {
1071 return;
1072 }
1073
1074 switch (mode) {
1075 case Theme::Transparent:
1076 mTheme->setGroupHeaderBackgroundMode(Theme::Transparent);
1077 break;
1078 case Theme::AutoColor:
1079 mTheme->setGroupHeaderBackgroundMode(Theme::AutoColor);
1080 break;
1081 case Theme::CustomColor: {
1082 QColor clr;
1083 clr = QColorDialog::getColor(mTheme->groupHeaderBackgroundColor(), this);
1084 if (!clr.isValid()) {
1085 return;
1086 }
1087
1088 mTheme->setGroupHeaderBackgroundMode(Theme::CustomColor);
1089 mTheme->setGroupHeaderBackgroundColor(clr);
1090 break;
1091 }
1092 }
1093
1094 setTheme(mTheme); // this will reset theme cache and trigger a global update
1095}
1096
1097void ThemePreviewWidget::slotGroupHeaderBackgroundStyleMenuTriggered(QAction *act)
1098{
1099 bool ok;
1100 Theme::GroupHeaderBackgroundStyle mode = static_cast<Theme::GroupHeaderBackgroundStyle>(act->data().toInt(&ok));
1101 if (!ok) {
1102 return;
1103 }
1104
1105 mTheme->setGroupHeaderBackgroundStyle(mode);
1106
1107 setTheme(mTheme); // this will reset theme cache and trigger a global update
1108}
1109
1110void ThemePreviewWidget::paintEvent(QPaintEvent *e)
1111{
1113
1114 if (mThemeSelectedContentItemRect.isValid() || (mDropIndicatorPoint1 != mDropIndicatorPoint2)) {
1115 QPainter painter(viewport());
1116
1117 if (mThemeSelectedContentItemRect.isValid()) {
1118 painter.setPen(QPen(Qt::black));
1119 painter.drawRect(mThemeSelectedContentItemRect);
1120 }
1121 if (mDropIndicatorPoint1 != mDropIndicatorPoint2) {
1122 painter.setPen(QPen(Qt::black, 3));
1123 painter.drawLine(mDropIndicatorPoint1, mDropIndicatorPoint2);
1124 }
1125 }
1126}
1127
1128void ThemePreviewWidget::slotHeaderContextMenuRequested(const QPoint &pos)
1129{
1130 if (mReadOnly) {
1131 return;
1132 }
1133
1134 QTreeWidgetItem *hitem = headerItem();
1135 if (!hitem) {
1136 return; // ooops
1137 }
1138
1139 int col = header()->logicalIndexAt(pos);
1140
1141 if (col < 0) {
1142 return;
1143 }
1144
1145 if (col >= mTheme->columns().count()) {
1146 return;
1147 }
1148
1149 mSelectedThemeColumn = mTheme->column(col);
1150 if (!mSelectedThemeColumn) {
1151 return;
1152 }
1153
1154 QMenu menu;
1155
1156 menu.setTitle(mSelectedThemeColumn->label());
1157
1158 QAction *act = menu.addAction(i18n("Column Properties..."));
1159 connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotColumnProperties);
1160
1161 act = menu.addAction(i18n("Add Column..."));
1162 connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotAddColumn);
1163
1164 act = menu.addAction(i18n("Delete Column"));
1165 connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotDeleteColumn);
1166 act->setEnabled(col > 0);
1167
1168 menu.addSeparator();
1169
1170 act = menu.addAction(i18n("Move Column to Left"));
1171 connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotMoveColumnToLeft);
1172 act->setEnabled(col > 0);
1173
1174 act = menu.addAction(i18n("Move Column to Right"));
1175 connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotMoveColumnToRight);
1176 act->setEnabled(col < mTheme->columns().count() - 1);
1177
1178 menu.exec(header()->mapToGlobal(pos));
1179}
1180
1181void ThemePreviewWidget::slotMoveColumnToLeft()
1182{
1183 if (!mSelectedThemeColumn) {
1184 return;
1185 }
1186
1187 const int columnIndex = mTheme->columns().indexOf(mSelectedThemeColumn);
1188 mTheme->moveColumn(columnIndex, columnIndex - 1);
1189 setTheme(mTheme); // this will reset theme cache and trigger a global update
1190}
1191
1192void ThemePreviewWidget::slotMoveColumnToRight()
1193{
1194 if (!mSelectedThemeColumn) {
1195 return;
1196 }
1197
1198 const int columnIndex = mTheme->columns().indexOf(mSelectedThemeColumn);
1199 mTheme->moveColumn(columnIndex, columnIndex + 1);
1200 setTheme(mTheme); // this will reset theme cache and trigger a global update
1201}
1202
1203void ThemePreviewWidget::slotAddColumn()
1204{
1205 int newColumnIndex = mTheme->columns().count();
1206
1207 if (mSelectedThemeColumn) {
1208 newColumnIndex = mTheme->columns().indexOf(mSelectedThemeColumn);
1209 if (newColumnIndex < 0) {
1210 newColumnIndex = mTheme->columns().count();
1211 } else {
1212 newColumnIndex++;
1213 }
1214 }
1215
1216 mSelectedThemeColumn = new Theme::Column();
1217 mSelectedThemeColumn->setLabel(i18n("New Column"));
1218 mSelectedThemeColumn->setVisibleByDefault(true);
1219
1220 mSelectedThemeColumn->addMessageRow(new Theme::Row());
1221 mSelectedThemeColumn->addGroupHeaderRow(new Theme::Row());
1222
1223 auto dlg = new ThemeColumnPropertiesDialog(this, mSelectedThemeColumn, i18n("Add New Column"));
1224
1225 if (dlg->exec() == QDialog::Accepted) {
1226 mTheme->insertColumn(newColumnIndex, mSelectedThemeColumn);
1227
1228 mSelectedThemeContentItem = nullptr;
1229 mThemeSelectedContentItemRect = QRect();
1230 mDropIndicatorPoint1 = mDropIndicatorPoint2;
1231
1232 setTheme(mTheme); // this will reset theme cache and trigger a global update
1233 } else {
1234 delete mSelectedThemeColumn;
1235 mSelectedThemeColumn = nullptr;
1236 }
1237
1238 delete dlg;
1239}
1240
1241void ThemePreviewWidget::slotColumnProperties()
1242{
1243 if (!mSelectedThemeColumn) {
1244 return;
1245 }
1246
1247 auto dlg = new ThemeColumnPropertiesDialog(this, mSelectedThemeColumn, i18n("Column Properties"));
1248
1249 if (dlg->exec() == QDialog::Accepted) {
1250 mSelectedThemeContentItem = nullptr;
1251 mThemeSelectedContentItemRect = QRect();
1252 mDropIndicatorPoint1 = mDropIndicatorPoint2;
1253
1254 setTheme(mTheme); // this will reset theme cache and trigger a global update
1255 }
1256
1257 delete dlg;
1258}
1259
1260void ThemePreviewWidget::slotDeleteColumn()
1261{
1262 if (!mSelectedThemeColumn) {
1263 return;
1264 }
1265
1266 const int idx = mTheme->columns().indexOf(mSelectedThemeColumn);
1267 if (idx < 1) { // first column can't be deleted
1268 return;
1269 }
1270
1271 mTheme->removeColumn(mSelectedThemeColumn);
1272 delete mSelectedThemeColumn;
1273 mSelectedThemeColumn = nullptr;
1274
1275 mSelectedThemeContentItem = nullptr;
1276 mThemeSelectedContentItemRect = QRect();
1277 mDropIndicatorPoint1 = mDropIndicatorPoint2;
1278
1279 setTheme(mTheme); // this will reset theme cache and trigger a global update
1280}
1281
1282ThemeEditor::ThemeEditor(QWidget *parent)
1283 : OptionSetEditor(parent)
1284{
1285 mCurrentTheme = nullptr;
1286
1287 // Appearance tab
1288 auto tab = new QWidget(this);
1289 addTab(tab, i18n("Appearance"));
1290
1291 auto tabg = new QGridLayout(tab);
1292
1293 auto gb = new QGroupBox(i18n("Content Items"), tab);
1294 tabg->addWidget(gb, 0, 0);
1295
1296 auto gblayout = new QGridLayout(gb);
1297
1298 Theme dummyTheme;
1299
1300 auto cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Subject);
1301 cil->setText(Theme::ContentItem::description(cil->type()));
1302 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1303 gblayout->addWidget(cil, 0, 0);
1304
1305 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Date);
1306 cil->setText(Theme::ContentItem::description(cil->type()));
1307 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1308 gblayout->addWidget(cil, 1, 0);
1309
1310 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Size);
1311 cil->setText(Theme::ContentItem::description(cil->type()));
1312 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1313 gblayout->addWidget(cil, 2, 0);
1314
1315 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Sender);
1316 cil->setText(Theme::ContentItem::description(cil->type()));
1317 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1318 gblayout->addWidget(cil, 0, 1);
1319
1320 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Receiver);
1321 cil->setText(Theme::ContentItem::description(cil->type()));
1322 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1323 gblayout->addWidget(cil, 1, 1);
1324
1325 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SenderOrReceiver);
1326 cil->setText(Theme::ContentItem::description(cil->type()));
1327 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1328 gblayout->addWidget(cil, 2, 1);
1329
1330 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::MostRecentDate);
1331 cil->setText(Theme::ContentItem::description(cil->type()));
1332 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1333 gblayout->addWidget(cil, 0, 2);
1334
1335 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::TagList);
1336 cil->setText(Theme::ContentItem::description(cil->type()));
1337 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1338 gblayout->addWidget(cil, 1, 2);
1339
1340 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Folder);
1341 cil->setText(Theme::ContentItem::description(cil->type()));
1342 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1343 gblayout->addWidget(cil, 2, 2);
1344
1345 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::CombinedReadRepliedStateIcon);
1346 cil->setPixmap(*dummyTheme.pixmap(Theme::IconRepliedAndForwarded));
1347 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1348 gblayout->addWidget(cil, 0, 3);
1349
1350 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ReadStateIcon);
1351 cil->setPixmap(*dummyTheme.pixmap(Theme::IconNew));
1352 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1353 gblayout->addWidget(cil, 1, 3);
1354
1355 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::RepliedStateIcon);
1356 cil->setPixmap(*dummyTheme.pixmap(Theme::IconReplied));
1357 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1358 gblayout->addWidget(cil, 2, 3);
1359
1360 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::AttachmentStateIcon);
1361 cil->setPixmap(*dummyTheme.pixmap(Theme::IconAttachment));
1362 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1363 gblayout->addWidget(cil, 0, 4);
1364
1365 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::EncryptionStateIcon);
1366 cil->setPixmap(*dummyTheme.pixmap(Theme::IconFullyEncrypted));
1367 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1368 gblayout->addWidget(cil, 1, 4);
1369
1370 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SignatureStateIcon);
1371 cil->setPixmap(*dummyTheme.pixmap(Theme::IconFullySigned));
1372 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1373 gblayout->addWidget(cil, 2, 4);
1374
1375 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ActionItemStateIcon);
1376 cil->setPixmap(*dummyTheme.pixmap(Theme::IconActionItem));
1377 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1378 gblayout->addWidget(cil, 0, 5);
1379
1380 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::InvitationIcon);
1381 cil->setPixmap(*dummyTheme.pixmap(Theme::IconInvitation));
1382 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1383 gblayout->addWidget(cil, 2, 5);
1384
1385 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ImportantStateIcon);
1386 cil->setPixmap(*dummyTheme.pixmap(Theme::IconImportant));
1387 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1388 gblayout->addWidget(cil, 0, 6);
1389
1390 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SpamHamStateIcon);
1391 cil->setPixmap(*dummyTheme.pixmap(Theme::IconSpam));
1392 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1393 gblayout->addWidget(cil, 1, 6);
1394
1395 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::WatchedIgnoredStateIcon);
1396 cil->setPixmap(*dummyTheme.pixmap(Theme::IconWatched));
1397 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1398 gblayout->addWidget(cil, 2, 6);
1399
1400 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ExpandedStateIcon);
1401 cil->setPixmap(*dummyTheme.pixmap(Theme::IconShowMore));
1402 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1403 gblayout->addWidget(cil, 0, 7);
1404
1405 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::VerticalLine);
1406 cil->setPixmap(*dummyTheme.pixmap(Theme::IconVerticalLine));
1407 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1408 gblayout->addWidget(cil, 1, 7);
1409
1410 cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::HorizontalSpacer);
1411 cil->setPixmap(*dummyTheme.pixmap(Theme::IconHorizontalSpacer));
1412 cil->setToolTip(Theme::ContentItem::description(cil->type()));
1413 gblayout->addWidget(cil, 2, 7);
1414
1415 mPreviewWidget = new ThemePreviewWidget(tab);
1416 tabg->addWidget(mPreviewWidget, 1, 0);
1417
1418 auto l = new QLabel(tab);
1419 l->setText(
1420 i18n("Right click on the header to add or modify columns. Drag the content items and drop them on the columns in order to compose your theme. Right "
1421 "click on the items inside the view for more options."));
1422 l->setWordWrap(true);
1423 l->setAlignment(Qt::AlignCenter);
1424 tabg->addWidget(l, 2, 0);
1425
1426 tabg->setRowStretch(1, 1);
1427
1428 // Advanced tab
1429 tab = new QWidget(this);
1430 addTab(tab, i18nc("@title:tab Advanced theme settings", "Advanced"));
1431
1432 tabg = new QGridLayout(tab);
1433
1434 l = new QLabel(i18nc("@label:textbox", "Header:"), tab);
1435 tabg->addWidget(l, 0, 0);
1436
1437 mViewHeaderPolicyCombo = new QComboBox(tab);
1438 tabg->addWidget(mViewHeaderPolicyCombo, 0, 1);
1439
1440 l = new QLabel(i18nc("@label:textbox", "Icon size:"), tab);
1441 tabg->addWidget(l, 1, 0);
1442
1443 mIconSizeSpinBox = new QSpinBox(tab);
1444 mIconSizeSpinBox->setMinimum(8);
1445 mIconSizeSpinBox->setMaximum(64);
1446 KLocalization::setupSpinBoxFormatString(mIconSizeSpinBox, ki18ncp("suffix in a spinbox", "%v pixel", "%v pixels"));
1447
1448 QObject::connect(mIconSizeSpinBox, &QSpinBox::valueChanged, this, &ThemeEditor::slotIconSizeSpinBoxValueChanged);
1449
1450 tabg->addWidget(mIconSizeSpinBox, 1, 1);
1451
1452 tabg->setColumnStretch(1, 1);
1453 tabg->setRowStretch(2, 1);
1454 fillViewHeaderPolicyCombo();
1455}
1456
1457ThemeEditor::~ThemeEditor() = default;
1458
1459void ThemeEditor::editTheme(Theme *set)
1460{
1461 mCurrentTheme = set;
1462 mPreviewWidget->setTheme(mCurrentTheme);
1463
1464 if (!mCurrentTheme) {
1465 setEnabled(false);
1466 return;
1467 }
1468 setEnabled(true);
1469
1470 nameEdit()->setText(set->name());
1472
1473 ComboBoxUtils::setIntegerOptionComboValue(mViewHeaderPolicyCombo, (int)mCurrentTheme->viewHeaderPolicy());
1474
1475 mIconSizeSpinBox->setValue(set->iconSize());
1476 setReadOnly(mCurrentTheme->readOnly());
1477}
1478
1479void ThemeEditor::setReadOnly(bool readOnly)
1480{
1481 mPreviewWidget->setReadOnly(readOnly);
1482 mViewHeaderPolicyCombo->setEnabled(!readOnly);
1483 mIconSizeSpinBox->setEnabled(!readOnly);
1484 OptionSetEditor::setReadOnly(readOnly);
1485}
1486
1487void ThemeEditor::commit()
1488{
1489 if (!mCurrentTheme || mCurrentTheme->readOnly()) {
1490 return;
1491 }
1492
1493 mCurrentTheme->setName(nameEdit()->text());
1494 mCurrentTheme->setDescription(descriptionEdit()->toPlainText());
1495
1496 mCurrentTheme->setViewHeaderPolicy((Theme::ViewHeaderPolicy)ComboBoxUtils::getIntegerOptionComboValue(mViewHeaderPolicyCombo, 0));
1497 mCurrentTheme->setIconSize(mIconSizeSpinBox->value());
1498 // other settings are already committed to this theme
1499}
1500
1501void ThemeEditor::fillViewHeaderPolicyCombo()
1502{
1504}
1505
1506void ThemeEditor::slotNameEditTextEdited(const QString &newName)
1507{
1508 if (!mCurrentTheme) {
1509 return;
1510 }
1511 mCurrentTheme->setName(newName);
1512 Q_EMIT themeNameChanged();
1513}
1514
1515void ThemeEditor::slotIconSizeSpinBoxValueChanged(int val)
1516{
1517 if (!mCurrentTheme) {
1518 return;
1519 }
1520 mCurrentTheme->setIconSize(val);
1521
1522 mPreviewWidget->setTheme(mCurrentTheme); // will trigger a cache reset and a view update
1523}
1524
1525MessageList::Core::Theme *ThemeEditor::editedTheme() const
1526{
1527 return mCurrentTheme;
1528}
1529
1530#include "moc_themeeditor.cpp"
constexpr bool isEmpty() const
@ PerfectReferencesAndSubject
Thread by all of the above and try to match subjects too.
Definition aggregation.h:69
A message item that can have a fake tag list and a fake annotation.
This class is an optimizing helper for dealing with large flat QAbstractItemModel objects.
const QString & description() const
Returns a description of this option set.
Definition optionset.h:79
const QString & name() const
Returns the name of this OptionSet.
Definition optionset.h:59
MessageSorting
The available message sorting options.
Definition sortorder.h:60
@ NoMessageSorting
Don't sort the messages at all.
Definition sortorder.h:61
static QList< QPair< QString, int > > enumerateMessageSortingOptions(Aggregation::Threading t)
Enumerates the message sorting options compatible with the specified Threading setting.
Definition sortorder.cpp:60
The Column class defines a view column available inside this theme.
Definition theme.h:501
static QString description(Type type)
Returns a descriptive name for the specified content item type.
Definition theme.cpp:102
static bool applicableToMessageItems(Type type)
Static test that returns true if an instance of ContentItem with the specified type makes sense in a ...
Definition theme.cpp:275
@ HideWhenDisabled
In disabled state the icon should take no space (overrides SoftenByBlendingWhenDisabled)
Definition theme.h:211
@ SoftenByBlendingWhenDisabled
In disabled state the icon should be still shown, but made very soft by alpha blending.
Definition theme.h:212
@ UseCustomColor
For text and vertical line. If set then always use a custom color, otherwise use default text color.
Definition theme.h:213
@ IsItalic
Fot text items. If set then always show as italic, otherwise use the default font style.
Definition theme.h:215
@ IsBold
For text items. If set then always show as bold, otherwise use the default font weight.
Definition theme.h:214
Type
The available ContentItem types.
Definition theme.h:106
@ InvitationIcon
Whether the message is an invitation.
Definition theme.h:198
@ CombinedReadRepliedStateIcon
The combined icon that displays the unread/read/replied/forwarded state (never disabled)
Definition theme.h:190
@ SignatureStateIcon
The Signature state icon for messages.
Definition theme.h:174
@ Date
Formatted date time of the message/group.
Definition theme.h:114
@ MostRecentDate
The date of the most recent message in subtree.
Definition theme.h:186
@ ReadStateIcon
The icon that displays the unread/read state (never disabled)
Definition theme.h:134
@ RepliedStateIcon
The icon that displays the replied/forwarded state (may be disabled)
Definition theme.h:142
@ WatchedIgnoredStateIcon
The Watched/Ignored state icon.
Definition theme.h:162
@ ImportantStateIcon
The Important tag icon.
Definition theme.h:154
@ Folder
Folder of the message.
Definition theme.h:202
@ AttachmentStateIcon
The icon that displays the attachment state (may be disabled)
Definition theme.h:138
@ ExpandedStateIcon
The Expanded state icon for group headers.
Definition theme.h:166
@ SenderOrReceiver
From: or To: strip, depending on the folder settings.
Definition theme.h:118
@ Subject
Display the subject of the message item.
Definition theme.h:110
@ VerticalLine
A vertical separation line.
Definition theme.h:178
@ HorizontalSpacer
A small empty spacer usable as separator.
Definition theme.h:182
@ EncryptionStateIcon
The Encryption state icon for messages.
Definition theme.h:170
@ TagList
The list of MessageItem::Tag entries.
Definition theme.h:194
@ Size
Formatted size of the message.
Definition theme.h:130
@ Receiver
To: strip, always.
Definition theme.h:126
@ ActionItemStateIcon
The ActionItem state icon.
Definition theme.h:150
@ Sender
From: strip, always.
Definition theme.h:122
@ SpamHamStateIcon
The Spam/Ham state icon.
Definition theme.h:158
static bool applicableToGroupHeaderItems(Type type)
Static test that returns true if an instance of ContentItem with the specified type makes sense in a ...
Definition theme.cpp:280
void addRightItem(ContentItem *item)
Adds a right aligned item to this row.
Definition theme.cpp:379
void insertRightItem(int idx, ContentItem *item)
Adds a right aligned item at the specified position in this row.
Definition theme.cpp:403
void insertLeftItem(int idx, ContentItem *item)
Adds a left aligned item at the specified position in this row.
Definition theme.cpp:384
void addLeftItem(ContentItem *item)
Adds a left aligned item to this row.
Definition theme.cpp:367
const QList< ContentItem * > & rightItems() const
Returns the list of right aligned items for this row.
Definition theme.cpp:398
const QList< ContentItem * > & leftItems() const
Returns the list of left aligned items for this row.
Definition theme.cpp:489
The Theme class defines the visual appearance of the MessageList.
Definition theme.h:48
GroupHeaderBackgroundMode
Which color do we use to paint group header background ?
Definition theme.h:793
@ Transparent
No background at all: use style default.
Definition theme.h:794
@ CustomColor
Use a custom color.
Definition theme.h:796
@ AutoColor
Automatically determine the color (somewhere in the middle between background and text)
Definition theme.h:795
ViewHeaderPolicy
How do we manage the QHeaderView attached to our View ?
Definition theme.h:816
static QList< QPair< QString, int > > enumerateGroupHeaderBackgroundStyles()
Enumerates the available group header background styles.
Definition theme.cpp:1032
static QList< QPair< QString, int > > enumerateViewHeaderPolicyOptions()
Enumerates the available view header policy options.
Definition theme.cpp:1027
int iconSize() const
Returns the currently set icon size.
Definition theme.cpp:1054
GroupHeaderBackgroundStyle
How do we paint group header background ?
Definition theme.h:802
The base class for the OptionSet editors.
QLineEdit * nameEdit() const
Returns the editor for the name of the OptionSet.
KTextEdit * descriptionEdit() const
Returns the editor for the description of the OptionSet.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
KLocalizedString KI18N_EXPORT ki18ncp(const char *context, const char *singular, const char *plural)
QString i18n(const char *text, const TYPE &arg...)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
void setupSpinBoxFormatString(T *spinBox, const KLocalizedString &formatString)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QString label(StandardShortcut id)
The implementation independent part of the MessageList library.
Definition aggregation.h:22
void fillIntegerOptionCombo(QComboBox *combo, const QList< QPair< QString, int > > &optionDescriptors)
Fills the specified QComboBox with the options available in optionDescriptors.
void setIntegerOptionComboValue(QComboBox *combo, int value)
Sets the currently selected option in the specified combo.
int getIntegerOptionComboValue(QComboBox *combo, int defaultValue)
Returns the identifier of the currently selected option in the specified combo.
void clicked(bool checked)
void setShortcut(const QKeySequence &key)
virtual void mouseMoveEvent(QMouseEvent *event) override
virtual void mousePressEvent(QMouseEvent *event) override
virtual void paintEvent(QPaintEvent *event) override
QWidget * viewport() const const
void setCheckable(bool)
void setChecked(bool)
QVariant data() const const
void setEnabled(bool)
void setData(const QVariant &data)
void setText(const QString &text)
void triggered(bool checked)
char * data()
void resize(qsizetype newSize, char c)
qsizetype size() const const
bool isValid() const const
QColor getColor(const QColor &initial, QWidget *parent, const QString &title, ColorDialogOptions options)
virtual void accept()
virtual void reject()
void acceptProposedAction()
const QMimeData * mimeData() const const
QPointF position() const const
void ignore()
virtual void changeEvent(QEvent *ev) override
int logicalIndexAt(const QPoint &pos) const const
void resizeSection(int logicalIndex, int size)
QIcon fromTheme(const QString &name)
void setText(const QString &)
typedef ConstIterator
void append(QList< T > &&value)
iterator begin()
const_iterator constBegin() const const
const_iterator constEnd() const const
qsizetype count() const const
iterator end()
bool isEmpty() const const
void reserve(qsizetype size)
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addMenu(QMenu *menu)
QAction * addSection(const QIcon &icon, const QString &text)
QAction * addSeparator()
QAction * exec()
bool isEmpty() const const
void setTitle(const QString &title)
void triggered(QAction *action)
QByteArray data(const QString &mimeType) const const
virtual bool hasFormat(const QString &mimeType) const const
bool isValid() const const
QModelIndex parent() const const
QPoint pos() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual bool event(QEvent *e)
int manhattanLength() const const
QPoint toPoint() const const
void setDefault(bool)
int bottom() const const
QPoint bottomLeft() const const
QPoint bottomRight() const const
bool contains(const QPoint &point, bool proper) const const
int left() const const
int right() const const
int top() const const
QPoint topLeft() const const
QPoint topRight() const const
int width() const const
Qt::MouseButton button() const const
Qt::MouseButtons buttons() const const
void valueChanged(int i)
QString arg(Args &&... args) const const
AlignCenter
CustomContextMenu
CopyAction
ItemIsEnabled
Key_Return
LeftButton
void setPlainText(const QString &text)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QHeaderView * header() const const
void setColumnCount(int columns)
QTreeWidgetItem * headerItem() const const
void setHeaderLabels(const QStringList &labels)
int toInt(bool *ok) const const
void customContextMenuRequested(const QPoint &pos)
void setEnabled(bool)
void hide()
QPoint mapToGlobal(const QPoint &pos) const const
void show()
virtual void showEvent(QShowEvent *event)
void update()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:47:09 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.