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 _caseSensitive(false),
37 _searchSenders(false),
39 _searchOnlyRegularMsgs(true)
44 void ChatViewSearchController::setSearchString(const QString &searchString)
46 QString oldSearchString = _searchString;
47 _searchString = searchString;
49 if (!searchString.startsWith(oldSearchString) || oldSearchString.isEmpty()) {
50 // we can't reuse our all findings... cler the scene and do it all over
55 updateHighlights(true);
61 void ChatViewSearchController::setScene(ChatScene *scene)
68 disconnect(_scene, 0, this, 0);
69 disconnect(Client::messageModel(), 0, this, 0);
70 qDeleteAll(_highlightItems);
71 _highlightItems.clear();
78 connect(_scene, SIGNAL(destroyed()), this, SLOT(sceneDestroyed()));
79 connect(_scene, SIGNAL(layoutChanged()), this, SLOT(repositionHighlights()));
80 connect(Client::messageModel(), SIGNAL(finishedBacklogFetch(BufferId)), this, SLOT(updateHighlights()));
85 void ChatViewSearchController::highlightNext()
87 if (_highlightItems.isEmpty())
90 if (_currentHighlight < _highlightItems.count()) {
91 _highlightItems.at(_currentHighlight)->setHighlighted(false);
95 if (_currentHighlight >= _highlightItems.count())
96 _currentHighlight = 0;
97 _highlightItems.at(_currentHighlight)->setHighlighted(true);
98 emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
102 void ChatViewSearchController::highlightPrev()
104 if (_highlightItems.isEmpty())
107 if (_currentHighlight < _highlightItems.count()) {
108 _highlightItems.at(_currentHighlight)->setHighlighted(false);
112 if (_currentHighlight < 0)
113 _currentHighlight = _highlightItems.count() - 1;
114 _highlightItems.at(_currentHighlight)->setHighlighted(true);
115 emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
119 void ChatViewSearchController::updateHighlights(bool reuse)
125 QSet<ChatLine *> chatLines;
126 foreach(SearchHighlightItem *highlightItem, _highlightItems) {
127 ChatLine *line = qgraphicsitem_cast<ChatLine *>(highlightItem->parentItem());
131 foreach(ChatLine *line, QList<ChatLine *>(chatLines.toList())) {
132 updateHighlights(line);
136 QPointF oldHighlightPos;
137 if (!_highlightItems.isEmpty() && _currentHighlight < _highlightItems.count()) {
138 oldHighlightPos = _highlightItems[_currentHighlight]->scenePos();
140 qDeleteAll(_highlightItems);
141 _highlightItems.clear();
142 Q_ASSERT(_highlightItems.isEmpty());
144 if (searchString().isEmpty() || !(_searchSenders || _searchMsgs))
147 checkMessagesForHighlight();
149 if (!_highlightItems.isEmpty()) {
150 if (!oldHighlightPos.isNull()) {
151 int start = 0; int end = _highlightItems.count() - 1;
155 startPos = _highlightItems[start]->scenePos();
156 endPos = _highlightItems[end]->scenePos();
157 if (startPos == oldHighlightPos) {
158 _currentHighlight = start;
161 if (endPos == oldHighlightPos) {
162 _currentHighlight = end;
165 if (end - start == 1) {
166 _currentHighlight = start;
169 if (end == 0 && start == 0) {
170 // Sometimes we can run into an issue where the start and end are both set
171 // to zero. Rather than endlessly spin this loop, bail out. Search seems
174 // Unfortunately, this seems specific to the contents of a buffer. First,
175 // find a buffer that you've encountered freezing, and keep track of what
176 // was loaded, where it was, and the two most recent search terms.
178 // 1. Load some backlog to a buffer
179 // 2. Search for term with any number of matches
180 // 3. Making sure to -type over existing words without first backspacing-,
181 // search for another term with only one match
182 // Expected: Search results found, no freezing
183 // Actual: Quassel hangs. startPos and endPos = same place, start = 0,
184 // end = 0, _currentHighlight appears to retain values from the
187 // Reset _currentHighlight to start, otherwise it'll retain the value from
188 // previous search, resulting in an index-out-of-bounds error.
189 _currentHighlight = start;
190 // Escape from the loop!
193 int pivot = (end + start) / 2;
194 QPointF pivotPos = _highlightItems[pivot]->scenePos();
195 if (startPos.y() == endPos.y()) {
196 if (oldHighlightPos.x() <= pivotPos.x())
202 if (oldHighlightPos.y() <= pivotPos.y())
210 _currentHighlight = _highlightItems.count() - 1;
212 _highlightItems[_currentHighlight]->setHighlighted(true);
213 emit newCurrentHighlight(_highlightItems[_currentHighlight]);
219 void ChatViewSearchController::checkMessagesForHighlight(int start, int end)
221 QAbstractItemModel *model = _scene->model();
225 end = model->rowCount() - 1;
231 for (int row = start; row <= end; row++) {
232 if (_searchOnlyRegularMsgs) {
233 index = model->index(row, 0);
234 if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
237 highlightLine(_scene->chatLine(row));
242 void ChatViewSearchController::updateHighlights(ChatLine *line)
244 QList<ChatItem *> checkItems;
246 checkItems << line->item(MessageModel::SenderColumn);
249 checkItems << line->item(MessageModel::ContentsColumn);
251 QHash<quint64, QHash<quint64, QRectF> > wordRects;
252 foreach(ChatItem *item, checkItems) {
253 foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
254 wordRects[(quint64)(wordRect.x() + item->x())][(quint64)(wordRect.y())] = wordRect;
258 bool deleteAll = false;
259 QAbstractItemModel *model = _scene->model();
261 if (_searchOnlyRegularMsgs) {
262 QModelIndex index = model->index(line->row(), 0);
263 if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
267 foreach(QGraphicsItem *child, line->childItems()) {
268 SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
272 if (!deleteAll && wordRects.contains((quint64)(highlightItem->pos().x())) && wordRects[(quint64)(highlightItem->pos().x())].contains((quint64)(highlightItem->pos().y()))) {
273 QRectF &wordRect = wordRects[(quint64)(highlightItem->pos().x())][(quint64)(highlightItem->pos().y())];
274 highlightItem->updateGeometry(wordRect.width(), wordRect.height());
277 int pos = _highlightItems.indexOf(highlightItem);
278 if (pos == _currentHighlight) {
281 else if (pos < _currentHighlight) {
285 _highlightItems.removeAt(pos);
286 delete highlightItem;
292 void ChatViewSearchController::highlightLine(ChatLine *line)
294 QList<ChatItem *> checkItems;
296 checkItems << line->item(MessageModel::SenderColumn);
299 checkItems << line->item(MessageModel::ContentsColumn);
301 foreach(ChatItem *item, checkItems) {
302 foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
303 _highlightItems << new SearchHighlightItem(wordRect.adjusted(item->x(), 0, item->x(), 0), line);
309 void ChatViewSearchController::repositionHighlights()
311 QSet<ChatLine *> chatLines;
312 foreach(SearchHighlightItem *item, _highlightItems) {
313 ChatLine *line = qgraphicsitem_cast<ChatLine *>(item->parentItem());
317 QList<ChatLine *> chatLineList(chatLines.toList());
318 foreach(ChatLine *line, chatLineList) {
319 repositionHighlights(line);
324 void ChatViewSearchController::repositionHighlights(ChatLine *line)
326 QList<SearchHighlightItem *> searchHighlights;
327 foreach(QGraphicsItem *child, line->childItems()) {
328 SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
330 searchHighlights << highlightItem;
333 if (searchHighlights.isEmpty())
336 QList<QPointF> wordPos;
337 if (_searchSenders) {
338 foreach(QRectF wordRect, line->senderItem()->findWords(searchString(), caseSensitive())) {
339 wordPos << QPointF(wordRect.x() + line->senderItem()->x(), wordRect.y());
343 foreach(QRectF wordRect, line->contentsItem()->findWords(searchString(), caseSensitive())) {
344 wordPos << QPointF(wordRect.x() + line->contentsItem()->x(), wordRect.y());
348 qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
350 Q_ASSERT(wordPos.count() == searchHighlights.count());
351 for (int i = 0; i < searchHighlights.count(); i++) {
352 searchHighlights.at(i)->setPos(wordPos.at(i));
357 void ChatViewSearchController::sceneDestroyed()
359 // WARNING: don't call any methods on scene!
361 // the items will be automatically deleted when the scene is destroyed
362 // so we just have to clear the list;
363 _highlightItems.clear();
367 void ChatViewSearchController::setCaseSensitive(bool caseSensitive)
369 if (_caseSensitive == caseSensitive)
372 _caseSensitive = caseSensitive;
374 // we can reuse the original search results if the new search
375 // parameters are a restriction of the original one
376 updateHighlights(caseSensitive);
380 void ChatViewSearchController::setSearchSenders(bool searchSenders)
382 if (_searchSenders == searchSenders)
385 _searchSenders = searchSenders;
386 // we can reuse the original search results if the new search
387 // parameters are a restriction of the original one
388 updateHighlights(!searchSenders);
392 void ChatViewSearchController::setSearchMsgs(bool searchMsgs)
394 if (_searchMsgs == searchMsgs)
397 _searchMsgs = searchMsgs;
399 // we can reuse the original search results if the new search
400 // parameters are a restriction of the original one
401 updateHighlights(!searchMsgs);
405 void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs)
407 if (_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
410 _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
412 // we can reuse the original search results if the new search
413 // parameters are a restriction of the original one
414 updateHighlights(searchOnlyRegularMsgs);
418 // ==================================================
419 // SearchHighlightItem
420 // ==================================================
421 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem *parent)
423 QGraphicsItem(parent),
428 setPos(wordRect.x(), wordRect.y());
429 updateGeometry(wordRect.width(), wordRect.height());
431 connect(&_timeLine, SIGNAL(valueChanged(qreal)), this, SLOT(updateHighlight(qreal)));
435 void SearchHighlightItem::setHighlighted(bool highlighted)
437 _highlighted = highlighted;
440 _timeLine.setDirection(QTimeLine::Forward);
442 _timeLine.setDirection(QTimeLine::Backward);
444 if (_timeLine.state() != QTimeLine::Running)
451 void SearchHighlightItem::updateHighlight(qreal value)
453 _alpha = 70 + (int)(80 * value);
458 void SearchHighlightItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
463 painter->setPen(QPen(QColor(0, 0, 0), 1.5));
464 painter->setBrush(QColor(254, 237, 45, _alpha));
465 painter->setRenderHints(QPainter::Antialiasing);
466 qreal radius = boundingRect().height() * 0.30;
467 painter->drawRoundedRect(boundingRect(), radius, radius);
471 void SearchHighlightItem::updateGeometry(qreal width, qreal height)
473 prepareGeometryChange();
474 qreal sizedelta = height * 0.1;
475 _boundingRect = QRectF(-sizedelta, -sizedelta, width + 2 * sizedelta, height + 2 * sizedelta);
480 bool SearchHighlightItem::firstInLine(QGraphicsItem *item1, QGraphicsItem *item2)
482 if (item1->pos().y() != item2->pos().y())
483 return item1->pos().y() < item2->pos().y();
485 return item1->pos().x() < item2->pos().x();