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