File indexing completed on 2025-01-05 04:58:21

0001 /*
0002   SPDX-FileCopyrightText: 2015-2024 Laurent Montel <montel@kde.org>
0003 
0004   SPDX-License-Identifier: LGPL-2.0-or-later
0005 
0006 */
0007 
0008 #include "completionorderwidget.h"
0009 
0010 #include <KDescendantsProxyModel>
0011 #include <KLDAPWidgets/LdapClient>
0012 #include <KLDAPWidgets/LdapClientSearch>
0013 
0014 #include <KContacts/Addressee>
0015 #include <KContacts/ContactGroup>
0016 #include <KLDAPWidgets/LdapClientSearchConfig>
0017 
0018 #include <Akonadi/ChangeRecorder>
0019 #include <Akonadi/CollectionFilterProxyModel>
0020 #include <Akonadi/EntityTreeModel>
0021 #include <Akonadi/Monitor>
0022 
0023 #include <kldapcore/ldapserver.h>
0024 
0025 #include <KConfigGroup>
0026 #include <KLocalizedString>
0027 #include <QDBusConnection>
0028 #include <QHBoxLayout>
0029 #include <QPushButton>
0030 #include <QTreeWidget>
0031 #include <QTreeWidgetItem>
0032 #include <QVBoxLayout>
0033 
0034 using namespace PimCommon;
0035 
0036 CompletionOrderEditorAdaptor::CompletionOrderEditorAdaptor(QObject *parent)
0037     : QDBusAbstractAdaptor(parent)
0038 {
0039     setAutoRelaySignals(true);
0040 }
0041 
0042 class LDAPCompletionItem : public CompletionItem
0043 {
0044 public:
0045     explicit LDAPCompletionItem(KLDAPWidgets::LdapClient *ldapClient)
0046         : mLdapClient(ldapClient)
0047     {
0048         mWeight = mLdapClient->completionWeight();
0049     }
0050 
0051     [[nodiscard]] QString label() const override
0052     {
0053         return i18n("LDAP server %1", mLdapClient->server().host());
0054     }
0055 
0056     [[nodiscard]] QIcon icon() const override
0057     {
0058         return QIcon::fromTheme(QStringLiteral("kmail"));
0059     }
0060 
0061     [[nodiscard]] int completionWeight() const override
0062     {
0063         return mWeight;
0064     }
0065 
0066     void save(CompletionOrderWidget *) override
0067     {
0068         KConfig *config = KLDAPWidgets::LdapClientSearchConfig::config();
0069         KConfigGroup group(config, QStringLiteral("LDAP"));
0070         group.writeEntry(QStringLiteral("SelectedCompletionWeight%1").arg(mLdapClient->clientNumber()), mWeight);
0071         group.sync();
0072     }
0073 
0074     [[nodiscard]] bool hasEnableSupport() const override
0075     {
0076         return false;
0077     }
0078 
0079     [[nodiscard]] bool isEnabled() const override
0080     {
0081         return true;
0082     }
0083 
0084     void setIsEnabled(bool b) override
0085     {
0086         Q_UNUSED(b)
0087     }
0088 
0089 protected:
0090     void setCompletionWeight(int weight) override
0091     {
0092         mWeight = weight;
0093     }
0094 
0095 private:
0096     KLDAPWidgets::LdapClient *mLdapClient = nullptr;
0097     int mWeight;
0098 };
0099 
0100 class SimpleCompletionItem : public CompletionItem
0101 {
0102 public:
0103     SimpleCompletionItem(CompletionOrderWidget *editor, const QString &label, const QString &identifier, int weight, bool enableSupport = false)
0104         : mLabel(label)
0105         , mIdentifier(identifier)
0106         , mHasEnableSupport(enableSupport)
0107         , mEnabled(true)
0108     {
0109         KConfigGroup groupCompletionWeights(editor->configFile(), QStringLiteral("CompletionWeights"));
0110         mWeight = groupCompletionWeights.readEntry(mIdentifier, weight);
0111         if (mHasEnableSupport) {
0112             KConfigGroup groupEnabled(editor->configFile(), QStringLiteral("CompletionEnabled"));
0113             mEnabled = groupEnabled.readEntry(mIdentifier, true);
0114         }
0115     }
0116 
0117     ~SimpleCompletionItem() override = default;
0118 
0119     [[nodiscard]] bool isEnabled() const override
0120     {
0121         return mEnabled;
0122     }
0123 
0124     [[nodiscard]] bool hasEnableSupport() const override
0125     {
0126         return mHasEnableSupport;
0127     }
0128 
0129     void setIcon(const QIcon &icon)
0130     {
0131         mIcon = icon;
0132     }
0133 
0134     [[nodiscard]] QString label() const override
0135     {
0136         return mLabel;
0137     }
0138 
0139     [[nodiscard]] QIcon icon() const override
0140     {
0141         return mIcon;
0142     }
0143 
0144     [[nodiscard]] int completionWeight() const override
0145     {
0146         return mWeight;
0147     }
0148 
0149     void setIsEnabled(bool b) override
0150     {
0151         mEnabled = b;
0152     }
0153 
0154     void save(CompletionOrderWidget *editor) override
0155     {
0156         KConfigGroup group(editor->configFile(), QStringLiteral("CompletionWeights"));
0157         group.writeEntry(mIdentifier, mWeight);
0158         if (mHasEnableSupport) {
0159             KConfigGroup groupEnabled(editor->configFile(), QStringLiteral("CompletionEnabled"));
0160             groupEnabled.writeEntry(mIdentifier, isEnabled());
0161         }
0162     }
0163 
0164 protected:
0165     void setCompletionWeight(int weight) override
0166     {
0167         mWeight = weight;
0168     }
0169 
0170 private:
0171     QString mLabel;
0172     QString mIdentifier;
0173     int mWeight;
0174     QIcon mIcon;
0175     bool mHasEnableSupport;
0176     bool mEnabled;
0177 };
0178 
0179 /////////
0180 
0181 class CompletionViewItem : public QTreeWidgetItem
0182 {
0183 public:
0184     CompletionViewItem(QTreeWidget *parent, CompletionItem *item)
0185         : QTreeWidgetItem(parent)
0186     {
0187         setItem(item);
0188     }
0189 
0190     ~CompletionViewItem() override
0191     {
0192         delete mItem;
0193     }
0194 
0195     void setItem(CompletionItem *item)
0196     {
0197         mItem = item;
0198         setText(0, mItem->label());
0199         setIcon(0, mItem->icon());
0200         if (mItem->hasEnableSupport()) {
0201             setFlags(flags() | Qt::ItemIsUserCheckable);
0202             setCheckState(0, mItem->isEnabled() ? Qt::Checked : Qt::Unchecked);
0203         } else {
0204             setFlags(flags() & ~Qt::ItemIsUserCheckable);
0205         }
0206     }
0207 
0208     [[nodiscard]] CompletionItem *item() const
0209     {
0210         return mItem;
0211     }
0212 
0213     bool operator<(const QTreeWidgetItem &other) const override
0214     {
0215         const QTreeWidgetItem *otherItem = &other;
0216         const auto completionItem = static_cast<const CompletionViewItem *>(otherItem);
0217         // item with weight 100 should be on the top -> reverse sorting
0218         return mItem->completionWeight() > completionItem->item()->completionWeight();
0219     }
0220 
0221 private:
0222     CompletionItem *mItem = nullptr;
0223 };
0224 
0225 CompletionOrderWidget::CompletionOrderWidget(QWidget *parent)
0226     : QWidget(parent)
0227     , mConfig(QStringLiteral("kpimcompletionorder"))
0228 {
0229     new CompletionOrderEditorAdaptor(this);
0230     QDBusConnection::sessionBus().registerObject(QStringLiteral("/"), this, QDBusConnection::ExportAdaptors);
0231 
0232     auto hbox = new QHBoxLayout(this);
0233 
0234     auto page = new QWidget(this);
0235     auto pageHBoxLayout = new QHBoxLayout(page);
0236     pageHBoxLayout->setContentsMargins({});
0237     hbox->addWidget(page);
0238     mListView = new QTreeWidget(page);
0239     mListView->setObjectName(QLatin1StringView("listview"));
0240 
0241     pageHBoxLayout->addWidget(mListView);
0242     mListView->setColumnCount(1);
0243     mListView->setAlternatingRowColors(true);
0244     mListView->setIndentation(0);
0245     mListView->setAllColumnsShowFocus(true);
0246     mListView->setHeaderHidden(true);
0247     mListView->setSortingEnabled(true);
0248 
0249     auto upDownBox = new QWidget(page);
0250     auto upDownBoxVBoxLayout = new QVBoxLayout(upDownBox);
0251     upDownBoxVBoxLayout->setContentsMargins({});
0252     pageHBoxLayout->addWidget(upDownBox);
0253     mUpButton = new QPushButton(upDownBox);
0254     upDownBoxVBoxLayout->addWidget(mUpButton);
0255     mUpButton->setAutoRepeat(true);
0256     mUpButton->setObjectName(QLatin1StringView("mUpButton"));
0257     mUpButton->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
0258     mUpButton->setEnabled(false); // b/c no item is selected yet
0259     mUpButton->setToolTip(i18n("Move Up"));
0260     mUpButton->setFocusPolicy(Qt::StrongFocus);
0261 
0262     mDownButton = new QPushButton(upDownBox);
0263     upDownBoxVBoxLayout->addWidget(mDownButton);
0264     mDownButton->setAutoRepeat(true);
0265     mDownButton->setObjectName(QLatin1StringView("mDownButton"));
0266     mDownButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down")));
0267     mDownButton->setEnabled(false); // b/c no item is selected yet
0268     mDownButton->setToolTip(i18n("Move Down"));
0269     mDownButton->setFocusPolicy(Qt::StrongFocus);
0270 
0271     auto spacer = new QWidget(upDownBox);
0272     upDownBoxVBoxLayout->addWidget(spacer);
0273     upDownBoxVBoxLayout->setStretchFactor(spacer, 100);
0274 
0275     connect(mListView, &QTreeWidget::itemSelectionChanged, this, &CompletionOrderWidget::slotSelectionChanged);
0276     connect(mListView, &QTreeWidget::currentItemChanged, this, &CompletionOrderWidget::slotSelectionChanged);
0277     connect(mListView, &QTreeWidget::itemChanged, this, &CompletionOrderWidget::slotItemChanged);
0278     connect(mUpButton, &QAbstractButton::clicked, this, &CompletionOrderWidget::slotMoveUp);
0279     connect(mDownButton, &QAbstractButton::clicked, this, &CompletionOrderWidget::slotMoveDown);
0280 }
0281 
0282 CompletionOrderWidget::~CompletionOrderWidget() = default;
0283 
0284 void CompletionOrderWidget::save()
0285 {
0286     if (mDirty) {
0287         int w = 100;
0288         // Clean up order
0289         KConfigGroup group(configFile(), QStringLiteral("CompletionWeights"));
0290         group.deleteGroup(QLatin1StringView());
0291 
0292         for (int itemIndex = 0; itemIndex < mListView->topLevelItemCount(); ++itemIndex) {
0293             auto item = static_cast<CompletionViewItem *>(mListView->topLevelItem(itemIndex));
0294             item->item()->setCompletionWeight(w);
0295             item->item()->setIsEnabled(item->checkState(0) == Qt::Checked);
0296             item->item()->save(this);
0297             --w;
0298         }
0299         Q_EMIT completionOrderChanged();
0300     }
0301 }
0302 
0303 KConfig *CompletionOrderWidget::configFile()
0304 {
0305     return &mConfig;
0306 }
0307 
0308 void CompletionOrderWidget::addRecentAddressItem()
0309 {
0310     // Be default it's the first.
0311     auto item = new SimpleCompletionItem(this, i18n("Recent Addresses"), QStringLiteral("Recent Addresses"), 10);
0312     item->setIcon(QIcon::fromTheme(QStringLiteral("kmail")));
0313     new CompletionViewItem(mListView, item);
0314 }
0315 
0316 void CompletionOrderWidget::addCompletionItemForCollection(const QModelIndex &index)
0317 {
0318     const auto collection = index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0319     if (!collection.isValid()) {
0320         return;
0321     }
0322 
0323     auto item = new SimpleCompletionItem(this, index.data().toString(), QString::number(collection.id()), mDefaultValue++, true);
0324     item->setIcon(index.data(Qt::DecorationRole).value<QIcon>());
0325 
0326     new CompletionViewItem(mListView, item);
0327 }
0328 
0329 void CompletionOrderWidget::loadCompletionItems()
0330 {
0331     if (mLdapSearch) {
0332         // The first step is to gather all the data, creating CompletionItem objects
0333         const QList<KLDAPWidgets::LdapClient *> listClients = mLdapSearch->clients();
0334         for (KLDAPWidgets::LdapClient *client : listClients) {
0335             new CompletionViewItem(mListView, new LDAPCompletionItem(client));
0336         }
0337     }
0338 
0339     auto monitor = new Akonadi::ChangeRecorder(this);
0340     monitor->fetchCollection(true);
0341     monitor->setCollectionMonitored(Akonadi::Collection::root());
0342     monitor->setMimeTypeMonitored(KContacts::Addressee::mimeType(), true);
0343     monitor->setMimeTypeMonitored(KContacts::ContactGroup::mimeType(), true);
0344 
0345     auto model = new Akonadi::EntityTreeModel(monitor, this);
0346     model->setItemPopulationStrategy(Akonadi::EntityTreeModel::NoItemPopulation);
0347 
0348     auto descendantsProxy = new KDescendantsProxyModel(this);
0349     descendantsProxy->setDisplayAncestorData(true);
0350     descendantsProxy->setSourceModel(model);
0351 
0352     auto mimeTypeProxy = new Akonadi::CollectionFilterProxyModel(this);
0353     mimeTypeProxy->addMimeTypeFilters({KContacts::Addressee::mimeType(), KContacts::ContactGroup::mimeType()});
0354     mimeTypeProxy->setSourceModel(descendantsProxy);
0355     mimeTypeProxy->setExcludeVirtualCollections(true);
0356 
0357     mCollectionModel = mimeTypeProxy;
0358 
0359     connect(mimeTypeProxy, &QAbstractItemModel::rowsInserted, this, &CompletionOrderWidget::rowsInserted);
0360     for (int row = 0; row < mCollectionModel->rowCount(); ++row) {
0361         addCompletionItemForCollection(mCollectionModel->index(row, 0));
0362     }
0363     addRecentAddressItem();
0364 
0365     mListView->sortItems(0, Qt::AscendingOrder);
0366 
0367     mDirty = false;
0368 }
0369 
0370 void CompletionOrderWidget::setLdapClientSearch(KLDAPWidgets::LdapClientSearch *ldapSearch)
0371 {
0372     mLdapSearch = ldapSearch;
0373 }
0374 
0375 void CompletionOrderWidget::rowsInserted(const QModelIndex &parent, int start, int end)
0376 {
0377     for (int row = start; row <= end; ++row) {
0378         addCompletionItemForCollection(mCollectionModel->index(row, 0, parent));
0379     }
0380 
0381     mListView->sortItems(0, Qt::AscendingOrder);
0382 }
0383 
0384 void CompletionOrderWidget::slotItemChanged()
0385 {
0386     mDirty = true;
0387 }
0388 
0389 void CompletionOrderWidget::slotSelectionChanged()
0390 {
0391     QTreeWidgetItem *item = mListView->currentItem();
0392     mDownButton->setEnabled(item && mListView->itemBelow(item));
0393     mUpButton->setEnabled(item && mListView->itemAbove(item));
0394 }
0395 
0396 static void swapItems(CompletionViewItem *one, CompletionViewItem *other)
0397 {
0398     CompletionItem *oneCompletion = one->item();
0399     CompletionItem *otherCompletion = other->item();
0400 
0401     int weight = otherCompletion->completionWeight();
0402     otherCompletion->setCompletionWeight(oneCompletion->completionWeight());
0403     oneCompletion->setCompletionWeight(weight);
0404 
0405     one->setItem(oneCompletion);
0406     other->setItem(otherCompletion);
0407 }
0408 
0409 void CompletionOrderWidget::slotMoveUp()
0410 {
0411     auto item = static_cast<CompletionViewItem *>(mListView->currentItem());
0412     if (!item) {
0413         return;
0414     }
0415     auto above = static_cast<CompletionViewItem *>(mListView->itemAbove(item));
0416     if (!above) {
0417         return;
0418     }
0419     swapItems(item, above);
0420     mListView->sortItems(0, Qt::AscendingOrder);
0421     slotSelectionChanged();
0422     mDirty = true;
0423 }
0424 
0425 void CompletionOrderWidget::slotMoveDown()
0426 {
0427     auto item = static_cast<CompletionViewItem *>(mListView->currentItem());
0428     if (!item) {
0429         return;
0430     }
0431     auto below = static_cast<CompletionViewItem *>(mListView->itemBelow(item));
0432     if (!below) {
0433         return;
0434     }
0435     swapItems(item, below);
0436     mListView->sortItems(0, Qt::AscendingOrder);
0437     slotSelectionChanged();
0438     mDirty = true;
0439 }
0440 
0441 #include "moc_completionorderwidget.cpp"