diff --git a/src/widgets/room/delegate/messagedelegatehelpertext.cpp b/src/widgets/room/delegate/messagedelegatehelpertext.cpp index d0524b08..f64005d5 100644 --- a/src/widgets/room/delegate/messagedelegatehelpertext.cpp +++ b/src/widgets/room/delegate/messagedelegatehelpertext.cpp @@ -1,313 +1,334 @@ /* Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "messagedelegatehelpertext.h" #include #include "rocketchataccount.h" #include "ruqola.h" #include "ruqolawidgets_debug.h" #include "textconverter.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include QString MessageDelegateHelperText::makeMessageText(const QModelIndex &index, const QWidget *widget) const { const Message *message = index.data(MessageModel::MessagePointer).value(); Q_ASSERT(message); // TODO: move MessageConvertedText implementation to Message? QString text = index.data(MessageModel::MessageConvertedText).toString(); if (mShowThreadContext) { const QString threadMessageId = message->threadMessageId(); if (!threadMessageId.isEmpty()) { auto *rcAccount = Ruqola::self()->rocketChatAccount(); const MessageModel *model = rcAccount->messageModelForRoom(message->roomId()); auto *that = const_cast(this); // Find the previous message in the same thread, to use it as context auto hasSameThread = [&](const Message &msg) { return msg.threadMessageId() == threadMessageId || msg.messageId() == threadMessageId; }; Message contextMessage = model->findLastMessageBefore(message->messageId(), hasSameThread); if (contextMessage.messageId().isEmpty()) { ThreadMessageModel *cachedModel = mMessageCache.threadMessageModel(threadMessageId); if (cachedModel) { contextMessage = cachedModel->findLastMessageBefore(message->messageId(), hasSameThread); if (contextMessage.messageId().isEmpty()) { Message *msg = mMessageCache.messageForId(threadMessageId); if (msg) { contextMessage = *msg; } else { QPersistentModelIndex persistentIndex(index); connect(&mMessageCache, &MessageCache::messageLoaded, this, [=](const QString &msgId){ if (msgId == threadMessageId) { that->updateView(widget, persistentIndex); } }); } } else { //qDebug() << "using cache, found" << contextMessage.messageId() << contextMessage.text(); } } else { QPersistentModelIndex persistentIndex(index); connect(&mMessageCache, &MessageCache::modelLoaded, this, [=](){ that->updateView(widget, persistentIndex); }); } } // Use TextConverter in case it starts with a [](URL) reply marker TextConverter textConverter(rcAccount->emojiManager()); const QString contextText = KStringHandler::rsqueeze(contextMessage.text(), 200); const QString contextString = textConverter.convertMessageText(contextText, rcAccount->userName(), {}); text.prepend(Utils::formatQuotedRichText(contextString)); } } return text; } void MessageDelegateHelperText::setClipboardSelection() { QClipboard *clipboard = QGuiApplication::clipboard(); if (mCurrentTextCursor.hasSelection() && clipboard->supportsSelection()) { const QTextDocumentFragment fragment(mCurrentTextCursor); const QString text = fragment.toPlainText(); clipboard->setText(text, QClipboard::Selection); } } void MessageDelegateHelperText::updateView(const QWidget *widget, const QModelIndex &index) { auto *view = qobject_cast(const_cast(widget)); Q_ASSERT(view); view->update(index); } static bool useItalicsForMessage(const QModelIndex &index) { const Message::MessageType messageType = index.data(MessageModel::MessageType).value(); const bool isSystemMessage = messageType == Message::System && index.data(MessageModel::SystemMessageType).toString() != QStringLiteral("jitsi_call_started"); return isSystemMessage || messageType == Message::Video || messageType == Message::Audio; } -// QTextDocument lacks a move constructor -static void fillTextDocument(const QModelIndex &index, QTextDocument &doc, const QString &text, int width) -{ - doc.setHtml(text); - doc.setTextWidth(width); - QFont font = doc.defaultFont(); - font.setItalic(useItalicsForMessage(index)); - doc.setDefaultFont(font); - QTextFrame *frame = doc.frameAt(0); - QTextFrameFormat frameFormat = frame->frameFormat(); - frameFormat.setMargin(0); - frame->setFrameFormat(frameFormat); -} - void MessageDelegateHelperText::draw(QPainter *painter, const QRect &rect, const QModelIndex &index, const QStyleOptionViewItem &option) { - const QString text = makeMessageText(index, option.widget); - - if (text.isEmpty()) { + auto *doc = documentForIndex(index, rect.width(), option.widget); + if (!doc) { return; } - // Possible optimisation: store the QTextDocument into the Message itself? - QTextDocument doc; - QTextDocument *pDoc = &doc; + QVector selections; if (index == mCurrentIndex) { - pDoc = &mCurrentDocument; // optimization, not stricly necessary QTextCharFormat selectionFormat; selectionFormat.setBackground(option.palette.brush(QPalette::Highlight)); selectionFormat.setForeground(option.palette.brush(QPalette::HighlightedText)); selections.append({mCurrentTextCursor, selectionFormat}); - } else { - fillTextDocument(index, doc, text, rect.width()); } if (useItalicsForMessage(index)) { - QTextCursor cursor(pDoc); + QTextCursor cursor(doc); cursor.select(QTextCursor::Document); QTextCharFormat format; format.setForeground(Qt::gray); //TODO use color from theme. cursor.mergeCharFormat(format); } painter->save(); painter->translate(rect.left(), rect.top()); const QRect clip(0, 0, rect.width(), rect.height()); // Same as pDoc->drawContents(painter, clip) but we also set selections QAbstractTextDocumentLayout::PaintContext ctx; ctx.selections = selections; if (clip.isValid()) { painter->setClipRect(clip); ctx.clip = clip; } - pDoc->documentLayout()->draw(painter, ctx); + doc->documentLayout()->draw(painter, ctx); painter->restore(); } QSize MessageDelegateHelperText::sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option, qreal *pBaseLine) const { Q_UNUSED(option) - const QString text = makeMessageText(index, option.widget); - if (text.isEmpty()) { + auto *doc = documentForIndex(index, maxWidth, option.widget); + if (!doc) { return QSize(); } - QTextDocument doc; - fillTextDocument(index, doc, text, maxWidth); - const QSize size(doc.idealWidth(), doc.size().height()); // do the layouting, required by lineAt(0) below + const QSize size(doc->idealWidth(), doc->size().height()); // do the layouting, required by lineAt(0) below - const QTextLine &line = doc.firstBlock().layout()->lineAt(0); + const QTextLine &line = doc->firstBlock().layout()->lineAt(0); *pBaseLine = line.y() + line.ascent(); // relative return size; } bool MessageDelegateHelperText::handleMouseEvent(QMouseEvent *mouseEvent, const QRect &messageRect, const QStyleOptionViewItem &option, const QModelIndex &index) { const QPoint pos = mouseEvent->pos() - messageRect.topLeft(); const QEvent::Type eventType = mouseEvent->type(); // Text selection switch (eventType) { case QEvent::MouseButtonPress: { if (mCurrentIndex.isValid()) { // The old index no longer has selection, repaint it updateView(option.widget, mCurrentIndex); } mCurrentIndex = index; - const QString text = makeMessageText(index, option.widget); - mCurrentDocument.clear(); - fillTextDocument(index, mCurrentDocument, text, messageRect.width()); - const int charPos = mCurrentDocument.documentLayout()->hitTest(pos, Qt::FuzzyHit); - // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection - if (charPos != -1) { - mCurrentTextCursor = QTextCursor(&mCurrentDocument); - mCurrentTextCursor.setPosition(charPos); - return true; + mCurrentDocument = documentForIndex(index, messageRect.width(), option.widget); + if (mCurrentDocument) { + const int charPos = mCurrentDocument->documentLayout()->hitTest(pos, Qt::FuzzyHit); + // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection + if (charPos != -1) { + mCurrentTextCursor = QTextCursor(mCurrentDocument); + mCurrentTextCursor.setPosition(charPos); + return true; + } + } else { + mCurrentIndex = QModelIndex(); } break; } case QEvent::MouseMove: - if (index == mCurrentIndex) { - const int charPos = mCurrentDocument.documentLayout()->hitTest(pos, Qt::FuzzyHit); + if (index == mCurrentIndex && mCurrentDocument) { + const int charPos = mCurrentDocument->documentLayout()->hitTest(pos, Qt::FuzzyHit); if (charPos != -1) { // QWidgetTextControl also has code to support dragging, isPreediting()/commitPreedit(), selectBlockOnTripleClick mCurrentTextCursor.setPosition(charPos, QTextCursor::KeepAnchor); return true; } } break; case QEvent::MouseButtonRelease: if (index == mCurrentIndex) { setClipboardSelection(); } break; case QEvent::MouseButtonDblClick: if (index == mCurrentIndex) { if (!mCurrentTextCursor.hasSelection()) { mCurrentTextCursor.select(QTextCursor::WordUnderCursor); // Interestingly the view repaints after mouse press, mouse move and mouse release // but not after double-click, so make it happen: updateView(option.widget, mCurrentIndex); setClipboardSelection(); } } break; default: break; } // Clicks on links if (eventType == QEvent::MouseButtonRelease) { - // ## we should really cache that QTextDocument... - const QString text = makeMessageText(index, option.widget); - QTextDocument doc; - fillTextDocument(index, doc, text, messageRect.width()); - - const QString link = doc.documentLayout()->anchorAt(pos); + const auto *doc = documentForIndex(index, messageRect.width(), option.widget); + if (!doc) { + return false; + } + const QString link = doc->documentLayout()->anchorAt(pos); if (!link.isEmpty()) { auto *rcAccount = Ruqola::self()->rocketChatAccount(); Q_EMIT rcAccount->openLinkRequested(link); return true; } } return false; } bool MessageDelegateHelperText::handleHelpEvent(QHelpEvent *helpEvent, QWidget *view, const QRect &messageRect, const QModelIndex &index) { if (helpEvent->type() != QEvent::ToolTip) { return false; } - // ## we should really cache that QTextDocument... - const auto text = makeMessageText(index, view); - QTextDocument doc; - fillTextDocument(index, doc, text, messageRect.width()); + const auto *doc = documentForIndex(index, messageRect.width(), view); + if (!doc) { + return false; + } const QPoint pos = helpEvent->pos() - messageRect.topLeft(); - const auto format = doc.documentLayout()->formatAt(pos); + const auto format = doc->documentLayout()->formatAt(pos); const auto tooltip = format.property(QTextFormat::TextToolTip).toString(); const auto href = format.property(QTextFormat::AnchorHref).toString(); if (tooltip.isEmpty() && (href.isEmpty() || href.startsWith(QLatin1String("ruqola:/")))) { return false; } QString formattedTooltip; QTextStream stream(&formattedTooltip); auto addLine = [&](const QString &line) { if (!line.isEmpty()) { stream << QLatin1String("

") << line << QLatin1String("

"); } }; stream << QLatin1String(""); addLine(tooltip); addLine(href); stream << QLatin1String(""); QToolTip::showText(helpEvent->globalPos(), formattedTooltip, view); return true; } void MessageDelegateHelperText::setShowThreadContext(bool b) { mShowThreadContext = b; } + +static std::unique_ptr createTextDocument(const QModelIndex &index, const QString &text, int width) +{ + std::unique_ptr doc(new QTextDocument); + doc->setHtml(text); + doc->setTextWidth(width); + QFont font = doc->defaultFont(); + font.setItalic(useItalicsForMessage(index)); + doc->setDefaultFont(font); + QTextFrame *frame = doc->frameAt(0); + QTextFrameFormat frameFormat = frame->frameFormat(); + frameFormat.setMargin(0); + frame->setFrameFormat(frameFormat); + return doc; +} + +QTextDocument * MessageDelegateHelperText::documentForIndex(const QModelIndex& index, int width, const QWidget *widget) const +{ + const Message *message = index.data(MessageModel::MessagePointer).value(); + Q_ASSERT(message); + const auto messageId = message->messageId(); + Q_ASSERT(!messageId.isEmpty()); + + auto it = mDocumentCache.find(messageId); + if (it != mDocumentCache.end()) { + auto ret = it->value.get(); + if (ret->textWidth() != width) { + ret->setTextWidth(width); + } + return ret; + } + + const QString text = makeMessageText(index, widget); + if (text.isEmpty()) { + return nullptr; + } + + auto doc = createTextDocument(index, text, width); + auto ret = doc.get(); + mDocumentCache.insert(messageId, std::move(doc)); + return ret; +} diff --git a/src/widgets/room/delegate/messagedelegatehelpertext.h b/src/widgets/room/delegate/messagedelegatehelpertext.h index 416e38aa..47d9aa03 100644 --- a/src/widgets/room/delegate/messagedelegatehelpertext.h +++ b/src/widgets/room/delegate/messagedelegatehelpertext.h @@ -1,60 +1,67 @@ /* Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MESSAGEDELEGATEHELPERTEXT_H #define MESSAGEDELEGATEHELPERTEXT_H #include #include #include #include +#include #include +#include + +#include + class QPainter; class QRect; class QModelIndex; class QMouseEvent; class QHelpEvent; class QStyleOptionViewItem; class QWidget; class MessageDelegateHelperText : public QObject { Q_OBJECT public: void draw(QPainter *painter, const QRect &rect, const QModelIndex &index, const QStyleOptionViewItem &option); QSize sizeHint(const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option, qreal *pBaseLine) const; bool handleMouseEvent(QMouseEvent *mouseEvent, const QRect &messageRect, const QStyleOptionViewItem &option, const QModelIndex &index); bool handleHelpEvent(QHelpEvent *helpEvent, QWidget *view, const QRect &messageRect, const QModelIndex &index); void setShowThreadContext(bool b); private: QString makeMessageText(const QModelIndex &index, const QWidget *widget) const; void setClipboardSelection(); void updateView(const QWidget *widget, const QModelIndex &index); + QTextDocument *documentForIndex(const QModelIndex &index, int width, const QWidget *widget) const; bool mShowThreadContext = true; QPersistentModelIndex mCurrentIndex; // during selection - QTextDocument mCurrentDocument; // during selection + QPointer mCurrentDocument = nullptr; // during selection QTextCursor mCurrentTextCursor; // during selection mutable MessageCache mMessageCache; + mutable LRUCache, 32> mDocumentCache; }; #endif // MESSAGEDELEGATEHELPERTEXT_H diff --git a/src/widgets/room/delegate/messagelistdelegate.cpp b/src/widgets/room/delegate/messagelistdelegate.cpp index 90ed628f..cddcb6d7 100644 --- a/src/widgets/room/delegate/messagelistdelegate.cpp +++ b/src/widgets/room/delegate/messagelistdelegate.cpp @@ -1,469 +1,469 @@ /* Copyright (c) 2020 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "messagelistdelegate.h" #include "messagedelegatehelperbase.h" #include "messagedelegatehelpertext.h" #include "messagedelegatehelperimage.h" #include "messagedelegatehelperfile.h" #include "messagedelegatehelperreactions.h" #include "messagedelegatehelpervideo.h" #include "messagedelegatehelpersound.h" #include "model/messagemodel.h" #include "ruqola.h" #include "ruqolawidgets_debug.h" #include "rocketchataccount.h" #include "misc/emoticonmenuwidget.h" #include "common/delegatepaintutil.h" #include #include #include #include #include #include #include #include #include #include MessageListDelegate::MessageListDelegate(QObject *parent) : QItemDelegate(parent) , mEditedIcon(QIcon::fromTheme(QStringLiteral("document-edit"))) , mRolesIcon(QIcon::fromTheme(QStringLiteral("documentinfo"))) // https://bugs.kde.org/show_bug.cgi?id=417298 added smiley-add to KF 5.68 , mAddReactionIcon(QIcon::fromTheme(QStringLiteral("smiley-add"), QIcon::fromTheme(QStringLiteral("face-smile")))) , mHelperText(new MessageDelegateHelperText) , mHelperImage(new MessageDelegateHelperImage) , mHelperFile(new MessageDelegateHelperFile) , mHelperReactions(new MessageDelegateHelperReactions) , mHelperVideo(new MessageDelegateHelperVideo) , mHelperSound(new MessageDelegateHelperSound) { } MessageListDelegate::~MessageListDelegate() { } void MessageListDelegate::setRocketChatAccount(RocketChatAccount *rcAccount) { mRocketChatAccount = rcAccount; } static qreal basicMargin() { return 8; } static QSize timeStampSize(const QString &timeStampText, const QStyleOptionViewItem &option) { // This gives incorrect results (too small bounding rect), no idea why! //const QSize timeSize = painter->fontMetrics().boundingRect(timeStampText).size(); return QSize(option.fontMetrics.horizontalAdvance(timeStampText), option.fontMetrics.height()); } QPixmap MessageListDelegate::makeAvatarPixmap(const QModelIndex &index, int maxHeight) const { const QString userId = index.data(MessageModel::UserId).toString(); const QString iconUrlStr = mRocketChatAccount->avatarUrl(userId); QPixmap pix; if (!iconUrlStr.isEmpty() && !QPixmapCache::find(iconUrlStr, &pix)) { const QUrl iconUrl(iconUrlStr); Q_ASSERT(iconUrl.isLocalFile()); if (pix.load(iconUrl.toLocalFile())) { pix = pix.scaledToHeight(maxHeight); QPixmapCache::insert(iconUrlStr, pix); } else { qCWarning(RUQOLAWIDGETS_LOG) << "Could not load" << iconUrl.toLocalFile(); } } return pix; } // [Optional date header] // [margin] [margin] [margin] [margin] [margin] [margin] [margin/2] // // // MessageListDelegate::Layout MessageListDelegate::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const { const Message *message = index.data(MessageModel::MessagePointer).value(); Q_ASSERT(message); const int iconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize); Layout layout; layout.senderText = QLatin1Char('@') + message->username(); layout.senderFont = option.font; layout.senderFont.setBold(true); const QFontMetricsF senderFontMetrics(layout.senderFont); const qreal senderAscent = senderFontMetrics.ascent(); const QSizeF senderTextSize = senderFontMetrics.size(Qt::TextSingleLine, layout.senderText); layout.avatarPixmap = makeAvatarPixmap(index, senderTextSize.height()); QRect usableRect = option.rect; if (index.data(MessageModel::DateDiffersFromPrevious).toBool()) { usableRect.setTop(usableRect.top() + option.fontMetrics.height()); } layout.usableRect = usableRect; // Just for the top, for now. The left will move later on. const qreal margin = basicMargin(); const int senderX = option.rect.x() + layout.avatarPixmap.width() + 2 * margin; int textLeft = senderX + senderTextSize.width() + margin; // Roles icon const bool hasRoles = !index.data(MessageModel::Roles).toString().isEmpty(); if (hasRoles) { textLeft += iconSize + margin; } // Edit icon const int editIconX = textLeft; if (message->wasEdited()) { textLeft += iconSize + margin; } // Timestamp layout.timeStampText = index.data(MessageModel::Timestamp).toString(); const QSize timeSize = timeStampSize(layout.timeStampText, option); // Message (using the rest of the available width) const int widthAfterMessage = iconSize + margin + timeSize.width() + margin / 2; const int maxWidth = qMax(30, option.rect.width() - textLeft - widthAfterMessage); layout.baseLine = 0; - const QSize textSize = mHelperText->sizeHint(index, maxWidth, option, &layout.baseLine); // TODO share the QTextDocument + const QSize textSize = mHelperText->sizeHint(index, maxWidth, option, &layout.baseLine); int attachmentsY; const int textVMargin = 3; // adjust this for "compactness" if (textSize.isValid()) { layout.textRect = QRect(textLeft, usableRect.top() + textVMargin, maxWidth, textSize.height() + textVMargin); attachmentsY = layout.textRect.y() + layout.textRect.height(); layout.baseLine += layout.textRect.top(); // make it absolute } else { attachmentsY = usableRect.top() + textVMargin; layout.baseLine = attachmentsY + option.fontMetrics.ascent(); } layout.usableRect.setLeft(textLeft); // Align top of sender rect so it matches the baseline of the richtext layout.senderRect = QRectF(senderX, layout.baseLine - senderAscent, senderTextSize.width(), senderTextSize.height()); // Align top of avatar with top of sender rect layout.avatarPos = QPointF(option.rect.x() + margin, layout.senderRect.y()); // Same for the roles and edit icon if (hasRoles) { layout.rolesIconRect = QRect(editIconX - iconSize - margin, layout.senderRect.y(), iconSize, iconSize); } if (message->wasEdited()) { layout.editedIconRect = QRect(editIconX, layout.senderRect.y(), iconSize, iconSize); } layout.addReactionRect = QRect(textLeft + maxWidth, layout.senderRect.y(), iconSize, iconSize); layout.timeStampPos = QPoint(option.rect.width() - timeSize.width() - margin / 2, layout.baseLine); if (!message->attachements().isEmpty()) { const MessageDelegateHelperBase *helper = attachmentsHelper(message); const QSize attachmentsSize = helper ? helper->sizeHint(index, maxWidth, option) : QSize(0, 0); layout.attachmentsRect = QRect(textLeft, attachmentsY, attachmentsSize.width(), attachmentsSize.height()); layout.reactionsY = attachmentsY + attachmentsSize.height(); } else { layout.reactionsY = attachmentsY; } layout.reactionsHeight = mHelperReactions->sizeHint(index, maxWidth, option).height(); // Replies layout.repliesY = layout.reactionsY + layout.reactionsHeight; if (message->threadCount() > 0) { layout.repliesHeight = option.fontMetrics.height(); } // Discussions if (!message->discussionRoomId().isEmpty()) { layout.discussionsHeight = option.fontMetrics.height(); } return layout; } MessageDelegateHelperBase *MessageListDelegate::attachmentsHelper(const Message *message) const { switch (message->messageType()) { case Message::Image: return mHelperImage.data(); case Message::File: return mHelperFile.data(); case Message::Video: return mHelperVideo.data(); case Message::Audio: return mHelperSound.data(); default: break; } return nullptr; } void MessageListDelegate::drawDate(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option) const { const QPen origPen = painter->pen(); const qreal margin = basicMargin(); const QString dateStr = index.data(MessageModel::Date).toString(); const QSize dateSize = option.fontMetrics.size(Qt::TextSingleLine, dateStr); const QRect dateAreaRect(option.rect.x(), option.rect.y(), option.rect.width(), dateSize.height()); // the whole row const QRect dateTextRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, Qt::AlignCenter, dateSize, dateAreaRect); painter->drawText(dateTextRect, dateStr); const int lineY = (dateAreaRect.top() + dateAreaRect.bottom()) / 2; QColor lightColor(painter->pen().color()); lightColor.setAlpha(60); painter->setPen(lightColor); painter->drawLine(dateAreaRect.left(), lineY, dateTextRect.left() - margin, lineY); painter->drawLine(dateTextRect.right() + margin, lineY, dateAreaRect.right(), lineY); painter->setPen(origPen); } void MessageListDelegate::setShowThreadContext(bool b) { mHelperText->setShowThreadContext(b); } void MessageListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); drawBackground(painter, option, index); // Draw date if it differs from the previous message if (index.data(MessageModel::DateDiffersFromPrevious).toBool()) { drawDate(painter, index, option); } const Message *message = index.data(MessageModel::MessagePointer).value(); const Layout layout = doLayout(option, index); // Timestamp DelegatePaintUtil::drawTimestamp(painter, layout.timeStampText, layout.timeStampPos); const Message::MessageType messageType = message->messageType(); const bool isSystemMessage = (messageType == Message::System) || (messageType == Message::Information); if (!isSystemMessage) { mAddReactionIcon.paint(painter, layout.addReactionRect, Qt::AlignCenter); } // Message if (layout.textRect.isValid()) { const QRect messageRect = layout.textRect; mHelperText->draw(painter, messageRect, index, option); } // Draw the pixmap painter->drawPixmap(layout.avatarPos, layout.avatarPixmap); // Draw the sender const QFont oldFont = painter->font(); painter->setFont(layout.senderFont); painter->drawText(layout.senderRect.x(), layout.baseLine, layout.senderText); painter->setFont(oldFont); // Draw the roles icon if (!index.data(MessageModel::Roles).toString().isEmpty()) { mRolesIcon.paint(painter, layout.rolesIconRect); } // Draw the edited icon if (message->wasEdited()) { mEditedIcon.paint(painter, layout.editedIconRect); } // Attachments const MessageDelegateHelperBase *helper = attachmentsHelper(message); if (helper) { helper->draw(painter, layout.attachmentsRect, index, option); } // Reactions const QRect reactionsRect(layout.usableRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight); mHelperReactions->draw(painter, reactionsRect, index, option); // Replies KColorScheme scheme; const int threadCount = message->threadCount(); if (threadCount > 0) { const QString repliesText = i18np("1 reply", "%1 replies", threadCount); painter->setPen(scheme.foreground(KColorScheme::NegativeText).color()); painter->drawText(layout.usableRect.x(), layout.repliesY + option.fontMetrics.ascent(), repliesText); } // Discussion if (!message->discussionRoomId().isEmpty()) { const QString discussionsText = (message->discussionCount() > 0) ? i18np("1 message", "%1 messages", message->discussionCount()) : i18n("No message yet"); painter->setPen(scheme.foreground(KColorScheme::LinkText).color()); painter->drawText(layout.usableRect.x(), layout.repliesY + layout.repliesHeight + option.fontMetrics.ascent(), discussionsText); // Note: pen still blue, currently relying on restore() } //drawFocus(painter, option, messageRect); // debug painter->drawRect(option.rect.adjusted(0, 0, -1, -1)); painter->restore(); } QSize MessageListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { // Note: option.rect in this method is huge (as big as the viewport) const Layout layout = doLayout(option, index); int additionalHeight = 0; // A little bit of margin below the very last item, it just looks better if (index.row() == index.model()->rowCount() - 1) { additionalHeight += 4; } // contents is date + text + attachments + reactions + replies + discussions (where all of those are optional) const int contentsHeight = layout.repliesY + layout.repliesHeight + layout.discussionsHeight - option.rect.y(); const int senderAndAvatarHeight = qMax(layout.senderRect.y() + layout.senderRect.height() - option.rect.y(), layout.avatarPos.y() + layout.avatarPixmap.height() - option.rect.y()); //qDebug() << "senderAndAvatarHeight" << senderAndAvatarHeight << "text" << layout.textRect.height() // << "attachments" << layout.attachmentsRect.height() << "reactions" << layout.reactionsHeight << "total contents" << contentsHeight; //qDebug() << "=> returning" << qMax(senderAndAvatarHeight, contentsHeight) + additionalHeight; return QSize(option.rect.width(), qMax(senderAndAvatarHeight, contentsHeight) + additionalHeight); } static void positionPopup(const QPoint &pos, QWidget *parentWindow, QWidget *popup) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) const QRect screenRect = parentWindow->screen()->availableGeometry(); #else const int screenNum = QApplication::desktop()->screenNumber(parentWindow); auto *screen = QApplication::screens().value(screenNum); Q_ASSERT(screen); const QRect screenRect = screen->availableGeometry(); #endif QRect popupRect(pos, popup->sizeHint()); if (popupRect.width() > screenRect.width()) { popupRect.setWidth(screenRect.width()); } if (popupRect.right() > screenRect.right()) { popupRect.moveRight(screenRect.right()); } if (popupRect.top() < screenRect.top()) { popupRect.moveTop(screenRect.top()); } if (popupRect.bottom() > screenRect.bottom()) { popupRect.moveBottom(screenRect.bottom()); } popup->setGeometry(popupRect); } bool MessageListDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) { const QEvent::Type eventType = event->type(); if (eventType == QEvent::MouseButtonRelease) { auto *mev = static_cast(event); const Message *message = index.data(MessageModel::MessagePointer).value(); const Layout layout = doLayout(option, index); if (layout.addReactionRect.contains(mev->pos())) { QWidget *parentWidget = const_cast(option.widget); EmoticonMenuWidget *mEmoticonMenuWidget = new EmoticonMenuWidget(parentWidget); mEmoticonMenuWidget->setWindowFlag(Qt::Popup); mEmoticonMenuWidget->setCurrentRocketChatAccount(mRocketChatAccount); positionPopup(mev->globalPos(), parentWidget, mEmoticonMenuWidget); mEmoticonMenuWidget->show(); connect(mEmoticonMenuWidget, &EmoticonMenuWidget::insertEmoticons, this, [=](const QString &id) { mRocketChatAccount->reactOnMessage(message->messageId(), id, true /*add*/); }); return true; } if (!message->reactions().isEmpty()) { const QRect reactionsRect(layout.usableRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight); if (mHelperReactions->handleMouseEvent(mev, reactionsRect, option, message)) { return true; } } if (message->threadCount() > 0) { const QRect threadRect(layout.usableRect.x(), layout.repliesY, layout.usableRect.width(), layout.repliesHeight); if (threadRect.contains(mev->pos())) { const QString threadMessagePreview = index.data(MessageModel::ThreadMessagePreview).toString(); Q_EMIT mRocketChatAccount->openThreadRequested(message->messageId(), threadMessagePreview); return true; } } if (!message->discussionRoomId().isEmpty()) { const QRect discussionRect(layout.usableRect.x(), layout.repliesY + layout.repliesHeight, layout.usableRect.width(), layout.discussionsHeight); if (discussionRect.contains(mev->pos())) { qDebug() << " Open discussion" << message->discussionRoomId(); // We need to fix rest api first mRocketChatAccount->joinDiscussion(message->discussionRoomId(), QString()); return true; } } if (mHelperText->handleMouseEvent(mev, layout.textRect, option, index)) { return true; } MessageDelegateHelperBase *helper = attachmentsHelper(message); if (helper && helper->handleMouseEvent(mev, layout.attachmentsRect, option, index)) { return true; } } else if (eventType == QEvent::MouseButtonPress || eventType == QEvent::MouseMove || eventType == QEvent::MouseButtonDblClick) { auto *mev = static_cast(event); if (mev->buttons() & Qt::LeftButton) { const Layout layout = doLayout(option, index); if (mHelperText->handleMouseEvent(mev, layout.textRect, option, index)) { return true; } } } return QItemDelegate::editorEvent(event, model, option, index); } bool MessageListDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { if (event->type() == QEvent::ToolTip) { auto *helpEvent = static_cast(event); const Message *message = index.data(MessageModel::MessagePointer).value(); if (!message) { // tooltip was requested in an empty space below the last message, nothing to do return false; } const Layout layout = doLayout(option, index); if (!message->reactions().isEmpty()) { const QRect reactionsRect(layout.usableRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight); if (mHelperReactions->handleHelpEvent(helpEvent, view, reactionsRect, option, message)) { return true; } } if (layout.rolesIconRect.contains(helpEvent->pos())) { const QString tooltip = index.data(MessageModel::Roles).toString(); QToolTip::showText(helpEvent->globalPos(), tooltip, view); return true; } if (layout.textRect.contains(helpEvent->pos()) && mHelperText->handleHelpEvent(helpEvent, view, layout.textRect, index)) { return true; } } return false; }