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"
25 #include <QApplication>
26 #include <QDesktopWidget>
28 #include <QFontMetrics>
29 #include <QHeaderView>
32 #include <QSignalMapper>
34 #include "ColoredItemDelegate.h"
35 #include "Imap/Model/MsgListModel.h"
36 #include "Imap/Model/PrettyMsgListModel.h"
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
);
54 setRootIsDecorated(false);
55 // Some subthreads might be huuuuuuuuuuge, so prevent indenting them too heavily
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
);
126 case Imap::Mailbox::MsgListModel::SEEN
:
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();
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
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
))
167 if (index
.column() == Imap::Mailbox::MsgListModel::SUBJECT
)
168 baseIndexes
<< index
;
171 if (!baseIndexes
.isEmpty()) {
172 QMimeData
*data
= model()->mimeData(baseIndexes
);
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
;
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
);
197 for (int i
= 0; i
< baseIndexes
.size(); ++i
) {
198 opt
.rect
.moveTop(i
* opt
.rect
.height());
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
));
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();
229 if (it
->right() != (model()->columnCount(parent
) - 1))
231 int count
= it
->bottom() - it
->top() + 1;
232 model()->removeRows(it
->top(), count
, parent
);
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
)
254 if (header()->visualIndex(Imap::Mailbox::MsgListModel::SUBJECT
) == -1) {
255 // calling setResizeMode() would assert()
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
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
;
277 return QHeaderView::Interactive
;
281 void MsgListView::slotExpandWholeSubtree(const QModelIndex
&rootIndex
)
283 if (rootIndex
.parent().isValid())
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 if (currentIndex
.model()->hasChildren(currentIndex
))
294 expand(currentIndex
);
298 void MsgListView::slotUpdateHeaderActions()
301 // At first, remove all actions
302 QList
<QAction
*> actions
= header()->actions();
303 Q_FOREACH(QAction
*action
, actions
) {
304 header()->removeAction(action
);
305 headerFieldsMapper
->removeMappings(action
);
306 action
->deleteLater();
309 // Now add them again
310 for (int i
= 0; i
< header()->count(); ++i
) {
311 QString message
= header()->model() ? header()->model()->headerData(i
, Qt::Horizontal
).toString() : QString::number(i
);
312 QAction
*action
= new QAction(message
, this);
313 action
->setCheckable(true);
314 action
->setChecked(true);
315 connect(action
, &QAction::toggled
, headerFieldsMapper
, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map
));
316 headerFieldsMapper
->setMapping(action
, i
);
317 header()->addAction(action
);
319 // Next, add some special handling of certain columns
321 case Imap::Mailbox::MsgListModel::SEEN
:
322 // This column doesn't have a textual description
323 action
->setText(tr("Seen status"));
325 case Imap::Mailbox::MsgListModel::FLAGGED
:
326 action
->setText(tr("Flagged status"));
328 case Imap::Mailbox::MsgListModel::ATTACHMENT
:
329 action
->setText(tr("Attachment"));
331 case Imap::Mailbox::MsgListModel::TO
:
332 case Imap::Mailbox::MsgListModel::CC
:
333 case Imap::Mailbox::MsgListModel::BCC
:
334 case Imap::Mailbox::MsgListModel::RECEIVED_DATE
:
335 // And these should be hidden by default
343 // Make sure to kick the header again so that it shows reasonable sizing
347 /** @short Handle columns added to MsgListModel and set their default properties
349 * 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.
350 * Therefore it is important to explicitly restore the default for new columns, if any.
352 void MsgListView::slotHandleNewColumns(int oldCount
, int newCount
)
354 for (int i
= oldCount
; i
< newCount
; ++i
) {
356 case Imap::Mailbox::MsgListModel::FLAGGED
:
357 header()->moveSection(i
,0);
359 case Imap::Mailbox::MsgListModel::ATTACHMENT
:
360 header()->moveSection(i
,0);
364 setColumnWidth(i
, sizeHintForColumn(i
));
368 void MsgListView::slotHeaderSectionVisibilityToggled(int section
)
370 QList
<QAction
*> actions
= header()->actions();
371 if (section
>= actions
.size() || section
< 0)
373 bool hide
= ! actions
[section
]->isChecked();
375 if (hide
&& header()->hiddenSectionCount() == header()->count() - 1) {
376 // This would hide the very last section, which would hide the whole header view
377 actions
[section
]->setChecked(true);
379 header()->setSectionHidden(section
, hide
);
383 void MsgListView::updateActionsAfterRestoredState()
385 m_autoResizeSections
= false;
386 QList
<QAction
*> actions
= header()->actions();
387 for (int i
= 0; i
< actions
.size(); ++i
) {
388 actions
[i
]->setChecked(!header()->isSectionHidden(i
));
392 /** @short Overridden from QTreeView::setModel
394 The whole point is that we have to listen for sortingPreferenceChanged to update your header view when sorting is requested
395 but cannot be fulfilled.
397 void MsgListView::setModel(QAbstractItemModel
*model
)
400 if (Imap::Mailbox::PrettyMsgListModel
*prettyModel
= findPrettyMsgListModel(this->model())) {
401 disconnect(prettyModel
, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged
,
402 this, &MsgListView::slotHandleSortCriteriaChanged
);
405 QTreeView::setModel(model
);
406 if (Imap::Mailbox::PrettyMsgListModel
*prettyModel
= findPrettyMsgListModel(model
)) {
407 connect(prettyModel
, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged
,
408 this, &MsgListView::slotHandleSortCriteriaChanged
);
412 void MsgListView::slotHandleSortCriteriaChanged(int column
, Qt::SortOrder order
)
414 // The if-clause is needed to prevent infinite recursion
415 if (header()->sortIndicatorSection() != column
|| header()->sortIndicatorOrder() != order
) {
416 header()->setSortIndicator(column
, order
);
420 /** @short Walk the hierarchy of proxy models up until we stop at the PrettyMsgListModel or the first non-proxy model */
421 Imap::Mailbox::PrettyMsgListModel
*MsgListView::findPrettyMsgListModel(QAbstractItemModel
*model
)
423 while (QAbstractProxyModel
*proxy
= qobject_cast
<QAbstractProxyModel
*>(model
)) {
424 Imap::Mailbox::PrettyMsgListModel
*prettyModel
= qobject_cast
<Imap::Mailbox::PrettyMsgListModel
*>(proxy
);
428 model
= proxy
->sourceModel();
433 void MsgListView::setAutoActivateAfterKeyNavigation(bool enabled
)
435 m_autoActivateAfterKeyNavigation
= enabled
;