File indexing completed on 2024-12-01 04:36:54

0001 /*
0002    SPDX-FileCopyrightText: 2022-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "notificationhistorydelegate.h"
0008 #include "accountmanager.h"
0009 #include "common/delegatepaintutil.h"
0010 #include "config-ruqola.h"
0011 #include "delegateutils/messagedelegateutils.h"
0012 #include "delegateutils/textselectionimpl.h"
0013 #include "model/notificationhistorymodel.h"
0014 #include "rocketchataccount.h"
0015 #include "ruqola.h"
0016 #if USE_SIZEHINT_CACHE_SUPPORT
0017 #include "ruqola_sizehint_cache_debug.h"
0018 #endif
0019 #include <QAbstractItemView>
0020 #include <QPainter>
0021 #include <QToolTip>
0022 
0023 NotificationHistoryDelegate::NotificationHistoryDelegate(QListView *view, QObject *parent)
0024     : MessageListDelegateBase{view, parent}
0025 {
0026 }
0027 
0028 NotificationHistoryDelegate::~NotificationHistoryDelegate() = default;
0029 
0030 NotificationHistoryDelegate::RoomAccount roomAccountInfo(const QModelIndex &index)
0031 {
0032     NotificationHistoryDelegate::RoomAccount info;
0033     const QString accountName = index.data(NotificationHistoryModel::AccountName).toString();
0034     QString channelName = index.data(NotificationHistoryModel::RoomName).toString();
0035     if (channelName.isEmpty()) {
0036         channelName = index.data(NotificationHistoryModel::SenderUserName).toString();
0037     }
0038     info.accountName = accountName;
0039     info.channelName = channelName;
0040     return info;
0041 }
0042 
0043 void NotificationHistoryDelegate::drawAccountRoomInfo(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option) const
0044 {
0045     const QPen origPen = painter->pen();
0046     const qreal margin = MessageDelegateUtils::basicMargin();
0047     const RoomAccount info = roomAccountInfo(index);
0048 
0049     const QString infoStr = QStringLiteral("%1 - %2").arg(info.accountName, info.channelName);
0050     const QSize infoSize = option.fontMetrics.size(Qt::TextSingleLine, infoStr);
0051     const QRect infoAreaRect(option.rect.x(), option.rect.y(), option.rect.width(), infoSize.height()); // the whole row
0052     const QRect infoTextRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, Qt::AlignCenter, infoSize, infoAreaRect);
0053     painter->drawText(infoTextRect, infoStr);
0054     const int lineY = (infoAreaRect.top() + infoAreaRect.bottom()) / 2;
0055     QColor lightColor(painter->pen().color());
0056     lightColor.setAlpha(60);
0057     painter->setPen(lightColor);
0058     painter->drawLine(infoAreaRect.left(), lineY, infoTextRect.left() - margin, lineY);
0059     painter->drawLine(infoTextRect.right() + margin, lineY, infoAreaRect.right(), lineY);
0060     painter->setPen(origPen);
0061 }
0062 
0063 void NotificationHistoryDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0064 {
0065     painter->save();
0066     drawBackground(painter, option, index);
0067 
0068     const Layout layout = doLayout(option, index);
0069 
0070     if (!layout.sameAccountRoomAsPreviousMessage) {
0071         drawAccountRoomInfo(painter, index, option);
0072     }
0073 
0074     // Draw the pixmap
0075     if (!layout.avatarPixmap.isNull()) {
0076 #if USE_ROUNDED_RECT_PIXMAP
0077         DelegatePaintUtil::createClipRoundedRectangle(painter, QRectF(layout.avatarPos, layout.avatarPixmap.size()), layout.avatarPos, layout.avatarPixmap);
0078 #else
0079         painter->drawPixmap(layout.avatarPos, layout.avatarPixmap);
0080 #endif
0081     }
0082 
0083     // Draw the sender
0084     const QFont oldFont = painter->font();
0085     painter->setFont(layout.senderFont);
0086     painter->drawText(layout.senderRect.x(), layout.baseLine, layout.senderText);
0087     painter->setFont(oldFont);
0088 
0089     // Draw Text
0090     if (layout.textRect.isValid()) {
0091         auto *doc = documentForModelIndex(index, layout.textRect.width());
0092         if (doc) {
0093             MessageDelegateUtils::drawSelection(doc,
0094                                                 layout.textRect,
0095                                                 layout.textRect.top(),
0096                                                 painter,
0097                                                 index,
0098                                                 option,
0099                                                 mTextSelectionImpl->textSelection(),
0100                                                 {},
0101                                                 {},
0102                                                 false);
0103         }
0104     }
0105 
0106     // Timestamp
0107     DelegatePaintUtil::drawLighterText(painter, layout.timeStampText, layout.timeStampPos);
0108 
0109     // debug (TODO remove it for release)
0110     // painter->drawRect(option.rect.adjusted(0, 0, -1, -1));
0111 
0112     painter->restore();
0113 }
0114 
0115 QSize NotificationHistoryDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0116 {
0117 #if USE_SIZEHINT_CACHE_SUPPORT
0118     const QString identifier = cacheIdentifier(index);
0119     auto it = mSizeHintCache.find(identifier);
0120     if (it != mSizeHintCache.end()) {
0121         const QSize result = it->value;
0122         qCDebug(RUQOLA_SIZEHINT_CACHE_LOG) << "NotificationHistoryDelegate: SizeHint found in cache: " << result;
0123         return result;
0124     }
0125 #endif
0126 
0127     // Note: option.rect in this method is huge (as big as the viewport)
0128     const Layout layout = doLayout(option, index);
0129     int additionalHeight = 0;
0130     // A little bit of margin below the very last item, it just looks better
0131     if (index.row() == index.model()->rowCount() - 1) {
0132         additionalHeight += 4;
0133     }
0134 
0135     // contents is date + text
0136     const int contentsHeight = layout.textRect.y() + layout.textRect.height() - option.rect.y();
0137     const int senderAndAvatarHeight = qMax<int>(layout.senderRect.y() + layout.senderRect.height() - option.rect.y(),
0138                                                 layout.avatarPos.y() + MessageDelegateUtils::dprAwareSize(layout.avatarPixmap).height() - option.rect.y());
0139 
0140     //    qDebug() << "senderAndAvatarHeight" << senderAndAvatarHeight << "text" << layout.textRect.height() << "total contents" << contentsHeight;
0141     //    qDebug() << "=> returning" << qMax(senderAndAvatarHeight, contentsHeight) + additionalHeight;
0142 
0143     const QSize size = {option.rect.width(), qMax(senderAndAvatarHeight, contentsHeight) + additionalHeight};
0144 #if USE_SIZEHINT_CACHE_SUPPORT
0145     if (!size.isEmpty()) {
0146         mSizeHintCache.insert(identifier, size);
0147     }
0148 #endif
0149     return size;
0150 }
0151 
0152 // text AccountName/room
0153 // [margin] <pixmap> [margin] <sender> [margin] <text message> [margin] <date/time> [margin]
0154 NotificationHistoryDelegate::Layout NotificationHistoryDelegate::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const
0155 {
0156     NotificationHistoryDelegate::Layout layout;
0157     const auto sameAccountRoomAsPreviousMessage = [&] {
0158         if (index.row() < 1) {
0159             return false;
0160         }
0161 
0162         const auto previousIndex = index.siblingAtRow(index.row() - 1);
0163         const RoomAccount previewInfo = roomAccountInfo(previousIndex);
0164         const RoomAccount info = roomAccountInfo(index);
0165 
0166         return previewInfo == info;
0167     }();
0168 
0169     layout.sameAccountRoomAsPreviousMessage = sameAccountRoomAsPreviousMessage;
0170 
0171     const QString userName = index.data(NotificationHistoryModel::SenderUserName).toString();
0172     const int margin = MessageDelegateUtils::basicMargin();
0173     layout.senderText = QLatin1Char('@') + userName;
0174     layout.senderFont = option.font;
0175     layout.senderFont.setBold(true);
0176 
0177     // Timestamp
0178     layout.timeStampText = index.data(NotificationHistoryModel::DateTime).toString();
0179 
0180     // Message (using the rest of the available width)
0181     const int iconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize);
0182     const QFontMetricsF senderFontMetrics(layout.senderFont);
0183     const qreal senderAscent = senderFontMetrics.ascent();
0184     const QSizeF senderTextSize = senderFontMetrics.size(Qt::TextSingleLine, layout.senderText);
0185     // Resize pixmap TODO cache ?
0186     const auto pix = index.data(NotificationHistoryModel::Pixmap).value<QPixmap>();
0187     if (!pix.isNull()) {
0188         const QPixmap scaledPixmap = pix.scaled(senderTextSize.height(), senderTextSize.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
0189         layout.avatarPixmap = scaledPixmap;
0190     }
0191 
0192     const int senderX = option.rect.x() + MessageDelegateUtils::dprAwareSize(layout.avatarPixmap).width() + 2 * margin;
0193 
0194     const int textLeft = senderX + senderTextSize.width() + margin;
0195     const QSize timeSize = MessageDelegateUtils::timeStampSize(layout.timeStampText, option);
0196     const int widthAfterMessage = iconSize + margin + timeSize.width() + margin / 2;
0197     const int maxWidth = qMax(30, option.rect.width() - textLeft - widthAfterMessage);
0198 
0199     layout.baseLine = 0;
0200     const QSize textSize = textSizeHint(index, maxWidth, option, &layout.baseLine);
0201 
0202     const int textVMargin = 3; // adjust this for "compactness"
0203     QRect usableRect = option.rect;
0204     // Add area for account/room info
0205     if (!layout.sameAccountRoomAsPreviousMessage) {
0206         usableRect.setTop(usableRect.top() + option.fontMetrics.height());
0207     }
0208 
0209     layout.textRect = QRect(textLeft, usableRect.top() + textVMargin, maxWidth, textSize.height() + textVMargin);
0210     layout.baseLine += layout.textRect.top(); // make it absolute
0211 
0212     layout.timeStampPos = QPoint(option.rect.width() - timeSize.width() - margin / 2, layout.baseLine);
0213 
0214     layout.senderRect = QRectF(senderX, layout.baseLine - senderAscent, senderTextSize.width(), senderTextSize.height());
0215     // Align top of avatar with top of sender rect
0216     layout.avatarPos = QPointF(option.rect.x() + margin, layout.senderRect.y());
0217 
0218     return layout;
0219 }
0220 
0221 QString NotificationHistoryDelegate::cacheIdentifier(const QModelIndex &index) const
0222 {
0223     const QString identifier = index.data(NotificationHistoryModel::MessageId).toString();
0224     Q_ASSERT(!identifier.isEmpty());
0225     return identifier;
0226 }
0227 
0228 QTextDocument *NotificationHistoryDelegate::documentForModelIndex(const QModelIndex &index, int width) const
0229 {
0230     Q_ASSERT(index.isValid());
0231     const QString messageId = cacheIdentifier(index);
0232     const QString messageStr = index.data(NotificationHistoryModel::MessageStr).toString();
0233     auto *rcAccount = rocketChatAccount(index);
0234     return documentForDelegate(rcAccount, messageId, messageStr, width);
0235 }
0236 
0237 bool NotificationHistoryDelegate::helpEvent(QHelpEvent *helpEvent, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
0238 {
0239     if (!helpEvent || !view || !index.isValid()) {
0240         return QItemDelegate::helpEvent(helpEvent, view, option, index);
0241     }
0242 
0243     if (helpEvent->type() != QEvent::ToolTip) {
0244         return false;
0245     }
0246 
0247     const Layout layout = doLayout(option, index);
0248     const auto *doc = documentForModelIndex(index, layout.textRect.width());
0249     if (!doc) {
0250         return false;
0251     }
0252 
0253     const QPoint helpEventPos{helpEvent->pos()};
0254     if (layout.senderRect.contains(helpEventPos)) {
0255         auto account = rocketChatAccount(index);
0256         if (account) {
0257             const QString senderName = index.data(NotificationHistoryModel::SenderName).toString();
0258             QString tooltip = senderName;
0259             if (account->useRealName() && !tooltip.isEmpty()) {
0260                 const QString senderUserName = index.data(NotificationHistoryModel::SenderUserName).toString();
0261                 tooltip = QLatin1Char('@') + senderUserName;
0262             }
0263             if (!tooltip.isEmpty()) {
0264                 QToolTip::showText(helpEvent->globalPos(), tooltip, view);
0265                 return true;
0266             }
0267         }
0268     }
0269 
0270     const QPoint relativePos = adaptMousePosition(helpEvent->pos(), layout.textRect, option);
0271     QString formattedTooltip;
0272     if (MessageDelegateUtils::generateToolTip(doc, relativePos, formattedTooltip)) {
0273         QToolTip::showText(helpEvent->globalPos(), formattedTooltip, view);
0274         return true;
0275     }
0276     return true;
0277 }
0278 
0279 QPoint NotificationHistoryDelegate::adaptMousePosition(const QPoint &pos, QRect textRect, const QStyleOptionViewItem &option)
0280 {
0281     Q_UNUSED(option);
0282     const QPoint relativePos = pos - textRect.topLeft();
0283     return relativePos;
0284 }
0285 
0286 bool NotificationHistoryDelegate::mouseEvent(QEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
0287 {
0288     const QEvent::Type eventType = event->type();
0289     if (eventType == QEvent::MouseButtonRelease) {
0290         auto mev = static_cast<QMouseEvent *>(event);
0291         const Layout layout = doLayout(option, index);
0292         if (handleMouseEvent(mev, layout.textRect, option, index)) {
0293             return true;
0294         }
0295     } else if (eventType == QEvent::MouseButtonPress || eventType == QEvent::MouseMove || eventType == QEvent::MouseButtonDblClick) {
0296         auto mev = static_cast<QMouseEvent *>(event);
0297         if (mev->buttons() & Qt::LeftButton) {
0298             const Layout layout = doLayout(option, index);
0299             if (handleMouseEvent(mev, layout.textRect, option, index)) {
0300                 return true;
0301             }
0302         }
0303     }
0304     return false;
0305 }
0306 
0307 bool NotificationHistoryDelegate::maybeStartDrag(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
0308 {
0309     const Layout layout = doLayout(option, index);
0310     if (MessageListDelegateBase::maybeStartDrag(event, layout.textRect, option, index)) {
0311         return true;
0312     }
0313     return false;
0314 }
0315 
0316 RocketChatAccount *NotificationHistoryDelegate::rocketChatAccount(const QModelIndex &index) const
0317 {
0318     const QString accountName = index.data(NotificationHistoryModel::AccountName).toString();
0319     return Ruqola::self()->accountManager()->accountFromName(accountName);
0320 }
0321 
0322 #include "moc_notificationhistorydelegate.cpp"