Merge branch 'review/roland_pallai/favtags'
[trojita.git] / src / Gui / MsgListView.cpp
blob0a2849d3a95320c7f65f9cdeced506663defcf6e
1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include "MsgListView.h"
24 #include <QAction>
25 #include <QApplication>
26 #include <QDesktopWidget>
27 #include <QDrag>
28 #include <QFontMetrics>
29 #include <QHeaderView>
30 #include <QKeyEvent>
31 #include <QPainter>
32 #include <QSignalMapper>
33 #include <QTimer>
34 #include "ColoredItemDelegate.h"
35 #include "Imap/Model/MsgListModel.h"
36 #include "Imap/Model/PrettyMsgListModel.h"
38 namespace Gui
41 MsgListView::MsgListView(QWidget *parent): QTreeView(parent), m_autoActivateAfterKeyNavigation(true), m_autoResizeSections(true)
43 connect(header(), &QHeaderView::geometriesChanged, this, &MsgListView::slotFixSize);
44 connect(this, &QTreeView::expanded, this, &MsgListView::slotExpandWholeSubtree);
45 connect(header(), &QHeaderView::sectionCountChanged, this, &MsgListView::slotUpdateHeaderActions);
46 header()->setContextMenuPolicy(Qt::ActionsContextMenu);
47 headerFieldsMapper = new QSignalMapper(this);
48 connect(headerFieldsMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), this, &MsgListView::slotHeaderSectionVisibilityToggled);
50 setUniformRowHeights(true);
51 setAllColumnsShowFocus(true);
52 setSelectionMode(ExtendedSelection);
53 setDragEnabled(true);
54 setRootIsDecorated(false);
55 // Some subthreads might be huuuuuuuuuuge, so prevent indenting them too heavily
56 setIndentation(15);
58 setItemDelegate(new ColoredItemDelegate(this));
60 setSortingEnabled(true);
61 // By default, we don't do any sorting
62 header()->setSortIndicator(-1, Qt::AscendingOrder);
64 m_naviActivationTimer = new QTimer(this);
65 m_naviActivationTimer->setSingleShot(true);
66 connect(m_naviActivationTimer, &QTimer::timeout, this, &MsgListView::slotCurrentActivated);
69 // left might collapse a thread, question is whether ending there (on closing the thread) should be
70 // taken as mail loading request (i don't think so, but it's sth. that needs to be figured over time)
71 // NOTICE: reasonably Triggers should be a (non strict) subset of Blockers (user changed his mind)
73 // the list of key events which pot. lead to loading a new message.
74 static QList<int> gs_naviActivationTriggers = QList<int>() << Qt::Key_Up << Qt::Key_Down << Qt::Key_Right << Qt::Key_Left
75 << Qt::Key_PageUp << Qt::Key_PageDown
76 << Qt::Key_Home << Qt::Key_End;
77 // the list of key events which cancel naviActivationTrigger induced action.
78 static QList<int> gs_naviActivationBlockers = QList<int>() << Qt::Key_Up << Qt::Key_Down << Qt::Key_Left
79 << Qt::Key_PageUp << Qt::Key_PageDown
80 << Qt::Key_Home << Qt::Key_End;
83 void MsgListView::keyPressEvent(QKeyEvent *ke)
85 if (gs_naviActivationBlockers.contains(ke->key()))
86 m_naviActivationTimer->stop();
87 QTreeView::keyPressEvent(ke);
90 void MsgListView::keyReleaseEvent(QKeyEvent *ke)
92 if (ke->modifiers() == Qt::NoModifier && gs_naviActivationTriggers.contains(ke->key()))
93 m_naviActivationTimer->start(150); // few ms for the user to re-orientate. 150ms is not much
94 QTreeView::keyReleaseEvent(ke);
97 bool MsgListView::event(QEvent *event)
99 if (event->type() == QEvent::ShortcutOverride
100 && !gs_naviActivationBlockers.contains(static_cast<QKeyEvent*>(event)->key())
101 && m_naviActivationTimer->isActive()) {
102 // Make sure that the delayed timer is broken ASAP when the key looks like something which might possibly be a shortcut
103 m_naviActivationTimer->stop();
104 slotCurrentActivated();
106 return QTreeView::event(event);
109 void MsgListView::slotCurrentActivated()
111 if (currentIndex().isValid() && m_autoActivateAfterKeyNavigation) {
112 // The "current index" is the one with that funny dot which only triggers the read/unread status toggle.
113 // If we don't do anything, subsequent pressing of key_up or key_down will move the cursor up/down one row
114 // while preserving the column which will lead to toggling the read/unread state of *that* message.
115 // That's unexpected; the key shall just move the cursor and change the current message.
116 emit activated(currentIndex().sibling(currentIndex().row(), Imap::Mailbox::MsgListModel::SUBJECT));
120 int MsgListView::sizeHintForColumn(int column) const
122 QFont boldFont = font();
123 boldFont.setBold(true);
124 QFontMetrics metric(boldFont);
125 switch (column) {
126 case Imap::Mailbox::MsgListModel::SEEN:
127 return 0;
128 case Imap::Mailbox::MsgListModel::FLAGGED:
129 case Imap::Mailbox::MsgListModel::ATTACHMENT:
130 return style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, nullptr);
131 case Imap::Mailbox::MsgListModel::SUBJECT:
132 return metric.size(Qt::TextSingleLine, QStringLiteral("Blesmrt Trojita Foo Bar Random Text")).width();
133 case Imap::Mailbox::MsgListModel::FROM:
134 case Imap::Mailbox::MsgListModel::TO:
135 case Imap::Mailbox::MsgListModel::CC:
136 case Imap::Mailbox::MsgListModel::BCC:
137 return metric.size(Qt::TextSingleLine, QStringLiteral("Blesmrt Trojita")).width();
138 case Imap::Mailbox::MsgListModel::DATE:
139 case Imap::Mailbox::MsgListModel::RECEIVED_DATE:
140 return metric.size(Qt::TextSingleLine,
141 //: Translators: use a text which is returned for e-mails older than one day but newer than one week
142 //: (see UiUtils::Formatting::prettyDate() for the string formats); the idea here
143 //: is to have a text which is "wide enough" in a typical UI font.
144 //: The English version uses "Mon" because of the M letter; you should use something similar.
145 tr("Mon 10")).width();
146 case Imap::Mailbox::MsgListModel::SIZE:
147 return metric.size(Qt::TextSingleLine, tr("88.8 kB")).width();
148 default:
149 return QTreeView::sizeHintForColumn(column);
153 /** @short Reimplemented to show custom pixmap during drag&drop
155 Qt's model-view classes don't provide any means of interfering with the
156 QDrag's pixmap so we just rip off QAbstractItemView::startDrag and provide
157 our own QPixmap.
159 void MsgListView::startDrag(Qt::DropActions supportedActions)
161 // indexes for column 0, i.e. subject
162 QModelIndexList baseIndexes;
164 Q_FOREACH(const QModelIndex &index, selectedIndexes()) {
165 if (!(model()->flags(index) & Qt::ItemIsDragEnabled))
166 continue;
167 if (index.column() == Imap::Mailbox::MsgListModel::SUBJECT)
168 baseIndexes << index;
171 if (!baseIndexes.isEmpty()) {
172 QMimeData *data = model()->mimeData(baseIndexes);
173 if (!data)
174 return;
176 // use screen width and itemDelegate()->sizeHint() to determine size of the pixmap
177 int screenWidth = QApplication::desktop()->screenGeometry(this).width();
178 int maxWidth = qMax(400, screenWidth / 4);
179 QSize size(maxWidth, 0);
181 // Show a "+ X more items" text after so many entries
182 const int maxItems = 20;
184 QStyleOptionViewItem opt;
185 opt.initFrom(this);
186 opt.rect.setWidth(maxWidth);
187 opt.rect.setHeight(itemDelegate()->sizeHint(opt, baseIndexes.at(0)).height());
188 size.setHeight(qMin(maxItems + 1, baseIndexes.size()) * opt.rect.height());
189 // State_Selected provides for nice background of the items
190 opt.state |= QStyle::State_Selected;
192 // paint list of selected items using itemDelegate() to be consistent with style
193 QPixmap pixmap(size);
194 pixmap.fill(Qt::transparent);
195 QPainter p(&pixmap);
197 for (int i = 0; i < baseIndexes.size(); ++i) {
198 opt.rect.moveTop(i * opt.rect.height());
199 if (i == maxItems) {
200 p.fillRect(opt.rect, palette().color(QPalette::Disabled, QPalette::Highlight));
201 p.setBrush(palette().color(QPalette::Disabled, QPalette::HighlightedText));
202 p.drawText(opt.rect, Qt::AlignRight, tr("+ %n additional item(s)", 0, baseIndexes.size() - maxItems));
203 break;
205 itemDelegate()->paint(&p, opt, baseIndexes.at(i));
208 QDrag *drag = new QDrag(this);
209 drag->setPixmap(pixmap);
210 drag->setMimeData(data);
211 drag->setHotSpot(QPoint(0, 0));
213 Qt::DropAction dropAction = Qt::IgnoreAction;
214 if (defaultDropAction() != Qt::IgnoreAction && (supportedActions & defaultDropAction()))
215 dropAction = defaultDropAction();
216 else if (supportedActions & Qt::CopyAction && dragDropMode() != QAbstractItemView::InternalMove)
217 dropAction = Qt::CopyAction;
218 if (drag->exec(supportedActions, dropAction) == Qt::MoveAction) {
219 // QAbstractItemView::startDrag calls d->clearOrRemove() here, so
220 // this is a copy of QAbstractItemModelPrivate::clearOrRemove();
221 const QItemSelection selection = selectionModel()->selection();
222 QList<QItemSelectionRange>::const_iterator it = selection.constBegin();
224 if (!dragDropOverwriteMode()) {
225 for (; it != selection.constEnd(); ++it) {
226 QModelIndex parent = it->parent();
227 if (it->left() != 0)
228 continue;
229 if (it->right() != (model()->columnCount(parent) - 1))
230 continue;
231 int count = it->bottom() - it->top() + 1;
232 model()->removeRows(it->top(), count, parent);
234 } else {
235 // we can't remove the rows so reset the items (i.e. the view is like a table)
236 QModelIndexList list = selection.indexes();
237 for (int i = 0; i < list.size(); ++i) {
238 QModelIndex index = list.at(i);
239 QMap<int, QVariant> roles = model()->itemData(index);
240 for (QMap<int, QVariant>::Iterator it = roles.begin(); it != roles.end(); ++it)
241 it.value() = QVariant();
242 model()->setItemData(index, roles);
249 void MsgListView::slotFixSize()
251 if (!m_autoResizeSections)
252 return;
254 if (header()->visualIndex(Imap::Mailbox::MsgListModel::SUBJECT) == -1) {
255 // calling setResizeMode() would assert()
256 return;
259 header()->setStretchLastSection(false);
260 for (int i = 0; i < Imap::Mailbox::MsgListModel::COLUMN_COUNT; ++i) {
261 QHeaderView::ResizeMode resizeMode = resizeModeForColumn(i);
262 header()->setSectionResizeMode(i, resizeMode);
263 setColumnWidth(i, sizeHintForColumn(i));
267 QHeaderView::ResizeMode MsgListView::resizeModeForColumn(const int column) const
269 switch (column) {
270 case Imap::Mailbox::MsgListModel::SUBJECT:
271 return QHeaderView::Stretch;
272 case Imap::Mailbox::MsgListModel::SEEN:
273 case Imap::Mailbox::MsgListModel::FLAGGED:
274 case Imap::Mailbox::MsgListModel::ATTACHMENT:
275 return QHeaderView::Fixed;
276 default:
277 return QHeaderView::Interactive;
281 void MsgListView::slotExpandWholeSubtree(const QModelIndex &rootIndex)
283 if (rootIndex.parent().isValid())
284 return;
286 QVector<QModelIndex> queue(1, rootIndex);
287 for (int i = 0; i < queue.size(); ++i) {
288 const QModelIndex currentIndex = queue[i];
289 // Append all children to the queue...
290 for (int j = 0; j < currentIndex.model()->rowCount(currentIndex); ++j)
291 queue.append(currentIndex.child(j, 0));
292 // ...and expand the current index
293 expand(currentIndex);
297 void MsgListView::slotUpdateHeaderActions()
299 Q_ASSERT(header());
300 // At first, remove all actions
301 QList<QAction *> actions = header()->actions();
302 Q_FOREACH(QAction *action, actions) {
303 header()->removeAction(action);
304 headerFieldsMapper->removeMappings(action);
305 action->deleteLater();
307 actions.clear();
308 // Now add them again
309 for (int i = 0; i < header()->count(); ++i) {
310 QString message = header()->model() ? header()->model()->headerData(i, Qt::Horizontal).toString() : QString::number(i);
311 QAction *action = new QAction(message, this);
312 action->setCheckable(true);
313 action->setChecked(true);
314 connect(action, &QAction::toggled, headerFieldsMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
315 headerFieldsMapper->setMapping(action, i);
316 header()->addAction(action);
318 // Next, add some special handling of certain columns
319 switch (i) {
320 case Imap::Mailbox::MsgListModel::SEEN:
321 // This column doesn't have a textual description
322 action->setText(tr("Seen status"));
323 break;
324 case Imap::Mailbox::MsgListModel::FLAGGED:
325 action->setText(tr("Flagged status"));
326 break;
327 case Imap::Mailbox::MsgListModel::ATTACHMENT:
328 action->setText(tr("Attachment"));
329 break;
330 case Imap::Mailbox::MsgListModel::TO:
331 case Imap::Mailbox::MsgListModel::CC:
332 case Imap::Mailbox::MsgListModel::BCC:
333 case Imap::Mailbox::MsgListModel::RECEIVED_DATE:
334 // And these should be hidden by default
335 action->toggle();
336 break;
337 default:
338 break;
342 // Make sure to kick the header again so that it shows reasonable sizing
343 slotFixSize();
346 /** @short Handle columns added to MsgListModel and set their default properties
348 * When a new version of the underlying model got a new column, the old saved state of the GUI might only contain data for the old columns.
349 * Therefore it is important to explicitly restore the default for new columns, if any.
351 void MsgListView::slotHandleNewColumns(int oldCount, int newCount)
353 for (int i = oldCount; i < newCount; ++i) {
354 switch(i) {
355 case Imap::Mailbox::MsgListModel::FLAGGED:
356 header()->moveSection(i,0);
357 break;
358 case Imap::Mailbox::MsgListModel::ATTACHMENT:
359 header()->moveSection(i,0);
360 break;
363 setColumnWidth(i, sizeHintForColumn(i));
367 void MsgListView::slotHeaderSectionVisibilityToggled(int section)
369 QList<QAction *> actions = header()->actions();
370 if (section >= actions.size() || section < 0)
371 return;
372 bool hide = ! actions[section]->isChecked();
374 if (hide && header()->hiddenSectionCount() == header()->count() - 1) {
375 // This would hide the very last section, which would hide the whole header view
376 actions[section]->setChecked(true);
377 } else {
378 header()->setSectionHidden(section, hide);
382 void MsgListView::updateActionsAfterRestoredState()
384 m_autoResizeSections = false;
385 QList<QAction *> actions = header()->actions();
386 for (int i = 0; i < actions.size(); ++i) {
387 actions[i]->setChecked(!header()->isSectionHidden(i));
391 /** @short Overridden from QTreeView::setModel
393 The whole point is that we have to listen for sortingPreferenceChanged to update your header view when sorting is requested
394 but cannot be fulfilled.
396 void MsgListView::setModel(QAbstractItemModel *model)
398 if (this->model()) {
399 if (Imap::Mailbox::PrettyMsgListModel *prettyModel = findPrettyMsgListModel(this->model())) {
400 disconnect(prettyModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged,
401 this, &MsgListView::slotHandleSortCriteriaChanged);
404 QTreeView::setModel(model);
405 if (Imap::Mailbox::PrettyMsgListModel *prettyModel = findPrettyMsgListModel(model)) {
406 connect(prettyModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged,
407 this, &MsgListView::slotHandleSortCriteriaChanged);
411 void MsgListView::slotHandleSortCriteriaChanged(int column, Qt::SortOrder order)
413 // The if-clause is needed to prevent infinite recursion
414 if (header()->sortIndicatorSection() != column || header()->sortIndicatorOrder() != order) {
415 header()->setSortIndicator(column, order);
419 /** @short Walk the hierarchy of proxy models up until we stop at the PrettyMsgListModel or the first non-proxy model */
420 Imap::Mailbox::PrettyMsgListModel *MsgListView::findPrettyMsgListModel(QAbstractItemModel *model)
422 while (QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(model)) {
423 Imap::Mailbox::PrettyMsgListModel *prettyModel = qobject_cast<Imap::Mailbox::PrettyMsgListModel*>(proxy);
424 if (prettyModel)
425 return prettyModel;
426 else
427 model = proxy->sourceModel();
429 return 0;
432 void MsgListView::setAutoActivateAfterKeyNavigation(bool enabled)
434 m_autoActivateAfterKeyNavigation = enabled;