Quassel IRC  Pre-Release
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros
chatscene.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 
21 #include <QApplication>
22 #include <QClipboard>
23 #include <QDesktopServices>
24 #include <QDrag>
25 #include <QGraphicsSceneMouseEvent>
26 #include <QIcon>
27 #include <QMenu>
28 #include <QMenuBar>
29 #include <QMimeData>
30 #include <QPersistentModelIndex>
31 #include <QUrl>
32 
33 #ifdef HAVE_KDE4
34 # include <KMenuBar>
35 #else
36 # include <QMenuBar>
37 #endif
38 
39 #ifdef HAVE_WEBKIT
40 # include <QWebView>
41 #endif
42 
43 #include "chatitem.h"
44 #include "chatline.h"
45 #include "chatlinemodelitem.h"
46 #include "chatscene.h"
47 #include "chatview.h"
48 #include "client.h"
49 #include "clientbacklogmanager.h"
50 #include "columnhandleitem.h"
52 #include "mainwin.h"
53 #include "markerlineitem.h"
54 #include "messagefilter.h"
55 #include "qtui.h"
56 #include "qtuistyle.h"
57 #include "chatviewsettings.h"
58 #include "webpreviewitem.h"
59 
60 const qreal minContentsWidth = 200;
61 
62 ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal width, ChatView *parent)
63  : QGraphicsScene(0, 0, width, 0, (QObject *)parent),
64  _chatView(parent),
65  _idString(idString),
66  _model(model),
67  _singleBufferId(BufferId()),
68  _sceneRect(0, 0, width, 0),
69  _firstLineRow(-1),
70  _viewportHeight(0),
71  _markerLine(new MarkerLineItem(width)),
72  _markerLineVisible(false),
73  _markerLineValid(false),
74  _markerLineJumpPending(false),
75  _cutoffMode(CutoffRight),
76  _selectingItem(0),
77  _selectionStart(-1),
78  _isSelecting(false),
79  _clickMode(NoClick),
80  _clickHandled(true),
81  _leftButtonPressed(false)
82 {
83  MessageFilter *filter = qobject_cast<MessageFilter *>(model);
84  if (filter && filter->isSingleBufferFilter()) {
85  _singleBufferId = filter->singleBufferId();
86  }
87 
88  addItem(_markerLine);
89  connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _markerLine, SLOT(sceneRectChanged(const QRectF &)));
90 
91  ChatViewSettings defaultSettings;
92  _defaultFirstColHandlePos = defaultSettings.value("FirstColumnHandlePos", 80).toInt();
93  _defaultSecondColHandlePos = defaultSettings.value("SecondColumnHandlePos", 200).toInt();
94 
95  ChatViewSettings viewSettings(this);
96  _firstColHandlePos = viewSettings.value("FirstColumnHandlePos", _defaultFirstColHandlePos).toInt();
97  _secondColHandlePos = viewSettings.value("SecondColumnHandlePos", _defaultSecondColHandlePos).toInt();
98 
99  _firstColHandle = new ColumnHandleItem(QtUi::style()->firstColumnSeparator());
100  addItem(_firstColHandle);
102  connect(_firstColHandle, SIGNAL(positionChanged(qreal)), this, SLOT(firstHandlePositionChanged(qreal)));
103  connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _firstColHandle, SLOT(sceneRectChanged(const QRectF &)));
104 
105  _secondColHandle = new ColumnHandleItem(QtUi::style()->secondColumnSeparator());
106  addItem(_secondColHandle);
108  connect(_secondColHandle, SIGNAL(positionChanged(qreal)), this, SLOT(secondHandlePositionChanged(qreal)));
109 
110  connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _secondColHandle, SLOT(sceneRectChanged(const QRectF &)));
111 
113 
114  if (model->rowCount() > 0)
115  rowsInserted(QModelIndex(), 0, model->rowCount() - 1);
116 
117  connect(model, SIGNAL(rowsInserted(const QModelIndex &, int, int)),
118  this, SLOT(rowsInserted(const QModelIndex &, int, int)));
119  connect(model, SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)),
120  this, SLOT(rowsAboutToBeRemoved(const QModelIndex &, int, int)));
121  connect(model, SIGNAL(rowsRemoved(QModelIndex, int, int)),
122  this, SLOT(rowsRemoved()));
123  connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(dataChanged(QModelIndex, QModelIndex)));
124 
125 #ifdef HAVE_WEBKIT
126  webPreview.timer.setSingleShot(true);
127  connect(&webPreview.timer, SIGNAL(timeout()), this, SLOT(webPreviewNextStep()));
128 #endif
129  _showWebPreview = defaultSettings.showWebPreview();
130  defaultSettings.notify("ShowWebPreview", this, SLOT(showWebPreviewChanged()));
131 
132  _clickTimer.setInterval(QApplication::doubleClickInterval());
133  _clickTimer.setSingleShot(true);
134  connect(&_clickTimer, SIGNAL(timeout()), SLOT(clickTimeout()));
135 
136  setItemIndexMethod(QGraphicsScene::NoIndex);
137 }
138 
139 
141 {
142 }
143 
144 
146 {
147  return _chatView;
148 }
149 
150 
152 {
153  return _firstColHandle;
154 }
155 
156 
158 {
159  return _secondColHandle;
160 }
161 
163 {
164  //make sure first column is at least 80 px wide, second 120 px
165  int firstColHandlePos = qMax(_defaultFirstColHandlePos,
166  80);
167  int secondColHandlePos = qMax(_defaultSecondColHandlePos,
168  firstColHandlePos + 120);
169 
170  _firstColHandle->setXPos(firstColHandlePos);
171  _secondColHandle->setXPos(secondColHandlePos);
172 }
173 
174 ChatLine *ChatScene::chatLine(MsgId msgId, bool matchExact, bool ignoreDayChange) const
175 {
176  if (!_lines.count())
177  return 0;
178 
179  QList<ChatLine *>::ConstIterator start = _lines.begin();
180  QList<ChatLine *>::ConstIterator end = _lines.end();
181  QList<ChatLine *>::ConstIterator middle;
182 
183  int n = int(end - start);
184  int half;
185 
186  while (n > 0) {
187  half = n >> 1;
188  middle = start + half;
189  if ((*middle)->msgId() < msgId) {
190  start = middle + 1;
191  n -= half + 1;
192  }
193  else {
194  n = half;
195  }
196  }
197 
198  if (start != end && (*start)->msgId() == msgId && (ignoreDayChange ? (*start)->msgType() != Message::DayChange : true))
199  return *start;
200 
201  if (matchExact)
202  return 0;
203 
204  if (start == _lines.begin()) // not (yet?) in our scene
205  return 0;
206 
207  // if we didn't find the exact msgId, take the next-lower one (this makes sense for lastSeen)
208 
209  if (start == end) { // higher than last element
210  if (!ignoreDayChange)
211  return _lines.last();
212 
213  for (int i = _lines.count() -1; i >= 0; i--) {
214  if (_lines.at(i)->msgType() != Message::DayChange)
215  return _lines.at(i);
216  }
217  return 0;
218  }
219 
220  // return the next-lower line
221  if (!ignoreDayChange)
222  return *(--start);
223 
224  do {
225  if ((*(--start))->msgType() != Message::DayChange)
226  return *start;
227  }
228  while (start != _lines.begin());
229  return 0;
230 }
231 
232 
233 ChatItem *ChatScene::chatItemAt(const QPointF &scenePos) const
234 {
235  foreach(QGraphicsItem *item, items(scenePos, Qt::IntersectsItemBoundingRect, Qt::AscendingOrder)) {
236  ChatLine *line = qgraphicsitem_cast<ChatLine *>(item);
237  if (line)
238  return line->itemAt(line->mapFromScene(scenePos));
239  }
240  return 0;
241 }
242 
243 
244 bool ChatScene::containsBuffer(const BufferId &id) const
245 {
246  MessageFilter *filter = qobject_cast<MessageFilter *>(model());
247  if (filter)
248  return filter->containsBuffer(id);
249  else
250  return false;
251 }
252 
253 
255 {
256  _markerLineVisible = visible;
257  if (visible && _markerLineValid)
258  markerLine()->setVisible(true);
259  else
260  markerLine()->setVisible(false);
261 }
262 
263 
265 {
266  if (!isSingleBufferScene())
267  return;
268 
269  if (!msgId.isValid())
271 
272  if (msgId.isValid()) {
273  ChatLine *line = chatLine(msgId, false, true);
274  if (line) {
275  markerLine()->setChatLine(line);
276  // if this was the last line, we won't see it because it's outside the sceneRect
277  // .. which is exactly what we want :)
278  markerLine()->setPos(line->pos() + QPointF(0, line->height()));
279 
280  // DayChange messages might have been hidden outside the scene rect, don't make the markerline visible then!
281  if (markerLine()->pos().y() >= sceneRect().y()) {
282  _markerLineValid = true;
283  if (_markerLineVisible)
284  markerLine()->setVisible(true);
286  _markerLineJumpPending = false;
287  if (markerLine()->isVisible()) {
288  markerLine()->ensureVisible(QRectF(), 50, 50);
289  }
290  }
291  return;
292  }
293  }
294  }
295  _markerLineValid = false;
296  markerLine()->setVisible(false);
297 }
298 
299 
300 void ChatScene::jumpToMarkerLine(bool requestBacklog)
301 {
302  if (!isSingleBufferScene())
303  return;
304 
305  if (markerLine()->isVisible()) {
306  markerLine()->ensureVisible(QRectF(), 50, 50);
307  return;
308  }
309  if (!_markerLineValid && requestBacklog) {
311  if (msgId.isValid()) {
312  _markerLineJumpPending = true;
313  Client::backlogManager()->requestBacklog(singleBufferId(), msgId, -1, -1, 0);
314 
315  // If we filtered out the lastSeenMsg (by changing filters after setting it), we'd never jump because the above request
316  // won't fetch any prior lines. Thus, trigger a dynamic backlog request just in case, so repeated
317  // jump tries will eventually cause enough backlog to be fetched.
318  // This is a bit hackish, but not wasteful, as jumping to the top of the ChatView would trigger a dynamic fetch anyway.
319  this->requestBacklog();
320  }
321  }
322 }
323 
324 
325 void ChatScene::rowsInserted(const QModelIndex &index, int start, int end)
326 {
327  Q_UNUSED(index);
328 
329 // QModelIndex sidx = model()->index(start, 2);
330 // QModelIndex eidx = model()->index(end, 2);
331 // qDebug() << "rowsInserted:";
332 // if(start > 0) {
333 // QModelIndex ssidx = model()->index(start - 1, 2);
334 // qDebug() << "Start--:" << start - 1 << ssidx.data(MessageModel::MsgIdRole).value<MsgId>()
335 // << ssidx.data(Qt::DisplayRole).toString();
336 // }
337 // qDebug() << "Start:" << start << sidx.data(MessageModel::MsgIdRole).value<MsgId>()
338 // << sidx.data(Qt::DisplayRole).toString();
339 // qDebug() << "End:" << end << eidx.data(MessageModel::MsgIdRole).value<MsgId>()
340 // << eidx.data(Qt::DisplayRole).toString();
341 // if(end + 1 < model()->rowCount()) {
342 // QModelIndex eeidx = model()->index(end + 1, 2);
343 // qDebug() << "End++:" << end + 1 << eeidx.data(MessageModel::MsgIdRole).value<MsgId>()
344 // << eeidx.data(Qt::DisplayRole).toString();
345 // }
346 
347  qreal h = 0;
348  qreal y = 0;
349  qreal width = _sceneRect.width();
350  bool atBottom = (start == _lines.count());
351  bool atTop = !atBottom && (start == 0);
352 
353  if (start < _lines.count()) {
354  y = _lines.value(start)->y();
355  }
356  else if (atBottom && !_lines.isEmpty()) {
357  y = _lines.last()->y() + _lines.last()->height();
358  }
359 
360  qreal contentsWidth = width - secondColumnHandle()->sceneRight();
361  qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
362  qreal timestampWidth = firstColumnHandle()->sceneLeft();
363  QPointF contentsPos(secondColumnHandle()->sceneRight(), 0);
364  QPointF senderPos(firstColumnHandle()->sceneRight(), 0);
365 
366  if (atTop) {
367  for (int i = end; i >= start; i--) {
368  ChatLine *line = new ChatLine(i, model(),
369  width,
370  timestampWidth, senderWidth, contentsWidth,
371  senderPos, contentsPos);
372  h += line->height();
373  line->setPos(0, y-h);
374  _lines.insert(start, line);
375  addItem(line);
376  }
377  }
378  else {
379  for (int i = start; i <= end; i++) {
380  ChatLine *line = new ChatLine(i, model(),
381  width,
382  timestampWidth, senderWidth, contentsWidth,
383  senderPos, contentsPos);
384  line->setPos(0, y+h);
385  h += line->height();
386  _lines.insert(i, line);
387  addItem(line);
388  }
389  }
390 
391  // update existing items
392  for (int i = end+1; i < _lines.count(); i++) {
393  _lines[i]->setRow(i);
394  }
395 
396  // update selection
397  if (_selectionStart >= 0) {
398  int offset = end - start + 1;
399  int oldStart = _selectionStart;
400  if (_selectionStart >= start)
401  _selectionStart += offset;
402  if (_selectionEnd >= start) {
403  _selectionEnd += offset;
404  if (_selectionStart == oldStart)
405  for (int i = start; i < start + offset; i++)
406  _lines[i]->setSelected(true);
407  }
408  if (_firstSelectionRow >= start)
409  _firstSelectionRow += offset;
410  }
411 
412  // neither pre- or append means we have to do dirty work: move items...
413  if (!(atTop || atBottom)) {
414  ChatLine *line = 0;
415  for (int i = 0; i <= end; i++) {
416  line = _lines.at(i);
417  line->setPos(0, line->pos().y() - h);
418  if (line == markerLine()->chatLine())
419  markerLine()->setPos(line->pos() + QPointF(0, line->height()));
420  }
421  }
422 
423  // check if all went right
424  Q_ASSERT(start == 0 || _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() == _lines.at(start)->pos().y());
425 // if(start != 0) {
426 // if(_lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() != _lines.at(start)->pos().y()) {
427 // qDebug() << "lines:" << _lines.count() << "start:" << start << "end:" << end;
428 // qDebug() << "line[start - 1]:" << _lines.at(start - 1)->pos().y() << "+" << _lines.at(start - 1)->height() << "=" << _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height();
429 // qDebug() << "line[start]" << _lines.at(start)->pos().y();
430 // qDebug() << "needed moving:" << !(atTop || atBottom) << moveTop << moveStart << moveEnd << offset;
431 // Q_ASSERT(false)
432 // }
433 // }
434  Q_ASSERT(end + 1 == _lines.count() || _lines.at(end)->pos().y() + _lines.at(end)->height() == _lines.at(end + 1)->pos().y());
435 // if(end + 1 < _lines.count()) {
436 // if(_lines.at(end)->pos().y() + _lines.at(end)->height() != _lines.at(end + 1)->pos().y()) {
437 // qDebug() << "lines:" << _lines.count() << "start:" << start << "end:" << end;
438 // qDebug() << "line[end]:" << _lines.at(end)->pos().y() << "+" << _lines.at(end)->height() << "=" << _lines.at(end)->pos().y() + _lines.at(end)->height();
439 // qDebug() << "line[end+1]" << _lines.at(end + 1)->pos().y();
440 // qDebug() << "needed moving:" << !(atTop || atBottom) << moveTop << moveStart << moveEnd << offset;
441 // Q_ASSERT(false);
442 // }
443 // }
444 
445  if (!atBottom) {
446  if (start < _firstLineRow) {
447  int prevFirstLineRow = _firstLineRow + (end - start + 1);
448  for (int i = end + 1; i < prevFirstLineRow; i++) {
449  _lines.at(i)->show();
450  }
451  }
452  // force new search for first proper line
453  _firstLineRow = -1;
454  }
455  updateSceneRect();
456  if (atBottom) {
457  emit lastLineChanged(_lines.last(), h);
458  }
459 
460  // now move the marker line if necessary. we don't need to do anything if we appended lines though...
461  if (!_markerLineValid)
462  setMarkerLine();
463 }
464 
465 
466 void ChatScene::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
467 {
468  Q_UNUSED(parent);
469 
470  qreal h = 0; // total height of removed items;
471 
472  bool atTop = (start == 0);
473  bool atBottom = (end == _lines.count() - 1);
474 
475  // clear selection
476  if (_selectingItem) {
477  int row = _selectingItem->row();
478  if (row >= start && row <= end)
479  setSelectingItem(0);
480  }
481 
482  // remove items from scene
483  QList<ChatLine *>::iterator lineIter = _lines.begin() + start;
484  int lineCount = start;
485  while (lineIter != _lines.end() && lineCount <= end) {
486  if ((*lineIter) == markerLine()->chatLine())
487  markerLine()->setChatLine(0);
488  h += (*lineIter)->height();
489  delete *lineIter;
490  lineIter = _lines.erase(lineIter);
491  lineCount++;
492  }
493 
494  // update rows of remaining chatlines
495  for (int i = start; i < _lines.count(); i++) {
496  _lines.at(i)->setRow(i);
497  }
498 
499  // update selection
500  if (_selectionStart >= 0) {
501  int offset = end - start + 1;
502  if (_selectionStart >= start)
503  _selectionStart = qMax(_selectionStart - offset, start);
504  if (_selectionEnd >= start)
505  _selectionEnd -= offset;
506  if (_firstSelectionRow >= start)
507  _firstSelectionRow -= offset;
508 
510  _isSelecting = false;
511  _selectionStart = -1;
512  }
513  }
514 
515  // neither removing at bottom or top means we have to move items...
516  if (!(atTop || atBottom)) {
517  qreal offset = h;
518  int moveStart = 0;
519  int moveEnd = _lines.count() - 1;
520  if (start < _lines.count() - start) {
521  // move top part
522  moveEnd = start - 1;
523  }
524  else {
525  // move bottom part
526  moveStart = start;
527  offset = -offset;
528  }
529  ChatLine *line = 0;
530  for (int i = moveStart; i <= moveEnd; i++) {
531  line = _lines.at(i);
532  line->setPos(0, line->pos().y() + offset);
533  }
534  }
535 
536  Q_ASSERT(start == 0 || start >= _lines.count() || _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() == _lines.at(start)->pos().y());
537 
538  // update sceneRect
539  // when searching for the first non-date-line we have to take into account that our
540  // model still contains the just removed lines so we cannot simply call updateSceneRect()
541  int numRows = model()->rowCount();
542  QModelIndex firstLineIdx;
543  _firstLineRow = -1;
544  bool needOffset = false;
545  do {
546  _firstLineRow++;
547  if (_firstLineRow >= start && _firstLineRow <= end) {
548  _firstLineRow = end + 1;
549  needOffset = true;
550  }
551  firstLineIdx = model()->index(_firstLineRow, 0);
552  }
553  while ((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) == Message::DayChange && _firstLineRow < numRows);
554 
555  if (needOffset)
556  _firstLineRow -= end - start + 1;
557  updateSceneRect();
558 }
559 
560 
562 {
563  // move the marker line if necessary
564  setMarkerLine();
565 }
566 
567 
568 void ChatScene::dataChanged(const QModelIndex &tl, const QModelIndex &br)
569 {
570  layout(tl.row(), br.row(), _sceneRect.width());
571 }
572 
573 
574 void ChatScene::updateForViewport(qreal width, qreal height)
575 {
576  _viewportHeight = height;
577  setWidth(width);
578 }
579 
580 
581 void ChatScene::setWidth(qreal width)
582 {
583  if (width == _sceneRect.width())
584  return;
585  layout(0, _lines.count()-1, width);
586 }
587 
588 
589 void ChatScene::layout(int start, int end, qreal width)
590 {
591  // clock_t startT = clock();
592 
593  // disabling the index while doing this complex updates is about
594  // 2 to 10 times faster!
595  //setItemIndexMethod(QGraphicsScene::NoIndex);
596 
597  if (end >= 0) {
598  int row = end;
599  qreal linePos = _lines.at(row)->scenePos().y() + _lines.at(row)->height();
600  qreal contentsWidth = width - secondColumnHandle()->sceneRight();
601  while (row >= start) {
602  _lines.at(row--)->setGeometryByWidth(width, contentsWidth, linePos);
603  }
604 
605  if (row >= 0) {
606  // remaining items don't need geometry changes, but maybe repositioning?
607  ChatLine *line = _lines.at(row);
608  qreal offset = linePos - (line->scenePos().y() + line->height());
609  if (offset != 0) {
610  while (row >= 0) {
611  line = _lines.at(row--);
612  line->setPos(0, line->scenePos().y() + offset);
613  }
614  }
615  }
616  }
617 
618  //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
619 
620  updateSceneRect(width);
622  setMarkerLine();
623  emit layoutChanged();
624 
625 // clock_t endT = clock();
626 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
627 }
628 
629 
631 {
632  if (_firstColHandlePos == xpos)
633  return;
634 
635  _firstColHandlePos = xpos >= 0 ? xpos : 0;
636  ChatViewSettings viewSettings(this);
637  viewSettings.setValue("FirstColumnHandlePos", _firstColHandlePos);
638  ChatViewSettings defaultSettings;
639  defaultSettings.setValue("FirstColumnHandlePos", _firstColHandlePos);
640 
641  // clock_t startT = clock();
642 
643  // disabling the index while doing this complex updates is about
644  // 2 to 10 times faster!
645  //setItemIndexMethod(QGraphicsScene::NoIndex);
646 
647  QList<ChatLine *>::iterator lineIter = _lines.end();
648  QList<ChatLine *>::iterator lineIterBegin = _lines.begin();
649  qreal timestampWidth = firstColumnHandle()->sceneLeft();
650  qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
651  QPointF senderPos(firstColumnHandle()->sceneRight(), 0);
652 
653  while (lineIter != lineIterBegin) {
654  lineIter--;
655  (*lineIter)->setFirstColumn(timestampWidth, senderWidth, senderPos);
656  }
657  //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
658 
660 
661 // clock_t endT = clock();
662 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
663 }
664 
665 
667 {
668  if (_secondColHandlePos == xpos)
669  return;
670 
671  _secondColHandlePos = xpos;
672  ChatViewSettings viewSettings(this);
673  viewSettings.setValue("SecondColumnHandlePos", _secondColHandlePos);
674  ChatViewSettings defaultSettings;
675  defaultSettings.setValue("SecondColumnHandlePos", _secondColHandlePos);
676 
677  // clock_t startT = clock();
678 
679  // disabling the index while doing this complex updates is about
680  // 2 to 10 times faster!
681  //setItemIndexMethod(QGraphicsScene::NoIndex);
682 
683  QList<ChatLine *>::iterator lineIter = _lines.end();
684  QList<ChatLine *>::iterator lineIterBegin = _lines.begin();
685  qreal linePos = _sceneRect.y() + _sceneRect.height();
686  qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
687  qreal contentsWidth = _sceneRect.width() - secondColumnHandle()->sceneRight();
688  QPointF contentsPos(secondColumnHandle()->sceneRight(), 0);
689  while (lineIter != lineIterBegin) {
690  lineIter--;
691  (*lineIter)->setSecondColumn(senderWidth, contentsWidth, contentsPos, linePos);
692  }
693  //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
694 
695  updateSceneRect();
697  emit layoutChanged();
698 
699 // clock_t endT = clock();
700 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
701 }
702 
703 
705 {
708  update();
709 }
710 
711 
713 {
715  _selectingItem = item;
716 }
717 
718 
719 void ChatScene::startGlobalSelection(ChatItem *item, const QPointF &itemPos)
720 {
723  _isSelecting = true;
725  updateSelection(item->mapToScene(itemPos));
726 }
727 
728 
729 void ChatScene::updateSelection(const QPointF &pos)
730 {
731  int curRow = rowByScenePos(pos);
732  if (curRow < 0) return;
733  int curColumn = (int)columnByScenePos(pos);
735  if (minColumn != _selectionMinCol) {
736  _selectionMinCol = minColumn;
737  for (int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++) {
738  _lines[l]->setSelected(true, minColumn);
739  }
740  }
741  int newstart = qMin(curRow, _firstSelectionRow);
742  int newend = qMax(curRow, _firstSelectionRow);
743  if (newstart < _selectionStart) {
744  for (int l = newstart; l < _selectionStart; l++)
745  _lines[l]->setSelected(true, minColumn);
746  }
747  if (newstart > _selectionStart) {
748  for (int l = _selectionStart; l < newstart; l++)
749  _lines[l]->setSelected(false);
750  }
751  if (newend > _selectionEnd) {
752  for (int l = _selectionEnd+1; l <= newend; l++)
753  _lines[l]->setSelected(true, minColumn);
754  }
755  if (newend < _selectionEnd) {
756  for (int l = newend+1; l <= _selectionEnd; l++)
757  _lines[l]->setSelected(false);
758  }
759 
760  _selectionStart = newstart;
761  _selectionEnd = newend;
762 
763  if (newstart == newend && minColumn == ChatLineModel::ContentsColumn) {
764  if (!_selectingItem) {
765  // _selectingItem has been removed already
766  return;
767  }
768  _lines[curRow]->setSelected(false);
769  _isSelecting = false;
770  _selectionStart = -1;
772  }
773 }
774 
775 
776 bool ChatScene::isPosOverSelection(const QPointF &pos) const
777 {
778  ChatItem *chatItem = chatItemAt(pos);
779  if (!chatItem)
780  return false;
781  if (hasGlobalSelection()) {
782  int row = chatItem->row();
783  if (row >= qMin(_selectionStart, _selectionEnd) && row <= qMax(_selectionStart, _selectionEnd))
784  return columnByScenePos(pos) >= _selectionMinCol;
785  }
786  else {
787  return chatItem->isPosOverSelection(chatItem->mapFromScene(pos));
788  }
789  return false;
790 }
791 
792 
794 {
795  if (_isSelecting)
796  return false;
797 
798  // TODO: Handle clicks and single-item selections too
799 
800  return true;
801 }
802 
803 
804 /******** MOUSE HANDLING **************************************************************************/
805 
806 void ChatScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
807 {
808  QPointF pos = event->scenePos();
809  QMenu menu;
810 
811  // zoom actions and similar
812  chatView()->addActionsToMenu(&menu, pos);
813  menu.addSeparator();
814 
815  // item-specific options (select link etc)
816  ChatItem *item = chatItemAt(pos);
817  if (item)
818  item->addActionsToMenu(&menu, item->mapFromScene(pos));
819  else
820  // no item -> default scene actions
822 
823  // If we have text selected, insert the Copy Selection as first item
824  if (isPosOverSelection(pos)) {
825  QAction *sep = menu.insertSeparator(menu.actions().first());
826  QAction *act = new Action(QIcon::fromTheme("edit-copy"), tr("Copy Selection"), &menu, this,
827  SLOT(selectionToClipboard()), QKeySequence::Copy);
828  menu.insertAction(sep, act);
829 
830  QString searchSelectionText = selection();
831  if (searchSelectionText.length() > _webSearchSelectionTextMaxVisible)
832  searchSelectionText = searchSelectionText.left(_webSearchSelectionTextMaxVisible).append(QString::fromUtf8("…"));
833  searchSelectionText = tr("Search '%1'").arg(searchSelectionText);
834 
835  QAction *webSearchAction = new Action(QIcon::fromTheme("edit-find"), searchSelectionText, &menu, this, SLOT(webSearchOnSelection()));
836  menu.insertAction(sep, webSearchAction);
837  }
838 
839  if (QtUi::mainWindow()->menuBar()->isHidden())
840  menu.addAction(QtUi::actionCollection("General")->action("ToggleMenuBar"));
841 
842  // show column reset action if columns have been resized in this session or there is at least one very narrow column
845  menu.addAction(new Action(tr("Reset Column Widths"), &menu, this, SLOT(resetColumnWidths()), 0));
846 
847  menu.exec(event->screenPos());
848 }
849 
850 
851 void ChatScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
852 {
853  if (event->buttons() == Qt::LeftButton) {
854  if (!_clickHandled && (event->scenePos() - _clickPos).toPoint().manhattanLength() >= QApplication::startDragDistance()) {
855  if (_clickTimer.isActive())
856  _clickTimer.stop();
858  initiateDrag(event->widget());
859  else {
861  handleClick(Qt::LeftButton, _clickPos);
862  }
864  }
865  if (_isSelecting) {
866  updateSelection(event->scenePos());
867  emit mouseMoveWhileSelecting(event->scenePos());
868  event->accept();
869  }
870  else if (_clickHandled && _clickMode < DoubleClick)
872  }
873  else
875 }
876 
877 
878 void ChatScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
879 {
880  if (event->buttons() == Qt::LeftButton) {
881  _leftButtonPressed = true;
882  _clickHandled = false;
883  if (!isPosOverSelection(event->scenePos())) {
884  // immediately clear selection if clicked outside; otherwise, wait for potential drag
885  clearSelection();
886  }
887  if (_clickMode != NoClick && _clickTimer.isActive()) {
888  switch (_clickMode) {
889  case NoClick:
890  _clickMode = SingleClick; break;
891  case SingleClick:
892  _clickMode = DoubleClick; break;
893  case DoubleClick:
894  _clickMode = TripleClick; break;
895  case TripleClick:
896  _clickMode = DoubleClick; break;
897  case DragStartClick:
898  break;
899  }
900  handleClick(Qt::LeftButton, _clickPos);
901  }
902  else {
904  _clickPos = event->scenePos();
905  }
906  _clickTimer.start();
907  }
908  if (event->type() == QEvent::GraphicsSceneMouseDoubleClick)
909  QGraphicsScene::mouseDoubleClickEvent(event);
910  else
912 }
913 
914 
915 void ChatScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
916 {
917  // we check for doubleclick ourselves, so just call press handler
918  mousePressEvent(event);
919 }
920 
921 
922 void ChatScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
923 {
924  if (event->button() == Qt::LeftButton && _leftButtonPressed) {
925  _leftButtonPressed = false;
926  if (_clickMode != NoClick) {
927  if (_clickMode == SingleClick)
928  clearSelection();
929  event->accept();
930  if (!_clickTimer.isActive())
931  handleClick(Qt::LeftButton, _clickPos);
932  }
933  else {
934  // no click -> drag or selection move
935  if (isGloballySelecting()) {
936  selectionToClipboard(QClipboard::Selection);
937  _isSelecting = false;
938  event->accept();
939  return;
940  }
941  }
942  }
944 }
945 
946 
948 {
950  handleClick(Qt::LeftButton, _clickPos);
951 }
952 
953 
954 void ChatScene::handleClick(Qt::MouseButton button, const QPointF &scenePos)
955 {
956  if (button == Qt::LeftButton) {
957  clearSelection();
958 
959  // Now send click down to items
960  ChatItem *chatItem = chatItemAt(scenePos);
961  if (chatItem) {
962  chatItem->handleClick(chatItem->mapFromScene(scenePos), _clickMode);
963  }
964  _clickHandled = true;
965  }
966 }
967 
968 
969 void ChatScene::initiateDrag(QWidget *source)
970 {
971  QDrag *drag = new QDrag(source);
972  QMimeData *mimeData = new QMimeData;
973  mimeData->setText(selection());
974  drag->setMimeData(mimeData);
975 
976  drag->exec(Qt::CopyAction);
977 }
978 
979 
980 /******** SELECTIONS ******************************************************************************/
981 
982 void ChatScene::selectionToClipboard(QClipboard::Mode mode)
983 {
984  if (!hasSelection())
985  return;
986 
987  stringToClipboard(selection(), mode);
988 }
989 
990 
991 void ChatScene::stringToClipboard(const QString &str_, QClipboard::Mode mode)
992 {
993  QString str = str_;
994  // remove trailing linefeeds
995  if (str.endsWith('\n'))
996  str.chop(1);
997 
998  switch (mode) {
999  case QClipboard::Clipboard:
1000  QApplication::clipboard()->setText(str);
1001  break;
1002  case QClipboard::Selection:
1003  if (QApplication::clipboard()->supportsSelection())
1004  QApplication::clipboard()->setText(str, QClipboard::Selection);
1005  break;
1006  default:
1007  break;
1008  };
1009 }
1010 
1011 
1013 QString ChatScene::selection() const
1014 {
1015  //TODO Make selection format configurable!
1016  if (hasGlobalSelection()) {
1017  int start = qMin(_selectionStart, _selectionEnd);
1018  int end = qMax(_selectionStart, _selectionEnd);
1019  if (start < 0 || end >= _lines.count()) {
1020  qDebug() << "Invalid selection range:" << start << end;
1021  return QString();
1022  }
1023  QString result;
1024  for (int l = start; l <= end; l++) {
1026  result += _lines[l]->item(ChatLineModel::TimestampColumn)->data(MessageModel::DisplayRole).toString() + " ";
1028  result += _lines[l]->item(ChatLineModel::SenderColumn)->data(MessageModel::DisplayRole).toString() + " ";
1029  result += _lines[l]->item(ChatLineModel::ContentsColumn)->data(MessageModel::DisplayRole).toString() + "\n";
1030  }
1031  return result;
1032  }
1033  else if (selectingItem())
1034  return selectingItem()->selection();
1035  return QString();
1036 }
1037 
1038 
1040 {
1042 }
1043 
1044 
1046 {
1047  return _selectionStart >= 0;
1048 }
1049 
1050 
1052 {
1053  return _isSelecting;
1054 }
1055 
1056 
1058 {
1059  if (hasGlobalSelection()) {
1060  for (int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++)
1061  _lines[l]->setSelected(false);
1062  _isSelecting = false;
1063  _selectionStart = -1;
1064  }
1065 }
1066 
1067 
1069 {
1071  if (selectingItem())
1073 }
1074 
1075 
1076 /******** *************************************************************************************/
1077 
1079 {
1080  if (!hasSelection())
1081  return;
1082 
1083  ChatViewSettings settings;
1084  QString webSearchBaseUrl = settings.webSearchUrlFormatString();
1085  QString webSearchUrl = webSearchBaseUrl.replace(QString("%s"), selection());
1086  QUrl url = QUrl::fromUserInput(webSearchUrl);
1087  QDesktopServices::openUrl(url);
1088 }
1089 
1090 
1091 /******** *************************************************************************************/
1092 
1094 {
1095  MessageFilter *filter = qobject_cast<MessageFilter *>(model());
1096  if (filter)
1097  return filter->requestBacklog();
1098  return;
1099 }
1100 
1101 
1103 {
1104  if (x < _firstColHandle->x())
1106  if (x < _secondColHandle->x())
1108 
1110 }
1111 
1112 
1113 int ChatScene::rowByScenePos(qreal y) const
1114 {
1115  QList<QGraphicsItem *> itemList = items(QPointF(0, y));
1116 
1117  // ChatLine should be at the bottom of the list
1118  for (int i = itemList.count()-1; i >= 0; i--) {
1119  ChatLine *line = qgraphicsitem_cast<ChatLine *>(itemList.at(i));
1120  if (line)
1121  return line->row();
1122  }
1123  return -1;
1124 }
1125 
1126 
1128 {
1129  if (_lines.isEmpty()) {
1130  updateSceneRect(QRectF(0, 0, width, 0));
1131  return;
1132  }
1133 
1134  // we hide day change messages at the top by making the scene rect smaller
1135  // and by calling QGraphicsItem::hide() on all leading day change messages
1136  // the first one is needed to ensure proper scrollbar ranges
1137  // the second for cases where the viewport is larger then the set scenerect
1138  // (in this case the items are shown anyways)
1139  if (_firstLineRow == -1) {
1140  int numRows = model()->rowCount();
1141  _firstLineRow = 0;
1142  QModelIndex firstLineIdx;
1143  while (_firstLineRow < numRows) {
1144  firstLineIdx = model()->index(_firstLineRow, 0);
1145  if ((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) != Message::DayChange)
1146  break;
1147  _lines.at(_firstLineRow)->hide();
1148  _firstLineRow++;
1149  }
1150  }
1151 
1152  // the following call should be safe. If it crashes something went wrong during insert/remove
1153  if (_firstLineRow < _lines.count()) {
1154  ChatLine *firstLine = _lines.at(_firstLineRow);
1155  ChatLine *lastLine = _lines.last();
1156  updateSceneRect(QRectF(0, firstLine->pos().y(), width, lastLine->pos().y() + lastLine->height() - firstLine->pos().y()));
1157  }
1158  else {
1159  // empty scene rect
1160  updateSceneRect(QRectF(0, 0, width, 0));
1161  }
1162 }
1163 
1164 
1165 void ChatScene::updateSceneRect(const QRectF &rect)
1166 {
1167  _sceneRect = rect;
1168  setSceneRect(rect);
1169  update();
1170 }
1171 
1172 
1173 // ========================================
1174 // Webkit Only stuff
1175 // ========================================
1176 #ifdef HAVE_WEBKIT
1177 void ChatScene::loadWebPreview(ChatItem *parentItem, const QUrl &url, const QRectF &urlRect)
1178 {
1179  if (!_showWebPreview)
1180  return;
1181 
1182  if (webPreview.urlRect != urlRect)
1183  webPreview.urlRect = urlRect;
1184 
1185  if (webPreview.parentItem != parentItem)
1186  webPreview.parentItem = parentItem;
1187 
1188  if (webPreview.url != url) {
1189  webPreview.url = url;
1190  // prepare to load a different URL
1191  if (webPreview.previewItem) {
1192  if (webPreview.previewItem->scene())
1193  removeItem(webPreview.previewItem);
1194  delete webPreview.previewItem;
1195  webPreview.previewItem = 0;
1196  }
1197  webPreview.previewState = WebPreview::NoPreview;
1198  }
1199 
1200  if (webPreview.url.isEmpty())
1201  return;
1202 
1203  // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1204  switch (webPreview.previewState) {
1205  case WebPreview::NoPreview:
1206  webPreview.previewState = WebPreview::NewPreview;
1207  webPreview.timer.start(500);
1208  break;
1209  case WebPreview::NewPreview:
1210  case WebPreview::DelayPreview:
1211  case WebPreview::ShowPreview:
1212  // we're already waiting for the next step or showing the preview
1213  break;
1214  case WebPreview::HidePreview:
1215  // we still have a valid preview
1216  webPreview.previewState = WebPreview::DelayPreview;
1217  webPreview.timer.start(1000);
1218  break;
1219  }
1220  // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
1221 }
1222 
1223 
1224 void ChatScene::webPreviewNextStep()
1225 {
1226  // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1227  switch (webPreview.previewState) {
1228  case WebPreview::NoPreview:
1229  break;
1230  case WebPreview::NewPreview:
1231  Q_ASSERT(!webPreview.previewItem);
1232  webPreview.previewItem = new WebPreviewItem(webPreview.url);
1233  webPreview.previewState = WebPreview::DelayPreview;
1234  webPreview.timer.start(1000);
1235  break;
1236  case WebPreview::DelayPreview:
1237  Q_ASSERT(webPreview.previewItem);
1238  // calc position and show
1239  {
1240  qreal previewY = webPreview.urlRect.bottom();
1241  qreal previewX = webPreview.urlRect.x();
1242  if (previewY + webPreview.previewItem->boundingRect().height() > sceneRect().bottom())
1243  previewY = webPreview.urlRect.y() - webPreview.previewItem->boundingRect().height();
1244 
1245  if (previewX + webPreview.previewItem->boundingRect().width() > sceneRect().width())
1246  previewX = sceneRect().right() - webPreview.previewItem->boundingRect().width();
1247 
1248  webPreview.previewItem->setPos(previewX, previewY);
1249  }
1250  addItem(webPreview.previewItem);
1251  webPreview.previewState = WebPreview::ShowPreview;
1252  break;
1253  case WebPreview::ShowPreview:
1254  qWarning() << "ChatScene::webPreviewNextStep() called while in ShowPreview Step!";
1255  qWarning() << "removing preview";
1256  if (webPreview.previewItem && webPreview.previewItem->scene())
1257  removeItem(webPreview.previewItem);
1258  // Fall through to deletion!
1259  case WebPreview::HidePreview:
1260  if (webPreview.previewItem) {
1261  delete webPreview.previewItem;
1262  webPreview.previewItem = 0;
1263  }
1264  webPreview.parentItem = 0;
1265  webPreview.url = QUrl();
1266  webPreview.urlRect = QRectF();
1267  webPreview.previewState = WebPreview::NoPreview;
1268  }
1269  // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
1270 }
1271 
1272 
1273 void ChatScene::clearWebPreview(ChatItem *parentItem)
1274 {
1275  // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1276  switch (webPreview.previewState) {
1277  case WebPreview::NewPreview:
1278  webPreview.previewState = WebPreview::NoPreview; // we haven't loaded anything yet
1279  break;
1280  case WebPreview::ShowPreview:
1281  if (parentItem == 0 || webPreview.parentItem == parentItem) {
1282  if (webPreview.previewItem && webPreview.previewItem->scene())
1283  removeItem(webPreview.previewItem);
1284  }
1285  // fall through into to set hidden state
1286  case WebPreview::DelayPreview:
1287  // we're just loading, so haven't shown the preview yet.
1288  webPreview.previewState = WebPreview::HidePreview;
1289  webPreview.timer.start(5000);
1290  break;
1291  case WebPreview::NoPreview:
1292  case WebPreview::HidePreview:
1293  break;
1294  }
1295  // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
1296 }
1297 
1298 
1299 #endif
1300 
1301 // ========================================
1302 // end of webkit only
1303 // ========================================
1304 
1306 {
1307  ChatViewSettings settings;
1308  _showWebPreview = settings.showWebPreview();
1309 }