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