File indexing completed on 2024-12-15 04:54:45

0001 /******************************************************************************
0002  *
0003  *  SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  *
0007  *******************************************************************************/
0008 
0009 #include "utils/themeeditor.h"
0010 #include "core/groupheaderitem.h"
0011 #include "core/messageitem.h"
0012 #include "core/modelinvariantrowmapper.h"
0013 #include "utils/comboboxutils.h"
0014 
0015 #include <Akonadi/MessageStatus>
0016 
0017 #include <KTextEdit>
0018 
0019 #include <QActionGroup>
0020 #include <QCheckBox>
0021 #include <QCursor>
0022 #include <QDrag>
0023 #include <QGridLayout>
0024 #include <QGroupBox>
0025 #include <QHeaderView>
0026 #include <QMimeData>
0027 #include <QMouseEvent>
0028 #include <QPaintEvent>
0029 #include <QPainter>
0030 #include <QPixmap>
0031 #include <QPushButton>
0032 #include <QStringList>
0033 
0034 #include <KIconLoader>
0035 #include <KLocalizedString>
0036 #include <KPluralHandlingSpinBox>
0037 #include <QColorDialog>
0038 #include <QComboBox>
0039 #include <QLineEdit>
0040 #include <QMenu>
0041 
0042 #include <QDialogButtonBox>
0043 #include <QVBoxLayout>
0044 #include <ctime> // for time_t
0045 
0046 using namespace MessageList::Utils;
0047 using namespace MessageList::Core;
0048 
0049 static const char gThemeContentItemTypeDndMimeDataFormat[] = "application/x-kmail-messagelistview-theme-contentitem-type";
0050 
0051 ThemeColumnPropertiesDialog::ThemeColumnPropertiesDialog(QWidget *parent, Theme::Column *column, const QString &title)
0052     : QDialog(parent)
0053     , mColumn(column)
0054 {
0055     auto mainLayout = new QVBoxLayout(this);
0056     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
0057     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
0058     okButton->setDefault(true);
0059     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0060     connect(buttonBox, &QDialogButtonBox::rejected, this, &ThemeColumnPropertiesDialog::reject);
0061     setWindowTitle(title);
0062 
0063     auto base = new QWidget(this);
0064     mainLayout->addWidget(base);
0065     mainLayout->addWidget(buttonBox);
0066 
0067     auto g = new QGridLayout(base);
0068 
0069     auto l = new QLabel(i18nc("@label:textbox Property name", "Name:"), base);
0070     g->addWidget(l, 0, 0);
0071 
0072     mNameEdit = new QLineEdit(base);
0073     mNameEdit->setToolTip(i18n("The label that will be displayed in the column header."));
0074     g->addWidget(mNameEdit, 0, 1);
0075 
0076     l = new QLabel(i18n("Header click sorts messages:"), base);
0077     g->addWidget(l, 1, 0);
0078 
0079     mMessageSortingCombo = new QComboBox(base);
0080     mMessageSortingCombo->setToolTip(i18n("The sorting order that clicking on this column header will switch to."));
0081     g->addWidget(mMessageSortingCombo, 1, 1);
0082 
0083     mVisibleByDefaultCheck = new QCheckBox(i18n("Visible by default"), base);
0084     mVisibleByDefaultCheck->setToolTip(i18n("Check this if this column should be visible when the theme is selected."));
0085     g->addWidget(mVisibleByDefaultCheck, 2, 1);
0086 
0087     mIsSenderOrReceiverCheck = new QCheckBox(i18n("Contains \"Sender or Receiver\" field"), base);
0088     mIsSenderOrReceiverCheck->setToolTip(i18n("Check this if this column label should be updated depending on the folder \"inbound\"/\"outbound\" type."));
0089     g->addWidget(mIsSenderOrReceiverCheck, 3, 1);
0090 
0091     g->setColumnStretch(1, 1);
0092     g->setRowStretch(10, 1);
0093 
0094     connect(okButton, &QPushButton::clicked, this, &ThemeColumnPropertiesDialog::slotOkButtonClicked);
0095 
0096     // Display the current settings
0097     mNameEdit->setText(mColumn->label());
0098     mVisibleByDefaultCheck->setChecked(mColumn->visibleByDefault());
0099     mIsSenderOrReceiverCheck->setChecked(mColumn->isSenderOrReceiver());
0100     ComboBoxUtils::fillIntegerOptionCombo(mMessageSortingCombo, SortOrder::enumerateMessageSortingOptions(Aggregation::PerfectReferencesAndSubject));
0101     ComboBoxUtils::setIntegerOptionComboValue(mMessageSortingCombo, mColumn->messageSorting());
0102 }
0103 
0104 void ThemeColumnPropertiesDialog::slotOkButtonClicked()
0105 {
0106     QString text = mNameEdit->text();
0107     if (text.isEmpty()) {
0108         text = i18n("Unnamed Column");
0109     }
0110     mColumn->setLabel(text);
0111     mColumn->setVisibleByDefault(mVisibleByDefaultCheck->isChecked());
0112     mColumn->setIsSenderOrReceiver(mIsSenderOrReceiverCheck->isChecked());
0113     mColumn->setMessageSorting(
0114         static_cast<SortOrder::MessageSorting>(ComboBoxUtils::getIntegerOptionComboValue(mMessageSortingCombo, SortOrder::NoMessageSorting)));
0115 
0116     accept();
0117 }
0118 
0119 ThemeContentItemSourceLabel::ThemeContentItemSourceLabel(QWidget *parent, Theme::ContentItem::Type type)
0120     : QLabel(parent)
0121     , mType(type)
0122 {
0123     setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
0124 }
0125 
0126 ThemeContentItemSourceLabel::~ThemeContentItemSourceLabel() = default;
0127 
0128 MessageList::Core::Theme::ContentItem::Type ThemeContentItemSourceLabel::type() const
0129 {
0130     return mType;
0131 }
0132 
0133 void ThemeContentItemSourceLabel::mousePressEvent(QMouseEvent *e)
0134 {
0135     if (e->button() == Qt::LeftButton) {
0136         mMousePressPoint = e->pos();
0137     }
0138 }
0139 
0140 void ThemeContentItemSourceLabel::mouseMoveEvent(QMouseEvent *e)
0141 {
0142     if (e->buttons() & Qt::LeftButton) {
0143         const QPoint diff = mMousePressPoint - e->pos();
0144         if (diff.manhattanLength() > 4) {
0145             startDrag();
0146         }
0147     }
0148 }
0149 
0150 void ThemeContentItemSourceLabel::startDrag()
0151 {
0152     // QPixmap pix = QPixmap::grabWidget( this );
0153     // QPixmap alpha( pix.width(), pix.height() );
0154     // alpha.fill(0x0f0f0f0f);
0155     // pix.setAlphaChannel( alpha ); // <-- this crashes... no alpha for dragged pixmap :(
0156     auto data = new QMimeData();
0157     QByteArray arry;
0158     arry.resize(sizeof(Theme::ContentItem::Type));
0159     *((Theme::ContentItem::Type *)arry.data()) = mType;
0160     data->setData(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat), arry);
0161     auto drag = new QDrag(this);
0162     drag->setMimeData(data);
0163     // drag->setPixmap( pix );
0164     // drag->setHotSpot( mapFromGlobal( QCursor::pos() ) );
0165     drag->exec(Qt::CopyAction, Qt::CopyAction);
0166 }
0167 
0168 ThemePreviewDelegate::ThemePreviewDelegate(QAbstractItemView *parent)
0169     : ThemeDelegate(parent)
0170 {
0171     mRowMapper = new ModelInvariantRowMapper();
0172 
0173     mSampleGroupHeaderItem = new GroupHeaderItem(i18n("Message Group"));
0174 
0175     mSampleGroupHeaderItem->setDate(time(nullptr));
0176     mSampleGroupHeaderItem->setMaxDate(time(nullptr) + 31337);
0177     mSampleGroupHeaderItem->setSubject(i18n("Very long subject very long subject very long subject very long subject very long subject very long"));
0178 
0179     mSampleMessageItem = new FakeItem();
0180 
0181     mSampleMessageItem->setDate(time(nullptr));
0182     mSampleMessageItem->setSize(0x31337);
0183     mSampleMessageItem->setMaxDate(time(nullptr) + 31337);
0184     mSampleMessageItem->setSender(i18n("Sender"));
0185     mSampleMessageItem->setReceiver(i18n("Receiver"));
0186     mSampleMessageItem->setSubject(i18n("Very long subject very long subject very long subject very long subject very long subject very long"));
0187     mSampleMessageItem->setFolder(i18n("Folder"));
0188     mSampleMessageItem->setSignatureState(MessageItem::FullySigned);
0189     mSampleMessageItem->setEncryptionState(MessageItem::FullyEncrypted);
0190 
0191     QList<MessageItem::Tag *> list;
0192     list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 1"), QString()));
0193     list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 2"), QString()));
0194     list.append(new MessageItem::Tag(QIcon::fromTheme(QStringLiteral("feed-subscribe")).pixmap(KIconLoader::SizeSmall), i18n("Sample Tag 3"), QString()));
0195     mSampleMessageItem->setFakeTags(list);
0196 
0197     mRowMapper->createModelInvariantIndex(0, mSampleMessageItem);
0198 
0199     mSampleGroupHeaderItem->rawAppendChildItem(mSampleMessageItem);
0200     mSampleMessageItem->setParent(mSampleGroupHeaderItem);
0201 
0202     Akonadi::MessageStatus stat;
0203 
0204     stat.fromQInt32(0x7fffffff);
0205     stat.setQueued(false);
0206     stat.setSent(false);
0207     stat.setSpam(true);
0208     stat.setWatched(true);
0209     stat.setHasInvitation();
0210     // stat.setHasAttachment( false );
0211 
0212     mSampleMessageItem->setStatus(stat);
0213 }
0214 
0215 ThemePreviewDelegate::~ThemePreviewDelegate()
0216 {
0217     delete mSampleGroupHeaderItem;
0218     // delete mSampleMessageItem; (deleted by the parent)
0219     delete mRowMapper;
0220 }
0221 
0222 Item *ThemePreviewDelegate::itemFromIndex(const QModelIndex &index) const
0223 {
0224     if (index.parent().isValid()) {
0225         return mSampleMessageItem;
0226     }
0227 
0228     return mSampleGroupHeaderItem;
0229 }
0230 
0231 ThemePreviewWidget::ThemePreviewWidget(QWidget *parent)
0232     : QTreeWidget(parent)
0233     , mTheme(nullptr)
0234 {
0235     mSelectedThemeContentItem = nullptr;
0236     mSelectedThemeColumn = nullptr;
0237     mFirstShow = true;
0238     mReadOnly = false;
0239 
0240     mDelegate = new ThemePreviewDelegate(this);
0241     setItemDelegate(mDelegate);
0242     setRootIsDecorated(false);
0243     viewport()->setAcceptDrops(true);
0244 
0245     header()->setContextMenuPolicy(Qt::CustomContextMenu);
0246     connect(header(), &QWidget::customContextMenuRequested, this, &ThemePreviewWidget::slotHeaderContextMenuRequested);
0247 
0248     mGroupHeaderSampleItem = new QTreeWidgetItem(this);
0249     mGroupHeaderSampleItem->setText(0, QString());
0250     mGroupHeaderSampleItem->setFlags(Qt::ItemIsEnabled);
0251 
0252     auto m = new QTreeWidgetItem(mGroupHeaderSampleItem);
0253     m->setText(0, QString());
0254 
0255     mGroupHeaderSampleItem->setExpanded(true);
0256     header()->setSectionsMovable(false);
0257 }
0258 
0259 void ThemePreviewWidget::changeEvent(QEvent *event)
0260 {
0261     if (event->type() == QEvent::FontChange) {
0262         mDelegate->generalFontChanged();
0263     }
0264     QTreeWidget::changeEvent(event);
0265 }
0266 
0267 ThemePreviewWidget::~ThemePreviewWidget() = default;
0268 
0269 QSize ThemePreviewWidget::sizeHint() const
0270 {
0271     return {350, 180};
0272 }
0273 
0274 void ThemePreviewWidget::setReadOnly(bool readOnly)
0275 {
0276     mReadOnly = readOnly;
0277 }
0278 
0279 void ThemePreviewWidget::applyThemeColumnWidths()
0280 {
0281     if (!mTheme) {
0282         return;
0283     }
0284 
0285     const QList<Theme::Column *> &columns = mTheme->columns();
0286 
0287     if (columns.isEmpty()) {
0288         viewport()->update(); // trigger a repaint
0289         return;
0290     }
0291 
0292     // Now we want to distribute the available width on all the columns.
0293     // The algorithm used here is very similar to the one used in View::applyThemeColumns().
0294     // It just takes care of ALL the columns instead of the visible ones.
0295 
0296     QList<Theme::Column *>::ConstIterator it;
0297 
0298     // Gather size hints for all sections.
0299     int idx = 0;
0300     int totalVisibleWidthHint = 0;
0301     QList<Theme::Column *>::ConstIterator end(columns.constEnd());
0302 
0303     for (it = columns.constBegin(); it != end; ++it) {
0304         totalVisibleWidthHint += mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width();
0305         idx++;
0306     }
0307 
0308     if (totalVisibleWidthHint < 16) {
0309         totalVisibleWidthHint = 16; // be reasonable
0310     }
0311 
0312     // Now we can compute proportional widths.
0313 
0314     idx = 0;
0315 
0316     QList<int> realWidths;
0317     realWidths.reserve(columns.count());
0318     int totalVisibleWidth = 0;
0319 
0320     end = columns.constEnd();
0321     for (it = columns.constBegin(); it != end; ++it) {
0322         int hintWidth = mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width();
0323         int realWidth;
0324         if ((*it)->containsTextItems()) {
0325             // the column contains text items, it should get more space
0326             realWidth = ((hintWidth * viewport()->width()) / totalVisibleWidthHint) - 2; // -2 is heuristic
0327             if (realWidth < (hintWidth + 2)) {
0328                 realWidth = hintWidth + 2; // can't be less
0329             }
0330         } else {
0331             // the column contains no text items, it should get just a little bit more than its sizeHint().
0332             realWidth = hintWidth + 2;
0333         }
0334 
0335         realWidths.append(realWidth);
0336         totalVisibleWidth += realWidth;
0337 
0338         idx++;
0339     }
0340 
0341     idx = 0;
0342 
0343     totalVisibleWidth += 4; // account for some view's border
0344 
0345     if (totalVisibleWidth < viewport()->width()) {
0346         // give the additional space to the text columns
0347         // also give more space to the first ones and less space to the last ones
0348         int available = viewport()->width() - totalVisibleWidth;
0349 
0350         for (it = columns.begin(); it != columns.end(); ++it) {
0351             if (((*it)->visibleByDefault() || (idx == 0)) && (*it)->containsTextItems()) {
0352                 // give more space to this column
0353                 available >>= 1; // eat half of the available space
0354                 realWidths[idx] += available; // and give it to this column
0355             }
0356 
0357             idx++;
0358         }
0359 
0360         // if any space is still available, give it to the first column
0361         if (available) {
0362             realWidths[0] += available;
0363         }
0364     }
0365 
0366     idx = 0;
0367 
0368     // We're ready.
0369     // Assign widths. Hide the sections that are not visible by default, show the other ones.
0370     for (it = columns.begin(); it != columns.end(); ++it) {
0371         header()->resizeSection(idx, realWidths[idx]);
0372         idx++;
0373     }
0374 
0375 #if 0
0376     if (mTheme->viewHeaderPolicy() == Theme::NeverShowHeader) {
0377         header()->hide();
0378     } else {
0379         header()->show();
0380     }
0381 #endif
0382 }
0383 
0384 void ThemePreviewWidget::setTheme(Theme *theme)
0385 {
0386     bool themeChanged = theme != mTheme;
0387 
0388     mSelectedThemeContentItem = nullptr;
0389     mThemeSelectedContentItemRect = QRect();
0390     mDropIndicatorPoint1 = QPoint();
0391     mDropIndicatorPoint2 = QPoint();
0392     mTheme = theme;
0393     mDelegate->setTheme(theme);
0394     if (!mTheme) {
0395         return;
0396     }
0397     mGroupHeaderSampleItem->setExpanded(true);
0398 
0399     const QList<Theme::Column *> &columns = mTheme->columns();
0400 
0401     setColumnCount(columns.count());
0402 
0403     QStringList headerLabels;
0404     headerLabels.reserve(columns.count());
0405     QList<Theme::Column *>::ConstIterator end(columns.constEnd());
0406     for (QList<Theme::Column *>::ConstIterator it = columns.constBegin(); it != end; ++it) {
0407         QString label = (*it)->label();
0408         if ((*it)->visibleByDefault()) {
0409             label += QStringLiteral(" (%1)").arg(i18nc("Indicates whether or not a header label is visible", "Visible"));
0410         }
0411 
0412         headerLabels.append(label);
0413     }
0414 
0415     setHeaderLabels(headerLabels);
0416 
0417     if (themeChanged) {
0418         applyThemeColumnWidths();
0419     }
0420 
0421     viewport()->update(); // trigger a repaint
0422 }
0423 
0424 void ThemePreviewWidget::internalHandleDragEnterEvent(QDragEnterEvent *e)
0425 {
0426     e->ignore();
0427 
0428     if (!e->mimeData()) {
0429         return;
0430     }
0431     if (!e->mimeData()->hasFormat(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat))) {
0432         return;
0433     }
0434 
0435     e->accept();
0436 }
0437 
0438 void ThemePreviewWidget::showEvent(QShowEvent *e)
0439 {
0440     QTreeWidget::showEvent(e);
0441 
0442     if (mFirstShow) {
0443         // Make sure we re-apply column widths the first time we're shown.
0444         // The first "apply" call was made while the widget was still hidden and
0445         // almost surely had wrong sizes.
0446         applyThemeColumnWidths();
0447         mFirstShow = false;
0448     }
0449 }
0450 
0451 void ThemePreviewWidget::dragEnterEvent(QDragEnterEvent *e)
0452 {
0453     internalHandleDragEnterEvent(e);
0454 
0455     mThemeSelectedContentItemRect = QRect();
0456 
0457     viewport()->update(); // trigger a repaint
0458 }
0459 
0460 void ThemePreviewWidget::internalHandleDragMoveEvent(QDragMoveEvent *e)
0461 {
0462     e->ignore();
0463 
0464     if (mReadOnly) {
0465         return;
0466     }
0467 
0468     if (!e->mimeData()) {
0469         return;
0470     }
0471     if (!e->mimeData()->hasFormat(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat))) {
0472         return;
0473     }
0474 
0475     QByteArray arry = e->mimeData()->data(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat));
0476 
0477     if (arry.size() != sizeof(Theme::ContentItem::Type)) {
0478         return; // ugh
0479     }
0480 
0481     Theme::ContentItem::Type type = *((Theme::ContentItem::Type *)arry.data());
0482     if (!computeContentItemInsertPosition(e->position().toPoint(), type)) {
0483         return;
0484     }
0485 
0486     e->accept();
0487 }
0488 
0489 void ThemePreviewWidget::dragMoveEvent(QDragMoveEvent *e)
0490 {
0491     if (mReadOnly) {
0492         return;
0493     }
0494 
0495     internalHandleDragMoveEvent(e);
0496 
0497     mThemeSelectedContentItemRect = QRect();
0498 
0499     viewport()->update(); // trigger a repaint
0500 }
0501 
0502 void ThemePreviewWidget::dropEvent(QDropEvent *e)
0503 {
0504     mDropIndicatorPoint1 = mDropIndicatorPoint2;
0505 
0506     e->ignore();
0507 
0508     if (mReadOnly) {
0509         return;
0510     }
0511 
0512     if (!e->mimeData()) {
0513         return;
0514     }
0515 
0516     if (!e->mimeData()->hasFormat(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat))) {
0517         return;
0518     }
0519 
0520     QByteArray arry = e->mimeData()->data(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat));
0521 
0522     if (arry.size() != sizeof(Theme::ContentItem::Type)) {
0523         return; // ugh
0524     }
0525 
0526     Theme::ContentItem::Type type = *((Theme::ContentItem::Type *)arry.data());
0527     if (!computeContentItemInsertPosition(e->position().toPoint(), type)) {
0528         viewport()->update();
0529         return;
0530     }
0531 
0532     Theme::Row *row = nullptr;
0533 
0534     switch (mRowInsertPosition) {
0535     case AboveRow:
0536         row = new Theme::Row();
0537         if (mDelegate->hitItem()->type() == Item::Message) {
0538             const_cast<Theme::Column *>(mDelegate->hitColumn())->insertMessageRow(mDelegate->hitRowIndex(), row);
0539         } else {
0540             const_cast<Theme::Column *>(mDelegate->hitColumn())->insertGroupHeaderRow(mDelegate->hitRowIndex(), row);
0541         }
0542         break;
0543     case InsideRow:
0544         row = const_cast<Theme::Row *>(mDelegate->hitRow());
0545         break;
0546     case BelowRow:
0547         row = new Theme::Row();
0548         if (mDelegate->hitItem()->type() == Item::Message) {
0549             const_cast<Theme::Column *>(mDelegate->hitColumn())->insertMessageRow(mDelegate->hitRowIndex() + 1, row);
0550         } else {
0551             const_cast<Theme::Column *>(mDelegate->hitColumn())->insertGroupHeaderRow(mDelegate->hitRowIndex() + 1, row);
0552         }
0553         break;
0554     }
0555 
0556     if (!row) {
0557         return;
0558     }
0559 
0560     auto ci = new Theme::ContentItem(type);
0561     if (ci->canBeDisabled()) {
0562         if (ci->isClickable()) {
0563             ci->setSoftenByBlendingWhenDisabled(true); // default to softened
0564         } else {
0565             ci->setHideWhenDisabled(true); // default to hidden
0566         }
0567     }
0568 
0569     int idx;
0570 
0571     switch (mItemInsertPosition) {
0572     case OnLeftOfItem:
0573         if (!mDelegate->hitContentItem()) {
0574             // bleah
0575             delete ci;
0576             return;
0577         }
0578         idx = mDelegate->hitContentItemRight() ? row->rightItems().indexOf(const_cast<Theme::ContentItem *>(mDelegate->hitContentItem()))
0579                                                : row->leftItems().indexOf(const_cast<Theme::ContentItem *>(mDelegate->hitContentItem()));
0580         if (idx < 0) {
0581             // bleah
0582             delete ci;
0583             return;
0584         }
0585         if (mDelegate->hitContentItemRight()) {
0586             row->insertRightItem(idx + 1, ci);
0587         } else {
0588             row->insertLeftItem(idx, ci);
0589         }
0590         break;
0591     case OnRightOfItem:
0592         if (!mDelegate->hitContentItem()) {
0593             // bleah
0594             delete ci;
0595             return;
0596         }
0597         idx = mDelegate->hitContentItemRight() ? row->rightItems().indexOf(const_cast<Theme::ContentItem *>(mDelegate->hitContentItem()))
0598                                                : row->leftItems().indexOf(const_cast<Theme::ContentItem *>(mDelegate->hitContentItem()));
0599         if (idx < 0) {
0600             // bleah
0601             delete ci;
0602             return;
0603         }
0604         if (mDelegate->hitContentItemRight()) {
0605             row->insertRightItem(idx, ci);
0606         } else {
0607             row->insertLeftItem(idx + 1, ci);
0608         }
0609         break;
0610     case AsLastLeftItem:
0611         row->addLeftItem(ci);
0612         break;
0613     case AsLastRightItem:
0614         row->addRightItem(ci);
0615         break;
0616     case AsFirstLeftItem:
0617         row->insertLeftItem(0, ci);
0618         break;
0619     case AsFirstRightItem:
0620         row->insertRightItem(0, ci);
0621         break;
0622     default: // should never happen
0623         row->addRightItem(ci);
0624         break;
0625     }
0626 
0627     e->acceptProposedAction();
0628 
0629     mThemeSelectedContentItemRect = QRect();
0630     mDropIndicatorPoint1 = mDropIndicatorPoint2;
0631     mSelectedThemeContentItem = nullptr;
0632 
0633     setTheme(mTheme); // this will reset theme cache and trigger a global update
0634 }
0635 
0636 bool ThemePreviewWidget::computeContentItemInsertPosition(const QPoint &pos, Theme::ContentItem::Type type)
0637 {
0638     mDropIndicatorPoint1 = mDropIndicatorPoint2; // this marks the position as invalid
0639 
0640     if (!mDelegate->hitTest(pos, false)) {
0641         return false;
0642     }
0643 
0644     if (!mDelegate->hitRow()) {
0645         return false;
0646     }
0647 
0648     if (mDelegate->hitRowIsMessageRow()) {
0649         if (!Theme::ContentItem::applicableToMessageItems(type)) {
0650             return false;
0651         }
0652     } else {
0653         if (!Theme::ContentItem::applicableToGroupHeaderItems(type)) {
0654             return false;
0655         }
0656     }
0657 
0658     QRect rowRect = mDelegate->hitRowRect();
0659 
0660     if (pos.y() < rowRect.top() + 3) {
0661         // above a row
0662         mRowInsertPosition = AboveRow;
0663         if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) {
0664             mDropIndicatorPoint1 = rowRect.topLeft();
0665             mItemInsertPosition = AsLastLeftItem;
0666         } else {
0667             mDropIndicatorPoint1 = rowRect.topRight();
0668             mItemInsertPosition = AsLastRightItem;
0669         }
0670         mDropIndicatorPoint2 = QPoint(rowRect.left() + (rowRect.width() / 2), rowRect.top());
0671         return true;
0672     }
0673 
0674     if (pos.y() > rowRect.bottom() - 3) {
0675         // below a row
0676         mRowInsertPosition = BelowRow;
0677         if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) {
0678             mDropIndicatorPoint1 = rowRect.bottomLeft();
0679             mItemInsertPosition = AsLastLeftItem;
0680         } else {
0681             mDropIndicatorPoint1 = rowRect.bottomRight();
0682             mItemInsertPosition = AsLastRightItem;
0683         }
0684         mDropIndicatorPoint2 = QPoint(rowRect.left() + (rowRect.width() / 2), rowRect.bottom());
0685         return true;
0686     }
0687 
0688     mRowInsertPosition = InsideRow;
0689 
0690     if (!mDelegate->hitContentItem()) {
0691         // didn't hit anything... probably no items in the row
0692         if (pos.x() < (rowRect.left() + (rowRect.width() / 2))) {
0693             mItemInsertPosition = AsLastLeftItem;
0694             mDropIndicatorPoint1 = QPoint(rowRect.left(), rowRect.top());
0695             mDropIndicatorPoint2 = QPoint(rowRect.left(), rowRect.bottom());
0696         } else {
0697             mItemInsertPosition = AsLastRightItem;
0698             mDropIndicatorPoint1 = QPoint(rowRect.right(), rowRect.top());
0699             mDropIndicatorPoint2 = QPoint(rowRect.right(), rowRect.bottom());
0700         }
0701         return true;
0702     }
0703 
0704     // hit something, maybe inexactly
0705     QRect itemRect = mDelegate->hitContentItemRect();
0706 
0707     if (!itemRect.contains(pos)) {
0708         // inexact hit: outside an item
0709         if (pos.x() > itemRect.right()) {
0710             // right side of an item
0711             if (mDelegate->hitRow()->rightItems().count() < 1) {
0712                 // between the last left item and the right side
0713                 if (pos.x() > (itemRect.right() + ((rowRect.right() - itemRect.right()) / 2))) {
0714                     // first/last right item
0715                     mItemInsertPosition = AsFirstRightItem;
0716                     mDropIndicatorPoint1 = rowRect.topRight();
0717                     mDropIndicatorPoint2 = rowRect.bottomRight();
0718                 }
0719                 return true;
0720             }
0721             // either there were some right items (so the theme delegate knows that the reported item is the closest)
0722             // or there were no right items but the position is closest to the left item than the right row end
0723             mItemInsertPosition = OnRightOfItem;
0724             mDropIndicatorPoint1 = itemRect.topRight();
0725             mDropIndicatorPoint2 = itemRect.bottomRight();
0726             return true;
0727         }
0728 
0729         // left side of an item
0730         if (mDelegate->hitRow()->leftItems().count() < 1) {
0731             // between the left side and the leftmost right item
0732             if (pos.x() < (itemRect.left() - ((itemRect.left() - rowRect.left()) / 2))) {
0733                 mItemInsertPosition = AsFirstLeftItem;
0734                 mDropIndicatorPoint1 = rowRect.topLeft();
0735                 mDropIndicatorPoint2 = rowRect.bottomLeft();
0736                 return true;
0737             }
0738         }
0739         mItemInsertPosition = OnLeftOfItem;
0740         mDropIndicatorPoint1 = itemRect.topLeft();
0741         mDropIndicatorPoint2 = itemRect.bottomLeft();
0742         return true;
0743     }
0744 
0745     // exact hit
0746     if (pos.x() < (itemRect.left() + (itemRect.width() / 2))) {
0747         // left side
0748         mItemInsertPosition = OnLeftOfItem;
0749         mDropIndicatorPoint1 = itemRect.topLeft();
0750         mDropIndicatorPoint2 = itemRect.bottomLeft();
0751         return true;
0752     }
0753 
0754     // right side
0755     mItemInsertPosition = OnRightOfItem;
0756     mDropIndicatorPoint1 = itemRect.topRight();
0757     mDropIndicatorPoint2 = itemRect.bottomRight();
0758     return true;
0759 }
0760 
0761 void ThemePreviewWidget::mouseMoveEvent(QMouseEvent *e)
0762 {
0763     if (!(mSelectedThemeContentItem && (e->buttons() & Qt::LeftButton)) || mReadOnly) {
0764         QTreeWidget::mouseMoveEvent(e);
0765         return;
0766     }
0767 
0768     if (mSelectedThemeContentItem != mDelegate->hitContentItem()) {
0769         QTreeWidget::mouseMoveEvent(e);
0770         return; // ugh.. something weird happened
0771     }
0772 
0773     // starting a drag ?
0774     const QPoint diff = e->pos() - mMouseDownPoint;
0775     if (diff.manhattanLength() <= 4) {
0776         QTreeWidget::mouseMoveEvent(e);
0777         return; // ugh.. something weird happened
0778     }
0779 
0780     // starting a drag
0781     auto data = new QMimeData();
0782     QByteArray arry;
0783     arry.resize(sizeof(Theme::ContentItem::Type));
0784     *((Theme::ContentItem::Type *)arry.data()) = mSelectedThemeContentItem->type();
0785     data->setData(QLatin1StringView(gThemeContentItemTypeDndMimeDataFormat), arry);
0786     auto drag = new QDrag(this);
0787     drag->setMimeData(data);
0788 
0789     // remove the Theme::ContentItem from the Theme
0790     if (mDelegate->hitContentItemRight()) {
0791         const_cast<Theme::Row *>(mDelegate->hitRow())->removeRightItem(mSelectedThemeContentItem);
0792     } else {
0793         const_cast<Theme::Row *>(mDelegate->hitRow())->removeLeftItem(mSelectedThemeContentItem);
0794     }
0795 
0796     delete mSelectedThemeContentItem;
0797 
0798     if (mDelegate->hitRow()->rightItems().isEmpty() && mDelegate->hitRow()->leftItems().isEmpty()) {
0799         if (mDelegate->hitItem()->type() == Item::Message) {
0800             if (mDelegate->hitColumn()->messageRows().count() > 1) {
0801                 const_cast<Theme::Column *>(mDelegate->hitColumn())->removeMessageRow(const_cast<Theme::Row *>(mDelegate->hitRow()));
0802                 delete mDelegate->hitRow();
0803             }
0804         } else {
0805             if (mDelegate->hitColumn()->groupHeaderRows().count() > 1) {
0806                 const_cast<Theme::Column *>(mDelegate->hitColumn())->removeGroupHeaderRow(const_cast<Theme::Row *>(mDelegate->hitRow()));
0807                 delete mDelegate->hitRow();
0808             }
0809         }
0810     }
0811 
0812     mSelectedThemeContentItem = nullptr;
0813     mThemeSelectedContentItemRect = QRect();
0814     mDropIndicatorPoint1 = mDropIndicatorPoint2;
0815 
0816     setTheme(mTheme); // this will reset theme cache and trigger a global update
0817 
0818     // and do drag
0819     drag->exec(Qt::CopyAction, Qt::CopyAction);
0820 }
0821 
0822 void ThemePreviewWidget::mousePressEvent(QMouseEvent *e)
0823 {
0824     if (mReadOnly) {
0825         QTreeWidget::mousePressEvent(e);
0826         return;
0827     }
0828 
0829     mMouseDownPoint = e->pos();
0830 
0831     if (mDelegate->hitTest(mMouseDownPoint)) {
0832         mSelectedThemeContentItem = const_cast<Theme::ContentItem *>(mDelegate->hitContentItem());
0833         mThemeSelectedContentItemRect = mSelectedThemeContentItem ? mDelegate->hitContentItemRect() : QRect();
0834     } else {
0835         mSelectedThemeContentItem = nullptr;
0836         mThemeSelectedContentItemRect = QRect();
0837     }
0838 
0839     QTreeWidget::mousePressEvent(e);
0840     viewport()->update();
0841 
0842     if (e->button() == Qt::RightButton) {
0843         QMenu menu;
0844 
0845         if (mSelectedThemeContentItem) {
0846             menu.addSection(Theme::ContentItem::description(mSelectedThemeContentItem->type()));
0847 
0848             if (mSelectedThemeContentItem->displaysText()) {
0849                 QAction *act = menu.addAction(i18nc("@action:inmenu soften the text color", "Soften"));
0850                 act->setCheckable(true);
0851                 act->setChecked(mSelectedThemeContentItem->softenByBlending());
0852                 connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotSoftenActionTriggered);
0853 
0854                 auto childmenu = new QMenu(&menu);
0855 
0856                 act = childmenu->addAction(i18nc("@action:inmenu Font setting", "Bold"));
0857                 act->setData(QVariant(static_cast<int>(Theme::ContentItem::IsBold)));
0858                 act->setCheckable(true);
0859                 act->setChecked(mSelectedThemeContentItem->isBold());
0860                 act = childmenu->addAction(i18nc("@action:inmenu Font setting", "Italic"));
0861                 act->setData(QVariant(static_cast<int>(Theme::ContentItem::IsItalic)));
0862                 act->setCheckable(true);
0863                 act->setChecked(mSelectedThemeContentItem->isItalic());
0864 
0865                 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotFontMenuTriggered);
0866 
0867                 menu.addMenu(childmenu)->setText(i18n("Font"));
0868             }
0869 
0870             if (mSelectedThemeContentItem->canUseCustomColor()) {
0871                 auto childmenu = new QMenu(&menu);
0872 
0873                 auto grp = new QActionGroup(childmenu);
0874 
0875                 QAction *act = childmenu->addAction(i18nc("@action:inmenu Foreground color setting", "Default"));
0876                 act->setData(QVariant(static_cast<int>(0)));
0877                 act->setCheckable(true);
0878                 act->setChecked(!mSelectedThemeContentItem->useCustomColor());
0879                 grp->addAction(act);
0880                 act = childmenu->addAction(i18nc("@action:inmenu Foreground color setting", "Custom..."));
0881                 act->setData(QVariant(static_cast<int>(Theme::ContentItem::UseCustomColor)));
0882                 act->setCheckable(true);
0883                 act->setChecked(mSelectedThemeContentItem->useCustomColor());
0884                 grp->addAction(act);
0885 
0886                 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotForegroundColorMenuTriggered);
0887 
0888                 menu.addMenu(childmenu)->setText(i18n("Foreground Color"));
0889             }
0890 
0891             if (mSelectedThemeContentItem->canBeDisabled()) {
0892                 auto childmenu = new QMenu(&menu);
0893 
0894                 auto grp = new QActionGroup(childmenu);
0895 
0896                 QAction *act =
0897                     childmenu->addAction(i18nc("Hide a mark if the mail does not have the attribute, e.g. Important mark on a non important mail", "Hide"));
0898                 act->setData(QVariant(static_cast<int>(Theme::ContentItem::HideWhenDisabled)));
0899                 act->setCheckable(true);
0900                 act->setChecked(mSelectedThemeContentItem->hideWhenDisabled());
0901                 grp->addAction(act);
0902                 act = childmenu->addAction(
0903                     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",
0904                           "Keep Empty Space"));
0905                 act->setData(QVariant(static_cast<int>(0)));
0906                 act->setCheckable(true);
0907                 act->setChecked(!(mSelectedThemeContentItem->softenByBlendingWhenDisabled() || mSelectedThemeContentItem->hideWhenDisabled()));
0908                 grp->addAction(act);
0909                 act = childmenu->addAction(
0910                     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",
0911                           "Keep Softened Icon"));
0912                 act->setData(QVariant(static_cast<int>(Theme::ContentItem::SoftenByBlendingWhenDisabled)));
0913                 act->setCheckable(true);
0914                 act->setChecked(mSelectedThemeContentItem->softenByBlendingWhenDisabled());
0915                 grp->addAction(act);
0916 
0917                 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotDisabledFlagsMenuTriggered);
0918 
0919                 menu.addMenu(childmenu)->setText(i18n("When Disabled"));
0920             }
0921         }
0922 
0923         if (mDelegate->hitItem()) {
0924             if (mDelegate->hitItem()->type() == Item::GroupHeader) {
0925                 menu.addSection(i18n("Group Header"));
0926 
0927                 // Background color (mode) submenu
0928                 auto childmenu = new QMenu(&menu);
0929 
0930                 auto grp = new QActionGroup(childmenu);
0931 
0932                 QAction *act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "None"));
0933                 act->setData(QVariant(static_cast<int>(Theme::Transparent)));
0934                 act->setCheckable(true);
0935                 act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::Transparent);
0936                 grp->addAction(act);
0937                 act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "Automatic"));
0938                 act->setData(QVariant(static_cast<int>(Theme::AutoColor)));
0939                 act->setCheckable(true);
0940                 act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::AutoColor);
0941                 grp->addAction(act);
0942                 act = childmenu->addAction(i18nc("@action:inmenu Group header background color setting", "Custom..."));
0943                 act->setData(QVariant(static_cast<int>(Theme::CustomColor)));
0944                 act->setCheckable(true);
0945                 act->setChecked(mTheme->groupHeaderBackgroundMode() == Theme::CustomColor);
0946                 grp->addAction(act);
0947 
0948                 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotGroupHeaderBackgroundModeMenuTriggered);
0949 
0950                 menu.addMenu(childmenu)->setText(i18n("Background Color"));
0951 
0952                 // Background style submenu
0953                 childmenu = new QMenu(&menu);
0954 
0955                 grp = new QActionGroup(childmenu);
0956                 QList<QPair<QString, int>> styles = Theme::enumerateGroupHeaderBackgroundStyles();
0957                 QList<QPair<QString, int>>::ConstIterator end(styles.constEnd());
0958 
0959                 for (QList<QPair<QString, int>>::ConstIterator it = styles.constBegin(); it != end; ++it) {
0960                     act = childmenu->addAction((*it).first);
0961                     act->setData(QVariant((*it).second));
0962                     act->setCheckable(true);
0963                     act->setChecked(mTheme->groupHeaderBackgroundStyle() == static_cast<Theme::GroupHeaderBackgroundStyle>((*it).second));
0964                     grp->addAction(act);
0965                 }
0966 
0967                 connect(childmenu, &QMenu::triggered, this, &ThemePreviewWidget::slotGroupHeaderBackgroundStyleMenuTriggered);
0968 
0969                 act = menu.addMenu(childmenu);
0970                 act->setText(i18n("Background Style"));
0971                 if (mTheme->groupHeaderBackgroundMode() == Theme::Transparent) {
0972                     act->setEnabled(false);
0973                 }
0974             }
0975         }
0976 
0977         if (menu.isEmpty()) {
0978             return;
0979         }
0980 
0981         menu.exec(viewport()->mapToGlobal(e->pos()));
0982     }
0983 }
0984 
0985 void ThemePreviewWidget::slotDisabledFlagsMenuTriggered(QAction *act)
0986 {
0987     if (!mSelectedThemeContentItem) {
0988         return;
0989     }
0990 
0991     bool ok;
0992     const int flags = act->data().toInt(&ok);
0993     if (!ok) {
0994         return;
0995     }
0996 
0997     mSelectedThemeContentItem->setHideWhenDisabled(flags == Theme::ContentItem::HideWhenDisabled);
0998     mSelectedThemeContentItem->setSoftenByBlendingWhenDisabled(flags == Theme::ContentItem::SoftenByBlendingWhenDisabled);
0999 
1000     setTheme(mTheme); // this will reset theme cache and trigger a global update
1001 }
1002 
1003 void ThemePreviewWidget::slotForegroundColorMenuTriggered(QAction *act)
1004 {
1005     if (!mSelectedThemeContentItem) {
1006         return;
1007     }
1008 
1009     bool ok;
1010     const int flag = act->data().toInt(&ok);
1011     if (!ok) {
1012         return;
1013     }
1014 
1015     if (flag == 0) {
1016         mSelectedThemeContentItem->setUseCustomColor(false);
1017         setTheme(mTheme); // this will reset theme cache and trigger a global update
1018         return;
1019     }
1020 
1021     QColor clr;
1022     clr = QColorDialog::getColor(mSelectedThemeContentItem->customColor(), this);
1023     if (!clr.isValid()) {
1024         return;
1025     }
1026 
1027     mSelectedThemeContentItem->setCustomColor(clr);
1028     mSelectedThemeContentItem->setUseCustomColor(true);
1029 
1030     setTheme(mTheme); // this will reset theme cache and trigger a global update
1031 }
1032 
1033 void ThemePreviewWidget::slotSoftenActionTriggered(bool)
1034 {
1035     if (!mSelectedThemeContentItem) {
1036         return;
1037     }
1038 
1039     mSelectedThemeContentItem->setSoftenByBlending(!mSelectedThemeContentItem->softenByBlending());
1040     setTheme(mTheme); // this will reset theme cache and trigger a global update
1041 }
1042 
1043 void ThemePreviewWidget::slotFontMenuTriggered(QAction *act)
1044 {
1045     if (!mSelectedThemeContentItem) {
1046         return;
1047     }
1048 
1049     bool ok;
1050     const int flag = act->data().toInt(&ok);
1051     if (!ok) {
1052         return;
1053     }
1054 
1055     if (flag == Theme::ContentItem::IsBold && mSelectedThemeContentItem->isBold() != act->isChecked()) {
1056         mSelectedThemeContentItem->setBold(act->isChecked());
1057         setTheme(mTheme);
1058     } else if (flag == Theme::ContentItem::IsItalic && mSelectedThemeContentItem->isItalic() != act->isChecked()) {
1059         mSelectedThemeContentItem->setItalic(act->isChecked());
1060         setTheme(mTheme);
1061     }
1062 }
1063 
1064 void ThemePreviewWidget::slotGroupHeaderBackgroundModeMenuTriggered(QAction *act)
1065 {
1066     bool ok;
1067     Theme::GroupHeaderBackgroundMode mode = static_cast<Theme::GroupHeaderBackgroundMode>(act->data().toInt(&ok));
1068     if (!ok) {
1069         return;
1070     }
1071 
1072     switch (mode) {
1073     case Theme::Transparent:
1074         mTheme->setGroupHeaderBackgroundMode(Theme::Transparent);
1075         break;
1076     case Theme::AutoColor:
1077         mTheme->setGroupHeaderBackgroundMode(Theme::AutoColor);
1078         break;
1079     case Theme::CustomColor: {
1080         QColor clr;
1081         clr = QColorDialog::getColor(mTheme->groupHeaderBackgroundColor(), this);
1082         if (!clr.isValid()) {
1083             return;
1084         }
1085 
1086         mTheme->setGroupHeaderBackgroundMode(Theme::CustomColor);
1087         mTheme->setGroupHeaderBackgroundColor(clr);
1088         break;
1089     }
1090     }
1091 
1092     setTheme(mTheme); // this will reset theme cache and trigger a global update
1093 }
1094 
1095 void ThemePreviewWidget::slotGroupHeaderBackgroundStyleMenuTriggered(QAction *act)
1096 {
1097     bool ok;
1098     Theme::GroupHeaderBackgroundStyle mode = static_cast<Theme::GroupHeaderBackgroundStyle>(act->data().toInt(&ok));
1099     if (!ok) {
1100         return;
1101     }
1102 
1103     mTheme->setGroupHeaderBackgroundStyle(mode);
1104 
1105     setTheme(mTheme); // this will reset theme cache and trigger a global update
1106 }
1107 
1108 void ThemePreviewWidget::paintEvent(QPaintEvent *e)
1109 {
1110     QTreeWidget::paintEvent(e);
1111 
1112     if (mThemeSelectedContentItemRect.isValid() || (mDropIndicatorPoint1 != mDropIndicatorPoint2)) {
1113         QPainter painter(viewport());
1114 
1115         if (mThemeSelectedContentItemRect.isValid()) {
1116             painter.setPen(QPen(Qt::black));
1117             painter.drawRect(mThemeSelectedContentItemRect);
1118         }
1119         if (mDropIndicatorPoint1 != mDropIndicatorPoint2) {
1120             painter.setPen(QPen(Qt::black, 3));
1121             painter.drawLine(mDropIndicatorPoint1, mDropIndicatorPoint2);
1122         }
1123     }
1124 }
1125 
1126 void ThemePreviewWidget::slotHeaderContextMenuRequested(const QPoint &pos)
1127 {
1128     if (mReadOnly) {
1129         return;
1130     }
1131 
1132     QTreeWidgetItem *hitem = headerItem();
1133     if (!hitem) {
1134         return; // ooops
1135     }
1136 
1137     int col = header()->logicalIndexAt(pos);
1138 
1139     if (col < 0) {
1140         return;
1141     }
1142 
1143     if (col >= mTheme->columns().count()) {
1144         return;
1145     }
1146 
1147     mSelectedThemeColumn = mTheme->column(col);
1148     if (!mSelectedThemeColumn) {
1149         return;
1150     }
1151 
1152     QMenu menu;
1153 
1154     menu.setTitle(mSelectedThemeColumn->label());
1155 
1156     QAction *act = menu.addAction(i18n("Column Properties..."));
1157     connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotColumnProperties);
1158 
1159     act = menu.addAction(i18n("Add Column..."));
1160     connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotAddColumn);
1161 
1162     act = menu.addAction(i18n("Delete Column"));
1163     connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotDeleteColumn);
1164     act->setEnabled(col > 0);
1165 
1166     menu.addSeparator();
1167 
1168     act = menu.addAction(i18n("Move Column to Left"));
1169     connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotMoveColumnToLeft);
1170     act->setEnabled(col > 0);
1171 
1172     act = menu.addAction(i18n("Move Column to Right"));
1173     connect(act, &QAction::triggered, this, &ThemePreviewWidget::slotMoveColumnToRight);
1174     act->setEnabled(col < mTheme->columns().count() - 1);
1175 
1176     menu.exec(header()->mapToGlobal(pos));
1177 }
1178 
1179 void ThemePreviewWidget::slotMoveColumnToLeft()
1180 {
1181     if (!mSelectedThemeColumn) {
1182         return;
1183     }
1184 
1185     const int columnIndex = mTheme->columns().indexOf(mSelectedThemeColumn);
1186     mTheme->moveColumn(columnIndex, columnIndex - 1);
1187     setTheme(mTheme); // this will reset theme cache and trigger a global update
1188 }
1189 
1190 void ThemePreviewWidget::slotMoveColumnToRight()
1191 {
1192     if (!mSelectedThemeColumn) {
1193         return;
1194     }
1195 
1196     const int columnIndex = mTheme->columns().indexOf(mSelectedThemeColumn);
1197     mTheme->moveColumn(columnIndex, columnIndex + 1);
1198     setTheme(mTheme); // this will reset theme cache and trigger a global update
1199 }
1200 
1201 void ThemePreviewWidget::slotAddColumn()
1202 {
1203     int newColumnIndex = mTheme->columns().count();
1204 
1205     if (mSelectedThemeColumn) {
1206         newColumnIndex = mTheme->columns().indexOf(mSelectedThemeColumn);
1207         if (newColumnIndex < 0) {
1208             newColumnIndex = mTheme->columns().count();
1209         } else {
1210             newColumnIndex++;
1211         }
1212     }
1213 
1214     mSelectedThemeColumn = new Theme::Column();
1215     mSelectedThemeColumn->setLabel(i18n("New Column"));
1216     mSelectedThemeColumn->setVisibleByDefault(true);
1217 
1218     mSelectedThemeColumn->addMessageRow(new Theme::Row());
1219     mSelectedThemeColumn->addGroupHeaderRow(new Theme::Row());
1220 
1221     auto dlg = new ThemeColumnPropertiesDialog(this, mSelectedThemeColumn, i18n("Add New Column"));
1222 
1223     if (dlg->exec() == QDialog::Accepted) {
1224         mTheme->insertColumn(newColumnIndex, mSelectedThemeColumn);
1225 
1226         mSelectedThemeContentItem = nullptr;
1227         mThemeSelectedContentItemRect = QRect();
1228         mDropIndicatorPoint1 = mDropIndicatorPoint2;
1229 
1230         setTheme(mTheme); // this will reset theme cache and trigger a global update
1231     } else {
1232         delete mSelectedThemeColumn;
1233         mSelectedThemeColumn = nullptr;
1234     }
1235 
1236     delete dlg;
1237 }
1238 
1239 void ThemePreviewWidget::slotColumnProperties()
1240 {
1241     if (!mSelectedThemeColumn) {
1242         return;
1243     }
1244 
1245     auto dlg = new ThemeColumnPropertiesDialog(this, mSelectedThemeColumn, i18n("Column Properties"));
1246 
1247     if (dlg->exec() == QDialog::Accepted) {
1248         mSelectedThemeContentItem = nullptr;
1249         mThemeSelectedContentItemRect = QRect();
1250         mDropIndicatorPoint1 = mDropIndicatorPoint2;
1251 
1252         setTheme(mTheme); // this will reset theme cache and trigger a global update
1253     }
1254 
1255     delete dlg;
1256 }
1257 
1258 void ThemePreviewWidget::slotDeleteColumn()
1259 {
1260     if (!mSelectedThemeColumn) {
1261         return;
1262     }
1263 
1264     const int idx = mTheme->columns().indexOf(mSelectedThemeColumn);
1265     if (idx < 1) { // first column can't be deleted
1266         return;
1267     }
1268 
1269     mTheme->removeColumn(mSelectedThemeColumn);
1270     delete mSelectedThemeColumn;
1271     mSelectedThemeColumn = nullptr;
1272 
1273     mSelectedThemeContentItem = nullptr;
1274     mThemeSelectedContentItemRect = QRect();
1275     mDropIndicatorPoint1 = mDropIndicatorPoint2;
1276 
1277     setTheme(mTheme); // this will reset theme cache and trigger a global update
1278 }
1279 
1280 ThemeEditor::ThemeEditor(QWidget *parent)
1281     : OptionSetEditor(parent)
1282 {
1283     mCurrentTheme = nullptr;
1284 
1285     // Appearance tab
1286     auto tab = new QWidget(this);
1287     addTab(tab, i18n("Appearance"));
1288 
1289     auto tabg = new QGridLayout(tab);
1290 
1291     auto gb = new QGroupBox(i18n("Content Items"), tab);
1292     tabg->addWidget(gb, 0, 0);
1293 
1294     auto gblayout = new QGridLayout(gb);
1295 
1296     Theme dummyTheme;
1297 
1298     auto cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Subject);
1299     cil->setText(Theme::ContentItem::description(cil->type()));
1300     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1301     gblayout->addWidget(cil, 0, 0);
1302 
1303     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Date);
1304     cil->setText(Theme::ContentItem::description(cil->type()));
1305     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1306     gblayout->addWidget(cil, 1, 0);
1307 
1308     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Size);
1309     cil->setText(Theme::ContentItem::description(cil->type()));
1310     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1311     gblayout->addWidget(cil, 2, 0);
1312 
1313     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Sender);
1314     cil->setText(Theme::ContentItem::description(cil->type()));
1315     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1316     gblayout->addWidget(cil, 0, 1);
1317 
1318     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Receiver);
1319     cil->setText(Theme::ContentItem::description(cil->type()));
1320     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1321     gblayout->addWidget(cil, 1, 1);
1322 
1323     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SenderOrReceiver);
1324     cil->setText(Theme::ContentItem::description(cil->type()));
1325     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1326     gblayout->addWidget(cil, 2, 1);
1327 
1328     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::MostRecentDate);
1329     cil->setText(Theme::ContentItem::description(cil->type()));
1330     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1331     gblayout->addWidget(cil, 0, 2);
1332 
1333     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::TagList);
1334     cil->setText(Theme::ContentItem::description(cil->type()));
1335     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1336     gblayout->addWidget(cil, 1, 2);
1337 
1338     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::Folder);
1339     cil->setText(Theme::ContentItem::description(cil->type()));
1340     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1341     gblayout->addWidget(cil, 2, 2);
1342 
1343     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::CombinedReadRepliedStateIcon);
1344     cil->setPixmap(*dummyTheme.pixmap(Theme::IconRepliedAndForwarded));
1345     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1346     gblayout->addWidget(cil, 0, 3);
1347 
1348     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ReadStateIcon);
1349     cil->setPixmap(*dummyTheme.pixmap(Theme::IconNew));
1350     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1351     gblayout->addWidget(cil, 1, 3);
1352 
1353     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::RepliedStateIcon);
1354     cil->setPixmap(*dummyTheme.pixmap(Theme::IconReplied));
1355     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1356     gblayout->addWidget(cil, 2, 3);
1357 
1358     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::AttachmentStateIcon);
1359     cil->setPixmap(*dummyTheme.pixmap(Theme::IconAttachment));
1360     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1361     gblayout->addWidget(cil, 0, 4);
1362 
1363     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::EncryptionStateIcon);
1364     cil->setPixmap(*dummyTheme.pixmap(Theme::IconFullyEncrypted));
1365     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1366     gblayout->addWidget(cil, 1, 4);
1367 
1368     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SignatureStateIcon);
1369     cil->setPixmap(*dummyTheme.pixmap(Theme::IconFullySigned));
1370     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1371     gblayout->addWidget(cil, 2, 4);
1372 
1373     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ActionItemStateIcon);
1374     cil->setPixmap(*dummyTheme.pixmap(Theme::IconActionItem));
1375     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1376     gblayout->addWidget(cil, 0, 5);
1377 
1378     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::AnnotationIcon);
1379     cil->setPixmap(*dummyTheme.pixmap(Theme::IconAnnotation));
1380     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1381     gblayout->addWidget(cil, 1, 5);
1382 
1383     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::InvitationIcon);
1384     cil->setPixmap(*dummyTheme.pixmap(Theme::IconInvitation));
1385     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1386     gblayout->addWidget(cil, 2, 5);
1387 
1388     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ImportantStateIcon);
1389     cil->setPixmap(*dummyTheme.pixmap(Theme::IconImportant));
1390     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1391     gblayout->addWidget(cil, 0, 6);
1392 
1393     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::SpamHamStateIcon);
1394     cil->setPixmap(*dummyTheme.pixmap(Theme::IconSpam));
1395     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1396     gblayout->addWidget(cil, 1, 6);
1397 
1398     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::WatchedIgnoredStateIcon);
1399     cil->setPixmap(*dummyTheme.pixmap(Theme::IconWatched));
1400     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1401     gblayout->addWidget(cil, 2, 6);
1402 
1403     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::ExpandedStateIcon);
1404     cil->setPixmap(*dummyTheme.pixmap(Theme::IconShowMore));
1405     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1406     gblayout->addWidget(cil, 0, 7);
1407 
1408     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::VerticalLine);
1409     cil->setPixmap(*dummyTheme.pixmap(Theme::IconVerticalLine));
1410     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1411     gblayout->addWidget(cil, 1, 7);
1412 
1413     cil = new ThemeContentItemSourceLabel(gb, Theme::ContentItem::HorizontalSpacer);
1414     cil->setPixmap(*dummyTheme.pixmap(Theme::IconHorizontalSpacer));
1415     cil->setToolTip(Theme::ContentItem::description(cil->type()));
1416     gblayout->addWidget(cil, 2, 7);
1417 
1418     mPreviewWidget = new ThemePreviewWidget(tab);
1419     tabg->addWidget(mPreviewWidget, 1, 0);
1420 
1421     auto l = new QLabel(tab);
1422     l->setText(
1423         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 "
1424              "click on the items inside the view for more options."));
1425     l->setWordWrap(true);
1426     l->setAlignment(Qt::AlignCenter);
1427     tabg->addWidget(l, 2, 0);
1428 
1429     tabg->setRowStretch(1, 1);
1430 
1431     // Advanced tab
1432     tab = new QWidget(this);
1433     addTab(tab, i18nc("@title:tab Advanced theme settings", "Advanced"));
1434 
1435     tabg = new QGridLayout(tab);
1436 
1437     l = new QLabel(i18n("Header:"), tab);
1438     tabg->addWidget(l, 0, 0);
1439 
1440     mViewHeaderPolicyCombo = new QComboBox(tab);
1441     tabg->addWidget(mViewHeaderPolicyCombo, 0, 1);
1442 
1443     l = new QLabel(i18n("Icon size:"), tab);
1444     tabg->addWidget(l, 1, 0);
1445 
1446     mIconSizeSpinBox = new KPluralHandlingSpinBox(tab);
1447     mIconSizeSpinBox->setMinimum(8);
1448     mIconSizeSpinBox->setMaximum(64);
1449     mIconSizeSpinBox->setSuffix(ki18ncp("suffix in a spinbox", " pixel", " pixels"));
1450 
1451     QObject::connect(mIconSizeSpinBox, &KPluralHandlingSpinBox::valueChanged, this, &ThemeEditor::slotIconSizeSpinBoxValueChanged);
1452 
1453     tabg->addWidget(mIconSizeSpinBox, 1, 1);
1454 
1455     tabg->setColumnStretch(1, 1);
1456     tabg->setRowStretch(2, 1);
1457     fillViewHeaderPolicyCombo();
1458 }
1459 
1460 ThemeEditor::~ThemeEditor() = default;
1461 
1462 void ThemeEditor::editTheme(Theme *set)
1463 {
1464     mCurrentTheme = set;
1465     mPreviewWidget->setTheme(mCurrentTheme);
1466 
1467     if (!mCurrentTheme) {
1468         setEnabled(false);
1469         return;
1470     }
1471     setEnabled(true);
1472 
1473     nameEdit()->setText(set->name());
1474     descriptionEdit()->setPlainText(set->description());
1475 
1476     ComboBoxUtils::setIntegerOptionComboValue(mViewHeaderPolicyCombo, (int)mCurrentTheme->viewHeaderPolicy());
1477 
1478     mIconSizeSpinBox->setValue(set->iconSize());
1479     setReadOnly(mCurrentTheme->readOnly());
1480 }
1481 
1482 void ThemeEditor::setReadOnly(bool readOnly)
1483 {
1484     mPreviewWidget->setReadOnly(readOnly);
1485     mViewHeaderPolicyCombo->setEnabled(!readOnly);
1486     mIconSizeSpinBox->setEnabled(!readOnly);
1487     OptionSetEditor::setReadOnly(readOnly);
1488 }
1489 
1490 void ThemeEditor::commit()
1491 {
1492     if (!mCurrentTheme || mCurrentTheme->readOnly()) {
1493         return;
1494     }
1495 
1496     mCurrentTheme->setName(nameEdit()->text());
1497     mCurrentTheme->setDescription(descriptionEdit()->toPlainText());
1498 
1499     mCurrentTheme->setViewHeaderPolicy((Theme::ViewHeaderPolicy)ComboBoxUtils::getIntegerOptionComboValue(mViewHeaderPolicyCombo, 0));
1500     mCurrentTheme->setIconSize(mIconSizeSpinBox->value());
1501     // other settings are already committed to this theme
1502 }
1503 
1504 void ThemeEditor::fillViewHeaderPolicyCombo()
1505 {
1506     ComboBoxUtils::fillIntegerOptionCombo(mViewHeaderPolicyCombo, Theme::enumerateViewHeaderPolicyOptions());
1507 }
1508 
1509 void ThemeEditor::slotNameEditTextEdited(const QString &newName)
1510 {
1511     if (!mCurrentTheme) {
1512         return;
1513     }
1514     mCurrentTheme->setName(newName);
1515     Q_EMIT themeNameChanged();
1516 }
1517 
1518 void ThemeEditor::slotIconSizeSpinBoxValueChanged(int val)
1519 {
1520     if (!mCurrentTheme) {
1521         return;
1522     }
1523     mCurrentTheme->setIconSize(val);
1524 
1525     mPreviewWidget->setTheme(mCurrentTheme); // will trigger a cache reset and a view update
1526 }
1527 
1528 MessageList::Core::Theme *ThemeEditor::editedTheme() const
1529 {
1530     return mCurrentTheme;
1531 }
1532 
1533 #include "moc_themeeditor.cpp"