Fix compiler warnings
[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
433   // clear selection
434   if(_selectingItem) {
435     int row = _selectingItem->row();
436     if(row >= start && row <= end)
437       setSelectingItem(0);
438   }
439
440   // remove items from scene
441   QList<ChatLine *>::iterator lineIter = _lines.begin() + start;
442   int lineCount = start;
443   while(lineIter != _lines.end() && lineCount <= end) {
444     if((*lineIter) == markerLine()->chatLine())
445       markerLine()->setChatLine(0);
446     h += (*lineIter)->height();
447     delete *lineIter;
448     lineIter = _lines.erase(lineIter);
449     lineCount++;
450   }
451
452   // update rows of remaining chatlines
453   for(int i = start; i < _lines.count(); i++) {
454     _lines.at(i)->setRow(i);
455   }
456
457   // update selection
458   if(_selectionStart >= 0) {
459     int offset = end - start + 1;
460     if(_selectionStart >= start)
461       _selectionStart = qMax(_selectionStart - offset, start);
462     if(_selectionEnd >= start)
463       _selectionEnd -= offset;
464     if(_firstSelectionRow >= start)
465       _firstSelectionRow -= offset;
466
467     if(_selectionEnd < _selectionStart) {
468       _isSelecting = false;
469       _selectionStart = -1;
470     }
471   }
472
473   // neither removing at bottom or top means we have to move items...
474   if(!(atTop || atBottom)) {
475     qreal offset = h;
476     int moveStart = 0;
477     int moveEnd = _lines.count() - 1;
478     if(start < _lines.count() - start) {
479       // move top part
480       moveEnd = start - 1;
481     } else {
482       // move bottom part
483       moveStart = start;
484       offset = -offset;
485     }
486     ChatLine *line = 0;
487     for(int i = moveStart; i <= moveEnd; i++) {
488       line = _lines.at(i);
489       line->setPos(0, line->pos().y() + offset);
490     }
491   }
492
493   Q_ASSERT(start == 0 || start >= _lines.count() || _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() == _lines.at(start)->pos().y());
494
495   // update sceneRect
496   // when searching for the first non-date-line we have to take into account that our
497   // model still contains the just removed lines so we cannot simply call updateSceneRect()
498   int numRows = model()->rowCount();
499   QModelIndex firstLineIdx;
500   _firstLineRow = -1;
501   bool needOffset = false;
502   do {
503     _firstLineRow++;
504     if(_firstLineRow >= start && _firstLineRow <= end) {
505       _firstLineRow = end + 1;
506       needOffset = true;
507     }
508     firstLineIdx = model()->index(_firstLineRow, 0);
509   } while((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) == Message::DayChange && _firstLineRow < numRows);
510
511   if(needOffset)
512     _firstLineRow -= end - start + 1;
513   updateSceneRect();
514 }
515
516 void ChatScene::rowsRemoved() {
517   // move the marker line if necessary
518   setMarkerLine();
519 }
520
521 void ChatScene::dataChanged(const QModelIndex &tl, const QModelIndex &br) {
522   layout(tl.row(), br.row(), _sceneRect.width());
523 }
524
525 void ChatScene::updateForViewport(qreal width, qreal height) {
526   _viewportHeight = height;
527   setWidth(width);
528 }
529
530 void ChatScene::setWidth(qreal width) {
531   if(width == _sceneRect.width())
532     return;
533   layout(0, _lines.count()-1, width);
534 }
535
536 void ChatScene::layout(int start, int end, qreal width) {
537   // clock_t startT = clock();
538
539   // disabling the index while doing this complex updates is about
540   // 2 to 10 times faster!
541   //setItemIndexMethod(QGraphicsScene::NoIndex);
542
543   if(end >= 0) {
544     int row = end;
545     qreal linePos = _lines.at(row)->scenePos().y() + _lines.at(row)->height();
546     qreal contentsWidth = width - secondColumnHandle()->sceneRight();
547     while(row >= start) {
548       _lines.at(row--)->setGeometryByWidth(width, contentsWidth, linePos);
549     }
550
551     if(row >= 0) {
552       // remaining items don't need geometry changes, but maybe repositioning?
553       ChatLine *line = _lines.at(row);
554       qreal offset = linePos - (line->scenePos().y() + line->height());
555       if(offset != 0) {
556         while(row >= 0) {
557           line = _lines.at(row--);
558           line->setPos(0, line->scenePos().y() + offset);
559         }
560       }
561     }
562   }
563
564   //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
565
566   updateSceneRect(width);
567   setHandleXLimits();
568   setMarkerLine();
569   emit layoutChanged();
570
571 //   clock_t endT = clock();
572 //   qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
573 }
574
575 void ChatScene::firstHandlePositionChanged(qreal xpos) {
576   if(_firstColHandlePos == xpos)
577     return;
578
579   _firstColHandlePos = xpos >= 0 ? xpos : 0;
580   ChatViewSettings viewSettings(this);
581   viewSettings.setValue("FirstColumnHandlePos", _firstColHandlePos);
582   ChatViewSettings defaultSettings;
583   defaultSettings.setValue("FirstColumnHandlePos", _firstColHandlePos);
584
585   // clock_t startT = clock();
586
587   // disabling the index while doing this complex updates is about
588   // 2 to 10 times faster!
589   //setItemIndexMethod(QGraphicsScene::NoIndex);
590
591   QList<ChatLine *>::iterator lineIter = _lines.end();
592   QList<ChatLine *>::iterator lineIterBegin = _lines.begin();
593   qreal timestampWidth = firstColumnHandle()->sceneLeft();
594   qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
595   QPointF senderPos(firstColumnHandle()->sceneRight(), 0);
596
597   while(lineIter != lineIterBegin) {
598     lineIter--;
599     (*lineIter)->setFirstColumn(timestampWidth, senderWidth, senderPos);
600   }
601   //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
602
603   setHandleXLimits();
604
605 //   clock_t endT = clock();
606 //   qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
607 }
608
609 void ChatScene::secondHandlePositionChanged(qreal xpos) {
610   if(_secondColHandlePos == xpos)
611     return;
612
613   _secondColHandlePos = xpos;
614   ChatViewSettings viewSettings(this);
615   viewSettings.setValue("SecondColumnHandlePos", _secondColHandlePos);
616   ChatViewSettings defaultSettings;
617   defaultSettings.setValue("SecondColumnHandlePos", _secondColHandlePos);
618
619   // clock_t startT = clock();
620
621   // disabling the index while doing this complex updates is about
622   // 2 to 10 times faster!
623   //setItemIndexMethod(QGraphicsScene::NoIndex);
624
625   QList<ChatLine *>::iterator lineIter = _lines.end();
626   QList<ChatLine *>::iterator lineIterBegin = _lines.begin();
627   qreal linePos = _sceneRect.y() + _sceneRect.height();
628   qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
629   qreal contentsWidth = _sceneRect.width() - secondColumnHandle()->sceneRight();
630   QPointF contentsPos(secondColumnHandle()->sceneRight(), 0);
631   while(lineIter != lineIterBegin) {
632     lineIter--;
633     (*lineIter)->setSecondColumn(senderWidth, contentsWidth, contentsPos, linePos);
634   }
635   //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
636
637   updateSceneRect();
638   setHandleXLimits();
639   emit layoutChanged();
640
641 //   clock_t endT = clock();
642 //   qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
643 }
644
645 void ChatScene::setHandleXLimits() {
646   _firstColHandle->setXLimits(0, _secondColHandle->sceneLeft());
647   _secondColHandle->setXLimits(_firstColHandle->sceneRight(), width() - minContentsWidth);
648   update();
649 }
650
651 void ChatScene::setSelectingItem(ChatItem *item) {
652   if(_selectingItem) _selectingItem->clearSelection();
653   _selectingItem = item;
654 }
655
656 void ChatScene::startGlobalSelection(ChatItem *item, const QPointF &itemPos) {
657   _selectionStart = _selectionEnd = _firstSelectionRow = item->row();
658   _selectionStartCol = _selectionMinCol = item->column();
659   _isSelecting = true;
660   _lines[_selectionStart]->setSelected(true, (ChatLineModel::ColumnType)_selectionMinCol);
661   updateSelection(item->mapToScene(itemPos));
662 }
663
664 void ChatScene::updateSelection(const QPointF &pos) {
665   int curRow = rowByScenePos(pos);
666   if(curRow < 0) return;
667   int curColumn = (int)columnByScenePos(pos);
668   ChatLineModel::ColumnType minColumn = (ChatLineModel::ColumnType)qMin(curColumn, _selectionStartCol);
669   if(minColumn != _selectionMinCol) {
670     _selectionMinCol = minColumn;
671     for(int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++) {
672       _lines[l]->setSelected(true, minColumn);
673     }
674   }
675   int newstart = qMin(curRow, _firstSelectionRow);
676   int newend = qMax(curRow, _firstSelectionRow);
677   if(newstart < _selectionStart) {
678     for(int l = newstart; l < _selectionStart; l++)
679       _lines[l]->setSelected(true, minColumn);
680   }
681   if(newstart > _selectionStart) {
682     for(int l = _selectionStart; l < newstart; l++)
683       _lines[l]->setSelected(false);
684   }
685   if(newend > _selectionEnd) {
686     for(int l = _selectionEnd+1; l <= newend; l++)
687       _lines[l]->setSelected(true, minColumn);
688   }
689   if(newend < _selectionEnd) {
690     for(int l = newend+1; l <= _selectionEnd; l++)
691       _lines[l]->setSelected(false);
692   }
693
694   _selectionStart = newstart;
695   _selectionEnd = newend;
696
697   if(newstart == newend && minColumn == ChatLineModel::ContentsColumn) {
698     if(!_selectingItem) {
699       // _selectingItem has been removed already
700       return;
701     }
702     _lines[curRow]->setSelected(false);
703     _isSelecting = false;
704     _selectionStart = -1;
705     _selectingItem->continueSelecting(_selectingItem->mapFromScene(pos));
706   }
707 }
708
709 bool ChatScene::isPosOverSelection(const QPointF &pos) const {
710   ChatItem *chatItem = chatItemAt(pos);
711   if(!chatItem)
712     return false;
713   if(hasGlobalSelection()) {
714     int row = chatItem->row();
715     if(row >= qMin(_selectionStart, _selectionEnd) && row <= qMax(_selectionStart, _selectionEnd))
716       return columnByScenePos(pos) >= _selectionMinCol;
717   } else {
718     return chatItem->isPosOverSelection(chatItem->mapFromScene(pos));
719   }
720   return false;
721 }
722
723 bool ChatScene::isScrollingAllowed() const {
724   if(_isSelecting)
725     return false;
726
727   // TODO: Handle clicks and single-item selections too
728
729   return true;
730 }
731
732 /******** MOUSE HANDLING **************************************************************************/
733
734 void ChatScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
735   QPointF pos = event->scenePos();
736   QMenu menu;
737
738   // zoom actions and similar
739   chatView()->addActionsToMenu(&menu, pos);
740   menu.addSeparator();
741
742   if(isPosOverSelection(pos))
743     menu.addAction(SmallIcon("edit-copy"), tr("Copy Selection"),
744                     this, SLOT(selectionToClipboard()),
745                     QKeySequence::Copy);
746
747   // item-specific options (select link etc)
748   ChatItem *item = chatItemAt(pos);
749   if(item)
750     item->addActionsToMenu(&menu, item->mapFromScene(pos));
751   else
752     // no item -> default scene actions
753     GraphicalUi::contextMenuActionProvider()->addActions(&menu, filter(), BufferId());
754
755   if (QtUi::mainWindow()->menuBar()->isHidden())
756     menu.addAction(QtUi::actionCollection("General")->action("ToggleMenuBar"));
757
758   menu.exec(event->screenPos());
759
760 }
761
762 void ChatScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
763   if(event->buttons() == Qt::LeftButton) {
764     if(!_clickHandled && (event->scenePos() - _clickPos).toPoint().manhattanLength() >= QApplication::startDragDistance()) {
765       if(_clickTimer.isActive())
766         _clickTimer.stop();
767       if(_clickMode == SingleClick && isPosOverSelection(_clickPos))
768         initiateDrag(event->widget());
769       else {
770         _clickMode = DragStartClick;
771         handleClick(Qt::LeftButton, _clickPos);
772       }
773       _clickMode = NoClick;
774     }
775     if(_isSelecting) {
776       updateSelection(event->scenePos());
777       emit mouseMoveWhileSelecting(event->scenePos());
778       event->accept();
779     } else if(_clickHandled && _clickMode < DoubleClick)
780       QGraphicsScene::mouseMoveEvent(event);
781   } else
782     QGraphicsScene::mouseMoveEvent(event);
783 }
784
785 void ChatScene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
786   if(event->buttons() == Qt::LeftButton) {
787     _leftButtonPressed = true;
788     _clickHandled = false;
789     if(!isPosOverSelection(event->scenePos())) {
790       // immediately clear selection if clicked outside; otherwise, wait for potential drag
791       clearSelection();
792     }
793     if(_clickMode != NoClick && _clickTimer.isActive()) {
794       switch(_clickMode) {
795         case NoClick: _clickMode = SingleClick; break;
796         case SingleClick: _clickMode = DoubleClick; break;
797         case DoubleClick: _clickMode = TripleClick; break;
798         case TripleClick: _clickMode = DoubleClick; break;
799         case DragStartClick: break;
800       }
801       handleClick(Qt::LeftButton, _clickPos);
802     } else {
803       _clickMode = SingleClick;
804       _clickPos = event->scenePos();
805     }
806     _clickTimer.start();
807   }
808   if(event->type() == QEvent::GraphicsSceneMouseDoubleClick)
809     QGraphicsScene::mouseDoubleClickEvent(event);
810   else
811     QGraphicsScene::mousePressEvent(event);
812 }
813
814 void ChatScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
815   // we check for doubleclick ourselves, so just call press handler
816   mousePressEvent(event);
817 }
818
819 void ChatScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
820   if(event->button() == Qt::LeftButton && _leftButtonPressed) {
821     _leftButtonPressed = false;
822     if(_clickMode != NoClick) {
823       if(_clickMode == SingleClick)
824         clearSelection();
825       event->accept();
826       if(!_clickTimer.isActive())
827         handleClick(Qt::LeftButton, _clickPos);
828     } else {
829       // no click -> drag or selection move
830       if(isGloballySelecting()) {
831         selectionToClipboard(QClipboard::Selection);
832         _isSelecting = false;
833         event->accept();
834         return;
835       }
836     }
837   }
838   QGraphicsScene::mouseReleaseEvent(event);
839 }
840
841 void ChatScene::clickTimeout() {
842   if(!_leftButtonPressed && _clickMode == SingleClick)
843     handleClick(Qt::LeftButton, _clickPos);
844 }
845
846 void ChatScene::handleClick(Qt::MouseButton button, const QPointF &scenePos) {
847   if(button == Qt::LeftButton) {
848     clearSelection();
849
850     // Now send click down to items
851     ChatItem *chatItem = chatItemAt(scenePos);
852     if(chatItem) {
853       chatItem->handleClick(chatItem->mapFromScene(scenePos), _clickMode);
854     }
855     _clickHandled = true;
856   }
857 }
858
859 void ChatScene::initiateDrag(QWidget *source) {
860   QDrag *drag = new QDrag(source);
861   QMimeData *mimeData = new QMimeData;
862   mimeData->setText(selection());
863   drag->setMimeData(mimeData);
864
865   drag->exec(Qt::CopyAction);
866 }
867
868 /******** SELECTIONS ******************************************************************************/
869
870 void ChatScene::selectionToClipboard(QClipboard::Mode mode) {
871   if(!hasSelection())
872     return;
873
874   stringToClipboard(selection(), mode);
875 }
876
877 void ChatScene::stringToClipboard(const QString &str_, QClipboard::Mode mode) {
878   QString str = str_;
879   // remove trailing linefeeds
880   if(str.endsWith('\n'))
881     str.chop(1);
882
883   switch(mode) {
884     case QClipboard::Clipboard:
885       QApplication::clipboard()->setText(str);
886       break;
887     case QClipboard::Selection:
888       if(QApplication::clipboard()->supportsSelection())
889         QApplication::clipboard()->setText(str, QClipboard::Selection);
890       break;
891     default:
892       break;
893   };
894 }
895
896 //!\brief Convert current selection to human-readable string.
897 QString ChatScene::selection() const {
898   //TODO Make selection format configurable!
899   if(hasGlobalSelection()) {
900     int start = qMin(_selectionStart, _selectionEnd);
901     int end = qMax(_selectionStart, _selectionEnd);
902     if(start < 0 || end >= _lines.count()) {
903       qDebug() << "Invalid selection range:" << start << end;
904       return QString();
905     }
906     QString result;
907     for(int l = start; l <= end; l++) {
908       if(_selectionMinCol == ChatLineModel::TimestampColumn)
909         result += _lines[l]->item(ChatLineModel::TimestampColumn)->data(MessageModel::DisplayRole).toString() + " ";
910       if(_selectionMinCol <= ChatLineModel::SenderColumn)
911         result += _lines[l]->item(ChatLineModel::SenderColumn)->data(MessageModel::DisplayRole).toString() + " ";
912       result += _lines[l]->item(ChatLineModel::ContentsColumn)->data(MessageModel::DisplayRole).toString() + "\n";
913     }
914     return result;
915   } else if(selectingItem())
916     return selectingItem()->selection();
917   return QString();
918 }
919
920 bool ChatScene::hasSelection() const {
921   return hasGlobalSelection() || (selectingItem() && selectingItem()->hasSelection());
922 }
923
924 bool ChatScene::hasGlobalSelection() const {
925   return _selectionStart >= 0;
926 }
927
928 bool ChatScene::isGloballySelecting() const {
929   return _isSelecting;
930 }
931
932 void ChatScene::clearGlobalSelection() {
933   if(hasGlobalSelection()) {
934     for(int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++)
935       _lines[l]->setSelected(false);
936     _isSelecting = false;
937     _selectionStart = -1;
938   }
939 }
940
941 void ChatScene::clearSelection() {
942   clearGlobalSelection();
943   if(selectingItem())
944     selectingItem()->clearSelection();
945 }
946
947 /******** *************************************************************************************/
948
949 void ChatScene::requestBacklog() {
950   MessageFilter *filter = qobject_cast<MessageFilter*>(model());
951   if(filter)
952     return filter->requestBacklog();
953   return;
954 }
955
956 ChatLineModel::ColumnType ChatScene::columnByScenePos(qreal x) const {
957   if(x < _firstColHandle->x())
958     return ChatLineModel::TimestampColumn;
959   if(x < _secondColHandle->x())
960     return ChatLineModel::SenderColumn;
961
962   return ChatLineModel::ContentsColumn;
963 }
964
965 int ChatScene::rowByScenePos(qreal y) const {
966   QList<QGraphicsItem*> itemList = items(QPointF(0, y));
967
968   // ChatLine should be at the bottom of the list
969   for(int i = itemList.count()-1; i >= 0; i--) {
970     ChatLine *line = qgraphicsitem_cast<ChatLine *>(itemList.at(i));
971     if(line)
972       return line->row();
973   }
974   return -1;
975 }
976
977 void ChatScene::updateSceneRect(qreal width) {
978   if(_lines.isEmpty()) {
979     updateSceneRect(QRectF(0, 0, width, 0));
980     return;
981   }
982
983   // we hide day change messages at the top by making the scene rect smaller
984   // and by calling QGraphicsItem::hide() on all leading day change messages
985   // the first one is needed to ensure proper scrollbar ranges
986   // the second for cases where the viewport is larger then the set scenerect
987   //  (in this case the items are shown anyways)
988   if(_firstLineRow == -1) {
989     int numRows = model()->rowCount();
990     _firstLineRow = 0;
991     QModelIndex firstLineIdx;
992     while(_firstLineRow < numRows) {
993       firstLineIdx = model()->index(_firstLineRow, 0);
994       if((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) != Message::DayChange)
995         break;
996       _lines.at(_firstLineRow)->hide();
997       _firstLineRow++;
998     }
999   }
1000
1001   // the following call should be safe. If it crashes something went wrong during insert/remove
1002   if(_firstLineRow < _lines.count()) {
1003     ChatLine *firstLine = _lines.at(_firstLineRow);
1004     ChatLine *lastLine = _lines.last();
1005     updateSceneRect(QRectF(0, firstLine->pos().y(), width, lastLine->pos().y() + lastLine->height() - firstLine->pos().y()));
1006   } else {
1007     // empty scene rect
1008     updateSceneRect(QRectF(0, 0, width, 0));
1009   }
1010 }
1011
1012 void ChatScene::updateSceneRect(const QRectF &rect) {
1013   _sceneRect = rect;
1014   setSceneRect(rect);
1015   update();
1016 }
1017
1018 // ========================================
1019 //  Webkit Only stuff
1020 // ========================================
1021 #ifdef HAVE_WEBKIT
1022 void ChatScene::loadWebPreview(ChatItem *parentItem, const QUrl &url, const QRectF &urlRect) {
1023   if(!_showWebPreview)
1024     return;
1025
1026   if(webPreview.urlRect != urlRect)
1027     webPreview.urlRect = urlRect;
1028
1029   if(webPreview.parentItem != parentItem)
1030     webPreview.parentItem = parentItem;
1031
1032   if(webPreview.url != url) {
1033     webPreview.url = url;
1034     // prepare to load a different URL
1035     if(webPreview.previewItem) {
1036       if(webPreview.previewItem->scene())
1037         removeItem(webPreview.previewItem);
1038       delete webPreview.previewItem;
1039       webPreview.previewItem = 0;
1040     }
1041     webPreview.previewState = WebPreview::NoPreview;
1042   }
1043
1044   if(webPreview.url.isEmpty())
1045     return;
1046
1047   // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1048   switch(webPreview.previewState) {
1049   case WebPreview::NoPreview:
1050     webPreview.previewState = WebPreview::NewPreview;
1051     webPreview.timer.start(500);
1052     break;
1053   case WebPreview::NewPreview:
1054   case WebPreview::DelayPreview:
1055   case WebPreview::ShowPreview:
1056     // we're already waiting for the next step or showing the preview
1057     break;
1058   case WebPreview::HidePreview:
1059     // we still have a valid preview
1060     webPreview.previewState = WebPreview::DelayPreview;
1061     webPreview.timer.start(1000);
1062     break;
1063   }
1064   // qDebug() << "  new State:" << webPreview.previewState << webPreview.timer.isActive();
1065 }
1066
1067 void ChatScene::webPreviewNextStep() {
1068   // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1069   switch(webPreview.previewState) {
1070   case WebPreview::NoPreview:
1071     break;
1072   case WebPreview::NewPreview:
1073     Q_ASSERT(!webPreview.previewItem);
1074     webPreview.previewItem = new WebPreviewItem(webPreview.url);
1075     webPreview.previewState = WebPreview::DelayPreview;
1076     webPreview.timer.start(1000);
1077     break;
1078   case WebPreview::DelayPreview:
1079     Q_ASSERT(webPreview.previewItem);
1080     // calc position and show
1081     {
1082       qreal previewY = webPreview.urlRect.bottom();
1083       qreal previewX = webPreview.urlRect.x();
1084       if(previewY + webPreview.previewItem->boundingRect().height() > sceneRect().bottom())
1085         previewY = webPreview.urlRect.y() - webPreview.previewItem->boundingRect().height();
1086
1087       if(previewX + webPreview.previewItem->boundingRect().width() > sceneRect().width())
1088         previewX = sceneRect().right() - webPreview.previewItem->boundingRect().width();
1089
1090       webPreview.previewItem->setPos(previewX, previewY);
1091     }
1092     addItem(webPreview.previewItem);
1093     webPreview.previewState = WebPreview::ShowPreview;
1094     break;
1095   case WebPreview::ShowPreview:
1096     qWarning() << "ChatScene::webPreviewNextStep() called while in ShowPreview Step!";
1097     qWarning() << "removing preview";
1098     if(webPreview.previewItem && webPreview.previewItem->scene())
1099       removeItem(webPreview.previewItem);
1100     // Fall through to deletion!
1101   case WebPreview::HidePreview:
1102     if(webPreview.previewItem) {
1103       delete webPreview.previewItem;
1104       webPreview.previewItem = 0;
1105     }
1106     webPreview.parentItem = 0;
1107     webPreview.url = QUrl();
1108     webPreview.urlRect = QRectF();
1109     webPreview.previewState = WebPreview::NoPreview;
1110   }
1111   // qDebug() << "  new State:" << webPreview.previewState << webPreview.timer.isActive();
1112 }
1113
1114 void ChatScene::clearWebPreview(ChatItem *parentItem) {
1115   // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1116   switch(webPreview.previewState) {
1117   case WebPreview::NewPreview:
1118     webPreview.previewState = WebPreview::NoPreview; // we haven't loaded anything yet
1119     break;
1120   case WebPreview::ShowPreview:
1121     if(parentItem == 0 || webPreview.parentItem == parentItem) {
1122       if(webPreview.previewItem && webPreview.previewItem->scene())
1123         removeItem(webPreview.previewItem);
1124     }
1125     // fall through into to set hidden state
1126   case WebPreview::DelayPreview:
1127     // we're just loading, so haven't shown the preview yet.
1128     webPreview.previewState = WebPreview::HidePreview;
1129     webPreview.timer.start(5000);
1130     break;
1131   case WebPreview::NoPreview:
1132   case WebPreview::HidePreview:
1133     break;
1134   }
1135   // qDebug() << "  new State:" << webPreview.previewState << webPreview.timer.isActive();
1136 }
1137 #endif
1138
1139 // ========================================
1140 //  end of webkit only
1141 // ========================================
1142
1143 void ChatScene::showWebPreviewChanged() {
1144   ChatViewSettings settings;
1145   _showWebPreview = settings.showWebPreview();
1146 }