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"