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