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"
25 #include <QApplication>
30 #include <QToolButton>
31 #include <QVBoxLayout>
32 #include <QWidgetAction>
34 #include "MsgListView.h"
35 #include "ReplaceCharValidator.h"
36 #include "UiUtils/IconLoader.h"
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)."
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())
131 m_quickSearchText
->setFocus(Qt::ShortcutFocusReason
);
134 void MessageListWidget::focusRawSearch()
136 if (!m_quickSearchText
->isEnabled() || m_quickSearchText
->hasFocus() || !m_rawSearch
->isChecked())
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()
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.
156 } else if (tree
&& tree
->model()) {
157 isEnabled
= tree
->model()->rowCount();
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);
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('>'))
194 if (m_quickSearchText
->text().at(i
) == QLatin1Char('<')) {
196 break; // found TagStart
200 return; // not inside tag!
201 for (int i
= cp
; i
< m_quickSearchText
->text().length(); ++i
) {
202 if (m_quickSearchText
->text().at(i
) == QLatin1Char('<'))
204 if (m_quickSearchText
->text().at(i
) == QLatin1Char('>')) {
206 break; // found TagEnd
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
);
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());
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");
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"));
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
);