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