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