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