File indexing completed on 2024-12-22 05:01:13

0001 /*
0002  * SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "incompleteindexdialog.h"
0008 #include "kmail_debug.h"
0009 #include "kmkernel.h"
0010 #include "ui_incompleteindexdialog.h"
0011 
0012 #include <KDescendantsProxyModel>
0013 #include <KLocalizedString>
0014 #include <QAbstractItemView>
0015 #include <QProgressDialog>
0016 
0017 #include <Akonadi/EntityMimeTypeFilterModel>
0018 #include <Akonadi/EntityTreeModel>
0019 
0020 #include <PimCommon/PimUtil>
0021 #include <PimCommonAkonadi/MailUtil>
0022 
0023 #include <KConfigGroup>
0024 #include <KSharedConfig>
0025 #include <KWindowConfig>
0026 #include <QDBusInterface>
0027 #include <QDBusMetaType>
0028 #include <QDialogButtonBox>
0029 #include <QHBoxLayout>
0030 #include <QTimer>
0031 #include <QWindow>
0032 #include <chrono>
0033 
0034 using namespace std::chrono_literals;
0035 Q_DECLARE_METATYPE(Qt::CheckState)
0036 Q_DECLARE_METATYPE(QList<qint64>)
0037 
0038 class SearchCollectionProxyModel : public QSortFilterProxyModel
0039 {
0040 public:
0041     explicit SearchCollectionProxyModel(const QList<qint64> &unindexedCollections, QObject *parent = nullptr)
0042         : QSortFilterProxyModel(parent)
0043     {
0044         mFilterCollections.reserve(unindexedCollections.size());
0045         for (qint64 col : unindexedCollections) {
0046             mFilterCollections.insert(col, true);
0047         }
0048     }
0049 
0050     [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override
0051     {
0052         if (role == Qt::CheckStateRole) {
0053             if (index.isValid() && index.column() == 0) {
0054                 const qint64 colId = collectionIdForIndex(index);
0055                 return mFilterCollections.value(colId) ? Qt::Checked : Qt::Unchecked;
0056             }
0057         }
0058 
0059         return QSortFilterProxyModel::data(index, role);
0060     }
0061 
0062     bool setData(const QModelIndex &index, const QVariant &data, int role) override
0063     {
0064         if (role == Qt::CheckStateRole) {
0065             if (index.isValid() && index.column() == 0) {
0066                 const qint64 colId = collectionIdForIndex(index);
0067                 mFilterCollections[colId] = data.value<Qt::CheckState>();
0068                 return true;
0069             }
0070         }
0071 
0072         return QSortFilterProxyModel::setData(index, data, role);
0073     }
0074 
0075     [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override
0076     {
0077         if (index.isValid() && index.column() == 0) {
0078             return QSortFilterProxyModel::flags(index) | Qt::ItemIsUserCheckable;
0079         } else {
0080             return QSortFilterProxyModel::flags(index);
0081         }
0082     }
0083 
0084 protected:
0085     [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
0086     {
0087         const QModelIndex source_idx = sourceModel()->index(source_row, 0, source_parent);
0088         const qint64 colId = sourceModel()->data(source_idx, Akonadi::EntityTreeModel::CollectionIdRole).toLongLong();
0089         return mFilterCollections.contains(colId);
0090     }
0091 
0092 private:
0093     [[nodiscard]] qint64 collectionIdForIndex(const QModelIndex &index) const
0094     {
0095         return data(index, Akonadi::EntityTreeModel::CollectionIdRole).toLongLong();
0096     }
0097 
0098 private:
0099     QHash<qint64, bool> mFilterCollections;
0100 };
0101 
0102 namespace
0103 {
0104 static const char myIncompleteIndexDialogGroupName[] = "IncompleteIndexDialog";
0105 }
0106 
0107 IncompleteIndexDialog::IncompleteIndexDialog(const QList<qint64> &unindexedCollections, QWidget *parent)
0108     : QDialog(parent)
0109     , mUi(new Ui::IncompleteIndexDialog)
0110 {
0111     auto mainLayout = new QHBoxLayout(this);
0112     auto w = new QWidget(this);
0113     mainLayout->addWidget(w);
0114     qDBusRegisterMetaType<QList<qint64>>();
0115 
0116     mUi->setupUi(w);
0117 
0118     Akonadi::EntityTreeModel *etm = KMKernel::self()->entityTreeModel();
0119     auto mimeProxy = new Akonadi::EntityMimeTypeFilterModel(this);
0120     mimeProxy->addMimeTypeInclusionFilter(Akonadi::Collection::mimeType());
0121     mimeProxy->setSourceModel(etm);
0122 
0123     auto flatProxy = new KDescendantsProxyModel(this);
0124     flatProxy->setDisplayAncestorData(true);
0125     flatProxy->setAncestorSeparator(QStringLiteral(" / "));
0126     flatProxy->setSourceModel(mimeProxy);
0127 
0128     auto proxy = new SearchCollectionProxyModel(unindexedCollections, this);
0129     proxy->setSourceModel(flatProxy);
0130 
0131     mUi->collectionView->setModel(proxy);
0132 
0133     mUi->collectionView->setEditTriggers(QAbstractItemView::NoEditTriggers);
0134     connect(mUi->selectAllBtn, &QPushButton::clicked, this, &IncompleteIndexDialog::selectAll);
0135     connect(mUi->unselectAllBtn, &QPushButton::clicked, this, &IncompleteIndexDialog::unselectAll);
0136     mUi->buttonBox->button(QDialogButtonBox::Ok)->setText(i18n("Reindex"));
0137     mUi->buttonBox->button(QDialogButtonBox::Cancel)->setText(i18n("Search Anyway"));
0138     connect(mUi->buttonBox, &QDialogButtonBox::accepted, this, &IncompleteIndexDialog::waitForIndexer);
0139     connect(mUi->buttonBox, &QDialogButtonBox::rejected, this, &IncompleteIndexDialog::reject);
0140     readConfig();
0141 }
0142 
0143 IncompleteIndexDialog::~IncompleteIndexDialog()
0144 {
0145     writeConfig();
0146 }
0147 
0148 void IncompleteIndexDialog::readConfig()
0149 {
0150     create(); // ensure a window is created
0151     windowHandle()->resize(QSize(500, 400));
0152     KConfigGroup group(KSharedConfig::openStateConfig(), QLatin1StringView(myIncompleteIndexDialogGroupName));
0153     KWindowConfig::restoreWindowSize(windowHandle(), group);
0154     resize(windowHandle()->size()); // workaround for QTBUG-40584
0155 }
0156 
0157 void IncompleteIndexDialog::writeConfig()
0158 {
0159     KConfigGroup group(KSharedConfig::openStateConfig(), QLatin1StringView(myIncompleteIndexDialogGroupName));
0160     KWindowConfig::saveWindowSize(windowHandle(), group);
0161     group.sync();
0162 }
0163 
0164 void IncompleteIndexDialog::selectAll()
0165 {
0166     updateAllSelection(true);
0167 }
0168 
0169 void IncompleteIndexDialog::unselectAll()
0170 {
0171     updateAllSelection(false);
0172 }
0173 
0174 void IncompleteIndexDialog::updateAllSelection(bool select)
0175 {
0176     QAbstractItemModel *model = mUi->collectionView->model();
0177     for (int i = 0, cnt = model->rowCount(); i < cnt; ++i) {
0178         const QModelIndex idx = model->index(i, 0, QModelIndex());
0179         model->setData(idx, select ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
0180     }
0181 }
0182 
0183 QList<qlonglong> IncompleteIndexDialog::collectionsToReindex() const
0184 {
0185     QList<qlonglong> res;
0186 
0187     QAbstractItemModel *model = mUi->collectionView->model();
0188     for (int i = 0, cnt = model->rowCount(); i < cnt; ++i) {
0189         const QModelIndex idx = model->index(i, 0, QModelIndex());
0190         if (model->data(idx, Qt::CheckStateRole).toInt() == Qt::Checked) {
0191             res.push_back(model->data(idx, Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>().id());
0192         }
0193     }
0194 
0195     return res;
0196 }
0197 
0198 void IncompleteIndexDialog::waitForIndexer()
0199 {
0200     mIndexer = new QDBusInterface(PimCommon::MailUtil::indexerServiceName(),
0201                                   QStringLiteral("/"),
0202                                   QStringLiteral("org.freedesktop.Akonadi.Indexer"),
0203                                   QDBusConnection::sessionBus(),
0204                                   this);
0205 
0206     if (!mIndexer->isValid()) {
0207         qCWarning(KMAIL_LOG) << "Invalid indexer dbus interface ";
0208         accept();
0209         return;
0210     }
0211     mIndexingQueue = collectionsToReindex();
0212     if (mIndexingQueue.isEmpty()) {
0213         accept();
0214         return;
0215     }
0216 
0217     mProgressDialog = new QProgressDialog(this);
0218     mProgressDialog->setWindowTitle(i18nc("@title:window", "Indexing"));
0219     mProgressDialog->setMaximum(mIndexingQueue.size());
0220     mProgressDialog->setValue(0);
0221     mProgressDialog->setLabelText(i18n("Indexing Collections..."));
0222     connect(mProgressDialog, &QDialog::rejected, this, &IncompleteIndexDialog::slotStopIndexing);
0223 
0224     connect(mIndexer, SIGNAL(collectionIndexingFinished(qlonglong)), this, SLOT(slotCurrentlyIndexingCollectionChanged(qlonglong)));
0225 
0226     mIndexer->asyncCall(QStringLiteral("reindexCollections"), QVariant::fromValue(mIndexingQueue));
0227     mProgressDialog->show();
0228 }
0229 
0230 void IncompleteIndexDialog::slotStopIndexing()
0231 {
0232     mProgressDialog->close();
0233     reject();
0234 }
0235 
0236 void IncompleteIndexDialog::slotCurrentlyIndexingCollectionChanged(qlonglong colId)
0237 {
0238     const int idx = mIndexingQueue.indexOf(colId);
0239     if (idx > -1) {
0240         mIndexingQueue.removeAt(idx);
0241         mProgressDialog->setValue(mProgressDialog->maximum() - mIndexingQueue.size());
0242 
0243         if (mIndexingQueue.isEmpty()) {
0244             QTimer::singleShot(1s, this, &IncompleteIndexDialog::accept);
0245         }
0246     }
0247 }
0248 
0249 #include "moc_incompleteindexdialog.cpp"