Search result highlights are now properly repositioned on resize
[quassel.git] / src / qtui / chatviewsearchcontroller.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-08 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   QAbstractItemModel *model = _scene->model();
111   Q_ASSERT(model);
112
113
114   QList<ChatLine *> chatLines;
115   if(reuse) {
116     foreach(SearchHighlightItem *highlightItem, _highlightItems) {
117       ChatLine *line = dynamic_cast<ChatLine *>(highlightItem->parentItem());
118       if(!line || chatLines.contains(line))
119         continue;
120       chatLines << line;
121     }
122   }
123
124   qDeleteAll(_highlightItems);
125   _highlightItems.clear();
126   Q_ASSERT(_highlightItems.isEmpty());
127
128   if(searchString().isEmpty() || !(_searchSenders || _searchMsgs))
129     return;
130
131   if(reuse) {
132     QModelIndex index;
133     foreach(ChatLine *line, chatLines) {
134       if(_searchOnlyRegularMsgs) {
135         index = model->index(line->row(), 0);
136         if(!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
137           continue;
138       }
139       highlightLine(line);
140     }
141   } else {
142     // we have to crawl through the data
143     QModelIndex index;
144     QString plainText;
145     int rowCount = model->rowCount();
146     for(int row = 0; row < rowCount; row++) {
147       ChatLine *line = _scene->chatLine(row);
148
149       if(_searchOnlyRegularMsgs) {
150         index = model->index(row, 0);
151         if(!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
152           continue;
153       }
154       highlightLine(line);
155     }
156   }
157
158   if(!_highlightItems.isEmpty()) {
159     _highlightItems.last()->setHighlighted(true);
160     _currentHighlight = _highlightItems.count() - 1;
161     emit newCurrentHighlight(_highlightItems.last());
162   }
163 }
164
165 void ChatViewSearchController::highlightLine(ChatLine *line) {
166   QList<ChatItem *> checkItems;
167   if(_searchSenders)
168     checkItems << &(line->item(MessageModel::SenderColumn));
169
170   if(_searchMsgs)
171     checkItems << &(line->item(MessageModel::ContentsColumn));
172
173   foreach(ChatItem *item, checkItems) {
174     foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
175       _highlightItems << new SearchHighlightItem(wordRect.adjusted(item->x(), 0, item->x(), 0), line);
176     }
177   }
178 }
179
180 void ChatViewSearchController::repositionHighlights() {
181   QSet<ChatLine *> chatLines;
182   foreach(SearchHighlightItem *item, _highlightItems) {
183     ChatLine *line = qgraphicsitem_cast<ChatLine *>(item->parentItem());
184     if(line)
185       chatLines << line;
186   }
187   QList<ChatLine *> chatLineList(chatLines.toList());
188   foreach(ChatLine *line, chatLineList) {
189     repositionHighlights(line);
190   }
191 }
192
193 void ChatViewSearchController::repositionHighlights(ChatLine *line) {
194   QList<SearchHighlightItem *> searchHighlights;
195   foreach(QGraphicsItem *child, line->childItems()) {
196     SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
197     if(highlightItem)
198       searchHighlights << highlightItem;
199   }
200
201   if(searchHighlights.isEmpty())
202     return;
203
204   QList<QPointF> wordPos;
205   if(_searchSenders) {
206     foreach(QRectF wordRect, line->senderItem().findWords(searchString(), caseSensitive())) {
207       wordPos << QPointF(wordRect.x() + line->senderItem().x(), wordRect.y());
208     }
209   }
210   if(_searchMsgs) {
211     foreach(QRectF wordRect, line->contentsItem().findWords(searchString(), caseSensitive())) {
212       wordPos << QPointF(wordRect.x() + line->contentsItem().x(), wordRect.y());
213     }
214   }
215
216   qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
217
218   Q_ASSERT(wordPos.count() == searchHighlights.count());
219   for(int i = 0; i < searchHighlights.count(); i++) {
220     searchHighlights.at(i)->setPos(wordPos.at(i));
221   }
222 }
223
224 void ChatViewSearchController::sceneDestroyed() {
225   // WARNING: don't call any methods on scene!
226   _scene = 0;
227   // the items will be automatically deleted when the scene is destroyed
228   // so we just have to clear the list;
229   _highlightItems.clear();
230 }
231
232 void ChatViewSearchController::setCaseSensitive(bool caseSensitive) {
233   if(_caseSensitive == caseSensitive)
234     return;
235
236   _caseSensitive = caseSensitive;
237
238   // we can reuse the original search results if the new search
239   // parameters are a restriction of the original one
240   updateHighlights(caseSensitive);
241 }
242
243 void ChatViewSearchController::setSearchSenders(bool searchSenders) {
244   if(_searchSenders == searchSenders)
245     return;
246
247   _searchSenders = searchSenders;
248   // we can reuse the original search results if the new search
249   // parameters are a restriction of the original one
250   updateHighlights(!searchSenders);
251 }
252
253 void ChatViewSearchController::setSearchMsgs(bool searchMsgs) {
254   if(_searchMsgs == searchMsgs)
255     return;
256
257   _searchMsgs = searchMsgs;
258
259   // we can reuse the original search results if the new search
260   // parameters are a restriction of the original one
261   updateHighlights(!searchMsgs);
262 }
263
264 void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs) {
265   if(_searchOnlyRegularMsgs == searchOnlyRegularMsgs)
266     return;
267
268   _searchOnlyRegularMsgs = searchOnlyRegularMsgs;
269
270   // we can reuse the original search results if the new search
271   // parameters are a restriction of the original one
272   updateHighlights(searchOnlyRegularMsgs);
273 }
274
275
276 // ==================================================
277 //  SearchHighlightItem
278 // ==================================================
279 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem *parent)
280   : QObject(),
281     QGraphicsItem(parent),
282     _highlighted(false),
283     _alpha(100),
284     _timeLine(150)
285 {
286   setPos(wordRect.x(), wordRect.y());
287   qreal sizedelta = wordRect.height() * 0.1;
288   _boundingRect = QRectF(-sizedelta, -sizedelta, wordRect.width() + 2 * sizedelta, wordRect.height() + 2 * sizedelta);
289
290   connect(&_timeLine, SIGNAL(valueChanged(qreal)), this, SLOT(updateHighlight(qreal)));
291 }
292
293 void SearchHighlightItem::setHighlighted(bool highlighted) {
294   _highlighted = highlighted;
295
296   if(highlighted)
297     _timeLine.setDirection(QTimeLine::Forward);
298   else
299     _timeLine.setDirection(QTimeLine::Backward);
300
301   if(_timeLine.state() != QTimeLine::Running)
302     _timeLine.start();
303
304   update();
305 }
306
307 void SearchHighlightItem::updateHighlight(qreal value) {
308   _alpha = 100 + 155 * value;
309   update();
310 }
311
312 void SearchHighlightItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
313   Q_UNUSED(option);
314   Q_UNUSED(widget);
315
316   painter->setPen(QPen(QColor(0, 0, 0, _alpha), 1.5));
317   painter->setBrush(QColor(254, 237, 45, _alpha));
318   painter->setRenderHints(QPainter::Antialiasing);
319   qreal radius = boundingRect().height() * 0.30;
320   painter->drawRoundedRect(boundingRect(), radius, radius);
321 }
322
323 bool SearchHighlightItem::firstInLine(QGraphicsItem *item1, QGraphicsItem *item2) {
324   if(item1->pos().y() != item2->pos().y())
325     return item1->pos().y() < item2->pos().y();
326   else
327     return item1->pos().x() < item2->pos().x();
328 }