Replace deprecated additionalFormats with formats
[quassel.git] / src / qtui / chatitem.cpp
index cbbb54b..913c81e 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2013 by the Quassel Project                        *
+ *   Copyright (C) 2005-2019 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
+#include "chatitem.h"
+
+#include <algorithm>
+#include <iterator>
+
 #include <QApplication>
 #include <QClipboard>
 #include <QDesktopServices>
 #include <QFontMetrics>
 #include <QGraphicsSceneMouseEvent>
+#include <QMenu>
 #include <QPainter>
 #include <QPalette>
 #include <QTextLayout>
-#include <QMenu>
 
+#include "action.h"
 #include "buffermodel.h"
 #include "bufferview.h"
-#include "chatitem.h"
 #include "chatline.h"
 #include "chatlinemodel.h"
 #include "chatview.h"
 #include "contextmenuactionprovider.h"
-#include "iconloader.h"
+#include "icon.h"
 #include "mainwin.h"
 #include "qtui.h"
 #include "qtuistyle.h"
 
-ChatItem::ChatItem(const QRectF &boundingRect, ChatLine *parent)
-    : _parent(parent),
-    _boundingRect(boundingRect),
-    _selectionMode(NoSelection),
-    _selectionStart(-1),
-    _cachedLayout(0)
-{
-}
-
+ChatItem::ChatItem(const QRectF& boundingRect, ChatLine* parent)
+    : _parent(parent)
+    , _boundingRect(boundingRect)
+    , _selectionMode(NoSelection)
+    , _selectionStart(-1)
+    , _cachedLayout(nullptr)
+{}
 
 ChatItem::~ChatItem()
 {
     delete _cachedLayout;
 }
 
-
-ChatLine *ChatItem::chatLine() const
+ChatLine* ChatItem::chatLine() const
 {
     return _parent;
 }
 
-
-ChatScene *ChatItem::chatScene() const
+ChatScene* ChatItem::chatScene() const
 {
     return chatLine()->chatScene();
 }
 
-
-ChatView *ChatItem::chatView() const
+ChatView* ChatItem::chatView() const
 {
     return chatScene()->chatView();
 }
 
-
-const QAbstractItemModel *ChatItem::model() const
+const QAbstractItemModel* ChatItem::model() const
 {
     return chatLine()->model();
 }
 
-
 int ChatItem::row() const
 {
     return chatLine()->row();
 }
 
-
-QPointF ChatItem::mapToLine(const QPointF &p) const
+QPointF ChatItem::mapToLine(const QPointF& p) const
 {
     return p + pos();
 }
 
-
-QPointF ChatItem::mapFromLine(const QPointF &p) const
+QPointF ChatItem::mapFromLine(const QPointF& p) const
 {
     return p - pos();
 }
 
-
 // relative to the ChatLine
-QPointF ChatItem::mapToScene(const QPointF &p) const
+QPointF ChatItem::mapToScene(const QPointFp) const
 {
     return chatLine()->mapToScene(p /* + pos() */);
 }
 
-
-QPointF ChatItem::mapFromScene(const QPointF &p) const
+QPointF ChatItem::mapFromScene(const QPointF& p) const
 {
     return chatLine()->mapFromScene(p) /* - pos() */;
 }
 
-
 QVariant ChatItem::data(int role) const
 {
     QModelIndex index = model()->index(row(), column());
@@ -121,8 +114,7 @@ QVariant ChatItem::data(int role) const
     return model()->data(index, role);
 }
 
-
-QTextLayout *ChatItem::layout() const
+QTextLayout* ChatItem::layout() const
 {
     if (_cachedLayout)
         return _cachedLayout;
@@ -133,15 +125,13 @@ QTextLayout *ChatItem::layout() const
     return _cachedLayout;
 }
 
-
 void ChatItem::clearCache()
 {
     delete _cachedLayout;
-    _cachedLayout = 0;
+    _cachedLayout = nullptr;
 }
 
-
-void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapMode, Qt::Alignment alignment) const
+void ChatItem::initLayoutHelper(QTextLayout* layout, QTextOption::WrapMode wrapMode, Qt::Alignment alignment) const
 {
     Q_ASSERT(layout);
 
@@ -152,20 +142,21 @@ void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapM
     option.setAlignment(alignment);
     layout->setTextOption(option);
 
-    QList<QTextLayout::FormatRange> formatRanges
-        = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).toUInt());
-    layout->setAdditionalFormats(formatRanges);
+    UiStyle::FormatContainer formatRanges = QtUi::style()->toTextLayoutList(
+        formatList(),
+        layout->text().length(),
+        data(ChatLineModel::MsgLabelRole).value<UiStyle::MessageLabel>()
+    );
+    UiStyle::setTextLayoutFormats(*layout, formatRanges);
 }
 
-
-void ChatItem::initLayout(QTextLayout *layout) const
+void ChatItem::initLayout(QTextLayout* layout) const
 {
     initLayoutHelper(layout, QTextOption::NoWrap);
     doLayout(layout);
 }
 
-
-void ChatItem::doLayout(QTextLayout *layout) const
+void ChatItem::doLayout(QTextLayout* layout) const
 {
     layout->beginLayout();
     QTextLine line = layout->createLine();
@@ -176,14 +167,12 @@ void ChatItem::doLayout(QTextLayout *layout) const
     layout->endLayout();
 }
 
-
 UiStyle::FormatList ChatItem::formatList() const
 {
     return data(MessageModel::FormatRole).value<UiStyle::FormatList>();
 }
 
-
-qint16 ChatItem::posToCursor(const QPointF &posInLine) const
+qint16 ChatItem::posToCursor(const QPointF& posInLine) const
 {
     QPointF pos = mapFromLine(posInLine);
     if (pos.y() > height())
@@ -200,8 +189,7 @@ qint16 ChatItem::posToCursor(const QPointF &posInLine) const
     return 0;
 }
 
-
-void ChatItem::paintBackground(QPainter *painter)
+void ChatItem::paintBackground(QPainter* painter)
 {
     QVariant bgBrush;
     if (_selectionMode == FullSelection)
@@ -212,12 +200,12 @@ void ChatItem::paintBackground(QPainter *painter)
         painter->fillRect(boundingRect(), bgBrush.value<QBrush>());
 }
 
-
 // NOTE: This is not the most time-efficient implementation, but it saves space by not caching unnecessary data
 //       This is a deliberate trade-off. (-> selectFmt creation, data() call)
-void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
+void ChatItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
 {
-    Q_UNUSED(option); Q_UNUSED(widget);
+    Q_UNUSED(option);
+    Q_UNUSED(widget);
     painter->save();
     painter->setClipRect(boundingRect());
     paintBackground(painter);
@@ -230,38 +218,37 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
     // uncomment partially or all of the following stuff:
     //
     // 0) alternativ painter color for debug stuff
-//   if(row() % 2)
-//     painter->setPen(Qt::red);
-//   else
-//     painter->setPen(Qt::blue);
-// 1) draw wordwrap points in the first line
-//   if(column() == 2) {
-//     ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
-//     foreach(ChatLineModel::Word word, wrapList) {
-//       if(word.endX > width())
-//      break;
-//       painter->drawLine(word.endX, 0, word.endX, height());
-//     }
-//   }
-// 2) draw MsgId over the time column
-//   if(column() == 0) {
-//     QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value<MsgId>().toInt());
-//     QPointF bottomPoint = boundingRect().bottomLeft();
-//     bottomPoint.ry() -= 2;
-//     painter->drawText(bottomPoint, msgIdString);
-//   }
-// 3) draw bounding rect
-//   painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1));
+    //   if(row() % 2)
+    //     painter->setPen(Qt::red);
+    //   else
+    //     painter->setPen(Qt::blue);
+    // 1) draw wordwrap points in the first line
+    //   if(column() == 2) {
+    //     ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
+    //     foreach(ChatLineModel::Word word, wrapList) {
+    //       if(word.endX > width())
+    //      break;
+    //       painter->drawLine(word.endX, 0, word.endX, height());
+    //     }
+    //   }
+    // 2) draw MsgId over the time column
+    //   if(column() == 0) {
+    //     QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value<MsgId>().toLongLong());
+    //     QPointF bottomPoint = boundingRect().bottomLeft();
+    //     bottomPoint.ry() -= 2;
+    //     painter->drawText(bottomPoint, msgIdString);
+    //   }
+    // 3) draw bounding rect
+    //   painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1));
 
     painter->restore();
 }
 
-
-void ChatItem::overlayFormat(UiStyle::FormatList &fmtList, int start, int end, quint32 overlayFmt) const
+void ChatItem::overlayFormat(UiStyle::FormatList& fmtList, quint16 start, quint16 end, UiStyle::FormatType overlayFmt) const
 {
-    for (int i = 0; i < fmtList.count(); i++) {
+    for (size_t i = 0; i < fmtList.size(); i++) {
         int fmtStart = fmtList.at(i).first;
-        int fmtEnd = (i < fmtList.count()-1 ? fmtList.at(i+1).first : data(MessageModel::DisplayRole).toString().length());
+        int fmtEnd = (i < fmtList.size() - 1 ? fmtList.at(i + 1).first : data(MessageModel::DisplayRole).toString().length());
 
         if (fmtEnd <= start)
             continue;
@@ -270,54 +257,109 @@ void ChatItem::overlayFormat(UiStyle::FormatList &fmtList, int start, int end, q
 
         // split the format if necessary
         if (fmtStart < start) {
-            fmtList.insert(i, fmtList.at(i));
+            fmtList.insert(fmtList.begin() + i, fmtList.at(i));
             fmtList[++i].first = start;
         }
         if (end < fmtEnd) {
-            fmtList.insert(i, fmtList.at(i));
-            fmtList[i+1].first = end;
+            fmtList.insert(fmtList.begin() + i, fmtList.at(i));
+            fmtList[i + 1].first = end;
         }
 
-        fmtList[i].second |= overlayFmt;
+        fmtList[i].second.type |= overlayFmt;
     }
 }
 
-
 QVector<QTextLayout::FormatRange> ChatItem::additionalFormats() const
 {
-    return selectionFormats();
-}
+    // Calculate formats to overlay (only) if there's a selection, and/or a hovered clickable
+    if (!hasSelection() && !hasActiveClickable()) {
+        return {};
+    }
 
+    using Label = UiStyle::MessageLabel;
+    using Format = UiStyle::Format;
 
-QVector<QTextLayout::FormatRange> ChatItem::selectionFormats() const
-{
-    if (!hasSelection())
-        return QVector<QTextLayout::FormatRange>();
+    auto itemLabel = data(ChatLineModel::MsgLabelRole).value<Label>();
+    const auto& fmtList = formatList();
 
-    int start, end;
-    if (_selectionMode == FullSelection) {
-        start = 0;
-        end = data(MessageModel::DisplayRole).toString().length();
-    }
-    else {
-        start = qMin(_selectionStart, _selectionEnd);
-        end = qMax(_selectionStart, _selectionEnd);
-    }
+    struct LabelFormat
+    {
+        quint16 offset;
+        Format format;
+        Label label;
+    };
 
-    UiStyle::FormatList fmtList = formatList();
+    // Transform formatList() into an extended list of LabelFormats
+    std::vector<LabelFormat> labelFmtList;
+    std::transform(fmtList.cbegin(), fmtList.cend(), std::back_inserter(labelFmtList), [itemLabel](const std::pair<quint16, Format>& f) {
+        return LabelFormat{f.first, f.second, itemLabel};
+    });
+    // Append dummy element to avoid special-casing handling the last real format
+    labelFmtList.push_back(LabelFormat{quint16(data(MessageModel::DisplayRole).toString().length()), Format(), itemLabel});
 
-    while (fmtList.count() > 1 && fmtList.at(1).first <= start)
-        fmtList.removeFirst();
+    // Apply the given label to the given range in the format list, splitting formats as necessary
+    auto applyLabel = [&labelFmtList](quint16 start, quint16 end, Label label) {
+        size_t i = 0;
+
+        // Skip unaffected formats
+        for (; i < labelFmtList.size() - 1; ++i) {
+            if (labelFmtList[i + 1].offset > start)
+                break;
+        }
+        // Range start doesn't align; split affected format and let the index point to the newly inserted copy
+        if (labelFmtList[i].offset < start) {
+            labelFmtList.insert(labelFmtList.begin() + i, labelFmtList[i]);
+            labelFmtList[++i].offset = start;
+        }
+
+        // Apply label to formats fully affected
+        for (; i < labelFmtList.size() - 1; ++i) {
+            if (labelFmtList[i + 1].offset <= end) {
+                labelFmtList[i].label |= label;
+                continue;
+            }
+            // Last affected format, split if end of range doesn't align
+            if (labelFmtList[i + 1].offset > end) {
+                labelFmtList.insert(labelFmtList.begin() + i, labelFmtList[i]);
+                labelFmtList[i].label |= label;
+                labelFmtList[i + 1].offset = end;
+            }
+            break;
+        }
+    };
+
+    // Apply selection label
+    if (hasSelection()) {
+        quint16 start, end;
+        if (_selectionMode == FullSelection) {
+            start = 0;
+            end = data(MessageModel::DisplayRole).toString().length();
+        }
+        else {
+            start = qMin(_selectionStart, _selectionEnd);
+            end = qMax(_selectionStart, _selectionEnd);
+        }
+        applyLabel(start, end, Label::Selected);
+    }
 
-    fmtList.first().first = start;
+    // Apply hovered label
+    if (hasActiveClickable()) {
+        applyLabel(activeClickableRange().first, activeClickableRange().second, Label::Hovered);
+    }
 
-    while (fmtList.count() > 1 && fmtList.last().first >= end)
-        fmtList.removeLast();
+    // Add all formats that have an extra label to the additionalFormats list
+    UiStyle::FormatContainer additionalFormats;
+    for (size_t i = 0; i < labelFmtList.size() - 1; ++i) {
+        if (labelFmtList[i].label != itemLabel) {
+            additionalFormats << QtUi::style()->toTextLayoutList({std::make_pair(labelFmtList[i].offset, labelFmtList[i].format)},
+                                                                   labelFmtList[i + 1].offset,
+                                                                   labelFmtList[i].label);
+        }
+    }
 
-    return QtUi::style()->toTextLayoutList(fmtList, end, UiStyle::Selected|data(ChatLineModel::MsgLabelRole).toUInt()).toVector();
+    return UiStyle::containerToVector(additionalFormats);
 }
 
-
 bool ChatItem::hasSelection() const
 {
     if (_selectionMode == NoSelection)
@@ -328,7 +370,6 @@ bool ChatItem::hasSelection() const
     return _selectionStart != _selectionEnd;
 }
 
-
 QString ChatItem::selection() const
 {
     if (_selectionMode == FullSelection)
@@ -338,7 +379,6 @@ QString ChatItem::selection() const
     return QString();
 }
 
-
 void ChatItem::setSelection(SelectionMode mode, qint16 start, qint16 end)
 {
     _selectionMode = mode;
@@ -347,7 +387,6 @@ void ChatItem::setSelection(SelectionMode mode, qint16 start, qint16 end)
     chatLine()->update();
 }
 
-
 void ChatItem::setFullSelection()
 {
     if (_selectionMode != FullSelection) {
@@ -356,7 +395,6 @@ void ChatItem::setFullSelection()
     }
 }
 
-
 void ChatItem::clearSelection()
 {
     if (_selectionMode != NoSelection) {
@@ -365,16 +403,14 @@ void ChatItem::clearSelection()
     }
 }
 
-
-void ChatItem::continueSelecting(const QPointF &pos)
+void ChatItem::continueSelecting(const QPointF& pos)
 {
     _selectionMode = PartialSelection;
     _selectionEnd = posToCursor(pos);
     chatLine()->update();
 }
 
-
-bool ChatItem::isPosOverSelection(const QPointF &pos) const
+bool ChatItem::isPosOverSelection(const QPointF& pos) const
 {
     if (_selectionMode == FullSelection)
         return true;
@@ -385,11 +421,20 @@ bool ChatItem::isPosOverSelection(const QPointF &pos) const
     return false;
 }
 
+bool ChatItem::hasActiveClickable() const
+{
+    return false;
+}
+
+std::pair<quint16, quint16> ChatItem::activeClickableRange() const
+{
+    return {};
+}
 
-QList<QRectF> ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive)
+QList<QRectF> ChatItem::findWords(const QStringsearchWord, Qt::CaseSensitivity caseSensitive)
 {
     QList<QRectF> resultList;
-    const QAbstractItemModel *model_ = model();
+    const QAbstractItemModelmodel_ = model();
     if (!model_)
         return resultList;
 
@@ -401,7 +446,7 @@ QList<QRectF> ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity
         searchIdx = plainText.indexOf(searchWord, searchIdx + 1, caseSensitive);
     }
 
-    foreach(int idx, indexList) {
+    foreach (int idx, indexList) {
         QTextLine line = layout()->lineForTextPosition(idx);
         qreal x = line.cursorToX(idx);
         qreal width = line.cursorToX(idx + searchWord.count()) - x;
@@ -413,20 +458,18 @@ QList<QRectF> ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity
     return resultList;
 }
 
-
-void ChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
+void ChatItem::handleClick(const QPointF& pos, ChatScene::ClickMode clickMode)
 {
     // single clicks are already handled by the scene (for clearing the selection)
     if (clickMode == ChatScene::DragStartClick) {
         chatScene()->setSelectingItem(this);
         _selectionStart = _selectionEnd = posToCursor(pos);
-        _selectionMode = NoSelection; // will be set to PartialSelection by mouseMoveEvent
+        _selectionMode = NoSelection;  // will be set to PartialSelection by mouseMoveEvent
         chatLine()->update();
     }
 }
 
-
-void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
+void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
 {
     if (event->buttons() == Qt::LeftButton) {
         if (boundingRect().contains(event->pos())) {
@@ -448,8 +491,7 @@ void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
     }
 }
 
-
-void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
+void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
 {
     if (event->buttons() == Qt::LeftButton)
         event->accept();
@@ -457,8 +499,7 @@ void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
         event->ignore();
 }
 
-
-void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
+void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
 {
     if (_selectionMode != NoSelection && event->button() == Qt::LeftButton) {
         chatScene()->selectionToClipboard(QClipboard::Selection);
@@ -468,29 +509,27 @@ void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
         event->ignore();
 }
 
-
-void ChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
+void ChatItem::addActionsToMenu(QMenu* menu, const QPointF& pos)
 {
     Q_UNUSED(pos);
 
     GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>());
 }
 
-
 // ************************************************************
 // SenderChatItem
 // ************************************************************
 
-void SenderChatItem::initLayout(QTextLayout *layout) const
+void SenderChatItem::initLayout(QTextLayoutlayout) const
 {
     initLayoutHelper(layout, QTextOption::ManualWrap, Qt::AlignRight);
     doLayout(layout);
 }
 
-
-void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
+void SenderChatItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
 {
-    Q_UNUSED(option); Q_UNUSED(widget);
+    Q_UNUSED(option);
+    Q_UNUSED(widget);
     painter->save();
     painter->setClipRect(boundingRect());
     paintBackground(painter);
@@ -504,31 +543,29 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op
 
     if (layoutWidth > width()) {
         // Draw a nice gradient for longer items
-        // Qt's text drawing with a gradient brush sucks, so we use an alpha-channeled pixmap instead
+        // Qt's text drawing with a gradient brush sucks, so we use compositing instead
         QPixmap pixmap(layout()->boundingRect().toRect().size());
         pixmap.fill(Qt::transparent);
+
         QPainter pixPainter(&pixmap);
         layout()->draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), additionalFormats());
-        pixPainter.end();
 
         // Create alpha channel mask
-        QPixmap mask(pixmap.size());
-        QPainter maskPainter(&mask);
         QLinearGradient gradient;
         if (offset < 0) {
             gradient.setStart(0, 0);
             gradient.setFinalStop(12, 0);
-            gradient.setColorAt(0, Qt::black);
+            gradient.setColorAt(0, Qt::transparent);
             gradient.setColorAt(1, Qt::white);
         }
         else {
-            gradient.setStart(width()-10, 0);
+            gradient.setStart(width() - 10, 0);
             gradient.setFinalStop(width(), 0);
             gradient.setColorAt(0, Qt::white);
-            gradient.setColorAt(1, Qt::black);
+            gradient.setColorAt(1, Qt::transparent);
         }
-        maskPainter.fillRect(0, 0, pixmap.width(), pixmap.height(), gradient);
-        pixmap.setAlphaChannel(mask);
+        pixPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn);  // gradient's alpha gets applied to the pixmap
+        pixPainter.fillRect(pixmap.rect(), gradient);
         painter->drawPixmap(pos(), pixmap);
     }
     else {
@@ -537,8 +574,7 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op
     painter->restore();
 }
 
-
-void SenderChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
+void SenderChatItem::handleClick(const QPointF& pos, ChatScene::ClickMode clickMode)
 {
     if (clickMode == ChatScene::DoubleClick) {
         BufferInfo curBufInfo = Client::networkModel()->bufferInfo(data(MessageModel::BufferIdRole).value<BufferId>());
@@ -551,52 +587,47 @@ void SenderChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickM
         ChatItem::handleClick(pos, clickMode);
 }
 
-
 // ************************************************************
 // ContentsChatItem
 // ************************************************************
 
 ContentsChatItem::ActionProxy ContentsChatItem::_actionProxy;
 
-ContentsChatItem::ContentsChatItem(const QPointF &pos, const qreal &width, ChatLine *parent)
-    : ChatItem(QRectF(pos, QSizeF(width, 0)), parent),
-    _data(0)
+ContentsChatItem::ContentsChatItem(const QPointF& pos, const qreal& width, ChatLine* parent)
+    : ChatItem(QRectF(pos, QSizeF(width, 0)), parent)
+    , _data(nullptr)
 {
     setPos(pos);
     setGeometryByWidth(width);
 }
 
-
-QFontMetricsF *ContentsChatItem::fontMetrics() const
+QFontMetricsF* ContentsChatItem::fontMetrics() const
 {
-    return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second, 0);
+    return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second.type,
+                                      UiStyle::MessageLabel::None);
 }
 
-
 ContentsChatItem::~ContentsChatItem()
 {
     delete _data;
 }
 
-
 void ContentsChatItem::clearCache()
 {
     delete _data;
-    _data = 0;
+    _data = nullptr;
     ChatItem::clearCache();
 }
 
-
-ContentsChatItemPrivate *ContentsChatItem::privateData() const
+ContentsChatItemPrivate* ContentsChatItem::privateData() const
 {
     if (!_data) {
-        ContentsChatItem *that = const_cast<ContentsChatItem *>(this);
+        auto* that = const_cast<ContentsChatItem*>(this);
         that->_data = new ContentsChatItemPrivate(ClickableList::fromString(data(ChatLineModel::DisplayRole).toString()), that);
     }
     return _data;
 }
 
-
 qreal ContentsChatItem::setGeometryByWidth(qreal w)
 {
     // We use this for reloading layout info as well, so we can't bail out if the width doesn't change
@@ -606,10 +637,10 @@ qreal ContentsChatItem::setGeometryByWidth(qreal w)
     WrapColumnFinder finder(this);
     while (finder.nextWrapColumn(w) > 0)
         lines++;
-    qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height()); // cope with negative leading()
+    qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height());  // cope with negative leading()
     qreal h = lines * spacing;
     delete _data;
-    _data = 0;
+    _data = nullptr;
 
     if (w != width() || h != height())
         setGeometry(w, h);
@@ -617,24 +648,24 @@ qreal ContentsChatItem::setGeometryByWidth(qreal w)
     return h;
 }
 
-
-void ContentsChatItem::initLayout(QTextLayout *layout) const
+void ContentsChatItem::initLayout(QTextLayout* layout) const
 {
     initLayoutHelper(layout, QTextOption::WrapAtWordBoundaryOrAnywhere);
     doLayout(layout);
 }
 
-
-void ContentsChatItem::doLayout(QTextLayout *layout) const
+void ContentsChatItem::doLayout(QTextLayout* layout) const
 {
     ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
-    if (!wrapList.count()) return;  // empty chatitem
+    if (!wrapList.count())
+        return;  // empty chatitem
 
     qreal h = 0;
-    qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height()); // cope with negative leading()
+    qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height());  // cope with negative leading()
     WrapColumnFinder finder(this);
     layout->beginLayout();
-    forever {
+    forever
+    {
         QTextLine line = layout->createLine();
         if (!line.isValid())
             break;
@@ -649,7 +680,7 @@ void ContentsChatItem::doLayout(QTextLayout *layout) const
         // Sometimes, setNumColumns will create a line that's too long (cf. Qt bug 238249)
         // We verify this and try setting the width again, making it shorter each time until the lengths match.
         // Dead fugly, but seems to work…
-        for (int i = line.textLength()-1; i >= 0 && line.textLength() > num; i--) {
+        for (int i = line.textLength() - 1; i >= 0 && line.textLength() > num; i--) {
             line.setNumColumns(i);
         }
         if (num != line.textLength()) {
@@ -663,42 +694,37 @@ void ContentsChatItem::doLayout(QTextLayout *layout) const
     layout->endLayout();
 }
 
+bool ContentsChatItem::hasActiveClickable() const
+{
+    return privateData()->currentClickable.isValid();
+}
 
-Clickable ContentsChatItem::clickableAt(const QPointF &pos) const
+std::pair<quint16, quint16> ContentsChatItem::activeClickableRange() const
 {
-    return privateData()->clickables.atCursorPos(posToCursor(pos));
+    const auto& clickable = privateData()->currentClickable;
+    if (clickable.isValid()) {
+        return {clickable.start(), clickable.start() + clickable.length()};
+    }
+    return {};
 }
 
+Clickable ContentsChatItem::clickableAt(const QPointF& pos) const
+{
+    return privateData()->clickables.atCursorPos(posToCursor(pos));
+}
 
 UiStyle::FormatList ContentsChatItem::formatList() const
 {
     UiStyle::FormatList fmtList = ChatItem::formatList();
-    for (int i = 0; i < privateData()->clickables.count(); i++) {
+    for (size_t i = 0; i < privateData()->clickables.size(); i++) {
         Clickable click = privateData()->clickables.at(i);
         if (click.type() == Clickable::Url) {
-            overlayFormat(fmtList, click.start(), click.start() + click.length(), UiStyle::Url);
+            overlayFormat(fmtList, click.start(), click.start() + click.length(), UiStyle::FormatType::Url);
         }
     }
     return fmtList;
 }
 
-
-QVector<QTextLayout::FormatRange> ContentsChatItem::additionalFormats() const
-{
-    QVector<QTextLayout::FormatRange> fmt = ChatItem::additionalFormats();
-    // mark a clickable if hovered upon
-    if (privateData()->currentClickable.isValid()) {
-        Clickable click = privateData()->currentClickable;
-        QTextLayout::FormatRange f;
-        f.start = click.start();
-        f.length = click.length();
-        f.format.setFontUnderline(true);
-        fmt.append(f);
-    }
-    return fmt;
-}
-
-
 void ContentsChatItem::endHoverMode()
 {
     if (privateData()) {
@@ -711,8 +737,7 @@ void ContentsChatItem::endHoverMode()
     }
 }
 
-
-void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
+void ContentsChatItem::handleClick(const QPointF& pos, ChatScene::ClickMode clickMode)
 {
     if (clickMode == ChatScene::SingleClick) {
         qint16 idx = posToCursor(pos);
@@ -737,7 +762,8 @@ void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clic
             qint16 cursor = posToCursor(pos);
             qint16 start = str.lastIndexOf(QRegExp("\\W"), cursor) + 1;
             qint16 end = qMin(str.indexOf(QRegExp("\\W"), cursor), str.length());
-            if (end < 0) end = str.length();
+            if (end < 0)
+                end = str.length();
             setSelectionStart(start);
             setSelectionEnd(end);
         }
@@ -749,23 +775,20 @@ void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clic
     ChatItem::handleClick(pos, clickMode);
 }
 
-
-void ContentsChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
+void ContentsChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
 {
     // mouse move events always mean we're not hovering anymore...
     endHoverMode();
     ChatItem::mouseMoveEvent(event);
 }
 
-
-void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
+void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent* event)
 {
     endHoverMode();
     event->accept();
 }
 
-
-void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
+void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent* event)
 {
     bool onClickable = false;
     Clickable click = clickableAt(event->pos());
@@ -788,28 +811,31 @@ void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
             return;
         }
     }
-    if (!onClickable) endHoverMode();
+    if (!onClickable)
+        endHoverMode();
     event->accept();
 }
 
-
-void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
+void ContentsChatItem::addActionsToMenu(QMenu* menu, const QPointF& pos)
 {
     if (privateData()->currentClickable.isValid()) {
         Clickable click = privateData()->currentClickable;
         switch (click.type()) {
-        case Clickable::Url:
+        case Clickable::Url: {
             privateData()->activeClickable = click;
-            menu->addAction(SmallIcon("edit-copy"), tr("Copy Link Address"),
-                &_actionProxy, SLOT(copyLinkToClipboard()))->setData(QVariant::fromValue<void *>(this));
+            auto action = new Action{icon::get("edit-copy"), tr("Copy Link Address"), menu, &_actionProxy, &ActionProxy::copyLinkToClipboard};
+            action->setData(QVariant::fromValue<void*>(this));
+            menu->addAction(action);
             break;
-        case Clickable::Channel:
-        {
-            // Hide existing menu actions, they confuse us when right-clicking on a clickable
-            foreach(QAction *action, menu->actions())
-            action->setVisible(false);
+        }
+        case Clickable::Channel: {
+            // Remove existing menu actions, they confuse us when right-clicking on a clickable
+            menu->clear();
             QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
-            GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>(), name);
+            GraphicalUi::contextMenuActionProvider()->addActions(menu,
+                                                                 chatScene()->filter(),
+                                                                 data(MessageModel::BufferIdRole).value<BufferId>(),
+                                                                 name);
             break;
         }
         default:
@@ -822,7 +848,6 @@ void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
     }
 }
 
-
 void ContentsChatItem::copyLinkToClipboard()
 {
     Clickable click = privateData()->activeClickable;
@@ -834,12 +859,11 @@ void ContentsChatItem::copyLinkToClipboard()
     }
 }
 
-
 /******** WEB PREVIEW *****************************************************************************/
 
-void ContentsChatItem::showWebPreview(const Clickable &click)
+void ContentsChatItem::showWebPreview(const Clickableclick)
 {
-#ifndef HAVE_WEBKIT
+#if !defined HAVE_WEBKIT && !defined HAVE_WEBENGINE
     Q_UNUSED(click);
 #else
     QTextLine line = layout()->lineForTextPosition(click.start());
@@ -859,31 +883,22 @@ void ContentsChatItem::showWebPreview(const Clickable &click)
 #endif
 }
 
-
 void ContentsChatItem::clearWebPreview()
 {
-#ifdef HAVE_WEBKIT
+#if defined HAVE_WEBKIT || defined HAVE_WEBENGINE
     chatScene()->clearWebPreview(this);
 #endif
 }
 
-
 /*************************************************************************************************/
 
-ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item)
-    : item(_item),
-    wrapList(item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>()),
-    wordidx(0),
-    lineCount(0),
-    choppedTrailing(0)
-{
-}
-
-
-ContentsChatItem::WrapColumnFinder::~WrapColumnFinder()
-{
-}
-
+ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem* _item)
+    : item(_item)
+    , wrapList(item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>())
+    , wordidx(0)
+    , lineCount(0)
+    , choppedTrailing(0)
+{}
 
 qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width)
 {
@@ -897,7 +912,7 @@ qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width)
     qint16 end = wrapList.count() - 1;
 
     // check if the whole line fits
-    if (wrapList.at(end).endX <= targetWidth) //  || start == end)
+    if (wrapList.at(end).endX <= targetWidth)  //  || start == end)
         return -1;
 
     // check if we have a very long word that needs inter word wrap
@@ -914,11 +929,11 @@ qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width)
     while (true) {
         if (start + 1 == end) {
             wordidx = end;
-            const ChatLineModel::Word &lastWord = wrapList.at(start); // the last word we were able to squeeze in
+            const ChatLineModel::Word& lastWord = wrapList.at(start);  // the last word we were able to squeeze in
 
             // both cases should be cought preliminary
-            Q_ASSERT(lastWord.endX <= targetWidth); // ensure that "start" really fits in
-            Q_ASSERT(end < wrapList.count()); // ensure that start isn't the last word
+            Q_ASSERT(lastWord.endX <= targetWidth);  // ensure that "start" really fits in
+            Q_ASSERT(end < wrapList.count());        // ensure that start isn't the last word
 
             choppedTrailing += lastWord.trailing - (targetWidth - lastWord.endX);
             return wrapList.at(wordidx).start;
@@ -936,5 +951,4 @@ qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width)
     return -1;
 }
 
-
 /*************************************************************************************************/