File indexing completed on 2024-05-12 16:27:16

0001 /*
0002    SPDX-FileCopyrightText: 2020-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "showimagewidget.h"
0008 #include "common/delegateutil.h"
0009 #include "rocketchataccount.h"
0010 #include "ruqolawidgets_showimage_debug.h"
0011 #include <KLocalizedString>
0012 #include <QApplication>
0013 #include <QClipboard>
0014 #include <QDoubleSpinBox>
0015 #include <QGraphicsPixmapItem>
0016 #include <QGraphicsProxyWidget>
0017 #include <QGraphicsScene>
0018 #include <QLabel>
0019 #include <QMimeData>
0020 #include <QMovie>
0021 #include <QPushButton>
0022 #include <QScopedValueRollback>
0023 #include <QSlider>
0024 #include <QTimer>
0025 #include <QVBoxLayout>
0026 #include <QWheelEvent>
0027 
0028 namespace
0029 {
0030 constexpr qreal defaultMinimumZoomScale = (qreal)0.1;
0031 constexpr qreal defaultMaximumZoomScale = (qreal)10.0;
0032 
0033 qreal fitToViewZoomScale(QSize imageSize, QSize widgetSize)
0034 {
0035     if (imageSize.width() > widgetSize.width() || imageSize.height() > widgetSize.height()) {
0036         // Make sure it fits, we care only about the first two decimal points, so round to the smaller value
0037         const qreal hZoom = (qreal)widgetSize.width() / imageSize.width();
0038         const qreal vZoom = (qreal)widgetSize.height() / imageSize.height();
0039         return std::max((int)(std::min(hZoom, vZoom) * 100) / 100.0, defaultMinimumZoomScale);
0040     }
0041 
0042     return 1.0;
0043 }
0044 
0045 }
0046 
0047 ImageGraphicsView::ImageGraphicsView(RocketChatAccount *account, QWidget *parent)
0048     : QGraphicsView(parent)
0049     , mAnimatedLabel(new QLabel)
0050     , mRocketChatAccount(account)
0051     , mMinimumZoom(defaultMinimumZoomScale)
0052     , mMaximumZoom(defaultMaximumZoomScale)
0053 {
0054     setDragMode(QGraphicsView::ScrollHandDrag);
0055 
0056     auto scene = new QGraphicsScene(this);
0057     setScene(scene);
0058 
0059     mAnimatedLabel->setObjectName(QStringLiteral("mAnimatedLabel"));
0060     mAnimatedLabel->setBackgroundRole(QPalette::Base);
0061     mAnimatedLabel->setAlignment(Qt::AlignCenter);
0062 
0063     mGraphicsProxyWidget = scene->addWidget(mAnimatedLabel);
0064     mGraphicsProxyWidget->setObjectName(QStringLiteral("mGraphicsProxyWidget"));
0065     mGraphicsProxyWidget->setFlag(QGraphicsItem::ItemIsMovable, true);
0066 
0067     mGraphicsPixmapItem = scene->addPixmap({});
0068     mGraphicsPixmapItem->setTransformationMode(Qt::SmoothTransformation);
0069 
0070     updateRanges();
0071 }
0072 
0073 ImageGraphicsView::~ImageGraphicsView() = default;
0074 
0075 void ImageGraphicsView::updatePixmap(const QPixmap &pix, const QString &path)
0076 {
0077     clearContents();
0078     if (!mImageInfo.isAnimatedImage) {
0079         mGraphicsPixmapItem->setPixmap(pix);
0080         QTimer::singleShot(0, this, [=] {
0081             updateRanges();
0082 
0083             fitToView();
0084         });
0085     } else {
0086         mMovie.reset(new QMovie(this));
0087         mMovie->setFileName(path);
0088         mMovie->start();
0089         mMovie->stop();
0090         mAnimatedLabel->setMovie(mMovie.data());
0091 
0092         QTimer::singleShot(0, this, [=] {
0093             mOriginalMovieSize = mMovie->currentPixmap().size();
0094             updateRanges();
0095 
0096             fitToView();
0097             mMovie->start();
0098         });
0099     }
0100 }
0101 
0102 void ImageGraphicsView::setImageInfo(const ShowImageWidget::ImageInfo &info)
0103 {
0104     mImageInfo = info;
0105     qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << "ShowImageWidget::ImageInfo  " << info;
0106     if (info.needToDownloadBigImage) {
0107         qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << " Download big image " << info.needToDownloadBigImage << " use same image";
0108         // We just need to download image not get url as it will be empty as we need to download it.
0109         if (mRocketChatAccount) {
0110             (void)mRocketChatAccount->attachmentUrlFromLocalCache(info.bigImagePath);
0111         }
0112         updatePixmap(mImageInfo.pixmap, mImageInfo.bigImagePath);
0113     } else {
0114         // Use big image.
0115         if (mRocketChatAccount) {
0116             qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << " Big image already downloaded " << info.needToDownloadBigImage;
0117             const QString pixBigImagePath{mRocketChatAccount->attachmentUrlFromLocalCache(mImageInfo.bigImagePath).toLocalFile()};
0118             const QPixmap pix(pixBigImagePath);
0119             updatePixmap(pix, pixBigImagePath);
0120         }
0121     }
0122 }
0123 
0124 void ImageGraphicsView::zoomIn(QPointF centerPos)
0125 {
0126     setZoom(zoom() * 1.1, centerPos);
0127 }
0128 
0129 void ImageGraphicsView::zoomOut(QPointF centerPos)
0130 {
0131     setZoom(zoom() * 0.9, centerPos);
0132 }
0133 
0134 void ImageGraphicsView::clearContents()
0135 {
0136     mOriginalMovieSize = {};
0137     mAnimatedLabel->setMovie(nullptr);
0138     mMovie.reset();
0139 
0140     mGraphicsPixmapItem->setPixmap({});
0141 }
0142 
0143 QPixmap ImageGraphicsView::pixmap() const
0144 {
0145     return mGraphicsPixmapItem->pixmap();
0146 }
0147 
0148 qreal ImageGraphicsView::minimumZoom() const
0149 {
0150     return mMinimumZoom;
0151 }
0152 
0153 qreal ImageGraphicsView::maximumZoom() const
0154 {
0155     return mMaximumZoom;
0156 }
0157 
0158 void ImageGraphicsView::updateRanges()
0159 {
0160     const auto newMinimumZoom = fitToViewZoomScale(originalImageSize(), size());
0161     if (!qFuzzyCompare(mMinimumZoom, newMinimumZoom)) {
0162         mMinimumZoom = fitToViewZoomScale(originalImageSize(), size());
0163         Q_EMIT minimumZoomChanged(mMinimumZoom);
0164     }
0165     // note: mMaximumZoom is constant for now
0166 }
0167 
0168 void ImageGraphicsView::wheelEvent(QWheelEvent *e)
0169 {
0170     if (e->modifiers() == Qt::ControlModifier) {
0171         const int y = e->angleDelta().y();
0172         if (y < 0) {
0173             zoomOut(e->position());
0174         } else if (y > 0) {
0175             zoomIn(e->position());
0176         } // else: y == 0 => horizontal scroll => do not handle
0177     } else {
0178         QGraphicsView::wheelEvent(e);
0179     }
0180 }
0181 
0182 QSize ImageGraphicsView::originalImageSize() const
0183 {
0184     if (mOriginalMovieSize.isValid()) {
0185         return mOriginalMovieSize;
0186     }
0187 
0188     return mGraphicsPixmapItem->pixmap().size();
0189 }
0190 
0191 const ShowImageWidget::ImageInfo &ImageGraphicsView::imageInfo() const
0192 {
0193     return mImageInfo;
0194 }
0195 
0196 qreal ImageGraphicsView::zoom() const
0197 {
0198     return transform().m11();
0199 }
0200 
0201 void ImageGraphicsView::setZoom(qreal zoom, QPointF centerPos)
0202 {
0203     // clamp value
0204     zoom = qBound(minimumZoom(), zoom, maximumZoom());
0205 
0206     if (qFuzzyCompare(this->zoom(), zoom)) {
0207         return;
0208     }
0209 
0210     if (mIsUpdatingZoom) {
0211         return;
0212     }
0213 
0214     QScopedValueRollback<bool> guard(mIsUpdatingZoom, true);
0215 
0216     QPointF targetScenePos;
0217     if (!centerPos.isNull()) {
0218         targetScenePos = mapToScene(centerPos.toPoint());
0219     } else {
0220         targetScenePos = sceneRect().center();
0221     }
0222 
0223     ViewportAnchor oldAnchor = this->transformationAnchor();
0224     setTransformationAnchor(QGraphicsView::NoAnchor);
0225 
0226     QTransform matrix;
0227     matrix.translate(targetScenePos.x(), targetScenePos.y()).scale(zoom, zoom).translate(-targetScenePos.x(), -targetScenePos.y());
0228     setTransform(matrix);
0229 
0230     setTransformationAnchor(oldAnchor);
0231     Q_EMIT zoomChanged(zoom);
0232 }
0233 
0234 void ImageGraphicsView::fitToView()
0235 {
0236     setZoom(fitToViewZoomScale(originalImageSize(), size()));
0237     centerOn(mGraphicsPixmapItem);
0238 }
0239 
0240 ShowImageWidget::ShowImageWidget(RocketChatAccount *account, QWidget *parent)
0241     : QWidget(parent)
0242     , mImageGraphicsView(new ImageGraphicsView(account, this))
0243     , mZoomControls(new QWidget(this))
0244     , mZoomSpin(new QDoubleSpinBox(this))
0245     , mSlider(new QSlider(this))
0246     , mRocketChatAccount(account)
0247 {
0248     auto mainLayout = new QVBoxLayout(this);
0249     mainLayout->setObjectName(QStringLiteral("mainLayout"));
0250     mainLayout->setContentsMargins({});
0251 
0252     mImageGraphicsView->setObjectName(QStringLiteral("mImageGraphicsView"));
0253     mainLayout->addWidget(mImageGraphicsView);
0254     connect(mImageGraphicsView, &ImageGraphicsView::zoomChanged, this, [this](qreal zoom) {
0255         mSlider->setValue(static_cast<int>(zoom * 100));
0256         mZoomSpin->setValue(zoom);
0257     });
0258     connect(mImageGraphicsView, &ImageGraphicsView::minimumZoomChanged, this, &ShowImageWidget::updateRanges);
0259     connect(mImageGraphicsView, &ImageGraphicsView::maximumZoomChanged, this, &ShowImageWidget::updateRanges);
0260 
0261     mZoomControls->setObjectName(QStringLiteral("zoomControls"));
0262     auto zoomLayout = new QHBoxLayout;
0263     zoomLayout->setObjectName(QStringLiteral("zoomLayout"));
0264     mZoomControls->setLayout(zoomLayout);
0265     mainLayout->addWidget(mZoomControls);
0266 
0267     auto mLabel = new QLabel(i18n("Zoom:"), this);
0268     mLabel->setObjectName(QStringLiteral("zoomLabel"));
0269     zoomLayout->addWidget(mLabel);
0270 
0271     mZoomSpin->setObjectName(QStringLiteral("mZoomSpin"));
0272 
0273     mZoomSpin->setValue(1);
0274     mZoomSpin->setDecimals(1);
0275     mZoomSpin->setSingleStep(0.1);
0276     zoomLayout->addWidget(mZoomSpin);
0277 
0278     mSlider->setObjectName(QStringLiteral("mSlider"));
0279     mSlider->setOrientation(Qt::Horizontal);
0280     zoomLayout->addWidget(mSlider);
0281     mSlider->setValue(mZoomSpin->value() * 100.0);
0282 
0283     auto resetButton = new QPushButton(i18n("100%"), this);
0284     resetButton->setObjectName(QStringLiteral("resetButton"));
0285     zoomLayout->addWidget(resetButton);
0286     connect(resetButton, &QPushButton::clicked, this, [=] {
0287         mImageGraphicsView->setZoom(1.0);
0288     });
0289 
0290     auto fitToViewButton = new QPushButton(i18n("Fit to View"), this);
0291     fitToViewButton->setObjectName(QStringLiteral("fitToViewButton"));
0292     zoomLayout->addWidget(fitToViewButton);
0293     connect(fitToViewButton, &QPushButton::clicked, mImageGraphicsView, &ImageGraphicsView::fitToView);
0294 
0295     connect(mZoomSpin, &QDoubleSpinBox::valueChanged, this, [this](double value) {
0296         mImageGraphicsView->setZoom(static_cast<qreal>(value));
0297     });
0298     connect(mSlider, &QSlider::valueChanged, this, [this](int value) {
0299         mImageGraphicsView->setZoom(static_cast<qreal>(value) / 100);
0300     });
0301 
0302     if (mRocketChatAccount) {
0303         connect(mRocketChatAccount, &RocketChatAccount::fileDownloaded, this, &ShowImageWidget::slotFileDownloaded);
0304     }
0305     updateRanges();
0306 }
0307 
0308 ShowImageWidget::~ShowImageWidget() = default;
0309 
0310 void ShowImageWidget::keyPressEvent(QKeyEvent *event)
0311 {
0312     const bool isControlClicked = event->modifiers() & Qt::ControlModifier;
0313     if (isControlClicked) {
0314         if (event->key() == Qt::Key_Plus) {
0315             mSlider->setValue(mSlider->value() + mSlider->singleStep());
0316         } else if (event->key() == Qt::Key_Minus) {
0317             mSlider->setValue(mSlider->value() - mSlider->singleStep());
0318         }
0319     } else {
0320         QWidget::keyPressEvent(event);
0321     }
0322 }
0323 
0324 void ShowImageWidget::slotFileDownloaded(const QString &filePath, const QUrl &cacheImageUrl)
0325 {
0326     qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << "File Downloaded : " << filePath << " cacheImageUrl " << cacheImageUrl;
0327     const ImageInfo &info = imageInfo();
0328     qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << "info.bigImagePath  " << info.bigImagePath;
0329     if (filePath == QUrl(info.bigImagePath).toString()) {
0330         qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << "Update image  " << info << "filePath" << filePath << "cacheImageUrl " << cacheImageUrl;
0331         const QString cacheImageUrlPath{cacheImageUrl.toLocalFile()};
0332         const QPixmap pixmap(cacheImageUrlPath);
0333         mImageGraphicsView->updatePixmap(pixmap, cacheImageUrlPath);
0334     }
0335 }
0336 
0337 void ShowImageWidget::updateRanges()
0338 {
0339     const auto min = mImageGraphicsView->minimumZoom();
0340     const auto max = mImageGraphicsView->maximumZoom();
0341     mZoomSpin->setRange(min, max);
0342     mSlider->setRange(min * 100.0, max * 100.0);
0343 }
0344 
0345 void ShowImageWidget::setImageInfo(const ShowImageWidget::ImageInfo &info)
0346 {
0347     mImageGraphicsView->setImageInfo(info);
0348 }
0349 
0350 const ShowImageWidget::ImageInfo &ShowImageWidget::imageInfo() const
0351 {
0352     return mImageGraphicsView->imageInfo();
0353 }
0354 
0355 void ShowImageWidget::saveAs()
0356 {
0357     DelegateUtil::saveFile(this,
0358                            mRocketChatAccount->attachmentUrlFromLocalCache(mImageGraphicsView->imageInfo().bigImagePath).toLocalFile(),
0359                            i18n("Save Image"));
0360 }
0361 
0362 void ShowImageWidget::copyImage()
0363 {
0364     auto data = new QMimeData();
0365     data->setImageData(mImageGraphicsView->pixmap().toImage());
0366     data->setData(QStringLiteral("x-kde-force-image-copy"), QByteArray());
0367     QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard);
0368 }
0369 
0370 void ShowImageWidget::copyLocation()
0371 {
0372     const QString imagePath = mRocketChatAccount->attachmentUrlFromLocalCache(mImageGraphicsView->imageInfo().bigImagePath).toLocalFile();
0373     QApplication::clipboard()->setText(imagePath);
0374 }
0375 
0376 QDebug operator<<(QDebug d, const ShowImageWidget::ImageInfo &t)
0377 {
0378     d << "bigImagePath : " << t.bigImagePath;
0379     d << "previewImagePath : " << t.previewImagePath;
0380     d << "isAnimatedImage : " << t.isAnimatedImage;
0381     d << " pixmap is null ? " << t.pixmap.isNull();
0382     d << " needToDownloadBigImage ? " << t.needToDownloadBigImage;
0383     return d;
0384 }
0385 
0386 #include "moc_showimagewidget.cpp"