1589 lines
65 KiB
C++
1589 lines
65 KiB
C++
#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>
|
||
#include <QImageReader>
|
||
#include <QPersistentModelIndex>
|
||
#include <QRegularExpression>
|
||
|
||
/* Один 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<QTextLayout::FormatRange> 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) {}
|
||
|
||
struct VoicePlayState {
|
||
uint64_t playbackId = 0;
|
||
bool loading = false;
|
||
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->loading) { anyActive = true; continue; }
|
||
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->isPcmPlaying(it->playbackId)) {
|
||
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.isVideo) {
|
||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||
|
||
if (dlState == 2) { /* скачано — открыть плеер */
|
||
QString fp = index.data(MsgVoiceFileRole).toString();
|
||
if (!fp.isEmpty() && QFile::exists(fp) && m_videoOpenCb) {
|
||
m_videoOpenCb(fp);
|
||
return true;
|
||
}
|
||
/* файл пропал (вычистка кеша/удаление) — перекачиваем ниже */
|
||
}
|
||
if (dlState == 1) return false; /* скачивается */
|
||
|
||
return m_downloadCb && m_downloadCb(index);
|
||
}
|
||
|
||
if (L.isImage) {
|
||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||
|
||
if (dlState == 2) { /* скачано — открыть просмотрщик */
|
||
QString fp = index.data(MsgVoiceFileRole).toString();
|
||
if (!fp.isEmpty() && QFile::exists(fp) && m_imageOpenCb) {
|
||
m_imageOpenCb(fp);
|
||
return true;
|
||
}
|
||
/* файл пропал (вычистка кеша/удаление) — перекачиваем ниже */
|
||
}
|
||
if (dlState == 1) return false; /* скачивается */
|
||
|
||
return m_downloadCb && m_downloadCb(index);
|
||
}
|
||
|
||
if (L.isFile) {
|
||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||
qint64 msgId = index.data(MsgIdRole).toLongLong();
|
||
QString chId = index.data(MsgChannelIdRole).toString();
|
||
|
||
if (chId.startsWith(QStringLiteral("dm:"))) return false; /* PM сохраняется через контекстное меню. */
|
||
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 == 1) return false; /* downloading — no action */
|
||
|
||
return m_downloadCb && m_downloadCb(index);
|
||
}
|
||
|
||
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();
|
||
|
||
GUI_DEBUG("editorEvent voice: playBtn=%d wave=%d file='%s' exists=%d dlState=%d msgId=%lld",
|
||
(int)onPlayBtn, (int)onWaveform, qPrintable(filePath), QFile::exists(filePath),
|
||
dlState, (long long)msgId);
|
||
|
||
/* not downloaded (or file evicted): start download (play will start on completion) */
|
||
bool fileMissing = filePath.isEmpty() || !QFile::exists(filePath);
|
||
if (fileMissing && dlState != 1) {
|
||
if (!m_downloadCb || !m_downloadCb(index)) return false;
|
||
model->setData(index, true, MsgVoiceDownloadPendingRole);
|
||
return true;
|
||
}
|
||
|
||
if (fileMissing) 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];
|
||
if (st.loading) return true;
|
||
|
||
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.loading = true;
|
||
QPersistentModelIndex persistent(index);
|
||
VoicePlayer::playOpusFile(filePath, this, [filePath, clickFrac, persistent](float actualDur, uint64_t playbackId) {
|
||
s_voiceStates[filePath].loading = false;
|
||
if (actualDur <= 0) return;
|
||
MessageDelegate::startAutoPlay(filePath, actualDur, playbackId);
|
||
float position = clickFrac * actualDur;
|
||
auto& state = s_voiceStates[filePath];
|
||
state.startMs -= (qint64)(position * 1000); state.pausedPosSec = position;
|
||
state.displayFrac = clickFrac; state.displayElapsed = position;
|
||
int rate = SoundManager::instance()->pcmSampleRate(playbackId);
|
||
if (rate > 0) SoundManager::instance()->seekPcm(playbackId, (unsigned long long)(position * rate));
|
||
if (persistent.isValid()) const_cast<QAbstractItemModel*>(persistent.model())->setData(persistent, 1, MsgVoicePlayStateRole);
|
||
});
|
||
s_voiceTimer->start();
|
||
} 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()->pcmSampleRate(st.playbackId);
|
||
if (sr > 0)
|
||
SoundManager::instance()->seekPcm(st.playbackId, (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()->pausePcm(st.playbackId);
|
||
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()->resumePcm(st.playbackId);
|
||
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;
|
||
}
|
||
VoicePlayer::stop();
|
||
}
|
||
|
||
st.loading = true;
|
||
QPersistentModelIndex persistent(index);
|
||
VoicePlayer::playOpusFile(filePath, this, [filePath, persistent](float actualDur, uint64_t playbackId) {
|
||
s_voiceStates[filePath].loading = false;
|
||
if (actualDur <= 0) return;
|
||
MessageDelegate::startAutoPlay(filePath, actualDur, playbackId);
|
||
if (persistent.isValid()) const_cast<QAbstractItemModel*>(persistent.model())->setData(persistent, 1, MsgVoicePlayStateRole);
|
||
});
|
||
s_voiceTimer->start();
|
||
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;
|
||
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();
|
||
}
|
||
|
||
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 QString formatBitrate(qint64 bytes, qint64 durMs) {
|
||
if (bytes <= 0 || durMs <= 0) return QString();
|
||
qint64 kbps = bytes * 8 / durMs;
|
||
if (kbps >= 1000) return QString("%1Mbps").arg(kbps / 1000.0, 0, 'f', 1);
|
||
return QString("%1kbps").arg(kbps);
|
||
}
|
||
|
||
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 (!index.data(MsgDeliveryStateRole).toString().isEmpty()) {
|
||
L.deliveryRect = QRect(bx, bubbleH + 2, L.bubbleWidth, th);
|
||
L.totalHeight += th + 2;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* Video message detection */
|
||
L.isVideo = (contentType == "video/mp4");
|
||
if (L.isVideo) {
|
||
int barW = 3, barPad = 4;
|
||
int thW = 200, thH = 112; /* 16:9 превью */
|
||
int infoH = 16; /* строка под пузырём: res·size·bitrate + время */
|
||
L.bubbleWidth = thW;
|
||
L.totalHeight = qMax(thH, kAvatar) + infoH + kItemGap;
|
||
|
||
int contentEdge = L.isOutgoing
|
||
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
|
||
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap));
|
||
int bx = L.isOutgoing ? (contentEdge - thW - barW - barPad) : contentEdge;
|
||
L.bubbleRect = QRect(bx, 0, thW, thH);
|
||
L.videoThumbRect = L.bubbleRect;
|
||
L.mediaBarRect = QRect(L.isOutgoing ? bx + thW + 1 : bx - barW - 1, 0, barW, thH);
|
||
L.statusRect = QRect(bx, thH + 2, thW, infoH - 2);
|
||
if (!index.data(MsgDeliveryStateRole).toString().isEmpty()) {
|
||
L.deliveryRect = QRect(bx, thH + infoH + 2, thW, infoH - 2);
|
||
L.totalHeight += infoH;
|
||
}
|
||
|
||
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, thH - 3, thW, 3); }
|
||
|
||
L.videoPlayBtnRect = QRect(bx + thW / 2 - 24, thH / 2 - 24, 48, 48);
|
||
L.videoDurBadgeRect = QRect(bx + thW - 66, thH - 26, 58, 18);
|
||
|
||
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;
|
||
}
|
||
|
||
/* Image message detection */
|
||
L.isImage = contentType.startsWith("image/");
|
||
if (L.isImage) {
|
||
int barW = 3, barPad = 4;
|
||
int thW = 220, thH = 160;
|
||
L.bubbleWidth = thW;
|
||
L.totalHeight = qMax(thH, 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 - thW - barW - barPad) : contentEdge;
|
||
L.bubbleRect = QRect(bx, 0, thW, thH);
|
||
L.imageThumbRect = L.bubbleRect;
|
||
L.mediaBarRect = QRect(L.isOutgoing ? bx + thW + 1 : bx - barW - 1, 0, barW, thH);
|
||
|
||
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, thH - 3, thW, 3); }
|
||
|
||
L.imageBadgeRect = QRect(bx + thW - 150, thH - 26, 142, 18);
|
||
|
||
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.deliveryRect.isValid()) {
|
||
QFont font = option.font;
|
||
font.setPointSize(qMax(1, font.pointSize() - 2));
|
||
painter->setFont(font);
|
||
painter->setPen(option.palette.text().color().lighter(150));
|
||
const auto text = QFontMetrics(font).elidedText(index.data(MsgDeliveryStateRole).toString(), Qt::ElideRight, L.deliveryRect.width());
|
||
painter->drawText(L.deliveryRect, Qt::AlignRight | Qt::AlignVCenter, text);
|
||
}
|
||
|
||
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;
|
||
bool found = false;
|
||
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();
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (found) 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->isPcmPlaying(st->playbackId)) {
|
||
int sr = sm->pcmSampleRate(st->playbackId);
|
||
if (sr > 0) elapsed = (float)((double)sm->pcmCursor(st->playbackId) / 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));
|
||
}
|
||
}
|
||
|
||
/* 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.isVideo) {
|
||
drawBubble(painter, L, option);
|
||
if (!L.narrow) drawAvatar(painter, L, index);
|
||
|
||
QString filePath = index.data(MsgVoiceFileRole).toString();
|
||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||
QString thumbPath = filePath + ".jpg";
|
||
|
||
painter->save();
|
||
painter->setRenderHint(QPainter::Antialiasing);
|
||
QPainterPath clip;
|
||
clip.addRoundedRect(L.videoThumbRect, kRadius, kRadius);
|
||
painter->setClipPath(clip);
|
||
|
||
QPixmap thumb;
|
||
if (!thumbPath.isEmpty() && QFile::exists(thumbPath))
|
||
thumb.load(thumbPath);
|
||
|
||
if (!thumb.isNull()) {
|
||
QPixmap scaled = thumb.scaled(L.videoThumbRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||
int dx = (scaled.width() - L.videoThumbRect.width()) / 2;
|
||
int dy = (scaled.height() - L.videoThumbRect.height()) / 2;
|
||
painter->drawPixmap(L.videoThumbRect.topLeft(), scaled, QRect(dx, dy, L.videoThumbRect.width(), L.videoThumbRect.height()));
|
||
} else {
|
||
painter->fillRect(L.videoThumbRect, QColor(30, 30, 35));
|
||
QFont iconFont = option.font;
|
||
iconFont.setPointSize(iconFont.pointSize() + 10);
|
||
painter->setFont(iconFont);
|
||
painter->setPen(QColor(150, 150, 150));
|
||
painter->drawText(L.videoThumbRect, Qt::AlignCenter, QString::fromUtf8("\xF0\x9F\x8E\xAC"));
|
||
}
|
||
|
||
/* play-оверлей */
|
||
if (dlState == 2) {
|
||
painter->setPen(Qt::NoPen);
|
||
painter->setBrush(QColor(0, 0, 0, 110));
|
||
painter->drawEllipse(L.videoPlayBtnRect);
|
||
QPainterPath tri;
|
||
int cx = L.videoPlayBtnRect.center().x() - L.videoPlayBtnRect.width() / 12;
|
||
int cy = L.videoPlayBtnRect.center().y();
|
||
int hs = L.videoPlayBtnRect.height() / 4;
|
||
tri.moveTo(cx, cy - hs);
|
||
tri.lineTo(cx + hs * 2, cy);
|
||
tri.lineTo(cx, cy + hs);
|
||
tri.closeSubpath();
|
||
painter->setBrush(Qt::white);
|
||
painter->drawPath(tri);
|
||
}
|
||
|
||
/* бейдж: длительность (m:ss) */
|
||
QString durStr = index.data(MsgVoiceDurationRole).toString();
|
||
if (!durStr.isEmpty()) {
|
||
QFont durFont = option.font;
|
||
durFont.setPointSize(durFont.pointSize() - 1);
|
||
painter->setFont(durFont);
|
||
painter->setPen(Qt::NoPen);
|
||
painter->setBrush(QColor(0, 0, 0, 160));
|
||
painter->drawRoundedRect(L.videoDurBadgeRect, 5, 5);
|
||
painter->setPen(Qt::white);
|
||
painter->drawText(L.videoDurBadgeRect, Qt::AlignCenter, durStr);
|
||
}
|
||
painter->restore();
|
||
|
||
/* progress overlay (скачивание) */
|
||
if (dlState == 1) {
|
||
painter->setBrush(QColor(0, 0, 0, 140));
|
||
painter->drawRect(L.videoThumbRect);
|
||
QFont pfont = option.font;
|
||
pfont.setPointSize(pfont.pointSize() + 2);
|
||
painter->setFont(pfont);
|
||
painter->setPen(Qt::white);
|
||
painter->drawText(L.videoThumbRect, Qt::AlignCenter,
|
||
QStringLiteral("%1%").arg((int)(L.progressFrac * 100)));
|
||
}
|
||
|
||
/* media bar (левая кромка) */
|
||
{ 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); }
|
||
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); }
|
||
}
|
||
|
||
/* строка под пузырём: res · size · bitrate + время */
|
||
{
|
||
qint64 fsz = index.data(MsgFileSizeRole).toLongLong();
|
||
qint64 durMs = index.data(MsgVideoDurationMsRole).toLongLong();
|
||
int vw = index.data(MsgVideoWidthRole).toInt();
|
||
int vh = index.data(MsgVideoHeightRole).toInt();
|
||
QStringList info;
|
||
if (vw > 0 && vh > 0) info << QString("%1×%2").arg(vw).arg(vh);
|
||
QString sz = formatFileSize(fsz);
|
||
if (!sz.isEmpty()) info << sz;
|
||
QString br = formatBitrate(fsz, durMs);
|
||
if (!br.isEmpty()) info << br;
|
||
|
||
QFont smallFont = option.font;
|
||
smallFont.setPointSize(smallFont.pointSize() - 2);
|
||
painter->setFont(smallFont);
|
||
painter->setPen(QColor(0x80, 0x80, 0x80));
|
||
|
||
if (!info.isEmpty())
|
||
painter->drawText(L.statusRect, Qt::AlignLeft | Qt::AlignVCenter, info.join(" \xC2\xB7 "));
|
||
painter->drawText(L.statusRect, Qt::AlignRight | Qt::AlignVCenter,
|
||
index.data(MsgTimeRole).toString());
|
||
}
|
||
|
||
painter->restore();
|
||
return;
|
||
}
|
||
|
||
if (L.isImage) {
|
||
drawBubble(painter, L, option);
|
||
if (!L.narrow) drawAvatar(painter, L, index);
|
||
|
||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||
QString filePath = index.data(MsgVoiceFileRole).toString();
|
||
|
||
painter->save();
|
||
painter->setRenderHint(QPainter::Antialiasing);
|
||
QPainterPath clip;
|
||
clip.addRoundedRect(L.imageThumbRect, kRadius, kRadius);
|
||
painter->setClipPath(clip);
|
||
|
||
QPixmap img;
|
||
if (!filePath.isEmpty() && QFile::exists(filePath)) {
|
||
QImageReader reader(filePath);
|
||
reader.setAutoTransform(true);
|
||
reader.setScaledSize(L.imageThumbRect.size() * 2);
|
||
QImage im = reader.read();
|
||
if (!im.isNull()) img = QPixmap::fromImage(im);
|
||
}
|
||
|
||
if (!img.isNull()) {
|
||
painter->fillRect(L.imageThumbRect, QColor(20, 20, 24));
|
||
QPixmap scaled = img.scaled(L.imageThumbRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||
int dx = (L.imageThumbRect.width() - scaled.width()) / 2;
|
||
int dy = (L.imageThumbRect.height() - scaled.height()) / 2;
|
||
painter->drawPixmap(L.imageThumbRect.left() + dx, L.imageThumbRect.top() + dy, scaled);
|
||
} else {
|
||
painter->fillRect(L.imageThumbRect, QColor(30, 30, 35));
|
||
QFont iconFont = option.font;
|
||
iconFont.setPointSize(iconFont.pointSize() + 10);
|
||
painter->setFont(iconFont);
|
||
painter->setPen(QColor(150, 150, 150));
|
||
painter->drawText(L.imageThumbRect, Qt::AlignCenter, QString::fromUtf8("\xF0\x9F\x93\xB7"));
|
||
}
|
||
|
||
/* бейдж: разрешение · размер */
|
||
qint64 fsz = index.data(MsgFileSizeRole).toLongLong();
|
||
int iw = index.data(MsgImageWidthRole).toInt();
|
||
int ih = index.data(MsgImageHeightRole).toInt();
|
||
QString res = (iw > 0 && ih > 0) ? QString("%1×%2").arg(iw).arg(ih) : QString();
|
||
QString sizeStr = formatFileSize(fsz);
|
||
QString badge = res.isEmpty() ? sizeStr : res + " \xC2\xB7 " + sizeStr;
|
||
if (!badge.isEmpty()) {
|
||
QFont durFont = option.font;
|
||
durFont.setPointSize(durFont.pointSize() - 1);
|
||
painter->setFont(durFont);
|
||
painter->setPen(Qt::NoPen);
|
||
painter->setBrush(QColor(0, 0, 0, 160));
|
||
painter->drawRoundedRect(L.imageBadgeRect, 5, 5);
|
||
painter->setPen(Qt::white);
|
||
painter->drawText(L.imageBadgeRect, Qt::AlignCenter, badge);
|
||
}
|
||
painter->restore();
|
||
|
||
/* progress overlay (скачивание) */
|
||
if (dlState == 1) {
|
||
painter->setBrush(QColor(0, 0, 0, 140));
|
||
painter->drawRect(L.imageThumbRect);
|
||
QFont pfont = option.font;
|
||
pfont.setPointSize(pfont.pointSize() + 2);
|
||
painter->setFont(pfont);
|
||
painter->setPen(Qt::white);
|
||
painter->drawText(L.imageThumbRect, Qt::AlignCenter,
|
||
QStringLiteral("%1%").arg((int)(L.progressFrac * 100)));
|
||
}
|
||
|
||
/* media bar (левая кромка) */
|
||
{ 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); }
|
||
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 deliveryState = index.data(MsgDeliveryStateRole).toString();
|
||
if (!deliveryState.isEmpty()) stateStr = deliveryState;
|
||
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();
|
||
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();
|
||
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();
|
||
}
|
||
|
||
void MessageDelegate::startAutoPlay(const QString& filePath, float actualDurationSec, uint64_t playbackId) {
|
||
if (actualDurationSec <= 0) return;
|
||
ensureVoiceTimer();
|
||
VoicePlayState& st = s_voiceStates[filePath];
|
||
st.playbackId = playbackId;
|
||
st.playing = true;
|
||
st.paused = false;
|
||
st.durationSec = (actualDurationSec > 0) ? actualDurationSec : 1.0f;
|
||
st.startMs = QDateTime::currentMSecsSinceEpoch();
|
||
st.pausedPosSec = 0.0f;
|
||
st.displayFrac = 0.0f;
|
||
st.displayElapsed = 0.0f;
|
||
s_voiceTimer->start();
|
||
if (s_voiceView) s_voiceView->viewport()->update();
|
||
}
|
||
|
||
void MessageDelegate::stopAllAutoPlay() {
|
||
VoicePlayer::stop();
|
||
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) {
|
||
it->playing = false; it->paused = false; it->loading = false;
|
||
}
|
||
}
|