1 /*
2    SPDX-FileCopyrightText: 2020-2021 Laurent Montel <montel@kde.org>
3 
4    SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "showimagewidget.h"
8 #include "common/delegateutil.h"
9 #include "rocketchataccount.h"
10 #include "ruqola.h"
11 #include "ruqolawidgets_showimage_debug.h"
12 #include <KLocalizedString>
13 #include <QApplication>
14 #include <QClipboard>
15 #include <QDoubleSpinBox>
16 #include <QGraphicsPixmapItem>
17 #include <QGraphicsProxyWidget>
18 #include <QGraphicsScene>
19 #include <QLabel>
20 #include <QMimeData>
21 #include <QMovie>
22 #include <QPushButton>
23 #include <QScopedValueRollback>
24 #include <QSlider>
25 #include <QTimer>
26 #include <QVBoxLayout>
27 #include <QWheelEvent>
28 #include <QtMath>
29 
30 namespace
31 {
32 constexpr qreal defaultMinimumZoomScale = (qreal)0.1;
33 constexpr qreal defaultMaximumZoomScale = (qreal)10.0;
34 
fitToViewZoomScale(QSize imageSize,QSize widgetSize)35 qreal fitToViewZoomScale(QSize imageSize, QSize widgetSize)
36 {
37     if (imageSize.width() > widgetSize.width() || imageSize.height() > widgetSize.height()) {
38         // Make sure it fits, we care only about the first two decimal points, so round to the smaller value
39         const qreal hZoom = (qreal)widgetSize.width() / imageSize.width();
40         const qreal vZoom = (qreal)widgetSize.height() / imageSize.height();
41         return std::max((int)(std::min(hZoom, vZoom) * 100) / 100.0, defaultMinimumZoomScale);
42     }
43 
44     return 1.0;
45 }
46 
47 }
48 
ImageGraphicsView(RocketChatAccount * account,QWidget * parent)49 ImageGraphicsView::ImageGraphicsView(RocketChatAccount *account, QWidget *parent)
50     : QGraphicsView(parent)
51     , mAnimatedLabel(new QLabel)
52     , mRocketChatAccount(account)
53     , mMinimumZoom(defaultMinimumZoomScale)
54     , mMaximumZoom(defaultMaximumZoomScale)
55 {
56     setDragMode(QGraphicsView::ScrollHandDrag);
57 
58     auto scene = new QGraphicsScene(this);
59     setScene(scene);
60 
61     mAnimatedLabel->setObjectName(QStringLiteral("mAnimatedLabel"));
62     mAnimatedLabel->setBackgroundRole(QPalette::Base);
63     mAnimatedLabel->setAlignment(Qt::AlignCenter);
64 
65     mGraphicsProxyWidget = scene->addWidget(mAnimatedLabel);
66     mGraphicsProxyWidget->setObjectName(QStringLiteral("mGraphicsProxyWidget"));
67     mGraphicsProxyWidget->setFlag(QGraphicsItem::ItemIsMovable, true);
68 
69     mGraphicsPixmapItem = scene->addPixmap({});
70     mGraphicsPixmapItem->setTransformationMode(Qt::SmoothTransformation);
71 
72     updateRanges();
73 }
74 
75 ImageGraphicsView::~ImageGraphicsView() = default;
76 
updatePixmap(const QPixmap & pix,const QString & path)77 void ImageGraphicsView::updatePixmap(const QPixmap &pix, const QString &path)
78 {
79     clearContents();
80     if (!mImageInfo.isAnimatedImage) {
81         mGraphicsPixmapItem->setPixmap(pix);
82         QTimer::singleShot(0, this, [=] {
83             updateRanges();
84 
85             fitToView();
86         });
87     } else {
88         mMovie.reset(new QMovie(this));
89         mMovie->setFileName(path);
90         mMovie->start();
91         mMovie->stop();
92         mAnimatedLabel->setMovie(mMovie.data());
93 
94         QTimer::singleShot(0, this, [=] {
95             mOriginalMovieSize = mMovie->currentPixmap().size();
96             updateRanges();
97 
98             fitToView();
99             mMovie->start();
100         });
101     }
102 }
103 
setImageInfo(const ShowImageWidget::ImageInfo & info)104 void ImageGraphicsView::setImageInfo(const ShowImageWidget::ImageInfo &info)
105 {
106     mImageInfo = info;
107     qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << "ShowImageWidget::ImageInfo  " << info;
108     if (info.needToDownloadBigImage) {
109         qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << " Download big image " << info.needToDownloadBigImage << " use same image";
110         // We just need to download image not get url as it will be empty as we need to download it.
111         if (mRocketChatAccount) {
112             (void)mRocketChatAccount->attachmentUrlFromLocalCache(info.bigImagePath);
113         }
114         updatePixmap(mImageInfo.pixmap, mImageInfo.bigImagePath);
115     } else {
116         // Use big image.
117         if (mRocketChatAccount) {
118             qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << " Big image already downloaded " << info.needToDownloadBigImage;
119             const QString pixBigImagePath{mRocketChatAccount->attachmentUrlFromLocalCache(mImageInfo.bigImagePath).toLocalFile()};
120             const QPixmap pix(pixBigImagePath);
121             updatePixmap(pix, pixBigImagePath);
122         }
123     }
124 }
125 
zoomIn(QPointF centerPos)126 void ImageGraphicsView::zoomIn(QPointF centerPos)
127 {
128     setZoom(zoom() * 1.1, centerPos);
129 }
130 
zoomOut(QPointF centerPos)131 void ImageGraphicsView::zoomOut(QPointF centerPos)
132 {
133     setZoom(zoom() * 0.9, centerPos);
134 }
135 
clearContents()136 void ImageGraphicsView::clearContents()
137 {
138     mOriginalMovieSize = {};
139     mAnimatedLabel->setMovie(nullptr);
140     mMovie.reset();
141 
142     mGraphicsPixmapItem->setPixmap({});
143 }
144 
pixmap() const145 QPixmap ImageGraphicsView::pixmap() const
146 {
147     return mGraphicsPixmapItem->pixmap();
148 }
149 
minimumZoom() const150 qreal ImageGraphicsView::minimumZoom() const
151 {
152     return mMinimumZoom;
153 }
154 
maximumZoom() const155 qreal ImageGraphicsView::maximumZoom() const
156 {
157     return mMaximumZoom;
158 }
159 
updateRanges()160 void ImageGraphicsView::updateRanges()
161 {
162     const auto newMinimumZoom = fitToViewZoomScale(originalImageSize(), size());
163     if (!qFuzzyCompare(mMinimumZoom, newMinimumZoom)) {
164         mMinimumZoom = fitToViewZoomScale(originalImageSize(), size());
165         Q_EMIT minimumZoomChanged(mMinimumZoom);
166     }
167     // note: mMaximumZoom is constant for now
168 }
169 
wheelEvent(QWheelEvent * e)170 void ImageGraphicsView::wheelEvent(QWheelEvent *e)
171 {
172     if (e->modifiers() == Qt::ControlModifier) {
173         const int y = e->angleDelta().y();
174         if (y < 0) {
175             zoomOut(e->position());
176         } else if (y > 0) {
177             zoomIn(e->position());
178         } // else: y == 0 => horizontal scroll => do not handle
179     } else {
180         QGraphicsView::wheelEvent(e);
181     }
182 }
183 
originalImageSize() const184 QSize ImageGraphicsView::originalImageSize() const
185 {
186     if (mOriginalMovieSize.isValid()) {
187         return mOriginalMovieSize;
188     }
189 
190     return mGraphicsPixmapItem->pixmap().size();
191 }
192 
imageInfo() const193 const ShowImageWidget::ImageInfo &ImageGraphicsView::imageInfo() const
194 {
195     return mImageInfo;
196 }
197 
zoom() const198 qreal ImageGraphicsView::zoom() const
199 {
200     return transform().m11();
201 }
202 
setZoom(qreal zoom,QPointF centerPos)203 void ImageGraphicsView::setZoom(qreal zoom, QPointF centerPos)
204 {
205     // clamp value
206     zoom = qBound(minimumZoom(), zoom, maximumZoom());
207 
208     if (qFuzzyCompare(this->zoom(), zoom)) {
209         return;
210     }
211 
212     if (mIsUpdatingZoom) {
213         return;
214     }
215 
216     QScopedValueRollback<bool> guard(mIsUpdatingZoom, true);
217 
218     QPointF targetScenePos;
219     if (!centerPos.isNull()) {
220         targetScenePos = mapToScene(centerPos.toPoint());
221     } else {
222         targetScenePos = sceneRect().center();
223     }
224 
225     ViewportAnchor oldAnchor = this->transformationAnchor();
226     setTransformationAnchor(QGraphicsView::NoAnchor);
227 
228     QTransform matrix;
229     matrix.translate(targetScenePos.x(), targetScenePos.y()).scale(zoom, zoom).translate(-targetScenePos.x(), -targetScenePos.y());
230     setTransform(matrix);
231 
232     setTransformationAnchor(oldAnchor);
233     Q_EMIT zoomChanged(zoom);
234 }
235 
fitToView()236 void ImageGraphicsView::fitToView()
237 {
238     setZoom(fitToViewZoomScale(originalImageSize(), size()));
239     centerOn(mGraphicsPixmapItem);
240 }
241 
ShowImageWidget(RocketChatAccount * account,QWidget * parent)242 ShowImageWidget::ShowImageWidget(RocketChatAccount *account, QWidget *parent)
243     : QWidget(parent)
244     , mImageGraphicsView(new ImageGraphicsView(account, this))
245     , mZoomControls(new QWidget(this))
246     , mZoomSpin(new QDoubleSpinBox(this))
247     , mSlider(new QSlider(this))
248     , mRocketChatAccount(account)
249 {
250     auto mainLayout = new QVBoxLayout(this);
251     mainLayout->setObjectName(QStringLiteral("mainLayout"));
252     mainLayout->setContentsMargins({});
253 
254     mImageGraphicsView->setObjectName(QStringLiteral("mImageGraphicsView"));
255     mainLayout->addWidget(mImageGraphicsView);
256     connect(mImageGraphicsView, &ImageGraphicsView::zoomChanged, this, [this](qreal zoom) {
257         mSlider->setValue(static_cast<int>(zoom * 100));
258         mZoomSpin->setValue(zoom);
259     });
260     connect(mImageGraphicsView, &ImageGraphicsView::minimumZoomChanged, this, &ShowImageWidget::updateRanges);
261     connect(mImageGraphicsView, &ImageGraphicsView::maximumZoomChanged, this, &ShowImageWidget::updateRanges);
262 
263     mZoomControls->setObjectName(QStringLiteral("zoomControls"));
264     auto zoomLayout = new QHBoxLayout;
265     zoomLayout->setObjectName(QStringLiteral("zoomLayout"));
266     mZoomControls->setLayout(zoomLayout);
267     mainLayout->addWidget(mZoomControls);
268 
269     auto mLabel = new QLabel(i18n("Zoom:"), this);
270     mLabel->setObjectName(QStringLiteral("zoomLabel"));
271     zoomLayout->addWidget(mLabel);
272 
273     mZoomSpin->setObjectName(QStringLiteral("mZoomSpin"));
274 
275     mZoomSpin->setValue(1);
276     mZoomSpin->setDecimals(1);
277     mZoomSpin->setSingleStep(0.1);
278     zoomLayout->addWidget(mZoomSpin);
279 
280     mSlider->setObjectName(QStringLiteral("mSlider"));
281     mSlider->setOrientation(Qt::Horizontal);
282     zoomLayout->addWidget(mSlider);
283     mSlider->setValue(mZoomSpin->value() * 100.0);
284 
285     auto resetButton = new QPushButton(i18n("100%"), this);
286     resetButton->setObjectName(QStringLiteral("resetButton"));
287     zoomLayout->addWidget(resetButton);
288     connect(resetButton, &QPushButton::clicked, this, [=] {
289         mImageGraphicsView->setZoom(1.0);
290     });
291 
292     auto fitToViewButton = new QPushButton(i18n("Fit to View"), this);
293     fitToViewButton->setObjectName(QStringLiteral("fitToViewButton"));
294     zoomLayout->addWidget(fitToViewButton);
295     connect(fitToViewButton, &QPushButton::clicked, mImageGraphicsView, &ImageGraphicsView::fitToView);
296 
297     connect(mZoomSpin, qOverload<double>(&QDoubleSpinBox::valueChanged), this, [this](double value) {
298         mImageGraphicsView->setZoom(static_cast<qreal>(value));
299     });
300     connect(mSlider, &QSlider::valueChanged, this, [this](int value) {
301         mImageGraphicsView->setZoom(static_cast<qreal>(value) / 100);
302     });
303 
304     if (mRocketChatAccount) {
305         connect(mRocketChatAccount, &RocketChatAccount::fileDownloaded, this, &ShowImageWidget::slotFileDownloaded);
306     }
307     updateRanges();
308 }
309 
310 ShowImageWidget::~ShowImageWidget() = default;
311 
slotFileDownloaded(const QString & filePath,const QUrl & cacheImageUrl)312 void ShowImageWidget::slotFileDownloaded(const QString &filePath, const QUrl &cacheImageUrl)
313 {
314     qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << "File Downloaded : " << filePath << " cacheImageUrl " << cacheImageUrl;
315     const ImageInfo &info = imageInfo();
316     qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << "info.bigImagePath  " << info.bigImagePath;
317     if (filePath == QUrl(info.bigImagePath).toString()) {
318         qCDebug(RUQOLAWIDGETS_SHOWIMAGE_LOG) << "Update image  " << info << "filePath" << filePath << "cacheImageUrl " << cacheImageUrl;
319         const QString cacheImageUrlPath{cacheImageUrl.toLocalFile()};
320         const QPixmap pixmap(cacheImageUrlPath);
321         mImageGraphicsView->updatePixmap(pixmap, cacheImageUrlPath);
322     }
323 }
324 
updateRanges()325 void ShowImageWidget::updateRanges()
326 {
327     const auto min = mImageGraphicsView->minimumZoom();
328     const auto max = mImageGraphicsView->maximumZoom();
329     mZoomSpin->setRange(min, max);
330     mSlider->setRange(min * 100.0, max * 100.0);
331 }
332 
setImageInfo(const ShowImageWidget::ImageInfo & info)333 void ShowImageWidget::setImageInfo(const ShowImageWidget::ImageInfo &info)
334 {
335     mImageGraphicsView->setImageInfo(info);
336 }
337 
imageInfo() const338 const ShowImageWidget::ImageInfo &ShowImageWidget::imageInfo() const
339 {
340     return mImageGraphicsView->imageInfo();
341 }
342 
saveAs()343 void ShowImageWidget::saveAs()
344 {
345     DelegateUtil::saveFile(this,
346                            mRocketChatAccount->attachmentUrlFromLocalCache(mImageGraphicsView->imageInfo().bigImagePath).toLocalFile(),
347                            i18n("Save Image"));
348 }
349 
copyImage()350 void ShowImageWidget::copyImage()
351 {
352     auto data = new QMimeData();
353     data->setImageData(mImageGraphicsView->pixmap().toImage());
354     data->setData(QStringLiteral("x-kde-force-image-copy"), QByteArray());
355     QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard);
356 }
357 
copyLocation()358 void ShowImageWidget::copyLocation()
359 {
360     const QString imagePath = mRocketChatAccount->attachmentUrlFromLocalCache(mImageGraphicsView->imageInfo().bigImagePath).toLocalFile();
361     QApplication::clipboard()->setText(imagePath);
362 }
363 
operator <<(QDebug d,const ShowImageWidget::ImageInfo & t)364 QDebug operator<<(QDebug d, const ShowImageWidget::ImageInfo &t)
365 {
366     d << "bigImagePath : " << t.bigImagePath;
367     d << "previewImagePath : " << t.previewImagePath;
368     d << "isAnimatedImage : " << t.isAnimatedImage;
369     d << " pixmap is null ? " << t.pixmap.isNull();
370     d << " needToDownloadBigImage ? " << t.needToDownloadBigImage;
371     return d;
372 }
373