From edf2a2f4f43778d7573fb017ea2ca138fe86a0c3 Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Sun, 28 Aug 2022 22:42:54 +0200 Subject: [PATCH] profile: implement panning of profile When zoomed in, the profile position was moved by hovering with the mouse. What a horrible user experience. This is especially useless if we want to implement an interactive profile on mobile. Instead, let the user start the panning with a mouse click. The code is somewhat nasty, because the position is given as a real in the [0,1] range, which represents all possible positions from completely to the left to completely to the right. This commit also removes the restriction that the planner handles can only be moved when fully zoomed out. It is not completely clear what the implications are. Let's see. Signed-off-by: Berthold Stoeger --- CHANGELOG.md | 2 ++ profile-widget/divecartesianaxis.cpp | 10 ++++++ profile-widget/divecartesianaxis.h | 1 + profile-widget/profilescene.cpp | 25 +++++++++++--- profile-widget/profilescene.h | 1 + profile-widget/profilewidget2.cpp | 50 +++++++++++++++------------- profile-widget/profilewidget2.h | 3 ++ 7 files changed, 63 insertions(+), 29 deletions(-) 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;