From ebf9ce6d86a0f233c34c7083938a33024eddf9da Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Sat, 12 Aug 2023 22:59:56 +0200 Subject: [PATCH] profile: port picture code to qt-quick This was very painful, because I had to implement rearranging the paint order of the QSGNodes. The resulting code appears quite brittle. Let's see where that brings us. Signed-off-by: Berthold Stoeger --- Subsurface-mobile.pro | 2 + desktop-widgets/profilewidget.cpp | 67 +++- desktop-widgets/profilewidget.h | 1 + profile-widget/CMakeLists.txt | 2 + profile-widget/divepixmapitem.cpp | 101 ------ profile-widget/divepixmapitem.h | 33 -- profile-widget/pictureitem.cpp | 91 +++++ profile-widget/pictureitem.h | 33 ++ profile-widget/profilescene.cpp | 26 +- profile-widget/profilescene.h | 8 +- profile-widget/profileview.cpp | 538 ++++++++++++++++++++++++++++-- profile-widget/profileview.h | 46 ++- profile-widget/profilewidget2.cpp | 351 ------------------- profile-widget/profilewidget2.h | 31 -- profile-widget/zvalues.h | 1 + qt-quick/chartitem.cpp | 31 +- qt-quick/chartitem.h | 74 +++- qt-quick/chartitem_private.h | 66 ++++ qt-quick/chartitem_ptr.h | 3 + qt-quick/chartview.cpp | 50 ++- qt-quick/chartview.h | 8 +- stats/chartitem.cpp | 8 +- 22 files changed, 979 insertions(+), 592 deletions(-) create mode 100644 profile-widget/pictureitem.cpp create mode 100644 profile-widget/pictureitem.h create mode 100644 qt-quick/chartitem_private.h diff --git a/Subsurface-mobile.pro b/Subsurface-mobile.pro index 3559a9be4..83236338a 100644 --- a/Subsurface-mobile.pro +++ b/Subsurface-mobile.pro @@ -178,6 +178,7 @@ SOURCES += subsurface-mobile-main.cpp \ profile-widget/animationfunctions.cpp \ profile-widget/divepixmapcache.cpp \ profile-widget/divepixmapitem.cpp \ + profile-widget/pictureitem.cpp \ profile-widget/tankitem.cpp \ profile-widget/tooltipitem.cpp \ profile-widget/divelineitem.cpp \ @@ -338,6 +339,7 @@ HEADERS += \ profile-widget/diveprofileitem.h \ profile-widget/profilescene.h \ profile-widget/diveeventitem.h \ + profile-widget/pictureitem.h \ profile-widget/tankitem.h \ profile-widget/tooltipitem.h \ profile-widget/animationfunctions.h \ diff --git a/desktop-widgets/profilewidget.cpp b/desktop-widgets/profilewidget.cpp index 189fa09bf..f7cb687db 100644 --- a/desktop-widgets/profilewidget.cpp +++ b/desktop-widgets/profilewidget.cpp @@ -12,12 +12,13 @@ #include "core/subsurface-string.h" #include "qt-models/diveplannermodel.h" -#include #include +#include +#include #include #include #include -#include +#include // A resizing display of the Subsurface logo when no dive is shown class EmptyView : public QLabel { @@ -56,6 +57,57 @@ void EmptyView::resizeEvent(QResizeEvent *) update(); } +// We subclass the QQuickWidget so that we can easily react to drag&drop events +class ProfileViewWidget : public QQuickWidget +{ +public: + ProfileViewWidget(ProfileWidget &w) : w(w) + { + setAcceptDrops(true); + } +private: + static constexpr const char *picture_mime_format = "application/x-subsurfaceimagedrop"; + ProfileWidget &w; + void dropEvent(QDropEvent *event) override + { + if (event->mimeData()->hasFormat(picture_mime_format)) { + QByteArray itemData = event->mimeData()->data(picture_mime_format); + QDataStream dataStream(&itemData, QIODevice::ReadOnly); + + QString filename; + dataStream >> filename; + w.dropPicture(filename, event->pos()); + + if (event->source() == this) { + event->setDropAction(Qt::MoveAction); + event->accept(); + } else { + event->acceptProposedAction(); + } + } else { + event->ignore(); + } + } + void dragEnterEvent(QDragEnterEvent *event) override + { + // Does the same thing as dragMove event...? + dragMoveEvent(event); + } + void dragMoveEvent(QDragMoveEvent *event) override + { + if (event->mimeData()->hasFormat(picture_mime_format)) { + if (event->source() == this) { + event->setDropAction(Qt::MoveAction); + event->accept(); + } else { + event->acceptProposedAction(); + } + } else { + event->ignore(); + } + } +}; + static const QUrl urlProfileView = QUrl(QStringLiteral("qrc:/qml/profileview.qml")); ProfileWidget::ProfileWidget() : d(nullptr), dc(0), placingCommand(false) { @@ -77,7 +129,7 @@ ProfileWidget::ProfileWidget() : d(nullptr), dc(0), placingCommand(false) emptyView.reset(new EmptyView); - viewWidget.reset(new QQuickWidget); + viewWidget.reset(new ProfileViewWidget(*this)); viewWidget->setSource(urlProfileView); viewWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); @@ -421,3 +473,12 @@ void ProfileWidget::stopEdited() Setter s(placingCommand, true); Command::editProfile(editedDive.get(), dc, Command::EditProfileType::EDIT, 0); } + +void ProfileWidget::dropPicture(const QString &filename, QPoint pos) +{ + auto view = getView(); + if (!d || !view) + return; + offset_t offset { .seconds = int_cast(view->timeAt(pos)) }; + Command::setPictureOffset(d, filename, offset); +} diff --git a/desktop-widgets/profilewidget.h b/desktop-widgets/profilewidget.h index c36cf8f76..9e78c1bd5 100644 --- a/desktop-widgets/profilewidget.h +++ b/desktop-widgets/profilewidget.h @@ -27,6 +27,7 @@ public: void nextDC(); void prevDC(); void exitEditMode(); + void dropPicture(const QString &filename, QPoint p); dive *d; int dc; private diff --git a/profile-widget/CMakeLists.txt b/profile-widget/CMakeLists.txt index 5cd28655f..571336b41 100644 --- a/profile-widget/CMakeLists.txt +++ b/profile-widget/CMakeLists.txt @@ -20,6 +20,8 @@ set(SUBSURFACE_PROFILE_LIB_SRCS diverectitem.h divetextitem.cpp divetextitem.h + pictureitem.h + pictureitem.cpp profilescene.cpp profilescene.h profiletranslations.h diff --git a/profile-widget/divepixmapitem.cpp b/profile-widget/divepixmapitem.cpp index 91b7ac469..f3d548281 100644 --- a/profile-widget/divepixmapitem.cpp +++ b/profile-widget/divepixmapitem.cpp @@ -1,107 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 #include "profile-widget/divepixmapitem.h" -#include "profile-widget/animationfunctions.h" -#include "core/pref.h" -#include "core/qthelper.h" -#include "core/settings/qPrefDisplay.h" -#include "core/subsurface-qt/divelistnotifier.h" - -#include -#include -#include -#include DivePixmapItem::DivePixmapItem(QGraphicsItem *parent) : QGraphicsPixmapItem(parent) { } - -CloseButtonItem::CloseButtonItem(QGraphicsItem *parent): DivePixmapItem(parent) -{ - static QPixmap p = QPixmap(":list-remove-icon"); - setPixmap(p); - setFlag(ItemIgnoresTransformations); -} - -void CloseButtonItem::mousePressEvent(QGraphicsSceneMouseEvent *) -{ - emit clicked(); -} - -DivePictureItem::DivePictureItem(QGraphicsItem *parent): DivePixmapItem(parent), - canvas(new QGraphicsRectItem(this)), - shadow(new QGraphicsRectItem(this)), - button(new CloseButtonItem(this)), - baseZValue(0.0) -{ - setFlag(ItemIgnoresTransformations); - setAcceptHoverEvents(true); - setScale(0.2); - connect(&diveListNotifier, &DiveListNotifier::settingsChanged, this, &DivePictureItem::settingsChanged); - connect(button, &CloseButtonItem::clicked, [this] () { emit removePicture(fileUrl); }); - - canvas->setPen(Qt::NoPen); - canvas->setBrush(QColor(Qt::white)); - canvas->setFlag(ItemStacksBehindParent); - canvas->setZValue(-1); - - shadow->setPos(5,5); - shadow->setPen(Qt::NoPen); - shadow->setBrush(QColor(Qt::lightGray)); - shadow->setFlag(ItemStacksBehindParent); - shadow->setZValue(-2); - - button->setScale(0.2); - button->setZValue(7); - 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); -} - -void DivePictureItem::setPixmap(const QPixmap &pix) -{ - DivePixmapItem::setPixmap(pix); - QRectF r = boundingRect(); - canvas->setRect(0 - 10, 0 -10, r.width() + 20, r.height() + 20); - shadow->setRect(canvas->rect()); - button->setPos(boundingRect().width() - button->boundingRect().width() * 0.2, - boundingRect().height() - button->boundingRect().height() * 0.2); -} - -void DivePictureItem::hoverEnterEvent(QGraphicsSceneHoverEvent*) -{ - Animations::scaleTo(this, qPrefDisplay::animation_speed(), 1.0); - setZValue(baseZValue + 5.0); - - button->setOpacity(0); - button->show(); - Animations::show(button, qPrefDisplay::animation_speed()); -} - -void DivePictureItem::setFileUrl(const QString &s) -{ - fileUrl = s; -} - -void DivePictureItem::hoverLeaveEvent(QGraphicsSceneHoverEvent*) -{ - Animations::scaleTo(this, qPrefDisplay::animation_speed(), 0.2); - setZValue(baseZValue); - Animations::hide(button, qPrefDisplay::animation_speed()); -} - -void DivePictureItem::mousePressEvent(QGraphicsSceneMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - QDesktopServices::openUrl(QUrl::fromLocalFile(localFilePath(fileUrl))); -} diff --git a/profile-widget/divepixmapitem.h b/profile-widget/divepixmapitem.h index 44c5dd88e..97a17bf43 100644 --- a/profile-widget/divepixmapitem.h +++ b/profile-widget/divepixmapitem.h @@ -15,37 +15,4 @@ public: DivePixmapItem(QGraphicsItem *parent = 0); }; -class CloseButtonItem : public DivePixmapItem { - Q_OBJECT -public: - CloseButtonItem(QGraphicsItem *parent = 0); -signals: - void clicked(); -private: - void mousePressEvent(QGraphicsSceneMouseEvent *event) override; -}; - -class DivePictureItem : public DivePixmapItem { - Q_OBJECT - Q_PROPERTY(qreal scale WRITE setScale READ scale) -public: - DivePictureItem(QGraphicsItem *parent = 0); - void setPixmap(const QPixmap& pix); - void setBaseZValue(double z); - void setFileUrl(const QString& s); -signals: - void removePicture(const QString &fileUrl); -public slots: - void settingsChanged(); -private: - void hoverEnterEvent(QGraphicsSceneHoverEvent *event); - void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); - void mousePressEvent(QGraphicsSceneMouseEvent *event); - QString fileUrl; - QGraphicsRectItem *canvas; - QGraphicsRectItem *shadow; - CloseButtonItem *button; - double baseZValue; -}; - #endif // DIVEPIXMAPITEM_H diff --git a/profile-widget/pictureitem.cpp b/profile-widget/pictureitem.cpp new file mode 100644 index 000000000..738750ad9 --- /dev/null +++ b/profile-widget/pictureitem.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "pictureitem.h" +#include "zvalues.h" +#include + +static constexpr double scaleFactor = 0.2; +static constexpr double shadowSize = 5.0; +static constexpr double removeIconSize = 20.0; + +PictureItem::PictureItem(ChartView &view, double dpr) : + ChartPixmapItem(view, ProfileZValue::Pictures, false), + dpr(dpr) +{ + setScale(scaleFactor); // Start small +} + +PictureItem::~PictureItem() +{ +} + +void PictureItem::setPixmap(const QPixmap &picture) +{ + static QPixmap removeIcon = QPixmap(":list-remove-icon"); + + int shadowSizeInt = lrint(shadowSize * dpr); + resize(QSizeF(picture.width() + shadowSizeInt, picture.height() + shadowSizeInt)); // initializes canvas + img->fill(Qt::transparent); + painter->setPen(Qt::NoPen); + painter->setBrush(QColor(Qt::lightGray)); + painter->drawRect(shadowSizeInt, shadowSizeInt, picture.width(), picture.height()); + painter->drawPixmap(0, 0, picture, 0, 0, picture.width(), picture.height()); + + int removeIconSizeInt = lrint(::removeIconSize * dpr); + QPixmap icon = removeIcon.scaledToWidth(removeIconSizeInt, Qt::SmoothTransformation); + removeIconRect = QRect(picture.width() - icon.width(), 0, icon.width(), icon.height()); + painter->drawPixmap(picture.width() - icon.width(), 0, icon, 0, 0, icon.width(), icon.height()); +} + +double PictureItem::left() const +{ + return rect.left(); +} + +double PictureItem::right() const +{ + return rect.right(); +} + +bool PictureItem::underMouse(QPointF pos) const +{ + return rect.contains(pos); +} + +bool PictureItem::removeIconUnderMouse(QPointF pos) const +{ + if (!underMouse(pos)) + return false; + QPointF pos_rel = (pos - rect.topLeft()) / scale; + return removeIconRect.contains(pos_rel); +} + +void PictureItem::initAnimation(double scale, int animSpeed) +{ + if (animSpeed <= 0) + return setScale(1.0); + + fromScale = this->scale; + toScale = scale; +} + +void PictureItem::grow(int animSpeed) +{ + initAnimation(1.0, animSpeed); +} + +void PictureItem::shrink(int animSpeed) +{ + initAnimation(scaleFactor, animSpeed); +} + +static double mid(double from, double to, double fraction) +{ + return fraction == 1.0 ? to + : from + (to - from) * fraction; +} + +void PictureItem::anim(double progress) +{ + setScale(mid(fromScale, toScale, progress)); + setPositionDirty(); +} diff --git a/profile-widget/pictureitem.h b/profile-widget/pictureitem.h new file mode 100644 index 000000000..8cbce3575 --- /dev/null +++ b/profile-widget/pictureitem.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0 +// Shows a picture or video on the profile +#ifndef PICTUREMAPITEM_H +#define PICTUREMAPITEM_H + +#include "qt-quick/chartitem.h" +#include +#include + +class QPixmap; + +class PictureItem : public ChartPixmapItem { +public: + PictureItem(ChartView &view, double dpr); + ~PictureItem(); + void setPixmap(const QPixmap &pix); + void setFileUrl(const QString &s); + double right() const; + double left() const; + bool underMouse(QPointF pos) const; + bool removeIconUnderMouse(QPointF pos) const; + void grow(int animSpeed); + void shrink(int animSpeed); + void anim(double progress); +private: + double dpr; + QRectF removeIconRect; + double fromScale, toScale; // For animation + + void initAnimation(double scale, int animSpeed); +}; + +#endif diff --git a/profile-widget/profilescene.cpp b/profile-widget/profilescene.cpp index ff6c78fe1..9cddc28e3 100644 --- a/profile-widget/profilescene.cpp +++ b/profile-widget/profilescene.cpp @@ -377,7 +377,8 @@ static double max_gas(const plot_info &pi, double gas_pressures::*gas) return ret; } -void ProfileScene::plotDive(const struct dive *dIn, int dcIn, int animSpeed, DivePlannerPointsModel *plannerModel, +void ProfileScene::plotDive(const struct dive *dIn, int dcIn, int animSpeed, bool simplified, + DivePlannerPointsModel *plannerModel, bool inPlanner, bool keepPlotInfo, bool calcMax, double zoom, double zoomedPosition) { d = dIn; @@ -428,11 +429,6 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, int animSpeed, Div bool hasHeartBeat = plotInfo.maxhr; // For mobile we might want to turn of some features that are normally shown. -#ifdef SUBSURFACE_MOBILE - bool simplified = true; -#else - bool simplified = false; -#endif updateVisibility(hasHeartBeat, simplified); updateAxes(hasHeartBeat, simplified); @@ -570,7 +566,7 @@ void ProfileScene::draw(QPainter *painter, const QRect &pos, { QSize size = pos.size(); resize(QSizeF(size)); - plotDive(d, dc, 0, plannerModel, inPlanner, false, true); + plotDive(d, dc, 0, false, plannerModel, inPlanner, false, true); QImage image(pos.size(), QImage::Format_ARGB32); image.fill(getColor(::BACKGROUND, isGrayscale)); @@ -616,6 +612,22 @@ int ProfileScene::timeAt(QPointF pos) const return lrint(timeAxis->valueAt(pos)); } +std::pair ProfileScene::minMaxTime() +{ + return { timeAxis->minimum(), timeAxis->maximum() }; +} + +double ProfileScene::yToScreen(double y) +{ + auto [min, max] = profileYAxis->screenMinMax(); + return min + (max - min) * y; +} + +double ProfileScene::posAtTime(double time) +{ + return timeAxis->posAtValue(time); +} + std::vector> ProfileScene::eventsAt(QPointF pos) const { std::vector> res; diff --git a/profile-widget/profilescene.h b/profile-widget/profilescene.h index b65aa1a58..c71dbc151 100644 --- a/profile-widget/profilescene.h +++ b/profile-widget/profilescene.h @@ -41,7 +41,8 @@ public: // Can be compared with literal 1.0 to determine "end" state. // If a plannerModel is passed, the deco-information is taken from there. - void plotDive(const struct dive *d, int dc, int animSpeed, DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false, + void plotDive(const struct dive *d, int dc, int animSpeed, bool simplified, + DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false, bool keepPlotInfo = false, bool calcMax = true, double zoom = 1.0, double zoomedPosition = 0.0); void draw(QPainter *painter, const QRect &pos, @@ -49,7 +50,10 @@ public: DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false); double calcZoomPosition(double zoom, double originalPos, double delta); const plot_info &getPlotInfo() const; - int timeAt(QPointF pos) const; + int timeAt(QPointF pos) const; // time in seconds + std::pair minMaxTime(); // time in seconds + double posAtTime(double time); // time in seconds + double yToScreen(double y); // For pictures: depth given in fration of displayed range. std::vector> eventsAt(QPointF pos) const; const struct dive *d; diff --git a/profile-widget/profileview.cpp b/profile-widget/profileview.cpp index 576d7d9d4..37cab097d 100644 --- a/profile-widget/profileview.cpp +++ b/profile-widget/profileview.cpp @@ -1,25 +1,32 @@ // SPDX-License-Identifier: GPL-2.0 #include "profileview.h" +#include "pictureitem.h" #include "profilescene.h" #include "tooltipitem.h" #include "zvalues.h" #include "core/dive.h" #include "core/divelog.h" +#include "commands/command.h" #include "core/errorhelper.h" +#include "core/imagedownloader.h" #include "core/pref.h" +#include "core/qthelper.h" // for localFilePath() +#include "core/range.h" #include "core/settings/qPrefDisplay.h" #include "core/settings/qPrefPartialPressureGas.h" #include "core/settings/qPrefTechnicalDetails.h" +#include "core/subsurface-qt/divelistnotifier.h" #include "qt-quick/chartitem.h" #include #include #include +#include #include // Class templates for animations (if any). Might want to do our own. // Calls the function object passed in the constructor with a time argument, -// where 0.0 = start at 1.0 = end.. +// where 0.0 = start at 1.0 = end. // On the last invocation, a 1.0 literal is passed, so floating-point // comparison is OK. class ProfileAnimation : public QAbstractAnimation { @@ -56,22 +63,26 @@ public: } }; -// Helper function to make creation of animations somewhat more palatable +// Helper function to make creation of animations somewhat more palatable. +// Returns a null-pointer if animSpeed is <= 0 to simplify logic. template std::unique_ptr> make_anim(FUNC func, int animSpeed) { - return std::make_unique>(func, animSpeed); + return animSpeed > 0 ? std::make_unique>(func, animSpeed) + : std::unique_ptr>(); } ProfileView::ProfileView(QQuickItem *parent) : ChartView(parent, ProfileZValue::Count), d(nullptr), dc(0), + simplified(false), dpr(1.0), zoomLevel(1.00), zoomedPosition(0.0), panning(false), empty(true), - shouldCalculateMax(true) + shouldCalculateMax(true), + highlightedPicture(nullptr) { setBackgroundColor(Qt::black); setFlag(ItemHasContents, true); @@ -94,7 +105,12 @@ ProfileView::ProfileView(QQuickItem *parent) : ChartView(parent, ProfileZValue:: connect(tec, &qPrefTechnicalDetails::show_sacChanged , this, &ProfileView::replot); connect(tec, &qPrefTechnicalDetails::zoomed_plotChanged , this, &ProfileView::replot); connect(tec, &qPrefTechnicalDetails::decoinfoChanged , this, &ProfileView::replot); - connect(tec, &qPrefTechnicalDetails::show_pictures_in_profileChanged , this, &ProfileView::replot); + connect(tec, &qPrefTechnicalDetails::show_pictures_in_profileChanged , [this]() { + if (d) { + plotPictures(d, false); + update(); + } + } ); connect(tec, &qPrefTechnicalDetails::tankbarChanged , this, &ProfileView::replot); connect(tec, &qPrefTechnicalDetails::percentagegraphChanged , this, &ProfileView::replot); connect(tec, &qPrefTechnicalDetails::infoboxChanged , this, &ProfileView::replot); @@ -104,6 +120,12 @@ ProfileView::ProfileView(QQuickItem *parent) : ChartView(parent, ProfileZValue:: connect(pp_gas, &qPrefPartialPressureGas::pn2Changed, this, &ProfileView::replot); connect(pp_gas, &qPrefPartialPressureGas::po2Changed, this, &ProfileView::replot); + connect(Thumbnailer::instance(), &Thumbnailer::thumbnailChanged, this, &ProfileView::updateThumbnail, Qt::QueuedConnection); + + connect(&diveListNotifier, &DiveListNotifier::picturesRemoved, this, &ProfileView::picturesRemoved); + connect(&diveListNotifier, &DiveListNotifier::picturesAdded, this, &ProfileView::picturesAdded); + connect(&diveListNotifier, &DiveListNotifier::pictureOffsetChanged, this, &ProfileView::pictureOffsetChanged); + setAcceptTouchEvents(true); setAcceptHoverEvents(true); } @@ -119,23 +141,28 @@ ProfileView::~ProfileView() void ProfileView::resetPointers() { profileItem.reset(); + tooltip.reset(); + pictures.clear(); + highlightedPicture = nullptr; } void ProfileView::plotAreaChanged(const QSizeF &s) { + int flags = simplified ? RenderFlags::Simplified : RenderFlags::None; if (!empty) - plotDive(d, dc, RenderFlags::Instant); + plotDive(d, dc, flags | RenderFlags::Instant); } void ProfileView::replot() { + int flags = simplified ? RenderFlags::Simplified : RenderFlags::None; if (!empty) - plotDive(d, dc, RenderFlags::None); + plotDive(d, dc, flags); } void ProfileView::clear() { - //clearPictures(); + clearPictures(); //disconnectPlannerConnections(); if (profileScene) profileScene->clear(); @@ -152,6 +179,7 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags) { d = dIn; dc = dcIn; + simplified = flags & RenderFlags::Simplified; if (!d) { clear(); return; @@ -183,7 +211,7 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags) int animSpeed = flags & RenderFlags::Instant ? 0 : qPrefDisplay::animation_speed(); profileScene->resize(size()); - profileScene->plotDive(d, dc, animSpeed, model, inPlanner, + profileScene->plotDive(d, dc, animSpeed, simplified, model, inPlanner, flags & RenderFlags::DontRecalculatePlotInfo, shouldCalculateMax, zoomLevel, zoomedPosition); background = inPlanner ? QColor("#D7E3EF") : getColor(::BACKGROUND, false); @@ -198,10 +226,14 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags) //} // On zoom / pan don't recreate the picture thumbnails, only change their position. - //if (flags & RenderFlags::DontRecalculatePlotInfo) - //updateThumbnails(); - //else - //plotPicturesInternal(d, flags & RenderFlags::Instant); + if (!inPlanner) { + if (flags & RenderFlags::DontRecalculatePlotInfo) + updateThumbnails(); + else + plotPictures(d, flags); + } else { + clearPictures(); + } update(); @@ -226,10 +258,7 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags) } // Reset animation. - if (animSpeed <= 0) - animation.reset(); - else - animation = make_anim([this](double progress) { anim(progress); }, animSpeed); + animation = make_anim([this](double progress) { anim(progress); }, animSpeed); } void ProfileView::anim(double fraction) @@ -251,8 +280,9 @@ void ProfileView::setZoom(double level) { level = std::clamp(level, 1.0, 20.0); double old = std::exchange(zoomLevel, level); + int flags = simplified ? RenderFlags::Simplified : RenderFlags::None; if (level != old) - plotDive(d, dc, RenderFlags::DontRecalculatePlotInfo); + plotDive(d, dc, flags | RenderFlags::DontRecalculatePlotInfo); emit zoomLevelChanged(); } @@ -285,11 +315,32 @@ void ProfileView::mousePressEvent(QMouseEvent *event) if (event->isAccepted()) return; - panning = true; - QPointF pos = mapToScene(event->pos()); - panStart(pos.x(), pos.y()); - setCursor(Qt::ClosedHandCursor); - event->accept(); + // Check if current picture is clicked + if (highlightedPicture && + highlightedPicture->thumbnail->underMouse(event->pos()) && + event->button() == Qt::LeftButton) { + if (highlightedPicture->thumbnail->removeIconUnderMouse(event->pos())) { + if (d) { + dive *d_nonconst = const_cast(d); // Ouch. Let's just make the dive pointer non-const. + Command::removePictures({ { d_nonconst, { highlightedPicture->filename.toStdString() } } }); + } + } else { + QDesktopServices::openUrl( + QUrl::fromLocalFile(localFilePath(highlightedPicture->filename)) + ); + } + event->accept(); + return; + } + + // Do panning + if (event->button() == Qt::LeftButton) { + panning = true; + QPointF pos = mapToScene(event->pos()); + panStart(pos.x(), pos.y()); + setCursor(Qt::ClosedHandCursor); + event->accept(); + } } void ProfileView::mouseReleaseEvent(QMouseEvent *event) @@ -331,7 +382,8 @@ int ProfileView::getDiveId() const void ProfileView::setDiveId(int id) { - plotDive(divelog.dives.get_by_uniq_id(id), 0); + // This is used by mobile, therefore use the simplified version + plotDive(divelog.dives.get_by_uniq_id(id), RenderFlags::Simplified); } int ProfileView::numDC() const @@ -382,14 +434,61 @@ void ProfileView::pan(double x, double y) zoomedPosition = profileScene->calcZoomPosition(zoomLevel, panningOriginalProfilePosition, panningOriginalMousePosition - x); + int flags = simplified ? RenderFlags::Simplified : RenderFlags::None; if (oldPos != zoomedPosition) - plotDive(d, dc, RenderFlags::Instant | RenderFlags::DontRecalculatePlotInfo); // TODO: animations don't work when scrolling + plotDive(d, dc, flags | RenderFlags::Instant | RenderFlags::DontRecalculatePlotInfo); // TODO: animations don't work when scrolling } void ProfileView::hoverEnterEvent(QHoverEvent *) { } +void ProfileView::shrinkPictureItem(PictureEntry &e, int animSpeed) +{ + auto it = std::find_if(pictures.begin(), pictures.end(), [&e](const PictureEntry &e2) + { return &e == &e2; }); + if (it != pictures.end()) { // If we didn't find it, something is very weird. + ++it; + if (it != pictures.end() && &*it == highlightedPicture) + ++it; + } + if (it != pictures.end()) { + e.thumbnail->moveBefore(*it->thumbnail); + if (e.durationLine) + e.durationLine->moveBefore(*it->thumbnail); + } + e.thumbnail->shrink(animSpeed); + e.animation = make_anim([this, thumbnail = e.thumbnail](double progress) + { thumbnail->anim(progress); update(); }, animSpeed); +} + +void ProfileView::growPictureItem(PictureEntry &e, int animSpeed) +{ + e.thumbnail->grow(animSpeed); + e.thumbnail->moveBack(); + if (e.durationLine) + e.durationLine->moveBack(); + e.animation = make_anim([this, thumbnail = e.thumbnail](double progress) + { thumbnail->anim(progress); update(); }, animSpeed); +} + +ProfileView::PictureEntry *ProfileView::getPictureUnderMouse(QPointF pos) +{ + // First, check highlighted picture. + if (highlightedPicture && highlightedPicture->thumbnail->underMouse(pos)) + return highlightedPicture; + + // Do binary search using the fact that pictures are stored chronologically. + auto it1 = std::lower_bound(pictures.begin(), pictures.end(), pos.x(), [](PictureEntry &p, double x) + { return p.thumbnail->right() < x; }); // Skip over pictures to left of mouse. + auto it2 = std::lower_bound(it1, pictures.end(), pos.x(), [](PictureEntry &p, double x) + { return p.thumbnail->left() < x; }); // Search until pictures are right of mouse. + // Check potential pictures from the rear, because these are on top of the prior pictures. + auto it = std::find_if(std::reverse_iterator(it2), std::reverse_iterator(it1), + [pos](PictureEntry &p) { return p.thumbnail->underMouse(pos); }); + return it != std::reverse_iterator(it1) ? &*it : nullptr; +} + void ProfileView::hoverMoveEvent(QHoverEvent *event) { if (!profileScene) @@ -399,25 +498,398 @@ void ProfileView::hoverMoveEvent(QHoverEvent *event) // resizing the ToolTipItem we get spurious hoverMoveEvents, which // restarts the animation, giving an infinite loop. // Prevent this by comparing to the old mouse position. - if (std::exchange(previousHoveMovePosition, event->pos()) == previousHoveMovePosition) + QPointF pos = event->pos(); + if (std::exchange(previousHoverMovePosition, pos) == previousHoverMovePosition) return; if (tooltip && prefs.infobox) { - updateTooltip(event->pos(), false, qPrefDisplay::animation_speed()); // TODO: plan mode + updateTooltip(pos, false, qPrefDisplay::animation_speed()); // TODO: plan mode + update(); + } + + PictureEntry *pictureUnderMouse = getPictureUnderMouse(pos); + if (pictureUnderMouse) { + PictureEntry *oldHighlighted = std::exchange(highlightedPicture, pictureUnderMouse); + if (highlightedPicture != oldHighlighted) { + int animSpeed = qPrefDisplay::animation_speed(); + growPictureItem(*pictureUnderMouse, animSpeed); + if (oldHighlighted) + shrinkPictureItem(*oldHighlighted, animSpeed); + } + update(); + } else if (highlightedPicture) { + int animSpeed = qPrefDisplay::animation_speed(); + shrinkPictureItem(*highlightedPicture, animSpeed); + highlightedPicture = nullptr; update(); } } +void ProfileView::unhighlightPicture() +{ + PictureEntry *oldHighlighted = std::exchange(highlightedPicture, nullptr); + int animSpeed = qPrefDisplay::animation_speed(); + if (oldHighlighted) + shrinkPictureItem(*oldHighlighted, animSpeed); +} + +int ProfileView::timeAt(QPointF pos) const +{ + return profileScene->timeAt(pos); +} + void ProfileView::updateTooltip(QPointF pos, bool plannerMode, int animSpeed) { - int time = profileScene->timeAt(pos); + int time = timeAt(pos); auto events = profileScene->eventsAt(pos); tooltip->update(d, dpr, time, profileScene->getPlotInfo(), events, plannerMode, animSpeed); // Reset animation. - if (animSpeed <= 0) - tooltip_animation.reset(); - else - tooltip_animation = make_anim([this](double progress) - { if (tooltip) tooltip->anim(progress); update(); }, animSpeed); + tooltip_animation = make_anim([this](double progress) + { if (tooltip) tooltip->anim(progress); update(); }, animSpeed); +} + +// Create a PictureEntry object and add its thumbnail to the scene if profile pictures are shown. +ProfileView::PictureEntry::PictureEntry(offset_t offset, const QString &filename, ChartItemPtr thumbnail, double dpr, bool synchronous) : offset(offset), + filename(filename), + thumbnail(thumbnail) +{ + int size = lrint(Thumbnailer::defaultThumbnailSize() * dpr); + QImage img = Thumbnailer::instance()->fetchThumbnail(filename, false).scaled(size, size, Qt::KeepAspectRatio); + thumbnail->setPixmap(QPixmap::fromImage(img)); +} + +// Define a default sort order for picture-entries: sort lexicographically by timestamp and filename. +bool ProfileView::PictureEntry::operator< (const PictureEntry &e) const +{ + // Use std::tie() for lexicographical sorting. + return std::tie(offset.seconds, filename) < std::tie(e.offset.seconds, e.filename); +} + +static constexpr double durationLineWidth = 2.5; +static constexpr double durationLinePenWidth = 1.0; + +// Reset the duration line after an image was moved or we found a new duration +void ProfileView::updateDurationLine(PictureEntry &e) +{ + if (e.duration.seconds > 0) { + // We know the duration of this video, reset the line symbolizing its extent accordingly + double begin = profileScene->posAtTime(e.offset.seconds); + double end = profileScene->posAtTime(e.offset.seconds + e.duration.seconds); + + if (!e.durationLine) + e.durationLine = createChartItem(ProfileZValue::Pictures, + QPen(getColor(DURATION_LINE, false)), + getColor(::BACKGROUND, false), + durationLinePenWidth * dpr, + false); + e.durationLine->resize(QSizeF(end - begin, durationLineWidth * dpr)); + e.durationLine->setPos(QPointF(begin, e.y - durationLineWidth * dpr - durationLinePenWidth * dpr)); + e.durationLine->moveAfter(*e.thumbnail); + } else { + // This is either a picture or a video with unknown duration. + // In case there was a line (how could that be?) remove it. + if (e.durationLine) + deleteChartItem(e.durationLine); + } +} + +// This function is called asynchronously by the thumbnailer if a thumbnail +// was fetched from disk or freshly calculated. +void ProfileView::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) + { return e.filename == filename; }); + + // If we didn't find a picture, it does either not belong to the current dive, + // or its timestamp is outside of the profile. + if (it != pictures.end()) { + // Replace the pixmap of the thumbnail with the newly calculated one. + int size = lrint(Thumbnailer::defaultThumbnailSize() * dpr); + 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); + } + + update(); + } +} + +// Calculate the y-coordinates of the thumbnails, which are supposed to be sorted by x-coordinate. +void ProfileView::calculatePictureYPositions() +{ + double lastX = -1.0, lastY = 0.0; + constexpr double yStart = 0.05; // At which depth the thumbnails start (in fraction of total depth). + constexpr double yStep = 0.01; // Increase of depth for overlapping thumbnails (in fraction of total depth). + const double xSpace = 18.0 * dpr; // Horizontal range in which thumbnails are supposed to be overlapping (in pixels). + constexpr int maxDepth = 14; // Maximal depth of thumbnail stack (in thumbnails). + for (PictureEntry &e: pictures) { + // Invisible items are outside of the shown range - ignore. + if (!e.thumbnail->isVisible()) + continue; + + // 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. + if (e.x < 0.0) + continue; + double y; + if (lastX >= 0.0 && fabs(e.x - lastX) < xSpace * dpr && lastY <= (yStart + maxDepth * yStep) - 1e-10) + y = lastY + yStep; + else + y = yStart; + lastX = e.x; + lastY = y; + e.y = profileScene->yToScreen(y); + e.thumbnail->setPos(QPointF(e.x, e.y)); + updateDurationLine(e); // If we changed the y-position, we also have to change the duration-line. + } +} + +void ProfileView::updateThumbnailXPos(PictureEntry &e) +{ + // Here, we only set the x-coordinate of the picture. The y-coordinate + // will be set later in calculatePictureYPositions(). + // Thumbnails outside of the shown range are hidden. + double time = e.offset.seconds; + auto [min, max] = profileScene->minMaxTime(); + if (time >= min && time <= max) { + e.x = profileScene->posAtTime(time); + e.thumbnail->setVisible(true); + if (e.durationLine) + e.durationLine->setVisible(true); + } else { + e.thumbnail->setVisible(false); + if (e.durationLine) + e.durationLine->setVisible(false); + } +} + +void ProfileView::clearPictures() +{ + // The ChartItemPtrs are non-owning, so we have to delete the pictures manually. Sad. + for (auto &e: pictures) { + if (e.durationLine) + deleteChartItem(e.durationLine); + deleteChartItem(e.thumbnail); + } + pictures.clear(); + highlightedPicture = nullptr; +} + +// Helper function to compare offset_ts. +static bool operator<(offset_t o1, offset_t o2) +{ + return o1.seconds < o2.seconds; +} + +// Note: the synchronous flag is currently not used. +void ProfileView::plotPictures(const struct dive *d, bool synchronous) +{ + clearPictures(); + + if (!prefs.show_pictures_in_profile) + return; + + // Collect and sort pictures, so that we can add them in the correct order. + // Make sure the sorting function is equivalent to PictureEntry::operator<(). + std::vector> picturesToAdd; + picturesToAdd.reserve(d->pictures.size()); + for (auto &picture: d->pictures) { + if (picture.offset.seconds > 0 && picture.offset.seconds <= d->duration.seconds) + picturesToAdd.emplace_back(picture.offset, QString::fromStdString(picture.filename)); + } + if (picturesToAdd.empty()) + return; + std::sort(picturesToAdd.begin(), picturesToAdd.end()); // Use lexicographical comparison of std::pair + + // Fetch all pictures of the dive, but consider only those that are within the dive time. + // For each picture, create a PictureEntry object in the pictures-vector. + // emplace_back() constructs an object at the end of the vector. The parameters are passed directly to the constructor. + for (auto [offset, fn]: picturesToAdd) { + pictures.emplace_back(offset, fn, createChartItem(dpr), + dpr, synchronous); + } + + updateThumbnails(); +} + +void ProfileView::updateThumbnails() +{ + // Calculate thumbnail positions. First the x-coordinates and and then the y-coordinates. + for (PictureEntry &e: pictures) + updateThumbnailXPos(e); + calculatePictureYPositions(); +} + +// I dislike that we need this - the object should free its resources autonomously. +void ProfileView::removePictureThumbnail(PictureEntry &entry) +{ + if (&entry == highlightedPicture) + highlightedPicture = nullptr; + if (entry.durationLine) + deleteChartItem(entry.durationLine); + deleteChartItem(entry.thumbnail); +} + +// Remove the pictures with the given filenames from the profile plot. +void ProfileView::picturesRemoved(dive *d, QVector fileUrls) +{ + if (!prefs.show_pictures_in_profile) + return; + + unhighlightPicture(); + + // Use a custom implementation of the erase-remove idiom to erase pictures: + // https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom + // In contrast to std::remove_if() we can act on the item to be removed. + // (c.f. erase-remove idiom: https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom) + auto it1 = pictures.begin(); + for(auto it2 = pictures.begin(); it2 != pictures.end(); ++it2) { + // Check whether filename of entry is in list of provided filenames + if (std::find(fileUrls.begin(), fileUrls.end(), it2->filename) != fileUrls.end()) { + removePictureThumbnail(*it2); + } else { + if (it2 != it1) + *it1 = std::move(*it2); + it1++; + } + } + pictures.erase(it1, pictures.end()); + calculatePictureYPositions(); + update(); +} + +void ProfileView::moveThumbnailBefore(PictureEntry &e, std::vector::iterator &before) +{ + if (before != pictures.end()) + e.thumbnail->moveBefore(*before->thumbnail); + else + e.thumbnail->moveBack(); + if (e.durationLine) + e.durationLine->moveAfter(*e.thumbnail); +} + +void ProfileView::picturesAdded(dive *d, QVector pics) +{ + if (!prefs.show_pictures_in_profile) + return; + + // We might rearrange pictures, which makes the highlighted picture pointer invalid. + unhighlightPicture(); + + // Collect and sort pictures, so that we can add them in the correct order. + // Make sure the sorting function is equivalent to PictureEntry::operator<(). + std::vector> picturesToAdd; + picturesToAdd.reserve(pics.size()); + for (const picture &pic: pics) { + if (pic.offset.seconds > 0 && pic.offset.seconds <= d->duration.seconds) + picturesToAdd.emplace_back(pic.offset, QString::fromStdString(pic.filename)); + } + if (picturesToAdd.empty()) + return; + + std::sort(picturesToAdd.begin(), picturesToAdd.end()); // Use lexicographical comparison of std::pair + + auto it = pictures.begin(); + for (auto &[offset, fn]: picturesToAdd) { + // Do binary search. + it = std::lower_bound(it, pictures.end(), std::make_pair(offset, fn), + [](const PictureEntry &e, const std::tuple &p) + { return std::tie(e.offset, e.filename) < p; }); + it = pictures.emplace(it, offset, fn, createChartItem(dpr), dpr, false); + updateThumbnailXPos(*it); + auto it2 = std::next(it); + + // Assert correct drawing order. + if (it2 == pictures.end()) + it->thumbnail->moveBack(); + else + it->thumbnail->moveBefore(*it2->thumbnail); + it = it2; + } + + calculatePictureYPositions(); + update(); +} + +void ProfileView::pictureOffsetChanged(dive *dIn, QString filename, offset_t offset) +{ + if (!prefs.show_pictures_in_profile) + return; + + if (dIn != d) + return; // Picture of a different dive than the one shown changed. + + // We might rearrange pictures, which makes the highlighted picture pointer invalid. + unhighlightPicture(); + + // Calculate time in dive where picture was dropped and whether the new position is during the dive. + bool duringDive = d && offset.seconds > 0 && offset.seconds < d->duration.seconds; + + // A picture was drag&dropped onto the profile: We have four cases to consider: + // 1a) The image was already shown on the profile and is moved to a different position on the profile. + // Calculate the new position and move the picture. + // 1b) The image was on the profile and is moved outside of the dive time. + // Remove the picture. + // 2a) The image was not on the profile and is moved into the dive time. + // Add the picture to the profile. + // 2b) The image was not on the profile and is moved outside of the dive time. + // Do nothing. + auto oldPos = std::find_if(pictures.begin(), pictures.end(), [filename](const PictureEntry &e) + { return e.filename == filename; }); + if (oldPos != pictures.end()) { + // Cases 1a) and 1b): picture is on profile + if (duringDive) { + // Case 1a): move to new position + // First, find new position. Note that we also have to compare filenames, + // because it is quite easy to generate equal offsets. + auto newPos = std::find_if(pictures.begin(), pictures.end(), [offset, &filename](const PictureEntry &e) + { return std::tie(e.offset.seconds, e.filename) > std::tie(offset.seconds, filename); }); + // Set new offset + oldPos->offset.seconds = offset.seconds; + updateThumbnailXPos(*oldPos); + + // Update drawing order + // Move image from old to new position + moveThumbnailBefore(*oldPos, newPos); + + int oldIndex = oldPos - pictures.begin(); + int newIndex = newPos - pictures.begin(); + move_in_range(pictures, oldIndex, oldIndex + 1, newIndex); + } else { + // Case 1b): remove picture + removePictureThumbnail(*oldPos); + pictures.erase(oldPos); + } + + // In both cases the picture list changed, therefore we must recalculate the y-coordinates. + calculatePictureYPositions(); + } else { + // Cases 2a) and 2b): picture not on profile. We only have to take action for + // the first case: picture is moved into dive-time. + if (duringDive) { + // Case 2a): add the picture at the appropriate position. + // The case move from outside-to-outside of the profile plot was handled by + // the "duringDive" condition in the if above. + // As in the case 1a), we have to also consider filenames in the case of equal offsets. + auto newPos = std::find_if(pictures.begin(), pictures.end(), [offset, &filename](const PictureEntry &e) + { return std::tie(e.offset.seconds, e.filename) > std::tie(offset.seconds, filename); }); + // emplace() constructs the element at the given position in the vector. + // The parameters are passed directly to the contructor. + // The call returns an iterator to the new element (which might differ from + // the old iterator, since the buffer might have been reallocated). + newPos = pictures.emplace(newPos, offset, filename, createChartItem(dpr), dpr, false); + + // Update thumbnail paint order + auto nextPos = std::next(newPos); + moveThumbnailBefore(*newPos, nextPos); + updateThumbnailXPos(*newPos); + calculatePictureYPositions(); + } + } + update(); } diff --git a/profile-widget/profileview.h b/profile-widget/profileview.h index 350d5aac1..de8aec20f 100644 --- a/profile-widget/profileview.h +++ b/profile-widget/profileview.h @@ -3,12 +3,16 @@ #define PROFILE_VIEW_H #include "qt-quick/chartview.h" +#include "core/units.h" #include class ChartGraphicsSceneItem; +class ChartRectItem; +class PictureItem; class ProfileAnimation; class ProfileScene; class ToolTipItem; +struct picture; class ProfileView : public ChartView { Q_OBJECT @@ -28,9 +32,11 @@ public: static constexpr int DontRecalculatePlotInfo = 1 << 1; static constexpr int EditMode = 1 << 2; static constexpr int PlanMode = 1 << 3; + static constexpr int Simplified = 1 << 4; // For mobile's overview page }; void plotDive(const struct dive *d, int dc, int flags = RenderFlags::None); + int timeAt(QPointF pos) const; void clear(); void resetZoom(); void anim(double fraction); @@ -48,6 +54,7 @@ signals: private: const struct dive *d; int dc; + bool simplified; double dpr; double zoomLevel, zoomLevelPinchStart; double zoomedPosition; // Position when zoomed: 0.0 = beginning, 1.0 = end. @@ -77,7 +84,44 @@ private: void updateTooltip(QPointF pos, bool plannerMode, int animSpeed); std::unique_ptr tooltip_animation; - QPointF previousHoveMovePosition; + QPointF previousHoverMovePosition; + + // The list of pictures in this plot. The pictures are sorted by offset in seconds. + // For the same offset, sort by filename. + // Pictures that are outside of the dive time are not shown. + struct PictureEntry { + offset_t offset; + double x, y; + duration_t duration; + QString filename; + ChartItemPtr thumbnail; + // For videos with known duration, we represent the duration of the video by a line + ChartItemPtr durationLine; + std::unique_ptr animation; + PictureEntry (offset_t offset, const QString &filename, ChartItemPtr thumbnail, double dpr, bool synchronous); + bool operator< (const PictureEntry &e) const; + }; + std::vector pictures; + PictureEntry *highlightedPicture; + + // Picture (media) related functions + void picturesRemoved(dive *d, QVector filenames); + void picturesAdded(dive *d, QVector pics); + void pictureOffsetChanged(dive *d, QString filename, offset_t offset); + void updateDurationLine(PictureEntry &e); + void updateThumbnail(QString filename, QImage thumbnail, duration_t duration); + void updateThumbnailPaintOrder(); + void calculatePictureYPositions(); + void updateThumbnailXPos(PictureEntry &e); + void plotPictures(const struct dive *d, bool synchronous); + void updateThumbnails(); + void clearPictures(); + void removePictureThumbnail(PictureEntry &entry); + void unhighlightPicture(); + void shrinkPictureItem(PictureEntry &e, int animSpeed); + void growPictureItem(PictureEntry &e, int animSpeed); + void moveThumbnailBefore(PictureEntry &e, std::vector::iterator &before); + PictureEntry *getPictureUnderMouse(QPointF pos); // For mobile int getDiveId() const; diff --git a/profile-widget/profilewidget2.cpp b/profile-widget/profilewidget2.cpp index 732174a51..975db59cb 100644 --- a/profile-widget/profilewidget2.cpp +++ b/profile-widget/profilewidget2.cpp @@ -75,11 +75,8 @@ ProfileWidget2::ProfileWidget2(DivePlannerPointsModel *plannerModelIn, double dp setAcceptDrops(true); connect(Thumbnailer::instance(), &Thumbnailer::thumbnailChanged, this, &ProfileWidget2::updateThumbnail, Qt::QueuedConnection); - connect(&diveListNotifier, &DiveListNotifier::picturesRemoved, this, &ProfileWidget2::picturesRemoved); - connect(&diveListNotifier, &DiveListNotifier::picturesAdded, this, &ProfileWidget2::picturesAdded); connect(&diveListNotifier, &DiveListNotifier::cylinderEdited, this, &ProfileWidget2::profileChanged); connect(&diveListNotifier, &DiveListNotifier::eventsChanged, this, &ProfileWidget2::profileChanged); - connect(&diveListNotifier, &DiveListNotifier::pictureOffsetChanged, this, &ProfileWidget2::pictureOffsetChanged); connect(&diveListNotifier, &DiveListNotifier::divesChanged, this, &ProfileWidget2::divesChanged); connect(&diveListNotifier, &DiveListNotifier::deviceEdited, this, &ProfileWidget2::replot); connect(&diveListNotifier, &DiveListNotifier::diveComputerEdited, this, &ProfileWidget2::replot); @@ -916,234 +913,6 @@ void ProfileWidget2::keyDeleteAction() } } -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 = profileScene->timeAxis->posAtValue(e.offset.seconds); - double end = profileScene->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(DURATION_LINE, profileScene->isGrayscale), durationLinePenWidth)); - e.durationLine->setBrush(getColor(::BACKGROUND, profileScene->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 filenameIn, QImage thumbnail, duration_t duration) -{ - std::string filename = filenameIn.toStdString(); - - // Find the picture with the given filename - auto it = std::find_if(pictures.begin(), pictures.end(), [&filename](const PictureEntry &e) - { return e.filename == filename; }); - - // If we didn't find a picture, it does either not belong to the current dive, - // or its timestamp is outside of the profile. - if (it != pictures.end()) { - // 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 std::string &filenameIn, ProfileWidget2 *profile, bool synchronous) : offset(offsetIn), - filename(filenameIn), - thumbnail(new DivePictureItem) -{ - QGraphicsScene *scene = profile->scene(); - int size = Thumbnailer::defaultThumbnailSize(); - scene->addItem(thumbnail.get()); - thumbnail->setVisible(prefs.show_pictures_in_profile); - QImage img = Thumbnailer::instance()->fetchThumbnail(QString::fromStdString(filename), synchronous).scaled(size, size, Qt::KeepAspectRatio); - thumbnail->setPixmap(QPixmap::fromImage(img)); - thumbnail->setFileUrl(QString::fromStdString(filename)); - connect(thumbnail.get(), &DivePictureItem::removePicture, profile, &ProfileWidget2::removePicture); -} - -// Define a default sort order for picture-entries: sort lexicographically by timestamp and filename. -bool ProfileWidget2::PictureEntry::operator< (const PictureEntry &e) const -{ - // Use std::tie() for lexicographical sorting. - 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() -{ - double lastX = -1.0, lastY = 0.0; - const double yStart = 0.05; // At which depth the thumbnails start (in fraction of total depth). - const double yStep = 0.01; // Increase of depth for overlapping thumbnails (in fraction of total depth). - const double xSpace = 18.0 * profileScene->dpr; // Horizontal range in which thumbnails are supposed to be overlapping (in pixels). - const int maxDepth = 14; // Maximal depth of thumbnail stack (in thumbnails). - for (PictureEntry &e: pictures) { - // Invisible items are outside of the shown range - ignore. - if (!e.thumbnail->isVisible()) - continue; - - // 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 = e.thumbnail->x(); - if (x < 0.0) - continue; - double y; - if (lastX >= 0.0 && fabs(x - lastX) < xSpace * profileScene->dpr && lastY <= (yStart + maxDepth * yStep) - 1e-10) - y = lastY + yStep; - else - y = yStart; - lastX = x; - lastY = y; - double yScreen = profileScene->timeAxis->screenPosition(y); - e.thumbnail->setY(yScreen); - updateDurationLine(e); // If we changed the y-position, we also have to change the duration-line. - } - updateThumbnailPaintOrder(); -} - -void ProfileWidget2::updateThumbnailXPos(PictureEntry &e) -{ - // Here, we only set the x-coordinate of the picture. The y-coordinate - // will be set later in calculatePictureYPositions(). - // Thumbnails outside of the shown range are hidden. - double time = e.offset.seconds; - if (time >= profileScene->timeAxis->minimum() && time <= profileScene->timeAxis->maximum()) { - double x = profileScene->timeAxis->posAtValue(time); - e.thumbnail->setX(x); - e.thumbnail->setVisible(true); - } else { - e.thumbnail->setVisible(false); - } -} - -// This function resets the picture thumbnails of the current dive. -void ProfileWidget2::plotPictures() -{ - plotPicturesInternal(d, false); -} - -void ProfileWidget2::plotPicturesInternal(const struct dive *d, bool synchronous) -{ - pictures.clear(); - if (currentState == EDIT || currentState == PLAN) - return; - - if (!d) - return; - - // Fetch all pictures of the dive, but consider only those that are within the dive time. - // For each picture, create a PictureEntry object in the pictures-vector. - // emplace_back() constructs an object at the end of the vector. The parameters are passed directly to the constructor. - for (auto &picture: d->pictures) { - if (picture.offset.seconds > 0 && picture.offset.seconds <= d->duration.seconds) - pictures.emplace_back(picture.offset, picture.filename, this, synchronous); - } - if (pictures.empty()) - return; - // Sort pictures by timestamp (and filename if equal timestamps). - // This will allow for proper location of the pictures on the profile plot. - std::sort(pictures.begin(), pictures.end()); - updateThumbnails(); -} - -void ProfileWidget2::updateThumbnails() -{ - // Calculate thumbnail positions. First the x-coordinates and and then the y-coordinates. - for (PictureEntry &e: pictures) - updateThumbnailXPos(e); - calculatePictureYPositions(); -} - -// Remove the pictures with the given filenames from the profile plot. -void ProfileWidget2::picturesRemoved(dive *d, QVector fileUrls) -{ - // To remove the pictures, we use the std::remove_if() algorithm. - // std::remove_if() does not actually delete the elements, but moves - // them to the end of the given range. It returns an iterator to the - // end of the new range of non-deleted elements. A subsequent call to - // std::erase on the range of deleted elements then ultimately shrinks the vector. - // (c.f. erase-remove idiom: https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom) - auto it = std::remove_if(pictures.begin(), pictures.end(), [&fileUrls](const PictureEntry &e) - // Check whether filename of entry is in list of provided filenames - { return std::find(fileUrls.begin(), fileUrls.end(), QString::fromStdString(e.filename)) != fileUrls.end(); }); - pictures.erase(it, pictures.end()); - calculatePictureYPositions(); -} - -void ProfileWidget2::picturesAdded(dive *d, QVector pics) -{ - for (const picture &pic: pics) { - if (pic.offset.seconds > 0 && pic.offset.seconds <= d->duration.seconds) { - pictures.emplace_back(pic.offset, pic.filename, this, false); - updateThumbnailXPos(pictures.back()); - } - } - - // Sort pictures by timestamp (and filename if equal timestamps). - // This will allow for proper location of the pictures on the profile plot. - std::sort(pictures.begin(), pictures.end()); - - calculatePictureYPositions(); -} - -void ProfileWidget2::removePicture(const QString &fileUrl) -{ - if (d) - Command::removePictures({ { mutable_dive(), { fileUrl.toStdString() } } }); -} - void ProfileWidget2::profileChanged(dive *dive) { if (dive != d) @@ -1153,126 +922,6 @@ void ProfileWidget2::profileChanged(dive *dive) #endif -void ProfileWidget2::dropEvent(QDropEvent *event) -{ -#ifndef SUBSURFACE_MOBILE - if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop") && d) { - QByteArray itemData = event->mimeData()->data("application/x-subsurfaceimagedrop"); - QDataStream dataStream(&itemData, QIODevice::ReadOnly); - - QString filename; - dataStream >> filename; - QPointF mappedPos = mapToScene(event->pos()); - offset_t offset { .seconds = (int32_t)lrint(profileScene->timeAxis->valueAt(mappedPos)) }; - Command::setPictureOffset(mutable_dive(), filename, offset); - - if (event->source() == this) { - event->setDropAction(Qt::MoveAction); - event->accept(); - } else { - event->acceptProposedAction(); - } - } else { - event->ignore(); - } -#endif -} - -#ifndef SUBSURFACE_MOBILE -void ProfileWidget2::pictureOffsetChanged(dive *dIn, QString filenameIn, offset_t offset) -{ - if (dIn != d) - return; // Picture of a different dive than the one shown changed. - - std::string filename = filenameIn.toStdString(); // TODO: can we move std::string through Qt's signal/slot system? - - // Calculate time in dive where picture was dropped and whether the new position is during the dive. - bool duringDive = d && offset.seconds > 0 && offset.seconds < d->duration.seconds; - - // A picture was drag&dropped onto the profile: We have four cases to consider: - // 1a) The image was already shown on the profile and is moved to a different position on the profile. - // Calculate the new position and move the picture. - // 1b) The image was on the profile and is moved outside of the dive time. - // Remove the picture. - // 2a) The image was not on the profile and is moved into the dive time. - // Add the picture to the profile. - // 2b) The image was not on the profile and is moved outside of the dive time. - // Do nothing. - auto oldPos = std::find_if(pictures.begin(), pictures.end(), [filename](const PictureEntry &e) - { return e.filename == filename; }); - if (oldPos != pictures.end()) { - // Cases 1a) and 1b): picture is on profile - if (duringDive) { - // Case 1a): move to new position - // First, find new position. Note that we also have to compare filenames, - // because it is quite easy to generate equal offsets. - auto newPos = std::find_if(pictures.begin(), pictures.end(), [offset, &filename](const PictureEntry &e) - { return std::tie(e.offset.seconds, e.filename) > std::tie(offset.seconds, filename); }); - // Set new offset - oldPos->offset = offset; - updateThumbnailXPos(*oldPos); - - // Move image from old to new position - int oldIndex = oldPos - pictures.begin(); - int newIndex = newPos - pictures.begin(); - move_in_range(pictures, oldIndex, oldIndex + 1, newIndex); - } else { - // Case 1b): remove picture - pictures.erase(oldPos); - } - - // In both cases the picture list changed, therefore we must recalculate the y-coordinatesA. - calculatePictureYPositions(); - } else { - // Cases 2a) and 2b): picture not on profile. We only have to take action for - // the first case: picture is moved into dive-time. - if (duringDive) { - // Case 2a): add the picture at the appropriate position. - // The case move from outside-to-outside of the profile plot was handled by - // the "&& duringDive" condition in the if above. - // As for case 1a), we have to also consider filenames in the case of equal offsets. - auto newPos = std::find_if(pictures.begin(), pictures.end(), [offset, &filename](const PictureEntry &e) - { return std::tie(e.offset.seconds, e.filename) > std::tie(offset.seconds, filename); }); - // emplace() constructs the element at the given position in the vector. - // The parameters are passed directly to the contructor. - // The call returns an iterator to the new element (which might differ from - // the old iterator, since the buffer might have been reallocated). - newPos = pictures.emplace(newPos, offset, filename, this, false); - updateThumbnailXPos(*newPos); - calculatePictureYPositions(); - } - } -} -#endif - -void ProfileWidget2::dragEnterEvent(QDragEnterEvent *event) -{ - if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop")) { - if (event->source() == this) { - event->setDropAction(Qt::MoveAction); - event->accept(); - } else { - event->acceptProposedAction(); - } - } else { - event->ignore(); - } -} - -void ProfileWidget2::dragMoveEvent(QDragMoveEvent *event) -{ - if (event->mimeData()->hasFormat("application/x-subsurfaceimagedrop")) { - if (event->source() == this) { - event->setDropAction(Qt::MoveAction); - event->accept(); - } else { - event->acceptProposedAction(); - } - } else { - event->ignore(); - } -} - struct dive *ProfileWidget2::mutable_dive() const { return const_cast(d); diff --git a/profile-widget/profilewidget2.h b/profile-widget/profilewidget2.h index e6d372852..574e24436 100644 --- a/profile-widget/profilewidget2.h +++ b/profile-widget/profilewidget2.h @@ -29,7 +29,6 @@ class DivePlannerPointsModel; class DiveHandler; class QGraphicsSimpleTextItem; class QModelIndex; -class DivePictureItem; class ProfileWidget2 : public QGraphicsView { Q_OBJECT @@ -74,17 +73,12 @@ slots: // Necessary to call from QAction's signals. void actionRequestedReplot(bool triggered); void divesChanged(const QVector &dives, DiveField field); #ifndef SUBSURFACE_MOBILE - void plotPictures(); - void picturesRemoved(dive *d, QVector filenames); - void picturesAdded(dive *d, QVector pics); void pointsReset(); void pointInserted(const QModelIndex &parent, int start, int end); void pointsRemoved(const QModelIndex &, int start, int end); void pointsMoved(const QModelIndex &, int start, int end, const QModelIndex &destination, int row); void updateThumbnail(QString filename, QImage thumbnail, duration_t duration); void profileChanged(dive *d); - void pictureOffsetChanged(dive *d, QString filename, offset_t offset); - void removePicture(const QString &fileUrl); /* this is called for every move on the handlers. maybe we can speed up this a bit? */ void divePlannerHandlerMoved(); @@ -105,10 +99,6 @@ private: void keyPressEvent(QKeyEvent *e) override; void addGasChangeMenu(QMenu &m, QString menuTitle, const struct dive &d, int dcNr, int changeTime); #endif - void dropEvent(QDropEvent *event) override; - void dragEnterEvent(QDragEnterEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void replot(); void setZoom(int level); void addGasSwitch(int tank, int seconds); @@ -117,8 +107,6 @@ private: void addItemsToScene(); void setupItemOnScene(); struct plot_data *getEntryFromPos(QPointF pos); - void clearPictures(); - void plotPicturesInternal(const struct dive *d, bool synchronous); void updateThumbnails(); void addDivemodeSwitch(int seconds, int divemode); void addBookmark(int seconds); @@ -156,25 +144,6 @@ private: std::vector> gases; #ifndef SUBSURFACE_MOBILE - // The list of pictures in this plot. The pictures are sorted by offset in seconds. - // For the same offset, sort by filename. - // Pictures that are outside of the dive time are not shown. - struct PictureEntry { - offset_t offset; - duration_t duration; - std::string 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 std::string &filenameIn, ProfileWidget2 *profile, bool synchronous); - bool operator< (const PictureEntry &e) const; - }; - void updateThumbnailXPos(PictureEntry &e); - std::vector pictures; - void calculatePictureYPositions(); - void updateDurationLine(PictureEntry &e); - void updateThumbnailPaintOrder(); - void keyDeleteAction(); void keyUpAction(); void keyDownAction(); diff --git a/profile-widget/zvalues.h b/profile-widget/zvalues.h index a2f16207c..6791483a1 100644 --- a/profile-widget/zvalues.h +++ b/profile-widget/zvalues.h @@ -12,6 +12,7 @@ struct ProfileZValue { enum ZValues { Profile = 0, + Pictures, ToolTipItem, Count }; diff --git a/qt-quick/chartitem.cpp b/qt-quick/chartitem.cpp index 736d5ab42..8cd3f47a9 100644 --- a/qt-quick/chartitem.cpp +++ b/qt-quick/chartitem.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 #include "chartitem.h" #include "chartitemhelper.h" -#include "chartview.h" +#include "chartitem_private.h" #include #include @@ -51,7 +51,7 @@ static int round_up(double f) } ChartPixmapItem::ChartPixmapItem(ChartView &v, size_t z, bool dragable) : HideableChartItem(v, z, dragable), - positionDirty(false), textureDirty(false) + scale(1.0), positionDirty(false), textureDirty(false) { } @@ -74,9 +74,10 @@ void ChartPixmapItem::setPositionDirty() void ChartPixmapItem::render() { + doRearrange(); if (!node) { createNode(view.w()->createImageNode()); - view.addQSGNode(node.get(), zValue); + addNodeToView(); } updateVisible(); @@ -95,6 +96,13 @@ void ChartPixmapItem::render() } } +// Scale size and round to integer (because non-integers give strange artifacts for me). +static QSizeF scaleSize(const QSizeF &s, double scale) +{ + return { round(s.width() * scale), + round(s.height() * scale) }; +} + void ChartPixmapItem::resize(QSizeF size) { QSize s_int(round_up(size.width()), round_up(size.height())); @@ -106,7 +114,7 @@ void ChartPixmapItem::resize(QSizeF size) painter.reset(new QPainter(img.get())); painter->setRenderHint(QPainter::Antialiasing); } - rect.setSize(size); + rect.setSize(scaleSize(size, scale)); setPositionDirty(); // position includes the size. setTextureDirty(); } @@ -117,6 +125,15 @@ void ChartPixmapItem::setPos(QPointF pos) setPositionDirty(); } +void ChartPixmapItem::setScale(double scaleIn) +{ + scale = scaleIn; + if (img) { + rect.setSize(scaleSize(img->size(), scale)); + setPositionDirty(); // position includes the size. + } +} + void ChartGraphicsSceneItem::draw(QSizeF s, QColor background, QGraphicsScene &scene) { resize(s); // Noop if size doesn't change @@ -238,6 +255,7 @@ void ChartLineItemBase::setLine(QPointF from, QPointF to) void ChartLineItem::render() { + doRearrange(); if (!node) { geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2)); geometry->setDrawingMode(QSGGeometry::DrawLines); @@ -245,7 +263,7 @@ void ChartLineItem::render() createNode(); node->setGeometry(geometry.get()); node->setMaterial(material.get()); - view.addQSGNode(node.get(), zValue); + addNodeToView(); positionDirty = materialDirty = true; } updateVisible(); @@ -269,6 +287,7 @@ void ChartLineItem::render() void ChartRectLineItem::render() { + doRearrange(); if (!node) { geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 5)); geometry->setDrawingMode(QSGGeometry::DrawLineStrip); @@ -276,7 +295,7 @@ void ChartRectLineItem::render() createNode(); node->setGeometry(geometry.get()); node->setMaterial(material.get()); - view.addQSGNode(node.get(), zValue); + addNodeToView(); positionDirty = materialDirty = true; } updateVisible(); diff --git a/qt-quick/chartitem.h b/qt-quick/chartitem.h index 5e0a9866a..47527ec05 100644 --- a/qt-quick/chartitem.h +++ b/qt-quick/chartitem.h @@ -47,11 +47,27 @@ protected: std::unique_ptr node; bool visible; bool visibleChanged; + enum class MoveMode { + none, before, after + } moveMode; // Node will be moved before or after other node. + QSGNode *moveNode; // Node to be moved before/after, or nullptr if move to beginning/end. template void createNode(Args&&... args); // Call to create node with visibility flag. + void updateVisible(); // Must be called by child class to update visibility flag! + void addNodeToView(); // Must be called by child class after creating and initializing the QSG node. + void doRearrange(); // Call at beginning of render(), so that the node can be rearranged, if necessary. public: + template + friend class HideableChartItem; + template + void moveBefore(HideableChartItem &item); + void moveBack(); + template + void moveAfter(HideableChartItem &item); + void moveFront(); void setVisible(bool visible); + bool isVisible() const; }; // A shortcut for ChartItems based on a hideable proxy item @@ -59,12 +75,14 @@ template using HideableChartProxyItem = HideableChartItem>>; // A chart item that blits a precalculated pixmap onto the scene. +// Can be scaled with setScale(). class ChartPixmapItem : public HideableChartProxyItem { public: ChartPixmapItem(ChartView &v, size_t z, bool dragable = false); ~ChartPixmapItem(); void setPos(QPointF pos) override; + void setScale(double scale); void render() override; protected: void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*. @@ -72,6 +90,7 @@ protected: std::unique_ptr img; void setTextureDirty(); void setPositionDirty(); + double scale; private: bool positionDirty; // true if the position changed since last render bool textureDirty; // true if the pixmap changed since last render @@ -161,6 +180,41 @@ public: }; // Implementation detail of templates - move to serparate header file + +template +template +void HideableChartItem::moveBefore(HideableChartItem &item) +{ + moveMode = MoveMode::before; + moveNode = item.node.get(); + markDirty(); +} + +template +void HideableChartItem::moveBack() +{ + moveMode = MoveMode::before; + moveNode = nullptr; + markDirty(); +} + +template +template +void HideableChartItem::moveAfter(HideableChartItem &item) +{ + moveMode = MoveMode::after; + moveNode = item.node.get(); + markDirty(); +} + +template +void HideableChartItem::moveFront() +{ + moveMode = MoveMode::after; + moveNode = nullptr; + markDirty(); +} + template void HideableChartItem::setVisible(bool visibleIn) { @@ -172,25 +226,9 @@ void HideableChartItem::setVisible(bool visibleIn) } template -template -void HideableChartItem::createNode(Args&&... args) +bool HideableChartItem::isVisible() const { - node.reset(new Node(visible, std::forward(args)...)); - visibleChanged = false; -} - -template -HideableChartItem::HideableChartItem(ChartView &v, size_t z, bool dragable) : ChartItem(v, z, dragable), - visible(true), visibleChanged(false) -{ -} - -template -void HideableChartItem::updateVisible() -{ - if (visibleChanged) - node->setVisible(visible); - visibleChanged = false; + return visible; } #endif diff --git a/qt-quick/chartitem_private.h b/qt-quick/chartitem_private.h new file mode 100644 index 000000000..948dd0eef --- /dev/null +++ b/qt-quick/chartitem_private.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0 +// Private template implementation for ChartItem child classes + +#ifndef CHARTITEM_PRIVATE_H +#define CHARTITEM_PRIVATE_H + +#include "chartitem.h" +#include "chartview.h" + +template +template +void HideableChartItem::createNode(Args&&... args) +{ + node.reset(new Node(visible, std::forward(args)...)); + visibleChanged = false; +} + +template +void HideableChartItem::addNodeToView() +{ + view.addQSGNode(node.get(), zValue, moveMode == MoveMode::after, moveNode); + moveNode = nullptr; + moveMode = MoveMode::none; +} + +template +HideableChartItem::HideableChartItem(ChartView &v, size_t z, bool dragable) : ChartItem(v, z, dragable), + visible(true), visibleChanged(false), moveMode(MoveMode::none), moveNode(nullptr) +{ +} + +template +void HideableChartItem::updateVisible() +{ + if (visibleChanged) + node->setVisible(visible); + visibleChanged = false; +} + +template +void HideableChartItem::doRearrange() +{ + if (!node) + return; + switch (moveMode) { + default: + case MoveMode::none: + return; + case MoveMode::before: + if (moveNode) + view.moveNodeBefore(node.get(), zValue, moveNode); + else + view.moveNodeBack(node.get(), zValue); + break; + case MoveMode::after: + if (moveNode) + view.moveNodeAfter(node.get(), zValue, moveNode); + else + view.moveNodeFront(node.get(), zValue); + break; + } + moveNode = nullptr; + moveMode = MoveMode::none; +} + +#endif diff --git a/qt-quick/chartitem_ptr.h b/qt-quick/chartitem_ptr.h index 35401798b..6d59d59f9 100644 --- a/qt-quick/chartitem_ptr.h +++ b/qt-quick/chartitem_ptr.h @@ -21,6 +21,9 @@ public: ChartItemPtr(const ChartItemPtr &p) : ptr(p.ptr) { } + ChartItemPtr(ChartItemPtr &&p) : ptr(p.ptr) + { + } void reset() { ptr = nullptr; diff --git a/qt-quick/chartview.cpp b/qt-quick/chartview.cpp index 4df27fee5..1fb4e7607 100644 --- a/qt-quick/chartview.cpp +++ b/qt-quick/chartview.cpp @@ -127,10 +127,54 @@ void ChartView::clearItems() dirtyItems.splice(deletedItems); } -void ChartView::addQSGNode(QSGNode *node, size_t z) +static ZNode &getZNode(RootNode &rootNode, size_t z) { - size_t idx = std::clamp(z, (size_t)0, maxZ); - rootNode->zNodes[idx]->appendChildNode(node); + size_t idx = std::clamp(z, (size_t)0, rootNode.zNodes.size()); + return *rootNode.zNodes[idx]; +} + +void ChartView::addQSGNode(QSGNode *node, size_t z, bool moveAfter, QSGNode *node2) +{ + auto &parent = getZNode(*rootNode, z); + if (node2) { + if (moveAfter) + parent.insertChildNodeAfter(node, node2); + else + parent.insertChildNodeBefore(node, node2); + } else { + if (moveAfter) + parent.prependChildNode(node); + else + parent.appendChildNode(node); + } +} + +void ChartView::moveNodeBefore(QSGNode *node, size_t z, QSGNode *before) +{ + auto &parent = getZNode(*rootNode, z); + parent.removeChildNode(node); + parent.insertChildNodeBefore(node, before); +} + +void ChartView::moveNodeBack(QSGNode *node, size_t z) +{ + auto &parent = getZNode(*rootNode, z); + parent.removeChildNode(node); + parent.appendChildNode(node); +} + +void ChartView::moveNodeAfter(QSGNode *node, size_t z, QSGNode *before) +{ + auto &parent = getZNode(*rootNode, z); + parent.removeChildNode(node); + parent.insertChildNodeAfter(node, before); +} + +void ChartView::moveNodeFront(QSGNode *node, size_t z) +{ + auto &parent = getZNode(*rootNode, z); + parent.removeChildNode(node); + parent.prependChildNode(node); } void ChartView::registerChartItem(ChartItem &item) diff --git a/qt-quick/chartview.h b/qt-quick/chartview.h index 90b6180d6..02cb67dca 100644 --- a/qt-quick/chartview.h +++ b/qt-quick/chartview.h @@ -20,7 +20,13 @@ public: QSizeF size() const; QRectF plotArea() const; void setBackgroundColor(QColor color); // Chart must be replot for color to become effective. - void addQSGNode(QSGNode *node, size_t z); // Must only be called in render thread! + void addQSGNode(QSGNode *node, size_t z, bool moveAfter, QSGNode *node2); + // Must only be called in render thread! + // If node2 is nullptr move to begin or end of list. + void moveNodeBefore(QSGNode *node, size_t z, QSGNode *before); + void moveNodeBack(QSGNode *node, size_t z); + void moveNodeAfter(QSGNode *node, size_t z, QSGNode *after); + void moveNodeFront(QSGNode *node, size_t z); void registerChartItem(ChartItem &item); void registerDirtyChartItem(ChartItem &item); void emergencyShutdown(); // Called when QQuick decides to delete our root node. diff --git a/stats/chartitem.cpp b/stats/chartitem.cpp index 7169bffda..690f5e279 100644 --- a/stats/chartitem.cpp +++ b/stats/chartitem.cpp @@ -4,6 +4,7 @@ #include "statsview.h" #include "core/globals.h" #include "qt-quick/chartitemhelper.h" +#include "qt-quick/chartitem_private.h" #include #include @@ -61,6 +62,7 @@ QSGTexture *ChartScatterItem::getTexture() const void ChartScatterItem::render() { + doRearrange(); if (!theme.scatterItemTexture) { theme.scatterItemTexture = register_global(createScatterTexture(view, theme.fillColor, theme.borderColor)); theme.scatterItemSelectedTexture = register_global(createScatterTexture(view, theme.selectedColor, theme.selectedBorderColor)); @@ -68,7 +70,7 @@ void ChartScatterItem::render() } if (!node) { createNode(view.w()->createImageNode()); - view.addQSGNode(node.get(), zValue); + addNodeToView(); textureDirty = positionDirty = true; } updateVisible(); @@ -193,6 +195,7 @@ QSGTexture *ChartBarItem::getSelectedTexture() const void ChartBarItem::render() { + doRearrange(); if (!node) { createNode(view.w()->createRectangleNode()); @@ -205,7 +208,7 @@ void ChartBarItem::render() borderNode->setMaterial(borderMaterial.get()); node->node->appendChildNode(borderNode.get()); - view.addQSGNode(node.get(), zValue); + addNodeToView(); positionDirty = colorDirty = selectedDirty = true; } updateVisible(); @@ -311,6 +314,7 @@ ChartBoxItem::~ChartBoxItem() void ChartBoxItem::render() { + doRearrange(); // Remember old dirty values, since ChartBarItem::render() will clear them bool oldPositionDirty = positionDirty; bool oldColorDirty = colorDirty;