Messagelib

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

KDE's Doxygen guidelines are available online.