File indexing completed on 2024-04-21 05:10:34
0001 /* 0002 This file is part of Akregator. 0003 0004 SPDX-FileCopyrightText: 2004 Stanislav Karchebny <Stanislav.Karchebny@kdemail.net> 0005 SPDX-FileCopyrightText: 2005-2008 Frank Osterfeld <osterfeld@kde.org> 0006 0007 SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0 0008 */ 0009 0010 #include "articlelistview.h" 0011 #include "actionmanager.h" 0012 #include "akregatorconfig.h" 0013 #include "articlemodel.h" 0014 #include "types.h" 0015 0016 #include "utils/filtercolumnsproxymodel.h" 0017 0018 #include <KColorScheme> 0019 #include <KLocalizedString> 0020 #include <QDateTime> 0021 #include <QIcon> 0022 #include <QLocale> 0023 #include <QMenu> 0024 0025 #include <QApplication> 0026 #include <QContextMenuEvent> 0027 #include <QHeaderView> 0028 #include <QPainter> 0029 #include <QPalette> 0030 #include <QScrollBar> 0031 0032 #include <cassert> 0033 0034 using namespace Akregator; 0035 0036 FilterDeletedProxyModel::FilterDeletedProxyModel(QObject *parent) 0037 : QSortFilterProxyModel(parent) 0038 { 0039 setDynamicSortFilter(true); 0040 } 0041 0042 bool FilterDeletedProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const 0043 { 0044 return !sourceModel()->index(source_row, 0, source_parent).data(ArticleModel::IsDeletedRole).toBool(); 0045 } 0046 0047 SortColorizeProxyModel::SortColorizeProxyModel(QObject *parent) 0048 : QSortFilterProxyModel(parent) 0049 , m_keepFlagIcon(QIcon::fromTheme(QStringLiteral("mail-mark-important"))) 0050 { 0051 m_unreadColor = KColorScheme(QPalette::Normal, KColorScheme::View).foreground(KColorScheme::PositiveText).color(); 0052 m_newColor = KColorScheme(QPalette::Normal, KColorScheme::View).foreground(KColorScheme::NegativeText).color(); 0053 } 0054 0055 bool SortColorizeProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const 0056 { 0057 if (source_parent.isValid()) { 0058 return false; 0059 } 0060 0061 for (ulong i = 0, total = m_matchers.size(); i < total; ++i) { 0062 if (!static_cast<ArticleModel *>(sourceModel())->rowMatches(source_row, m_matchers[i])) { 0063 return false; 0064 } 0065 } 0066 0067 return true; 0068 } 0069 0070 void SortColorizeProxyModel::setFilters(const std::vector<QSharedPointer<const Filters::AbstractMatcher>> &matchers) 0071 { 0072 if (m_matchers == matchers) { 0073 return; 0074 } 0075 m_matchers = matchers; 0076 invalidateFilter(); 0077 } 0078 0079 QVariant SortColorizeProxyModel::data(const QModelIndex &idx, int role) const 0080 { 0081 if (!idx.isValid() || !sourceModel()) { 0082 return {}; 0083 } 0084 0085 const QModelIndex sourceIdx = mapToSource(idx); 0086 0087 switch (role) { 0088 case Qt::ForegroundRole: 0089 switch (static_cast<ArticleStatus>(sourceIdx.data(ArticleModel::StatusRole).toInt())) { 0090 case Unread: 0091 return Settings::useCustomColors() ? Settings::colorUnreadArticles() : m_unreadColor; 0092 case New: 0093 return Settings::useCustomColors() ? Settings::colorNewArticles() : m_newColor; 0094 case Read: 0095 return QApplication::palette().color(QPalette::Text); 0096 } 0097 break; 0098 case Qt::DecorationRole: 0099 if (sourceIdx.column() == ArticleModel::ItemTitleColumn) { 0100 return sourceIdx.data(ArticleModel::IsImportantRole).toBool() ? m_keepFlagIcon : QVariant(); 0101 } 0102 break; 0103 } 0104 return sourceIdx.data(role); 0105 } 0106 0107 namespace 0108 { 0109 static bool isRead(const QModelIndex &idx) 0110 { 0111 if (!idx.isValid()) { 0112 return false; 0113 } 0114 0115 return static_cast<ArticleStatus>(idx.data(ArticleModel::StatusRole).toInt()) == Read; 0116 } 0117 } 0118 0119 void ArticleListView::setArticleModel(ArticleModel *model) 0120 { 0121 if (!model) { 0122 setModel(model); 0123 return; 0124 } 0125 0126 m_proxy = new SortColorizeProxyModel(model); 0127 m_proxy->setSortRole(ArticleModel::SortRole); 0128 m_proxy->setFilters(m_matchers); 0129 m_proxy->setSourceModel(model); 0130 auto const proxy2 = new FilterDeletedProxyModel(model); 0131 proxy2->setSortRole(ArticleModel::SortRole); 0132 proxy2->setSourceModel(m_proxy); 0133 0134 connect(model, &QAbstractItemModel::rowsInserted, m_proxy.data(), &QSortFilterProxyModel::invalidate); 0135 0136 auto const columnsProxy = new FilterColumnsProxyModel(model); 0137 columnsProxy->setSortRole(ArticleModel::SortRole); 0138 columnsProxy->setColumnEnabled(ArticleModel::ItemTitleColumn); 0139 columnsProxy->setColumnEnabled(ArticleModel::FeedTitleColumn); 0140 columnsProxy->setColumnEnabled(ArticleModel::DateColumn); 0141 columnsProxy->setColumnEnabled(ArticleModel::AuthorColumn); 0142 columnsProxy->setSourceModel(proxy2); 0143 0144 setModel(columnsProxy); 0145 header()->setContextMenuPolicy(Qt::CustomContextMenu); 0146 header()->setSectionResizeMode(QHeaderView::Interactive); 0147 } 0148 0149 void ArticleListView::showHeaderMenu(const QPoint &pos) 0150 { 0151 if (!model()) { 0152 return; 0153 } 0154 0155 QPointer<QMenu> menu = new QMenu(this); 0156 menu->setTitle(i18n("Columns")); 0157 menu->setAttribute(Qt::WA_DeleteOnClose); 0158 0159 const int colCount = model()->columnCount(); 0160 int visibleColumns = 0; // number of column currently shown 0161 QAction *visibleColumnsAction = nullptr; 0162 for (int i = 0; i < colCount; ++i) { 0163 QAction *act = menu->addAction(model()->headerData(i, Qt::Horizontal).toString()); 0164 act->setCheckable(true); 0165 act->setData(i); 0166 bool sectionVisible = !header()->isSectionHidden(i); 0167 act->setChecked(sectionVisible); 0168 if (sectionVisible) { 0169 ++visibleColumns; 0170 visibleColumnsAction = act; 0171 } 0172 } 0173 0174 // Avoid that the last shown column is also hidden 0175 if (visibleColumns == 1) { 0176 visibleColumnsAction->setEnabled(false); 0177 } 0178 0179 QPointer<QObject> that(this); 0180 QAction *const action = menu->exec(header()->mapToGlobal(pos)); 0181 if (that && action) { 0182 const int col = action->data().toInt(); 0183 if (action->isChecked()) { 0184 header()->showSection(col); 0185 } else { 0186 header()->hideSection(col); 0187 } 0188 } 0189 delete menu; 0190 } 0191 0192 void ArticleListView::generalPaletteChanged() 0193 { 0194 const QPalette palette = viewport()->palette(); 0195 QColor color = palette.text().color(); 0196 color.setAlpha(128); 0197 mTextColor = color; 0198 } 0199 0200 void ArticleListView::paintEvent(QPaintEvent *event) 0201 { 0202 if ((m_matchers.size() != 0) && (model() && model()->rowCount() == 0)) { 0203 QPainter p(viewport()); 0204 0205 QFont font = p.font(); 0206 font.setItalic(true); 0207 p.setFont(font); 0208 0209 if (!mTextColor.isValid()) { 0210 generalPaletteChanged(); 0211 } 0212 p.setPen(mTextColor); 0213 0214 p.drawText(QRect(0, 0, width(), height()), Qt::AlignCenter, i18n("No result found")); 0215 } else { 0216 QTreeView::paintEvent(event); 0217 } 0218 } 0219 0220 void ArticleListView::saveHeaderSettings() 0221 { 0222 if (model()) { 0223 const QByteArray state = header()->saveState(); 0224 if (m_columnMode == FeedMode) { 0225 m_feedHeaderState = state; 0226 } else { 0227 m_groupHeaderState = state; 0228 } 0229 } 0230 0231 KConfigGroup conf(Settings::self()->config(), QStringLiteral("General")); 0232 conf.writeEntry("ArticleListFeedHeaders", m_feedHeaderState.toBase64()); 0233 conf.writeEntry("ArticleListGroupHeaders", m_groupHeaderState.toBase64()); 0234 } 0235 0236 void ArticleListView::loadHeaderSettings() 0237 { 0238 KConfigGroup conf(Settings::self()->config(), QStringLiteral("General")); 0239 m_feedHeaderState = QByteArray::fromBase64(conf.readEntry("ArticleListFeedHeaders").toLatin1()); 0240 m_groupHeaderState = QByteArray::fromBase64(conf.readEntry("ArticleListGroupHeaders").toLatin1()); 0241 } 0242 0243 QItemSelectionModel *ArticleListView::articleSelectionModel() const 0244 { 0245 return selectionModel(); 0246 } 0247 0248 const QAbstractItemView *ArticleListView::itemView() const 0249 { 0250 return this; 0251 } 0252 0253 QAbstractItemView *ArticleListView::itemView() 0254 { 0255 return this; 0256 } 0257 0258 QPoint ArticleListView::scrollBarPositions() const 0259 { 0260 return {horizontalScrollBar()->value(), verticalScrollBar()->value()}; 0261 } 0262 0263 void ArticleListView::setScrollBarPositions(const QPoint &p) 0264 { 0265 horizontalScrollBar()->setValue(p.x()); 0266 verticalScrollBar()->setValue(p.y()); 0267 } 0268 0269 void ArticleListView::setGroupMode() 0270 { 0271 if (m_columnMode == GroupMode) { 0272 return; 0273 } 0274 0275 if (model()) { 0276 m_feedHeaderState = header()->saveState(); 0277 } 0278 m_columnMode = GroupMode; 0279 restoreHeaderState(); 0280 } 0281 0282 void ArticleListView::setFeedMode() 0283 { 0284 if (m_columnMode == FeedMode) { 0285 return; 0286 } 0287 0288 if (model()) { 0289 m_groupHeaderState = header()->saveState(); 0290 } 0291 m_columnMode = FeedMode; 0292 restoreHeaderState(); 0293 } 0294 0295 static int maxDateColumnWidth(const QFontMetrics &fm) 0296 { 0297 int width = 0; 0298 QDateTime date(QDate::currentDate(), QTime(23, 59)); 0299 for (int x = 0; x < 10; ++x, date = date.addDays(-1)) { 0300 QString txt = QLatin1Char(' ') + QLocale().toString(date, QLocale::ShortFormat) + QLatin1Char(' '); 0301 width = qMax(width, fm.boundingRect(txt).width()); 0302 } 0303 return width; 0304 } 0305 0306 void ArticleListView::restoreHeaderState() 0307 { 0308 QByteArray state = m_columnMode == GroupMode ? m_groupHeaderState : m_feedHeaderState; 0309 header()->restoreState(state); 0310 if (state.isEmpty()) { 0311 // No state, set a default config: 0312 // - hide the feed column in feed mode (no need to see the same feed title over and over) 0313 // - set the date column wide enough to fit all possible dates 0314 header()->setSectionHidden(ArticleModel::FeedTitleColumn, m_columnMode == FeedMode); 0315 header()->setStretchLastSection(false); 0316 header()->resizeSection(ArticleModel::DateColumn, maxDateColumnWidth(fontMetrics())); 0317 if (model()) { 0318 startResizingTitleColumn(); 0319 } 0320 } 0321 0322 if (header()->sectionSize(ArticleModel::DateColumn) == 1) { 0323 header()->resizeSection(ArticleModel::DateColumn, maxDateColumnWidth(fontMetrics())); 0324 } 0325 } 0326 0327 void ArticleListView::startResizingTitleColumn() 0328 { 0329 // set the title column to Stretch resize mode so that it adapts to the 0330 // content. finishResizingTitleColumn() will turn the resize mode back to 0331 // Interactive so that the user can still resize the column himself if he 0332 // wants to 0333 header()->setSectionResizeMode(ArticleModel::ItemTitleColumn, QHeaderView::Stretch); 0334 QMetaObject::invokeMethod(this, &ArticleListView::finishResizingTitleColumn, Qt::QueuedConnection); 0335 } 0336 0337 void ArticleListView::finishResizingTitleColumn() 0338 { 0339 if (QApplication::mouseButtons() != Qt::NoButton) { 0340 // Come back later: user is still resizing the widget 0341 QMetaObject::invokeMethod(this, &ArticleListView::finishResizingTitleColumn, Qt::QueuedConnection); 0342 return; 0343 } 0344 header()->setSectionResizeMode(QHeaderView::Interactive); 0345 } 0346 0347 ArticleListView::~ArticleListView() 0348 { 0349 saveHeaderSettings(); 0350 } 0351 0352 void ArticleListView::setIsAggregation(bool aggregation) 0353 { 0354 if (aggregation) { 0355 setGroupMode(); 0356 } else { 0357 setFeedMode(); 0358 } 0359 } 0360 0361 ArticleListView::ArticleListView(QWidget *parent) 0362 : QTreeView(parent) 0363 , m_columnMode(FeedMode) 0364 { 0365 setSortingEnabled(true); 0366 setAlternatingRowColors(true); 0367 setSelectionMode(QAbstractItemView::ExtendedSelection); 0368 setUniformRowHeights(true); 0369 setRootIsDecorated(false); 0370 setAllColumnsShowFocus(true); 0371 setDragDropMode(QAbstractItemView::DragOnly); 0372 0373 setMinimumSize(250, 150); 0374 setWhatsThis( 0375 i18n("<h2>Article list</h2>" 0376 "Here you can browse articles from the currently selected feed. " 0377 "You can also manage articles, as marking them as persistent (\"Mark as Important\") or delete them, using the right mouse button menu. " 0378 "To view the web page of the article, you can open the article internally in a tab or in an external browser window.")); 0379 0380 // connect exactly once 0381 disconnect(header(), &QWidget::customContextMenuRequested, this, &ArticleListView::showHeaderMenu); 0382 connect(header(), &QWidget::customContextMenuRequested, this, &ArticleListView::showHeaderMenu); 0383 loadHeaderSettings(); 0384 } 0385 0386 void ArticleListView::mousePressEvent(QMouseEvent *ev) 0387 { 0388 // let's push the event, so we can use currentIndex() to get the newly selected article.. 0389 QTreeView::mousePressEvent(ev); 0390 0391 if (ev->button() == Qt::MiddleButton) { 0392 const QUrl url = currentIndex().data(ArticleModel::LinkRole).toUrl(); 0393 0394 Q_EMIT signalMouseButtonPressed(ev->button(), url); 0395 } 0396 } 0397 0398 void ArticleListView::contextMenuEvent(QContextMenuEvent *event) 0399 { 0400 QWidget *w = ActionManager::getInstance()->container(QStringLiteral("article_popup")); 0401 auto popup = qobject_cast<QMenu *>(w); 0402 if (popup) { 0403 popup->exec(event->globalPos()); 0404 } 0405 } 0406 0407 void ArticleListView::setModel(QAbstractItemModel *m) 0408 { 0409 const bool groupMode = m_columnMode == GroupMode; 0410 0411 QAbstractItemModel *const oldModel = model(); 0412 if (oldModel) { 0413 const QByteArray state = header()->saveState(); 0414 if (groupMode) { 0415 m_groupHeaderState = state; 0416 } else { 0417 m_feedHeaderState = state; 0418 } 0419 } 0420 0421 QTreeView::setModel(m); 0422 0423 if (m) { 0424 sortByColumn(ArticleModel::DateColumn, Qt::DescendingOrder); 0425 restoreHeaderState(); 0426 0427 // Ensure at least one column is visible 0428 if (header()->hiddenSectionCount() == header()->count()) { 0429 header()->showSection(ArticleModel::ItemTitleColumn); 0430 } 0431 } 0432 } 0433 0434 void ArticleListView::slotClear() 0435 { 0436 setModel(nullptr); 0437 } 0438 0439 void ArticleListView::slotPreviousArticle() 0440 { 0441 if (!model()) { 0442 return; 0443 } 0444 Q_EMIT userActionTakingPlace(); 0445 const QModelIndex idx = currentIndex(); 0446 const int newRow = qMax(0, (idx.isValid() ? idx.row() : model()->rowCount()) - 1); 0447 const QModelIndex newIdx = idx.isValid() ? idx.sibling(newRow, 0) : model()->index(newRow, 0); 0448 selectIndex(newIdx); 0449 } 0450 0451 void ArticleListView::slotNextArticle() 0452 { 0453 if (!model()) { 0454 return; 0455 } 0456 0457 Q_EMIT userActionTakingPlace(); 0458 const QModelIndex idx = currentIndex(); 0459 const int newRow = idx.isValid() ? (idx.row() + 1) : 0; 0460 const QModelIndex newIdx = model()->index(qMin(newRow, model()->rowCount() - 1), 0); 0461 selectIndex(newIdx); 0462 } 0463 0464 void ArticleListView::slotNextUnreadArticle() 0465 { 0466 if (!model()) { 0467 return; 0468 } 0469 0470 const int rowCount = model()->rowCount(); 0471 const int startRow = qMin(rowCount - 1, (currentIndex().isValid() ? currentIndex().row() + 1 : 0)); 0472 0473 int i = startRow; 0474 bool foundUnread = false; 0475 0476 do { 0477 if (!::isRead(model()->index(i, 0))) { 0478 foundUnread = true; 0479 } else { 0480 i = (i + 1) % rowCount; 0481 } 0482 } while (!foundUnread && i != startRow); 0483 0484 if (foundUnread) { 0485 selectIndex(model()->index(i, 0)); 0486 } 0487 } 0488 0489 void ArticleListView::selectIndex(const QModelIndex &idx) 0490 { 0491 if (!idx.isValid()) { 0492 return; 0493 } 0494 setCurrentIndex(idx); 0495 scrollTo(idx, PositionAtCenter); 0496 } 0497 0498 void ArticleListView::slotPreviousUnreadArticle() 0499 { 0500 if (!model()) { 0501 return; 0502 } 0503 0504 const int rowCount = model()->rowCount(); 0505 const int startRow = qMax(0, (currentIndex().isValid() ? currentIndex().row() : rowCount) - 1); 0506 0507 int i = startRow; 0508 bool foundUnread = false; 0509 0510 do { 0511 if (!::isRead(model()->index(i, 0))) { 0512 foundUnread = true; 0513 } else { 0514 i = i > 0 ? i - 1 : rowCount - 1; 0515 } 0516 } while (!foundUnread && i != startRow); 0517 0518 if (foundUnread) { 0519 selectIndex(model()->index(i, 0)); 0520 } 0521 } 0522 0523 void ArticleListView::forceFilterUpdate() 0524 { 0525 if (m_proxy) { 0526 m_proxy->invalidate(); 0527 } 0528 } 0529 0530 void ArticleListView::setFilters(const std::vector<QSharedPointer<const Filters::AbstractMatcher>> &matchers) 0531 { 0532 if (m_matchers == matchers) { 0533 return; 0534 } 0535 m_matchers = matchers; 0536 if (m_proxy) { 0537 m_proxy->setFilters(matchers); 0538 } 0539 } 0540 0541 #include "moc_articlelistview.cpp"