Messagelib

view.cpp
1 /******************************************************************************
2  *
3  * SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
4  *
5  * SPDX-License-Identifier: GPL-2.0-or-later
6  *
7  *******************************************************************************/
8 
9 #include "core/view.h"
10 #include "core/aggregation.h"
11 #include "core/delegate.h"
12 #include "core/groupheaderitem.h"
13 #include "core/item.h"
14 #include "core/messageitem.h"
15 #include "core/model.h"
16 #include "core/storagemodelbase.h"
17 #include "core/theme.h"
18 #include "core/widgetbase.h"
19 #include "messagelistsettings.h"
20 #include "messagelistutil.h"
21 #include "messagelistutil_p.h"
22 
23 #include "MessageCore/StringUtil"
24 
25 #include <KMime/DateFormatter> // kdepimlibs
26 
27 #include <Akonadi/Item>
28 #include <KTwoFingerTap>
29 #include <QApplication>
30 #include <QGestureEvent>
31 #include <QHeaderView>
32 #include <QHelpEvent>
33 #include <QLineEdit>
34 #include <QMenu>
35 #include <QPainter>
36 #include <QScrollBar>
37 #include <QScroller>
38 #include <QTimer>
39 #include <QToolTip>
40 
41 #include "messagelist_debug.h"
42 #include <KLocalizedString>
43 
44 using namespace MessageList::Core;
45 
46 class View::ViewPrivate
47 {
48 public:
49  ViewPrivate(View *owner, Widget *parent)
50  : q(owner)
51  , mWidget(parent)
52  , mDelegate(new Delegate(owner))
53  {
54  }
55 
56  void expandFullThread(const QModelIndex &index);
57  void generalPaletteChanged();
58  void onPressed(QMouseEvent *e);
59  void gestureEvent(QGestureEvent *e);
60  void tapTriggered(QTapGesture *tap);
61  void tapAndHoldTriggered(QTapAndHoldGesture *tap);
62  void twoFingerTapTriggered(KTwoFingerTap *tap);
63 
64  QColor mTextColor;
65  View *const q;
66 
67  Widget *const mWidget;
68  Model *mModel = nullptr;
69  Delegate *const mDelegate;
70 
71  const Aggregation *mAggregation = nullptr; ///< The Aggregation we're using now, shallow pointer
72  Theme *mTheme = nullptr; ///< The Theme we're using now, shallow pointer
73  bool mNeedToApplyThemeColumns = false; ///< Flag signaling a pending application of theme columns
74  Item *mLastCurrentItem = nullptr;
75  QPoint mMousePressPosition;
76  bool mSaveThemeColumnStateOnSectionResize = true; ///< This is used to filter out programmatic column resizes in slotSectionResized().
77  QTimer *mSaveThemeColumnStateTimer = nullptr; ///< Used to trigger a delayed "save theme state"
78  QTimer *mApplyThemeColumnsTimer = nullptr; ///< Used to trigger a delayed "apply theme columns"
79  int mLastViewportWidth = -1;
80  bool mIgnoreUpdateGeometries = false; ///< Shall we ignore the "update geometries" calls ?
81  QScroller *mScroller = nullptr;
82  bool mIsTouchEvent = false;
83  bool mMousePressed = false;
85  bool mTapAndHoldActive = false;
86  QRubberBand *mRubberBand = nullptr;
87  Qt::GestureType mTwoFingerTap = Qt::CustomGesture;
88 };
89 
90 View::View(Widget *pParent)
91  : QTreeView(pParent)
92  , d(new ViewPrivate(this, pParent))
93 {
94  d->mSaveThemeColumnStateTimer = new QTimer();
95  connect(d->mSaveThemeColumnStateTimer, &QTimer::timeout, this, &View::saveThemeColumnState);
96 
97  d->mApplyThemeColumnsTimer = new QTimer();
98  connect(d->mApplyThemeColumnsTimer, &QTimer::timeout, this, &View::applyThemeColumns);
99 
100  setItemDelegate(d->mDelegate);
101  setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
102  setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
103  setAlternatingRowColors(true);
104  setAllColumnsShowFocus(true);
105  setSelectionMode(QAbstractItemView::ExtendedSelection);
106  viewport()->setAcceptDrops(true);
107 
108  d->mScroller = QScroller::scroller(viewport());
109  QScrollerProperties scrollerProp;
110  scrollerProp.setScrollMetric(QScrollerProperties::AcceleratingFlickMaximumTime, 0.2); // QTBUG-88249
111  d->mScroller->setScrollerProperties(scrollerProp);
112  d->mScroller->grabGesture(viewport());
113 
114  setAttribute(Qt::WA_AcceptTouchEvents);
116  viewport()->grabGesture(d->mTwoFingerTap);
117  viewport()->grabGesture(Qt::TapGesture);
118  viewport()->grabGesture(Qt::TapAndHoldGesture);
119 
120  d->mRubberBand = new QRubberBand(QRubberBand::Rectangle, this);
121 
122  header()->setContextMenuPolicy(Qt::CustomContextMenu);
123  connect(header(), &QWidget::customContextMenuRequested, this, &View::slotHeaderContextMenuRequested);
124  connect(header(), &QHeaderView::sectionResized, this, &View::slotHeaderSectionResized);
125 
126  header()->setSectionsClickable(true);
127  header()->setSectionResizeMode(QHeaderView::Interactive);
128  header()->setMinimumSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value
129  header()->setDefaultSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value
130 
131  d->mModel = new Model(this);
132  setModel(d->mModel);
133 
134  connect(d->mModel, &Model::statusMessage, pParent, &Widget::statusMessage);
135 
136  connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection);
137 
138  // as in KDE3, when a root-item of a message thread is expanded, expand all children
139  connect(this, &View::expanded, this, [this](const QModelIndex &index) {
140  d->expandFullThread(index);
141  });
142 }
143 
144 View::~View()
145 {
146  if (d->mSaveThemeColumnStateTimer->isActive()) {
147  d->mSaveThemeColumnStateTimer->stop();
148  }
149  delete d->mSaveThemeColumnStateTimer;
150  if (d->mApplyThemeColumnsTimer->isActive()) {
151  d->mApplyThemeColumnsTimer->stop();
152  }
153  delete d->mApplyThemeColumnsTimer;
154 
155  // Zero out the theme, aggregation and ApplyThemeColumnsTimer so Model will not cause accesses to them in its destruction process
156  d->mApplyThemeColumnsTimer = nullptr;
157 
158  d->mTheme = nullptr;
159  d->mAggregation = nullptr;
160 }
161 
162 Model *View::model() const
163 {
164  return d->mModel;
165 }
166 
167 Delegate *View::delegate() const
168 {
169  return d->mDelegate;
170 }
171 
172 void View::ignoreCurrentChanges(bool ignore)
173 {
174  if (ignore) {
175  disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged);
176  viewport()->setUpdatesEnabled(false);
177  } else {
178  connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection);
179  viewport()->setUpdatesEnabled(true);
180  }
181 }
182 
183 void View::ignoreUpdateGeometries(bool ignore)
184 {
185  d->mIgnoreUpdateGeometries = ignore;
186 }
187 
188 bool View::isScrollingLocked() const
189 {
190  // There is another popular requisite: people want the view to automatically
191  // scroll in order to show new arriving mail. This actually makes sense
192  // only when the view is sorted by date and the new mail is (usually) either
193  // appended at the bottom or inserted at the top. It would be also confusing
194  // when the user is browsing some other thread in the meantime.
195  //
196  // So here we make a simple guess: if the view is scrolled somewhere in the
197  // middle then we assume that the user is browsing other threads and we
198  // try to keep the currently selected item steady on the screen.
199  // When the view is "locked" to the top (scrollbar value 0) or to the
200  // bottom (scrollbar value == maximum) then we assume that the user
201  // isn't browsing and we should attempt to show the incoming messages
202  // by keeping the view "locked".
203  //
204  // The "locking" also doesn't make sense in the first big fill view job.
205  // [Well this concept is pre-akonadi. Now the loading is all async anyway...
206  // So all this code is actually triggered during the initial loading, too.]
207  const int scrollBarPosition = verticalScrollBar()->value();
208  const int scrollBarMaximum = verticalScrollBar()->maximum();
209  const SortOrder *sortOrder = d->mModel->sortOrder();
210  const bool lockView = (
211  // not the first loading job
212  !d->mModel->isLoading())
213  && (
214  // messages sorted by date
215  (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTime)
216  || (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTimeOfMostRecent))
217  && (
218  // scrollbar at top (Descending order) or bottom (Ascending order)
219  (scrollBarPosition == 0 && sortOrder->messageSortDirection() == SortOrder::Descending)
220  || (scrollBarPosition == scrollBarMaximum && sortOrder->messageSortDirection() == SortOrder::Ascending));
221  return lockView;
222 }
223 
224 void View::updateGeometries()
225 {
226  if (d->mIgnoreUpdateGeometries || !d->mModel) {
227  return;
228  }
229 
230  const int scrollBarPositionBefore = verticalScrollBar()->value();
231  const bool lockView = isScrollingLocked();
232 
234 
235  if (lockView) {
236  // we prefer to keep the view locked to the top or bottom
237  if (scrollBarPositionBefore != 0) {
238  // we wanted the view to be locked to the bottom
239  if (verticalScrollBar()->value() != verticalScrollBar()->maximum()) {
240  verticalScrollBar()->setValue(verticalScrollBar()->maximum());
241  }
242  } // else we wanted the view to be locked to top and we shouldn't need to do anything
243  }
244 }
245 
246 StorageModel *View::storageModel() const
247 {
248  return d->mModel->storageModel();
249 }
250 
251 void View::setAggregation(const Aggregation *aggregation)
252 {
253  d->mAggregation = aggregation;
254  d->mModel->setAggregation(aggregation);
255 
256  // use uniform row heights to speed up, but only if there are no group headers used
257  setUniformRowHeights(d->mAggregation->grouping() == Aggregation::NoGrouping);
258 }
259 
260 void View::setTheme(Theme *theme)
261 {
262  d->mNeedToApplyThemeColumns = true;
263  d->mTheme = theme;
264  d->mDelegate->setTheme(theme);
265  d->mModel->setTheme(theme);
266 }
267 
268 void View::setSortOrder(const SortOrder *sortOrder)
269 {
270  d->mModel->setSortOrder(sortOrder);
271 }
272 
273 void View::reload()
274 {
275  setStorageModel(storageModel());
276 }
277 
278 void View::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode)
279 {
280  // This will cause the model to be reset.
281  d->mSaveThemeColumnStateOnSectionResize = false;
282  d->mModel->setStorageModel(storageModel, preSelectionMode);
283  d->mSaveThemeColumnStateOnSectionResize = true;
284 }
285 
286 //////////////////////////////////////////////////////////////////////////////////////////////////////
287 // Theme column state machinery
288 //
289 // This is yet another beast to beat. The QHeaderView behaviour, at the time of writing,
290 // is quite unpredictable. This is due to the complex interaction with the model, with the QTreeView
291 // and due to its attempts to delay the layout jobs. The delayed layouts, especially, may
292 // cause the widths of the columns to quickly change in an unexpected manner in a place
293 // where previously they have been always settled to the values you set...
294 //
295 // So here we have the tools to:
296 //
297 // - Apply the saved state of the theme columns (applyThemeColumns()).
298 // This function computes the "best fit" state of the visible columns and tries
299 // to apply it to QHeaderView. It also saves the new computed state to the Theme object.
300 //
301 // - Explicitly save the column state, used when the user changes the widths or visibility manually.
302 // This is called through a delayed timer after a column has been resized or used directly
303 // when the visibility state of a column has been changed by toggling a popup menu entry.
304 //
305 // - Display the column state context popup menu and handle its actions
306 //
307 // - Apply the theme columns when the theme changes, when the model changes or when
308 // the widget is resized.
309 //
310 // - Avoid saving a corrupted column state in that QHeaderView can be found *very* frequently.
311 //
312 
313 void View::applyThemeColumns()
314 {
315  if (!d->mApplyThemeColumnsTimer) {
316  return;
317  }
318 
319  if (d->mApplyThemeColumnsTimer->isActive()) {
320  d->mApplyThemeColumnsTimer->stop();
321  }
322 
323  if (!d->mTheme) {
324  return;
325  }
326 
327  // qCDebug(MESSAGELIST_LOG) << "Apply theme columns";
328 
329  const QList<Theme::Column *> &columns = d->mTheme->columns();
330 
331  if (columns.isEmpty()) {
332  return; // bad theme
333  }
334 
335  if (!viewport()->isVisible()) {
336  return; // invisible
337  }
338 
339  if (viewport()->width() < 1) {
340  return; // insane width
341  }
342  const int viewportWidth = viewport()->width();
343  d->mLastViewportWidth = viewportWidth;
344 
345  // Now we want to distribute the available width on all the visible columns.
346  //
347  // The rules:
348  // - The visible columns will span the width of the view, if possible.
349  // - The columns with a saved width should take that width.
350  // - The columns on the left should take more space, if possible.
351  // - The columns with no text take just slightly more than their size hint.
352  // while the columns with text take possibly a lot more.
353  //
354 
355  // Note that the first column is always shown (it can't be hidden at all)
356 
357  // The algorithm below is a sort of compromise between:
358  // - Saving the user preferences for widths
359  // - Using exactly the available view space
360  //
361  // It "tends to work" in all cases:
362  // - When there are no user preferences saved and the column widths must be
363  // automatically computed to make best use of available space
364  // - When there are user preferences for only some of the columns
365  // and that should be somewhat preserved while still using all the
366  // available space.
367  // - When all the columns have well defined saved widths
368 
369  int idx = 0;
370 
371  // Gather total size "hint" for visible sections: if the widths of the columns wers
372  // all saved then the total hint is equal to the total saved width.
373 
374  int totalVisibleWidthHint = 0;
375  QList<int> lColumnSizeHints;
376  for (const auto col : std::as_const(columns)) {
377  if (col->currentlyVisible() || (idx == 0)) {
378  // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be visible";
379  // Column visible
380  const int savedWidth = col->currentWidth();
381  const int hintWidth = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width();
382  totalVisibleWidthHint += savedWidth > 0 ? savedWidth : hintWidth;
383  lColumnSizeHints.append(hintWidth);
384  // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " size hint is " << hintWidth;
385  } else {
386  // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be not visible";
387  // The column is not visible
388  lColumnSizeHints.append(-1); // dummy
389  }
390  idx++;
391  }
392 
393  if (totalVisibleWidthHint < 16) {
394  totalVisibleWidthHint = 16; // be reasonable
395  }
396 
397  // Now compute somewhat "proportional" widths.
398  idx = 0;
399 
400  QList<double> lColumnWidths;
401  lColumnWidths.reserve(columns.count());
402  int totalVisibleWidth = 0;
403  for (const auto col : std::as_const(columns)) {
404  double savedWidth = col->currentWidth();
405  double hintWidth = savedWidth > 0 ? savedWidth : lColumnSizeHints.at(idx);
406  double realWidth;
407 
408  if (col->currentlyVisible() || (idx == 0)) {
409  if (col->containsTextItems()) {
410  // the column contains text items, it should get more space (if possible)
411  realWidth = ((hintWidth * viewportWidth) / totalVisibleWidthHint);
412  } else {
413  // the column contains no text items, it should get exactly its hint/saved width.
414  realWidth = hintWidth;
415  }
416 
417  if (realWidth < 2) {
418  realWidth = 2; // don't allow very insane values
419  }
420 
421  totalVisibleWidth += realWidth;
422  } else {
423  // Column not visible
424  realWidth = -1;
425  }
426 
427  lColumnWidths.append(realWidth);
428 
429  idx++;
430  }
431 
432  // Now the algorithm above may be wrong for several reasons...
433  // - We're using fixed widths for certain columns and proportional
434  // for others...
435  // - The user might have changed the width of the view from the
436  // time in that the widths have been saved
437  // - There are some (not well identified) issues with the QTreeView
438  // scrollbar that make our view appear larger or shorter by 2-3 pixels
439  // sometimes.
440  // - ...
441  // So we correct the previous estimates by trying to use exactly
442  // the available space.
443 
444  idx = 0;
445 
446  if (totalVisibleWidth != viewportWidth) {
447  // The estimated widths were not using exactly the available space.
448  if (totalVisibleWidth < viewportWidth) {
449  // We were using less space than available.
450 
451  // Give the additional space to the text columns
452  // also give more space to the first ones and less space to the last ones
453  qreal available = viewportWidth - totalVisibleWidth;
454 
455  for (int idx = 0; idx < columns.count(); ++idx) {
456  Theme::Column *column = columns.at(idx);
457  if ((column->currentlyVisible() || (idx == 0)) && column->containsTextItems()) {
458  // give more space to this column
459  available /= 2; // eat half of the available space
460  lColumnWidths[idx] += available; // and give it to this column
461  if (available < 1) {
462  break; // no more space to give away
463  }
464  }
465  }
466 
467  // if any space is still available, give it to the first column
468  if (available >= 1) {
469  lColumnWidths[0] += available;
470  }
471  } else {
472  // We were using more space than available
473 
474  // If the columns span more than the view then
475  // try to squeeze them in order to make them fit
476  double missing = totalVisibleWidth - viewportWidth;
477  if (missing > 0) {
478  const int count = lColumnWidths.count();
479  idx = count - 1;
480 
481  while (idx >= 0) {
482  if (columns.at(idx)->currentlyVisible() || (idx == 0)) {
483  double chop = lColumnWidths.at(idx) - lColumnSizeHints.at(idx);
484  if (chop > 0) {
485  if (chop > missing) {
486  chop = missing;
487  }
488  lColumnWidths[idx] -= chop;
489  missing -= chop;
490  if (missing < 1) {
491  break; // no more space to recover
492  }
493  }
494  } // else it's invisible
495  idx--;
496  }
497  }
498  }
499  }
500 
501  // We're ready to assign widths.
502 
503  bool oldSave = d->mSaveThemeColumnStateOnSectionResize;
504  d->mSaveThemeColumnStateOnSectionResize = false;
505 
506  // A huge problem here is that QHeaderView goes quite nuts if we show or hide sections
507  // while resizing them. This is because it has several machineries aimed to delay
508  // the layout to the last possible moment. So if we show a column, it will tend to
509  // screw up the layout of other ones.
510 
511  // We first loop showing/hiding columns then.
512 
513  idx = 0;
514 
515  for (const auto col : std::as_const(columns)) {
516  bool visible = (idx == 0) || col->currentlyVisible();
517  // qCDebug(MESSAGELIST_LOG) << "Column " << idx << " visible " << visible;
518  col->setCurrentlyVisible(visible);
519  header()->setSectionHidden(idx, !visible);
520  idx++;
521  }
522 
523  // Then we loop assigning widths. This is still complicated since QHeaderView tries
524  // very badly to stretch the last section and thus will resize it in the meantime.
525  // But seems to work most of the times...
526 
527  idx = 0;
528 
529  for (const auto col : std::as_const(columns)) {
530  if (col->currentlyVisible()) {
531  const double columnWidth(lColumnWidths.at(idx));
532  col->setCurrentWidth(columnWidth);
533  // Laurent Bug 358855 - message list column widths lost when program closed
534  // I need to investigate if this code is still necessary (all method)
535  header()->resizeSection(idx, static_cast<int>(columnWidth));
536  } else {
537  col->setCurrentWidth(-1);
538  }
539  idx++;
540  }
541 
542  idx = 0;
543 
544  bool bTriggeredQtBug = false;
545  for (const auto col : std::as_const(columns)) {
546  if (!header()->isSectionHidden(idx)) {
547  if (!col->currentlyVisible()) {
548  bTriggeredQtBug = true;
549  }
550  }
551  idx++;
552  }
553 
554  setHeaderHidden(d->mTheme->viewHeaderPolicy() == Theme::NeverShowHeader);
555 
556  d->mSaveThemeColumnStateOnSectionResize = oldSave;
557  d->mNeedToApplyThemeColumns = false;
558 
559  static bool bAllowRecursion = true;
560 
561  if (bTriggeredQtBug && bAllowRecursion) {
562  bAllowRecursion = false;
563  // qCDebug(MESSAGELIST_LOG) << "I've triggered the QHeaderView bug: trying to fix by calling myself again";
564  applyThemeColumns();
565  bAllowRecursion = true;
566  }
567 }
568 
569 void View::triggerDelayedApplyThemeColumns()
570 {
571  if (d->mApplyThemeColumnsTimer->isActive()) {
572  d->mApplyThemeColumnsTimer->stop();
573  }
574  d->mApplyThemeColumnsTimer->setSingleShot(true);
575  d->mApplyThemeColumnsTimer->start(100);
576 }
577 
578 void View::saveThemeColumnState()
579 {
580  if (d->mSaveThemeColumnStateTimer->isActive()) {
581  d->mSaveThemeColumnStateTimer->stop();
582  }
583 
584  if (!d->mTheme) {
585  return;
586  }
587 
588  if (d->mNeedToApplyThemeColumns) {
589  return; // don't save the state if it hasn't been applied at all
590  }
591 
592  // qCDebug(MESSAGELIST_LOG) << "Save theme column state";
593 
594  const auto columns = d->mTheme->columns();
595 
596  if (columns.isEmpty()) {
597  return; // bad theme
598  }
599 
600  int idx = 0;
601 
602  for (const auto col : std::as_const(columns)) {
603  if (header()->isSectionHidden(idx)) {
604  // qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is hidden";
605  col->setCurrentlyVisible(false);
606  col->setCurrentWidth(-1); // reset (hmmm... we could use the "don't touch" policy here too...)
607  } else {
608  // qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is visible and has size " << header()->sectionSize( idx );
609  col->setCurrentlyVisible(true);
610  col->setCurrentWidth(header()->sectionSize(idx));
611  }
612  idx++;
613  }
614 }
615 
616 void View::triggerDelayedSaveThemeColumnState()
617 {
618  if (d->mSaveThemeColumnStateTimer->isActive()) {
619  d->mSaveThemeColumnStateTimer->stop();
620  }
621  d->mSaveThemeColumnStateTimer->setSingleShot(true);
622  d->mSaveThemeColumnStateTimer->start(200);
623 }
624 
625 void View::resizeEvent(QResizeEvent *e)
626 {
627  qCDebug(MESSAGELIST_LOG) << "Resize event enter (viewport width is " << viewport()->width() << ")";
628 
630 
631  if (!isVisible()) {
632  return; // don't play with
633  }
634 
635  if (d->mLastViewportWidth != viewport()->width()) {
636  triggerDelayedApplyThemeColumns();
637  }
638 
639  if (header()->isVisible()) {
640  return;
641  }
642 
643  // header invisible
644 
645  bool oldSave = d->mSaveThemeColumnStateOnSectionResize;
646  d->mSaveThemeColumnStateOnSectionResize = false;
647 
648  const int count = header()->count();
649  if ((count - header()->hiddenSectionCount()) < 2) {
650  // a single column visible: resize it
651  int visibleIndex;
652  for (visibleIndex = 0; visibleIndex < count; visibleIndex++) {
653  if (!header()->isSectionHidden(visibleIndex)) {
654  break;
655  }
656  }
657  if (visibleIndex < count) {
658  header()->resizeSection(visibleIndex, viewport()->width() - 4);
659  }
660  }
661 
662  d->mSaveThemeColumnStateOnSectionResize = oldSave;
663 
664  triggerDelayedSaveThemeColumnState();
665 }
666 
667 void View::paintEvent(QPaintEvent *event)
668 {
669 #if 0
670  if (/*mFirstResult &&*/ (!model() || model()->rowCount() == 0)) {
671  QPainter p(viewport());
672 
673  QFont font = p.font();
674  font.setItalic(true);
675  p.setFont(font);
676 
677  if (!d->mTextColor.isValid()) {
678  d->generalPaletteChanged();
679  }
680  p.setPen(d->mTextColor);
681 
682  p.drawText(QRect(0, 0, width(), height()), Qt::AlignCenter, i18n("No result found"));
683  } else {
684  QTreeView::paintEvent(event);
685  }
686 #else
687  QTreeView::paintEvent(event);
688 #endif
689 }
690 
691 void View::modelAboutToEmitLayoutChanged()
692 {
693  // QHeaderView goes totally NUTS with a layoutChanged() call
694  d->mSaveThemeColumnStateOnSectionResize = false;
695 }
696 
697 void View::modelEmittedLayoutChanged()
698 {
699  // This is after a first chunk of work has been done by the model: do apply column states
700  d->mSaveThemeColumnStateOnSectionResize = true;
701  applyThemeColumns();
702 }
703 
704 void View::slotHeaderSectionResized(int logicalIndex, int oldWidth, int newWidth)
705 {
706  Q_UNUSED(logicalIndex)
707  Q_UNUSED(oldWidth)
708  Q_UNUSED(newWidth)
709 
710  if (d->mSaveThemeColumnStateOnSectionResize) {
711  triggerDelayedSaveThemeColumnState();
712  }
713 }
714 
715 int View::sizeHintForColumn(int logicalColumnIndex) const
716 {
717  // QTreeView: please don't touch my column widths...
718  int w = header()->sectionSize(logicalColumnIndex);
719  if (w > 0) {
720  return w;
721  }
722  if (!d->mDelegate) {
723  return 32; // dummy
724  }
725  w = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, logicalColumnIndex).width();
726  return w;
727 }
728 
729 void View::slotHeaderContextMenuRequested(const QPoint &pnt)
730 {
731  if (!d->mTheme) {
732  return;
733  }
734 
735  const auto columns = d->mTheme->columns();
736 
737  if (columns.isEmpty()) {
738  return; // bad theme
739  }
740 
741  // the menu for the columns
742  QMenu menu;
743 
744  int idx = 0;
745  for (const auto col : std::as_const(columns)) {
746  QAction *act = menu.addAction(col->label());
747  act->setCheckable(true);
748  act->setChecked(!header()->isSectionHidden(idx));
749  if (idx == 0) {
750  act->setEnabled(false);
751  }
752  QObject::connect(act, &QAction::triggered, this, [this, idx] {
753  slotShowHideColumn(idx);
754  });
755 
756  idx++;
757  }
758 
759  menu.addSeparator();
760  {
761  QAction *act = menu.addAction(i18n("Adjust Column Sizes"));
762  QObject::connect(act, &QAction::triggered, this, &View::slotAdjustColumnSizes);
763  }
764  {
765  QAction *act = menu.addAction(i18n("Show Default Columns"));
766  QObject::connect(act, &QAction::triggered, this, &View::slotShowDefaultColumns);
767  }
768  menu.addSeparator();
769  {
770  QAction *act = menu.addAction(i18n("Display Tooltips"));
771  act->setCheckable(true);
772  act->setChecked(MessageListSettings::self()->messageToolTipEnabled());
773  QObject::connect(act, &QAction::triggered, this, &View::slotDisplayTooltips);
774  }
775  menu.addSeparator();
776 
777  MessageList::Util::fillViewMenu(&menu, d->mWidget);
778 
779  menu.exec(header()->mapToGlobal(pnt));
780 }
781 
782 void View::slotAdjustColumnSizes()
783 {
784  if (!d->mTheme) {
785  return;
786  }
787 
788  d->mTheme->resetColumnSizes();
789  applyThemeColumns();
790 }
791 
792 void View::slotShowDefaultColumns()
793 {
794  if (!d->mTheme) {
795  return;
796  }
797 
798  d->mTheme->resetColumnState();
799  applyThemeColumns();
800 }
801 
802 void View::slotDisplayTooltips(bool showTooltips)
803 {
804  MessageListSettings::self()->setMessageToolTipEnabled(showTooltips);
805 }
806 
807 void View::slotShowHideColumn(int columnIdx)
808 {
809  if (!d->mTheme) {
810  return; // oops
811  }
812 
813  if (columnIdx == 0) {
814  return; // can never be hidden
815  }
816 
817  if (columnIdx >= d->mTheme->columns().count()) {
818  return;
819  }
820 
821  const bool showIt = header()->isSectionHidden(columnIdx);
822 
823  Theme::Column *column = d->mTheme->columns().at(columnIdx);
824  Q_ASSERT(column);
825 
826  // first save column state (as it is, with the column still in previous state)
827  saveThemeColumnState();
828 
829  // If a section has just been shown, invalidate its width in the skin
830  // since QTreeView assigned it a (possibly insane) default width.
831  // If a section has been hidden, then invalidate its width anyway...
832  // so finally invalidate width always, here.
833  column->setCurrentlyVisible(showIt);
834  column->setCurrentWidth(-1);
835 
836  // then apply theme columns to re-compute proportional widths (so we hopefully stay in the view)
837  applyThemeColumns();
838 }
839 
840 Item *View::currentItem() const
841 {
842  QModelIndex idx = currentIndex();
843  if (!idx.isValid()) {
844  return nullptr;
845  }
846  Item *it = static_cast<Item *>(idx.internalPointer());
847  Q_ASSERT(it);
848  return it;
849 }
850 
851 MessageItem *View::currentMessageItem(bool selectIfNeeded) const
852 {
853  Item *it = currentItem();
854  if (!it || (it->type() != Item::Message)) {
855  return nullptr;
856  }
857 
858  if (selectIfNeeded) {
859  // Keep things coherent, if the user didn't select it, but acted on it via
860  // a shortcut, do select it now.
861  if (!selectionModel()->isSelected(currentIndex())) {
862  selectionModel()->select(currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows);
863  }
864  }
865 
866  return static_cast<MessageItem *>(it);
867 }
868 
869 void View::setCurrentMessageItem(MessageItem *it, bool center)
870 {
871  if (it) {
872  qCDebug(MESSAGELIST_LOG) << "Setting current message to" << it->subject();
873 
874  const QModelIndex index = d->mModel->index(it, 0);
875  selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows);
876  if (center) {
877  scrollTo(index, QAbstractItemView::PositionAtCenter);
878  }
879  } else {
880  selectionModel()->setCurrentIndex(QModelIndex(), QItemSelectionModel::Current | QItemSelectionModel::Clear);
881  }
882 }
883 
884 bool View::selectionEmpty() const
885 {
886  return selectionModel()->selectedRows().isEmpty();
887 }
888 
889 QList<MessageItem *> View::selectionAsMessageItemList(bool includeCollapsedChildren) const
890 {
891  QList<MessageItem *> selectedMessages;
892 
893  QModelIndexList lSelected = selectionModel()->selectedRows();
894  if (lSelected.isEmpty()) {
895  return selectedMessages;
896  }
897  for (const auto &idx : std::as_const(lSelected)) {
898  // The asserts below are theoretically valid but at the time
899  // of writing they fail because of a bug in QItemSelectionModel::selectedRows()
900  // which returns also non-selectable items.
901 
902  // Q_ASSERT( selectedItem->type() == Item::Message );
903  // Q_ASSERT( ( *it ).isValid() );
904 
905  if (!idx.isValid()) {
906  continue;
907  }
908 
909  Item *selectedItem = static_cast<Item *>(idx.internalPointer());
910  Q_ASSERT(selectedItem);
911 
912  if (selectedItem->type() != Item::Message) {
913  continue;
914  }
915 
916  if (!static_cast<MessageItem *>(selectedItem)->isValid()) {
917  continue;
918  }
919 
920  Q_ASSERT(!selectedMessages.contains(static_cast<MessageItem *>(selectedItem)));
921 
922  if (includeCollapsedChildren && (selectedItem->childItemCount() > 0) && (!isExpanded(idx))) {
923  static_cast<MessageItem *>(selectedItem)->subTreeToList(selectedMessages);
924  } else {
925  selectedMessages.append(static_cast<MessageItem *>(selectedItem));
926  }
927  }
928 
929  return selectedMessages;
930 }
931 
932 QList<MessageItem *> View::currentThreadAsMessageItemList() const
933 {
934  QList<MessageItem *> currentThread;
935 
936  MessageItem *msg = currentMessageItem();
937  if (!msg) {
938  return currentThread;
939  }
940 
941  while (msg->parent()) {
942  if (msg->parent()->type() != Item::Message) {
943  break;
944  }
945  msg = static_cast<MessageItem *>(msg->parent());
946  }
947 
948  msg->subTreeToList(currentThread);
949 
950  return currentThread;
951 }
952 
953 void View::setChildrenExpanded(const Item *root, bool expand)
954 {
955  Q_ASSERT(root);
956  auto childList = root->childItems();
957  if (!childList) {
958  return;
959  }
960  for (const auto child : std::as_const(*childList)) {
961  QModelIndex idx = d->mModel->index(child, 0);
962  Q_ASSERT(idx.isValid());
963  Q_ASSERT(static_cast<Item *>(idx.internalPointer()) == child);
964 
965  if (expand) {
966  setExpanded(idx, true);
967 
968  if (child->childItemCount() > 0) {
969  setChildrenExpanded(child, true);
970  }
971  } else {
972  if (child->childItemCount() > 0) {
973  setChildrenExpanded(child, false);
974  }
975 
976  setExpanded(idx, false);
977  }
978  }
979 }
980 
981 void View::ViewPrivate::generalPaletteChanged()
982 {
983  const QPalette palette = q->viewport()->palette();
984  QColor color = palette.text().color();
985  color.setAlpha(128);
986  mTextColor = color;
987 }
988 
989 void View::ViewPrivate::expandFullThread(const QModelIndex &index)
990 {
991  if (!index.isValid()) {
992  return;
993  }
994 
995  Item *item = static_cast<Item *>(index.internalPointer());
996  if (item->type() != Item::Message) {
997  return;
998  }
999 
1000  if (!static_cast<MessageItem *>(item)->parent() || (static_cast<MessageItem *>(item)->parent()->type() != Item::Message)) {
1001  q->setChildrenExpanded(item, true);
1002  }
1003 }
1004 
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 
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
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());
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)) {
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:
2130  case QEvent::LocaleChange:
2132  // All of these affect the theme's internal cache.
2133  setTheme(d->mTheme);
2134  // A layoutChanged() event will screw up the view state a bit.
2135  // Since this is a rare event we just reload the view.
2136  reload();
2137  break;
2138  default:
2139  // make gcc happy by default
2140  break;
2141  }
2142 
2144 }
2145 
2147 {
2148  if (e->type() == QEvent::TouchBegin) {
2149  d->mIsTouchEvent = true;
2150  d->mMousePressed = false;
2151  return false;
2152  }
2153 
2154  if (e->type() == QEvent::Gesture) {
2155  d->gestureEvent(static_cast<QGestureEvent *>(e));
2156  e->accept();
2157  return true;
2158  }
2159 
2160  // We catch ToolTip events and pass everything else
2161 
2162  if (e->type() != QEvent::ToolTip) {
2163  return QTreeView::event(e);
2164  }
2165 
2166  if (!MessageListSettings::self()->messageToolTipEnabled()) {
2167  return true; // don't display tooltips
2168  }
2169 
2170  auto he = dynamic_cast<QHelpEvent *>(e);
2171  if (!he) {
2172  return true; // eh ?
2173  }
2174 
2175  QPoint pnt = viewport()->mapFromGlobal(mapToGlobal(he->pos()));
2176 
2177  if (pnt.y() < 0) {
2178  return true; // don't display the tooltip for items hidden under the header
2179  }
2180 
2181  QModelIndex idx = indexAt(pnt);
2182  if (!idx.isValid()) {
2183  return true; // may be
2184  }
2185 
2186  Item *it = static_cast<Item *>(idx.internalPointer());
2187  if (!it) {
2188  return true; // hum
2189  }
2190 
2191  Q_ASSERT(storageModel());
2192 
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),
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()
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()
2656  it->status().isIgnored()
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()),
2840  onPressed(&fakeMousePress);
2841  }
2842 }
2843 
2844 #include "moc_view.cpp"
static const MessageStatus statusImportant()
void append(const T &value)
const QColor & color(QPalette::ColorGroup group, QPalette::ColorRole role) const const
T & first()
const QColor & color() const const
QPoint pos() const const
void showText(const QPoint &pos, const QString &text, QWidget *w)
AlignCenter
Item * itemBelow()
Returns the item that is visually below this item in the tree.
Definition: item.cpp:103
virtual void updateGeometries() override
void setScrollMetric(QScrollerProperties::ScrollMetric metric, const QVariant &value)
The MessageItem class.
Definition: messageitem.h:34
GestureStarted
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
QPoint topLeft() const const
QString toHtmlEscaped() const const
void setCurrentWidth(double currentWidth)
Sets the current shared width setting for this column.
Definition: theme.cpp:713
void * internalPointer() const const
The MessageList::View is the real display of the message list.
Definition: view.h:46
Qt::MouseButton button() const const
A structure used with MessageList::Item::childItemStats().
Definition: item.h:184
bool isViewable() const
Is this item attached to the viewable root ?
Definition: item.cpp:357
void customContextMenuRequested(const QPoint &pos)
int count(const T &value) const const
QString trimmed() const const
QString name() const const
MessageSorting messageSorting() const
Returns the current message sorting option.
Definition: sortorder.cpp:40
int red() const const
static const MessageStatus statusWatched()
QAction * addSeparator()
Type type() const
Returns the type of this item.
Definition: item.cpp:342
void setRead(bool read=true)
static const MessageStatus statusSpam()
CustomContextMenu
ScrollBarAsNeeded
Provides a widget which has the messagelist and the most important helper widgets,...
Definition: widgetbase.h:40
int y() const const
void setStatus(Akonadi::MessageStatus status)
Sets the status associated to this Item.
Definition: item.cpp:451
virtual bool event(QEvent *event) override
LeftButton
bool contains(const T &value) const const
Item * parent() const
Returns the parent Item in the tree, or 0 if this item isn't attached to the tree.
Definition: item.cpp:436
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void setItalic(bool enable)
QAction * addAction(const QString &text)
void reserve(int alloc)
bool isWatched() const
QList< Item * > * childItems() const
Return the list of child items.
Definition: item.cpp:59
int bottom() const const
virtual void changeEvent(QEvent *ev) override
int top() const const
virtual bool event(QEvent *e)
ItemIsSelectable
QString i18n(const char *text, const TYPE &arg...)
int indexOfChildItem(Item *item) const
Returns the actual index of the child Item item or -1 if item is not a child of this Item.
Definition: item.cpp:168
void setCurrentlyVisible(bool currentlyVisible)
Sets the current shared visibility state for this column.
Definition: theme.cpp:703
bool isIgnored() const
void timeout()
bool isEmpty() const const
Qt::MouseButtons buttons() const const
QScroller * scroller(QObject *target)
bool isValid() const const
Item * childItem(int idx) const
Returns the child item at position idx or 0 if idx is out of the allowable range.
Definition: item.cpp:64
const T & at(int i) const const
Q_SCRIPTABLE CaptureState status()
void setAlpha(int alpha)
Qt::MouseEventSource source() const const
virtual void mouseMoveEvent(QMouseEvent *event) override
static const MessageStatus statusIgnored()
UniqueConnection
int green() const const
bool isEmpty() const const
PreSelectionMode
Pre-selection is the action of automatically selecting a message just after the folder has finished l...
int childItemCount() const
Returns the number of children of this Item.
Definition: item.cpp:158
A class which holds information about sorting, e.g.
Definition: sortorder.h:34
virtual void mousePressEvent(QMouseEvent *event) override
Item * itemAboveChild(Item *child)
Returns the item that is visually above the specified child if this item.
Definition: item.cpp:129
QPoint bottomLeft() const const
bool isValid() const const
void subTreeToList(QList< MessageItem * > &list)
Appends the whole subtree originating at this item to the specified list.
void setCheckable(bool)
QString & replace(int position, int n, QChar after)
GestureType
T & last()
const QBrush & text() const const
int row() const const
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
bool currentlyVisible() const
Returns the current shared visibility state for this column.
Definition: theme.cpp:698
The Theme class defines the visual appearance of the MessageList.
Definition: theme.h:47
void triggered(bool checked)
SortDirection messageSortDirection() const
Returns the current message SortDirection.
Definition: sortorder.cpp:50
void setEnabled(bool)
bool containsTextItems() const
Returns true if this column contains text items.
Definition: theme.cpp:780
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QEvent::Type type() const const
int blue() const const
QPointF pos() const
virtual void resizeEvent(QResizeEvent *event) override
void setRowHidden(int row, const QModelIndex &parent, bool hide)
LeftToRight
ExistingSelectionBehaviour
This enum is used in the view message selection functions (for instance View::selectNextMessage())
The QAbstractItemModel based interface that you need to provide for your storage to work with Message...
QString i18nc(const char *context, const char *text, const TYPE &arg...)
const Akonadi::MessageStatus & status() const
Returns the status associated to this Item.
Definition: item.cpp:446
qint32 toQInt32() const
MessageTypeFilter
This enum is used in the view message selection functions (for instance View::nextMessageItem()).
static const MessageStatus statusHam()
This class manages the huge tree of displayable objects: GroupHeaderItems and MessageItems.
Definition: model.h:65
void setChecked(bool)
A single item of the MessageList tree managed by MessageList::Model.
Definition: item.h:47
Item * itemAbove()
Returns the item that is visually above this item in the tree.
Definition: item.cpp:144
MouseEventSource
NoModifier
Item * itemBelowChild(Item *child)
Returns the item that is visually below the specified child if this item.
Definition: item.cpp:83
virtual void paintEvent(QPaintEvent *event) override
A set of aggregation options that can be applied to the MessageList::Model in a single shot.
Definition: aggregation.h:28
bool isImportant() const
QObject * parent() const const
WA_AcceptTouchEvents
QGesture * gesture(Qt::GestureType type) const const
Qt::GestureType registerRecognizer(QGestureRecognizer *recognizer)
Item * deepestItem()
Returns the deepest item in the subtree originating at this item.
Definition: item.cpp:118
The Column class defines a view column available inside this theme.
Definition: theme.h:505
QAction * exec()
const QString & subject() const
Returns the subject associated to this Item.
Definition: item.cpp:531
void accept()
void sectionResized(int logicalIndex, int oldSize, int newSize)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Thu Feb 15 2024 03:55:21 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.