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