1 | /*************************************************************************** |
2 | * Copyright (C) 2005-2014 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 | |
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 | |
44 | void ChatViewSearchController::setSearchString(const QString &searchString) |
45 | { |
46 | QString oldSearchString = _searchString; |
47 | _searchString = 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 |
51 | updateHighlights(); |
52 | } |
53 | else { |
54 | // reuse all findings |
55 | updateHighlights(true); |
56 | } |
57 | } |
58 | } |
59 | |
60 | |
61 | void ChatViewSearchController::setScene(ChatScene *scene) |
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())); |
81 | updateHighlights(); |
82 | } |
83 | |
84 | |
85 | void ChatViewSearchController::highlightNext() |
86 | { |
87 | if (_highlightItems.isEmpty()) |
88 | return; |
89 | |
90 | if (_currentHighlight < _highlightItems.count()) { |
91 | _highlightItems.at(_currentHighlight)->setHighlighted(false); |
92 | } |
93 | |
94 | _currentHighlight++; |
95 | if (_currentHighlight >= _highlightItems.count()) |
96 | _currentHighlight = 0; |
97 | _highlightItems.at(_currentHighlight)->setHighlighted(true); |
98 | emit newCurrentHighlight(_highlightItems.at(_currentHighlight)); |
99 | } |
100 | |
101 | |
102 | void ChatViewSearchController::highlightPrev() |
103 | { |
104 | if (_highlightItems.isEmpty()) |
105 | return; |
106 | |
107 | if (_currentHighlight < _highlightItems.count()) { |
108 | _highlightItems.at(_currentHighlight)->setHighlighted(false); |
109 | } |
110 | |
111 | _currentHighlight--; |
112 | if (_currentHighlight < 0) |
113 | _currentHighlight = _highlightItems.count() - 1; |
114 | _highlightItems.at(_currentHighlight)->setHighlighted(true); |
115 | emit newCurrentHighlight(_highlightItems.at(_currentHighlight)); |
116 | } |
117 | |
118 | |
119 | void ChatViewSearchController::updateHighlights(bool reuse) |
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 | |
147 | checkMessagesForHighlight(); |
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); |
189 | emit newCurrentHighlight(_highlightItems[_currentHighlight]); |
190 | } |
191 | } |
192 | } |
193 | |
194 | |
195 | void ChatViewSearchController::checkMessagesForHighlight(int start, int end) |
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++) { |
208 | if (_searchOnlyRegularMsgs) { |
209 | index = model->index(row, 0); |
210 | if (!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt())) |
211 | continue; |
212 | } |
213 | highlightLine(_scene->chatLine(row)); |
214 | } |
215 | } |
216 | |
217 | |
218 | void ChatViewSearchController::updateHighlights(ChatLine *line) |
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); |
237 | if (_searchOnlyRegularMsgs) { |
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) { |
258 | _currentHighlight--; |
259 | } |
260 | |
261 | _highlightItems.removeAt(pos); |
262 | delete highlightItem; |
263 | } |
264 | } |
265 | } |
266 | |
267 | |
268 | void ChatViewSearchController::highlightLine(ChatLine *line) |
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 | |
285 | void ChatViewSearchController::repositionHighlights() |
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 | |
300 | void ChatViewSearchController::repositionHighlights(ChatLine *line) |
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 | |
333 | void ChatViewSearchController::sceneDestroyed() |
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 | |
343 | void ChatViewSearchController::setCaseSensitive(bool caseSensitive) |
344 | { |
345 | if (_caseSensitive == caseSensitive) |
346 | return; |
347 | |
348 | _caseSensitive = caseSensitive; |
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 | |
356 | void ChatViewSearchController::setSearchSenders(bool searchSenders) |
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 | |
368 | void ChatViewSearchController::setSearchMsgs(bool searchMsgs) |
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 | |
381 | void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMsgs) |
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 | |
411 | void SearchHighlightItem::setHighlighted(bool highlighted) |
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 | |
427 | void SearchHighlightItem::updateHighlight(qreal value) |
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 | } |
463 | |