File indexing completed on 2024-06-09 04:59:22
0001 /* 0002 SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "messageattachmentdelegatehelperfile.h" 0008 #include "common/delegatepaintutil.h" 0009 #include "common/delegateutil.h" 0010 #include "connection.h" 0011 #include "downloadfilejob.h" 0012 #include "rocketchataccount.h" 0013 #include "ruqolautils.h" 0014 #include <KApplicationTrader> 0015 #include <KIO/ApplicationLauncherJob> 0016 #include <KIO/JobUiDelegate> 0017 #include <KIO/JobUiDelegateFactory> 0018 #include <KLocalizedString> 0019 #include <KService> 0020 0021 #include <QAbstractTextDocumentLayout> 0022 #include <QMessageBox> 0023 #include <QMimeDatabase> 0024 #include <QMouseEvent> 0025 #include <QPainter> 0026 #include <QPushButton> 0027 #include <QStyleOptionViewItem> 0028 #include <QTemporaryDir> 0029 0030 // Name <download icon> 0031 // Description 0032 0033 MessageAttachmentDelegateHelperFile::MessageAttachmentDelegateHelperFile(RocketChatAccount *account, QListView *view, TextSelectionImpl *textSelectionImpl) 0034 : MessageAttachmentDelegateHelperBase(account, view, textSelectionImpl) 0035 , mDownloadIcon(QIcon::fromTheme(QStringLiteral("cloud-download"))) 0036 { 0037 } 0038 0039 MessageAttachmentDelegateHelperFile::~MessageAttachmentDelegateHelperFile() = default; 0040 0041 void MessageAttachmentDelegateHelperFile::draw(const MessageAttachment &msgAttach, 0042 QPainter *painter, 0043 QRect attachmentsRect, 0044 const QModelIndex &index, 0045 const QStyleOptionViewItem &option) const 0046 { 0047 const FileLayout layout = doLayout(msgAttach, option, attachmentsRect.width()); 0048 const QPen oldPen = painter->pen(); 0049 const QFont oldFont = painter->font(); 0050 const int y = attachmentsRect.y() + layout.y; 0051 const bool hasLink = !layout.link.isEmpty(); 0052 if (hasLink) { 0053 QFont underlinedFont = oldFont; 0054 underlinedFont.setUnderline(true); 0055 painter->setPen(option.palette.color(QPalette::Link)); 0056 painter->setFont(underlinedFont); 0057 } 0058 painter->drawText(attachmentsRect.x(), y + option.fontMetrics.ascent(), layout.title); 0059 if (layout.downloadButtonRect.isValid()) { 0060 mDownloadIcon.paint(painter, layout.downloadButtonRect.translated(attachmentsRect.topLeft())); 0061 } 0062 0063 if (hasLink) { 0064 painter->setPen(oldPen); 0065 painter->setFont(oldFont); 0066 } 0067 const int descriptionY = y + layout.titleSize.height() + DelegatePaintUtil::margin(); 0068 drawDescription(msgAttach, attachmentsRect, painter, descriptionY, index, option); 0069 } 0070 0071 QSize MessageAttachmentDelegateHelperFile::sizeHint(const MessageAttachment &msgAttach, 0072 const QModelIndex &index, 0073 int maxWidth, 0074 const QStyleOptionViewItem &option) const 0075 { 0076 Q_UNUSED(index) 0077 const FileLayout layout = doLayout(msgAttach, option, maxWidth); 0078 return {maxWidth, // should be qMax of all sizes, but doesn't really matter 0079 layout.y + layout.height + DelegatePaintUtil::margin()}; 0080 } 0081 0082 MessageAttachmentDelegateHelperFile::FileLayout 0083 MessageAttachmentDelegateHelperFile::doLayout(const MessageAttachment &msgAttach, const QStyleOptionViewItem &option, int attachmentsWidth) const 0084 { 0085 const int buttonMargin = DelegatePaintUtil::margin(); 0086 const int iconSize = option.widget->style()->pixelMetric(QStyle::PM_ButtonIconSize); 0087 const int y = 0; 0088 FileLayout layout; 0089 layout.title = msgAttach.title(); 0090 layout.description = msgAttach.description(); 0091 layout.link = msgAttach.link(); 0092 layout.titleSize = option.fontMetrics.size(Qt::TextSingleLine, layout.title); 0093 layout.descriptionSize = documentDescriptionForIndexSize(convertAttachmentToDocumentDescriptionInfo(msgAttach, attachmentsWidth)); 0094 layout.y = y; 0095 layout.height = layout.titleSize.height() + (layout.description.isEmpty() ? 0 : DelegatePaintUtil::margin() + layout.descriptionSize.height()); 0096 if (msgAttach.canDownloadAttachment()) { 0097 layout.downloadButtonRect = QRect(layout.titleSize.width() + buttonMargin, y, iconSize, iconSize); 0098 } 0099 return layout; 0100 } 0101 0102 enum class UserChoice { 0103 Save, 0104 Open, 0105 OpenWith, 0106 Cancel, 0107 }; 0108 Q_DECLARE_METATYPE(UserChoice) 0109 0110 static UserChoice askUser(const QUrl &url, const KService::Ptr &offer, QWidget *widget) 0111 { 0112 const QString title = i18nc("@title:window", "Open Attachment?"); 0113 const QString text = xi18nc("@info", "Open attachment <filename>%1</filename>?<nl/>", url.fileName()); 0114 QMessageBox msgBox(QMessageBox::Question, title, text, QMessageBox::NoButton, widget); 0115 const char *prop = "_enumValue"; 0116 if (offer) { 0117 auto *b = msgBox.addButton(i18n("&Open With '%1'", offer->name()), QMessageBox::YesRole); 0118 b->setProperty(prop, QVariant::fromValue(UserChoice::Open)); 0119 } 0120 msgBox.addButton(i18n("Open &With..."), QMessageBox::YesRole)->setProperty(prop, QVariant::fromValue(UserChoice::OpenWith)); 0121 msgBox.addButton(i18n("Save &As..."), QMessageBox::ActionRole)->setProperty(prop, QVariant::fromValue(UserChoice::Save)); 0122 msgBox.addButton(QMessageBox::Cancel)->setProperty(prop, QVariant::fromValue(UserChoice::Cancel)); 0123 msgBox.exec(); 0124 return msgBox.clickedButton()->property(prop).value<UserChoice>(); 0125 } 0126 0127 static void runApplication(const KService::Ptr &offer, const QString &link, QWidget *widget, RocketChatAccount *account) 0128 { 0129 std::unique_ptr<QTemporaryDir> tempDir(new QTemporaryDir(QDir::tempPath() + QLatin1String("/ruqola_attachment_XXXXXX"))); 0130 if (!tempDir->isValid()) { 0131 return; 0132 } 0133 tempDir->setAutoRemove(false); // can't delete them, same problem as in messagelib ViewerPrivate::attachmentOpenWith 0134 const QString tempFile = tempDir->filePath(QUrl(link).fileName()); 0135 const QUrl fileUrl = QUrl::fromLocalFile(tempFile); 0136 0137 const QUrl downloadUrl = account->urlForLink(link); 0138 auto *job = account->restApi()->downloadFile(downloadUrl, fileUrl, QByteArrayLiteral("text/plain")); 0139 QObject::connect(job, &RocketChatRestApi::DownloadFileJob::downloadFileDone, widget, [=](const QUrl &, const QUrl &localFileUrl) { 0140 auto job = new KIO::ApplicationLauncherJob(offer); // asks the user if offer is nullptr 0141 job->setUrls({localFileUrl}); 0142 job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); 0143 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, widget)); 0144 job->start(); 0145 }); 0146 } 0147 0148 void MessageAttachmentDelegateHelperFile::handleDownloadClicked(const QString &link, QWidget *widget) 0149 { 0150 const QUrl url(link); 0151 QMimeDatabase db; 0152 const QMimeType mimeType = db.mimeTypeForUrl(url); 0153 const bool valid = mimeType.isValid() && !mimeType.isDefault(); 0154 const KService::Ptr offer = valid ? KApplicationTrader::preferredService(mimeType.name()) : KService::Ptr{}; 0155 const UserChoice choice = askUser(url, offer, widget); 0156 switch (choice) { 0157 case UserChoice::Save: { 0158 const QString file = DelegateUtil::querySaveFileName(widget, i18n("Save File"), url); 0159 if (!file.isEmpty()) { 0160 const QUrl fileUrl = QUrl::fromLocalFile(file); 0161 mRocketChatAccount->downloadFile(link, fileUrl); 0162 } 0163 break; 0164 } 0165 case UserChoice::Open: 0166 runApplication(offer, link, widget, mRocketChatAccount); 0167 break; 0168 case UserChoice::OpenWith: 0169 runApplication({}, link, widget, mRocketChatAccount); 0170 break; 0171 case UserChoice::Cancel: 0172 break; 0173 } 0174 } 0175 0176 QPoint MessageAttachmentDelegateHelperFile::adaptMousePosition(const QPoint &pos, 0177 const MessageAttachment &msgAttach, 0178 QRect attachmentsRect, 0179 const QStyleOptionViewItem &option) 0180 { 0181 const FileLayout layout = doLayout(msgAttach, option, attachmentsRect.width()); 0182 const QPoint relativePos = pos - attachmentsRect.topLeft() - QPoint(0, layout.titleSize.height() + DelegatePaintUtil::margin()); 0183 return relativePos; 0184 } 0185 0186 bool MessageAttachmentDelegateHelperFile::handleMouseEvent(const MessageAttachment &msgAttach, 0187 QMouseEvent *mouseEvent, 0188 QRect attachmentsRect, 0189 const QStyleOptionViewItem &option, 0190 const QModelIndex &index) 0191 { 0192 const QEvent::Type eventType = mouseEvent->type(); 0193 switch (eventType) { 0194 case QEvent::MouseButtonRelease: { 0195 const QPoint pos = mouseEvent->pos(); 0196 const FileLayout layout = doLayout(msgAttach, option, attachmentsRect.width()); 0197 0198 if (layout.downloadButtonRect.translated(attachmentsRect.topLeft()).contains(pos)) { 0199 handleDownloadClicked(layout.link, const_cast<QWidget *>(option.widget)); 0200 return true; 0201 } 0202 if (!layout.link.isEmpty()) { 0203 const int y = attachmentsRect.y() + layout.y; 0204 const QSize linkSize = option.fontMetrics.size(Qt::TextSingleLine, layout.title); 0205 const QRect linkRect(attachmentsRect.x(), y, linkSize.width(), linkSize.height()); 0206 if (linkRect.contains(pos)) { 0207 if (layout.downloadButtonRect.isValid()) { 0208 handleDownloadClicked(layout.link, const_cast<QWidget *>(option.widget)); 0209 } else { 0210 RuqolaUtils::self()->openUrl(layout.link); 0211 } 0212 return true; 0213 } 0214 } 0215 break; 0216 } 0217 default: 0218 break; 0219 } 0220 0221 return MessageAttachmentDelegateHelperBase::handleMouseEvent(msgAttach, mouseEvent, attachmentsRect, option, index); 0222 }