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