File indexing completed on 2024-12-01 12:36:08

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2001 Martin R. Jones <mjones@kde.org>
0004     SPDX-FileCopyrightText: 2001 Carsten Pfeiffer <pfeiffer@kde.org>
0005     SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-only
0008 */
0009 
0010 #include "kimagefilepreview.h"
0011 
0012 #include <QCheckBox>
0013 #include <QLabel>
0014 #include <QPainter>
0015 #include <QResizeEvent>
0016 #include <QStyle>
0017 #include <QTimeLine>
0018 #include <QVBoxLayout>
0019 
0020 #include <KConfig>
0021 #include <KConfigGroup>
0022 #include <KIconLoader>
0023 #include <KLocalizedString>
0024 #include <kfileitem.h>
0025 #include <kio/previewjob.h>
0026 
0027 /**** KImageFilePreview ****/
0028 
0029 class KImageFilePreviewPrivate
0030 {
0031 public:
0032     KImageFilePreviewPrivate(KImageFilePreview *qq)
0033         : q(qq)
0034     {
0035         if (q->style()->styleHint(QStyle::SH_Widget_Animate, nullptr, q)) {
0036             m_timeLine = new QTimeLine(150, q);
0037             m_timeLine->setEasingCurve(QEasingCurve::InCurve);
0038             m_timeLine->setDirection(QTimeLine::Forward);
0039             m_timeLine->setFrameRange(0, 100);
0040         }
0041     }
0042 
0043     void slotResult(KJob *);
0044     void slotFailed(const KFileItem &);
0045     void slotStepAnimation();
0046     void slotFinished();
0047     void slotActuallyClear();
0048 
0049     KImageFilePreview *q = nullptr;
0050     QUrl currentURL;
0051     QUrl lastShownURL;
0052     QLabel *imageLabel;
0053     KIO::PreviewJob *m_job = nullptr;
0054     QTimeLine *m_timeLine = nullptr;
0055     QPixmap m_pmCurrent;
0056     QPixmap m_pmTransition;
0057     float m_pmCurrentOpacity = 1;
0058     float m_pmTransitionOpacity = 0;
0059     bool clear = true;
0060 };
0061 
0062 KImageFilePreview::KImageFilePreview(QWidget *parent)
0063     : KPreviewWidgetBase(parent)
0064     , d(new KImageFilePreviewPrivate(this))
0065 {
0066     QVBoxLayout *vb = new QVBoxLayout(this);
0067     vb->setContentsMargins(0, 0, 0, 0);
0068 
0069     d->imageLabel = new QLabel(this);
0070     d->imageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
0071     d->imageLabel->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
0072     vb->addWidget(d->imageLabel);
0073 
0074     setSupportedMimeTypes(KIO::PreviewJob::supportedMimeTypes());
0075     setMinimumWidth(50);
0076 
0077     if (d->m_timeLine) {
0078         connect(d->m_timeLine, &QTimeLine::frameChanged, this, [this]() {
0079             d->slotStepAnimation();
0080         });
0081         connect(d->m_timeLine, &QTimeLine::finished, this, [this]() {
0082             d->slotFinished();
0083         });
0084     }
0085 }
0086 
0087 KImageFilePreview::~KImageFilePreview()
0088 {
0089     if (d->m_job) {
0090         d->m_job->kill();
0091     }
0092 }
0093 
0094 void KImageFilePreview::showPreview()
0095 {
0096     // Pass a copy since clearPreview() will clear currentURL
0097     QUrl url = d->currentURL;
0098     showPreview(url, true);
0099 }
0100 
0101 // called via KPreviewWidgetBase interface
0102 void KImageFilePreview::showPreview(const QUrl &url)
0103 {
0104     showPreview(url, false);
0105 }
0106 
0107 void KImageFilePreview::showPreview(const QUrl &url, bool force)
0108 {
0109     /* clang-format off */
0110     if (!url.isValid()
0111         || (d->lastShownURL.isValid()
0112             && url.matches(d->lastShownURL, QUrl::StripTrailingSlash)
0113             && d->currentURL.isValid())) {
0114         return;
0115     }
0116     /* clang-format on*/
0117 
0118     d->clear = false;
0119     d->currentURL = url;
0120     d->lastShownURL = url;
0121 
0122     int w = d->imageLabel->contentsRect().width() - 4;
0123     int h = d->imageLabel->contentsRect().height() - 4;
0124 
0125     if (d->m_job) {
0126         disconnect(d->m_job, nullptr, this, nullptr);
0127 
0128         d->m_job->kill();
0129     }
0130 
0131     d->m_job = createJob(url, w, h);
0132     if (force) { // explicitly requested previews shall always be generated!
0133         d->m_job->setIgnoreMaximumSize(true);
0134     }
0135 
0136     connect(d->m_job, &KJob::result, this, [this](KJob *job) {
0137         d->slotResult(job);
0138     });
0139     connect(d->m_job, &KIO::PreviewJob::gotPreview, this, &KImageFilePreview::gotPreview);
0140     connect(d->m_job, &KIO::PreviewJob::failed, this, [this](const KFileItem &item) {
0141         d->slotFailed(item);
0142     });
0143 }
0144 
0145 void KImageFilePreview::resizeEvent(QResizeEvent *)
0146 {
0147     // Nothing to do, if no current preview
0148     if (d->imageLabel->pixmap().isNull()) {
0149         return;
0150     }
0151 
0152     clearPreview();
0153     d->currentURL = QUrl(); // force this to actually happen
0154     showPreview(d->lastShownURL);
0155 }
0156 
0157 QSize KImageFilePreview::sizeHint() const
0158 {
0159     return QSize(100, 200);
0160 }
0161 
0162 KIO::PreviewJob *KImageFilePreview::createJob(const QUrl &url, int w, int h)
0163 {
0164     if (!url.isValid()) {
0165         return nullptr;
0166     }
0167 
0168     KFileItemList items;
0169     items.append(KFileItem(url));
0170     QStringList plugins = KIO::PreviewJob::availablePlugins();
0171 
0172     KIO::PreviewJob *previewJob = KIO::filePreview(items, QSize(w, h), &plugins);
0173 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0174     previewJob->setOverlayIconAlpha(0);
0175 #endif
0176     previewJob->setScaleType(KIO::PreviewJob::Scaled);
0177     return previewJob;
0178 }
0179 
0180 void KImageFilePreview::gotPreview(const KFileItem &item, const QPixmap &pm)
0181 {
0182     if (item.url() != d->currentURL) { // Shouldn't happen
0183         return;
0184     }
0185 
0186     if (d->m_timeLine) {
0187         if (d->m_timeLine->state() == QTimeLine::Running) {
0188             d->m_timeLine->setCurrentTime(0);
0189         }
0190 
0191         d->m_pmTransition = pm;
0192         d->m_pmTransitionOpacity = 0;
0193         d->m_pmCurrentOpacity = 1;
0194         d->m_timeLine->setDirection(QTimeLine::Forward);
0195         d->m_timeLine->start();
0196     } else {
0197         d->imageLabel->setPixmap(pm);
0198     }
0199 }
0200 
0201 void KImageFilePreviewPrivate::slotFailed(const KFileItem &item)
0202 {
0203     if (item.isDir()) {
0204         imageLabel->clear();
0205     } else if (item.url() == currentURL) { // should always be the case
0206         imageLabel->setPixmap(QIcon::fromTheme(QStringLiteral("image-missing")).pixmap(KIconLoader::SizeLarge, QIcon::Disabled));
0207     }
0208 }
0209 
0210 void KImageFilePreviewPrivate::slotResult(KJob *job)
0211 {
0212     if (job == m_job) {
0213         m_job = nullptr;
0214     }
0215 }
0216 
0217 void KImageFilePreviewPrivate::slotStepAnimation()
0218 {
0219     const QSize currSize = m_pmCurrent.size();
0220     const QSize transitionSize = m_pmTransition.size();
0221     const int width = std::max(currSize.width(), transitionSize.width());
0222     const int height = std::max(currSize.height(), transitionSize.height());
0223     QPixmap pm(QSize(width, height));
0224     pm.fill(Qt::transparent);
0225 
0226     QPainter p(&pm);
0227     p.setOpacity(m_pmCurrentOpacity);
0228 
0229     // If we have a current pixmap
0230     if (!m_pmCurrent.isNull()) {
0231         p.drawPixmap(QPoint(((float)pm.size().width() - m_pmCurrent.size().width()) / 2.0, ((float)pm.size().height() - m_pmCurrent.size().height()) / 2.0),
0232                      m_pmCurrent);
0233     }
0234     if (!m_pmTransition.isNull()) {
0235         p.setOpacity(m_pmTransitionOpacity);
0236         p.drawPixmap(
0237             QPoint(((float)pm.size().width() - m_pmTransition.size().width()) / 2.0, ((float)pm.size().height() - m_pmTransition.size().height()) / 2.0),
0238             m_pmTransition);
0239     }
0240     p.end();
0241 
0242     imageLabel->setPixmap(pm);
0243 
0244     m_pmCurrentOpacity = qMax(m_pmCurrentOpacity - 0.4, 0.0); // krazy:exclude=qminmax
0245     m_pmTransitionOpacity = qMin(m_pmTransitionOpacity + 0.4, 1.0); // krazy:exclude=qminmax
0246 }
0247 
0248 void KImageFilePreviewPrivate::slotFinished()
0249 {
0250     m_pmCurrent = m_pmTransition;
0251     m_pmTransitionOpacity = 0;
0252     m_pmCurrentOpacity = 1;
0253     m_pmTransition = QPixmap();
0254     // The animation might have lost some frames. Be sure that if the last one
0255     // was dropped, the last image shown is the opaque one.
0256     imageLabel->setPixmap(m_pmCurrent);
0257     clear = false;
0258 }
0259 
0260 void KImageFilePreview::clearPreview()
0261 {
0262     if (d->m_job) {
0263         d->m_job->kill();
0264         d->m_job = nullptr;
0265     }
0266 
0267     if (d->clear || (d->m_timeLine && d->m_timeLine->state() == QTimeLine::Running)) {
0268         return;
0269     }
0270 
0271     if (d->m_timeLine) {
0272         d->m_pmTransition = QPixmap();
0273         // If we add a previous preview then we run the animation
0274         if (!d->m_pmCurrent.isNull()) {
0275             d->m_timeLine->setCurrentTime(0);
0276             d->m_timeLine->setDirection(QTimeLine::Backward);
0277             d->m_timeLine->start();
0278         }
0279         d->currentURL.clear();
0280         d->clear = true;
0281     } else {
0282         d->imageLabel->clear();
0283     }
0284 }
0285 
0286 #include "moc_kimagefilepreview.cpp"