File indexing completed on 2024-05-05 05:13:11

0001 /*
0002     This file is part of Akregator.
0003 
0004     SPDX-FileCopyrightText: 2007 Frank Osterfeld <frank.osterfeld@kdemail.net>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0007 */
0008 
0009 #include "subscriptionlistview.h"
0010 #include "akregator_debug.h"
0011 #include "akregatorconfig.h"
0012 #include "subscriptionlistdelegate.h"
0013 #include "subscriptionlistmodel.h"
0014 
0015 #include <QHeaderView>
0016 #include <QPointer>
0017 #include <QQueue>
0018 #include <QStack>
0019 
0020 #include <KConfigGroup>
0021 #include <KLocalizedString>
0022 #include <QMenu>
0023 
0024 using namespace Akregator;
0025 
0026 static QModelIndex prevIndex(const QModelIndex &idx)
0027 {
0028     if (!idx.isValid()) {
0029         return {};
0030     }
0031     const QAbstractItemModel *const model = idx.model();
0032     Q_ASSERT(model);
0033 
0034     if (idx.row() > 0) {
0035         QModelIndex i = idx.sibling(idx.row() - 1, idx.column());
0036         while (model->hasChildren(i)) {
0037             i = model->index(model->rowCount(i) - 1, i.column(), i);
0038         }
0039         return i;
0040     } else {
0041         return idx.parent();
0042     }
0043 }
0044 
0045 static QModelIndex prevFeedIndex(const QModelIndex &idx, bool allowPassed = false)
0046 {
0047     QModelIndex prev = allowPassed ? idx : prevIndex(idx);
0048     while (prev.isValid() && prev.data(SubscriptionListModel::IsAggregationRole).toBool()) {
0049         prev = prevIndex(prev);
0050     }
0051     return prev;
0052 }
0053 
0054 static QModelIndex prevUnreadFeedIndex(const QModelIndex &idx, bool allowPassed = false)
0055 {
0056     QModelIndex prev = allowPassed ? idx : prevIndex(idx);
0057     while (prev.isValid()
0058            && (prev.data(SubscriptionListModel::IsAggregationRole).toBool()
0059                || prev.sibling(prev.row(), SubscriptionListModel::UnreadCountColumn).data().toInt() == 0)) {
0060         prev = prevIndex(prev);
0061     }
0062     return prev;
0063 }
0064 
0065 static QModelIndex lastLeaveChild(const QAbstractItemModel *const model)
0066 {
0067     Q_ASSERT(model);
0068     if (model->rowCount() == 0) {
0069         return {};
0070     }
0071     QModelIndex idx = model->index(model->rowCount() - 1, 0);
0072     while (model->hasChildren(idx)) {
0073         idx = model->index(model->rowCount(idx) - 1, idx.column(), idx);
0074     }
0075     return idx;
0076 }
0077 
0078 static QModelIndex nextIndex(const QModelIndex &idx)
0079 {
0080     if (!idx.isValid()) {
0081         return {};
0082     }
0083     const QAbstractItemModel *const model = idx.model();
0084     Q_ASSERT(model);
0085     if (model->hasChildren(idx)) {
0086         return model->index(0, idx.column(), idx);
0087     }
0088     QModelIndex i = idx;
0089     while (true) {
0090         if (!i.isValid()) {
0091             return i;
0092         }
0093         const int siblings = model->rowCount(i.parent());
0094         if (i.row() + 1 < siblings) {
0095             return i.sibling(i.row() + 1, i.column());
0096         }
0097         i = i.parent();
0098     }
0099 }
0100 
0101 static QModelIndex nextFeedIndex(const QModelIndex &idx)
0102 {
0103     QModelIndex next = nextIndex(idx);
0104     while (next.isValid() && next.data(SubscriptionListModel::IsAggregationRole).toBool()) {
0105         next = nextIndex(next);
0106     }
0107     return next;
0108 }
0109 
0110 static QModelIndex nextUnreadFeedIndex(const QModelIndex &idx)
0111 {
0112     QModelIndex next = nextIndex(idx);
0113     while (next.isValid()
0114            && (next.data(SubscriptionListModel::IsAggregationRole).toBool()
0115                || next.sibling(next.row(), SubscriptionListModel::UnreadCountColumn).data().toInt() == 0)) {
0116         next = nextIndex(next);
0117     }
0118     return next;
0119 }
0120 
0121 Akregator::SubscriptionListView::SubscriptionListView(QWidget *parent)
0122     : QTreeView(parent)
0123 {
0124     setFocusPolicy(Qt::NoFocus);
0125     setSelectionMode(QAbstractItemView::SingleSelection);
0126     setRootIsDecorated(false);
0127     setAlternatingRowColors(true);
0128     setContextMenuPolicy(Qt::CustomContextMenu);
0129     setDragDropMode(Settings::lockFeedsInPlace() ? QAbstractItemView::NoDragDrop : QAbstractItemView::DragDrop);
0130     setDropIndicatorShown(true);
0131     setAcceptDrops(true);
0132     setUniformRowHeights(true);
0133     setItemDelegate(new SubscriptionListDelegate(this));
0134     connect(header(), &QWidget::customContextMenuRequested, this, &SubscriptionListView::showHeaderMenu);
0135 
0136     loadHeaderSettings();
0137 }
0138 
0139 Akregator::SubscriptionListView::~SubscriptionListView()
0140 {
0141     saveHeaderSettings();
0142 }
0143 
0144 void Akregator::SubscriptionListView::setModel(QAbstractItemModel *m)
0145 {
0146     Q_ASSERT(m);
0147 
0148     if (model()) {
0149         m_headerState = header()->saveState();
0150     }
0151 
0152     QTreeView::setModel(m);
0153 
0154     restoreHeaderState();
0155 
0156     QStack<QModelIndex> stack;
0157     stack.push(rootIndex());
0158     while (!stack.isEmpty()) {
0159         const QModelIndex i = stack.pop();
0160         const int childCount = m->rowCount(i);
0161         for (int j = 0; j < childCount; ++j) {
0162             const QModelIndex child = m->index(j, 0, i);
0163             if (child.isValid()) {
0164                 stack.push(child);
0165             }
0166         }
0167         setExpanded(i, i.data(Akregator::SubscriptionListModel::IsOpenRole).toBool());
0168     }
0169 
0170     header()->setContextMenuPolicy(Qt::CustomContextMenu);
0171 }
0172 
0173 void Akregator::SubscriptionListView::showHeaderMenu(const QPoint &pos)
0174 {
0175     if (!model()) {
0176         return;
0177     }
0178 
0179     QPointer<QMenu> menu = new QMenu(this);
0180     menu->setTitle(i18n("Columns"));
0181     menu->setAttribute(Qt::WA_DeleteOnClose);
0182     connect(menu.data(), &QMenu::triggered, this, &SubscriptionListView::headerMenuItemTriggered);
0183 
0184     for (int i = 0; i < model()->columnCount(); ++i) {
0185         if (SubscriptionListModel::TitleColumn == i) {
0186             continue;
0187         }
0188         QString col = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
0189         QAction *act = menu->addAction(col);
0190         act->setCheckable(true);
0191         act->setChecked(!header()->isSectionHidden(i));
0192         act->setData(i);
0193     }
0194 
0195     menu->popup(header()->mapToGlobal(pos));
0196 }
0197 
0198 void Akregator::SubscriptionListView::headerMenuItemTriggered(QAction *act)
0199 {
0200     Q_ASSERT(act);
0201     const int col = act->data().toInt();
0202     if (act->isChecked()) {
0203         header()->showSection(col);
0204     } else {
0205         header()->hideSection(col);
0206     }
0207 }
0208 
0209 void Akregator::SubscriptionListView::saveHeaderSettings()
0210 {
0211     if (model()) {
0212         m_headerState = header()->saveState();
0213     }
0214     KConfigGroup conf(Settings::self()->config(), QStringLiteral("General"));
0215     conf.writeEntry("SubscriptionListHeaders", m_headerState.toBase64());
0216 }
0217 
0218 void Akregator::SubscriptionListView::loadHeaderSettings()
0219 {
0220     const KConfigGroup conf(Settings::self()->config(), QStringLiteral("General"));
0221     m_headerState = QByteArray::fromBase64(conf.readEntry("SubscriptionListHeaders").toLatin1());
0222     restoreHeaderState();
0223 }
0224 
0225 void Akregator::SubscriptionListView::restoreHeaderState()
0226 {
0227     header()->restoreState(m_headerState); // needed, even with Qt 4.5
0228     // Always shows the title column
0229     header()->showSection(SubscriptionListModel::TitleColumn);
0230     if (m_headerState.isEmpty()) {
0231         // Default configuration: only show the title column
0232         header()->hideSection(SubscriptionListModel::UnreadCountColumn);
0233         header()->hideSection(SubscriptionListModel::TotalCountColumn);
0234     }
0235 }
0236 
0237 void Akregator::SubscriptionListView::slotPrevFeed()
0238 {
0239     if (!model()) {
0240         return;
0241     }
0242     const QModelIndex current = currentIndex();
0243     QModelIndex prev = prevFeedIndex(current);
0244     if (!prev.isValid()) {
0245         prev = prevFeedIndex(lastLeaveChild(model()), true);
0246     }
0247     if (prev.isValid()) {
0248         setCurrentIndex(prev);
0249     }
0250 }
0251 
0252 void Akregator::SubscriptionListView::slotNextFeed()
0253 {
0254     if (!model()) {
0255         return;
0256     }
0257     Q_EMIT userActionTakingPlace();
0258     const QModelIndex current = currentIndex();
0259     QModelIndex next = nextFeedIndex(current);
0260     if (!next.isValid()) {
0261         next = nextFeedIndex(model()->index(0, 0));
0262     }
0263     if (next.isValid()) {
0264         setCurrentIndex(next);
0265     }
0266 }
0267 
0268 void Akregator::SubscriptionListView::slotPrevUnreadFeed()
0269 {
0270     if (!model()) {
0271         return;
0272     }
0273     Q_EMIT userActionTakingPlace();
0274     const QModelIndex current = currentIndex();
0275     QModelIndex prev = prevUnreadFeedIndex(current);
0276     if (!prev.isValid()) {
0277         prev = prevUnreadFeedIndex(lastLeaveChild(model()), true);
0278     }
0279     if (prev.isValid()) {
0280         setCurrentIndex(prev);
0281     }
0282 }
0283 
0284 void Akregator::SubscriptionListView::slotNextUnreadFeed()
0285 {
0286     if (!model()) {
0287         return;
0288     }
0289     Q_EMIT userActionTakingPlace();
0290     const QModelIndex current = currentIndex();
0291     QModelIndex next = nextUnreadFeedIndex(current);
0292     if (!next.isValid()) {
0293         next = nextUnreadFeedIndex(model()->index(0, 0));
0294     }
0295     if (next.isValid()) {
0296         setCurrentIndex(next);
0297     }
0298 }
0299 
0300 void SubscriptionListView::slotItemBegin()
0301 {
0302     if (!model()) {
0303         return;
0304     }
0305     Q_EMIT userActionTakingPlace();
0306     setCurrentIndex(nextFeedIndex(model()->index(0, 0)));
0307 }
0308 
0309 void SubscriptionListView::slotItemEnd()
0310 {
0311     if (!model()) {
0312         return;
0313     }
0314     Q_EMIT userActionTakingPlace();
0315     setCurrentIndex(lastLeaveChild(model()));
0316 }
0317 
0318 void SubscriptionListView::slotItemLeft()
0319 {
0320     if (!model()) {
0321         return;
0322     }
0323     Q_EMIT userActionTakingPlace();
0324     const QModelIndex current = currentIndex();
0325     if (!current.isValid()) {
0326         setCurrentIndex(nextFeedIndex(model()->index(0, 0)));
0327         return;
0328     }
0329     if (current.parent().isValid()) {
0330         setCurrentIndex(current.parent());
0331     }
0332 }
0333 
0334 void SubscriptionListView::slotItemRight()
0335 {
0336     if (!model()) {
0337         return;
0338     }
0339     Q_EMIT userActionTakingPlace();
0340     const QModelIndex current = currentIndex();
0341     if (!current.isValid()) {
0342         setCurrentIndex(nextFeedIndex(model()->index(0, 0)));
0343         return;
0344     }
0345     if (model()->rowCount(current) > 0) {
0346         setCurrentIndex(model()->index(0, 0, current));
0347     }
0348 }
0349 
0350 void SubscriptionListView::slotItemUp()
0351 {
0352     if (!model()) {
0353         return;
0354     }
0355     Q_EMIT userActionTakingPlace();
0356     const QModelIndex current = currentIndex();
0357     QModelIndex prev = current.row() > 0 ? current.sibling(current.row() - 1, current.column()) : current.parent();
0358     if (!prev.isValid()) {
0359         prev = lastLeaveChild(model());
0360     }
0361     if (prev.isValid()) {
0362         setCurrentIndex(prev);
0363     }
0364 }
0365 
0366 void SubscriptionListView::slotItemDown()
0367 {
0368     if (!model()) {
0369         return;
0370     }
0371     Q_EMIT userActionTakingPlace();
0372     const QModelIndex current = currentIndex();
0373     if (current.row() >= model()->rowCount(current.parent())) {
0374         return;
0375     }
0376     setCurrentIndex(current.sibling(current.row() + 1, current.column()));
0377 }
0378 
0379 void SubscriptionListView::slotSetHideReadFeeds(bool setting)
0380 {
0381     QAbstractItemModel *m = model();
0382     if (!m) {
0383         return;
0384     }
0385 
0386     auto filter = qobject_cast<FilterUnreadProxyModel *>(m);
0387     if (!filter) {
0388         qCCritical(AKREGATOR_LOG) << "Unable to cast model to FilterUnreadProxyModel*";
0389         return;
0390     }
0391 
0392     Settings::setHideReadFeeds(setting);
0393     filter->setDoFilter(setting);
0394 }
0395 
0396 void SubscriptionListView::slotSetLockFeedsInPlace(bool setting)
0397 {
0398     if (!model()) {
0399         return;
0400     }
0401 
0402     setDragDropMode(setting ? QAbstractItemView::NoDragDrop : QAbstractItemView::DragDrop);
0403 
0404     Settings::setLockFeedsInPlace(setting);
0405 }
0406 
0407 void Akregator::SubscriptionListView::slotSetAutoExpandFolders(bool setting)
0408 {
0409     Settings::setAutoExpandFolders(setting);
0410     if (!setting) {
0411         return;
0412     }
0413 
0414     // expand any current subscriptions with unread items
0415     QQueue<QModelIndex> indexes;
0416     // start at the root node
0417     indexes.enqueue(QModelIndex());
0418 
0419     QAbstractItemModel *m = model();
0420     if (!m) {
0421         return;
0422     }
0423 
0424     while (!indexes.isEmpty()) {
0425         QModelIndex parent = indexes.dequeue();
0426         int rows = m->rowCount(parent);
0427 
0428         for (int row = 0; row < rows; ++row) {
0429             QModelIndex current = m->index(row, 0, parent);
0430 
0431             if (m->hasChildren(current)) {
0432                 indexes.enqueue(current);
0433             }
0434 
0435             if (!m->data(current, SubscriptionListModel::HasUnreadRole).toBool()) {
0436                 continue;
0437             }
0438 
0439             setExpanded(current, true);
0440         }
0441     }
0442 }
0443 
0444 void Akregator::SubscriptionListView::ensureNodeVisible(Akregator::TreeNode *)
0445 {
0446 }
0447 
0448 void Akregator::SubscriptionListView::startNodeRenaming(Akregator::TreeNode *node)
0449 {
0450     Q_UNUSED(node)
0451     const QModelIndex current = currentIndex();
0452     if (!current.isValid()) {
0453         return;
0454     }
0455     edit(current);
0456 }
0457 
0458 #include "moc_subscriptionlistview.cpp"