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