Yearly bump
[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     if (isPosOverSelection(pos))
803         menu.addAction(SmallIcon("edit-copy"), tr("Copy Selection"),
804             this, SLOT(selectionToClipboard()),
805             QKeySequence::Copy);
806
807     // item-specific options (select link etc)
808     ChatItem *item = chatItemAt(pos);
809     if (item)
810         item->addActionsToMenu(&menu, item->mapFromScene(pos));
811     else
812         // no item -> default scene actions
813         GraphicalUi::contextMenuActionProvider()->addActions(&menu, filter(), BufferId());
814
815     if (QtUi::mainWindow()->menuBar()->isHidden())
816         menu.addAction(QtUi::actionCollection("General")->action("ToggleMenuBar"));
817
818     menu.exec(event->screenPos());
819 }
820
821
822 void ChatScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
823 {
824     if (event->buttons() == Qt::LeftButton) {
825         if (!_clickHandled && (event->scenePos() - _clickPos).toPoint().manhattanLength() >= QApplication::startDragDistance()) {
826             if (_clickTimer.isActive())
827                 _clickTimer.stop();
828             if (_clickMode == SingleClick && isPosOverSelection(_clickPos))
829                 initiateDrag(event->widget());
830             else {
831                 _clickMode = DragStartClick;
832                 handleClick(Qt::LeftButton, _clickPos);
833             }
834             _clickMode = NoClick;
835         }
836         if (_isSelecting) {
837             updateSelection(event->scenePos());
838             emit mouseMoveWhileSelecting(event->scenePos());
839             event->accept();
840         }
841         else if (_clickHandled && _clickMode < DoubleClick)
842             QGraphicsScene::mouseMoveEvent(event);
843     }
844     else
845         QGraphicsScene::mouseMoveEvent(event);
846 }
847
848
849 void ChatScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
850 {
851     if (event->buttons() == Qt::LeftButton) {
852         _leftButtonPressed = true;
853         _clickHandled = false;
854         if (!isPosOverSelection(event->scenePos())) {
855             // immediately clear selection if clicked outside; otherwise, wait for potential drag
856             clearSelection();
857         }
858         if (_clickMode != NoClick && _clickTimer.isActive()) {
859             switch (_clickMode) {
860             case NoClick:
861                 _clickMode = SingleClick; break;
862             case SingleClick:
863                 _clickMode = DoubleClick; break;
864             case DoubleClick:
865                 _clickMode = TripleClick; break;
866             case TripleClick:
867                 _clickMode = DoubleClick; break;
868             case DragStartClick:
869                 break;
870             }
871             handleClick(Qt::LeftButton, _clickPos);
872         }
873         else {
874             _clickMode = SingleClick;
875             _clickPos = event->scenePos();
876         }
877         _clickTimer.start();
878     }
879     if (event->type() == QEvent::GraphicsSceneMouseDoubleClick)
880         QGraphicsScene::mouseDoubleClickEvent(event);
881     else
882         QGraphicsScene::mousePressEvent(event);
883 }
884
885
886 void ChatScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
887 {
888     // we check for doubleclick ourselves, so just call press handler
889     mousePressEvent(event);
890 }
891
892
893 void ChatScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
894 {
895     if (event->button() == Qt::LeftButton && _leftButtonPressed) {
896         _leftButtonPressed = false;
897         if (_clickMode != NoClick) {
898             if (_clickMode == SingleClick)
899                 clearSelection();
900             event->accept();
901             if (!_clickTimer.isActive())
902                 handleClick(Qt::LeftButton, _clickPos);
903         }
904         else {
905             // no click -> drag or selection move
906             if (isGloballySelecting()) {
907                 selectionToClipboard(QClipboard::Selection);
908                 _isSelecting = false;
909                 event->accept();
910                 return;
911             }
912         }
913     }
914     QGraphicsScene::mouseReleaseEvent(event);
915 }
916
917
918 void ChatScene::clickTimeout()
919 {
920     if (!_leftButtonPressed && _clickMode == SingleClick)
921         handleClick(Qt::LeftButton, _clickPos);
922 }
923
924
925 void ChatScene::handleClick(Qt::MouseButton button, const QPointF &scenePos)
926 {
927     if (button == Qt::LeftButton) {
928         clearSelection();
929
930         // Now send click down to items
931         ChatItem *chatItem = chatItemAt(scenePos);
932         if (chatItem) {
933             chatItem->handleClick(chatItem->mapFromScene(scenePos), _clickMode);
934         }
935         _clickHandled = true;
936     }
937 }
938
939
940 void ChatScene::initiateDrag(QWidget *source)
941 {
942     QDrag *drag = new QDrag(source);
943     QMimeData *mimeData = new QMimeData;
944     mimeData->setText(selection());
945     drag->setMimeData(mimeData);
946
947     drag->exec(Qt::CopyAction);
948 }
949
950
951 /******** SELECTIONS ******************************************************************************/
952
953 void ChatScene::selectionToClipboard(QClipboard::Mode mode)
954 {
955     if (!hasSelection())
956         return;
957
958     stringToClipboard(selection(), mode);
959 }
960
961
962 void ChatScene::stringToClipboard(const QString &str_, QClipboard::Mode mode)
963 {
964     QString str = str_;
965     // remove trailing linefeeds
966     if (str.endsWith('\n'))
967         str.chop(1);
968
969     switch (mode) {
970     case QClipboard::Clipboard:
971         QApplication::clipboard()->setText(str);
972         break;
973     case QClipboard::Selection:
974         if (QApplication::clipboard()->supportsSelection())
975             QApplication::clipboard()->setText(str, QClipboard::Selection);
976         break;
977     default:
978         break;
979     };
980 }
981
982
983 //!\brief Convert current selection to human-readable string.
984 QString ChatScene::selection() const
985 {
986     //TODO Make selection format configurable!
987     if (hasGlobalSelection()) {
988         int start = qMin(_selectionStart, _selectionEnd);
989         int end = qMax(_selectionStart, _selectionEnd);
990         if (start < 0 || end >= _lines.count()) {
991             qDebug() << "Invalid selection range:" << start << end;
992             return QString();
993         }
994         QString result;
995         for (int l = start; l <= end; l++) {
996             if (_selectionMinCol == ChatLineModel::TimestampColumn)
997                 result += _lines[l]->item(ChatLineModel::TimestampColumn)->data(MessageModel::DisplayRole).toString() + " ";
998             if (_selectionMinCol <= ChatLineModel::SenderColumn)
999                 result += _lines[l]->item(ChatLineModel::SenderColumn)->data(MessageModel::DisplayRole).toString() + " ";
1000             result += _lines[l]->item(ChatLineModel::ContentsColumn)->data(MessageModel::DisplayRole).toString() + "\n";
1001         }
1002         return result;
1003     }
1004     else if (selectingItem())
1005         return selectingItem()->selection();
1006     return QString();
1007 }
1008
1009
1010 bool ChatScene::hasSelection() const
1011 {
1012     return hasGlobalSelection() || (selectingItem() && selectingItem()->hasSelection());
1013 }
1014
1015
1016 bool ChatScene::hasGlobalSelection() const
1017 {
1018     return _selectionStart >= 0;
1019 }
1020
1021
1022 bool ChatScene::isGloballySelecting() const
1023 {
1024     return _isSelecting;
1025 }
1026
1027
1028 void ChatScene::clearGlobalSelection()
1029 {
1030     if (hasGlobalSelection()) {
1031         for (int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++)
1032             _lines[l]->setSelected(false);
1033         _isSelecting = false;
1034         _selectionStart = -1;
1035     }
1036 }
1037
1038
1039 void ChatScene::clearSelection()
1040 {
1041     clearGlobalSelection();
1042     if (selectingItem())
1043         selectingItem()->clearSelection();
1044 }
1045
1046
1047 /******** *************************************************************************************/
1048
1049 void ChatScene::requestBacklog()
1050 {
1051     MessageFilter *filter = qobject_cast<MessageFilter *>(model());
1052     if (filter)
1053         return filter->requestBacklog();
1054     return;
1055 }
1056
1057
1058 ChatLineModel::ColumnType ChatScene::columnByScenePos(qreal x) const
1059 {
1060     if (x < _firstColHandle->x())
1061         return ChatLineModel::TimestampColumn;
1062     if (x < _secondColHandle->x())
1063         return ChatLineModel::SenderColumn;
1064
1065     return ChatLineModel::ContentsColumn;
1066 }
1067
1068
1069 int ChatScene::rowByScenePos(qreal y) const
1070 {
1071     QList<QGraphicsItem *> itemList = items(QPointF(0, y));
1072
1073     // ChatLine should be at the bottom of the list
1074     for (int i = itemList.count()-1; i >= 0; i--) {
1075         ChatLine *line = qgraphicsitem_cast<ChatLine *>(itemList.at(i));
1076         if (line)
1077             return line->row();
1078     }
1079     return -1;
1080 }
1081
1082
1083 void ChatScene::updateSceneRect(qreal width)
1084 {
1085     if (_lines.isEmpty()) {
1086         updateSceneRect(QRectF(0, 0, width, 0));
1087         return;
1088     }
1089
1090     // we hide day change messages at the top by making the scene rect smaller
1091     // and by calling QGraphicsItem::hide() on all leading day change messages
1092     // the first one is needed to ensure proper scrollbar ranges
1093     // the second for cases where the viewport is larger then the set scenerect
1094     //  (in this case the items are shown anyways)
1095     if (_firstLineRow == -1) {
1096         int numRows = model()->rowCount();
1097         _firstLineRow = 0;
1098         QModelIndex firstLineIdx;
1099         while (_firstLineRow < numRows) {
1100             firstLineIdx = model()->index(_firstLineRow, 0);
1101             if ((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) != Message::DayChange)
1102                 break;
1103             _lines.at(_firstLineRow)->hide();
1104             _firstLineRow++;
1105         }
1106     }
1107
1108     // the following call should be safe. If it crashes something went wrong during insert/remove
1109     if (_firstLineRow < _lines.count()) {
1110         ChatLine *firstLine = _lines.at(_firstLineRow);
1111         ChatLine *lastLine = _lines.last();
1112         updateSceneRect(QRectF(0, firstLine->pos().y(), width, lastLine->pos().y() + lastLine->height() - firstLine->pos().y()));
1113     }
1114     else {
1115         // empty scene rect
1116         updateSceneRect(QRectF(0, 0, width, 0));
1117     }
1118 }
1119
1120
1121 void ChatScene::updateSceneRect(const QRectF &rect)
1122 {
1123     _sceneRect = rect;
1124     setSceneRect(rect);
1125     update();
1126 }
1127
1128
1129 // ========================================
1130 //  Webkit Only stuff
1131 // ========================================
1132 #ifdef HAVE_WEBKIT
1133 void ChatScene::loadWebPreview(ChatItem *parentItem, const QUrl &url, const QRectF &urlRect)
1134 {
1135     if (!_showWebPreview)
1136         return;
1137
1138     if (webPreview.urlRect != urlRect)
1139         webPreview.urlRect = urlRect;
1140
1141     if (webPreview.parentItem != parentItem)
1142         webPreview.parentItem = parentItem;
1143
1144     if (webPreview.url != url) {
1145         webPreview.url = url;
1146         // prepare to load a different URL
1147         if (webPreview.previewItem) {
1148             if (webPreview.previewItem->scene())
1149                 removeItem(webPreview.previewItem);
1150             delete webPreview.previewItem;
1151             webPreview.previewItem = 0;
1152         }
1153         webPreview.previewState = WebPreview::NoPreview;
1154     }
1155
1156     if (webPreview.url.isEmpty())
1157         return;
1158
1159     // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1160     switch (webPreview.previewState) {
1161     case WebPreview::NoPreview:
1162         webPreview.previewState = WebPreview::NewPreview;
1163         webPreview.timer.start(500);
1164         break;
1165     case WebPreview::NewPreview:
1166     case WebPreview::DelayPreview:
1167     case WebPreview::ShowPreview:
1168         // we're already waiting for the next step or showing the preview
1169         break;
1170     case WebPreview::HidePreview:
1171         // we still have a valid preview
1172         webPreview.previewState = WebPreview::DelayPreview;
1173         webPreview.timer.start(1000);
1174         break;
1175     }
1176     // qDebug() << "  new State:" << webPreview.previewState << webPreview.timer.isActive();
1177 }
1178
1179
1180 void ChatScene::webPreviewNextStep()
1181 {
1182     // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1183     switch (webPreview.previewState) {
1184     case WebPreview::NoPreview:
1185         break;
1186     case WebPreview::NewPreview:
1187         Q_ASSERT(!webPreview.previewItem);
1188         webPreview.previewItem = new WebPreviewItem(webPreview.url);
1189         webPreview.previewState = WebPreview::DelayPreview;
1190         webPreview.timer.start(1000);
1191         break;
1192     case WebPreview::DelayPreview:
1193         Q_ASSERT(webPreview.previewItem);
1194         // calc position and show
1195         {
1196             qreal previewY = webPreview.urlRect.bottom();
1197             qreal previewX = webPreview.urlRect.x();
1198             if (previewY + webPreview.previewItem->boundingRect().height() > sceneRect().bottom())
1199                 previewY = webPreview.urlRect.y() - webPreview.previewItem->boundingRect().height();
1200
1201             if (previewX + webPreview.previewItem->boundingRect().width() > sceneRect().width())
1202                 previewX = sceneRect().right() - webPreview.previewItem->boundingRect().width();
1203
1204             webPreview.previewItem->setPos(previewX, previewY);
1205         }
1206         addItem(webPreview.previewItem);
1207         webPreview.previewState = WebPreview::ShowPreview;
1208         break;
1209     case WebPreview::ShowPreview:
1210         qWarning() << "ChatScene::webPreviewNextStep() called while in ShowPreview Step!";
1211         qWarning() << "removing preview";
1212         if (webPreview.previewItem && webPreview.previewItem->scene())
1213             removeItem(webPreview.previewItem);
1214     // Fall through to deletion!
1215     case WebPreview::HidePreview:
1216         if (webPreview.previewItem) {
1217             delete webPreview.previewItem;
1218             webPreview.previewItem = 0;
1219         }
1220         webPreview.parentItem = 0;
1221         webPreview.url = QUrl();
1222         webPreview.urlRect = QRectF();
1223         webPreview.previewState = WebPreview::NoPreview;
1224     }
1225     // qDebug() << "  new State:" << webPreview.previewState << webPreview.timer.isActive();
1226 }
1227
1228
1229 void ChatScene::clearWebPreview(ChatItem *parentItem)
1230 {
1231     // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1232     switch (webPreview.previewState) {
1233     case WebPreview::NewPreview:
1234         webPreview.previewState = WebPreview::NoPreview; // we haven't loaded anything yet
1235         break;
1236     case WebPreview::ShowPreview:
1237         if (parentItem == 0 || webPreview.parentItem == parentItem) {
1238             if (webPreview.previewItem && webPreview.previewItem->scene())
1239                 removeItem(webPreview.previewItem);
1240         }
1241     // fall through into to set hidden state
1242     case WebPreview::DelayPreview:
1243         // we're just loading, so haven't shown the preview yet.
1244         webPreview.previewState = WebPreview::HidePreview;
1245         webPreview.timer.start(5000);
1246         break;
1247     case WebPreview::NoPreview:
1248     case WebPreview::HidePreview:
1249         break;
1250     }
1251     // qDebug() << "  new State:" << webPreview.previewState << webPreview.timer.isActive();
1252 }
1253
1254
1255 #endif
1256
1257 // ========================================
1258 //  end of webkit only
1259 // ========================================
1260
1261 void ChatScene::showWebPreviewChanged()
1262 {
1263     ChatViewSettings settings;
1264     _showWebPreview = settings.showWebPreview();
1265 }