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