Merge "persistent color scheme selection"
[trojita.git] / src / Gui / MessageListWidget.cpp
blobd70837341290bbfb32fb59d22eaefdf3e4240f42
1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
23 #include "MessageListWidget.h"
24 #include <QAction>
25 #include <QApplication>
26 #include <QCheckBox>
27 #include <QFrame>
28 #include <QMenu>
29 #include <QTimer>
30 #include <QToolButton>
31 #include <QVBoxLayout>
32 #include <QWidgetAction>
33 #include "LineEdit.h"
34 #include "MsgListView.h"
35 #include "ReplaceCharValidator.h"
36 #include "UiUtils/IconLoader.h"
38 namespace Gui {
40 MessageListWidget::MessageListWidget(QWidget *parent, Imap::Mailbox::FavoriteTagsModel *m_favoriteTagsModel) :
41 QWidget(parent), m_supportsFuzzySearch(false)
43 tree = new MsgListView(this, m_favoriteTagsModel);
45 m_quickSearchText = new LineEdit(this);
46 m_quickSearchText->setHistoryEnabled(true);
47 // Filter out newline. It will wreak havoc into the direct IMAP passthrough and could lead to data loss.
48 QValidator *validator = new ReplaceCharValidator(QLatin1Char('\n'), QLatin1Char(' '), m_quickSearchText);
49 m_quickSearchText->setValidator(validator);
50 m_quickSearchText->setPlaceholderText(tr("Quick Search"));
51 m_quickSearchText->setToolTip(tr("Type in a text to search for within this mailbox. "
52 "The icon on the left can be used to limit the search options "
53 "(like whether to include addresses or message bodies, etc)."
54 "<br/><hr/>"
55 "Experts who have read RFC3501 can use the <code>:=</code> prefix and switch to a raw IMAP mode."));
56 m_queryPlaceholder = tr("<query>");
58 connect(m_quickSearchText, &QLineEdit::returnPressed, this, &MessageListWidget::slotApplySearch);
59 connect(m_quickSearchText, &QLineEdit::textChanged, this, &MessageListWidget::slotConditionalSearchReset);
60 connect(m_quickSearchText, &QLineEdit::cursorPositionChanged, this, &MessageListWidget::slotUpdateSearchCursor);
61 connect(m_quickSearchText, &LineEdit::escapePressed, tree, static_cast<void (QWidget::*)()>(&QWidget::setFocus));
63 m_searchOptions = new QAction(UiUtils::loadIcon(QStringLiteral("imap-search-details")), QStringLiteral("*"), this);
64 m_searchOptions->setToolTip(tr("Options for the IMAP search..."));
65 QMenu *optionsMenu = new QMenu(this);
66 m_searchOptions->setMenu(optionsMenu);
67 m_searchFuzzy = optionsMenu->addAction(tr("Fuzzy Search"));
68 m_searchFuzzy->setCheckable(true);
69 optionsMenu->addSeparator();
70 m_searchInSubject = optionsMenu->addAction(tr("Subject"));
71 m_searchInSubject->setCheckable(true);
72 m_searchInSubject->setChecked(true);
73 m_searchInBody = optionsMenu->addAction(tr("Body"));
74 m_searchInBody->setCheckable(true);
75 m_searchInSenders = optionsMenu->addAction(tr("Senders"));
76 m_searchInSenders->setCheckable(true);
77 m_searchInSenders->setChecked(true);
78 m_searchInRecipients = optionsMenu->addAction(tr("Recipients"));
79 m_searchInRecipients->setCheckable(true);
81 optionsMenu->addSeparator();
83 QMenu *complexMenu = new QMenu(tr("Complex IMAP query"), optionsMenu);
84 connect(complexMenu, &QMenu::triggered, this, &MessageListWidget::slotComplexSearchInput);
85 complexMenu->addAction(tr("Not ..."))->setData(QString(QLatin1String("NOT ") + m_queryPlaceholder));
86 complexMenu->addAction(tr("Either... or..."))->setData(QString(QLatin1String("OR ") + m_queryPlaceholder + QLatin1Char(' ') + m_queryPlaceholder));
87 complexMenu->addSeparator();
88 complexMenu->addAction(tr("From sender"))->setData(QString(QLatin1String("FROM ") + m_queryPlaceholder));
89 complexMenu->addAction(tr("To receiver"))->setData(QString(QLatin1String("TO ") + m_queryPlaceholder));
90 complexMenu->addSeparator();
91 complexMenu->addAction(tr("About subject"))->setData(QString(QLatin1String("SUBJECT " )+ m_queryPlaceholder));
92 complexMenu->addAction(tr("Message contains ..."))->setData(QString(QLatin1String("BODY ") + m_queryPlaceholder));
93 complexMenu->addSeparator();
94 complexMenu->addAction(tr("Before date"))->setData(QLatin1String("BEFORE <d-mmm-yyyy>"));
95 complexMenu->addAction(tr("Since date"))->setData(QLatin1String("SINCE <d-mmm-yyyy>"));
96 complexMenu->addSeparator();
97 complexMenu->addAction(tr("Has been seen"))->setData(QLatin1String("SEEN"));
99 m_rawSearch = optionsMenu->addAction(tr("Allow raw IMAP search"));
100 m_rawSearch->setCheckable(true);
101 QAction *rawSearchMenu = optionsMenu->addMenu(complexMenu);
102 rawSearchMenu->setVisible(false);
103 connect(m_rawSearch, &QAction::toggled, rawSearchMenu, &QAction::setVisible);
104 connect(m_rawSearch, &QAction::toggled, this, &MessageListWidget::rawSearchSettingChanged);
106 m_searchOptions->setMenu(optionsMenu);
107 connect(optionsMenu, &QMenu::aboutToShow, this, &MessageListWidget::slotDeActivateSimpleSearch);
109 m_quickSearchText->addAction(m_searchOptions, QLineEdit::LeadingPosition);
110 connect(m_searchOptions, &QAction::triggered, optionsMenu, [this, optionsMenu](){
111 optionsMenu->popup(m_quickSearchText->mapToGlobal(QPoint(0, m_quickSearchText->height())), nullptr);
114 QVBoxLayout *layout = new QVBoxLayout(this);
115 layout->setSpacing(0);
116 layout->setContentsMargins(0, 0, 0, 0);
117 layout->addWidget(m_quickSearchText);
118 layout->addWidget(tree);
120 m_searchResetTimer = new QTimer(this);
121 m_searchResetTimer->setSingleShot(true);
122 connect(m_searchResetTimer, &QTimer::timeout, this, &MessageListWidget::slotApplySearch);
124 slotAutoEnableDisableSearch();
127 void MessageListWidget::focusSearch()
129 if (!m_quickSearchText->isEnabled() || m_quickSearchText->hasFocus())
130 return;
131 m_quickSearchText->setFocus(Qt::ShortcutFocusReason);
134 void MessageListWidget::focusRawSearch()
136 if (!m_quickSearchText->isEnabled() || m_quickSearchText->hasFocus() || !m_rawSearch->isChecked())
137 return;
138 m_quickSearchText->setFocus(Qt::ShortcutFocusReason);
139 m_quickSearchText->setText(QStringLiteral(":="));
140 m_quickSearchText->deselect();
141 m_quickSearchText->setCursorPosition(m_quickSearchText->text().length());
144 void MessageListWidget::slotApplySearch()
146 emit requestingSearch(searchConditions());
149 void MessageListWidget::slotAutoEnableDisableSearch()
151 bool isEnabled;
152 if (!m_quickSearchText->text().isEmpty()) {
153 // Some search criteria are in effect and suddenly all matching messages
154 // disappear. We have to make sure that the search bar remains enabled.
155 isEnabled = true;
156 } else if (tree && tree->model()) {
157 isEnabled = tree->model()->rowCount();
158 } else {
159 isEnabled = false;
161 m_quickSearchText->setEnabled(isEnabled);
162 m_searchOptions->setEnabled(isEnabled);
165 void MessageListWidget::slotSortingFailed()
167 QPalette pal = m_quickSearchText->palette();
168 pal.setColor(m_quickSearchText->backgroundRole(), Qt::red);
169 pal.setColor(m_quickSearchText->foregroundRole(), Qt::white);
170 m_quickSearchText->setPalette(pal);
171 QTimer::singleShot(500, this, SLOT(slotResetSortingFailed()));
174 void MessageListWidget::slotResetSortingFailed()
176 m_quickSearchText->setPalette(QPalette());
179 void MessageListWidget::slotConditionalSearchReset()
181 if (m_quickSearchText->text().isEmpty())
182 m_searchResetTimer->start(250);
183 else
184 m_searchResetTimer->stop();
187 void MessageListWidget::slotUpdateSearchCursor()
189 int cp = m_quickSearchText->cursorPosition();
190 int ts = -1, te = -1;
191 for (int i = cp-1; i > -1; --i) {
192 if (m_quickSearchText->text().at(i) == QLatin1Char('>'))
193 break; // invalid
194 if (m_quickSearchText->text().at(i) == QLatin1Char('<')) {
195 ts = i;
196 break; // found TagStart
199 if (ts < 0)
200 return; // not inside tag!
201 for (int i = cp; i < m_quickSearchText->text().length(); ++i) {
202 if (m_quickSearchText->text().at(i) == QLatin1Char('<'))
203 break; // invalid
204 if (m_quickSearchText->text().at(i) == QLatin1Char('>')) {
205 te = i;
206 break; // found TagEnd
209 if (te < 0)
210 return; // not inside tag?
211 if (m_quickSearchText->text().midRef(ts, m_queryPlaceholder.length()) == m_queryPlaceholder)
212 m_quickSearchText->setSelection(ts, m_queryPlaceholder.length());
215 void MessageListWidget::slotComplexSearchInput(QAction *act)
217 QString s = act->data().toString();
218 const int selectionStart = m_quickSearchText->selectionStart() - 1;
219 if (selectionStart > -1 && m_quickSearchText->text().at(selectionStart) != QLatin1Char(' '))
220 s.prepend(QLatin1Char(' '));
221 m_quickSearchText->insert(s);
222 if (!m_quickSearchText->text().startsWith(QLatin1String(":="))) {
223 s = m_quickSearchText->text().trimmed();
224 m_quickSearchText->setText(QLatin1String(":=") + s);
226 m_quickSearchText->setFocus();
227 const int pos = m_quickSearchText->text().indexOf(m_queryPlaceholder);
228 if (pos > -1)
229 m_quickSearchText->setSelection(pos, m_queryPlaceholder.length());
232 void MessageListWidget::slotDeActivateSimpleSearch()
234 const bool isEnabled = !(m_rawSearch->isChecked() && m_quickSearchText->text().startsWith(QLatin1String(":=")));
235 m_searchInSubject->setEnabled(isEnabled);
236 m_searchInBody->setEnabled(isEnabled);
237 m_searchInSenders->setEnabled(isEnabled);
238 m_searchInRecipients->setEnabled(isEnabled);
239 m_searchFuzzy->setEnabled(isEnabled && m_supportsFuzzySearch);
242 QStringList MessageListWidget::searchConditions() const
244 if (!m_quickSearchText->isEnabled() || m_quickSearchText->text().isEmpty())
245 return QStringList();
247 static QString rawPrefix = QStringLiteral(":=");
249 if (m_rawSearch->isChecked() && m_quickSearchText->text().startsWith(rawPrefix)) {
250 // It's a "raw" IMAP search, let's simply pass it through
251 return QStringList() << m_quickSearchText->text().mid(rawPrefix.size());
254 QStringList keys;
255 if (m_searchInSubject->isChecked())
256 keys << QStringLiteral("SUBJECT");
257 if (m_searchInBody->isChecked())
258 keys << QStringLiteral("BODY");
259 if (m_searchInRecipients->isChecked())
260 keys << QStringLiteral("TO") << QStringLiteral("CC") << QStringLiteral("BCC");
261 if (m_searchInSenders->isChecked())
262 keys << QStringLiteral("FROM");
264 if (keys.isEmpty())
265 return keys;
267 QStringList res;
268 Q_FOREACH(const QString &key, keys) {
269 if (m_supportsFuzzySearch)
270 res << QStringLiteral("FUZZY");
271 res << key << m_quickSearchText->text();
273 if (keys.size() > 1) {
274 // Got to make this a conjunction. The OR operator's reverse-polish-notation accepts just two operands, though.
275 int num = keys.size() - 1;
276 for (int i = 0; i < num; ++i) {
277 res.prepend(QStringLiteral("OR"));
281 return res;
284 void MessageListWidget::setFuzzySearchSupported(bool supported)
286 m_supportsFuzzySearch = supported;
287 m_searchFuzzy->setEnabled(supported);
288 m_searchFuzzy->setChecked(supported);
291 void MessageListWidget::setRawSearchEnabled(bool enabled)
293 m_rawSearch->setChecked(enabled);