clazy: Convert many old-style connects into function pointer based
[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 {
35 }
36
37
38 void ChatViewSearchController::setSearchString(const QString &searchString)
39 {
40     QString oldSearchString = _searchString;
41     _searchString = searchString;
42     if (_scene) {
43         if (!searchString.startsWith(oldSearchString) || oldSearchString.isEmpty()) {
44             // we can't reuse our all findings... cler the scene and do it all over
45             updateHighlights();
46         }
47         else {
48             // reuse all findings
49             updateHighlights(true);
50         }
51     }
52 }
53
54
55 void ChatViewSearchController::setScene(ChatScene *scene)
56 {
57     Q_ASSERT(scene);
58     if (scene == _scene)
59         return;
60
61     if (_scene) {
62         disconnect(_scene, nullptr, this, nullptr);
63         disconnect(Client::messageModel(), nullptr, this, nullptr);
64         qDeleteAll(_highlightItems);
65         _highlightItems.clear();
66     }
67
68     _scene = scene;
69     if (!scene)
70         return;
71
72     connect(_scene, &QObject::destroyed, this, &ChatViewSearchController::sceneDestroyed);
73     connect(_scene, SIGNAL(layoutChanged()), this, SLOT(repositionHighlights()));
74     connect(Client::messageModel(), SIGNAL(finishedBacklogFetch(BufferId)), this, SLOT(updateHighlights()));
75     updateHighlights();
76 }
77
78
79 void ChatViewSearchController::highlightNext()
80 {
81     if (_highlightItems.isEmpty())
82         return;
83
84     if (_currentHighlight < _highlightItems.count()) {
85         _highlightItems.at(_currentHighlight)->setHighlighted(false);
86     }
87
88     _currentHighlight++;
89     if (_currentHighlight >= _highlightItems.count())
90         _currentHighlight = 0;
91     _highlightItems.at(_currentHighlight)->setHighlighted(true);
92     emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
93 }
94
95
96 void ChatViewSearchController::highlightPrev()
97 {
98     if (_highlightItems.isEmpty())
99         return;
100
101     if (_currentHighlight < _highlightItems.count()) {
102         _highlightItems.at(_currentHighlight)->setHighlighted(false);
103     }
104
105     _currentHighlight--;
106     if (_currentHighlight < 0)
107         _currentHighlight = _highlightItems.count() - 1;
108     _highlightItems.at(_currentHighlight)->setHighlighted(true);
109     emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
110 }
111
112
113 void ChatViewSearchController::updateHighlights(bool reuse)
114 {
115     if (!_scene)
116         return;
117
118     if (reuse) {
119         QSet<ChatLine *> chatLines;
120         foreach(SearchHighlightItem *highlightItem, _highlightItems) {
121             auto *line = qgraphicsitem_cast<ChatLine *>(highlightItem->parentItem());
122             if (line)
123                 chatLines << line;
124         }
125         foreach(ChatLine *line, QList<ChatLine *>(chatLines.toList())) {
126             updateHighlights(line);
127         }
128     }
129     else {
130         QPointF oldHighlightPos;
131         if (!_highlightItems.isEmpty() && _currentHighlight < _highlightItems.count()) {
132             oldHighlightPos = _highlightItems[_currentHighlight]->scenePos();
133         }
134         qDeleteAll(_highlightItems);
135         _highlightItems.clear();
136         Q_ASSERT(_highlightItems.isEmpty());
137
138         if (searchString().isEmpty() || !(_searchSenders || _searchMsgs))
139             return;
140
141         checkMessagesForHighlight();
142
143         if (!_highlightItems.isEmpty()) {
144             if (!oldHighlightPos.isNull()) {
145                 int start = 0; int end = _highlightItems.count() - 1;
146                 QPointF startPos;
147                 QPointF endPos;
148                 while (true) {
149                     startPos = _highlightItems[start]->scenePos();
150                     endPos = _highlightItems[end]->scenePos();
151                     if (startPos == oldHighlightPos) {
152                         _currentHighlight = start;
153                         break;
154                     }
155                     if (endPos == oldHighlightPos) {
156                         _currentHighlight = end;
157                         break;
158                     }
159                     if (end - start == 1) {
160                         _currentHighlight = start;
161                         break;
162                     }
163                     if (end == 0 && start == 0) {
164                         // Sometimes we can run into an issue where the start and end are both set
165                         // to zero.  Rather than endlessly spin this loop, bail out.  Search seems
166                         // to work fine.
167                         // [Test case]
168                         // Unfortunately, this seems specific to the contents of a buffer.  First,
169                         // find a buffer that you've encountered freezing, and keep track of what
170                         // was loaded, where it was, and the two most recent search terms.
171                         // For example...
172                         // 1.  Load some backlog to a buffer
173                         // 2.  Search for term with any number of matches
174                         // 3.  Making sure to -type over existing words without first backspacing-,
175                         //     search for another term with only one match
176                         // Expected: Search results found, no freezing
177                         // Actual:   Quassel hangs.  startPos and endPos = same place, start = 0,
178                         //           end = 0, _currentHighlight appears to retain values from the
179                         //           previous search.
180
181                         // Reset _currentHighlight to start, otherwise it'll retain the value from
182                         // previous search, resulting in an index-out-of-bounds error.
183                         _currentHighlight = start;
184                         // Escape from the loop!
185                         break;
186                     }
187                     int pivot = (end + start) / 2;
188                     QPointF pivotPos = _highlightItems[pivot]->scenePos();
189                     if (startPos.y() == endPos.y()) {
190                         if (oldHighlightPos.x() <= pivotPos.x())
191                             end = pivot;
192                         else
193                             start = pivot;
194                     }
195                     else {
196                         if (oldHighlightPos.y() <= pivotPos.y())
197                             end = pivot;
198                         else
199                             start = pivot;
200                     }
201                 }
202             }
203             else {
204                 _currentHighlight = _highlightItems.count() - 1;
205             }
206             _highlightItems[_currentHighlight]->setHighlighted(true);
207             emit newCurrentHighlight(_highlightItems[_currentHighlight]);
208         }
209     }
210 }
211
212
213 void ChatViewSearchController::checkMessagesForHighlight(int start, int end)
214 {
215     QAbstractItemModel *model = _scene->model();
216     Q_ASSERT(model);
217
218     if (end == -1) {
219         end = model->rowCount() - 1;
220         if (end == -1)
221             return;
222     }
223
224     QModelIndex index;
225     for (int row = start; row <= end; row++) {
226         if (_searchOnlyRegularMsgs) {
227             index = model->index(row, 0);
228             if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
229                 continue;
230         }
231         highlightLine(_scene->chatLine(row));
232     }
233 }
234
235
236 void ChatViewSearchController::updateHighlights(ChatLine *line)
237 {
238     QList<ChatItem *> checkItems;
239     if (_searchSenders)
240         checkItems << line->item(MessageModel::SenderColumn);
241
242     if (_searchMsgs)
243         checkItems << line->item(MessageModel::ContentsColumn);
244
245     QHash<quint64, QHash<quint64, QRectF> > wordRects;
246     foreach(ChatItem *item, checkItems) {
247         foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
248             wordRects[(quint64)(wordRect.x() + item->x())][(quint64)(wordRect.y())] = wordRect;
249         }
250     }
251
252     bool deleteAll = false;
253     QAbstractItemModel *model = _scene->model();
254     Q_ASSERT(model);
255     if (_searchOnlyRegularMsgs) {
256         QModelIndex index = model->index(line->row(), 0);
257         if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
258             deleteAll = true;
259     }
260
261     foreach(QGraphicsItem *child, line->childItems()) {
262         auto *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
263         if (!highlightItem)
264             continue;
265
266         if (!deleteAll && wordRects.contains((quint64)(highlightItem->pos().x())) && wordRects[(quint64)(highlightItem->pos().x())].contains((quint64)(highlightItem->pos().y()))) {
267             QRectF &wordRect = wordRects[(quint64)(highlightItem->pos().x())][(quint64)(highlightItem->pos().y())];
268             highlightItem->updateGeometry(wordRect.width(), wordRect.height());
269         }
270         else {
271             int pos = _highlightItems.indexOf(highlightItem);
272             if (pos == _currentHighlight) {
273                 highlightPrev();
274             }
275             else if (pos < _currentHighlight) {
276                 _currentHighlight--;
277             }
278
279             _highlightItems.removeAt(pos);
280             delete highlightItem;
281         }
282     }
283 }
284
285
286 void ChatViewSearchController::highlightLine(ChatLine *line)
287 {
288     QList<ChatItem *> checkItems;
289     if (_searchSenders)
290         checkItems << line->item(MessageModel::SenderColumn);
291
292     if (_searchMsgs)
293         checkItems << line->item(MessageModel::ContentsColumn);
294
295     foreach(ChatItem *item, checkItems) {
296         foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
297             _highlightItems << new SearchHighlightItem(wordRect.adjusted(item->x(), 0, item->x(), 0), line);
298         }
299     }
300 }
301
302
303 void ChatViewSearchController::repositionHighlights()
304 {
305     QSet<ChatLine *> chatLines;
306     foreach(SearchHighlightItem *item, _highlightItems) {
307         auto *line = qgraphicsitem_cast<ChatLine *>(item->parentItem());
308         if (line)
309             chatLines << line;
310     }
311     QList<ChatLine *> chatLineList(chatLines.toList());
312     foreach(ChatLine *line, chatLineList) {
313         repositionHighlights(line);
314     }
315 }
316
317
318 void ChatViewSearchController::repositionHighlights(ChatLine *line)
319 {
320     QList<SearchHighlightItem *> searchHighlights;
321     foreach(QGraphicsItem *child, line->childItems()) {
322         auto *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
323         if (highlightItem)
324             searchHighlights << highlightItem;
325     }
326
327     if (searchHighlights.isEmpty())
328         return;
329
330     QList<QPointF> wordPos;
331     if (_searchSenders) {
332         foreach(QRectF wordRect, line->senderItem()->findWords(searchString(), caseSensitive())) {
333             wordPos << QPointF(wordRect.x() + line->senderItem()->x(), wordRect.y());
334         }
335     }
336     if (_searchMsgs) {
337         foreach(QRectF wordRect, line->contentsItem()->findWords(searchString(), caseSensitive())) {
338             wordPos << QPointF(wordRect.x() + line->contentsItem()->x(), wordRect.y());
339         }
340     }
341
342     qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
343
344     Q_ASSERT(wordPos.count() == searchHighlights.count());
345     for (int i = 0; i < searchHighlights.count(); i++) {
346         searchHighlights.at(i)->setPos(wordPos.at(i));
347     }
348 }
349
350
351 void ChatViewSearchController::sceneDestroyed()
352 {
353     // WARNING: don't call any methods on scene!
354     _scene = nullptr;
355     // the items will be automatically deleted when the scene is destroyed
356     // so we just have to clear the list;
357     _highlightItems.clear();
358 }
359
360
361 void ChatViewSearchController::setCaseSensitive(bool caseSensitive)
362 {
363     if (_caseSensitive == caseSensitive)
364         return;
365
366     _caseSensitive = caseSensitive;
367
368     // we can reuse the original search results if the new search
369     // parameters are a restriction of the original one
370     updateHighlights(caseSensitive);
371 }
372
373
374 void ChatViewSearchController::setSearchSenders(bool searchSenders)
375 {
376     if (_searchSenders == searchSenders)
377         return;
378
379     _searchSenders = searchSenders;
380     // we can reuse the original search results if the new search
381     // parameters are a restriction of the original one
382     updateHighlights(!searchSenders);
383 }
384
385
386 void ChatViewSearchController::setSearchMsgs(bool searchMsgs)
387 {
388     if (_searchMsgs == searchMsgs)
389         return;
390
391     _searchMsgs = searchMsgs;
392
393     // we can reuse the original search results if the new search
394     // parameters are a restriction of the original one
395     updateHighlights(!searchMsgs);
396 }
397
398
399 void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs)
400 {
401     if (_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
402         return;
403
404     _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
405
406     // we can reuse the original search results if the new search
407     // parameters are a restriction of the original one
408     updateHighlights(searchOnlyRegularMsgs);
409 }
410
411
412 // ==================================================
413 //  SearchHighlightItem
414 // ==================================================
415 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem *parent)
416     : QObject(),
417     QGraphicsItem(parent),
418     _highlighted(false),
419     _alpha(70),
420     _timeLine(150)
421 {
422     setPos(wordRect.x(), wordRect.y());
423     updateGeometry(wordRect.width(), wordRect.height());
424
425     connect(&_timeLine, &QTimeLine::valueChanged, this, &SearchHighlightItem::updateHighlight);
426 }
427
428
429 void SearchHighlightItem::setHighlighted(bool highlighted)
430 {
431     _highlighted = highlighted;
432
433     if (highlighted)
434         _timeLine.setDirection(QTimeLine::Forward);
435     else
436         _timeLine.setDirection(QTimeLine::Backward);
437
438     if (_timeLine.state() != QTimeLine::Running)
439         _timeLine.start();
440
441     update();
442 }
443
444
445 void SearchHighlightItem::updateHighlight(qreal value)
446 {
447     _alpha = 70 + (int)(80 * value);
448     update();
449 }
450
451
452 void SearchHighlightItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
453 {
454     Q_UNUSED(option);
455     Q_UNUSED(widget);
456
457     painter->setPen(QPen(QColor(0, 0, 0), 1.5));
458     painter->setBrush(QColor(254, 237, 45, _alpha));
459     painter->setRenderHints(QPainter::Antialiasing);
460     qreal radius = boundingRect().height() * 0.30;
461     painter->drawRoundedRect(boundingRect(), radius, radius);
462 }
463
464
465 void SearchHighlightItem::updateGeometry(qreal width, qreal height)
466 {
467     prepareGeometryChange();
468     qreal sizedelta = height * 0.1;
469     _boundingRect = QRectF(-sizedelta, -sizedelta, width + 2 * sizedelta, height + 2 * sizedelta);
470     update();
471 }
472
473
474 bool SearchHighlightItem::firstInLine(QGraphicsItem *item1, QGraphicsItem *item2)
475 {
476     if (item1->pos().y() != item2->pos().y())
477         return item1->pos().y() < item2->pos().y();
478     else
479         return item1->pos().x() < item2->pos().x();
480 }