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

0001 /*
0002  * kmail: KDE mail client
0003  * SPDX-FileCopyrightText: 1996-1998 Stefan Taferner <taferner@kde.org>
0004  * SPDX-FileCopyrightText: 2001 Aaron J. Seigo <aseigo@kde.org>
0005  * SPDX-FileCopyrightText: 2010 Till Adam <adam@kde.org>
0006  * SPDX-FileCopyrightText: 2011-2024 Laurent Montel <montel@kde.org>
0007  *
0008  * SPDX-License-Identifier: GPL-2.0-or-later
0009  *
0010  */
0011 
0012 #include "searchwindow.h"
0013 #include "incompleteindexdialog.h"
0014 
0015 #include "kmcommands.h"
0016 #include "kmkernel.h"
0017 #include "kmmainwidget.h"
0018 #include "kmsearchmessagemodel.h"
0019 #include "searchdescriptionattribute.h"
0020 #include "searchpatternwarning.h"
0021 #include <MailCommon/FolderRequester>
0022 #include <MailCommon/FolderTreeView>
0023 #include <MailCommon/MailKernel>
0024 #include <MailCommon/SearchPatternEdit>
0025 #include <PimCommon/PimUtil>
0026 #include <PimCommonAkonadi/SelectMultiCollectionDialog>
0027 #include <TextUtils/ConvertText>
0028 
0029 #include "kmail_debug.h"
0030 #include <Akonadi/CachePolicy>
0031 #include <Akonadi/ChangeRecorder>
0032 #include <Akonadi/CollectionFetchJob>
0033 #include <Akonadi/CollectionModifyJob>
0034 #include <Akonadi/EntityHiddenAttribute>
0035 #include <Akonadi/EntityMimeTypeFilterModel>
0036 #include <Akonadi/EntityTreeModel>
0037 #include <Akonadi/EntityTreeView>
0038 #include <Akonadi/PersistentSearchAttribute>
0039 #include <Akonadi/SearchCreateJob>
0040 #include <Akonadi/StandardActionManager>
0041 #include <KActionMenu>
0042 #include <KMessageBox>
0043 #include <KMime/KMimeMessage>
0044 #include <KStandardAction>
0045 #include <KStandardGuiItem>
0046 #include <PIM/indexeditems.h>
0047 #include <QIcon>
0048 #include <QSortFilterProxyModel>
0049 
0050 #include <QCheckBox>
0051 #include <QCloseEvent>
0052 #include <QCursor>
0053 #include <QDialogButtonBox>
0054 #include <QHeaderView>
0055 #include <QKeyEvent>
0056 #include <QMenu>
0057 #include <QPushButton>
0058 #include <QVBoxLayout>
0059 #include <chrono>
0060 
0061 using namespace std::chrono_literals;
0062 
0063 using namespace KPIM;
0064 using namespace MailCommon;
0065 
0066 using namespace KMail;
0067 
0068 SearchWindow::SearchWindow(KMMainWidget *widget, const Akonadi::Collection &collection)
0069     : QDialog(nullptr)
0070     , mSearchButton(new QPushButton(this))
0071     , mKMMainWidget(widget)
0072 {
0073     setWindowTitle(i18nc("@title:window", "Find Messages"));
0074 
0075     auto mainLayout = new QVBoxLayout(this);
0076 
0077     auto topWidget = new QWidget;
0078     auto lay = new QVBoxLayout(topWidget);
0079     lay->setContentsMargins({});
0080     mSearchPatternWidget = new SearchPatternWarning;
0081     lay->addWidget(mSearchPatternWidget);
0082     mainLayout->addWidget(topWidget);
0083 
0084     auto searchWidget = new QWidget(this);
0085     mUi.setupUi(searchWidget);
0086 
0087     lay->addWidget(searchWidget);
0088 
0089     mStartSearchGuiItem = KGuiItem(i18nc("@action:button Search for messages", "&Search"), QStringLiteral("edit-find"));
0090     mStopSearchGuiItem = KStandardGuiItem::stop();
0091     KGuiItem::assign(mSearchButton, mStartSearchGuiItem);
0092     mUi.mButtonBox->addButton(mSearchButton, QDialogButtonBox::ActionRole);
0093     connect(mUi.mButtonBox, &QDialogButtonBox::rejected, this, &SearchWindow::slotClose);
0094     searchWidget->layout()->setContentsMargins({});
0095 
0096     mUi.mCbxFolders->setMustBeReadWrite(false);
0097     mUi.mCbxFolders->setNotAllowToCreateNewFolder(true);
0098     activateFolder(collection);
0099     connect(mUi.mPatternEdit, &KMail::KMailSearchPatternEdit::returnPressed, this, &SearchWindow::slotSearch);
0100 
0101     // enable/disable widgets depending on radio buttons:
0102     connect(mUi.mChkbxAllFolders, &QRadioButton::toggled, this, &SearchWindow::setEnabledSearchButton);
0103 
0104     mUi.mLbxMatches->setXmlGuiClient(mKMMainWidget->guiClient());
0105 
0106     /*
0107     Default is to sort by date. TODO: Unfortunately this sorts *while*
0108     inserting, which looks rather strange - the user cannot read
0109     the results so far as they are constantly re-sorted --dnaber
0110 
0111     Sorting is now disabled when a search is started and reenabled
0112     when it stops. Items are appended to the list. This not only
0113     solves the above problem, but speeds searches with many hits
0114     up considerably. - till
0115 
0116     TODO: subclass QTreeWidgetItem and do proper (and performant)
0117     compare functions
0118     */
0119     mUi.mLbxMatches->setSortingEnabled(true);
0120 
0121     connect(mUi.mLbxMatches, &Akonadi::EntityTreeView::customContextMenuRequested, this, &SearchWindow::slotContextMenuRequested);
0122     connect(mUi.mLbxMatches, qOverload<const Akonadi::Item &>(&Akonadi::EntityTreeView::doubleClicked), this, &SearchWindow::slotViewMsg);
0123     connect(mUi.mLbxMatches, qOverload<const Akonadi::Item &>(&Akonadi::EntityTreeView::currentChanged), this, &SearchWindow::slotCurrentChanged);
0124     connect(mUi.selectMultipleFolders, &QPushButton::clicked, this, &SearchWindow::slotSelectMultipleFolders);
0125 
0126     connect(KMKernel::self()->folderCollectionMonitor(), &Akonadi::Monitor::collectionStatisticsChanged, this, &SearchWindow::updateCollectionStatistic);
0127 
0128     connect(mUi.mSearchFolderEdt, &KLineEdit::textChanged, this, &SearchWindow::scheduleRename);
0129     connect(&mRenameTimer, &QTimer::timeout, this, &SearchWindow::renameSearchFolder);
0130     connect(mUi.mSearchFolderOpenBtn, &QPushButton::clicked, this, &SearchWindow::openSearchFolder);
0131 
0132     connect(mUi.mSearchResultOpenBtn, &QPushButton::clicked, this, &SearchWindow::slotViewSelectedMsg);
0133 
0134     const int mainWidth = KMailSettings::self()->searchWidgetWidth();
0135     const int mainHeight = KMailSettings::self()->searchWidgetHeight();
0136 
0137     if (mainWidth || mainHeight) {
0138         resize(mainWidth, mainHeight);
0139     }
0140 
0141     connect(mSearchButton, &QPushButton::clicked, this, &SearchWindow::slotSearch);
0142     connect(this, &SearchWindow::finished, this, &SearchWindow::deleteLater);
0143     connect(mUi.mButtonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &SearchWindow::slotClose);
0144 
0145     // give focus to the value field of the first search rule
0146     auto r = mUi.mPatternEdit->findChild<KLineEdit *>(QStringLiteral("regExpLineEdit"));
0147     if (r) {
0148         r->setFocus();
0149     } else {
0150         qCDebug(KMAIL_LOG) << "SearchWindow: regExpLineEdit not found";
0151     }
0152 
0153     // set up actions
0154     KActionCollection *ac = actionCollection();
0155     mReplyAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-reply-sender")), i18n("&Reply..."), this);
0156     actionCollection()->addAction(QStringLiteral("search_reply"), mReplyAction);
0157     connect(mReplyAction, &QAction::triggered, this, &SearchWindow::slotReplyToMsg);
0158 
0159     mReplyAllAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-reply-all")), i18n("Reply to &All..."), this);
0160     actionCollection()->addAction(QStringLiteral("search_reply_all"), mReplyAllAction);
0161     connect(mReplyAllAction, &QAction::triggered, this, &SearchWindow::slotReplyAllToMsg);
0162 
0163     mReplyListAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-reply-list")), i18n("Reply to Mailing-&List..."), this);
0164     actionCollection()->addAction(QStringLiteral("search_reply_list"), mReplyListAction);
0165     connect(mReplyListAction, &QAction::triggered, this, &SearchWindow::slotReplyListToMsg);
0166 
0167     mForwardActionMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("mail-forward")), i18nc("Message->", "&Forward"), this);
0168     actionCollection()->addAction(QStringLiteral("search_message_forward"), mForwardActionMenu);
0169     connect(mForwardActionMenu, &KActionMenu::triggered, this, &SearchWindow::slotForwardMsg);
0170 
0171     mForwardInlineAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-forward")), i18nc("@action:inmenu Forward message inline.", "&Inline..."), this);
0172     actionCollection()->addAction(QStringLiteral("search_message_forward_inline"), mForwardInlineAction);
0173     connect(mForwardInlineAction, &QAction::triggered, this, &SearchWindow::slotForwardMsg);
0174 
0175     mForwardAttachedAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-forward")), i18nc("Message->Forward->", "As &Attachment..."), this);
0176     actionCollection()->addAction(QStringLiteral("search_message_forward_as_attachment"), mForwardAttachedAction);
0177     connect(mForwardAttachedAction, &QAction::triggered, this, &SearchWindow::slotForwardAttachedMsg);
0178 
0179     if (KMailSettings::self()->forwardingInlineByDefault()) {
0180         mForwardActionMenu->addAction(mForwardInlineAction);
0181         mForwardActionMenu->addAction(mForwardAttachedAction);
0182     } else {
0183         mForwardActionMenu->addAction(mForwardAttachedAction);
0184         mForwardActionMenu->addAction(mForwardInlineAction);
0185     }
0186 
0187     mSaveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("search_file_save_as"));
0188     connect(mSaveAsAction, &QAction::triggered, this, &SearchWindow::slotSaveMsg);
0189 
0190     mSaveAtchAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Save Attachments..."), this);
0191     actionCollection()->addAction(QStringLiteral("search_save_attachments"), mSaveAtchAction);
0192     connect(mSaveAtchAction, &QAction::triggered, this, &SearchWindow::slotSaveAttachments);
0193 
0194     mPrintAction = actionCollection()->addAction(KStandardAction::Print, QStringLiteral("search_print"));
0195     connect(mPrintAction, &QAction::triggered, this, &SearchWindow::slotPrintMsg);
0196 
0197     mClearAction = new QAction(i18n("Clear Selection"), this);
0198     actionCollection()->addAction(QStringLiteral("search_clear_selection"), mClearAction);
0199     connect(mClearAction, &QAction::triggered, this, &SearchWindow::slotClearSelection);
0200 
0201     mJumpToFolderAction = new QAction(i18n("Jump to original folder"), this);
0202     actionCollection()->addAction(QStringLiteral("search_jump_folder"), mJumpToFolderAction);
0203     connect(mJumpToFolderAction, &QAction::triggered, this, &SearchWindow::slotJumpToFolder);
0204 
0205     connect(mUi.mCbxFolders, &MailCommon::FolderRequester::folderChanged, this, &SearchWindow::slotFolderActivated);
0206 
0207     ac->addAssociatedWidget(this);
0208     const QList<QAction *> actList = ac->actions();
0209     for (QAction *action : actList) {
0210         action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0211     }
0212     mUi.mProgressIndicator->hide();
0213 }
0214 
0215 SearchWindow::~SearchWindow()
0216 {
0217     if (mResultModel) {
0218         if (mUi.mLbxMatches->columnWidth(0) > 0) {
0219             KMailSettings::self()->setCollectionWidth(mUi.mLbxMatches->columnWidth(0));
0220         }
0221         if (mUi.mLbxMatches->columnWidth(1) > 0) {
0222             KMailSettings::self()->setSubjectWidth(mUi.mLbxMatches->columnWidth(1));
0223         }
0224         if (mUi.mLbxMatches->columnWidth(2) > 0) {
0225             KMailSettings::self()->setSenderWidth(mUi.mLbxMatches->columnWidth(2));
0226         }
0227         if (mUi.mLbxMatches->columnWidth(3) > 0) {
0228             KMailSettings::self()->setReceiverWidth(mUi.mLbxMatches->columnWidth(3));
0229         }
0230         if (mUi.mLbxMatches->columnWidth(4) > 0) {
0231             KMailSettings::self()->setDateWidth(mUi.mLbxMatches->columnWidth(4));
0232         }
0233         if (mUi.mLbxMatches->columnWidth(5) > 0) {
0234             KMailSettings::self()->setFolderWidth(mUi.mLbxMatches->columnWidth(5));
0235         }
0236         KMailSettings::self()->setSearchWidgetWidth(width());
0237         KMailSettings::self()->setSearchWidgetHeight(height());
0238         KMailSettings::self()->requestSync();
0239         mResultModel->deleteLater();
0240     }
0241 }
0242 
0243 void SearchWindow::createSearchModel()
0244 {
0245     if (mResultModel) {
0246         mResultModel->deleteLater();
0247     }
0248     auto monitor = new Akonadi::Monitor();
0249     monitor->setCollectionMonitored(mFolder);
0250     mResultModel = new KMSearchMessageModel(monitor, this);
0251     mResultModel->setCollectionMonitored(mFolder);
0252     monitor->setParent(mResultModel);
0253     auto sortproxy = new QSortFilterProxyModel(mResultModel);
0254     sortproxy->setDynamicSortFilter(true);
0255     sortproxy->setSortRole(Qt::EditRole);
0256     sortproxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
0257     sortproxy->setSourceModel(mResultModel);
0258     mUi.mLbxMatches->setModel(sortproxy);
0259 
0260     mUi.mLbxMatches->setColumnWidth(0, KMailSettings::self()->collectionWidth());
0261     mUi.mLbxMatches->setColumnWidth(1, KMailSettings::self()->subjectWidth());
0262     mUi.mLbxMatches->setColumnWidth(2, KMailSettings::self()->senderWidth());
0263     mUi.mLbxMatches->setColumnWidth(3, KMailSettings::self()->receiverWidth());
0264     mUi.mLbxMatches->setColumnWidth(4, KMailSettings::self()->dateWidth());
0265     mUi.mLbxMatches->setColumnWidth(5, KMailSettings::self()->folderWidth());
0266     mUi.mLbxMatches->setColumnHidden(6, true);
0267     mUi.mLbxMatches->setColumnHidden(7, true);
0268     mUi.mLbxMatches->header()->setSortIndicator(2, Qt::DescendingOrder);
0269     mUi.mLbxMatches->header()->setStretchLastSection(false);
0270     mUi.mLbxMatches->header()->restoreState(mHeaderState);
0271     // mUi.mLbxMatches->header()->setResizeMode( 3, QHeaderView::Stretch );
0272     if (!mAkonadiStandardAction) {
0273         mAkonadiStandardAction = new Akonadi::StandardMailActionManager(actionCollection(), this);
0274     }
0275     mAkonadiStandardAction->setItemSelectionModel(mUi.mLbxMatches->selectionModel());
0276     mAkonadiStandardAction->setCollectionSelectionModel(mKMMainWidget->folderTreeView()->selectionModel());
0277 }
0278 
0279 void SearchWindow::setEnabledSearchButton(bool)
0280 {
0281     // Make sure that button is enable
0282     // Before when we selected a folder == "Local Folder" as that it was not a folder
0283     // search button was disable, and when we select "Search in all local folder"
0284     // Search button was never enabled :(
0285     mSearchButton->setEnabled(true);
0286 }
0287 
0288 void SearchWindow::updateCollectionStatistic(Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistic)
0289 {
0290     QString genMsg;
0291     if (id == mFolder.id()) {
0292         genMsg = i18np("%1 match", "%1 matches", statistic.count());
0293     }
0294     mUi.mStatusLbl->setText(genMsg);
0295 }
0296 
0297 void SearchWindow::keyPressEvent(QKeyEvent *event)
0298 {
0299     if (event->key() == Qt::Key_Escape && mSearchJob) {
0300         slotStop();
0301         return;
0302     }
0303 
0304     QDialog::keyPressEvent(event);
0305 }
0306 
0307 void SearchWindow::slotFolderActivated()
0308 {
0309     mUi.mChkbxSpecificFolders->setChecked(true);
0310 }
0311 
0312 void SearchWindow::activateFolder(const Akonadi::Collection &collection)
0313 {
0314     mUi.mChkbxSpecificFolders->setChecked(true);
0315     mSearchPattern.clear();
0316     bool currentFolderIsSearchFolder = false;
0317 
0318     if (!collection.hasAttribute<Akonadi::PersistentSearchAttribute>()) {
0319         // it's not a search folder, make a new search
0320         mSearchPattern.append(SearchRule::createInstance("Subject"));
0321         mUi.mCbxFolders->setCollection(collection);
0322     } else {
0323         // it's a search folder
0324         if (collection.hasAttribute<Akonadi::SearchDescriptionAttribute>()) {
0325             currentFolderIsSearchFolder = true; // FIXME is there a better way to tell?
0326 
0327             const auto searchDescription = collection.attribute<Akonadi::SearchDescriptionAttribute>();
0328             mSearchPattern.deserialize(searchDescription->description());
0329 
0330             const QList<Akonadi::Collection::Id> lst = searchDescription->listCollection();
0331             if (!lst.isEmpty()) {
0332                 mUi.mChkMultiFolders->setChecked(true);
0333                 mCollectionId.clear();
0334                 for (Akonadi::Collection::Id col : lst) {
0335                     mCollectionId.append(Akonadi::Collection(col));
0336                 }
0337             } else {
0338                 const Akonadi::Collection col = searchDescription->baseCollection();
0339                 if (col.isValid()) {
0340                     mUi.mChkbxSpecificFolders->setChecked(true);
0341                     mUi.mCbxFolders->setCollection(col);
0342                     mUi.mChkSubFolders->setChecked(searchDescription->recursive());
0343                 } else {
0344                     mUi.mChkbxAllFolders->setChecked(true);
0345                     mUi.mChkSubFolders->setChecked(searchDescription->recursive());
0346                 }
0347             }
0348         } else {
0349             // it's a search folder, but not one of ours, warn the user that we can't edit it
0350             // FIXME show results, but disable edit GUI
0351             qCWarning(KMAIL_LOG) << "This search was not created with KMail. It cannot be edited within it.";
0352             mSearchPattern.clear();
0353         }
0354     }
0355 
0356     mUi.mPatternEdit->setSearchPattern(&mSearchPattern);
0357     if (currentFolderIsSearchFolder) {
0358         mFolder = collection;
0359         mUi.mSearchFolderEdt->setText(collection.name());
0360         createSearchModel();
0361     } else if (mUi.mSearchFolderEdt->text().isEmpty()) {
0362         mUi.mSearchFolderEdt->setText(i18n("Last Search"));
0363         // find last search and reuse it if possible
0364         mFolder = CommonKernel->collectionFromId(KMailSettings::lastSearchCollectionId());
0365         // when the last folder got renamed, create a new one
0366         if (mFolder.isValid() && mFolder.name() != mUi.mSearchFolderEdt->text()) {
0367             mFolder = Akonadi::Collection();
0368         }
0369     }
0370 }
0371 
0372 void SearchWindow::slotSearch()
0373 {
0374     if (mFolder.isValid()) {
0375         doSearch();
0376         return;
0377     }
0378     // We're going to try to create a new search folder, let's ensure first the name is not yet used.
0379 
0380     // Fetch all search collections
0381     auto fetchJob = new Akonadi::CollectionFetchJob(Akonadi::Collection(1), Akonadi::CollectionFetchJob::FirstLevel);
0382     connect(fetchJob, &Akonadi::CollectionFetchJob::result, this, &SearchWindow::slotSearchCollectionsFetched);
0383 }
0384 
0385 void SearchWindow::slotSearchCollectionsFetched(KJob *job)
0386 {
0387     if (job->error()) {
0388         qCWarning(KMAIL_LOG) << job->errorString();
0389     }
0390     auto fetchJob = static_cast<Akonadi::CollectionFetchJob *>(job);
0391     const Akonadi::Collection::List lstCol = fetchJob->collections();
0392     for (const Akonadi::Collection &col : lstCol) {
0393         if (col.name() == mUi.mSearchFolderEdt->text()) {
0394             mFolder = col;
0395         }
0396     }
0397     doSearch();
0398 }
0399 
0400 void SearchWindow::doSearch()
0401 {
0402     mSearchPatternWidget->hideWarningPattern();
0403     if (mUi.mSearchFolderEdt->text().isEmpty()) {
0404         mUi.mSearchFolderEdt->setText(i18n("Last Search"));
0405     }
0406 
0407     if (mResultModel) {
0408         mHeaderState = mUi.mLbxMatches->header()->saveState();
0409     }
0410 
0411     mUi.mLbxMatches->setModel(nullptr);
0412 
0413     mSortColumn = mUi.mLbxMatches->header()->sortIndicatorSection();
0414     mSortOrder = mUi.mLbxMatches->header()->sortIndicatorOrder();
0415     mUi.mLbxMatches->setSortingEnabled(false);
0416 
0417     if (mSearchJob) {
0418         mSearchJob->kill(KJob::Quietly);
0419         mSearchJob->deleteLater();
0420         mSearchJob = nullptr;
0421     }
0422 
0423     mUi.mSearchFolderEdt->setEnabled(false);
0424 
0425     QList<Akonadi::Collection> searchCollections;
0426     bool recursive = false;
0427     if (mUi.mChkbxSpecificFolders->isChecked()) {
0428         const Akonadi::Collection col = mUi.mCbxFolders->collection();
0429         if (!col.isValid()) {
0430             mSearchPatternWidget->showWarningPattern(QStringList() << i18n("You did not selected a valid folder."));
0431             mUi.mSearchFolderEdt->setEnabled(true);
0432             return;
0433         }
0434         searchCollections << col;
0435         if (mUi.mChkSubFolders->isChecked()) {
0436             recursive = true;
0437         }
0438     } else if (mUi.mChkMultiFolders->isChecked()) {
0439         if (mSelectMultiCollectionDialog) {
0440             mCollectionId = mSelectMultiCollectionDialog->selectedCollection();
0441         }
0442         if (mCollectionId.isEmpty()) {
0443             mUi.mSearchFolderEdt->setEnabled(true);
0444             mSearchPatternWidget->showWarningPattern(QStringList() << i18n("You forgot to select collections."));
0445             mQuery = Akonadi::SearchQuery();
0446             return;
0447         }
0448         searchCollections << mCollectionId;
0449     }
0450 
0451     mUi.mPatternEdit->updateSearchPattern();
0452 
0453     SearchPattern searchPattern(mSearchPattern);
0454     searchPattern.purify();
0455 
0456     MailCommon::SearchPattern::SparqlQueryError queryError = searchPattern.asAkonadiQuery(mQuery);
0457     switch (queryError) {
0458     case MailCommon::SearchPattern::NoError:
0459         break;
0460     case MailCommon::SearchPattern::MissingCheck:
0461         mUi.mSearchFolderEdt->setEnabled(true);
0462         mSearchPatternWidget->showWarningPattern(QStringList() << i18n("You forgot to define condition."));
0463         mQuery = Akonadi::SearchQuery();
0464         return;
0465     case MailCommon::SearchPattern::FolderEmptyOrNotIndexed:
0466         mUi.mSearchFolderEdt->setEnabled(true);
0467         mSearchPatternWidget->showWarningPattern(QStringList() << i18n("All folders selected are empty or were not indexed."));
0468         mQuery = Akonadi::SearchQuery();
0469         return;
0470     case MailCommon::SearchPattern::EmptyResult:
0471         mUi.mSearchFolderEdt->setEnabled(true);
0472         mQuery = Akonadi::SearchQuery();
0473         mSearchPatternWidget->showWarningPattern(QStringList() << i18n("You forgot to add conditions."));
0474         return;
0475     case MailCommon::SearchPattern::NotEnoughCharacters:
0476         mUi.mSearchFolderEdt->setEnabled(true);
0477         mSearchPatternWidget->showWarningPattern(QStringList() << i18n("Contains condition cannot be used with a number of characters inferior to 4."));
0478         mQuery = Akonadi::SearchQuery();
0479         return;
0480     }
0481     mSearchPatternWidget->hideWarningPattern();
0482     qCDebug(KMAIL_LOG) << mQuery.toJSON();
0483     mUi.mSearchFolderOpenBtn->setEnabled(true);
0484 
0485     const QList<qint64> unindexedCollections = checkIncompleteIndex(searchCollections, recursive);
0486     if (!unindexedCollections.isEmpty()) {
0487         IncompleteIndexDialog dlg(unindexedCollections);
0488         dlg.exec();
0489     }
0490 
0491     auto config = KConfig(QStringLiteral("akonadi_indexing_agent"));
0492     KConfigGroup cfg = config.group(QStringLiteral("General"));
0493     const bool respectDiacriticAndAccents = cfg.readEntry("respectDiacriticAndAccents", true);
0494 
0495     if (mFolder.isValid()) {
0496         qCDebug(KMAIL_LOG) << " use existing folder " << mFolder.id();
0497         auto attribute = new Akonadi::PersistentSearchAttribute();
0498         mFolder.setContentMimeTypes(QStringList() << QStringLiteral("message/rfc822"));
0499         attribute->setQueryString(QString::fromLatin1(mQuery.toJSON()));
0500         attribute->setQueryCollections(searchCollections);
0501         attribute->setRecursive(recursive);
0502         attribute->setRemoteSearchEnabled(false);
0503         mFolder.addAttribute(attribute);
0504         mSearchJob = new Akonadi::CollectionModifyJob(mFolder, this);
0505     } else {
0506         const QString searchString =
0507             respectDiacriticAndAccents ? mUi.mSearchFolderEdt->text() : TextUtils::ConvertText::normalize(mUi.mSearchFolderEdt->text());
0508         qCDebug(KMAIL_LOG) << " create new folder " << searchString;
0509         auto searchJob = new Akonadi::SearchCreateJob(searchString, mQuery, this);
0510         searchJob->setSearchMimeTypes(QStringList() << QStringLiteral("message/rfc822"));
0511         searchJob->setSearchCollections(searchCollections);
0512         searchJob->setRecursive(recursive);
0513         searchJob->setRemoteSearchEnabled(false);
0514         mSearchJob = searchJob;
0515     }
0516 
0517     connect(mSearchJob, &Akonadi::CollectionModifyJob::result, this, &SearchWindow::searchDone);
0518     mUi.mProgressIndicator->show();
0519     enableGUI();
0520     mUi.mStatusLbl->setText(i18n("Searching..."));
0521 }
0522 
0523 void SearchWindow::searchDone(KJob *job)
0524 {
0525     qDebug() << " void SearchWindow::searchDone(KJob *job)";
0526     Q_ASSERT(job == mSearchJob);
0527     mSearchJob = nullptr;
0528     QMetaObject::invokeMethod(this, &SearchWindow::enableGUI, Qt::QueuedConnection);
0529 
0530     mUi.mProgressIndicator->hide();
0531     if (job->error()) {
0532         qCDebug(KMAIL_LOG) << job->errorString();
0533         KMessageBox::error(this, i18n("Cannot get search result. %1", job->errorString()));
0534         enableGUI();
0535         mUi.mSearchFolderEdt->setEnabled(true);
0536         mUi.mStatusLbl->setText(i18n("Search failed."));
0537     } else {
0538         if (const auto searchJob = qobject_cast<Akonadi::SearchCreateJob *>(job)) {
0539             mFolder = searchJob->createdCollection();
0540         } else if (const auto modifyJob = qobject_cast<Akonadi::CollectionModifyJob *>(job)) {
0541             mFolder = modifyJob->collection();
0542         }
0543         /// TODO: cope better with cases where this fails
0544         Q_ASSERT(mFolder.isValid());
0545         Q_ASSERT(mFolder.hasAttribute<Akonadi::PersistentSearchAttribute>());
0546 
0547         KMailSettings::setLastSearchCollectionId(mFolder.id());
0548         KMailSettings::self()->save();
0549         KMailSettings::self()->requestSync();
0550 
0551         // store the kmail specific serialization of the search in an attribute on
0552         // the server, for easy retrieval when editing it again
0553         const QByteArray search = mSearchPattern.serialize();
0554         Q_ASSERT(!search.isEmpty());
0555         auto searchDescription = mFolder.attribute<Akonadi::SearchDescriptionAttribute>(Akonadi::Collection::AddIfMissing);
0556         searchDescription->setDescription(search);
0557         if (mUi.mChkMultiFolders->isChecked()) {
0558             searchDescription->setBaseCollection(Akonadi::Collection());
0559             QList<Akonadi::Collection::Id> lst;
0560             lst.reserve(mCollectionId.count());
0561             for (const Akonadi::Collection &col : std::as_const(mCollectionId)) {
0562                 lst << col.id();
0563             }
0564             searchDescription->setListCollection(lst);
0565         } else if (mUi.mChkbxSpecificFolders->isChecked()) {
0566             const Akonadi::Collection collection = mUi.mCbxFolders->collection();
0567             searchDescription->setBaseCollection(collection);
0568         } else {
0569             searchDescription->setBaseCollection(Akonadi::Collection());
0570         }
0571         searchDescription->setRecursive(mUi.mChkSubFolders->isChecked());
0572         new Akonadi::CollectionModifyJob(mFolder, this);
0573         auto fetch = new Akonadi::CollectionFetchJob(mFolder, Akonadi::CollectionFetchJob::Base, this);
0574         fetch->fetchScope().setIncludeStatistics(true);
0575         connect(fetch, &KJob::result, this, &SearchWindow::slotCollectionStatisticsRetrieved);
0576 
0577         mUi.mStatusLbl->setText(i18n("Search complete."));
0578         createSearchModel();
0579 
0580         if (mCloseRequested) {
0581             close();
0582         }
0583 
0584         mUi.mLbxMatches->setSortingEnabled(true);
0585         mUi.mLbxMatches->header()->setSortIndicator(mSortColumn, mSortOrder);
0586 
0587         mUi.mSearchFolderEdt->setEnabled(true);
0588     }
0589 }
0590 
0591 void SearchWindow::slotCollectionStatisticsRetrieved(KJob *job)
0592 {
0593     auto fetch = qobject_cast<Akonadi::CollectionFetchJob *>(job);
0594     if (!fetch || fetch->error()) {
0595         return;
0596     }
0597 
0598     const Akonadi::Collection::List cols = fetch->collections();
0599     if (cols.isEmpty()) {
0600         mUi.mStatusLbl->clear();
0601         return;
0602     }
0603 
0604     const Akonadi::Collection col = cols.at(0);
0605     updateCollectionStatistic(col.id(), col.statistics());
0606 }
0607 
0608 void SearchWindow::slotStop()
0609 {
0610     mUi.mProgressIndicator->hide();
0611     if (mSearchJob) {
0612         mSearchJob->kill(KJob::Quietly);
0613         mSearchJob->deleteLater();
0614         mSearchJob = nullptr;
0615         mUi.mStatusLbl->setText(i18n("Search stopped."));
0616     }
0617 
0618     enableGUI();
0619 }
0620 
0621 void SearchWindow::slotClose()
0622 {
0623     accept();
0624 }
0625 
0626 void SearchWindow::closeEvent(QCloseEvent *event)
0627 {
0628     if (mSearchJob) {
0629         mCloseRequested = true;
0630         // Cancel search in progress
0631         mSearchJob->kill(KJob::Quietly);
0632         mSearchJob->deleteLater();
0633         mSearchJob = nullptr;
0634         QTimer::singleShot(0, this, &SearchWindow::slotClose);
0635     } else {
0636         QDialog::closeEvent(event);
0637     }
0638 }
0639 
0640 void SearchWindow::scheduleRename(const QString &text)
0641 {
0642     if (!text.isEmpty()) {
0643         mRenameTimer.setSingleShot(true);
0644         mRenameTimer.start(250ms);
0645         mUi.mSearchFolderOpenBtn->setEnabled(false);
0646     } else {
0647         mRenameTimer.stop();
0648         mUi.mSearchFolderOpenBtn->setEnabled(!text.isEmpty());
0649     }
0650 }
0651 
0652 void SearchWindow::renameSearchFolder()
0653 {
0654     const QString name = mUi.mSearchFolderEdt->text();
0655     if (mFolder.isValid()) {
0656         const QString oldFolderName = mFolder.name();
0657         if (oldFolderName != name) {
0658             mFolder.setName(name);
0659             auto job = new Akonadi::CollectionModifyJob(mFolder, this);
0660             job->setProperty("oldfoldername", oldFolderName);
0661             connect(job, &Akonadi::CollectionModifyJob::result, this, &SearchWindow::slotSearchFolderRenameDone);
0662         }
0663         mUi.mSearchFolderOpenBtn->setEnabled(true);
0664     }
0665 }
0666 
0667 void SearchWindow::slotSearchFolderRenameDone(KJob *job)
0668 {
0669     Q_ASSERT(job);
0670     if (job->error()) {
0671         qCWarning(KMAIL_LOG) << "Job failed:" << job->errorText();
0672         KMessageBox::information(this,
0673                                  i18n("There was a problem renaming your search folder. "
0674                                       "A common reason for this is that another search folder "
0675                                       "with the same name already exists. Error returned \"%1\".",
0676                                       job->errorText()));
0677         mUi.mSearchFolderEdt->blockSignals(true);
0678         mUi.mSearchFolderEdt->setText(job->property("oldfoldername").toString());
0679         mUi.mSearchFolderEdt->blockSignals(false);
0680     }
0681 }
0682 
0683 void SearchWindow::openSearchFolder()
0684 {
0685     Q_ASSERT(mFolder.isValid());
0686     renameSearchFolder();
0687     mKMMainWidget->slotSelectCollectionFolder(mFolder);
0688     slotClose();
0689 }
0690 
0691 void SearchWindow::slotViewSelectedMsg()
0692 {
0693     mKMMainWidget->slotMessageActivated(selectedMessage());
0694 }
0695 
0696 void SearchWindow::slotViewMsg(const Akonadi::Item &item)
0697 {
0698     if (item.isValid()) {
0699         mKMMainWidget->slotMessageActivated(item);
0700     }
0701 }
0702 
0703 void SearchWindow::slotCurrentChanged(const Akonadi::Item &item)
0704 {
0705     mUi.mSearchResultOpenBtn->setEnabled(item.isValid());
0706 }
0707 
0708 void SearchWindow::enableGUI()
0709 {
0710     const bool searching = (mSearchJob != nullptr);
0711 
0712     KGuiItem::assign(mSearchButton, searching ? mStopSearchGuiItem : mStartSearchGuiItem);
0713     if (searching) {
0714         disconnect(mSearchButton, &QPushButton::clicked, this, &SearchWindow::slotSearch);
0715         connect(mSearchButton, &QPushButton::clicked, this, &SearchWindow::slotStop);
0716     } else {
0717         disconnect(mSearchButton, &QPushButton::clicked, this, &SearchWindow::slotStop);
0718         connect(mSearchButton, &QPushButton::clicked, this, &SearchWindow::slotSearch);
0719     }
0720 }
0721 
0722 Akonadi::Item::List SearchWindow::selectedMessages() const
0723 {
0724     Akonadi::Item::List messages;
0725 
0726     const QModelIndexList lst = mUi.mLbxMatches->selectionModel()->selectedRows();
0727     for (const QModelIndex &index : lst) {
0728         const auto item = index.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0729         if (item.isValid()) {
0730             messages.append(item);
0731         }
0732     }
0733 
0734     return messages;
0735 }
0736 
0737 Akonadi::Item SearchWindow::selectedMessage() const
0738 {
0739     return mUi.mLbxMatches->currentIndex().data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0740 }
0741 
0742 void SearchWindow::updateContextMenuActions()
0743 {
0744     const int count = selectedMessages().count();
0745     const bool singleActions = (count == 1);
0746     const bool notEmpty = (count > 0);
0747 
0748     mJumpToFolderAction->setEnabled(singleActions);
0749 
0750     mReplyAction->setEnabled(singleActions);
0751     mReplyAllAction->setEnabled(singleActions);
0752     mReplyListAction->setEnabled(singleActions);
0753     mPrintAction->setEnabled(singleActions);
0754     mSaveAtchAction->setEnabled(notEmpty);
0755     mSaveAsAction->setEnabled(notEmpty);
0756     mClearAction->setEnabled(notEmpty);
0757 }
0758 
0759 void SearchWindow::slotContextMenuRequested(const QPoint &)
0760 {
0761     if (!selectedMessage().isValid() || selectedMessages().isEmpty()) {
0762         return;
0763     }
0764 
0765     updateContextMenuActions();
0766     QMenu menu(this);
0767 
0768     // show most used actions
0769     menu.addAction(mReplyAction);
0770     menu.addAction(mReplyAllAction);
0771     menu.addAction(mReplyListAction);
0772     menu.addAction(mForwardActionMenu);
0773     menu.addSeparator();
0774     menu.addAction(mJumpToFolderAction);
0775     menu.addSeparator();
0776     QAction *act = mAkonadiStandardAction->createAction(Akonadi::StandardActionManager::CopyItems);
0777     mAkonadiStandardAction->setActionText(Akonadi::StandardActionManager::CopyItems, ki18np("Copy Message", "Copy %1 Messages"));
0778     menu.addAction(act);
0779     act = mAkonadiStandardAction->createAction(Akonadi::StandardActionManager::CutItems);
0780     mAkonadiStandardAction->setActionText(Akonadi::StandardActionManager::CutItems, ki18np("Cut Message", "Cut %1 Messages"));
0781     menu.addAction(act);
0782     menu.addAction(mAkonadiStandardAction->createAction(Akonadi::StandardActionManager::CopyItemToMenu));
0783     menu.addAction(mAkonadiStandardAction->createAction(Akonadi::StandardActionManager::MoveItemToMenu));
0784     menu.addSeparator();
0785     menu.addAction(mSaveAsAction);
0786     menu.addAction(mSaveAtchAction);
0787     menu.addAction(mPrintAction);
0788     menu.addSeparator();
0789     menu.addAction(mClearAction);
0790     menu.exec(QCursor::pos(), nullptr);
0791 }
0792 
0793 void SearchWindow::slotClearSelection()
0794 {
0795     mUi.mLbxMatches->clearSelection();
0796 }
0797 
0798 void SearchWindow::slotReplyToMsg()
0799 {
0800     KMCommand *command = new KMReplyCommand(this, selectedMessage(), MessageComposer::ReplySmart);
0801 
0802     command->start();
0803 }
0804 
0805 void SearchWindow::slotReplyAllToMsg()
0806 {
0807     KMCommand *command = new KMReplyCommand(this, selectedMessage(), MessageComposer::ReplyAll);
0808     command->start();
0809 }
0810 
0811 void SearchWindow::slotReplyListToMsg()
0812 {
0813     KMCommand *command = new KMReplyCommand(this, selectedMessage(), MessageComposer::ReplyList);
0814     command->start();
0815 }
0816 
0817 void SearchWindow::slotForwardMsg()
0818 {
0819     KMCommand *command = new KMForwardCommand(this, selectedMessages());
0820     command->start();
0821 }
0822 
0823 void SearchWindow::slotForwardAttachedMsg()
0824 {
0825     KMCommand *command = new KMForwardAttachedCommand(this, selectedMessages());
0826     command->start();
0827 }
0828 
0829 void SearchWindow::slotSaveMsg()
0830 {
0831     auto saveCommand = new KMSaveMsgCommand(this, selectedMessages());
0832     saveCommand->start();
0833 }
0834 
0835 void SearchWindow::slotSaveAttachments()
0836 {
0837     auto saveCommand = new KMSaveAttachmentsCommand(this, selectedMessages(), nullptr);
0838     saveCommand->start();
0839 }
0840 
0841 void SearchWindow::slotPrintMsg()
0842 {
0843     KMPrintCommandInfo info;
0844     info.mMsg = selectedMessage();
0845     KMCommand *command = new KMPrintCommand(this, info);
0846     command->start();
0847 }
0848 
0849 void SearchWindow::addRulesToSearchPattern(const SearchPattern &pattern)
0850 {
0851     SearchPattern p(mSearchPattern);
0852     p.purify();
0853 
0854     QList<SearchRule::Ptr>::const_iterator it;
0855     QList<SearchRule::Ptr>::const_iterator end(pattern.constEnd());
0856     p.reserve(pattern.count());
0857 
0858     for (it = pattern.constBegin(); it != end; ++it) {
0859         p.append(SearchRule::createInstance(**it));
0860     }
0861 
0862     mSearchPattern = p;
0863     mUi.mPatternEdit->setSearchPattern(&mSearchPattern);
0864 }
0865 
0866 void SearchWindow::slotSelectMultipleFolders()
0867 {
0868     mUi.mChkMultiFolders->setChecked(true);
0869     if (!mSelectMultiCollectionDialog) {
0870         QList<Akonadi::Collection::Id> lst;
0871         lst.reserve(mCollectionId.count());
0872         for (const Akonadi::Collection &col : std::as_const(mCollectionId)) {
0873             lst << col.id();
0874         }
0875         mSelectMultiCollectionDialog = new PimCommon::SelectMultiCollectionDialog(KMime::Message::mimeType(), lst, this);
0876     }
0877     mSelectMultiCollectionDialog->show();
0878 }
0879 
0880 void SearchWindow::slotJumpToFolder()
0881 {
0882     if (selectedMessage().isValid()) {
0883         mKMMainWidget->slotSelectCollectionFolder(selectedMessage().parentCollection());
0884     }
0885 }
0886 
0887 QList<qint64> SearchWindow::checkIncompleteIndex(const Akonadi::Collection::List &searchCols, bool recursive)
0888 {
0889     QList<qint64> results;
0890     Akonadi::Collection::List cols;
0891     if (recursive) {
0892         cols = searchCollectionsRecursive(searchCols);
0893     } else {
0894         for (const Akonadi::Collection &col : searchCols) {
0895             QAbstractItemModel *etm = KMKernel::self()->collectionModel();
0896             const QModelIndex idx = Akonadi::EntityTreeModel::modelIndexForCollection(etm, col);
0897             const auto modelCol = etm->data(idx, Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0898             // Only index offline IMAP collections
0899             if (PimCommon::Util::isImapResource(modelCol.resource()) && !modelCol.cachePolicy().localParts().contains(QLatin1StringView("RFC822"))) {
0900                 continue;
0901             } else {
0902                 cols.push_back(modelCol);
0903             }
0904         }
0905     }
0906 
0907     enableGUI();
0908     mUi.mProgressIndicator->hide();
0909     mUi.mStatusLbl->setText(i18n("Checking index status..."));
0910     // Fetch collection ?
0911     for (const Akonadi::Collection &col : std::as_const(cols)) {
0912         const qlonglong num = KMKernel::self()->indexedItems()->indexedItems((qlonglong)col.id());
0913         if (col.statistics().count() != num) {
0914             results.push_back(col.id());
0915         }
0916     }
0917     return results;
0918 }
0919 
0920 Akonadi::Collection::List SearchWindow::searchCollectionsRecursive(const Akonadi::Collection::List &cols) const
0921 {
0922     QAbstractItemModel *etm = KMKernel::self()->collectionModel();
0923     Akonadi::Collection::List result;
0924 
0925     for (const Akonadi::Collection &col : cols) {
0926         const QModelIndex colIdx = Akonadi::EntityTreeModel::modelIndexForCollection(etm, col);
0927         if (col.statistics().count() > -1) {
0928             if (col.cachePolicy().localParts().contains(QLatin1StringView("RFC822"))) {
0929                 result.push_back(col);
0930             }
0931         } else {
0932             const auto collection = etm->data(colIdx, Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0933             if (!collection.hasAttribute<Akonadi::EntityHiddenAttribute>() && collection.cachePolicy().localParts().contains(QLatin1StringView("RFC822"))) {
0934                 result.push_back(collection);
0935             }
0936         }
0937 
0938         const int childrenCount = etm->rowCount(colIdx);
0939         if (childrenCount > 0) {
0940             Akonadi::Collection::List subCols;
0941             subCols.reserve(childrenCount);
0942             for (int i = 0; i < childrenCount; ++i) {
0943                 const QModelIndex idx = etm->index(i, 0, colIdx);
0944                 const auto child = etm->data(idx, Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0945                 if (child.cachePolicy().localParts().contains(QLatin1StringView("RFC822"))) {
0946                     subCols.push_back(child);
0947                 }
0948             }
0949 
0950             result += searchCollectionsRecursive(subCols);
0951         }
0952     }
0953 
0954     return result;
0955 }
0956 
0957 #include "moc_searchwindow.cpp"