File indexing completed on 2024-04-21 05:10:37

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 "selectioncontroller.h"
0010 
0011 #include "actionmanager.h"
0012 #include "akregatorconfig.h"
0013 #include "article.h"
0014 #include "articlejobs.h"
0015 #include "articlemodel.h"
0016 #include "feedlist.h"
0017 #include "subscriptionlistmodel.h"
0018 #include "treenode.h"
0019 
0020 #include "akregator_debug.h"
0021 
0022 #include <QAbstractItemView>
0023 #include <QMenu>
0024 #include <QTreeView>
0025 #include <memory>
0026 using namespace Akregator;
0027 
0028 namespace
0029 {
0030 static Akregator::Article articleForIndex(const QModelIndex &index, FeedList *feedList)
0031 {
0032     if (!index.isValid()) {
0033         return {};
0034     }
0035 
0036     const QString guid = index.data(ArticleModel::GuidRole).toString();
0037     const QString feedId = index.data(ArticleModel::FeedIdRole).toString();
0038     return feedList->findArticle(feedId, guid);
0039 }
0040 
0041 static QList<Akregator::Article> articlesForIndexes(const QModelIndexList &indexes, FeedList *feedList)
0042 {
0043     QList<Akregator::Article> articles;
0044     for (const QModelIndex &i : indexes) {
0045         const Article a = articleForIndex(i, feedList);
0046         if (a.isNull()) {
0047             continue;
0048         }
0049         articles.append(articleForIndex(i, feedList));
0050     }
0051 
0052     return articles;
0053 }
0054 
0055 static Akregator::TreeNode *subscriptionForIndex(const QModelIndex &index, Akregator::FeedList *feedList)
0056 {
0057     if (!index.isValid()) {
0058         return nullptr;
0059     }
0060 
0061     return feedList->findByID(index.data(Akregator::SubscriptionListModel::SubscriptionIdRole).toInt());
0062 }
0063 } // anon namespace
0064 
0065 SelectionController::SelectionController(QObject *parent)
0066     : AbstractSelectionController(parent)
0067     , m_feedList()
0068     , m_feedSelector()
0069     , m_subscriptionModel(new FilterUnreadProxyModel(this))
0070     , m_selectedSubscription()
0071 {
0072     m_subscriptionModel->setDoFilter(Settings::hideReadFeeds());
0073     m_subscriptionModel->setSourceModel(new SubscriptionListModel(QSharedPointer<FeedList>(), this));
0074 
0075     connect(m_subscriptionModel, &FilterUnreadProxyModel::dataChanged, this, &SelectionController::subscriptionDataChanged);
0076 }
0077 
0078 SelectionController::~SelectionController()
0079 {
0080     delete m_articleModel;
0081 }
0082 
0083 void SelectionController::setFeedSelector(QAbstractItemView *feedSelector)
0084 {
0085     if (m_feedSelector == feedSelector) {
0086         return;
0087     }
0088 
0089     if (m_feedSelector) {
0090         m_feedSelector->disconnect(this);
0091         m_feedSelector->selectionModel()->disconnect(this);
0092         m_feedSelector->selectionModel()->disconnect(m_subscriptionModel);
0093     }
0094 
0095     m_feedSelector = feedSelector;
0096 
0097     if (!m_feedSelector) {
0098         return;
0099     }
0100 
0101     m_feedSelector->setModel(m_subscriptionModel);
0102     m_subscriptionModel->clearCache();
0103 
0104     connect(m_feedSelector.data(), &QAbstractItemView::customContextMenuRequested, this, &SelectionController::subscriptionContextMenuRequested);
0105     connect(m_feedSelector->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelectionController::selectedSubscriptionChanged);
0106     connect(m_feedSelector.data(), &QAbstractItemView::activated, this, &SelectionController::selectedSubscriptionChanged);
0107     connect(m_feedSelector->selectionModel(), &QItemSelectionModel::selectionChanged, m_subscriptionModel, &FilterUnreadProxyModel::selectionChanged);
0108 }
0109 
0110 void SelectionController::setArticleLister(Akregator::ArticleLister *lister)
0111 {
0112     if (m_articleLister == lister) {
0113         return;
0114     }
0115 
0116     if (m_articleLister) {
0117         m_articleLister->articleSelectionModel()->disconnect(this);
0118     }
0119     if (m_articleLister && m_articleLister->itemView()) {
0120         m_articleLister->itemView()->disconnect(this);
0121     }
0122 
0123     m_articleLister = lister;
0124 
0125     if (m_articleLister && m_articleLister->itemView()) {
0126         connect(m_articleLister->itemView(), &QAbstractItemView::doubleClicked, this, &SelectionController::articleIndexDoubleClicked);
0127     }
0128 }
0129 
0130 void SelectionController::setSingleArticleDisplay(Akregator::SingleArticleDisplay *display)
0131 {
0132     m_singleDisplay = display;
0133 }
0134 
0135 Akregator::Article SelectionController::currentArticle() const
0136 {
0137     if (!m_articleLister || !m_articleLister->articleSelectionModel()) {
0138         return {};
0139     }
0140     return ::articleForIndex(m_articleLister->articleSelectionModel()->currentIndex(), m_feedList.data());
0141 }
0142 
0143 QModelIndex SelectionController::currentArticleIndex() const
0144 {
0145     return m_articleLister->articleSelectionModel()->currentIndex();
0146 }
0147 
0148 QList<Akregator::Article> SelectionController::selectedArticles() const
0149 {
0150     if (!m_articleLister || !m_articleLister->articleSelectionModel()) {
0151         return {};
0152     }
0153     return ::articlesForIndexes(m_articleLister->articleSelectionModel()->selectedRows(), m_feedList.data());
0154 }
0155 
0156 Akregator::TreeNode *SelectionController::selectedSubscription() const
0157 {
0158     return ::subscriptionForIndex(m_feedSelector->selectionModel()->currentIndex(), m_feedList.data());
0159 }
0160 
0161 void SelectionController::setFeedList(const QSharedPointer<FeedList> &list)
0162 {
0163     if (m_feedList == list) {
0164         return;
0165     }
0166 
0167     m_feedList = list;
0168     auto m = qobject_cast<SubscriptionListModel *>(m_subscriptionModel->sourceModel());
0169     std::unique_ptr<SubscriptionListModel> oldModel(m);
0170     m_subscriptionModel->setSourceModel(new SubscriptionListModel(m_feedList, this));
0171 
0172     if (m_folderExpansionHandler) {
0173         m_folderExpansionHandler->setFeedList(m_feedList);
0174         m_folderExpansionHandler->setModel(m_subscriptionModel);
0175     }
0176 
0177     if (m_feedSelector) {
0178         if (m_feedList) {
0179             m_feedSelector->setModel(m_subscriptionModel);
0180             disconnect(m_feedSelector->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelectionController::selectedSubscriptionChanged);
0181             connect(m_feedSelector->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelectionController::selectedSubscriptionChanged);
0182         } else {
0183             disconnect(m_feedSelector->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelectionController::selectedSubscriptionChanged);
0184         }
0185     }
0186 }
0187 
0188 void SelectionController::setFolderExpansionHandler(Akregator::FolderExpansionHandler *handler)
0189 {
0190     if (handler == m_folderExpansionHandler) {
0191         return;
0192     }
0193     m_folderExpansionHandler = handler;
0194     if (!m_folderExpansionHandler) {
0195         return;
0196     }
0197     handler->setFeedList(m_feedList);
0198     handler->setModel(m_subscriptionModel);
0199 }
0200 
0201 void SelectionController::articleHeadersAvailable(KJob *job)
0202 {
0203     Q_ASSERT(job);
0204     Q_ASSERT(job == m_listJob);
0205 
0206     if (job->error()) {
0207         qCWarning(AKREGATOR_LOG) << job->errorText();
0208         return;
0209     }
0210     TreeNode *const node = m_listJob->node();
0211 
0212     Q_ASSERT(node); // if there was no error, the node must still exist
0213     Q_ASSERT(node == m_selectedSubscription); //...and equal the previously selected node
0214 
0215     auto const newModel = new ArticleModel(m_listJob->articles());
0216 
0217     connect(node, &QObject::destroyed, newModel, &ArticleModel::clear);
0218     connect(node, &TreeNode::signalArticlesAdded, newModel, &ArticleModel::articlesAdded);
0219     connect(node, &TreeNode::signalArticlesRemoved, newModel, &ArticleModel::articlesRemoved);
0220     connect(node, &TreeNode::signalArticlesUpdated, newModel, &ArticleModel::articlesUpdated);
0221 
0222     m_articleLister->setIsAggregation(node->isAggregation());
0223     m_articleLister->setArticleModel(newModel);
0224     delete m_articleModel; // order is important: do not delete the old model before the new model is set in the view
0225     m_articleModel = newModel;
0226 
0227     disconnect(m_articleLister->articleSelectionModel(), &QItemSelectionModel::selectionChanged, this, &SelectionController::articleSelectionChanged);
0228     connect(m_articleLister->articleSelectionModel(), &QItemSelectionModel::selectionChanged, this, &SelectionController::articleSelectionChanged);
0229 
0230     m_articleLister->setScrollBarPositions(node->listViewScrollBarPositions());
0231 }
0232 
0233 void SelectionController::subscriptionDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
0234 {
0235     if (!Settings::autoExpandFolders()) {
0236         return;
0237     }
0238 
0239     if (!m_subscriptionModel) {
0240         qCCritical(AKREGATOR_LOG) << "m_subscriptionModel is NULL";
0241         return;
0242     }
0243 
0244     // need access to setExpanded
0245     auto tv = qobject_cast<QTreeView *>(m_feedSelector);
0246     if (!tv) {
0247         qCCritical(AKREGATOR_LOG) << "Unable to cast m_feedSelector to QTreeView";
0248         return;
0249     }
0250 
0251     int startRow = topLeft.row();
0252     int endRow = bottomRight.row();
0253     QModelIndex parent = topLeft.parent();
0254 
0255     for (int row = startRow; row <= endRow; ++row) {
0256         QModelIndex idx = m_subscriptionModel->index(row, 0, parent);
0257         QVariant v = m_subscriptionModel->data(idx, SubscriptionListModel::HasUnreadRole);
0258         if (!v.toBool()) {
0259             return;
0260         }
0261         tv->setExpanded(idx, true);
0262     }
0263 }
0264 
0265 void SelectionController::selectedSubscriptionChanged(const QModelIndex &index)
0266 {
0267     if (!index.isValid()) {
0268         return;
0269     }
0270 
0271     if (m_selectedSubscription && m_articleLister) {
0272         m_selectedSubscription->setListViewScrollBarPositions(m_articleLister->scrollBarPositions());
0273     }
0274 
0275     m_selectedSubscription = selectedSubscription();
0276     Q_EMIT currentSubscriptionChanged(m_selectedSubscription);
0277 
0278     // using a timer here internally to simulate async data fetching (which is still synchronous),
0279     // to ensure the UI copes with async behavior later on
0280 
0281     if (m_listJob) {
0282         m_listJob->disconnect(this); // Ignore if ~KJob() emits finished()
0283         delete m_listJob;
0284     }
0285 
0286     if (!m_selectedSubscription) {
0287         return;
0288     }
0289 
0290     auto const job(new ArticleListJob(m_selectedSubscription));
0291     connect(job, &KJob::finished, this, &SelectionController::articleHeadersAvailable);
0292     m_listJob = job;
0293     m_listJob->start();
0294 }
0295 
0296 void SelectionController::subscriptionContextMenuRequested(const QPoint &point)
0297 {
0298     Q_ASSERT(m_feedSelector);
0299     const TreeNode *const node = ::subscriptionForIndex(m_feedSelector->indexAt(point), m_feedList.data());
0300     if (!node) {
0301         return;
0302     }
0303 
0304     QWidget *w = ActionManager::getInstance()->container(node->isGroup() ? QStringLiteral("feedgroup_popup") : QStringLiteral("feeds_popup"));
0305     auto popup = qobject_cast<QMenu *>(w);
0306     if (popup) {
0307         const QPoint globalPos = m_feedSelector->viewport()->mapToGlobal(point);
0308         popup->exec(globalPos);
0309     }
0310 }
0311 
0312 void SelectionController::articleSelectionChanged()
0313 {
0314     const Akregator::Article article = currentArticle();
0315     if (m_singleDisplay) {
0316         m_singleDisplay->showArticle(article);
0317     }
0318     Q_EMIT currentArticleChanged(article);
0319 }
0320 
0321 void SelectionController::articleIndexDoubleClicked(const QModelIndex &index)
0322 {
0323     const Akregator::Article article = ::articleForIndex(index, m_feedList.data());
0324     Q_EMIT articleDoubleClicked(article);
0325 }
0326 
0327 /**
0328  * Called when the applications settings are changed; sets whether we apply a the filter or not.
0329  */
0330 void SelectionController::settingsChanged()
0331 {
0332     m_subscriptionModel->setDoFilter(Settings::hideReadFeeds());
0333 }
0334 
0335 void SelectionController::setFilters(const std::vector<QSharedPointer<const Filters::AbstractMatcher>> &matchers)
0336 {
0337     Q_ASSERT(m_articleLister);
0338     m_articleLister->setFilters(matchers);
0339 }
0340 
0341 void SelectionController::forceFilterUpdate()
0342 {
0343     Q_ASSERT(m_articleLister);
0344     m_articleLister->forceFilterUpdate();
0345 }
0346 
0347 #include "moc_selectioncontroller.cpp"