File indexing completed on 2024-05-26 05:14:40

0001 /*
0002     SPDX-FileCopyrightText: 2008 Ingo Klöcker <kloecker@kde.org>
0003     SPDX-FileCopyrightText: 2010-2024 Laurent Montel <montel@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "collectiondialog.h"
0009 
0010 #include "asyncselectionhandler_p.h"
0011 
0012 #include "collectioncreatejob.h"
0013 #include "collectionfetchscope.h"
0014 #include "collectionfilterproxymodel.h"
0015 #include "collectionutils.h"
0016 #include "entityrightsfiltermodel.h"
0017 #include "entitytreemodel.h"
0018 #include "entitytreeview.h"
0019 #include "monitor.h"
0020 #include "session.h"
0021 
0022 #include <QCheckBox>
0023 #include <QDialogButtonBox>
0024 #include <QHeaderView>
0025 #include <QLabel>
0026 #include <QVBoxLayout>
0027 
0028 #include <KConfig>
0029 #include <KConfigGroup>
0030 #include <KLineEditEventHandler>
0031 #include <KLocalizedString>
0032 #include <QInputDialog>
0033 #include <QLineEdit>
0034 #include <QMessageBox>
0035 #include <QPushButton>
0036 
0037 using namespace Akonadi;
0038 
0039 class Akonadi::CollectionDialogPrivate
0040 {
0041 public:
0042     CollectionDialogPrivate(QAbstractItemModel *customModel, CollectionDialog *parent, CollectionDialog::CollectionDialogOptions options)
0043         : mParent(parent)
0044     {
0045         // setup GUI
0046         auto layout = new QVBoxLayout(mParent);
0047 
0048         mTextLabel = new QLabel(mParent);
0049         layout->addWidget(mTextLabel);
0050         mTextLabel->hide();
0051 
0052         auto filterCollectionLineEdit = new QLineEdit(mParent);
0053         KLineEditEventHandler::catchReturnKey(filterCollectionLineEdit);
0054         filterCollectionLineEdit->setClearButtonEnabled(true);
0055         filterCollectionLineEdit->setPlaceholderText(
0056             i18nc("@info Displayed grayed-out inside the "
0057                   "textbox, verb to search",
0058                   "Search"));
0059         layout->addWidget(filterCollectionLineEdit);
0060 
0061         mView = new EntityTreeView(mParent);
0062         mView->setDragDropMode(QAbstractItemView::NoDragDrop);
0063         mView->header()->hide();
0064         layout->addWidget(mView);
0065 
0066         mUseByDefault = new QCheckBox(i18n("Use folder by default"), mParent);
0067         mUseByDefault->hide();
0068         layout->addWidget(mUseByDefault);
0069 
0070         mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, mParent);
0071         mParent->connect(mButtonBox, &QDialogButtonBox::accepted, mParent, &QDialog::accept);
0072         mParent->connect(mButtonBox, &QDialogButtonBox::rejected, mParent, &QDialog::reject);
0073         layout->addWidget(mButtonBox);
0074         mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
0075 
0076         // setup models
0077         QAbstractItemModel *baseModel = nullptr;
0078 
0079         if (customModel) {
0080             baseModel = customModel;
0081         } else {
0082             mMonitor = new Akonadi::Monitor(mParent);
0083             mMonitor->setObjectName(QLatin1StringView("CollectionDialogMonitor"));
0084             mMonitor->fetchCollection(true);
0085             mMonitor->setCollectionMonitored(Akonadi::Collection::root());
0086 
0087             auto model = new EntityTreeModel(mMonitor, mParent);
0088             model->setItemPopulationStrategy(EntityTreeModel::NoItemPopulation);
0089             model->setListFilter(CollectionFetchScope::Display);
0090             baseModel = model;
0091         }
0092 
0093         mMimeTypeFilterModel = new CollectionFilterProxyModel(mParent);
0094         mMimeTypeFilterModel->setSourceModel(baseModel);
0095         mMimeTypeFilterModel->setExcludeVirtualCollections(true);
0096 
0097         mRightsFilterModel = new EntityRightsFilterModel(mParent);
0098         mRightsFilterModel->setSourceModel(mMimeTypeFilterModel);
0099 
0100         mFilterCollection = new QSortFilterProxyModel(mParent);
0101         mFilterCollection->setRecursiveFilteringEnabled(true);
0102         mFilterCollection->setSourceModel(mRightsFilterModel);
0103         mFilterCollection->setFilterCaseSensitivity(Qt::CaseInsensitive);
0104         mView->setModel(mFilterCollection);
0105 
0106         changeCollectionDialogOptions(options);
0107         mParent->connect(filterCollectionLineEdit, &QLineEdit::textChanged, mParent, [this](const QString &str) {
0108             slotFilterFixedString(str);
0109         });
0110 
0111         mParent->connect(mView->selectionModel(), &QItemSelectionModel::selectionChanged, mParent, [this]() {
0112             slotSelectionChanged();
0113         });
0114         mParent->connect(mView, qOverload<const QModelIndex &>(&QAbstractItemView::doubleClicked), mParent, [this]() {
0115             slotDoubleClicked();
0116         });
0117 
0118         mSelectionHandler = new AsyncSelectionHandler(mFilterCollection, mParent);
0119         mParent->connect(mSelectionHandler, &AsyncSelectionHandler::collectionAvailable, mParent, [this](const QModelIndex &index) {
0120             slotCollectionAvailable(index);
0121         });
0122         readConfig();
0123     }
0124 
0125     ~CollectionDialogPrivate()
0126     {
0127         writeConfig();
0128     }
0129 
0130     void slotCollectionAvailable(const QModelIndex &index)
0131     {
0132         mView->expandAll();
0133         mView->setCurrentIndex(index);
0134     }
0135 
0136     void slotFilterFixedString(const QString &filter)
0137     {
0138         mFilterCollection->setFilterFixedString(filter);
0139         if (mKeepTreeExpanded) {
0140             mView->expandAll();
0141         }
0142     }
0143 
0144     void readConfig()
0145     {
0146         KConfig config(QStringLiteral("akonadi_contactrc"));
0147         KConfigGroup group(&config, QStringLiteral("CollectionDialog"));
0148         const QSize size = group.readEntry("Size", QSize(800, 500));
0149         if (size.isValid()) {
0150             mParent->resize(size);
0151         }
0152     }
0153 
0154     void writeConfig() const
0155     {
0156         KConfig config(QStringLiteral("akonadi_contactrc"));
0157         KConfigGroup group(&config, QStringLiteral("CollectionDialog"));
0158         group.writeEntry("Size", mParent->size());
0159         group.sync();
0160     }
0161 
0162     CollectionDialog *const mParent;
0163 
0164     Monitor *mMonitor = nullptr;
0165     CollectionFilterProxyModel *mMimeTypeFilterModel = nullptr;
0166     EntityRightsFilterModel *mRightsFilterModel = nullptr;
0167     EntityTreeView *mView = nullptr;
0168     AsyncSelectionHandler *mSelectionHandler = nullptr;
0169     QLabel *mTextLabel = nullptr;
0170     QSortFilterProxyModel *mFilterCollection = nullptr;
0171     QCheckBox *mUseByDefault = nullptr;
0172     QStringList mContentMimeTypes;
0173     QDialogButtonBox *mButtonBox = nullptr;
0174     QPushButton *mNewSubfolderButton = nullptr;
0175     bool mAllowToCreateNewChildCollection = false;
0176     bool mKeepTreeExpanded = false;
0177 
0178     void slotDoubleClicked();
0179     void slotSelectionChanged();
0180     void slotAddChildCollection();
0181     void slotCollectionCreationResult(KJob *job);
0182     bool canCreateCollection(const Akonadi::Collection &parentCollection) const;
0183     void changeCollectionDialogOptions(CollectionDialog::CollectionDialogOptions options);
0184     bool canSelectCollection() const;
0185 };
0186 
0187 void CollectionDialogPrivate::slotDoubleClicked()
0188 {
0189     if (canSelectCollection()) {
0190         mParent->accept();
0191     }
0192 }
0193 
0194 bool CollectionDialogPrivate::canSelectCollection() const
0195 {
0196     bool result = (!mView->selectionModel()->selectedIndexes().isEmpty());
0197     if (mAllowToCreateNewChildCollection) {
0198         const Akonadi::Collection parentCollection = mParent->selectedCollection();
0199 
0200         if (parentCollection.isValid()) {
0201             result = (parentCollection.rights() & Akonadi::Collection::CanCreateItem);
0202         }
0203     }
0204     return result;
0205 }
0206 
0207 void CollectionDialogPrivate::slotSelectionChanged()
0208 {
0209     mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(!mView->selectionModel()->selectedIndexes().isEmpty());
0210     if (mAllowToCreateNewChildCollection) {
0211         const Akonadi::Collection parentCollection = mParent->selectedCollection();
0212         const bool canCreateChildCollections = canCreateCollection(parentCollection);
0213 
0214         mNewSubfolderButton->setEnabled(canCreateChildCollections && !parentCollection.isVirtual());
0215         if (parentCollection.isValid()) {
0216             const bool canCreateItems = (parentCollection.rights() & Akonadi::Collection::CanCreateItem);
0217             mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(canCreateItems);
0218         }
0219     }
0220 }
0221 
0222 void CollectionDialogPrivate::changeCollectionDialogOptions(CollectionDialog::CollectionDialogOptions options)
0223 {
0224     mAllowToCreateNewChildCollection = (options & CollectionDialog::AllowToCreateNewChildCollection);
0225     if (mAllowToCreateNewChildCollection) {
0226         mNewSubfolderButton = mButtonBox->addButton(i18n("&New Subfolder..."), QDialogButtonBox::NoRole);
0227         mNewSubfolderButton->setIcon(QIcon::fromTheme(QStringLiteral("folder-new")));
0228         mNewSubfolderButton->setToolTip(i18n("Create a new subfolder under the currently selected folder"));
0229         mNewSubfolderButton->setEnabled(false);
0230         QObject::connect(mNewSubfolderButton, &QPushButton::clicked, mParent, [this]() {
0231             slotAddChildCollection();
0232         });
0233     }
0234     mKeepTreeExpanded = (options & CollectionDialog::KeepTreeExpanded);
0235     if (mKeepTreeExpanded) {
0236         mParent->connect(mRightsFilterModel, &EntityRightsFilterModel::rowsInserted, mView, &EntityTreeView::expandAll, Qt::UniqueConnection);
0237         mView->expandAll();
0238     }
0239 }
0240 
0241 bool CollectionDialogPrivate::canCreateCollection(const Akonadi::Collection &parentCollection) const
0242 {
0243     if (!parentCollection.isValid()) {
0244         return false;
0245     }
0246 
0247     if ((parentCollection.rights() & Akonadi::Collection::CanCreateCollection)) {
0248         const QStringList dialogMimeTypeFilter = mParent->mimeTypeFilter();
0249         const QStringList parentCollectionMimeTypes = parentCollection.contentMimeTypes();
0250         for (const QString &mimetype : dialogMimeTypeFilter) {
0251             if (parentCollectionMimeTypes.contains(mimetype)) {
0252                 return true;
0253             }
0254         }
0255         return true;
0256     }
0257     return false;
0258 }
0259 
0260 void CollectionDialogPrivate::slotAddChildCollection()
0261 {
0262     const Akonadi::Collection parentCollection = mParent->selectedCollection();
0263     if (canCreateCollection(parentCollection)) {
0264         bool ok = false;
0265         const QString name =
0266             QInputDialog::getText(mParent, i18nc("@title:window", "New Folder"), i18nc("@label:textbox, name of a thing", "Name"), {}, {}, &ok);
0267         if (name.trimmed().isEmpty() || !ok) {
0268             return;
0269         }
0270 
0271         Akonadi::Collection collection;
0272         collection.setName(name);
0273         collection.setParentCollection(parentCollection);
0274         if (!mContentMimeTypes.isEmpty()) {
0275             collection.setContentMimeTypes(mContentMimeTypes);
0276         }
0277         auto job = new Akonadi::CollectionCreateJob(collection);
0278         QObject::connect(job, &Akonadi::CollectionCreateJob::result, mParent, [this](KJob *job) {
0279             slotCollectionCreationResult(job);
0280         });
0281     }
0282 }
0283 
0284 void CollectionDialogPrivate::slotCollectionCreationResult(KJob *job)
0285 {
0286     if (job->error()) {
0287         QMessageBox::critical(mParent, i18nc("@title:window", "Folder Creation Failed"), i18n("Could not create folder: %1", job->errorString()));
0288     }
0289 }
0290 
0291 CollectionDialog::CollectionDialog(QWidget *parent)
0292     : QDialog(parent)
0293     , d(new CollectionDialogPrivate(nullptr, this, CollectionDialog::None))
0294 {
0295 }
0296 
0297 CollectionDialog::CollectionDialog(QAbstractItemModel *model, QWidget *parent)
0298     : QDialog(parent)
0299     , d(new CollectionDialogPrivate(model, this, CollectionDialog::None))
0300 {
0301 }
0302 
0303 CollectionDialog::CollectionDialog(CollectionDialogOptions options, QAbstractItemModel *model, QWidget *parent)
0304     : QDialog(parent)
0305     , d(new CollectionDialogPrivate(model, this, options))
0306 {
0307 }
0308 
0309 CollectionDialog::~CollectionDialog() = default;
0310 
0311 Akonadi::Collection CollectionDialog::selectedCollection() const
0312 {
0313     if (selectionMode() == QAbstractItemView::SingleSelection) {
0314         const QModelIndex index = d->mView->currentIndex();
0315         if (index.isValid()) {
0316             return index.model()->data(index, EntityTreeModel::CollectionRole).value<Collection>();
0317         }
0318     }
0319 
0320     return Collection();
0321 }
0322 
0323 Akonadi::Collection::List CollectionDialog::selectedCollections() const
0324 {
0325     Collection::List collections;
0326     const QItemSelectionModel *selectionModel = d->mView->selectionModel();
0327     const QModelIndexList selectedIndexes = selectionModel->selectedIndexes();
0328     for (const QModelIndex &index : selectedIndexes) {
0329         if (index.isValid()) {
0330             const auto collection = index.model()->data(index, EntityTreeModel::CollectionRole).value<Collection>();
0331             if (collection.isValid()) {
0332                 collections.append(collection);
0333             }
0334         }
0335     }
0336 
0337     return collections;
0338 }
0339 
0340 void CollectionDialog::setMimeTypeFilter(const QStringList &mimeTypes)
0341 {
0342     if (mimeTypeFilter() == mimeTypes) {
0343         return;
0344     }
0345 
0346     d->mMimeTypeFilterModel->clearFilters();
0347     d->mMimeTypeFilterModel->addMimeTypeFilters(mimeTypes);
0348 
0349     if (d->mMonitor) {
0350         for (const QString &mimetype : mimeTypes) {
0351             d->mMonitor->setMimeTypeMonitored(mimetype);
0352         }
0353     }
0354 }
0355 
0356 QStringList CollectionDialog::mimeTypeFilter() const
0357 {
0358     return d->mMimeTypeFilterModel->mimeTypeFilters();
0359 }
0360 
0361 void CollectionDialog::setAccessRightsFilter(Collection::Rights rights)
0362 {
0363     if (accessRightsFilter() == rights) {
0364         return;
0365     }
0366     d->mRightsFilterModel->setAccessRights(rights);
0367 }
0368 
0369 Akonadi::Collection::Rights CollectionDialog::accessRightsFilter() const
0370 {
0371     return d->mRightsFilterModel->accessRights();
0372 }
0373 
0374 void CollectionDialog::setDescription(const QString &text)
0375 {
0376     d->mTextLabel->setText(text);
0377     d->mTextLabel->show();
0378 }
0379 
0380 void CollectionDialog::setDefaultCollection(const Collection &collection)
0381 {
0382     d->mSelectionHandler->waitForCollection(collection);
0383 }
0384 
0385 void CollectionDialog::setSelectionMode(QAbstractItemView::SelectionMode mode)
0386 {
0387     d->mView->setSelectionMode(mode);
0388 }
0389 
0390 QAbstractItemView::SelectionMode CollectionDialog::selectionMode() const
0391 {
0392     return d->mView->selectionMode();
0393 }
0394 
0395 void CollectionDialog::changeCollectionDialogOptions(CollectionDialogOptions options)
0396 {
0397     d->changeCollectionDialogOptions(options);
0398 }
0399 
0400 void CollectionDialog::setUseFolderByDefault(bool b)
0401 {
0402     d->mUseByDefault->setChecked(b);
0403     d->mUseByDefault->show();
0404 }
0405 
0406 bool CollectionDialog::useFolderByDefault() const
0407 {
0408     return d->mUseByDefault->isChecked();
0409 }
0410 
0411 void CollectionDialog::setContentMimeTypes(const QStringList &mimetypes)
0412 {
0413     d->mContentMimeTypes = mimetypes;
0414 }
0415 
0416 #include "moc_collectiondialog.cpp"