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