1 /*
2 ark: A program for modifying archives via a GUI.
3
4 SPDX-FileCopyrightText: 2004-2008 Henrique Pinto <henrique.pinto@kdemail.net>
5
6 SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 #include "arkviewer.h"
10 #include "ark_debug.h"
11
12 #include <KIO/JobUiDelegate>
13 #include <KIO/ApplicationLauncherJob>
14 #include <KLocalizedString>
15 #include <KMimeTypeTrader>
16 #include <KMessageBox>
17 #include <KParts/OpenUrlArguments>
18 #include <KXMLGUIFactory>
19 #include <KActionCollection>
20 #include <KAboutPluginDialog>
21 #include <KPluginMetaData>
22
23 #include <QFile>
24 #include <QMimeDatabase>
25 #include <QProgressDialog>
26 #include <QPushButton>
27 #include <QStyle>
28 #include <QAction>
29
30 #include <algorithm>
31
ArkViewer()32 ArkViewer::ArkViewer()
33 : KParts::MainWindow()
34 {
35 setupUi(this);
36
37 KStandardAction::close(this, &QMainWindow::close, actionCollection());
38
39 QAction *closeAction = actionCollection()->addAction(QStringLiteral("close"), this, &ArkViewer::close);
40 closeAction->setShortcut(Qt::Key_Escape);
41
42 setXMLFile(QStringLiteral("ark_viewer.rc"));
43 setupGUI(ToolBar);
44 }
45
~ArkViewer()46 ArkViewer::~ArkViewer()
47 {
48 if (m_part) {
49 QProgressDialog progressDialog(this);
50 progressDialog.setWindowTitle(i18n("Closing preview"));
51 progressDialog.setLabelText(i18n("Please wait while the preview is being closed..."));
52
53 progressDialog.setMinimumDuration(500);
54 progressDialog.setModal(true);
55 progressDialog.setCancelButton(nullptr);
56 progressDialog.setRange(0, 0);
57
58 // #261785: this preview dialog is not modal, so we need to delete
59 // the previewed file ourselves when the dialog is closed;
60
61 m_part.data()->closeUrl();
62
63 if (!m_fileName.isEmpty()) {
64 QFile::remove(m_fileName);
65 }
66 }
67
68 guiFactory()->removeClient(m_part);
69 delete m_part;
70 }
71
openExternalViewer(const KService::Ptr viewer,const QString & fileName)72 void ArkViewer::openExternalViewer(const KService::Ptr viewer, const QString& fileName)
73 {
74 qCDebug(ARK) << "Using external viewer";
75
76 const QList<QUrl> fileUrlList = {QUrl::fromLocalFile(fileName)};
77 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(viewer);
78 job->setUrls(fileUrlList);
79 job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
80 // The temporary file will be removed when the viewer application exits.
81 job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
82 job->start();
83 }
84
openInternalViewer(const KService::Ptr viewer,const QString & fileName,const QMimeType & mimeType)85 void ArkViewer::openInternalViewer(const KService::Ptr viewer, const QString& fileName, const QMimeType& mimeType)
86 {
87 qCDebug(ARK) << "Opening internal viewer";
88
89 ArkViewer *internalViewer = new ArkViewer();
90 internalViewer->show();
91 if (internalViewer->viewInInternalViewer(viewer, fileName, mimeType)) {
92 // The internal viewer is showing the file, and will
93 // remove the temporary file in its destructor. So there
94 // is no more to do here.
95 return;
96 }
97 else {
98 KMessageBox::sorry(nullptr, i18n("The internal viewer cannot preview this file."));
99 delete internalViewer;
100
101 qCDebug(ARK) << "Removing temporary file:" << fileName;
102 QFile::remove(fileName);
103 }
104 }
105
askViewAsPlainText(const QMimeType & mimeType)106 bool ArkViewer::askViewAsPlainText(const QMimeType& mimeType)
107 {
108 int response;
109 if (!mimeType.isDefault()) {
110 // File has a defined MIME type, and not the default
111 // application/octet-stream. So it could be viewable as
112 // plain text, ask the user.
113 response = KMessageBox::warningContinueCancel(nullptr,
114 xi18n("The internal viewer cannot preview this type of file<nl/>(%1).<nl/><nl/>Do you want to try to view it as plain text?", mimeType.name()),
115 i18nc("@title:window", "Cannot Preview File"),
116 KGuiItem(i18nc("@action:button", "Preview as Text"), QIcon::fromTheme(QStringLiteral("text-plain"))),
117 KStandardGuiItem::cancel(),
118 QStringLiteral("PreviewAsText_%1").arg(mimeType.name()));
119 }
120 else {
121 // No defined MIME type, or the default application/octet-stream.
122 // There is still a possibility that it could be viewable as plain
123 // text, so ask the user. Not the same as the message/question
124 // above, because the wording and default are different.
125 response = KMessageBox::warningContinueCancel(nullptr,
126 xi18n("The internal viewer cannot preview this unknown type of file.<nl/><nl/>Do you want to try to view it as plain text?"),
127 i18nc("@title:window", "Cannot Preview File"),
128 KGuiItem(i18nc("@action:button", "Preview as Text"), QIcon::fromTheme(QStringLiteral("text-plain"))),
129 KStandardGuiItem::cancel(),
130 QString(),
131 KMessageBox::Dangerous);
132 }
133
134 return response != KMessageBox::Cancel;
135 }
136
view(const QString & fileName)137 void ArkViewer::view(const QString& fileName)
138 {
139 QMimeDatabase db;
140 QMimeType mimeType = db.mimeTypeForFile(fileName);
141 qCDebug(ARK) << "viewing" << fileName << "with mime type:" << mimeType.name();
142
143 const KService::Ptr internalViewer = ArkViewer::getInternalViewer(mimeType.name());
144
145 if (internalViewer) {
146 openInternalViewer(internalViewer, fileName, mimeType);
147 return;
148 }
149
150 const KService::Ptr externalViewer = ArkViewer::getExternalViewer(mimeType.name());
151
152 if (externalViewer) {
153 openExternalViewer(externalViewer, fileName);
154 return;
155 }
156
157 // No internal or external viewer available for the file. Ask the user if it
158 // should be previewed as text/plain.
159 if (askViewAsPlainText(mimeType)) {
160 const KService::Ptr textViewer = ArkViewer::getInternalViewer(QStringLiteral("text/plain"));
161 openInternalViewer(textViewer, fileName, db.mimeTypeForName(QStringLiteral("text/plain")));
162 } else {
163 qCDebug(ARK) << "Removing temporary file:" << fileName;
164 QFile::remove(fileName);
165 }
166 }
167
viewInInternalViewer(const KService::Ptr viewer,const QString & fileName,const QMimeType & mimeType)168 bool ArkViewer::viewInInternalViewer(const KService::Ptr viewer, const QString& fileName, const QMimeType &mimeType)
169 {
170 setWindowFilePath(fileName);
171
172 // Set icon and comment for the mimetype.
173 m_iconLabel->setPixmap(QIcon::fromTheme(mimeType.iconName()).pixmap(style()->pixelMetric(QStyle::PixelMetric::PM_SmallIconSize)));
174 m_commentLabel->setText(mimeType.comment());
175
176 // Create the ReadOnlyPart instance.
177 m_part = viewer->createInstance<KParts::ReadOnlyPart>(this, this);
178
179 if (!m_part.data()) {
180 return false;
181 }
182
183 // Insert the KPart into its placeholder.
184 mainLayout->insertWidget(0, m_part->widget());
185
186 QAction* action = actionCollection()->addAction(QStringLiteral("help_about_kpart"));
187 const KPluginMetaData partMetaData = m_part->metaData();
188 const QString iconName = partMetaData.iconName();
189 if (!iconName.isEmpty()) {
190 action->setIcon(QIcon::fromTheme(iconName));
191 }
192 action->setText(i18nc("@action", "About Viewer Component"));
193 connect(action, &QAction::triggered, this, &ArkViewer::aboutKPart);
194
195 createGUI(m_part.data());
196 setAutoSaveSettings(QStringLiteral("Viewer"), true);
197
198 m_part.data()->openUrl(QUrl::fromLocalFile(fileName));
199 m_part.data()->widget()->setFocus();
200 m_fileName = fileName;
201
202 return true;
203 }
204
getExternalViewer(const QString & mimeType)205 KService::Ptr ArkViewer::getExternalViewer(const QString &mimeType)
206 {
207 KService::List offers = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("Application"));
208
209 if (!offers.isEmpty()) {
210 return offers.first();
211 } else {
212 return KService::Ptr();
213 }
214 }
215
getInternalViewer(const QString & mimeType)216 KService::Ptr ArkViewer::getInternalViewer(const QString& mimeType)
217 {
218 // No point in even trying to find anything for application/octet-stream
219 if (mimeType == QLatin1String("application/octet-stream")) {
220 return KService::Ptr();
221 }
222
223 // Try to get a read-only kpart for the internal viewer
224 KService::List offers = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("KParts/ReadOnlyPart"));
225
226 auto arkPartIt = std::find_if(offers.begin(), offers.end(), [](KService::Ptr service) {
227 return service->storageId() == QLatin1String("ark_part.desktop");
228 });
229
230 // Use the Ark part only when the mime type matches an archive type directly.
231 // Many file types (e.g. Open Document) are technically just archives
232 // but browsing their internals is typically not what the user wants.
233 if (arkPartIt != offers.end()) {
234 // Not using hasMimeType() as we're explicitly not interested in inheritance.
235 if (!(*arkPartIt)->mimeTypes().contains(mimeType)) {
236 offers.erase(arkPartIt);
237 }
238 }
239
240 // Skip the KHTML part
241 auto khtmlPart = std::find_if(offers.begin(), offers.end(), [](KService::Ptr service) {
242 return service->desktopEntryName() == QLatin1String("khtml");
243 });
244
245 if (khtmlPart != offers.end()) {
246 offers.erase(khtmlPart);
247 }
248
249 if (!offers.isEmpty()) {
250 return offers.first();
251 } else {
252 return KService::Ptr();
253 }
254 }
255
aboutKPart()256 void ArkViewer::aboutKPart()
257 {
258 if (!m_part) {
259 return;
260 }
261
262 auto *dialog = new KAboutPluginDialog(m_part->metaData(), this);
263 dialog->setAttribute(Qt::WA_DeleteOnClose);
264 dialog->show();
265 }
266