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"