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