Messagelib

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

KDE's Doxygen guidelines are available online.