1 /***************************************************************************
2 * Copyright (C) 2005-2018 by the Quassel Project *
3 * devel@quassel-irc.org *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) version 3. *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
21 #include "chatviewsearchcontroller.h"
23 #include <QAbstractItemModel>
28 #include "chatlinemodel.h"
29 #include "chatscene.h"
30 #include "messagemodel.h"
32 ChatViewSearchController::ChatViewSearchController(QObject* parent)
36 void ChatViewSearchController::setSearchString(const QString& searchString)
38 QString oldSearchString = _searchString;
39 _searchString = searchString;
41 if (!searchString.startsWith(oldSearchString) || oldSearchString.isEmpty()) {
42 // we can't reuse our all findings... cler the scene and do it all over
47 updateHighlights(true);
52 void ChatViewSearchController::setScene(ChatScene* scene)
59 disconnect(_scene, nullptr, this, nullptr);
60 disconnect(Client::messageModel(), nullptr, this, nullptr);
61 qDeleteAll(_highlightItems);
62 _highlightItems.clear();
69 connect(_scene, &QObject::destroyed, this, &ChatViewSearchController::sceneDestroyed);
70 connect(_scene, &ChatScene::layoutChanged, this, [this]() { repositionHighlights(); });
71 connect(Client::messageModel(), &MessageModel::finishedBacklogFetch, this, [this]() { updateHighlights(); });
75 void ChatViewSearchController::highlightNext()
77 if (_highlightItems.isEmpty())
80 if (_currentHighlight < _highlightItems.count()) {
81 _highlightItems.at(_currentHighlight)->setHighlighted(false);
85 if (_currentHighlight >= _highlightItems.count())
86 _currentHighlight = 0;
87 _highlightItems.at(_currentHighlight)->setHighlighted(true);
88 emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
91 void ChatViewSearchController::highlightPrev()
93 if (_highlightItems.isEmpty())
96 if (_currentHighlight < _highlightItems.count()) {
97 _highlightItems.at(_currentHighlight)->setHighlighted(false);
101 if (_currentHighlight < 0)
102 _currentHighlight = _highlightItems.count() - 1;
103 _highlightItems.at(_currentHighlight)->setHighlighted(true);
104 emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
107 void ChatViewSearchController::updateHighlights(bool reuse)
113 QSet<ChatLine*> chatLines;
114 foreach (SearchHighlightItem* highlightItem, _highlightItems) {
115 auto* line = qgraphicsitem_cast<ChatLine*>(highlightItem->parentItem());
119 foreach (ChatLine* line, QList<ChatLine*>(chatLines.toList())) {
120 updateHighlights(line);
124 QPointF oldHighlightPos;
125 if (!_highlightItems.isEmpty() && _currentHighlight < _highlightItems.count()) {
126 oldHighlightPos = _highlightItems[_currentHighlight]->scenePos();
128 qDeleteAll(_highlightItems);
129 _highlightItems.clear();
130 Q_ASSERT(_highlightItems.isEmpty());
132 if (searchString().isEmpty() || !(_searchSenders || _searchMsgs))
135 checkMessagesForHighlight();
137 if (!_highlightItems.isEmpty()) {
138 if (!oldHighlightPos.isNull()) {
140 int end = _highlightItems.count() - 1;
144 startPos = _highlightItems[start]->scenePos();
145 endPos = _highlightItems[end]->scenePos();
146 if (startPos == oldHighlightPos) {
147 _currentHighlight = start;
150 if (endPos == oldHighlightPos) {
151 _currentHighlight = end;
154 if (end - start == 1) {
155 _currentHighlight = start;
158 if (end == 0 && start == 0) {
159 // Sometimes we can run into an issue where the start and end are both set
160 // to zero. Rather than endlessly spin this loop, bail out. Search seems
163 // Unfortunately, this seems specific to the contents of a buffer. First,
164 // find a buffer that you've encountered freezing, and keep track of what
165 // was loaded, where it was, and the two most recent search terms.
167 // 1. Load some backlog to a buffer
168 // 2. Search for term with any number of matches
169 // 3. Making sure to -type over existing words without first backspacing-,
170 // search for another term with only one match
171 // Expected: Search results found, no freezing
172 // Actual: Quassel hangs. startPos and endPos = same place, start = 0,
173 // end = 0, _currentHighlight appears to retain values from the
176 // Reset _currentHighlight to start, otherwise it'll retain the value from
177 // previous search, resulting in an index-out-of-bounds error.
178 _currentHighlight = start;
179 // Escape from the loop!
182 int pivot = (end + start) / 2;
183 QPointF pivotPos = _highlightItems[pivot]->scenePos();
184 if (startPos.y() == endPos.y()) {
185 if (oldHighlightPos.x() <= pivotPos.x())
191 if (oldHighlightPos.y() <= pivotPos.y())
199 _currentHighlight = _highlightItems.count() - 1;
201 _highlightItems[_currentHighlight]->setHighlighted(true);
202 emit newCurrentHighlight(_highlightItems[_currentHighlight]);
207 void ChatViewSearchController::checkMessagesForHighlight(int start, int end)
209 QAbstractItemModel* model = _scene->model();
213 end = model->rowCount() - 1;
219 for (int row = start; row <= end; row++) {
220 if (_searchOnlyRegularMsgs) {
221 index = model->index(row, 0);
222 if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
225 highlightLine(_scene->chatLine(row));
229 void ChatViewSearchController::updateHighlights(ChatLine* line)
231 QList<ChatItem*> checkItems;
233 checkItems << line->item(MessageModel::SenderColumn);
236 checkItems << line->item(MessageModel::ContentsColumn);
238 QHash<quint64, QHash<quint64, QRectF>> wordRects;
239 foreach (ChatItem* item, checkItems) {
240 foreach (QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
241 wordRects[(quint64)(wordRect.x() + item->x())][(quint64)(wordRect.y())] = wordRect;
245 bool deleteAll = false;
246 QAbstractItemModel* model = _scene->model();
248 if (_searchOnlyRegularMsgs) {
249 QModelIndex index = model->index(line->row(), 0);
250 if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
254 foreach (QGraphicsItem* child, line->childItems()) {
255 auto* highlightItem = qgraphicsitem_cast<SearchHighlightItem*>(child);
259 if (!deleteAll && wordRects.contains((quint64)(highlightItem->pos().x()))
260 && wordRects[(quint64)(highlightItem->pos().x())].contains((quint64)(highlightItem->pos().y()))) {
261 QRectF& wordRect = wordRects[(quint64)(highlightItem->pos().x())][(quint64)(highlightItem->pos().y())];
262 highlightItem->updateGeometry(wordRect.width(), wordRect.height());
265 int pos = _highlightItems.indexOf(highlightItem);
266 if (pos == _currentHighlight) {
269 else if (pos < _currentHighlight) {
273 _highlightItems.removeAt(pos);
274 delete highlightItem;
279 void ChatViewSearchController::highlightLine(ChatLine* line)
281 QList<ChatItem*> checkItems;
283 checkItems << line->item(MessageModel::SenderColumn);
286 checkItems << line->item(MessageModel::ContentsColumn);
288 foreach (ChatItem* item, checkItems) {
289 foreach (QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
290 _highlightItems << new SearchHighlightItem(wordRect.adjusted(item->x(), 0, item->x(), 0), line);
295 void ChatViewSearchController::repositionHighlights()
297 QSet<ChatLine*> chatLines;
298 foreach (SearchHighlightItem* item, _highlightItems) {
299 auto* line = qgraphicsitem_cast<ChatLine*>(item->parentItem());
303 QList<ChatLine*> chatLineList(chatLines.toList());
304 foreach (ChatLine* line, chatLineList) {
305 repositionHighlights(line);
309 void ChatViewSearchController::repositionHighlights(ChatLine* line)
311 QList<SearchHighlightItem*> searchHighlights;
312 foreach (QGraphicsItem* child, line->childItems()) {
313 auto* highlightItem = qgraphicsitem_cast<SearchHighlightItem*>(child);
315 searchHighlights << highlightItem;
318 if (searchHighlights.isEmpty())
321 QList<QPointF> wordPos;
322 if (_searchSenders) {
323 foreach (QRectF wordRect, line->senderItem()->findWords(searchString(), caseSensitive())) {
324 wordPos << QPointF(wordRect.x() + line->senderItem()->x(), wordRect.y());
328 foreach (QRectF wordRect, line->contentsItem()->findWords(searchString(), caseSensitive())) {
329 wordPos << QPointF(wordRect.x() + line->contentsItem()->x(), wordRect.y());
333 qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
335 Q_ASSERT(wordPos.count() == searchHighlights.count());
336 for (int i = 0; i < searchHighlights.count(); i++) {
337 searchHighlights.at(i)->setPos(wordPos.at(i));
341 void ChatViewSearchController::sceneDestroyed()
343 // WARNING: don't call any methods on scene!
345 // the items will be automatically deleted when the scene is destroyed
346 // so we just have to clear the list;
347 _highlightItems.clear();
350 void ChatViewSearchController::setCaseSensitive(bool caseSensitive)
352 if (_caseSensitive == caseSensitive)
355 _caseSensitive = caseSensitive;
357 // we can reuse the original search results if the new search
358 // parameters are a restriction of the original one
359 updateHighlights(caseSensitive);
362 void ChatViewSearchController::setSearchSenders(bool searchSenders)
364 if (_searchSenders == searchSenders)
367 _searchSenders = searchSenders;
368 // we can reuse the original search results if the new search
369 // parameters are a restriction of the original one
370 updateHighlights(!searchSenders);
373 void ChatViewSearchController::setSearchMsgs(bool searchMsgs)
375 if (_searchMsgs == searchMsgs)
378 _searchMsgs = searchMsgs;
380 // we can reuse the original search results if the new search
381 // parameters are a restriction of the original one
382 updateHighlights(!searchMsgs);
385 void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs)
387 if (_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
390 _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
392 // we can reuse the original search results if the new search
393 // parameters are a restriction of the original one
394 updateHighlights(searchOnlyRegularMsgs);
397 // ==================================================
398 // SearchHighlightItem
399 // ==================================================
400 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem* parent)
402 , QGraphicsItem(parent)
403 , _highlighted(false)
407 setPos(wordRect.x(), wordRect.y());
408 updateGeometry(wordRect.width(), wordRect.height());
410 connect(&_timeLine, &QTimeLine::valueChanged, this, &SearchHighlightItem::updateHighlight);
413 void SearchHighlightItem::setHighlighted(bool highlighted)
415 _highlighted = highlighted;
418 _timeLine.setDirection(QTimeLine::Forward);
420 _timeLine.setDirection(QTimeLine::Backward);
422 if (_timeLine.state() != QTimeLine::Running)
428 void SearchHighlightItem::updateHighlight(qreal value)
430 _alpha = 70 + (int)(80 * value);
434 void SearchHighlightItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
439 painter->setPen(QPen(QColor(0, 0, 0), 1.5));
440 painter->setBrush(QColor(254, 237, 45, _alpha));
441 painter->setRenderHints(QPainter::Antialiasing);
442 qreal radius = boundingRect().height() * 0.30;
443 painter->drawRoundedRect(boundingRect(), radius, radius);
446 void SearchHighlightItem::updateGeometry(qreal width, qreal height)
448 prepareGeometryChange();
449 qreal sizedelta = height * 0.1;
450 _boundingRect = QRectF(-sizedelta, -sizedelta, width + 2 * sizedelta, height + 2 * sizedelta);
454 bool SearchHighlightItem::firstInLine(QGraphicsItem* item1, QGraphicsItem* item2)
456 if (item1->pos().y() != item2->pos().y())
457 return item1->pos().y() < item2->pos().y();
459 return item1->pos().x() < item2->pos().x();