File indexing completed on 2024-12-22 04:46:06
0001 /* 0002 SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "messagedelegatehelperreactions.h" 0008 #include "common/delegatepaintutil.h" 0009 #include "emoticons/emojimanager.h" 0010 #include "model/messagesmodel.h" 0011 #include "rocketchataccount.h" 0012 #include "ruqolaglobalconfig.h" 0013 #include "utils.h" 0014 0015 #include "ruqola.h" 0016 #include <QAbstractItemView> 0017 #include <QHelpEvent> 0018 #include <QMouseEvent> 0019 #include <QMovie> 0020 #include <QPainter> 0021 #include <QStyleOptionViewItem> 0022 #include <QToolTip> 0023 0024 MessageDelegateHelperReactions::MessageDelegateHelperReactions(RocketChatAccount *account) 0025 : mEmojiFont(Utils::emojiFontName()) 0026 , mRocketChatAccount(account) 0027 { 0028 // For emoji not necessary to limit cache. (mPixmapCache) 0029 } 0030 0031 QVector<MessageDelegateHelperReactions::ReactionLayout> 0032 MessageDelegateHelperReactions::layoutReactions(const QVector<Reaction> &reactions, QRect reactionsRect, const QStyleOptionViewItem &option) const 0033 { 0034 QVector<ReactionLayout> layouts; 0035 layouts.reserve(reactions.count()); 0036 auto *emojiManager = mRocketChatAccount->emojiManager(); 0037 const QFontMetricsF emojiFontMetrics(mEmojiFont); 0038 const qreal smallMargin = DelegatePaintUtil::margin() / 2.0; 0039 qreal x = reactionsRect.x(); 0040 qreal y = reactionsRect.y(); 0041 0042 for (const Reaction &reaction : reactions) { 0043 ReactionLayout layout; 0044 layout.emojiString = emojiManager->unicodeEmoticonForEmoji(reaction.reactionName()).unicode(); 0045 qreal emojiWidth = 0; 0046 if (!layout.emojiString.isEmpty()) { 0047 emojiWidth = emojiFontMetrics.horizontalAdvance(layout.emojiString); 0048 layout.useEmojiFont = true; 0049 } else { 0050 const QString fileName = emojiManager->customEmojiFileName(reaction.reactionName()); 0051 if (!fileName.isEmpty()) { 0052 const QUrl emojiUrl = mRocketChatAccount->attachmentUrlFromLocalCache(fileName); 0053 if (emojiUrl.isEmpty()) { 0054 // The download is happening, this will all be updated again later 0055 } else { 0056 if (!mPixmapCache.pixmapForLocalFile(emojiUrl.toLocalFile()).isNull()) { 0057 layout.emojiImagePath = emojiUrl.toLocalFile(); 0058 const int iconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize); 0059 emojiWidth = iconSize; 0060 } 0061 } 0062 } 0063 if (layout.emojiImagePath.isEmpty()) { 0064 layout.emojiString = reaction.reactionName(); // ugly fallback: ":1md" 0065 emojiWidth = option.fontMetrics.horizontalAdvance(layout.emojiString) + smallMargin; 0066 } 0067 layout.useEmojiFont = false; 0068 } 0069 layout.countStr = QString::number(reaction.count()); 0070 const int countWidth = option.fontMetrics.horizontalAdvance(layout.countStr) + smallMargin; 0071 // [reactionRect] = [emojiOffset (margin)] [emojiWidth] [countWidth] [margin/2] 0072 layout.reactionRect = QRectF(x, y, emojiWidth + countWidth + DelegatePaintUtil::margin(), reactionsRect.height()); 0073 layout.emojiOffset = smallMargin + 1; 0074 layout.countRect = layout.reactionRect.adjusted(layout.emojiOffset + emojiWidth, smallMargin, 0, 0); 0075 layout.reaction = reaction; 0076 0077 layouts.append(layout); 0078 x += layout.reactionRect.width() + DelegatePaintUtil::margin(); 0079 if (x > reactionsRect.width()) { 0080 x = reactionsRect.x(); 0081 y += reactionsRect.height() + DelegatePaintUtil::margin(); 0082 } 0083 } 0084 return layouts; 0085 } 0086 0087 void MessageDelegateHelperReactions::setRocketChatAccount(RocketChatAccount *newRocketChatAccount) 0088 { 0089 mRocketChatAccount = newRocketChatAccount; 0090 } 0091 0092 void MessageDelegateHelperReactions::draw(QPainter *painter, QRect reactionsRect, const QModelIndex &index, const QStyleOptionViewItem &option) const 0093 { 0094 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0095 0096 const QVector<Reaction> reactions = message->reactions().reactions(); 0097 if (reactions.isEmpty()) { 0098 return; 0099 } 0100 #if 0 0101 painter->save(); 0102 painter->setPen(Qt::red); 0103 painter->drawRect(reactionsRect); 0104 painter->restore(); 0105 #endif 0106 const QVector<ReactionLayout> layouts = layoutReactions(reactions, reactionsRect, option); 0107 0108 const QPen origPen = painter->pen(); 0109 const QBrush origBrush = painter->brush(); 0110 const QPen buttonPen(option.palette.color(QPalette::Highlight).darker()); 0111 QColor backgroundColor = option.palette.color(QPalette::Highlight); 0112 backgroundColor.setAlpha(60); 0113 const QBrush buttonBrush(backgroundColor); 0114 const qreal smallMargin = 4; 0115 for (const ReactionLayout &reactionLayout : layouts) { 0116 Q_ASSERT(!reactionLayout.emojiString.isEmpty() || !reactionLayout.emojiImagePath.isEmpty()); 0117 const QRectF reactionRect = reactionLayout.reactionRect; 0118 0119 // Rounded rect 0120 painter->setPen(buttonPen); 0121 painter->setBrush(buttonBrush); 0122 painter->drawRoundedRect(reactionRect.adjusted(0, 0, -1, -1), 5, 5); 0123 painter->setBrush(origBrush); 0124 painter->setPen(origPen); 0125 0126 // Emoji 0127 const QRectF r = reactionRect.adjusted(reactionLayout.emojiOffset, smallMargin, 0, 0); 0128 if (!reactionLayout.emojiString.isEmpty()) { 0129 if (reactionLayout.useEmojiFont) { 0130 painter->setFont(mEmojiFont); 0131 } 0132 painter->drawText(r, reactionLayout.emojiString); 0133 } else { 0134 if (reactionLayout.reaction.isAnimatedImage() && RuqolaGlobalConfig::self()->animateGifImage()) { 0135 const int maxIconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize); 0136 0137 QPixmap scaledPixmap; 0138 auto it = findRunningAnimatedImage(index); 0139 if (it != mRunningAnimatedImages.end()) { 0140 scaledPixmap = (*it).movie->currentPixmap(); 0141 } else { 0142 mRunningAnimatedImages.emplace_back(index); 0143 auto &rai = mRunningAnimatedImages.back(); 0144 rai.movie->setFileName(reactionLayout.emojiImagePath); 0145 rai.movie->setScaledSize(QSize(maxIconSize, maxIconSize)); 0146 auto view = qobject_cast<QAbstractItemView *>(const_cast<QWidget *>(option.widget)); 0147 const QPersistentModelIndex &idx = rai.index; 0148 QObject::connect( 0149 rai.movie, 0150 &QMovie::frameChanged, 0151 view, 0152 [view, idx, this]() { 0153 if (view->viewport()->rect().contains(view->visualRect(idx))) { 0154 view->update(idx); 0155 } else { 0156 removeRunningAnimatedImage(idx); 0157 } 0158 }, 0159 Qt::QueuedConnection); 0160 rai.movie->start(); 0161 scaledPixmap = rai.movie->currentPixmap(); 0162 } 0163 scaledPixmap.setDevicePixelRatio(option.widget->devicePixelRatioF()); 0164 painter->drawPixmap(r.x(), r.y(), scaledPixmap); 0165 } else { 0166 const QPixmap pixmap = mPixmapCache.pixmapForLocalFile(reactionLayout.emojiImagePath); 0167 const int maxIconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize); 0168 const QPixmap scaledPixmap = pixmap.scaled(maxIconSize, maxIconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0169 painter->drawPixmap(r.x(), r.y(), scaledPixmap); 0170 } 0171 } 0172 // Count 0173 painter->setFont(option.font); 0174 painter->drawText(reactionLayout.countRect, reactionLayout.countStr); 0175 } 0176 } 0177 0178 std::vector<RunningAnimatedImage>::iterator MessageDelegateHelperReactions::findRunningAnimatedImage(const QModelIndex &index) const 0179 { 0180 auto matchesIndex = [&](const RunningAnimatedImage &rai) { 0181 return rai.index == index; 0182 }; 0183 return std::find_if(mRunningAnimatedImages.begin(), mRunningAnimatedImages.end(), matchesIndex); 0184 } 0185 0186 void MessageDelegateHelperReactions::removeRunningAnimatedImage(const QModelIndex &index) const 0187 { 0188 auto it = findRunningAnimatedImage(index); 0189 if (it != mRunningAnimatedImages.end()) { 0190 mRunningAnimatedImages.erase(it); 0191 } 0192 } 0193 0194 QSize MessageDelegateHelperReactions::sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option) const 0195 { 0196 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0197 int reactionsHeight = 0; 0198 if (!message->reactions().isEmpty()) { 0199 const QFontMetrics emojiFontMetrics(mEmojiFont); 0200 // const QVector<ReactionLayout> layouts = layoutReactions(message->reactions().reactions(), QRect(0, 0, maxWidth, emojiFontMetrics.height()), option); 0201 // for (auto t : layouts) { 0202 // qDebug() << " t " << t.reactionRect << " maxWidth " << maxWidth; 0203 // } 0204 // qDebug() << " layouts" << layouts; 0205 reactionsHeight = qMax<qreal>(emojiFontMetrics.height(), option.fontMetrics.height()) + DelegatePaintUtil::margin(); 0206 } 0207 return {maxWidth, reactionsHeight}; 0208 } 0209 0210 bool MessageDelegateHelperReactions::handleMouseEvent(QMouseEvent *mouseEvent, QRect reactionsRect, const QStyleOptionViewItem &option, const Message *message) 0211 { 0212 if (mouseEvent->type() == QEvent::MouseButtonRelease) { 0213 const QPoint pos = mouseEvent->pos(); 0214 const QVector<ReactionLayout> reactions = layoutReactions(message->reactions().reactions(), reactionsRect, option); 0215 for (const ReactionLayout &reactionLayout : reactions) { 0216 if (reactionLayout.reactionRect.contains(pos)) { 0217 const Reaction &reaction = reactionLayout.reaction; 0218 const bool doAdd = !reaction.userNames().contains(mRocketChatAccount->userName()); 0219 mRocketChatAccount->reactOnMessage(message->messageId(), reaction.reactionName(), doAdd); 0220 return true; 0221 } 0222 } 0223 } 0224 return false; 0225 } 0226 0227 bool MessageDelegateHelperReactions::handleHelpEvent(QHelpEvent *helpEvent, 0228 QWidget *view, 0229 QRect reactionsRect, 0230 const QStyleOptionViewItem &option, 0231 const Message *message) 0232 { 0233 const QVector<ReactionLayout> reactions = layoutReactions(message->reactions().reactions(), reactionsRect, option); 0234 for (const ReactionLayout &reactionLayout : reactions) { 0235 if (reactionLayout.reactionRect.contains(helpEvent->pos())) { 0236 const Reaction &reaction = reactionLayout.reaction; 0237 const QString tooltip = reaction.convertedUsersNameAtToolTip(); 0238 QToolTip::showText(helpEvent->globalPos(), tooltip, view); 0239 return true; 0240 } 0241 } 0242 return false; 0243 }