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