Untie the marker line from lastSeenMsg
[quassel.git] / src / qtui / chatviewsearchcontroller.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-09 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  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, 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     _scene(0),
35     _currentHighlight(0),
36     _caseSensitive(false),
37     _searchSenders(false),
38     _searchMsgs(true),
39     _searchOnlyRegularMsgs(true)
40 {
41 }
42
43 void ChatViewSearchController::setSearchString(const QString &searchString) {
44   QString oldSearchString = _searchString;
45   _searchString = searchString;
46   if(_scene) {
47     if(!searchString.startsWith(oldSearchString) || oldSearchString.isEmpty()) {
48       // we can't reuse our all findings... cler the scene and do it all over
49       updateHighlights();
50     } else {
51       // reuse all findings
52       updateHighlights(true);
53     }
54   }
55 }
56
57  void ChatViewSearchController::setScene(ChatScene *scene) {
58   Q_ASSERT(scene);
59   if(scene == _scene)
60     return;
61
62   if(_scene) {
63     disconnect(_scene, 0, this, 0);
64     qDeleteAll(_highlightItems);
65     _highlightItems.clear();
66   }
67
68   _scene = scene;
69   if(!scene)
70     return;
71
72   connect(_scene, SIGNAL(destroyed()), this, SLOT(sceneDestroyed()));
73   connect(_scene, SIGNAL(layoutChanged()), this, SLOT(repositionHighlights()));
74   updateHighlights();
75  }
76
77 void ChatViewSearchController::highlightNext() {
78   if(_highlightItems.isEmpty())
79     return;
80
81   if(_currentHighlight < _highlightItems.count()) {
82     _highlightItems.at(_currentHighlight)->setHighlighted(false);
83   }
84
85   _currentHighlight++;
86   if(_currentHighlight >= _highlightItems.count())
87     _currentHighlight = 0;
88   _highlightItems.at(_currentHighlight)->setHighlighted(true);
89   emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
90 }
91
92 void ChatViewSearchController::highlightPrev() {
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   if(!_scene)
109     return;
110
111   if(reuse) {
112     QSet<ChatLine *> chatLines;
113     foreach(SearchHighlightItem *highlightItem, _highlightItems) {
114       ChatLine *line = qgraphicsitem_cast<ChatLine *>(highlightItem->parentItem());
115       if(line)
116       chatLines << line;
117     }
118     foreach(ChatLine *line, QList<ChatLine *>(chatLines.toList())) {
119       updateHighlights(line);
120     }
121   } else {
122     QPointF oldHighlightPos;
123     if(!_highlightItems.isEmpty() && _currentHighlight < _highlightItems.count()) {
124       oldHighlightPos = _highlightItems[_currentHighlight]->scenePos();
125     }
126     qDeleteAll(_highlightItems);
127     _highlightItems.clear();
128     Q_ASSERT(_highlightItems.isEmpty());
129
130     if(searchString().isEmpty() || !(_searchSenders || _searchMsgs))
131       return;
132
133     checkMessagesForHighlight();
134
135     if(!_highlightItems.isEmpty()) {
136       if(!oldHighlightPos.isNull()) {
137         int start = 0; int end = _highlightItems.count() - 1;
138         QPointF startPos;
139         QPointF endPos;
140         while(1) {
141           startPos = _highlightItems[start]->scenePos();
142           endPos = _highlightItems[end]->scenePos();
143           if(startPos == oldHighlightPos) {
144             _currentHighlight = start;
145             break;
146           }
147           if(endPos == oldHighlightPos) {
148             _currentHighlight = end;
149             break;
150           }
151           if(end - start == 1) {
152             _currentHighlight = start;
153             break;
154           }
155           int pivot = (end + start) / 2;
156           QPointF pivotPos = _highlightItems[pivot]->scenePos();
157           if(startPos.y() == endPos.y()) {
158             if(oldHighlightPos.x() <= pivotPos.x())
159               end = pivot;
160             else
161               start = pivot;
162           } else {
163             if(oldHighlightPos.y() <= pivotPos.y())
164               end = pivot;
165             else
166               start = pivot;
167           }
168         }
169       } else {
170         _currentHighlight = _highlightItems.count() - 1;
171       }
172       _highlightItems[_currentHighlight]->setHighlighted(true);
173       emit newCurrentHighlight(_highlightItems[_currentHighlight]);
174     }
175   }
176 }
177
178 void ChatViewSearchController::checkMessagesForHighlight(int start, int end) {
179   QAbstractItemModel *model = _scene->model();
180   Q_ASSERT(model);
181
182   if(end == -1) {
183     end = model->rowCount() - 1;
184     if(end == -1)
185       return;
186   }
187
188   QModelIndex index;
189   for(int row = start; row <= end; row++) {
190     if(_searchOnlyRegularMsgs) {
191       index = model->index(row, 0);
192       if(!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
193         continue;
194     }
195     highlightLine(_scene->chatLine(row));
196   }
197 }
198
199 void ChatViewSearchController::updateHighlights(ChatLine *line) {
200   QList<ChatItem *> checkItems;
201   if(_searchSenders)
202     checkItems << &(line->item(MessageModel::SenderColumn));
203
204   if(_searchMsgs)
205     checkItems << &(line->item(MessageModel::ContentsColumn));
206
207   QHash<quint64, QHash<quint64, QRectF> > wordRects;
208   foreach(ChatItem *item, checkItems) {
209     foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
210       wordRects[(quint64)(wordRect.x() + item->x())][(quint64)(wordRect.y())] = wordRect;
211     }
212   }
213
214   bool deleteAll = false;
215   QAbstractItemModel *model = _scene->model();
216   Q_ASSERT(model);
217   if(_searchOnlyRegularMsgs) {
218     QModelIndex index = model->index(line->row(), 0);
219     if(!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
220       deleteAll = true;
221   }
222
223   
224   foreach(QGraphicsItem *child, line->childItems()) {
225     SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
226     if(!highlightItem)
227       continue;
228
229     if(!deleteAll && wordRects.contains((quint64)(highlightItem->pos().x())) && wordRects[(quint64)(highlightItem->pos().x())].contains((quint64)(highlightItem->pos().y()))) {
230       QRectF &wordRect = wordRects[(quint64)(highlightItem->pos().x())][(quint64)(highlightItem->pos().y())];
231       highlightItem->updateGeometry(wordRect.width(), wordRect.height());
232     } else {
233       int pos = _highlightItems.indexOf(highlightItem);
234       if(pos == _currentHighlight) {
235         highlightPrev();
236       } else if (pos < _currentHighlight) {
237         _currentHighlight--;
238       }
239
240       _highlightItems.removeAt(pos);
241       delete highlightItem;
242     }
243   }
244 }
245
246 void ChatViewSearchController::highlightLine(ChatLine *line) {
247   QList<ChatItem *> checkItems;
248   if(_searchSenders)
249     checkItems << &(line->item(MessageModel::SenderColumn));
250
251   if(_searchMsgs)
252     checkItems << &(line->item(MessageModel::ContentsColumn));
253
254   foreach(ChatItem *item, checkItems) {
255     foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
256       _highlightItems << new SearchHighlightItem(wordRect.adjusted(item->x(), 0, item->x(), 0), line);
257     }
258   }
259 }
260
261 void ChatViewSearchController::repositionHighlights() {
262   QSet<ChatLine *> chatLines;
263   foreach(SearchHighlightItem *item, _highlightItems) {
264     ChatLine *line = qgraphicsitem_cast<ChatLine *>(item->parentItem());
265     if(line)
266       chatLines << line;
267   }
268   QList<ChatLine *> chatLineList(chatLines.toList());
269   foreach(ChatLine *line, chatLineList) {
270     repositionHighlights(line);
271   }
272 }
273
274 void ChatViewSearchController::repositionHighlights(ChatLine *line) {
275   QList<SearchHighlightItem *> searchHighlights;
276   foreach(QGraphicsItem *child, line->childItems()) {
277     SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
278     if(highlightItem)
279       searchHighlights << highlightItem;
280   }
281
282   if(searchHighlights.isEmpty())
283     return;
284
285   QList<QPointF> wordPos;
286   if(_searchSenders) {
287     foreach(QRectF wordRect, line->senderItem().findWords(searchString(), caseSensitive())) {
288       wordPos << QPointF(wordRect.x() + line->senderItem().x(), wordRect.y());
289     }
290   }
291   if(_searchMsgs) {
292     foreach(QRectF wordRect, line->contentsItem().findWords(searchString(), caseSensitive())) {
293       wordPos << QPointF(wordRect.x() + line->contentsItem().x(), wordRect.y());
294     }
295   }
296
297   qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
298
299   Q_ASSERT(wordPos.count() == searchHighlights.count());
300   for(int i = 0; i < searchHighlights.count(); i++) {
301     searchHighlights.at(i)->setPos(wordPos.at(i));
302   }
303 }
304
305 void ChatViewSearchController::sceneDestroyed() {
306   // WARNING: don't call any methods on scene!
307   _scene = 0;
308   // the items will be automatically deleted when the scene is destroyed
309   // so we just have to clear the list;
310   _highlightItems.clear();
311 }
312
313 void ChatViewSearchController::setCaseSensitive(bool caseSensitive) {
314   if(_caseSensitive == caseSensitive)
315     return;
316
317   _caseSensitive = caseSensitive;
318
319   // we can reuse the original search results if the new search
320   // parameters are a restriction of the original one
321   updateHighlights(caseSensitive);
322 }
323
324 void ChatViewSearchController::setSearchSenders(bool searchSenders) {
325   if(_searchSenders == searchSenders)
326     return;
327
328   _searchSenders = searchSenders;
329   // we can reuse the original search results if the new search
330   // parameters are a restriction of the original one
331   updateHighlights(!searchSenders);
332 }
333
334 void ChatViewSearchController::setSearchMsgs(bool searchMsgs) {
335   if(_searchMsgs == searchMsgs)
336     return;
337
338   _searchMsgs = searchMsgs;
339
340   // we can reuse the original search results if the new search
341   // parameters are a restriction of the original one
342   updateHighlights(!searchMsgs);
343 }
344
345 void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs) {
346   if(_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
347     return;
348
349   _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
350
351   // we can reuse the original search results if the new search
352   // parameters are a restriction of the original one
353   updateHighlights(searchOnlyRegularMsgs);
354 }
355
356
357 // ==================================================
358 //  SearchHighlightItem
359 // ==================================================
360 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem *parent)
361   : QObject(),
362     QGraphicsItem(parent),
363     _highlighted(false),
364     _alpha(100),
365     _timeLine(150)
366 {
367   setPos(wordRect.x(), wordRect.y());
368   updateGeometry(wordRect.width(), wordRect.height());
369
370   connect(&_timeLine, SIGNAL(valueChanged(qreal)), this, SLOT(updateHighlight(qreal)));
371 }
372
373 void SearchHighlightItem::setHighlighted(bool highlighted) {
374   _highlighted = highlighted;
375
376   if(highlighted)
377     _timeLine.setDirection(QTimeLine::Forward);
378   else
379     _timeLine.setDirection(QTimeLine::Backward);
380
381   if(_timeLine.state() != QTimeLine::Running)
382     _timeLine.start();
383
384   update();
385 }
386
387 void SearchHighlightItem::updateHighlight(qreal value) {
388   _alpha = 100 + (int)(155 * value);
389   update();
390 }
391
392 void SearchHighlightItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
393   Q_UNUSED(option);
394   Q_UNUSED(widget);
395
396   painter->setPen(QPen(QColor(0, 0, 0, _alpha), 1.5));
397   painter->setBrush(QColor(254, 237, 45, _alpha));
398   painter->setRenderHints(QPainter::Antialiasing);
399   qreal radius = boundingRect().height() * 0.30;
400   painter->drawRoundedRect(boundingRect(), radius, radius);
401 }
402
403 void SearchHighlightItem::updateGeometry(qreal width, qreal height) {
404   prepareGeometryChange();
405   qreal sizedelta = height * 0.1;
406   _boundingRect = QRectF(-sizedelta, -sizedelta, width + 2 * sizedelta, height + 2 * sizedelta);
407   update();
408 }
409
410 bool SearchHighlightItem::firstInLine(QGraphicsItem *item1, QGraphicsItem *item2) {
411   if(item1->pos().y() != item2->pos().y())
412     return item1->pos().y() < item2->pos().y();
413   else
414     return item1->pos().x() < item2->pos().x();
415 }