From 56fe1cab954948f72bb8842edf2b16f74f1073da Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 4 Oct 2026 14:56:10 +0200 Subject: [PATCH] Render and hit-test wrapped invitation links in desktop messages --- tools/chatgui/src/messagedelegate.cpp | 61 ++++++++++++++++++++++++++- tools/chatgui/src/messagedelegate.h | 1 + 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index 42eff6a8..fb146008 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -24,6 +24,55 @@ #include #include #include +#include + +/* Один layout для размеров, отрисовки и hit-test длинных invite-ссылок. */ +static qreal layoutInviteText(QTextLayout& layout, int width) { + QTextOption option; option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + layout.setTextOption(option); + static const QRegularExpression pattern(QStringLiteral("utun://[A-Za-z0-9+/]+={0,2}")); + QVector formats; + auto matches = pattern.globalMatch(layout.text()); + while (matches.hasNext()) { + auto match = matches.next(); + QTextLayout::FormatRange range; + range.start = match.capturedStart(); range.length = match.capturedLength(); + range.format.setForeground(QColor(0x33, 0x90, 0xEC)); range.format.setFontUnderline(true); + formats.append(range); + } + layout.setFormats(formats); + layout.beginLayout(); + qreal y = 0; + while (true) { + auto line = layout.createLine(); + if (!line.isValid()) break; + line.setLineWidth(qMax(1, width)); line.setPosition(QPointF(0, y)); y += line.height(); + } + layout.endLayout(); + return y; +} + +QString MessageDelegate::inviteLinkAt(const QStyleOptionViewItem& option, const QModelIndex& index, const QPoint& point) const { + const QString text = index.data(MsgTextRole).toString(); + if (!text.contains(QStringLiteral("utun://"))) return {}; + const Layout geometry = calcLayout(option, index); + if (geometry.isVoice || geometry.isFile || geometry.isVideo || geometry.isImage || geometry.emojiOnly) return {}; + const QPoint local = point - option.rect.topLeft() - geometry.textRect.topLeft(); + if (!QRect(QPoint(0, 0), geometry.textRect.size()).contains(local)) return {}; + QString display = text; display.replace('\n', QChar(0x2028)); + QTextLayout layout(display, option.font); + layoutInviteText(layout, geometry.textRect.width()); + for (int i = 0; i < layout.lineCount(); ++i) { + const auto line = layout.lineAt(i); + if (local.y() < line.y() || local.y() >= line.y() + line.height()) continue; + if (local.x() < line.x() || local.x() >= line.x() + line.naturalTextWidth()) return {}; + const int position = line.xToCursor(local.x(), QTextLine::CursorOnCharacter); + for (const auto& range : layout.formats()) { + if (position >= range.start && position < range.start + range.length) return text.mid(range.start, range.length); + } + } + return {}; +} MessageDelegate::MessageDelegate(QObject *parent) : QStyledItemDelegate(parent) {} @@ -267,6 +316,11 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem &option, static int measureTextHeight(const QFont &font, const QString &text, int width) { if (text.isEmpty() || width <= 0) return 0; + if (text.contains(QStringLiteral("utun://"))) { + QString display = text; display.replace('\n', QChar(0x2028)); + QTextLayout layout(display, font); + return (int)std::ceil(layoutInviteText(layout, width)); + } QRect r = QFontMetrics(font).boundingRect(QRect(0, 0, width, 100000), Qt::AlignLeft | Qt::TextWordWrap, text); return r.height(); @@ -1314,7 +1368,12 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio painter->setFont(textFont); QString msgText = index.data(MsgTextRole).toString(); - painter->drawText(L.textRect, Qt::AlignLeft | Qt::TextWordWrap, msgText); + if (msgText.contains(QStringLiteral("utun://"))) { + QString display = msgText; display.replace('\n', QChar(0x2028)); + QTextLayout layout(display, textFont); + layoutInviteText(layout, L.textRect.width()); + layout.draw(painter, L.textRect.topLeft()); + } else painter->drawText(L.textRect, Qt::AlignLeft | Qt::TextWordWrap, msgText); if (singleTextSel && cv) { const auto &ts = cv->textSelection(); diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index 85be5b79..3d9fbd77 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -57,6 +57,7 @@ public: bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; + QString inviteLinkAt(const QStyleOptionViewItem& option, const QModelIndex& index, const QPoint& point) const; static void startAutoPlay(const QString& filePath, float actualDurationSec, uint64_t playbackId); static void stopAllAutoPlay();