Messagelib

view.cpp
1/******************************************************************************
2 *
3 * SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 *
7 *******************************************************************************/
8
9#include "core/view.h"
10#include "core/aggregation.h"
11#include "core/delegate.h"
12#include "core/groupheaderitem.h"
13#include "core/item.h"
14#include "core/messageitem.h"
15#include "core/model.h"
16#include "core/storagemodelbase.h"
17#include "core/theme.h"
18#include "core/widgetbase.h"
19#include "messagelistsettings.h"
20#include "messagelistutil.h"
21#include "messagelistutil_p.h"
22
23#include "MessageCore/StringUtil"
24
25#include <KMime/DateFormatter> // kdepimlibs
26
27#include <Akonadi/Item>
28#include <KTwoFingerTap>
29#include <QApplication>
30#include <QGestureEvent>
31#include <QHeaderView>
32#include <QHelpEvent>
33#include <QLineEdit>
34#include <QMenu>
35#include <QPainter>
36#include <QScrollBar>
37#include <QScroller>
38#include <QTimer>
39#include <QToolTip>
40
41#include "messagelist_debug.h"
42#include <KLocalizedString>
43
44using namespace MessageList::Core;
45
46class View::ViewPrivate
47{
48public:
49 ViewPrivate(View *owner, Widget *parent)
50 : q(owner)
51 , mWidget(parent)
52 , mDelegate(new Delegate(owner))
53 {
54 }
55
56 void expandFullThread(const QModelIndex &index);
57 void generalPaletteChanged();
58 void onPressed(QMouseEvent *e);
59 void gestureEvent(QGestureEvent *e);
60 void tapTriggered(QTapGesture *tap);
61 void tapAndHoldTriggered(QTapAndHoldGesture *tap);
62 void twoFingerTapTriggered(KTwoFingerTap *tap);
63
64 QColor mTextColor;
65 View *const q;
66
67 Widget *const mWidget;
68 Model *mModel = nullptr;
69 Delegate *const mDelegate;
70
71 const Aggregation *mAggregation = nullptr; ///< The Aggregation we're using now, shallow pointer
72 Theme *mTheme = nullptr; ///< The Theme we're using now, shallow pointer
73 bool mNeedToApplyThemeColumns = false; ///< Flag signaling a pending application of theme columns
74 Item *mLastCurrentItem = nullptr;
75 QPoint mMousePressPosition;
76 bool mSaveThemeColumnStateOnSectionResize = true; ///< This is used to filter out programmatic column resizes in slotSectionResized().
77 QTimer *mSaveThemeColumnStateTimer = nullptr; ///< Used to trigger a delayed "save theme state"
78 QTimer *mApplyThemeColumnsTimer = nullptr; ///< Used to trigger a delayed "apply theme columns"
79 int mLastViewportWidth = -1;
80 bool mIgnoreUpdateGeometries = false; ///< Shall we ignore the "update geometries" calls ?
81 QScroller *mScroller = nullptr;
82 bool mIsTouchEvent = false;
83 bool mMousePressed = false;
85 bool mTapAndHoldActive = false;
86 QRubberBand *mRubberBand = nullptr;
87 Qt::GestureType mTwoFingerTap = Qt::CustomGesture;
88};
89
90View::View(Widget *pParent)
91 : QTreeView(pParent)
92 , d(new ViewPrivate(this, pParent))
93{
94 d->mSaveThemeColumnStateTimer = new QTimer();
95 connect(d->mSaveThemeColumnStateTimer, &QTimer::timeout, this, &View::saveThemeColumnState);
96
97 d->mApplyThemeColumnsTimer = new QTimer();
98 connect(d->mApplyThemeColumnsTimer, &QTimer::timeout, this, &View::applyThemeColumns);
99
100 setItemDelegate(d->mDelegate);
101 setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
102 setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
103 setAlternatingRowColors(true);
104 setAllColumnsShowFocus(true);
105 setSelectionMode(QAbstractItemView::ExtendedSelection);
106 viewport()->setAcceptDrops(true);
107
108 d->mScroller = QScroller::scroller(viewport());
109 QScrollerProperties scrollerProp;
111 d->mScroller->setScrollerProperties(scrollerProp);
112 d->mScroller->grabGesture(viewport());
113
114 setAttribute(Qt::WA_AcceptTouchEvents);
116 viewport()->grabGesture(d->mTwoFingerTap);
117 viewport()->grabGesture(Qt::TapGesture);
118 viewport()->grabGesture(Qt::TapAndHoldGesture);
119
120 d->mRubberBand = new QRubberBand(QRubberBand::Rectangle, this);
121
122 header()->setContextMenuPolicy(Qt::CustomContextMenu);
123 connect(header(), &QWidget::customContextMenuRequested, this, &View::slotHeaderContextMenuRequested);
124 connect(header(), &QHeaderView::sectionResized, this, &View::slotHeaderSectionResized);
125
126 header()->setSectionsClickable(true);
127 header()->setSectionResizeMode(QHeaderView::Interactive);
128 header()->setMinimumSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value
129 header()->setDefaultSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value
130
131 d->mModel = new Model(this);
132 setModel(d->mModel);
133
134 connect(d->mModel, &Model::statusMessage, pParent, &Widget::statusMessage);
135
136 connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection);
137
138 // as in KDE3, when a root-item of a message thread is expanded, expand all children
139 connect(this, &View::expanded, this, [this](const QModelIndex &index) {
140 d->expandFullThread(index);
141 });
142}
143
144View::~View()
145{
146 if (d->mSaveThemeColumnStateTimer->isActive()) {
147 d->mSaveThemeColumnStateTimer->stop();
148 }
149 delete d->mSaveThemeColumnStateTimer;
150 if (d->mApplyThemeColumnsTimer->isActive()) {
151 d->mApplyThemeColumnsTimer->stop();
152 }
153 delete d->mApplyThemeColumnsTimer;
154
155 // Zero out the theme, aggregation and ApplyThemeColumnsTimer so Model will not cause accesses to them in its destruction process
156 d->mApplyThemeColumnsTimer = nullptr;
157
158 d->mTheme = nullptr;
159 d->mAggregation = nullptr;
160}
161
162Model *View::model() const
163{
164 return d->mModel;
165}
166
167Delegate *View::delegate() const
168{
169 return d->mDelegate;
170}
171
172void View::ignoreCurrentChanges(bool ignore)
173{
174 if (ignore) {
175 disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged);
176 viewport()->setUpdatesEnabled(false);
177 } else {
178 connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection);
179 viewport()->setUpdatesEnabled(true);
180 }
181}
182
183void View::ignoreUpdateGeometries(bool ignore)
184{
185 d->mIgnoreUpdateGeometries = ignore;
186}
187
188bool View::isScrollingLocked() const
189{
190 // There is another popular requisite: people want the view to automatically
191 // scroll in order to show new arriving mail. This actually makes sense
192 // only when the view is sorted by date and the new mail is (usually) either
193 // appended at the bottom or inserted at the top. It would be also confusing
194 // when the user is browsing some other thread in the meantime.
195 //
196 // So here we make a simple guess: if the view is scrolled somewhere in the
197 // middle then we assume that the user is browsing other threads and we
198 // try to keep the currently selected item steady on the screen.
199 // When the view is "locked" to the top (scrollbar value 0) or to the
200 // bottom (scrollbar value == maximum) then we assume that the user
201 // isn't browsing and we should attempt to show the incoming messages
202 // by keeping the view "locked".
203 //
204 // The "locking" also doesn't make sense in the first big fill view job.
205 // [Well this concept is pre-akonadi. Now the loading is all async anyway...
206 // So all this code is actually triggered during the initial loading, too.]
207 const int scrollBarPosition = verticalScrollBar()->value();
208 const int scrollBarMaximum = verticalScrollBar()->maximum();
209 const SortOrder *sortOrder = d->mModel->sortOrder();
210 const bool lockView = (
211 // not the first loading job
212 !d->mModel->isLoading())
213 && (
214 // messages sorted by date
217 && (
218 // scrollbar at top (Descending order) or bottom (Ascending order)
219 (scrollBarPosition == 0 && sortOrder->messageSortDirection() == SortOrder::Descending)
220 || (scrollBarPosition == scrollBarMaximum && sortOrder->messageSortDirection() == SortOrder::Ascending));
221 return lockView;
222}
223
224void View::updateGeometries()
225{
226 if (d->mIgnoreUpdateGeometries || !d->mModel) {
227 return;
228 }
229
230 const int scrollBarPositionBefore = verticalScrollBar()->value();
231 const bool lockView = isScrollingLocked();
232
234
235 if (lockView) {
236 // we prefer to keep the view locked to the top or bottom
237 if (scrollBarPositionBefore != 0) {
238 // we wanted the view to be locked to the bottom
239 if (verticalScrollBar()->value() != verticalScrollBar()->maximum()) {
240 verticalScrollBar()->setValue(verticalScrollBar()->maximum());
241 }
242 } // else we wanted the view to be locked to top and we shouldn't need to do anything
243 }
244}
245
246StorageModel *View::storageModel() const
247{
248 return d->mModel->storageModel();
249}
250
251void View::setAggregation(const Aggregation *aggregation)
252{
253 d->mAggregation = aggregation;
254 d->mModel->setAggregation(aggregation);
255
256 // use uniform row heights to speed up, but only if there are no group headers used
257 setUniformRowHeights(d->mAggregation->grouping() == Aggregation::NoGrouping);
258}
259
260void View::setTheme(Theme *theme)
261{
262 d->mNeedToApplyThemeColumns = true;
263 d->mTheme = theme;
264 d->mDelegate->setTheme(theme);
265 d->mModel->setTheme(theme);
266}
267
268void View::setSortOrder(const SortOrder *sortOrder)
269{
270 d->mModel->setSortOrder(sortOrder);
271}
272
273void View::reload()
274{
275 setStorageModel(storageModel());
276}
277
278void View::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode)
279{
280 // This will cause the model to be reset.
281 d->mSaveThemeColumnStateOnSectionResize = false;
282 d->mModel->setStorageModel(storageModel, preSelectionMode);
283 d->mSaveThemeColumnStateOnSectionResize = true;
284}
285
286//////////////////////////////////////////////////////////////////////////////////////////////////////
287// Theme column state machinery
288//
289// This is yet another beast to beat. The QHeaderView behaviour, at the time of writing,
290// is quite unpredictable. This is due to the complex interaction with the model, with the QTreeView
291// and due to its attempts to delay the layout jobs. The delayed layouts, especially, may
292// cause the widths of the columns to quickly change in an unexpected manner in a place
293// where previously they have been always settled to the values you set...
294//
295// So here we have the tools to:
296//
297// - Apply the saved state of the theme columns (applyThemeColumns()).
298// This function computes the "best fit" state of the visible columns and tries
299// to apply it to QHeaderView. It also saves the new computed state to the Theme object.
300//
301// - Explicitly save the column state, used when the user changes the widths or visibility manually.
302// This is called through a delayed timer after a column has been resized or used directly
303// when the visibility state of a column has been changed by toggling a popup menu entry.
304//
305// - Display the column state context popup menu and handle its actions
306//
307// - Apply the theme columns when the theme changes, when the model changes or when
308// the widget is resized.
309//
310// - Avoid saving a corrupted column state in that QHeaderView can be found *very* frequently.
311//
312
313void View::applyThemeColumns()
314{
315 if (!d->mApplyThemeColumnsTimer) {
316 return;
317 }
318
319 if (d->mApplyThemeColumnsTimer->isActive()) {
320 d->mApplyThemeColumnsTimer->stop();
321 }
322
323 if (!d->mTheme) {
324 return;
325 }
326
327 // qCDebug(MESSAGELIST_LOG) << "Apply theme columns";
328
329 const QList<Theme::Column *> &columns = d->mTheme->columns();
330
331 if (columns.isEmpty()) {
332 return; // bad theme
333 }
334
335 if (!viewport()->isVisible()) {
336 return; // invisible
337 }
338
339 if (viewport()->width() < 1) {
340 return; // insane width
341 }
342 const int viewportWidth = viewport()->width();
343 d->mLastViewportWidth = viewportWidth;
344
345 // Now we want to distribute the available width on all the visible columns.
346 //
347 // The rules:
348 // - The visible columns will span the width of the view, if possible.
349 // - The columns with a saved width should take that width.
350 // - The columns on the left should take more space, if possible.
351 // - The columns with no text take just slightly more than their size hint.
352 // while the columns with text take possibly a lot more.
353 //
354
355 // Note that the first column is always shown (it can't be hidden at all)
356
357 // The algorithm below is a sort of compromise between:
358 // - Saving the user preferences for widths
359 // - Using exactly the available view space
360 //
361 // It "tends to work" in all cases:
362 // - When there are no user preferences saved and the column widths must be
363 // automatically computed to make best use of available space
364 // - When there are user preferences for only some of the columns
365 // and that should be somewhat preserved while still using all the
366 // available space.
367 // - When all the columns have well defined saved widths
368
369 int idx = 0;
370
371 // Gather total size "hint" for visible sections: if the widths of the columns wers
372 // all saved then the total hint is equal to the total saved width.
373
374 int totalVisibleWidthHint = 0;
376 for (const auto col : std::as_const(columns)) {
377 if (col->currentlyVisible() || (idx == 0)) {
378 // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be visible";
379 // Column visible
380 const int savedWidth = col->currentWidth();
381 const int hintWidth = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width();
384 // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " size hint is " << hintWidth;
385 } else {
386 // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be not visible";
387 // The column is not visible
388 lColumnSizeHints.append(-1); // dummy
389 }
390 idx++;
391 }
392
393 if (totalVisibleWidthHint < 16) {
394 totalVisibleWidthHint = 16; // be reasonable
395 }
396
397 // Now compute somewhat "proportional" widths.
398 idx = 0;
399
401 lColumnWidths.reserve(columns.count());
402 int totalVisibleWidth = 0;
403 for (const auto col : std::as_const(columns)) {
404 double savedWidth = col->currentWidth();
406 double realWidth;
407
408 if (col->currentlyVisible() || (idx == 0)) {
409 if (col->containsTextItems()) {
410 // the column contains text items, it should get more space (if possible)
412 } else {
413 // the column contains no text items, it should get exactly its hint/saved width.
415 }
416
417 if (realWidth < 2) {
418 realWidth = 2; // don't allow very insane values
419 }
420
422 } else {
423 // Column not visible
424 realWidth = -1;
425 }
426
427 lColumnWidths.append(realWidth);
428
429 idx++;
430 }
431
432 // Now the algorithm above may be wrong for several reasons...
433 // - We're using fixed widths for certain columns and proportional
434 // for others...
435 // - The user might have changed the width of the view from the
436 // time in that the widths have been saved
437 // - There are some (not well identified) issues with the QTreeView
438 // scrollbar that make our view appear larger or shorter by 2-3 pixels
439 // sometimes.
440 // - ...
441 // So we correct the previous estimates by trying to use exactly
442 // the available space.
443
444 idx = 0;
445
447 // The estimated widths were not using exactly the available space.
449 // We were using less space than available.
450
451 // Give the additional space to the text columns
452 // also give more space to the first ones and less space to the last ones
453 qreal available = viewportWidth - totalVisibleWidth;
454
455 for (int idx = 0; idx < columns.count(); ++idx) {
456 Theme::Column *column = columns.at(idx);
457 if ((column->currentlyVisible() || (idx == 0)) && column->containsTextItems()) {
458 // give more space to this column
459 available /= 2; // eat half of the available space
460 lColumnWidths[idx] += available; // and give it to this column
461 if (available < 1) {
462 break; // no more space to give away
463 }
464 }
465 }
466
467 // if any space is still available, give it to the first column
468 if (available >= 1) {
469 lColumnWidths[0] += available;
470 }
471 } else {
472 // We were using more space than available
473
474 // If the columns span more than the view then
475 // try to squeeze them in order to make them fit
477 if (missing > 0) {
478 const int count = lColumnWidths.count();
479 idx = count - 1;
480
481 while (idx >= 0) {
482 if (columns.at(idx)->currentlyVisible() || (idx == 0)) {
483 double chop = lColumnWidths.at(idx) - lColumnSizeHints.at(idx);
484 if (chop > 0) {
485 if (chop > missing) {
486 chop = missing;
487 }
488 lColumnWidths[idx] -= chop;
489 missing -= chop;
490 if (missing < 1) {
491 break; // no more space to recover
492 }
493 }
494 } // else it's invisible
495 idx--;
496 }
497 }
498 }
499 }
500
501 // We're ready to assign widths.
502
503 bool oldSave = d->mSaveThemeColumnStateOnSectionResize;
504 d->mSaveThemeColumnStateOnSectionResize = false;
505
506 // A huge problem here is that QHeaderView goes quite nuts if we show or hide sections
507 // while resizing them. This is because it has several machineries aimed to delay
508 // the layout to the last possible moment. So if we show a column, it will tend to
509 // screw up the layout of other ones.
510
511 // We first loop showing/hiding columns then.
512
513 idx = 0;
514
515 for (const auto col : std::as_const(columns)) {
516 bool visible = (idx == 0) || col->currentlyVisible();
517 // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " visible " << visible;
518 col->setCurrentlyVisible(visible);
519 header()->setSectionHidden(idx, !visible);
520 idx++;
521 }
522
523 // Then we loop assigning widths. This is still complicated since QHeaderView tries
524 // very badly to stretch the last section and thus will resize it in the meantime.
525 // But seems to work most of the times...
526
527 idx = 0;
528
529 for (const auto col : std::as_const(columns)) {
530 if (col->currentlyVisible()) {
531 const double columnWidth(lColumnWidths.at(idx));
532 col->setCurrentWidth(columnWidth);
533 // Laurent Bug 358855 - message list column widths lost when program closed
534 // I need to investigate if this code is still necessary (all method)
535 header()->resizeSection(idx, static_cast<int>(columnWidth));
536 } else {
537 col->setCurrentWidth(-1);
538 }
539 idx++;
540 }
541
542 idx = 0;
543
544 bool bTriggeredQtBug = false;
545 for (const auto col : std::as_const(columns)) {
546 if (!header()->isSectionHidden(idx)) {
547 if (!col->currentlyVisible()) {
548 bTriggeredQtBug = true;
549 }
550 }
551 idx++;
552 }
553
554 setHeaderHidden(d->mTheme->viewHeaderPolicy() == Theme::NeverShowHeader);
555
556 d->mSaveThemeColumnStateOnSectionResize = oldSave;
557 d->mNeedToApplyThemeColumns = false;
558
559 static bool bAllowRecursion = true;
560
562 bAllowRecursion = false;
563 // qCDebug(MESSAGELIST_LOG) << "I've triggered the QHeaderView bug: trying to fix by calling myself again";
564 applyThemeColumns();
565 bAllowRecursion = true;
566 }
567}
568
569void View::triggerDelayedApplyThemeColumns()
570{
571 if (d->mApplyThemeColumnsTimer->isActive()) {
572 d->mApplyThemeColumnsTimer->stop();
573 }
574 d->mApplyThemeColumnsTimer->setSingleShot(true);
575 d->mApplyThemeColumnsTimer->start(100);
576}
577
578void View::saveThemeColumnState()
579{
580 if (d->mSaveThemeColumnStateTimer->isActive()) {
581 d->mSaveThemeColumnStateTimer->stop();
582 }
583
584 if (!d->mTheme) {
585 return;
586 }
587
588 if (d->mNeedToApplyThemeColumns) {
589 return; // don't save the state if it hasn't been applied at all
590 }
591
592 // qCDebug(MESSAGELIST_LOG) << "Save theme column state";
593
594 const auto columns = d->mTheme->columns();
595
596 if (columns.isEmpty()) {
597 return; // bad theme
598 }
599
600 int idx = 0;
601
602 for (const auto col : std::as_const(columns)) {
603 if (header()->isSectionHidden(idx)) {
604 // qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is hidden";
605 col->setCurrentlyVisible(false);
606 col->setCurrentWidth(-1); // reset (hmmm... we could use the "don't touch" policy here too...)
607 } else {
608 // qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is visible and has size " << header()->sectionSize( idx );
609 col->setCurrentlyVisible(true);
610 col->setCurrentWidth(header()->sectionSize(idx));
611 }
612 idx++;
613 }
614}
615
616void View::triggerDelayedSaveThemeColumnState()
617{
618 if (d->mSaveThemeColumnStateTimer->isActive()) {
619 d->mSaveThemeColumnStateTimer->stop();
620 }
621 d->mSaveThemeColumnStateTimer->setSingleShot(true);
622 d->mSaveThemeColumnStateTimer->start(200);
623}
624
625void View::resizeEvent(QResizeEvent *e)
626{
627 qCDebug(MESSAGELIST_LOG) << "Resize event enter (viewport width is " << viewport()->width() << ")";
628
630
631 if (!isVisible()) {
632 return; // don't play with
633 }
634
635 if (d->mLastViewportWidth != viewport()->width()) {
636 triggerDelayedApplyThemeColumns();
637 }
638
639 if (header()->isVisible()) {
640 return;
641 }
642
643 // header invisible
644
645 bool oldSave = d->mSaveThemeColumnStateOnSectionResize;
646 d->mSaveThemeColumnStateOnSectionResize = false;
647
648 const int count = header()->count();
649 if ((count - header()->hiddenSectionCount()) < 2) {
650 // a single column visible: resize it
651 int visibleIndex;
652 for (visibleIndex = 0; visibleIndex < count; visibleIndex++) {
653 if (!header()->isSectionHidden(visibleIndex)) {
654 break;
655 }
656 }
657 if (visibleIndex < count) {
658 header()->resizeSection(visibleIndex, viewport()->width() - 4);
659 }
660 }
661
662 d->mSaveThemeColumnStateOnSectionResize = oldSave;
663
664 triggerDelayedSaveThemeColumnState();
665}
666
667void View::paintEvent(QPaintEvent *event)
668{
669#if 0
670 if (/*mFirstResult &&*/ (!model() || model()->rowCount() == 0)) {
671 QPainter p(viewport());
672
673 QFont font = p.font();
674 font.setItalic(true);
675 p.setFont(font);
676
677 if (!d->mTextColor.isValid()) {
678 d->generalPaletteChanged();
679 }
680 p.setPen(d->mTextColor);
681
682 p.drawText(QRect(0, 0, width(), height()), Qt::AlignCenter, i18n("No result found"));
683 } else {
685 }
686#else
688#endif
689}
690
691void View::modelAboutToEmitLayoutChanged()
692{
693 // QHeaderView goes totally NUTS with a layoutChanged() call
694 d->mSaveThemeColumnStateOnSectionResize = false;
695}
696
697void View::modelEmittedLayoutChanged()
698{
699 // This is after a first chunk of work has been done by the model: do apply column states
700 d->mSaveThemeColumnStateOnSectionResize = true;
701 applyThemeColumns();
702}
703
704void View::slotHeaderSectionResized(int logicalIndex, int oldWidth, int newWidth)
705{
706 Q_UNUSED(logicalIndex)
709
710 if (d->mSaveThemeColumnStateOnSectionResize) {
711 triggerDelayedSaveThemeColumnState();
712 }
713}
714
715int View::sizeHintForColumn(int logicalColumnIndex) const
716{
717 // QTreeView: please don't touch my column widths...
718 int w = header()->sectionSize(logicalColumnIndex);
719 if (w > 0) {
720 return w;
721 }
722 if (!d->mDelegate) {
723 return 32; // dummy
724 }
725 w = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, logicalColumnIndex).width();
726 return w;
727}
728
729void View::slotHeaderContextMenuRequested(const QPoint &pnt)
730{
731 if (!d->mTheme) {
732 return;
733 }
734
735 const auto columns = d->mTheme->columns();
736
737 if (columns.isEmpty()) {
738 return; // bad theme
739 }
740
741 // the menu for the columns
742 QMenu menu;
743
744 int idx = 0;
745 for (const auto col : std::as_const(columns)) {
746 QAction *act = menu.addAction(col->label());
747 act->setCheckable(true);
748 act->setChecked(!header()->isSectionHidden(idx));
749 if (idx == 0) {
750 act->setEnabled(false);
751 }
752 QObject::connect(act, &QAction::triggered, this, [this, idx] {
753 slotShowHideColumn(idx);
754 });
755
756 idx++;
757 }
758
759 menu.addSeparator();
760 {
761 QAction *act = menu.addAction(i18n("Adjust Column Sizes"));
762 QObject::connect(act, &QAction::triggered, this, &View::slotAdjustColumnSizes);
763 }
764 {
765 QAction *act = menu.addAction(i18n("Show Default Columns"));
766 QObject::connect(act, &QAction::triggered, this, &View::slotShowDefaultColumns);
767 }
768 menu.addSeparator();
769 {
770 QAction *act = menu.addAction(i18n("Display Tooltips"));
771 act->setCheckable(true);
772 act->setChecked(MessageListSettings::self()->messageToolTipEnabled());
773 QObject::connect(act, &QAction::triggered, this, &View::slotDisplayTooltips);
774 }
775 menu.addSeparator();
776
777 MessageList::Util::fillViewMenu(&menu, d->mWidget);
778
779 menu.exec(header()->mapToGlobal(pnt));
780}
781
782void View::slotAdjustColumnSizes()
783{
784 if (!d->mTheme) {
785 return;
786 }
787
788 d->mTheme->resetColumnSizes();
789 applyThemeColumns();
790}
791
792void View::slotShowDefaultColumns()
793{
794 if (!d->mTheme) {
795 return;
796 }
797
798 d->mTheme->resetColumnState();
799 applyThemeColumns();
800}
801
802void View::slotDisplayTooltips(bool showTooltips)
803{
804 MessageListSettings::self()->setMessageToolTipEnabled(showTooltips);
805}
806
807void View::slotShowHideColumn(int columnIdx)
808{
809 if (!d->mTheme) {
810 return; // oops
811 }
812
813 if (columnIdx == 0) {
814 return; // can never be hidden
815 }
816
817 if (columnIdx >= d->mTheme->columns().count()) {
818 return;
819 }
820
821 const bool showIt = header()->isSectionHidden(columnIdx);
822
823 Theme::Column *column = d->mTheme->columns().at(columnIdx);
824 Q_ASSERT(column);
825
826 // first save column state (as it is, with the column still in previous state)
827 saveThemeColumnState();
828
829 // If a section has just been shown, invalidate its width in the skin
830 // since QTreeView assigned it a (possibly insane) default width.
831 // If a section has been hidden, then invalidate its width anyway...
832 // so finally invalidate width always, here.
834 column->setCurrentWidth(-1);
835
836 // then apply theme columns to re-compute proportional widths (so we hopefully stay in the view)
837 applyThemeColumns();
838}
839
840Item *View::currentItem() const
841{
842 QModelIndex idx = currentIndex();
843 if (!idx.isValid()) {
844 return nullptr;
845 }
846 Item *it = static_cast<Item *>(idx.internalPointer());
847 Q_ASSERT(it);
848 return it;
849}
850
851MessageItem *View::currentMessageItem(bool selectIfNeeded) const
852{
853 Item *it = currentItem();
854 if (!it || (it->type() != Item::Message)) {
855 return nullptr;
856 }
857
858 if (selectIfNeeded) {
859 // Keep things coherent, if the user didn't select it, but acted on it via
860 // a shortcut, do select it now.
861 if (!selectionModel()->isSelected(currentIndex())) {
862 selectionModel()->select(currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows);
863 }
864 }
865
866 return static_cast<MessageItem *>(it);
867}
868
869void View::setCurrentMessageItem(MessageItem *it, bool center)
870{
871 if (it) {
872 qCDebug(MESSAGELIST_LOG) << "Setting current message to" << it->subject();
873
874 const QModelIndex index = d->mModel->index(it, 0);
875 selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows);
876 if (center) {
878 }
879 } else {
880 selectionModel()->setCurrentIndex(QModelIndex(), QItemSelectionModel::Current | QItemSelectionModel::Clear);
881 }
882}
883
884bool View::selectionEmpty() const
885{
886 return selectionModel()->selectedRows().isEmpty();
887}
888
889QList<MessageItem *> View::selectionAsMessageItemList(bool includeCollapsedChildren) const
890{
892
893 QModelIndexList lSelected = selectionModel()->selectedRows();
894 if (lSelected.isEmpty()) {
895 return selectedMessages;
896 }
897 for (const auto &idx : std::as_const(lSelected)) {
898 // The asserts below are theoretically valid but at the time
899 // of writing they fail because of a bug in QItemSelectionModel::selectedRows()
900 // which returns also non-selectable items.
901
902 // Q_ASSERT( selectedItem->type() == Item::Message );
903 // Q_ASSERT( ( *it ).isValid() );
904
905 if (!idx.isValid()) {
906 continue;
907 }
908
909 Item *selectedItem = static_cast<Item *>(idx.internalPointer());
910 Q_ASSERT(selectedItem);
911
912 if (selectedItem->type() != Item::Message) {
913 continue;
914 }
915
916 if (!static_cast<MessageItem *>(selectedItem)->isValid()) {
917 continue;
918 }
919
920 Q_ASSERT(!selectedMessages.contains(static_cast<MessageItem *>(selectedItem)));
921
922 if (includeCollapsedChildren && (selectedItem->childItemCount() > 0) && (!isExpanded(idx))) {
923 static_cast<MessageItem *>(selectedItem)->subTreeToList(selectedMessages);
924 } else {
925 selectedMessages.append(static_cast<MessageItem *>(selectedItem));
926 }
927 }
928
929 return selectedMessages;
930}
931
932QList<MessageItem *> View::currentThreadAsMessageItemList() const
933{
934 QList<MessageItem *> currentThread;
935
936 MessageItem *msg = currentMessageItem();
937 if (!msg) {
938 return currentThread;
939 }
940
941 while (msg->parent()) {
942 if (msg->parent()->type() != Item::Message) {
943 break;
944 }
945 msg = static_cast<MessageItem *>(msg->parent());
946 }
947
948 msg->subTreeToList(currentThread);
949
950 return currentThread;
951}
952
953void View::setChildrenExpanded(const Item *root, bool expand)
954{
955 Q_ASSERT(root);
956 auto childList = root->childItems();
957 if (!childList) {
958 return;
959 }
960 for (const auto child : std::as_const(*childList)) {
961 QModelIndex idx = d->mModel->index(child, 0);
962 Q_ASSERT(idx.isValid());
963 Q_ASSERT(static_cast<Item *>(idx.internalPointer()) == child);
964
965 if (expand) {
966 setExpanded(idx, true);
967
968 if (child->childItemCount() > 0) {
969 setChildrenExpanded(child, true);
970 }
971 } else {
972 if (child->childItemCount() > 0) {
973 setChildrenExpanded(child, false);
974 }
975
976 setExpanded(idx, false);
977 }
978 }
979}
980
981void View::ViewPrivate::generalPaletteChanged()
982{
983 const QPalette palette = q->viewport()->palette();
984 QColor color = palette.text().color();
985 color.setAlpha(128);
986 mTextColor = color;
987}
988
989void View::ViewPrivate::expandFullThread(const QModelIndex &index)
990{
991 if (!index.isValid()) {
992 return;
993 }
994
995 Item *item = static_cast<Item *>(index.internalPointer());
996 if (item->type() != Item::Message) {
997 return;
998 }
999
1000 if (!static_cast<MessageItem *>(item)->parent() || (static_cast<MessageItem *>(item)->parent()->type() != Item::Message)) {
1001 q->setChildrenExpanded(item, true);
1002 }
1003}
1004
1005void View::setCurrentThreadExpanded(bool expand)
1006{
1007 Item *it = currentItem();
1008 if (!it) {
1009 return;
1010 }
1011
1012 if (it->type() == Item::GroupHeader) {
1013 setExpanded(currentIndex(), expand);
1014 } else if (it->type() == Item::Message) {
1015 auto message = static_cast<MessageItem *>(it);
1016 while (message->parent()) {
1017 if (message->parent()->type() != Item::Message) {
1018 break;
1019 }
1020 message = static_cast<MessageItem *>(message->parent());
1021 }
1022
1023 if (expand) {
1024 setExpanded(d->mModel->index(message, 0), true);
1025 setChildrenExpanded(message, true);
1026 } else {
1027 setChildrenExpanded(message, false);
1028 setExpanded(d->mModel->index(message, 0), false);
1029 }
1030 }
1031}
1032
1033void View::setAllThreadsExpanded(bool expand)
1034{
1035 scheduleDelayedItemsLayout();
1036 if (d->mAggregation->grouping() == Aggregation::NoGrouping) {
1037 // we have no groups so threads start under the root item: just expand/unexpand all
1038 setChildrenExpanded(d->mModel->rootItem(), expand);
1039 return;
1040 }
1041
1042 // grouping is in effect: must expand/unexpand one level lower
1043
1044 auto childList = d->mModel->rootItem()->childItems();
1045 if (!childList) {
1046 return;
1047 }
1048
1049 for (const auto item : std::as_const(*childList)) {
1050 setChildrenExpanded(item, expand);
1051 }
1052}
1053
1054void View::setAllGroupsExpanded(bool expand)
1055{
1056 if (d->mAggregation->grouping() == Aggregation::NoGrouping) {
1057 return; // no grouping in effect
1058 }
1059
1060 Item *item = d->mModel->rootItem();
1061
1062 auto childList = item->childItems();
1063 if (!childList) {
1064 return;
1065 }
1066
1067 scheduleDelayedItemsLayout();
1068 for (const auto item : std::as_const(*childList)) {
1069 Q_ASSERT(item->type() == Item::GroupHeader);
1070 QModelIndex idx = d->mModel->index(item, 0);
1071 Q_ASSERT(idx.isValid());
1072 Q_ASSERT(static_cast<Item *>(idx.internalPointer()) == item);
1073 if (expand) {
1074 if (!isExpanded(idx)) {
1075 setExpanded(idx, true);
1076 }
1077 } else {
1078 if (isExpanded(idx)) {
1079 setExpanded(idx, false);
1080 }
1081 }
1082 }
1083}
1084
1085void View::selectMessageItems(const QList<MessageItem *> &list)
1086{
1087 QItemSelection selection;
1088 for (const auto mi : list) {
1089 Q_ASSERT(mi);
1090 QModelIndex idx = d->mModel->index(mi, 0);
1091 Q_ASSERT(idx.isValid());
1092 Q_ASSERT(static_cast<MessageItem *>(idx.internalPointer()) == mi);
1093 if (!selectionModel()->isSelected(idx)) {
1094 selection.append(QItemSelectionRange(idx));
1095 }
1096 ensureDisplayedWithParentsExpanded(mi);
1097 }
1098 if (!selection.isEmpty()) {
1099 selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows);
1100 }
1101}
1102
1103static inline bool message_type_matches(Item *item, MessageTypeFilter messageTypeFilter)
1104{
1105 switch (messageTypeFilter) {
1106 case MessageTypeAny:
1107 return true;
1108 break;
1109 case MessageTypeUnreadOnly:
1110 return !item->status().isRead();
1111 break;
1112 default:
1113 // nothing here
1114 break;
1115 }
1116
1117 // never reached
1118 Q_ASSERT(false);
1119 return false;
1120}
1121
1122Item *View::messageItemAfter(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop)
1123{
1124 if (!storageModel()) {
1125 return nullptr; // no folder
1126 }
1127
1128 // find the item to start with
1129 Item *below;
1130
1131 if (referenceItem) {
1132 // there was a current item: we start just below it
1133 if ((referenceItem->childItemCount() > 0) && ((messageTypeFilter != MessageTypeAny) || isExpanded(d->mModel->index(referenceItem, 0)))) {
1134 // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't)
1135 below = referenceItem->itemBelow();
1136 } else {
1137 // the current item had no children: ask the parent to find the item below
1138 Q_ASSERT(referenceItem->parent());
1139 below = referenceItem->parent()->itemBelowChild(referenceItem);
1140 }
1141
1142 if (!below) {
1143 // reached the end
1144 if (loop) {
1145 // try re-starting from top
1146 below = d->mModel->rootItem()->itemBelow();
1147 Q_ASSERT(below); // must exist (we had a current item)
1148
1149 if (below == referenceItem) {
1150 return nullptr; // only one item in folder: loop complete
1151 }
1152 } else {
1153 // looping not requested
1154 return nullptr;
1155 }
1156 }
1157 } else {
1158 // there was no current item, start from beginning
1159 below = d->mModel->rootItem()->itemBelow();
1160
1161 if (!below) {
1162 return nullptr; // folder empty
1163 }
1164 }
1165
1166 // ok.. now below points to the next message.
1167 // While it doesn't satisfy our requirements, go further down
1168
1169 QModelIndex parentIndex = d->mModel->index(below->parent(), 0);
1170 QModelIndex belowIndex = d->mModel->index(below, 0);
1171
1172 Q_ASSERT(belowIndex.isValid());
1173
1174 while (
1175 // is not a message (we want messages, don't we ?)
1176 (below->type() != Item::Message) || // message filter doesn't match
1177 (!message_type_matches(below, messageTypeFilter)) || // is hidden (and we don't want hidden items as they aren't "officially" in the view)
1178 isRowHidden(belowIndex.row(), parentIndex) || // is not enabled or not selectable
1180 // find the next one
1181 if ((below->childItemCount() > 0) && ((messageTypeFilter != MessageTypeAny) || isExpanded(belowIndex))) {
1182 // the current item had children: either expanded or we want unread messages (and so we'll expand it if it isn't)
1183 below = below->itemBelow();
1184 } else {
1185 // the current item had no children: ask the parent to find the item below
1186 Q_ASSERT(below->parent());
1187 below = below->parent()->itemBelowChild(below);
1188 }
1189
1190 if (!below) {
1191 // we reached the end of the folder
1192 if (loop) {
1193 // looping requested
1194 if (referenceItem) { // <-- this means "we have started from something that is not the top: looping makes sense"
1195 below = d->mModel->rootItem()->itemBelow();
1196 }
1197 // else mi == 0 and below == 0: we have started from the beginning and reached the end (it will fail the test below and exit)
1198 } else {
1199 // looping not requested: nothing more to do
1200 return nullptr;
1201 }
1202 }
1203
1204 if (below == referenceItem) {
1205 Q_ASSERT(loop);
1206 return nullptr; // looped and returned back to the first message
1207 }
1208
1209 parentIndex = d->mModel->index(below->parent(), 0);
1210 belowIndex = d->mModel->index(below, 0);
1211
1212 Q_ASSERT(belowIndex.isValid());
1213 }
1214
1215 return below;
1216}
1217
1219{
1220 return messageItemAfter(nullptr, messageTypeFilter, false);
1221}
1222
1223Item *View::nextMessageItem(MessageTypeFilter messageTypeFilter, bool loop)
1224{
1225 return messageItemAfter(currentMessageItem(false), messageTypeFilter, loop);
1226}
1227
1228Item *View::deepestExpandedChild(Item *referenceItem) const
1229{
1230 const int children = referenceItem->childItemCount();
1231 if (children > 0 && isExpanded(d->mModel->index(referenceItem, 0))) {
1232 return deepestExpandedChild(referenceItem->childItem(children - 1));
1233 } else {
1234 return referenceItem;
1235 }
1236}
1237
1238Item *View::messageItemBefore(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop)
1239{
1240 if (!storageModel()) {
1241 return nullptr; // no folder
1242 }
1243
1244 // find the item to start with
1245 Item *above;
1246
1247 if (referenceItem) {
1248 Item *parent = referenceItem->parent();
1249 Item *siblingAbove = parent ? parent->itemAboveChild(referenceItem) : nullptr;
1250 // there was a current item: we start just above it
1251 if ((siblingAbove && siblingAbove != referenceItem && siblingAbove != parent) && (siblingAbove->childItemCount() > 0)
1252 && ((messageTypeFilter != MessageTypeAny) || (isExpanded(d->mModel->index(siblingAbove, 0))))) {
1253 // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't)
1254 above = deepestExpandedChild(siblingAbove);
1255 } else {
1256 // the current item had no children: ask the parent to find the item above
1257 Q_ASSERT(referenceItem->parent());
1258 above = referenceItem->parent()->itemAboveChild(referenceItem);
1259 }
1260
1261 if ((!above) || (above == d->mModel->rootItem())) {
1262 // reached the beginning
1263 if (loop) {
1264 // try re-starting from bottom
1265 above = d->mModel->rootItem()->deepestItem();
1266 Q_ASSERT(above); // must exist (we had a current item)
1267 Q_ASSERT(above != d->mModel->rootItem());
1268
1269 if (above == referenceItem) {
1270 return nullptr; // only one item in folder: loop complete
1271 }
1272 } else {
1273 // looping not requested
1274 return nullptr;
1275 }
1276 }
1277 } else {
1278 // there was no current item, start from end
1279 above = d->mModel->rootItem()->deepestItem();
1280
1281 if (!above || !above->parent() || (above == d->mModel->rootItem())) {
1282 return nullptr; // folder empty
1283 }
1284 }
1285
1286 // ok.. now below points to the previous message.
1287 // While it doesn't satisfy our requirements, go further up
1288
1289 QModelIndex parentIndex = d->mModel->index(above->parent(), 0);
1290 QModelIndex aboveIndex = d->mModel->index(above, 0);
1291
1292 Q_ASSERT(aboveIndex.isValid());
1293
1294 while (
1295 // is not a message (we want messages, don't we ?)
1296 (above->type() != Item::Message) || // message filter doesn't match
1297 (!message_type_matches(above, messageTypeFilter)) || // we don't expand items but the item has parents unexpanded (so should be skipped)
1298 (
1299 // !expand items
1300 (messageTypeFilter == MessageTypeAny) && // has unexpanded parents or is itself hidden
1301 (!isDisplayedWithParentsExpanded(above)))
1302 || // is hidden
1303 isRowHidden(aboveIndex.row(), parentIndex) || // is not enabled or not selectable
1305 above = above->itemAbove();
1306
1307 if ((!above) || (above == d->mModel->rootItem())) {
1308 // reached the beginning
1309 if (loop) {
1310 // looping requested
1311 if (referenceItem) { // <-- this means "we have started from something that is not the beginning: looping makes sense"
1312 above = d->mModel->rootItem()->deepestItem();
1313 }
1314 // else mi == 0 and above == 0: we have started from the end and reached the beginning (it will fail the test below and exit)
1315 } else {
1316 // looping not requested: nothing more to do
1317 return nullptr;
1318 }
1319 }
1320
1321 if (above == referenceItem) {
1322 Q_ASSERT(loop);
1323 return nullptr; // looped and returned back to the first message
1324 }
1325
1326 if (!above->parent()) {
1327 return nullptr;
1328 }
1329
1330 parentIndex = d->mModel->index(above->parent(), 0);
1331 aboveIndex = d->mModel->index(above, 0);
1332
1333 Q_ASSERT(aboveIndex.isValid());
1334 }
1335
1336 return above;
1337}
1338
1340{
1341 return messageItemBefore(nullptr, messageTypeFilter, false);
1342}
1343
1344Item *View::previousMessageItem(MessageTypeFilter messageTypeFilter, bool loop)
1345{
1346 return messageItemBefore(currentMessageItem(false), messageTypeFilter, loop);
1347}
1348
1349void View::growOrShrinkExistingSelection(const QModelIndex &newSelectedIndex, bool movingUp)
1350{
1351 // Qt: why visualIndex() is private? ...I'd really need it here...
1352
1353 int selectedVisualCoordinate = visualRect(newSelectedIndex).top();
1354
1355 int topVisualCoordinate = 0xfffffff; // huuuuuge number
1356 int bottomVisualCoordinate = -(0xfffffff);
1357
1360
1361 // find out the actual selection range
1362 const QItemSelection selection = selectionModel()->selection();
1363
1364 for (const QItemSelectionRange &range : selection) {
1365 // We're asking the model for the index as range.topLeft() and range.bottomRight()
1366 // can return indexes in invisible columns which have a null visualRect().
1367 // Column 0, instead, is always visible.
1368
1369 QModelIndex top = d->mModel->index(range.top(), 0, range.parent());
1370 QModelIndex bottom = d->mModel->index(range.bottom(), 0, range.parent());
1371
1372 if (top.isValid()) {
1373 if (!bottom.isValid()) {
1374 bottom = top;
1375 }
1376 } else {
1377 if (!top.isValid()) {
1378 top = bottom;
1379 }
1380 }
1381 int candidate = visualRect(bottom).bottom();
1384 bottomIndex = range.bottomRight();
1385 }
1386
1387 candidate = visualRect(top).top();
1390 topIndex = range.topLeft();
1391 }
1392 }
1393
1394 if (topIndex.isValid() && bottomIndex.isValid()) {
1395 if (movingUp) {
1397 // selecting something above the top: grow selection
1399 } else {
1400 // selecting something below the top: shrink selection
1401 const QModelIndexList selectedIndexes = selection.indexes();
1402 for (const QModelIndex &idx : selectedIndexes) {
1403 if ((idx.column() == 0) && (visualRect(idx).top() > selectedVisualCoordinate)) {
1404 selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect);
1405 }
1406 }
1407 }
1408 } else {
1410 // selecting something below bottom: grow selection
1412 } else {
1413 // selecting something above bottom: shrink selection
1414 const QModelIndexList selectedIndexes = selection.indexes();
1415 for (const QModelIndex &idx : selectedIndexes) {
1416 if ((idx.column() == 0) && (visualRect(idx).top() < selectedVisualCoordinate)) {
1417 selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect);
1418 }
1419 }
1420 }
1421 }
1422 } else {
1423 // no existing selection, just grow
1425 }
1426}
1427
1429{
1430 Item *it = nextMessageItem(messageTypeFilter, loop);
1431 if (!it) {
1432 return false;
1433 }
1434
1435 if (it->parent() != d->mModel->rootItem()) {
1436 ensureDisplayedWithParentsExpanded(it);
1437 }
1438
1439 QModelIndex idx = d->mModel->index(it, 0);
1440
1441 Q_ASSERT(idx.isValid());
1442
1444 case ExpandExistingSelection:
1445 selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1446 selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1447 break;
1448 case GrowOrShrinkExistingSelection:
1449 selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1450 growOrShrinkExistingSelection(idx, false);
1451 break;
1452 default:
1453 // case ClearExistingSelection:
1454 setCurrentIndex(idx);
1455 break;
1456 }
1457
1458 if (centerItem) {
1460 }
1461
1462 return true;
1463}
1464
1466{
1467 Item *it = previousMessageItem(messageTypeFilter, loop);
1468 if (!it) {
1469 return false;
1470 }
1471
1472 if (it->parent() != d->mModel->rootItem()) {
1473 ensureDisplayedWithParentsExpanded(it);
1474 }
1475
1476 QModelIndex idx = d->mModel->index(it, 0);
1477
1478 Q_ASSERT(idx.isValid());
1479
1481 case ExpandExistingSelection:
1482 selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1483 selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1484 break;
1485 case GrowOrShrinkExistingSelection:
1486 selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1487 growOrShrinkExistingSelection(idx, true);
1488 break;
1489 default:
1490 // case ClearExistingSelection:
1491 setCurrentIndex(idx);
1492 break;
1493 }
1494
1495 if (centerItem) {
1497 }
1498
1499 return true;
1500}
1501
1502bool View::focusNextMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop)
1503{
1504 Item *it = nextMessageItem(messageTypeFilter, loop);
1505 if (!it) {
1506 return false;
1507 }
1508
1509 if (it->parent() != d->mModel->rootItem()) {
1510 ensureDisplayedWithParentsExpanded(it);
1511 }
1512
1513 QModelIndex idx = d->mModel->index(it, 0);
1514
1515 Q_ASSERT(idx.isValid());
1516
1517 selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1518
1519 if (centerItem) {
1521 }
1522
1523 return true;
1524}
1525
1526bool View::focusPreviousMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop)
1527{
1528 Item *it = previousMessageItem(messageTypeFilter, loop);
1529 if (!it) {
1530 return false;
1531 }
1532
1533 if (it->parent() != d->mModel->rootItem()) {
1534 ensureDisplayedWithParentsExpanded(it);
1535 }
1536
1537 QModelIndex idx = d->mModel->index(it, 0);
1538
1539 Q_ASSERT(idx.isValid());
1540
1541 selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1542
1543 if (centerItem) {
1545 }
1546
1547 return true;
1548}
1549
1550void View::selectFocusedMessageItem(bool centerItem)
1551{
1552 QModelIndex idx = currentIndex();
1553 if (!idx.isValid()) {
1554 return;
1555 }
1556
1557 if (selectionModel()->isSelected(idx)) {
1558 return;
1559 }
1560
1562
1563 if (centerItem) {
1565 }
1566}
1567
1568bool View::selectFirstMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem)
1569{
1570 if (!storageModel()) {
1571 return false; // nothing to do
1572 }
1573
1574 Item *it = firstMessageItem(messageTypeFilter);
1575 if (!it) {
1576 return false;
1577 }
1578
1579 Q_ASSERT(it != d->mModel->rootItem()); // must never happen (obviously)
1580
1581 ensureDisplayedWithParentsExpanded(it);
1582
1583 QModelIndex idx = d->mModel->index(it, 0);
1584
1585 Q_ASSERT(idx.isValid());
1586
1587 setCurrentIndex(idx);
1588
1589 if (centerItem) {
1591 }
1592
1593 return true;
1594}
1595
1596bool View::selectLastMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem)
1597{
1598 if (!storageModel()) {
1599 return false;
1600 }
1601
1602 Item *it = lastMessageItem(messageTypeFilter);
1603 if (!it) {
1604 return false;
1605 }
1606
1607 Q_ASSERT(it != d->mModel->rootItem());
1608
1609 ensureDisplayedWithParentsExpanded(it);
1610
1611 QModelIndex idx = d->mModel->index(it, 0);
1612
1613 Q_ASSERT(idx.isValid());
1614
1615 setCurrentIndex(idx);
1616
1617 if (centerItem) {
1619 }
1620
1621 return true;
1622}
1623
1624void View::modelFinishedLoading()
1625{
1626 Q_ASSERT(storageModel());
1627 Q_ASSERT(!d->mModel->isLoading());
1628
1629 // nothing here for now :)
1630}
1631
1632MessageItemSetReference View::createPersistentSet(const QList<MessageItem *> &items)
1633{
1634 return d->mModel->createPersistentSet(items);
1635}
1636
1637QList<MessageItem *> View::persistentSetCurrentMessageItemList(MessageItemSetReference ref)
1638{
1639 return d->mModel->persistentSetCurrentMessageItemList(ref);
1640}
1641
1642void View::deletePersistentSet(MessageItemSetReference ref)
1643{
1644 d->mModel->deletePersistentSet(ref);
1645}
1646
1647void View::markMessageItemsAsAboutToBeRemoved(const QList<MessageItem *> &items, bool bMark)
1648{
1649 if (!bMark) {
1650 for (const auto mi : items) {
1651 if (mi->isValid()) { // hasn't been removed in the meantime
1652 mi->setAboutToBeRemoved(false);
1653 }
1654 }
1655
1656 viewport()->update();
1657
1658 return;
1659 }
1660
1661 // ok.. we're going to mark the messages as "about to be deleted".
1662 // This means that we're going to make them non selectable.
1663
1664 // What happens to the selection is generally an untrackable big mess.
1665 // Several components and entities are involved.
1666
1667 // Qutie tries to apply some kind of internal logic in order to keep
1668 // "something" selected and "something" (else) to be current.
1669 // The results sometimes appear to depend on the current moon phase.
1670
1671 // The Model will do crazy things in order to preserve the current
1672 // selection (and possibly the current item). If it's impossible then
1673 // it will make its own guesses about what should be selected next.
1674 // A problem is that the Model will do it one message at a time.
1675 // When item reparenting/reordering is involved then the guesses
1676 // can produce non-intuitive results.
1677
1678 // Add the fact that selection and current item are distinct concepts,
1679 // their relative interaction depends on the settings and is often quite
1680 // unclear.
1681
1682 // Add the fact that (at the time of writing) several styles don't show
1683 // the current item (only Yoda knows why) and this causes some confusion to the user.
1684
1685 // Add the fact that the operations are asynchronous: deletion will start
1686 // a job, do some event loop processing and then complete the work at a later time.
1687 // The Qutie views also tend to accumulate the changes and perform them
1688 // all at once at the latest possible stage.
1689
1690 // A radical approach is needed: we FIRST deal with the selection
1691 // by trying to move it away from the messages about to be deleted
1692 // and THEN mark the (hopefully no longer selected) messages as "about to be deleted".
1693
1694 // First of all, find out if we're going to clear the entire selection (very likely).
1695
1696 bool clearingEntireSelection = true;
1697
1698 const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0);
1699
1700 if (selectedIndexes.count() > items.count()) {
1701 // the selection is bigger: we can't clear it completely
1703 } else {
1704 // the selection has same size or is smaller: we can clear it completely with our removal
1705 for (const QModelIndex &selectedIndex : selectedIndexes) {
1706 Q_ASSERT(selectedIndex.isValid());
1707 Q_ASSERT(selectedIndex.column() == 0);
1708
1709 Item *selectedItem = static_cast<Item *>(selectedIndex.internalPointer());
1710 Q_ASSERT(selectedItem);
1711
1712 if (selectedItem->type() != Item::Message) {
1713 continue;
1714 }
1715
1716 if (!items.contains(static_cast<MessageItem *>(selectedItem))) {
1717 // the selection contains something that we aren't going to remove:
1718 // we will not clear the selection completely
1720 break;
1721 }
1722 }
1723 }
1724
1726 // Try to clear the current selection and select something sensible instead,
1727 // so after the deletion we will not end up with a random selection.
1728 // Pick up a message in the set (which is very likely to be contiguous), walk the tree
1729 // and select the next message that is NOT in the set.
1730
1731 MessageItem *aMessage = items.last();
1733
1734 // Avoid infinite loops by carrying only a limited number of attempts.
1735 // If there is any message that is not in the set then items.count() attempts should find it.
1736 int maxAttempts = items.count();
1737
1738 while (items.contains(aMessage) && (maxAttempts > 0)) {
1739 Item *next = messageItemAfter(aMessage, MessageTypeAny, false);
1740 if (!next) {
1741 // no way
1742 aMessage = nullptr;
1743 break;
1744 }
1745 Q_ASSERT(next->type() == Item::Message);
1746 aMessage = static_cast<MessageItem *>(next);
1747 maxAttempts--;
1748 }
1749
1750 if (!aMessage) {
1751 // try backwards
1752 aMessage = items.first();
1754 maxAttempts = items.count();
1755
1756 while (items.contains(aMessage) && (maxAttempts > 0)) {
1757 Item *prev = messageItemBefore(aMessage, MessageTypeAny, false);
1758 if (!prev) {
1759 // no way
1760 aMessage = nullptr;
1761 break;
1762 }
1763 Q_ASSERT(prev->type() == Item::Message);
1764 aMessage = static_cast<MessageItem *>(prev);
1765 maxAttempts--;
1766 }
1767 }
1768
1769 if (aMessage) {
1770 QModelIndex aMessageIndex = d->mModel->index(aMessage, 0);
1771 Q_ASSERT(aMessageIndex.isValid());
1772 Q_ASSERT(static_cast<MessageItem *>(aMessageIndex.internalPointer()) == aMessage);
1773 Q_ASSERT(!selectionModel()->isSelected(aMessageIndex));
1774 setCurrentIndex(aMessageIndex);
1776 }
1777 } // else we aren't clearing the entire selection so something should just stay selected.
1778
1779 // Now mark messages as about to be removed.
1780
1781 for (const auto mi : items) {
1782 mi->setAboutToBeRemoved(true);
1783 QModelIndex idx = d->mModel->index(mi, 0);
1784 Q_ASSERT(idx.isValid());
1785 Q_ASSERT(static_cast<MessageItem *>(idx.internalPointer()) == mi);
1786 if (selectionModel()->isSelected(idx)) {
1787 selectionModel()->select(idx, QItemSelectionModel::Deselect | QItemSelectionModel::Rows);
1788 }
1789 }
1790
1791 viewport()->update();
1792}
1793
1794void View::ensureDisplayedWithParentsExpanded(Item *it)
1795{
1796 Q_ASSERT(it);
1797 Q_ASSERT(it->parent());
1798 Q_ASSERT(it->isViewable()); // must be attached to the viewable root
1799
1800 if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) {
1801 setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false);
1802 }
1803
1804 it = it->parent();
1805
1806 while (it->parent()) {
1807 if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) {
1808 setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false);
1809 }
1810
1811 QModelIndex idx = d->mModel->index(it, 0);
1812
1813 Q_ASSERT(idx.isValid());
1814 Q_ASSERT(static_cast<Item *>(idx.internalPointer()) == it);
1815
1816 if (!isExpanded(idx)) {
1817 setExpanded(idx, true);
1818 }
1819
1820 it = it->parent();
1821 }
1822}
1823
1824bool View::isDisplayedWithParentsExpanded(Item *it) const
1825{
1826 // An item is currently viewable iff
1827 // - it is marked as viewable in the item structure (that is, qt knows about its existence)
1828 // (and this means that all of its parents are marked as viewable)
1829 // - it is not explicitly hidden
1830 // - all of its parents are expanded
1831
1832 if (!it) {
1833 return false; // be nice and allow the caller not to care
1834 }
1835
1836 if (!it->isViewable()) {
1837 return false; // item not viewable (not attached to the viewable root or qt not yet aware of it)
1838 }
1839
1840 // the item and all the parents are marked as viewable.
1841
1842 if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) {
1843 return false; // item qt representation explicitly hidden
1844 }
1845
1846 // the item (and theoretically all the parents) are not explicitly hidden
1847
1848 // check the parent chain
1849
1850 it = it->parent();
1851
1852 while (it) {
1853 if (it == d->mModel->rootItem()) {
1854 return true; // parent is root item: ok
1855 }
1856
1857 // parent is not root item
1858
1859 if (!isExpanded(d->mModel->index(it, 0))) {
1860 return false; // parent is not expanded (so child not actually visible)
1861 }
1862
1863 it = it->parent(); // climb up
1864 }
1865
1866 // parent hierarchy interrupted somewhere
1867 return false;
1868}
1869
1870bool View::isThreaded() const
1871{
1872 if (!d->mAggregation) {
1873 return false;
1874 }
1875 return d->mAggregation->threading() != Aggregation::NoThreading;
1876}
1877
1878void View::slotSelectionChanged(const QItemSelection &, const QItemSelection &)
1879{
1880 // We assume that when selection changes, current item also changes.
1881 QModelIndex current = currentIndex();
1882
1883 if (!current.isValid()) {
1884 d->mLastCurrentItem = nullptr;
1885 d->mWidget->viewMessageSelected(nullptr);
1886 d->mWidget->viewSelectionChanged();
1887 return;
1888 }
1889
1890 if (!selectionModel()->isSelected(current)) {
1891 if (selectedIndexes().count() < 1) {
1892 // It may happen after row removals: Model calls this slot on currentIndex()
1893 // that actually might have changed "silently", without being selected.
1894 QItemSelection selection;
1895 selection.append(QItemSelectionRange(current));
1896 selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows);
1897 return; // the above recurses
1898 } else {
1899 // something is still selected anyway
1900 // This is probably a result of CTRL+Click which unselected current: leave it as it is.
1901 return;
1902 }
1903 }
1904
1905 Item *it = static_cast<Item *>(current.internalPointer());
1906 Q_ASSERT(it);
1907
1908 switch (it->type()) {
1909 case Item::Message:
1910 if (d->mLastCurrentItem != it) {
1911 qCDebug(MESSAGELIST_LOG) << "View message selected [" << static_cast<MessageItem *>(it)->subject() << "]";
1912 d->mWidget->viewMessageSelected(static_cast<MessageItem *>(it));
1913 d->mLastCurrentItem = it;
1914 }
1915 break;
1916 case Item::GroupHeader:
1917 if (d->mLastCurrentItem) {
1918 d->mWidget->viewMessageSelected(nullptr);
1919 d->mLastCurrentItem = nullptr;
1920 }
1921 break;
1922 default:
1923 // should never happen
1924 Q_ASSERT(false);
1925 break;
1926 }
1927
1928 d->mWidget->viewSelectionChanged();
1929}
1930
1931void View::mouseDoubleClickEvent(QMouseEvent *e)
1932{
1933 // Perform a hit test
1934 if (!d->mDelegate->hitTest(e->pos(), true)) {
1935 return;
1936 }
1937
1938 // Something was hit :)
1939
1940 Item *it = static_cast<Item *>(d->mDelegate->hitItem());
1941 if (!it) {
1942 return; // should never happen
1943 }
1944
1945 switch (it->type()) {
1946 case Item::Message:
1947 // Let QTreeView handle the expansion
1949
1950 switch (e->button()) {
1951 case Qt::LeftButton:
1952
1953 if (d->mDelegate->hitContentItem()) {
1954 // Double clicking on clickable icons does NOT activate the message
1955 if (d->mDelegate->hitContentItem()->isIcon() && d->mDelegate->hitContentItem()->isClickable()) {
1956 return;
1957 }
1958 }
1959
1960 d->mWidget->viewMessageActivated(static_cast<MessageItem *>(it));
1961 break;
1962 default:
1963 // make gcc happy
1964 break;
1965 }
1966 break;
1967 case Item::GroupHeader:
1968 // Don't let QTreeView handle the selection (as it deselects the current messages)
1969 switch (e->button()) {
1970 case Qt::LeftButton:
1971 if (it->childItemCount() > 0) {
1972 // toggle expanded state
1973 setExpanded(d->mDelegate->hitIndex(), !isExpanded(d->mDelegate->hitIndex()));
1974 }
1975 break;
1976 default:
1977 // make gcc happy
1978 break;
1979 }
1980 break;
1981 default:
1982 // should never happen
1983 Q_ASSERT(false);
1984 break;
1985 }
1986}
1987
1988void View::changeMessageStatusRead(MessageItem *it, bool read)
1989{
1990 Akonadi::MessageStatus set = it->status();
1991 Akonadi::MessageStatus unset = it->status();
1992 if (read) {
1993 set.setRead(true);
1994 unset.setRead(false);
1995 } else {
1996 set.setRead(false);
1997 unset.setRead(true);
1998 }
1999 viewport()->update();
2000
2001 // This will actually request the widget to perform a status change on the storage.
2002 // The request will be then processed by the Model and the message will be updated again.
2003
2004 d->mWidget->viewMessageStatusChangeRequest(it, set, unset);
2005}
2006
2008{
2009 // We first change the status of MessageItem itself. This will make the change
2010 // visible to the user even if the Model is actually in the middle of a long job (maybe it's loading)
2011 // and can't process the status change request immediately.
2012 // Here we actually desynchronize the cache and trust that the later call to
2013 // d->mWidget->viewMessageStatusChangeRequest() will really perform the status change on the storage.
2014 // Well... in KMail it will unless something is really screwed. Anyway, if it will not, at the next
2015 // load the status will be just unchanged: no animals will be harmed.
2016
2017 qint32 stat = it->status().toQInt32();
2018 stat |= set.toQInt32();
2019 stat &= ~(unset.toQInt32());
2021 status.fromQInt32(stat);
2022 it->setStatus(status);
2023
2024 // Trigger an update so the immediate change will be shown to the user
2025
2026 viewport()->update();
2027
2028 // This will actually request the widget to perform a status change on the storage.
2029 // The request will be then processed by the Model and the message will be updated again.
2030
2031 d->mWidget->viewMessageStatusChangeRequest(it, set, unset);
2032}
2033
2034void View::mousePressEvent(QMouseEvent *e)
2035{
2036 d->mMousePressed = true;
2037 d->mLastMouseSource = e->source();
2038
2039 if (d->mIsTouchEvent) {
2040 return;
2041 }
2042
2043 d->onPressed(e);
2044}
2045
2046void View::mouseMoveEvent(QMouseEvent *e)
2047{
2048 if (d->mIsTouchEvent && !d->mTapAndHoldActive) {
2049 return;
2050 }
2051
2052 if (!(e->buttons() & Qt::LeftButton)) {
2054 return;
2055 }
2056
2057 if (d->mMousePressPosition.isNull()) {
2058 return;
2059 }
2060
2061 if ((e->pos() - d->mMousePressPosition).manhattanLength() <= QApplication::startDragDistance()) {
2062 return;
2063 }
2064
2065 d->mTapAndHoldActive = false;
2066 if (d->mRubberBand->isVisible()) {
2067 d->mRubberBand->hide();
2068 }
2069
2070 d->mWidget->viewStartDragRequest();
2071}
2072
2073#if 0
2074void View::contextMenuEvent(QContextMenuEvent *e)
2075{
2076 Q_UNUSED(e)
2077 QModelIndex index = currentIndex();
2078 if (index.isValid()) {
2079 QRect indexRect = this->visualRect(index);
2080 QPoint pos;
2081
2082 if ((indexRect.isValid()) && (indexRect.bottom() > 0)) {
2083 if (indexRect.bottom() > viewport()->height()) {
2084 if (indexRect.top() <= viewport()->height()) {
2085 pos = indexRect.topLeft();
2086 }
2087 } else {
2088 pos = indexRect.bottomLeft();
2089 }
2090 }
2091
2092 Item *item = static_cast< Item * >(index.internalPointer());
2093 if (item) {
2094 if (item->type() == Item::GroupHeader) {
2095 d->mWidget->viewGroupHeaderContextPopupRequest(static_cast< GroupHeaderItem * >(item), viewport()->mapToGlobal(pos));
2096 } else if (!selectionEmpty()) {
2097 d->mWidget->viewMessageListContextPopupRequest(selectionAsMessageItemList(), viewport()->mapToGlobal(pos));
2098 e->accept();
2099 }
2100 }
2101 }
2102}
2103
2104#endif
2105
2106void View::dragEnterEvent(QDragEnterEvent *e)
2107{
2108 d->mWidget->viewDragEnterEvent(e);
2109}
2110
2111void View::dragMoveEvent(QDragMoveEvent *e)
2112{
2113 d->mWidget->viewDragMoveEvent(e);
2114}
2115
2116void View::dropEvent(QDropEvent *e)
2117{
2118 d->mWidget->viewDropEvent(e);
2119}
2120
2121void View::changeEvent(QEvent *e)
2122{
2123 switch (e->type()) {
2124 case QEvent::FontChange:
2125 d->mDelegate->generalFontChanged();
2126 [[fallthrough]];
2132 // All of these affect the theme's internal cache.
2133 setTheme(d->mTheme);
2134 // A layoutChanged() event will screw up the view state a bit.
2135 // Since this is a rare event we just reload the view.
2136 reload();
2137 break;
2138 default:
2139 // make gcc happy by default
2140 break;
2141 }
2142
2144}
2145
2147{
2148 if (e->type() == QEvent::TouchBegin) {
2149 d->mIsTouchEvent = true;
2150 d->mMousePressed = false;
2151 return false;
2152 }
2153
2154 if (e->type() == QEvent::Gesture) {
2155 d->gestureEvent(static_cast<QGestureEvent *>(e));
2156 e->accept();
2157 return true;
2158 }
2159
2160 // We catch ToolTip events and pass everything else
2161
2162 if (e->type() != QEvent::ToolTip) {
2163 return QTreeView::event(e);
2164 }
2165
2166 if (!MessageListSettings::self()->messageToolTipEnabled()) {
2167 return true; // don't display tooltips
2168 }
2169
2170 auto he = dynamic_cast<QHelpEvent *>(e);
2171 if (!he) {
2172 return true; // eh ?
2173 }
2174
2175 QPoint pnt = viewport()->mapFromGlobal(mapToGlobal(he->pos()));
2176
2177 if (pnt.y() < 0) {
2178 return true; // don't display the tooltip for items hidden under the header
2179 }
2180
2181 QModelIndex idx = indexAt(pnt);
2182 if (!idx.isValid()) {
2183 return true; // may be
2184 }
2185
2186 Item *it = static_cast<Item *>(idx.internalPointer());
2187 if (!it) {
2188 return true; // hum
2189 }
2190
2191 Q_ASSERT(storageModel());
2192
2195 QColor darkerColor(((bckColor.red() * 8) + (txtColor.red() * 2)) / 10,
2196 ((bckColor.green() * 8) + (txtColor.green() * 2)) / 10,
2197 ((bckColor.blue() * 8) + (txtColor.blue() * 2)) / 10);
2198
2199 QString bckColorName = bckColor.name();
2200 QString txtColorName = txtColor.name();
2203 const QString textDirection = textIsLeftToRight ? QStringLiteral("left") : QStringLiteral("right");
2204
2205 QString tip = QStringLiteral("<table width=\"100%\" border=\"0\" cellpadding=\"2\" cellspacing=\"0\">");
2206
2207 switch (it->type()) {
2208 case Item::Message: {
2209 auto mi = static_cast<MessageItem *>(it);
2210
2211 tip += QStringLiteral(
2212 "<tr>"
2213 "<td bgcolor=\"%1\" align=\"%4\" valign=\"middle\">"
2214 "<div style=\"color: %2; font-weight: bold;\">"
2215 "%3"
2216 "</div>"
2217 "</td>"
2218 "</tr>")
2219 .arg(txtColorName, bckColorName, mi->subject().toHtmlEscaped(), textDirection);
2220
2221 tip += QLatin1StringView(
2222 "<tr>"
2223 "<td align=\"center\" valign=\"middle\">"
2224 "<table width=\"100%\" border=\"0\" cellpadding=\"2\" cellspacing=\"0\">");
2225
2226 const QString htmlCodeForStandardRow = QStringLiteral(
2227 "<tr>"
2228 "<td align=\"right\" valign=\"top\" width=\"45\">"
2229 "<div style=\"font-weight: bold;\"><nobr>"
2230 "%1:"
2231 "</nobr></div>"
2232 "</td>"
2233 "<td align=\"left\" valign=\"top\">"
2234 "%2"
2235 "</td>"
2236 "</tr>");
2237
2238 if (textIsLeftToRight) {
2239 tip += htmlCodeForStandardRow.arg(i18n("From"), mi->displaySender().toHtmlEscaped());
2240 tip += htmlCodeForStandardRow.arg(i18nc("Receiver of the email", "To"), mi->displayReceiver().toHtmlEscaped());
2241 tip += htmlCodeForStandardRow.arg(i18n("Date"), mi->formattedDate());
2242 } else {
2243 tip += htmlCodeForStandardRow.arg(mi->displaySender().toHtmlEscaped(), i18n("From"));
2244 tip += htmlCodeForStandardRow.arg(mi->displayReceiver().toHtmlEscaped(), i18nc("Receiver of the email", "To"));
2245 tip += htmlCodeForStandardRow.arg(mi->formattedDate(), i18n("Date"));
2246 }
2247
2248 QString status = mi->statusDescription();
2249 const QString tags = mi->tagListDescription();
2250 if (!tags.isEmpty()) {
2251 if (!status.isEmpty()) {
2252 status += QLatin1StringView(", ");
2253 }
2254 status += tags;
2255 }
2256
2257 if (textIsLeftToRight) {
2258 tip += htmlCodeForStandardRow.arg(i18n("Status"), status);
2259 tip += htmlCodeForStandardRow.arg(i18n("Size"), mi->formattedSize());
2260 tip += htmlCodeForStandardRow.arg(i18n("Folder"), mi->folder());
2261 } else {
2262 tip += htmlCodeForStandardRow.arg(status, i18n("Status"));
2263 tip += htmlCodeForStandardRow.arg(mi->formattedSize(), i18n("Size"));
2264 tip += htmlCodeForStandardRow.arg(mi->folder(), i18n("Folder"));
2265 }
2266
2267 if (mi->hasAnnotation()) {
2268 if (textIsLeftToRight) {
2269 tip += htmlCodeForStandardRow.arg(i18n("Note"), mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("<br>")));
2270 } else {
2271 tip += htmlCodeForStandardRow.arg(mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("<br>"))).arg(i18n("Note"));
2272 }
2273 }
2274
2275 QString content = MessageList::Util::contentSummary(mi->akonadiItem());
2276 if (!content.trimmed().isEmpty()) {
2277 if (textIsLeftToRight) {
2278 tip += htmlCodeForStandardRow.arg(i18n("Preview"), content.replace(QLatin1Char('\n'), QStringLiteral("<br>")));
2279 } else {
2280 tip += htmlCodeForStandardRow.arg(content.replace(QLatin1Char('\n'), QStringLiteral("<br>"))).arg(i18n("Preview"));
2281 }
2282 }
2283
2284 tip += QLatin1StringView(
2285 "</table>"
2286 "</td>"
2287 "</tr>");
2288
2289 // FIXME: Find a way to show also CC and other header fields ?
2290
2291 if (mi->hasChildren()) {
2293 mi->childItemStats(stats);
2294
2296
2297 statsText = i18np("<b>%1</b> reply", "<b>%1</b> replies", mi->childItemCount());
2299
2300 statsText += i18np("<b>%1</b> message in subtree (<b>%2</b> unread)",
2301 "<b>%1</b> messages in subtree (<b>%2</b> unread)",
2302 stats.mTotalChildCount,
2303 stats.mUnreadChildCount);
2304
2305 tip += QStringLiteral(
2306 "<tr>"
2307 "<td bgcolor=\"%1\" align=\"%3\" valign=\"middle\">"
2308 "<nobr>%2</nobr>"
2309 "</td>"
2310 "</tr>")
2311 .arg(darkerColorName, statsText, textDirection);
2312 }
2313
2314 break;
2315 }
2316 case Item::GroupHeader: {
2317 auto ghi = static_cast<GroupHeaderItem *>(it);
2318
2319 tip += QStringLiteral(
2320 "<tr>"
2321 "<td bgcolor=\"%1\" align=\"%4\" valign=\"middle\">"
2322 "<div style=\"color: %2; font-weight: bold;\">"
2323 "%3"
2324 "</div>"
2325 "</td>"
2326 "</tr>")
2327 .arg(txtColorName, bckColorName, ghi->label(), textDirection);
2328
2329 QString description;
2330
2331 switch (d->mAggregation->grouping()) {
2333 if (d->mAggregation->threading() != Aggregation::NoThreading) {
2334 switch (d->mAggregation->threadLeader()) {
2336 if (ghi->label().contains(QRegularExpression(QStringLiteral("[0-9]")))) {
2337 description = i18nc("@info:tooltip Formats to something like 'Threads started on 2008-12-21'", "Threads started on %1", ghi->label());
2338 } else {
2339 description = i18nc("@info:tooltip Formats to something like 'Threads started Yesterday'", "Threads started %1", ghi->label());
2340 }
2341 break;
2343 description = i18n("Threads with messages dated %1", ghi->label());
2344 break;
2345 default:
2346 // nuthin, make gcc happy
2347 break;
2348 }
2349 } else {
2350 static const QRegularExpression reg(QStringLiteral("[0-9]"));
2351 if (ghi->label().contains(reg)) {
2352 if (storageModel()->containsOutboundMessages()) {
2353 description = i18nc("@info:tooltip Formats to something like 'Messages sent on 2008-12-21'", "Messages sent on %1", ghi->label());
2354 } else {
2355 description =
2356 i18nc("@info:tooltip Formats to something like 'Messages received on 2008-12-21'", "Messages received on %1", ghi->label());
2357 }
2358 } else {
2359 if (storageModel()->containsOutboundMessages()) {
2360 description = i18nc("@info:tooltip Formats to something like 'Messages sent Yesterday'", "Messages sent %1", ghi->label());
2361 } else {
2362 description = i18nc("@info:tooltip Formats to something like 'Messages received Yesterday'", "Messages received %1", ghi->label());
2363 }
2364 }
2365 }
2366 break;
2368 if (d->mAggregation->threading() != Aggregation::NoThreading) {
2369 switch (d->mAggregation->threadLeader()) {
2371 description = i18n("Threads started within %1", ghi->label());
2372 break;
2374 description = i18n("Threads containing messages with dates within %1", ghi->label());
2375 break;
2376 default:
2377 // nuthin, make gcc happy
2378 break;
2379 }
2380 } else {
2381 if (storageModel()->containsOutboundMessages()) {
2382 description = i18n("Messages sent within %1", ghi->label());
2383 } else {
2384 description = i18n("Messages received within %1", ghi->label());
2385 }
2386 }
2387 break;
2390 if (d->mAggregation->threading() != Aggregation::NoThreading) {
2391 switch (d->mAggregation->threadLeader()) {
2393 description = i18n("Threads started by %1", ghi->label());
2394 break;
2396 description = i18n("Threads with most recent message by %1", ghi->label());
2397 break;
2398 default:
2399 // nuthin, make gcc happy
2400 break;
2401 }
2402 } else {
2403 if (storageModel()->containsOutboundMessages()) {
2404 if (d->mAggregation->grouping() == Aggregation::GroupBySenderOrReceiver) {
2405 description = i18n("Messages sent to %1", ghi->label());
2406 } else {
2407 description = i18n("Messages sent by %1", ghi->label());
2408 }
2409 } else {
2410 description = i18n("Messages received from %1", ghi->label());
2411 }
2412 }
2413 break;
2415 if (d->mAggregation->threading() != Aggregation::NoThreading) {
2416 switch (d->mAggregation->threadLeader()) {
2418 description = i18n("Threads directed to %1", ghi->label());
2419 break;
2421 description = i18n("Threads with most recent message directed to %1", ghi->label());
2422 break;
2423 default:
2424 // nuthin, make gcc happy
2425 break;
2426 }
2427 } else {
2428 if (storageModel()->containsOutboundMessages()) {
2429 description = i18n("Messages sent to %1", ghi->label());
2430 } else {
2431 description = i18n("Messages received by %1", ghi->label());
2432 }
2433 }
2434 break;
2435 default:
2436 // nuthin, make gcc happy
2437 break;
2438 }
2439
2440 if (!description.isEmpty()) {
2441 tip += QStringLiteral(
2442 "<tr>"
2443 "<td align=\"%2\" valign=\"middle\">"
2444 "%1"
2445 "</td>"
2446 "</tr>")
2447 .arg(description, textDirection);
2448 }
2449
2450 if (ghi->hasChildren()) {
2452 ghi->childItemStats(stats);
2453
2455
2456 if (d->mAggregation->threading() != Aggregation::NoThreading) {
2457 statsText = i18np("<b>%1</b> thread", "<b>%1</b> threads", ghi->childItemCount());
2459 }
2460
2461 statsText +=
2462 i18np("<b>%1</b> message (<b>%2</b> unread)", "<b>%1</b> messages (<b>%2</b> unread)", stats.mTotalChildCount, stats.mUnreadChildCount);
2463
2464 tip += QStringLiteral(
2465 "<tr>"
2466 "<td bgcolor=\"%1\" align=\"%3\" valign=\"middle\">"
2467 "<nobr>%2</nobr>"
2468 "</td>"
2469 "</tr>")
2470 .arg(darkerColorName, statsText, textDirection);
2471 }
2472
2473 break;
2474 }
2475 default:
2476 // nuthin (just make gcc happy for now)
2477 break;
2478 }
2479
2480 tip += QLatin1StringView("</table>");
2481
2482 QToolTip::showText(he->globalPos(), tip, viewport(), visualRect(idx));
2483
2484 return true;
2485}
2486
2487void View::slotExpandAllThreads()
2488{
2489 setAllThreadsExpanded(true);
2490}
2491
2492void View::slotCollapseAllThreads()
2493{
2494 setAllThreadsExpanded(false);
2495}
2496
2497void View::slotCollapseAllGroups()
2498{
2499 setAllGroupsExpanded(false);
2500}
2501
2502void View::slotExpandAllGroups()
2503{
2504 setAllGroupsExpanded(true);
2505}
2506
2507void View::slotCollapseCurrentItem()
2508{
2509 setCurrentThreadExpanded(false);
2510}
2511
2512void View::slotExpandCurrentItem()
2513{
2514 setCurrentThreadExpanded(true);
2515}
2516
2517void View::focusQuickSearch(const QString &selectedText)
2518{
2519 d->mWidget->focusQuickSearch(selectedText);
2520}
2521
2522QList<Akonadi::MessageStatus> View::currentFilterStatus() const
2523{
2524 return d->mWidget->currentFilterStatus();
2525}
2526
2527MessageList::Core::QuickSearchLine::SearchOptions View::currentOptions() const
2528{
2529 return d->mWidget->currentOptions();
2530}
2531
2532QString View::currentFilterSearchString() const
2533{
2534 return d->mWidget->currentFilterSearchString();
2535}
2536
2537void View::setRowHidden(int row, const QModelIndex &parent, bool hide)
2538{
2539 const QModelIndex rowModelIndex = model()->index(row, 0, parent);
2540 const Item *const rowItem = static_cast<Item *>(rowModelIndex.internalPointer());
2541
2542 if (rowItem) {
2543 const bool currentlyHidden = isRowHidden(row, parent);
2544
2545 if (currentlyHidden != hide) {
2546 if (currentMessageItem() == rowItem) {
2547 selectionModel()->clear();
2548 selectionModel()->clearSelection();
2549 }
2550 }
2551 }
2552
2553 QTreeView::setRowHidden(row, parent, hide);
2554}
2555
2556void View::sortOrderMenuAboutToShow(QMenu *menu)
2557{
2558 d->mWidget->sortOrderMenuAboutToShow(menu);
2559}
2560
2561void View::aggregationMenuAboutToShow(QMenu *menu)
2562{
2563 d->mWidget->aggregationMenuAboutToShow(menu);
2564}
2565
2566void View::themeMenuAboutToShow(QMenu *menu)
2567{
2568 d->mWidget->themeMenuAboutToShow(menu);
2569}
2570
2571void View::setCollapseItem(const QModelIndex &index)
2572{
2573 if (index.isValid()) {
2574 setExpanded(index, false);
2575 }
2576}
2577
2578void View::setExpandItem(const QModelIndex &index)
2579{
2580 if (index.isValid()) {
2581 setExpanded(index, true);
2582 }
2583}
2584
2585void View::setQuickSearchClickMessage(const QString &msg)
2586{
2587 d->mWidget->quickSearch()->setPlaceholderText(msg);
2588}
2589
2590void View::ViewPrivate::onPressed(QMouseEvent *e)
2591{
2592 mMousePressPosition = QPoint();
2593
2594 // Perform a hit test
2595 if (!mDelegate->hitTest(e->pos(), true)) {
2596 return;
2597 }
2598
2599 // Something was hit :)
2600
2601 Item *it = static_cast<Item *>(mDelegate->hitItem());
2602 if (!it) {
2603 return; // should never happen
2604 }
2605
2606 // Abort any pending message pre-selection as the user is probably
2607 // already navigating the view (so pre-selection would make his view jump
2608 // to an unexpected place).
2609 mModel->setPreSelectionMode(PreSelectNone);
2610
2611 switch (it->type()) {
2612 case Item::Message:
2613 mMousePressPosition = e->pos();
2614
2615 switch (e->button()) {
2616 case Qt::LeftButton:
2617 // if we have multi selection then the meaning of hitting
2618 // the content item is quite unclear.
2619 if (mDelegate->hitContentItem() && (q->selectedIndexes().count() > 1)) {
2620 qCDebug(MESSAGELIST_LOG) << "Left hit with selectedIndexes().count() == " << q->selectedIndexes().count();
2621
2622 switch (mDelegate->hitContentItem()->type()) {
2624 static_cast<MessageItem *>(it)->editAnnotation(q);
2625 return; // don't select the item
2626 break;
2628 q->changeMessageStatus(static_cast<MessageItem *>(it),
2629 it->status().isToAct() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusToAct(),
2630 it->status().isToAct() ? Akonadi::MessageStatus::statusToAct() : Akonadi::MessageStatus());
2631 return; // don't select the item
2632 break;
2634 q->changeMessageStatus(static_cast<MessageItem *>(it),
2635 it->status().isImportant() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusImportant(),
2636 it->status().isImportant() ? Akonadi::MessageStatus::statusImportant() : Akonadi::MessageStatus());
2637 return; // don't select the item
2639 q->changeMessageStatusRead(static_cast<MessageItem *>(it), it->status().isRead() ? false : true);
2640 return;
2641 break;
2643 q->changeMessageStatus(static_cast<MessageItem *>(it),
2644 it->status().isSpam()
2646 : (it->status().isHam() ? Akonadi::MessageStatus::statusSpam() : Akonadi::MessageStatus::statusHam()),
2647 it->status().isSpam() ? Akonadi::MessageStatus::statusSpam()
2648 : (it->status().isHam() ? Akonadi::MessageStatus::statusHam() : Akonadi::MessageStatus()));
2649 return; // don't select the item
2650 break;
2652 q->changeMessageStatus(static_cast<MessageItem *>(it),
2653 it->status().isIgnored()
2655 : (it->status().isWatched() ? Akonadi::MessageStatus::statusIgnored() : Akonadi::MessageStatus::statusWatched()),
2656 it->status().isIgnored()
2657 ? Akonadi::MessageStatus::statusIgnored()
2658 : (it->status().isWatched() ? Akonadi::MessageStatus::statusWatched() : Akonadi::MessageStatus()));
2659 return; // don't select the item
2660 break;
2661 default:
2662 // make gcc happy
2663 break;
2664 }
2665 }
2666
2667 // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called)
2668 q->QTreeView::mousePressEvent(e);
2669
2670 break;
2671 case Qt::RightButton:
2672 // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called)
2673 q->QTreeView::mousePressEvent(e);
2674 e->accept();
2675 mWidget->viewMessageListContextPopupRequest(q->selectionAsMessageItemList(), q->viewport()->mapToGlobal(e->pos()));
2676
2677 break;
2678 default:
2679 // make gcc happy
2680 break;
2681 }
2682 break;
2683 case Item::GroupHeader: {
2684 // Don't let QTreeView handle the selection (as it deselects the current messages)
2685 auto groupHeaderItem = static_cast<GroupHeaderItem *>(it);
2686
2687 switch (e->button()) {
2688 case Qt::LeftButton: {
2689 QModelIndex index = mModel->index(groupHeaderItem, 0);
2690
2691 if (index.isValid()) {
2692 q->setCurrentIndex(index);
2693 }
2694
2695 if (!mDelegate->hitContentItem()) {
2696 return;
2697 }
2698
2699 if (mDelegate->hitContentItem()->type() == Theme::ContentItem::ExpandedStateIcon) {
2700 if (groupHeaderItem->childItemCount() > 0) {
2701 // toggle expanded state
2702 q->setExpanded(mDelegate->hitIndex(), !q->isExpanded(mDelegate->hitIndex()));
2703 }
2704 }
2705 break;
2706 }
2707 case Qt::RightButton:
2708 mWidget->viewGroupHeaderContextPopupRequest(groupHeaderItem, q->viewport()->mapToGlobal(e->pos()));
2709 break;
2710 default:
2711 // make gcc happy
2712 break;
2713 }
2714 break;
2715 }
2716 default:
2717 // should never happen
2718 Q_ASSERT(false);
2719 break;
2720 }
2721}
2722
2723void View::ViewPrivate::gestureEvent(QGestureEvent *e)
2724{
2725 if (QGesture *gesture = e->gesture(Qt::TapGesture)) {
2726 tapTriggered(static_cast<QTapGesture *>(gesture));
2727 }
2728 if (QGesture *gesture = e->gesture(Qt::TapAndHoldGesture)) {
2729 tapAndHoldTriggered(static_cast<QTapAndHoldGesture *>(gesture));
2730 }
2731 if (QGesture *gesture = e->gesture(mTwoFingerTap)) {
2732 twoFingerTapTriggered(static_cast<KTwoFingerTap *>(gesture));
2733 }
2734}
2735
2736void View::ViewPrivate::tapTriggered(QTapGesture *tap)
2737{
2738 static bool scrollerWasScrolling = false;
2739
2740 if (tap->state() == Qt::GestureStarted) {
2741 mTapAndHoldActive = false;
2742
2743 // if QScroller state is Scrolling or Dragging, the user makes the tap to stop the scrolling
2744 if (mScroller->state() == QScroller::Scrolling || mScroller->state() == QScroller::Dragging) {
2745 scrollerWasScrolling = true;
2746 } else if (mScroller->state() == QScroller::Pressed || mScroller->state() == QScroller::Inactive) {
2747 scrollerWasScrolling = false;
2748 }
2749 }
2750
2751 if (tap->state() == Qt::GestureFinished && !scrollerWasScrolling) {
2752 mIsTouchEvent = false;
2753
2754 // with touch you can touch multiple widgets at the same time, but only one widget will get a mousePressEvent.
2755 // we use this to select the right window
2756 if (!mMousePressed) {
2757 return;
2758 }
2759
2760 if (mRubberBand->isVisible()) {
2761 mRubberBand->hide();
2762 }
2763
2764 // simulate a mousePressEvent, to allow QTreeView to select the items
2766 tap->position(),
2767 q->viewport()->mapToGlobal(tap->position()),
2768 mTapAndHoldActive ? Qt::RightButton : Qt::LeftButton,
2769 mTapAndHoldActive ? Qt::RightButton : Qt::LeftButton,
2770 Qt::NoModifier);
2771
2772 onPressed(&fakeMousePress);
2773 mTapAndHoldActive = false;
2774 }
2775
2776 if (tap->state() == Qt::GestureCanceled) {
2777 mIsTouchEvent = false;
2778 if (mRubberBand->isVisible()) {
2779 mRubberBand->hide();
2780 }
2781 mTapAndHoldActive = false;
2782 }
2783}
2784
2785void View::ViewPrivate::tapAndHoldTriggered(QTapAndHoldGesture *tap)
2786{
2787 if (tap->state() == Qt::GestureFinished) {
2788 // with touch you can touch multiple widgets at the same time, but only one widget will get a mousePressEvent.
2789 // we use this to select the right window
2790 if (!mMousePressed) {
2791 return;
2792 }
2793
2794 // the TapAndHoldGesture is triggerable the with mouse, we don't want this
2795 if (mLastMouseSource == Qt::MouseEventNotSynthesized) {
2796 return;
2797 }
2798
2799 // the TapAndHoldGesture is triggerable the with stylus, we don't want this
2800 if (!mIsTouchEvent) {
2801 return;
2802 }
2803
2804 mTapAndHoldActive = true;
2805 mScroller->stop();
2806
2807 // simulate a mousePressEvent, to allow QTreeView to select the items
2808 const QPoint tapViewportPos(q->viewport()->mapFromGlobal(tap->position().toPoint()));
2810 onPressed(&fakeMousePress);
2811
2812 const QPoint tapIndicatorSize(80, 80); // size for the tapAndHold indicator
2813 const QPoint pos(q->mapFromGlobal(tap->position().toPoint()));
2814 const QRect tapIndicatorRect(pos - (tapIndicatorSize / 2), pos + (tapIndicatorSize / 2));
2815 mRubberBand->setGeometry(tapIndicatorRect.normalized());
2816 mRubberBand->show();
2817 }
2818}
2819
2820void View::ViewPrivate::twoFingerTapTriggered(KTwoFingerTap *tap)
2821{
2822 if (tap->state() == Qt::GestureFinished) {
2823 if (mTapAndHoldActive) {
2824 return;
2825 }
2826
2827 // with touch you can touch multiple widgets at the same time, but only one widget will get a mousePressEvent.
2828 // we use this to select the right window
2829 if (!mMousePressed) {
2830 return;
2831 }
2832
2833 // simulate a mousePressEvent with Qt::ControlModifier, to allow QTreeView to select the items
2835 tap->pos(),
2836 q->viewport()->mapToGlobal(tap->pos()),
2840 onPressed(&fakeMousePress);
2841 }
2842}
2843
2844#include "moc_view.cpp"
void fromQInt32(qint32 status)
void setRead(bool read=true)
qint32 toQInt32() const
A set of aggregation options that can be applied to the MessageList::Model in a single shot.
Definition aggregation.h:29
@ GroupBySender
Group by sender, always.
Definition aggregation.h:41
@ NoGrouping
Don't group messages at all.
Definition aggregation.h:37
@ GroupByDateRange
Use smart (thread leader) date ranges ("Today","Yesterday","Last Week"...)
Definition aggregation.h:39
@ GroupBySenderOrReceiver
Group by sender (incoming) or receiver (outgoing) field.
Definition aggregation.h:40
@ GroupByDate
Group the messages by the date of the thread leader.
Definition aggregation.h:38
@ GroupByReceiver
Group by receiver, always.
Definition aggregation.h:42
@ NoThreading
Perform no threading at all.
Definition aggregation.h:66
@ TopmostMessage
The thread grouping is computed from the topmost message (very similar to least recent,...
Definition aggregation.h:79
@ MostRecentMessage
The thread grouping is computed from the most recent message.
Definition aggregation.h:81
A structure used with MessageList::Item::childItemStats().
Definition item.h:173
A single item of the MessageList tree managed by MessageList::Model.
Definition item.h:36
const Akonadi::MessageStatus & status() const
Returns the status associated to this Item.
Definition item.cpp:447
Item * itemAbove()
Returns the item that is visually above this item in the tree.
Definition item.cpp:144
Type type() const
Returns the type of this item.
Definition item.cpp:343
Item * itemBelowChild(Item *child)
Returns the item that is visually below the specified child if this item.
Definition item.cpp:83
Item * parent() const
Returns the parent Item in the tree, or 0 if this item isn't attached to the tree.
Definition item.cpp:437
Item * deepestItem()
Returns the deepest item in the subtree originating at this item.
Definition item.cpp:118
Item * itemBelow()
Returns the item that is visually below this item in the tree.
Definition item.cpp:103
int childItemCount() const
Returns the number of children of this Item.
Definition item.cpp:158
QList< Item * > * childItems() const
Return the list of child items.
Definition item.cpp:59
The MessageItem class.
Definition messageitem.h:35
void subTreeToList(QList< MessageItem * > &list)
Appends the whole subtree originating at this item to the specified list.
This class manages the huge tree of displayable objects: GroupHeaderItems and MessageItems.
Definition model.h:54
void statusMessage(const QString &message)
Notify the outside when updating the status bar with a message could be useful.
A class which holds information about sorting, e.g.
Definition sortorder.h:23
MessageSorting messageSorting() const
Returns the current message sorting option.
Definition sortorder.cpp:40
@ SortMessagesByDateTime
Sort the messages by date and time.
Definition sortorder.h:62
@ SortMessagesByDateTimeOfMostRecent
Sort the messages by date and time of the most recent message in subtree.
Definition sortorder.h:63
SortDirection messageSortDirection() const
Returns the current message SortDirection.
Definition sortorder.cpp:50
The QAbstractItemModel based interface that you need to provide for your storage to work with Message...
The Column class defines a view column available inside this theme.
Definition theme.h:506
void setCurrentWidth(double currentWidth)
Sets the current shared width setting for this column.
Definition theme.cpp:713
bool currentlyVisible() const
Returns the current shared visibility state for this column.
Definition theme.cpp:698
void setCurrentlyVisible(bool currentlyVisible)
Sets the current shared visibility state for this column.
Definition theme.cpp:703
bool containsTextItems() const
Returns true if this column contains text items.
Definition theme.cpp:780
@ ReadStateIcon
The icon that displays the unread/read state (never disabled)
Definition theme.h:134
@ WatchedIgnoredStateIcon
The Watched/Ignored state icon.
Definition theme.h:162
@ ImportantStateIcon
The Important tag icon.
Definition theme.h:154
@ ExpandedStateIcon
The Expanded state icon for group headers.
Definition theme.h:166
@ AnnotationIcon
Whether the message has a annotation/note.
Definition theme.h:198
@ ActionItemStateIcon
The ActionItem state icon.
Definition theme.h:150
@ SpamHamStateIcon
The Spam/Ham state icon.
Definition theme.h:158
The Theme class defines the visual appearance of the MessageList.
Definition theme.h:48
Provides a widget which has the messagelist and the most important helper widgets,...
Definition widgetbase.h:41
void statusMessage(const QString &message)
Notify the outside when updating the status bar with a message could be useful.
bool visible() const
Q_SCRIPTABLE CaptureState status()
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
The implementation independent part of the MessageList library.
Definition aggregation.h:22
ExistingSelectionBehaviour
This enum is used in the view message selection functions (for instance View::selectNextMessage())
PreSelectionMode
Pre-selection is the action of automatically selecting a message just after the folder has finished l...
MessageTypeFilter
This enum is used in the view message selection functions (for instance View::nextMessageItem()).
virtual bool event(QEvent *event) override
virtual void resizeEvent(QResizeEvent *event) override
QWidget * viewport() const const
void triggered(bool checked)
const QColor & color() const const
void setAlpha(int alpha)
void accept()
Type type() const const
void setItalic(bool enable)
QGesture * gesture(Qt::GestureType type) const const
Qt::GestureType registerRecognizer(QGestureRecognizer *recognizer)
void sectionResized(int logicalIndex, int oldSize, int newSize)
QModelIndexList indexes() const const
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
bool contains(const AT &value) const const
qsizetype count() const const
T & first()
bool isEmpty() const const
T & last()
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addSeparator()
QAction * exec()
void * internalPointer() const const
bool isValid() const const
QPoint pos() const const
Qt::MouseEventSource source() const const
const QObjectList & children() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
virtual bool event(QEvent *e)
QObject * parent() const const
T qobject_cast(QObject *object)
const QColor & color(ColorGroup group, ColorRole role) const const
const QBrush & text() const const
QScroller * scroller(QObject *target)
void setScrollMetric(ScrollMetric metric, const QVariant &value)
Qt::MouseButton button() const const
Qt::MouseButtons buttons() const const
QString arg(Args &&... args) const const
bool isEmpty() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString trimmed() const const
AlignCenter
UniqueConnection
CustomContextMenu
GestureStarted
GestureType
ItemIsSelectable
NoModifier
LeftToRight
LeftButton
MouseEventSource
ScrollBarAsNeeded
WA_AcceptTouchEvents
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
void showText(const QPoint &pos, const QString &text, QWidget *w, const QRect &rect, int msecDisplayTime)
virtual void changeEvent(QEvent *event) override
virtual void mouseMoveEvent(QMouseEvent *event) override
virtual void mousePressEvent(QMouseEvent *event) override
virtual void paintEvent(QPaintEvent *event) override
void setRowHidden(int row, const QModelIndex &parent, bool hide)
virtual void updateGeometries() override
void customContextMenuRequested(const QPoint &pos)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.