1 | // -*- c++ -*- |
2 | /* |
3 | This file is part of the KDE libraries |
4 | SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org> |
5 | SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org> |
6 | SPDX-FileCopyrightText: 2001 Malte Starostik <malte.starostik@t-online.de> |
7 | |
8 | SPDX-License-Identifier: LGPL-2.0-or-later |
9 | */ |
10 | |
11 | #include "previewjob.h" |
12 | #include "filepreviewjob.h" |
13 | #include "kiogui_debug.h" |
14 | #include "kprotocolinfo.h" |
15 | |
16 | #include <KConfigGroup> |
17 | #include <KSharedConfig> |
18 | #include <QMetaMethod> |
19 | #include <QMimeDatabase> |
20 | #include <QPixmap> |
21 | #include <QStandardPaths> |
22 | |
23 | #include "job_p.h" |
24 | |
25 | #ifdef WITH_QTDBUS |
26 | #include <QDBusConnection> |
27 | #include <QDBusError> |
28 | |
29 | #endif |
30 | |
31 | namespace |
32 | { |
33 | static qreal s_defaultDevicePixelRatio = 1.0; |
34 | } |
35 | |
36 | using namespace KIO; |
37 | |
38 | class KIO::PreviewJobPrivate : public KIO::JobPrivate |
39 | { |
40 | public: |
41 | PreviewJobPrivate(const KFileItemList &items, const QSize &size) |
42 | : initialItems(items) |
43 | , size(size) |
44 | , scaleType(PreviewJob::ScaleType::ScaledAndCached) |
45 | , ignoreMaximumSize(false) |
46 | , sequenceIndex(0) |
47 | { |
48 | // https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY |
49 | thumbRoot = QStandardPaths::writableLocation(type: QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/" ); |
50 | } |
51 | |
52 | KFileItemList initialItems; |
53 | QStringList enabledPlugins; |
54 | // Our todo list :) |
55 | // We remove the first item at every step, so use std::list |
56 | QSize size; |
57 | std::list<PreviewItem> items; |
58 | // Whether the thumbnail should be scaled ando/or saved |
59 | PreviewJob::ScaleType scaleType; |
60 | bool ignoreMaximumSize; |
61 | int sequenceIndex; |
62 | // Root of thumbnail cache |
63 | QString thumbRoot; |
64 | // Metadata returned from the KIO thumbnail worker |
65 | QMap<QString, QString> thumbnailWorkerMetaData; |
66 | qreal devicePixelRatio = s_defaultDevicePixelRatio; |
67 | // Cache the deviceIdMap so we dont need to stat the files every time |
68 | QMap<QString, int> deviceIdMap; |
69 | |
70 | QMap<QString, KPluginMetaData> mimeMap; |
71 | |
72 | void determineNextFile(); |
73 | void startPreview(); |
74 | |
75 | Q_DECLARE_PUBLIC(PreviewJob) |
76 | |
77 | private: |
78 | QDir createTemporaryDir(); |
79 | int maximumWorkers = 1; |
80 | }; |
81 | |
82 | void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio) |
83 | { |
84 | s_defaultDevicePixelRatio = defaultDevicePixelRatio; |
85 | } |
86 | |
87 | PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) |
88 | : KIO::Job(*new PreviewJobPrivate(items, size)) |
89 | { |
90 | Q_D(PreviewJob); |
91 | |
92 | const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings" )); |
93 | if (enabledPlugins) { |
94 | d->enabledPlugins = *enabledPlugins; |
95 | } else { |
96 | d->enabledPlugins = |
97 | globalConfig.readEntry(key: "Plugins" , |
98 | aDefault: QStringList{QStringLiteral("directorythumbnail" ), QStringLiteral("imagethumbnail" ), QStringLiteral("jpegthumbnail" )}); |
99 | } |
100 | |
101 | d->maximumWorkers = KProtocolInfo::maxWorkers(QStringLiteral("thumbnail" )); |
102 | // Return to event loop first, determineNextFile() might delete this; |
103 | QTimer::singleShot(interval: 0, receiver: this, slot: [d]() { |
104 | d->startPreview(); |
105 | }); |
106 | } |
107 | |
108 | PreviewJob::~PreviewJob() |
109 | { |
110 | } |
111 | |
112 | void PreviewJob::setScaleType(ScaleType type) |
113 | { |
114 | Q_D(PreviewJob); |
115 | d->scaleType = type; |
116 | } |
117 | |
118 | PreviewJob::ScaleType PreviewJob::scaleType() const |
119 | { |
120 | Q_D(const PreviewJob); |
121 | return d->scaleType; |
122 | } |
123 | |
124 | void PreviewJobPrivate::startPreview() |
125 | { |
126 | Q_Q(PreviewJob); |
127 | |
128 | // Load the list of plugins to determine which MIME types are supported |
129 | const QList<KPluginMetaData> plugins = KIO::FilePreviewJob::loadAvailablePlugins(); |
130 | |
131 | for (const KPluginMetaData &plugin : plugins) { |
132 | bool pluginIsEnabled = enabledPlugins.contains(str: plugin.pluginId()); |
133 | const auto mimeTypes = plugin.mimeTypes(); |
134 | for (const QString &mimeType : mimeTypes) { |
135 | if (pluginIsEnabled && !mimeMap.contains(key: mimeType)) { |
136 | mimeMap.insert(key: mimeType, value: plugin); |
137 | } |
138 | } |
139 | } |
140 | |
141 | for (const auto &fileItem : std::as_const(t&: initialItems)) { |
142 | PreviewItem previewItem; |
143 | previewItem.item = fileItem; |
144 | previewItem.devicePixelRatio = devicePixelRatio; |
145 | previewItem.sequenceIndex = sequenceIndex; |
146 | previewItem.ignoreMaximumSize = ignoreMaximumSize; |
147 | previewItem.scaleType = scaleType; |
148 | previewItem.size = size; |
149 | previewItem.deviceIdMap = deviceIdMap; |
150 | items.push_back(x: previewItem); |
151 | } |
152 | |
153 | initialItems.clear(); |
154 | determineNextFile(); |
155 | } |
156 | |
157 | void PreviewJob::removeItem(const QUrl &url) |
158 | { |
159 | Q_D(PreviewJob); |
160 | |
161 | auto it = std::find_if(first: d->items.cbegin(), last: d->items.cend(), pred: [&url](const PreviewItem &pItem) { |
162 | return url == pItem.item.url(); |
163 | }); |
164 | if (it != d->items.cend()) { |
165 | d->items.erase(position: it); |
166 | } |
167 | |
168 | for (auto subjob : subjobs()) { |
169 | FilePreviewJob *previewJob = static_cast<KIO::FilePreviewJob *>(subjob); |
170 | if (previewJob && previewJob->item().item.url() == url) { |
171 | subjob->kill(); |
172 | removeSubjob(job: subjob); |
173 | d->determineNextFile(); |
174 | break; |
175 | } |
176 | } |
177 | } |
178 | |
179 | void KIO::PreviewJob::setSequenceIndex(int index) |
180 | { |
181 | d_func()->sequenceIndex = index; |
182 | } |
183 | |
184 | int KIO::PreviewJob::sequenceIndex() const |
185 | { |
186 | return d_func()->sequenceIndex; |
187 | } |
188 | |
189 | float KIO::PreviewJob::sequenceIndexWraparoundPoint() const |
190 | { |
191 | return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint" ), QStringLiteral("-1.0" )).toFloat(); |
192 | } |
193 | |
194 | bool KIO::PreviewJob::handlesSequences() const |
195 | { |
196 | return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("handlesSequences" )) == QStringLiteral("1" ); |
197 | } |
198 | |
199 | void KIO::PreviewJob::setDevicePixelRatio(qreal dpr) |
200 | { |
201 | d_func()->devicePixelRatio = dpr; |
202 | } |
203 | |
204 | void PreviewJob::setIgnoreMaximumSize(bool ignoreSize) |
205 | { |
206 | d_func()->ignoreMaximumSize = ignoreSize; |
207 | } |
208 | |
209 | void PreviewJobPrivate::determineNextFile() |
210 | { |
211 | Q_Q(PreviewJob); |
212 | |
213 | if (q->subjobs().count() == 0 && items.empty()) { |
214 | q->emitResult(); |
215 | return; |
216 | } |
217 | |
218 | const int jobsToRun = qMin(a: (int)items.size(), b: maximumWorkers - q->subjobs().count()); |
219 | for (int i = 0; i < jobsToRun; i++) { |
220 | auto item = items.front(); |
221 | items.pop_front(); |
222 | FilePreviewJob *job = KIO::filePreviewJob(item, thumbRoot, mimeMap); |
223 | q->addSubjob(job); |
224 | job->start(); |
225 | } |
226 | } |
227 | |
228 | void PreviewJob::slotResult(KJob *job) |
229 | { |
230 | Q_D(PreviewJob); |
231 | FilePreviewJob *previewJob = static_cast<KIO::FilePreviewJob *>(job); |
232 | if (previewJob) { |
233 | auto fileItem = previewJob->item().item; |
234 | if (!previewJob->previewImage().isNull()) { |
235 | d->thumbnailWorkerMetaData = previewJob->thumbnailWorkerMetaData(); |
236 | d->deviceIdMap = previewJob->deviceIdMap(); |
237 | auto previewImage = previewJob->previewImage(); |
238 | Q_EMIT generated(item: fileItem, preview: previewImage); |
239 | if (isSignalConnected(signal: QMetaMethod::fromSignal(signal: &PreviewJob::gotPreview))) { |
240 | QPixmap pixmap = QPixmap::fromImage(image: previewImage); |
241 | pixmap.setDevicePixelRatio(d->devicePixelRatio); |
242 | Q_EMIT gotPreview(item: fileItem, preview: pixmap); |
243 | } |
244 | } else { |
245 | Q_EMIT failed(item: fileItem); |
246 | } |
247 | } |
248 | removeSubjob(job); |
249 | if (job->error() > 0) { |
250 | qCWarning(KIO_GUI) << "PreviewJob subjob had an error:" << job->errorString(); |
251 | } |
252 | d->determineNextFile(); |
253 | } |
254 | |
255 | QList<KPluginMetaData> PreviewJob::availableThumbnailerPlugins() |
256 | { |
257 | return FilePreviewJob::loadAvailablePlugins(); |
258 | } |
259 | |
260 | QStringList PreviewJob::availablePlugins() |
261 | { |
262 | QStringList result; |
263 | const auto plugins = KIO::FilePreviewJob::loadAvailablePlugins(); |
264 | for (const KPluginMetaData &plugin : plugins) { |
265 | result << plugin.pluginId(); |
266 | } |
267 | return result; |
268 | } |
269 | |
270 | QStringList PreviewJob::defaultPlugins() |
271 | { |
272 | const QStringList exclusionList = QStringList() << QStringLiteral("textthumbnail" ); |
273 | |
274 | QStringList defaultPlugins = availablePlugins(); |
275 | for (const QString &plugin : exclusionList) { |
276 | defaultPlugins.removeAll(t: plugin); |
277 | } |
278 | |
279 | return defaultPlugins; |
280 | } |
281 | |
282 | QStringList PreviewJob::supportedMimeTypes() |
283 | { |
284 | QStringList result; |
285 | const auto plugins = KIO::FilePreviewJob::loadAvailablePlugins(); |
286 | for (const KPluginMetaData &plugin : plugins) { |
287 | result += plugin.mimeTypes(); |
288 | } |
289 | return result; |
290 | } |
291 | |
292 | PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins) |
293 | { |
294 | return new PreviewJob(items, size, enabledPlugins); |
295 | } |
296 | |
297 | #include "moc_previewjob.cpp" |
298 | |