diff --git a/Subsurface-mobile.pro b/Subsurface-mobile.pro index d7218a0c6..3559a9be4 100644 --- a/Subsurface-mobile.pro +++ b/Subsurface-mobile.pro @@ -128,7 +128,6 @@ SOURCES += subsurface-mobile-main.cpp \ core/subsurface-qt/divelistnotifier.cpp \ backend-shared/exportfuncs.cpp \ backend-shared/plannershared.cpp \ - backend-shared/roundrectitem.cpp \ qt-quick/chartitem.cpp \ qt-quick/chartview.cpp \ stats/statsvariables.cpp \ @@ -179,8 +178,8 @@ SOURCES += subsurface-mobile-main.cpp \ profile-widget/animationfunctions.cpp \ profile-widget/divepixmapcache.cpp \ profile-widget/divepixmapitem.cpp \ - profile-widget/divetooltipitem.cpp \ profile-widget/tankitem.cpp \ + profile-widget/tooltipitem.cpp \ profile-widget/divelineitem.cpp \ profile-widget/diverectitem.cpp \ profile-widget/divetextitem.cpp \ @@ -287,7 +286,6 @@ HEADERS += \ core/subsurface-qt/divelistnotifier.h \ backend-shared/exportfuncs.h \ backend-shared/plannershared.h \ - backend-shared/roundrectitem.h \ qt-quick/chartitem.h \ qt-quick/chartitemhelper.h \ qt-quick/chartitem_ptr.h \ @@ -340,8 +338,8 @@ HEADERS += \ profile-widget/diveprofileitem.h \ profile-widget/profilescene.h \ profile-widget/diveeventitem.h \ - profile-widget/divetooltipitem.h \ profile-widget/tankitem.h \ + profile-widget/tooltipitem.h \ profile-widget/animationfunctions.h \ profile-widget/divecartesianaxis.h \ profile-widget/divelineitem.h \ diff --git a/backend-shared/CMakeLists.txt b/backend-shared/CMakeLists.txt index 736b03279..fdfdb1e05 100644 --- a/backend-shared/CMakeLists.txt +++ b/backend-shared/CMakeLists.txt @@ -5,8 +5,6 @@ set(BACKEND_SRCS exportfuncs.h plannershared.cpp plannershared.h - roundrectitem.cpp - roundrectitem.h ) add_library(subsurface_backend_shared STATIC ${BACKEND_SRCS}) diff --git a/backend-shared/roundrectitem.cpp b/backend-shared/roundrectitem.cpp deleted file mode 100644 index 52200b017..000000000 --- a/backend-shared/roundrectitem.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "roundrectitem.h" -#include -#include - -RoundRectItem::RoundRectItem(double radius, QGraphicsItem *parent) : QGraphicsRectItem(parent), - radius(radius) -{ -} - -RoundRectItem::RoundRectItem(double radius) : RoundRectItem(radius, nullptr) -{ -} - -void RoundRectItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) -{ - painter->save(); - painter->setClipRect(option->rect); - painter->setPen(pen()); - painter->setBrush(brush()); - painter->drawRoundedRect(rect(), radius, radius, Qt::AbsoluteSize); - painter->restore(); -} diff --git a/backend-shared/roundrectitem.h b/backend-shared/roundrectitem.h deleted file mode 100644 index b8cb3f89b..000000000 --- a/backend-shared/roundrectitem.h +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -#ifndef ROUNDRECTITEM_H -#define ROUNDRECTITEM_H - -#include - -class RoundRectItem : public QGraphicsRectItem { -public: - RoundRectItem(double radius, QGraphicsItem *parent); - RoundRectItem(double radius); -private: - void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; - double radius; -}; - -#endif diff --git a/profile-widget/CMakeLists.txt b/profile-widget/CMakeLists.txt index 9804f1189..5cd28655f 100644 --- a/profile-widget/CMakeLists.txt +++ b/profile-widget/CMakeLists.txt @@ -20,14 +20,15 @@ set(SUBSURFACE_PROFILE_LIB_SRCS diverectitem.h divetextitem.cpp divetextitem.h - divetooltipitem.cpp - divetooltipitem.h profilescene.cpp profilescene.h + profiletranslations.h profileview.cpp profileview.h tankitem.cpp tankitem.h + tooltipitem.h + tooltipitem.cpp ) if (SUBSURFACE_TARGET_EXECUTABLE MATCHES "MobileExecutable") set(SUBSURFACE_PROFILE_LIB_SRCS diff --git a/profile-widget/profilescene.cpp b/profile-widget/profilescene.cpp index 8c7f56652..0886a110a 100644 --- a/profile-widget/profilescene.cpp +++ b/profile-widget/profilescene.cpp @@ -162,6 +162,11 @@ ProfileScene::~ProfileScene() { } +const plot_info &ProfileScene::getPlotInfo() const +{ + return plotInfo; +} + void ProfileScene::clear() { for (AbstractProfilePolygonItem *item: profileItems) @@ -613,3 +618,8 @@ double ProfileScene::calcZoomPosition(double zoom, double originalPos, double de double newPos = newRelStart / factor; return std::clamp(newPos, 0.0, 1.0); } + +int ProfileScene::timeAt(QPointF pos) const +{ + return lrint(timeAxis->valueAt(pos)); +} diff --git a/profile-widget/profilescene.h b/profile-widget/profilescene.h index 566a92cb1..960bfdb64 100644 --- a/profile-widget/profilescene.h +++ b/profile-widget/profilescene.h @@ -48,6 +48,8 @@ public: const struct dive *d, int dc, DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false); double calcZoomPosition(double zoom, double originalPos, double delta); + const plot_info &getPlotInfo() const; + int timeAt(QPointF pos) const; const struct dive *d; int dc; diff --git a/profile-widget/profiletranslations.h b/profile-widget/profiletranslations.h new file mode 100644 index 000000000..d95d8e6c8 --- /dev/null +++ b/profile-widget/profiletranslations.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0 +// Dummy object for profile-module related translations + +#ifndef PROFILE_TRANSLATIONS_H +#define PROFILE_TRANSLATIONS_H + +#include + +class ProfileTranslations : public QObject +{ + Q_OBJECT +}; + +#endif diff --git a/profile-widget/profileview.cpp b/profile-widget/profileview.cpp index 2f1f5f6cc..2b49cfe4b 100644 --- a/profile-widget/profileview.cpp +++ b/profile-widget/profileview.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 #include "profileview.h" #include "profilescene.h" +#include "tooltipitem.h" #include "zvalues.h" #include "core/dive.h" #include "core/divelog.h" @@ -46,6 +47,7 @@ public: ProfileView::ProfileView(QQuickItem *parent) : ChartView(parent, ProfileZValue::Count), d(nullptr), dc(0), + dpr(1.0), zoomLevel(1.00), zoomedPosition(0.0), panning(false), @@ -84,6 +86,7 @@ ProfileView::ProfileView(QQuickItem *parent) : ChartView(parent, ProfileZValue:: connect(pp_gas, &qPrefPartialPressureGas::po2Changed, this, &ProfileView::replot); setAcceptTouchEvents(true); + setAcceptHoverEvents(true); } ProfileView::ProfileView() : ProfileView(nullptr) @@ -119,6 +122,7 @@ void ProfileView::clear() profileScene->clear(); //handles.clear(); //gases.clear(); + tooltip.reset(); empty = true; d = nullptr; dc = 0; @@ -135,7 +139,7 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags) // We can't create the scene in the constructor, because we can't get the DPR property there. Oh joy! if (!profileScene) { - double dpr = std::clamp(property("dpr").toReal(), 0.5, 100.0); + dpr = std::clamp(property("dpr").toReal(), 0.5, 100.0); profileScene = std::make_unique(dpr, false, false); } // If there was no previously displayed dive, turn off animations @@ -166,7 +170,6 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags) profileItem->draw(size(), background, *profileScene); //rulerItem->setVisible(prefs.rulergraph && currentState != PLAN && currentState != EDIT); - //toolTipItem->setPlotInfo(profileScene->plotInfo); //rulerItem->setPlotInfo(d, profileScene->plotInfo); //if ((currentState == EDIT || currentState == PLAN) && plannerModel) { @@ -194,6 +197,15 @@ void ProfileView::plotDive(const struct dive *dIn, int dcIn, int flags) report_error("%s", qPrintable(tr("Show NDL / TTS was disabled because of excessive processing time"))); } + if (!tooltip) + tooltip = createChartItem(dpr); + if (prefs.infobox) { + tooltip->setVisible(true); + tooltip->update(d, dpr, 0, profileScene->getPlotInfo(), flags & RenderFlags::PlanMode); + } else { + tooltip->setVisible(false); + } + // Reset animation. if (animSpeed <= 0) animation.reset(); @@ -347,3 +359,23 @@ void ProfileView::pan(double x, double y) if (oldPos != zoomedPosition) plotDive(d, dc, RenderFlags::Instant | RenderFlags::DontRecalculatePlotInfo); // TODO: animations don't work when scrolling } + +void ProfileView::hoverEnterEvent(QHoverEvent *) +{ +} + +void ProfileView::hoverMoveEvent(QHoverEvent *event) +{ + if (!profileScene) + return; + QPointF pos = event->pos(); + int time = profileScene->timeAt(pos); + bool requires_update = false; + if (tooltip) { + tooltip->update(d, dpr, time, profileScene->getPlotInfo(), false); // TODO: plan mode + requires_update = true; + } + + if (requires_update) + update(); +} diff --git a/profile-widget/profileview.h b/profile-widget/profileview.h index cc5ce255c..5aaf449bc 100644 --- a/profile-widget/profileview.h +++ b/profile-widget/profileview.h @@ -8,6 +8,7 @@ class ChartGraphicsSceneItem; class ProfileAnimation; class ProfileScene; +class ToolTipItem; class ProfileView : public ChartView { Q_OBJECT @@ -47,6 +48,7 @@ signals: private: const struct dive *d; int dc; + double dpr; double zoomLevel, zoomLevelPinchStart; double zoomedPosition; // Position when zoomed: 0.0 = beginning, 1.0 = end. bool panning; // Currently panning. @@ -64,11 +66,15 @@ private: void replot(); void setZoom(double level); + void hoverEnterEvent(QHoverEvent *event) override; + void hoverMoveEvent(QHoverEvent *event) override; void wheelEvent(QWheelEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; + ChartItemPtr tooltip; + // For mobile int getDiveId() const; void setDiveId(int id); diff --git a/profile-widget/tooltipitem.cpp b/profile-widget/tooltipitem.cpp new file mode 100644 index 000000000..151fd658d --- /dev/null +++ b/profile-widget/tooltipitem.cpp @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "tooltipitem.h" +#include "profiletranslations.h" +#include "zvalues.h" + +#include "core/color.h" +#include "core/membuffer.h" +#include "core/profile.h" +#include "core/qthelper.h" // for decoMode + +#include +#include +#include + +static const int tooltipBorder = 2; +static const double tooltipBorderRadius = 4.0; // Radius of rounded corners + +static QColor tooltipBorderColor(Qt::white); +static QColor tooltipColor(0, 0, 0, 155); +static QColor tooltipFontColor(Qt::white); + +static QFont makeFont(double dpr) +{ + QFont font(qApp->font()); + if (dpr != 1.0) { + int pixelSize = font.pixelSize(); + if (pixelSize > 0) { + pixelSize = lrint(static_cast(pixelSize) * dpr); + font.setPixelSize(pixelSize); + } else { + font.setPointSizeF(font.pointSizeF() * dpr); + } + } + return font; +} + +ToolTipItem::ToolTipItem(ChartView &view, double dpr) : + ChartRectItem(view, ProfileZValue::ToolTipItem, + QPen(tooltipBorderColor, tooltipBorder), + QBrush(tooltipColor), tooltipBorderRadius), + font(makeFont(dpr)), + fm(font), + fontHeight(fm.height()) +{ + title = stringToPixmap(ProfileTranslations::tr("Information")); +} + +QPixmap ToolTipItem::stringToPixmap(const QString &str) const +{ + QSize s = fm.size(Qt::TextSingleLine, str); + if (s.width() <= 0 || s.height() <= 0) + return QPixmap(1,1); + QPixmap res(s); + res.fill(Qt::transparent); + QPainter painter(&res); + painter.setFont(font); + painter.setPen(tooltipFontColor); + painter.drawText(QRect(QPoint(), s), str); + return res; +} + +// Split a membuffer into strings, skip empty lines. +// Return string / width (in pixels) pairs. +static std::vector> split_mb_into_strings(const membuffer &mb, const QFontMetrics &fm) +{ + std::vector> res; + for (size_t i = 0; i < mb.len; ++i) { + size_t j; + for (j = i; j < mb.len && mb.buffer[j] != '\n'; ++j) + ; + if (j > i) { + QString s = QString::fromUtf8(mb.buffer + i, j - i); + int width = fm.size(Qt::TextSingleLine, s).width(); + res.emplace_back(s, width); + } + i = j; // Note: loop iteration will skip over '\n' + } + return res; +} + +static QPixmap drawTissues(const plot_info &pInfo, double dpr, int idx, bool inPlanner) +{ + QPixmap tissues(16,60); + QPainter painter(&tissues); + tissues.fill(); + painter.setPen(QColor(0, 0, 0, 0)); + painter.setBrush(QColor(LIMENADE1)); + painter.drawRect(0, 10 + (100 - AMB_PERCENTAGE) / 2, 16, AMB_PERCENTAGE / 2); + painter.setBrush(QColor(SPRINGWOOD1)); + painter.drawRect(0, 10, 16, (100 - AMB_PERCENTAGE) / 2); + painter.setBrush(QColor(Qt::red)); + painter.drawRect(0,0,16,10); + + if (!idx) + return tissues; + + const struct plot_data *entry = &pInfo.entry[idx]; + painter.setPen(QColor(0, 0, 0, 255)); + if (decoMode(inPlanner) == BUEHLMANN) + painter.drawLine(0, lrint(60 - entry->gfline / 2), 16, lrint(60 - entry->gfline / 2)); + painter.drawLine(0, lrint(60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure / 2), + 16, lrint(60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure /2)); + painter.setPen(QColor(0, 0, 0, 127)); + for (int i = 0; i < 16; i++) + painter.drawLine(i, 60, i, 60 - entry->percentages[i] / 2); + + if (dpr == 1.0) + return tissues; + + // Scale according to DPR + int new_width = lrint(tissues.width() * dpr); + int new_height = lrint(tissues.width() * dpr); + return tissues.scaled(new_width, new_height); +} + +void ToolTipItem::update(const dive *d, double dpr, int time, const plot_info &pInfo, bool inPlanner) +{ + struct membufferpp mb; + + int idx = get_plot_details_new(d, &pInfo, time, &mb); + + QPixmap tissues = drawTissues(pInfo, dpr, idx, inPlanner); + std::vector> strings = split_mb_into_strings(mb, fm); + + //TODO: add event tool tips! + //const auto l = scene()->items(pos, Qt::IntersectsItemBoundingRect, Qt::DescendingOrder, + //scene()->views().first()->transform()); + //for (QGraphicsItem *item: l) { + //if (!item->toolTip().isEmpty()) + //addToolTip(item->toolTip(), QPixmap()); + //} + + width = title.size().width(); + for (auto &[s,w]: strings) + width = std::max(width, static_cast(w)); + width += tissues.width(); + width += 6.0 * tooltipBorder; + + height = 4.0 * tooltipBorder + title.height() + + std::max((static_cast(strings.size()) + 1.0) * fontHeight, + static_cast(tissues.height())); + + ChartRectItem::resize(QSizeF(width, height)); + painter->setFont(font); + painter->setPen(QPen(tooltipFontColor)); // QPainter uses QPen to set text color! + double x = 4.0 * tooltipBorder + tissues.width(); + double y = 2.0 * tooltipBorder; + double titleOffset = (width - title.width()) / 2.0; + painter->drawPixmap(lrint(titleOffset), lrint(y), title, 0, 0, title.width(), title.height()); + y += round(fontHeight); + painter->drawPixmap(lrint(2.0 * tooltipBorder), lrint(y), tissues, 0, 0, tissues.width(), tissues.height()); + y += round(fontHeight); + for (auto &[s,w]: strings) { + QRectF rect(x, y, w, fontHeight); + painter->drawText(rect, s); + y += fontHeight; + } + setTextureDirty(); +} diff --git a/profile-widget/tooltipitem.h b/profile-widget/tooltipitem.h new file mode 100644 index 000000000..7a8643dac --- /dev/null +++ b/profile-widget/tooltipitem.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifndef PROFILE_TOOLTIPITEM_H +#define PROFILE_TOOLTIPITEM_H + +#include "qt-quick/chartitem.h" + +#include +#include + +struct dive; +struct plot_info; + +class ToolTipItem : public ChartRectItem { +public: + ToolTipItem(ChartView &view, double dpr); + void update(const dive *d, double dpr, int time, const plot_info &pInfo, bool inPlanner); +private: + QFont font; + QFontMetrics fm; + double fontHeight; + QPixmap title; + double width, height; + + QPixmap stringToPixmap(const QString &s) const; +}; + + +#endif // PROFILE_TOOLTIPITEM diff --git a/profile-widget/zvalues.h b/profile-widget/zvalues.h index 2d3d2989e..a2f16207c 100644 --- a/profile-widget/zvalues.h +++ b/profile-widget/zvalues.h @@ -12,6 +12,7 @@ struct ProfileZValue { enum ZValues { Profile = 0, + ToolTipItem, Count }; }; diff --git a/qt-quick/chartitem.cpp b/qt-quick/chartitem.cpp index 56278ee28..e63303ed1 100644 --- a/qt-quick/chartitem.cpp +++ b/qt-quick/chartitem.cpp @@ -94,6 +94,7 @@ void ChartPixmapItem::resize(QSizeF size) painter->setRenderHint(QPainter::Antialiasing); } rect.setSize(size); + setPositionDirty(); // position includes the size. setTextureDirty(); }