cmake: avoid de-duplication of user's CXXFLAGS
[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, chatLines) {
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     foreach (ChatLine* line, chatLines) {
306         repositionHighlights(line);
307     }
308 }
309
310 void ChatViewSearchController::repositionHighlights(ChatLine* line)
311 {
312     QList<SearchHighlightItem*> searchHighlights;
313     foreach (QGraphicsItem* child, line->childItems()) {
314         auto* highlightItem = qgraphicsitem_cast<SearchHighlightItem*>(child);
315         if (highlightItem)
316             searchHighlights << highlightItem;
317     }
318
319     if (searchHighlights.isEmpty())
320         return;
321
322     QList<QPointF> wordPos;
323     if (_searchSenders) {
324         foreach (QRectF wordRect, line->senderItem()->findWords(searchString(), caseSensitive())) {
325             wordPos << QPointF(wordRect.x() + line->senderItem()->x(), wordRect.y());
326         }
327     }
328     if (_searchMsgs) {
329         foreach (QRectF wordRect, line->contentsItem()->findWords(searchString(), caseSensitive())) {
330             wordPos << QPointF(wordRect.x() + line->contentsItem()->x(), wordRect.y());
331         }
332     }
333
334     std::sort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
335
336     Q_ASSERT(wordPos.count() == searchHighlights.count());
337     for (int i = 0; i < searchHighlights.count(); i++) {
338         searchHighlights.at(i)->setPos(wordPos.at(i));
339     }
340 }
341
342 void ChatViewSearchController::sceneDestroyed()
343 {
344     // WARNING: don't call any methods on scene!
345     _scene = nullptr;
346     // the items will be automatically deleted when the scene is destroyed
347     // so we just have to clear the list;
348     _highlightItems.clear();
349 }
350
351 void ChatViewSearchController::setCaseSensitive(bool caseSensitive)
352 {
353     if (_caseSensitive == caseSensitive)
354         return;
355
356     _caseSensitive = caseSensitive;
357
358     // we can reuse the original search results if the new search
359     // parameters are a restriction of the original one
360     updateHighlights(caseSensitive);
361 }
362
363 void ChatViewSearchController::setSearchSenders(bool searchSenders)
364 {
365     if (_searchSenders == searchSenders)
366         return;
367
368     _searchSenders = searchSenders;
369     // we can reuse the original search results if the new search
370     // parameters are a restriction of the original one
371     updateHighlights(!searchSenders);
372 }
373
374 void ChatViewSearchController::setSearchMsgs(bool searchMsgs)
375 {
376     if (_searchMsgs == searchMsgs)
377         return;
378
379     _searchMsgs = searchMsgs;
380
381     // we can reuse the original search results if the new search
382     // parameters are a restriction of the original one
383     updateHighlights(!searchMsgs);
384 }
385
386 void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs)
387 {
388     if (_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
389         return;
390
391     _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
392
393     // we can reuse the original search results if the new search
394     // parameters are a restriction of the original one
395     updateHighlights(searchOnlyRegularMsgs);
396 }
397
398 // ==================================================
399 //  SearchHighlightItem
400 // ==================================================
401 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem* parent)
402     : QObject()
403     , QGraphicsItem(parent)
404     , _highlighted(false)
405     , _alpha(70)
406     , _timeLine(150)
407 {
408     setPos(wordRect.x(), wordRect.y());
409     updateGeometry(wordRect.width(), wordRect.height());
410
411     connect(&_timeLine, &QTimeLine::valueChanged, this, &SearchHighlightItem::updateHighlight);
412 }
413
414 void SearchHighlightItem::setHighlighted(bool highlighted)
415 {
416     _highlighted = highlighted;
417
418     if (highlighted)
419         _timeLine.setDirection(QTimeLine::Forward);
420     else
421         _timeLine.setDirection(QTimeLine::Backward);
422
423     if (_timeLine.state() != QTimeLine::Running)
424         _timeLine.start();
425
426     update();
427 }
428
429 void SearchHighlightItem::updateHighlight(qreal value)
430 {
431     _alpha = 70 + (int)(80 * value);
432     update();
433 }
434
435 void SearchHighlightItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
436 {
437     Q_UNUSED(option);
438     Q_UNUSED(widget);
439
440     painter->setPen(QPen(QColor(0, 0, 0), 1.5));
441     painter->setBrush(QColor(254, 237, 45, _alpha));
442     painter->setRenderHints(QPainter::Antialiasing);
443     qreal radius = boundingRect().height() * 0.30;
444     painter->drawRoundedRect(boundingRect(), radius, radius);
445 }
446
447 void SearchHighlightItem::updateGeometry(qreal width, qreal height)
448 {
449     prepareGeometryChange();
450     qreal sizedelta = height * 0.1;
451     _boundingRect = QRectF(-sizedelta, -sizedelta, width + 2 * sizedelta, height + 2 * sizedelta);
452     update();
453 }
454
455 bool SearchHighlightItem::firstInLine(QGraphicsItem* item1, QGraphicsItem* item2)
456 {
457     if (item1->pos().y() != item2->pos().y())
458         return item1->pos().y() < item2->pos().y();
459     else
460         return item1->pos().x() < item2->pos().x();
461 }