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