Merge remote-tracking branch 'origin/Applications/16.08'
[kdepim.git] / akregator / src / articlelistview.cpp
blob53d88959fc72f2003c095f21271b70ec2262edae
1 /*
2 This file is part of Akregator.
4 Copyright (C) 2004 Stanislav Karchebny <Stanislav.Karchebny@kdemail.net>
5 2005-2008 Frank Osterfeld <osterfeld@kde.org>
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 As a special exception, permission is given to link this program
21 with any edition of Qt, and distribute the resulting executable,
22 without including the source code for Qt in the source distribution.
25 #include "articlelistview.h"
26 #include "actionmanager.h"
27 #include "akregatorconfig.h"
28 #include "article.h"
29 #include "articlemodel.h"
30 #include "kernel.h"
31 #include "types.h"
33 #include <utils/filtercolumnsproxymodel.h>
35 #include <QDateTime>
36 #include <QIcon>
37 #include <KLocalizedString>
38 #include <QUrl>
39 #include <QMenu>
40 #include <KColorScheme>
41 #include <QLocale>
43 #include <QApplication>
44 #include <QContextMenuEvent>
45 #include <QHeaderView>
46 #include <QPaintEvent>
47 #include <QPalette>
48 #include <QScrollBar>
50 #include <cassert>
52 using namespace Akregator;
54 FilterDeletedProxyModel::FilterDeletedProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
56 setDynamicSortFilter(true);
59 bool FilterDeletedProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
61 return !sourceModel()->index(source_row, 0, source_parent).data(ArticleModel::IsDeletedRole).toBool();
64 SortColorizeProxyModel::SortColorizeProxyModel(QObject *parent) : QSortFilterProxyModel(parent), m_keepFlagIcon(QIcon::fromTheme(QStringLiteral("mail-mark-important")))
66 m_unreadColor = KColorScheme(QPalette::Normal, KColorScheme::View).foreground(KColorScheme::PositiveText).color();
67 m_newColor = KColorScheme(QPalette::Normal, KColorScheme::View).foreground(KColorScheme::NegativeText).color();
70 bool SortColorizeProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
72 if (source_parent.isValid()) {
73 return false;
76 for (uint i = 0; i < m_matchers.size(); ++i) {
77 if (!static_cast<ArticleModel *>(sourceModel())->rowMatches(source_row, m_matchers[i])) {
78 return false;
82 return true;
85 void SortColorizeProxyModel::setFilters(const std::vector<QSharedPointer<const Filters::AbstractMatcher> > &matchers)
87 if (m_matchers == matchers) {
88 return;
90 m_matchers = matchers;
91 invalidateFilter();
94 QVariant SortColorizeProxyModel::data(const QModelIndex &idx, int role) const
96 if (!idx.isValid() || !sourceModel()) {
97 return QVariant();
100 const QModelIndex sourceIdx = mapToSource(idx);
102 switch (role) {
103 case Qt::ForegroundRole: {
104 switch (static_cast<ArticleStatus>(sourceIdx.data(ArticleModel::StatusRole).toInt())) {
105 case Unread: {
106 return Settings::useCustomColors() ?
107 Settings::colorUnreadArticles() : m_unreadColor;
109 case New: {
110 return Settings::useCustomColors() ?
111 Settings::colorNewArticles() : m_newColor;
113 case Read: {
114 return QApplication::palette().color(QPalette::Text);
118 break;
119 case Qt::DecorationRole: {
120 if (sourceIdx.column() == ArticleModel::ItemTitleColumn) {
121 return sourceIdx.data(ArticleModel::IsImportantRole).toBool() ? m_keepFlagIcon : QVariant();
124 break;
126 return sourceIdx.data(role);
129 namespace
132 static bool isRead(const QModelIndex &idx)
134 if (!idx.isValid()) {
135 return false;
138 return static_cast<ArticleStatus>(idx.data(ArticleModel::StatusRole).toInt()) == Read;
142 void ArticleListView::setArticleModel(ArticleModel *model)
144 if (!model) {
145 setModel(model);
146 return;
149 m_proxy = new SortColorizeProxyModel(model);
150 m_proxy->setSourceModel(model);
151 m_proxy->setSortRole(ArticleModel::SortRole);
152 m_proxy->setFilters(m_matchers);
153 FilterDeletedProxyModel *const proxy2 = new FilterDeletedProxyModel(model);
154 proxy2->setSortRole(ArticleModel::SortRole);
155 proxy2->setSourceModel(m_proxy);
157 connect(model, &QAbstractItemModel::rowsInserted,
158 m_proxy.data(), &QSortFilterProxyModel::invalidate);
160 FilterColumnsProxyModel *const columnsProxy = new FilterColumnsProxyModel(model);
161 columnsProxy->setSortRole(ArticleModel::SortRole);
162 columnsProxy->setSourceModel(proxy2);
163 columnsProxy->setColumnEnabled(ArticleModel::ItemTitleColumn);
164 columnsProxy->setColumnEnabled(ArticleModel::FeedTitleColumn);
165 columnsProxy->setColumnEnabled(ArticleModel::DateColumn);
166 columnsProxy->setColumnEnabled(ArticleModel::AuthorColumn);
168 setModel(columnsProxy);
169 header()->setContextMenuPolicy(Qt::CustomContextMenu);
170 header()->setResizeMode(QHeaderView::Interactive);
173 void ArticleListView::showHeaderMenu(const QPoint &pos)
175 if (!model()) {
176 return;
179 QPointer<QMenu> menu = new QMenu(this);
180 menu->setTitle(i18n("Columns"));
181 menu->setAttribute(Qt::WA_DeleteOnClose);
183 const int colCount = model()->columnCount();
184 int visibleColumns = 0; // number of column currently shown
185 QAction *visibleColumnsAction = 0;
186 for (int i = 0; i < colCount; ++i) {
187 QAction *act = menu->addAction(model()->headerData(i, Qt::Horizontal).toString());
188 act->setCheckable(true);
189 act->setData(i);
190 bool sectionVisible = !header()->isSectionHidden(i);
191 act->setChecked(sectionVisible);
192 if (sectionVisible) {
193 ++visibleColumns;
194 visibleColumnsAction = act;
198 // Avoid that the last shown column is also hidden
199 if (visibleColumns == 1) {
200 visibleColumnsAction->setEnabled(false);
203 QPointer<QObject> that(this);
204 QAction *const action = menu->exec(header()->mapToGlobal(pos));
205 if (that && action) {
206 const int col = action->data().toInt();
207 if (action->isChecked()) {
208 header()->showSection(col);
209 } else {
210 header()->hideSection(col);
213 delete menu;
216 void ArticleListView::saveHeaderSettings()
218 if (model()) {
219 const QByteArray state = header()->saveState();
220 if (m_columnMode == FeedMode) {
221 m_feedHeaderState = state;
222 } else {
223 m_groupHeaderState = state;
227 KConfigGroup conf(Settings::self()->config(), "General");
228 conf.writeEntry("ArticleListFeedHeaders", m_feedHeaderState.toBase64());
229 conf.writeEntry("ArticleListGroupHeaders", m_groupHeaderState.toBase64());
232 void ArticleListView::loadHeaderSettings()
234 KConfigGroup conf(Settings::self()->config(), "General");
235 m_feedHeaderState = QByteArray::fromBase64(conf.readEntry("ArticleListFeedHeaders").toLatin1());
236 m_groupHeaderState = QByteArray::fromBase64(conf.readEntry("ArticleListGroupHeaders").toLatin1());
239 QItemSelectionModel *ArticleListView::articleSelectionModel() const
241 return selectionModel();
244 const QAbstractItemView *ArticleListView::itemView() const
246 return this;
249 QAbstractItemView *ArticleListView::itemView()
251 return this;
254 QPoint ArticleListView::scrollBarPositions() const
256 return QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());
259 void ArticleListView::setScrollBarPositions(const QPoint &p)
261 horizontalScrollBar()->setValue(p.x());
262 verticalScrollBar()->setValue(p.y());
265 void ArticleListView::setGroupMode()
267 if (m_columnMode == GroupMode) {
268 return;
271 if (model()) {
272 m_feedHeaderState = header()->saveState();
274 m_columnMode = GroupMode;
275 restoreHeaderState();
278 void ArticleListView::setFeedMode()
280 if (m_columnMode == FeedMode) {
281 return;
284 if (model()) {
285 m_groupHeaderState = header()->saveState();
287 m_columnMode = FeedMode;
288 restoreHeaderState();
291 static int maxDateColumnWidth(const QFontMetrics &fm)
293 int width = 0;
294 QDateTime date(QDate::currentDate(), QTime(23, 59));
295 for (int x = 0; x < 10; ++x, date = date.addDays(-1)) {
296 QString txt = QLatin1Char(' ') + QLocale().toString(date, QLocale::ShortFormat) + QLatin1Char(' ');
297 width = qMax(width, fm.width(txt));
299 return width;
302 void ArticleListView::restoreHeaderState()
304 QByteArray state = m_columnMode == GroupMode ? m_groupHeaderState : m_feedHeaderState;
305 header()->restoreState(state);
306 if (state.isEmpty()) {
307 // No state, set a default config:
308 // - hide the feed column in feed mode (no need to see the same feed title over and over)
309 // - set the date column wide enough to fit all possible dates
310 header()->setSectionHidden(ArticleModel::FeedTitleColumn, m_columnMode == FeedMode);
311 header()->setStretchLastSection(false);
312 header()->resizeSection(ArticleModel::DateColumn, maxDateColumnWidth(fontMetrics()));
313 if (model()) {
314 startResizingTitleColumn();
318 if (header()->sectionSize(ArticleModel::DateColumn) == 1) {
319 header()->resizeSection(ArticleModel::DateColumn, maxDateColumnWidth(fontMetrics()));
323 void ArticleListView::startResizingTitleColumn()
325 // set the title column to Stretch resize mode so that it adapts to the
326 // content. finishResizingTitleColumn() will turn the resize mode back to
327 // Interactive so that the user can still resize the column himself if he
328 // wants to
329 header()->setResizeMode(ArticleModel::ItemTitleColumn, QHeaderView::Stretch);
330 QMetaObject::invokeMethod(this, "finishResizingTitleColumn", Qt::QueuedConnection);
333 void ArticleListView::finishResizingTitleColumn()
335 if (QApplication::mouseButtons() != Qt::NoButton) {
336 // Come back later: user is still resizing the widget
337 QMetaObject::invokeMethod(this, "finishResizingTitleColumn", Qt::QueuedConnection);
338 return;
340 header()->setResizeMode(QHeaderView::Interactive);
343 ArticleListView::~ArticleListView()
345 saveHeaderSettings();
348 void ArticleListView::setIsAggregation(bool aggregation)
350 if (aggregation) {
351 setGroupMode();
352 } else {
353 setFeedMode();
357 ArticleListView::ArticleListView(QWidget *parent)
358 : QTreeView(parent),
359 m_columnMode(FeedMode)
361 setSortingEnabled(true);
362 setAlternatingRowColors(true);
363 setSelectionMode(QAbstractItemView::ExtendedSelection);
364 setUniformRowHeights(true);
365 setRootIsDecorated(false);
366 setAllColumnsShowFocus(true);
367 setDragDropMode(QAbstractItemView::DragOnly);
369 setMinimumSize(250, 150);
370 setWhatsThis(i18n("<h2>Article list</h2>"
371 "Here you can browse articles from the currently selected feed. "
372 "You can also manage articles, as marking them as persistent (\"Mark as Important\") or delete them, using the right mouse button menu. "
373 "To view the web page of the article, you can open the article internally in a tab or in an external browser window."));
375 //connect exactly once
376 disconnect(header(), &QWidget::customContextMenuRequested, this, &ArticleListView::showHeaderMenu);
377 connect(header(), &QWidget::customContextMenuRequested, this, &ArticleListView::showHeaderMenu);
378 loadHeaderSettings();
381 void ArticleListView::mousePressEvent(QMouseEvent *ev)
383 // let's push the event, so we can use currentIndex() to get the newly selected article..
384 QTreeView::mousePressEvent(ev);
386 if (ev->button() == Qt::MidButton) {
387 const QUrl url = currentIndex().data(ArticleModel::LinkRole).toUrl();
389 Q_EMIT signalMouseButtonPressed(ev->button(), url);
393 void ArticleListView::contextMenuEvent(QContextMenuEvent *event)
395 QWidget *w = ActionManager::getInstance()->container(QStringLiteral("article_popup"));
396 QMenu *popup = qobject_cast<QMenu *>(w);
397 if (popup) {
398 popup->exec(event->globalPos());
402 void ArticleListView::setModel(QAbstractItemModel *m)
404 const bool groupMode = m_columnMode == GroupMode;
406 QAbstractItemModel *const oldModel = model();
407 if (oldModel) {
408 const QByteArray state = header()->saveState();
409 if (groupMode) {
410 m_groupHeaderState = state;
411 } else {
412 m_feedHeaderState = state;
416 QTreeView::setModel(m);
418 if (m) {
419 sortByColumn(ArticleModel::DateColumn, Qt::DescendingOrder);
420 restoreHeaderState();
422 // Ensure at least one column is visible
423 if (header()->hiddenSectionCount() == header()->count()) {
424 header()->showSection(ArticleModel::ItemTitleColumn);
429 void ArticleListView::slotClear()
431 setModel(Q_NULLPTR);
434 void ArticleListView::slotPreviousArticle()
436 if (!model()) {
437 return;
439 Q_EMIT userActionTakingPlace();
440 const QModelIndex idx = currentIndex();
441 const int newRow = qMax(0, (idx.isValid() ? idx.row() : model()->rowCount()) - 1);
442 const QModelIndex newIdx = idx.isValid() ? idx.sibling(newRow, 0) : model()->index(newRow, 0);
443 selectIndex(newIdx);
446 void ArticleListView::slotNextArticle()
448 if (!model()) {
449 return;
452 Q_EMIT userActionTakingPlace();
453 const QModelIndex idx = currentIndex();
454 const int newRow = idx.isValid() ? (idx.row() + 1) : 0;
455 const QModelIndex newIdx = model()->index(qMin(newRow, model()->rowCount() - 1), 0);
456 selectIndex(newIdx);
459 void ArticleListView::slotNextUnreadArticle()
461 if (!model()) {
462 return;
465 const int rowCount = model()->rowCount();
466 const int startRow = qMin(rowCount - 1, (currentIndex().isValid() ? currentIndex().row() + 1 : 0));
468 int i = startRow;
469 bool foundUnread = false;
471 do {
472 if (!::isRead(model()->index(i, 0))) {
473 foundUnread = true;
474 } else {
475 i = (i + 1) % rowCount;
477 } while (!foundUnread && i != startRow);
479 if (foundUnread) {
480 selectIndex(model()->index(i, 0));
484 void ArticleListView::selectIndex(const QModelIndex &idx)
486 if (!idx.isValid()) {
487 return;
489 setCurrentIndex(idx);
490 clearSelection();
491 Q_ASSERT(selectionModel());
492 selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows);
493 scrollTo(idx, PositionAtCenter);
496 void ArticleListView::slotPreviousUnreadArticle()
498 if (!model()) {
499 return;
502 const int rowCount = model()->rowCount();
503 const int startRow = qMax(0, (currentIndex().isValid() ? currentIndex().row() : rowCount) - 1);
505 int i = startRow;
506 bool foundUnread = false;
508 do {
509 if (!::isRead(model()->index(i, 0))) {
510 foundUnread = true;
511 } else {
512 i = i > 0 ? i - 1 : rowCount - 1;
514 } while (!foundUnread && i != startRow);
516 if (foundUnread) {
517 selectIndex(model()->index(i, 0));
521 void ArticleListView::forceFilterUpdate()
523 if (m_proxy) {
524 m_proxy->invalidate();
528 void ArticleListView::setFilters(const std::vector<QSharedPointer<const Filters::AbstractMatcher> > &matchers)
530 if (m_matchers == matchers) {
531 return;
533 m_matchers = matchers;
534 if (m_proxy) {
535 m_proxy->setFilters(matchers);