From b3feaa80e26510928c73e92049ec346841f6093a Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Sun, 15 Jul 2018 17:56:18 +0200 Subject: [PATCH] Dive video: paint duration-bar above thumbnail in profile plot Paint a rectangle on top of thumbnails indicating the run-time of the video. Use the z=100.0-101.0 range for painting the thumbnails, whereby the z-value increases uniformly from first to last thumbnail (sorted by timestamp). The duration-bars are placed at z-values midway between those of the thumbnails. Signed-off-by: Berthold Stoeger --- core/imagedownloader.cpp | 90 +++++++++++++------------- core/imagedownloader.h | 8 +-- profile-widget/divepixmapitem.cpp | 15 ++++- profile-widget/divepixmapitem.h | 2 + profile-widget/profilewidget2.cpp | 102 ++++++++++++++++++++++++------ profile-widget/profilewidget2.h | 8 ++- qt-models/divepicturemodel.cpp | 2 +- qt-models/divepicturemodel.h | 4 +- subsurface-helper.cpp | 8 +++ 9 files changed, 163 insertions(+), 76 deletions(-) diff --git a/core/imagedownloader.cpp b/core/imagedownloader.cpp index fd155bec6..7dc35695d 100644 --- a/core/imagedownloader.cpp +++ b/core/imagedownloader.cpp @@ -93,40 +93,36 @@ Thumbnailer::Thumbnail Thumbnailer::fetchImage(const QString &filename, const QS mediatype_t type = get_metadata(qPrintable(filename), &md); // For io error or video, return early with the appropriate dummy-icon. - if (type == MEDIATYPE_IO_ERROR) { - return { failImage, MEDIATYPE_IO_ERROR }; - } else if (type == MEDIATYPE_VIDEO) { - addVideoThumbnailToCache(originalFilename, md.duration); - return { videoImage, MEDIATYPE_VIDEO }; - } + if (type == MEDIATYPE_IO_ERROR) + return { failImage, MEDIATYPE_IO_ERROR, 0 }; + else if (type == MEDIATYPE_VIDEO) + return addVideoThumbnailToCache(originalFilename, md.duration); // Try if Qt can parse this image. If it does, use this as a thumbnail. QImage thumb(filename); if (!thumb.isNull()) { - addPictureThumbnailToCache(originalFilename, thumb); - return { thumb, MEDIATYPE_PICTURE }; + int size = maxThumbnailSize(); + thumb = thumb.scaled(size, size, Qt::KeepAspectRatio); + return addPictureThumbnailToCache(originalFilename, thumb); } // Neither our code, nor Qt could determine the type of this object from looking at the data. // Try to check for a video-file extension. Since we couldn't parse the video file, // we pass 0 as the duration. - if (hasVideoFileExtension(filename)) { - addVideoThumbnailToCache(originalFilename, {0} ); - return { videoImage, MEDIATYPE_VIDEO }; - } + if (hasVideoFileExtension(filename)) + return addVideoThumbnailToCache(originalFilename, {0} ); // Give up: we simply couldn't determine what this thing is. // But since we managed to read this file, mark this file in the cache as unknown. - addUnknownThumbnailToCache(originalFilename); - return { unknownImage, MEDIATYPE_UNKNOWN }; + return addUnknownThumbnailToCache(originalFilename); } else if (tryDownload) { // This has to be done in UI main thread, because QNetworkManager refuses // to treat requests from other threads. invokeMethod() is Qt's way of calling a // function in a different thread, namely the thread the called object is associated to. QMetaObject::invokeMethod(ImageDownloader::instance(), "load", Qt::AutoConnection, Q_ARG(QUrl, url), Q_ARG(QString, originalFilename)); - return { QImage(), MEDIATYPE_STILL_LOADING }; + return { QImage(), MEDIATYPE_STILL_LOADING, 0 }; } - return { QImage(), MEDIATYPE_IO_ERROR }; + return { QImage(), MEDIATYPE_IO_ERROR, 0 }; } // Fetch a picture based on its original filename. If there is a translated filename (obtained either @@ -142,7 +138,7 @@ Thumbnailer::Thumbnail Thumbnailer::getHashedImage(const QString &filename, bool // If there is a translated filename, try that first. // Note that we set the default type to io-error, so that if we didn't try // the local filename first, we will load the file from the canonical filename. - Thumbnail thumbnail { QImage(), MEDIATYPE_IO_ERROR }; + Thumbnail thumbnail { QImage(), MEDIATYPE_IO_ERROR, 0 }; if (localFilename != filename) thumbnail = fetchImage(localFilename, filename, tryDownload); @@ -199,7 +195,9 @@ Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &str // If reading did not succeed, schedule for recalculation - this thumbnail might // have been written by an older version, which couldn't extract the duration. - if (stream.status() != QDataStream::Ok) + // Likewise test the duration and number of pictures for sanity (no videos longer than 10 h, + // no more than 10000 pictures). + if (stream.status() != QDataStream::Ok || duration > 36000 || numPics > 10000) return { QImage(), MEDIATYPE_VIDEO, 0 }; // Currently, we support only one picture @@ -211,7 +209,7 @@ Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &str } // No picture -> show dummy-icon - return { res.isNull() ? videoImage : res, MEDIATYPE_VIDEO, {(int32_t)duration} }; + return { res.isNull() ? videoImage : res, MEDIATYPE_VIDEO, (int32_t)duration }; } // Fetch a thumbnail from cache. @@ -220,7 +218,7 @@ Thumbnailer::Thumbnail Thumbnailer::getThumbnailFromCache(const QString &picture { QString filename = thumbnailFileName(picture_filename); if (filename.isEmpty()) - return { QImage(), MEDIATYPE_UNKNOWN }; + return { QImage(), MEDIATYPE_UNKNOWN, 0 }; QFile file(filename); if (prefs.auto_recalculate_thumbnails) { @@ -256,7 +254,7 @@ Thumbnailer::Thumbnail Thumbnailer::getThumbnailFromCache(const QString &picture } } -void Thumbnailer::addVideoThumbnailToCache(const QString &picture_filename, duration_t duration) +Thumbnailer::Thumbnail Thumbnailer::addVideoThumbnailToCache(const QString &picture_filename, duration_t duration) { // The format of video thumbnails: // uint32 MEDIATYPE_VIDEO @@ -267,43 +265,43 @@ void Thumbnailer::addVideoThumbnailToCache(const QString &picture_filename, dura // QImage frame QString filename = thumbnailFileName(picture_filename); QSaveFile file(filename); - if (!file.open(QIODevice::WriteOnly)) - return; - QDataStream stream(&file); + if (file.open(QIODevice::WriteOnly)) { + QDataStream stream(&file); - stream << (quint32)MEDIATYPE_VIDEO; - stream << (quint32)duration.seconds; - stream << (quint32)0; // Currently, we don't support extraction of images - file.commit(); + stream << (quint32)MEDIATYPE_VIDEO; + stream << (quint32)duration.seconds; + stream << (quint32)0; // Currently, we don't support extraction of images + file.commit(); + } + return { videoImage, MEDIATYPE_VIDEO, duration }; } -void Thumbnailer::addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail) +Thumbnailer::Thumbnail Thumbnailer::addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail) { - if (thumbnail.isNull()) - return; - // The format of a picture-thumbnail is very simple: // uint32 MEDIATYPE_PICTURE // QImage thumbnail QString filename = thumbnailFileName(picture_filename); QSaveFile file(filename); - if (!file.open(QIODevice::WriteOnly)) - return; - QDataStream stream(&file); + if (file.open(QIODevice::WriteOnly)) { + QDataStream stream(&file); - stream << (quint32)MEDIATYPE_PICTURE; - stream << thumbnail; - file.commit(); + stream << (quint32)MEDIATYPE_PICTURE; + stream << thumbnail; + file.commit(); + } + return { thumbnail, MEDIATYPE_PICTURE, 0 }; } -void Thumbnailer::addUnknownThumbnailToCache(const QString &picture_filename) +Thumbnailer::Thumbnail Thumbnailer::addUnknownThumbnailToCache(const QString &picture_filename) { QString filename = thumbnailFileName(picture_filename); QSaveFile file(filename); - if (!file.open(QIODevice::WriteOnly)) - return; - QDataStream stream(&file); - stream << (quint32)MEDIATYPE_UNKNOWN; + if (file.open(QIODevice::WriteOnly)) { + QDataStream stream(&file); + stream << (quint32)MEDIATYPE_UNKNOWN; + } + return { unknownImage, MEDIATYPE_UNKNOWN, 0 }; } void Thumbnailer::recalculate(QString filename) @@ -317,7 +315,7 @@ void Thumbnailer::recalculate(QString filename) return; QMutexLocker l(&lock); - emit thumbnailChanged(filename, thumbnail.img); + emit thumbnailChanged(filename, thumbnail.img, thumbnail.duration); workingOn.remove(filename); } @@ -339,7 +337,7 @@ void Thumbnailer::processItem(QString filename, bool tryDownload) } QMutexLocker l(&lock); - emit thumbnailChanged(filename, thumbnail.img); + emit thumbnailChanged(filename, thumbnail.img, thumbnail.duration); workingOn.remove(filename); } @@ -352,7 +350,7 @@ void Thumbnailer::imageDownloaded(QString filename) void Thumbnailer::imageDownloadFailed(QString filename) { - emit thumbnailChanged(filename, failImage); + emit thumbnailChanged(filename, failImage, duration_t{ 0 }); QMutexLocker l(&lock); workingOn.remove(filename); } diff --git a/core/imagedownloader.h b/core/imagedownloader.h index dcdf08d20..cab945e5f 100644 --- a/core/imagedownloader.h +++ b/core/imagedownloader.h @@ -47,7 +47,7 @@ public slots: void imageDownloaded(QString filename); void imageDownloadFailed(QString filename); signals: - void thumbnailChanged(QString filename, QImage thumbnail); + void thumbnailChanged(QString filename, QImage thumbnail, duration_t duration); private: struct Thumbnail { QImage img; @@ -56,9 +56,9 @@ private: }; Thumbnailer(); - static void addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail); - static void addVideoThumbnailToCache(const QString &picture_filename, duration_t duration); - static void addUnknownThumbnailToCache(const QString &picture_filename); + Thumbnail addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail); + Thumbnail addVideoThumbnailToCache(const QString &picture_filename, duration_t duration); + Thumbnail addUnknownThumbnailToCache(const QString &picture_filename); void recalculate(QString filename); void processItem(QString filename, bool tryDownload); Thumbnail getThumbnailFromCache(const QString &picture_filename); diff --git a/profile-widget/divepixmapitem.cpp b/profile-widget/divepixmapitem.cpp index d295fb4dd..730cdf606 100644 --- a/profile-widget/divepixmapitem.cpp +++ b/profile-widget/divepixmapitem.cpp @@ -42,7 +42,8 @@ void CloseButtonItem::show() DivePictureItem::DivePictureItem(QGraphicsItem *parent): DivePixmapItem(parent), canvas(new QGraphicsRectItem(this)), shadow(new QGraphicsRectItem(this)), - button(new CloseButtonItem(this)) + button(new CloseButtonItem(this)), + baseZValue(0.0) { setFlag(ItemIgnoresTransformations); setAcceptHoverEvents(true); @@ -67,6 +68,14 @@ DivePictureItem::DivePictureItem(QGraphicsItem *parent): DivePixmapItem(parent), button->hide(); } +// The base z-value is used for correct paint-order of the thumbnails. On hoverEnter the z-value is raised +// so that the thumbnail is drawn on top of all other thumbnails and on hoverExit it is restored to the base value. +void DivePictureItem::setBaseZValue(double z) +{ + baseZValue = z; + setZValue(z); +} + void DivePictureItem::settingsChanged() { setVisible(prefs.show_pictures_in_profile); @@ -85,7 +94,7 @@ void DivePictureItem::setPixmap(const QPixmap &pix) void DivePictureItem::hoverEnterEvent(QGraphicsSceneHoverEvent*) { Animations::scaleTo(this, 1.0); - setZValue(5); + setZValue(baseZValue + 5.0); button->setOpacity(0); button->show(); @@ -100,7 +109,7 @@ void DivePictureItem::setFileUrl(const QString &s) void DivePictureItem::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { Animations::scaleTo(this, 0.2); - setZValue(0); + setZValue(baseZValue); Animations::hide(button); } diff --git a/profile-widget/divepixmapitem.h b/profile-widget/divepixmapitem.h index 6abe410a6..f8b922981 100644 --- a/profile-widget/divepixmapitem.h +++ b/profile-widget/divepixmapitem.h @@ -32,6 +32,7 @@ class DivePictureItem : public DivePixmapItem { public: DivePictureItem(QGraphicsItem *parent = 0); void setPixmap(const QPixmap& pix); + void setBaseZValue(double z); public slots: void settingsChanged(); void removePicture(); @@ -45,6 +46,7 @@ private: QGraphicsRectItem *canvas; QGraphicsRectItem *shadow; CloseButtonItem *button; + double baseZValue; }; #endif // DIVEPIXMAPITEM_H diff --git a/profile-widget/profilewidget2.cpp b/profile-widget/profilewidget2.cpp index 11dddaa41..79aa2e876 100644 --- a/profile-widget/profilewidget2.cpp +++ b/profile-widget/profilewidget2.cpp @@ -87,6 +87,10 @@ static struct _ItemPos { _Axis heartBeatWithTankBar; } itemPos; +// Constant describing at which z-level the thumbnails are located. +// We might add more constants here for easier customability. +static const double thumbnailBaseZValue = 100.0; + ProfileWidget2::ProfileWidget2(QWidget *parent) : QGraphicsView(parent), currentState(INVALID), dataModel(new DivePlotDataModel(this)), @@ -975,6 +979,21 @@ void ProfileWidget2::fixBackgroundPos() background->setY(mapToScene(y, 20).y()); } +void ProfileWidget2::scale(qreal sx, qreal sy) +{ + QGraphicsView::scale(sx, sy); + +#ifndef SUBSURFACE_MOBILE + // Since the zoom level changed, adjust the duration bars accordingly. + // We want to grow/shrink the length, but not the height and pen. + for (PictureEntry &p: pictures) + updateDurationLine(p); + + // Since we created new duration lines, we have to update the order in which the thumbnails is painted. + updateThumbnailPaintOrder(); +#endif +} + #ifndef SUBSURFACE_MOBILE void ProfileWidget2::wheelEvent(QWheelEvent *event) { @@ -2064,9 +2083,37 @@ void ProfileWidget2::clearPictures() pictures.clear(); } +static const double unscaledDurationLineWidth = 2.5; +static const double unscaledDurationLinePenWidth = 0.5; + +// Reset the duration line after an image was moved or we found a new duration +void ProfileWidget2::updateDurationLine(PictureEntry &e) +{ + if (e.duration.seconds > 0) { + // We know the duration of this video, reset the line symbolizing its extent accordingly + double begin = timeAxis->posAtValue(e.offset.seconds); + double end = timeAxis->posAtValue(e.offset.seconds + e.duration.seconds); + double y = e.thumbnail->y(); + + // Undo scaling for pen-width and line-width. For this purpose, we use the scaling of the y-axis. + double scale = transform().m22(); + double durationLineWidth = unscaledDurationLineWidth / scale; + double durationLinePenWidth = unscaledDurationLinePenWidth / scale; + e.durationLine.reset(new QGraphicsRectItem(begin, y - durationLineWidth - durationLinePenWidth, end - begin, durationLineWidth)); + e.durationLine->setPen(QPen(getColor(GF_LINE, isGrayscale), durationLinePenWidth)); + e.durationLine->setBrush(getColor(::BACKGROUND, isGrayscale)); + e.durationLine->setVisible(prefs.show_pictures_in_profile); + scene()->addItem(e.durationLine.get()); + } else { + // This is either a picture or a video with unknown duration. + // In case there was a line (how could that be?) remove it. + e.durationLine.reset(); + } +} + // This function is called asynchronously by the thumbnailer if a thumbnail // was fetched from disk or freshly calculated. -void ProfileWidget2::updateThumbnail(QString filename, QImage thumbnail) +void ProfileWidget2::updateThumbnail(QString filename, QImage thumbnail, duration_t duration) { // Find the picture with the given filename auto it = std::find_if(pictures.begin(), pictures.end(), [&filename](const PictureEntry &e) @@ -2078,11 +2125,20 @@ void ProfileWidget2::updateThumbnail(QString filename, QImage thumbnail) // Replace the pixmap of the thumbnail with the newly calculated one. int size = Thumbnailer::defaultThumbnailSize(); it->thumbnail->setPixmap(QPixmap::fromImage(thumbnail.scaled(size, size, Qt::KeepAspectRatio))); + + // If the duration changed, update the line + if (duration.seconds != it->duration.seconds) { + it->duration = duration; + updateDurationLine(*it); + // If we created / removed a duration line, we have to update the thumbnail paint order. + updateThumbnailPaintOrder(); + } } } // Create a PictureEntry object and add its thumbnail to the scene if profile pictures are shown. ProfileWidget2::PictureEntry::PictureEntry(offset_t offsetIn, const QString &filenameIn, QGraphicsScene *scene) : offset(offsetIn), + duration(duration_t {0}), filename(filenameIn), thumbnail(new DivePictureItem) { @@ -2101,23 +2157,37 @@ bool ProfileWidget2::PictureEntry::operator< (const PictureEntry &e) const return std::tie(offset.seconds, filename) < std::tie(e.offset.seconds, e.filename); } +// This function updates the paint order of the thumbnails and duration-lines, such that later +// thumbnails are painted on top of previous thumbnails and duration-lines on top of the thumbnail +// they belong to. +void ProfileWidget2::updateThumbnailPaintOrder() +{ + if (!pictures.size()) + return; + // To get the correct sort order, we place in thumbnails at equal z-distances + // between thumbnailBaseZValue and (thumbnailBaseZValue + 1.0). + // Duration-lines are placed between the thumbnails. + double z = thumbnailBaseZValue; + double step = 1.0 / (double)pictures.size(); + for (PictureEntry &e: pictures) { + e.thumbnail->setBaseZValue(z); + if (e.durationLine) + e.durationLine->setZValue(z + step / 2.0); + z += step; + } +} + // Calculate the y-coordinates of the thumbnails, which are supposed to be sorted by x-coordinate. // This will also change the order in which the thumbnails are painted, to avoid weird effects, // when items are added later to the scene. This is done using the QGraphicsItem::packBefore() function. // We can't use the z-value, because that will be modified on hoverEnter and hoverExit events. void ProfileWidget2::calculatePictureYPositions() { - // Quit early if there are no items. The last loop in this function assumes that the vector is not empty. - if (pictures.empty()) - return; - double lastX = -1.0, lastY = 0.0; - for (auto it = pictures.begin(); it != pictures.end(); ++it) { - if (!it->thumbnail) - continue; + for (PictureEntry &e: pictures) { // 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. - double x = it->thumbnail->x(); + double x = e.thumbnail->x(); double y; if (lastX >= 0.0 && fabs(x - lastX) < 3 && lastY <= (10 + 14 * 3)) y = lastY + 3; @@ -2125,18 +2195,10 @@ void ProfileWidget2::calculatePictureYPositions() y = 10; lastX = x; lastY = y; - it->thumbnail->setY(y); - - // hoverEnter and hoverExit events modify the z-value. Objects with different z-values - // are not considered in stackBefore() calls. Therefore, just to be sure, reset the - // z-values of all picture entries. - it->thumbnail->setZValue(0.0); + e.thumbnail->setY(y); + updateDurationLine(e); // If we changed the y-position, we also have to change the duration-line. } - - // Plot the items in the correct order. Experience showed that this works only - // if we rearrange the items starting from the back. Therefore, use rbegin() and rend(). - for (auto it = pictures.rbegin(); std::next(it) != pictures.rend(); ++it) - std::next(it)->thumbnail->stackBefore(it->thumbnail.get()); + updateThumbnailPaintOrder(); } void ProfileWidget2::updateThumbnailXPos(PictureEntry &e) diff --git a/profile-widget/profilewidget2.h b/profile-widget/profilewidget2.h index 2640fb85a..bbad5b7cc 100644 --- a/profile-widget/profilewidget2.h +++ b/profile-widget/profilewidget2.h @@ -74,6 +74,7 @@ public: ProfileWidget2(QWidget *parent = 0); void resetZoom(); + void scale(qreal sx, qreal sy); void plotDive(struct dive *d = 0, bool force = false, bool clearPictures = false); void setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *vAxis, int vData, int hData, int zValue); void setPrintMode(bool mode, bool grayscale = false); @@ -127,7 +128,7 @@ slots: // Necessary to call from QAction's signals. void deleteCurrentDC(); void pointInserted(const QModelIndex &parent, int start, int end); void pointsRemoved(const QModelIndex &, int start, int end); - void updateThumbnail(QString filename, QImage thumbnail); + void updateThumbnail(QString filename, QImage thumbnail, duration_t duration); /* this is called for every move on the handlers. maybe we can speed up this a bit? */ void recreatePlannedDive(); @@ -234,14 +235,19 @@ private: // Pictures that are outside of the dive time are not shown. struct PictureEntry { offset_t offset; + duration_t duration; QString filename; std::unique_ptr thumbnail; + // For videos with known duration, we represent the duration of the video by a line + std::unique_ptr durationLine; PictureEntry (offset_t offsetIn, const QString &filenameIn, QGraphicsScene *scene); bool operator< (const PictureEntry &e) const; }; void updateThumbnailXPos(PictureEntry &e); std::vector pictures; void calculatePictureYPositions(); + void updateDurationLine(PictureEntry &e); + void updateThumbnailPaintOrder(); QList handles; void repositionDiveHandlers(); diff --git a/qt-models/divepicturemodel.cpp b/qt-models/divepicturemodel.cpp index c881ce5bf..f0b5eff47 100644 --- a/qt-models/divepicturemodel.cpp +++ b/qt-models/divepicturemodel.cpp @@ -165,7 +165,7 @@ int DivePictureModel::findPictureId(const QString &filename) return -1; } -void DivePictureModel::updateThumbnail(QString filename, QImage thumbnail) +void DivePictureModel::updateThumbnail(QString filename, QImage thumbnail, duration_t) { int i = findPictureId(filename); if (i >= 0) { diff --git a/qt-models/divepicturemodel.h b/qt-models/divepicturemodel.h index 6dc633fb2..3bd872d6a 100644 --- a/qt-models/divepicturemodel.h +++ b/qt-models/divepicturemodel.h @@ -2,6 +2,8 @@ #ifndef DIVEPICTUREMODEL_H #define DIVEPICTUREMODEL_H +#include "core/units.h" + #include #include #include @@ -28,7 +30,7 @@ signals: void picturesRemoved(const QVector &fileUrls); public slots: void setZoomLevel(int level); - void updateThumbnail(QString filename, QImage thumbnail); + void updateThumbnail(QString filename, QImage thumbnail, duration_t duration); private: DivePictureModel(); QVector pictures; diff --git a/subsurface-helper.cpp b/subsurface-helper.cpp index f37bd332c..e0c3b6353 100644 --- a/subsurface-helper.cpp +++ b/subsurface-helper.cpp @@ -31,10 +31,12 @@ #ifndef SUBSURFACE_TEST_DATA QObject *qqWindowObject = NULL; +static void register_meta_types(); void init_ui() { init_qt_late(); register_qml_types(); + register_meta_types(); #ifndef SUBSURFACE_MOBILE PluginManager::instance().loadPlugins(); @@ -137,6 +139,12 @@ void run_ui() #endif // SUBSURFACE_MOBILE qApp->exec(); } + +Q_DECLARE_METATYPE(duration_t) +static void register_meta_types() +{ + qRegisterMetaType(); +} #endif // not SUBSURFACE_TEST_DATA void register_qml_types()