Quassel IRC  Pre-Release
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros
chatviewsearchcontroller.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * Copyright (C) 2005-2015 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 
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 
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 
44 void ChatViewSearchController::setSearchString(const QString &searchString)
45 {
46  QString oldSearchString = _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
52  }
53  else {
54  // reuse all findings
55  updateHighlights(true);
56  }
57  }
58 }
59 
60 
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()));
82 }
83 
84 
86 {
87  if (_highlightItems.isEmpty())
88  return;
89 
90  if (_currentHighlight < _highlightItems.count()) {
91  _highlightItems.at(_currentHighlight)->setHighlighted(false);
92  }
93 
95  if (_currentHighlight >= _highlightItems.count())
97  _highlightItems.at(_currentHighlight)->setHighlighted(true);
99 }
100 
101 
103 {
104  if (_highlightItems.isEmpty())
105  return;
106 
107  if (_currentHighlight < _highlightItems.count()) {
108  _highlightItems.at(_currentHighlight)->setHighlighted(false);
109  }
110 
112  if (_currentHighlight < 0)
113  _currentHighlight = _highlightItems.count() - 1;
114  _highlightItems.at(_currentHighlight)->setHighlighted(true);
116 }
117 
118 
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 
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);
190  }
191  }
192 }
193 
194 
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++) {
209  index = model->index(row, 0);
210  if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
211  continue;
212  }
214  }
215 }
216 
217 
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);
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) {
259  }
260 
261  _highlightItems.removeAt(pos);
262  delete highlightItem;
263  }
264  }
265 }
266 
267 
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 
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 
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 
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 
344 {
345  if (_caseSensitive == caseSensitive)
346  return;
347 
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 
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 
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 
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 // ==================================================
397 SearchHighlightItem::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 
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 
428 {
429  _alpha = 70 + (int)(80 * value);
430  update();
431 }
432 
433 
434 void 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 
447 void 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 
456 bool 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 }