File indexing completed on 2024-12-15 04:54:36

0001 /*
0002   SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org>
0003 
0004   SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "searchlinestatus.h"
0008 #include "configurefiltersdialog.h"
0009 #include "core/filtersavedmanager.h"
0010 #include "filtersavedmenu.h"
0011 #include "messagelist_debug.h"
0012 #include <KStatefulBrush>
0013 
0014 #include <KColorScheme>
0015 #include <KIconEngine>
0016 #include <KIconLoader>
0017 #include <KLocalizedString>
0018 #include <QAbstractItemView>
0019 #include <QAction>
0020 #include <QActionGroup>
0021 #include <QCompleter>
0022 #include <QContextMenuEvent>
0023 #include <QMenu>
0024 #include <QPushButton>
0025 #include <QStandardPaths>
0026 #include <QStringListModel>
0027 #include <QWidgetAction>
0028 
0029 static const char qLineEditclearButtonActionNameC[] = "_q_qlineeditclearaction";
0030 #define MAX_COMPLETION_ITEMS 20
0031 using namespace MessageList::Core;
0032 SearchLineStatus::SearchLineStatus(QWidget *parent)
0033     : QLineEdit(parent)
0034     , mCompleter(new QCompleter(this))
0035     , mCompleterListModel(new QStringListModel(this))
0036 {
0037     setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::BottomEdge}));
0038     mCompleter->setCaseSensitivity(Qt::CaseInsensitive);
0039     mCompleter->setModel(mCompleterListModel);
0040     setCompleter(mCompleter);
0041 
0042     setClearButtonEnabled(true);
0043     initializeActions();
0044     createMenuSearch();
0045     auto act = findChild<QAction *>(QLatin1StringView(qLineEditclearButtonActionNameC));
0046     if (act) {
0047         connect(act, &QAction::triggered, this, &SearchLineStatus::slotClear);
0048     } else {
0049         qCWarning(MESSAGELIST_LOG) << "Clear button name was changed ! Please verify qt code";
0050     }
0051     connect(FilterSavedManager::self(), &FilterSavedManager::activateFilter, this, &SearchLineStatus::slotActivateFilter);
0052 }
0053 
0054 SearchLineStatus::~SearchLineStatus() = default;
0055 
0056 void SearchLineStatus::keyPressEvent(QKeyEvent *e)
0057 {
0058     if (e->key() == Qt::Key_Escape) {
0059         if (mCompleter->popup()->isVisible()) {
0060             QLineEdit::keyPressEvent(e);
0061         } else {
0062             Q_EMIT forceLostFocus();
0063         }
0064     } else if (e->key() == Qt::Key_Q && (e->modifiers().testFlag(Qt::ShiftModifier) && e->modifiers().testFlag(Qt::AltModifier))) {
0065         mLockAction->trigger();
0066     } else {
0067         QLineEdit::keyPressEvent(e);
0068     }
0069 }
0070 
0071 void SearchLineStatus::slotClear()
0072 {
0073     Q_EMIT clearButtonClicked();
0074 }
0075 
0076 void SearchLineStatus::updateLockAction()
0077 {
0078     if (mLocked) {
0079         mLockAction->setIcon(QIcon::fromTheme(QStringLiteral("object-locked")));
0080         mLockAction->setToolTip(i18nc("@info:tooltip", "Prevent the quick search field from being cleared when changing folders"));
0081     } else {
0082         mLockAction->setIcon(QIcon::fromTheme(QStringLiteral("object-unlocked")));
0083         mLockAction->setToolTip(i18nc("@info:tooltip", "Clear the quick search field when changing folders"));
0084     }
0085 }
0086 
0087 void SearchLineStatus::setLocked(bool b)
0088 {
0089     if (mLocked != b) {
0090         slotToggledLockAction();
0091     }
0092 }
0093 
0094 bool SearchLineStatus::locked() const
0095 {
0096     return mLocked;
0097 }
0098 
0099 void SearchLineStatus::initializeActions()
0100 {
0101     mLockAction = addAction(QIcon::fromTheme(QStringLiteral("object-locked")), QLineEdit::TrailingPosition);
0102     mLockAction->setWhatsThis(i18nc("@info:whatsthis",
0103                                     "Toggle this button if you want to keep your quick search "
0104                                     "locked when moving to other folders or when narrowing the search "
0105                                     "by message status."));
0106 
0107     connect(mLockAction, &QAction::triggered, this, &SearchLineStatus::slotToggledLockAction);
0108     updateLockAction();
0109 
0110     const QStringList overlays = QStringList() << QStringLiteral("list-add");
0111     mWithFilter = QIcon(new KIconEngine(QStringLiteral("view-filter"), KIconLoader::global(), overlays));
0112     mWithoutFilter = QIcon::fromTheme(QStringLiteral("view-filter"));
0113     mFiltersAction = addAction(mWithoutFilter, QLineEdit::LeadingPosition);
0114     mFiltersAction->setToolTip(i18n("Filter Mails by Status"));
0115     connect(mFiltersAction, &QAction::triggered, this, &SearchLineStatus::showMenu);
0116 
0117     mSaveFilterAction = addAction(QIcon::fromTheme(QStringLiteral("edit-find")), QLineEdit::LeadingPosition);
0118     mSaveFilterAction->setToolTip(i18n("Saved Filter"));
0119     mFilterSavedMenu = new FilterSavedMenu(this);
0120     mSaveFilterAction->setMenu(mFilterSavedMenu);
0121     connect(mSaveFilterAction, &QAction::triggered, this, &SearchLineStatus::showSavedFiltersMenu);
0122     connect(mFilterSavedMenu, &FilterSavedMenu::saveFilter, this, &SearchLineStatus::saveFilter);
0123     connect(mFilterSavedMenu, &FilterSavedMenu::configureFilters, this, &SearchLineStatus::slotConfigureFilters);
0124 }
0125 
0126 void SearchLineStatus::slotActivateFilter(const QString &identifier)
0127 {
0128     Filter *f = FilterSavedManager::self()->loadFilter(identifier);
0129     if (f) {
0130         Q_EMIT activateFilter(f);
0131     } else {
0132         qCWarning(MESSAGELIST_LOG) << "Impossible to load Filter from identifier :" << identifier;
0133     }
0134 }
0135 
0136 void SearchLineStatus::slotConfigureFilters()
0137 {
0138     ConfigureFiltersDialog dlg(this);
0139     dlg.exec();
0140 }
0141 
0142 void SearchLineStatus::slotToggledLockAction()
0143 {
0144     mLocked = !mLocked;
0145     updateLockAction();
0146 }
0147 
0148 void SearchLineStatus::updateFilters()
0149 {
0150     QList<Akonadi::MessageStatus> lstStatus;
0151 
0152     for (QAction *act : std::as_const(mFilterListActions)) {
0153         if (act->isChecked()) {
0154             Akonadi::MessageStatus status;
0155             status.fromQInt32(static_cast<qint32>(act->data().toInt()));
0156             lstStatus.append(status);
0157         }
0158     }
0159     mHasFilter = !lstStatus.isEmpty();
0160     Q_EMIT filterActionChanged(lstStatus);
0161     updateFilterActionIcon();
0162 }
0163 
0164 void SearchLineStatus::showSavedFiltersMenu()
0165 {
0166     mFilterSavedMenu->exec(mapToGlobal(QPoint(0, height())));
0167 }
0168 
0169 void SearchLineStatus::showMenu()
0170 {
0171     if (mFilterMenu->exec(mapToGlobal(QPoint(0, height())))) {
0172         updateFilters();
0173     }
0174 }
0175 
0176 void SearchLineStatus::clearFilterAction()
0177 {
0178     for (QAction *act : std::as_const(mFilterListActions)) {
0179         act->setChecked(false);
0180     }
0181     mHasFilter = false;
0182     updateFilterActionIcon();
0183 }
0184 
0185 void SearchLineStatus::createFilterAction(const QIcon &icon, const QString &text, int value)
0186 {
0187     auto act = new QAction(icon, text, this);
0188     act->setCheckable(true);
0189     act->setData(value);
0190     mFilterMenu->addAction(act);
0191     mFilterListActions.append(act);
0192 }
0193 
0194 void SearchLineStatus::updateFilterActionIcon()
0195 {
0196     mFiltersAction->setIcon(mHasFilter ? mWithFilter : mWithoutFilter);
0197     if (mColorName.isEmpty()) {
0198         const KColorScheme::BackgroundRole bgColorScheme(KColorScheme::PositiveBackground);
0199         KStatefulBrush bgBrush(KColorScheme::View, bgColorScheme);
0200         mColorName = bgBrush.brush(palette()).color().name();
0201     }
0202     setStyleSheet(mHasFilter ? QStringLiteral("QLineEdit{ background-color:%1 }").arg(mColorName) : QString());
0203 }
0204 
0205 void SearchLineStatus::clearFilterButtonClicked()
0206 {
0207     clearFilterAction();
0208     clearFilterByAction();
0209     updateFilters();
0210 }
0211 
0212 void SearchLineStatus::createMenuSearch()
0213 {
0214     mFilterMenu = new QMenu(this);
0215     mFilterMenu->setObjectName(QLatin1StringView("filtermenu"));
0216     auto clearWidgetAction = new QWidgetAction(mFilterMenu);
0217     auto clearFilterButton = new QPushButton(i18n("Clear Filter"), mFilterMenu);
0218     connect(clearFilterButton, &QPushButton::clicked, this, &SearchLineStatus::clearFilterButtonClicked);
0219 
0220     clearWidgetAction->setDefaultWidget(clearFilterButton);
0221     mFilterMenu->addAction(clearWidgetAction);
0222     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-unread")),
0223                        i18nc("@action:inmenu Status of a message", "Unread"),
0224                        Akonadi::MessageStatus::statusUnread().toQInt32());
0225 
0226     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-replied")),
0227                        i18nc("@action:inmenu Status of a message", "Replied"),
0228                        Akonadi::MessageStatus::statusReplied().toQInt32());
0229 
0230     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-forwarded")),
0231                        i18nc("@action:inmenu Status of a message", "Forwarded"),
0232                        Akonadi::MessageStatus::statusForwarded().toQInt32());
0233 
0234     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-mark-important")),
0235                        i18nc("@action:inmenu Status of a message", "Important"),
0236                        Akonadi::MessageStatus::statusImportant().toQInt32());
0237 
0238     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-task")),
0239                        i18nc("@action:inmenu Status of a message", "Action Item"),
0240                        Akonadi::MessageStatus::statusToAct().toQInt32());
0241 
0242     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-thread-watch")),
0243                        i18nc("@action:inmenu Status of a message", "Watched"),
0244                        Akonadi::MessageStatus::statusWatched().toQInt32());
0245 
0246     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-thread-ignored")),
0247                        i18nc("@action:inmenu Status of a message", "Ignored"),
0248                        Akonadi::MessageStatus::statusIgnored().toQInt32());
0249 
0250     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-attachment")),
0251                        i18nc("@action:inmenu Status of a message", "Has Attachment"),
0252                        Akonadi::MessageStatus::statusHasAttachment().toQInt32());
0253 
0254     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-invitation")),
0255                        i18nc("@action:inmenu Status of a message", "Has Invitation"),
0256                        Akonadi::MessageStatus::statusHasInvitation().toQInt32());
0257 
0258     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-mark-junk")),
0259                        i18nc("@action:inmenu Status of a message", "Spam"),
0260                        Akonadi::MessageStatus::statusSpam().toQInt32());
0261 
0262     createFilterAction(QIcon::fromTheme(QStringLiteral("mail-mark-notjunk")),
0263                        i18nc("@action:inmenu Status of a message", "Ham"),
0264                        Akonadi::MessageStatus::statusHam().toQInt32());
0265     createFilterByAction();
0266 }
0267 
0268 void SearchLineStatus::createFilterByAction()
0269 {
0270     mFilterMenu->addSeparator();
0271     auto grp = new QActionGroup(mFilterMenu);
0272 
0273     mSearchEveryWhereAction = new QAction(i18n("Full Message"), mFilterMenu);
0274     mSearchEveryWhereAction->setCheckable(true);
0275     mSearchEveryWhereAction->setChecked(true);
0276 
0277     mFilterMenu->addAction(mSearchEveryWhereAction);
0278     grp->addAction(mSearchEveryWhereAction);
0279 
0280     mSearchAgainstBodyAction = new QAction(i18n("Body"), mFilterMenu);
0281     grp->addAction(mSearchAgainstBodyAction);
0282     mFilterMenu->addAction(mSearchAgainstBodyAction);
0283     mSearchAgainstBodyAction->setCheckable(true);
0284 
0285     mSearchAgainstSubjectAction = new QAction(i18n("Subject"), mFilterMenu);
0286     grp->addAction(mSearchAgainstSubjectAction);
0287     mFilterMenu->addAction(mSearchAgainstSubjectAction);
0288     mSearchAgainstSubjectAction->setCheckable(true);
0289 
0290     mSearchAgainstFromOrToAction = new QAction(mFilterMenu);
0291     changeSearchAgainstFromOrToText();
0292     grp->addAction(mSearchAgainstFromOrToAction);
0293     mFilterMenu->addAction(mSearchAgainstFromOrToAction);
0294     mSearchAgainstFromOrToAction->setCheckable(true);
0295 
0296     mSearchAgainstBccAction = new QAction(i18n("BCC"), mFilterMenu);
0297     grp->addAction(mSearchAgainstBccAction);
0298     mFilterMenu->addAction(mSearchAgainstBccAction);
0299     mSearchAgainstBccAction->setCheckable(true);
0300     connect(grp, &QActionGroup::triggered, this, &SearchLineStatus::slotFilterActionClicked);
0301 }
0302 
0303 void SearchLineStatus::clearFilterByAction()
0304 {
0305     mSearchEveryWhereAction->setChecked(true);
0306 }
0307 
0308 bool SearchLineStatus::containsOutboundMessages() const
0309 {
0310     return mContainsOutboundMessages;
0311 }
0312 
0313 void SearchLineStatus::setContainsOutboundMessages(bool containsOutboundMessages)
0314 {
0315     if (mContainsOutboundMessages != containsOutboundMessages) {
0316         mContainsOutboundMessages = containsOutboundMessages;
0317         changeSearchAgainstFromOrToText();
0318     }
0319 }
0320 
0321 void SearchLineStatus::changeSearchAgainstFromOrToText()
0322 {
0323     if (mContainsOutboundMessages) {
0324         mSearchAgainstFromOrToAction->setText(i18n("To"));
0325     } else {
0326         mSearchAgainstFromOrToAction->setText(i18n("From"));
0327     }
0328 }
0329 
0330 QuickSearchLine::SearchOptions SearchLineStatus::searchOptions() const
0331 {
0332     QuickSearchLine::SearchOptions searchOptions;
0333     if (mSearchEveryWhereAction->isChecked()) {
0334         searchOptions |= QuickSearchLine::SearchEveryWhere;
0335     }
0336     if (mSearchAgainstBodyAction->isChecked()) {
0337         searchOptions |= QuickSearchLine::SearchAgainstBody;
0338     }
0339     if (mSearchAgainstSubjectAction->isChecked()) {
0340         searchOptions |= QuickSearchLine::SearchAgainstSubject;
0341     }
0342     if (mSearchAgainstFromOrToAction->isChecked()) {
0343         if (mContainsOutboundMessages) {
0344             searchOptions |= QuickSearchLine::SearchAgainstTo;
0345         } else {
0346             searchOptions |= QuickSearchLine::SearchAgainstFrom;
0347         }
0348     }
0349     if (mSearchAgainstBccAction->isChecked()) {
0350         searchOptions |= QuickSearchLine::SearchAgainstBcc;
0351     }
0352     return searchOptions;
0353 }
0354 
0355 void SearchLineStatus::setSearchOptions(QuickSearchLine::SearchOptions opts)
0356 {
0357     mSearchEveryWhereAction->setChecked(opts & QuickSearchLine::SearchEveryWhere);
0358     mSearchAgainstBodyAction->setChecked(opts & QuickSearchLine::SearchAgainstBody);
0359     mSearchAgainstSubjectAction->setChecked(opts & QuickSearchLine::SearchAgainstSubject);
0360     mSearchAgainstBccAction->setChecked(opts & QuickSearchLine::SearchAgainstBcc);
0361     if (mContainsOutboundMessages) {
0362         mSearchAgainstFromOrToAction->setChecked(opts & QuickSearchLine::SearchAgainstTo);
0363     } else {
0364         mSearchAgainstFromOrToAction->setChecked(opts & QuickSearchLine::SearchAgainstFrom);
0365     }
0366 }
0367 
0368 void SearchLineStatus::setFilterMessageStatus(const QList<Akonadi::MessageStatus> &newLstStatus)
0369 {
0370     clearFilterAction();
0371     clearFilterByAction();
0372     // TODO unchecked all checkbox
0373     for (const Akonadi::MessageStatus &status : newLstStatus) {
0374         for (QAction *act : std::as_const(mFilterListActions)) {
0375             if (static_cast<qint32>(act->data().toInt()) == status.toQInt32()) {
0376                 act->setChecked(true);
0377                 mHasFilter = true;
0378                 break;
0379             }
0380         }
0381     }
0382     updateFilterActionIcon();
0383 }
0384 
0385 void SearchLineStatus::slotFilterActionClicked(QAction *act)
0386 {
0387     Q_UNUSED(act)
0388     Q_EMIT searchOptionChanged();
0389 }
0390 
0391 void SearchLineStatus::addCompletionItem(const QString &str)
0392 {
0393     mListCompetion.removeAll(str);
0394     mListCompetion.prepend(str);
0395     while (mListCompetion.size() > MAX_COMPLETION_ITEMS) {
0396         mListCompetion.removeLast();
0397     }
0398     mCompleterListModel->setStringList(mListCompetion);
0399 }
0400 
0401 void SearchLineStatus::contextMenuEvent(QContextMenuEvent *e)
0402 {
0403     QMenu *popup = QLineEdit::createStandardContextMenu();
0404     if (popup) {
0405         popup->addSeparator();
0406         popup->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl")), i18n("Clear History"), this, &SearchLineStatus::slotClearHistory);
0407         popup->exec(e->globalPos());
0408         delete popup;
0409     }
0410 }
0411 
0412 void SearchLineStatus::slotClearHistory()
0413 {
0414     mListCompetion.clear();
0415     mCompleterListModel->setStringList(mListCompetion);
0416 }
0417 
0418 #include "moc_searchlinestatus.cpp"