| 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 | |