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