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