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

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