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"