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

0001 /******************************************************************************
0002  *
0003  *  SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  *
0007  *******************************************************************************/
0008 
0009 #include "core/view.h"
0010 #include "core/aggregation.h"
0011 #include "core/delegate.h"
0012 #include "core/groupheaderitem.h"
0013 #include "core/item.h"
0014 #include "core/messageitem.h"
0015 #include "core/model.h"
0016 #include "core/storagemodelbase.h"
0017 #include "core/theme.h"
0018 #include "core/widgetbase.h"
0019 #include "messagelistsettings.h"
0020 #include "messagelistutil.h"
0021 #include "messagelistutil_p.h"
0022 
0023 #include "MessageCore/StringUtil"
0024 
0025 #include <KMime/DateFormatter> // kdepimlibs
0026 
0027 #include <Akonadi/Item>
0028 #include <KTwoFingerTap>
0029 #include <QApplication>
0030 #include <QGestureEvent>
0031 #include <QHeaderView>
0032 #include <QHelpEvent>
0033 #include <QLineEdit>
0034 #include <QMenu>
0035 #include <QPainter>
0036 #include <QScrollBar>
0037 #include <QScroller>
0038 #include <QTimer>
0039 #include <QToolTip>
0040 
0041 #include "messagelist_debug.h"
0042 #include <KLocalizedString>
0043 
0044 using namespace MessageList::Core;
0045 
0046 class View::ViewPrivate
0047 {
0048 public:
0049     ViewPrivate(View *owner, Widget *parent)
0050         : q(owner)
0051         , mWidget(parent)
0052         , mDelegate(new Delegate(owner))
0053     {
0054     }
0055 
0056     void expandFullThread(const QModelIndex &index);
0057     void generalPaletteChanged();
0058     void onPressed(QMouseEvent *e);
0059     void gestureEvent(QGestureEvent *e);
0060     void tapTriggered(QTapGesture *tap);
0061     void tapAndHoldTriggered(QTapAndHoldGesture *tap);
0062     void twoFingerTapTriggered(KTwoFingerTap *tap);
0063 
0064     QColor mTextColor;
0065     View *const q;
0066 
0067     Widget *const mWidget;
0068     Model *mModel = nullptr;
0069     Delegate *const mDelegate;
0070 
0071     const Aggregation *mAggregation = nullptr; ///< The Aggregation we're using now, shallow pointer
0072     Theme *mTheme = nullptr; ///< The Theme we're using now, shallow pointer
0073     bool mNeedToApplyThemeColumns = false; ///< Flag signaling a pending application of theme columns
0074     Item *mLastCurrentItem = nullptr;
0075     QPoint mMousePressPosition;
0076     bool mSaveThemeColumnStateOnSectionResize = true; ///< This is used to filter out programmatic column resizes in slotSectionResized().
0077     QTimer *mSaveThemeColumnStateTimer = nullptr; ///< Used to trigger a delayed "save theme state"
0078     QTimer *mApplyThemeColumnsTimer = nullptr; ///< Used to trigger a delayed "apply theme columns"
0079     int mLastViewportWidth = -1;
0080     bool mIgnoreUpdateGeometries = false; ///< Shall we ignore the "update geometries" calls ?
0081     QScroller *mScroller = nullptr;
0082     bool mIsTouchEvent = false;
0083     bool mMousePressed = false;
0084     Qt::MouseEventSource mLastMouseSource = Qt::MouseEventNotSynthesized;
0085     bool mTapAndHoldActive = false;
0086     QRubberBand *mRubberBand = nullptr;
0087     Qt::GestureType mTwoFingerTap = Qt::CustomGesture;
0088 };
0089 
0090 View::View(Widget *pParent)
0091     : QTreeView(pParent)
0092     , d(new ViewPrivate(this, pParent))
0093 {
0094     d->mSaveThemeColumnStateTimer = new QTimer();
0095     connect(d->mSaveThemeColumnStateTimer, &QTimer::timeout, this, &View::saveThemeColumnState);
0096 
0097     d->mApplyThemeColumnsTimer = new QTimer();
0098     connect(d->mApplyThemeColumnsTimer, &QTimer::timeout, this, &View::applyThemeColumns);
0099 
0100     setItemDelegate(d->mDelegate);
0101     setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
0102     setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
0103     setAlternatingRowColors(true);
0104     setAllColumnsShowFocus(true);
0105     setSelectionMode(QAbstractItemView::ExtendedSelection);
0106     viewport()->setAcceptDrops(true);
0107 
0108     d->mScroller = QScroller::scroller(viewport());
0109     QScrollerProperties scrollerProp;
0110     scrollerProp.setScrollMetric(QScrollerProperties::AcceleratingFlickMaximumTime, 0.2); // QTBUG-88249
0111     d->mScroller->setScrollerProperties(scrollerProp);
0112     d->mScroller->grabGesture(viewport());
0113 
0114     setAttribute(Qt::WA_AcceptTouchEvents);
0115     d->mTwoFingerTap = QGestureRecognizer::registerRecognizer(new KTwoFingerTapRecognizer());
0116     viewport()->grabGesture(d->mTwoFingerTap);
0117     viewport()->grabGesture(Qt::TapGesture);
0118     viewport()->grabGesture(Qt::TapAndHoldGesture);
0119 
0120     d->mRubberBand = new QRubberBand(QRubberBand::Rectangle, this);
0121 
0122     header()->setContextMenuPolicy(Qt::CustomContextMenu);
0123     connect(header(), &QWidget::customContextMenuRequested, this, &View::slotHeaderContextMenuRequested);
0124     connect(header(), &QHeaderView::sectionResized, this, &View::slotHeaderSectionResized);
0125 
0126     header()->setSectionsClickable(true);
0127     header()->setSectionResizeMode(QHeaderView::Interactive);
0128     header()->setMinimumSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value
0129     header()->setDefaultSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value
0130 
0131     d->mModel = new Model(this);
0132     setModel(d->mModel);
0133 
0134     connect(d->mModel, &Model::statusMessage, pParent, &Widget::statusMessage);
0135 
0136     connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection);
0137 
0138     // as in KDE3, when a root-item of a message thread is expanded, expand all children
0139     connect(this, &View::expanded, this, [this](const QModelIndex &index) {
0140         d->expandFullThread(index);
0141     });
0142 }
0143 
0144 View::~View()
0145 {
0146     if (d->mSaveThemeColumnStateTimer->isActive()) {
0147         d->mSaveThemeColumnStateTimer->stop();
0148     }
0149     delete d->mSaveThemeColumnStateTimer;
0150     if (d->mApplyThemeColumnsTimer->isActive()) {
0151         d->mApplyThemeColumnsTimer->stop();
0152     }
0153     delete d->mApplyThemeColumnsTimer;
0154 
0155     // Zero out the theme, aggregation and ApplyThemeColumnsTimer so Model will not cause accesses to them in its destruction process
0156     d->mApplyThemeColumnsTimer = nullptr;
0157 
0158     d->mTheme = nullptr;
0159     d->mAggregation = nullptr;
0160 }
0161 
0162 Model *View::model() const
0163 {
0164     return d->mModel;
0165 }
0166 
0167 Delegate *View::delegate() const
0168 {
0169     return d->mDelegate;
0170 }
0171 
0172 void View::ignoreCurrentChanges(bool ignore)
0173 {
0174     if (ignore) {
0175         disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged);
0176         viewport()->setUpdatesEnabled(false);
0177     } else {
0178         connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection);
0179         viewport()->setUpdatesEnabled(true);
0180     }
0181 }
0182 
0183 void View::ignoreUpdateGeometries(bool ignore)
0184 {
0185     d->mIgnoreUpdateGeometries = ignore;
0186 }
0187 
0188 bool View::isScrollingLocked() const
0189 {
0190     // There is another popular requisite: people want the view to automatically
0191     // scroll in order to show new arriving mail. This actually makes sense
0192     // only when the view is sorted by date and the new mail is (usually) either
0193     // appended at the bottom or inserted at the top. It would be also confusing
0194     // when the user is browsing some other thread in the meantime.
0195     //
0196     // So here we make a simple guess: if the view is scrolled somewhere in the
0197     // middle then we assume that the user is browsing other threads and we
0198     // try to keep the currently selected item steady on the screen.
0199     // When the view is "locked" to the top (scrollbar value 0) or to the
0200     // bottom (scrollbar value == maximum) then we assume that the user
0201     // isn't browsing and we should attempt to show the incoming messages
0202     // by keeping the view "locked".
0203     //
0204     // The "locking" also doesn't make sense in the first big fill view job.
0205     // [Well this concept is pre-akonadi. Now the loading is all async anyway...
0206     //  So all this code is actually triggered during the initial loading, too.]
0207     const int scrollBarPosition = verticalScrollBar()->value();
0208     const int scrollBarMaximum = verticalScrollBar()->maximum();
0209     const SortOrder *sortOrder = d->mModel->sortOrder();
0210     const bool lockView = (
0211                               // not the first loading job
0212                               !d->mModel->isLoading())
0213         && (
0214                               // messages sorted by date
0215                               (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTime)
0216                               || (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTimeOfMostRecent))
0217         && (
0218                               // scrollbar at top (Descending order) or bottom (Ascending order)
0219                               (scrollBarPosition == 0 && sortOrder->messageSortDirection() == SortOrder::Descending)
0220                               || (scrollBarPosition == scrollBarMaximum && sortOrder->messageSortDirection() == SortOrder::Ascending));
0221     return lockView;
0222 }
0223 
0224 void View::updateGeometries()
0225 {
0226     if (d->mIgnoreUpdateGeometries || !d->mModel) {
0227         return;
0228     }
0229 
0230     const int scrollBarPositionBefore = verticalScrollBar()->value();
0231     const bool lockView = isScrollingLocked();
0232 
0233     QTreeView::updateGeometries();
0234 
0235     if (lockView) {
0236         // we prefer to keep the view locked to the top or bottom
0237         if (scrollBarPositionBefore != 0) {
0238             // we wanted the view to be locked to the bottom
0239             if (verticalScrollBar()->value() != verticalScrollBar()->maximum()) {
0240                 verticalScrollBar()->setValue(verticalScrollBar()->maximum());
0241             }
0242         } // else we wanted the view to be locked to top and we shouldn't need to do anything
0243     }
0244 }
0245 
0246 StorageModel *View::storageModel() const
0247 {
0248     return d->mModel->storageModel();
0249 }
0250 
0251 void View::setAggregation(const Aggregation *aggregation)
0252 {
0253     d->mAggregation = aggregation;
0254     d->mModel->setAggregation(aggregation);
0255 
0256     // use uniform row heights to speed up, but only if there are no group headers used
0257     setUniformRowHeights(d->mAggregation->grouping() == Aggregation::NoGrouping);
0258 }
0259 
0260 void View::setTheme(Theme *theme)
0261 {
0262     d->mNeedToApplyThemeColumns = true;
0263     d->mTheme = theme;
0264     d->mDelegate->setTheme(theme);
0265     d->mModel->setTheme(theme);
0266 }
0267 
0268 void View::setSortOrder(const SortOrder *sortOrder)
0269 {
0270     d->mModel->setSortOrder(sortOrder);
0271 }
0272 
0273 void View::reload()
0274 {
0275     setStorageModel(storageModel());
0276 }
0277 
0278 void View::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode)
0279 {
0280     // This will cause the model to be reset.
0281     d->mSaveThemeColumnStateOnSectionResize = false;
0282     d->mModel->setStorageModel(storageModel, preSelectionMode);
0283     d->mSaveThemeColumnStateOnSectionResize = true;
0284 }
0285 
0286 //////////////////////////////////////////////////////////////////////////////////////////////////////
0287 // Theme column state machinery
0288 //
0289 // This is yet another beast to beat. The QHeaderView behaviour, at the time of writing,
0290 // is quite unpredictable. This is due to the complex interaction with the model, with the QTreeView
0291 // and due to its attempts to delay the layout jobs. The delayed layouts, especially, may
0292 // cause the widths of the columns to quickly change in an unexpected manner in a place
0293 // where previously they have been always settled to the values you set...
0294 //
0295 // So here we have the tools to:
0296 //
0297 // - Apply the saved state of the theme columns (applyThemeColumns()).
0298 //   This function computes the "best fit" state of the visible columns and tries
0299 //   to apply it to QHeaderView. It also saves the new computed state to the Theme object.
0300 //
0301 // - Explicitly save the column state, used when the user changes the widths or visibility manually.
0302 //   This is called through a delayed timer after a column has been resized or used directly
0303 //   when the visibility state of a column has been changed by toggling a popup menu entry.
0304 //
0305 // - Display the column state context popup menu and handle its actions
0306 //
0307 // - Apply the theme columns when the theme changes, when the model changes or when
0308 //   the widget is resized.
0309 //
0310 // - Avoid saving a corrupted column state in that QHeaderView can be found *very* frequently.
0311 //
0312 
0313 void View::applyThemeColumns()
0314 {
0315     if (!d->mApplyThemeColumnsTimer) {
0316         return;
0317     }
0318 
0319     if (d->mApplyThemeColumnsTimer->isActive()) {
0320         d->mApplyThemeColumnsTimer->stop();
0321     }
0322 
0323     if (!d->mTheme) {
0324         return;
0325     }
0326 
0327     // qCDebug(MESSAGELIST_LOG) << "Apply theme columns";
0328 
0329     const QList<Theme::Column *> &columns = d->mTheme->columns();
0330 
0331     if (columns.isEmpty()) {
0332         return; // bad theme
0333     }
0334 
0335     if (!viewport()->isVisible()) {
0336         return; // invisible
0337     }
0338 
0339     if (viewport()->width() < 1) {
0340         return; // insane width
0341     }
0342     const int viewportWidth = viewport()->width();
0343     d->mLastViewportWidth = viewportWidth;
0344 
0345     // Now we want to distribute the available width on all the visible columns.
0346     //
0347     // The rules:
0348     // - The visible columns will span the width of the view, if possible.
0349     // - The columns with a saved width should take that width.
0350     // - The columns on the left should take more space, if possible.
0351     // - The columns with no text take just slightly more than their size hint.
0352     //   while the columns with text take possibly a lot more.
0353     //
0354 
0355     // Note that the first column is always shown (it can't be hidden at all)
0356 
0357     // The algorithm below is a sort of compromise between:
0358     // - Saving the user preferences for widths
0359     // - Using exactly the available view space
0360     //
0361     // It "tends to work" in all cases:
0362     // - When there are no user preferences saved and the column widths must be
0363     //   automatically computed to make best use of available space
0364     // - When there are user preferences for only some of the columns
0365     //   and that should be somewhat preserved while still using all the
0366     //   available space.
0367     // - When all the columns have well defined saved widths
0368 
0369     int idx = 0;
0370 
0371     // Gather total size "hint" for visible sections: if the widths of the columns wers
0372     // all saved then the total hint is equal to the total saved width.
0373 
0374     int totalVisibleWidthHint = 0;
0375     QList<int> lColumnSizeHints;
0376     for (const auto col : std::as_const(columns)) {
0377         if (col->currentlyVisible() || (idx == 0)) {
0378             // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be visible";
0379             // Column visible
0380             const int savedWidth = col->currentWidth();
0381             const int hintWidth = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width();
0382             totalVisibleWidthHint += savedWidth > 0 ? savedWidth : hintWidth;
0383             lColumnSizeHints.append(hintWidth);
0384             // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " size hint is " << hintWidth;
0385         } else {
0386             // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be not visible";
0387             // The column is not visible
0388             lColumnSizeHints.append(-1); // dummy
0389         }
0390         idx++;
0391     }
0392 
0393     if (totalVisibleWidthHint < 16) {
0394         totalVisibleWidthHint = 16; // be reasonable
0395     }
0396 
0397     // Now compute somewhat "proportional" widths.
0398     idx = 0;
0399 
0400     QList<double> lColumnWidths;
0401     lColumnWidths.reserve(columns.count());
0402     int totalVisibleWidth = 0;
0403     for (const auto col : std::as_const(columns)) {
0404         double savedWidth = col->currentWidth();
0405         double hintWidth = savedWidth > 0 ? savedWidth : lColumnSizeHints.at(idx);
0406         double realWidth;
0407 
0408         if (col->currentlyVisible() || (idx == 0)) {
0409             if (col->containsTextItems()) {
0410                 // the column contains text items, it should get more space (if possible)
0411                 realWidth = ((hintWidth * viewportWidth) / totalVisibleWidthHint);
0412             } else {
0413                 // the column contains no text items, it should get exactly its hint/saved width.
0414                 realWidth = hintWidth;
0415             }
0416 
0417             if (realWidth < 2) {
0418                 realWidth = 2; // don't allow very insane values
0419             }
0420 
0421             totalVisibleWidth += realWidth;
0422         } else {
0423             // Column not visible
0424             realWidth = -1;
0425         }
0426 
0427         lColumnWidths.append(realWidth);
0428 
0429         idx++;
0430     }
0431 
0432     // Now the algorithm above may be wrong for several reasons...
0433     // - We're using fixed widths for certain columns and proportional
0434     //   for others...
0435     // - The user might have changed the width of the view from the
0436     //   time in that the widths have been saved
0437     // - There are some (not well identified) issues with the QTreeView
0438     //   scrollbar that make our view appear larger or shorter by 2-3 pixels
0439     //   sometimes.
0440     // - ...
0441     // So we correct the previous estimates by trying to use exactly
0442     // the available space.
0443 
0444     idx = 0;
0445 
0446     if (totalVisibleWidth != viewportWidth) {
0447         // The estimated widths were not using exactly the available space.
0448         if (totalVisibleWidth < viewportWidth) {
0449             // We were using less space than available.
0450 
0451             // Give the additional space to the text columns
0452             // also give more space to the first ones and less space to the last ones
0453             qreal available = viewportWidth - totalVisibleWidth;
0454 
0455             for (int idx = 0; idx < columns.count(); ++idx) {
0456                 Theme::Column *column = columns.at(idx);
0457                 if ((column->currentlyVisible() || (idx == 0)) && column->containsTextItems()) {
0458                     // give more space to this column
0459                     available /= 2; // eat half of the available space
0460                     lColumnWidths[idx] += available; // and give it to this column
0461                     if (available < 1) {
0462                         break; // no more space to give away
0463                     }
0464                 }
0465             }
0466 
0467             // if any space is still available, give it to the first column
0468             if (available >= 1) {
0469                 lColumnWidths[0] += available;
0470             }
0471         } else {
0472             // We were using more space than available
0473 
0474             // If the columns span more than the view then
0475             // try to squeeze them in order to make them fit
0476             double missing = totalVisibleWidth - viewportWidth;
0477             if (missing > 0) {
0478                 const int count = lColumnWidths.count();
0479                 idx = count - 1;
0480 
0481                 while (idx >= 0) {
0482                     if (columns.at(idx)->currentlyVisible() || (idx == 0)) {
0483                         double chop = lColumnWidths.at(idx) - lColumnSizeHints.at(idx);
0484                         if (chop > 0) {
0485                             if (chop > missing) {
0486                                 chop = missing;
0487                             }
0488                             lColumnWidths[idx] -= chop;
0489                             missing -= chop;
0490                             if (missing < 1) {
0491                                 break; // no more space to recover
0492                             }
0493                         }
0494                     } // else it's invisible
0495                     idx--;
0496                 }
0497             }
0498         }
0499     }
0500 
0501     // We're ready to assign widths.
0502 
0503     bool oldSave = d->mSaveThemeColumnStateOnSectionResize;
0504     d->mSaveThemeColumnStateOnSectionResize = false;
0505 
0506     // A huge problem here is that QHeaderView goes quite nuts if we show or hide sections
0507     // while resizing them. This is because it has several machineries aimed to delay
0508     // the layout to the last possible moment. So if we show a column, it will tend to
0509     // screw up the layout of other ones.
0510 
0511     // We first loop showing/hiding columns then.
0512 
0513     idx = 0;
0514 
0515     for (const auto col : std::as_const(columns)) {
0516         bool visible = (idx == 0) || col->currentlyVisible();
0517         // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " visible " << visible;
0518         col->setCurrentlyVisible(visible);
0519         header()->setSectionHidden(idx, !visible);
0520         idx++;
0521     }
0522 
0523     // Then we loop assigning widths. This is still complicated since QHeaderView tries
0524     // very badly to stretch the last section and thus will resize it in the meantime.
0525     // But seems to work most of the times...
0526 
0527     idx = 0;
0528 
0529     for (const auto col : std::as_const(columns)) {
0530         if (col->currentlyVisible()) {
0531             const double columnWidth(lColumnWidths.at(idx));
0532             col->setCurrentWidth(columnWidth);
0533             // Laurent Bug 358855 - message list column widths lost when program closed
0534             // I need to investigate if this code is still necessary (all method)
0535             header()->resizeSection(idx, static_cast<int>(columnWidth));
0536         } else {
0537             col->setCurrentWidth(-1);
0538         }
0539         idx++;
0540     }
0541 
0542     idx = 0;
0543 
0544     bool bTriggeredQtBug = false;
0545     for (const auto col : std::as_const(columns)) {
0546         if (!header()->isSectionHidden(idx)) {
0547             if (!col->currentlyVisible()) {
0548                 bTriggeredQtBug = true;
0549             }
0550         }
0551         idx++;
0552     }
0553 
0554     setHeaderHidden(d->mTheme->viewHeaderPolicy() == Theme::NeverShowHeader);
0555 
0556     d->mSaveThemeColumnStateOnSectionResize = oldSave;
0557     d->mNeedToApplyThemeColumns = false;
0558 
0559     static bool bAllowRecursion = true;
0560 
0561     if (bTriggeredQtBug && bAllowRecursion) {
0562         bAllowRecursion = false;
0563         // qCDebug(MESSAGELIST_LOG) << "I've triggered the QHeaderView bug: trying to fix by calling myself again";
0564         applyThemeColumns();
0565         bAllowRecursion = true;
0566     }
0567 }
0568 
0569 void View::triggerDelayedApplyThemeColumns()
0570 {
0571     if (d->mApplyThemeColumnsTimer->isActive()) {
0572         d->mApplyThemeColumnsTimer->stop();
0573     }
0574     d->mApplyThemeColumnsTimer->setSingleShot(true);
0575     d->mApplyThemeColumnsTimer->start(100);
0576 }
0577 
0578 void View::saveThemeColumnState()
0579 {
0580     if (d->mSaveThemeColumnStateTimer->isActive()) {
0581         d->mSaveThemeColumnStateTimer->stop();
0582     }
0583 
0584     if (!d->mTheme) {
0585         return;
0586     }
0587 
0588     if (d->mNeedToApplyThemeColumns) {
0589         return; // don't save the state if it hasn't been applied at all
0590     }
0591 
0592     // qCDebug(MESSAGELIST_LOG) << "Save theme column state";
0593 
0594     const auto columns = d->mTheme->columns();
0595 
0596     if (columns.isEmpty()) {
0597         return; // bad theme
0598     }
0599 
0600     int idx = 0;
0601 
0602     for (const auto col : std::as_const(columns)) {
0603         if (header()->isSectionHidden(idx)) {
0604             // qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is hidden";
0605             col->setCurrentlyVisible(false);
0606             col->setCurrentWidth(-1); // reset (hmmm... we could use the "don't touch" policy here too...)
0607         } else {
0608             // qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is visible and has size " << header()->sectionSize( idx );
0609             col->setCurrentlyVisible(true);
0610             col->setCurrentWidth(header()->sectionSize(idx));
0611         }
0612         idx++;
0613     }
0614 }
0615 
0616 void View::triggerDelayedSaveThemeColumnState()
0617 {
0618     if (d->mSaveThemeColumnStateTimer->isActive()) {
0619         d->mSaveThemeColumnStateTimer->stop();
0620     }
0621     d->mSaveThemeColumnStateTimer->setSingleShot(true);
0622     d->mSaveThemeColumnStateTimer->start(200);
0623 }
0624 
0625 void View::resizeEvent(QResizeEvent *e)
0626 {
0627     qCDebug(MESSAGELIST_LOG) << "Resize event enter (viewport width is " << viewport()->width() << ")";
0628 
0629     QTreeView::resizeEvent(e);
0630 
0631     if (!isVisible()) {
0632         return; // don't play with
0633     }
0634 
0635     if (d->mLastViewportWidth != viewport()->width()) {
0636         triggerDelayedApplyThemeColumns();
0637     }
0638 
0639     if (header()->isVisible()) {
0640         return;
0641     }
0642 
0643     // header invisible
0644 
0645     bool oldSave = d->mSaveThemeColumnStateOnSectionResize;
0646     d->mSaveThemeColumnStateOnSectionResize = false;
0647 
0648     const int count = header()->count();
0649     if ((count - header()->hiddenSectionCount()) < 2) {
0650         // a single column visible: resize it
0651         int visibleIndex;
0652         for (visibleIndex = 0; visibleIndex < count; visibleIndex++) {
0653             if (!header()->isSectionHidden(visibleIndex)) {
0654                 break;
0655             }
0656         }
0657         if (visibleIndex < count) {
0658             header()->resizeSection(visibleIndex, viewport()->width() - 4);
0659         }
0660     }
0661 
0662     d->mSaveThemeColumnStateOnSectionResize = oldSave;
0663 
0664     triggerDelayedSaveThemeColumnState();
0665 }
0666 
0667 void View::paintEvent(QPaintEvent *event)
0668 {
0669 #if 0
0670     if (/*mFirstResult &&*/ (!model() || model()->rowCount() == 0)) {
0671         QPainter p(viewport());
0672 
0673         QFont font = p.font();
0674         font.setItalic(true);
0675         p.setFont(font);
0676 
0677         if (!d->mTextColor.isValid()) {
0678             d->generalPaletteChanged();
0679         }
0680         p.setPen(d->mTextColor);
0681 
0682         p.drawText(QRect(0, 0, width(), height()), Qt::AlignCenter, i18n("No result found"));
0683     } else {
0684         QTreeView::paintEvent(event);
0685     }
0686 #else
0687     QTreeView::paintEvent(event);
0688 #endif
0689 }
0690 
0691 void View::modelAboutToEmitLayoutChanged()
0692 {
0693     // QHeaderView goes totally NUTS with a layoutChanged() call
0694     d->mSaveThemeColumnStateOnSectionResize = false;
0695 }
0696 
0697 void View::modelEmittedLayoutChanged()
0698 {
0699     // This is after a first chunk of work has been done by the model: do apply column states
0700     d->mSaveThemeColumnStateOnSectionResize = true;
0701     applyThemeColumns();
0702 }
0703 
0704 void View::slotHeaderSectionResized(int logicalIndex, int oldWidth, int newWidth)
0705 {
0706     Q_UNUSED(logicalIndex)
0707     Q_UNUSED(oldWidth)
0708     Q_UNUSED(newWidth)
0709 
0710     if (d->mSaveThemeColumnStateOnSectionResize) {
0711         triggerDelayedSaveThemeColumnState();
0712     }
0713 }
0714 
0715 int View::sizeHintForColumn(int logicalColumnIndex) const
0716 {
0717     // QTreeView: please don't touch my column widths...
0718     int w = header()->sectionSize(logicalColumnIndex);
0719     if (w > 0) {
0720         return w;
0721     }
0722     if (!d->mDelegate) {
0723         return 32; // dummy
0724     }
0725     w = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, logicalColumnIndex).width();
0726     return w;
0727 }
0728 
0729 void View::slotHeaderContextMenuRequested(const QPoint &pnt)
0730 {
0731     if (!d->mTheme) {
0732         return;
0733     }
0734 
0735     const auto columns = d->mTheme->columns();
0736 
0737     if (columns.isEmpty()) {
0738         return; // bad theme
0739     }
0740 
0741     // the menu for the columns
0742     QMenu menu;
0743 
0744     int idx = 0;
0745     for (const auto col : std::as_const(columns)) {
0746         QAction *act = menu.addAction(col->label());
0747         act->setCheckable(true);
0748         act->setChecked(!header()->isSectionHidden(idx));
0749         if (idx == 0) {
0750             act->setEnabled(false);
0751         }
0752         QObject::connect(act, &QAction::triggered, this, [this, idx] {
0753             slotShowHideColumn(idx);
0754         });
0755 
0756         idx++;
0757     }
0758 
0759     menu.addSeparator();
0760     {
0761         QAction *act = menu.addAction(i18n("Adjust Column Sizes"));
0762         QObject::connect(act, &QAction::triggered, this, &View::slotAdjustColumnSizes);
0763     }
0764     {
0765         QAction *act = menu.addAction(i18n("Show Default Columns"));
0766         QObject::connect(act, &QAction::triggered, this, &View::slotShowDefaultColumns);
0767     }
0768     menu.addSeparator();
0769     {
0770         QAction *act = menu.addAction(i18n("Display Tooltips"));
0771         act->setCheckable(true);
0772         act->setChecked(MessageListSettings::self()->messageToolTipEnabled());
0773         QObject::connect(act, &QAction::triggered, this, &View::slotDisplayTooltips);
0774     }
0775     menu.addSeparator();
0776 
0777     MessageList::Util::fillViewMenu(&menu, d->mWidget);
0778 
0779     menu.exec(header()->mapToGlobal(pnt));
0780 }
0781 
0782 void View::slotAdjustColumnSizes()
0783 {
0784     if (!d->mTheme) {
0785         return;
0786     }
0787 
0788     d->mTheme->resetColumnSizes();
0789     applyThemeColumns();
0790 }
0791 
0792 void View::slotShowDefaultColumns()
0793 {
0794     if (!d->mTheme) {
0795         return;
0796     }
0797 
0798     d->mTheme->resetColumnState();
0799     applyThemeColumns();
0800 }
0801 
0802 void View::slotDisplayTooltips(bool showTooltips)
0803 {
0804     MessageListSettings::self()->setMessageToolTipEnabled(showTooltips);
0805 }
0806 
0807 void View::slotShowHideColumn(int columnIdx)
0808 {
0809     if (!d->mTheme) {
0810         return; // oops
0811     }
0812 
0813     if (columnIdx == 0) {
0814         return; // can never be hidden
0815     }
0816 
0817     if (columnIdx >= d->mTheme->columns().count()) {
0818         return;
0819     }
0820 
0821     const bool showIt = header()->isSectionHidden(columnIdx);
0822 
0823     Theme::Column *column = d->mTheme->columns().at(columnIdx);
0824     Q_ASSERT(column);
0825 
0826     // first save column state (as it is, with the column still in previous state)
0827     saveThemeColumnState();
0828 
0829     // If a section has just been shown, invalidate its width in the skin
0830     // since QTreeView assigned it a (possibly insane) default width.
0831     // If a section has been hidden, then invalidate its width anyway...
0832     // so finally invalidate width always, here.
0833     column->setCurrentlyVisible(showIt);
0834     column->setCurrentWidth(-1);
0835 
0836     // then apply theme columns to re-compute proportional widths (so we hopefully stay in the view)
0837     applyThemeColumns();
0838 }
0839 
0840 Item *View::currentItem() const
0841 {
0842     QModelIndex idx = currentIndex();
0843     if (!idx.isValid()) {
0844         return nullptr;
0845     }
0846     Item *it = static_cast<Item *>(idx.internalPointer());
0847     Q_ASSERT(it);
0848     return it;
0849 }
0850 
0851 MessageItem *View::currentMessageItem(bool selectIfNeeded) const
0852 {
0853     Item *it = currentItem();
0854     if (!it || (it->type() != Item::Message)) {
0855         return nullptr;
0856     }
0857 
0858     if (selectIfNeeded) {
0859         // Keep things coherent, if the user didn't select it, but acted on it via
0860         // a shortcut, do select it now.
0861         if (!selectionModel()->isSelected(currentIndex())) {
0862             selectionModel()->select(currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows);
0863         }
0864     }
0865 
0866     return static_cast<MessageItem *>(it);
0867 }
0868 
0869 void View::setCurrentMessageItem(MessageItem *it, bool center)
0870 {
0871     if (it) {
0872         qCDebug(MESSAGELIST_LOG) << "Setting current message to" << it->subject();
0873 
0874         const QModelIndex index = d->mModel->index(it, 0);
0875         selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows);
0876         if (center) {
0877             scrollTo(index, QAbstractItemView::PositionAtCenter);
0878         }
0879     } else {
0880         selectionModel()->setCurrentIndex(QModelIndex(), QItemSelectionModel::Current | QItemSelectionModel::Clear);
0881     }
0882 }
0883 
0884 bool View::selectionEmpty() const
0885 {
0886     return selectionModel()->selectedRows().isEmpty();
0887 }
0888 
0889 QList<MessageItem *> View::selectionAsMessageItemList(bool includeCollapsedChildren) const
0890 {
0891     QList<MessageItem *> selectedMessages;
0892 
0893     QModelIndexList lSelected = selectionModel()->selectedRows();
0894     if (lSelected.isEmpty()) {
0895         return selectedMessages;
0896     }
0897     for (const auto &idx : std::as_const(lSelected)) {
0898         // The asserts below are theoretically valid but at the time
0899         // of writing they fail because of a bug in QItemSelectionModel::selectedRows()
0900         // which returns also non-selectable items.
0901 
0902         // Q_ASSERT( selectedItem->type() == Item::Message );
0903         // Q_ASSERT( ( *it ).isValid() );
0904 
0905         if (!idx.isValid()) {
0906             continue;
0907         }
0908 
0909         Item *selectedItem = static_cast<Item *>(idx.internalPointer());
0910         Q_ASSERT(selectedItem);
0911 
0912         if (selectedItem->type() != Item::Message) {
0913             continue;
0914         }
0915 
0916         if (!static_cast<MessageItem *>(selectedItem)->isValid()) {
0917             continue;
0918         }
0919 
0920         Q_ASSERT(!selectedMessages.contains(static_cast<MessageItem *>(selectedItem)));
0921 
0922         if (includeCollapsedChildren && (selectedItem->childItemCount() > 0) && (!isExpanded(idx))) {
0923             static_cast<MessageItem *>(selectedItem)->subTreeToList(selectedMessages);
0924         } else {
0925             selectedMessages.append(static_cast<MessageItem *>(selectedItem));
0926         }
0927     }
0928 
0929     return selectedMessages;
0930 }
0931 
0932 QList<MessageItem *> View::currentThreadAsMessageItemList() const
0933 {
0934     QList<MessageItem *> currentThread;
0935 
0936     MessageItem *msg = currentMessageItem();
0937     if (!msg) {
0938         return currentThread;
0939     }
0940 
0941     while (msg->parent()) {
0942         if (msg->parent()->type() != Item::Message) {
0943             break;
0944         }
0945         msg = static_cast<MessageItem *>(msg->parent());
0946     }
0947 
0948     msg->subTreeToList(currentThread);
0949 
0950     return currentThread;
0951 }
0952 
0953 void View::setChildrenExpanded(const Item *root, bool expand)
0954 {
0955     Q_ASSERT(root);
0956     auto childList = root->childItems();
0957     if (!childList) {
0958         return;
0959     }
0960     for (const auto child : std::as_const(*childList)) {
0961         QModelIndex idx = d->mModel->index(child, 0);
0962         Q_ASSERT(idx.isValid());
0963         Q_ASSERT(static_cast<Item *>(idx.internalPointer()) == child);
0964 
0965         if (expand) {
0966             setExpanded(idx, true);
0967 
0968             if (child->childItemCount() > 0) {
0969                 setChildrenExpanded(child, true);
0970             }
0971         } else {
0972             if (child->childItemCount() > 0) {
0973                 setChildrenExpanded(child, false);
0974             }
0975 
0976             setExpanded(idx, false);
0977         }
0978     }
0979 }
0980 
0981 void View::ViewPrivate::generalPaletteChanged()
0982 {
0983     const QPalette palette = q->viewport()->palette();
0984     QColor color = palette.text().color();
0985     color.setAlpha(128);
0986     mTextColor = color;
0987 }
0988 
0989 void View::ViewPrivate::expandFullThread(const QModelIndex &index)
0990 {
0991     if (!index.isValid()) {
0992         return;
0993     }
0994 
0995     Item *item = static_cast<Item *>(index.internalPointer());
0996     if (item->type() != Item::Message) {
0997         return;
0998     }
0999 
1000     if (!static_cast<MessageItem *>(item)->parent() || (static_cast<MessageItem *>(item)->parent()->type() != Item::Message)) {
1001         q->setChildrenExpanded(item, true);
1002     }
1003 }
1004 
1005 void View::setCurrentThreadExpanded(bool expand)
1006 {
1007     Item *it = currentItem();
1008     if (!it) {
1009         return;
1010     }
1011 
1012     if (it->type() == Item::GroupHeader) {
1013         setExpanded(currentIndex(), expand);
1014     } else if (it->type() == Item::Message) {
1015         auto message = static_cast<MessageItem *>(it);
1016         while (message->parent()) {
1017             if (message->parent()->type() != Item::Message) {
1018                 break;
1019             }
1020             message = static_cast<MessageItem *>(message->parent());
1021         }
1022 
1023         if (expand) {
1024             setExpanded(d->mModel->index(message, 0), true);
1025             setChildrenExpanded(message, true);
1026         } else {
1027             setChildrenExpanded(message, false);
1028             setExpanded(d->mModel->index(message, 0), false);
1029         }
1030     }
1031 }
1032 
1033 void View::setAllThreadsExpanded(bool expand)
1034 {
1035     scheduleDelayedItemsLayout();
1036     if (d->mAggregation->grouping() == Aggregation::NoGrouping) {
1037         // we have no groups so threads start under the root item: just expand/unexpand all
1038         setChildrenExpanded(d->mModel->rootItem(), expand);
1039         return;
1040     }
1041 
1042     // grouping is in effect: must expand/unexpand one level lower
1043 
1044     auto childList = d->mModel->rootItem()->childItems();
1045     if (!childList) {
1046         return;
1047     }
1048 
1049     for (const auto item : std::as_const(*childList)) {
1050         setChildrenExpanded(item, expand);
1051     }
1052 }
1053 
1054 void View::setAllGroupsExpanded(bool expand)
1055 {
1056     if (d->mAggregation->grouping() == Aggregation::NoGrouping) {
1057         return; // no grouping in effect
1058     }
1059 
1060     Item *item = d->mModel->rootItem();
1061 
1062     auto childList = item->childItems();
1063     if (!childList) {
1064         return;
1065     }
1066 
1067     scheduleDelayedItemsLayout();
1068     for (const auto item : std::as_const(*childList)) {
1069         Q_ASSERT(item->type() == Item::GroupHeader);
1070         QModelIndex idx = d->mModel->index(item, 0);
1071         Q_ASSERT(idx.isValid());
1072         Q_ASSERT(static_cast<Item *>(idx.internalPointer()) == item);
1073         if (expand) {
1074             if (!isExpanded(idx)) {
1075                 setExpanded(idx, true);
1076             }
1077         } else {
1078             if (isExpanded(idx)) {
1079                 setExpanded(idx, false);
1080             }
1081         }
1082     }
1083 }
1084 
1085 void View::selectMessageItems(const QList<MessageItem *> &list)
1086 {
1087     QItemSelection selection;
1088     for (const auto mi : list) {
1089         Q_ASSERT(mi);
1090         QModelIndex idx = d->mModel->index(mi, 0);
1091         Q_ASSERT(idx.isValid());
1092         Q_ASSERT(static_cast<MessageItem *>(idx.internalPointer()) == mi);
1093         if (!selectionModel()->isSelected(idx)) {
1094             selection.append(QItemSelectionRange(idx));
1095         }
1096         ensureDisplayedWithParentsExpanded(mi);
1097     }
1098     if (!selection.isEmpty()) {
1099         selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows);
1100     }
1101 }
1102 
1103 static inline bool message_type_matches(Item *item, MessageTypeFilter messageTypeFilter)
1104 {
1105     switch (messageTypeFilter) {
1106     case MessageTypeAny:
1107         return true;
1108         break;
1109     case MessageTypeUnreadOnly:
1110         return !item->status().isRead();
1111         break;
1112     default:
1113         // nothing here
1114         break;
1115     }
1116 
1117     // never reached
1118     Q_ASSERT(false);
1119     return false;
1120 }
1121 
1122 Item *View::messageItemAfter(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop)
1123 {
1124     if (!storageModel()) {
1125         return nullptr; // no folder
1126     }
1127 
1128     // find the item to start with
1129     Item *below;
1130 
1131     if (referenceItem) {
1132         // there was a current item: we start just below it
1133         if ((referenceItem->childItemCount() > 0) && ((messageTypeFilter != MessageTypeAny) || isExpanded(d->mModel->index(referenceItem, 0)))) {
1134             // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't)
1135             below = referenceItem->itemBelow();
1136         } else {
1137             // the current item had no children: ask the parent to find the item below
1138             Q_ASSERT(referenceItem->parent());
1139             below = referenceItem->parent()->itemBelowChild(referenceItem);
1140         }
1141 
1142         if (!below) {
1143             // reached the end
1144             if (loop) {
1145                 // try re-starting from top
1146                 below = d->mModel->rootItem()->itemBelow();
1147                 Q_ASSERT(below); // must exist (we had a current item)
1148 
1149                 if (below == referenceItem) {
1150                     return nullptr; // only one item in folder: loop complete
1151                 }
1152             } else {
1153                 // looping not requested
1154                 return nullptr;
1155             }
1156         }
1157     } else {
1158         // there was no current item, start from beginning
1159         below = d->mModel->rootItem()->itemBelow();
1160 
1161         if (!below) {
1162             return nullptr; // folder empty
1163         }
1164     }
1165 
1166     // ok.. now below points to the next message.
1167     // While it doesn't satisfy our requirements, go further down
1168 
1169     QModelIndex parentIndex = d->mModel->index(below->parent(), 0);
1170     QModelIndex belowIndex = d->mModel->index(below, 0);
1171 
1172     Q_ASSERT(belowIndex.isValid());
1173 
1174     while (
1175         // is not a message (we want messages, don't we ?)
1176         (below->type() != Item::Message) || // message filter doesn't match
1177         (!message_type_matches(below, messageTypeFilter)) || // is hidden (and we don't want hidden items as they aren't "officially" in the view)
1178         isRowHidden(belowIndex.row(), parentIndex) || // is not enabled or not selectable
1179         ((d->mModel->flags(belowIndex) & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) != (Qt::ItemIsSelectable | Qt::ItemIsEnabled))) {
1180         // find the next one
1181         if ((below->childItemCount() > 0) && ((messageTypeFilter != MessageTypeAny) || isExpanded(belowIndex))) {
1182             // the current item had children: either expanded or we want unread messages (and so we'll expand it if it isn't)
1183             below = below->itemBelow();
1184         } else {
1185             // the current item had no children: ask the parent to find the item below
1186             Q_ASSERT(below->parent());
1187             below = below->parent()->itemBelowChild(below);
1188         }
1189 
1190         if (!below) {
1191             // we reached the end of the folder
1192             if (loop) {
1193                 // looping requested
1194                 if (referenceItem) { // <-- this means "we have started from something that is not the top: looping makes sense"
1195                     below = d->mModel->rootItem()->itemBelow();
1196                 }
1197                 // else mi == 0 and below == 0: we have started from the beginning and reached the end (it will fail the test below and exit)
1198             } else {
1199                 // looping not requested: nothing more to do
1200                 return nullptr;
1201             }
1202         }
1203 
1204         if (below == referenceItem) {
1205             Q_ASSERT(loop);
1206             return nullptr; // looped and returned back to the first message
1207         }
1208 
1209         parentIndex = d->mModel->index(below->parent(), 0);
1210         belowIndex = d->mModel->index(below, 0);
1211 
1212         Q_ASSERT(belowIndex.isValid());
1213     }
1214 
1215     return below;
1216 }
1217 
1218 Item *View::firstMessageItem(MessageTypeFilter messageTypeFilter)
1219 {
1220     return messageItemAfter(nullptr, messageTypeFilter, false);
1221 }
1222 
1223 Item *View::nextMessageItem(MessageTypeFilter messageTypeFilter, bool loop)
1224 {
1225     return messageItemAfter(currentMessageItem(false), messageTypeFilter, loop);
1226 }
1227 
1228 Item *View::deepestExpandedChild(Item *referenceItem) const
1229 {
1230     const int children = referenceItem->childItemCount();
1231     if (children > 0 && isExpanded(d->mModel->index(referenceItem, 0))) {
1232         return deepestExpandedChild(referenceItem->childItem(children - 1));
1233     } else {
1234         return referenceItem;
1235     }
1236 }
1237 
1238 Item *View::messageItemBefore(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop)
1239 {
1240     if (!storageModel()) {
1241         return nullptr; // no folder
1242     }
1243 
1244     // find the item to start with
1245     Item *above;
1246 
1247     if (referenceItem) {
1248         Item *parent = referenceItem->parent();
1249         Item *siblingAbove = parent ? parent->itemAboveChild(referenceItem) : nullptr;
1250         // there was a current item: we start just above it
1251         if ((siblingAbove && siblingAbove != referenceItem && siblingAbove != parent) && (siblingAbove->childItemCount() > 0)
1252             && ((messageTypeFilter != MessageTypeAny) || (isExpanded(d->mModel->index(siblingAbove, 0))))) {
1253             // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't)
1254             above = deepestExpandedChild(siblingAbove);
1255         } else {
1256             // the current item had no children: ask the parent to find the item above
1257             Q_ASSERT(referenceItem->parent());
1258             above = referenceItem->parent()->itemAboveChild(referenceItem);
1259         }
1260 
1261         if ((!above) || (above == d->mModel->rootItem())) {
1262             // reached the beginning
1263             if (loop) {
1264                 // try re-starting from bottom
1265                 above = d->mModel->rootItem()->deepestItem();
1266                 Q_ASSERT(above); // must exist (we had a current item)
1267                 Q_ASSERT(above != d->mModel->rootItem());
1268 
1269                 if (above == referenceItem) {
1270                     return nullptr; // only one item in folder: loop complete
1271                 }
1272             } else {
1273                 // looping not requested
1274                 return nullptr;
1275             }
1276         }
1277     } else {
1278         // there was no current item, start from end
1279         above = d->mModel->rootItem()->deepestItem();
1280 
1281         if (!above || !above->parent() || (above == d->mModel->rootItem())) {
1282             return nullptr; // folder empty
1283         }
1284     }
1285 
1286     // ok.. now below points to the previous message.
1287     // While it doesn't satisfy our requirements, go further up
1288 
1289     QModelIndex parentIndex = d->mModel->index(above->parent(), 0);
1290     QModelIndex aboveIndex = d->mModel->index(above, 0);
1291 
1292     Q_ASSERT(aboveIndex.isValid());
1293 
1294     while (
1295         // is not a message (we want messages, don't we ?)
1296         (above->type() != Item::Message) || // message filter doesn't match
1297         (!message_type_matches(above, messageTypeFilter)) || // we don't expand items but the item has parents unexpanded (so should be skipped)
1298         (
1299             // !expand items
1300             (messageTypeFilter == MessageTypeAny) && // has unexpanded parents or is itself hidden
1301             (!isDisplayedWithParentsExpanded(above)))
1302         || // is hidden
1303         isRowHidden(aboveIndex.row(), parentIndex) || // is not enabled or not selectable
1304         ((d->mModel->flags(aboveIndex) & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) != (Qt::ItemIsSelectable | Qt::ItemIsEnabled))) {
1305         above = above->itemAbove();
1306 
1307         if ((!above) || (above == d->mModel->rootItem())) {
1308             // reached the beginning
1309             if (loop) {
1310                 // looping requested
1311                 if (referenceItem) { // <-- this means "we have started from something that is not the beginning: looping makes sense"
1312                     above = d->mModel->rootItem()->deepestItem();
1313                 }
1314                 // else mi == 0 and above == 0: we have started from the end and reached the beginning (it will fail the test below and exit)
1315             } else {
1316                 // looping not requested: nothing more to do
1317                 return nullptr;
1318             }
1319         }
1320 
1321         if (above == referenceItem) {
1322             Q_ASSERT(loop);
1323             return nullptr; // looped and returned back to the first message
1324         }
1325 
1326         if (!above->parent()) {
1327             return nullptr;
1328         }
1329 
1330         parentIndex = d->mModel->index(above->parent(), 0);
1331         aboveIndex = d->mModel->index(above, 0);
1332 
1333         Q_ASSERT(aboveIndex.isValid());
1334     }
1335 
1336     return above;
1337 }
1338 
1339 Item *View::lastMessageItem(MessageTypeFilter messageTypeFilter)
1340 {
1341     return messageItemBefore(nullptr, messageTypeFilter, false);
1342 }
1343 
1344 Item *View::previousMessageItem(MessageTypeFilter messageTypeFilter, bool loop)
1345 {
1346     return messageItemBefore(currentMessageItem(false), messageTypeFilter, loop);
1347 }
1348 
1349 void View::growOrShrinkExistingSelection(const QModelIndex &newSelectedIndex, bool movingUp)
1350 {
1351     // Qt: why visualIndex() is private? ...I'd really need it here...
1352 
1353     int selectedVisualCoordinate = visualRect(newSelectedIndex).top();
1354 
1355     int topVisualCoordinate = 0xfffffff; // huuuuuge number
1356     int bottomVisualCoordinate = -(0xfffffff);
1357 
1358     QModelIndex bottomIndex;
1359     QModelIndex topIndex;
1360 
1361     // find out the actual selection range
1362     const QItemSelection selection = selectionModel()->selection();
1363 
1364     for (const QItemSelectionRange &range : selection) {
1365         // We're asking the model for the index as range.topLeft() and range.bottomRight()
1366         // can return indexes in invisible columns which have a null visualRect().
1367         // Column 0, instead, is always visible.
1368 
1369         QModelIndex top = d->mModel->index(range.top(), 0, range.parent());
1370         QModelIndex bottom = d->mModel->index(range.bottom(), 0, range.parent());
1371 
1372         if (top.isValid()) {
1373             if (!bottom.isValid()) {
1374                 bottom = top;
1375             }
1376         } else {
1377             if (!top.isValid()) {
1378                 top = bottom;
1379             }
1380         }
1381         int candidate = visualRect(bottom).bottom();
1382         if (candidate > bottomVisualCoordinate) {
1383             bottomVisualCoordinate = candidate;
1384             bottomIndex = range.bottomRight();
1385         }
1386 
1387         candidate = visualRect(top).top();
1388         if (candidate < topVisualCoordinate) {
1389             topVisualCoordinate = candidate;
1390             topIndex = range.topLeft();
1391         }
1392     }
1393 
1394     if (topIndex.isValid() && bottomIndex.isValid()) {
1395         if (movingUp) {
1396             if (selectedVisualCoordinate < topVisualCoordinate) {
1397                 // selecting something above the top: grow selection
1398                 selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1399             } else {
1400                 // selecting something below the top: shrink selection
1401                 const QModelIndexList selectedIndexes = selection.indexes();
1402                 for (const QModelIndex &idx : selectedIndexes) {
1403                     if ((idx.column() == 0) && (visualRect(idx).top() > selectedVisualCoordinate)) {
1404                         selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect);
1405                     }
1406                 }
1407             }
1408         } else {
1409             if (selectedVisualCoordinate > bottomVisualCoordinate) {
1410                 // selecting something below bottom: grow selection
1411                 selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1412             } else {
1413                 // selecting something above bottom: shrink selection
1414                 const QModelIndexList selectedIndexes = selection.indexes();
1415                 for (const QModelIndex &idx : selectedIndexes) {
1416                     if ((idx.column() == 0) && (visualRect(idx).top() < selectedVisualCoordinate)) {
1417                         selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect);
1418                     }
1419                 }
1420             }
1421         }
1422     } else {
1423         // no existing selection, just grow
1424         selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1425     }
1426 }
1427 
1428 bool View::selectNextMessageItem(MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop)
1429 {
1430     Item *it = nextMessageItem(messageTypeFilter, loop);
1431     if (!it) {
1432         return false;
1433     }
1434 
1435     if (it->parent() != d->mModel->rootItem()) {
1436         ensureDisplayedWithParentsExpanded(it);
1437     }
1438 
1439     QModelIndex idx = d->mModel->index(it, 0);
1440 
1441     Q_ASSERT(idx.isValid());
1442 
1443     switch (existingSelectionBehaviour) {
1444     case ExpandExistingSelection:
1445         selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1446         selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1447         break;
1448     case GrowOrShrinkExistingSelection:
1449         selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1450         growOrShrinkExistingSelection(idx, false);
1451         break;
1452     default:
1453         // case ClearExistingSelection:
1454         setCurrentIndex(idx);
1455         break;
1456     }
1457 
1458     if (centerItem) {
1459         scrollTo(idx, QAbstractItemView::PositionAtCenter);
1460     }
1461 
1462     return true;
1463 }
1464 
1465 bool View::selectPreviousMessageItem(MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop)
1466 {
1467     Item *it = previousMessageItem(messageTypeFilter, loop);
1468     if (!it) {
1469         return false;
1470     }
1471 
1472     if (it->parent() != d->mModel->rootItem()) {
1473         ensureDisplayedWithParentsExpanded(it);
1474     }
1475 
1476     QModelIndex idx = d->mModel->index(it, 0);
1477 
1478     Q_ASSERT(idx.isValid());
1479 
1480     switch (existingSelectionBehaviour) {
1481     case ExpandExistingSelection:
1482         selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1483         selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1484         break;
1485     case GrowOrShrinkExistingSelection:
1486         selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1487         growOrShrinkExistingSelection(idx, true);
1488         break;
1489     default:
1490         // case ClearExistingSelection:
1491         setCurrentIndex(idx);
1492         break;
1493     }
1494 
1495     if (centerItem) {
1496         scrollTo(idx, QAbstractItemView::PositionAtCenter);
1497     }
1498 
1499     return true;
1500 }
1501 
1502 bool View::focusNextMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop)
1503 {
1504     Item *it = nextMessageItem(messageTypeFilter, loop);
1505     if (!it) {
1506         return false;
1507     }
1508 
1509     if (it->parent() != d->mModel->rootItem()) {
1510         ensureDisplayedWithParentsExpanded(it);
1511     }
1512 
1513     QModelIndex idx = d->mModel->index(it, 0);
1514 
1515     Q_ASSERT(idx.isValid());
1516 
1517     selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1518 
1519     if (centerItem) {
1520         scrollTo(idx, QAbstractItemView::PositionAtCenter);
1521     }
1522 
1523     return true;
1524 }
1525 
1526 bool View::focusPreviousMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop)
1527 {
1528     Item *it = previousMessageItem(messageTypeFilter, loop);
1529     if (!it) {
1530         return false;
1531     }
1532 
1533     if (it->parent() != d->mModel->rootItem()) {
1534         ensureDisplayedWithParentsExpanded(it);
1535     }
1536 
1537     QModelIndex idx = d->mModel->index(it, 0);
1538 
1539     Q_ASSERT(idx.isValid());
1540 
1541     selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
1542 
1543     if (centerItem) {
1544         scrollTo(idx, QAbstractItemView::PositionAtCenter);
1545     }
1546 
1547     return true;
1548 }
1549 
1550 void View::selectFocusedMessageItem(bool centerItem)
1551 {
1552     QModelIndex idx = currentIndex();
1553     if (!idx.isValid()) {
1554         return;
1555     }
1556 
1557     if (selectionModel()->isSelected(idx)) {
1558         return;
1559     }
1560 
1561     selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows);
1562 
1563     if (centerItem) {
1564         scrollTo(idx, QAbstractItemView::PositionAtCenter);
1565     }
1566 }
1567 
1568 bool View::selectFirstMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem)
1569 {
1570     if (!storageModel()) {
1571         return false; // nothing to do
1572     }
1573 
1574     Item *it = firstMessageItem(messageTypeFilter);
1575     if (!it) {
1576         return false;
1577     }
1578 
1579     Q_ASSERT(it != d->mModel->rootItem()); // must never happen (obviously)
1580 
1581     ensureDisplayedWithParentsExpanded(it);
1582 
1583     QModelIndex idx = d->mModel->index(it, 0);
1584 
1585     Q_ASSERT(idx.isValid());
1586 
1587     setCurrentIndex(idx);
1588 
1589     if (centerItem) {
1590         scrollTo(idx, QAbstractItemView::PositionAtCenter);
1591     }
1592 
1593     return true;
1594 }
1595 
1596 bool View::selectLastMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem)
1597 {
1598     if (!storageModel()) {
1599         return false;
1600     }
1601 
1602     Item *it = lastMessageItem(messageTypeFilter);
1603     if (!it) {
1604         return false;
1605     }
1606 
1607     Q_ASSERT(it != d->mModel->rootItem());
1608 
1609     ensureDisplayedWithParentsExpanded(it);
1610 
1611     QModelIndex idx = d->mModel->index(it, 0);
1612 
1613     Q_ASSERT(idx.isValid());
1614 
1615     setCurrentIndex(idx);
1616 
1617     if (centerItem) {
1618         scrollTo(idx, QAbstractItemView::PositionAtCenter);
1619     }
1620 
1621     return true;
1622 }
1623 
1624 void View::modelFinishedLoading()
1625 {
1626     Q_ASSERT(storageModel());
1627     Q_ASSERT(!d->mModel->isLoading());
1628 
1629     // nothing here for now :)
1630 }
1631 
1632 MessageItemSetReference View::createPersistentSet(const QList<MessageItem *> &items)
1633 {
1634     return d->mModel->createPersistentSet(items);
1635 }
1636 
1637 QList<MessageItem *> View::persistentSetCurrentMessageItemList(MessageItemSetReference ref)
1638 {
1639     return d->mModel->persistentSetCurrentMessageItemList(ref);
1640 }
1641 
1642 void View::deletePersistentSet(MessageItemSetReference ref)
1643 {
1644     d->mModel->deletePersistentSet(ref);
1645 }
1646 
1647 void View::markMessageItemsAsAboutToBeRemoved(const QList<MessageItem *> &items, bool bMark)
1648 {
1649     if (!bMark) {
1650         for (const auto mi : items) {
1651             if (mi->isValid()) { // hasn't been removed in the meantime
1652                 mi->setAboutToBeRemoved(false);
1653             }
1654         }
1655 
1656         viewport()->update();
1657 
1658         return;
1659     }
1660 
1661     // ok.. we're going to mark the messages as "about to be deleted".
1662     // This means that we're going to make them non selectable.
1663 
1664     // What happens to the selection is generally an untrackable big mess.
1665     // Several components and entities are involved.
1666 
1667     // Qutie tries to apply some kind of internal logic in order to keep
1668     // "something" selected and "something" (else) to be current.
1669     // The results sometimes appear to depend on the current moon phase.
1670 
1671     // The Model will do crazy things in order to preserve the current
1672     // selection (and possibly the current item). If it's impossible then
1673     // it will make its own guesses about what should be selected next.
1674     // A problem is that the Model will do it one message at a time.
1675     // When item reparenting/reordering is involved then the guesses
1676     // can produce non-intuitive results.
1677 
1678     // Add the fact that selection and current item are distinct concepts,
1679     // their relative interaction depends on the settings and is often quite
1680     // unclear.
1681 
1682     // Add the fact that (at the time of writing) several styles don't show
1683     // the current item (only Yoda knows why) and this causes some confusion to the user.
1684 
1685     // Add the fact that the operations are asynchronous: deletion will start
1686     // a job, do some event loop processing and then complete the work at a later time.
1687     // The Qutie views also tend to accumulate the changes and perform them
1688     // all at once at the latest possible stage.
1689 
1690     // A radical approach is needed: we FIRST deal with the selection
1691     // by trying to move it away from the messages about to be deleted
1692     // and THEN mark the (hopefully no longer selected) messages as "about to be deleted".
1693 
1694     // First of all, find out if we're going to clear the entire selection (very likely).
1695 
1696     bool clearingEntireSelection = true;
1697 
1698     const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0);
1699 
1700     if (selectedIndexes.count() > items.count()) {
1701         // the selection is bigger: we can't clear it completely
1702         clearingEntireSelection = false;
1703     } else {
1704         // the selection has same size or is smaller: we can clear it completely with our removal
1705         for (const QModelIndex &selectedIndex : selectedIndexes) {
1706             Q_ASSERT(selectedIndex.isValid());
1707             Q_ASSERT(selectedIndex.column() == 0);
1708 
1709             Item *selectedItem = static_cast<Item *>(selectedIndex.internalPointer());
1710             Q_ASSERT(selectedItem);
1711 
1712             if (selectedItem->type() != Item::Message) {
1713                 continue;
1714             }
1715 
1716             if (!items.contains(static_cast<MessageItem *>(selectedItem))) {
1717                 // the selection contains something that we aren't going to remove:
1718                 // we will not clear the selection completely
1719                 clearingEntireSelection = false;
1720                 break;
1721             }
1722         }
1723     }
1724 
1725     if (clearingEntireSelection) {
1726         // Try to clear the current selection and select something sensible instead,
1727         // so after the deletion we will not end up with a random selection.
1728         // Pick up a message in the set (which is very likely to be contiguous), walk the tree
1729         // and select the next message that is NOT in the set.
1730 
1731         MessageItem *aMessage = items.last();
1732         Q_ASSERT(aMessage);
1733 
1734         // Avoid infinite loops by carrying only a limited number of attempts.
1735         // If there is any message that is not in the set then items.count() attempts should find it.
1736         int maxAttempts = items.count();
1737 
1738         while (items.contains(aMessage) && (maxAttempts > 0)) {
1739             Item *next = messageItemAfter(aMessage, MessageTypeAny, false);
1740             if (!next) {
1741                 // no way
1742                 aMessage = nullptr;
1743                 break;
1744             }
1745             Q_ASSERT(next->type() == Item::Message);
1746             aMessage = static_cast<MessageItem *>(next);
1747             maxAttempts--;
1748         }
1749 
1750         if (!aMessage) {
1751             // try backwards
1752             aMessage = items.first();
1753             Q_ASSERT(aMessage);
1754             maxAttempts = items.count();
1755 
1756             while (items.contains(aMessage) && (maxAttempts > 0)) {
1757                 Item *prev = messageItemBefore(aMessage, MessageTypeAny, false);
1758                 if (!prev) {
1759                     // no way
1760                     aMessage = nullptr;
1761                     break;
1762                 }
1763                 Q_ASSERT(prev->type() == Item::Message);
1764                 aMessage = static_cast<MessageItem *>(prev);
1765                 maxAttempts--;
1766             }
1767         }
1768 
1769         if (aMessage) {
1770             QModelIndex aMessageIndex = d->mModel->index(aMessage, 0);
1771             Q_ASSERT(aMessageIndex.isValid());
1772             Q_ASSERT(static_cast<MessageItem *>(aMessageIndex.internalPointer()) == aMessage);
1773             Q_ASSERT(!selectionModel()->isSelected(aMessageIndex));
1774             setCurrentIndex(aMessageIndex);
1775             selectionModel()->select(aMessageIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
1776         }
1777     } // else we aren't clearing the entire selection so something should just stay selected.
1778 
1779     // Now mark messages as about to be removed.
1780 
1781     for (const auto mi : items) {
1782         mi->setAboutToBeRemoved(true);
1783         QModelIndex idx = d->mModel->index(mi, 0);
1784         Q_ASSERT(idx.isValid());
1785         Q_ASSERT(static_cast<MessageItem *>(idx.internalPointer()) == mi);
1786         if (selectionModel()->isSelected(idx)) {
1787             selectionModel()->select(idx, QItemSelectionModel::Deselect | QItemSelectionModel::Rows);
1788         }
1789     }
1790 
1791     viewport()->update();
1792 }
1793 
1794 void View::ensureDisplayedWithParentsExpanded(Item *it)
1795 {
1796     Q_ASSERT(it);
1797     Q_ASSERT(it->parent());
1798     Q_ASSERT(it->isViewable()); // must be attached to the viewable root
1799 
1800     if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) {
1801         setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false);
1802     }
1803 
1804     it = it->parent();
1805 
1806     while (it->parent()) {
1807         if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) {
1808             setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false);
1809         }
1810 
1811         QModelIndex idx = d->mModel->index(it, 0);
1812 
1813         Q_ASSERT(idx.isValid());
1814         Q_ASSERT(static_cast<Item *>(idx.internalPointer()) == it);
1815 
1816         if (!isExpanded(idx)) {
1817             setExpanded(idx, true);
1818         }
1819 
1820         it = it->parent();
1821     }
1822 }
1823 
1824 bool View::isDisplayedWithParentsExpanded(Item *it) const
1825 {
1826     // An item is currently viewable iff
1827     //  - it is marked as viewable in the item structure (that is, qt knows about its existence)
1828     //      (and this means that all of its parents are marked as viewable)
1829     //  - it is not explicitly hidden
1830     //  - all of its parents are expanded
1831 
1832     if (!it) {
1833         return false; // be nice and allow the caller not to care
1834     }
1835 
1836     if (!it->isViewable()) {
1837         return false; // item not viewable (not attached to the viewable root or qt not yet aware of it)
1838     }
1839 
1840     // the item and all the parents are marked as viewable.
1841 
1842     if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) {
1843         return false; // item qt representation explicitly hidden
1844     }
1845 
1846     // the item (and theoretically all the parents) are not explicitly hidden
1847 
1848     // check the parent chain
1849 
1850     it = it->parent();
1851 
1852     while (it) {
1853         if (it == d->mModel->rootItem()) {
1854             return true; // parent is root item: ok
1855         }
1856 
1857         // parent is not root item
1858 
1859         if (!isExpanded(d->mModel->index(it, 0))) {
1860             return false; // parent is not expanded (so child not actually visible)
1861         }
1862 
1863         it = it->parent(); // climb up
1864     }
1865 
1866     // parent hierarchy interrupted somewhere
1867     return false;
1868 }
1869 
1870 bool View::isThreaded() const
1871 {
1872     if (!d->mAggregation) {
1873         return false;
1874     }
1875     return d->mAggregation->threading() != Aggregation::NoThreading;
1876 }
1877 
1878 void View::slotSelectionChanged(const QItemSelection &, const QItemSelection &)
1879 {
1880     // We assume that when selection changes, current item also changes.
1881     QModelIndex current = currentIndex();
1882 
1883     if (!current.isValid()) {
1884         d->mLastCurrentItem = nullptr;
1885         d->mWidget->viewMessageSelected(nullptr);
1886         d->mWidget->viewSelectionChanged();
1887         return;
1888     }
1889 
1890     if (!selectionModel()->isSelected(current)) {
1891         if (selectedIndexes().count() < 1) {
1892             // It may happen after row removals: Model calls this slot on currentIndex()
1893             // that actually might have changed "silently", without being selected.
1894             QItemSelection selection;
1895             selection.append(QItemSelectionRange(current));
1896             selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows);
1897             return; // the above recurses
1898         } else {
1899             // something is still selected anyway
1900             // This is probably a result of CTRL+Click which unselected current: leave it as it is.
1901             return;
1902         }
1903     }
1904 
1905     Item *it = static_cast<Item *>(current.internalPointer());
1906     Q_ASSERT(it);
1907 
1908     switch (it->type()) {
1909     case Item::Message:
1910         if (d->mLastCurrentItem != it) {
1911             qCDebug(MESSAGELIST_LOG) << "View message selected [" << static_cast<MessageItem *>(it)->subject() << "]";
1912             d->mWidget->viewMessageSelected(static_cast<MessageItem *>(it));
1913             d->mLastCurrentItem = it;
1914         }
1915         break;
1916     case Item::GroupHeader:
1917         if (d->mLastCurrentItem) {
1918             d->mWidget->viewMessageSelected(nullptr);
1919             d->mLastCurrentItem = nullptr;
1920         }
1921         break;
1922     default:
1923         // should never happen
1924         Q_ASSERT(false);
1925         break;
1926     }
1927 
1928     d->mWidget->viewSelectionChanged();
1929 }
1930 
1931 void View::mouseDoubleClickEvent(QMouseEvent *e)
1932 {
1933     // Perform a hit test
1934     if (!d->mDelegate->hitTest(e->pos(), true)) {
1935         return;
1936     }
1937 
1938     // Something was hit :)
1939 
1940     Item *it = static_cast<Item *>(d->mDelegate->hitItem());
1941     if (!it) {
1942         return; // should never happen
1943     }
1944 
1945     switch (it->type()) {
1946     case Item::Message:
1947         // Let QTreeView handle the expansion
1948         QTreeView::mousePressEvent(e);
1949 
1950         switch (e->button()) {
1951         case Qt::LeftButton:
1952 
1953             if (d->mDelegate->hitContentItem()) {
1954                 // Double clicking on clickable icons does NOT activate the message
1955                 if (d->mDelegate->hitContentItem()->isIcon() && d->mDelegate->hitContentItem()->isClickable()) {
1956                     return;
1957                 }
1958             }
1959 
1960             d->mWidget->viewMessageActivated(static_cast<MessageItem *>(it));
1961             break;
1962         default:
1963             // make gcc happy
1964             break;
1965         }
1966         break;
1967     case Item::GroupHeader:
1968         // Don't let QTreeView handle the selection (as it deselects the current messages)
1969         switch (e->button()) {
1970         case Qt::LeftButton:
1971             if (it->childItemCount() > 0) {
1972                 // toggle expanded state
1973                 setExpanded(d->mDelegate->hitIndex(), !isExpanded(d->mDelegate->hitIndex()));
1974             }
1975             break;
1976         default:
1977             // make gcc happy
1978             break;
1979         }
1980         break;
1981     default:
1982         // should never happen
1983         Q_ASSERT(false);
1984         break;
1985     }
1986 }
1987 
1988 void View::changeMessageStatusRead(MessageItem *it, bool read)
1989 {
1990     Akonadi::MessageStatus set = it->status();
1991     Akonadi::MessageStatus unset = it->status();
1992     if (read) {
1993         set.setRead(true);
1994         unset.setRead(false);
1995     } else {
1996         set.setRead(false);
1997         unset.setRead(true);
1998     }
1999     viewport()->update();
2000 
2001     // This will actually request the widget to perform a status change on the storage.
2002     // The request will be then processed by the Model and the message will be updated again.
2003 
2004     d->mWidget->viewMessageStatusChangeRequest(it, set, unset);
2005 }
2006 
2007 void View::changeMessageStatus(MessageItem *it, Akonadi::MessageStatus set, Akonadi::MessageStatus unset)
2008 {
2009     // We first change the status of MessageItem itself. This will make the change
2010     // visible to the user even if the Model is actually in the middle of a long job (maybe it's loading)
2011     // and can't process the status change request immediately.
2012     // Here we actually desynchronize the cache and trust that the later call to
2013     // d->mWidget->viewMessageStatusChangeRequest() will really perform the status change on the storage.
2014     // Well... in KMail it will unless something is really screwed. Anyway, if it will not, at the next
2015     // load the status will be just unchanged: no animals will be harmed.
2016 
2017     qint32 stat = it->status().toQInt32();
2018     stat |= set.toQInt32();
2019     stat &= ~(unset.toQInt32());
2020     Akonadi::MessageStatus status;
2021     status.fromQInt32(stat);
2022     it->setStatus(status);
2023 
2024     // Trigger an update so the immediate change will be shown to the user
2025 
2026     viewport()->update();
2027 
2028     // This will actually request the widget to perform a status change on the storage.
2029     // The request will be then processed by the Model and the message will be updated again.
2030 
2031     d->mWidget->viewMessageStatusChangeRequest(it, set, unset);
2032 }
2033 
2034 void View::mousePressEvent(QMouseEvent *e)
2035 {
2036     d->mMousePressed = true;
2037     d->mLastMouseSource = e->source();
2038 
2039     if (d->mIsTouchEvent) {
2040         return;
2041     }
2042 
2043     d->onPressed(e);
2044 }
2045 
2046 void View::mouseMoveEvent(QMouseEvent *e)
2047 {
2048     if (d->mIsTouchEvent && !d->mTapAndHoldActive) {
2049         return;
2050     }
2051 
2052     if (!(e->buttons() & Qt::LeftButton)) {
2053         QTreeView::mouseMoveEvent(e);
2054         return;
2055     }
2056 
2057     if (d->mMousePressPosition.isNull()) {
2058         return;
2059     }
2060 
2061     if ((e->pos() - d->mMousePressPosition).manhattanLength() <= QApplication::startDragDistance()) {
2062         return;
2063     }
2064 
2065     d->mTapAndHoldActive = false;
2066     if (d->mRubberBand->isVisible()) {
2067         d->mRubberBand->hide();
2068     }
2069 
2070     d->mWidget->viewStartDragRequest();
2071 }
2072 
2073 #if 0
2074 void View::contextMenuEvent(QContextMenuEvent *e)
2075 {
2076     Q_UNUSED(e)
2077     QModelIndex index = currentIndex();
2078     if (index.isValid()) {
2079         QRect indexRect = this->visualRect(index);
2080         QPoint pos;
2081 
2082         if ((indexRect.isValid()) && (indexRect.bottom() > 0)) {
2083             if (indexRect.bottom() > viewport()->height()) {
2084                 if (indexRect.top() <= viewport()->height()) {
2085                     pos = indexRect.topLeft();
2086                 }
2087             } else {
2088                 pos = indexRect.bottomLeft();
2089             }
2090         }
2091 
2092         Item *item = static_cast< Item * >(index.internalPointer());
2093         if (item) {
2094             if (item->type() == Item::GroupHeader) {
2095                 d->mWidget->viewGroupHeaderContextPopupRequest(static_cast< GroupHeaderItem * >(item), viewport()->mapToGlobal(pos));
2096             } else if (!selectionEmpty()) {
2097                 d->mWidget->viewMessageListContextPopupRequest(selectionAsMessageItemList(), viewport()->mapToGlobal(pos));
2098                 e->accept();
2099             }
2100         }
2101     }
2102 }
2103 
2104 #endif
2105 
2106 void View::dragEnterEvent(QDragEnterEvent *e)
2107 {
2108     d->mWidget->viewDragEnterEvent(e);
2109 }
2110 
2111 void View::dragMoveEvent(QDragMoveEvent *e)
2112 {
2113     d->mWidget->viewDragMoveEvent(e);
2114 }
2115 
2116 void View::dropEvent(QDropEvent *e)
2117 {
2118     d->mWidget->viewDropEvent(e);
2119 }
2120 
2121 void View::changeEvent(QEvent *e)
2122 {
2123     switch (e->type()) {
2124     case QEvent::FontChange:
2125         d->mDelegate->generalFontChanged();
2126         [[fallthrough]];
2127     case QEvent::PaletteChange:
2128     case QEvent::StyleChange:
2129     case QEvent::LayoutDirectionChange:
2130     case QEvent::LocaleChange:
2131     case QEvent::LanguageChange:
2132         // All of these affect the theme's internal cache.
2133         setTheme(d->mTheme);
2134         // A layoutChanged() event will screw up the view state a bit.
2135         // Since this is a rare event we just reload the view.
2136         reload();
2137         break;
2138     default:
2139         // make gcc happy by default
2140         break;
2141     }
2142 
2143     QTreeView::changeEvent(e);
2144 }
2145 
2146 bool View::event(QEvent *e)
2147 {
2148     if (e->type() == QEvent::TouchBegin) {
2149         d->mIsTouchEvent = true;
2150         d->mMousePressed = false;
2151         return false;
2152     }
2153 
2154     if (e->type() == QEvent::Gesture) {
2155         d->gestureEvent(static_cast<QGestureEvent *>(e));
2156         e->accept();
2157         return true;
2158     }
2159 
2160     // We catch ToolTip events and pass everything else
2161 
2162     if (e->type() != QEvent::ToolTip) {
2163         return QTreeView::event(e);
2164     }
2165 
2166     if (!MessageListSettings::self()->messageToolTipEnabled()) {
2167         return true; // don't display tooltips
2168     }
2169 
2170     auto he = dynamic_cast<QHelpEvent *>(e);
2171     if (!he) {
2172         return true; // eh ?
2173     }
2174 
2175     QPoint pnt = viewport()->mapFromGlobal(mapToGlobal(he->pos()));
2176 
2177     if (pnt.y() < 0) {
2178         return true; // don't display the tooltip for items hidden under the header
2179     }
2180 
2181     QModelIndex idx = indexAt(pnt);
2182     if (!idx.isValid()) {
2183         return true; // may be
2184     }
2185 
2186     Item *it = static_cast<Item *>(idx.internalPointer());
2187     if (!it) {
2188         return true; // hum
2189     }
2190 
2191     Q_ASSERT(storageModel());
2192 
2193     QColor bckColor = palette().color(QPalette::ToolTipBase);
2194     QColor txtColor = palette().color(QPalette::ToolTipText);
2195     QColor darkerColor(((bckColor.red() * 8) + (txtColor.red() * 2)) / 10,
2196                        ((bckColor.green() * 8) + (txtColor.green() * 2)) / 10,
2197                        ((bckColor.blue() * 8) + (txtColor.blue() * 2)) / 10);
2198 
2199     QString bckColorName = bckColor.name();
2200     QString txtColorName = txtColor.name();
2201     QString darkerColorName = darkerColor.name();
2202     const bool textIsLeftToRight = (QApplication::layoutDirection() == Qt::LeftToRight);
2203     const QString textDirection = textIsLeftToRight ? QStringLiteral("left") : QStringLiteral("right");
2204 
2205     QString tip = QStringLiteral("<table width=\"100%\" border=\"0\" cellpadding=\"2\" cellspacing=\"0\">");
2206 
2207     switch (it->type()) {
2208     case Item::Message: {
2209         auto mi = static_cast<MessageItem *>(it);
2210 
2211         tip += QStringLiteral(
2212                    "<tr>"
2213                    "<td bgcolor=\"%1\" align=\"%4\" valign=\"middle\">"
2214                    "<div style=\"color: %2; font-weight: bold;\">"
2215                    "%3"
2216                    "</div>"
2217                    "</td>"
2218                    "</tr>")
2219                    .arg(txtColorName, bckColorName, mi->subject().toHtmlEscaped(), textDirection);
2220 
2221         tip += QLatin1StringView(
2222             "<tr>"
2223             "<td align=\"center\" valign=\"middle\">"
2224             "<table width=\"100%\" border=\"0\" cellpadding=\"2\" cellspacing=\"0\">");
2225 
2226         const QString htmlCodeForStandardRow = QStringLiteral(
2227             "<tr>"
2228             "<td align=\"right\" valign=\"top\" width=\"45\">"
2229             "<div style=\"font-weight: bold;\"><nobr>"
2230             "%1:"
2231             "</nobr></div>"
2232             "</td>"
2233             "<td align=\"left\" valign=\"top\">"
2234             "%2"
2235             "</td>"
2236             "</tr>");
2237 
2238         if (textIsLeftToRight) {
2239             tip += htmlCodeForStandardRow.arg(i18n("From"), mi->displaySender().toHtmlEscaped());
2240             tip += htmlCodeForStandardRow.arg(i18nc("Receiver of the email", "To"), mi->displayReceiver().toHtmlEscaped());
2241             tip += htmlCodeForStandardRow.arg(i18n("Date"), mi->formattedDate());
2242         } else {
2243             tip += htmlCodeForStandardRow.arg(mi->displaySender().toHtmlEscaped(), i18n("From"));
2244             tip += htmlCodeForStandardRow.arg(mi->displayReceiver().toHtmlEscaped(), i18nc("Receiver of the email", "To"));
2245             tip += htmlCodeForStandardRow.arg(mi->formattedDate(), i18n("Date"));
2246         }
2247 
2248         QString status = mi->statusDescription();
2249         const QString tags = mi->tagListDescription();
2250         if (!tags.isEmpty()) {
2251             if (!status.isEmpty()) {
2252                 status += QLatin1StringView(", ");
2253             }
2254             status += tags;
2255         }
2256 
2257         if (textIsLeftToRight) {
2258             tip += htmlCodeForStandardRow.arg(i18n("Status"), status);
2259             tip += htmlCodeForStandardRow.arg(i18n("Size"), mi->formattedSize());
2260             tip += htmlCodeForStandardRow.arg(i18n("Folder"), mi->folder());
2261         } else {
2262             tip += htmlCodeForStandardRow.arg(status, i18n("Status"));
2263             tip += htmlCodeForStandardRow.arg(mi->formattedSize(), i18n("Size"));
2264             tip += htmlCodeForStandardRow.arg(mi->folder(), i18n("Folder"));
2265         }
2266 
2267         if (mi->hasAnnotation()) {
2268             if (textIsLeftToRight) {
2269                 tip += htmlCodeForStandardRow.arg(i18n("Note"), mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("<br>")));
2270             } else {
2271                 tip += htmlCodeForStandardRow.arg(mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("<br>"))).arg(i18n("Note"));
2272             }
2273         }
2274 
2275         QString content = MessageList::Util::contentSummary(mi->akonadiItem());
2276         if (!content.trimmed().isEmpty()) {
2277             if (textIsLeftToRight) {
2278                 tip += htmlCodeForStandardRow.arg(i18n("Preview"), content.replace(QLatin1Char('\n'), QStringLiteral("<br>")));
2279             } else {
2280                 tip += htmlCodeForStandardRow.arg(content.replace(QLatin1Char('\n'), QStringLiteral("<br>"))).arg(i18n("Preview"));
2281             }
2282         }
2283 
2284         tip += QLatin1StringView(
2285             "</table>"
2286             "</td>"
2287             "</tr>");
2288 
2289         // FIXME: Find a way to show also CC and other header fields ?
2290 
2291         if (mi->hasChildren()) {
2292             Item::ChildItemStats stats;
2293             mi->childItemStats(stats);
2294 
2295             QString statsText;
2296 
2297             statsText = i18np("<b>%1</b> reply", "<b>%1</b> replies", mi->childItemCount());
2298             statsText += QLatin1StringView(", ");
2299 
2300             statsText += i18np("<b>%1</b> message in subtree (<b>%2</b> unread)",
2301                                "<b>%1</b> messages in subtree (<b>%2</b> unread)",
2302                                stats.mTotalChildCount,
2303                                stats.mUnreadChildCount);
2304 
2305             tip += QStringLiteral(
2306                        "<tr>"
2307                        "<td bgcolor=\"%1\" align=\"%3\" valign=\"middle\">"
2308                        "<nobr>%2</nobr>"
2309                        "</td>"
2310                        "</tr>")
2311                        .arg(darkerColorName, statsText, textDirection);
2312         }
2313 
2314         break;
2315     }
2316     case Item::GroupHeader: {
2317         auto ghi = static_cast<GroupHeaderItem *>(it);
2318 
2319         tip += QStringLiteral(
2320                    "<tr>"
2321                    "<td bgcolor=\"%1\" align=\"%4\" valign=\"middle\">"
2322                    "<div style=\"color: %2; font-weight: bold;\">"
2323                    "%3"
2324                    "</div>"
2325                    "</td>"
2326                    "</tr>")
2327                    .arg(txtColorName, bckColorName, ghi->label(), textDirection);
2328 
2329         QString description;
2330 
2331         switch (d->mAggregation->grouping()) {
2332         case Aggregation::GroupByDate:
2333             if (d->mAggregation->threading() != Aggregation::NoThreading) {
2334                 switch (d->mAggregation->threadLeader()) {
2335                 case Aggregation::TopmostMessage:
2336                     if (ghi->label().contains(QRegularExpression(QStringLiteral("[0-9]")))) {
2337                         description = i18nc("@info:tooltip Formats to something like 'Threads started on 2008-12-21'", "Threads started on %1", ghi->label());
2338                     } else {
2339                         description = i18nc("@info:tooltip Formats to something like 'Threads started Yesterday'", "Threads started %1", ghi->label());
2340                     }
2341                     break;
2342                 case Aggregation::MostRecentMessage:
2343                     description = i18n("Threads with messages dated %1", ghi->label());
2344                     break;
2345                 default:
2346                     // nuthin, make gcc happy
2347                     break;
2348                 }
2349             } else {
2350                 static const QRegularExpression reg(QStringLiteral("[0-9]"));
2351                 if (ghi->label().contains(reg)) {
2352                     if (storageModel()->containsOutboundMessages()) {
2353                         description = i18nc("@info:tooltip Formats to something like 'Messages sent on 2008-12-21'", "Messages sent on %1", ghi->label());
2354                     } else {
2355                         description =
2356                             i18nc("@info:tooltip Formats to something like 'Messages received on 2008-12-21'", "Messages received on %1", ghi->label());
2357                     }
2358                 } else {
2359                     if (storageModel()->containsOutboundMessages()) {
2360                         description = i18nc("@info:tooltip Formats to something like 'Messages sent Yesterday'", "Messages sent %1", ghi->label());
2361                     } else {
2362                         description = i18nc("@info:tooltip Formats to something like 'Messages received Yesterday'", "Messages received %1", ghi->label());
2363                     }
2364                 }
2365             }
2366             break;
2367         case Aggregation::GroupByDateRange:
2368             if (d->mAggregation->threading() != Aggregation::NoThreading) {
2369                 switch (d->mAggregation->threadLeader()) {
2370                 case Aggregation::TopmostMessage:
2371                     description = i18n("Threads started within %1", ghi->label());
2372                     break;
2373                 case Aggregation::MostRecentMessage:
2374                     description = i18n("Threads containing messages with dates within %1", ghi->label());
2375                     break;
2376                 default:
2377                     // nuthin, make gcc happy
2378                     break;
2379                 }
2380             } else {
2381                 if (storageModel()->containsOutboundMessages()) {
2382                     description = i18n("Messages sent within %1", ghi->label());
2383                 } else {
2384                     description = i18n("Messages received within %1", ghi->label());
2385                 }
2386             }
2387             break;
2388         case Aggregation::GroupBySenderOrReceiver:
2389         case Aggregation::GroupBySender:
2390             if (d->mAggregation->threading() != Aggregation::NoThreading) {
2391                 switch (d->mAggregation->threadLeader()) {
2392                 case Aggregation::TopmostMessage:
2393                     description = i18n("Threads started by %1", ghi->label());
2394                     break;
2395                 case Aggregation::MostRecentMessage:
2396                     description = i18n("Threads with most recent message by %1", ghi->label());
2397                     break;
2398                 default:
2399                     // nuthin, make gcc happy
2400                     break;
2401                 }
2402             } else {
2403                 if (storageModel()->containsOutboundMessages()) {
2404                     if (d->mAggregation->grouping() == Aggregation::GroupBySenderOrReceiver) {
2405                         description = i18n("Messages sent to %1", ghi->label());
2406                     } else {
2407                         description = i18n("Messages sent by %1", ghi->label());
2408                     }
2409                 } else {
2410                     description = i18n("Messages received from %1", ghi->label());
2411                 }
2412             }
2413             break;
2414         case Aggregation::GroupByReceiver:
2415             if (d->mAggregation->threading() != Aggregation::NoThreading) {
2416                 switch (d->mAggregation->threadLeader()) {
2417                 case Aggregation::TopmostMessage:
2418                     description = i18n("Threads directed to %1", ghi->label());
2419                     break;
2420                 case Aggregation::MostRecentMessage:
2421                     description = i18n("Threads with most recent message directed to %1", ghi->label());
2422                     break;
2423                 default:
2424                     // nuthin, make gcc happy
2425                     break;
2426                 }
2427             } else {
2428                 if (storageModel()->containsOutboundMessages()) {
2429                     description = i18n("Messages sent to %1", ghi->label());
2430                 } else {
2431                     description = i18n("Messages received by %1", ghi->label());
2432                 }
2433             }
2434             break;
2435         default:
2436             // nuthin, make gcc happy
2437             break;
2438         }
2439 
2440         if (!description.isEmpty()) {
2441             tip += QStringLiteral(
2442                        "<tr>"
2443                        "<td align=\"%2\" valign=\"middle\">"
2444                        "%1"
2445                        "</td>"
2446                        "</tr>")
2447                        .arg(description, textDirection);
2448         }
2449 
2450         if (ghi->hasChildren()) {
2451             Item::ChildItemStats stats;
2452             ghi->childItemStats(stats);
2453 
2454             QString statsText;
2455 
2456             if (d->mAggregation->threading() != Aggregation::NoThreading) {
2457                 statsText = i18np("<b>%1</b> thread", "<b>%1</b> threads", ghi->childItemCount());
2458                 statsText += QLatin1StringView(", ");
2459             }
2460 
2461             statsText +=
2462                 i18np("<b>%1</b> message (<b>%2</b> unread)", "<b>%1</b> messages (<b>%2</b> unread)", stats.mTotalChildCount, stats.mUnreadChildCount);
2463 
2464             tip += QStringLiteral(
2465                        "<tr>"
2466                        "<td bgcolor=\"%1\" align=\"%3\" valign=\"middle\">"
2467                        "<nobr>%2</nobr>"
2468                        "</td>"
2469                        "</tr>")
2470                        .arg(darkerColorName, statsText, textDirection);
2471         }
2472 
2473         break;
2474     }
2475     default:
2476         // nuthin (just make gcc happy for now)
2477         break;
2478     }
2479 
2480     tip += QLatin1StringView("</table>");
2481 
2482     QToolTip::showText(he->globalPos(), tip, viewport(), visualRect(idx));
2483 
2484     return true;
2485 }
2486 
2487 void View::slotExpandAllThreads()
2488 {
2489     setAllThreadsExpanded(true);
2490 }
2491 
2492 void View::slotCollapseAllThreads()
2493 {
2494     setAllThreadsExpanded(false);
2495 }
2496 
2497 void View::slotCollapseAllGroups()
2498 {
2499     setAllGroupsExpanded(false);
2500 }
2501 
2502 void View::slotExpandAllGroups()
2503 {
2504     setAllGroupsExpanded(true);
2505 }
2506 
2507 void View::slotCollapseCurrentItem()
2508 {
2509     setCurrentThreadExpanded(false);
2510 }
2511 
2512 void View::slotExpandCurrentItem()
2513 {
2514     setCurrentThreadExpanded(true);
2515 }
2516 
2517 void View::focusQuickSearch(const QString &selectedText)
2518 {
2519     d->mWidget->focusQuickSearch(selectedText);
2520 }
2521 
2522 QList<Akonadi::MessageStatus> View::currentFilterStatus() const
2523 {
2524     return d->mWidget->currentFilterStatus();
2525 }
2526 
2527 MessageList::Core::QuickSearchLine::SearchOptions View::currentOptions() const
2528 {
2529     return d->mWidget->currentOptions();
2530 }
2531 
2532 QString View::currentFilterSearchString() const
2533 {
2534     return d->mWidget->currentFilterSearchString();
2535 }
2536 
2537 void View::setRowHidden(int row, const QModelIndex &parent, bool hide)
2538 {
2539     const QModelIndex rowModelIndex = model()->index(row, 0, parent);
2540     const Item *const rowItem = static_cast<Item *>(rowModelIndex.internalPointer());
2541 
2542     if (rowItem) {
2543         const bool currentlyHidden = isRowHidden(row, parent);
2544 
2545         if (currentlyHidden != hide) {
2546             if (currentMessageItem() == rowItem) {
2547                 selectionModel()->clear();
2548                 selectionModel()->clearSelection();
2549             }
2550         }
2551     }
2552 
2553     QTreeView::setRowHidden(row, parent, hide);
2554 }
2555 
2556 void View::sortOrderMenuAboutToShow(QMenu *menu)
2557 {
2558     d->mWidget->sortOrderMenuAboutToShow(menu);
2559 }
2560 
2561 void View::aggregationMenuAboutToShow(QMenu *menu)
2562 {
2563     d->mWidget->aggregationMenuAboutToShow(menu);
2564 }
2565 
2566 void View::themeMenuAboutToShow(QMenu *menu)
2567 {
2568     d->mWidget->themeMenuAboutToShow(menu);
2569 }
2570 
2571 void View::setCollapseItem(const QModelIndex &index)
2572 {
2573     if (index.isValid()) {
2574         setExpanded(index, false);
2575     }
2576 }
2577 
2578 void View::setExpandItem(const QModelIndex &index)
2579 {
2580     if (index.isValid()) {
2581         setExpanded(index, true);
2582     }
2583 }
2584 
2585 void View::setQuickSearchClickMessage(const QString &msg)
2586 {
2587     d->mWidget->quickSearch()->setPlaceholderText(msg);
2588 }
2589 
2590 void View::ViewPrivate::onPressed(QMouseEvent *e)
2591 {
2592     mMousePressPosition = QPoint();
2593 
2594     // Perform a hit test
2595     if (!mDelegate->hitTest(e->pos(), true)) {
2596         return;
2597     }
2598 
2599     // Something was hit :)
2600 
2601     Item *it = static_cast<Item *>(mDelegate->hitItem());
2602     if (!it) {
2603         return; // should never happen
2604     }
2605 
2606     // Abort any pending message pre-selection as the user is probably
2607     // already navigating the view (so pre-selection would make his view jump
2608     // to an unexpected place).
2609     mModel->setPreSelectionMode(PreSelectNone);
2610 
2611     switch (it->type()) {
2612     case Item::Message:
2613         mMousePressPosition = e->pos();
2614 
2615         switch (e->button()) {
2616         case Qt::LeftButton:
2617             // if we have multi selection then the meaning of hitting
2618             // the content item is quite unclear.
2619             if (mDelegate->hitContentItem() && (q->selectedIndexes().count() > 1)) {
2620                 qCDebug(MESSAGELIST_LOG) << "Left hit with selectedIndexes().count() == " << q->selectedIndexes().count();
2621 
2622                 switch (mDelegate->hitContentItem()->type()) {
2623                 case Theme::ContentItem::AnnotationIcon:
2624                     static_cast<MessageItem *>(it)->editAnnotation(q);
2625                     return; // don't select the item
2626                     break;
2627                 case Theme::ContentItem::ActionItemStateIcon:
2628                     q->changeMessageStatus(static_cast<MessageItem *>(it),
2629                                            it->status().isToAct() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusToAct(),
2630                                            it->status().isToAct() ? Akonadi::MessageStatus::statusToAct() : Akonadi::MessageStatus());
2631                     return; // don't select the item
2632                     break;
2633                 case Theme::ContentItem::ImportantStateIcon:
2634                     q->changeMessageStatus(static_cast<MessageItem *>(it),
2635                                            it->status().isImportant() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusImportant(),
2636                                            it->status().isImportant() ? Akonadi::MessageStatus::statusImportant() : Akonadi::MessageStatus());
2637                     return; // don't select the item
2638                 case Theme::ContentItem::ReadStateIcon:
2639                     q->changeMessageStatusRead(static_cast<MessageItem *>(it), it->status().isRead() ? false : true);
2640                     return;
2641                     break;
2642                 case Theme::ContentItem::SpamHamStateIcon:
2643                     q->changeMessageStatus(static_cast<MessageItem *>(it),
2644                                            it->status().isSpam()
2645                                                ? Akonadi::MessageStatus()
2646                                                : (it->status().isHam() ? Akonadi::MessageStatus::statusSpam() : Akonadi::MessageStatus::statusHam()),
2647                                            it->status().isSpam() ? Akonadi::MessageStatus::statusSpam()
2648                                                                  : (it->status().isHam() ? Akonadi::MessageStatus::statusHam() : Akonadi::MessageStatus()));
2649                     return; // don't select the item
2650                     break;
2651                 case Theme::ContentItem::WatchedIgnoredStateIcon:
2652                     q->changeMessageStatus(static_cast<MessageItem *>(it),
2653                                            it->status().isIgnored()
2654                                                ? Akonadi::MessageStatus()
2655                                                : (it->status().isWatched() ? Akonadi::MessageStatus::statusIgnored() : Akonadi::MessageStatus::statusWatched()),
2656                                            it->status().isIgnored()
2657                                                ? Akonadi::MessageStatus::statusIgnored()
2658                                                : (it->status().isWatched() ? Akonadi::MessageStatus::statusWatched() : Akonadi::MessageStatus()));
2659                     return; // don't select the item
2660                     break;
2661                 default:
2662                     // make gcc happy
2663                     break;
2664                 }
2665             }
2666 
2667             // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called)
2668             q->QTreeView::mousePressEvent(e);
2669 
2670             break;
2671         case Qt::RightButton:
2672             // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called)
2673             q->QTreeView::mousePressEvent(e);
2674             e->accept();
2675             mWidget->viewMessageListContextPopupRequest(q->selectionAsMessageItemList(), q->viewport()->mapToGlobal(e->pos()));
2676 
2677             break;
2678         default:
2679             // make gcc happy
2680             break;
2681         }
2682         break;
2683     case Item::GroupHeader: {
2684         // Don't let QTreeView handle the selection (as it deselects the current messages)
2685         auto groupHeaderItem = static_cast<GroupHeaderItem *>(it);
2686 
2687         switch (e->button()) {
2688         case Qt::LeftButton: {
2689             QModelIndex index = mModel->index(groupHeaderItem, 0);
2690 
2691             if (index.isValid()) {
2692                 q->setCurrentIndex(index);
2693             }
2694 
2695             if (!mDelegate->hitContentItem()) {
2696                 return;
2697             }
2698 
2699             if (mDelegate->hitContentItem()->type() == Theme::ContentItem::ExpandedStateIcon) {
2700                 if (groupHeaderItem->childItemCount() > 0) {
2701                     // toggle expanded state
2702                     q->setExpanded(mDelegate->hitIndex(), !q->isExpanded(mDelegate->hitIndex()));
2703                 }
2704             }
2705             break;
2706         }
2707         case Qt::RightButton:
2708             mWidget->viewGroupHeaderContextPopupRequest(groupHeaderItem, q->viewport()->mapToGlobal(e->pos()));
2709             break;
2710         default:
2711             // make gcc happy
2712             break;
2713         }
2714         break;
2715     }
2716     default:
2717         // should never happen
2718         Q_ASSERT(false);
2719         break;
2720     }
2721 }
2722 
2723 void View::ViewPrivate::gestureEvent(QGestureEvent *e)
2724 {
2725     if (QGesture *gesture = e->gesture(Qt::TapGesture)) {
2726         tapTriggered(static_cast<QTapGesture *>(gesture));
2727     }
2728     if (QGesture *gesture = e->gesture(Qt::TapAndHoldGesture)) {
2729         tapAndHoldTriggered(static_cast<QTapAndHoldGesture *>(gesture));
2730     }
2731     if (QGesture *gesture = e->gesture(mTwoFingerTap)) {
2732         twoFingerTapTriggered(static_cast<KTwoFingerTap *>(gesture));
2733     }
2734 }
2735 
2736 void View::ViewPrivate::tapTriggered(QTapGesture *tap)
2737 {
2738     static bool scrollerWasScrolling = false;
2739 
2740     if (tap->state() == Qt::GestureStarted) {
2741         mTapAndHoldActive = false;
2742 
2743         // if QScroller state is Scrolling or Dragging, the user makes the tap to stop the scrolling
2744         if (mScroller->state() == QScroller::Scrolling || mScroller->state() == QScroller::Dragging) {
2745             scrollerWasScrolling = true;
2746         } else if (mScroller->state() == QScroller::Pressed || mScroller->state() == QScroller::Inactive) {
2747             scrollerWasScrolling = false;
2748         }
2749     }
2750 
2751     if (tap->state() == Qt::GestureFinished && !scrollerWasScrolling) {
2752         mIsTouchEvent = false;
2753 
2754         // with touch you can touch multiple widgets at the same time, but only one widget will get a mousePressEvent.
2755         // we use this to select the right window
2756         if (!mMousePressed) {
2757             return;
2758         }
2759 
2760         if (mRubberBand->isVisible()) {
2761             mRubberBand->hide();
2762         }
2763 
2764         // simulate a mousePressEvent, to allow QTreeView to select the items
2765         QMouseEvent fakeMousePress(QEvent::MouseButtonPress,
2766                                    tap->position(),
2767                                    q->viewport()->mapToGlobal(tap->position()),
2768                                    mTapAndHoldActive ? Qt::RightButton : Qt::LeftButton,
2769                                    mTapAndHoldActive ? Qt::RightButton : Qt::LeftButton,
2770                                    Qt::NoModifier);
2771 
2772         onPressed(&fakeMousePress);
2773         mTapAndHoldActive = false;
2774     }
2775 
2776     if (tap->state() == Qt::GestureCanceled) {
2777         mIsTouchEvent = false;
2778         if (mRubberBand->isVisible()) {
2779             mRubberBand->hide();
2780         }
2781         mTapAndHoldActive = false;
2782     }
2783 }
2784 
2785 void View::ViewPrivate::tapAndHoldTriggered(QTapAndHoldGesture *tap)
2786 {
2787     if (tap->state() == Qt::GestureFinished) {
2788         // with touch you can touch multiple widgets at the same time, but only one widget will get a mousePressEvent.
2789         // we use this to select the right window
2790         if (!mMousePressed) {
2791             return;
2792         }
2793 
2794         // the TapAndHoldGesture is triggerable the with mouse, we don't want this
2795         if (mLastMouseSource == Qt::MouseEventNotSynthesized) {
2796             return;
2797         }
2798 
2799         // the TapAndHoldGesture is triggerable the with stylus, we don't want this
2800         if (!mIsTouchEvent) {
2801             return;
2802         }
2803 
2804         mTapAndHoldActive = true;
2805         mScroller->stop();
2806 
2807         // simulate a mousePressEvent, to allow QTreeView to select the items
2808         const QPoint tapViewportPos(q->viewport()->mapFromGlobal(tap->position().toPoint()));
2809         QMouseEvent fakeMousePress(QEvent::MouseButtonPress, tapViewportPos, tapViewportPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
2810         onPressed(&fakeMousePress);
2811 
2812         const QPoint tapIndicatorSize(80, 80); // size for the tapAndHold indicator
2813         const QPoint pos(q->mapFromGlobal(tap->position().toPoint()));
2814         const QRect tapIndicatorRect(pos - (tapIndicatorSize / 2), pos + (tapIndicatorSize / 2));
2815         mRubberBand->setGeometry(tapIndicatorRect.normalized());
2816         mRubberBand->show();
2817     }
2818 }
2819 
2820 void View::ViewPrivate::twoFingerTapTriggered(KTwoFingerTap *tap)
2821 {
2822     if (tap->state() == Qt::GestureFinished) {
2823         if (mTapAndHoldActive) {
2824             return;
2825         }
2826 
2827         // with touch you can touch multiple widgets at the same time, but only one widget will get a mousePressEvent.
2828         // we use this to select the right window
2829         if (!mMousePressed) {
2830             return;
2831         }
2832 
2833         // simulate a mousePressEvent with Qt::ControlModifier, to allow QTreeView to select the items
2834         QMouseEvent fakeMousePress(QEvent::MouseButtonPress,
2835                                    tap->pos(),
2836                                    q->viewport()->mapToGlobal(tap->pos()),
2837                                    Qt::LeftButton,
2838                                    Qt::LeftButton,
2839                                    Qt::ControlModifier);
2840         onPressed(&fakeMousePress);
2841     }
2842 }
2843 
2844 #include "moc_view.cpp"