1/***************************************************************************
2 * Copyright (C) 2005-2014 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
32ChatViewSearchController::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
44void 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
61void 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
85void 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
102void 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
119void 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 int pivot = (end + start) / 2;
170 QPointF pivotPos = _highlightItems[pivot]->scenePos();
171 if (startPos.y() == endPos.y()) {
172 if (oldHighlightPos.x() <= pivotPos.x())
173 end = pivot;
174 else
175 start = pivot;
176 }
177 else {
178 if (oldHighlightPos.y() <= pivotPos.y())
179 end = pivot;
180 else
181 start = pivot;
182 }
183 }
184 }
185 else {
186 _currentHighlight = _highlightItems.count() - 1;
187 }
188 _highlightItems[_currentHighlight]->setHighlighted(true);
189 emit newCurrentHighlight(_highlightItems[_currentHighlight]);
190 }
191 }
192}
193
194
195void ChatViewSearchController::checkMessagesForHighlight(int start, int end)
196{
197 QAbstractItemModel *model = _scene->model();
198 Q_ASSERT(model);
199
200 if (end == -1) {
201 end = model->rowCount() - 1;
202 if (end == -1)
203 return;
204 }
205
206 QModelIndex index;
207 for (int row = start; row <= end; row++) {
208 if (_searchOnlyRegularMsgs) {
209 index = model->index(row, 0);
210 if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
211 continue;
212 }
213 highlightLine(_scene->chatLine(row));
214 }
215}
216
217
218void ChatViewSearchController::updateHighlights(ChatLine *line)
219{
220 QList<ChatItem *> checkItems;
221 if (_searchSenders)
222 checkItems << line->item(MessageModel::SenderColumn);
223
224 if (_searchMsgs)
225 checkItems << line->item(MessageModel::ContentsColumn);
226
227 QHash<quint64, QHash<quint64, QRectF> > wordRects;
228 foreach(ChatItem *item, checkItems) {
229 foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
230 wordRects[(quint64)(wordRect.x() + item->x())][(quint64)(wordRect.y())] = wordRect;
231 }
232 }
233
234 bool deleteAll = false;
235 QAbstractItemModel *model = _scene->model();
236 Q_ASSERT(model);
237 if (_searchOnlyRegularMsgs) {
238 QModelIndex index = model->index(line->row(), 0);
239 if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
240 deleteAll = true;
241 }
242
243 foreach(QGraphicsItem *child, line->childItems()) {
244 SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
245 if (!highlightItem)
246 continue;
247
248 if (!deleteAll && wordRects.contains((quint64)(highlightItem->pos().x())) && wordRects[(quint64)(highlightItem->pos().x())].contains((quint64)(highlightItem->pos().y()))) {
249 QRectF &wordRect = wordRects[(quint64)(highlightItem->pos().x())][(quint64)(highlightItem->pos().y())];
250 highlightItem->updateGeometry(wordRect.width(), wordRect.height());
251 }
252 else {
253 int pos = _highlightItems.indexOf(highlightItem);
254 if (pos == _currentHighlight) {
255 highlightPrev();
256 }
257 else if (pos < _currentHighlight) {
258 _currentHighlight--;
259 }
260
261 _highlightItems.removeAt(pos);
262 delete highlightItem;
263 }
264 }
265}
266
267
268void ChatViewSearchController::highlightLine(ChatLine *line)
269{
270 QList<ChatItem *> checkItems;
271 if (_searchSenders)
272 checkItems << line->item(MessageModel::SenderColumn);
273
274 if (_searchMsgs)
275 checkItems << line->item(MessageModel::ContentsColumn);
276
277 foreach(ChatItem *item, checkItems) {
278 foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
279 _highlightItems << new SearchHighlightItem(wordRect.adjusted(item->x(), 0, item->x(), 0), line);
280 }
281 }
282}
283
284
285void ChatViewSearchController::repositionHighlights()
286{
287 QSet<ChatLine *> chatLines;
288 foreach(SearchHighlightItem *item, _highlightItems) {
289 ChatLine *line = qgraphicsitem_cast<ChatLine *>(item->parentItem());
290 if (line)
291 chatLines << line;
292 }
293 QList<ChatLine *> chatLineList(chatLines.toList());
294 foreach(ChatLine *line, chatLineList) {
295 repositionHighlights(line);
296 }
297}
298
299
300void ChatViewSearchController::repositionHighlights(ChatLine *line)
301{
302 QList<SearchHighlightItem *> searchHighlights;
303 foreach(QGraphicsItem *child, line->childItems()) {
304 SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
305 if (highlightItem)
306 searchHighlights << highlightItem;
307 }
308
309 if (searchHighlights.isEmpty())
310 return;
311
312 QList<QPointF> wordPos;
313 if (_searchSenders) {
314 foreach(QRectF wordRect, line->senderItem()->findWords(searchString(), caseSensitive())) {
315 wordPos << QPointF(wordRect.x() + line->senderItem()->x(), wordRect.y());
316 }
317 }
318 if (_searchMsgs) {
319 foreach(QRectF wordRect, line->contentsItem()->findWords(searchString(), caseSensitive())) {
320 wordPos << QPointF(wordRect.x() + line->contentsItem()->x(), wordRect.y());
321 }
322 }
323
324 qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
325
326 Q_ASSERT(wordPos.count() == searchHighlights.count());
327 for (int i = 0; i < searchHighlights.count(); i++) {
328 searchHighlights.at(i)->setPos(wordPos.at(i));
329 }
330}
331
332
333void ChatViewSearchController::sceneDestroyed()
334{
335 // WARNING: don't call any methods on scene!
336 _scene = 0;
337 // the items will be automatically deleted when the scene is destroyed
338 // so we just have to clear the list;
339 _highlightItems.clear();
340}
341
342
343void ChatViewSearchController::setCaseSensitive(bool caseSensitive)
344{
345 if (_caseSensitive == caseSensitive)
346 return;
347
348 _caseSensitive = caseSensitive;
349
350 // we can reuse the original search results if the new search
351 // parameters are a restriction of the original one
352 updateHighlights(caseSensitive);
353}
354
355
356void ChatViewSearchController::setSearchSenders(bool searchSenders)
357{
358 if (_searchSenders == searchSenders)
359 return;
360
361 _searchSenders = searchSenders;
362 // we can reuse the original search results if the new search
363 // parameters are a restriction of the original one
364 updateHighlights(!searchSenders);
365}
366
367
368void ChatViewSearchController::setSearchMsgs(bool searchMsgs)
369{
370 if (_searchMsgs == searchMsgs)
371 return;
372
373 _searchMsgs = searchMsgs;
374
375 // we can reuse the original search results if the new search
376 // parameters are a restriction of the original one
377 updateHighlights(!searchMsgs);
378}
379
380
381void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs)
382{
383 if (_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
384 return;
385
386 _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
387
388 // we can reuse the original search results if the new search
389 // parameters are a restriction of the original one
390 updateHighlights(searchOnlyRegularMsgs);
391}
392
393
394// ==================================================
395// SearchHighlightItem
396// ==================================================
397SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem *parent)
398 : QObject(),
399 QGraphicsItem(parent),
400 _highlighted(false),
401 _alpha(70),
402 _timeLine(150)
403{
404 setPos(wordRect.x(), wordRect.y());
405 updateGeometry(wordRect.width(), wordRect.height());
406
407 connect(&_timeLine, SIGNAL(valueChanged(qreal)), this, SLOT(updateHighlight(qreal)));
408}
409
410
411void SearchHighlightItem::setHighlighted(bool highlighted)
412{
413 _highlighted = highlighted;
414
415 if (highlighted)
416 _timeLine.setDirection(QTimeLine::Forward);
417 else
418 _timeLine.setDirection(QTimeLine::Backward);
419
420 if (_timeLine.state() != QTimeLine::Running)
421 _timeLine.start();
422
423 update();
424}
425
426
427void SearchHighlightItem::updateHighlight(qreal value)
428{
429 _alpha = 70 + (int)(80 * value);
430 update();
431}
432
433
434void 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
447void 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
456bool 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}
463