Remove unused cipher map.
[quassel.git] / src / qtui / chatitem.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2013 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 <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 "chatview.h"
37 #include "contextmenuactionprovider.h"
38 #include "iconloader.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 an alpha-channeled pixmap instead
508         QPixmap pixmap(layout()->boundingRect().toRect().size());
509         pixmap.fill(Qt::transparent);
510         QPainter pixPainter(&pixmap);
511         layout()->draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), additionalFormats());
512         pixPainter.end();
513
514         // Create alpha channel mask
515         QPixmap mask(pixmap.size());
516         QPainter maskPainter(&mask);
517         QLinearGradient gradient;
518         if (offset < 0) {
519             gradient.setStart(0, 0);
520             gradient.setFinalStop(12, 0);
521             gradient.setColorAt(0, Qt::black);
522             gradient.setColorAt(1, Qt::white);
523         }
524         else {
525             gradient.setStart(width()-10, 0);
526             gradient.setFinalStop(width(), 0);
527             gradient.setColorAt(0, Qt::white);
528             gradient.setColorAt(1, Qt::black);
529         }
530         maskPainter.fillRect(0, 0, pixmap.width(), pixmap.height(), gradient);
531         pixmap.setAlphaChannel(mask);
532         painter->drawPixmap(pos(), pixmap);
533     }
534     else {
535         layout()->draw(painter, pos(), additionalFormats(), boundingRect());
536     }
537     painter->restore();
538 }
539
540
541 void SenderChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
542 {
543     if (clickMode == ChatScene::DoubleClick) {
544         BufferInfo curBufInfo = Client::networkModel()->bufferInfo(data(MessageModel::BufferIdRole).value<BufferId>());
545         QString nick = data(MessageModel::EditRole).toString();
546         // check if the nick is a valid ircUser
547         if (!nick.isEmpty() && Client::network(curBufInfo.networkId())->ircUser(nick))
548             Client::bufferModel()->switchToOrStartQuery(curBufInfo.networkId(), nick);
549     }
550     else
551         ChatItem::handleClick(pos, clickMode);
552 }
553
554
555 // ************************************************************
556 // ContentsChatItem
557 // ************************************************************
558
559 ContentsChatItem::ActionProxy ContentsChatItem::_actionProxy;
560
561 ContentsChatItem::ContentsChatItem(const QPointF &pos, const qreal &width, ChatLine *parent)
562     : ChatItem(QRectF(pos, QSizeF(width, 0)), parent),
563     _data(0)
564 {
565     setPos(pos);
566     setGeometryByWidth(width);
567 }
568
569
570 QFontMetricsF *ContentsChatItem::fontMetrics() const
571 {
572     return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second, 0);
573 }
574
575
576 ContentsChatItem::~ContentsChatItem()
577 {
578     delete _data;
579 }
580
581
582 void ContentsChatItem::clearCache()
583 {
584     delete _data;
585     _data = 0;
586     ChatItem::clearCache();
587 }
588
589
590 ContentsChatItemPrivate *ContentsChatItem::privateData() const
591 {
592     if (!_data) {
593         ContentsChatItem *that = const_cast<ContentsChatItem *>(this);
594         that->_data = new ContentsChatItemPrivate(ClickableList::fromString(data(ChatLineModel::DisplayRole).toString()), that);
595     }
596     return _data;
597 }
598
599
600 qreal ContentsChatItem::setGeometryByWidth(qreal w)
601 {
602     // We use this for reloading layout info as well, so we can't bail out if the width doesn't change
603
604     // compute height
605     int lines = 1;
606     WrapColumnFinder finder(this);
607     while (finder.nextWrapColumn(w) > 0)
608         lines++;
609     qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height()); // cope with negative leading()
610     qreal h = lines * spacing;
611     delete _data;
612     _data = 0;
613
614     if (w != width() || h != height())
615         setGeometry(w, h);
616
617     return h;
618 }
619
620
621 void ContentsChatItem::initLayout(QTextLayout *layout) const
622 {
623     initLayoutHelper(layout, QTextOption::WrapAtWordBoundaryOrAnywhere);
624     doLayout(layout);
625 }
626
627
628 void ContentsChatItem::doLayout(QTextLayout *layout) const
629 {
630     ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
631     if (!wrapList.count()) return;  // empty chatitem
632
633     qreal h = 0;
634     qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height()); // cope with negative leading()
635     WrapColumnFinder finder(this);
636     layout->beginLayout();
637     forever {
638         QTextLine line = layout->createLine();
639         if (!line.isValid())
640             break;
641
642         int col = finder.nextWrapColumn(width());
643         if (col < 0)
644             col = layout->text().length();
645         int num = col - line.textStart();
646
647         line.setNumColumns(num);
648
649         // Sometimes, setNumColumns will create a line that's too long (cf. Qt bug 238249)
650         // We verify this and try setting the width again, making it shorter each time until the lengths match.
651         // Dead fugly, but seems to work…
652         for (int i = line.textLength()-1; i >= 0 && line.textLength() > num; i--) {
653             line.setNumColumns(i);
654         }
655         if (num != line.textLength()) {
656             qWarning() << "WARNING: Layout engine couldn't workaround Qt bug 238249, please report!";
657             // qDebug() << num << line.textLength() << t.mid(line.textStart(), line.textLength()) << t.mid(line.textStart() + line.textLength());
658         }
659
660         line.setPosition(QPointF(0, h));
661         h += spacing;
662     }
663     layout->endLayout();
664 }
665
666
667 Clickable ContentsChatItem::clickableAt(const QPointF &pos) const
668 {
669     return privateData()->clickables.atCursorPos(posToCursor(pos));
670 }
671
672
673 UiStyle::FormatList ContentsChatItem::formatList() const
674 {
675     UiStyle::FormatList fmtList = ChatItem::formatList();
676     for (int i = 0; i < privateData()->clickables.count(); i++) {
677         Clickable click = privateData()->clickables.at(i);
678         if (click.type() == Clickable::Url) {
679             overlayFormat(fmtList, click.start(), click.start() + click.length(), UiStyle::Url);
680         }
681     }
682     return fmtList;
683 }
684
685
686 QVector<QTextLayout::FormatRange> ContentsChatItem::additionalFormats() const
687 {
688     QVector<QTextLayout::FormatRange> fmt = ChatItem::additionalFormats();
689     // mark a clickable if hovered upon
690     if (privateData()->currentClickable.isValid()) {
691         Clickable click = privateData()->currentClickable;
692         QTextLayout::FormatRange f;
693         f.start = click.start();
694         f.length = click.length();
695         f.format.setFontUnderline(true);
696         fmt.append(f);
697     }
698     return fmt;
699 }
700
701
702 void ContentsChatItem::endHoverMode()
703 {
704     if (privateData()) {
705         if (privateData()->currentClickable.isValid()) {
706             chatLine()->unsetCursor();
707             privateData()->currentClickable = Clickable();
708         }
709         clearWebPreview();
710         chatLine()->update();
711     }
712 }
713
714
715 void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
716 {
717     if (clickMode == ChatScene::SingleClick) {
718         qint16 idx = posToCursor(pos);
719         Clickable foo = privateData()->clickables.atCursorPos(idx);
720         if (foo.isValid()) {
721             NetworkId networkId = Client::networkModel()->networkId(data(MessageModel::BufferIdRole).value<BufferId>());
722             QString text = data(ChatLineModel::DisplayRole).toString();
723             foo.activate(networkId, text);
724         }
725     }
726     else if (clickMode == ChatScene::DoubleClick) {
727         chatScene()->setSelectingItem(this);
728         setSelectionMode(PartialSelection);
729         Clickable click = clickableAt(pos);
730         if (click.isValid()) {
731             setSelectionStart(click.start());
732             setSelectionEnd(click.start() + click.length());
733         }
734         else {
735             // find word boundary
736             QString str = data(ChatLineModel::DisplayRole).toString();
737             qint16 cursor = posToCursor(pos);
738             qint16 start = str.lastIndexOf(QRegExp("\\W"), cursor) + 1;
739             qint16 end = qMin(str.indexOf(QRegExp("\\W"), cursor), str.length());
740             if (end < 0) end = str.length();
741             setSelectionStart(start);
742             setSelectionEnd(end);
743         }
744         chatLine()->update();
745     }
746     else if (clickMode == ChatScene::TripleClick) {
747         setSelection(PartialSelection, 0, data(ChatLineModel::DisplayRole).toString().length());
748     }
749     ChatItem::handleClick(pos, clickMode);
750 }
751
752
753 void ContentsChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
754 {
755     // mouse move events always mean we're not hovering anymore...
756     endHoverMode();
757     ChatItem::mouseMoveEvent(event);
758 }
759
760
761 void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
762 {
763     endHoverMode();
764     event->accept();
765 }
766
767
768 void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
769 {
770     bool onClickable = false;
771     Clickable click = clickableAt(event->pos());
772     if (click.isValid()) {
773         if (click.type() == Clickable::Url) {
774             onClickable = true;
775             showWebPreview(click);
776         }
777         else if (click.type() == Clickable::Channel) {
778             QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
779             // don't make clickable if it's our own name
780             BufferId myId = data(MessageModel::BufferIdRole).value<BufferId>();
781             if (Client::networkModel()->bufferName(myId) != name)
782                 onClickable = true;
783         }
784         if (onClickable) {
785             chatLine()->setCursor(Qt::PointingHandCursor);
786             privateData()->currentClickable = click;
787             chatLine()->update();
788             return;
789         }
790     }
791     if (!onClickable) endHoverMode();
792     event->accept();
793 }
794
795
796 void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
797 {
798     if (privateData()->currentClickable.isValid()) {
799         Clickable click = privateData()->currentClickable;
800         switch (click.type()) {
801         case Clickable::Url:
802             privateData()->activeClickable = click;
803             menu->addAction(SmallIcon("edit-copy"), tr("Copy Link Address"),
804                 &_actionProxy, SLOT(copyLinkToClipboard()))->setData(QVariant::fromValue<void *>(this));
805             break;
806         case Clickable::Channel:
807         {
808             // Hide existing menu actions, they confuse us when right-clicking on a clickable
809             foreach(QAction *action, menu->actions())
810             action->setVisible(false);
811             QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
812             GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>(), name);
813             break;
814         }
815         default:
816             break;
817         }
818     }
819     else {
820         // Buffer-specific actions
821         ChatItem::addActionsToMenu(menu, pos);
822     }
823 }
824
825
826 void ContentsChatItem::copyLinkToClipboard()
827 {
828     Clickable click = privateData()->activeClickable;
829     if (click.isValid() && click.type() == Clickable::Url) {
830         QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
831         if (!url.contains("://"))
832             url = "http://" + url;
833         chatScene()->stringToClipboard(url);
834     }
835 }
836
837
838 /******** WEB PREVIEW *****************************************************************************/
839
840 void ContentsChatItem::showWebPreview(const Clickable &click)
841 {
842 #ifndef HAVE_WEBKIT
843     Q_UNUSED(click);
844 #else
845     QTextLine line = layout()->lineForTextPosition(click.start());
846     qreal x = line.cursorToX(click.start());
847     qreal width = line.cursorToX(click.start() + click.length()) - x;
848     qreal height = line.height();
849     qreal y = height * line.lineNumber();
850
851     QPointF topLeft = mapToScene(pos()) + QPointF(x, y);
852     QRectF urlRect = QRectF(topLeft.x(), topLeft.y(), width, height);
853
854     QString urlstr = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
855     if (!urlstr.contains("://"))
856         urlstr = "http://" + urlstr;
857     QUrl url = QUrl::fromEncoded(urlstr.toUtf8(), QUrl::TolerantMode);
858     chatScene()->loadWebPreview(this, url, urlRect);
859 #endif
860 }
861
862
863 void ContentsChatItem::clearWebPreview()
864 {
865 #ifdef HAVE_WEBKIT
866     chatScene()->clearWebPreview(this);
867 #endif
868 }
869
870
871 /*************************************************************************************************/
872
873 ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item)
874     : item(_item),
875     wrapList(item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>()),
876     wordidx(0),
877     lineCount(0),
878     choppedTrailing(0)
879 {
880 }
881
882
883 ContentsChatItem::WrapColumnFinder::~WrapColumnFinder()
884 {
885 }
886
887
888 qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width)
889 {
890     if (wordidx >= wrapList.count())
891         return -1;
892
893     lineCount++;
894     qreal targetWidth = lineCount * width + choppedTrailing;
895
896     qint16 start = wordidx;
897     qint16 end = wrapList.count() - 1;
898
899     // check if the whole line fits
900     if (wrapList.at(end).endX <= targetWidth) //  || start == end)
901         return -1;
902
903     // check if we have a very long word that needs inter word wrap
904     if (wrapList.at(start).endX > targetWidth) {
905         if (!line.isValid()) {
906             item->initLayoutHelper(&layout, QTextOption::NoWrap);
907             layout.beginLayout();
908             line = layout.createLine();
909             layout.endLayout();
910         }
911         return line.xToCursor(targetWidth, QTextLine::CursorOnCharacter);
912     }
913
914     while (true) {
915         if (start + 1 == end) {
916             wordidx = end;
917             const ChatLineModel::Word &lastWord = wrapList.at(start); // the last word we were able to squeeze in
918
919             // both cases should be cought preliminary
920             Q_ASSERT(lastWord.endX <= targetWidth); // ensure that "start" really fits in
921             Q_ASSERT(end < wrapList.count()); // ensure that start isn't the last word
922
923             choppedTrailing += lastWord.trailing - (targetWidth - lastWord.endX);
924             return wrapList.at(wordidx).start;
925         }
926
927         qint16 pivot = (end + start) / 2;
928         if (wrapList.at(pivot).endX > targetWidth) {
929             end = pivot;
930         }
931         else {
932             start = pivot;
933         }
934     }
935     Q_ASSERT(false);
936     return -1;
937 }
938
939
940 /*************************************************************************************************/