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