File indexing completed on 2024-12-22 04:46:04

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 "messagelistcompactlayout.h"
0008 #include "delegateutils/messagedelegateutils.h"
0009 #include "model/messagesmodel.h"
0010 #include "rocketchataccount.h"
0011 #include "room/delegate/messageattachmentdelegatehelperbase.h"
0012 #include "room/delegate/messageblockdelegatehelperbase.h"
0013 #include "room/delegate/messagedelegatehelperreactions.h"
0014 #include "room/delegate/messagedelegatehelpertext.h"
0015 #include "room/delegate/messagelistdelegate.h"
0016 
0017 MessageListCompactLayout::MessageListCompactLayout(MessageListDelegate *delegate)
0018     : MessageListLayoutBase(delegate)
0019 {
0020 }
0021 
0022 MessageListCompactLayout::~MessageListCompactLayout() = default;
0023 
0024 // [Optional date header]
0025 // [margin] <pixmap> [margin] <sender> [margin] <editicon> [margin] <text message> [margin] <add reaction> [margin] <timestamp> [margin/2]
0026 //                                                                  <attachments>
0027 //                                                                  <blocks>
0028 //                                                                  <reactions>
0029 //                                                                  <N replies>
0030 MessageListLayoutBase::Layout MessageListCompactLayout::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const
0031 {
0032     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0033     Q_ASSERT(message);
0034     const int iconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize);
0035 
0036     Layout layout;
0037     generateSenderInfo(layout, message, option, index);
0038 
0039     const QFontMetricsF senderFontMetrics(layout.senderFont);
0040     const qreal senderAscent = senderFontMetrics.ascent();
0041     const QSizeF senderTextSize = senderFontMetrics.size(Qt::TextSingleLine, layout.senderText);
0042 
0043     if (mRocketChatAccount && mRocketChatAccount->displayAvatars()) {
0044         layout.avatarPixmap = mDelegate->makeAvatarPixmap(option.widget, index, senderTextSize.height());
0045     }
0046 
0047     QRect usableRect = option.rect;
0048     const bool displayLastSeenMessage = index.data(MessagesModel::DisplayLastSeenMessage).toBool();
0049     if (index.data(MessagesModel::DateDiffersFromPrevious).toBool()) {
0050         usableRect.setTop(usableRect.top() + option.fontMetrics.height());
0051     } else if (displayLastSeenMessage) {
0052         layout.displayLastSeenMessageY = usableRect.top();
0053     }
0054 
0055     layout.usableRect = usableRect; // Just for the top, for now. The left will move later on.
0056 
0057     const qreal margin = MessageDelegateUtils::basicMargin();
0058     const int senderX = option.rect.x() + MessageDelegateUtils::dprAwareSize(layout.avatarPixmap).width() + 2 * margin;
0059     int textLeft = senderX + senderTextSize.width() + margin;
0060 
0061     const qreal iconSizeMargin = iconSize + margin;
0062     // Roles icon
0063     const bool hasRoles = !index.data(MessagesModel::Roles).toString().isEmpty() && mRocketChatAccount && !mRocketChatAccount->hideRoles();
0064     if (hasRoles) {
0065         textLeft += iconSizeMargin;
0066     }
0067 
0068     // Edit icon
0069     const int editIconX = textLeft;
0070     if (message->wasEdited()) {
0071         textLeft += iconSizeMargin;
0072     }
0073 
0074     const int favoriteIconX = textLeft;
0075     // Favorite icon
0076     if (message->isStarred()) {
0077         textLeft += iconSizeMargin;
0078     }
0079 
0080     const int pinIconX = textLeft;
0081     // Pin icon
0082     if (message->isPinned()) {
0083         textLeft += iconSizeMargin;
0084     }
0085 
0086     const int followingIconX = textLeft;
0087     layout.messageIsFollowing = mRocketChatAccount && message->replies().contains(mRocketChatAccount->userId());
0088     // Following icon
0089     if (layout.messageIsFollowing) {
0090         textLeft += iconSizeMargin;
0091     }
0092 
0093     const int translatedIconX = textLeft;
0094     // translated icon
0095     if (message->isAutoTranslated()) {
0096         textLeft += iconSizeMargin;
0097     }
0098 
0099     const int showIgnoreMessageIconX = textLeft;
0100     // showIgnoreMessage icon
0101     const bool ignoreMessage = MessageDelegateUtils::showIgnoreMessages(index);
0102     if (ignoreMessage) {
0103         textLeft += iconSizeMargin;
0104     }
0105 
0106     // Timestamp
0107     layout.timeStampText = index.data(MessagesModel::Timestamp).toString();
0108     const QSize timeSize = MessageDelegateUtils::timeStampSize(layout.timeStampText, option);
0109 
0110     // Message (using the rest of the available width)
0111     const int widthAfterMessage = iconSizeMargin + timeSize.width() + margin / 2;
0112     const int maxWidth = qMax(30, option.rect.width() - textLeft - widthAfterMessage);
0113     layout.baseLine = 0;
0114     const QSize textSize = mDelegate->helperText()->sizeHint(index, maxWidth, option, &layout.baseLine);
0115     int attachmentsY = 0;
0116     const int textVMargin = 3; // adjust this for "compactness"
0117     if (textSize.isValid()) {
0118         layout.textRect = QRect(textLeft, usableRect.top() + textVMargin, maxWidth, textSize.height() + textVMargin);
0119         attachmentsY = layout.textRect.y() + layout.textRect.height();
0120         layout.baseLine += layout.textRect.top(); // make it absolute
0121     } else {
0122         attachmentsY = usableRect.top() + textVMargin;
0123         layout.baseLine = attachmentsY + option.fontMetrics.ascent();
0124     }
0125     layout.usableRect.setLeft(textLeft);
0126 
0127     // Align top of sender rect so it matches the baseline of the richtext
0128     layout.senderRect = QRectF(senderX, layout.baseLine - senderAscent, senderTextSize.width(), senderTextSize.height());
0129     // Align top of avatar with top of sender rect
0130     const double senderRectY{layout.senderRect.y()};
0131     layout.avatarPos = QPointF(option.rect.x() + margin, senderRectY);
0132     // Same for the roles and edit icon
0133     if (hasRoles) {
0134         layout.rolesIconRect = QRect(editIconX - iconSize - margin, senderRectY, iconSize, iconSize);
0135     }
0136     if (message->wasEdited()) {
0137         layout.editedIconRect = QRect(editIconX, senderRectY, iconSize, iconSize);
0138     }
0139 
0140     if (message->isStarred()) {
0141         layout.favoriteIconRect = QRect(favoriteIconX, senderRectY, iconSize, iconSize);
0142     }
0143 
0144     if (message->isPinned()) {
0145         layout.pinIconRect = QRect(pinIconX, senderRectY, iconSize, iconSize);
0146     }
0147     if (layout.messageIsFollowing) {
0148         layout.followingIconRect = QRect(followingIconX, senderRectY, iconSize, iconSize);
0149     }
0150     if (message->isAutoTranslated()) {
0151         layout.translatedIconRect = QRect(translatedIconX, senderRectY, iconSize, iconSize);
0152     }
0153 
0154     if (ignoreMessage) {
0155         layout.showIgnoredMessageIconRect = QRect(showIgnoreMessageIconX, senderRectY, iconSize, iconSize);
0156         layout.showIgnoreMessage = index.data(MessagesModel::ShowIgnoredMessage).toBool();
0157     }
0158     layout.addReactionRect = QRect(textLeft + textSize.width() + margin, senderRectY, iconSize, iconSize);
0159     layout.timeStampPos = QPoint(option.rect.width() - timeSize.width() - margin / 2, layout.baseLine);
0160     layout.timeStampRect = QRect(QPoint(layout.timeStampPos.x(), usableRect.top()), timeSize);
0161 
0162     generateAttachmentBlockAndUrlPreviewLayout(mDelegate, layout, message, attachmentsY, textLeft, maxWidth, option, index);
0163     layout.reactionsHeight = mDelegate->helperReactions()->sizeHint(index, maxWidth, option).height();
0164 
0165     // Replies
0166     layout.repliesY = layout.reactionsY + layout.reactionsHeight;
0167     if (message->threadCount() > 0) {
0168         layout.repliesHeight = option.fontMetrics.height();
0169     }
0170     // Discussions
0171     if (!message->discussionRoomId().isEmpty()) {
0172         layout.discussionsHeight = option.fontMetrics.height();
0173     }
0174 
0175     return layout;
0176 }
0177 
0178 QSize MessageListCompactLayout::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0179 {
0180     // Note: option.rect in this method is huge (as big as the viewport)
0181     const MessageListLayoutBase::Layout layout = doLayout(option, index);
0182 
0183     int additionalHeight = 0;
0184     // A little bit of margin below the very last item, it just looks better
0185     if (index.row() == index.model()->rowCount() - 1) {
0186         additionalHeight += 4;
0187     }
0188 
0189     // contents is date + text + attachments + reactions + replies + discussions (where all of those are optional)
0190     const int contentsHeight = layout.repliesY + layout.repliesHeight + layout.discussionsHeight - option.rect.y();
0191     const int senderAndAvatarHeight = qMax<int>(layout.senderRect.y() + layout.senderRect.height() - option.rect.y(),
0192                                                 layout.avatarPos.y() + MessageDelegateUtils::dprAwareSize(layout.avatarPixmap).height() - option.rect.y());
0193 
0194     // qDebug() << "senderAndAvatarHeight" << senderAndAvatarHeight << "text" << layout.textRect.height()
0195     //         << "attachments" << layout.attachmentsRect.height() << "reactions" << layout.reactionsHeight << "total contents" << contentsHeight;
0196     // qDebug() << "=> returning" << qMax(senderAndAvatarHeight, contentsHeight) + additionalHeight;
0197 
0198     const QSize size = {option.rect.width(), qMax(senderAndAvatarHeight, contentsHeight) + additionalHeight};
0199     return size;
0200 }