1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2001 Martin R. Jones <mjones@kde.org>
4 SPDX-FileCopyrightText: 2001 Carsten Pfeiffer <pfeiffer@kde.org>
5 SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-only
8*/
9
10#include "kimagefilepreview.h"
11
12#include <QCheckBox>
13#include <QLabel>
14#include <QPainter>
15#include <QResizeEvent>
16#include <QStyle>
17#include <QTimeLine>
18#include <QVBoxLayout>
19
20#include <KConfig>
21#include <KConfigGroup>
22#include <KIconLoader>
23#include <KLocalizedString>
24#include <kfileitem.h>
25#include <kio/previewjob.h>
26
27/**** KImageFilePreview ****/
28
29class KImageFilePreviewPrivate
30{
31public:
32 KImageFilePreviewPrivate(KImageFilePreview *qq)
33 : q(qq)
34 {
35 if (q->style()->styleHint(stylehint: QStyle::SH_Widget_Animate, opt: nullptr, widget: q)) {
36 m_timeLine = new QTimeLine(150, q);
37 m_timeLine->setEasingCurve(QEasingCurve::InCurve);
38 m_timeLine->setDirection(QTimeLine::Forward);
39 m_timeLine->setFrameRange(startFrame: 0, endFrame: 100);
40 }
41 }
42
43 void slotResult(KJob *);
44 void slotFailed(const KFileItem &);
45 void slotStepAnimation();
46 void slotFinished();
47 void slotActuallyClear();
48
49 KImageFilePreview *q = nullptr;
50 QUrl currentURL;
51 QUrl lastShownURL;
52 QLabel *imageLabel;
53 KIO::PreviewJob *m_job = nullptr;
54 QTimeLine *m_timeLine = nullptr;
55 QPixmap m_pmCurrent;
56 QPixmap m_pmTransition;
57 float m_pmCurrentOpacity = 1;
58 float m_pmTransitionOpacity = 0;
59 bool clear = true;
60};
61
62KImageFilePreview::KImageFilePreview(QWidget *parent)
63 : KPreviewWidgetBase(parent)
64 , d(new KImageFilePreviewPrivate(this))
65{
66 QVBoxLayout *vb = new QVBoxLayout(this);
67 vb->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
68
69 d->imageLabel = new QLabel(this);
70 d->imageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
71 d->imageLabel->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
72 vb->addWidget(d->imageLabel);
73
74 setSupportedMimeTypes(KIO::PreviewJob::supportedMimeTypes());
75 setMinimumWidth(50);
76
77 if (d->m_timeLine) {
78 connect(sender: d->m_timeLine, signal: &QTimeLine::frameChanged, context: this, slot: [this]() {
79 d->slotStepAnimation();
80 });
81 connect(sender: d->m_timeLine, signal: &QTimeLine::finished, context: this, slot: [this]() {
82 d->slotFinished();
83 });
84 }
85}
86
87KImageFilePreview::~KImageFilePreview()
88{
89 if (d->m_job) {
90 d->m_job->kill();
91 }
92}
93
94void KImageFilePreview::showPreview()
95{
96 // Pass a copy since clearPreview() will clear currentURL
97 QUrl url = d->currentURL;
98 showPreview(url, force: true);
99}
100
101// called via KPreviewWidgetBase interface
102void KImageFilePreview::showPreview(const QUrl &url)
103{
104 showPreview(url, force: false);
105}
106
107void KImageFilePreview::showPreview(const QUrl &url, bool force)
108{
109 /* clang-format off */
110 if (!url.isValid()
111 || (d->lastShownURL.isValid()
112 && url.matches(url: d->lastShownURL, options: QUrl::StripTrailingSlash)
113 && d->currentURL.isValid())) {
114 return;
115 }
116 /* clang-format on*/
117
118 d->clear = false;
119 d->currentURL = url;
120 d->lastShownURL = url;
121
122 int w = d->imageLabel->contentsRect().width() - 4;
123 int h = d->imageLabel->contentsRect().height() - 4;
124
125 if (d->m_job) {
126 disconnect(sender: d->m_job, signal: nullptr, receiver: this, member: nullptr);
127
128 d->m_job->kill();
129 }
130
131 d->m_job = createJob(url, width: w, height: h);
132 if (force) { // explicitly requested previews shall always be generated!
133 d->m_job->setIgnoreMaximumSize(true);
134 }
135
136 connect(sender: d->m_job, signal: &KJob::result, context: this, slot: [this](KJob *job) {
137 d->slotResult(job);
138 });
139 connect(sender: d->m_job, signal: &KIO::PreviewJob::gotPreview, context: this, slot: &KImageFilePreview::gotPreview);
140 connect(sender: d->m_job, signal: &KIO::PreviewJob::failed, context: this, slot: [this](const KFileItem &item) {
141 d->slotFailed(item);
142 });
143}
144
145void KImageFilePreview::resizeEvent(QResizeEvent *)
146{
147 // Nothing to do, if no current preview
148 if (d->imageLabel->pixmap().isNull()) {
149 return;
150 }
151
152 clearPreview();
153 d->currentURL = QUrl(); // force this to actually happen
154 showPreview(url: d->lastShownURL);
155}
156
157QSize KImageFilePreview::sizeHint() const
158{
159 return QSize(100, 200);
160}
161
162KIO::PreviewJob *KImageFilePreview::createJob(const QUrl &url, int w, int h)
163{
164 if (!url.isValid()) {
165 return nullptr;
166 }
167
168 KFileItemList items;
169 items.append(t: KFileItem(url));
170 QStringList plugins = KIO::PreviewJob::availablePlugins();
171
172 KIO::PreviewJob *previewJob = KIO::filePreview(items, size: QSize(w, h), enabledPlugins: &plugins);
173 previewJob->setScaleType(KIO::PreviewJob::Scaled);
174 return previewJob;
175}
176
177void KImageFilePreview::gotPreview(const KFileItem &item, const QPixmap &pm)
178{
179 if (item.url() != d->currentURL) { // Shouldn't happen
180 return;
181 }
182
183 if (d->m_timeLine) {
184 if (d->m_timeLine->state() == QTimeLine::Running) {
185 d->m_timeLine->setCurrentTime(0);
186 }
187
188 d->m_pmTransition = pm;
189 d->m_pmTransitionOpacity = 0;
190 d->m_pmCurrentOpacity = 1;
191 d->m_timeLine->setDirection(QTimeLine::Forward);
192 d->m_timeLine->start();
193 } else {
194 d->imageLabel->setPixmap(pm);
195 }
196}
197
198void KImageFilePreviewPrivate::slotFailed(const KFileItem &item)
199{
200 if (item.isDir()) {
201 imageLabel->clear();
202 } else if (item.url() == currentURL) { // should always be the case
203 imageLabel->setPixmap(QIcon::fromTheme(QStringLiteral("image-missing")).pixmap(extent: KIconLoader::SizeLarge, mode: QIcon::Disabled));
204 }
205}
206
207void KImageFilePreviewPrivate::slotResult(KJob *job)
208{
209 if (job == m_job) {
210 m_job = nullptr;
211 }
212}
213
214void KImageFilePreviewPrivate::slotStepAnimation()
215{
216 const QSize currSize = m_pmCurrent.size();
217 const QSize transitionSize = m_pmTransition.size();
218 const int width = std::max(a: currSize.width(), b: transitionSize.width());
219 const int height = std::max(a: currSize.height(), b: transitionSize.height());
220 QPixmap pm(QSize(width, height));
221 pm.fill(fillColor: Qt::transparent);
222
223 QPainter p(&pm);
224 p.setOpacity(m_pmCurrentOpacity);
225
226 // If we have a current pixmap
227 if (!m_pmCurrent.isNull()) {
228 p.drawPixmap(p: QPoint(((float)pm.size().width() - m_pmCurrent.size().width()) / 2.0, ((float)pm.size().height() - m_pmCurrent.size().height()) / 2.0),
229 pm: m_pmCurrent);
230 }
231 if (!m_pmTransition.isNull()) {
232 p.setOpacity(m_pmTransitionOpacity);
233 p.drawPixmap(
234 p: QPoint(((float)pm.size().width() - m_pmTransition.size().width()) / 2.0, ((float)pm.size().height() - m_pmTransition.size().height()) / 2.0),
235 pm: m_pmTransition);
236 }
237 p.end();
238
239 imageLabel->setPixmap(pm);
240
241 m_pmCurrentOpacity = qMax(a: m_pmCurrentOpacity - 0.4, b: 0.0); // krazy:exclude=qminmax
242 m_pmTransitionOpacity = qMin(a: m_pmTransitionOpacity + 0.4, b: 1.0); // krazy:exclude=qminmax
243}
244
245void KImageFilePreviewPrivate::slotFinished()
246{
247 m_pmCurrent = m_pmTransition;
248 m_pmTransitionOpacity = 0;
249 m_pmCurrentOpacity = 1;
250 m_pmTransition = QPixmap();
251 // The animation might have lost some frames. Be sure that if the last one
252 // was dropped, the last image shown is the opaque one.
253 imageLabel->setPixmap(m_pmCurrent);
254 clear = false;
255}
256
257void KImageFilePreview::clearPreview()
258{
259 if (d->m_job) {
260 d->m_job->kill();
261 d->m_job = nullptr;
262 }
263
264 if (d->clear || (d->m_timeLine && d->m_timeLine->state() == QTimeLine::Running)) {
265 return;
266 }
267
268 if (d->m_timeLine) {
269 d->m_pmTransition = QPixmap();
270 // If we add a previous preview then we run the animation
271 if (!d->m_pmCurrent.isNull()) {
272 d->m_timeLine->setCurrentTime(0);
273 d->m_timeLine->setDirection(QTimeLine::Backward);
274 d->m_timeLine->start();
275 }
276 d->currentURL.clear();
277 d->clear = true;
278 } else {
279 d->imageLabel->clear();
280 }
281}
282
283#include "moc_kimagefilepreview.cpp"
284

source code of kio/src/filewidgets/kimagefilepreview.cpp