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