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 | |
29 | class KImageFilePreviewPrivate |
30 | { |
31 | public: |
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 | |
62 | KImageFilePreview::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 | |
87 | KImageFilePreview::~KImageFilePreview() |
88 | { |
89 | if (d->m_job) { |
90 | d->m_job->kill(); |
91 | } |
92 | } |
93 | |
94 | void 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 |
102 | void KImageFilePreview::showPreview(const QUrl &url) |
103 | { |
104 | showPreview(url, force: false); |
105 | } |
106 | |
107 | void 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 | |
145 | void 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 | |
157 | QSize KImageFilePreview::sizeHint() const |
158 | { |
159 | return QSize(100, 200); |
160 | } |
161 | |
162 | KIO::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 | |
177 | void 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 | |
198 | void 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 | |
207 | void KImageFilePreviewPrivate::slotResult(KJob *job) |
208 | { |
209 | if (job == m_job) { |
210 | m_job = nullptr; |
211 | } |
212 | } |
213 | |
214 | void 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 | |
245 | void 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 | |
257 | void 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 | |