File indexing completed on 2024-05-05 05:13:10
0001 /* 0002 This file is part of Akregator. 0003 0004 SPDX-FileCopyrightText: 2007 Frank Osterfeld <osterfeld@kde.org> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0 0007 */ 0008 0009 #include "subscriptionlistmodel.h" 0010 #include "feed.h" 0011 #include "feedlist.h" 0012 #include "folder.h" 0013 #include "subscriptionlistjobs.h" 0014 #include "treenode.h" 0015 0016 #include "akregator_debug.h" 0017 #include <KLocalizedString> 0018 0019 #include <KColorScheme> 0020 #include <QApplication> 0021 #include <QByteArray> 0022 #include <QDataStream> 0023 #include <QIODevice> 0024 #include <QIcon> 0025 #include <QItemSelection> 0026 #include <QList> 0027 #include <QMimeData> 0028 #include <QPalette> 0029 #include <QStyle> 0030 #include <QUrl> 0031 #include <QVariant> 0032 0033 using namespace Akregator; 0034 using namespace Syndication; 0035 0036 #define AKREGATOR_TREENODE_MIMETYPE QStringLiteral("akregator/treenode-id") 0037 0038 namespace 0039 { 0040 static uint nodeIdForIndex(const QModelIndex &idx) 0041 { 0042 return idx.isValid() ? idx.internalId() : 0; 0043 } 0044 0045 static QString errorCodeToString(Syndication::ErrorCode err) 0046 { 0047 switch (err) { 0048 case Timeout: 0049 return i18n("Timeout on remote server"); 0050 case UnknownHost: 0051 return i18n("Unknown host"); 0052 case FileNotFound: 0053 return i18n("Feed file not found on remote server"); 0054 case InvalidXml: 0055 return i18n("Could not read feed (invalid XML)"); 0056 case XmlNotAccepted: 0057 return i18n("Could not read feed (unknown format)"); 0058 case InvalidFormat: 0059 return i18n("Could not read feed (invalid feed)"); 0060 case Success: 0061 case Aborted: 0062 default: 0063 return {}; 0064 } 0065 } 0066 0067 static const Akregator::TreeNode *nodeForIndex(const QModelIndex &index, const FeedList *feedList) 0068 { 0069 return (!index.isValid() || !feedList) ? nullptr : feedList->findByID(index.internalId()); 0070 } 0071 } 0072 0073 Akregator::FilterUnreadProxyModel::FilterUnreadProxyModel(QObject *parent) 0074 : QSortFilterProxyModel(parent) 0075 , m_selectedHierarchy() 0076 { 0077 setDynamicSortFilter(true); 0078 } 0079 0080 bool Akregator::FilterUnreadProxyModel::doFilter() const 0081 { 0082 return m_doFilter; 0083 } 0084 0085 void Akregator::FilterUnreadProxyModel::setDoFilter(bool v) 0086 { 0087 m_doFilter = v; 0088 invalidateFilter(); 0089 } 0090 0091 void Akregator::FilterUnreadProxyModel::setSourceModel(QAbstractItemModel *src) 0092 { 0093 clearCache(); 0094 QSortFilterProxyModel::setSourceModel(src); 0095 } 0096 0097 bool Akregator::FilterUnreadProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const 0098 { 0099 if (!m_doFilter) { 0100 return true; 0101 } 0102 0103 QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); 0104 0105 if (m_selectedHierarchy.contains(idx)) { 0106 return true; 0107 } 0108 0109 QVariant v = idx.data(SubscriptionListModel::HasUnreadRole); 0110 if (v.isNull()) { 0111 return true; 0112 } 0113 0114 return v.toBool(); 0115 } 0116 0117 /** 0118 * This caches the hierarchy of the selected node. Its purpose is to allow 0119 * feeds/folders with no unread content not to be filtered out immediately, 0120 * which would occur otherwise (we'd select the last article to read, it would 0121 * become unread, and disappear from the list without letting us view it). 0122 **/ 0123 void Akregator::FilterUnreadProxyModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) 0124 { 0125 QModelIndexList desel = mapSelectionToSource(deselected).indexes(); 0126 // calling invalidateFilter causes refiltering at the call point, so we should 0127 // call it ONLY after we recreate our node cache 0128 bool doInvalidate = false; 0129 0130 // if we're deselecting an empty feed/folder, we need to hide it 0131 if (!desel.isEmpty()) { 0132 if (m_selectedHierarchy.contains(desel.at(0))) { 0133 doInvalidate = true; 0134 } 0135 } 0136 0137 clearCache(); 0138 0139 QModelIndexList sel = mapSelectionToSource(selected).indexes(); 0140 if (!sel.isEmpty()) { 0141 // XXX add support for multiple selections? this doesn't generally make sense in this case honestly 0142 QModelIndex current = sel.at(0); 0143 while (current.isValid()) { 0144 m_selectedHierarchy.insert(current); 0145 current = current.parent(); 0146 } 0147 } 0148 0149 if (doInvalidate && doFilter()) { 0150 invalidateFilter(); 0151 } 0152 } 0153 0154 void Akregator::FilterUnreadProxyModel::clearCache() 0155 { 0156 m_selectedHierarchy.clear(); 0157 } 0158 0159 Akregator::SubscriptionListModel::SubscriptionListModel(const QSharedPointer<const FeedList> &feedList, QObject *parent) 0160 : QAbstractItemModel(parent) 0161 , m_feedList(feedList) 0162 , m_beganRemoval(false) 0163 { 0164 if (!m_feedList) { 0165 return; 0166 } 0167 connect(m_feedList.data(), &FeedList::signalNodeAdded, this, &SubscriptionListModel::subscriptionAdded); 0168 connect(m_feedList.data(), &FeedList::signalAboutToRemoveNode, this, &SubscriptionListModel::aboutToRemoveSubscription); 0169 connect(m_feedList.data(), &FeedList::signalNodeRemoved, this, &SubscriptionListModel::subscriptionRemoved); 0170 connect(m_feedList.data(), &FeedList::signalNodeChanged, this, &SubscriptionListModel::subscriptionChanged); 0171 connect(m_feedList.data(), &FeedList::fetchStarted, this, &SubscriptionListModel::fetchStarted); 0172 connect(m_feedList.data(), &FeedList::fetched, this, &SubscriptionListModel::fetched); 0173 connect(m_feedList.data(), &FeedList::fetchAborted, this, &SubscriptionListModel::fetchAborted); 0174 0175 m_errorColor = KColorScheme(QPalette::Normal, KColorScheme::View).foreground(KColorScheme::NegativeText).color(); 0176 } 0177 0178 int Akregator::SubscriptionListModel::columnCount(const QModelIndex &) const 0179 { 0180 return 3; 0181 } 0182 0183 int Akregator::SubscriptionListModel::rowCount(const QModelIndex &parent) const 0184 { 0185 if (!parent.isValid()) { 0186 return 1; 0187 } 0188 0189 const Akregator::TreeNode *const node = nodeForIndex(parent, m_feedList.data()); 0190 return node ? node->children().count() : 0; 0191 } 0192 0193 QVariant Akregator::SubscriptionListModel::data(const QModelIndex &index, int role) const 0194 { 0195 if (!index.isValid()) { 0196 return {}; 0197 } 0198 0199 const Akregator::TreeNode *const node = nodeForIndex(index, m_feedList.data()); 0200 0201 if (!node) { 0202 return {}; 0203 } 0204 0205 const Feed *const feed = qobject_cast<const Feed *const>(node); 0206 0207 switch (role) { 0208 case Qt::EditRole: 0209 case Qt::DisplayRole: 0210 switch (index.column()) { 0211 case TitleColumn: 0212 return node->title(); 0213 case UnreadCountColumn: 0214 return node->unread(); 0215 case TotalCountColumn: 0216 return node->totalCount(); 0217 } 0218 break; 0219 case Qt::ForegroundRole: 0220 return feed && feed->fetchErrorCode() ? m_errorColor : QApplication::palette().color(QPalette::Text); 0221 case Qt::ToolTipRole: { 0222 if (node->isGroup() || node->isAggregation()) { 0223 return node->title(); 0224 } 0225 if (!feed) { 0226 return QString(); 0227 } 0228 if (feed->fetchErrorOccurred()) { 0229 return i18n("Could not fetch feed: %1", errorCodeToString(feed->fetchErrorCode())); 0230 } 0231 return feed->title(); 0232 } 0233 case Qt::DecorationRole: { 0234 if (index.column() != TitleColumn) { 0235 return {}; 0236 } 0237 const auto iconSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); 0238 return feed && feed->isFetching() ? node->icon().pixmap(iconSize, QIcon::Active) : node->icon(); 0239 } 0240 case SubscriptionIdRole: 0241 return node->id(); 0242 case IsGroupRole: 0243 return node->isGroup(); 0244 case IsFetchableRole: 0245 return !node->isGroup() && !node->isAggregation(); 0246 case IsAggregationRole: 0247 return node->isAggregation(); 0248 case LinkRole: { 0249 return feed ? feed->xmlUrl() : QVariant(); 0250 } 0251 case IsOpenRole: { 0252 if (!node->isGroup()) { 0253 return false; 0254 } 0255 const auto *const folder = qobject_cast<const Akregator::Folder *const>(node); 0256 Q_ASSERT(folder); 0257 return folder->isOpen(); 0258 } 0259 case HasUnreadRole: 0260 return node->unread() > 0; 0261 } 0262 0263 return {}; 0264 } 0265 0266 QVariant Akregator::SubscriptionListModel::headerData(int section, Qt::Orientation, int role) const 0267 { 0268 if (role != Qt::DisplayRole) { 0269 return {}; 0270 } 0271 0272 switch (section) { 0273 case TitleColumn: 0274 return i18nc("Feedlist's column header", "Feeds"); 0275 case UnreadCountColumn: 0276 return i18nc("Feedlist's column header", "Unread"); 0277 case TotalCountColumn: 0278 return i18nc("Feedlist's column header", "Total"); 0279 } 0280 0281 return {}; 0282 } 0283 0284 QModelIndex Akregator::SubscriptionListModel::parent(const QModelIndex &index) const 0285 { 0286 const Akregator::TreeNode *const node = nodeForIndex(index, m_feedList.data()); 0287 0288 if (!node || !node->parent()) { 0289 return {}; 0290 } 0291 0292 const Akregator::Folder *parent = node->parent(); 0293 0294 if (!parent->parent()) { 0295 return createIndex(0, 0, parent->id()); 0296 } 0297 0298 const Akregator::Folder *const grandparent = parent->parent(); 0299 0300 const int row = grandparent->indexOf(parent); 0301 0302 Q_ASSERT(row != -1); 0303 0304 return createIndex(row, 0, parent->id()); 0305 } 0306 0307 QModelIndex Akregator::SubscriptionListModel::index(int row, int column, const QModelIndex &parent) const 0308 { 0309 if (!parent.isValid()) { 0310 return (row == 0 && m_feedList) ? createIndex(row, column, m_feedList->allFeedsFolder()->id()) : QModelIndex(); 0311 } 0312 0313 const Akregator::TreeNode *const parentNode = nodeForIndex(parent, m_feedList.data()); 0314 0315 if (!parentNode) { 0316 return {}; 0317 } 0318 0319 const Akregator::TreeNode *const childNode = parentNode->childAt(row); 0320 return childNode ? createIndex(row, column, childNode->id()) : QModelIndex(); 0321 } 0322 0323 QModelIndex SubscriptionListModel::indexForNode(const TreeNode *node) const 0324 { 0325 if (!node || !m_feedList) { 0326 return {}; 0327 } 0328 const Folder *const parent = node->parent(); 0329 if (!parent) { 0330 return index(0, 0); 0331 } 0332 const int row = parent->indexOf(node); 0333 Q_ASSERT(row >= 0); 0334 const QModelIndex idx = index(row, 0, indexForNode(parent)); 0335 Q_ASSERT(idx.internalId() == node->id()); 0336 return idx; 0337 } 0338 0339 void Akregator::SubscriptionListModel::subscriptionAdded(Akregator::TreeNode *subscription) 0340 { 0341 const Folder *const parent = subscription->parent(); 0342 const int row = parent ? parent->indexOf(subscription) : 0; 0343 Q_ASSERT(row >= 0); 0344 beginInsertRows(indexForNode(parent), row, row); 0345 endInsertRows(); 0346 } 0347 0348 void Akregator::SubscriptionListModel::aboutToRemoveSubscription(Akregator::TreeNode *subscription) 0349 { 0350 qCDebug(AKREGATOR_LOG) << subscription->id(); 0351 const Folder *const parent = subscription->parent(); 0352 const int row = parent ? parent->indexOf(subscription) : -1; 0353 if (row < 0) { 0354 return; 0355 } 0356 beginRemoveRows(indexForNode(parent), row, row); 0357 m_beganRemoval = true; 0358 } 0359 0360 void Akregator::SubscriptionListModel::subscriptionRemoved(TreeNode *subscription) 0361 { 0362 qCDebug(AKREGATOR_LOG) << subscription->id(); 0363 if (m_beganRemoval) { 0364 m_beganRemoval = false; 0365 endRemoveRows(); 0366 } 0367 } 0368 0369 void Akregator::SubscriptionListModel::subscriptionChanged(TreeNode *node) 0370 { 0371 const QModelIndex idx = indexForNode(node); 0372 if (!idx.isValid()) { 0373 return; 0374 } 0375 Q_EMIT dataChanged(index(idx.row(), 0, idx.parent()), index(idx.row(), ColumnCount - 1, idx.parent())); 0376 } 0377 0378 void SubscriptionListModel::fetchStarted(Akregator::Feed *node) 0379 { 0380 subscriptionChanged(node); 0381 } 0382 0383 void SubscriptionListModel::fetched(Akregator::Feed *node) 0384 { 0385 subscriptionChanged(node); 0386 } 0387 0388 void SubscriptionListModel::fetchError(Akregator::Feed *node) 0389 { 0390 subscriptionChanged(node); 0391 } 0392 0393 void SubscriptionListModel::fetchAborted(Akregator::Feed *node) 0394 { 0395 subscriptionChanged(node); 0396 } 0397 0398 void Akregator::FolderExpansionHandler::itemExpanded(const QModelIndex &idx) 0399 { 0400 setExpanded(idx, true); 0401 } 0402 0403 void Akregator::FolderExpansionHandler::itemCollapsed(const QModelIndex &idx) 0404 { 0405 setExpanded(idx, false); 0406 } 0407 0408 void Akregator::FolderExpansionHandler::setExpanded(const QModelIndex &idx, bool expanded) 0409 { 0410 if (!m_feedList || !m_model) { 0411 return; 0412 } 0413 Akregator::TreeNode *const node = m_feedList->findByID(nodeIdForIndex(idx)); 0414 if (!node || !node->isGroup()) { 0415 return; 0416 } 0417 0418 auto const folder = qobject_cast<Akregator::Folder *>(node); 0419 Q_ASSERT(folder); 0420 folder->setOpen(expanded); 0421 } 0422 0423 FolderExpansionHandler::FolderExpansionHandler(QObject *parent) 0424 : QObject(parent) 0425 , m_feedList() 0426 , m_model(nullptr) 0427 { 0428 } 0429 0430 void FolderExpansionHandler::setModel(QAbstractItemModel *model) 0431 { 0432 m_model = model; 0433 } 0434 0435 void FolderExpansionHandler::setFeedList(const QSharedPointer<FeedList> &feedList) 0436 { 0437 m_feedList = feedList; 0438 } 0439 0440 Qt::ItemFlags SubscriptionListModel::flags(const QModelIndex &idx) const 0441 { 0442 const Qt::ItemFlags flags = QAbstractItemModel::flags(idx); 0443 if (!idx.isValid() || (idx.column() != TitleColumn)) { 0444 return flags; 0445 } 0446 if (!idx.parent().isValid()) { // the root folder is neither draggable nor editable 0447 return flags | Qt::ItemIsDropEnabled; 0448 } 0449 return flags | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable; 0450 } 0451 0452 QStringList SubscriptionListModel::mimeTypes() const 0453 { 0454 QStringList types; 0455 types << QStringLiteral("text/uri-list") << AKREGATOR_TREENODE_MIMETYPE; 0456 return types; 0457 } 0458 0459 QMimeData *SubscriptionListModel::mimeData(const QModelIndexList &indexes) const 0460 { 0461 auto mimeData = new QMimeData; 0462 0463 QList<QUrl> urls; 0464 for (const QModelIndex &i : indexes) { 0465 const QUrl url(i.data(LinkRole).toString()); 0466 if (!url.isEmpty()) { 0467 urls << url; 0468 } 0469 } 0470 0471 mimeData->setUrls(urls); 0472 0473 QByteArray idList; 0474 QDataStream idStream(&idList, QIODevice::WriteOnly); 0475 for (const QModelIndex &i : indexes) { 0476 if (i.isValid()) { 0477 idStream << i.data(SubscriptionIdRole).toInt(); 0478 } 0479 } 0480 0481 mimeData->setData(AKREGATOR_TREENODE_MIMETYPE, idList); 0482 0483 return mimeData; 0484 } 0485 0486 bool SubscriptionListModel::setData(const QModelIndex &idx, const QVariant &value, int role) 0487 { 0488 if (!idx.isValid() || idx.column() != TitleColumn || role != Qt::EditRole) { 0489 return false; 0490 } 0491 const TreeNode *const node = nodeForIndex(idx, m_feedList.data()); 0492 if (!node) { 0493 return false; 0494 } 0495 auto job = new RenameSubscriptionJob(this); 0496 job->setSubscriptionId(node->id()); 0497 job->setName(value.toString()); 0498 job->start(); 0499 return true; 0500 } 0501 0502 bool SubscriptionListModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) 0503 { 0504 Q_UNUSED(column) 0505 0506 if (action == Qt::IgnoreAction) { 0507 return true; 0508 } 0509 0510 // if ( column != TitleColumn ) 0511 // return false; 0512 0513 if (!data->hasFormat(AKREGATOR_TREENODE_MIMETYPE)) { 0514 return false; 0515 } 0516 0517 const auto *const droppedOnNode = qobject_cast<const TreeNode *>(nodeForIndex(parent, m_feedList.data())); 0518 0519 if (!droppedOnNode) { 0520 return false; 0521 } 0522 0523 const Folder *const destFolder = droppedOnNode->isGroup() ? qobject_cast<const Folder *>(droppedOnNode) : droppedOnNode->parent(); 0524 if (!destFolder) { 0525 return false; 0526 } 0527 0528 QByteArray idData = data->data(AKREGATOR_TREENODE_MIMETYPE); 0529 QList<int> ids; 0530 QDataStream stream(&idData, QIODevice::ReadOnly); 0531 while (!stream.atEnd()) { 0532 int id; 0533 stream >> id; 0534 ids << id; 0535 } 0536 0537 // don't drop nodes into their own subtree 0538 for (const int id : std::as_const(ids)) { 0539 const auto *const asFolder = qobject_cast<const Folder *>(m_feedList->findByID(id)); 0540 if (asFolder && (asFolder == destFolder || asFolder->subtreeContains(destFolder))) { 0541 return false; 0542 } 0543 } 0544 0545 const TreeNode *const after = droppedOnNode->isGroup() ? destFolder->childAt(row) : droppedOnNode; 0546 0547 for (const int id : std::as_const(ids)) { 0548 const TreeNode *const node = m_feedList->findByID(id); 0549 if (!node) { 0550 continue; 0551 } 0552 auto job = new MoveSubscriptionJob(this); 0553 job->setSubscriptionId(node->id()); 0554 job->setDestination(destFolder->id(), after ? after->id() : 0); 0555 job->start(); 0556 } 0557 0558 return true; 0559 } 0560 0561 #include "moc_subscriptionlistmodel.cpp"