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