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"