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"
25 #include <QApplication>
26 #include <QDesktopWidget>
28 #include <QFontMetrics>
29 #include <QHeaderView>
32 #include <QSignalMapper>
34 #include "Imap/Model/MsgListModel.h"
35 #include "Imap/Model/PrettyMsgListModel.h"
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
);
53 setRootIsDecorated(false);
54 // Some subthreads might be huuuuuuuuuuge, so prevent indenting them too heavily
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
);
104 case Imap::Mailbox::MsgListModel::SEEN
:
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();
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
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
))
142 if (index
.column() == 0)
143 baseIndexes
<< index
;
146 if (!baseIndexes
.isEmpty()) {
147 QMimeData
*data
= model()->mimeData(baseIndexes
);
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
;
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
);
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();
195 if (it
->right() != (model()->columnCount(parent
) - 1))
197 int count
= it
->bottom() - it
->top() + 1;
198 model()->removeRows(it
->top(), count
, parent
);
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()
221 header()->setStretchLastSection(false);
223 for (int i
= 0; i
< Imap::Mailbox::MsgListModel::COLUMN_COUNT
; ++i
) {
224 QHeaderView::ResizeMode resizeMode
= QHeaderView::Interactive
;
226 case Imap::Mailbox::MsgListModel::SUBJECT
:
227 resizeMode
= QHeaderView::Stretch
;
229 case Imap::Mailbox::MsgListModel::SEEN
:
230 resizeMode
= QHeaderView::Fixed
;
233 setColumnWidth(i
, sizeHintForColumn(i
));
234 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
235 header()->setSectionResizeMode(i
, resizeMode
);
237 header()->setResizeMode(i
, resizeMode
);
242 void MsgListView::slotExpandWholeSubtree(const QModelIndex
&rootIndex
)
244 if (rootIndex
.parent().isValid())
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()
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();
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
281 case Imap::Mailbox::MsgListModel::SEEN
:
282 // This column doesn't have a textual description
283 action
->setText(tr("Seen status"));
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
297 // Make sure to kick the header again so that it shows reasonable sizing
301 void MsgListView::slotHeaderSectionVisibilityToggled(int section
)
303 QList
<QAction
*> actions
= header()->actions();
304 if (section
>= actions
.size() || section
< 0)
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);
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
)
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
);
352 model
= proxy
->sourceModel();