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 }