modernize: Reformat ALL the source... again!
[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 void ChatViewSearchController::setSearchString(const QString& searchString)
37 {
38     QString oldSearchString = _searchString;
39     _searchString = searchString;
40     if (_scene) {
41         if (!searchString.startsWith(oldSearchString) || oldSearchString.isEmpty()) {
42             // we can't reuse our all findings... cler the scene and do it all over
43             updateHighlights();
44         }
45         else {
46             // reuse all findings
47             updateHighlights(true);
48         }
49     }
50 }
51
52 void ChatViewSearchController::setScene(ChatScene* scene)
53 {
54     Q_ASSERT(scene);
55     if (scene == _scene)
56         return;
57
58     if (_scene) {
59         disconnect(_scene, nullptr, this, nullptr);
60         disconnect(Client::messageModel(), nullptr, this, nullptr);
61         qDeleteAll(_highlightItems);
62         _highlightItems.clear();
63     }
64
65     _scene = scene;
66     if (!scene)
67         return;
68
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(); });
72     updateHighlights();
73 }
74
75 void ChatViewSearchController::highlightNext()
76 {
77     if (_highlightItems.isEmpty())
78         return;
79
80     if (_currentHighlight < _highlightItems.count()) {
81         _highlightItems.at(_currentHighlight)->setHighlighted(false);
82     }
83
84     _currentHighlight++;
85     if (_currentHighlight >= _highlightItems.count())
86         _currentHighlight = 0;
87     _highlightItems.at(_currentHighlight)->setHighlighted(true);
88     emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
89 }
90
91 void ChatViewSearchController::highlightPrev()
92 {
93     if (_highlightItems.isEmpty())
94         return;
95
96     if (_currentHighlight < _highlightItems.count()) {
97         _highlightItems.at(_currentHighlight)->setHighlighted(false);
98     }
99
100     _currentHighlight--;
101     if (_currentHighlight < 0)
102         _currentHighlight = _highlightItems.count() - 1;
103     _highlightItems.at(_currentHighlight)->setHighlighted(true);
104     emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
105 }
106
107 void ChatViewSearchController::updateHighlights(bool reuse)
108 {
109     if (!_scene)
110         return;
111
112     if (reuse) {
113         QSet<ChatLine*> chatLines;
114         foreach (SearchHighlightItem* highlightItem, _highlightItems) {
115             auto* line = qgraphicsitem_cast<ChatLine*>(highlightItem->parentItem());
116             if (line)
117                 chatLines << line;
118         }
119         foreach (ChatLine* line, QList<ChatLine*>(chatLines.toList())) {
120             updateHighlights(line);
121         }
122     }
123     else {
124         QPointF oldHighlightPos;
125         if (!_highlightItems.isEmpty() && _currentHighlight < _highlightItems.count()) {
126             oldHighlightPos = _highlightItems[_currentHighlight]->scenePos();
127         }
128         qDeleteAll(_highlightItems);
129         _highlightItems.clear();
130         Q_ASSERT(_highlightItems.isEmpty());
131
132         if (searchString().isEmpty() || !(_searchSenders || _searchMsgs))
133             return;
134
135         checkMessagesForHighlight();
136
137         if (!_highlightItems.isEmpty()) {
138             if (!oldHighlightPos.isNull()) {
139                 int start = 0;
140                 int end = _highlightItems.count() - 1;
141                 QPointF startPos;
142                 QPointF endPos;
143                 while (true) {
144                     startPos = _highlightItems[start]->scenePos();
145                     endPos = _highlightItems[end]->scenePos();
146                     if (startPos == oldHighlightPos) {
147                         _currentHighlight = start;
148                         break;
149                     }
150                     if (endPos == oldHighlightPos) {
151                         _currentHighlight = end;
152                         break;
153                     }
154                     if (end - start == 1) {
155                         _currentHighlight = start;
156                         break;
157                     }
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
161                         // to work fine.
162                         // [Test case]
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.
166                         // For example...
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
174                         //           previous search.
175
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!
180                         break;
181                     }
182                     int pivot = (end + start) / 2;
183                     QPointF pivotPos = _highlightItems[pivot]->scenePos();
184                     if (startPos.y() == endPos.y()) {
185                         if (oldHighlightPos.x() <= pivotPos.x())
186                             end = pivot;
187                         else
188                             start = pivot;
189                     }
190                     else {
191                         if (oldHighlightPos.y() <= pivotPos.y())
192                             end = pivot;
193                         else
194                             start = pivot;
195                     }
196                 }
197             }
198             else {
199                 _currentHighlight = _highlightItems.count() - 1;
200             }
201             _highlightItems[_currentHighlight]->setHighlighted(true);
202             emit newCurrentHighlight(_highlightItems[_currentHighlight]);
203         }
204     }
205 }
206
207 void ChatViewSearchController::checkMessagesForHighlight(int start, int end)
208 {
209     QAbstractItemModel* model = _scene->model();
210     Q_ASSERT(model);
211
212     if (end == -1) {
213         end = model->rowCount() - 1;
214         if (end == -1)
215             return;
216     }
217
218     QModelIndex index;
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()))
223                 continue;
224         }
225         highlightLine(_scene->chatLine(row));
226     }
227 }
228
229 void ChatViewSearchController::updateHighlights(ChatLine* line)
230 {
231     QList<ChatItem*> checkItems;
232     if (_searchSenders)
233         checkItems << line->item(MessageModel::SenderColumn);
234
235     if (_searchMsgs)
236         checkItems << line->item(MessageModel::ContentsColumn);
237
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;
242         }
243     }
244
245     bool deleteAll = false;
246     QAbstractItemModel* model = _scene->model();
247     Q_ASSERT(model);
248     if (_searchOnlyRegularMsgs) {
249         QModelIndex index = model->index(line->row(), 0);
250         if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
251             deleteAll = true;
252     }
253
254     foreach (QGraphicsItem* child, line->childItems()) {
255         auto* highlightItem = qgraphicsitem_cast<SearchHighlightItem*>(child);
256         if (!highlightItem)
257             continue;
258
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());
263         }
264         else {
265             int pos = _highlightItems.indexOf(highlightItem);
266             if (pos == _currentHighlight) {
267                 highlightPrev();
268             }
269             else if (pos < _currentHighlight) {
270                 _currentHighlight--;
271             }
272
273             _highlightItems.removeAt(pos);
274             delete highlightItem;
275         }
276     }
277 }
278
279 void ChatViewSearchController::highlightLine(ChatLine* line)
280 {
281     QList<ChatItem*> checkItems;
282     if (_searchSenders)
283         checkItems << line->item(MessageModel::SenderColumn);
284
285     if (_searchMsgs)
286         checkItems << line->item(MessageModel::ContentsColumn);
287
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);
291         }
292     }
293 }
294
295 void ChatViewSearchController::repositionHighlights()
296 {
297     QSet<ChatLine*> chatLines;
298     foreach (SearchHighlightItem* item, _highlightItems) {
299         auto* line = qgraphicsitem_cast<ChatLine*>(item->parentItem());
300         if (line)
301             chatLines << line;
302     }
303     QList<ChatLine*> chatLineList(chatLines.toList());
304     foreach (ChatLine* line, chatLineList) {
305         repositionHighlights(line);
306     }
307 }
308
309 void ChatViewSearchController::repositionHighlights(ChatLine* line)
310 {
311     QList<SearchHighlightItem*> searchHighlights;
312     foreach (QGraphicsItem* child, line->childItems()) {
313         auto* highlightItem = qgraphicsitem_cast<SearchHighlightItem*>(child);
314         if (highlightItem)
315             searchHighlights << highlightItem;
316     }
317
318     if (searchHighlights.isEmpty())
319         return;
320
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());
325         }
326     }
327     if (_searchMsgs) {
328         foreach (QRectF wordRect, line->contentsItem()->findWords(searchString(), caseSensitive())) {
329             wordPos << QPointF(wordRect.x() + line->contentsItem()->x(), wordRect.y());
330         }
331     }
332
333     qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
334
335     Q_ASSERT(wordPos.count() == searchHighlights.count());
336     for (int i = 0; i < searchHighlights.count(); i++) {
337         searchHighlights.at(i)->setPos(wordPos.at(i));
338     }
339 }
340
341 void ChatViewSearchController::sceneDestroyed()
342 {
343     // WARNING: don't call any methods on scene!
344     _scene = nullptr;
345     // the items will be automatically deleted when the scene is destroyed
346     // so we just have to clear the list;
347     _highlightItems.clear();
348 }
349
350 void ChatViewSearchController::setCaseSensitive(bool caseSensitive)
351 {
352     if (_caseSensitive == caseSensitive)
353         return;
354
355     _caseSensitive = caseSensitive;
356
357     // we can reuse the original search results if the new search
358     // parameters are a restriction of the original one
359     updateHighlights(caseSensitive);
360 }
361
362 void ChatViewSearchController::setSearchSenders(bool searchSenders)
363 {
364     if (_searchSenders == searchSenders)
365         return;
366
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);
371 }
372
373 void ChatViewSearchController::setSearchMsgs(bool searchMsgs)
374 {
375     if (_searchMsgs == searchMsgs)
376         return;
377
378     _searchMsgs = searchMsgs;
379
380     // we can reuse the original search results if the new search
381     // parameters are a restriction of the original one
382     updateHighlights(!searchMsgs);
383 }
384
385 void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs)
386 {
387     if (_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
388         return;
389
390     _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
391
392     // we can reuse the original search results if the new search
393     // parameters are a restriction of the original one
394     updateHighlights(searchOnlyRegularMsgs);
395 }
396
397 // ==================================================
398 //  SearchHighlightItem
399 // ==================================================
400 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem* parent)
401     : QObject()
402     , QGraphicsItem(parent)
403     , _highlighted(false)
404     , _alpha(70)
405     , _timeLine(150)
406 {
407     setPos(wordRect.x(), wordRect.y());
408     updateGeometry(wordRect.width(), wordRect.height());
409
410     connect(&_timeLine, &QTimeLine::valueChanged, this, &SearchHighlightItem::updateHighlight);
411 }
412
413 void SearchHighlightItem::setHighlighted(bool highlighted)
414 {
415     _highlighted = highlighted;
416
417     if (highlighted)
418         _timeLine.setDirection(QTimeLine::Forward);
419     else
420         _timeLine.setDirection(QTimeLine::Backward);
421
422     if (_timeLine.state() != QTimeLine::Running)
423         _timeLine.start();
424
425     update();
426 }
427
428 void SearchHighlightItem::updateHighlight(qreal value)
429 {
430     _alpha = 70 + (int)(80 * value);
431     update();
432 }
433
434 void SearchHighlightItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
435 {
436     Q_UNUSED(option);
437     Q_UNUSED(widget);
438
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);
444 }
445
446 void SearchHighlightItem::updateGeometry(qreal width, qreal height)
447 {
448     prepareGeometryChange();
449     qreal sizedelta = height * 0.1;
450     _boundingRect = QRectF(-sizedelta, -sizedelta, width + 2 * sizedelta, height + 2 * sizedelta);
451     update();
452 }
453
454 bool SearchHighlightItem::firstInLine(QGraphicsItem* item1, QGraphicsItem* item2)
455 {
456     if (item1->pos().y() != item2->pos().y())
457         return item1->pos().y() < item2->pos().y();
458     else
459         return item1->pos().x() < item2->pos().x();
460 }