Browse Source

Render and hit-test reaction footer for every desktop message type

master
evgeny 5 days ago
parent
commit
00ca006d26
  1. 153
      tools/chatgui/src/messagedelegate.cpp
  2. 11
      tools/chatgui/src/messagedelegate.h

153
tools/chatgui/src/messagedelegate.cpp

@ -137,10 +137,21 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
Layout L = calcLayout(option, index); Layout L = calcLayout(option, index);
const auto* mouse = static_cast<QMouseEvent*>(event); const auto* mouse = static_cast<QMouseEvent*>(event);
const QPoint point = mouse->pos() - option.rect.topLeft();
int reaction = L.reactionHeart.contains(point) ? -1 : -2;
for (const auto& chip : L.reactionChips) if (chip.rect.contains(point)) { reaction = chip.id; break; }
const bool onReaction = mouse->button() == Qt::LeftButton && reaction != -2;
const bool onAvatar=mouse->button()==Qt::LeftButton && !L.narrow && L.avatarRect.contains(mouse->pos()-option.rect.topLeft()); const bool onAvatar=mouse->button()==Qt::LeftButton && !L.narrow && L.avatarRect.contains(mouse->pos()-option.rect.topLeft());
if (event->type()==QEvent::MouseButtonPress) { if (event->type()==QEvent::MouseButtonPress) {
m_avatarPress=onAvatar ? index : QModelIndex(); m_avatarPressPosition=mouse->pos(); return onAvatar; m_reactionPress = onReaction ? index : QModelIndex(); m_pressedReaction = reaction; m_reactionPressPosition = mouse->pos();
m_avatarPress=onAvatar ? index : QModelIndex(); m_avatarPressPosition=mouse->pos(); return onAvatar || onReaction;
} }
const bool hadReactionPress = m_reactionPress.isValid();
const bool activateReaction = onReaction && m_reactionPress == index && m_pressedReaction == reaction
&& (mouse->pos() - m_reactionPressPosition).manhattanLength() < 5;
m_reactionPress = QPersistentModelIndex(); m_pressedReaction = -2;
if (activateReaction) { emit reactionRequested(index, reaction); return true; }
if (hadReactionPress || onReaction || (L.reactionHeart.isValid() && point.y() >= L.reactionHeart.top())) return true;
const bool activateAvatar=onAvatar && m_avatarPress==index && (mouse->pos()-m_avatarPressPosition).manhattanLength()<5; const bool activateAvatar=onAvatar && m_avatarPress==index && (mouse->pos()-m_avatarPressPosition).manhattanLength()<5;
m_avatarPress=QPersistentModelIndex(); m_avatarPress=QPersistentModelIndex();
if (activateAvatar) { if (activateAvatar) {
@ -391,6 +402,29 @@ static int calcMaxLineWidth(const QFont &font, const QString &text, int availW)
} }
MessageDelegate::Layout MessageDelegate::calcLayout( MessageDelegate::Layout MessageDelegate::calcLayout(
const QStyleOptionViewItem& option, const QModelIndex& index) const {
Layout layout = calcBodyLayout(option, index);
const QString channel = index.data(MsgChannelIdRole).toString();
if (layout.emojiOnly || channel.isEmpty() || channel.startsWith("dm:")
|| index.data(MsgMessageKeyRole).toString().size() != 64 || layout.bubbleRect.isEmpty()) return layout;
QFont font = option.font; font.setPointSize(qMax(1, font.pointSize() - 1));
const int height = qMax(28, QFontMetrics(font).height() + 8), left = layout.bubbleRect.left();
int y = layout.bubbleRect.bottom() + 4, x = left + height + 4;
layout.reactionHeart = QRect(left, y, height, height);
for (const auto& value : index.data(MsgReactionsRole).toList()) {
const auto reaction = value.toMap();
font.setBold(reaction.value("mine").toBool());
const int width = qMin(layout.bubbleRect.width(), 32 + QFontMetrics(font).horizontalAdvance(reaction.value("count").toString()));
if (x + width > left + layout.bubbleRect.width()) { x = left; y += height + 4; }
layout.reactionChips.append({QRect(x, y, width, height), reaction.value("id").toInt(),
reaction.value("emoji").toString(), reaction.value("count").toInt(), reaction.value("mine").toBool()});
x += width + 4;
}
layout.totalHeight = qMax(layout.totalHeight, y + height + kItemGap);
return layout;
}
MessageDelegate::Layout MessageDelegate::calcBodyLayout(
const QStyleOptionViewItem &option, const QModelIndex &index) const const QStyleOptionViewItem &option, const QModelIndex &index) const
{ {
Layout L{}; Layout L{};
@ -897,11 +931,20 @@ static void drawAnimatedEmojiOverlay(QPainter *p, const QFont &font,
void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const { const QModelIndex &index) const {
const Layout layout = calcLayout(option, index);
paintBody(painter, option, index, layout);
if (!layout.reactionHeart.isValid()) return;
painter->save(); painter->translate(option.rect.topLeft());
painter->setClipRect(QRect(QPoint(0, 0), option.rect.size()));
drawReactions(painter, layout, option); painter->restore();
}
void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index, const Layout& L) const {
painter->save(); painter->save();
painter->translate(option.rect.topLeft()); painter->translate(option.rect.topLeft());
painter->setClipRect(QRect(QPoint(0, 0), option.rect.size())); painter->setClipRect(QRect(QPoint(0, 0), option.rect.size()));
Layout L = calcLayout(option, index);
if (L.deliveryRect.isValid()) { if (L.deliveryRect.isValid()) {
QFont font = option.font; QFont font = option.font;
font.setPointSize(qMax(1, font.pointSize() - 2)); font.setPointSize(qMax(1, font.pointSize() - 2));
@ -1533,80 +1576,42 @@ void MessageDelegate::drawBubble(QPainter *p, const Layout &L, const QStyleOptio
p->restore(); p->restore();
} }
void MessageDelegate::drawStatusBar(QPainter *p, const Layout &L, void MessageDelegate::drawReactions(QPainter* p, const Layout& layout, const QStyleOptionViewItem& option) const {
const QModelIndex &index, p->save(); p->setRenderHint(QPainter::Antialiasing);
const QStyleOptionViewItem &opt) const { const QPointF origin = QRectF(layout.reactionHeart).center() - QPointF(12, 12);
p->save(); QPainterPath heart;
heart.moveTo(origin + QPointF(12, 21));
QRect r = L.statusRect; heart.cubicTo(origin + QPointF(10.5, 19.5), origin + QPointF(3, 14.2), origin + QPointF(3, 8.5));
heart.cubicTo(origin + QPointF(3, 3), origin + QPointF(9, 2), origin + QPointF(12, 6));
QVariant reactionsVar = index.data(MsgReactionsRole); heart.cubicTo(origin + QPointF(15, 2), origin + QPointF(21, 3), origin + QPointF(21, 8.5));
QVariantList reactions = reactionsVar.toList(); heart.cubicTo(origin + QPointF(21, 14.2), origin + QPointF(13.5, 19.5), origin + QPointF(12, 21));
heart.closeSubpath();
int rx = r.left(); p->setBrush(Qt::NoBrush);
if (!reactions.isEmpty()) { p->setPen(QPen(QColor(0, 0, 0, 153), 1.8 + 2.0 / p->device()->devicePixelRatioF(), Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
for (const QVariant &rv : reactions) { p->drawPath(heart);
QVariantMap rm = rv.toMap(); p->setPen(QPen(Qt::white, 1.8, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); p->drawPath(heart);
QString emoji = rm["emoji"].toString(); for (const auto& chip : layout.reactionChips) {
int count = rm["count"].toInt(); p->setPen(Qt::NoPen); p->setBrush(chip.mine ? QColor(51, 144, 236, 100) : QColor(0, 0, 0, 40));
QVariant colorsVar = rm["colors"]; p->drawRoundedRect(chip.rect, 9, 9);
QList<QColor> colors = colorsVar.value<QList<QColor>>(); const QRect iconRect(chip.rect.left() + 5, chip.rect.top() + (chip.rect.height() - 20) / 2, 20, 20);
LottieIcon* icon = g_animatedEmojiMap.contains(chip.emoji) ? AnimTimer::instance()->icon(g_animatedEmojiMap[chip.emoji]) : nullptr;
if (count < 4 && !colors.isEmpty()) { QFont font = option.font; font.setPointSize(qMax(1, font.pointSize() - 1)); p->setFont(font);
int iconSize = 16; if (icon && icon->isValid()) p->drawImage(iconRect, icon->renderFrame(icon->currentFrame(), QSize(20, 20)));
for (int i = colors.size() - 1; i >= 0; --i) { else { p->setPen(option.palette.text().color()); p->drawText(iconRect, Qt::AlignCenter, chip.emoji); }
int cx = rx + i * 11; font.setBold(chip.mine); p->setFont(font); p->setPen(option.palette.text().color());
p->setPen(Qt::NoPen); p->drawText(chip.rect.adjusted(27, 0, -5, 0), Qt::AlignLeft | Qt::AlignVCenter, QString::number(chip.count));
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.setBold(rm.value("mine").toBool());
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;
}
}
} }
p->restore();
}
QFont smallFont = opt.font; void MessageDelegate::drawStatusBar(QPainter* p, const Layout& layout, const QModelIndex& index,
smallFont.setPointSize(smallFont.pointSize() - 2); const QStyleOptionViewItem& option) const {
p->setFont(smallFont); p->save();
p->setPen(QColor(0x80, 0x80, 0x80)); QFont font = option.font; font.setPointSize(qMax(1, font.pointSize() - 2)); p->setFont(font);
p->setPen(QColor(128, 128, 128));
QString statusText; QString text = index.data(MsgEditedRole).toBool() ? QCoreApplication::translate("ChatGui", "edited ") : QString();
if (index.data(MsgEditedRole).toBool()) { text += index.data(MsgTimeRole).toString();
statusText = QCoreApplication::translate("ChatGui", "edited "); p->drawText(layout.statusRect, Qt::AlignRight | Qt::AlignVCenter, text);
}
statusText += index.data(MsgTimeRole).toString();
p->drawText(QRect(r.left(), r.top(), r.width(), r.height()),
Qt::AlignRight | Qt::AlignVCenter, statusText);
p->restore(); p->restore();
} }

11
tools/chatgui/src/messagedelegate.h

@ -5,6 +5,7 @@
#include <QStyledItemDelegate> #include <QStyledItemDelegate>
#include <QPersistentModelIndex> #include <QPersistentModelIndex>
#include <QVector>
#include <functional> #include <functional>
enum MessageDataRole { enum MessageDataRole {
@ -73,8 +74,10 @@ public:
void setDownloadCallback(std::function<bool(const QModelIndex&)> cb) { m_downloadCb = std::move(cb); } void setDownloadCallback(std::function<bool(const QModelIndex&)> cb) { m_downloadCb = std::move(cb); }
signals: signals:
void avatarRequested(const QString& channel, quint64 owner); void avatarRequested(const QString& channel, quint64 owner);
void reactionRequested(const QModelIndex& index, int emoji); /* -1 открывает выбор реакции */
private: private:
struct ReactionChip { QRect rect; int id; QString emoji; int count; bool mine; };
struct Layout { struct Layout {
int viewWidth; int viewWidth;
int bubbleWidth; int bubbleWidth;
@ -112,16 +115,24 @@ private:
QRect videoDurBadgeRect; QRect videoDurBadgeRect;
QRect imageThumbRect; QRect imageThumbRect;
QRect imageBadgeRect; QRect imageBadgeRect;
QRect reactionHeart;
QVector<ReactionChip> reactionChips;
}; };
std::function<void(const QString&)> m_videoOpenCb; std::function<void(const QString&)> m_videoOpenCb;
std::function<void(const QString&)> m_imageOpenCb; std::function<void(const QString&)> m_imageOpenCb;
QPersistentModelIndex m_avatarPress; QPersistentModelIndex m_avatarPress;
QPoint m_avatarPressPosition; QPoint m_avatarPressPosition;
QPersistentModelIndex m_reactionPress;
QPoint m_reactionPressPosition;
int m_pressedReaction = -2;
std::function<bool(const QModelIndex&)> m_downloadCb; std::function<bool(const QModelIndex&)> m_downloadCb;
Layout calcLayout(const QStyleOptionViewItem &option, Layout calcLayout(const QStyleOptionViewItem &option,
const QModelIndex &index) const; const QModelIndex &index) const;
Layout calcBodyLayout(const QStyleOptionViewItem& option, const QModelIndex& index) const;
void paintBody(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const Layout& layout) const;
void drawReactions(QPainter* painter, const Layout& layout, const QStyleOptionViewItem& option) const;
void drawQuote(QPainter *p, const Layout &L, void drawQuote(QPainter *p, const Layout &L,
const QModelIndex &index, const QStyleOptionViewItem &opt) const; const QModelIndex &index, const QStyleOptionViewItem &opt) const;
void drawBubble(QPainter *p, const Layout &L, const QStyleOptionViewItem &opt) const; void drawBubble(QPainter *p, const Layout &L, const QStyleOptionViewItem &opt) const;

Loading…
Cancel
Save