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 <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2023-08-12 22:59:56 +02:00
parent d0c26f42d7
commit ebf9ce6d86
22 changed files with 979 additions and 592 deletions

View file

@ -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 \

View file

@ -12,12 +12,13 @@
#include "core/subsurface-string.h"
#include "qt-models/diveplannermodel.h"
#include <QToolBar>
#include <QHBoxLayout>
#include <QLabel>
#include <QMimeData>
#include <QQmlEngine>
#include <QQuickWidget>
#include <QStackedWidget>
#include <QLabel>
#include <QToolBar>
// 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<int32_t>(view->timeAt(pos)) };
Command::setPictureOffset(d, filename, offset);
}

View file

@ -27,6 +27,7 @@ public:
void nextDC();
void prevDC();
void exitEditMode();
void dropPicture(const QString &filename, QPoint p);
dive *d;
int dc;
private

View file

@ -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

View file

@ -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 <QDesktopServices>
#include <QPen>
#include <QUrl>
#include <QGraphicsSceneMouseEvent>
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)));
}

View file

@ -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

View file

@ -0,0 +1,91 @@
// SPDX-License-Identifier: GPL-2.0
#include "pictureitem.h"
#include "zvalues.h"
#include <cmath>
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();
}

View file

@ -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 <QString>
#include <QRectF>
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

View file

@ -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<double, double> 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<std::pair<QString, QPixmap>> ProfileScene::eventsAt(QPointF pos) const
{
std::vector<std::pair<QString, QPixmap>> res;

View file

@ -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<double, double> 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<std::pair<QString, QPixmap>> eventsAt(QPointF pos) const;
const struct dive *d;

View file

@ -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 <QAbstractAnimation>
#include <QCursor>
#include <QDebug>
#include <QDesktopServices>
#include <QElapsedTimer>
// 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 <typename FUNC>
std::unique_ptr<ProfileAnimationTemplate<FUNC>> make_anim(FUNC func, int animSpeed)
{
return std::make_unique<ProfileAnimationTemplate<FUNC>>(func, animSpeed);
return animSpeed > 0 ? std::make_unique<ProfileAnimationTemplate<FUNC>>(func, animSpeed)
: std::unique_ptr<ProfileAnimationTemplate<FUNC>>();
}
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<dive *>(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<PictureItem> 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<ChartRectItem>(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<std::pair<offset_t, QString>> 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<PictureItem>(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<QString> 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<PictureEntry>::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<picture> 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<std::pair<offset_t, QString>> 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<offset_t, QString> &p)
{ return std::tie(e.offset, e.filename) < p; });
it = pictures.emplace(it, offset, fn, createChartItem<PictureItem>(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<PictureItem>(dpr), dpr, false);
// Update thumbnail paint order
auto nextPos = std::next(newPos);
moveThumbnailBefore(*newPos, nextPos);
updateThumbnailXPos(*newPos);
calculatePictureYPositions();
}
}
update();
}

View file

@ -3,12 +3,16 @@
#define PROFILE_VIEW_H
#include "qt-quick/chartview.h"
#include "core/units.h"
#include <memory>
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<ProfileAnimation> 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<PictureItem> thumbnail;
// For videos with known duration, we represent the duration of the video by a line
ChartItemPtr<ChartRectItem> durationLine;
std::unique_ptr<ProfileAnimation> animation;
PictureEntry (offset_t offset, const QString &filename, ChartItemPtr<PictureItem> thumbnail, double dpr, bool synchronous);
bool operator< (const PictureEntry &e) const;
};
std::vector<PictureEntry> pictures;
PictureEntry *highlightedPicture;
// Picture (media) related functions
void picturesRemoved(dive *d, QVector<QString> filenames);
void picturesAdded(dive *d, QVector<picture> 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<PictureEntry>::iterator &before);
PictureEntry *getPictureUnderMouse(QPointF pos);
// For mobile
int getDiveId() const;

View file

@ -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<QString> 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<picture> 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<dive *>(d);

View file

@ -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<dive *> &dives, DiveField field);
#ifndef SUBSURFACE_MOBILE
void plotPictures();
void picturesRemoved(dive *d, QVector<QString> filenames);
void picturesAdded(dive *d, QVector<picture> 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<std::unique_ptr<QGraphicsSimpleTextItem>> 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<DivePictureItem> thumbnail;
// For videos with known duration, we represent the duration of the video by a line
std::unique_ptr<QGraphicsRectItem> 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<PictureEntry> pictures;
void calculatePictureYPositions();
void updateDurationLine(PictureEntry &e);
void updateThumbnailPaintOrder();
void keyDeleteAction();
void keyUpAction();
void keyDownAction();

View file

@ -12,6 +12,7 @@
struct ProfileZValue {
enum ZValues {
Profile = 0,
Pictures,
ToolTipItem,
Count
};

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include "chartitem.h"
#include "chartitemhelper.h"
#include "chartview.h"
#include "chartitem_private.h"
#include <cmath>
#include <QGraphicsScene>
@ -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();

View file

@ -47,11 +47,27 @@ protected:
std::unique_ptr<Node> 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<class... Args>
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 <typename Node2>
friend class HideableChartItem;
template <typename Node2>
void moveBefore(HideableChartItem<Node2> &item);
void moveBack();
template <typename Node2>
void moveAfter(HideableChartItem<Node2> &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 <typename Node>
using HideableChartProxyItem = HideableChartItem<HideableQSGNode<QSGProxyNode<Node>>>;
// A chart item that blits a precalculated pixmap onto the scene.
// Can be scaled with setScale().
class ChartPixmapItem : public HideableChartProxyItem<QSGImageNode> {
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<QImage> 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 <typename Node>
template <typename Node2>
void HideableChartItem<Node>::moveBefore(HideableChartItem<Node2> &item)
{
moveMode = MoveMode::before;
moveNode = item.node.get();
markDirty();
}
template <typename Node>
void HideableChartItem<Node>::moveBack()
{
moveMode = MoveMode::before;
moveNode = nullptr;
markDirty();
}
template <typename Node>
template <typename Node2>
void HideableChartItem<Node>::moveAfter(HideableChartItem<Node2> &item)
{
moveMode = MoveMode::after;
moveNode = item.node.get();
markDirty();
}
template <typename Node>
void HideableChartItem<Node>::moveFront()
{
moveMode = MoveMode::after;
moveNode = nullptr;
markDirty();
}
template <typename Node>
void HideableChartItem<Node>::setVisible(bool visibleIn)
{
@ -172,25 +226,9 @@ void HideableChartItem<Node>::setVisible(bool visibleIn)
}
template <typename Node>
template<class... Args>
void HideableChartItem<Node>::createNode(Args&&... args)
bool HideableChartItem<Node>::isVisible() const
{
node.reset(new Node(visible, std::forward<Args>(args)...));
visibleChanged = false;
}
template <typename Node>
HideableChartItem<Node>::HideableChartItem(ChartView &v, size_t z, bool dragable) : ChartItem(v, z, dragable),
visible(true), visibleChanged(false)
{
}
template <typename Node>
void HideableChartItem<Node>::updateVisible()
{
if (visibleChanged)
node->setVisible(visible);
visibleChanged = false;
return visible;
}
#endif

View file

@ -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 <typename Node>
template<class... Args>
void HideableChartItem<Node>::createNode(Args&&... args)
{
node.reset(new Node(visible, std::forward<Args>(args)...));
visibleChanged = false;
}
template <typename Node>
void HideableChartItem<Node>::addNodeToView()
{
view.addQSGNode(node.get(), zValue, moveMode == MoveMode::after, moveNode);
moveNode = nullptr;
moveMode = MoveMode::none;
}
template <typename Node>
HideableChartItem<Node>::HideableChartItem(ChartView &v, size_t z, bool dragable) : ChartItem(v, z, dragable),
visible(true), visibleChanged(false), moveMode(MoveMode::none), moveNode(nullptr)
{
}
template <typename Node>
void HideableChartItem<Node>::updateVisible()
{
if (visibleChanged)
node->setVisible(visible);
visibleChanged = false;
}
template <typename Node>
void HideableChartItem<Node>::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

View file

@ -21,6 +21,9 @@ public:
ChartItemPtr(const ChartItemPtr &p) : ptr(p.ptr)
{
}
ChartItemPtr(ChartItemPtr &&p) : ptr(p.ptr)
{
}
void reset()
{
ptr = nullptr;

View file

@ -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)

View file

@ -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.

View file

@ -4,6 +4,7 @@
#include "statsview.h"
#include "core/globals.h"
#include "qt-quick/chartitemhelper.h"
#include "qt-quick/chartitem_private.h"
#include <cmath>
#include <QQuickWindow>
@ -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;