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