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