File indexing completed on 2024-05-19 05:14:36

0001 /*
0002     SPDX-FileCopyrightText: 2014 Jonathan Marten <jjm@keelhaul.me.uk>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "categoryselectwidget.h"
0008 
0009 #include "kaddressbook_debug.h"
0010 #include <Akonadi/Monitor>
0011 #include <Akonadi/TagModel>
0012 #include <KLocalizedString>
0013 #include <QHBoxLayout>
0014 #include <QStandardItemModel>
0015 #include <QTimer>
0016 #include <QToolButton>
0017 
0018 #include <Libkdepim/KCheckComboBox>
0019 #include <chrono>
0020 using namespace std::chrono_literals;
0021 using namespace Akonadi;
0022 
0023 static const int FILTER_ROLE = Qt::UserRole + 1;
0024 
0025 class CategorySelectWidgetPrivate : public QObject
0026 {
0027     Q_OBJECT
0028     Q_DECLARE_PUBLIC(CategorySelectWidget)
0029 
0030 public:
0031     explicit CategorySelectWidgetPrivate(CategorySelectWidget *parent);
0032 
0033     Akonadi::TagModel *tagModel = nullptr;
0034     int rowOffset = 0;
0035     QTimer *updateTimer = nullptr;
0036     KPIM::KCheckComboBox *checkCombo = nullptr;
0037 
0038     void init();
0039     [[nodiscard]] QStandardItemModel *itemModel() const;
0040     void selectAll(Qt::CheckState state) const;
0041     [[nodiscard]] QList<Akonadi::Tag> filterTags() const;
0042 
0043 public Q_SLOTS:
0044     void slotSelectAll();
0045     void slotSelectNone();
0046 
0047     void slotTagsInserted(const QModelIndex &parent, int start, int end);
0048     void slotTagsRemoved(const QModelIndex &parent, int start, int end);
0049     void slotTagsChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
0050 
0051     void slotCheckedItemsChanged();
0052     void slotCheckedItemsTimer();
0053 
0054 private:
0055     CategorySelectWidget *const q_ptr;
0056 };
0057 
0058 CategorySelectWidgetPrivate::CategorySelectWidgetPrivate(CategorySelectWidget *parent)
0059     : QObject()
0060     , q_ptr(parent)
0061 {
0062 }
0063 
0064 void CategorySelectWidgetPrivate::init()
0065 {
0066     Q_Q(CategorySelectWidget);
0067 
0068     auto hbox = new QHBoxLayout(q);
0069     hbox->setSpacing(0);
0070     hbox->setContentsMargins({});
0071 
0072     checkCombo = new KPIM::KCheckComboBox;
0073     checkCombo->setMinimumWidth(150);
0074     checkCombo->setSqueezeText(true);
0075     connect(checkCombo, &KPIM::KCheckComboBox::checkedItemsChanged, this, &CategorySelectWidgetPrivate::slotCheckedItemsChanged);
0076     hbox->addWidget(checkCombo);
0077 
0078     auto monitor = new Monitor(this);
0079     monitor->setTypeMonitored(Monitor::Tags);
0080     tagModel = new Akonadi::TagModel(monitor, this);
0081 
0082     connect(tagModel, &QAbstractItemModel::rowsInserted, this, &CategorySelectWidgetPrivate::slotTagsInserted);
0083     connect(tagModel, &QAbstractItemModel::rowsRemoved, this, &CategorySelectWidgetPrivate::slotTagsRemoved);
0084     connect(tagModel, &QAbstractItemModel::dataChanged, this, &CategorySelectWidgetPrivate::slotTagsChanged);
0085 
0086     updateTimer = new QTimer(this);
0087     updateTimer->setSingleShot(true);
0088     updateTimer->setInterval(200ms);
0089     connect(updateTimer, &QTimer::timeout, this, &CategorySelectWidgetPrivate::slotCheckedItemsTimer);
0090 
0091     auto but = new QToolButton(q);
0092     but->setAutoRaise(true);
0093     but->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo")));
0094     but->setToolTip(i18nc("@action:button", "Reset category filter"));
0095     connect(but, &QToolButton::clicked, this, &CategorySelectWidgetPrivate::slotSelectAll);
0096     hbox->addWidget(but);
0097 
0098     but = new QToolButton(q);
0099     but->setAutoRaise(true);
0100     but->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear")));
0101     but->setToolTip(i18nc("@action:button", "Clear category filter"));
0102     connect(but, &QToolButton::clicked, this, &CategorySelectWidgetPrivate::slotSelectNone);
0103     hbox->addWidget(but);
0104 
0105     auto item = new QStandardItem(i18n("(Untagged)"));
0106     item->setCheckState(Qt::Checked);
0107     item->setData(CategorySelectWidget::FilterUntagged, FILTER_ROLE);
0108     itemModel()->appendRow(item);
0109 
0110     item = new QStandardItem(i18n("(Groups)"));
0111     item->setCheckState(Qt::Checked);
0112     item->setData(CategorySelectWidget::FilterGroups, FILTER_ROLE);
0113     itemModel()->appendRow(item);
0114 
0115     checkCombo->setAlwaysShowDefaultText(false);
0116     checkCombo->setDefaultText(i18n("(All)"));
0117 
0118     rowOffset = itemModel()->rowCount();
0119 }
0120 
0121 QStandardItemModel *CategorySelectWidgetPrivate::itemModel() const
0122 {
0123     auto m = qobject_cast<QStandardItemModel *>(checkCombo->model());
0124     Q_ASSERT(m != nullptr);
0125     return m;
0126 }
0127 
0128 void CategorySelectWidgetPrivate::slotTagsRemoved(const QModelIndex &parent, int start, int end)
0129 {
0130     itemModel()->removeRows(start + rowOffset, end + rowOffset, parent);
0131 }
0132 
0133 void CategorySelectWidgetPrivate::slotTagsChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
0134 {
0135     for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
0136         QStandardItem *it = itemModel()->item(row + rowOffset);
0137         Q_ASSERT(it != nullptr);
0138 
0139         QModelIndex idx = tagModel->index(row, 0);
0140         it->setText(tagModel->data(idx, TagModel::NameRole).toString());
0141         it->setIcon(tagModel->data(idx, Qt::DecorationRole).value<QIcon>());
0142         it->setData(tagModel->data(idx, TagModel::IdRole), FILTER_ROLE);
0143     }
0144 }
0145 
0146 void CategorySelectWidgetPrivate::slotTagsInserted(const QModelIndex &parent, int start, int end)
0147 {
0148     for (int row = start; row <= end; ++row) {
0149         QModelIndex idx = tagModel->index(row, 0, parent);
0150 #if 0
0151         qCDebug(KADDRESSBOOK_LOG) << "idx" << idx << "=" << tagModel->data(idx, Qt::DisplayRole).toString()
0152                                   << "name" << tagModel->data(idx, TagModel::NameRole).toString()
0153                                   << "tag" << tagModel->data(idx, TagModel::TagRole)
0154                                   << "id" << tagModel->data(idx, TagModel::IdRole).toInt();
0155 #endif
0156         auto it = new QStandardItem(tagModel->data(idx, TagModel::NameRole).toString());
0157         it->setIcon(tagModel->data(idx, Qt::DecorationRole).value<QIcon>());
0158         it->setData(tagModel->data(idx, TagModel::IdRole), FILTER_ROLE);
0159         it->setCheckState(Qt::Checked);
0160 
0161         // If a tag with a parent arrives from the model, we know that its parent
0162         // must already have arrived.  So there is no need for a list of pending
0163         // tags, as is required in Akonadi::TagModel.
0164         //
0165         // FIXME: not tested (no way to create hierarchical tags at present)
0166         if (parent != QModelIndex()) {
0167             const auto parentId = tagModel->data(idx, TagModel::IdRole).value<Tag::Id>();
0168             QModelIndexList matchList = itemModel()->match(itemModel()->index(0, 0), FILTER_ROLE, parentId, 1, Qt::MatchExactly | Qt::MatchRecursive);
0169             if (matchList.count() == 1) { // found the parent tag
0170                 QModelIndex parentIndex = matchList.at(0);
0171                 itemModel()->itemFromIndex(parentIndex)->appendRow(it);
0172             } else {
0173                 qCWarning(KADDRESSBOOK_LOG) << "Cannot find parent with ID" << parentId;
0174                 itemModel()->insertRow(row + rowOffset, it);
0175             }
0176         } else {
0177             itemModel()->insertRow(row + rowOffset, it);
0178         }
0179     }
0180 }
0181 
0182 void CategorySelectWidgetPrivate::selectAll(Qt::CheckState state) const
0183 {
0184     for (int row = 0; row < itemModel()->rowCount(); ++row) {
0185         QStandardItem *it = itemModel()->item(row);
0186         it->setCheckState(state);
0187     }
0188 }
0189 
0190 void CategorySelectWidgetPrivate::slotSelectAll()
0191 {
0192     selectAll(Qt::Checked);
0193 }
0194 
0195 void CategorySelectWidgetPrivate::slotSelectNone()
0196 {
0197     selectAll(Qt::Unchecked);
0198 }
0199 
0200 void CategorySelectWidgetPrivate::slotCheckedItemsChanged()
0201 {
0202     updateTimer->start();
0203 }
0204 
0205 void CategorySelectWidgetPrivate::slotCheckedItemsTimer()
0206 {
0207     Q_Q(CategorySelectWidget);
0208 
0209     bool allOn = true;
0210     for (int row = 0; row < itemModel()->rowCount(); ++row) {
0211         const QStandardItem *it = itemModel()->item(row);
0212         Qt::CheckState rowState = static_cast<Qt::CheckState>(it->data(Qt::CheckStateRole).toInt());
0213         if (rowState != Qt::Checked) {
0214             allOn = false;
0215             break;
0216         }
0217     }
0218 
0219     if (allOn) {
0220         checkCombo->setAlwaysShowDefaultText(true);
0221         checkCombo->setDefaultText(i18n("(All)"));
0222     } else {
0223         checkCombo->setAlwaysShowDefaultText(false);
0224         checkCombo->setDefaultText(i18n("(None)"));
0225     }
0226 
0227     const QStringList checkedList = checkCombo->checkedItems();
0228     if (!checkedList.isEmpty()) {
0229         checkCombo->setToolTip(i18n("<qt>Category filter: %1", checkedList.join(i18n(", "))));
0230     } else {
0231         checkCombo->setToolTip(QString());
0232     }
0233 
0234     Q_EMIT q->filterChanged(filterTags());
0235 }
0236 
0237 QList<Akonadi::Tag> CategorySelectWidgetPrivate::filterTags() const
0238 {
0239     QList<Tag> filter;
0240     bool allOn = true;
0241     for (int row = 0; row < itemModel()->rowCount(); ++row) {
0242         const QStandardItem *it = itemModel()->item(row);
0243         Q_ASSERT(it != nullptr);
0244         if (it->checkState() == Qt::Checked) {
0245             Tag tag(it->data(FILTER_ROLE).toInt());
0246             tag.setName(it->text());
0247             if (tag.id() != 0) {
0248                 filter.append(tag);
0249             }
0250         } else {
0251             allOn = false;
0252         }
0253     }
0254 
0255     if (allOn) {
0256         filter.clear();
0257         filter.append(Tag(CategorySelectWidget::FilterAll));
0258     }
0259 
0260     // qCDebug(KADDRESSBOOK_LOG) << "filter" << filter;
0261     return filter;
0262 }
0263 
0264 CategorySelectWidget::CategorySelectWidget(QWidget *parent)
0265     : QWidget(parent)
0266     , d_ptr(new CategorySelectWidgetPrivate(this))
0267 {
0268     Q_D(CategorySelectWidget);
0269     d->init();
0270 }
0271 
0272 CategorySelectWidget::~CategorySelectWidget() = default;
0273 
0274 QList<Akonadi::Tag> CategorySelectWidget::filterTags() const
0275 {
0276     Q_D(const CategorySelectWidget);
0277     return d->filterTags();
0278 }
0279 
0280 #include "categoryselectwidget.moc"
0281 
0282 #include "moc_categoryselectwidget.cpp"