1 /*
2    SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
3 
4    SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "messagedelegatehelperreactions.h"
8 #include "common/delegatepaintutil.h"
9 #include "emoticons/emojimanager.h"
10 #include "emoticons/unicodeemoticon.h"
11 #include "rocketchataccount.h"
12 #include "ruqola.h"
13 #include "ruqolautils.h"
14 #include "utils.h"
15 #include <model/messagemodel.h>
16 
17 #include <QAbstractTextDocumentLayout>
18 #include <QPainter>
19 #include <QStyleOptionViewItem>
20 #include <QToolTip>
21 #include <ruqola.h>
22 
MessageDelegateHelperReactions()23 MessageDelegateHelperReactions::MessageDelegateHelperReactions()
24     : mEmojiFont(Utils::emojiFontName())
25 {
26 }
27 
28 QVector<MessageDelegateHelperReactions::ReactionLayout>
layoutReactions(const QVector<Reaction> & reactions,QRect reactionsRect,const QStyleOptionViewItem & option) const29 MessageDelegateHelperReactions::layoutReactions(const QVector<Reaction> &reactions, QRect reactionsRect, const QStyleOptionViewItem &option) const
30 {
31     QVector<ReactionLayout> layouts;
32     layouts.reserve(reactions.count());
33     auto *rcAccount = Ruqola::self()->rocketChatAccount();
34     auto *emojiManager = rcAccount->emojiManager();
35     const QFontMetricsF emojiFontMetrics(mEmojiFont);
36     const qreal smallMargin = DelegatePaintUtil::margin() / 2.0;
37     qreal x = reactionsRect.x();
38 
39     for (const Reaction &reaction : reactions) {
40         ReactionLayout layout;
41         layout.emojiString = emojiManager->unicodeEmoticonForEmoji(reaction.reactionName()).unicode();
42         qreal emojiWidth = 0;
43         if (!layout.emojiString.isEmpty()) {
44             emojiWidth = emojiFontMetrics.horizontalAdvance(layout.emojiString);
45             layout.useEmojiFont = true;
46         } else {
47             const QString fileName = emojiManager->customEmojiFileName(reaction.reactionName());
48             if (!fileName.isEmpty()) {
49                 const QUrl emojiUrl = rcAccount->attachmentUrlFromLocalCache(fileName);
50                 if (emojiUrl.isEmpty()) {
51                     // The download is happening, this will all be updated again later
52                 } else {
53                     if (!mPixmapCache.pixmapForLocalFile(emojiUrl.toLocalFile()).isNull()) {
54                         layout.emojiImagePath = emojiUrl.toLocalFile();
55                         const int iconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize);
56                         emojiWidth = iconSize;
57                     }
58                 }
59             }
60             if (layout.emojiImagePath.isEmpty()) {
61                 layout.emojiString = reaction.reactionName(); // ugly fallback: ":1md"
62                 emojiWidth = option.fontMetrics.horizontalAdvance(layout.emojiString) + smallMargin;
63             }
64             layout.useEmojiFont = false;
65         }
66         layout.countStr = QString::number(reaction.count());
67         const int countWidth = option.fontMetrics.horizontalAdvance(layout.countStr) + smallMargin;
68         // [reactionRect] = [emojiOffset (margin)] [emojiWidth] [countWidth] [margin/2]
69         layout.reactionRect = QRectF(x, reactionsRect.y(), emojiWidth + countWidth + DelegatePaintUtil::margin(), reactionsRect.height());
70         layout.emojiOffset = smallMargin + 1;
71         layout.countRect = layout.reactionRect.adjusted(layout.emojiOffset + emojiWidth, smallMargin, 0, 0);
72         layout.reaction = reaction;
73 
74         layouts.append(layout);
75         x += layout.reactionRect.width() + DelegatePaintUtil::margin();
76     }
77     return layouts;
78 }
79 
draw(QPainter * painter,QRect reactionsRect,const QModelIndex & index,const QStyleOptionViewItem & option) const80 void MessageDelegateHelperReactions::draw(QPainter *painter, QRect reactionsRect, const QModelIndex &index, const QStyleOptionViewItem &option) const
81 {
82     const Message *message = index.data(MessageModel::MessagePointer).value<Message *>();
83 
84     const QVector<Reaction> reactions = message->reactions().reactions();
85     if (reactions.isEmpty()) {
86         return;
87     }
88 
89     const QVector<ReactionLayout> layouts = layoutReactions(reactions, reactionsRect, option);
90 
91     const QPen origPen = painter->pen();
92     const QBrush origBrush = painter->brush();
93     const QPen buttonPen(option.palette.color(QPalette::Highlight).darker());
94     QColor backgroundColor = option.palette.color(QPalette::Highlight);
95     backgroundColor.setAlpha(60);
96     const QBrush buttonBrush(backgroundColor);
97     const qreal smallMargin = 4;
98     for (const ReactionLayout &reactionLayout : layouts) {
99         Q_ASSERT(!reactionLayout.emojiString.isEmpty() || !reactionLayout.emojiImagePath.isEmpty());
100         const QRectF reactionRect = reactionLayout.reactionRect;
101 
102         // Rounded rect
103         painter->setPen(buttonPen);
104         painter->setBrush(buttonBrush);
105         painter->drawRoundedRect(reactionRect.adjusted(0, 0, -1, -1), 5, 5);
106         painter->setBrush(origBrush);
107         painter->setPen(origPen);
108 
109         // Emoji
110         const QRectF r = reactionRect.adjusted(reactionLayout.emojiOffset, smallMargin, 0, 0);
111         if (!reactionLayout.emojiString.isEmpty()) {
112             if (reactionLayout.useEmojiFont) {
113                 painter->setFont(mEmojiFont);
114             }
115             painter->drawText(r, reactionLayout.emojiString);
116         } else {
117             const QPixmap pixmap = mPixmapCache.pixmapForLocalFile(reactionLayout.emojiImagePath);
118             const int maxIconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize);
119             const QPixmap scaledPixmap = pixmap.scaled(maxIconSize, maxIconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
120             painter->drawPixmap(r.x(), r.y(), scaledPixmap);
121         }
122 
123         // Count
124         painter->setFont(option.font);
125         painter->drawText(reactionLayout.countRect, reactionLayout.countStr);
126     }
127 }
128 
sizeHint(const QModelIndex & index,int maxWidth,const QStyleOptionViewItem & option) const129 QSize MessageDelegateHelperReactions::sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option) const
130 {
131     const Message *message = index.data(MessageModel::MessagePointer).value<Message *>();
132     int reactionsHeight = 0;
133     const QVector<Reaction> reactions = message->reactions().reactions();
134     if (!reactions.isEmpty()) {
135         const QFontMetrics emojiFontMetrics(mEmojiFont);
136         reactionsHeight = qMax<qreal>(emojiFontMetrics.height(), option.fontMetrics.height()) + DelegatePaintUtil::margin();
137     }
138     return {maxWidth, reactionsHeight};
139 }
140 
handleMouseEvent(QMouseEvent * mouseEvent,QRect reactionsRect,const QStyleOptionViewItem & option,const Message * message)141 bool MessageDelegateHelperReactions::handleMouseEvent(QMouseEvent *mouseEvent, QRect reactionsRect, const QStyleOptionViewItem &option, const Message *message)
142 {
143     if (mouseEvent->type() == QEvent::MouseButtonRelease) {
144         const QPoint pos = mouseEvent->pos();
145         const QVector<ReactionLayout> reactions = layoutReactions(message->reactions().reactions(), reactionsRect, option);
146         for (const ReactionLayout &reactionLayout : reactions) {
147             if (reactionLayout.reactionRect.contains(pos)) {
148                 const Reaction &reaction = reactionLayout.reaction;
149                 auto *rcAccount = Ruqola::self()->rocketChatAccount();
150                 const bool doAdd = !reaction.userNames().contains(rcAccount->userName());
151                 rcAccount->reactOnMessage(message->messageId(), reaction.reactionName(), doAdd);
152                 return true;
153             }
154         }
155     }
156     return false;
157 }
158 
handleHelpEvent(QHelpEvent * helpEvent,QWidget * view,QRect reactionsRect,const QStyleOptionViewItem & option,const Message * message)159 bool MessageDelegateHelperReactions::handleHelpEvent(QHelpEvent *helpEvent,
160                                                      QWidget *view,
161                                                      QRect reactionsRect,
162                                                      const QStyleOptionViewItem &option,
163                                                      const Message *message)
164 {
165     const QVector<ReactionLayout> reactions = layoutReactions(message->reactions().reactions(), reactionsRect, option);
166     for (const ReactionLayout &reactionLayout : reactions) {
167         if (reactionLayout.reactionRect.contains(helpEvent->pos())) {
168             const Reaction &reaction = reactionLayout.reaction;
169             const QString tooltip = reaction.convertedUsersNameAtToolTip();
170             QToolTip::showText(helpEvent->globalPos(), tooltip, view);
171             return true;
172         }
173     }
174     return false;
175 }
176