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