File indexing completed on 2024-05-26 05:05:51

0001 /*
0002    SPDX-FileCopyrightText: 2020-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 #include "listdiscussiondelegate.h"
0007 #include "config-ruqola.h"
0008 #if USE_SIZEHINT_CACHE_SUPPORT
0009 #include "ruqola_sizehint_cache_debug.h"
0010 #endif
0011 #include <KColorScheme>
0012 #include <KLocalizedString>
0013 #include <QAbstractItemView>
0014 #include <QPainter>
0015 #include <QToolTip>
0016 
0017 #include "colorsandmessageviewstyle.h"
0018 #include "common/delegatepaintutil.h"
0019 #include "delegateutils/messagedelegateutils.h"
0020 #include "delegateutils/textselectionimpl.h"
0021 #include "misc/avatarcachemanager.h"
0022 #include "model/discussionsmodel.h"
0023 #include "rocketchataccount.h"
0024 
0025 ListDiscussionDelegate::ListDiscussionDelegate(QListView *view, RocketChatAccount *account, QObject *parent)
0026     : MessageListDelegateBase(view, parent)
0027     , mRocketChatAccount(account)
0028     , mAvatarCacheManager(new AvatarCacheManager(Utils::AvatarType::User, this))
0029 {
0030     mAvatarCacheManager->setCurrentRocketChatAccount(mRocketChatAccount);
0031 }
0032 
0033 ListDiscussionDelegate::~ListDiscussionDelegate() = default;
0034 
0035 // [date]
0036 // text
0037 // number of discussion + last date
0038 // add text for opening dicussion.
0039 void ListDiscussionDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0040 {
0041     painter->save();
0042 
0043     const Layout layout = doLayout(option, index);
0044 
0045     // Draw the pixmap
0046     if (!layout.avatarPixmap.isNull()) {
0047 #if USE_ROUNDED_RECT_PIXMAP
0048         DelegatePaintUtil::createClipRoundedRectangle(painter, QRectF(layout.avatarPos, layout.avatarPixmap.size()), layout.avatarPos, layout.avatarPixmap);
0049 #else
0050         painter->drawPixmap(layout.avatarPos, layout.avatarPixmap);
0051 #endif
0052     }
0053 
0054     // Draw the sender
0055     const QFont oldFont = painter->font();
0056     painter->setFont(layout.senderFont);
0057     painter->drawText(layout.senderRect.x(), layout.baseLine, layout.senderText);
0058     painter->setFont(oldFont);
0059 
0060     // Draw Text
0061     if (layout.textRect.isValid()) {
0062         auto *doc = documentForModelIndex(index, layout.textRect.width());
0063         if (doc) {
0064             MessageDelegateUtils::drawSelection(doc,
0065                                                 layout.textRect,
0066                                                 layout.textRect.top(),
0067                                                 painter,
0068                                                 index,
0069                                                 option,
0070                                                 mTextSelectionImpl->textSelection(),
0071                                                 {},
0072                                                 {},
0073                                                 false);
0074         }
0075     }
0076 
0077     // Draw the number of message + timestamp (below the sender)
0078     const QString messageStr = i18np("%1 message", "%1 messages", layout.numberOfMessages) + QLatin1Char(' ') + layout.lastMessageTimeText;
0079     DelegatePaintUtil::drawLighterText(painter, messageStr, QPoint(layout.textRect.left(), layout.lastMessageTimeY + painter->fontMetrics().ascent()));
0080 
0081     const QString discussionsText = i18n("Open Discussion");
0082     painter->setPen(ColorsAndMessageViewStyle::self().schemeView().foreground(KColorScheme::LinkText).color());
0083     painter->drawText(layout.textRect.x(), layout.openDiscussionTextY + painter->fontMetrics().ascent(), discussionsText);
0084 
0085     // debug
0086     // painter->drawRect(option.rect.adjusted(0, 0, -1, -1));
0087     painter->restore();
0088 }
0089 
0090 QSize ListDiscussionDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0091 {
0092 #if USE_SIZEHINT_CACHE_SUPPORT
0093     const QString identifier = cacheIdentifier(index);
0094     auto it = mSizeHintCache.find(identifier);
0095     if (it != mSizeHintCache.end()) {
0096         const QSize result = it->value;
0097         qCDebug(RUQOLA_SIZEHINT_CACHE_LOG) << "ListDiscussionDelegate: SizeHint found in cache: " << result;
0098         return result;
0099     }
0100 #endif
0101     // Note: option.rect in this method is huge (as big as the viewport)
0102     const Layout layout = doLayout(option, index);
0103 
0104     int additionalHeight = 0;
0105     // A little bit of margin below the very last item, it just looks better
0106     if (index.row() == index.model()->rowCount() - 1) {
0107         additionalHeight += 4;
0108     }
0109 
0110     // contents is date + text
0111     const int contentsHeight = layout.openDiscussionTextY + layout.textRect.height() - option.rect.y();
0112     const int senderAndAvatarHeight = qMax<int>(layout.senderRect.y() + layout.senderRect.height() - option.rect.y(),
0113                                                 layout.avatarPos.y() + MessageDelegateUtils::dprAwareSize(layout.avatarPixmap).height() - option.rect.y());
0114 
0115     //    qDebug() << "senderAndAvatarHeight" << senderAndAvatarHeight << "text" << layout.textRect.height() << "total contents" << contentsHeight;
0116     //    qDebug() << "=> returning" << qMax(senderAndAvatarHeight, contentsHeight) + additionalHeight;
0117 
0118     const QSize size = {option.rect.width(), qMax(senderAndAvatarHeight, contentsHeight) + additionalHeight};
0119 #if USE_SIZEHINT_CACHE_SUPPORT
0120     if (!size.isEmpty()) {
0121         mSizeHintCache.insert(identifier, size);
0122     }
0123 #endif
0124     return size;
0125 }
0126 
0127 bool ListDiscussionDelegate::helpEvent(QHelpEvent *helpEvent, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
0128 {
0129     if (!helpEvent || !view || !index.isValid()) {
0130         return QItemDelegate::helpEvent(helpEvent, view, option, index);
0131     }
0132 
0133     if (helpEvent->type() != QEvent::ToolTip) {
0134         return false;
0135     }
0136 
0137     const Layout layout = doLayout(option, index);
0138     const auto *doc = documentForModelIndex(index, layout.textRect.width());
0139     if (!doc) {
0140         return false;
0141     }
0142 
0143     const QPoint relativePos = adaptMousePosition(helpEvent->pos(), layout.textRect, option);
0144     QString formattedTooltip;
0145     if (MessageDelegateUtils::generateToolTip(doc, relativePos, formattedTooltip)) {
0146         QToolTip::showText(helpEvent->globalPos(), formattedTooltip, view);
0147         return true;
0148     }
0149     return true;
0150 }
0151 
0152 bool ListDiscussionDelegate::mouseEvent(QEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
0153 {
0154     const QEvent::Type eventType = event->type();
0155     if (eventType == QEvent::MouseButtonRelease) {
0156         auto mev = static_cast<QMouseEvent *>(event);
0157         const Layout layout = doLayout(option, index);
0158 
0159         const QRect discussionRect(layout.textRect.x(), layout.openDiscussionTextY, layout.textRect.width(), layout.openDiscussionTextHeight);
0160         if (discussionRect.contains(mev->pos())) {
0161             const QString discussionRoomId = index.data(DiscussionsModel::DiscussionRoomId).toString();
0162             Q_EMIT openDiscussion(discussionRoomId);
0163             return true;
0164         }
0165         if (handleMouseEvent(mev, layout.textRect, option, index)) {
0166             return true;
0167         }
0168     } else if (eventType == QEvent::MouseButtonPress || eventType == QEvent::MouseMove || eventType == QEvent::MouseButtonDblClick) {
0169         auto mev = static_cast<QMouseEvent *>(event);
0170         if (mev->buttons() & Qt::LeftButton) {
0171             const Layout layout = doLayout(option, index);
0172             if (handleMouseEvent(mev, layout.textRect, option, index)) {
0173                 return true;
0174             }
0175         }
0176     }
0177     return false;
0178 }
0179 
0180 QPoint ListDiscussionDelegate::adaptMousePosition(const QPoint &pos, QRect textRect, const QStyleOptionViewItem &option)
0181 {
0182     Q_UNUSED(option);
0183     const QPoint relativePos = pos - textRect.topLeft();
0184     return relativePos;
0185 }
0186 
0187 QPixmap ListDiscussionDelegate::makeAvatarPixmap(const QWidget *widget, const QModelIndex &index, int maxHeight) const
0188 {
0189     Utils::AvatarInfo info;
0190     info.avatarType = Utils::AvatarType::User;
0191     info.identifier = index.data(DiscussionsModel::UserName).toString();
0192 
0193     return mAvatarCacheManager->makeAvatarPixmap(widget, info, maxHeight);
0194 }
0195 
0196 ListDiscussionDelegate::Layout ListDiscussionDelegate::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const
0197 {
0198     Layout layout;
0199     const QString userName = index.data(DiscussionsModel::UserName).toString();
0200     const int margin = MessageDelegateUtils::basicMargin();
0201     layout.senderText = QLatin1Char('@') + userName;
0202     layout.senderFont = option.font;
0203     layout.senderFont.setBold(true);
0204 
0205     // Message (using the rest of the available width)
0206     const int iconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize);
0207     const QFontMetricsF senderFontMetrics(layout.senderFont);
0208     const qreal senderAscent = senderFontMetrics.ascent();
0209     const QSizeF senderTextSize = senderFontMetrics.size(Qt::TextSingleLine, layout.senderText);
0210 
0211     const QPixmap pix = makeAvatarPixmap(option.widget, index, senderTextSize.height());
0212     if (!pix.isNull()) {
0213         const QPixmap scaledPixmap = pix.scaled(senderTextSize.height(), senderTextSize.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
0214         layout.avatarPixmap = scaledPixmap;
0215     }
0216 
0217     const int senderX = option.rect.x() + MessageDelegateUtils::dprAwareSize(layout.avatarPixmap).width() + 2 * margin;
0218 
0219     const int textLeft = senderX + senderTextSize.width() + margin;
0220     const int widthAfterMessage = iconSize + margin + margin / 2;
0221     const int maxWidth = qMax(30, option.rect.width() - textLeft - widthAfterMessage);
0222 
0223     layout.baseLine = 0;
0224     const QSize textSize = textSizeHint(index, maxWidth, option, &layout.baseLine);
0225 
0226     const int textVMargin = 3; // adjust this for "compactness"
0227     QRect usableRect = option.rect;
0228     layout.textRect = QRect(textLeft, usableRect.top() + textVMargin, maxWidth, textSize.height() + textVMargin);
0229     layout.baseLine += layout.textRect.top(); // make it absolute
0230 
0231     layout.senderRect = QRectF(senderX, layout.baseLine - senderAscent, senderTextSize.width(), senderTextSize.height());
0232     // Align top of avatar with top of sender rect
0233     layout.avatarPos = QPointF(option.rect.x() + margin, layout.senderRect.y());
0234 
0235     layout.lastMessageTimeText = index.data(DiscussionsModel::LastMessage).toString();
0236     layout.lastMessageTimeY = layout.textRect.bottom();
0237 
0238     layout.numberOfMessages = index.data(DiscussionsModel::NumberOfMessages).toInt();
0239 
0240     layout.openDiscussionTextY = layout.lastMessageTimeY + option.fontMetrics.height();
0241     layout.openDiscussionTextHeight = option.fontMetrics.height();
0242 
0243     return layout;
0244 }
0245 
0246 QString ListDiscussionDelegate::cacheIdentifier(const QModelIndex &index) const
0247 {
0248     const QString discussionRoomId = index.data(DiscussionsModel::DiscussionRoomId).toString();
0249     Q_ASSERT(!discussionRoomId.isEmpty());
0250     return discussionRoomId;
0251 }
0252 
0253 QTextDocument *ListDiscussionDelegate::documentForModelIndex(const QModelIndex &index, int width) const
0254 {
0255     Q_ASSERT(index.isValid());
0256     const QString messageId = cacheIdentifier(index);
0257     const QString messageStr = index.data(DiscussionsModel::Description).toString();
0258     return documentForDelegate(mRocketChatAccount, messageId, messageStr, width);
0259 }
0260 
0261 bool ListDiscussionDelegate::maybeStartDrag(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
0262 {
0263     const Layout layout = doLayout(option, index);
0264     if (MessageListDelegateBase::maybeStartDrag(event, layout.textRect, option, index)) {
0265         return true;
0266     }
0267     return false;
0268 }
0269 
0270 RocketChatAccount *ListDiscussionDelegate::rocketChatAccount(const QModelIndex &index) const
0271 {
0272     Q_UNUSED(index);
0273     return mRocketChatAccount;
0274 }
0275 
0276 #include "moc_listdiscussiondelegate.cpp"