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/animationfunctions.cpp \
|
||||||
profile-widget/divepixmapcache.cpp \
|
profile-widget/divepixmapcache.cpp \
|
||||||
profile-widget/divepixmapitem.cpp \
|
profile-widget/divepixmapitem.cpp \
|
||||||
|
profile-widget/pictureitem.cpp \
|
||||||
profile-widget/tankitem.cpp \
|
profile-widget/tankitem.cpp \
|
||||||
profile-widget/tooltipitem.cpp \
|
profile-widget/tooltipitem.cpp \
|
||||||
profile-widget/divelineitem.cpp \
|
profile-widget/divelineitem.cpp \
|
||||||
|
@ -338,6 +339,7 @@ HEADERS += \
|
||||||
profile-widget/diveprofileitem.h \
|
profile-widget/diveprofileitem.h \
|
||||||
profile-widget/profilescene.h \
|
profile-widget/profilescene.h \
|
||||||
profile-widget/diveeventitem.h \
|
profile-widget/diveeventitem.h \
|
||||||
|
profile-widget/pictureitem.h \
|
||||||
profile-widget/tankitem.h \
|
profile-widget/tankitem.h \
|
||||||
profile-widget/tooltipitem.h \
|
profile-widget/tooltipitem.h \
|
||||||
profile-widget/animationfunctions.h \
|
profile-widget/animationfunctions.h \
|
||||||
|
|
|
@ -12,12 +12,13 @@
|
||||||
#include "core/subsurface-string.h"
|
#include "core/subsurface-string.h"
|
||||||
#include "qt-models/diveplannermodel.h"
|
#include "qt-models/diveplannermodel.h"
|
||||||
|
|
||||||
#include <QToolBar>
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMimeData>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QQuickWidget>
|
#include <QQuickWidget>
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
#include <QLabel>
|
#include <QToolBar>
|
||||||
|
|
||||||
// A resizing display of the Subsurface logo when no dive is shown
|
// A resizing display of the Subsurface logo when no dive is shown
|
||||||
class EmptyView : public QLabel {
|
class EmptyView : public QLabel {
|
||||||
|
@ -56,6 +57,57 @@ void EmptyView::resizeEvent(QResizeEvent *)
|
||||||
update();
|
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"));
|
static const QUrl urlProfileView = QUrl(QStringLiteral("qrc:/qml/profileview.qml"));
|
||||||
ProfileWidget::ProfileWidget() : d(nullptr), dc(0), placingCommand(false)
|
ProfileWidget::ProfileWidget() : d(nullptr), dc(0), placingCommand(false)
|
||||||
{
|
{
|
||||||
|
@ -77,7 +129,7 @@ ProfileWidget::ProfileWidget() : d(nullptr), dc(0), placingCommand(false)
|
||||||
|
|
||||||
emptyView.reset(new EmptyView);
|
emptyView.reset(new EmptyView);
|
||||||
|
|
||||||
viewWidget.reset(new QQuickWidget);
|
viewWidget.reset(new ProfileViewWidget(*this));
|
||||||
viewWidget->setSource(urlProfileView);
|
viewWidget->setSource(urlProfileView);
|
||||||
viewWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
viewWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||||
|
|
||||||
|
@ -421,3 +473,12 @@ void ProfileWidget::stopEdited()
|
||||||
Setter s(placingCommand, true);
|
Setter s(placingCommand, true);
|
||||||
Command::editProfile(editedDive.get(), dc, Command::EditProfileType::EDIT, 0);
|
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 nextDC();
|
||||||
void prevDC();
|
void prevDC();
|
||||||
void exitEditMode();
|
void exitEditMode();
|
||||||
|
void dropPicture(const QString &filename, QPoint p);
|
||||||
dive *d;
|
dive *d;
|
||||||
int dc;
|
int dc;
|
||||||
private
|
private
|
||||||
|
|
|
@ -20,6 +20,8 @@ set(SUBSURFACE_PROFILE_LIB_SRCS
|
||||||
diverectitem.h
|
diverectitem.h
|
||||||
divetextitem.cpp
|
divetextitem.cpp
|
||||||
divetextitem.h
|
divetextitem.h
|
||||||
|
pictureitem.h
|
||||||
|
pictureitem.cpp
|
||||||
profilescene.cpp
|
profilescene.cpp
|
||||||
profilescene.h
|
profilescene.h
|
||||||
profiletranslations.h
|
profiletranslations.h
|
||||||
|
|
|
@ -1,107 +1,6 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
#include "profile-widget/divepixmapitem.h"
|
#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)
|
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);
|
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
|
#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;
|
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)
|
bool inPlanner, bool keepPlotInfo, bool calcMax, double zoom, double zoomedPosition)
|
||||||
{
|
{
|
||||||
d = dIn;
|
d = dIn;
|
||||||
|
@ -428,11 +429,6 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, int animSpeed, Div
|
||||||
|
|
||||||
bool hasHeartBeat = plotInfo.maxhr;
|
bool hasHeartBeat = plotInfo.maxhr;
|
||||||
// For mobile we might want to turn of some features that are normally shown.
|
// 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);
|
updateVisibility(hasHeartBeat, simplified);
|
||||||
updateAxes(hasHeartBeat, simplified);
|
updateAxes(hasHeartBeat, simplified);
|
||||||
|
|
||||||
|
@ -570,7 +566,7 @@ void ProfileScene::draw(QPainter *painter, const QRect &pos,
|
||||||
{
|
{
|
||||||
QSize size = pos.size();
|
QSize size = pos.size();
|
||||||
resize(QSizeF(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);
|
QImage image(pos.size(), QImage::Format_ARGB32);
|
||||||
image.fill(getColor(::BACKGROUND, isGrayscale));
|
image.fill(getColor(::BACKGROUND, isGrayscale));
|
||||||
|
@ -616,6 +612,22 @@ int ProfileScene::timeAt(QPointF pos) const
|
||||||
return lrint(timeAxis->valueAt(pos));
|
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>> ProfileScene::eventsAt(QPointF pos) const
|
||||||
{
|
{
|
||||||
std::vector<std::pair<QString, QPixmap>> res;
|
std::vector<std::pair<QString, QPixmap>> res;
|
||||||
|
|
|
@ -41,7 +41,8 @@ public:
|
||||||
// Can be compared with literal 1.0 to determine "end" state.
|
// Can be compared with literal 1.0 to determine "end" state.
|
||||||
|
|
||||||
// If a plannerModel is passed, the deco-information is taken from there.
|
// 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);
|
bool keepPlotInfo = false, bool calcMax = true, double zoom = 1.0, double zoomedPosition = 0.0);
|
||||||
|
|
||||||
void draw(QPainter *painter, const QRect &pos,
|
void draw(QPainter *painter, const QRect &pos,
|
||||||
|
@ -49,7 +50,10 @@ public:
|
||||||
DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false);
|
DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false);
|
||||||
double calcZoomPosition(double zoom, double originalPos, double delta);
|
double calcZoomPosition(double zoom, double originalPos, double delta);
|
||||||
const plot_info &getPlotInfo() const;
|
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;
|
std::vector<std::pair<QString, QPixmap>> eventsAt(QPointF pos) const;
|
||||||
|
|
||||||
const struct dive *d;
|
const struct dive *d;
|
||||||
|
|
|
@ -1,25 +1,32 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
#include "profileview.h"
|
#include "profileview.h"
|
||||||
|
#include "pictureitem.h"
|
||||||
#include "profilescene.h"
|
#include "profilescene.h"
|
||||||
#include "tooltipitem.h"
|
#include "tooltipitem.h"
|
||||||
#include "zvalues.h"
|
#include "zvalues.h"
|
||||||
#include "core/dive.h"
|
#include "core/dive.h"
|
||||||
#include "core/divelog.h"
|
#include "core/divelog.h"
|
||||||
|
#include "commands/command.h"
|
||||||
#include "core/errorhelper.h"
|
#include "core/errorhelper.h"
|
||||||
|
#include "core/imagedownloader.h"
|
||||||
#include "core/pref.h"
|
#include "core/pref.h"
|
||||||
|
#include "core/qthelper.h" // for localFilePath()
|
||||||
|
#include "core/range.h"
|
||||||
#include "core/settings/qPrefDisplay.h"
|
#include "core/settings/qPrefDisplay.h"
|
||||||
#include "core/settings/qPrefPartialPressureGas.h"
|
#include "core/settings/qPrefPartialPressureGas.h"
|
||||||
#include "core/settings/qPrefTechnicalDetails.h"
|
#include "core/settings/qPrefTechnicalDetails.h"
|
||||||
|
#include "core/subsurface-qt/divelistnotifier.h"
|
||||||
#include "qt-quick/chartitem.h"
|
#include "qt-quick/chartitem.h"
|
||||||
|
|
||||||
#include <QAbstractAnimation>
|
#include <QAbstractAnimation>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QDesktopServices>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
|
|
||||||
// Class templates for animations (if any). Might want to do our own.
|
// Class templates for animations (if any). Might want to do our own.
|
||||||
// Calls the function object passed in the constructor with a time argument,
|
// 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
|
// On the last invocation, a 1.0 literal is passed, so floating-point
|
||||||
// comparison is OK.
|
// comparison is OK.
|
||||||
class ProfileAnimation : public QAbstractAnimation {
|
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>
|
template <typename FUNC>
|
||||||
std::unique_ptr<ProfileAnimationTemplate<FUNC>> make_anim(FUNC func, int animSpeed)
|
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),
|
ProfileView::ProfileView(QQuickItem *parent) : ChartView(parent, ProfileZValue::Count),
|
||||||
d(nullptr),
|
d(nullptr),
|
||||||
dc(0),
|
dc(0),
|
||||||
|
simplified(false),
|
||||||
dpr(1.0),
|
dpr(1.0),
|
||||||
zoomLevel(1.00),
|
zoomLevel(1.00),
|
||||||
zoomedPosition(0.0),
|
zoomedPosition(0.0),
|
||||||
panning(false),
|
panning(false),
|
||||||
empty(true),
|
empty(true),
|
||||||
shouldCalculateMax(true)
|
shouldCalculateMax(true),
|
||||||
|
highlightedPicture(nullptr)
|
||||||
{
|
{
|
||||||
setBackgroundColor(Qt::black);
|
setBackgroundColor(Qt::black);
|
||||||
setFlag(ItemHasContents, true);
|
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::show_sacChanged , this, &ProfileView::replot);
|
||||||
connect(tec, &qPrefTechnicalDetails::zoomed_plotChanged , this, &ProfileView::replot);
|
connect(tec, &qPrefTechnicalDetails::zoomed_plotChanged , this, &ProfileView::replot);
|
||||||
connect(tec, &qPrefTechnicalDetails::decoinfoChanged , 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::tankbarChanged , this, &ProfileView::replot);
|
||||||
connect(tec, &qPrefTechnicalDetails::percentagegraphChanged , this, &ProfileView::replot);
|
connect(tec, &qPrefTechnicalDetails::percentagegraphChanged , this, &ProfileView::replot);
|
||||||
connect(tec, &qPrefTechnicalDetails::infoboxChanged , 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::pn2Changed, this, &ProfileView::replot);
|
||||||
connect(pp_gas, &qPrefPartialPressureGas::po2Changed, 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);
|
setAcceptTouchEvents(true);
|
||||||
setAcceptHoverEvents(true);
|
setAcceptHoverEvents(true);
|
||||||
}
|
}
|
||||||
|
@ -119,23 +141,28 @@ ProfileView::~ProfileView()
|
||||||
void ProfileView::resetPointers()
|
void ProfileView::resetPointers()
|
||||||
{
|
{
|
||||||
profileItem.reset();
|
profileItem.reset();
|
||||||
|
tooltip.reset();
|
||||||
|
pictures.clear();
|
||||||
|
highlightedPicture = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfileView::plotAreaChanged(const QSizeF &s)
|
void ProfileView::plotAreaChanged(const QSizeF &s)
|
||||||
{
|
{
|
||||||
|
int flags = simplified ? RenderFlags::Simplified : RenderFlags::None;
|
||||||
if (!empty)
|
if (!empty)
|
||||||
plotDive(d, dc, RenderFlags::Instant);
|
plotDive(d, dc, flags | RenderFlags::Instant);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfileView::replot()
|
void ProfileView::replot()
|
||||||
{
|
{
|
||||||
|
int flags = simplified ? RenderFlags::Simplified : RenderFlags::None;
|
||||||
if (!empty)
|
if (!empty)
|
||||||
plotDive(d, dc, RenderFlags::None);
|
plotDive(d, dc, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfileView::clear()
|
void ProfileView::clear()
|
||||||
{
|
{
|
||||||
//clearPictures();
|
clearPictures();
|
||||||
//disconnectPlannerConnections();
|
//disconnectPlannerConnections();
|
||||||
if (profileScene)
|
if (profileScene)
|
||||||
profileScene->clear();
|
profileScene->clear();
|
||||||
|
@ -152,6 +179,7 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags)
|
||||||
{
|
{
|
||||||
d = dIn;
|
d = dIn;
|
||||||
dc = dcIn;
|
dc = dcIn;
|
||||||
|
simplified = flags & RenderFlags::Simplified;
|
||||||
if (!d) {
|
if (!d) {
|
||||||
clear();
|
clear();
|
||||||
return;
|
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();
|
int animSpeed = flags & RenderFlags::Instant ? 0 : qPrefDisplay::animation_speed();
|
||||||
|
|
||||||
profileScene->resize(size());
|
profileScene->resize(size());
|
||||||
profileScene->plotDive(d, dc, animSpeed, model, inPlanner,
|
profileScene->plotDive(d, dc, animSpeed, simplified, model, inPlanner,
|
||||||
flags & RenderFlags::DontRecalculatePlotInfo,
|
flags & RenderFlags::DontRecalculatePlotInfo,
|
||||||
shouldCalculateMax, zoomLevel, zoomedPosition);
|
shouldCalculateMax, zoomLevel, zoomedPosition);
|
||||||
background = inPlanner ? QColor("#D7E3EF") : getColor(::BACKGROUND, false);
|
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.
|
// On zoom / pan don't recreate the picture thumbnails, only change their position.
|
||||||
//if (flags & RenderFlags::DontRecalculatePlotInfo)
|
if (!inPlanner) {
|
||||||
//updateThumbnails();
|
if (flags & RenderFlags::DontRecalculatePlotInfo)
|
||||||
//else
|
updateThumbnails();
|
||||||
//plotPicturesInternal(d, flags & RenderFlags::Instant);
|
else
|
||||||
|
plotPictures(d, flags);
|
||||||
|
} else {
|
||||||
|
clearPictures();
|
||||||
|
}
|
||||||
|
|
||||||
update();
|
update();
|
||||||
|
|
||||||
|
@ -226,9 +258,6 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset animation.
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,8 +280,9 @@ void ProfileView::setZoom(double level)
|
||||||
{
|
{
|
||||||
level = std::clamp(level, 1.0, 20.0);
|
level = std::clamp(level, 1.0, 20.0);
|
||||||
double old = std::exchange(zoomLevel, level);
|
double old = std::exchange(zoomLevel, level);
|
||||||
|
int flags = simplified ? RenderFlags::Simplified : RenderFlags::None;
|
||||||
if (level != old)
|
if (level != old)
|
||||||
plotDive(d, dc, RenderFlags::DontRecalculatePlotInfo);
|
plotDive(d, dc, flags | RenderFlags::DontRecalculatePlotInfo);
|
||||||
emit zoomLevelChanged();
|
emit zoomLevelChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,11 +315,32 @@ void ProfileView::mousePressEvent(QMouseEvent *event)
|
||||||
if (event->isAccepted())
|
if (event->isAccepted())
|
||||||
return;
|
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;
|
panning = true;
|
||||||
QPointF pos = mapToScene(event->pos());
|
QPointF pos = mapToScene(event->pos());
|
||||||
panStart(pos.x(), pos.y());
|
panStart(pos.x(), pos.y());
|
||||||
setCursor(Qt::ClosedHandCursor);
|
setCursor(Qt::ClosedHandCursor);
|
||||||
event->accept();
|
event->accept();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfileView::mouseReleaseEvent(QMouseEvent *event)
|
void ProfileView::mouseReleaseEvent(QMouseEvent *event)
|
||||||
|
@ -331,7 +382,8 @@ int ProfileView::getDiveId() const
|
||||||
|
|
||||||
void ProfileView::setDiveId(int id)
|
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
|
int ProfileView::numDC() const
|
||||||
|
@ -382,14 +434,61 @@ void ProfileView::pan(double x, double y)
|
||||||
zoomedPosition = profileScene->calcZoomPosition(zoomLevel,
|
zoomedPosition = profileScene->calcZoomPosition(zoomLevel,
|
||||||
panningOriginalProfilePosition,
|
panningOriginalProfilePosition,
|
||||||
panningOriginalMousePosition - x);
|
panningOriginalMousePosition - x);
|
||||||
|
int flags = simplified ? RenderFlags::Simplified : RenderFlags::None;
|
||||||
if (oldPos != zoomedPosition)
|
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::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)
|
void ProfileView::hoverMoveEvent(QHoverEvent *event)
|
||||||
{
|
{
|
||||||
if (!profileScene)
|
if (!profileScene)
|
||||||
|
@ -399,25 +498,398 @@ void ProfileView::hoverMoveEvent(QHoverEvent *event)
|
||||||
// resizing the ToolTipItem we get spurious hoverMoveEvents, which
|
// resizing the ToolTipItem we get spurious hoverMoveEvents, which
|
||||||
// restarts the animation, giving an infinite loop.
|
// restarts the animation, giving an infinite loop.
|
||||||
// Prevent this by comparing to the old mouse position.
|
// 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;
|
return;
|
||||||
|
|
||||||
if (tooltip && prefs.infobox) {
|
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();
|
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)
|
void ProfileView::updateTooltip(QPointF pos, bool plannerMode, int animSpeed)
|
||||||
{
|
{
|
||||||
int time = profileScene->timeAt(pos);
|
int time = timeAt(pos);
|
||||||
auto events = profileScene->eventsAt(pos);
|
auto events = profileScene->eventsAt(pos);
|
||||||
tooltip->update(d, dpr, time, profileScene->getPlotInfo(), events, plannerMode, animSpeed);
|
tooltip->update(d, dpr, time, profileScene->getPlotInfo(), events, plannerMode, animSpeed);
|
||||||
|
|
||||||
// Reset animation.
|
// Reset animation.
|
||||||
if (animSpeed <= 0)
|
|
||||||
tooltip_animation.reset();
|
|
||||||
else
|
|
||||||
tooltip_animation = make_anim([this](double progress)
|
tooltip_animation = make_anim([this](double progress)
|
||||||
{ if (tooltip) tooltip->anim(progress); update(); }, animSpeed);
|
{ 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
|
#define PROFILE_VIEW_H
|
||||||
|
|
||||||
#include "qt-quick/chartview.h"
|
#include "qt-quick/chartview.h"
|
||||||
|
#include "core/units.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
class ChartGraphicsSceneItem;
|
class ChartGraphicsSceneItem;
|
||||||
|
class ChartRectItem;
|
||||||
|
class PictureItem;
|
||||||
class ProfileAnimation;
|
class ProfileAnimation;
|
||||||
class ProfileScene;
|
class ProfileScene;
|
||||||
class ToolTipItem;
|
class ToolTipItem;
|
||||||
|
struct picture;
|
||||||
|
|
||||||
class ProfileView : public ChartView {
|
class ProfileView : public ChartView {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -28,9 +32,11 @@ public:
|
||||||
static constexpr int DontRecalculatePlotInfo = 1 << 1;
|
static constexpr int DontRecalculatePlotInfo = 1 << 1;
|
||||||
static constexpr int EditMode = 1 << 2;
|
static constexpr int EditMode = 1 << 2;
|
||||||
static constexpr int PlanMode = 1 << 3;
|
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);
|
void plotDive(const struct dive *d, int dc, int flags = RenderFlags::None);
|
||||||
|
int timeAt(QPointF pos) const;
|
||||||
void clear();
|
void clear();
|
||||||
void resetZoom();
|
void resetZoom();
|
||||||
void anim(double fraction);
|
void anim(double fraction);
|
||||||
|
@ -48,6 +54,7 @@ signals:
|
||||||
private:
|
private:
|
||||||
const struct dive *d;
|
const struct dive *d;
|
||||||
int dc;
|
int dc;
|
||||||
|
bool simplified;
|
||||||
double dpr;
|
double dpr;
|
||||||
double zoomLevel, zoomLevelPinchStart;
|
double zoomLevel, zoomLevelPinchStart;
|
||||||
double zoomedPosition; // Position when zoomed: 0.0 = beginning, 1.0 = end.
|
double zoomedPosition; // Position when zoomed: 0.0 = beginning, 1.0 = end.
|
||||||
|
@ -77,7 +84,44 @@ private:
|
||||||
void updateTooltip(QPointF pos, bool plannerMode, int animSpeed);
|
void updateTooltip(QPointF pos, bool plannerMode, int animSpeed);
|
||||||
std::unique_ptr<ProfileAnimation> tooltip_animation;
|
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
|
// For mobile
|
||||||
int getDiveId() const;
|
int getDiveId() const;
|
||||||
|
|
|
@ -75,11 +75,8 @@ ProfileWidget2::ProfileWidget2(DivePlannerPointsModel *plannerModelIn, double dp
|
||||||
setAcceptDrops(true);
|
setAcceptDrops(true);
|
||||||
|
|
||||||
connect(Thumbnailer::instance(), &Thumbnailer::thumbnailChanged, this, &ProfileWidget2::updateThumbnail, Qt::QueuedConnection);
|
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::cylinderEdited, this, &ProfileWidget2::profileChanged);
|
||||||
connect(&diveListNotifier, &DiveListNotifier::eventsChanged, 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::divesChanged, this, &ProfileWidget2::divesChanged);
|
||||||
connect(&diveListNotifier, &DiveListNotifier::deviceEdited, this, &ProfileWidget2::replot);
|
connect(&diveListNotifier, &DiveListNotifier::deviceEdited, this, &ProfileWidget2::replot);
|
||||||
connect(&diveListNotifier, &DiveListNotifier::diveComputerEdited, 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)
|
void ProfileWidget2::profileChanged(dive *dive)
|
||||||
{
|
{
|
||||||
if (dive != d)
|
if (dive != d)
|
||||||
|
@ -1153,126 +922,6 @@ void ProfileWidget2::profileChanged(dive *dive)
|
||||||
|
|
||||||
#endif
|
#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
|
struct dive *ProfileWidget2::mutable_dive() const
|
||||||
{
|
{
|
||||||
return const_cast<dive *>(d);
|
return const_cast<dive *>(d);
|
||||||
|
|
|
@ -29,7 +29,6 @@ class DivePlannerPointsModel;
|
||||||
class DiveHandler;
|
class DiveHandler;
|
||||||
class QGraphicsSimpleTextItem;
|
class QGraphicsSimpleTextItem;
|
||||||
class QModelIndex;
|
class QModelIndex;
|
||||||
class DivePictureItem;
|
|
||||||
|
|
||||||
class ProfileWidget2 : public QGraphicsView {
|
class ProfileWidget2 : public QGraphicsView {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -74,17 +73,12 @@ slots: // Necessary to call from QAction's signals.
|
||||||
void actionRequestedReplot(bool triggered);
|
void actionRequestedReplot(bool triggered);
|
||||||
void divesChanged(const QVector<dive *> &dives, DiveField field);
|
void divesChanged(const QVector<dive *> &dives, DiveField field);
|
||||||
#ifndef SUBSURFACE_MOBILE
|
#ifndef SUBSURFACE_MOBILE
|
||||||
void plotPictures();
|
|
||||||
void picturesRemoved(dive *d, QVector<QString> filenames);
|
|
||||||
void picturesAdded(dive *d, QVector<picture> pics);
|
|
||||||
void pointsReset();
|
void pointsReset();
|
||||||
void pointInserted(const QModelIndex &parent, int start, int end);
|
void pointInserted(const QModelIndex &parent, int start, int end);
|
||||||
void pointsRemoved(const QModelIndex &, 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 pointsMoved(const QModelIndex &, int start, int end, const QModelIndex &destination, int row);
|
||||||
void updateThumbnail(QString filename, QImage thumbnail, duration_t duration);
|
void updateThumbnail(QString filename, QImage thumbnail, duration_t duration);
|
||||||
void profileChanged(dive *d);
|
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? */
|
/* this is called for every move on the handlers. maybe we can speed up this a bit? */
|
||||||
void divePlannerHandlerMoved();
|
void divePlannerHandlerMoved();
|
||||||
|
@ -105,10 +99,6 @@ private:
|
||||||
void keyPressEvent(QKeyEvent *e) override;
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
void addGasChangeMenu(QMenu &m, QString menuTitle, const struct dive &d, int dcNr, int changeTime);
|
void addGasChangeMenu(QMenu &m, QString menuTitle, const struct dive &d, int dcNr, int changeTime);
|
||||||
#endif
|
#endif
|
||||||
void dropEvent(QDropEvent *event) override;
|
|
||||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
|
||||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
|
||||||
|
|
||||||
void replot();
|
void replot();
|
||||||
void setZoom(int level);
|
void setZoom(int level);
|
||||||
void addGasSwitch(int tank, int seconds);
|
void addGasSwitch(int tank, int seconds);
|
||||||
|
@ -117,8 +107,6 @@ private:
|
||||||
void addItemsToScene();
|
void addItemsToScene();
|
||||||
void setupItemOnScene();
|
void setupItemOnScene();
|
||||||
struct plot_data *getEntryFromPos(QPointF pos);
|
struct plot_data *getEntryFromPos(QPointF pos);
|
||||||
void clearPictures();
|
|
||||||
void plotPicturesInternal(const struct dive *d, bool synchronous);
|
|
||||||
void updateThumbnails();
|
void updateThumbnails();
|
||||||
void addDivemodeSwitch(int seconds, int divemode);
|
void addDivemodeSwitch(int seconds, int divemode);
|
||||||
void addBookmark(int seconds);
|
void addBookmark(int seconds);
|
||||||
|
@ -156,25 +144,6 @@ private:
|
||||||
std::vector<std::unique_ptr<QGraphicsSimpleTextItem>> gases;
|
std::vector<std::unique_ptr<QGraphicsSimpleTextItem>> gases;
|
||||||
|
|
||||||
#ifndef SUBSURFACE_MOBILE
|
#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 keyDeleteAction();
|
||||||
void keyUpAction();
|
void keyUpAction();
|
||||||
void keyDownAction();
|
void keyDownAction();
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
struct ProfileZValue {
|
struct ProfileZValue {
|
||||||
enum ZValues {
|
enum ZValues {
|
||||||
Profile = 0,
|
Profile = 0,
|
||||||
|
Pictures,
|
||||||
ToolTipItem,
|
ToolTipItem,
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
#include "chartitem.h"
|
#include "chartitem.h"
|
||||||
#include "chartitemhelper.h"
|
#include "chartitemhelper.h"
|
||||||
#include "chartview.h"
|
#include "chartitem_private.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <QGraphicsScene>
|
#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),
|
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()
|
void ChartPixmapItem::render()
|
||||||
{
|
{
|
||||||
|
doRearrange();
|
||||||
if (!node) {
|
if (!node) {
|
||||||
createNode(view.w()->createImageNode());
|
createNode(view.w()->createImageNode());
|
||||||
view.addQSGNode(node.get(), zValue);
|
addNodeToView();
|
||||||
}
|
}
|
||||||
updateVisible();
|
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)
|
void ChartPixmapItem::resize(QSizeF size)
|
||||||
{
|
{
|
||||||
QSize s_int(round_up(size.width()), round_up(size.height()));
|
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.reset(new QPainter(img.get()));
|
||||||
painter->setRenderHint(QPainter::Antialiasing);
|
painter->setRenderHint(QPainter::Antialiasing);
|
||||||
}
|
}
|
||||||
rect.setSize(size);
|
rect.setSize(scaleSize(size, scale));
|
||||||
setPositionDirty(); // position includes the size.
|
setPositionDirty(); // position includes the size.
|
||||||
setTextureDirty();
|
setTextureDirty();
|
||||||
}
|
}
|
||||||
|
@ -117,6 +125,15 @@ void ChartPixmapItem::setPos(QPointF pos)
|
||||||
setPositionDirty();
|
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)
|
void ChartGraphicsSceneItem::draw(QSizeF s, QColor background, QGraphicsScene &scene)
|
||||||
{
|
{
|
||||||
resize(s); // Noop if size doesn't change
|
resize(s); // Noop if size doesn't change
|
||||||
|
@ -238,6 +255,7 @@ void ChartLineItemBase::setLine(QPointF from, QPointF to)
|
||||||
|
|
||||||
void ChartLineItem::render()
|
void ChartLineItem::render()
|
||||||
{
|
{
|
||||||
|
doRearrange();
|
||||||
if (!node) {
|
if (!node) {
|
||||||
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
|
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
|
||||||
geometry->setDrawingMode(QSGGeometry::DrawLines);
|
geometry->setDrawingMode(QSGGeometry::DrawLines);
|
||||||
|
@ -245,7 +263,7 @@ void ChartLineItem::render()
|
||||||
createNode();
|
createNode();
|
||||||
node->setGeometry(geometry.get());
|
node->setGeometry(geometry.get());
|
||||||
node->setMaterial(material.get());
|
node->setMaterial(material.get());
|
||||||
view.addQSGNode(node.get(), zValue);
|
addNodeToView();
|
||||||
positionDirty = materialDirty = true;
|
positionDirty = materialDirty = true;
|
||||||
}
|
}
|
||||||
updateVisible();
|
updateVisible();
|
||||||
|
@ -269,6 +287,7 @@ void ChartLineItem::render()
|
||||||
|
|
||||||
void ChartRectLineItem::render()
|
void ChartRectLineItem::render()
|
||||||
{
|
{
|
||||||
|
doRearrange();
|
||||||
if (!node) {
|
if (!node) {
|
||||||
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 5));
|
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 5));
|
||||||
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
|
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
|
||||||
|
@ -276,7 +295,7 @@ void ChartRectLineItem::render()
|
||||||
createNode();
|
createNode();
|
||||||
node->setGeometry(geometry.get());
|
node->setGeometry(geometry.get());
|
||||||
node->setMaterial(material.get());
|
node->setMaterial(material.get());
|
||||||
view.addQSGNode(node.get(), zValue);
|
addNodeToView();
|
||||||
positionDirty = materialDirty = true;
|
positionDirty = materialDirty = true;
|
||||||
}
|
}
|
||||||
updateVisible();
|
updateVisible();
|
||||||
|
|
|
@ -47,11 +47,27 @@ protected:
|
||||||
std::unique_ptr<Node> node;
|
std::unique_ptr<Node> node;
|
||||||
bool visible;
|
bool visible;
|
||||||
bool visibleChanged;
|
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>
|
template<class... Args>
|
||||||
void createNode(Args&&... args); // Call to create node with visibility flag.
|
void createNode(Args&&... args); // Call to create node with visibility flag.
|
||||||
|
|
||||||
void updateVisible(); // Must be called by child class to update 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:
|
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);
|
void setVisible(bool visible);
|
||||||
|
bool isVisible() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A shortcut for ChartItems based on a hideable proxy item
|
// A shortcut for ChartItems based on a hideable proxy item
|
||||||
|
@ -59,12 +75,14 @@ template <typename Node>
|
||||||
using HideableChartProxyItem = HideableChartItem<HideableQSGNode<QSGProxyNode<Node>>>;
|
using HideableChartProxyItem = HideableChartItem<HideableQSGNode<QSGProxyNode<Node>>>;
|
||||||
|
|
||||||
// A chart item that blits a precalculated pixmap onto the scene.
|
// A chart item that blits a precalculated pixmap onto the scene.
|
||||||
|
// Can be scaled with setScale().
|
||||||
class ChartPixmapItem : public HideableChartProxyItem<QSGImageNode> {
|
class ChartPixmapItem : public HideableChartProxyItem<QSGImageNode> {
|
||||||
public:
|
public:
|
||||||
ChartPixmapItem(ChartView &v, size_t z, bool dragable = false);
|
ChartPixmapItem(ChartView &v, size_t z, bool dragable = false);
|
||||||
~ChartPixmapItem();
|
~ChartPixmapItem();
|
||||||
|
|
||||||
void setPos(QPointF pos) override;
|
void setPos(QPointF pos) override;
|
||||||
|
void setScale(double scale);
|
||||||
void render() override;
|
void render() override;
|
||||||
protected:
|
protected:
|
||||||
void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*.
|
void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*.
|
||||||
|
@ -72,6 +90,7 @@ protected:
|
||||||
std::unique_ptr<QImage> img;
|
std::unique_ptr<QImage> img;
|
||||||
void setTextureDirty();
|
void setTextureDirty();
|
||||||
void setPositionDirty();
|
void setPositionDirty();
|
||||||
|
double scale;
|
||||||
private:
|
private:
|
||||||
bool positionDirty; // true if the position changed since last render
|
bool positionDirty; // true if the position changed since last render
|
||||||
bool textureDirty; // true if the pixmap 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
|
// 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>
|
template <typename Node>
|
||||||
void HideableChartItem<Node>::setVisible(bool visibleIn)
|
void HideableChartItem<Node>::setVisible(bool visibleIn)
|
||||||
{
|
{
|
||||||
|
@ -172,25 +226,9 @@ void HideableChartItem<Node>::setVisible(bool visibleIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Node>
|
template <typename Node>
|
||||||
template<class... Args>
|
bool HideableChartItem<Node>::isVisible() const
|
||||||
void HideableChartItem<Node>::createNode(Args&&... args)
|
|
||||||
{
|
{
|
||||||
node.reset(new Node(visible, std::forward<Args>(args)...));
|
return visible;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#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(const ChartItemPtr &p) : ptr(p.ptr)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
ChartItemPtr(ChartItemPtr &&p) : ptr(p.ptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
void reset()
|
void reset()
|
||||||
{
|
{
|
||||||
ptr = nullptr;
|
ptr = nullptr;
|
||||||
|
|
|
@ -127,10 +127,54 @@ void ChartView::clearItems()
|
||||||
dirtyItems.splice(deletedItems);
|
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);
|
size_t idx = std::clamp(z, (size_t)0, rootNode.zNodes.size());
|
||||||
rootNode->zNodes[idx]->appendChildNode(node);
|
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)
|
void ChartView::registerChartItem(ChartItem &item)
|
||||||
|
|
|
@ -20,7 +20,13 @@ public:
|
||||||
QSizeF size() const;
|
QSizeF size() const;
|
||||||
QRectF plotArea() const;
|
QRectF plotArea() const;
|
||||||
void setBackgroundColor(QColor color); // Chart must be replot for color to become effective.
|
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 registerChartItem(ChartItem &item);
|
||||||
void registerDirtyChartItem(ChartItem &item);
|
void registerDirtyChartItem(ChartItem &item);
|
||||||
void emergencyShutdown(); // Called when QQuick decides to delete our root node.
|
void emergencyShutdown(); // Called when QQuick decides to delete our root node.
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include "statsview.h"
|
#include "statsview.h"
|
||||||
#include "core/globals.h"
|
#include "core/globals.h"
|
||||||
#include "qt-quick/chartitemhelper.h"
|
#include "qt-quick/chartitemhelper.h"
|
||||||
|
#include "qt-quick/chartitem_private.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <QQuickWindow>
|
#include <QQuickWindow>
|
||||||
|
@ -61,6 +62,7 @@ QSGTexture *ChartScatterItem::getTexture() const
|
||||||
|
|
||||||
void ChartScatterItem::render()
|
void ChartScatterItem::render()
|
||||||
{
|
{
|
||||||
|
doRearrange();
|
||||||
if (!theme.scatterItemTexture) {
|
if (!theme.scatterItemTexture) {
|
||||||
theme.scatterItemTexture = register_global(createScatterTexture(view, theme.fillColor, theme.borderColor));
|
theme.scatterItemTexture = register_global(createScatterTexture(view, theme.fillColor, theme.borderColor));
|
||||||
theme.scatterItemSelectedTexture = register_global(createScatterTexture(view, theme.selectedColor, theme.selectedBorderColor));
|
theme.scatterItemSelectedTexture = register_global(createScatterTexture(view, theme.selectedColor, theme.selectedBorderColor));
|
||||||
|
@ -68,7 +70,7 @@ void ChartScatterItem::render()
|
||||||
}
|
}
|
||||||
if (!node) {
|
if (!node) {
|
||||||
createNode(view.w()->createImageNode());
|
createNode(view.w()->createImageNode());
|
||||||
view.addQSGNode(node.get(), zValue);
|
addNodeToView();
|
||||||
textureDirty = positionDirty = true;
|
textureDirty = positionDirty = true;
|
||||||
}
|
}
|
||||||
updateVisible();
|
updateVisible();
|
||||||
|
@ -193,6 +195,7 @@ QSGTexture *ChartBarItem::getSelectedTexture() const
|
||||||
|
|
||||||
void ChartBarItem::render()
|
void ChartBarItem::render()
|
||||||
{
|
{
|
||||||
|
doRearrange();
|
||||||
if (!node) {
|
if (!node) {
|
||||||
createNode(view.w()->createRectangleNode());
|
createNode(view.w()->createRectangleNode());
|
||||||
|
|
||||||
|
@ -205,7 +208,7 @@ void ChartBarItem::render()
|
||||||
borderNode->setMaterial(borderMaterial.get());
|
borderNode->setMaterial(borderMaterial.get());
|
||||||
|
|
||||||
node->node->appendChildNode(borderNode.get());
|
node->node->appendChildNode(borderNode.get());
|
||||||
view.addQSGNode(node.get(), zValue);
|
addNodeToView();
|
||||||
positionDirty = colorDirty = selectedDirty = true;
|
positionDirty = colorDirty = selectedDirty = true;
|
||||||
}
|
}
|
||||||
updateVisible();
|
updateVisible();
|
||||||
|
@ -311,6 +314,7 @@ ChartBoxItem::~ChartBoxItem()
|
||||||
|
|
||||||
void ChartBoxItem::render()
|
void ChartBoxItem::render()
|
||||||
{
|
{
|
||||||
|
doRearrange();
|
||||||
// Remember old dirty values, since ChartBarItem::render() will clear them
|
// Remember old dirty values, since ChartBarItem::render() will clear them
|
||||||
bool oldPositionDirty = positionDirty;
|
bool oldPositionDirty = positionDirty;
|
||||||
bool oldColorDirty = colorDirty;
|
bool oldColorDirty = colorDirty;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue