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

KDE's Doxygen guidelines are available online.