File indexing completed on 2024-05-19 05:04:06
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"