re-enable kbd navigation
[trojita.git] / src / Gui / MsgListView.cpp
blobf45aeaea7649281cfc0d3b05f3e6034ea6dede82
1 /* Copyright (C) 2006 - 2013 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 "Imap/Model/MsgListModel.h"
35 #include "Imap/Model/PrettyMsgListModel.h"
37 namespace Gui
40 MsgListView::MsgListView(QWidget *parent): QTreeView(parent)
42 connect(header(), SIGNAL(geometriesChanged()), this, SLOT(slotFixSize()));
43 connect(this, SIGNAL(expanded(QModelIndex)), this, SLOT(slotExpandWholeSubtree(QModelIndex)));
44 connect(header(), SIGNAL(sectionCountChanged(int,int)), this, SLOT(slotSectionCountChanged()));
45 header()->setContextMenuPolicy(Qt::ActionsContextMenu);
46 headerFieldsMapper = new QSignalMapper(this);
47 connect(headerFieldsMapper, SIGNAL(mapped(int)), this, SLOT(slotHeaderSectionVisibilityToggled(int)));
49 setUniformRowHeights(true);
50 setAllColumnsShowFocus(true);
51 setSelectionMode(ExtendedSelection);
52 setDragEnabled(true);
53 setRootIsDecorated(false);
54 // Some subthreads might be huuuuuuuuuuge, so prevent indenting them too heavily
55 setIndentation(15);
57 setSortingEnabled(true);
58 // By default, we don't do any sorting
59 header()->setSortIndicator(-1, Qt::AscendingOrder);
61 m_naviActivationTimer = new QTimer(this);
62 m_naviActivationTimer->setSingleShot(true);
63 connect (m_naviActivationTimer, SIGNAL(timeout()), SLOT(slotCurrentActivated()));
66 // up and down perform controlled selection, where PgUP, PgDown, Home and End jump to an
67 // usually unknown destination (-> no activation intended?!)
68 // left might collapse a thread, question is whether ending there (on closing the thread) should be
69 // taken as mail loading request (i don't think so, but it's sth. that needs to be figured over time)
70 // NOTICE: resonably Triggers should be a (non strict) subset of Blockers (user changed his mind)
72 // the list of key events which pot. lead to loading a new message.
73 static QList<int> gs_naviActivationTriggers = QList<int>() << Qt::Key_Up << Qt::Key_Down;
74 // the list of key events which cancel naviActivationTrigger induced action.
75 static QList<int> gs_naviActivationBlockers = QList<int>() << Qt::Key_Up << Qt::Key_Down << Qt::Key_Left;
78 void MsgListView::keyPressEvent(QKeyEvent *ke)
80 if (gs_naviActivationBlockers.contains(ke->key()))
81 m_naviActivationTimer->stop();
82 QTreeView::keyPressEvent(ke);
85 void MsgListView::keyReleaseEvent(QKeyEvent *ke)
87 if (ke->modifiers() == Qt::NoModifier && gs_naviActivationTriggers.contains(ke->key()))
88 m_naviActivationTimer->start(150); // few ms for the user to re-orientate. 150ms is not much
89 QTreeView::keyReleaseEvent(ke);
92 void MsgListView::slotCurrentActivated()
94 if (currentIndex().isValid())
95 emit activated(currentIndex());
98 int MsgListView::sizeHintForColumn(int column) const
100 QFont boldFont = font();
101 boldFont.setBold(true);
102 QFontMetrics metric(boldFont);
103 switch (column) {
104 case Imap::Mailbox::MsgListModel::SEEN:
105 return 16;
106 case Imap::Mailbox::MsgListModel::SUBJECT:
107 return metric.size(Qt::TextSingleLine, QLatin1String("Blesmrt Trojita Foo Bar Random Text")).width();
108 case Imap::Mailbox::MsgListModel::FROM:
109 case Imap::Mailbox::MsgListModel::TO:
110 case Imap::Mailbox::MsgListModel::CC:
111 case Imap::Mailbox::MsgListModel::BCC:
112 return metric.size(Qt::TextSingleLine, QLatin1String("Blesmrt Trojita")).width();
113 case Imap::Mailbox::MsgListModel::DATE:
114 case Imap::Mailbox::MsgListModel::RECEIVED_DATE:
115 return metric.size(Qt::TextSingleLine,
116 //: Translators: use a text which is returned for e-mails older than one day but newer than one week
117 //: (see Imap::Mailbox::PrettyMsgListModel::prettyFormatDate() for the string formats); the idea here
118 //: is to have a text which is "wide enough" in a typical UI font.
119 //: The English version uses "Mon" because of the M letter; you should use something similar.
120 tr("Mon 10")).width();
121 case Imap::Mailbox::MsgListModel::SIZE:
122 return metric.size(Qt::TextSingleLine, tr("88.8 kB")).width();
123 default:
124 return QTreeView::sizeHintForColumn(column);
128 /** @short Reimplemented to show custom pixmap during drag&drop
130 Qt's model-view classes don't provide any means of interfering with the
131 QDrag's pixmap so we just rip off QAbstractItemView::startDrag and provide
132 our own QPixmap.
134 void MsgListView::startDrag(Qt::DropActions supportedActions)
136 // indexes for column 0, i.e. subject
137 QModelIndexList baseIndexes;
139 Q_FOREACH(const QModelIndex &index, selectedIndexes()) {
140 if (!(model()->flags(index) & Qt::ItemIsDragEnabled))
141 continue;
142 if (index.column() == 0)
143 baseIndexes << index;
146 if (!baseIndexes.isEmpty()) {
147 QMimeData *data = model()->mimeData(baseIndexes);
148 if (!data)
149 return;
151 // use screen width and itemDelegate()->sizeHint() to determine size of the pixmap
152 int screenWidth = QApplication::desktop()->screenGeometry(this).width();
153 int maxWidth = qMax(400, screenWidth / 4);
154 QSize size(maxWidth, 0);
156 QStyleOptionViewItem opt;
157 opt.initFrom(this);
158 opt.rect.setWidth(maxWidth);
159 opt.rect.setHeight(itemDelegate()->sizeHint(opt, baseIndexes.at(0)).height());
160 size.setHeight(baseIndexes.size() * opt.rect.height());
161 // State_Selected provides for nice background of the items
162 opt.state |= QStyle::State_Selected;
164 // paint list of selected items using itemDelegate() to be consistent with style
165 QPixmap pixmap(size);
166 pixmap.fill(Qt::transparent);
167 QPainter p(&pixmap);
169 for (int i = 0; i < baseIndexes.size(); ++i) {
170 opt.rect.moveTop(i * opt.rect.height());
171 itemDelegate()->paint(&p, opt, baseIndexes.at(i));
174 QDrag *drag = new QDrag(this);
175 drag->setPixmap(pixmap);
176 drag->setMimeData(data);
177 drag->setHotSpot(QPoint(0, 0));
179 Qt::DropAction dropAction = Qt::IgnoreAction;
180 if (defaultDropAction() != Qt::IgnoreAction && (supportedActions & defaultDropAction()))
181 dropAction = defaultDropAction();
182 else if (supportedActions & Qt::CopyAction && dragDropMode() != QAbstractItemView::InternalMove)
183 dropAction = Qt::CopyAction;
184 if (drag->exec(supportedActions, dropAction) == Qt::MoveAction) {
185 // QAbstractItemView::startDrag calls d->clearOrRemove() here, so
186 // this is a copy of QAbstractItemModelPrivate::clearOrRemove();
187 const QItemSelection selection = selectionModel()->selection();
188 QList<QItemSelectionRange>::const_iterator it = selection.constBegin();
190 if (!dragDropOverwriteMode()) {
191 for (; it != selection.constEnd(); ++it) {
192 QModelIndex parent = it->parent();
193 if (it->left() != 0)
194 continue;
195 if (it->right() != (model()->columnCount(parent) - 1))
196 continue;
197 int count = it->bottom() - it->top() + 1;
198 model()->removeRows(it->top(), count, parent);
200 } else {
201 // we can't remove the rows so reset the items (i.e. the view is like a table)
202 QModelIndexList list = selection.indexes();
203 for (int i = 0; i < list.size(); ++i) {
204 QModelIndex index = list.at(i);
205 QMap<int, QVariant> roles = model()->itemData(index);
206 for (QMap<int, QVariant>::Iterator it = roles.begin(); it != roles.end(); ++it)
207 it.value() = QVariant();
208 model()->setItemData(index, roles);
215 void MsgListView::slotFixSize()
217 if (header()->visualIndex(Imap::Mailbox::MsgListModel::SEEN) == -1) {
218 // calling setResizeMode() would assert()
219 return;
221 header()->setStretchLastSection(false);
223 for (int i = 0; i < Imap::Mailbox::MsgListModel::COLUMN_COUNT; ++i) {
224 QHeaderView::ResizeMode resizeMode = QHeaderView::Interactive;
225 switch (i) {
226 case Imap::Mailbox::MsgListModel::SUBJECT:
227 resizeMode = QHeaderView::Stretch;
228 break;
229 case Imap::Mailbox::MsgListModel::SEEN:
230 resizeMode = QHeaderView::Fixed;
231 break;
233 setColumnWidth(i, sizeHintForColumn(i));
234 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
235 header()->setSectionResizeMode(i, resizeMode);
236 #else
237 header()->setResizeMode(i, resizeMode);
238 #endif
242 void MsgListView::slotExpandWholeSubtree(const QModelIndex &rootIndex)
244 if (rootIndex.parent().isValid())
245 return;
247 QVector<QModelIndex> queue(1, rootIndex);
248 for (int i = 0; i < queue.size(); ++i) {
249 const QModelIndex currentIndex = queue[i];
250 // Append all children to the queue...
251 for (int j = 0; j < currentIndex.model()->rowCount(currentIndex); ++j)
252 queue.append(currentIndex.child(j, 0));
253 // ...and expand the current index
254 expand(currentIndex);
258 void MsgListView::slotSectionCountChanged()
260 Q_ASSERT(header());
261 // At first, remove all actions
262 QList<QAction *> actions = header()->actions();
263 Q_FOREACH(QAction *action, actions) {
264 header()->removeAction(action);
265 headerFieldsMapper->removeMappings(action);
266 action->deleteLater();
268 actions.clear();
269 // Now add them again
270 for (int i = 0; i < header()->count(); ++i) {
271 QString message = header()->model() ? header()->model()->headerData(i, Qt::Horizontal).toString() : QString::number(i);
272 QAction *action = new QAction(message, this);
273 action->setCheckable(true);
274 action->setChecked(true);
275 connect(action, SIGNAL(toggled(bool)), headerFieldsMapper, SLOT(map()));
276 headerFieldsMapper->setMapping(action, i);
277 header()->addAction(action);
279 // Next, add some special handling of certain columns
280 switch (i) {
281 case Imap::Mailbox::MsgListModel::SEEN:
282 // This column doesn't have a textual description
283 action->setText(tr("Seen status"));
284 break;
285 case Imap::Mailbox::MsgListModel::TO:
286 case Imap::Mailbox::MsgListModel::CC:
287 case Imap::Mailbox::MsgListModel::BCC:
288 case Imap::Mailbox::MsgListModel::RECEIVED_DATE:
289 // And these should be hidden by default
290 action->toggle();
291 break;
292 default:
293 break;
297 // Make sure to kick the header again so that it shows reasonable sizing
298 slotFixSize();
301 void MsgListView::slotHeaderSectionVisibilityToggled(int section)
303 QList<QAction *> actions = header()->actions();
304 if (section >= actions.size() || section < 0)
305 return;
306 bool hide = ! actions[section]->isChecked();
308 if (hide && header()->hiddenSectionCount() == header()->count() - 1) {
309 // This would hide the very last section, which would hide the whole header view
310 actions[section]->setChecked(true);
311 } else {
312 header()->setSectionHidden(section, hide);
316 /** @short Overriden from QTreeView::setModel
318 The whole point is that we have to listen for sortingPreferenceChanged to update your header view when sorting is requested
319 but cannot be fulfilled.
321 void MsgListView::setModel(QAbstractItemModel *model)
323 if (this->model()) {
324 if (Imap::Mailbox::PrettyMsgListModel *prettyModel = findPrettyMsgListModel(this->model())) {
325 disconnect(prettyModel, SIGNAL(sortingPreferenceChanged(int,Qt::SortOrder)),
326 this, SLOT(slotHandleSortCriteriaChanged(int,Qt::SortOrder)));
329 QTreeView::setModel(model);
330 if (Imap::Mailbox::PrettyMsgListModel *prettyModel = findPrettyMsgListModel(model)) {
331 connect(prettyModel, SIGNAL(sortingPreferenceChanged(int,Qt::SortOrder)),
332 this, SLOT(slotHandleSortCriteriaChanged(int,Qt::SortOrder)));
336 void MsgListView::slotHandleSortCriteriaChanged(int column, Qt::SortOrder order)
338 // The if-clause is needed to prevent infinite recursion
339 if (header()->sortIndicatorSection() != column || header()->sortIndicatorOrder() != order) {
340 header()->setSortIndicator(column, order);
344 /** @short Walk the hierarchy of proxy models up until we stop at the PrettyMsgListModel or the first non-proxy model */
345 Imap::Mailbox::PrettyMsgListModel *MsgListView::findPrettyMsgListModel(QAbstractItemModel *model)
347 while (QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(model)) {
348 Imap::Mailbox::PrettyMsgListModel *prettyModel = qobject_cast<Imap::Mailbox::PrettyMsgListModel*>(proxy);
349 if (prettyModel)
350 return prettyModel;
351 else
352 model = proxy->sourceModel();
354 return 0;