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

KDE's Doxygen guidelines are available online.