mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-19 22:16:15 +00:00
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:
parent
d0c26f42d7
commit
ebf9ce6d86
22 changed files with 979 additions and 592 deletions
|
@ -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 \
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ public:
|
|||
void nextDC();
|
||||
void prevDC();
|
||||
void exitEditMode();
|
||||
void dropPicture(const QString &filename, QPoint p);
|
||||
dive *d;
|
||||
int dc;
|
||||
private
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
91
profile-widget/pictureitem.cpp
Normal file
91
profile-widget/pictureitem.cpp
Normal 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();
|
||||
}
|
33
profile-widget/pictureitem.h
Normal file
33
profile-widget/pictureitem.h
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,9 +258,6 @@ 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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
struct ProfileZValue {
|
||||
enum ZValues {
|
||||
Profile = 0,
|
||||
Pictures,
|
||||
ToolTipItem,
|
||||
Count
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
66
qt-quick/chartitem_private.h
Normal file
66
qt-quick/chartitem_private.h
Normal 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
|
|
@ -21,6 +21,9 @@ public:
|
|||
ChartItemPtr(const ChartItemPtr &p) : ptr(p.ptr)
|
||||
{
|
||||
}
|
||||
ChartItemPtr(ChartItemPtr &&p) : ptr(p.ptr)
|
||||
{
|
||||
}
|
||||
void reset()
|
||||
{
|
||||
ptr = nullptr;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue