File indexing completed on 2024-05-26 05:06:05
0001 /* 0002 SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org> 0003 SPDX-FileCopyrightText: 2024 Laurent Montel <montel@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "messagelistdelegate.h" 0009 #include "colorsandmessageviewstyle.h" 0010 #include "common/delegatepaintutil.h" 0011 #include "config-ruqola.h" 0012 #include "delegateutils/messagedelegateutils.h" 0013 #include "delegateutils/textselection.h" 0014 #include "messageattachmentdelegatehelperbase.h" 0015 #include "messageattachmentdelegatehelperfile.h" 0016 #include "messageattachmentdelegatehelperimage.h" 0017 #include "messageattachmentdelegatehelpersound.h" 0018 #include "messageattachmentdelegatehelpertext.h" 0019 #include "messageattachmentdelegatehelpervideo.h" 0020 #include "messagedelegatehelperconferencevideo.h" 0021 #include "messagedelegatehelperreactions.h" 0022 #include "messagedelegatehelpertext.h" 0023 #include "misc/avatarcachemanager.h" 0024 #include "misc/emoticonmenuwidget.h" 0025 #include "model/messagesmodel.h" 0026 #include "rocketchataccount.h" 0027 #include "room/delegate/messagelistlayout/messagelistcompactlayout.h" 0028 #include "room/delegate/messagelistlayout/messagelistcozylayout.h" 0029 #include "room/delegate/messagelistlayout/messagelistnormallayout.h" 0030 #if USE_SIZEHINT_CACHE_SUPPORT 0031 #include "ruqola_sizehint_cache_debug.h" 0032 #endif 0033 #include "messagedelegatehelperurlpreview.h" 0034 #include "ruqola_thread_message_widgets_debug.h" 0035 #include "ruqolaglobalconfig.h" 0036 #include "ruqolawidgets_debug.h" 0037 0038 #include <QAbstractItemView> 0039 #include <QApplication> 0040 #include <QClipboard> 0041 #include <QListView> 0042 #include <QMouseEvent> 0043 #include <QPainter> 0044 #include <QScreen> 0045 #include <QToolTip> 0046 0047 #include <KColorScheme> 0048 #include <KLocalizedString> 0049 #include <QMenu> 0050 0051 MessageListDelegate::MessageListDelegate(RocketChatAccount *account, QListView *view) 0052 : QItemDelegate(view) 0053 , mEditedIcon(QIcon::fromTheme(QStringLiteral("document-edit"))) 0054 , mRolesIcon(QIcon::fromTheme(QStringLiteral("documentinfo"))) 0055 , mAddReactionIcon(QIcon::fromTheme(QStringLiteral("smiley-add"))) 0056 , mFavoriteIcon(QIcon::fromTheme(QStringLiteral("favorite"))) 0057 , mFollowingIcon(QIcon::fromTheme(QStringLiteral("notifications"))) 0058 , mPinIcon(QIcon::fromTheme(QStringLiteral("pin"))) 0059 , mTranslatedIcon(QIcon::fromTheme(QStringLiteral("languages"))) // TODO use another icon for it. But kde doesn't correct icon perhaps flags ? 0060 , mListView(view) 0061 , mTextSelectionImpl(new TextSelectionImpl) 0062 , mHelperText(new MessageDelegateHelperText(account, view, mTextSelectionImpl)) 0063 , mHelperAttachmentImage(new MessageAttachmentDelegateHelperImage(account, view, mTextSelectionImpl)) 0064 , mHelperAttachmentFile(new MessageAttachmentDelegateHelperFile(account, view, mTextSelectionImpl)) 0065 , mHelperReactions(new MessageDelegateHelperReactions(account)) 0066 , mHelperAttachmentVideo(new MessageAttachmentDelegateHelperVideo(account, view, mTextSelectionImpl)) 0067 , mHelperAttachmentSound(new MessageAttachmentDelegateHelperSound(account, view, mTextSelectionImpl)) 0068 , mHelperAttachmentText(new MessageAttachmentDelegateHelperText(account, view, mTextSelectionImpl)) 0069 , mHelperConferenceVideo(new MessageDelegateHelperConferenceVideo(account, view, mTextSelectionImpl)) 0070 , mHelperUrlPreview(new MessageDelegateHelperUrlPreview(account, view, mTextSelectionImpl)) 0071 , mAvatarCacheManager(new AvatarCacheManager(Utils::AvatarType::User, this)) 0072 , mMessageListLayoutBase(new MessageListCompactLayout(this)) 0073 { 0074 mTextSelectionImpl->textSelection()->setMessageUrlHelperFactory(mHelperUrlPreview.data()); 0075 mTextSelectionImpl->textSelection()->setTextHelperFactory(mHelperText.data()); 0076 mTextSelectionImpl->textSelection()->setAttachmentFactories({mHelperAttachmentImage.data(), 0077 mHelperAttachmentFile.data(), 0078 mHelperAttachmentVideo.data(), 0079 mHelperAttachmentSound.data(), 0080 mHelperAttachmentText.data()}); 0081 // Hardcode color otherwise in dark mode otherwise scheme.background(KColorScheme::NeutralBackground).color(); is not correct for text color. 0082 mEditColorMode = QColor(255, 170, 127); 0083 connect(&ColorsAndMessageViewStyle::self(), &ColorsAndMessageViewStyle::needToUpdateColors, this, &MessageListDelegate::slotUpdateColors); 0084 connect(&ColorsAndMessageViewStyle::self(), &ColorsAndMessageViewStyle::needUpdateMessageStyle, this, &MessageListDelegate::switchMessageLayout); 0085 slotUpdateColors(); 0086 mSizeHintCache.setMaxEntries(32); // Enough ? 0087 } 0088 0089 MessageListDelegate::~MessageListDelegate() 0090 { 0091 delete mTextSelectionImpl; 0092 delete mMessageListLayoutBase; 0093 } 0094 0095 void MessageListDelegate::slotUpdateColors() 0096 { 0097 const KColorScheme scheme = ColorsAndMessageViewStyle::self().schemeView(); 0098 mThreadedMessageBackgroundColor = ColorsAndMessageViewStyle::self().schemeWindow().background(KColorScheme::AlternateBackground).color(); 0099 mOpenDiscussionColorMode = scheme.foreground(KColorScheme::LinkText).color(); 0100 mReplyThreadColorMode = scheme.foreground(KColorScheme::NegativeText).color(); 0101 mHoverHightlightColor = scheme.background(KColorScheme::AlternateBackground).color(); 0102 Q_EMIT updateView(); 0103 } 0104 0105 MessageListLayoutBase::Layout MessageListDelegate::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const 0106 { 0107 return mMessageListLayoutBase->doLayout(option, index); 0108 } 0109 0110 void MessageListDelegate::setRocketChatAccount(RocketChatAccount *rcAccount) 0111 { 0112 if (mRocketChatAccount) { 0113 disconnect(mRocketChatAccount, &RocketChatAccount::ownUserPreferencesChanged, this, &MessageListDelegate::switchMessageLayout); 0114 disconnect(mRocketChatAccount, &RocketChatAccount::privateSettingsChanged, this, &MessageListDelegate::slotPrivateSettingsChanged); 0115 } 0116 mRocketChatAccount = rcAccount; 0117 connect(mRocketChatAccount, &RocketChatAccount::ownUserPreferencesChanged, this, &MessageListDelegate::switchMessageLayout); 0118 connect(mRocketChatAccount, &RocketChatAccount::privateSettingsChanged, this, &MessageListDelegate::slotPrivateSettingsChanged); 0119 0120 mAvatarCacheManager->setCurrentRocketChatAccount(mRocketChatAccount); 0121 // Switch messageLayout after set rocketchatAccount 0122 switchMessageLayout(); 0123 0124 mHelperText->setRocketChatAccount(mRocketChatAccount); 0125 mHelperAttachmentImage->setRocketChatAccount(mRocketChatAccount); 0126 mHelperAttachmentFile->setRocketChatAccount(mRocketChatAccount); 0127 mHelperReactions->setRocketChatAccount(mRocketChatAccount); 0128 mHelperAttachmentVideo->setRocketChatAccount(mRocketChatAccount); 0129 mHelperAttachmentSound->setRocketChatAccount(mRocketChatAccount); 0130 mHelperAttachmentText->setRocketChatAccount(mRocketChatAccount); 0131 mHelperConferenceVideo->setRocketChatAccount(mRocketChatAccount); 0132 mHelperUrlPreview->setRocketChatAccount(mRocketChatAccount); 0133 } 0134 0135 QPixmap MessageListDelegate::makeAvatarPixmap(const QWidget *widget, const QModelIndex &index, int maxHeight) const 0136 { 0137 const QString emojiStr = index.data(MessagesModel::Emoji).toString(); 0138 const auto info = index.data(MessagesModel::AvatarInfo).value<Utils::AvatarInfo>(); 0139 if (emojiStr.isEmpty()) { 0140 const QString avatarUrl = index.data(MessagesModel::Avatar).toString(); 0141 if (!avatarUrl.isEmpty()) { 0142 // qDebug() << " avatarUrl is not empty " << avatarUrl; 0143 return mAvatarCacheManager->makeAvatarUrlPixmap(widget, avatarUrl, maxHeight); 0144 } else { 0145 return mAvatarCacheManager->makeAvatarPixmap(widget, info, maxHeight); 0146 } 0147 } else { 0148 return mAvatarCacheManager->makeAvatarEmojiPixmap(emojiStr, widget, info, maxHeight); 0149 } 0150 } 0151 0152 MessageBlockDelegateHelperBase *MessageListDelegate::blocksHelper(const Block &block) const 0153 { 0154 switch (block.blockType()) { 0155 case Block::BlockType::Unknown: 0156 qCWarning(RUQOLAWIDGETS_LOG) << "It's an unknown block ! It's a bug for sure"; 0157 return nullptr; 0158 case Block::BlockType::VideoConf: 0159 return mHelperConferenceVideo.get(); 0160 } 0161 return nullptr; 0162 } 0163 0164 MessageAttachmentDelegateHelperBase *MessageListDelegate::attachmentsHelper(const MessageAttachment &msgAttach) const 0165 { 0166 switch (msgAttach.attachmentType()) { 0167 case MessageAttachment::Image: 0168 return mHelperAttachmentImage.data(); 0169 case MessageAttachment::File: 0170 return mHelperAttachmentFile.data(); 0171 case MessageAttachment::Video: 0172 return mHelperAttachmentVideo.data(); 0173 case MessageAttachment::Audio: 0174 return mHelperAttachmentSound.data(); 0175 case MessageAttachment::NormalText: 0176 return mHelperAttachmentText.data(); 0177 case MessageAttachment::Unknown: 0178 qCWarning(RUQOLAWIDGETS_LOG) << "It's an unknown attachment ! It's a bug for sure"; 0179 break; 0180 } 0181 return nullptr; 0182 } 0183 0184 void MessageListDelegate::setSearchText(const QString &newSearchText) 0185 { 0186 bool needClearDocumentCache = false; 0187 if (mHelperText->searchText() != newSearchText) { 0188 mHelperText->setSearchText(newSearchText); 0189 mHelperText->clearTextDocumentCache(); 0190 needClearDocumentCache = true; 0191 } 0192 if (mPreviewEmbed) { 0193 if (mHelperUrlPreview->searchText() != newSearchText) { 0194 mHelperUrlPreview->setSearchText(newSearchText); 0195 mHelperUrlPreview->clearTextDocumentCache(); 0196 needClearDocumentCache = true; 0197 } 0198 } 0199 if (mHelperAttachmentText->searchText() != newSearchText) { 0200 mHelperAttachmentText->setSearchText(newSearchText); 0201 mHelperAttachmentText->clearTextDocumentCache(); 0202 needClearDocumentCache = true; 0203 } 0204 if (mHelperAttachmentImage->searchText() != newSearchText) { 0205 mHelperAttachmentImage->setSearchText(newSearchText); 0206 mHelperAttachmentImage->clearTextDocumentCache(); 0207 needClearDocumentCache = true; 0208 } 0209 if (mHelperAttachmentFile->searchText() != newSearchText) { 0210 mHelperAttachmentFile->setSearchText(newSearchText); 0211 mHelperAttachmentFile->clearTextDocumentCache(); 0212 needClearDocumentCache = true; 0213 } 0214 if (mHelperAttachmentVideo->searchText() != newSearchText) { 0215 mHelperAttachmentVideo->setSearchText(newSearchText); 0216 mHelperAttachmentVideo->clearTextDocumentCache(); 0217 needClearDocumentCache = true; 0218 } 0219 if (mHelperAttachmentSound->searchText() != newSearchText) { 0220 mHelperAttachmentSound->setSearchText(newSearchText); 0221 mHelperAttachmentSound->clearTextDocumentCache(); 0222 needClearDocumentCache = true; 0223 } 0224 if (needClearDocumentCache) { 0225 mSizeHintCache.clear(); 0226 } 0227 } 0228 0229 void MessageListDelegate::drawLastSeenLine(QPainter *painter, qint64 displayLastSeenY, const QStyleOptionViewItem &option) const 0230 { 0231 const QPen origPen = painter->pen(); 0232 const int lineY = displayLastSeenY; 0233 painter->setPen(Qt::red); 0234 painter->drawLine(option.rect.x(), lineY, option.rect.width(), lineY); 0235 painter->setPen(origPen); 0236 } 0237 0238 MessageDelegateHelperReactions *MessageListDelegate::helperReactions() const 0239 { 0240 return mHelperReactions.data(); 0241 } 0242 0243 MessageDelegateHelperText *MessageListDelegate::helperText() const 0244 { 0245 return mHelperText.data(); 0246 } 0247 0248 void MessageListDelegate::drawModerationDate(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option, const QString &roomName) const 0249 { 0250 const QPen origPen = painter->pen(); 0251 const qreal margin = MessageDelegateUtils::basicMargin(); 0252 const QString dateAndRoomNameStr = QStringLiteral("%1 - %2").arg(index.data(MessagesModel::Date).toString(), roomName); 0253 const QSize dateSize = option.fontMetrics.size(Qt::TextSingleLine, dateAndRoomNameStr); 0254 const QRect dateAreaRect(option.rect.x(), option.rect.y(), option.rect.width(), dateSize.height()); // the whole row 0255 const QRect dateTextRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, Qt::AlignCenter, dateSize, dateAreaRect); 0256 painter->drawText(dateTextRect, dateAndRoomNameStr); 0257 const int lineY = (dateAreaRect.top() + dateAreaRect.bottom()) / 2; 0258 QColor lightColor(painter->pen().color()); 0259 lightColor.setAlpha(60); 0260 painter->setPen(lightColor); 0261 painter->drawLine(dateAreaRect.left(), lineY, dateTextRect.left() - margin, lineY); 0262 painter->drawLine(dateTextRect.right() + margin, lineY, dateAreaRect.right(), lineY); 0263 painter->setPen(origPen); 0264 } 0265 0266 void MessageListDelegate::drawDate(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option, bool drawLastSeenLine) const 0267 { 0268 const QPen origPen = painter->pen(); 0269 const qreal margin = MessageDelegateUtils::basicMargin(); 0270 const QString dateStr = index.data(MessagesModel::Date).toString(); 0271 const QSize dateSize = option.fontMetrics.size(Qt::TextSingleLine, dateStr); 0272 const QRect dateAreaRect(option.rect.x(), option.rect.y(), option.rect.width(), dateSize.height()); // the whole row 0273 const QRect dateTextRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, Qt::AlignCenter, dateSize, dateAreaRect); 0274 painter->drawText(dateTextRect, dateStr); 0275 const int lineY = (dateAreaRect.top() + dateAreaRect.bottom()) / 2; 0276 if (drawLastSeenLine) { 0277 painter->setPen(Qt::red); 0278 } else { 0279 QColor lightColor(painter->pen().color()); 0280 lightColor.setAlpha(60); 0281 painter->setPen(lightColor); 0282 } 0283 painter->drawLine(dateAreaRect.left(), lineY, dateTextRect.left() - margin, lineY); 0284 painter->drawLine(dateTextRect.right() + margin, lineY, dateAreaRect.right(), lineY); 0285 painter->setPen(origPen); 0286 } 0287 0288 void MessageListDelegate::selectAll(const QStyleOptionViewItem &option, const QModelIndex &index) 0289 { 0290 Q_UNUSED(option); 0291 mTextSelectionImpl->textSelection()->selectMessage(index); 0292 mListView->update(index); 0293 MessageDelegateUtils::setClipboardSelection(mTextSelectionImpl->textSelection()); 0294 } 0295 0296 void MessageListDelegate::removeSizeHintCache(const QString &messageId) 0297 { 0298 mSizeHintCache.remove(messageId); 0299 } 0300 0301 void MessageListDelegate::removeMessageCache(const Message *message) 0302 { 0303 const QString messageId = message->messageId(); 0304 removeSizeHintCache(messageId); 0305 mHelperText->removeMessageCache(messageId); 0306 0307 const auto attachments{message->attachments()}; 0308 for (const auto &attachment : attachments) { 0309 const QString attachmentId = attachment.attachmentId(); 0310 mHelperAttachmentImage->removeMessageCache(attachmentId); 0311 mHelperAttachmentFile->removeMessageCache(attachmentId); 0312 mHelperAttachmentVideo->removeMessageCache(attachmentId); 0313 mHelperAttachmentSound->removeMessageCache(attachmentId); 0314 mHelperAttachmentText->removeMessageCache(attachmentId); 0315 } 0316 if (mPreviewEmbed) { 0317 const auto messageUrls{message->urls()}; 0318 for (const auto &url : messageUrls) { 0319 mHelperUrlPreview->removeMessageCache(url.urlId()); 0320 } 0321 } 0322 } 0323 0324 void MessageListDelegate::clearTextDocumentCache() 0325 { 0326 mSizeHintCache.clear(); 0327 mHelperText->clearTextDocumentCache(); 0328 mHelperAttachmentImage->clearTextDocumentCache(); 0329 mHelperAttachmentFile->clearTextDocumentCache(); 0330 mHelperAttachmentVideo->clearTextDocumentCache(); 0331 mHelperAttachmentSound->clearTextDocumentCache(); 0332 mHelperAttachmentText->clearTextDocumentCache(); 0333 if (mPreviewEmbed) { 0334 mHelperUrlPreview->clearTextDocumentCache(); 0335 } 0336 } 0337 0338 void MessageListDelegate::clearSelection() 0339 { 0340 mTextSelectionImpl->textSelection()->clear(); 0341 } 0342 0343 QString MessageListDelegate::urlAt(const QStyleOptionViewItem &option, const QModelIndex &index, QPoint pos) const 0344 { 0345 const MessageListLayoutBase::Layout layout = doLayout(option, index); 0346 const auto messageRect = layout.textRect; 0347 QString url = mHelperText->urlAt(index, pos - messageRect.topLeft()); 0348 if (url.isEmpty()) { 0349 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0350 Q_ASSERT(message); 0351 const auto attachments = message->attachments(); 0352 int i = 0; 0353 for (const MessageAttachment &msgAttach : attachments) { 0354 MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(msgAttach); 0355 url = helper->urlAt(option, msgAttach, layout.attachmentsRectList.at(i), pos); 0356 if (!url.isEmpty()) { 0357 return url; 0358 } 0359 i++; 0360 } 0361 0362 if (mPreviewEmbed) { 0363 const auto urlsMessage = message->urls(); 0364 int messageUrlIndex = 0; 0365 for (const MessageUrl &messageUrl : urlsMessage) { 0366 url = mHelperUrlPreview->urlAt(option, messageUrl, layout.messageUrlsRectList.at(messageUrlIndex), pos); 0367 if (!url.isEmpty()) { 0368 return url; 0369 } 0370 messageUrlIndex++; 0371 } 0372 } 0373 } 0374 return url; 0375 } 0376 0377 bool MessageListDelegate::contextMenu(const QStyleOptionViewItem &option, const QModelIndex &index, const MessageListDelegate::MenuInfo &info) 0378 { 0379 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0380 if (!message) { 0381 return false; 0382 } 0383 0384 const MessageListLayoutBase::Layout layout = doLayout(option, index); 0385 if (layout.senderRect.contains(info.pos) && !layout.sameSenderAsPreviousMessage) { 0386 QMenu menu; 0387 auto userInfoAction = new QAction(QIcon::fromTheme(QStringLiteral("documentinfo")), i18n("User Info"), &menu); 0388 connect(userInfoAction, &QAction::triggered, this, [message, this]() { 0389 Q_EMIT showUserInfo(message->username()); 0390 }); 0391 0392 if (info.editMode) { 0393 if (info.roomType != Room::RoomType::Direct) { 0394 if (mRocketChatAccount->hasPermission(QStringLiteral("create-d"))) { 0395 menu.addSeparator(); 0396 auto startPrivateConversationAction = new QAction(i18n("Start a Private Conversation"), &menu); 0397 connect(startPrivateConversationAction, &QAction::triggered, this, [=]() { 0398 Q_EMIT startPrivateConversation(message->username()); 0399 }); 0400 menu.addAction(startPrivateConversationAction); 0401 } 0402 } 0403 } 0404 0405 menu.addSeparator(); 0406 menu.addAction(userInfoAction); 0407 menu.exec(info.globalPos); 0408 return true; 0409 } 0410 return false; 0411 } 0412 0413 void MessageListDelegate::attachmentContextMenu(const QStyleOptionViewItem &option, 0414 const QModelIndex &index, 0415 const MessageListDelegate::MenuInfo &info, 0416 QMenu *menu) 0417 { 0418 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0419 if (!message) { 0420 return; 0421 } 0422 const MessageListLayoutBase::Layout layout = doLayout(option, index); 0423 const auto attachments = message->attachments(); 0424 int i = 0; 0425 for (const MessageAttachment &msgAttach : attachments) { 0426 MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(msgAttach); 0427 if (helper->contextMenu(info.pos, info.globalPos, msgAttach, layout.attachmentsRectList.at(i), option, menu)) { 0428 return; 0429 } 0430 ++i; 0431 } 0432 } 0433 0434 QString MessageListDelegate::selectedText() const 0435 { 0436 return mTextSelectionImpl->textSelection()->selectedText(TextSelection::Format::Text); 0437 } 0438 0439 bool MessageListDelegate::hasSelection() const 0440 { 0441 return mTextSelectionImpl->textSelection()->hasSelection(); 0442 } 0443 0444 void MessageListDelegate::setShowThreadContext(bool b) 0445 { 0446 mHelperText->setShowThreadContext(b); 0447 } 0448 0449 void MessageListDelegate::setEnableEmojiMenu(bool b) 0450 { 0451 mEmojiMenuEnabled = b; 0452 } 0453 0454 void MessageListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const 0455 { 0456 painter->save(); 0457 0458 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0459 0460 const QColor goToMessageBackgroundColor{message->goToMessageBackgroundColor()}; 0461 if (goToMessageBackgroundColor.isValid() && goToMessageBackgroundColor != QColor(Qt::transparent)) { 0462 painter->fillRect(option.rect, goToMessageBackgroundColor); 0463 } else if (message->isEditingMode()) { 0464 painter->fillRect(option.rect, mEditColorMode); 0465 } else if (message->hoverHighlight() && RuqolaGlobalConfig::self()->showHoverHighlights()) { 0466 painter->fillRect(option.rect, mHoverHightlightColor); 0467 } else if (mHelperText->showThreadContext() && !message->threadMessageId().isEmpty()) { 0468 painter->fillRect(option.rect, mThreadedMessageBackgroundColor); 0469 } else { 0470 drawBackground(painter, option, index); 0471 } 0472 0473 const MessageListLayoutBase::Layout layout = doLayout(option, index); 0474 0475 // Draw date if it differs from the previous message 0476 const bool displayLastSeenMessage = index.data(MessagesModel::DisplayLastSeenMessage).toBool(); 0477 if (!message->moderationMessage().isEmpty()) { 0478 drawModerationDate(painter, index, option, message->moderationMessage().roomName()); 0479 } else if (index.data(MessagesModel::DateDiffersFromPrevious).toBool()) { 0480 drawDate(painter, index, option, displayLastSeenMessage); 0481 } else if (displayLastSeenMessage) { 0482 drawLastSeenLine(painter, layout.displayLastSeenMessageY, option); 0483 } 0484 0485 // Timestamp 0486 DelegatePaintUtil::drawLighterText(painter, layout.timeStampText, layout.timeStampPos); 0487 if (!isSystemMessage(message) && message->hoverHighlight() && mEmojiMenuEnabled) { 0488 mAddReactionIcon.paint(painter, layout.addReactionRect, Qt::AlignCenter); 0489 } 0490 0491 // Message 0492 if (layout.textRect.isValid()) { 0493 #if 0 0494 painter->save(); 0495 painter->setPen(QPen(Qt::red)); 0496 painter->drawRect(layout.textRect); 0497 painter->restore(); 0498 #endif 0499 mHelperText->draw(painter, layout.textRect, index, option); 0500 } 0501 0502 // Draw the pixmap 0503 if (mRocketChatAccount->displayAvatars() && !layout.sameSenderAsPreviousMessage) { 0504 #if USE_ROUNDED_RECT_PIXMAP 0505 DelegatePaintUtil::createClipRoundedRectangle(painter, QRectF(layout.avatarPos, layout.avatarPixmap.size()), layout.avatarPos, layout.avatarPixmap); 0506 #else 0507 painter->drawPixmap(layout.avatarPos, layout.avatarPixmap); 0508 #endif 0509 } 0510 0511 if (!layout.sameSenderAsPreviousMessage) { 0512 // Draw the sender 0513 const QFont oldFont = painter->font(); 0514 painter->setFont(layout.senderFont); 0515 painter->drawText(layout.senderRect.x(), layout.baseLine, layout.senderText); 0516 painter->setFont(oldFont); 0517 0518 // Draw the roles icon 0519 if (!index.data(MessagesModel::Roles).toString().isEmpty() && !mRocketChatAccount->hideRoles()) { 0520 mRolesIcon.paint(painter, layout.rolesIconRect); 0521 } 0522 } 0523 0524 // Draw the edited icon 0525 if (message->wasEdited()) { 0526 mEditedIcon.paint(painter, layout.editedIconRect); 0527 } 0528 // Draw the favorite icon 0529 if (message->isStarred()) { 0530 mFavoriteIcon.paint(painter, layout.favoriteIconRect); 0531 } 0532 // Draw the pin icon 0533 if (message->isPinned()) { 0534 mPinIcon.paint(painter, layout.pinIconRect); 0535 } 0536 // Draw the following icon 0537 if (layout.messageIsFollowing) { 0538 mFollowingIcon.paint(painter, layout.followingIconRect); 0539 } 0540 // Draw translated string 0541 if (message->isAutoTranslated()) { 0542 mTranslatedIcon.paint(painter, layout.translatedIconRect); 0543 } 0544 0545 if (MessageDelegateUtils::showIgnoreMessages(index)) { 0546 const QIcon hideShowIcon = QIcon::fromTheme(layout.showIgnoreMessage ? QStringLiteral("visibility") : QStringLiteral("hint")); 0547 hideShowIcon.paint(painter, layout.showIgnoredMessageIconRect); 0548 } 0549 0550 // Attachments 0551 const auto attachments = message->attachments(); 0552 int i = 0; 0553 for (const MessageAttachment &att : attachments) { 0554 const MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att); 0555 if (helper) { 0556 #if 0 0557 painter->save(); 0558 painter->setPen(QPen(Qt::green)); 0559 painter->drawRect(layout.attachmentsRectList.at(i)); 0560 painter->restore(); 0561 #endif 0562 helper->draw(att, painter, layout.attachmentsRectList.at(i), index, option); 0563 } 0564 ++i; 0565 } 0566 // Blocks 0567 0568 const auto blocks = message->blocks(); 0569 int blockIndex = 0; 0570 for (const Block &block : blocks) { 0571 const MessageBlockDelegateHelperBase *helper = blocksHelper(block); 0572 if (helper) { 0573 #if 0 0574 painter->save(); 0575 painter->setPen(QPen(Qt::red)); 0576 painter->drawRect(layout.blocksRectList.at(blockIndex)); 0577 painter->restore(); 0578 #endif 0579 helper->draw(block, painter, layout.blocksRectList.at(blockIndex), index, option); 0580 } 0581 ++blockIndex; 0582 } 0583 0584 if (mPreviewEmbed) { 0585 // Preview Url 0586 const QVector<MessageUrl> messageUrls = message->urls(); 0587 int messageUrlIndex = 0; 0588 for (const MessageUrl &messageUrl : messageUrls) { 0589 if (messageUrl.hasPreviewUrl()) { 0590 // qDebug() << "messageUrl " << messageUrl; 0591 mHelperUrlPreview.get()->draw(messageUrl, painter, layout.messageUrlsRectList.at(messageUrlIndex), index, option); 0592 } 0593 ++messageUrlIndex; 0594 } 0595 } 0596 0597 // Reactions 0598 const QRect reactionsRect(layout.senderRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight); 0599 mHelperReactions->draw(painter, reactionsRect, index, option); 0600 0601 // Replies 0602 const int threadCount = message->threadCount(); 0603 if (threadCount > 0) { 0604 const QString repliesText = i18np("1 reply", "%1 replies", threadCount); 0605 const QColor oldColor = painter->pen().color(); 0606 painter->setPen(mReplyThreadColorMode); 0607 painter->drawText(layout.usableRect.x(), layout.repliesY + option.fontMetrics.ascent(), repliesText); 0608 painter->setPen(oldColor); 0609 } 0610 // Discussion 0611 if (!message->discussionRoomId().isEmpty()) { 0612 const QColor oldColor = painter->pen().color(); 0613 const int discussionCount{message->discussionCount()}; 0614 const QString discussionsText = (discussionCount > 0) ? i18np("1 message", "%1 messages", discussionCount) : i18n("No message yet"); 0615 painter->setPen(mOpenDiscussionColorMode); 0616 painter->drawText(layout.usableRect.x(), layout.repliesY + layout.repliesHeight + option.fontMetrics.ascent(), discussionsText); 0617 // Note: pen still blue, currently relying on restore() 0618 painter->setPen(oldColor); 0619 } 0620 0621 // drawFocus(painter, option, messageRect); 0622 0623 // debug painter->drawRect(option.rect.adjusted(0, 0, -1, -1)); 0624 0625 painter->restore(); 0626 } 0627 0628 void MessageListDelegate::clearSizeHintCache() 0629 { 0630 mSizeHintCache.clear(); 0631 } 0632 0633 QString MessageListDelegate::cacheIdentifier(const QModelIndex &index) const 0634 { 0635 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0636 Q_ASSERT(message); 0637 return message->messageId(); 0638 } 0639 0640 MessageDelegateHelperUrlPreview *MessageListDelegate::helperUrlPreview() const 0641 { 0642 return mHelperUrlPreview.get(); 0643 } 0644 0645 QSize MessageListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const 0646 { 0647 #if USE_SIZEHINT_CACHE_SUPPORT 0648 const QString identifier = cacheIdentifier(index); 0649 auto it = mSizeHintCache.find(identifier); 0650 if (it != mSizeHintCache.end()) { 0651 const QSize result = it->value; 0652 qCDebug(RUQOLA_SIZEHINT_CACHE_LOG) << "MessageListDelegate: SizeHint found in cache: " << result; 0653 return result; 0654 } 0655 #endif 0656 0657 const QSize size = mMessageListLayoutBase->sizeHint(option, index); 0658 #if USE_SIZEHINT_CACHE_SUPPORT 0659 if (!size.isEmpty()) { 0660 mSizeHintCache.insert(identifier, size); 0661 } 0662 #endif 0663 return size; 0664 } 0665 0666 static void positionPopup(QPoint pos, QWidget *parentWindow, QWidget *popup) 0667 { 0668 const QRect screenRect = parentWindow->screen()->availableGeometry(); 0669 0670 const QSize popupSize{popup->sizeHint()}; 0671 QRect popupRect(QPoint(pos.x() - popupSize.width(), pos.y() - popupSize.height()), popup->sizeHint()); 0672 if (popupRect.top() < screenRect.top()) { 0673 popupRect.moveTop(screenRect.top()); 0674 } 0675 0676 if ((pos.x() + popupSize.width()) > (screenRect.x() + screenRect.width())) { 0677 popupRect.setX(screenRect.x() + screenRect.width() - popupSize.width()); 0678 } 0679 if (pos.x() - popupSize.width() < screenRect.x()) { 0680 popupRect.setX(screenRect.x()); 0681 } 0682 0683 popup->setGeometry(popupRect); 0684 } 0685 0686 bool MessageListDelegate::isSystemMessage(const Message *message) const 0687 { 0688 const Message::MessageType messageType = message->messageType(); 0689 const bool isSystemMessage = (messageType == Message::System) || (messageType == Message::Information) || (messageType == Message::VideoConference); 0690 return isSystemMessage; 0691 } 0692 0693 bool MessageListDelegate::mouseEvent(QEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index) 0694 { 0695 const QEvent::Type eventType = event->type(); 0696 if (eventType == QEvent::MouseButtonRelease) { 0697 auto mev = static_cast<QMouseEvent *>(event); 0698 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0699 0700 const MessageListLayoutBase::Layout layout = doLayout(option, index); 0701 0702 if (layout.addReactionRect.contains(mev->pos()) && !isSystemMessage(message) && mEmojiMenuEnabled) { 0703 auto mEmoticonMenuWidget = new EmoticonMenuWidget(mListView); 0704 mEmoticonMenuWidget->setWindowFlag(Qt::Popup); 0705 mEmoticonMenuWidget->setCurrentRocketChatAccount(mRocketChatAccount); 0706 mEmoticonMenuWidget->forceLineEditFocus(); 0707 positionPopup(mev->globalPosition().toPoint(), mListView, mEmoticonMenuWidget); 0708 mEmoticonMenuWidget->show(); 0709 connect(mEmoticonMenuWidget, &EmoticonMenuWidget::insertEmojiIdentifier, this, [=](const QString &id) { 0710 mRocketChatAccount->reactOnMessage(message->messageId(), id, true /*add*/); 0711 }); 0712 return true; 0713 } 0714 0715 if (!message->reactions().isEmpty()) { 0716 const QRect reactionsRect(layout.senderRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight); 0717 if (mHelperReactions->handleMouseEvent(mev, reactionsRect, option, message)) { 0718 return true; 0719 } 0720 } 0721 if (message->threadCount() > 0) { 0722 qCDebug(RUQOLA_THREAD_MESSAGE_WIDGETS_LOG) << "Click on thread area"; 0723 const QRect threadRect(layout.usableRect.x(), layout.repliesY, layout.usableRect.width(), layout.repliesHeight); 0724 if (threadRect.contains(mev->pos())) { 0725 const QString threadMessagePreview = index.data(MessagesModel::ThreadMessagePreview).toString(); 0726 qCDebug(RUQOLA_THREAD_MESSAGE_WIDGETS_LOG) << "Click on thread area: " << message->messageId(); 0727 const bool threadIsFollowing = message->replies().contains(mRocketChatAccount->userId()); 0728 // We show current => use this message 0729 const Message threadMessage = *message; 0730 Q_EMIT mRocketChatAccount->openThreadRequested(message->messageId(), 0731 threadMessagePreview.isEmpty() ? index.data(MessagesModel::MessageConvertedText).toString() 0732 : threadMessagePreview, 0733 threadIsFollowing, 0734 threadMessage); 0735 return true; 0736 } 0737 } 0738 if (!message->discussionRoomId().isEmpty()) { 0739 const QRect discussionRect(layout.usableRect.x(), layout.repliesY + layout.repliesHeight, layout.usableRect.width(), layout.discussionsHeight); 0740 if (discussionRect.contains(mev->pos())) { 0741 // We need to fix rest api first 0742 mRocketChatAccount->joinDiscussion(message->discussionRoomId(), QString()); 0743 return true; 0744 } 0745 } 0746 if (MessageDelegateUtils::showIgnoreMessages(index)) { 0747 if (layout.showIgnoredMessageIconRect.contains(mev->pos())) { 0748 mHelperText->removeMessageCache(message->messageId()); 0749 auto model = const_cast<QAbstractItemModel *>(index.model()); 0750 model->setData(index, !layout.showIgnoreMessage, MessagesModel::ShowIgnoredMessage); 0751 return true; 0752 } 0753 } 0754 if (mHelperText->handleMouseEvent(mev, layout.textRect, option, index)) { 0755 return true; 0756 } 0757 0758 const auto attachments = message->attachments(); 0759 int i = 0; 0760 for (const MessageAttachment &att : attachments) { 0761 MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att); 0762 if (helper && helper->handleMouseEvent(att, mev, layout.attachmentsRectList.at(i), option, index)) { 0763 return true; 0764 } 0765 ++i; 0766 } 0767 0768 const auto blocks = message->blocks(); 0769 int blockIndex = 0; 0770 for (const Block &block : blocks) { 0771 MessageBlockDelegateHelperBase *helper = blocksHelper(block); 0772 if (helper && helper->handleMouseEvent(block, mev, layout.blocksRectList.at(blockIndex), option, index)) { 0773 return true; 0774 } 0775 ++blockIndex; 0776 } 0777 if (mPreviewEmbed) { 0778 const auto messageUrls = message->urls(); 0779 int messageUrlsIndex = 0; 0780 for (const MessageUrl &url : messageUrls) { 0781 if (mHelperUrlPreview->handleMouseEvent(url, mev, layout.messageUrlsRectList.at(messageUrlsIndex), option, index)) { 0782 return true; 0783 } 0784 ++messageUrlsIndex; 0785 } 0786 } 0787 } else if (eventType == QEvent::MouseButtonPress || eventType == QEvent::MouseMove || eventType == QEvent::MouseButtonDblClick) { 0788 auto mev = static_cast<QMouseEvent *>(event); 0789 if (mev->buttons() & Qt::LeftButton) { 0790 const MessageListLayoutBase::Layout layout = doLayout(option, index); 0791 if (mHelperText->handleMouseEvent(mev, layout.textRect, option, index)) { 0792 return true; 0793 } 0794 0795 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0796 const auto attachments = message->attachments(); 0797 int i = 0; 0798 for (const MessageAttachment &att : attachments) { 0799 MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att); 0800 if (helper && helper->handleMouseEvent(att, mev, layout.attachmentsRectList.at(i), option, index)) { 0801 return true; 0802 } 0803 ++i; 0804 } 0805 if (mPreviewEmbed) { 0806 const auto messageUrls = message->urls(); 0807 int messageUrlsIndex = 0; 0808 for (const MessageUrl &url : messageUrls) { 0809 if (mHelperUrlPreview->handleMouseEvent(url, mev, layout.messageUrlsRectList.at(messageUrlsIndex), option, index)) { 0810 return true; 0811 } 0812 ++messageUrlsIndex; 0813 } 0814 } 0815 } 0816 } 0817 return false; 0818 } 0819 0820 bool MessageListDelegate::maybeStartDrag(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index) 0821 { 0822 const MessageListLayoutBase::Layout layout = doLayout(option, index); 0823 if (mHelperText->maybeStartDrag(event, layout.textRect, option, index)) { 0824 return true; 0825 } 0826 0827 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0828 { 0829 const auto attachments = message->attachments(); 0830 int i = 0; 0831 for (const MessageAttachment &att : attachments) { 0832 MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att); 0833 if (helper && helper->maybeStartDrag(att, event, layout.attachmentsRectList.at(i), option, index)) { 0834 return true; 0835 } 0836 ++i; 0837 } 0838 } 0839 { 0840 if (mPreviewEmbed) { 0841 const auto urls = message->urls(); 0842 int i = 0; 0843 for (const MessageUrl &url : urls) { 0844 if (mHelperUrlPreview->maybeStartDrag(url, event, layout.messageUrlsRectList.at(i), option, index)) { 0845 return true; 0846 } 0847 ++i; 0848 } 0849 } 0850 } 0851 0852 return false; 0853 } 0854 0855 bool MessageListDelegate::helpEvent(QHelpEvent *helpEvent, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) 0856 { 0857 if (helpEvent->type() == QEvent::ToolTip) { 0858 const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>(); 0859 if (!message) { 0860 // tooltip was requested in an empty space below the last message, nothing to do 0861 return false; 0862 } 0863 0864 const MessageListLayoutBase::Layout layout = doLayout(option, index); 0865 if (!message->reactions().isEmpty()) { 0866 const QRect reactionsRect(layout.senderRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight); 0867 if (mHelperReactions->handleHelpEvent(helpEvent, view, reactionsRect, option, message)) { 0868 return true; 0869 } 0870 } 0871 const QPoint helpEventPos{helpEvent->pos()}; 0872 if (layout.senderRect.contains(helpEventPos)) { 0873 QString tooltip = message->name(); 0874 0875 if (mRocketChatAccount && mRocketChatAccount->useRealName() && !tooltip.isEmpty()) { 0876 tooltip = QLatin1Char('@') + message->username(); 0877 } 0878 0879 if (!tooltip.isEmpty()) { 0880 QToolTip::showText(helpEvent->globalPos(), tooltip, view); 0881 return true; 0882 } 0883 } 0884 if (layout.rolesIconRect.contains(helpEventPos)) { 0885 const QString tooltip = index.data(MessagesModel::Roles).toString(); 0886 QToolTip::showText(helpEvent->globalPos(), tooltip, view); 0887 return true; 0888 } 0889 if (layout.editedIconRect.contains(helpEventPos)) { 0890 const QString tooltip = index.data(MessagesModel::EditedToolTip).toString(); 0891 QToolTip::showText(helpEvent->globalPos(), tooltip, view); 0892 return true; 0893 } 0894 if (layout.followingIconRect.contains(helpEventPos)) { 0895 QToolTip::showText(helpEvent->globalPos(), i18n("Following"), view); 0896 return true; 0897 } 0898 if (layout.pinIconRect.contains(helpEventPos)) { 0899 QToolTip::showText(helpEvent->globalPos(), i18n("Message has been pinned"), view); 0900 return true; 0901 } 0902 if (layout.favoriteIconRect.contains(helpEventPos)) { 0903 QToolTip::showText(helpEvent->globalPos(), i18n("Message has been starred"), view); 0904 return true; 0905 } 0906 if (layout.textRect.contains(helpEvent->pos()) && mHelperText->handleHelpEvent(helpEvent, layout.textRect, index)) { 0907 return true; 0908 } 0909 // Attachments 0910 const auto attachments = message->attachments(); 0911 int i = 0; 0912 for (const MessageAttachment &att : attachments) { 0913 MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att); 0914 if (helper) { 0915 if (layout.attachmentsRectList.at(i).contains(helpEventPos) 0916 && helper->handleHelpEvent(helpEvent, layout.attachmentsRectList.at(i), att, option)) { 0917 return true; 0918 } 0919 } 0920 ++i; 0921 } 0922 0923 // Block 0924 const auto blocks = message->blocks(); 0925 int blockIndex = 0; 0926 for (const Block &block : blocks) { 0927 MessageBlockDelegateHelperBase *helper = blocksHelper(block); 0928 if (helper) { 0929 if (layout.blocksRectList.at(blockIndex).contains(helpEventPos) 0930 && helper->handleHelpEvent(helpEvent, layout.blocksRectList.at(blockIndex), block, option)) { 0931 return true; 0932 } 0933 } 0934 ++blockIndex; 0935 } 0936 0937 if (mPreviewEmbed) { 0938 // messageurls 0939 const auto messageUrls = message->urls(); 0940 int messageUrlsIndex = 0; 0941 for (const MessageUrl &url : messageUrls) { 0942 if (layout.messageUrlsRectList.at(messageUrlsIndex).contains(helpEventPos) 0943 && mHelperUrlPreview->handleHelpEvent(helpEvent, layout.messageUrlsRectList.at(messageUrlsIndex), url, option)) { 0944 return true; 0945 } 0946 ++messageUrlsIndex; 0947 } 0948 } 0949 if (layout.timeStampRect.contains(helpEvent->pos())) { 0950 const QString dateStr = index.data(MessagesModel::Date).toString(); 0951 QToolTip::showText(helpEvent->globalPos(), dateStr, view); 0952 return true; 0953 } 0954 } 0955 return false; 0956 } 0957 0958 void MessageListDelegate::slotPrivateSettingsChanged() 0959 { 0960 mPreviewEmbed = mRocketChatAccount ? mRocketChatAccount->previewEmbed() : true; 0961 } 0962 0963 void MessageListDelegate::switchMessageLayout() 0964 { 0965 clearSizeHintCache(); 0966 mAvatarCacheManager->clearCache(); 0967 delete mMessageListLayoutBase; 0968 switch (RuqolaGlobalConfig::self()->messageStyle()) { 0969 case RuqolaGlobalConfig::EnumMessageStyle::Normal: 0970 mMessageListLayoutBase = new MessageListNormalLayout(this); 0971 break; 0972 case RuqolaGlobalConfig::EnumMessageStyle::Cozy: 0973 mMessageListLayoutBase = new MessageListCozyLayout(this); 0974 break; 0975 case RuqolaGlobalConfig::EnumMessageStyle::Compact: 0976 mMessageListLayoutBase = new MessageListCompactLayout(this); 0977 break; 0978 default: 0979 qCWarning(RUQOLAWIDGETS_LOG) << "Invalid Message Layout type " << RuqolaGlobalConfig::self()->messageStyle(); 0980 mMessageListLayoutBase = new MessageListCompactLayout(this); 0981 break; 0982 } 0983 mMessageListLayoutBase->setRocketChatAccount(mRocketChatAccount); 0984 mPreviewEmbed = mRocketChatAccount ? mRocketChatAccount->previewEmbed() : true; 0985 Q_EMIT updateView(); 0986 } 0987 0988 #include "moc_messagelistdelegate.cpp"