diff --git a/configuration/ui/settings_general.ui b/configuration/ui/settings_general.ui --- a/configuration/ui/settings_general.ui +++ b/configuration/ui/settings_general.ui @@ -37,6 +37,13 @@ + + + + Hide feeds with no unread articles + + + diff --git a/interfaces/akregator.kcfg b/interfaces/akregator.kcfg --- a/interfaces/akregator.kcfg +++ b/interfaces/akregator.kcfg @@ -5,6 +5,11 @@ http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" > + + + Hides feeds with no unread articles + false + Show Quick Filter Bar @@ -87,7 +92,7 @@ Delete expired articles - + Do not save any articles @@ -164,17 +169,17 @@ Always show the tab bar, even when only one tab is open - false + false Show close buttons on tabs instead of icons - false + false Open a link which would normally open in a new window (external browser) in a new tab instead - false + false diff --git a/src/abstractselectioncontroller.h b/src/abstractselectioncontroller.h --- a/src/abstractselectioncontroller.h +++ b/src/abstractselectioncontroller.h @@ -121,6 +121,8 @@ public Q_SLOTS: + virtual void settingsChanged() = 0; + virtual void setFilters(const std::vector > &) = 0; virtual void forceFilterUpdate() = 0; diff --git a/src/actions/actionmanagerimpl.h b/src/actions/actionmanagerimpl.h --- a/src/actions/actionmanagerimpl.h +++ b/src/actions/actionmanagerimpl.h @@ -74,6 +74,7 @@ public Q_SLOTS: void slotNodeSelected(Akregator::TreeNode *node); + void slotSettingsChanged(); private Q_SLOTS: void slotServiceUrlSelected(PimCommon::ShareServiceUrlManager::ServiceType type); diff --git a/src/actions/actionmanagerimpl.cpp b/src/actions/actionmanagerimpl.cpp --- a/src/actions/actionmanagerimpl.cpp +++ b/src/actions/actionmanagerimpl.cpp @@ -124,6 +124,16 @@ QAction *mQuickSearchAction = nullptr; }; +void ActionManagerImpl::slotSettingsChanged() +{ + QAction *a = action(QStringLiteral("feed_hide_read")); + if (!a) { + qCCritical(AKREGATOR_LOG) << "Action not found"; + return; + } + a->setChecked(Settings::hideReadFeeds()); +} + void ActionManagerImpl::slotNodeSelected(TreeNode *node) { if (node != 0) { @@ -520,6 +530,12 @@ action->setText(i18n("Go Down in Tree")); connect(action, &QAction::triggered, subscriptionListView, &SubscriptionListView::slotItemDown); coll->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Down)); + + action = coll->addAction(QStringLiteral("feed_hide_read")); + action->setCheckable(true); + action->setText(i18n("Hide Read Feeds")); + action->setChecked(Settings::hideReadFeeds()); + connect(action, &QAction::triggered, subscriptionListView, &SubscriptionListView::slotSetHideReadFeeds); } void ActionManagerImpl::initTabWidget(TabWidget *tabWidget) diff --git a/src/data/akregator_part.rc b/src/data/akregator_part.rc --- a/src/data/akregator_part.rc +++ b/src/data/akregator_part.rc @@ -1,5 +1,5 @@ - + @@ -24,6 +24,8 @@ + + diff --git a/src/mainwidget.cpp b/src/mainwidget.cpp --- a/src/mainwidget.cpp +++ b/src/mainwidget.cpp @@ -120,6 +120,9 @@ m_displayingAboutPage = false; setFocusPolicy(Qt::StrongFocus); + connect(m_part, &Part::signalSettingsChanged, + m_actionManager, &ActionManagerImpl::slotSettingsChanged); + QVBoxLayout *lt = new QVBoxLayout(this); lt->setMargin(0); @@ -212,6 +215,9 @@ connect(m_searchBar, &SearchBar::signalSearch, m_selectionController, &AbstractSelectionController::setFilters); + connect(m_part, &Part::signalSettingsChanged, + m_selectionController, &AbstractSelectionController::settingsChanged); + FolderExpansionHandler *expansionHandler = new FolderExpansionHandler(this); connect(m_feedListView, &QTreeView::expanded, expansionHandler, &FolderExpansionHandler::itemExpanded); connect(m_feedListView, &QTreeView::collapsed, expansionHandler, &FolderExpansionHandler::itemCollapsed); diff --git a/src/selectioncontroller.h b/src/selectioncontroller.h --- a/src/selectioncontroller.h +++ b/src/selectioncontroller.h @@ -27,14 +27,16 @@ #include "abstractselectioncontroller.h" #include +#include class QModelIndex; class QPoint; class KJob; namespace Akregator { class ArticleListJob; +class FilterUnreadProxyModel; class SelectionController : public AbstractSelectionController { @@ -74,6 +76,9 @@ public Q_SLOTS: + //impl + void settingsChanged() override; + //impl void setFilters(const std::vector > &) override; @@ -90,11 +95,14 @@ private: + void setCurrentSubscriptionModel(); + QSharedPointer m_feedList; QPointer m_feedSelector; Akregator::ArticleLister *m_articleLister = nullptr; Akregator::SingleArticleDisplay *m_singleDisplay = nullptr; - Akregator::SubscriptionListModel *m_subscriptionModel = nullptr; + Akregator::FilterUnreadProxyModel *m_subscriptionModel = nullptr; + QAbstractItemModel *m_currentModel = nullptr; Akregator::FolderExpansionHandler *m_folderExpansionHandler = nullptr; Akregator::ArticleModel *m_articleModel = nullptr; QPointer m_selectedSubscription; diff --git a/src/selectioncontroller.cpp b/src/selectioncontroller.cpp --- a/src/selectioncontroller.cpp +++ b/src/selectioncontroller.cpp @@ -24,6 +24,7 @@ #include "selectioncontroller.h" +#include "akregatorconfig.h" #include "actionmanager.h" #include "article.h" #include "articlejobs.h" @@ -81,11 +82,13 @@ , m_feedSelector() , m_articleLister(0) , m_singleDisplay(0) - , m_subscriptionModel(new SubscriptionListModel(QSharedPointer(), this)) + , m_subscriptionModel(new FilterUnreadProxyModel(this)) , m_folderExpansionHandler(0) , m_articleModel(0) , m_selectedSubscription() { + m_subscriptionModel->setDoFilter(Settings::hideReadFeeds()); + m_subscriptionModel->setSourceModel(new SubscriptionListModel(QSharedPointer(), this)); } Akregator::SelectionController::~SelectionController() @@ -102,6 +105,7 @@ if (m_feedSelector) { m_feedSelector->disconnect(this); m_feedSelector->selectionModel()->disconnect(this); + m_feedSelector->selectionModel()->disconnect(m_subscriptionModel); } m_feedSelector = feedSelector; @@ -111,10 +115,12 @@ } m_feedSelector->setModel(m_subscriptionModel); + m_subscriptionModel->clearCache(); connect(m_feedSelector.data(), &QAbstractItemView::customContextMenuRequested, this, &SelectionController::subscriptionContextMenuRequested); connect(m_feedSelector->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelectionController::selectedSubscriptionChanged); connect(m_feedSelector.data(), &QAbstractItemView::activated, this, &SelectionController::selectedSubscriptionChanged); + connect(m_feedSelector->selectionModel(), &QItemSelectionModel::selectionChanged, m_subscriptionModel, &FilterUnreadProxyModel::selectionChanged); } void Akregator::SelectionController::setArticleLister(Akregator::ArticleLister *lister) @@ -175,8 +181,9 @@ } m_feedList = list; - std::unique_ptr oldModel(m_subscriptionModel); - m_subscriptionModel = new SubscriptionListModel(m_feedList, this); + SubscriptionListModel *m = qobject_cast(m_subscriptionModel->sourceModel()); + std::unique_ptr oldModel(m); + m_subscriptionModel->setSourceModel(new SubscriptionListModel(m_feedList, this)); if (m_folderExpansionHandler) { m_folderExpansionHandler->setFeedList(m_feedList); @@ -304,6 +311,14 @@ Q_EMIT articleDoubleClicked(article); } +/** + * Called when the applications settings are changed; sets whether we apply a the filter or not. + */ +void Akregator::SelectionController::settingsChanged() +{ + m_subscriptionModel->setDoFilter(Settings::hideReadFeeds()); +} + void SelectionController::setFilters(const std::vector > &matchers) { Q_ASSERT(m_articleLister); diff --git a/src/subscription/subscriptionlistmodel.h b/src/subscription/subscriptionlistmodel.h --- a/src/subscription/subscriptionlistmodel.h +++ b/src/subscription/subscriptionlistmodel.h @@ -27,6 +27,8 @@ #include "akregatorpart_export.h" #include +#include +#include #include @@ -36,6 +38,34 @@ class Folder; class TreeNode; +/** + * Filters feeds with unread counts. + */ +class FilterUnreadProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit FilterUnreadProxyModel(QObject* parent = nullptr); + + bool doFilter() const; + + void setDoFilter(bool v); + + void setSourceModel(QAbstractItemModel *src) override; + +public slots: + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + void clearCache(); + +private: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + typedef QSet SelectionHierarchy; + + bool m_doFilter; + SelectionHierarchy m_selectedHierarchy; +}; + class AKREGATORPART_EXPORT SubscriptionListModel : public QAbstractItemModel { Q_OBJECT @@ -83,8 +113,6 @@ bool setData(const QModelIndex &idx, const QVariant &value, int role = Qt::EditRole) override; - uint nodeIdForIndex(const QModelIndex &index) const; - private: QModelIndex indexForNode(const TreeNode *node) const; @@ -122,7 +150,7 @@ explicit FolderExpansionHandler(QObject *parent = nullptr); void setFeedList(const QSharedPointer &feedList); - void setModel(Akregator::SubscriptionListModel *model); + void setModel(QAbstractItemModel *model); public Q_SLOTS: void itemExpanded(const QModelIndex &index); @@ -133,7 +161,7 @@ private: QSharedPointer m_feedList; - SubscriptionListModel *m_model = nullptr; + QAbstractItemModel *m_model = nullptr; }; } // namespace Akregator diff --git a/src/subscription/subscriptionlistmodel.cpp b/src/subscription/subscriptionlistmodel.cpp --- a/src/subscription/subscriptionlistmodel.cpp +++ b/src/subscription/subscriptionlistmodel.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include @@ -49,6 +50,11 @@ #define AKREGATOR_TREENODE_MIMETYPE QStringLiteral("akregator/treenode-id") namespace { +static uint nodeIdForIndex(const QModelIndex &idx) +{ + return idx.isValid() ? idx.internalId() : 0; +} + static QString errorCodeToString(Syndication::ErrorCode err) { switch (err) { @@ -77,6 +83,90 @@ } } +Akregator::FilterUnreadProxyModel::FilterUnreadProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , m_doFilter(false) + , m_selectedHierarchy() +{ + setDynamicSortFilter(true); +} + +bool Akregator::FilterUnreadProxyModel::doFilter() const +{ + return m_doFilter; +} + +void Akregator::FilterUnreadProxyModel::setDoFilter(bool v) +{ + m_doFilter = v; + invalidateFilter(); +} + +void Akregator::FilterUnreadProxyModel::setSourceModel(QAbstractItemModel *src) +{ + clearCache(); + QSortFilterProxyModel::setSourceModel(src); +} + +bool Akregator::FilterUnreadProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (!m_doFilter) { + return true; + } + + QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + + if (m_selectedHierarchy.contains(idx)) + return true; + + QVariant v = idx.data(SubscriptionListModel::HasUnreadRole); + if (v.isNull()) + return true; + + return v.toBool(); +} + +/** + * This caches the hierarchy of the selected node. Its purpose is to allow + * feeds/folders with no unread content not to be filtered out immediately, + * which would occur otherwise (we'd select the last article to read, it would + * become unread, and disappear from the list without letting us view it). + **/ +void Akregator::FilterUnreadProxyModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + QModelIndexList desel = mapSelectionToSource(deselected).indexes(); + //calling invalidateFilter causes refiltering at the call point, so we should + //call it ONLY after we recreate our node cache + bool doInvalidate = false; + + //if we're deselecting an empty feed/folder, we need to hide it + if (!desel.isEmpty()) { + if (m_selectedHierarchy.contains(desel.at(0))) { + doInvalidate = true; + } + } + + clearCache(); + + QModelIndexList sel = mapSelectionToSource(selected).indexes(); + if (!sel.isEmpty()) { + //XXX add support for multiple selections? this doesn't generally make sense in this case honestly + QModelIndex current = sel.at(0); + while (current.isValid()) { + m_selectedHierarchy.insert(current); + current = current.parent(); + } + } + + if (doInvalidate && doFilter()) + invalidateFilter(); +} + +void Akregator::FilterUnreadProxyModel::clearCache() +{ + m_selectedHierarchy.clear(); +} + Akregator::SubscriptionListModel::SubscriptionListModel(const QSharedPointer &feedList, QObject *parent) : QAbstractItemModel(parent) , m_feedList(feedList) , m_beganRemoval(false) @@ -115,11 +205,6 @@ return node ? node->children().count() : 0; } -uint Akregator::SubscriptionListModel::nodeIdForIndex(const QModelIndex &idx) const -{ - return idx.isValid() ? idx.internalId() : 0; -} - QVariant Akregator::SubscriptionListModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { @@ -343,7 +428,7 @@ if (!m_feedList || !m_model) { return; } - Akregator::TreeNode *const node = m_feedList->findByID(m_model->nodeIdForIndex(idx)); + Akregator::TreeNode *const node = m_feedList->findByID(nodeIdForIndex(idx)); if (!node || !node->isGroup()) { return; } @@ -359,7 +444,7 @@ { } -void FolderExpansionHandler::setModel(Akregator::SubscriptionListModel *model) +void FolderExpansionHandler::setModel(QAbstractItemModel *model) { m_model = model; } diff --git a/src/subscription/subscriptionlistview.h b/src/subscription/subscriptionlistview.h --- a/src/subscription/subscriptionlistview.h +++ b/src/subscription/subscriptionlistview.h @@ -72,6 +72,8 @@ void slotItemUp(); void slotItemDown(); + void slotSetHideReadFeeds(bool setting); + Q_SIGNALS: void userActionTakingPlace(); diff --git a/src/subscription/subscriptionlistview.cpp b/src/subscription/subscriptionlistview.cpp --- a/src/subscription/subscriptionlistview.cpp +++ b/src/subscription/subscriptionlistview.cpp @@ -26,6 +26,7 @@ #include "subscriptionlistmodel.h" #include "subscriptionlistdelegate.h" #include "akregatorconfig.h" +#include "akregator_debug.h" #include #include @@ -387,6 +388,23 @@ setCurrentIndex(current.sibling(current.row() + 1, current.column())); } +void SubscriptionListView::slotSetHideReadFeeds(bool setting) +{ + QAbstractItemModel *m = model(); + if (!m) { + return; + } + + FilterUnreadProxyModel *filter = qobject_cast(m); + if (!filter) { + qCCritical(AKREGATOR_LOG) << "Unable to cast model to FilterUnreadProxyModel*"; + return; + } + + Settings::setHideReadFeeds(setting); + filter->setDoFilter(setting); +} + void Akregator::SubscriptionListView::ensureNodeVisible(Akregator::TreeNode *) { }