File indexing completed on 2024-04-28 15:29:19

0001 /*
0002     SPDX-FileCopyrightText: 2009, 2010 David Faure <faure@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "browseropenorsavequestion.h"
0008 
0009 #include <KConfigGroup>
0010 #include <KFileItemActions>
0011 #include <KGuiItem>
0012 #include <KLocalizedString>
0013 #include <KMessageBox>
0014 #include <KSharedConfig>
0015 #include <KSqueezedTextLabel>
0016 #include <KStandardGuiItem>
0017 
0018 #include <QAction>
0019 #include <QCheckBox>
0020 #include <QDialog>
0021 #include <QDialogButtonBox>
0022 #include <QLabel>
0023 #include <QMenu>
0024 #include <QMimeDatabase>
0025 #include <QPushButton>
0026 #include <QStyle>
0027 #include <QStyleOption>
0028 #include <QVBoxLayout>
0029 
0030 using namespace KParts;
0031 Q_DECLARE_METATYPE(KService::Ptr)
0032 
0033 class KParts::BrowserOpenOrSaveQuestionPrivate : public QDialog
0034 {
0035     Q_OBJECT
0036 public:
0037     enum {
0038         Save = QDialog::Accepted,
0039         OpenDefault = Save + 1,
0040         OpenWith = OpenDefault + 1,
0041         Cancel = QDialog::Rejected,
0042     };
0043 
0044     BrowserOpenOrSaveQuestionPrivate(QWidget *parent, const QUrl &url, const QString &mimeType)
0045         : QDialog(parent)
0046         , url(url)
0047         , mimeType(mimeType)
0048         , features(BrowserOpenOrSaveQuestion::BasicFeatures)
0049     {
0050         // Use askSave or askEmbedOrSave from filetypesrc
0051         dontAskConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals);
0052 
0053         setWindowTitle(url.host());
0054         setObjectName(QStringLiteral("questionYesNoCancel"));
0055 
0056         QVBoxLayout *mainLayout = new QVBoxLayout(this);
0057         const int verticalSpacing = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing);
0058         mainLayout->setSpacing(verticalSpacing * 2); // provide extra spacing
0059 
0060         QHBoxLayout *hLayout = new QHBoxLayout();
0061         mainLayout->addLayout(hLayout, 5);
0062 
0063         QLabel *iconLabel = new QLabel(this);
0064         QStyleOption option;
0065         option.initFrom(this);
0066         QIcon icon = QIcon::fromTheme(QStringLiteral("dialog-information"));
0067         iconLabel->setPixmap(icon.pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, this)));
0068 
0069         hLayout->addWidget(iconLabel, 0, Qt::AlignCenter);
0070         const int horizontalSpacing = style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
0071         hLayout->addSpacing(horizontalSpacing);
0072 
0073         QVBoxLayout *textVLayout = new QVBoxLayout;
0074         questionLabel = new KSqueezedTextLabel(this);
0075         textVLayout->addWidget(questionLabel);
0076 
0077         fileNameLabel = new QLabel(this);
0078         fileNameLabel->hide();
0079         textVLayout->addWidget(fileNameLabel);
0080 
0081         QMimeDatabase db;
0082         mime = db.mimeTypeForName(mimeType);
0083         QString mimeDescription(mimeType);
0084         if (mime.isValid()) {
0085             // Always prefer the mime-type comment over the raw type for display
0086             mimeDescription = (mime.comment().isEmpty() ? mime.name() : mime.comment());
0087         }
0088         QLabel *mimeTypeLabel = new QLabel(this);
0089         mimeTypeLabel->setText(i18nc("@label Type of file", "Type: %1", mimeDescription));
0090         mimeTypeLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
0091         textVLayout->addWidget(mimeTypeLabel);
0092 
0093         hLayout->addLayout(textVLayout, 5);
0094 
0095         mainLayout->addStretch(15);
0096         dontAskAgainCheckBox = new QCheckBox(this);
0097         dontAskAgainCheckBox->setText(i18nc("@label:checkbox", "Remember action for files of this type"));
0098         mainLayout->addWidget(dontAskAgainCheckBox);
0099 
0100         buttonBox = new QDialogButtonBox(this);
0101 
0102         saveButton = buttonBox->addButton(QDialogButtonBox::Yes);
0103         saveButton->setObjectName(QStringLiteral("saveButton"));
0104         KGuiItem::assign(saveButton, KStandardGuiItem::saveAs());
0105         saveButton->setDefault(true);
0106 
0107         openDefaultButton = new QPushButton;
0108         openDefaultButton->setObjectName(QStringLiteral("openDefaultButton"));
0109         buttonBox->addButton(openDefaultButton, QDialogButtonBox::ActionRole);
0110 
0111         openWithButton = new QPushButton;
0112         openWithButton->setObjectName(QStringLiteral("openWithButton"));
0113         buttonBox->addButton(openWithButton, QDialogButtonBox::ActionRole);
0114 
0115         QPushButton *cancelButton = buttonBox->addButton(QDialogButtonBox::Cancel);
0116         cancelButton->setObjectName(QStringLiteral("cancelButton"));
0117 
0118         connect(saveButton, &QPushButton::clicked, this, &BrowserOpenOrSaveQuestionPrivate::slotYesClicked);
0119         connect(openDefaultButton, &QPushButton::clicked, this, &BrowserOpenOrSaveQuestionPrivate::slotOpenDefaultClicked);
0120         connect(openWithButton, &QPushButton::clicked, this, &BrowserOpenOrSaveQuestionPrivate::slotOpenWithClicked);
0121         connect(buttonBox, &QDialogButtonBox::rejected, this, &BrowserOpenOrSaveQuestionPrivate::reject);
0122 
0123         mainLayout->addWidget(buttonBox);
0124     }
0125 
0126     bool autoEmbedMimeType(int flags);
0127 
0128     int executeDialog(const QString &dontShowAgainName)
0129     {
0130         KConfigGroup cg(dontAskConfig, "Notification Messages"); // group name comes from KMessageBox
0131         const QString dontAsk = cg.readEntry(dontShowAgainName, QString()).toLower();
0132         if (dontAsk == QLatin1String("yes") || dontAsk == QLatin1String("true")) {
0133             return Save;
0134         } else if (dontAsk == QLatin1String("no") || dontAsk == QLatin1String("false")) {
0135             return OpenDefault;
0136         }
0137 
0138         const int result = exec();
0139 
0140         if (dontAskAgainCheckBox->isChecked()) {
0141             cg.writeEntry(dontShowAgainName, result == BrowserOpenOrSaveQuestion::Save);
0142             cg.sync();
0143         }
0144         return result;
0145     }
0146 
0147     void showService(KService::Ptr selectedService)
0148     {
0149         KGuiItem openItem(i18nc("@label:button", "&Open with %1", selectedService->name()), selectedService->icon());
0150         KGuiItem::assign(openWithButton, openItem);
0151     }
0152 
0153     QUrl url;
0154     QString mimeType;
0155     QMimeType mime;
0156     KService::Ptr selectedService;
0157     KSqueezedTextLabel *questionLabel;
0158     BrowserOpenOrSaveQuestion::Features features;
0159     QLabel *fileNameLabel;
0160     QDialogButtonBox *buttonBox;
0161     QPushButton *saveButton;
0162     QPushButton *openDefaultButton;
0163     QPushButton *openWithButton;
0164 
0165 private:
0166     QCheckBox *dontAskAgainCheckBox;
0167     KSharedConfig::Ptr dontAskConfig;
0168 
0169 public Q_SLOTS:
0170     void reject() override
0171     {
0172         selectedService = nullptr;
0173         QDialog::reject();
0174     }
0175 
0176     void slotYesClicked()
0177     {
0178         selectedService = nullptr;
0179         done(Save);
0180     }
0181 
0182     void slotOpenDefaultClicked()
0183     {
0184         done(OpenDefault);
0185     }
0186 
0187     void slotOpenWithClicked()
0188     {
0189         if (!openWithButton->menu()) {
0190             selectedService = nullptr;
0191             done(OpenWith);
0192         }
0193     }
0194 
0195     void slotAppSelected(QAction *action)
0196     {
0197         selectedService = action->data().value<KService::Ptr>();
0198         // showService(selectedService);
0199         done(OpenDefault);
0200     }
0201 };
0202 
0203 BrowserOpenOrSaveQuestion::BrowserOpenOrSaveQuestion(QWidget *parent, const QUrl &url, const QString &mimeType)
0204     : d(new BrowserOpenOrSaveQuestionPrivate(parent, url, mimeType))
0205 {
0206 }
0207 
0208 BrowserOpenOrSaveQuestion::~BrowserOpenOrSaveQuestion() = default;
0209 
0210 static QAction *createAppAction(const KService::Ptr &service, QObject *parent)
0211 {
0212     QString actionName(service->name().replace(QLatin1Char('&'), QLatin1String("&&")));
0213     actionName = i18nc("@action:inmenu", "Open &with %1", actionName);
0214 
0215     QAction *act = new QAction(parent);
0216     act->setIcon(QIcon::fromTheme(service->icon()));
0217     act->setText(actionName);
0218     act->setData(QVariant::fromValue(service));
0219     return act;
0220 }
0221 
0222 BrowserOpenOrSaveQuestion::Result BrowserOpenOrSaveQuestion::askOpenOrSave()
0223 {
0224     d->questionLabel->setText(i18nc("@info", "Open '%1'?", d->url.toDisplayString(QUrl::PreferLocalFile)));
0225     d->questionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
0226     d->openWithButton->hide();
0227 
0228     KGuiItem openWithDialogItem(i18nc("@label:button", "&Open with..."), QStringLiteral("document-open"));
0229 
0230     // I thought about using KFileItemActions, but we don't want a submenu, nor the slots....
0231     // and we want no menu at all if there's only one offer.
0232     const KService::List apps = KFileItemActions::associatedApplications(QStringList{d->mimeType});
0233     if (apps.isEmpty()) {
0234         KGuiItem::assign(d->openDefaultButton, openWithDialogItem);
0235     } else {
0236         KService::Ptr offer = apps.first();
0237         KGuiItem openItem(i18nc("@label:button", "&Open with %1", offer->name()), offer->icon());
0238         KGuiItem::assign(d->openDefaultButton, openItem);
0239         if (d->features & ServiceSelection) {
0240             // OpenDefault shall use this service
0241             d->selectedService = apps.first();
0242             d->openWithButton->show();
0243             QMenu *menu = new QMenu(d.get());
0244             if (apps.count() > 1) {
0245                 // Provide an additional button with a menu of associated apps
0246                 KGuiItem openWithItem(i18nc("@label:button", "&Open with"), QStringLiteral("document-open"));
0247                 KGuiItem::assign(d->openWithButton, openWithItem);
0248                 d->openWithButton->setMenu(menu);
0249                 QObject::connect(menu, &QMenu::triggered, d.get(), &BrowserOpenOrSaveQuestionPrivate::slotAppSelected);
0250                 for (const auto &app : apps) {
0251                     QAction *act = createAppAction(app, d.get());
0252                     menu->addAction(act);
0253                 }
0254                 QAction *openWithDialogAction = new QAction(d.get());
0255                 openWithDialogAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
0256                 openWithDialogAction->setText(openWithDialogItem.text());
0257                 menu->addAction(openWithDialogAction);
0258             } else {
0259                 // Only one associated app, already offered by the other menu -> add "Open With..." button
0260                 KGuiItem::assign(d->openWithButton, openWithDialogItem);
0261             }
0262         } else {
0263             // qDebug() << "Not using new feature ServiceSelection; port the caller to BrowserOpenOrSaveQuestion::setFeature(ServiceSelection)";
0264         }
0265     }
0266 
0267     // KEEP IN SYNC with kdebase/runtime/keditfiletype/filetypedetails.cpp!!!
0268     const QString dontAskAgain = QLatin1String("askSave") + d->mimeType;
0269 
0270     const int choice = d->executeDialog(dontAskAgain);
0271     return choice == BrowserOpenOrSaveQuestionPrivate::Save ? Save : (choice == BrowserOpenOrSaveQuestionPrivate::Cancel ? Cancel : Open);
0272 }
0273 
0274 KService::Ptr BrowserOpenOrSaveQuestion::selectedService() const
0275 {
0276     return d->selectedService;
0277 }
0278 
0279 bool BrowserOpenOrSaveQuestionPrivate::autoEmbedMimeType(int flags)
0280 {
0281     // SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC
0282     // NOTE: Keep this function in sync with
0283     // kdebase/runtime/keditfiletype/filetypedetails.cpp
0284     //       FileTypeDetails::updateAskSave()
0285 
0286     // Don't ask for:
0287     // - html (even new tabs would ask, due to about:blank!)
0288     // - dirs obviously (though not common over HTTP :),
0289     // - images (reasoning: no need to save, most of the time, because fast to see)
0290     // e.g. postscript is different, because takes longer to read, so
0291     // it's more likely that the user might want to save it.
0292     // - multipart/* ("server push", see kmultipart)
0293     // KEEP IN SYNC!!!
0294     // clang-format off
0295     if (flags != static_cast<int>(BrowserOpenOrSaveQuestion::AttachmentDisposition) && mime.isValid() && (
0296                 mime.inherits(QStringLiteral("text/html")) ||
0297                 mime.inherits(QStringLiteral("application/xml")) ||
0298                 mime.inherits(QStringLiteral("inode/directory")) ||
0299                 mimeType.startsWith(QLatin1String("image")) ||
0300                 mime.inherits(QStringLiteral("multipart/x-mixed-replace")) ||
0301                 mime.inherits(QStringLiteral("multipart/replace")))) {
0302         return true;
0303     }
0304     // clang-format on
0305     return false;
0306 }
0307 
0308 BrowserOpenOrSaveQuestion::Result BrowserOpenOrSaveQuestion::askEmbedOrSave(int flags)
0309 {
0310     if (d->autoEmbedMimeType(flags)) {
0311         return Embed;
0312     }
0313 
0314     // don't use KStandardGuiItem::open() here which has trailing ellipsis!
0315     KGuiItem::assign(d->openDefaultButton, KGuiItem(i18nc("@label:button", "&Open"), QStringLiteral("document-open")));
0316     d->openWithButton->hide();
0317 
0318     d->questionLabel->setText(i18nc("@info", "Open '%1'?", d->url.toDisplayString(QUrl::PreferLocalFile)));
0319     d->questionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
0320 
0321     const QString dontAskAgain = QLatin1String("askEmbedOrSave") + d->mimeType; // KEEP IN SYNC!!!
0322     // SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC
0323 
0324     const int choice = d->executeDialog(dontAskAgain);
0325     return choice == BrowserOpenOrSaveQuestionPrivate::Save ? Save : (choice == BrowserOpenOrSaveQuestionPrivate::Cancel ? Cancel : Embed);
0326 }
0327 
0328 void BrowserOpenOrSaveQuestion::setFeatures(Features features)
0329 {
0330     d->features = features;
0331 }
0332 
0333 void BrowserOpenOrSaveQuestion::setSuggestedFileName(const QString &suggestedFileName)
0334 {
0335     if (suggestedFileName.isEmpty()) {
0336         return;
0337     }
0338 
0339     d->fileNameLabel->setText(i18nc("@label File name", "Name: %1", suggestedFileName));
0340     d->fileNameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
0341     d->fileNameLabel->setWhatsThis(i18nc("@info:whatsthis", "This is the file name suggested by the server"));
0342     d->fileNameLabel->show();
0343 }
0344 
0345 #include "browseropenorsavequestion.moc"