Semi-yearly copyright bump
[quassel.git] / src / qtui / chatviewsearchcontroller.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
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.                                           *
9  *                                                                         *
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.                          *
14  *                                                                         *
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  ***************************************************************************/
20
21 #include "chatviewsearchcontroller.h"
22
23 #include <QAbstractItemModel>
24 #include <QPainter>
25
26 #include "chatitem.h"
27 #include "chatline.h"
28 #include "chatlinemodel.h"
29 #include "chatscene.h"
30 #include "messagemodel.h"
31
32 ChatViewSearchController::ChatViewSearchController(QObject *parent)
33     : QObject(parent),
34     _scene(0),
35     _currentHighlight(0),
36     _caseSensitive(false),
37     _searchSenders(false),
38     _searchMsgs(true),
39     _searchOnlyRegularMsgs(true)
40 {
41 }
42
43
44 void ChatViewSearchController::setSearchString(const QString &searchString)
45 {
46     QString oldSearchString = _searchString;
47     _searchString = searchString;
48     if (_scene) {
49         if (!searchString.startsWith(oldSearchString) || oldSearchString.isEmpty()) {
50             // we can't reuse our all findings... cler the scene and do it all over
51             updateHighlights();
52         }
53         else {
54             // reuse all findings
55             updateHighlights(true);
56         }
57     }
58 }
59
60
61 void ChatViewSearchController::setScene(ChatScene *scene)
62 {
63     Q_ASSERT(scene);
64     if (scene == _scene)
65         return;
66
67     if (_scene) {
68         disconnect(_scene, 0, this, 0);
69         disconnect(Client::messageModel(), 0, this, 0);
70         qDeleteAll(_highlightItems);
71         _highlightItems.clear();
72     }
73
74     _scene = scene;
75     if (!scene)
76         return;
77
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()));
81     updateHighlights();
82 }
83
84
85 void ChatViewSearchController::highlightNext()
86 {
87     if (_highlightItems.isEmpty())
88         return;
89
90     if (_currentHighlight < _highlightItems.count()) {
91         _highlightItems.at(_currentHighlight)->setHighlighted(false);
92     }
93
94     _currentHighlight++;
95     if (_currentHighlight >= _highlightItems.count())
96         _currentHighlight = 0;
97     _highlightItems.at(_currentHighlight)->setHighlighted(true);
98     emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
99 }
100
101
102 void ChatViewSearchController::highlightPrev()
103 {
104     if (_highlightItems.isEmpty())
105         return;
106
107     if (_currentHighlight < _highlightItems.count()) {
108         _highlightItems.at(_currentHighlight)->setHighlighted(false);
109     }
110
111     _currentHighlight--;
112     if (_currentHighlight < 0)
113         _currentHighlight = _highlightItems.count() - 1;
114     _highlightItems.at(_currentHighlight)->setHighlighted(true);
115     emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
116 }
117
118
119 void ChatViewSearchController::updateHighlights(bool reuse)
120 {
121     if (!_scene)
122         return;
123
124     if (reuse) {
125         QSet<ChatLine *> chatLines;
126         foreach(SearchHighlightItem *highlightItem, _highlightItems) {
127             ChatLine *line = qgraphicsitem_cast<ChatLine *>(highlightItem->parentItem());
128             if (line)
129                 chatLines << line;
130         }
131         foreach(ChatLine *line, QList<ChatLine *>(chatLines.toList())) {
132             updateHighlights(line);
133         }
134     }
135     else {
136         QPointF oldHighlightPos;
137         if (!_highlightItems.isEmpty() && _currentHighlight < _highlightItems.count()) {
138             oldHighlightPos = _highlightItems[_currentHighlight]->scenePos();
139         }
140         qDeleteAll(_highlightItems);
141         _highlightItems.clear();
142         Q_ASSERT(_highlightItems.isEmpty());
143
144         if (searchString().isEmpty() || !(_searchSenders || _searchMsgs))
145             return;
146
147         checkMessagesForHighlight();
148
149         if (!_highlightItems.isEmpty()) {
150             if (!oldHighlightPos.isNull()) {
151                 int start = 0; int end = _highlightItems.count() - 1;
152                 QPointF startPos;
153                 QPointF endPos;
154                 while (1) {
155                     startPos = _highlightItems[start]->scenePos();
156                     endPos = _highlightItems[end]->scenePos();
157                     if (startPos == oldHighlightPos) {
158                         _currentHighlight = start;
159                         break;
160                     }
161                     if (endPos == oldHighlightPos) {
162                         _currentHighlight = end;
163                         break;
164                     }
165                     if (end - start == 1) {
166                         _currentHighlight = start;
167                         break;
168                     }
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
172                         // to work fine.
173                         // [Test case]
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.
177                         // For example...
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
185                         //           previous search.
186
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!
191                         break;
192                     }
193                     int pivot = (end + start) / 2;
194                     QPointF pivotPos = _highlightItems[pivot]->scenePos();
195                     if (startPos.y() == endPos.y()) {
196                         if (oldHighlightPos.x() <= pivotPos.x())
197                             end = pivot;
198                         else
199                             start = pivot;
200                     }
201                     else {
202                         if (oldHighlightPos.y() <= pivotPos.y())
203                             end = pivot;
204                         else
205                             start = pivot;
206                     }
207                 }
208             }
209             else {
210                 _currentHighlight = _highlightItems.count() - 1;
211             }
212             _highlightItems[_currentHighlight]->setHighlighted(true);
213             emit newCurrentHighlight(_highlightItems[_currentHighlight]);
214         }
215     }
216 }
217
218
219 void ChatViewSearchController::checkMessagesForHighlight(int start, int end)
220 {
221     QAbstractItemModel *model = _scene->model();
222     Q_ASSERT(model);
223
224     if (end == -1) {
225         end = model->rowCount() - 1;
226         if (end == -1)
227             return;
228     }
229
230     QModelIndex index;
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()))
235                 continue;
236         }
237         highlightLine(_scene->chatLine(row));
238     }
239 }
240
241
242 void ChatViewSearchController::updateHighlights(ChatLine *line)
243 {
244     QList<ChatItem *> checkItems;
245     if (_searchSenders)
246         checkItems << line->item(MessageModel::SenderColumn);
247
248     if (_searchMsgs)
249         checkItems << line->item(MessageModel::ContentsColumn);
250
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;
255         }
256     }
257
258     bool deleteAll = false;
259     QAbstractItemModel *model = _scene->model();
260     Q_ASSERT(model);
261     if (_searchOnlyRegularMsgs) {
262         QModelIndex index = model->index(line->row(), 0);
263         if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
264             deleteAll = true;
265     }
266
267     foreach(QGraphicsItem *child, line->childItems()) {
268         SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
269         if (!highlightItem)
270             continue;
271
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());
275         }
276         else {
277             int pos = _highlightItems.indexOf(highlightItem);
278             if (pos == _currentHighlight) {
279                 highlightPrev();
280             }
281             else if (pos < _currentHighlight) {
282                 _currentHighlight--;
283             }
284
285             _highlightItems.removeAt(pos);
286             delete highlightItem;
287         }
288     }
289 }
290
291
292 void ChatViewSearchController::highlightLine(ChatLine *line)
293 {
294     QList<ChatItem *> checkItems;
295     if (_searchSenders)
296         checkItems << line->item(MessageModel::SenderColumn);
297
298     if (_searchMsgs)
299         checkItems << line->item(MessageModel::ContentsColumn);
300
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);
304         }
305     }
306 }
307
308
309 void ChatViewSearchController::repositionHighlights()
310 {
311     QSet<ChatLine *> chatLines;
312     foreach(SearchHighlightItem *item, _highlightItems) {
313         ChatLine *line = qgraphicsitem_cast<ChatLine *>(item->parentItem());
314         if (line)
315             chatLines << line;
316     }
317     QList<ChatLine *> chatLineList(chatLines.toList());
318     foreach(ChatLine *line, chatLineList) {
319         repositionHighlights(line);
320     }
321 }
322
323
324 void ChatViewSearchController::repositionHighlights(ChatLine *line)
325 {
326     QList<SearchHighlightItem *> searchHighlights;
327     foreach(QGraphicsItem *child, line->childItems()) {
328         SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
329         if (highlightItem)
330             searchHighlights << highlightItem;
331     }
332
333     if (searchHighlights.isEmpty())
334         return;
335
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());
340         }
341     }
342     if (_searchMsgs) {
343         foreach(QRectF wordRect, line->contentsItem()->findWords(searchString(), caseSensitive())) {
344             wordPos << QPointF(wordRect.x() + line->contentsItem()->x(), wordRect.y());
345         }
346     }
347
348     qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
349
350     Q_ASSERT(wordPos.count() == searchHighlights.count());
351     for (int i = 0; i < searchHighlights.count(); i++) {
352         searchHighlights.at(i)->setPos(wordPos.at(i));
353     }
354 }
355
356
357 void ChatViewSearchController::sceneDestroyed()
358 {
359     // WARNING: don't call any methods on scene!
360     _scene = 0;
361     // the items will be automatically deleted when the scene is destroyed
362     // so we just have to clear the list;
363     _highlightItems.clear();
364 }
365
366
367 void ChatViewSearchController::setCaseSensitive(bool caseSensitive)
368 {
369     if (_caseSensitive == caseSensitive)
370         return;
371
372     _caseSensitive = caseSensitive;
373
374     // we can reuse the original search results if the new search
375     // parameters are a restriction of the original one
376     updateHighlights(caseSensitive);
377 }
378
379
380 void ChatViewSearchController::setSearchSenders(bool searchSenders)
381 {
382     if (_searchSenders == searchSenders)
383         return;
384
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);
389 }
390
391
392 void ChatViewSearchController::setSearchMsgs(bool searchMsgs)
393 {
394     if (_searchMsgs == searchMsgs)
395         return;
396
397     _searchMsgs = searchMsgs;
398
399     // we can reuse the original search results if the new search
400     // parameters are a restriction of the original one
401     updateHighlights(!searchMsgs);
402 }
403
404
405 void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs)
406 {
407     if (_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
408         return;
409
410     _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
411
412     // we can reuse the original search results if the new search
413     // parameters are a restriction of the original one
414     updateHighlights(searchOnlyRegularMsgs);
415 }
416
417
418 // ==================================================
419 //  SearchHighlightItem
420 // ==================================================
421 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem *parent)
422     : QObject(),
423     QGraphicsItem(parent),
424     _highlighted(false),
425     _alpha(70),
426     _timeLine(150)
427 {
428     setPos(wordRect.x(), wordRect.y());
429     updateGeometry(wordRect.width(), wordRect.height());
430
431     connect(&_timeLine, SIGNAL(valueChanged(qreal)), this, SLOT(updateHighlight(qreal)));
432 }
433
434
435 void SearchHighlightItem::setHighlighted(bool highlighted)
436 {
437     _highlighted = highlighted;
438
439     if (highlighted)
440         _timeLine.setDirection(QTimeLine::Forward);
441     else
442         _timeLine.setDirection(QTimeLine::Backward);
443
444     if (_timeLine.state() != QTimeLine::Running)
445         _timeLine.start();
446
447     update();
448 }
449
450
451 void SearchHighlightItem::updateHighlight(qreal value)
452 {
453     _alpha = 70 + (int)(80 * value);
454     update();
455 }
456
457
458 void SearchHighlightItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
459 {
460     Q_UNUSED(option);
461     Q_UNUSED(widget);
462
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);
468 }
469
470
471 void SearchHighlightItem::updateGeometry(qreal width, qreal height)
472 {
473     prepareGeometryChange();
474     qreal sizedelta = height * 0.1;
475     _boundingRect = QRectF(-sizedelta, -sizedelta, width + 2 * sizedelta, height + 2 * sizedelta);
476     update();
477 }
478
479
480 bool SearchHighlightItem::firstInLine(QGraphicsItem *item1, QGraphicsItem *item2)
481 {
482     if (item1->pos().y() != item2->pos().y())
483         return item1->pos().y() < item2->pos().y();
484     else
485         return item1->pos().x() < item2->pos().x();
486 }