diff --git a/src/core/lrucache.h b/src/core/lrucache.h index 2ec11b91..b4862c14 100644 --- a/src/core/lrucache.h +++ b/src/core/lrucache.h @@ -1,102 +1,108 @@ /* Copyright (c) 2020 Milian Wolff 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 LRUCACHE_H #define LRUCACHE_H #include #include template class LRUCache { public: struct Entry { Key key; Value value; bool operator==(const Key &rhs) const { return key == rhs; } }; using Entries = std::array; using value_type = typename Entries::value_type; using size_type = typename Entries::size_type; using difference_type = typename Entries::difference_type; // only const access using reference = typename Entries::const_reference; using const_reference = typename Entries::const_reference; using pointer = typename Entries::const_pointer; using iterator = typename Entries::const_iterator; using const_iterator = typename Entries::const_iterator; std::size_t size() const { return mNumEntries; } const_iterator begin() const { return mEntries.begin(); } const_iterator end() const { return std::next(mEntries.begin(), mNumEntries); } const_iterator find(const Key &key) { // using non-const iterators here since we will re-insert when we find const auto begin = mEntries.begin(); const auto end = std::next(mEntries.begin(), mNumEntries); auto it = std::find(begin, end, key); if (it == begin || it == end) { // not found or already the last recently used one return it; } // rotate to mark entry as last recently used one std::rotate(begin, it, it + 1); return mEntries.cbegin(); } void insert(Key key, Value value) { if (mNumEntries < mEntries.size()) { // open up a new slot ++mNumEntries; } // right shift to make space at the front std::rotate(mEntries.begin(), std::next(mEntries.begin(), mNumEntries - 1), std::next(mEntries.begin(), mNumEntries)); // insert up front mEntries.front() = {std::move(key), std::move(value)}; } + void clear() + { + mNumEntries = 0; + mEntries.fill({}); + } + private: Entries mEntries; std::size_t mNumEntries = 0; }; #endif diff --git a/src/widgets/room/autotests/messagelistdelegatetest.cpp b/src/widgets/room/autotests/messagelistdelegatetest.cpp index b0c7af12..04a5a973 100644 --- a/src/widgets/room/autotests/messagelistdelegatetest.cpp +++ b/src/widgets/room/autotests/messagelistdelegatetest.cpp @@ -1,163 +1,164 @@ /* 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 "messagelistdelegatetest.h" #include "room/delegate/messagelistdelegate.h" #include "ruqola.h" #include "rocketchataccount.h" #include "messagedelegatehelperimagetest.h" #include "messages/message.h" #include "messages/messageattachment.h" #include "testdata.h" #include #include #include #include QTEST_MAIN(MessageListDelegateTest) MessageListDelegateTest::MessageListDelegateTest(QObject *parent) : QObject(parent) { initTestAccount(); const QString userId = QStringLiteral("dfaureUserId"); Ruqola::self()->rocketChatAccount()->insertAvatarUrl(userId, avatarLink()); } void MessageListDelegateTest::layoutChecks_data() { QTest::addColumn("message"); QTest::addColumn("withDateHeader"); Message message; message.setMessageId(QStringLiteral("someNonEmptyId")); message.setUserId(QStringLiteral("dfaureUserId")); message.setUsername(QStringLiteral("dfaure")); message.setTimeStamp(QDateTime(QDate(2020, 2, 1), QTime(4, 7, 15)).toMSecsSinceEpoch()); message.setMessageType(Message::NormalText); QTest::newRow("text_no_date") << message << false; QTest::newRow("text_with_date") << message << true; message.setMessageType(Message::Image); const MessageAttachment msgAttach = testAttachment(); message.setAttachements({msgAttach}); QTest::newRow("attachment_no_text_no_date") << message << false; QTest::newRow("attachment_no_text_with_date") << message << true; message.setText(QStringLiteral("The text")); QTest::newRow("attachment_with_text_no_date") << message << false; QTest::newRow("attachment_with_text_with_date") << message << true; message.setEditedByUsername(message.username()); QTest::newRow("edited_with_attachment_with_text_with_date") << message << true; // TODO tests with reactions } void MessageListDelegateTest::layoutChecks() { QFETCH(Message, message); QFETCH(bool, withDateHeader); // GIVEN a delegate and an index pointing to a message MessageListDelegate delegate; delegate.setRocketChatAccount(Ruqola::self()->rocketChatAccount()); QStyleOptionViewItem option; QWidget fakeWidget; option.widget = &fakeWidget; option.rect = QRect(100, 100, 500, 500); QStandardItemModel model; auto *item = new QStandardItem; item->setData(message.username(), MessageModel::Username); item->setData(message.userId(), MessageModel::UserId); item->setData(withDateHeader, MessageModel::DateDiffersFromPrevious); item->setData(message.displayTime(), MessageModel::Timestamp); item->setData(QVariant::fromValue(&message), MessageModel::MessagePointer); item->setData(message.text(), MessageModel::OriginalMessage); item->setData(message.text(), MessageModel::MessageConvertedText); model.setItem(0, 0, item); // Ensure it's not last, that's a special case in sizeHint auto *item2 = new QStandardItem; model.setItem(1, 0, item2); const QModelIndex index = model.index(0, 0); // WHEN calculating sizehint const QSize sizeHint = delegate.sizeHint(option, index); QVERIFY(sizeHint.isValid()); option.rect.setSize(sizeHint); // ... and redoing layout while painting const MessageListDelegate::Layout layout = delegate.doLayout(option, index); // THEN QCOMPARE(layout.senderText, QStringLiteral("@dfaure")); QCOMPARE(layout.timeStampText, QStringLiteral("04:07")); QVERIFY(option.rect.contains(layout.usableRect)); QVERIFY(option.rect.contains(layout.senderRect.toRect())); if (message.attachements().isEmpty()) { QVERIFY(layout.attachmentsRect.isNull()); } else { QVERIFY(sizeHint.height() > layout.senderRect.height() + 1); QVERIFY(option.rect.contains(layout.attachmentsRect)); } // Text if (message.text().isEmpty()) { QVERIFY(!layout.textRect.isValid()); } else { QVERIFY(option.rect.contains(layout.textRect)); QCOMPARE(layout.usableRect.left(), layout.textRect.left()); QVERIFY(layout.textRect.top() >= layout.usableRect.top()); QVERIFY(!layout.senderRect.intersects(layout.textRect)); if (!message.attachements().isEmpty()) { QCOMPARE(layout.attachmentsRect.top(), layout.textRect.y() + layout.textRect.height()); } } const int bottom = layout.usableRect.y() + layout.usableRect.height(); // Avatar - QCOMPARE(layout.avatarPixmap.height(), layout.senderRect.height()); + QCOMPARE(layout.avatarPixmap.height() / layout.avatarPixmap.devicePixelRatioF(), layout.senderRect.height()); + QCOMPARE(layout.avatarPixmap.devicePixelRatioF(), fakeWidget.devicePixelRatioF()); //qDebug() << layout.avatarPos.y() << "+" << layout.avatarPixmap.height() << "must be <=" << bottom; - QVERIFY(layout.avatarPos.y() + layout.avatarPixmap.height() <= bottom); + QVERIFY(layout.avatarPos.y() + layout.avatarPixmap.height() / layout.avatarPixmap.devicePixelRatioF() <= bottom); // Reactions if (message.reactions().isEmpty()) { QCOMPARE(layout.reactionsHeight, 0); } else { QVERIFY(layout.reactionsHeight > 15); QVERIFY(layout.reactionsY + layout.reactionsHeight <= bottom); } // Edited if (message.wasEdited()) { QVERIFY(option.rect.contains(layout.editedIconRect)); QVERIFY(!layout.editedIconRect.intersects(layout.textRect)); QVERIFY(!layout.editedIconRect.intersects(layout.senderRect.toRect())); } } diff --git a/src/widgets/room/delegate/messagelistdelegate.cpp b/src/widgets/room/delegate/messagelistdelegate.cpp index ebe4b547..e1749fd1 100644 --- a/src/widgets/room/delegate/messagelistdelegate.cpp +++ b/src/widgets/room/delegate/messagelistdelegate.cpp @@ -1,506 +1,526 @@ /* 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 +static QSizeF dprAwareSize(const QPixmap &pixmap) +{ + if (pixmap.isNull()) + return {0, 0}; // prevent division-by-zero + return pixmap.size() / pixmap.devicePixelRatioF(); +} + 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 +QPixmap MessageListDelegate::makeAvatarPixmap(const QWidget *widget, 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)) { + if (iconUrlStr.isEmpty()) { + return {}; + } + + const auto dpr = widget->devicePixelRatioF(); + if (dpr != mAvatarCache.dpr) { + mAvatarCache.dpr = dpr; + mAvatarCache.cache.clear(); + } + + auto &cache = mAvatarCache.cache; + + auto downScaled = cache.findCachedPixmap(iconUrlStr); + if (downScaled.isNull()) { const QUrl iconUrl(iconUrlStr); Q_ASSERT(iconUrl.isLocalFile()); - if (pix.load(iconUrl.toLocalFile())) { - pix = pix.scaledToHeight(maxHeight); - QPixmapCache::insert(iconUrlStr, pix); - } else { + QPixmap fullScale; + if (!fullScale.load(iconUrl.toLocalFile())) { qCWarning(RUQOLAWIDGETS_LOG) << "Could not load" << iconUrl.toLocalFile(); + return {}; } + downScaled = fullScale.scaledToHeight(maxHeight * dpr); + downScaled.setDevicePixelRatio(dpr); + cache.insertCachedPixmap(iconUrlStr, downScaled); } - return pix; + return downScaled; } // [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()); + layout.avatarPixmap = makeAvatarPixmap(option.widget, index, senderTextSize.height()); QRect usableRect = option.rect; const bool displayLastSeenMessage = index.data(MessageModel::DisplayLastSeenMessage).toBool(); if (index.data(MessageModel::DateDiffersFromPrevious).toBool()) { usableRect.setTop(usableRect.top() + option.fontMetrics.height()); } else if (displayLastSeenMessage) { layout.displayLastSeenMessageY = usableRect.top(); } 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; + const int senderX = option.rect.x() + dprAwareSize(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); 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::drawLastSeenLine(QPainter *painter, qint64 displayLastSeenY, const QStyleOptionViewItem &option) const { const QPen origPen = painter->pen(); const int lineY = displayLastSeenY; painter->setPen(Qt::red); painter->drawLine(option.rect.x(), lineY, option.rect.width(), lineY); painter->setPen(origPen); } void MessageListDelegate::drawDate(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option, bool drawLastSeenLine) 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; if (drawLastSeenLine) { painter->setPen(Qt::red); } else { 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::selectAll(const QStyleOptionViewItem &option, const QModelIndex &index) { const Layout layout = doLayout(option, index); mHelperText->selectAll(option.widget, layout.textRect, index); } QString MessageListDelegate::selectedText() const { return mHelperText->selectedText(); } bool MessageListDelegate::hasSelection() const { return mHelperText->hasSelection(); } 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); const Layout layout = doLayout(option, index); // Draw date if it differs from the previous message const bool displayLastSeenMessage = index.data(MessageModel::DisplayLastSeenMessage).toBool(); if (index.data(MessageModel::DateDiffersFromPrevious).toBool()) { drawDate(painter, index, option, displayLastSeenMessage); } else if (displayLastSeenMessage) { drawLastSeenLine(painter, layout.displayLastSeenMessageY, option); } const Message *message = index.data(MessageModel::MessagePointer).value(); // 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()) { mHelperText->draw(painter, layout.textRect, 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()); + layout.avatarPos.y() + dprAwareSize(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(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); const Message::MessageType messageType = message->messageType(); const bool isSystemMessage = (messageType == Message::System) || (messageType == Message::Information); if (layout.addReactionRect.contains(mev->pos()) && !isSystemMessage) { 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.isEmpty() ? index.data(MessageModel::MessageConvertedText).toString() : 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; } diff --git a/src/widgets/room/delegate/messagelistdelegate.h b/src/widgets/room/delegate/messagelistdelegate.h index df822c99..6744c2ed 100644 --- a/src/widgets/room/delegate/messagelistdelegate.h +++ b/src/widgets/room/delegate/messagelistdelegate.h @@ -1,134 +1,142 @@ /* 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 MESSAGELISTDELEGATE_H #define MESSAGELISTDELEGATE_H #include "libruqolawidgets_private_export.h" #include #include #include +#include "pixmapcache.h" + class RocketChatAccount; class Message; class MessageDelegateHelperBase; class MessageDelegateHelperText; class MessageDelegateHelperImage; class MessageDelegateHelperFile; class MessageDelegateHelperReactions; class MessageDelegateHelperVideo; class MessageDelegateHelperSound; class LIBRUQOLAWIDGETS_TESTS_EXPORT MessageListDelegate : public QItemDelegate { Q_OBJECT public: explicit MessageListDelegate(QObject *parent = nullptr); ~MessageListDelegate() override; void setRocketChatAccount(RocketChatAccount *rcAccount); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override; void drawDate(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option, bool drawLastSeenLine) const; void setShowThreadContext(bool b); Q_REQUIRED_RESULT bool hasSelection() const; Q_REQUIRED_RESULT QString selectedText() const; void setLastSeenAt(qint64 lastSee); void selectAll(const QStyleOptionViewItem &option, const QModelIndex &index); private: - QPixmap makeAvatarPixmap(const QModelIndex &index, int maxHeight) const; + QPixmap makeAvatarPixmap(const QWidget *widget, const QModelIndex &index, int maxHeight) const; struct Layout { // Sender QString senderText; QFont senderFont; QRectF senderRect; // Avatar pixmap QPixmap avatarPixmap; QPointF avatarPos; // Roles icon QRect rolesIconRect; // Edited icon QRect editedIconRect; // add-reaction button and timestamp QRect addReactionRect; QString timeStampText; QPoint timeStampPos; QRect usableRect; // rect for everything except the date header (at the top) and the sender (on the left) // Text message QRect textRect; qreal baseLine; // used to draw sender/timestamp // Attachments QRect attachmentsRect; // Reactions qreal reactionsY = 0; qreal reactionsHeight = 0; // Replies qreal repliesY = 0; qreal repliesHeight = 0; // Discussions qreal discussionsHeight = 0; // Last See qreal displayLastSeenMessageY = 0; }; Layout doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawLastSeenLine(QPainter *painter, qint64 displayLastSeenY, const QStyleOptionViewItem &option) const; /// @note Ownership is not transferred MessageDelegateHelperBase *attachmentsHelper(const Message *message) const; friend class MessageListDelegateTest; QIcon mEditedIcon; QIcon mRolesIcon; QIcon mAddReactionIcon; RocketChatAccount *mRocketChatAccount = nullptr; QScopedPointer mHelperText; QScopedPointer mHelperImage; QScopedPointer mHelperFile; QScopedPointer mHelperReactions; QScopedPointer mHelperVideo; QScopedPointer mHelperSound; + // DPR-dependent cache of avatars + struct AvatarCache { + qreal dpr = 0.; + PixmapCache cache; + }; + mutable AvatarCache mAvatarCache; }; #endif // MESSAGELISTDELEGATE_H diff --git a/src/widgets/room/delegate/pixmapcache.cpp b/src/widgets/room/delegate/pixmapcache.cpp index 157cae58..66352333 100644 --- a/src/widgets/room/delegate/pixmapcache.cpp +++ b/src/widgets/room/delegate/pixmapcache.cpp @@ -1,37 +1,54 @@ /* 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 "pixmapcache.h" #include "ruqolawidgets_debug.h" QPixmap PixmapCache::pixmapForLocalFile(const QString &path) { - auto it = mCachedImages.find(path); - if (it != mCachedImages.end()) { - return it->value; - } - auto pixmap = QPixmap(path); + auto pixmap = findCachedPixmap(path); + if (pixmap.isNull()) { - qCWarning(RUQOLAWIDGETS_LOG) << "Could not load" << path; - return pixmap; + pixmap = QPixmap(path); + if (pixmap.isNull()) { + qCWarning(RUQOLAWIDGETS_LOG) << "Could not load" << path; + return pixmap; + } + insertCachedPixmap(path, pixmap); } - mCachedImages.insert(path, pixmap); + return pixmap; } + +QPixmap PixmapCache::findCachedPixmap(const QString &path) +{ + auto it = mCachedImages.find(path); + return it == mCachedImages.end() ? QPixmap() : it->value; +} + +void PixmapCache::insertCachedPixmap(const QString& path, const QPixmap& pixmap) +{ + mCachedImages.insert(path, pixmap); +} + +void PixmapCache::clear() +{ + mCachedImages.clear(); +} diff --git a/src/widgets/room/delegate/pixmapcache.h b/src/widgets/room/delegate/pixmapcache.h index 840275c2..fd2d20d6 100644 --- a/src/widgets/room/delegate/pixmapcache.h +++ b/src/widgets/room/delegate/pixmapcache.h @@ -1,42 +1,44 @@ /* 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 PIXMAPCACHE_H #define PIXMAPCACHE_H #include "libruqolawidgets_private_export.h" #include #include // QPixmapCache is too small for the big images in messages, let's have our own LRU cache class LIBRUQOLAWIDGETS_TESTS_EXPORT PixmapCache { public: QPixmap pixmapForLocalFile(const QString &path); + QPixmap findCachedPixmap(const QString &path); + void insertCachedPixmap(const QString &path, const QPixmap &pixmap); + void clear(); + private: friend class PixmapCacheTest; LRUCache mCachedImages; - QPixmap findCachedPixmap(const QString &link); - void insertCachedPixmap(const QString &link, const QPixmap &pixmap); }; #endif // PIXMAPCACHE_H