File indexing completed on 2024-06-16 05:01:28

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include "MessageListWidget.h"
0024 #include <QAction>
0025 #include <QApplication>
0026 #include <QCheckBox>
0027 #include <QFrame>
0028 #include <QMenu>
0029 #include <QTimer>
0030 #include <QToolButton>
0031 #include <QVBoxLayout>
0032 #include <QWidgetAction>
0033 #include "LineEdit.h"
0034 #include "MsgListView.h"
0035 #include "ReplaceCharValidator.h"
0036 #include "UiUtils/IconLoader.h"
0037 
0038 namespace Gui {
0039 
0040 MessageListWidget::MessageListWidget(QWidget *parent, Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel) :
0041     QWidget(parent), m_supportsFuzzySearch(false)
0042 {
0043     tree = new MsgListView(this, m_favoriteTagsModel);
0044 
0045     m_quickSearchText = new LineEdit(this);
0046     m_quickSearchText->setHistoryEnabled(true);
0047     // Filter out newline. It will wreak havoc into the direct IMAP passthrough and could lead to data loss.
0048     QValidator *validator = new ReplaceCharValidator(QLatin1Char('\n'), QLatin1Char(' '), m_quickSearchText);
0049     m_quickSearchText->setValidator(validator);
0050     m_quickSearchText->setPlaceholderText(tr("Quick Search"));
0051     m_quickSearchText->setToolTip(tr("Type in a text to search for within this mailbox. "
0052                                      "The icon on the left can be used to limit the search options "
0053                                      "(like whether to include addresses or message bodies, etc)."
0054                                      "<br/><hr/>"
0055                                      "Experts who have read RFC3501 can use the <code>:=</code> prefix and switch to a raw IMAP mode."));
0056     m_queryPlaceholder = tr("<query>");
0057 
0058     connect(m_quickSearchText, &QLineEdit::returnPressed, this, &MessageListWidget::slotApplySearch);
0059     connect(m_quickSearchText, &QLineEdit::textChanged, this, &MessageListWidget::slotConditionalSearchReset);
0060     connect(m_quickSearchText, &QLineEdit::cursorPositionChanged, this, &MessageListWidget::slotUpdateSearchCursor);
0061     connect(m_quickSearchText, &LineEdit::escapePressed, tree, static_cast<void (QWidget::*)()>(&QWidget::setFocus));
0062 
0063     m_searchOptions = new QAction(UiUtils::loadIcon(QStringLiteral("imap-search-details")), QStringLiteral("*"), this);
0064     m_searchOptions->setToolTip(tr("Options for the IMAP search..."));
0065     QMenu *optionsMenu = new QMenu(this);
0066     m_searchOptions->setMenu(optionsMenu);
0067     m_searchFuzzy = optionsMenu->addAction(tr("Fuzzy Search"));
0068     m_searchFuzzy->setCheckable(true);
0069     optionsMenu->addSeparator();
0070     m_searchInSubject = optionsMenu->addAction(tr("Subject"));
0071     m_searchInSubject->setCheckable(true);
0072     m_searchInSubject->setChecked(true);
0073     m_searchInBody = optionsMenu->addAction(tr("Body"));
0074     m_searchInBody->setCheckable(true);
0075     m_searchInSenders = optionsMenu->addAction(tr("Senders"));
0076     m_searchInSenders->setCheckable(true);
0077     m_searchInSenders->setChecked(true);
0078     m_searchInRecipients = optionsMenu->addAction(tr("Recipients"));
0079     m_searchInRecipients->setCheckable(true);
0080 
0081     optionsMenu->addSeparator();
0082 
0083     QMenu *complexMenu = new QMenu(tr("Complex IMAP query"), optionsMenu);
0084     connect(complexMenu, &QMenu::triggered, this, &MessageListWidget::slotComplexSearchInput);
0085     complexMenu->addAction(tr("Not ..."))->setData(QString(QLatin1String("NOT ") + m_queryPlaceholder));
0086     complexMenu->addAction(tr("Either... or..."))->setData(QString(QLatin1String("OR ") + m_queryPlaceholder + QLatin1Char(' ') + m_queryPlaceholder));
0087     complexMenu->addSeparator();
0088     complexMenu->addAction(tr("From sender"))->setData(QString(QLatin1String("FROM ") + m_queryPlaceholder));
0089     complexMenu->addAction(tr("To receiver"))->setData(QString(QLatin1String("TO ") + m_queryPlaceholder));
0090     complexMenu->addSeparator();
0091     complexMenu->addAction(tr("About subject"))->setData(QString(QLatin1String("SUBJECT " )+ m_queryPlaceholder));
0092     complexMenu->addAction(tr("Message contains ..."))->setData(QString(QLatin1String("BODY ") + m_queryPlaceholder));
0093     complexMenu->addSeparator();
0094     complexMenu->addAction(tr("Before date"))->setData(QLatin1String("BEFORE <d-mmm-yyyy>"));
0095     complexMenu->addAction(tr("Since date"))->setData(QLatin1String("SINCE <d-mmm-yyyy>"));
0096     complexMenu->addSeparator();
0097     complexMenu->addAction(tr("Has been seen"))->setData(QLatin1String("SEEN"));
0098 
0099     m_rawSearch = optionsMenu->addAction(tr("Allow raw IMAP search"));
0100     m_rawSearch->setCheckable(true);
0101     QAction *rawSearchMenu = optionsMenu->addMenu(complexMenu);
0102     rawSearchMenu->setVisible(false);
0103     connect(m_rawSearch, &QAction::toggled, rawSearchMenu, &QAction::setVisible);
0104     connect(m_rawSearch, &QAction::toggled, this, &MessageListWidget::rawSearchSettingChanged);
0105 
0106     m_searchOptions->setMenu(optionsMenu);
0107     connect(optionsMenu, &QMenu::aboutToShow, this, &MessageListWidget::slotDeActivateSimpleSearch);
0108 
0109     m_quickSearchText->addAction(m_searchOptions, QLineEdit::LeadingPosition);
0110     connect(m_searchOptions, &QAction::triggered, optionsMenu, [this, optionsMenu](){
0111         optionsMenu->popup(m_quickSearchText->mapToGlobal(QPoint(0, m_quickSearchText->height())), nullptr);
0112     });
0113 
0114     QVBoxLayout *layout = new QVBoxLayout(this);
0115     layout->setSpacing(0);
0116     layout->setContentsMargins(0, 0, 0, 0);
0117     layout->addWidget(m_quickSearchText);
0118     layout->addWidget(tree);
0119 
0120     m_searchResetTimer = new QTimer(this);
0121     m_searchResetTimer->setSingleShot(true);
0122     connect(m_searchResetTimer, &QTimer::timeout, this, &MessageListWidget::slotApplySearch);
0123 
0124     slotAutoEnableDisableSearch();
0125 }
0126 
0127 void MessageListWidget::focusSearch()
0128 {
0129     if (!m_quickSearchText->isEnabled() || m_quickSearchText->hasFocus())
0130         return;
0131     m_quickSearchText->setFocus(Qt::ShortcutFocusReason);
0132 }
0133 
0134 void MessageListWidget::focusRawSearch()
0135 {
0136     if (!m_quickSearchText->isEnabled() || m_quickSearchText->hasFocus() || !m_rawSearch->isChecked())
0137         return;
0138     m_quickSearchText->setFocus(Qt::ShortcutFocusReason);
0139     m_quickSearchText->setText(QStringLiteral(":="));
0140     m_quickSearchText->deselect();
0141     m_quickSearchText->setCursorPosition(m_quickSearchText->text().length());
0142 }
0143 
0144 void MessageListWidget::slotApplySearch()
0145 {
0146     emit requestingSearch(searchConditions());
0147 }
0148 
0149 void MessageListWidget::slotAutoEnableDisableSearch()
0150 {
0151     bool isEnabled;
0152     if (!m_quickSearchText->text().isEmpty()) {
0153         // Some search criteria are in effect and suddenly all matching messages
0154         // disappear. We have to make sure that the search bar remains enabled.
0155         isEnabled = true;
0156     } else if (tree && tree->model()) {
0157         isEnabled = tree->model()->rowCount();
0158     } else {
0159         isEnabled = false;
0160     }
0161     m_quickSearchText->setEnabled(isEnabled);
0162     m_searchOptions->setEnabled(isEnabled);
0163 }
0164 
0165 void MessageListWidget::slotSortingFailed()
0166 {
0167     QPalette pal = m_quickSearchText->palette();
0168     pal.setColor(m_quickSearchText->backgroundRole(), Qt::red);
0169     pal.setColor(m_quickSearchText->foregroundRole(), Qt::white);
0170     m_quickSearchText->setPalette(pal);
0171     QTimer::singleShot(500, this, SLOT(slotResetSortingFailed()));
0172 }
0173 
0174 void MessageListWidget::slotResetSortingFailed()
0175 {
0176     m_quickSearchText->setPalette(QPalette());
0177 }
0178 
0179 void MessageListWidget::slotConditionalSearchReset()
0180 {
0181     if (m_quickSearchText->text().isEmpty())
0182         m_searchResetTimer->start(250);
0183     else
0184         m_searchResetTimer->stop();
0185 }
0186 
0187 void MessageListWidget::slotUpdateSearchCursor()
0188 {
0189     int cp = m_quickSearchText->cursorPosition();
0190     int ts = -1, te = -1;
0191     for (int i = cp-1; i > -1; --i) {
0192         if (m_quickSearchText->text().at(i) == QLatin1Char('>'))
0193             break; // invalid
0194         if (m_quickSearchText->text().at(i) == QLatin1Char('<')) {
0195             ts = i;
0196             break; // found TagStart
0197         }
0198     }
0199     if (ts < 0)
0200         return; // not inside tag!
0201     for (int i = cp; i < m_quickSearchText->text().length(); ++i) {
0202         if (m_quickSearchText->text().at(i) == QLatin1Char('<'))
0203             break; // invalid
0204         if (m_quickSearchText->text().at(i) == QLatin1Char('>')) {
0205             te = i;
0206             break; // found TagEnd
0207         }
0208     }
0209     if (te < 0)
0210         return; // not inside tag?
0211     if (m_quickSearchText->text().midRef(ts, m_queryPlaceholder.length()) == m_queryPlaceholder)
0212         m_quickSearchText->setSelection(ts, m_queryPlaceholder.length());
0213 }
0214 
0215 void MessageListWidget::slotComplexSearchInput(QAction *act)
0216 {
0217     QString s = act->data().toString();
0218     const int selectionStart = m_quickSearchText->selectionStart() - 1;
0219     if (selectionStart > -1 && m_quickSearchText->text().at(selectionStart) != QLatin1Char(' '))
0220             s.prepend(QLatin1Char(' '));
0221     m_quickSearchText->insert(s);
0222     if (!m_quickSearchText->text().startsWith(QLatin1String(":="))) {
0223         s = m_quickSearchText->text().trimmed();
0224         m_quickSearchText->setText(QLatin1String(":=") + s);
0225     }
0226     m_quickSearchText->setFocus();
0227     const int pos = m_quickSearchText->text().indexOf(m_queryPlaceholder);
0228     if (pos > -1)
0229         m_quickSearchText->setSelection(pos, m_queryPlaceholder.length());
0230 }
0231 
0232 void MessageListWidget::slotDeActivateSimpleSearch()
0233 {
0234     const bool isEnabled = !(m_rawSearch->isChecked() && m_quickSearchText->text().startsWith(QLatin1String(":=")));
0235     m_searchInSubject->setEnabled(isEnabled);
0236     m_searchInBody->setEnabled(isEnabled);
0237     m_searchInSenders->setEnabled(isEnabled);
0238     m_searchInRecipients->setEnabled(isEnabled);
0239     m_searchFuzzy->setEnabled(isEnabled && m_supportsFuzzySearch);
0240 }
0241 
0242 QStringList MessageListWidget::searchConditions() const
0243 {
0244     if (!m_quickSearchText->isEnabled() || m_quickSearchText->text().isEmpty())
0245         return QStringList();
0246 
0247     static QString rawPrefix = QStringLiteral(":=");
0248 
0249     if (m_rawSearch->isChecked() && m_quickSearchText->text().startsWith(rawPrefix)) {
0250         // It's a "raw" IMAP search, let's simply pass it through
0251         return QStringList() << m_quickSearchText->text().mid(rawPrefix.size());
0252     }
0253 
0254     QStringList keys;
0255     if (m_searchInSubject->isChecked())
0256         keys << QStringLiteral("SUBJECT");
0257     if (m_searchInBody->isChecked())
0258         keys << QStringLiteral("BODY");
0259     if (m_searchInRecipients->isChecked())
0260         keys << QStringLiteral("TO") << QStringLiteral("CC") << QStringLiteral("BCC");
0261     if (m_searchInSenders->isChecked())
0262         keys << QStringLiteral("FROM");
0263 
0264     if (keys.isEmpty())
0265         return keys;
0266 
0267     QStringList res;
0268     Q_FOREACH(const QString &key, keys) {
0269         if (m_supportsFuzzySearch)
0270             res << QStringLiteral("FUZZY");
0271         res << key << m_quickSearchText->text();
0272     }
0273     if (keys.size() > 1) {
0274         // Got to make this a conjunction. The OR operator's reverse-polish-notation accepts just two operands, though.
0275         int num = keys.size() - 1;
0276         for (int i = 0; i < num; ++i) {
0277             res.prepend(QStringLiteral("OR"));
0278         }
0279     }
0280 
0281     return res;
0282 }
0283 
0284 void MessageListWidget::setFuzzySearchSupported(bool supported)
0285 {
0286     m_supportsFuzzySearch = supported;
0287     m_searchFuzzy->setEnabled(supported);
0288     m_searchFuzzy->setChecked(supported);
0289 }
0290 
0291 void MessageListWidget::setRawSearchEnabled(bool enabled)
0292 {
0293     m_rawSearch->setChecked(enabled);
0294 }
0295 
0296 }