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