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 }