#include "messagedelegate.h" #include "chatview.h" #include "debug_ui.h" #include "emoji.h" #include "animtimer.h" #include "lottieicon.h" #include #include #include #include #include #include #include MessageDelegate::MessageDelegate(QObject *parent) : QStyledItemDelegate(parent) {} QSize MessageDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { Layout L = calcLayout(option, index); return QSize(L.viewWidth, L.totalHeight); } static int measureTextHeight(const QFont &font, const QString &text, int width) { if (text.isEmpty() || width <= 0) return 0; QRect r = QFontMetrics(font).boundingRect(QRect(0, 0, width, 100000), Qt::AlignLeft | Qt::TextWordWrap, text); return r.height(); } static int calcMaxLineWidth(const QFont &font, const QString &text, int availW) { int maxW = 0; int pos = 0; while (pos <= text.length()) { int end = text.indexOf('\n', pos); if (end < 0) end = text.length(); QString para = text.mid(pos, end - pos); if (!para.isEmpty()) { QTextLayout layout(para, font); layout.beginLayout(); while (true) { QTextLine line = layout.createLine(); if (!line.isValid()) break; line.setLineWidth(availW); line.setPosition(QPointF(0, 0)); maxW = qMax(maxW, (int)line.naturalTextWidth()); } layout.endLayout(); } pos = end + 1; } return maxW; } MessageDelegate::Layout MessageDelegate::calcLayout( const QStyleOptionViewItem &option, const QModelIndex &index) const { Layout L{}; L.viewWidth = 300; if (option.widget) { auto *v = qobject_cast(option.widget); if (v) L.viewWidth = v->viewport()->width(); } if (L.viewWidth <= 0) L.viewWidth = 300; bool hasQuote = index.data(MsgHasQuoteRole).toBool(); QString text = index.data(MsgTextRole).toString(); QString qText = hasQuote ? index.data(MsgQuoteTextRole).toString() : QString(); int availW = L.viewWidth - kMarginH - kAvatar - kGap - kMarginH; L.narrow = (availW < 400); if (L.narrow) availW = L.viewWidth - 2 * kMarginH; int textLimitW = availW - 2 * kPadH; int limitW = text.contains(' ') ? qMin(textLimitW, 700) : qMin(textLimitW, 1200); QFontMetrics fm(option.font); int contentW = calcMaxLineWidth(option.font, text, limitW); QString author = index.data(MsgAuthorRole).toString(); QFont boldFm = option.font; boldFm.setBold(true); int authorW = QFontMetrics(boldFm).horizontalAdvance(author); contentW = qMax(contentW, authorW); if (hasQuote) { QString qAuthor = index.data(MsgQuoteAuthorRole).toString(); int qAuthorW = QFontMetrics(boldFm).horizontalAdvance(qAuthor); contentW = qMax(contentW, qAuthorW + kQuoteBarW + kQuotePadH + 4); int qTextW = qMin(contentW + 8, limitW); int qWidth = calcMaxLineWidth(option.font, qText, qTextW); contentW = qMax(contentW, qWidth + kQuoteBarW + kQuotePadH + 4); } int textW = qMax(200 - 2 * kPadH, contentW); textW = qMin(textW, textLimitW); int bubbleW = textW + 2 * kPadH; L.bubbleWidth = bubbleW; int qH = 0; if (hasQuote) { int qTextW = bubbleW - 2 * kPadH - kQuoteBarW - kQuotePadH; int qTextH = measureTextHeight(option.font, qText, qTextW); int qOneLineH = fm.height(); qH = kQuotePadV + qOneLineH + (qText.isEmpty() ? 0 : 2 + qTextH) + kQuotePadV; } int textH = measureTextHeight(option.font, text, textW); int authorH = fm.height(); int bubbleH = kPadTop + authorH + 2 + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot; L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap; int contentLeft = L.narrow ? kMarginH : (kMarginH + kAvatar + kGap); L.bubbleRect = QRect(contentLeft, 0, bubbleW, bubbleH); int contentTop = kPadTop; L.authorRect = QRect(contentLeft + kPadH, contentTop, textW, authorH); contentTop += authorH + 2; if (hasQuote) { L.quoteRect = QRect(contentLeft + kPadH, contentTop, bubbleW - 2 * kPadH, qH); L.quoteBarRect = QRect(L.quoteRect.left(), L.quoteRect.top(), kQuoteBarW, qH); int tx = L.quoteBarRect.right() + kQuotePadH; L.quoteAuthorRect = QRect(tx, L.quoteRect.top() + kQuotePadV, L.quoteRect.width() - kQuoteBarW - kQuotePadH, QFontMetrics(option.font).height()); L.quoteTextRect = QRect(tx, L.quoteAuthorRect.bottom() + 2, L.quoteAuthorRect.width(), qH - kQuotePadV - QFontMetrics(option.font).height() - 2 - kQuotePadV); contentTop += qH + kQuoteGap; } L.textRect = QRect(contentLeft + kPadH, contentTop, textW, textH); L.statusRect = QRect(contentLeft + kPadH, contentTop + textH + 2, textW, kStatusH); if (!L.narrow) { int avatarY = L.bubbleRect.bottom() - kAvatar; L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar); } return L; } static int paraLayoutHeight(const QFont &font, const QString &text, int textWidth, int globalChar, qreal globalY, int yTop, int yBot, int xTop, int xBot, bool &foundStart, bool &foundEnd, int &startChar, int &endChar) { if (text.isEmpty()) return QFontMetrics(font).lineSpacing(); QTextLayout layout(text, font); layout.beginLayout(); qreal localY = 0; while (true) { QTextLine line = layout.createLine(); if (!line.isValid()) break; line.setLineWidth(textWidth); line.setPosition(QPointF(0, globalY + localY)); localY += line.height(); if (!foundStart && globalY + localY > yTop) { startChar = globalChar + line.xToCursor(xTop); foundStart = true; } if (!foundEnd && globalY >= yBot) break; if (!foundEnd && globalY + localY >= yBot) { endChar = globalChar + line.xToCursor(xBot); foundEnd = true; } } layout.endLayout(); return (int)ceil(localY); } static void calcTextSelection(const QFont &font, const QString &text, int textWidth, const QPoint &p1, const QPoint &p2, int &startChar, int &endChar) { startChar = 0; endChar = 0; if (text.isEmpty() || textWidth <= 0) return; int yTop = qMin(p1.y(), p2.y()); int yBot = qMax(p1.y(), p2.y()); int xTop = (p1.y() < p2.y()) ? p1.x() : p2.x(); int xBot = (p1.y() < p2.y()) ? p2.x() : p1.x(); int globalChar = 0; qreal globalY = 0; bool foundStart = false, foundEnd = false; int paraStart = 0; while (paraStart <= text.length()) { int paraEnd = text.indexOf('\n', paraStart); if (paraEnd < 0) paraEnd = text.length(); QString para = text.mid(paraStart, paraEnd - paraStart); int paraH = paraLayoutHeight(font, para, textWidth, globalChar, globalY, yTop, yBot, xTop, xBot, foundStart, foundEnd, startChar, endChar); globalChar += para.length(); globalY += paraH; if (paraEnd < text.length()) globalChar++; paraStart = paraEnd + 1; if (foundStart && foundEnd) break; } if (!foundStart) startChar = 0; if (!foundEnd) endChar = text.length(); if (startChar == endChar && endChar < text.length()) endChar = startChar + 1; if (startChar > endChar) qSwap(startChar, endChar); } static void drawTextSelectionHighlight(QPainter *p, const QFont &font, const QString &text, int textWidth, const QRect &textRect, const QColor &selColor, int startChar, int endChar) { if (startChar >= endChar) return; int globalChar = 0; qreal globalY = 0; int paraStart = 0; while (paraStart <= text.length()) { int paraEnd = text.indexOf('\n', paraStart); if (paraEnd < 0) paraEnd = text.length(); QString para = text.mid(paraStart, paraEnd - paraStart); if (para.isEmpty()) { globalY += QFontMetrics(font).lineSpacing(); } else { QTextLayout layout(para, font); layout.beginLayout(); qreal localY = 0; while (true) { QTextLine line = layout.createLine(); if (!line.isValid()) break; line.setLineWidth(textWidth); line.setPosition(QPointF(0, globalY + localY)); int ls = globalChar + line.textStart(); int le = ls + line.textLength(); localY += line.height(); if (ls >= endChar || le <= startChar) continue; int cs = qMax(startChar, ls); int ce = qMin(endChar, le); qreal x1 = line.cursorToX(cs - globalChar); qreal x2 = line.cursorToX(ce - globalChar); QRectF hr(textRect.left() + x1, textRect.top() + globalY + line.y(), x2 - x1, line.height()); p->fillRect(hr, selColor); } layout.endLayout(); globalY += localY; } globalChar += para.length(); if (paraEnd < text.length()) globalChar++; paraStart = paraEnd + 1; } } static void drawAnimatedEmojiOverlay(QPainter *p, const QFont &font, const QString &text, int textWidth, const QRect &textRect) { if (g_animatedEmojiMap.isEmpty()) return; int globalChar = 0; qreal globalY = 0; int paraStart = 0; while (paraStart <= text.length()) { int paraEnd = text.indexOf('\n', paraStart); if (paraEnd < 0) paraEnd = text.length(); QString para = text.mid(paraStart, paraEnd - paraStart); if (!para.isEmpty()) { QTextLayout layout(para, font); layout.beginLayout(); qreal localY = 0; while (true) { QTextLine line = layout.createLine(); if (!line.isValid()) break; line.setLineWidth((qreal)textWidth); line.setPosition(QPointF(0, globalY + localY)); localY += line.height(); int ls = globalChar + line.textStart(); int le = ls + line.textLength(); for (auto it = g_animatedEmojiMap.begin(); it != g_animatedEmojiMap.end(); ++it) { const QString &unicode = it.key(); int pos = text.indexOf(unicode, ls); while (pos >= 0 && pos + unicode.length() <= le) { int linePos = pos - globalChar - line.textStart(); int cp1 = linePos, cp2 = linePos + unicode.length(); qreal x1 = line.cursorToX(&cp1); qreal x2 = line.cursorToX(&cp2); LottieIcon *icon = AnimTimer::instance()->icon(it.value()); if (icon && icon->isValid()) { int eW = qMax(1, (int)((x2 - x1) * 1.3)); int eH = qMax(1, (int)(line.height() * 1.3)); QImage frame = icon->renderFrame(icon->currentFrame(), QSize(eW, eH)); qreal cx = x1 + (x2 - x1 - eW) * 0.5; qreal cy = (line.height() - eH) * 0.5; p->drawImage(QPointF(textRect.left() + cx, textRect.top() + line.y() + cy), frame); } pos = text.indexOf(unicode, pos + unicode.length()); } } } layout.endLayout(); globalY += localY; } else { globalY += QFontMetrics(font).lineSpacing(); } globalChar += para.length(); if (paraEnd < text.length()) globalChar++; paraStart = paraEnd + 1; } } void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); painter->translate(option.rect.topLeft()); painter->setClipRect(QRect(QPoint(0, 0), option.rect.size())); Layout L = calcLayout(option, index); auto *cv = qobject_cast(option.widget); bool singleTextSel = false; bool multiRowSel = false; if (cv && cv->hasSelection()) { if (cv->isRowSelected(index)) { if (cv->selStart() == cv->selEnd()) { singleTextSel = true; } else { multiRowSel = true; } } } drawBubble(painter, L); if (multiRowSel) { painter->save(); painter->setPen(Qt::NoPen); painter->setBrush(QColor(0x33, 0x90, 0xEC, 40)); painter->drawRect(L.bubbleRect); painter->restore(); } if (index.data(MsgHasQuoteRole).toBool()) { drawQuote(painter, L, index, option); } if (!L.narrow) drawAvatar(painter, L, index); QFont authorFont = option.font; authorFont.setBold(true); painter->setFont(authorFont); painter->setPen(QColor(0x33, 0x90, 0xEC)); painter->drawText(L.authorRect, Qt::AlignLeft | Qt::AlignVCenter, index.data(MsgAuthorRole).toString()); painter->setPen(option.palette.text().color()); QFont textFont = option.font; textFont.setPointSize(textFont.pointSize()); painter->setFont(textFont); QString msgText = index.data(MsgTextRole).toString(); painter->drawText(L.textRect, Qt::AlignLeft | Qt::TextWordWrap, msgText); if (singleTextSel && cv) { const auto &ts = cv->textSelection(); QPoint localP1(ts.clickPos.x() - L.textRect.left(), ts.clickPos.y() - L.textRect.top()); QPoint localP2(ts.dragPos.x() - L.textRect.left(), ts.dragPos.y() - L.textRect.top()); int startChar, endChar; calcTextSelection(textFont, msgText, L.textRect.width(), localP1, localP2, startChar, endChar); const_cast(cv)->setTextSelChars(startChar, endChar); drawTextSelectionHighlight(painter, textFont, msgText, L.textRect.width(), L.textRect, QColor(0x33, 0x90, 0xEC, 60), startChar, endChar); } drawAnimatedEmojiOverlay(painter, textFont, msgText, L.textRect.width(), L.textRect); drawStatusBar(painter, L, index, option); painter->restore(); } void MessageDelegate::drawQuote(QPainter *p, const Layout &L, const QModelIndex &index, const QStyleOptionViewItem &opt) const { p->save(); p->setPen(Qt::NoPen); p->setBrush(QColor(0xFF, 0xEE, 0xEE)); p->drawRoundedRect(L.quoteRect, 5, 5); p->setPen(Qt::NoPen); p->setBrush(QColor(0xE0, 0x40, 0x40)); p->drawRect(L.quoteBarRect); QFont boldFont = opt.font; boldFont.setBold(true); p->setFont(boldFont); p->setPen(QColor(0xE0, 0x40, 0x40)); p->drawText(L.quoteAuthorRect, Qt::AlignLeft | Qt::AlignVCenter, index.data(MsgQuoteAuthorRole).toString()); p->setFont(opt.font); p->setPen(QColor(0x99, 0x55, 0x55)); p->drawText(L.quoteTextRect, Qt::AlignLeft | Qt::TextWordWrap, index.data(MsgQuoteTextRole).toString()); p->restore(); } void MessageDelegate::drawBubble(QPainter *p, const Layout &L) const { p->save(); p->setRenderHint(QPainter::Antialiasing); QRectF b = L.bubbleRect; qreal r = kRadius; qreal t = kTail; QPainterPath body; body.addRoundedRect(b, r, r); QPainterPath tail; qreal tx = b.left(); qreal ty = b.bottom(); tail.moveTo(tx, ty - r); tail.lineTo(tx - t, ty - t / 2.0); tail.lineTo(tx + r, ty); tail.closeSubpath(); QPainterPath combined = body.united(tail); p->setPen(QPen(QColor(0xE0, 0xE0, 0xE0), 0.5)); p->setBrush(QColor(255, 255, 255)); p->drawPath(combined); p->restore(); } void MessageDelegate::drawStatusBar(QPainter *p, const Layout &L, const QModelIndex &index, const QStyleOptionViewItem &opt) const { p->save(); QRect r = L.statusRect; QVariant reactionsVar = index.data(MsgReactionsRole); QVariantList reactions = reactionsVar.toList(); int rx = r.left(); if (!reactions.isEmpty()) { for (const QVariant &rv : reactions) { QVariantMap rm = rv.toMap(); QString emoji = rm["emoji"].toString(); int count = rm["count"].toInt(); QVariant colorsVar = rm["colors"]; QList colors = colorsVar.value>(); if (count < 4 && !colors.isEmpty()) { int iconSize = 16; for (int i = colors.size() - 1; i >= 0; --i) { int cx = rx + i * 11; p->setPen(Qt::NoPen); p->setBrush(colors[i]); p->drawEllipse(cx, r.top() + 1, iconSize, iconSize); } rx += colors.size() * 11 + 8; } else { LottieIcon *rIcon = nullptr; if (g_animatedEmojiMap.contains(emoji)) rIcon = AnimTimer::instance()->icon(g_animatedEmojiMap[emoji]); if (rIcon && rIcon->isValid()) { int iconSz = qMin((int)(20 * 1.3), r.height()); QImage frame = rIcon->renderFrame(rIcon->currentFrame(), QSize(iconSz, iconSz)); p->drawImage(rx, r.top() + (r.height() - iconSz) / 2, frame); rx += iconSz + 2; } else { QFont emojiFont = opt.font; emojiFont.setPointSize(emojiFont.pointSize() - 1); p->setFont(emojiFont); p->setPen(opt.palette.text().color()); QRect emojiRect(rx, r.top(), 20, r.height()); p->drawText(emojiRect, Qt::AlignLeft | Qt::AlignVCenter, emoji); rx += 20; } QFont countFont = opt.font; countFont.setPointSize(countFont.pointSize() - 2); p->setFont(countFont); p->setPen(QColor(0x80, 0x80, 0x80)); QRect cntRect(rx, r.top(), 24, r.height()); p->drawText(cntRect, Qt::AlignLeft | Qt::AlignVCenter, QString::number(count)); rx = cntRect.right() + 8; } } } QFont smallFont = opt.font; smallFont.setPointSize(smallFont.pointSize() - 2); p->setFont(smallFont); p->setPen(QColor(0x80, 0x80, 0x80)); QString statusText; if (index.data(MsgEditedRole).toBool()) { statusText = QString::fromUtf8("изменено "); } statusText += index.data(MsgTimeRole).toString(); p->drawText(QRect(r.left(), r.top(), r.width(), r.height()), Qt::AlignRight | Qt::AlignVCenter, statusText); p->restore(); } void MessageDelegate::drawAvatar(QPainter *p, const Layout &L, const QModelIndex &index) const { QPixmap av = index.data(MsgAvatarRole).value(); if (av.isNull()) return; p->save(); p->setRenderHint(QPainter::Antialiasing); QPainterPath clip; clip.addEllipse(L.avatarRect); p->setClipPath(clip); p->drawPixmap(L.avatarRect, av); p->restore(); }