modernize: Migrate action-related things to PMF connects
[quassel.git] / src / qtui / chatitem.cpp
index 4da2bb4..8063a97 100644 (file)
@@ -20,6 +20,9 @@
 
 #include "chatitem.h"
 
+#include <algorithm>
+#include <iterator>
+
 #include <QApplication>
 #include <QClipboard>
 #include <QDesktopServices>
@@ -30,6 +33,7 @@
 #include <QTextLayout>
 #include <QMenu>
 
+#include "action.h"
 #include "buffermodel.h"
 #include "bufferview.h"
 #include "chatline.h"
@@ -46,7 +50,7 @@ ChatItem::ChatItem(const QRectF &boundingRect, ChatLine *parent)
     _boundingRect(boundingRect),
     _selectionMode(NoSelection),
     _selectionStart(-1),
-    _cachedLayout(0)
+    _cachedLayout(nullptr)
 {
 }
 
@@ -138,7 +142,7 @@ QTextLayout *ChatItem::layout() const
 void ChatItem::clearCache()
 {
     delete _cachedLayout;
-    _cachedLayout = 0;
+    _cachedLayout = nullptr;
 }
 
 
@@ -286,36 +290,92 @@ void ChatItem::overlayFormat(UiStyle::FormatList &fmtList, quint16 start, quint1
 
 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();
 
-    quint16 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.size() > 1 && fmtList.at(1).first <= start)
-        fmtList.erase(fmtList.begin());
+    // 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.front().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.size() > 1 && fmtList.back().first >= end)
-        fmtList.pop_back();
+        // 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);
+    }
 
-    return QtUi::style()->toTextLayoutList(fmtList, end, data(ChatLineModel::MsgLabelRole).value<UiStyle::MessageLabel>()|UiStyle::MessageLabel::Selected).toVector();
+    // Apply hovered label
+    if (hasActiveClickable()) {
+        applyLabel(activeClickableRange().first, activeClickableRange().second, Label::Hovered);
+    }
+
+    // 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();
 }
 
 
@@ -387,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;
@@ -559,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);
@@ -581,7 +653,7 @@ ContentsChatItem::~ContentsChatItem()
 void ContentsChatItem::clearCache()
 {
     delete _data;
-    _data = 0;
+    _data = nullptr;
     ChatItem::clearCache();
 }
 
@@ -589,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;
@@ -608,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);
@@ -663,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));
@@ -672,7 +760,7 @@ 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::FormatType::Url);
@@ -682,22 +770,6 @@ UiStyle::FormatList ContentsChatItem::formatList() const
 }
 
 
-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()) {
@@ -798,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(icon::get("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
@@ -878,11 +953,6 @@ ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item)
 }
 
 
-ContentsChatItem::WrapColumnFinder::~WrapColumnFinder()
-{
-}
-
-
 qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width)
 {
     if (wordidx >= wrapList.count())