You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

565 lines
20 KiB

#include "messagedelegate.h"
#include "chatview.h"
#include "debug_ui.h"
#include "emoji.h"
#include "animtimer.h"
#include "lottieicon.h"
#include <cmath>
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QAbstractItemView>
#include <QFontMetrics>
#include <QTextLayout>
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<const QAbstractItemView *>(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<const ChatView *>(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<ChatView *>(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<QColor> colors = colorsVar.value<QList<QColor>>();
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<QPixmap>();
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();
}