File indexing completed on 2024-04-28 04:58:10

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