modernize: Migrate action-related things to PMF connects
[quassel.git] / src / qtui / chatitem.cpp
index 2526fa7..8063a97 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2014 by the Quassel Project                        *
+ *   Copyright (C) 2005-2018 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 <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"
@@ -45,7 +50,7 @@ ChatItem::ChatItem(const QRectF &boundingRect, ChatLine *parent)
     _boundingRect(boundingRect),
     _selectionMode(NoSelection),
     _selectionStart(-1),
-    _cachedLayout(0)
+    _cachedLayout(nullptr)
 {
 }
 
@@ -137,7 +142,7 @@ QTextLayout *ChatItem::layout() const
 void ChatItem::clearCache()
 {
     delete _cachedLayout;
-    _cachedLayout = 0;
+    _cachedLayout = nullptr;
 }
 
 
@@ -153,7 +158,7 @@ void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapM
     layout->setTextOption(option);
 
     QList<QTextLayout::FormatRange> formatRanges
-        = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).toUInt());
+        = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).value<UiStyle::MessageLabel>());
     layout->setAdditionalFormats(formatRanges);
 }
 
@@ -245,7 +250,7 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
 //   }
 // 2) draw MsgId over the time column
 //   if(column() == 0) {
-//     QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value<MsgId>().toInt());
+//     QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value<MsgId>().toLongLong());
 //     QPointF bottomPoint = boundingRect().bottomLeft();
 //     bottomPoint.ry() -= 2;
 //     painter->drawText(bottomPoint, msgIdString);
@@ -257,11 +262,11 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
 }
 
 
-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,51 +275,107 @@ 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.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;
 
-    fmtList.first().first = start;
+        // 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;
+        }
 
-    while (fmtList.count() > 1 && fmtList.last().first >= end)
-        fmtList.removeLast();
+        // 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);
+    }
+
+    // Apply hovered label
+    if (hasActiveClickable()) {
+        applyLabel(activeClickableRange().first, activeClickableRange().second, Label::Hovered);
+    }
 
-    return QtUi::style()->toTextLayoutList(fmtList, end, UiStyle::Selected|data(ChatLineModel::MsgLabelRole).toUInt()).toVector();
+    // Add all formats that have an extra label to the additionalFormats list
+    QList<QTextLayout::FormatRange> 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 additionalFormats.toVector();
 }
 
 
@@ -386,6 +447,18 @@ bool ChatItem::isPosOverSelection(const QPointF &pos) const
 }
 
 
+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> resultList;
@@ -504,9 +577,10 @@ 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());
 
@@ -515,17 +589,17 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op
         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.setFinalStop(width(), 0);
             gradient.setColorAt(0, Qt::white);
-            gradient.setColorAt(1, Qt::black);
+            gradient.setColorAt(1, Qt::transparent);
         }
-        pixPainter.fillRect(0, 0, pixmap.width(), pixmap.height(), gradient);
-        pixPainter.end();
+        pixPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn); // gradient's alpha gets applied to the pixmap
+        pixPainter.fillRect(pixmap.rect(), gradient);
         painter->drawPixmap(pos(), pixmap);
     }
     else {
@@ -557,7 +631,7 @@ ContentsChatItem::ActionProxy ContentsChatItem::_actionProxy;
 
 ContentsChatItem::ContentsChatItem(const QPointF &pos, const qreal &width, ChatLine *parent)
     : ChatItem(QRectF(pos, QSizeF(width, 0)), parent),
-    _data(0)
+    _data(nullptr)
 {
     setPos(pos);
     setGeometryByWidth(width);
@@ -566,7 +640,7 @@ ContentsChatItem::ContentsChatItem(const QPointF &pos, const qreal &width, ChatL
 
 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);
 }
 
 
@@ -579,7 +653,7 @@ ContentsChatItem::~ContentsChatItem()
 void ContentsChatItem::clearCache()
 {
     delete _data;
-    _data = 0;
+    _data = nullptr;
     ChatItem::clearCache();
 }
 
@@ -587,7 +661,7 @@ void ContentsChatItem::clearCache()
 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;
@@ -606,7 +680,7 @@ qreal ContentsChatItem::setGeometryByWidth(qreal w)
     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);
@@ -661,6 +735,22 @@ void ContentsChatItem::doLayout(QTextLayout *layout) const
 }
 
 
+bool ContentsChatItem::hasActiveClickable() const
+{
+    return privateData()->currentClickable.isValid();
+}
+
+
+std::pair<quint16, quint16> ContentsChatItem::activeClickableRange() const
+{
+    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));
@@ -670,32 +760,16 @@ Clickable ContentsChatItem::clickableAt(const QPointF &pos) const
 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()) {
@@ -796,10 +870,13 @@ void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
         Clickable click = privateData()->currentClickable;
         switch (click.type()) {
         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:
         {
             // Remove existing menu actions, they confuse us when right-clicking on a clickable
@@ -835,7 +912,7 @@ void ContentsChatItem::copyLinkToClipboard()
 
 void ContentsChatItem::showWebPreview(const Clickable &click)
 {
-#ifndef HAVE_WEBKIT
+#if !defined HAVE_WEBKIT && !defined HAVE_WEBENGINE
     Q_UNUSED(click);
 #else
     QTextLine line = layout()->lineForTextPosition(click.start());
@@ -858,7 +935,7 @@ void ContentsChatItem::showWebPreview(const Clickable &click)
 
 void ContentsChatItem::clearWebPreview()
 {
-#ifdef HAVE_WEBKIT
+#if defined HAVE_WEBKIT || defined HAVE_WEBENGINE
     chatScene()->clearWebPreview(this);
 #endif
 }
@@ -876,11 +953,6 @@ ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item)
 }
 
 
-ContentsChatItem::WrapColumnFinder::~WrapColumnFinder()
-{
-}
-
-
 qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width)
 {
     if (wordidx >= wrapList.count())