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.
251 lines
8.5 KiB
251 lines
8.5 KiB
#include "messagedelegate.h" |
|
#include <QPainter> |
|
#include <QPainterPath> |
|
#include <QApplication> |
|
#include <QAbstractItemView> |
|
#include <QFontMetrics> |
|
|
|
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(); |
|
} |
|
|
|
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 bubbleW = L.viewWidth - kMarginH - kAvatar - kGap - kMarginH; |
|
if (bubbleW < 100) bubbleW = 100; |
|
L.bubbleWidth = bubbleW; |
|
|
|
int textW = bubbleW - 2 * kPadH; |
|
|
|
int qH = 0; |
|
if (hasQuote) { |
|
int qTextW = bubbleW - 2 * kPadH - kQuoteBarW - kQuotePadH; |
|
int qTextH = measureTextHeight(option.font, qText, qTextW); |
|
int qOneLineH = QFontMetrics(option.font).height(); |
|
qH = kQuotePadV + qOneLineH + (qText.isEmpty() ? 0 : 2 + qTextH) + kQuotePadV; |
|
} |
|
|
|
int textH = measureTextHeight(option.font, text, textW); |
|
int bubbleH = kPadTop + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot; |
|
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap; |
|
|
|
int contentLeft = kMarginH + kAvatar + kGap; |
|
|
|
L.bubbleRect = QRect(contentLeft, 0, bubbleW, bubbleH); |
|
|
|
int contentTop = kPadTop; |
|
|
|
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); |
|
|
|
int avatarY = L.bubbleRect.bottom() - kAvatar; |
|
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar); |
|
|
|
return L; |
|
} |
|
|
|
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); |
|
|
|
drawBubble(painter, L); |
|
|
|
if (index.data(MsgHasQuoteRole).toBool()) { |
|
drawQuote(painter, L, index, option); |
|
} |
|
|
|
drawAvatar(painter, L, index); |
|
|
|
painter->setPen(option.palette.text().color()); |
|
QFont textFont = option.font; |
|
textFont.setPointSize(textFont.pointSize()); |
|
painter->setFont(textFont); |
|
painter->drawText(L.textRect, Qt::AlignLeft | Qt::TextWordWrap, |
|
index.data(MsgTextRole).toString()); |
|
|
|
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 { |
|
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(); |
|
}
|
|
|