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 }