From 05a1626c7e14978c55a9670f35ff72e2a3388096 Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Sun, 17 Dec 2017 16:17:38 +0100 Subject: [PATCH] Implement different zoom levels for dive photos tab This implements different zoom levels for the dive photos tab as suggested by Stefan Fuchs in #898. The zoom level can be changed using a slider or CTRL+mousewheel. Zoom levels range from a third of the standard thumbnail size to thrice the standard thumbnail size. Thumbnails are cached in maximum resolution and scaled down on the fly. Because the profile widget took its pictures from the photo list model, an extra picture copy with a fixed size had to be introduced. The UI is still a bit crude. Reported-by: Stefan Fuchs Signed-off-by: Berthold Stoeger --- desktop-widgets/divepicturewidget.cpp | 14 ++++ desktop-widgets/divepicturewidget.h | 2 + desktop-widgets/tab-widgets/TabDivePhotos.cpp | 9 +++ desktop-widgets/tab-widgets/TabDivePhotos.h | 1 + desktop-widgets/tab-widgets/TabDivePhotos.ui | 36 ++++++++++ profile-widget/profilewidget2.cpp | 2 +- qt-models/divepicturemodel.cpp | 68 ++++++++++++++----- qt-models/divepicturemodel.h | 5 ++ 8 files changed, 120 insertions(+), 17 deletions(-) diff --git a/desktop-widgets/divepicturewidget.cpp b/desktop-widgets/divepicturewidget.cpp index a6864a5d9..46de16d09 100644 --- a/desktop-widgets/divepicturewidget.cpp +++ b/desktop-widgets/divepicturewidget.cpp @@ -59,3 +59,17 @@ void DivePictureWidget::mousePressEvent(QMouseEvent *event) QListView::mousePressEvent(event); } } + +void DivePictureWidget::wheelEvent(QWheelEvent *event) +{ + if (event->modifiers() == Qt::ControlModifier) { + // Angle delta is given in eighth parts of a degree. A classical mouse + // wheel click is 15 degrees. Each click should correspond to one zoom step. + // Therefore, divide by 15*8=120. To also support touch pads and finer-grained + // mouse wheels, take care to always round away from zero. + int delta = event->angleDelta().y(); + int carry = delta > 0 ? 119 : -119; + emit zoomLevelChanged((delta + carry) / 120); + } else + QListView::wheelEvent(event); +} diff --git a/desktop-widgets/divepicturewidget.h b/desktop-widgets/divepicturewidget.h index 09d4608c4..bf48a9d1b 100644 --- a/desktop-widgets/divepicturewidget.h +++ b/desktop-widgets/divepicturewidget.h @@ -14,9 +14,11 @@ public: protected: void mouseDoubleClickEvent(QMouseEvent *event) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE; + void wheelEvent(QWheelEvent *event) Q_DECL_OVERRIDE; signals: void photoDoubleClicked(const QString filePath); + void zoomLevelChanged(int delta); }; class DivePictureThumbnailThread : public QThread { diff --git a/desktop-widgets/tab-widgets/TabDivePhotos.cpp b/desktop-widgets/tab-widgets/TabDivePhotos.cpp index 86749e770..571e0a3ac 100644 --- a/desktop-widgets/tab-widgets/TabDivePhotos.cpp +++ b/desktop-widgets/tab-widgets/TabDivePhotos.cpp @@ -29,6 +29,10 @@ TabDivePhotos::TabDivePhotos(QWidget *parent) QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } ); + connect(ui->photosView, &DivePictureWidget::zoomLevelChanged, + this, &TabDivePhotos::changeZoomLevel); + connect(ui->zoomSlider, &QAbstractSlider::valueChanged, + DivePictureModel::instance(), &DivePictureModel::setZoomLevel); } TabDivePhotos::~TabDivePhotos() @@ -98,3 +102,8 @@ void TabDivePhotos::updateData() divePictureModel->updateDivePictures(); } +void TabDivePhotos::changeZoomLevel(int delta) +{ + // We count on QSlider doing bound checks + ui->zoomSlider->setValue(ui->zoomSlider->value() + delta); +} diff --git a/desktop-widgets/tab-widgets/TabDivePhotos.h b/desktop-widgets/tab-widgets/TabDivePhotos.h index 9b711595c..e2e7aa05c 100644 --- a/desktop-widgets/tab-widgets/TabDivePhotos.h +++ b/desktop-widgets/tab-widgets/TabDivePhotos.h @@ -26,6 +26,7 @@ private slots: void addPhotosFromURL(); void removeAllPhotos(); void removeSelectedPhotos(); + void changeZoomLevel(int delta); private: Ui::TabDivePhotos *ui; diff --git a/desktop-widgets/tab-widgets/TabDivePhotos.ui b/desktop-widgets/tab-widgets/TabDivePhotos.ui index 35cfd375a..21e4544e6 100644 --- a/desktop-widgets/tab-widgets/TabDivePhotos.ui +++ b/desktop-widgets/tab-widgets/TabDivePhotos.ui @@ -21,6 +21,42 @@ + + + + + + Zoom level + + + + + + + -10 + + + 10 + + + 1 + + + 1 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1 + + + + + diff --git a/profile-widget/profilewidget2.cpp b/profile-widget/profilewidget2.cpp index b7b6057fe..55eb40000 100644 --- a/profile-widget/profilewidget2.cpp +++ b/profile-widget/profilewidget2.cpp @@ -2007,7 +2007,7 @@ void ProfileWidget2::plotPictures() if (!offsetSeconds) continue; DivePictureItem *item = new DivePictureItem(); - item->setPixmap(m->index(i, 0).data(Qt::DecorationRole).value()); + item->setPixmap(m->index(i, 0).data(Qt::UserRole).value()); item->setFileUrl(m->index(i, 1).data().toString()); // let's put the picture at the correct time, but at a fixed "depth" on the profile // not sure this is ideal, but it seems to look right. diff --git a/qt-models/divepicturemodel.cpp b/qt-models/divepicturemodel.cpp index ff4e187ae..47e64492b 100644 --- a/qt-models/divepicturemodel.cpp +++ b/qt-models/divepicturemodel.cpp @@ -9,25 +9,28 @@ extern QHash thumbnailCache; static QMutex thumbnailMutex; +static const int maxZoom = 3; // Maximum zoom: thrice of standard size -static void scaleImages(PictureEntry &entry) +static QImage getThumbnailFromCache(const PictureEntry &entry) { QMutexLocker l(&thumbnailMutex); - if (thumbnailCache.contains(entry.filename) && !thumbnailCache.value(entry.filename).isNull()) { - entry.image = thumbnailCache.value(entry.filename); - return; - } - l.unlock(); + return thumbnailCache.value(entry.filename); +} - int dim = defaultIconMetrics().sz_pic; - QImage p = SHashedImage(entry.picture); - if(!p.isNull()) { - p = p.scaled(dim, dim, Qt::KeepAspectRatio); +static void scaleImages(PictureEntry &entry, int size, int maxSize) +{ + QImage thumbnail = getThumbnailFromCache(entry); + // If thumbnails were written by an earlier version, they might be smaller than needed. + // Rescale in such a case to avoid resizing artifacts. + if (thumbnail.isNull() || (thumbnail.size().width() < maxSize && thumbnail.size().height() < maxSize)) { + thumbnail = SHashedImage(entry.picture).scaled(maxSize, maxSize, Qt::KeepAspectRatio); QMutexLocker l(&thumbnailMutex); - if (!thumbnailCache.contains(entry.filename)) - thumbnailCache.insert(entry.filename, p); + thumbnailCache.insert(entry.filename, thumbnail); } - entry.image = p; + + entry.imageProfile = thumbnail.scaled(maxSize / maxZoom, maxSize / maxZoom, Qt::KeepAspectRatio); + entry.image = size == maxSize ? thumbnail + : thumbnail.scaled(size, size, Qt::KeepAspectRatio); } DivePictureModel *DivePictureModel::instance() @@ -36,7 +39,9 @@ DivePictureModel *DivePictureModel::instance() return self; } -DivePictureModel::DivePictureModel() : rowDDStart(0), rowDDEnd(0) +DivePictureModel::DivePictureModel() : rowDDStart(0), + rowDDEnd(0), + zoomLevel(0.0) { } @@ -48,6 +53,33 @@ void DivePictureModel::updateDivePicturesWhenDone(QList> futures) updateDivePictures(); } +void DivePictureModel::setZoomLevel(int level) +{ + zoomLevel = level / 10.0; + // zoomLevel is bound by [-1.0 1.0], see comment below. + if (zoomLevel < -1.0) + zoomLevel = -1.0; + if (zoomLevel > 1.0) + zoomLevel = 1.0; + updateThumbnails(); + layoutChanged(); +} + +void DivePictureModel::updateThumbnails() +{ + // Calculate size of thumbnails. The standard size is defaultIconMetrics().sz_pic. + // We use exponential scaling so that the central point is the standard + // size and the minimum and maximum extreme points are a third respectively + // three times the standard size. + // Naturally, these three zoom levels are then represented by + // -1.0 (minimum), 0 (standard) and 1.0 (maximum). The actual size is + // calculated as standard_size*3.0^zoomLevel. + int defaultSize = defaultIconMetrics().sz_pic; + int maxSize = defaultSize * maxZoom; + int size = static_cast(round(defaultSize * pow(maxZoom, zoomLevel))); + QtConcurrent::blockingMap(pictures, [size, maxSize](PictureEntry &entry){scaleImages(entry, size, maxSize);}); +} + void DivePictureModel::updateDivePictures() { if (!pictures.isEmpty()) { @@ -68,12 +100,13 @@ void DivePictureModel::updateDivePictures() if (dive->id == displayed_dive.id) rowDDStart = pictures.count(); FOR_EACH_PICTURE(dive) - pictures.push_back({picture, picture->filename, QImage(), picture->offset.seconds}); + pictures.push_back({picture, picture->filename, {}, {}, picture->offset.seconds}); if (dive->id == displayed_dive.id) rowDDEnd = pictures.count(); } } - QtConcurrent::blockingMap(pictures, scaleImages); + + updateThumbnails(); beginInsertRows(QModelIndex(), 0, pictures.count() - 1); endInsertRows(); @@ -100,6 +133,9 @@ QVariant DivePictureModel::data(const QModelIndex &index, int role) const case Qt::DecorationRole: ret = entry.image; break; + case Qt::UserRole: // Used by profile widget to access bigger thumbnails + ret = entry.imageProfile; + break; case Qt::DisplayRole: ret = QFileInfo(entry.filename).fileName(); break; diff --git a/qt-models/divepicturemodel.h b/qt-models/divepicturemodel.h index 53a72076a..74f92f449 100644 --- a/qt-models/divepicturemodel.h +++ b/qt-models/divepicturemodel.h @@ -10,6 +10,7 @@ struct PictureEntry { struct picture *picture; QString filename; QImage image; + QImage imageProfile; // For the profile widget keep a copy of a constant sized image int offsetSeconds; }; @@ -24,9 +25,13 @@ public: void updateDivePicturesWhenDone(QList>); void removePicture(const QString& fileUrl, bool last); int rowDDStart, rowDDEnd; +public slots: + void setZoomLevel(int level); private: DivePictureModel(); QList pictures; + double zoomLevel; // -1.0: minimum, 0.0: standard, 1.0: maximum + void updateThumbnails(); }; #endif