diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f0a31994..b83b9b1f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ import: allow import of divesites without UUID +profile: implement panning of the profile +planner: allow handle manipulation in zoomed in state divelist: do not include planned versions of a dive if there is real data desktop: fix key composition in tag widgets and dive site widget desktop: use combobox for moving sensor between cylinders diff --git a/profile-widget/divecartesianaxis.cpp b/profile-widget/divecartesianaxis.cpp index b4f46da85..9220d7d3b 100644 --- a/profile-widget/divecartesianaxis.cpp +++ b/profile-widget/divecartesianaxis.cpp @@ -436,6 +436,16 @@ double DiveCartesianAxis::valueAt(const QPointF &p) const return fraction * (max - min) + min; } +double DiveCartesianAxis::deltaToValue(double delta) const +{ + QLineF m = line(); + double screenSize = position == Position::Bottom ? m.x2() - m.x1() + : m.y2() - m.y1(); + double axisSize = max - min; + double res = delta * axisSize / screenSize; + return ((position == Position::Bottom) == inverted) ? -res : res; +} + double DiveCartesianAxis::posAtValue(double value, double max, double min) const { QLineF m = line(); diff --git a/profile-widget/divecartesianaxis.h b/profile-widget/divecartesianaxis.h index 580ded8a1..487017586 100644 --- a/profile-widget/divecartesianaxis.h +++ b/profile-widget/divecartesianaxis.h @@ -31,6 +31,7 @@ public: std::pair screenMinMax() const; double valueAt(const QPointF &p) const; double posAtValue(double value) const; + double deltaToValue(double delta) const; // For panning: turn a screen distance to delta-value void setPosition(const QRectF &rect); double screenPosition(double pos) const; // 0.0 = begin, 1.0 = end of axis, independent of represented values double pointInRange(double pos) const; // Point on screen is in range of axis diff --git a/profile-widget/profilescene.cpp b/profile-widget/profilescene.cpp index eb799f6cb..a38824ad4 100644 --- a/profile-widget/profilescene.cpp +++ b/profile-widget/profilescene.cpp @@ -12,6 +12,7 @@ #include "core/pref.h" #include "core/profile.h" #include "core/qthelper.h" // for decoMode() +#include "core/subsurface-float.h" #include "core/subsurface-string.h" #include "core/settings/qPrefDisplay.h" #include "qt-models/diveplannermodel.h" @@ -491,11 +492,9 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM percentageAxis->updateTicks(animSpeed); animatedAxes.push_back(percentageAxis); - if (calcMax) { - double relStart = (1.0 - 1.0/zoom) * zoomedPosition; - double relEnd = relStart + 1.0/zoom; - timeAxis->setBounds(round(relStart * maxtime), round(relEnd * maxtime)); - } + double relStart = (1.0 - 1.0/zoom) * zoomedPosition; + double relEnd = relStart + 1.0/zoom; + timeAxis->setBounds(round(relStart * maxtime), round(relEnd * maxtime)); // Find first and last plotInfo entry int firstSecond = lrint(timeAxis->minimum()); @@ -627,3 +626,19 @@ void ProfileScene::draw(QPainter *painter, const QRect &pos, } painter->drawImage(pos, image); } + +// Calculate the new zoom position when the mouse is dragged by delta. +// This is annoyingly complex, because the zoom position is given as +// a real between 0 and 1. +double ProfileScene::calcZoomPosition(double zoom, double originalPos, double delta) +{ + double factor = 1.0 - 1.0/zoom; + if (nearly_0(factor)) + return 0.0; + double relStart = factor * originalPos; + double start = relStart * maxtime; + double newStart = start + timeAxis->deltaToValue(delta); + double newRelStart = newStart / maxtime; + double newPos = newRelStart / factor; + return std::clamp(newPos, 0.0, 1.0); +} diff --git a/profile-widget/profilescene.h b/profile-widget/profilescene.h index 61d97fc0d..eada4fb8d 100644 --- a/profile-widget/profilescene.h +++ b/profile-widget/profilescene.h @@ -48,6 +48,7 @@ public: void draw(QPainter *painter, const QRect &pos, const struct dive *d, int dc, DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false); + double calcZoomPosition(double zoom, double originalPos, double delta); const struct dive *d; int dc; diff --git a/profile-widget/profilewidget2.cpp b/profile-widget/profilewidget2.cpp index f9a05a488..819f0a0da 100644 --- a/profile-widget/profilewidget2.cpp +++ b/profile-widget/profilewidget2.cpp @@ -40,8 +40,12 @@ // We might add more constants here for easier customability. static const double thumbnailBaseZValue = 100.0; -// Base of exponential zoom function: one wheel-click will increase the zoom by 15%. -static const double zoomFactor = 1.15; +static double calcZoom(int zoomLevel) +{ + // Base of exponential zoom function: one wheel-click will increase the zoom by 15%. + constexpr double zoomFactor = 1.15; + return zoomLevel == 0 ? 1.0 : pow(zoomFactor, zoomLevel); +} ProfileWidget2::ProfileWidget2(DivePlannerPointsModel *plannerModelIn, double dpr, QWidget *parent) : QGraphicsView(parent), profileScene(new ProfileScene(dpr, false, false)), @@ -55,6 +59,7 @@ ProfileWidget2::ProfileWidget2(DivePlannerPointsModel *plannerModelIn, double dp d(nullptr), dc(0), empty(true), + panning(false), #ifndef SUBSURFACE_MOBILE mouseFollowerVertical(new DiveLineItem()), mouseFollowerHorizontal(new DiveLineItem()), @@ -202,7 +207,7 @@ void ProfileWidget2::plotDive(const struct dive *dIn, int dcIn, int flags) DivePlannerPointsModel *model = currentState == EDIT || currentState == PLAN ? plannerModel : nullptr; bool inPlanner = currentState == PLAN; - double zoom = zoomLevel == 0 ? 1.0 : pow(zoomFactor, zoomLevel); + double zoom = calcZoom(zoomLevel); profileScene->plotDive(d, dc, model, inPlanner, flags & RenderFlags::Instant, flags & RenderFlags::DontRecalculatePlotInfo, shouldCalculateMax, zoom, zoomedPosition); @@ -267,24 +272,22 @@ void ProfileWidget2::resizeEvent(QResizeEvent *event) #ifndef SUBSURFACE_MOBILE void ProfileWidget2::mousePressEvent(QMouseEvent *event) { - if (zoomLevel) - return; QGraphicsView::mousePressEvent(event); - if (currentState == PLAN || currentState == EDIT) - shouldCalculateMax = false; + + if (!event->isAccepted()) { + panning = true; + panningOriginalMousePosition = mapToScene(event->pos()).x(); + panningOriginalProfilePosition = zoomedPosition; + } } void ProfileWidget2::divePlannerHandlerClicked() { - if (zoomLevel) - return; shouldCalculateMax = false; } void ProfileWidget2::divePlannerHandlerReleased() { - if (zoomLevel) - return; if (currentState == EDIT) emit stopMoved(1); shouldCalculateMax = true; @@ -293,9 +296,8 @@ void ProfileWidget2::divePlannerHandlerReleased() void ProfileWidget2::mouseReleaseEvent(QMouseEvent *event) { - if (zoomLevel) - return; QGraphicsView::mouseReleaseEvent(event); + panning = false; if (currentState == PLAN || currentState == EDIT) { shouldCalculateMax = true; replot(); @@ -306,12 +308,6 @@ void ProfileWidget2::mouseReleaseEvent(QMouseEvent *event) void ProfileWidget2::setZoom(int level) { zoomLevel = level; - if (zoomLevel == 0) { - zoomedPosition = 0.0; - } else { - double pos = mapToScene(mapFromGlobal(QCursor::pos())).x(); - zoomedPosition = pos / profileScene->width(); - } plotDive(d, dc, RenderFlags::DontRecalculatePlotInfo); } @@ -320,6 +316,8 @@ void ProfileWidget2::wheelEvent(QWheelEvent *event) { if (!d) return; + if (panning) + return; // No change in zoom level while panning. if (event->buttons() == Qt::LeftButton) return; if (event->angleDelta().y() > 0 && zoomLevel < 20) @@ -348,13 +346,17 @@ void ProfileWidget2::mouseMoveEvent(QMouseEvent *event) QGraphicsView::mouseMoveEvent(event); QPointF pos = mapToScene(event->pos()); - toolTipItem->refresh(d, mapToScene(mapFromGlobal(QCursor::pos())), currentState == PLAN); - - if (zoomLevel != 0) { - zoomedPosition = pos.x() / profileScene->width(); - plotDive(d, dc, RenderFlags::Instant | RenderFlags::DontRecalculatePlotInfo); // TODO: animations don't work when scrolling + if (panning) { + double oldPos = zoomedPosition; + zoomedPosition = profileScene->calcZoomPosition(calcZoom(zoomLevel), + panningOriginalProfilePosition, + panningOriginalMousePosition - pos.x()); + if (oldPos != zoomedPosition) + plotDive(d, dc, RenderFlags::Instant | RenderFlags::DontRecalculatePlotInfo); // TODO: animations don't work when scrolling } + toolTipItem->refresh(d, mapToScene(mapFromGlobal(QCursor::pos())), currentState == PLAN); + if (currentState == PLAN || currentState == EDIT) { QRectF rect = profileScene->profileRegion; auto [miny, maxy] = profileScene->profileYAxis->screenMinMax(); diff --git a/profile-widget/profilewidget2.h b/profile-widget/profilewidget2.h index 474e05870..25663eecd 100644 --- a/profile-widget/profilewidget2.h +++ b/profile-widget/profilewidget2.h @@ -142,6 +142,9 @@ private: const struct dive *d; int dc; bool empty; // No dive shown. + bool panning; // Currently panning. + double panningOriginalMousePosition; + double panningOriginalProfilePosition; #ifndef SUBSURFACE_MOBILE DiveLineItem *mouseFollowerVertical; DiveLineItem *mouseFollowerHorizontal;