qtui: Properly render hovered-upon clickables
authorManuel Nickschas <sputnick@quassel-irc.org>
Tue, 24 Jul 2018 21:20:45 +0000 (23:20 +0200)
committerManuel Nickschas <sputnick@quassel-irc.org>
Tue, 28 Aug 2018 19:47:05 +0000 (21:47 +0200)
Additional formats given to QTextLayout::draw() are not cumulative.
Since we were using this feature for rendering both selections and
active (hovered-upon) clickables, things would break when both
overlapped. In particular, a highlighted *and* selected URL would
vanish while being hovered.

Fix this by explicitly calculating the range of affected formats
for both selections and active clickables, taking overlap into
account. Remove special handling in ContentsChatItem in favor of
the more generic solution.

Use the newly-introduced "hovered" message label to determine how
an active clickable should be rendered, rather than hardcoding
an underline.

src/qtui/chatitem.cpp
src/qtui/chatitem.h

index 4da2bb4..10b7d78 100644 (file)
@@ -20,6 +20,9 @@
 
 #include "chatitem.h"
 
+#include <algorithm>
+#include <iterator>
+
 #include <QApplication>
 #include <QClipboard>
 #include <QDesktopServices>
@@ -286,36 +289,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>();
+    Label 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;
+    };
+
+    // 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()), {}, itemLabel});
+
+    // 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;
 
-    UiStyle::FormatList fmtList = formatList();
+        // 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.at(1).first <= start)
-        fmtList.erase(fmtList.begin());
+        // 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.front().first = start;
+    // Apply hovered label
+    if (hasActiveClickable()) {
+        applyLabel(activeClickableRange().first, activeClickableRange().second, Label::Hovered);
+    }
 
-    while (fmtList.size() > 1 && fmtList.back().first >= end)
-        fmtList.pop_back();
+    // 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 QtUi::style()->toTextLayoutList(fmtList, end, data(ChatLineModel::MsgLabelRole).value<UiStyle::MessageLabel>()|UiStyle::MessageLabel::Selected).toVector();
+    return additionalFormats.toVector();
 }
 
 
@@ -387,6 +446,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;
@@ -663,6 +734,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));
@@ -682,22 +769,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()) {
index 04931f1..a6a94b2 100644 (file)
@@ -112,7 +112,6 @@ protected:
     virtual UiStyle::FormatList formatList() const;
 
     void paintBackground(QPainter *);
-    QVector<QTextLayout::FormatRange> selectionFormats() const;
     virtual QVector<QTextLayout::FormatRange> additionalFormats() const;
     void overlayFormat(UiStyle::FormatList &fmtList, quint16 start, quint16 end, UiStyle::FormatType overlayFmt) const;
 
@@ -124,6 +123,9 @@ protected:
     inline void setSelectionMode(SelectionMode mode) { _selectionMode = mode; }
     void setSelection(SelectionMode mode, qint16 selectionStart, qint16 selectionEnd);
 
+    virtual bool hasActiveClickable() const;
+    virtual std::pair<quint16, quint16> activeClickableRange() const;
+
     qint16 posToCursor(const QPointF &pos) const;
 
     inline void setGeometry(qreal width, qreal height) { clearCache(); _boundingRect.setSize(QSizeF(width, height)); }
@@ -206,11 +208,12 @@ protected:
     virtual void hoverMoveEvent(QGraphicsSceneHoverEvent *event);
     virtual void handleClick(const QPointF &pos, ChatScene::ClickMode clickMode);
 
+    virtual bool hasActiveClickable() const;
+    virtual std::pair<quint16, quint16> activeClickableRange() const;
+
     virtual void addActionsToMenu(QMenu *menu, const QPointF &itemPos);
     virtual void copyLinkToClipboard();
 
-    virtual QVector<QTextLayout::FormatRange> additionalFormats() const;
-
     virtual void initLayout(QTextLayout *layout) const;
     virtual void doLayout(QTextLayout *layout) const;
     virtual UiStyle::FormatList formatList() const;