File indexing completed on 2024-06-09 04:59:24

0001 /*
0002    SPDX-FileCopyrightText: 2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "messagedelegatehelperurlpreview.h"
0008 
0009 #include "common/delegatepaintutil.h"
0010 #include "delegateutils/messagedelegateutils.h"
0011 #include "messages/messageurl.h"
0012 #include "rocketchataccount.h"
0013 #include "ruqolawidgets_selection_debug.h"
0014 
0015 #include <KLocalizedString>
0016 
0017 #include <QDrag>
0018 #include <QListView>
0019 #include <QMimeData>
0020 #include <QPainter>
0021 #include <QStyleOptionViewItem>
0022 #include <QToolTip>
0023 
0024 MessageDelegateHelperUrlPreview::MessageDelegateHelperUrlPreview(RocketChatAccount *account, QListView *view, TextSelectionImpl *textSelectionImpl)
0025     : MessageDelegateHelperBase(account, view, textSelectionImpl)
0026 {
0027 }
0028 
0029 MessageDelegateHelperUrlPreview::~MessageDelegateHelperUrlPreview() = default;
0030 
0031 void MessageDelegateHelperUrlPreview::draw(const MessageUrl &messageUrl,
0032                                            QPainter *painter,
0033                                            QRect previewRect,
0034                                            const QModelIndex &index,
0035                                            const QStyleOptionViewItem &option) const
0036 {
0037     const PreviewLayout layout = layoutPreview(messageUrl, option, previewRect.width(), previewRect.height());
0038     const QFont oldFont = painter->font();
0039     const QPen origPen = painter->pen();
0040     QColor lightColor(painter->pen().color());
0041     lightColor.setAlpha(60);
0042     painter->setPen(lightColor);
0043     QFont italicFont = oldFont;
0044     italicFont.setItalic(true);
0045     // italicFont.setBold(true);
0046     painter->setFont(italicFont);
0047     painter->drawText(previewRect.x(), previewRect.y() + option.fontMetrics.ascent(), layout.previewTitle);
0048     painter->setFont(oldFont);
0049     painter->setPen(origPen);
0050 
0051     const QIcon hideShowIcon = QIcon::fromTheme(layout.isShown ? QStringLiteral("visibility") : QStringLiteral("hint"));
0052     hideShowIcon.paint(painter, layout.hideShowButtonRect.translated(previewRect.topLeft()));
0053     if (layout.isShown) {
0054         int nextY = previewRect.y() + option.fontMetrics.ascent() + DelegatePaintUtil::margin();
0055         if (!layout.pixmap.isNull()) {
0056             QPixmap scaledPixmap;
0057             scaledPixmap = layout.pixmap.scaled(layout.imageSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0058             painter->drawPixmap(previewRect.x(), nextY, scaledPixmap);
0059             // qDebug() << " image size " << scaledPixmap.size();
0060             nextY += scaledPixmap.height() / scaledPixmap.devicePixelRatioF() + DelegatePaintUtil::margin();
0061         }
0062         // qDebug() << " nextY " << nextY;
0063 #if 0
0064         painter->save();
0065         painter->setPen(Qt::red);
0066         painter->drawRect(previewRect);
0067         painter->restore();
0068 #endif
0069         drawDescription(messageUrl, previewRect, painter, nextY, index, option);
0070     }
0071 }
0072 
0073 MessageDelegateHelperUrlPreview::PreviewLayout MessageDelegateHelperUrlPreview::layoutPreview(const MessageUrl &messageUrl,
0074                                                                                               const QStyleOptionViewItem &option,
0075                                                                                               int urlsPreviewWidth,
0076                                                                                               int urlsPreviewHeight) const
0077 {
0078     Q_UNUSED(urlsPreviewHeight);
0079     MessageDelegateHelperUrlPreview::PreviewLayout layout;
0080     layout.previewTitle = i18n("Link Preview");
0081     layout.previewTitleSize = option.fontMetrics.size(Qt::TextSingleLine, layout.previewTitle);
0082     layout.hasDescription = messageUrl.hasHtmlDescription();
0083     const QUrl previewImageUrl =
0084         messageUrl.imageUrl().isEmpty() ? QUrl{} : (mRocketChatAccount ? mRocketChatAccount->previewUrlFromLocalCache(messageUrl.imageUrl()) : QUrl{});
0085     if (previewImageUrl.isLocalFile()) {
0086         layout.imageUrl = messageUrl.imageUrl();
0087 
0088         const QString imagePreviewPath = previewImageUrl.toLocalFile();
0089         layout.pixmap = mPixmapCache.pixmapForLocalFile(imagePreviewPath);
0090         layout.pixmap.setDevicePixelRatio(option.widget->devicePixelRatioF());
0091         const auto dpr = layout.pixmap.devicePixelRatioF();
0092         layout.imageSize = layout.pixmap.size().scaled(urlsPreviewWidth * dpr, /*imageMaxHeight*/ 100 * dpr, Qt::KeepAspectRatio);
0093         // qDebug() << " layout.imageSize " << layout.imageSize;
0094     }
0095     const int iconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize);
0096     layout.hideShowButtonRect = QRect(layout.previewTitleSize.width() + DelegatePaintUtil::margin(), 0, iconSize, iconSize);
0097     layout.isShown = messageUrl.showPreview();
0098     layout.descriptionSize =
0099         layout.isShown ? documentDescriptionForIndexSize(convertMessageUrlToDocumentDescriptionInfo(messageUrl, urlsPreviewWidth)) : QSize();
0100 
0101     return layout;
0102 }
0103 
0104 MessageDelegateHelperBase::DocumentDescriptionInfo MessageDelegateHelperUrlPreview::convertMessageUrlToDocumentDescriptionInfo(const MessageUrl &messageUrl,
0105                                                                                                                                int width) const
0106 {
0107     MessageDelegateHelperBase::DocumentDescriptionInfo info;
0108     info.documentId = messageUrl.urlId();
0109     info.description = messageUrl.htmlDescription();
0110     info.width = width;
0111     return info;
0112 }
0113 
0114 void MessageDelegateHelperUrlPreview::drawDescription(const MessageUrl &messageUrl,
0115                                                       QRect previewRect,
0116                                                       QPainter *painter,
0117                                                       int topPos,
0118                                                       const QModelIndex &index,
0119                                                       const QStyleOptionViewItem &option) const
0120 {
0121     auto *doc = documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, previewRect.width()));
0122     if (!doc) {
0123         return;
0124     }
0125 
0126     MessageDelegateUtils::drawSelection(doc, previewRect, topPos, painter, index, option, mTextSelectionImpl->textSelection(), {}, messageUrl);
0127 }
0128 
0129 QSize MessageDelegateHelperUrlPreview::sizeHint(const MessageUrl &messageUrl, const QModelIndex &index, int maxWidth, const QStyleOptionViewItem &option) const
0130 {
0131     Q_UNUSED(index);
0132     const PreviewLayout layout = layoutPreview(messageUrl, option, maxWidth, -1);
0133     int height = layout.previewTitleSize.height() + DelegatePaintUtil::margin();
0134     // qDebug() << " height 1 " << height;
0135     int pixmapWidth = 0;
0136     if (layout.isShown) {
0137         pixmapWidth = qMin(layout.pixmap.width(), maxWidth);
0138         height += qMin(layout.imageSize.height(), 100) + DelegatePaintUtil::margin();
0139         // qDebug() << " height 2 " << height << "  layout.pixmap.height() " << layout.imageSize.height();
0140     }
0141     int descriptionWidth = 0;
0142     if (layout.hasDescription && layout.isShown) {
0143         descriptionWidth = layout.descriptionSize.width();
0144         height += layout.descriptionSize.height() + DelegatePaintUtil::margin();
0145         // qDebug() << " height 3 " << height;
0146     }
0147     return {qMax(qMax(pixmapWidth, layout.previewTitleSize.width()), descriptionWidth), height};
0148 }
0149 
0150 bool MessageDelegateHelperUrlPreview::handleHelpEvent(QHelpEvent *helpEvent,
0151                                                       QRect previewRect,
0152                                                       const MessageUrl &messageUrl,
0153                                                       const QStyleOptionViewItem &option)
0154 {
0155     if (helpEvent->type() != QEvent::ToolTip) {
0156         return false;
0157     }
0158 
0159     const auto *doc = documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, previewRect.width()));
0160     if (!doc) {
0161         return false;
0162     }
0163     const PreviewLayout layout = layoutPreview(messageUrl, option, previewRect.width(), previewRect.height());
0164 
0165     const QPoint pos =
0166         helpEvent->pos() - previewRect.topLeft() - QPoint(0, layout.imageSize.height() + layout.previewTitleSize.height() + DelegatePaintUtil::margin());
0167     QString formattedTooltip;
0168     if (MessageDelegateUtils::generateToolTip(doc, pos, formattedTooltip)) {
0169         QToolTip::showText(helpEvent->globalPos(), formattedTooltip);
0170         return true;
0171     }
0172     return true;
0173 }
0174 
0175 bool MessageDelegateHelperUrlPreview::handleMouseEvent(const MessageUrl &messageUrl,
0176                                                        QMouseEvent *mouseEvent,
0177                                                        QRect previewRect,
0178                                                        const QStyleOptionViewItem &option,
0179                                                        const QModelIndex &index)
0180 {
0181     const QEvent::Type eventType = mouseEvent->type();
0182     const QPoint pos = mouseEvent->pos();
0183     switch (eventType) {
0184     case QEvent::MouseButtonRelease: {
0185         const PreviewLayout layout = layoutPreview(messageUrl, option, previewRect.width(), previewRect.height());
0186         if (layout.hideShowButtonRect.translated(previewRect.topLeft()).contains(pos)) {
0187             MessagesModel::AttachmentAndUrlPreviewVisibility previewUrlVisibility;
0188             previewUrlVisibility.show = !layout.isShown;
0189             previewUrlVisibility.ElementId = messageUrl.urlId();
0190             auto model = const_cast<QAbstractItemModel *>(index.model());
0191             model->setData(index, QVariant::fromValue(previewUrlVisibility), MessagesModel::DisplayUrlPreview);
0192             return true;
0193         }
0194         // Clicks on links
0195         auto *doc = documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, previewRect.width()));
0196         if (doc) {
0197             const QPoint mouseClickPos =
0198                 pos - previewRect.topLeft() - QPoint(0, layout.imageSize.height() + layout.previewTitleSize.height() + DelegatePaintUtil::margin());
0199             const QString link = doc->documentLayout()->anchorAt(mouseClickPos);
0200             if (!link.isEmpty()) {
0201                 Q_EMIT mRocketChatAccount->openLinkRequested(link);
0202                 return true;
0203             }
0204         }
0205         break;
0206     }
0207     case QEvent::MouseButtonPress:
0208         mTextSelectionImpl->setMightStartDrag(false);
0209         if (const auto *doc = documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, previewRect.width()))) {
0210             const int charPos = charPosition(doc, messageUrl, previewRect, pos, option);
0211             qCDebug(RUQOLAWIDGETS_SELECTION_LOG) << "pressed at pos" << charPos;
0212             if (charPos == -1) {
0213                 return false;
0214             }
0215             if (mTextSelectionImpl->textSelection()->contains(index, charPos) && doc->documentLayout()->hitTest(pos, Qt::ExactHit) != -1) {
0216                 mTextSelectionImpl->setMightStartDrag(true);
0217                 return true;
0218             }
0219 
0220             // QWidgetTextControl also has code to support selectBlockOnTripleClick, shift to extend selection
0221             // (look there if you want to add these things)
0222 
0223             mTextSelectionImpl->textSelection()->setStart(index, charPos);
0224             return true;
0225         } else {
0226             mTextSelectionImpl->textSelection()->clear();
0227         }
0228         break;
0229     case QEvent::MouseMove:
0230         if (!mTextSelectionImpl->mightStartDrag()) {
0231             if (const auto *doc = documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, previewRect.width()))) {
0232                 const int charPos = charPosition(doc, messageUrl, previewRect, pos, option);
0233                 if (charPos != -1) {
0234                     // QWidgetTextControl also has code to support isPreediting()/commitPreedit(), selectBlockOnTripleClick
0235                     mTextSelectionImpl->textSelection()->setEnd(index, charPos);
0236                     return true;
0237                 }
0238             }
0239         }
0240         break;
0241     case QEvent::MouseButtonDblClick:
0242         if (!mTextSelectionImpl->textSelection()->hasSelection()) {
0243             if (const auto *doc = documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, previewRect.width()))) {
0244                 const int charPos = charPosition(doc, messageUrl, previewRect, pos, option);
0245                 qCDebug(RUQOLAWIDGETS_SELECTION_LOG) << "double-clicked at pos" << charPos;
0246                 if (charPos == -1) {
0247                     return false;
0248                 }
0249                 mTextSelectionImpl->textSelection()->selectWordUnderCursor(index, charPos, this, messageUrl);
0250                 return true;
0251             }
0252         }
0253         break;
0254     default:
0255         break;
0256     }
0257     return false;
0258 }
0259 
0260 int MessageDelegateHelperUrlPreview::charPosition(const QTextDocument *doc,
0261                                                   const MessageUrl &messageUrl,
0262                                                   QRect previewRect,
0263                                                   const QPoint &pos,
0264                                                   const QStyleOptionViewItem &option)
0265 {
0266     const QPoint relativePos = adaptMousePosition(pos, messageUrl, previewRect, option);
0267     const int charPos = doc->documentLayout()->hitTest(relativePos, Qt::FuzzyHit);
0268     return charPos;
0269 }
0270 
0271 QPoint
0272 MessageDelegateHelperUrlPreview::adaptMousePosition(const QPoint &pos, const MessageUrl &messageUrl, QRect previewRect, const QStyleOptionViewItem &option)
0273 {
0274     const PreviewLayout layout = layoutPreview(messageUrl, option, previewRect.width(), previewRect.height());
0275     const QPoint relativePos =
0276         pos - previewRect.topLeft() - QPoint(0, layout.imageSize.height() + layout.previewTitleSize.height() + DelegatePaintUtil::margin());
0277     return relativePos;
0278 }
0279 
0280 QString MessageDelegateHelperUrlPreview::urlAt(const QStyleOptionViewItem &option, const MessageUrl &messageUrl, QRect previewsRect, QPoint pos)
0281 {
0282     auto document = documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, previewsRect.width()));
0283     if (!document) {
0284         return {};
0285     }
0286     const QPoint relativePos = adaptMousePosition(pos, messageUrl, previewsRect, option);
0287     return document->documentLayout()->anchorAt(relativePos);
0288 }
0289 
0290 QTextDocument *MessageDelegateHelperUrlPreview::documentForUrlPreview(const MessageUrl &messageUrl) const
0291 {
0292     return documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, -1));
0293 }
0294 
0295 bool MessageDelegateHelperUrlPreview::maybeStartDrag(const MessageUrl &messageUrl,
0296                                                      QMouseEvent *mouseEvent,
0297                                                      QRect previewsRect,
0298                                                      const QStyleOptionViewItem &option,
0299                                                      const QModelIndex &index)
0300 {
0301     if (!mTextSelectionImpl->mightStartDrag() || !previewsRect.contains(mouseEvent->pos())) {
0302         return false;
0303     }
0304     if (mTextSelectionImpl->textSelection()->hasSelection()) {
0305         const auto *doc = documentDescriptionForIndex(convertMessageUrlToDocumentDescriptionInfo(messageUrl, previewsRect.width()));
0306         const QPoint pos = mouseEvent->pos() - previewsRect.topLeft();
0307         const int charPos = doc->documentLayout()->hitTest(pos, Qt::FuzzyHit);
0308         if (charPos != -1 && mTextSelectionImpl->textSelection()->contains(index, charPos)) {
0309             auto mimeData = new QMimeData;
0310             mimeData->setHtml(mTextSelectionImpl->textSelection()->selectedText(TextSelection::Html));
0311             mimeData->setText(mTextSelectionImpl->textSelection()->selectedText(TextSelection::Text));
0312             auto drag = new QDrag(const_cast<QWidget *>(option.widget));
0313             drag->setMimeData(mimeData);
0314             drag->exec(Qt::CopyAction);
0315             mTextSelectionImpl->setMightStartDrag(false); // don't clear selection on release
0316             return true;
0317         }
0318     }
0319     return false;
0320 }
0321 
0322 void MessageDelegateHelperUrlPreview::dump(const PreviewLayout &layout)
0323 {
0324     // Don't use debug category as we want to show it.
0325     qDebug() << " pixmap " << layout.pixmap;
0326     qDebug() << " imageUrl " << layout.imageUrl;
0327     qDebug() << " hasDescription " << layout.hasDescription;
0328     qDebug() << " previewTitleSize " << layout.previewTitleSize;
0329     qDebug() << " previewTitle " << layout.previewTitle;
0330     qDebug() << " descriptionSize " << layout.descriptionSize;
0331     qDebug() << " imageSize " << layout.imageSize;
0332     qDebug() << " hideShowButtonRect " << layout.hideShowButtonRect;
0333     qDebug() << " isShown " << layout.isShown;
0334 }