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.
1188 lines
46 KiB
1188 lines
46 KiB
#include "messagedelegate.h" |
|
#include "chatview.h" |
|
#include "debug_ui.h" |
|
#include "emoji.h" |
|
#include "animtimer.h" |
|
#include "lottieicon.h" |
|
#include "voiceplayback.h" |
|
#include "sound_manager.h" |
|
#include <cmath> |
|
#include <QPainter> |
|
#include <QPainterPath> |
|
#include <QApplication> |
|
#include <QAbstractItemView> |
|
#include <QFontMetrics> |
|
#include <QTextLayout> |
|
#include <QMouseEvent> |
|
#include <QMap> |
|
#include <QDateTime> |
|
#include <QTimer> |
|
#include <QFile> |
|
#include <QFileInfo> |
|
#include <QDesktopServices> |
|
#include <QUrl> |
|
#include <QDebug> |
|
|
|
extern "C" { |
|
#include "../transport/gui_bridge.h" |
|
#include "chat/chat_core.h" |
|
#include "../../lib/u_async.h" |
|
#include "../../lib/mem.h" |
|
void chat_core_attachment_download_trampoline(void* arg); |
|
} |
|
|
|
MessageDelegate::MessageDelegate(QObject *parent) |
|
: QStyledItemDelegate(parent) {} |
|
|
|
struct VoicePlayState { |
|
bool playing = false; |
|
bool paused = false; |
|
qint64 startMs = 0; |
|
float durationSec = 0.0f; |
|
float pausedPosSec = 0.0f; |
|
float displayFrac = 0.0f; |
|
float displayElapsed = 0.0f; |
|
}; |
|
static QMap<QString, VoicePlayState> s_voiceStates; |
|
static QTimer* s_voiceTimer = nullptr; |
|
static QAbstractItemView* s_voiceView = nullptr; |
|
|
|
static void ensureVoiceTimer() { |
|
if (!s_voiceTimer) { |
|
s_voiceTimer = new QTimer(); |
|
s_voiceTimer->setInterval(16); |
|
QObject::connect(s_voiceTimer, &QTimer::timeout, []() { |
|
bool anyActive = false; |
|
SoundManager* sm = SoundManager::instance(); |
|
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) { |
|
if (!it->playing && !it->paused) { |
|
if (it->displayFrac > 0.001f || it->displayElapsed > 0.001f) anyActive = true; |
|
continue; |
|
} |
|
if (it->paused) { anyActive = true; continue; } |
|
if (!sm->isCurrentPcmPlaying()) { |
|
it->playing = false; |
|
it->paused = false; |
|
anyActive = true; |
|
} else { |
|
anyActive = true; |
|
} |
|
} |
|
if (!anyActive) s_voiceTimer->stop(); |
|
if (s_voiceView) s_voiceView->viewport()->update(); |
|
}); |
|
} |
|
} |
|
|
|
bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, |
|
const QStyleOptionViewItem &option, |
|
const QModelIndex &index) { |
|
if (event->type() != QEvent::MouseButtonRelease) return false; |
|
Layout L = calcLayout(option, index); |
|
|
|
if (L.isFile) { |
|
int dlState = index.data(MsgFileDownloadStateRole).toInt(); |
|
qint64 msgId = index.data(MsgIdRole).toLongLong(); |
|
QString chId = index.data(MsgChannelIdRole).toString(); |
|
|
|
if (dlState == 2) { /* downloaded: open file */ |
|
QString fp = index.data(MsgVoiceFileRole).toString(); |
|
if (!fp.isEmpty() && QFile::exists(fp)) |
|
QDesktopServices::openUrl(QUrl::fromLocalFile(fp)); |
|
return true; |
|
} |
|
if (dlState != 0) return false; /* downloading — no action */ |
|
|
|
if (chId.isEmpty() || msgId <= 0) return false; |
|
struct attachment_dl_req* req = (struct attachment_dl_req*) |
|
u_malloc(sizeof(struct attachment_dl_req)); |
|
if (!req) return false; |
|
memset(req, 0, sizeof(*req)); |
|
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1); |
|
req->msg_id = msgId; |
|
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req); |
|
return true; |
|
} |
|
|
|
if (!L.isVoice) return false; |
|
|
|
QMouseEvent* me = static_cast<QMouseEvent*>(event); |
|
QPoint localPos = me->pos() - option.rect.topLeft(); |
|
|
|
bool onPlayBtn = L.voicePlayBtnRect.contains(localPos); |
|
bool onWaveform = L.voiceWaveformRect.contains(localPos); |
|
if (!onPlayBtn && !onWaveform) return false; |
|
|
|
QString filePath = index.data(MsgVoiceFileRole).toString(); |
|
int dlState = index.data(MsgFileDownloadStateRole).toInt(); |
|
qint64 msgId = index.data(MsgIdRole).toLongLong(); |
|
QString chId = index.data(MsgChannelIdRole).toString(); |
|
|
|
/* not downloaded: start download (play will start on completion) */ |
|
if (filePath.isEmpty() && dlState != 1) { |
|
if (chId.isEmpty() || msgId <= 0) return false; |
|
struct attachment_dl_req* req = (struct attachment_dl_req*) |
|
u_malloc(sizeof(struct attachment_dl_req)); |
|
if (!req) return false; |
|
memset(req, 0, sizeof(*req)); |
|
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1); |
|
req->msg_id = msgId; |
|
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req); |
|
model->setData(index, true, MsgVoiceDownloadPendingRole); |
|
return true; |
|
} |
|
|
|
if (filePath.isEmpty()) return false; |
|
GUI_DEBUG("editorEvent voice click: filePath=%s exists=%d wave=%d", |
|
qPrintable(filePath), QFile::exists(filePath), (int)onWaveform); |
|
if (filePath.isEmpty()) return false; |
|
|
|
ensureVoiceTimer(); |
|
VoicePlayState& st = s_voiceStates[filePath]; |
|
|
|
QString durStr = index.data(MsgVoiceDurationRole).toString(); |
|
durStr.remove('s'); |
|
float dur = durStr.toFloat(); |
|
if (dur <= 0) dur = 1.0f; |
|
|
|
if (onWaveform) { |
|
float clickFrac = (float)(localPos.x() - L.voiceWaveformRect.x()) / L.voiceWaveformRect.width(); |
|
if (clickFrac < 0) clickFrac = 0; if (clickFrac > 1) clickFrac = 1; |
|
float targetSec = clickFrac * dur; |
|
qint64 now = QDateTime::currentMSecsSinceEpoch(); |
|
|
|
if (!st.playing && !st.paused) { |
|
/* Start playback then seek */ |
|
st.playing = true; |
|
st.paused = false; |
|
st.durationSec = dur; |
|
st.startMs = now - (qint64)(targetSec * 1000.0); |
|
st.pausedPosSec = targetSec; |
|
st.displayFrac = (targetSec > 0 && dur > 0) ? (targetSec / dur) : 0; |
|
st.displayElapsed = targetSec; |
|
VoicePlayer::playOpusFile(filePath); |
|
int sr = SoundManager::instance()->currentPcmSampleRate(); |
|
if (sr > 0) |
|
SoundManager::instance()->seekCurrentPcm((unsigned long long)(targetSec * sr)); |
|
s_voiceTimer->start(); |
|
model->setData(index, 1, MsgVoicePlayStateRole); |
|
} else { |
|
/* Currently playing or paused — seek */ |
|
st.startMs = now - (qint64)(targetSec * 1000.0); |
|
if (st.paused) |
|
st.pausedPosSec = targetSec; |
|
else if (!st.playing) |
|
{ st.playing = true; s_voiceTimer->start(); } |
|
st.displayFrac = (targetSec > 0 && dur > 0) ? (targetSec / dur) : 0; |
|
st.displayElapsed = targetSec; |
|
int sr = SoundManager::instance()->currentPcmSampleRate(); |
|
if (sr > 0) |
|
SoundManager::instance()->seekCurrentPcm((unsigned long long)(targetSec * sr)); |
|
} |
|
return true; |
|
} |
|
|
|
/* --- Play button click (original logic) --- */ |
|
bool isOtherPlaying = false; |
|
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) { |
|
if (it.key() != filePath && (it->playing || it->paused)) |
|
isOtherPlaying = true; |
|
} |
|
|
|
if (st.playing) { |
|
/* Playing → Pause */ |
|
qint64 now = QDateTime::currentMSecsSinceEpoch(); |
|
st.pausedPosSec = (now - st.startMs) / 1000.0f; |
|
st.playing = false; |
|
st.paused = true; |
|
SoundManager::instance()->pauseCurrentPcm(); |
|
model->setData(index, 2, MsgVoicePlayStateRole); |
|
return true; |
|
} |
|
|
|
if (st.paused) { |
|
/* Paused → Resume */ |
|
st.paused = false; |
|
st.playing = true; |
|
qint64 now = QDateTime::currentMSecsSinceEpoch(); |
|
st.startMs = now - (qint64)(st.pausedPosSec * 1000.0); |
|
st.displayFrac = (dur > 0) ? (st.pausedPosSec / dur) : 0; |
|
st.displayElapsed = st.pausedPosSec; |
|
SoundManager::instance()->resumeCurrentPcm(); |
|
s_voiceTimer->start(); |
|
model->setData(index, 1, MsgVoicePlayStateRole); |
|
return true; |
|
} |
|
|
|
/* Stopped or different file → stop old, play new */ |
|
if (isOtherPlaying) { |
|
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) { |
|
it->playing = false; it->paused = false; |
|
} |
|
SoundManager::instance()->stopCurrentPcm(); |
|
} |
|
|
|
st.playing = true; |
|
st.paused = false; |
|
st.pausedPosSec = 0; |
|
st.startMs = QDateTime::currentMSecsSinceEpoch(); |
|
st.durationSec = dur; |
|
st.displayFrac = 0; |
|
st.displayElapsed = 0; |
|
|
|
VoicePlayer::playOpusFile(filePath); |
|
s_voiceTimer->start(); |
|
model->setData(index, 1, MsgVoicePlayStateRole); |
|
return true; |
|
} |
|
|
|
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 QString formatFileSize(qint64 bytes) { |
|
if (bytes < 1024) return QString("%1B").arg(bytes); |
|
double kb = bytes / 1024.0; |
|
if (kb < 1024.0) return QString("%1KB").arg(kb, 0, 'f', kb < 10.0 ? 1 : 0); |
|
double mb = kb / 1024.0; |
|
if (mb < 1024.0) return QString("%1MB").arg(mb, 0, 'f', mb < 10.0 ? 1 : 0); |
|
double gb = mb / 1024.0; |
|
return QString("%1GB").arg(gb, 0, 'f', 1); |
|
} |
|
|
|
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(); |
|
L.isOutgoing = index.data(MsgIsOutgoingRole).toBool(); |
|
|
|
/* Voice message detection */ |
|
QString contentType = index.data(MsgContentTypeRole).toString(); |
|
L.isVoice = (contentType == "audio/opus"); |
|
if (L.isVoice) { |
|
L.voiceDuration = index.data(MsgVoiceDurationRole).toString(); |
|
int pbs = 16; // play button size |
|
int wh = 25; // waveform height |
|
int th = 14; // text line height |
|
L.bubbleWidth = 220; |
|
int barW = 3, barPad = 4; |
|
int bubbleH = kPadTop + wh + 4 + th + kPadBot; |
|
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap; |
|
|
|
int contentEdge = L.isOutgoing |
|
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap)) |
|
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap)); |
|
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth - barW - barPad) : contentEdge; |
|
L.bubbleRect = QRect(bx, 0, L.bubbleWidth, bubbleH); |
|
L.mediaBarRect = QRect(L.isOutgoing ? bx + L.bubbleWidth + 1 : bx - barW - 1, 0, barW, bubbleH); |
|
|
|
int dlState = index.data(MsgFileDownloadStateRole).toInt(); |
|
int bd = index.data(MsgMediaBlocksDoneRole).toInt(); |
|
int nb = index.data(MsgMediaNumBlocksRole).toInt(); |
|
L.mediaBarColor = (dlState == 2) ? 2 : (dlState == 1 || (bd > 0 && nb > 0)) ? 1 : 0; |
|
if (dlState == 1 && nb > 0) L.progressFrac = (float)bd / nb; |
|
if (dlState == 1) { L.progressBarRect = QRect(bx, bubbleH - 3, L.bubbleWidth, 3); } |
|
|
|
L.voicePlayBtnRect = QRect(bx + kPadH, kPadTop + 4, pbs, pbs); |
|
int wx = bx + kPadH + pbs + 4; |
|
int ww = L.bubbleWidth - 2*kPadH - pbs - 4; |
|
L.voiceWaveformRect = QRect(wx, kPadTop + 1, ww, wh); |
|
L.voiceDurationRect = QRect(wx, kPadTop + wh + 4, ww - 40, th); |
|
L.voiceTimeRect = QRect(wx + ww - 40, kPadTop + wh + 4, 40, th); |
|
|
|
if (!L.narrow) { |
|
int avatarY = L.bubbleRect.bottom() - kAvatar; |
|
if (!L.isOutgoing) |
|
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar); |
|
else |
|
L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, avatarY, kAvatar, kAvatar); |
|
} |
|
return L; |
|
} |
|
|
|
/* File attachment detection */ |
|
L.isFile = (contentType == "application/octet-stream"); |
|
if (L.isFile) { |
|
QFontMetrics fm(option.font); |
|
int th = fm.height(); |
|
int ih = 24; /* icon height */ |
|
L.bubbleWidth = 230; |
|
int barW = 3, barPad = 4; |
|
int bubbleH = kPadTop + ih + 2 + th + kPadBot; |
|
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap; |
|
|
|
int contentEdge = L.isOutgoing |
|
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap)) |
|
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap)); |
|
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth - barW - barPad) : contentEdge; |
|
L.bubbleRect = QRect(bx, 0, L.bubbleWidth, bubbleH); |
|
L.mediaBarRect = QRect(L.isOutgoing ? bx + L.bubbleWidth + 1 : bx - barW - 1, 0, barW, bubbleH); |
|
|
|
int dlState = index.data(MsgFileDownloadStateRole).toInt(); |
|
int bd = index.data(MsgMediaBlocksDoneRole).toInt(); |
|
int nb = index.data(MsgMediaNumBlocksRole).toInt(); |
|
L.mediaBarColor = (dlState == 2) ? 2 : (dlState == 1 || (bd > 0 && nb > 0)) ? 1 : 0; |
|
if (dlState == 1 && nb > 0) L.progressFrac = (float)bd / nb; |
|
if (dlState == 1) { L.progressBarRect = QRect(bx, bubbleH - 3, L.bubbleWidth, 3); } |
|
|
|
L.textRect = QRect(bx + kPadH + ih + 8, kPadTop, L.bubbleWidth - 2*kPadH - ih - 8, th); |
|
L.statusRect = QRect(bx + kPadH + ih + 8 - 48, kPadTop + th + 2, L.bubbleWidth - 2*kPadH - ih - 8 + 48 - 40, th); |
|
|
|
if (!L.narrow) { |
|
int avatarY = L.bubbleRect.bottom() - kAvatar; |
|
if (!L.isOutgoing) |
|
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar); |
|
else |
|
L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, avatarY, kAvatar, kAvatar); |
|
} |
|
return L; |
|
} |
|
|
|
L.emojiCount = detectEmojiOnly(text); |
|
L.emojiOnly = (L.emojiCount >= 1); |
|
|
|
if (L.emojiOnly) { |
|
QFontMetrics fm(option.font); |
|
int lineH = fm.lineSpacing(); |
|
|
|
int availW = L.viewWidth - kMarginH - kAvatar - kGap - kMarginH; |
|
bool narrow = (availW < 400); |
|
int contentEdge = !L.isOutgoing |
|
? (narrow ? kMarginH : (kMarginH + kAvatar + kGap)) |
|
: L.viewWidth - (narrow ? kMarginH : (kMarginH + kAvatar + kGap)); |
|
|
|
int eSize = (L.emojiCount == 1) ? lineH * 4 : lineH * 2; |
|
int emojiGap = lineH / 2; |
|
int totalW = L.emojiCount * eSize + (L.emojiCount - 1) * emojiGap; |
|
int x = !L.isOutgoing |
|
? (contentEdge + kPadH) |
|
: (contentEdge - kPadH - totalW); |
|
|
|
L.emojiArea = QRect(x, kGap * 2, totalW, eSize); |
|
L.totalHeight = eSize + kGap * 2 + kItemGap; |
|
L.bubbleRect = QRect(); |
|
return L; |
|
} |
|
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); |
|
|
|
if (!L.isOutgoing) { |
|
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(); |
|
QFont boldFm = option.font; |
|
boldFm.setBold(true); |
|
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 = L.isOutgoing |
|
? kPadTop + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot |
|
: kPadTop + authorH + 2 + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot; |
|
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap; |
|
|
|
int contentEdge = L.isOutgoing |
|
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap)) |
|
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap)); |
|
|
|
if (!L.isOutgoing) |
|
L.bubbleRect = QRect(contentEdge, 0, bubbleW, bubbleH); |
|
else |
|
L.bubbleRect = QRect(contentEdge - bubbleW, 0, bubbleW, bubbleH); |
|
|
|
int contentTop = kPadTop; |
|
|
|
if (!L.isOutgoing) { |
|
L.authorRect = QRect(contentEdge + kPadH, contentTop, textW, authorH); |
|
contentTop += authorH + 2; |
|
} |
|
|
|
if (hasQuote) { |
|
int qX = !L.isOutgoing |
|
? (contentEdge + kPadH) |
|
: (contentEdge - kPadH - (bubbleW - 2 * kPadH)); |
|
L.quoteRect = QRect(qX, contentTop, bubbleW - 2 * kPadH, qH); |
|
if (!L.isOutgoing) |
|
L.quoteBarRect = QRect(L.quoteRect.left(), L.quoteRect.top(), kQuoteBarW, qH); |
|
else |
|
L.quoteBarRect = QRect(L.quoteRect.right() - kQuoteBarW, L.quoteRect.top(), kQuoteBarW, qH); |
|
int tx = !L.isOutgoing |
|
? (L.quoteBarRect.right() + kQuotePadH) |
|
: (L.quoteRect.left() + 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; |
|
} |
|
|
|
int txRect = !L.isOutgoing |
|
? (contentEdge + kPadH) |
|
: (contentEdge - kPadH - textW); |
|
L.textRect = QRect(txRect, contentTop, textW, textH); |
|
L.statusRect = QRect(txRect, contentTop + textH + 2, textW, kStatusH); |
|
|
|
if (!L.narrow) { |
|
int avatarY = L.bubbleRect.bottom() - kAvatar; |
|
if (!L.isOutgoing) |
|
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar); |
|
else |
|
L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, 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); |
|
|
|
if (L.emojiOnly) { |
|
QString msgText = index.data(MsgTextRole).toString(); |
|
QString trimmed = msgText.trimmed(); |
|
QFontMetrics fm(option.font); |
|
int lineH = fm.lineSpacing(); |
|
int eSize = (L.emojiCount == 1) ? lineH * 4 : lineH * 2; |
|
|
|
int x = L.emojiArea.left(); |
|
int y = L.emojiArea.top(); |
|
int pos = 0; |
|
while (pos < trimmed.length()) { |
|
if (trimmed.at(pos).isSpace()) { ++pos; continue; } |
|
QString emojiStr; |
|
for (const auto &cat : builtinEmojiSet()) |
|
for (const auto &e : cat.items) { |
|
if (e.unicode.length() > trimmed.length() - pos) continue; |
|
if (QStringView(trimmed).mid(pos, e.unicode.length()) == e.unicode) { |
|
emojiStr = e.unicode; |
|
pos += e.unicode.length(); |
|
break; |
|
} |
|
} |
|
if (emojiStr.isEmpty()) { ++pos; continue; } |
|
|
|
bool animated = g_animatedEmojiMap.contains(emojiStr); |
|
if (animated) { |
|
LottieIcon *icon = AnimTimer::instance()->icon(g_animatedEmojiMap[emojiStr]); |
|
if (icon && icon->isValid()) { |
|
QImage frame = icon->renderFrame(icon->currentFrame(), QSize(eSize, eSize)); |
|
painter->drawImage(x, y, frame); |
|
} else { |
|
QFont big = option.font; |
|
big.setPointSize((int)(big.pointSize() * (L.emojiCount == 1 ? 4.0 : 2.0))); |
|
painter->setFont(big); |
|
painter->setPen(option.palette.text().color()); |
|
painter->drawText(QRect(x, y, eSize, eSize), Qt::AlignCenter, emojiStr); |
|
} |
|
} else { |
|
QFont big = option.font; |
|
big.setPointSize((int)(big.pointSize() * (L.emojiCount == 1 ? 4.0 : 2.0))); |
|
painter->setFont(big); |
|
painter->setPen(option.palette.text().color()); |
|
painter->drawText(QRect(x, y, eSize, eSize), Qt::AlignCenter, emojiStr); |
|
} |
|
x += eSize + lineH / 2; |
|
} |
|
painter->restore(); |
|
return; |
|
} |
|
|
|
if (L.isVoice) { |
|
if (option.widget) s_voiceView = (QAbstractItemView*)option.widget; |
|
drawBubble(painter, L, option); |
|
if (!L.narrow) drawAvatar(painter, L, index); |
|
|
|
QString filePath = index.data(MsgVoiceFileRole).toString(); |
|
VoicePlayState copySt; |
|
VoicePlayState* st = ©St; |
|
if (!filePath.isEmpty() && s_voiceStates.contains(filePath)) { |
|
st = &s_voiceStates[filePath]; |
|
} else { |
|
int stateVal = index.data(MsgVoicePlayStateRole).toInt(); |
|
copySt.playing = (stateVal == 1); |
|
st = ©St; |
|
} |
|
|
|
bool isPlaying = st->playing; |
|
bool isPaused = st->paused; |
|
|
|
/* compute elapsed/frac for progress */ |
|
float elapsed = 0, targetFrac = 0, durSec = st->durationSec; |
|
if (durSec <= 0) { |
|
QString ds = index.data(MsgVoiceDurationRole).toString(); |
|
ds.remove('s'); durSec = ds.toFloat(); |
|
} |
|
if (isPaused) { |
|
elapsed = st->pausedPosSec; |
|
} else if (isPlaying) { |
|
SoundManager* sm = SoundManager::instance(); |
|
if (sm->isCurrentPcmPlaying()) { |
|
int sr = sm->currentPcmSampleRate(); |
|
if (sr > 0) elapsed = (float)((double)sm->currentPcmCursor() / sr); |
|
} |
|
if (elapsed <= 0) { |
|
qint64 now = QDateTime::currentMSecsSinceEpoch(); |
|
elapsed = (now - st->startMs) / 1000.0f; |
|
} |
|
if (elapsed > durSec) elapsed = durSec; |
|
} |
|
targetFrac = (durSec > 0) ? (elapsed / durSec) : 0; |
|
if (targetFrac < 0) targetFrac = 0; if (targetFrac > 1) targetFrac = 1; |
|
st->displayFrac += (targetFrac - st->displayFrac) * 0.15f; |
|
st->displayElapsed += (elapsed - st->displayElapsed) * 0.15f; |
|
float frac = st->displayFrac; |
|
|
|
QRectF playR = L.voicePlayBtnRect; |
|
|
|
/* Play/Pause icon (small — 10px triangle) */ |
|
painter->setPen(Qt::NoPen); |
|
painter->setBrush(QColor(0x33, 0x90, 0xEC)); |
|
if (isPlaying || isPaused) { |
|
qreal bw = playR.width() / 5.0; |
|
qreal gap = playR.width() / 8.0; |
|
qreal top = playR.center().y() - playR.height() / 3.0; |
|
qreal bot = playR.center().y() + playR.height() / 3.0; |
|
painter->drawRoundedRect(QRectF(playR.center().x() - gap - bw, top, bw, bot - top), 1, 1); |
|
painter->drawRoundedRect(QRectF(playR.center().x() + gap, top, bw, bot - top), 1, 1); |
|
} else { |
|
QPainterPath path; |
|
qreal cx = playR.center().x() - playR.width() / 6.0; |
|
qreal cy = playR.center().y(); |
|
qreal hs = playR.height() / 2.5; |
|
path.moveTo(cx, cy - hs); |
|
path.lineTo(cx + hs * 1.5, cy); |
|
path.lineTo(cx, cy + hs); |
|
path.closeSubpath(); |
|
painter->drawPath(path); |
|
} |
|
|
|
/* Waveform (25px) */ |
|
QList<QVariant> wf = index.data(MsgVoiceWaveformRole).toList(); |
|
int wfN = wf.size(); |
|
QRect wfR = L.voiceWaveformRect; |
|
if (wfN > 0) { |
|
float barW = (float)wfR.width() / wfN; |
|
int ph = wfR.height() - 3; |
|
for (int i = 0; i < wfN; i++) { |
|
float logV = wf[i].toFloat(); /* already 0..1 normalized */ |
|
int barH = 2 + (int)(logV * (ph - 2)); |
|
int x = wfR.left() + (int)(i * barW); |
|
int y = wfR.top() + ph - barH; |
|
int bw = qMax(2, (int)(barW * 0.8f)); |
|
bool before = wfN > 0 && (float)i / wfN <= frac; |
|
QColor played(0x33, 0x90, 0xEC); |
|
QColor unplayed(0x33, 0x90, 0xEC, 80); |
|
QColor idle(0x70, 0x70, 0x78, 130); |
|
painter->setBrush((isPlaying || isPaused) ? (before ? played : unplayed) : idle); |
|
painter->drawRect(QRect(x, y, bw, barH)); |
|
if (i == 0) GUI_DEBUG("voice paint: wfN=%d wfW=%d logV=%.2f barH=%d", |
|
wfN, wfR.width(), logV, barH); |
|
} |
|
} |
|
|
|
/* Progress bar (3px, at bottom of waveform area) */ |
|
int ph = 3; |
|
int pbY = wfR.bottom() - ph; |
|
painter->setBrush(QColor(0x44, 0x44, 0x44)); |
|
painter->drawRoundedRect(QRect(wfR.left(), pbY, wfR.width(), ph), 2, 2); |
|
if (frac > 0) { |
|
painter->setBrush(QColor(0x33, 0x90, 0xEC)); |
|
painter->drawRoundedRect(QRect(wfR.left(), pbY, (int)(wfR.width() * frac), ph), 2, 2); |
|
} |
|
|
|
/* Duration + elapsed on bottom line */ |
|
QFont timeFont = option.font; |
|
timeFont.setPointSize(timeFont.pointSize() - 2); |
|
painter->setFont(timeFont); |
|
painter->setPen(option.palette.text().color().lighter(150)); |
|
int eSec = (int)st->displayElapsed, dSec = (int)durSec; |
|
QString durText = QString("%1:%2 / %3:%4") |
|
.arg(eSec / 60).arg(eSec % 60, 2, 10, QChar('0')) |
|
.arg(dSec / 60).arg(dSec % 60, 2, 10, QChar('0')); |
|
painter->drawText(L.voiceDurationRect, Qt::AlignLeft | Qt::AlignVCenter, durText); |
|
|
|
/* Timestamp */ |
|
QFont tsFont = option.font; |
|
tsFont.setPointSize(tsFont.pointSize() - 1); |
|
painter->setFont(tsFont); |
|
painter->drawText(L.voiceTimeRect, Qt::AlignRight | Qt::AlignVCenter, |
|
index.data(MsgTimeRole).toString()); |
|
|
|
/* media bar (left edge, 3px) */ |
|
{ QColor barC = L.mediaBarColor == 2 ? QColor(0x40, 0xC0, 0x40) : L.mediaBarColor == 1 ? QColor(0xF0, 0xC0, 0x40) : QColor(0x80, 0x80, 0x80); |
|
painter->setPen(Qt::NoPen); painter->setBrush(barC); painter->drawRect(L.mediaBarRect); } |
|
/* progress bar (bottom edge, 3px) */ |
|
if (L.progressBarRect.isValid()) { |
|
painter->setBrush(QColor(0x44, 0x44, 0x44)); |
|
painter->drawRoundedRect(L.progressBarRect, 2, 2); |
|
int pw = (int)(L.progressBarRect.width() * L.progressFrac); |
|
if (pw > 0) { painter->setBrush(QColor(0x33, 0x90, 0xEC)); painter->drawRoundedRect(QRect(L.progressBarRect.left(), L.progressBarRect.top(), pw, 3), 2, 2); } |
|
} |
|
|
|
painter->restore(); |
|
return; |
|
} |
|
|
|
if (L.isFile) { |
|
drawBubble(painter, L, option); |
|
if (!L.narrow) drawAvatar(painter, L, index); |
|
|
|
int dlState = index.data(MsgFileDownloadStateRole).toInt(); |
|
|
|
/* File icon based on download state: 0=not_started, 1=downloading, 2=downloaded */ |
|
QFont iconFont = option.font; |
|
iconFont.setPointSize(iconFont.pointSize() + 6); |
|
painter->setFont(iconFont); |
|
painter->setPen(option.palette.text().color()); |
|
QRect iconRect(L.bubbleRect.left() + kPadH, kPadTop, 24, 24); |
|
if (dlState == 2) |
|
painter->drawText(iconRect, Qt::AlignCenter, QString::fromUtf8("\xE2\x9C\x85")); |
|
else |
|
painter->drawText(iconRect, Qt::AlignCenter, QString::fromUtf8("\xE2\xAC\x87\xEF\xB8\x8F")); |
|
|
|
QFont fileFont = option.font; |
|
fileFont.setBold(true); |
|
painter->setFont(fileFont); |
|
painter->setPen(option.palette.text().color()); |
|
QString displayName = index.data(MsgFileDisplayNameRole).toString(); |
|
painter->drawText(L.textRect, Qt::AlignLeft | Qt::AlignVCenter, displayName); |
|
|
|
QFont sizeFont = option.font; |
|
sizeFont.setPointSize(sizeFont.pointSize() - 2); |
|
painter->setFont(sizeFont); |
|
painter->setPen(QColor(0x80, 0x80, 0x80)); |
|
qint64 fsz = index.data(MsgFileSizeRole).toLongLong(); |
|
QString sizeStr = formatFileSize(fsz); |
|
QString stateStr = (dlState == 2) ? QString::fromUtf8("\xE2\x9C\x85") : (dlState == 1) ? "Downloading..." : "Download"; |
|
QString info = sizeStr + " \xC2\xB7 " + stateStr; |
|
int nameH = QFontMetrics(fileFont).height(); |
|
QRect sizeRect(L.textRect.left(), L.textRect.top() + nameH, L.textRect.width(), QFontMetrics(sizeFont).height()); |
|
painter->drawText(sizeRect, Qt::AlignLeft | Qt::AlignVCenter, info); |
|
|
|
/* media bar (left edge, 3px) */ |
|
{ QColor barC = L.mediaBarColor == 2 ? QColor(0x40, 0xC0, 0x40) : L.mediaBarColor == 1 ? QColor(0xF0, 0xC0, 0x40) : QColor(0x80, 0x80, 0x80); |
|
painter->setPen(Qt::NoPen); painter->setBrush(barC); painter->drawRect(L.mediaBarRect); } |
|
/* progress bar (bottom edge, 3px) */ |
|
if (L.progressBarRect.isValid()) { |
|
painter->setBrush(QColor(0x44, 0x44, 0x44)); |
|
painter->drawRoundedRect(L.progressBarRect, 2, 2); |
|
int pw = (int)(L.progressBarRect.width() * L.progressFrac); |
|
if (pw > 0) { painter->setBrush(QColor(0x33, 0x90, 0xEC)); painter->drawRoundedRect(QRect(L.progressBarRect.left(), L.progressBarRect.top(), pw, 3), 2, 2); } |
|
} |
|
|
|
painter->restore(); |
|
return; |
|
|
|
|
|
} |
|
|
|
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, option); |
|
|
|
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); |
|
|
|
if (!L.isOutgoing) { |
|
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 QStyleOptionViewItem &opt) 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; |
|
if (!L.isOutgoing) { |
|
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); |
|
} else { |
|
qreal tx = b.right(); |
|
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); |
|
|
|
QColor bubbleFill; |
|
if (!L.isOutgoing) { |
|
QColor base = opt.palette.window().color(); |
|
bubbleFill = base.lighter(135); |
|
} else { |
|
QColor base = opt.palette.window().color(); |
|
bubbleFill = base.lighter(135); |
|
bubbleFill.setRed(qMax(0, bubbleFill.red() - 10)); |
|
bubbleFill.setGreen(qMin(255, bubbleFill.green() + 5)); |
|
bubbleFill.setBlue(qMax(0, bubbleFill.blue() - 10)); |
|
} |
|
p->setPen(Qt::NoPen); |
|
p->setBrush(bubbleFill); |
|
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(); |
|
}
|
|
|