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