1 /*
2     SPDX-FileCopyrightText: 2009, 2010 David Faure <faure@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6 
7 #include "browseropenorsavequestion.h"
8 
9 #include <KConfigGroup>
10 #include <KFileItemActions>
11 #include <KGuiItem>
12 #include <KLocalizedString>
13 #include <KMessageBox>
14 #include <KSharedConfig>
15 #include <KSqueezedTextLabel>
16 #include <KStandardGuiItem>
17 
18 #include <QAction>
19 #include <QCheckBox>
20 #include <QDialog>
21 #include <QDialogButtonBox>
22 #include <QLabel>
23 #include <QMenu>
24 #include <QMimeDatabase>
25 #include <QPushButton>
26 #include <QStyle>
27 #include <QStyleOption>
28 #include <QVBoxLayout>
29 
30 using namespace KParts;
31 Q_DECLARE_METATYPE(KService::Ptr)
32 
33 class KParts::BrowserOpenOrSaveQuestionPrivate : public QDialog
34 {
35     Q_OBJECT
36 public:
37     enum {
38         Save = QDialog::Accepted,
39         OpenDefault = Save + 1,
40         OpenWith = OpenDefault + 1,
41         Cancel = QDialog::Rejected,
42     };
43 
BrowserOpenOrSaveQuestionPrivate(QWidget * parent,const QUrl & url,const QString & mimeType)44     BrowserOpenOrSaveQuestionPrivate(QWidget *parent, const QUrl &url, const QString &mimeType)
45         : QDialog(parent)
46         , url(url)
47         , mimeType(mimeType)
48         , features(BrowserOpenOrSaveQuestion::BasicFeatures)
49     {
50         // Use askSave or askEmbedOrSave from filetypesrc
51         dontAskConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals);
52 
53         setWindowTitle(url.host());
54         setObjectName(QStringLiteral("questionYesNoCancel"));
55 
56         QVBoxLayout *mainLayout = new QVBoxLayout(this);
57         const int verticalSpacing = style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing);
58         mainLayout->setSpacing(verticalSpacing * 2); // provide extra spacing
59 
60         QHBoxLayout *hLayout = new QHBoxLayout();
61         mainLayout->addLayout(hLayout, 5);
62 
63         QLabel *iconLabel = new QLabel(this);
64         QStyleOption option;
65         option.initFrom(this);
66         QIcon icon = QIcon::fromTheme(QStringLiteral("dialog-information"));
67         iconLabel->setPixmap(icon.pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, this)));
68 
69         hLayout->addWidget(iconLabel, 0, Qt::AlignCenter);
70         const int horizontalSpacing = style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
71         hLayout->addSpacing(horizontalSpacing);
72 
73         QVBoxLayout *textVLayout = new QVBoxLayout;
74         questionLabel = new KSqueezedTextLabel(this);
75         textVLayout->addWidget(questionLabel);
76 
77         fileNameLabel = new QLabel(this);
78         fileNameLabel->hide();
79         textVLayout->addWidget(fileNameLabel);
80 
81         QMimeDatabase db;
82         mime = db.mimeTypeForName(mimeType);
83         QString mimeDescription(mimeType);
84         if (mime.isValid()) {
85             // Always prefer the mime-type comment over the raw type for display
86             mimeDescription = (mime.comment().isEmpty() ? mime.name() : mime.comment());
87         }
88         QLabel *mimeTypeLabel = new QLabel(this);
89         mimeTypeLabel->setText(i18nc("@label Type of file", "Type: %1", mimeDescription));
90         mimeTypeLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
91         textVLayout->addWidget(mimeTypeLabel);
92 
93         hLayout->addLayout(textVLayout, 5);
94 
95         mainLayout->addStretch(15);
96         dontAskAgainCheckBox = new QCheckBox(this);
97         dontAskAgainCheckBox->setText(i18nc("@label:checkbox", "Remember action for files of this type"));
98         mainLayout->addWidget(dontAskAgainCheckBox);
99 
100         buttonBox = new QDialogButtonBox(this);
101 
102         saveButton = buttonBox->addButton(QDialogButtonBox::Yes);
103         saveButton->setObjectName(QStringLiteral("saveButton"));
104         KGuiItem::assign(saveButton, KStandardGuiItem::saveAs());
105         saveButton->setDefault(true);
106 
107         openDefaultButton = new QPushButton;
108         openDefaultButton->setObjectName(QStringLiteral("openDefaultButton"));
109         buttonBox->addButton(openDefaultButton, QDialogButtonBox::ActionRole);
110 
111         openWithButton = new QPushButton;
112         openWithButton->setObjectName(QStringLiteral("openWithButton"));
113         buttonBox->addButton(openWithButton, QDialogButtonBox::ActionRole);
114 
115         QPushButton *cancelButton = buttonBox->addButton(QDialogButtonBox::Cancel);
116         cancelButton->setObjectName(QStringLiteral("cancelButton"));
117 
118         connect(saveButton, &QPushButton::clicked, this, &BrowserOpenOrSaveQuestionPrivate::slotYesClicked);
119         connect(openDefaultButton, &QPushButton::clicked, this, &BrowserOpenOrSaveQuestionPrivate::slotOpenDefaultClicked);
120         connect(openWithButton, &QPushButton::clicked, this, &BrowserOpenOrSaveQuestionPrivate::slotOpenWithClicked);
121         connect(buttonBox, &QDialogButtonBox::rejected, this, &BrowserOpenOrSaveQuestionPrivate::reject);
122 
123         mainLayout->addWidget(buttonBox);
124     }
125 
126     bool autoEmbedMimeType(int flags);
127 
executeDialog(const QString & dontShowAgainName)128     int executeDialog(const QString &dontShowAgainName)
129     {
130         KConfigGroup cg(dontAskConfig, "Notification Messages"); // group name comes from KMessageBox
131         const QString dontAsk = cg.readEntry(dontShowAgainName, QString()).toLower();
132         if (dontAsk == QLatin1String("yes") || dontAsk == QLatin1String("true")) {
133             return Save;
134         } else if (dontAsk == QLatin1String("no") || dontAsk == QLatin1String("false")) {
135             return OpenDefault;
136         }
137 
138         const int result = exec();
139 
140         if (dontAskAgainCheckBox->isChecked()) {
141             cg.writeEntry(dontShowAgainName, result == BrowserOpenOrSaveQuestion::Save);
142             cg.sync();
143         }
144         return result;
145     }
146 
showService(KService::Ptr selectedService)147     void showService(KService::Ptr selectedService)
148     {
149         KGuiItem openItem(i18nc("@label:button", "&Open with %1", selectedService->name()), selectedService->icon());
150         KGuiItem::assign(openWithButton, openItem);
151     }
152 
153     QUrl url;
154     QString mimeType;
155     QMimeType mime;
156     KService::Ptr selectedService;
157     KSqueezedTextLabel *questionLabel;
158     BrowserOpenOrSaveQuestion::Features features;
159     QLabel *fileNameLabel;
160     QDialogButtonBox *buttonBox;
161     QPushButton *saveButton;
162     QPushButton *openDefaultButton;
163     QPushButton *openWithButton;
164 
165 private:
166     QCheckBox *dontAskAgainCheckBox;
167     KSharedConfig::Ptr dontAskConfig;
168 
169 public Q_SLOTS:
reject()170     void reject() override
171     {
172         selectedService = nullptr;
173         QDialog::reject();
174     }
175 
slotYesClicked()176     void slotYesClicked()
177     {
178         selectedService = nullptr;
179         done(Save);
180     }
181 
slotOpenDefaultClicked()182     void slotOpenDefaultClicked()
183     {
184         done(OpenDefault);
185     }
186 
slotOpenWithClicked()187     void slotOpenWithClicked()
188     {
189         if (!openWithButton->menu()) {
190             selectedService = nullptr;
191             done(OpenWith);
192         }
193     }
194 
slotAppSelected(QAction * action)195     void slotAppSelected(QAction *action)
196     {
197         selectedService = action->data().value<KService::Ptr>();
198         // showService(selectedService);
199         done(OpenDefault);
200     }
201 };
202 
BrowserOpenOrSaveQuestion(QWidget * parent,const QUrl & url,const QString & mimeType)203 BrowserOpenOrSaveQuestion::BrowserOpenOrSaveQuestion(QWidget *parent, const QUrl &url, const QString &mimeType)
204     : d(new BrowserOpenOrSaveQuestionPrivate(parent, url, mimeType))
205 {
206 }
207 
208 BrowserOpenOrSaveQuestion::~BrowserOpenOrSaveQuestion() = default;
209 
createAppAction(const KService::Ptr & service,QObject * parent)210 static QAction *createAppAction(const KService::Ptr &service, QObject *parent)
211 {
212     QString actionName(service->name().replace(QLatin1Char('&'), QLatin1String("&&")));
213     actionName = i18nc("@action:inmenu", "Open &with %1", actionName);
214 
215     QAction *act = new QAction(parent);
216     act->setIcon(QIcon::fromTheme(service->icon()));
217     act->setText(actionName);
218     act->setData(QVariant::fromValue(service));
219     return act;
220 }
221 
askOpenOrSave()222 BrowserOpenOrSaveQuestion::Result BrowserOpenOrSaveQuestion::askOpenOrSave()
223 {
224     d->questionLabel->setText(i18nc("@info", "Open '%1'?", d->url.toDisplayString(QUrl::PreferLocalFile)));
225     d->questionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
226     d->openWithButton->hide();
227 
228     KGuiItem openWithDialogItem(i18nc("@label:button", "&Open with..."), QStringLiteral("document-open"));
229 
230     // I thought about using KFileItemActions, but we don't want a submenu, nor the slots....
231     // and we want no menu at all if there's only one offer.
232     // TODO: we probably need a setTraderConstraint(), to exclude the current application?
233     const KService::List apps = KFileItemActions::associatedApplications(QStringList() << d->mimeType, QString() /* TODO trader constraint */);
234     if (apps.isEmpty()) {
235         KGuiItem::assign(d->openDefaultButton, openWithDialogItem);
236     } else {
237         KService::Ptr offer = apps.first();
238         KGuiItem openItem(i18nc("@label:button", "&Open with %1", offer->name()), offer->icon());
239         KGuiItem::assign(d->openDefaultButton, openItem);
240         if (d->features & ServiceSelection) {
241             // OpenDefault shall use this service
242             d->selectedService = apps.first();
243             d->openWithButton->show();
244             QMenu *menu = new QMenu(d.get());
245             if (apps.count() > 1) {
246                 // Provide an additional button with a menu of associated apps
247                 KGuiItem openWithItem(i18nc("@label:button", "&Open with"), QStringLiteral("document-open"));
248                 KGuiItem::assign(d->openWithButton, openWithItem);
249                 d->openWithButton->setMenu(menu);
250                 QObject::connect(menu, &QMenu::triggered, d.get(), &BrowserOpenOrSaveQuestionPrivate::slotAppSelected);
251                 for (const auto &app : apps) {
252                     QAction *act = createAppAction(app, d.get());
253                     menu->addAction(act);
254                 }
255                 QAction *openWithDialogAction = new QAction(d.get());
256                 openWithDialogAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
257                 openWithDialogAction->setText(openWithDialogItem.text());
258                 menu->addAction(openWithDialogAction);
259             } else {
260                 // Only one associated app, already offered by the other menu -> add "Open With..." button
261                 KGuiItem::assign(d->openWithButton, openWithDialogItem);
262             }
263         } else {
264             // qDebug() << "Not using new feature ServiceSelection; port the caller to BrowserOpenOrSaveQuestion::setFeature(ServiceSelection)";
265         }
266     }
267 
268     // KEEP IN SYNC with kdebase/runtime/keditfiletype/filetypedetails.cpp!!!
269     const QString dontAskAgain = QLatin1String("askSave") + d->mimeType;
270 
271     const int choice = d->executeDialog(dontAskAgain);
272     return choice == BrowserOpenOrSaveQuestionPrivate::Save ? Save : (choice == BrowserOpenOrSaveQuestionPrivate::Cancel ? Cancel : Open);
273 }
274 
selectedService() const275 KService::Ptr BrowserOpenOrSaveQuestion::selectedService() const
276 {
277     return d->selectedService;
278 }
279 
autoEmbedMimeType(int flags)280 bool BrowserOpenOrSaveQuestionPrivate::autoEmbedMimeType(int flags)
281 {
282     // SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC
283     // NOTE: Keep this function in sync with
284     // kdebase/runtime/keditfiletype/filetypedetails.cpp
285     //       FileTypeDetails::updateAskSave()
286 
287     // Don't ask for:
288     // - html (even new tabs would ask, due to about:blank!)
289     // - dirs obviously (though not common over HTTP :),
290     // - images (reasoning: no need to save, most of the time, because fast to see)
291     // e.g. postscript is different, because takes longer to read, so
292     // it's more likely that the user might want to save it.
293     // - multipart/* ("server push", see kmultipart)
294     // KEEP IN SYNC!!!
295     // clang-format off
296     if (flags != static_cast<int>(BrowserOpenOrSaveQuestion::AttachmentDisposition) && mime.isValid() && (
297                 mime.inherits(QStringLiteral("text/html")) ||
298                 mime.inherits(QStringLiteral("application/xml")) ||
299                 mime.inherits(QStringLiteral("inode/directory")) ||
300                 mimeType.startsWith(QLatin1String("image")) ||
301                 mime.inherits(QStringLiteral("multipart/x-mixed-replace")) ||
302                 mime.inherits(QStringLiteral("multipart/replace")))) {
303         return true;
304     }
305     // clang-format on
306     return false;
307 }
308 
askEmbedOrSave(int flags)309 BrowserOpenOrSaveQuestion::Result BrowserOpenOrSaveQuestion::askEmbedOrSave(int flags)
310 {
311     if (d->autoEmbedMimeType(flags)) {
312         return Embed;
313     }
314 
315     // don't use KStandardGuiItem::open() here which has trailing ellipsis!
316     KGuiItem::assign(d->openDefaultButton, KGuiItem(i18nc("@label:button", "&Open"), QStringLiteral("document-open")));
317     d->openWithButton->hide();
318 
319     d->questionLabel->setText(i18nc("@info", "Open '%1'?", d->url.toDisplayString(QUrl::PreferLocalFile)));
320     d->questionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
321 
322     const QString dontAskAgain = QLatin1String("askEmbedOrSave") + d->mimeType; // KEEP IN SYNC!!!
323     // SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC SYNC
324 
325     const int choice = d->executeDialog(dontAskAgain);
326     return choice == BrowserOpenOrSaveQuestionPrivate::Save ? Save : (choice == BrowserOpenOrSaveQuestionPrivate::Cancel ? Cancel : Embed);
327 }
328 
setFeatures(Features features)329 void BrowserOpenOrSaveQuestion::setFeatures(Features features)
330 {
331     d->features = features;
332 }
333 
setSuggestedFileName(const QString & suggestedFileName)334 void BrowserOpenOrSaveQuestion::setSuggestedFileName(const QString &suggestedFileName)
335 {
336     if (suggestedFileName.isEmpty()) {
337         return;
338     }
339 
340     d->fileNameLabel->setText(i18nc("@label File name", "Name: %1", suggestedFileName));
341     d->fileNameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
342     d->fileNameLabel->setWhatsThis(i18nc("@info:whatsthis", "This is the file name suggested by the server"));
343     d->fileNameLabel->show();
344 }
345 
346 #include "browseropenorsavequestion.moc"
347