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 }