47106398ae0ba6e065f9f4052c8732aed21dbaa1
[quassel.git] / src / qtui / chatitem.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 <QFontMetrics>
25 #include <QGraphicsSceneMouseEvent>
26 #include <QIcon>
27 #include <QPainter>
28 #include <QPalette>
29 #include <QTextLayout>
30 #include <QMenu>
31
32 #include "buffermodel.h"
33 #include "bufferview.h"
34 #include "chatitem.h"
35 #include "chatline.h"
36 #include "chatlinemodel.h"
37 #include "chatview.h"
38 #include "contextmenuactionprovider.h"
39 #include "mainwin.h"
40 #include "qtui.h"
41 #include "qtuistyle.h"
42
43 ChatItem::ChatItem(const QRectF &boundingRect, ChatLine *parent)
44     : _parent(parent),
45     _boundingRect(boundingRect),
46     _selectionMode(NoSelection),
47     _selectionStart(-1),
48     _cachedLayout(0)
49 {
50 }
51
52
53 ChatItem::~ChatItem()
54 {
55     delete _cachedLayout;
56 }
57
58
59 ChatLine *ChatItem::chatLine() const
60 {
61     return _parent;
62 }
63
64
65 ChatScene *ChatItem::chatScene() const
66 {
67     return chatLine()->chatScene();
68 }
69
70
71 ChatView *ChatItem::chatView() const
72 {
73     return chatScene()->chatView();
74 }
75
76
77 const QAbstractItemModel *ChatItem::model() const
78 {
79     return chatLine()->model();
80 }
81
82
83 int ChatItem::row() const
84 {
85     return chatLine()->row();
86 }
87
88
89 QPointF ChatItem::mapToLine(const QPointF &p) const
90 {
91     return p + pos();
92 }
93
94
95 QPointF ChatItem::mapFromLine(const QPointF &p) const
96 {
97     return p - pos();
98 }
99
100
101 // relative to the ChatLine
102 QPointF ChatItem::mapToScene(const QPointF &p) const
103 {
104     return chatLine()->mapToScene(p /* + pos() */);
105 }
106
107
108 QPointF ChatItem::mapFromScene(const QPointF &p) const
109 {
110     return chatLine()->mapFromScene(p) /* - pos() */;
111 }
112
113
114 QVariant ChatItem::data(int role) const
115 {
116     QModelIndex index = model()->index(row(), column());
117     if (!index.isValid()) {
118         qWarning() << "ChatItem::data(): model index is invalid!" << index;
119         return QVariant();
120     }
121     return model()->data(index, role);
122 }
123
124
125 QTextLayout *ChatItem::layout() const
126 {
127     if (_cachedLayout)
128         return _cachedLayout;
129
130     _cachedLayout = new QTextLayout;
131     initLayout(_cachedLayout);
132     chatView()->setHasCache(chatLine());
133     return _cachedLayout;
134 }
135
136
137 void ChatItem::clearCache()
138 {
139     delete _cachedLayout;
140     _cachedLayout = 0;
141 }
142
143
144 void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapMode, Qt::Alignment alignment) const
145 {
146     Q_ASSERT(layout);
147
148     layout->setText(data(MessageModel::DisplayRole).toString());
149
150     QTextOption option;
151     option.setWrapMode(wrapMode);
152     option.setAlignment(alignment);
153     layout->setTextOption(option);
154
155     QList<QTextLayout::FormatRange> formatRanges
156         = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).toUInt());
157     layout->setAdditionalFormats(formatRanges);
158 }
159
160
161 void ChatItem::initLayout(QTextLayout *layout) const
162 {
163     initLayoutHelper(layout, QTextOption::NoWrap);
164     doLayout(layout);
165 }
166
167
168 void ChatItem::doLayout(QTextLayout *layout) const
169 {
170     layout->beginLayout();
171     QTextLine line = layout->createLine();
172     if (line.isValid()) {
173         line.setLineWidth(width());
174         line.setPosition(QPointF(0, 0));
175     }
176     layout->endLayout();
177 }
178
179
180 UiStyle::FormatList ChatItem::formatList() const
181 {
182     return data(MessageModel::FormatRole).value<UiStyle::FormatList>();
183 }
184
185
186 qint16 ChatItem::posToCursor(const QPointF &posInLine) const
187 {
188     QPointF pos = mapFromLine(posInLine);
189     if (pos.y() > height())
190         return data(MessageModel::DisplayRole).toString().length();
191     if (pos.y() < 0)
192         return 0;
193
194     for (int l = layout()->lineCount() - 1; l >= 0; l--) {
195         QTextLine line = layout()->lineAt(l);
196         if (pos.y() >= line.y()) {
197             return line.xToCursor(pos.x(), QTextLine::CursorOnCharacter);
198         }
199     }
200     return 0;
201 }
202
203
204 void ChatItem::paintBackground(QPainter *painter)
205 {
206     QVariant bgBrush;
207     if (_selectionMode == FullSelection)
208         bgBrush = data(ChatLineModel::SelectedBackgroundRole);
209     else
210         bgBrush = data(ChatLineModel::BackgroundRole);
211     if (bgBrush.isValid())
212         painter->fillRect(boundingRect(), bgBrush.value<QBrush>());
213 }
214
215
216 // NOTE: This is not the most time-efficient implementation, but it saves space by not caching unnecessary data
217 //       This is a deliberate trade-off. (-> selectFmt creation, data() call)
218 void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
219 {
220     Q_UNUSED(option); Q_UNUSED(widget);
221     painter->save();
222     painter->setClipRect(boundingRect());
223     paintBackground(painter);
224
225     layout()->draw(painter, pos(), additionalFormats(), boundingRect());
226
227     //  layout()->draw(painter, QPointF(0,0), formats, boundingRect());
228
229     // Debuging Stuff
230     // uncomment partially or all of the following stuff:
231     //
232     // 0) alternativ painter color for debug stuff
233 //   if(row() % 2)
234 //     painter->setPen(Qt::red);
235 //   else
236 //     painter->setPen(Qt::blue);
237 // 1) draw wordwrap points in the first line
238 //   if(column() == 2) {
239 //     ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
240 //     foreach(ChatLineModel::Word word, wrapList) {
241 //       if(word.endX > width())
242 //      break;
243 //       painter->drawLine(word.endX, 0, word.endX, height());
244 //     }
245 //   }
246 // 2) draw MsgId over the time column
247 //   if(column() == 0) {
248 //     QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value<MsgId>().toInt());
249 //     QPointF bottomPoint = boundingRect().bottomLeft();
250 //     bottomPoint.ry() -= 2;
251 //     painter->drawText(bottomPoint, msgIdString);
252 //   }
253 // 3) draw bounding rect
254 //   painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1));
255
256     painter->restore();
257 }
258
259
260 void ChatItem::overlayFormat(UiStyle::FormatList &fmtList, int start, int end, quint32 overlayFmt) const
261 {
262     for (int i = 0; i < fmtList.count(); i++) {
263         int fmtStart = fmtList.at(i).first;
264         int fmtEnd = (i < fmtList.count()-1 ? fmtList.at(i+1).first : data(MessageModel::DisplayRole).toString().length());
265
266         if (fmtEnd <= start)
267             continue;
268         if (fmtStart >= end)
269             break;
270
271         // split the format if necessary
272         if (fmtStart < start) {
273             fmtList.insert(i, fmtList.at(i));
274             fmtList[++i].first = start;
275         }
276         if (end < fmtEnd) {
277             fmtList.insert(i, fmtList.at(i));
278             fmtList[i+1].first = end;
279         }
280
281         fmtList[i].second |= overlayFmt;
282     }
283 }
284
285
286 QVector<QTextLayout::FormatRange> ChatItem::additionalFormats() const
287 {
288     return selectionFormats();
289 }
290
291
292 QVector<QTextLayout::FormatRange> ChatItem::selectionFormats() const
293 {
294     if (!hasSelection())
295         return QVector<QTextLayout::FormatRange>();
296
297     int start, end;
298     if (_selectionMode == FullSelection) {
299         start = 0;
300         end = data(MessageModel::DisplayRole).toString().length();
301     }
302     else {
303         start = qMin(_selectionStart, _selectionEnd);
304         end = qMax(_selectionStart, _selectionEnd);
305     }
306
307     UiStyle::FormatList fmtList = formatList();
308
309     while (fmtList.count() > 1 && fmtList.at(1).first <= start)
310         fmtList.removeFirst();
311
312     fmtList.first().first = start;
313
314     while (fmtList.count() > 1 && fmtList.last().first >= end)
315         fmtList.removeLast();
316
317     return QtUi::style()->toTextLayoutList(fmtList, end, UiStyle::Selected|data(ChatLineModel::MsgLabelRole).toUInt()).toVector();
318 }
319
320
321 bool ChatItem::hasSelection() const
322 {
323     if (_selectionMode == NoSelection)
324         return false;
325     if (_selectionMode == FullSelection)
326         return true;
327     // partial
328     return _selectionStart != _selectionEnd;
329 }
330
331
332 QString ChatItem::selection() const
333 {
334     if (_selectionMode == FullSelection)
335         return data(MessageModel::DisplayRole).toString();
336     if (_selectionMode == PartialSelection)
337         return data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd));
338     return QString();
339 }
340
341
342 void ChatItem::setSelection(SelectionMode mode, qint16 start, qint16 end)
343 {
344     _selectionMode = mode;
345     _selectionStart = start;
346     _selectionEnd = end;
347     chatLine()->update();
348 }
349
350
351 void ChatItem::setFullSelection()
352 {
353     if (_selectionMode != FullSelection) {
354         _selectionMode = FullSelection;
355         chatLine()->update();
356     }
357 }
358
359
360 void ChatItem::clearSelection()
361 {
362     if (_selectionMode != NoSelection) {
363         _selectionMode = NoSelection;
364         chatLine()->update();
365     }
366 }
367
368
369 void ChatItem::continueSelecting(const QPointF &pos)
370 {
371     _selectionMode = PartialSelection;
372     _selectionEnd = posToCursor(pos);
373     chatLine()->update();
374 }
375
376
377 bool ChatItem::isPosOverSelection(const QPointF &pos) const
378 {
379     if (_selectionMode == FullSelection)
380         return true;
381     if (_selectionMode == PartialSelection) {
382         int cursor = posToCursor(pos);
383         return cursor >= qMin(_selectionStart, _selectionEnd) && cursor <= qMax(_selectionStart, _selectionEnd);
384     }
385     return false;
386 }
387
388
389 QList<QRectF> ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive)
390 {
391     QList<QRectF> resultList;
392     const QAbstractItemModel *model_ = model();
393     if (!model_)
394         return resultList;
395
396     QString plainText = model_->data(model_->index(row(), column()), MessageModel::DisplayRole).toString();
397     QList<int> indexList;
398     int searchIdx = plainText.indexOf(searchWord, 0, caseSensitive);
399     while (searchIdx != -1) {
400         indexList << searchIdx;
401         searchIdx = plainText.indexOf(searchWord, searchIdx + 1, caseSensitive);
402     }
403
404     foreach(int idx, indexList) {
405         QTextLine line = layout()->lineForTextPosition(idx);
406         qreal x = line.cursorToX(idx);
407         qreal width = line.cursorToX(idx + searchWord.count()) - x;
408         qreal height = line.height();
409         qreal y = height * line.lineNumber();
410         resultList << QRectF(x, y, width, height);
411     }
412
413     return resultList;
414 }
415
416
417 void ChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
418 {
419     // single clicks are already handled by the scene (for clearing the selection)
420     if (clickMode == ChatScene::DragStartClick) {
421         chatScene()->setSelectingItem(this);
422         _selectionStart = _selectionEnd = posToCursor(pos);
423         _selectionMode = NoSelection; // will be set to PartialSelection by mouseMoveEvent
424         chatLine()->update();
425     }
426 }
427
428
429 void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
430 {
431     if (event->buttons() == Qt::LeftButton) {
432         if (boundingRect().contains(event->pos())) {
433             qint16 end = posToCursor(event->pos());
434             if (end != _selectionEnd) {
435                 _selectionEnd = end;
436                 _selectionMode = (_selectionStart != _selectionEnd ? PartialSelection : NoSelection);
437                 chatLine()->update();
438             }
439         }
440         else {
441             setFullSelection();
442             chatScene()->startGlobalSelection(this, event->pos());
443         }
444         event->accept();
445     }
446     else {
447         event->ignore();
448     }
449 }
450
451
452 void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
453 {
454     if (event->buttons() == Qt::LeftButton)
455         event->accept();
456     else
457         event->ignore();
458 }
459
460
461 void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
462 {
463     if (_selectionMode != NoSelection && event->button() == Qt::LeftButton) {
464         chatScene()->selectionToClipboard(QClipboard::Selection);
465         event->accept();
466     }
467     else
468         event->ignore();
469 }
470
471
472 void ChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
473 {
474     Q_UNUSED(pos);
475
476     GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>());
477 }
478
479
480 // ************************************************************
481 // SenderChatItem
482 // ************************************************************
483
484 void SenderChatItem::initLayout(QTextLayout *layout) const
485 {
486     initLayoutHelper(layout, QTextOption::ManualWrap, Qt::AlignRight);
487     doLayout(layout);
488 }
489
490
491 void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
492 {
493     Q_UNUSED(option); Q_UNUSED(widget);
494     painter->save();
495     painter->setClipRect(boundingRect());
496     paintBackground(painter);
497
498     qreal layoutWidth = layout()->minimumWidth();
499     qreal offset = 0;
500     if (chatScene()->senderCutoffMode() == ChatScene::CutoffLeft)
501         offset = qMin(width() - layoutWidth, (qreal)0);
502     else
503         offset = qMax(layoutWidth - width(), (qreal)0);
504
505     if (layoutWidth > width()) {
506         // Draw a nice gradient for longer items
507         // Qt's text drawing with a gradient brush sucks, so we use compositing instead
508         QPixmap pixmap(layout()->boundingRect().toRect().size());
509         pixmap.fill(Qt::transparent);
510
511         QPainter pixPainter(&pixmap);
512         layout()->draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), additionalFormats());
513
514         // Create alpha channel mask
515         QLinearGradient gradient;
516         if (offset < 0) {
517             gradient.setStart(0, 0);
518             gradient.setFinalStop(12, 0);
519             gradient.setColorAt(0, Qt::transparent);
520             gradient.setColorAt(1, Qt::white);
521         }
522         else {
523             gradient.setStart(width()-10, 0);
524             gradient.setFinalStop(width(), 0);
525             gradient.setColorAt(0, Qt::white);
526             gradient.setColorAt(1, Qt::transparent);
527         }
528         pixPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn); // gradient's alpha gets applied to the pixmap
529         pixPainter.fillRect(pixmap.rect(), gradient);
530         painter->drawPixmap(pos(), pixmap);
531     }
532     else {
533         layout()->draw(painter, pos(), additionalFormats(), boundingRect());
534     }
535     painter->restore();
536 }
537
538
539 void SenderChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
540 {
541     if (clickMode == ChatScene::DoubleClick) {
542         BufferInfo curBufInfo = Client::networkModel()->bufferInfo(data(MessageModel::BufferIdRole).value<BufferId>());
543         QString nick = data(MessageModel::EditRole).toString();
544         // check if the nick is a valid ircUser
545         if (!nick.isEmpty() && Client::network(curBufInfo.networkId())->ircUser(nick))
546             Client::bufferModel()->switchToOrStartQuery(curBufInfo.networkId(), nick);
547     }
548     else
549         ChatItem::handleClick(pos, clickMode);
550 }
551
552
553 // ************************************************************
554 // ContentsChatItem
555 // ************************************************************
556
557 ContentsChatItem::ActionProxy ContentsChatItem::_actionProxy;
558
559 ContentsChatItem::ContentsChatItem(const QPointF &pos, const qreal &width, ChatLine *parent)
560     : ChatItem(QRectF(pos, QSizeF(width, 0)), parent),
561     _data(0)
562 {
563     setPos(pos);
564     setGeometryByWidth(width);
565 }
566
567
568 QFontMetricsF *ContentsChatItem::fontMetrics() const
569 {
570     return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second, 0);
571 }
572
573
574 ContentsChatItem::~ContentsChatItem()
575 {
576     delete _data;
577 }
578
579
580 void ContentsChatItem::clearCache()
581 {
582     delete _data;
583     _data = 0;
584     ChatItem::clearCache();
585 }
586
587
588 ContentsChatItemPrivate *ContentsChatItem::privateData() const
589 {
590     if (!_data) {
591         ContentsChatItem *that = const_cast<ContentsChatItem *>(this);
592         that->_data = new ContentsChatItemPrivate(ClickableList::fromString(data(ChatLineModel::DisplayRole).toString()), that);
593     }
594     return _data;
595 }
596
597
598 qreal ContentsChatItem::setGeometryByWidth(qreal w)
599 {
600     // We use this for reloading layout info as well, so we can't bail out if the width doesn't change
601
602     // compute height
603     int lines = 1;
604     WrapColumnFinder finder(this);
605     while (finder.nextWrapColumn(w) > 0)
606         lines++;
607     qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height()); // cope with negative leading()
608     qreal h = lines * spacing;
609     delete _data;
610     _data = 0;
611
612     if (w != width() || h != height())
613         setGeometry(w, h);
614
615     return h;
616 }
617
618
619 void ContentsChatItem::initLayout(QTextLayout *layout) const
620 {
621     initLayoutHelper(layout, QTextOption::WrapAtWordBoundaryOrAnywhere);
622     doLayout(layout);
623 }
624
625
626 void ContentsChatItem::doLayout(QTextLayout *layout) const
627 {
628     ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
629     if (!wrapList.count()) return;  // empty chatitem
630
631     qreal h = 0;
632     qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height()); // cope with negative leading()
633     WrapColumnFinder finder(this);
634     layout->beginLayout();
635     forever {
636         QTextLine line = layout->createLine();
637         if (!line.isValid())
638             break;
639
640         int col = finder.nextWrapColumn(width());
641         if (col < 0)
642             col = layout->text().length();
643         int num = col - line.textStart();
644
645         line.setNumColumns(num);
646
647         // Sometimes, setNumColumns will create a line that's too long (cf. Qt bug 238249)
648         // We verify this and try setting the width again, making it shorter each time until the lengths match.
649         // Dead fugly, but seems to work…
650         for (int i = line.textLength()-1; i >= 0 && line.textLength() > num; i--) {
651             line.setNumColumns(i);
652         }
653         if (num != line.textLength()) {
654             qWarning() << "WARNING: Layout engine couldn't workaround Qt bug 238249, please report!";
655             // qDebug() << num << line.textLength() << t.mid(line.textStart(), line.textLength()) << t.mid(line.textStart() + line.textLength());
656         }
657
658         line.setPosition(QPointF(0, h));
659         h += spacing;
660     }
661     layout->endLayout();
662 }
663
664
665 Clickable ContentsChatItem::clickableAt(const QPointF &pos) const
666 {
667     return privateData()->clickables.atCursorPos(posToCursor(pos));
668 }
669
670
671 UiStyle::FormatList ContentsChatItem::formatList() const
672 {
673     UiStyle::FormatList fmtList = ChatItem::formatList();
674     for (int i = 0; i < privateData()->clickables.count(); i++) {
675         Clickable click = privateData()->clickables.at(i);
676         if (click.type() == Clickable::Url) {
677             overlayFormat(fmtList, click.start(), click.start() + click.length(), UiStyle::Url);
678         }
679     }
680     return fmtList;
681 }
682
683
684 QVector<QTextLayout::FormatRange> ContentsChatItem::additionalFormats() const
685 {
686     QVector<QTextLayout::FormatRange> fmt = ChatItem::additionalFormats();
687     // mark a clickable if hovered upon
688     if (privateData()->currentClickable.isValid()) {
689         Clickable click = privateData()->currentClickable;
690         QTextLayout::FormatRange f;
691         f.start = click.start();
692         f.length = click.length();
693         f.format.setFontUnderline(true);
694         fmt.append(f);
695     }
696     return fmt;
697 }
698
699
700 void ContentsChatItem::endHoverMode()
701 {
702     if (privateData()) {
703         if (privateData()->currentClickable.isValid()) {
704             chatLine()->unsetCursor();
705             privateData()->currentClickable = Clickable();
706         }
707         clearWebPreview();
708         chatLine()->update();
709     }
710 }
711
712
713 void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
714 {
715     if (clickMode == ChatScene::SingleClick) {
716         qint16 idx = posToCursor(pos);
717         Clickable foo = privateData()->clickables.atCursorPos(idx);
718         if (foo.isValid()) {
719             NetworkId networkId = Client::networkModel()->networkId(data(MessageModel::BufferIdRole).value<BufferId>());
720             QString text = data(ChatLineModel::DisplayRole).toString();
721             foo.activate(networkId, text);
722         }
723     }
724     else if (clickMode == ChatScene::DoubleClick) {
725         chatScene()->setSelectingItem(this);
726         setSelectionMode(PartialSelection);
727         Clickable click = clickableAt(pos);
728         if (click.isValid()) {
729             setSelectionStart(click.start());
730             setSelectionEnd(click.start() + click.length());
731         }
732         else {
733             // find word boundary
734             QString str = data(ChatLineModel::DisplayRole).toString();
735             qint16 cursor = posToCursor(pos);
736             qint16 start = str.lastIndexOf(QRegExp("\\W"), cursor) + 1;
737             qint16 end = qMin(str.indexOf(QRegExp("\\W"), cursor), str.length());
738             if (end < 0) end = str.length();
739             setSelectionStart(start);
740             setSelectionEnd(end);
741         }
742         chatLine()->update();
743     }
744     else if (clickMode == ChatScene::TripleClick) {
745         setSelection(PartialSelection, 0, data(ChatLineModel::DisplayRole).toString().length());
746     }
747     ChatItem::handleClick(pos, clickMode);
748 }
749
750
751 void ContentsChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
752 {
753     // mouse move events always mean we're not hovering anymore...
754     endHoverMode();
755     ChatItem::mouseMoveEvent(event);
756 }
757
758
759 void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
760 {
761     endHoverMode();
762     event->accept();
763 }
764
765
766 void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
767 {
768     bool onClickable = false;
769     Clickable click = clickableAt(event->pos());
770     if (click.isValid()) {
771         if (click.type() == Clickable::Url) {
772             onClickable = true;
773             showWebPreview(click);
774         }
775         else if (click.type() == Clickable::Channel) {
776             QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
777             // don't make clickable if it's our own name
778             BufferId myId = data(MessageModel::BufferIdRole).value<BufferId>();
779             if (Client::networkModel()->bufferName(myId) != name)
780                 onClickable = true;
781         }
782         if (onClickable) {
783             chatLine()->setCursor(Qt::PointingHandCursor);
784             privateData()->currentClickable = click;
785             chatLine()->update();
786             return;
787         }
788     }
789     if (!onClickable) endHoverMode();
790     event->accept();
791 }
792
793
794 void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
795 {
796     if (privateData()->currentClickable.isValid()) {
797         Clickable click = privateData()->currentClickable;
798         switch (click.type()) {
799         case Clickable::Url:
800             privateData()->activeClickable = click;
801             menu->addAction(QIcon::fromTheme("edit-copy"), tr("Copy Link Address"),
802                 &_actionProxy, SLOT(copyLinkToClipboard()))->setData(QVariant::fromValue<void *>(this));
803             break;
804         case Clickable::Channel:
805         {
806             // Remove existing menu actions, they confuse us when right-clicking on a clickable
807             menu->clear();
808             QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
809             GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>(), name);
810             break;
811         }
812         default:
813             break;
814         }
815     }
816     else {
817         // Buffer-specific actions
818         ChatItem::addActionsToMenu(menu, pos);
819     }
820 }
821
822
823 void ContentsChatItem::copyLinkToClipboard()
824 {
825     Clickable click = privateData()->activeClickable;
826     if (click.isValid() && click.type() == Clickable::Url) {
827         QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
828         if (!url.contains("://"))
829             url = "http://" + url;
830         chatScene()->stringToClipboard(url);
831     }
832 }
833
834
835 /******** WEB PREVIEW *****************************************************************************/
836
837 void ContentsChatItem::showWebPreview(const Clickable &click)
838 {
839 #if !defined HAVE_WEBKIT && !defined HAVE_WEBENGINE
840     Q_UNUSED(click);
841 #else
842     QTextLine line = layout()->lineForTextPosition(click.start());
843     qreal x = line.cursorToX(click.start());
844     qreal width = line.cursorToX(click.start() + click.length()) - x;
845     qreal height = line.height();
846     qreal y = height * line.lineNumber();
847
848     QPointF topLeft = mapToScene(pos()) + QPointF(x, y);
849     QRectF urlRect = QRectF(topLeft.x(), topLeft.y(), width, height);
850
851     QString urlstr = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
852     if (!urlstr.contains("://"))
853         urlstr = "http://" + urlstr;
854     QUrl url = QUrl::fromEncoded(urlstr.toUtf8(), QUrl::TolerantMode);
855     chatScene()->loadWebPreview(this, url, urlRect);
856 #endif
857 }
858
859
860 void ContentsChatItem::clearWebPreview()
861 {
862 #if defined HAVE_WEBKIT || defined HAVE_WEBENGINE
863     chatScene()->clearWebPreview(this);
864 #endif
865 }
866
867
868 /*************************************************************************************************/
869
870 ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item)
871     : item(_item),
872     wrapList(item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>()),
873     wordidx(0),
874     lineCount(0),
875     choppedTrailing(0)
876 {
877 }
878
879
880 ContentsChatItem::WrapColumnFinder::~WrapColumnFinder()
881 {
882 }
883
884
885 qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width)
886 {
887     if (wordidx >= wrapList.count())
888         return -1;
889
890     lineCount++;
891     qreal targetWidth = lineCount * width + choppedTrailing;
892
893     qint16 start = wordidx;
894     qint16 end = wrapList.count() - 1;
895
896     // check if the whole line fits
897     if (wrapList.at(end).endX <= targetWidth) //  || start == end)
898         return -1;
899
900     // check if we have a very long word that needs inter word wrap
901     if (wrapList.at(start).endX > targetWidth) {
902         if (!line.isValid()) {
903             item->initLayoutHelper(&layout, QTextOption::NoWrap);
904             layout.beginLayout();
905             line = layout.createLine();
906             layout.endLayout();
907         }
908         return line.xToCursor(targetWidth, QTextLine::CursorOnCharacter);
909     }
910
911     while (true) {
912         if (start + 1 == end) {
913             wordidx = end;
914             const ChatLineModel::Word &lastWord = wrapList.at(start); // the last word we were able to squeeze in
915
916             // both cases should be cought preliminary
917             Q_ASSERT(lastWord.endX <= targetWidth); // ensure that "start" really fits in
918             Q_ASSERT(end < wrapList.count()); // ensure that start isn't the last word
919
920             choppedTrailing += lastWord.trailing - (targetWidth - lastWord.endX);
921             return wrapList.at(wordidx).start;
922         }
923
924         qint16 pivot = (end + start) / 2;
925         if (wrapList.at(pivot).endX > targetWidth) {
926             end = pivot;
927         }
928         else {
929             start = pivot;
930         }
931     }
932     Q_ASSERT(false);
933     return -1;
934 }
935
936
937 /*************************************************************************************************/