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