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>
33 #include "IconLoader.h"
35 #include "MsgListView.h"
36 #include "ReplaceCharValidator.h"
40 MessageListWidget::MessageListWidget(QWidget
*parent
) :
41 QWidget(parent
), m_supportsFuzzySearch(false)
43 tree
= new MsgListView(this);
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 #if QT_VERSION >= 0x040700
51 m_quickSearchText
->setPlaceholderText(tr("Quick Search"));
53 m_quickSearchText
->setToolTip(tr("Type in a text to search for within this mailbox. "
54 "The icon on the left can be used to limit the search options "
55 "(like whether to include addresses or message bodies, etc)."
57 "Experts who have read RFC3501 can use the <code>:=</code> prefix and switch to a raw IMAP mode."));
58 m_queryPlaceholder
= tr("<query>");
60 connect(m_quickSearchText
, SIGNAL(returnPressed()), this, SLOT(slotApplySearch()));
61 connect(m_quickSearchText
, SIGNAL(textChanged(QString
)), this, SLOT(slotConditionalSearchReset()));
62 connect(m_quickSearchText
, SIGNAL(cursorPositionChanged(int, int)), this, SLOT(slotUpdateSearchCursor()));
64 m_searchOptions
= new QToolButton(this);
65 m_searchOptions
->setAutoRaise(true);
66 m_searchOptions
->setPopupMode(QToolButton::InstantPopup
);
67 m_searchOptions
->setText(QLatin1String("*"));
68 m_searchOptions
->setIcon(loadIcon(QLatin1String("imap-search-details")));
69 QMenu
*optionsMenu
= new QMenu(m_searchOptions
);
70 m_searchFuzzy
= optionsMenu
->addAction(tr("Fuzzy Search"));
71 m_searchFuzzy
->setCheckable(true);
72 optionsMenu
->addSeparator();
73 m_searchInSubject
= optionsMenu
->addAction(tr("Subject"));
74 m_searchInSubject
->setCheckable(true);
75 m_searchInSubject
->setChecked(true);
76 m_searchInBody
= optionsMenu
->addAction(tr("Body"));
77 m_searchInBody
->setCheckable(true);
78 m_searchInSenders
= optionsMenu
->addAction(tr("Senders"));
79 m_searchInSenders
->setCheckable(true);
80 m_searchInSenders
->setChecked(true);
81 m_searchInRecipients
= optionsMenu
->addAction(tr("Recipients"));
82 m_searchInRecipients
->setCheckable(true);
84 optionsMenu
->addSeparator();
86 QMenu
*complexMenu
= new QMenu(tr("Complex IMAP query"), optionsMenu
);
87 connect(complexMenu
, SIGNAL(triggered(QAction
*)), this, SLOT(slotComplexSearchInput(QAction
*)));
88 complexMenu
->addAction(tr("Not ..."))->setData(QLatin1String("NOT ") + m_queryPlaceholder
);
89 complexMenu
->addAction(tr("Either... or..."))->setData(QLatin1String("OR ") + m_queryPlaceholder
+ QLatin1Char(' ') + m_queryPlaceholder
);
90 complexMenu
->addSeparator();
91 complexMenu
->addAction(tr("From sender"))->setData(QLatin1String("FROM ") + m_queryPlaceholder
);
92 complexMenu
->addAction(tr("To receiver"))->setData(QLatin1String("TO ") + m_queryPlaceholder
);
93 complexMenu
->addSeparator();
94 complexMenu
->addAction(tr("About subject"))->setData(QLatin1String("SUBJECT " )+ m_queryPlaceholder
);
95 complexMenu
->addAction(tr("Message contains ..."))->setData(QLatin1String("BODY ") + m_queryPlaceholder
);
96 complexMenu
->addSeparator();
97 complexMenu
->addAction(tr("Before date"))->setData(QLatin1String("BEFORE <d-mmm-yyyy>"));
98 complexMenu
->addAction(tr("Since date"))->setData(QLatin1String("SINCE <d-mmm-yyyy>"));
99 complexMenu
->addSeparator();
100 complexMenu
->addAction(tr("Has been seen"))->setData(QLatin1String("SEEN"));
102 m_rawSearch
= optionsMenu
->addAction(tr("Allow raw IMAP search"));
103 m_rawSearch
->setCheckable(true);
104 QAction
*rawSearchMenu
= optionsMenu
->addMenu(complexMenu
);
105 rawSearchMenu
->setVisible(false);
106 connect (m_rawSearch
, SIGNAL(toggled(bool)), rawSearchMenu
, SLOT(setVisible(bool)));
107 connect (m_rawSearch
, SIGNAL(toggled(bool)), SIGNAL(rawSearchSettingChanged(bool)));
109 m_searchOptions
->setMenu(optionsMenu
);
110 connect (optionsMenu
, SIGNAL(aboutToShow()), SLOT(slotDeActivateSimpleSearch()));
112 delete m_quickSearchText
->layout();
113 QHBoxLayout
*hlayout
= new QHBoxLayout(m_quickSearchText
);
114 hlayout
->setContentsMargins(0, 0, 0, 0);
115 hlayout
->addWidget(m_searchOptions
);
116 hlayout
->addStretch();
117 hlayout
->addWidget(m_quickSearchText
->clearButton());
118 hlayout
->activate(); // this processes the layout and ensures the toolbutton has it's final dimensions
119 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
120 if (QGuiApplication::isLeftToRight())
122 if (qApp
->keyboardInputDirection() == Qt::LeftToRight
)
124 m_quickSearchText
->setTextMargins(m_searchOptions
->width(), 0, 0, 0);
125 else // ppl. in N Africa and the middle east write the wrong direction...
126 m_quickSearchText
->setTextMargins(0, 0, m_searchOptions
->width(), 0);
127 m_searchOptions
->setCursor(Qt::ArrowCursor
); // inherits I-Beam from lineedit otherwise
129 QVBoxLayout
*layout
= new QVBoxLayout(this);
130 layout
->setSpacing(0);
131 layout
->addWidget(m_quickSearchText
);
132 layout
->addWidget(tree
);
134 m_searchResetTimer
= new QTimer(this);
135 m_searchResetTimer
->setSingleShot(true);
136 connect(m_searchResetTimer
, SIGNAL(timeout()), SLOT(slotApplySearch()));
138 slotAutoEnableDisableSearch();
141 void MessageListWidget::focusSearch()
143 if (!m_quickSearchText
->isEnabled() || m_quickSearchText
->hasFocus())
145 m_quickSearchText
->setFocus(Qt::ShortcutFocusReason
);
148 void MessageListWidget::focusRawSearch()
150 if (!m_quickSearchText
->isEnabled() || m_quickSearchText
->hasFocus() || !m_rawSearch
->isChecked())
152 m_quickSearchText
->setFocus(Qt::ShortcutFocusReason
);
153 m_quickSearchText
->setText(QLatin1String(":="));
154 m_quickSearchText
->deselect();
155 m_quickSearchText
->setCursorPosition(m_quickSearchText
->text().length());
158 void MessageListWidget::slotApplySearch()
160 emit
requestingSearch(searchConditions());
163 void MessageListWidget::slotAutoEnableDisableSearch()
166 if (!m_quickSearchText
->text().isEmpty()) {
167 // Some search criteria are in effect and suddenly all matching messages
168 // disappear. We have to make sure that the search bar remains enabled.
170 } else if (tree
&& tree
->model()) {
171 isEnabled
= tree
->model()->rowCount();
175 m_quickSearchText
->setEnabled(isEnabled
);
176 m_searchOptions
->setEnabled(isEnabled
);
179 void MessageListWidget::slotSortingFailed()
181 QPalette pal
= m_quickSearchText
->palette();
182 pal
.setColor(m_quickSearchText
->backgroundRole(), Qt::red
);
183 pal
.setColor(m_quickSearchText
->foregroundRole(), Qt::white
);
184 m_quickSearchText
->setPalette(pal
);
185 QTimer::singleShot(500, this, SLOT(slotResetSortingFailed()));
188 void MessageListWidget::slotResetSortingFailed()
190 m_quickSearchText
->setPalette(QPalette());
193 void MessageListWidget::slotConditionalSearchReset()
195 if (m_quickSearchText
->text().isEmpty())
196 m_searchResetTimer
->start(250);
198 m_searchResetTimer
->stop();
201 void MessageListWidget::slotUpdateSearchCursor()
203 int cp
= m_quickSearchText
->cursorPosition();
204 int ts
= -1, te
= -1;
205 for (int i
= cp
-1; i
> -1; --i
) {
206 if (m_quickSearchText
->text().at(i
) == QLatin1Char('>'))
208 if (m_quickSearchText
->text().at(i
) == QLatin1Char('<')) {
210 break; // found TagStart
214 return; // not inside tag!
215 for (int i
= cp
; i
< m_quickSearchText
->text().length(); ++i
) {
216 if (m_quickSearchText
->text().at(i
) == QLatin1Char('<'))
218 if (m_quickSearchText
->text().at(i
) == QLatin1Char('>')) {
220 break; // found TagEnd
224 return; // not inside tag?
225 if (m_quickSearchText
->text().midRef(ts
, m_queryPlaceholder
.length()) == m_queryPlaceholder
)
226 m_quickSearchText
->setSelection(ts
, m_queryPlaceholder
.length());
229 void MessageListWidget::slotComplexSearchInput(QAction
*act
)
231 QString s
= act
->data().toString();
232 const int selectionStart
= m_quickSearchText
->selectionStart() - 1;
233 if (selectionStart
> -1 && m_quickSearchText
->text().at(selectionStart
) != QLatin1Char(' '))
234 s
.prepend(QLatin1Char(' '));
235 m_quickSearchText
->insert(s
);
236 if (!m_quickSearchText
->text().startsWith(QLatin1String(":="))) {
237 s
= m_quickSearchText
->text().trimmed();
238 m_quickSearchText
->setText(QLatin1String(":=") + s
);
240 m_quickSearchText
->setFocus();
241 const int pos
= m_quickSearchText
->text().indexOf(m_queryPlaceholder
);
243 m_quickSearchText
->setSelection(pos
, m_queryPlaceholder
.length());
246 void MessageListWidget::slotDeActivateSimpleSearch()
248 const bool isEnabled
= !(m_rawSearch
->isChecked() && m_quickSearchText
->text().startsWith(QLatin1String(":=")));
249 m_searchInSubject
->setEnabled(isEnabled
);
250 m_searchInBody
->setEnabled(isEnabled
);
251 m_searchInSenders
->setEnabled(isEnabled
);
252 m_searchInRecipients
->setEnabled(isEnabled
);
253 m_searchFuzzy
->setEnabled(isEnabled
&& m_supportsFuzzySearch
);
256 QStringList
MessageListWidget::searchConditions() const
258 if (!m_quickSearchText
->isEnabled() || m_quickSearchText
->text().isEmpty())
259 return QStringList();
261 static QString rawPrefix
= QLatin1String(":=");
263 if (m_rawSearch
->isChecked() && m_quickSearchText
->text().startsWith(rawPrefix
)) {
264 // It's a "raw" IMAP search, let's simply pass it through
265 return QStringList() << m_quickSearchText
->text().mid(rawPrefix
.size());
269 if (m_searchInSubject
->isChecked())
270 keys
<< QLatin1String("SUBJECT");
271 if (m_searchInBody
->isChecked())
272 keys
<< QLatin1String("BODY");
273 if (m_searchInRecipients
->isChecked())
274 keys
<< QLatin1String("TO") << QLatin1String("CC") << QLatin1String("BCC");
275 if (m_searchInSenders
->isChecked())
276 keys
<< QLatin1String("FROM");
282 Q_FOREACH(const QString
&key
, keys
) {
283 if (m_supportsFuzzySearch
)
284 res
<< QLatin1String("FUZZY");
285 res
<< key
<< m_quickSearchText
->text();
287 if (keys
.size() > 1) {
288 // Got to make this a conjunction. The OR operator's reverse-polish-notation accepts just two operands, though.
289 int num
= keys
.size() - 1;
290 for (int i
= 0; i
< num
; ++i
) {
291 res
.prepend(QLatin1String("OR"));
298 void MessageListWidget::setFuzzySearchSupported(bool supported
)
300 m_supportsFuzzySearch
= supported
;
301 m_searchFuzzy
->setEnabled(supported
);
302 m_searchFuzzy
->setChecked(supported
);
305 void MessageListWidget::setRawSearchEnabled(bool enabled
)
307 m_rawSearch
->setChecked(enabled
);