Compare commits

...

36 Commits

Author SHA1 Message Date
Berthold Stoeger
4293f39535 profile: add plan-mode flag to ProfileWidget::plotDive()
The flag is passed down to the ProfileView to give a different
background.

Amazingly, this seems to survive light testing.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
86309374cb profile: remove profilewidget2.cpp
The functionality was ported to QtQuick. Now let's make the
planner work.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
fade8ca95d profile: port context menu to QtQuick
This is for desktop only. We will have to think about what to
do on mobile. Either a "hamburger menu" or a "long click" seem
to be the most reasonable options.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
0bf1d35a4b profile: port event unhiding to QtQuick
This has UI changes:
- The unhiding options are accessed by a field that appears when
  there are hidden events.
- Only event-types of this particular dive are unhidden.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
bd089f3556 profile: don't derive DiveTextItem from QObject
This was used for animations, but these are now controlled
centrally, so no need for the Q_PROPERTY rigmarole.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
5f2c7c6504 profile: remove DiveLineItem
This was an empty wrapper around QGraphicsLineItem.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
376da40e43 profle: port event manipulation to QtQuick
UI change: the context menu is opened when left-clicking on
the event. This is probably more compatible with mobile UI.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
8580364481 profile: port divecomputer menu to QtQuick
This uses QWidgets and therefore will only compile on desktop.
We'll have to see how to integrate that on mobile.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
5a0f6fe84f profile: port basic dive editing to QtQuick
This needed a bit of refactoring of the ChartItem code, because
we have to be signaled on drag start. Currently only one handle
can be selected at a time. This was (implicitly) the case anyway,
as far as I can tell.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:28 +01:00
Berthold Stoeger
3c3997b24e profile: use QString to format profile data
This tries to stay as close as possible to the original.
Ultimately, the whole formatting code should be reworked.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
199cc61e07 profile: convert core/profile.c to C++
This contains mostly string manipulation, so C++ is much more
convenient.

This commit only changes the things that don't compile in C++:
- turn ?alloc/free into std::vector
- use the C++ version of the translation machinery
- add namespace for enum in pl0t_info struct

Contains minor whitespace fixes.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
d1b0bba530 profile: convert the "ruler item" to qt-quick
Code is mostly based on the "tooltip item". The dragging code was
slightly reworked to be more logical. A "disk item" was added for
the handles.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
bbfdf75283 qt-quick: initialize prev and next when adding chart items
The code assumed that when adding chart items to lists, prev
and next are initialized to null. Make this more robust.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
2c87a057cc profile: remove DiveRectItem
This class wasn't used anymore.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
0a7fc9b350 profile: remove DivePixmapItem
After porting the picture-items to qt-quick, all that was left
of DivePixmapItem was an empty hull. Remove it. The only problem
was that the DiveEventItem is not derived from QObject anymore,
so we have to explicitly add the translation functions with the
Q_DECLARE_TR_FUNCTIONS macro.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
605d7bb2c0 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>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
7391c60223 profile: implement animation of the tooltip item
To do so, generalize the animation routine.

This seems to expose a QtQuick bug: we get spurious
hover-events when the tooltip item is updated in the
animation. We have to check for that to prevent
en endless loop (until the user moves the mouse out
of the profile window).

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
944670bc62 profile: show events in ToolTipItem
Reimplement a feature that was lost when porting the ToolTipOtem
to QtQuick. This is a bit of a longer commit, because the icon
of the event is now drawn explicitly, instead of using HTML.
This encompasses a UI change: the icon is now the icon shown
on the profile and not a general "warning" icon.

This commit also fixes update of the tooltip when panning the
profile.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
3d9e021d8d profile: remove QList<> of DiveEventItems by std::vector
Turn the raw pointers into unique_ptrs to simplify memory
management.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
2aa1a95bde profile: remove old tooltipitem code
The not-yet implemented parts in the qt-quick port were taken over
as comments, so no need to keep these files around.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
489aafe4b3 profile: activate dragging of tooltipitem
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
ea96b9909f profile: move dragging code from stats to general qt-code
So far, the only dragable item was the legend in the statistics
code. On the profile, we will have multiple dragable items.

Therefore, move the code up and make it more general. This
took some reorganization. Now, the size of the ChartItem
is saved in the base class. Also, setPos() became a virtual
function.

The dragable items are kept as an unsorted list.
If there will be many of them, this should be changed to
some sort of sorted list (maybe quadtree?).

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
5f9c2b2d04 profile: first rudimentary port of the ToolTipItem to qt-quick
Still behaves weirdly when panning the chart.

No support for moving the ToolTipItem.

Doesn't add information on bookmarks under the mouse cursor.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
9a418401fa dummy: invert order of files in Subsurface.pro for testing
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
0ead02dce1 profile: Unify desktop and mobile widgets
This breaks DPR handling, but it is a start.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
0ede842a43 profile: move zooming/panning code to ProfileView
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
4b801de88a profile: replot profile when settings changed
It would be nice to have a single "any setting changed" signal and
not to have to listen to all of them individually...

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
0346bc13dc profile: move axis animation code to ProfileView
This feels quite a bit slower than the non-QtQuick version. This
makes sense, as there is an additional level of indirection. Instead
of painting directly, we paint into an QImage and turn that into
a QSGTexture.

Ultimately one would think that we should render directly using
QtQuick. Alas, we can't, since that would mean no more printing/
exporting of profiles. How sad.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
ed1604087f profile: read DPR from QML page
This is needed on mobile, where some screens have a high DPR.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
584d835693 profile: render profile on desktop via QtQuick
This breaks all dynamic features, including animations,
zooming tooltips, planner-handles, etc. They will have to be
converted one-by-one to QtQuick, which will be a major pain,
as the ProfileView is destroyed by Qt6 on reparenting.
This means that the view cannot store any persistent state.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 15:20:27 +01:00
Berthold Stoeger
cc081fd414 cleanup: fix include guards for stats/zvalues.h
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 14:19:46 +01:00
Berthold Stoeger
78f1e8b513 stats: break out common QtQuick part of the code
Move most of the QtQuick code to its own directory, so that it
can be reused in the future for the chart.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 14:19:46 +01:00
Berthold Stoeger
9d4d35976b stats: make number of z-levels dynamic
Other users of the qtquick code might have different needs
for z-levels.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 14:19:46 +01:00
Berthold Stoeger
db4108df6d stats: don't pass StatsTheme in ChartItem::render
To make the qt-quick code of the statistics module more general,
don't pass the StatsTheme in render calls to ChartItems. Items
that need it, may just store a reference to the theme.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 14:19:46 +01:00
Berthold Stoeger
e0f71f1a49 tests: set locale in testprofile.cpp to "C"
In locales using comma as decimal separator the test fails.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 14:19:46 +01:00
Berthold Stoeger
405399a091 stats: remove theme-access from QtQuick root node
To make the QtQuick code more general, remove the access to the
StatsTheme, which only makes sense for the statistics module.

Store the background color in a separate variable, since that
will be needed by any potential users of the code.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-03-30 14:19:46 +01:00
99 changed files with 4481 additions and 4186 deletions

View File

@ -395,9 +395,9 @@ if (NOT SUBSURFACE_TARGET_EXECUTABLE MATCHES "DownloaderExecutable")
qt_add_resources(SUBSURFACE_RESOURCES map-widget/qml/map-widget.qrc) qt_add_resources(SUBSURFACE_RESOURCES map-widget/qml/map-widget.qrc)
set(SUBSURFACE_MAPWIDGET subsurface_mapwidget) set(SUBSURFACE_MAPWIDGET subsurface_mapwidget)
endif() endif()
qt_add_resources(SUBSURFACE_RESOURCES subsurface.qrc profile.qrc stats/statsicons.qrc desktop-widgets/qml/statsview2.qrc) qt_add_resources(SUBSURFACE_RESOURCES subsurface.qrc profile.qrc stats/statsicons.qrc desktop-widgets/qml/statsview2.qrc desktop-widgets/qml/profileview.qrc)
else() else()
qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc profile.qrc stats/statsicons.qrc map-widget/qml/map-widget.qrc desktop-widgets/qml/statsview2.qrc) qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc profile.qrc stats/statsicons.qrc map-widget/qml/map-widget.qrc desktop-widgets/qml/statsview2.qrc desktop-widgets/qml/profileview.qrc)
set(SUBSURFACE_MAPWIDGET subsurface_mapwidget) set(SUBSURFACE_MAPWIDGET subsurface_mapwidget)
endif() endif()
endif() endif()
@ -420,6 +420,7 @@ if(MAPSUPPORT)
add_subdirectory(map-widget) add_subdirectory(map-widget)
endif() endif()
add_subdirectory(mobile-widgets) add_subdirectory(mobile-widgets)
add_subdirectory(qt-quick)
add_subdirectory(stats) add_subdirectory(stats)
endif() endif()
add_subdirectory(backend-shared) add_subdirectory(backend-shared)
@ -473,6 +474,7 @@ if (SUBSURFACE_TARGET_EXECUTABLE MATCHES "MobileExecutable")
subsurface_commands subsurface_commands
subsurface_corelib subsurface_corelib
subsurface_stats subsurface_stats
subsurface_qtquick
kirigamiplugin kirigamiplugin
${SUBSURFACE_LINK_LIBRARIES} ${SUBSURFACE_LINK_LIBRARIES}
) )
@ -499,6 +501,7 @@ elseif (SUBSURFACE_TARGET_EXECUTABLE MATCHES "DesktopExecutable")
subsurface_commands subsurface_commands
subsurface_corelib subsurface_corelib
subsurface_stats subsurface_stats
subsurface_qtquick
${SUBSURFACE_LINK_LIBRARIES} ${SUBSURFACE_LINK_LIBRARIES}
) )
add_dependencies(subsurface_desktop_preferences subsurface_generated_ui) add_dependencies(subsurface_desktop_preferences subsurface_generated_ui)

View File

@ -45,7 +45,7 @@ SOURCES += subsurface-mobile-main.cpp \
core/fulltext.cpp \ core/fulltext.cpp \
core/subsurfacestartup.cpp \ core/subsurfacestartup.cpp \
core/pref.c \ core/pref.c \
core/profile.c \ core/profile.cpp \
core/device.cpp \ core/device.cpp \
core/dive.cpp \ core/dive.cpp \
core/divecomputer.c \ core/divecomputer.c \
@ -126,7 +126,6 @@ SOURCES += subsurface-mobile-main.cpp \
core/subsurface-qt/divelistnotifier.cpp \ core/subsurface-qt/divelistnotifier.cpp \
backend-shared/exportfuncs.cpp \ backend-shared/exportfuncs.cpp \
backend-shared/plannershared.cpp \ backend-shared/plannershared.cpp \
backend-shared/roundrectitem.cpp \
stats/statsvariables.cpp \ stats/statsvariables.cpp \
stats/statsview.cpp \ stats/statsview.cpp \
stats/barseries.cpp \ stats/barseries.cpp \
@ -167,7 +166,6 @@ SOURCES += subsurface-mobile-main.cpp \
qt-models/weightsysteminfomodel.cpp \ qt-models/weightsysteminfomodel.cpp \
qt-models/filterconstraintmodel.cpp \ qt-models/filterconstraintmodel.cpp \
qt-models/filterpresetmodel.cpp \ qt-models/filterpresetmodel.cpp \
profile-widget/qmlprofile.cpp \
profile-widget/divecartesianaxis.cpp \ profile-widget/divecartesianaxis.cpp \
profile-widget/diveeventitem.cpp \ profile-widget/diveeventitem.cpp \
profile-widget/divepercentageitem.cpp \ profile-widget/divepercentageitem.cpp \
@ -175,12 +173,14 @@ SOURCES += subsurface-mobile-main.cpp \
profile-widget/profilescene.cpp \ profile-widget/profilescene.cpp \
profile-widget/animationfunctions.cpp \ profile-widget/animationfunctions.cpp \
profile-widget/divepixmapcache.cpp \ profile-widget/divepixmapcache.cpp \
profile-widget/divepixmapitem.cpp \ profile-widget/pictureitem.cpp \
profile-widget/divetooltipitem.cpp \ profile-widget/tooltipitem.cpp \
profile-widget/tankitem.cpp \ profile-widget/divetextitem.cpp \
profile-widget/divelineitem.cpp \ profile-widget/handleitem.cpp \
profile-widget/diverectitem.cpp \ profile-widget/profileview.cpp \
profile-widget/divetextitem.cpp profile-widget/ruleritem.cpp \
qt-quick/chartitem.cpp \
qt-quick/chartview.cpp
HEADERS += \ HEADERS += \
commands/command_base.h \ commands/command_base.h \
@ -282,7 +282,6 @@ HEADERS += \
core/subsurface-qt/divelistnotifier.h \ core/subsurface-qt/divelistnotifier.h \
backend-shared/exportfuncs.h \ backend-shared/exportfuncs.h \
backend-shared/plannershared.h \ backend-shared/plannershared.h \
backend-shared/roundrectitem.h \
stats/barseries.h \ stats/barseries.h \
stats/boxseries.h \ stats/boxseries.h \
stats/chartitem.h \ stats/chartitem.h \
@ -327,20 +326,24 @@ HEADERS += \
qt-models/weightsysteminfomodel.h \ qt-models/weightsysteminfomodel.h \
qt-models/filterconstraintmodel.h \ qt-models/filterconstraintmodel.h \
qt-models/filterpresetmodel.h \ qt-models/filterpresetmodel.h \
profile-widget/qmlprofile.h \
profile-widget/divepercentageitem.h \ profile-widget/divepercentageitem.h \
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/divetooltipitem.h \ profile-widget/pictureitem.h \
profile-widget/tankitem.h \ profile-widget/tooltipitem.h \
profile-widget/animationfunctions.h \ profile-widget/animationfunctions.h \
profile-widget/divecartesianaxis.h \ profile-widget/divecartesianaxis.h \
profile-widget/divelineitem.h \
profile-widget/divepixmapcache.h \ profile-widget/divepixmapcache.h \
profile-widget/divepixmapitem.h \ profile-widget/divetextitem.h \
profile-widget/diverectitem.h \ profile-widget/handleitem.h \
profile-widget/divetextitem.h profile-widget/profileview.h \
profile-widget/ruleritem.h \
profile-widget/profiletranslations.h \
qt-quick/chartitem.h \
qt-quick/chartitemhelper.h \
qt-quick/chartitem_ptr.h \
qt-quick/chartview.h
RESOURCES += mobile-widgets/qml/mobile-resources.qrc \ RESOURCES += mobile-widgets/qml/mobile-resources.qrc \
mobile-widgets/3rdparty/icons.qrc \ mobile-widgets/3rdparty/icons.qrc \

View File

@ -5,8 +5,6 @@ set(BACKEND_SRCS
exportfuncs.h exportfuncs.h
plannershared.cpp plannershared.cpp
plannershared.h plannershared.h
roundrectitem.cpp
roundrectitem.h
) )
add_library(subsurface_backend_shared STATIC ${BACKEND_SRCS}) add_library(subsurface_backend_shared STATIC ${BACKEND_SRCS})

View File

@ -1,22 +0,0 @@
#include "roundrectitem.h"
#include <QPainter>
#include <QStyleOptionGraphicsItem>
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();
}

View File

@ -1,16 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef ROUNDRECTITEM_H
#define ROUNDRECTITEM_H
#include <QGraphicsRectItem>
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

View File

@ -146,7 +146,7 @@ set(SUBSURFACE_CORE_LIB_SRCS
plannernotes.c plannernotes.c
pref.h pref.h
pref.c pref.c
profile.c profile.cpp
profile.h profile.h
qt-gui.h qt-gui.h
qt-init.cpp qt-init.cpp

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "event.h" #include "event.h"
#include "eventtype.h" #include "eventtype.h"
#include "divecomputer.h"
#include "subsurface-string.h" #include "subsurface-string.h"
#include <string.h> #include <string.h>
@ -116,3 +117,12 @@ extern enum event_severity get_event_severity(const struct event *ev)
return EVENT_SEVERITY_NONE; return EVENT_SEVERITY_NONE;
} }
} }
extern bool has_individually_hidden_events(const struct divecomputer *dc)
{
for (const struct event *ev = dc->events; ev; ev = ev->next) {
if (ev->hidden)
return true;
}
return false;
}

View File

@ -8,6 +8,8 @@
#include <libdivecomputer/parser.h> #include <libdivecomputer/parser.h>
struct divecomputer;
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@ -55,6 +57,7 @@ extern struct event *create_event(unsigned int time, int type, int flags, int va
extern struct event *clone_event_rename(const struct event *ev, const char *name); extern struct event *clone_event_rename(const struct event *ev, const char *name);
extern bool same_event(const struct event *a, const struct event *b); extern bool same_event(const struct event *a, const struct event *b);
extern enum event_severity get_event_severity(const struct event *ev); extern enum event_severity get_event_severity(const struct event *ev);
extern bool has_individually_hidden_events(const struct divecomputer *dc);
/* Since C doesn't have parameter-based overloading, two versions of get_next_event. */ /* Since C doesn't have parameter-based overloading, two versions of get_next_event. */
extern const struct event *get_next_event(const struct event *event, const char *name); extern const struct event *get_next_event(const struct event *event, const char *name);

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "eventtype.h" #include "eventtype.h"
#include "event.h" #include "event.h"
#include "divecomputer.h"
#include "gettextfromc.h" #include "gettextfromc.h"
#include "subsurface-string.h" #include "subsurface-string.h"
@ -55,10 +56,22 @@ extern "C" void hide_event_type(const struct event *ev)
it->plot = false; it->plot = false;
} }
extern "C" void show_all_event_types() static bool has_event_type(const divecomputer *dc, const event_type et)
{ {
for (event_type &e: event_types) for (const event *ev = dc->events; ev; ev = ev->next) {
if (ev->name == et.name &&
get_event_severity(ev) == et.severity)
return true;
}
return false;
}
extern "C" void show_all_event_types(const divecomputer *dc)
{
for (event_type &e: event_types) {
if (!e.plot && has_event_type(dc, e))
e.plot = true; e.plot = true;
}
} }
extern "C" void show_event_type(int idx) extern "C" void show_event_type(int idx)
@ -68,17 +81,12 @@ extern "C" void show_event_type(int idx)
event_types[idx].plot = true; event_types[idx].plot = true;
} }
extern "C" bool any_event_types_hidden() extern std::vector<int> hidden_event_types(const divecomputer *dc)
{
return std::any_of(event_types.begin(), event_types.end(),
[] (const event_type &e) { return !e.plot; });
}
extern std::vector<int> hidden_event_types()
{ {
std::vector<int> res; std::vector<int> res;
for (size_t i = 0; i < event_types.size(); ++i) { for (size_t i = 0; i < event_types.size(); ++i) {
if (!event_types[i].plot) const event_type &e = event_types[i];
if (!e.plot && has_event_type(dc, e))
res.push_back(i); res.push_back(i);
} }
return res; return res;

View File

@ -3,6 +3,8 @@
#ifndef EVENTNAME_H #ifndef EVENTNAME_H
#define EVENTNAME_H #define EVENTNAME_H
struct divecomputer;
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@ -11,9 +13,8 @@ extern void clear_event_types(void);
extern void remember_event_type(const struct event *ev); extern void remember_event_type(const struct event *ev);
extern bool is_event_type_hidden(const struct event *ev); extern bool is_event_type_hidden(const struct event *ev);
extern void hide_event_type(const struct event *ev); extern void hide_event_type(const struct event *ev);
extern void show_all_event_types(); extern void show_all_event_types(const struct divecomputer *dc);
extern void show_event_type(int idx); extern void show_event_type(int idx);
extern bool any_event_types_hidden();
#ifdef __cplusplus #ifdef __cplusplus
} }
@ -22,7 +23,7 @@ extern bool any_event_types_hidden();
#include <vector> #include <vector>
#include <QString> #include <QString>
extern std::vector<int> hidden_event_types(); extern std::vector<int> hidden_event_types(const divecomputer *dc);
QString event_type_name(const event *ev); QString event_type_name(const event *ev);
QString event_type_name(int idx); QString event_type_name(int idx);

View File

@ -117,7 +117,7 @@ int pscr_o2(const double amb_pressure, struct gasmix mix)
* *pressures = structure for communicating o2 sensor values from and gas pressures to the calling function. * *pressures = structure for communicating o2 sensor values from and gas pressures to the calling function.
* *mix = structure containing cylinder gas mixture information. * *mix = structure containing cylinder gas mixture information.
* divemode = the dive mode pertaining to this point in the dive profile. * divemode = the dive mode pertaining to this point in the dive profile.
* This function called by: calculate_gas_information_new() in profile.c; add_segment() in deco.c. * This function called by: calculate_gas_information_new() in profile.cpp; add_segment() in deco.c.
*/ */
void fill_pressures(struct gas_pressures *pressures, const double amb_pressure, struct gasmix mix, double po2, enum divemode_t divemode) void fill_pressures(struct gas_pressures *pressures, const double amb_pressure, struct gasmix mix, double po2, enum divemode_t divemode)
{ {

View File

@ -18,6 +18,16 @@ enum gas_component { N2, HE, O2 };
struct gasmix { struct gasmix {
fraction_t o2; fraction_t o2;
fraction_t he; fraction_t he;
#ifdef __cplusplus
bool operator==(const gasmix &g2) const
{
return o2.permille == g2.o2.permille && he.permille == g2.he.permille;
}
bool operator!=(const gasmix &g2) const
{
return !(*this == g2);
}
#endif
}; };
static const struct gasmix gasmix_invalid = { { -1 }, { -1 } }; static const struct gasmix gasmix_invalid = { { -1 }, { -1 } };
static const struct gasmix gasmix_air = { { 0 }, { 0 } }; static const struct gasmix gasmix_air = { { 0 }, { 0 } };

View File

@ -2,7 +2,7 @@
/* gaspressures.c /* gaspressures.c
* --------------- * ---------------
* This file contains the routines to calculate the gas pressures in the cylinders. * This file contains the routines to calculate the gas pressures in the cylinders.
* The functions below support the code in profile.c. * The functions below support the code in profile.cpp.
* The high-level function is populate_pressure_information(), called by function * The high-level function is populate_pressure_information(), called by function
* create_plot_info_new() in profile.c. The other functions below are, in turn, * create_plot_info_new() in profile.c. The other functions below are, in turn,
* called by populate_pressure_information(). The calling sequence is as follows: * called by populate_pressure_information(). The calling sequence is as follows:

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
/* profile.c */ /* profile.cpp */
/* creates all the necessary data for drawing the dive profile /* creates all the necessary data for drawing the dive profile
*/ */
#include "ssrf.h" #include "ssrf.h"
@ -8,6 +8,7 @@
#include <string.h> #include <string.h>
#include <assert.h> #include <assert.h>
#include <stdlib.h> #include <stdlib.h>
#include <vector>
#include "dive.h" #include "dive.h"
#include "divelist.h" #include "divelist.h"
@ -22,7 +23,6 @@
#include "errorhelper.h" #include "errorhelper.h"
#include "libdivecomputer/parser.h" #include "libdivecomputer/parser.h"
#include "libdivecomputer/version.h" #include "libdivecomputer/version.h"
#include "membuffer.h"
#include "qthelper.h" #include "qthelper.h"
#include "format.h" #include "format.h"
@ -30,7 +30,7 @@
#define MAX_PROFILE_DECO 7200 #define MAX_PROFILE_DECO 7200
extern int ascent_velocity(int depth, int avg_depth, int bottom_time); extern "C" int ascent_velocity(int depth, int avg_depth, int bottom_time);
#ifdef DEBUG_PI #ifdef DEBUG_PI
/* debugging tool - not normally used */ /* debugging tool - not normally used */
@ -57,8 +57,18 @@ static void dump_pi(struct plot_info *pi)
} }
#endif #endif
#define ROUND_UP(x, y) ((((x) + (y) - 1) / (y)) * (y))
#define DIV_UP(x, y) (((x) + (y) - 1) / (y)) template<typename T>
static T round_up(T x, T y)
{
return ((x + y - 1) / y) * y;
}
template<typename T>
static T div_up(T x, T y)
{
return (x + y - 1) / y;
}
/* /*
* When showing dive profiles, we scale things to the * When showing dive profiles, we scale things to the
@ -66,6 +76,7 @@ static void dump_pi(struct plot_info *pi)
* 30 minutes or 90 ft, just so that small dives show * 30 minutes or 90 ft, just so that small dives show
* up as such unless zoom is enabled. * up as such unless zoom is enabled.
*/ */
extern "C"
int get_maxtime(const struct plot_info *pi) int get_maxtime(const struct plot_info *pi)
{ {
int seconds = pi->maxtime; int seconds = pi->maxtime;
@ -74,6 +85,7 @@ int get_maxtime(const struct plot_info *pi)
} }
/* get the maximum depth to which we want to plot */ /* get the maximum depth to which we want to plot */
extern "C"
int get_maxdepth(const struct plot_info *pi) int get_maxdepth(const struct plot_info *pi)
{ {
/* 3m to spare */ /* 3m to spare */
@ -185,6 +197,7 @@ static void analyze_plot_info(struct plot_info *pi)
* Some dive computers give cylinder indices, some * Some dive computers give cylinder indices, some
* give just the gas mix. * give just the gas mix.
*/ */
extern "C"
int get_cylinder_index(const struct dive *dive, const struct event *ev) int get_cylinder_index(const struct dive *dive, const struct event *ev)
{ {
int best; int best;
@ -206,6 +219,7 @@ int get_cylinder_index(const struct dive *dive, const struct event *ev)
return best < 0 ? 0 : best; return best < 0 ? 0 : best;
} }
extern "C"
struct event *get_next_event_mutable(struct event *event, const char *name) struct event *get_next_event_mutable(struct event *event, const char *name)
{ {
if (!name || !*name) if (!name || !*name)
@ -218,6 +232,7 @@ struct event *get_next_event_mutable(struct event *event, const char *name)
return event; return event;
} }
extern "C"
const struct event *get_next_event(const struct event *event, const char *name) const struct event *get_next_event(const struct event *event, const char *name)
{ {
return get_next_event_mutable((struct event *)event, name); return get_next_event_mutable((struct event *)event, name);
@ -246,9 +261,8 @@ static int set_setpoint(struct plot_info *pi, int i, int setpoint, int end)
return i; return i;
} }
static void check_setpoint_events(const struct dive *dive, const struct divecomputer *dc, struct plot_info *pi) static void check_setpoint_events(const struct dive *, const struct divecomputer *dc, struct plot_info *pi)
{ {
UNUSED(dive);
int i = 0; int i = 0;
pressure_t setpoint; pressure_t setpoint;
setpoint.mbar = 0; setpoint.mbar = 0;
@ -381,6 +395,7 @@ static void insert_entry(struct plot_info *pi, int idx, int time, int depth, int
entry->bearing = -1; entry->bearing = -1;
} }
extern "C"
void free_plot_info_data(struct plot_info *pi) void free_plot_info_data(struct plot_info *pi)
{ {
free(pi->entry); free(pi->entry);
@ -390,7 +405,6 @@ void free_plot_info_data(struct plot_info *pi)
static void populate_plot_entries(const struct dive *dive, const struct divecomputer *dc, struct plot_info *pi) static void populate_plot_entries(const struct dive *dive, const struct divecomputer *dc, struct plot_info *pi)
{ {
UNUSED(dive);
int idx, maxtime, nr, i; int idx, maxtime, nr, i;
int lastdepth, lasttime, lasttemp = 0; int lastdepth, lasttime, lasttemp = 0;
struct plot_data *plot_data; struct plot_data *plot_data;
@ -407,10 +421,10 @@ static void populate_plot_entries(const struct dive *dive, const struct divecomp
* past "maxtime" in the original sample data) * past "maxtime" in the original sample data)
*/ */
nr = dc->samples + 6 + maxtime / 10 + count_events(dc); nr = dc->samples + 6 + maxtime / 10 + count_events(dc);
plot_data = calloc(nr, sizeof(struct plot_data)); plot_data = (struct plot_data *)calloc(nr, sizeof(struct plot_data));
pi->entry = plot_data; pi->entry = plot_data;
pi->nr_cylinders = dive->cylinders.nr; pi->nr_cylinders = dive->cylinders.nr;
pi->pressures = calloc(nr * (size_t)pi->nr_cylinders, sizeof(struct plot_pressure_data)); pi->pressures = (plot_pressure_data *)calloc(nr * (size_t)pi->nr_cylinders, sizeof(struct plot_pressure_data));
if (!plot_data) if (!plot_data)
return; return;
pi->nr = nr; pi->nr = nr;
@ -532,7 +546,7 @@ static void populate_plot_entries(const struct dive *dive, const struct divecomp
* *
* Everything in between has a cylinder pressure for at least some of the cylinders. * Everything in between has a cylinder pressure for at least some of the cylinders.
*/ */
static int sac_between(const struct dive *dive, struct plot_info *pi, int first, int last, const bool gases[]) static int sac_between(const struct dive *dive, struct plot_info *pi, int first, int last, const std::vector<char> &gases)
{ {
int i, airuse; int i, airuse;
double pressuretime; double pressuretime;
@ -580,7 +594,7 @@ static int sac_between(const struct dive *dive, struct plot_info *pi, int first,
} }
/* Is there pressure data for all gases? */ /* Is there pressure data for all gases? */
static bool all_pressures(struct plot_info *pi, int idx, const bool gases[]) static bool all_pressures(struct plot_info *pi, int idx, const std::vector<char> &gases)
{ {
int i; int i;
@ -593,7 +607,7 @@ static bool all_pressures(struct plot_info *pi, int idx, const bool gases[])
} }
/* Which of the set of gases have pressure data? Returns false if none of them. */ /* Which of the set of gases have pressure data? Returns false if none of them. */
static bool filter_pressures(struct plot_info *pi, int idx, const bool gases_in[], bool gases_out[]) static bool filter_pressures(struct plot_info *pi, int idx, const std::vector<char> &gases_in, std::vector<char> &gases_out)
{ {
int i; int i;
bool has_pressure = false; bool has_pressure = false;
@ -612,7 +626,7 @@ static bool filter_pressures(struct plot_info *pi, int idx, const bool gases_in[
* an array of gases, the caller passes in scratch memory in the last * an array of gases, the caller passes in scratch memory in the last
* argument. * argument.
*/ */
static void fill_sac(const struct dive *dive, struct plot_info *pi, int idx, const bool gases_in[], bool gases[]) static void fill_sac(const struct dive *dive, struct plot_info *pi, int idx, const std::vector<char> &gases_in, std::vector<char> &gases)
{ {
struct plot_data *entry = pi->entry + idx; struct plot_data *entry = pi->entry + idx;
int first, last; int first, last;
@ -670,7 +684,7 @@ static void fill_sac(const struct dive *dive, struct plot_info *pi, int idx, con
/* /*
* Create a bitmap of cylinders that match our current gasmix * Create a bitmap of cylinders that match our current gasmix
*/ */
static void matching_gases(const struct dive *dive, struct gasmix gasmix, bool gases[]) static void matching_gases(const struct dive *dive, struct gasmix gasmix, std::vector<char> &gases)
{ {
int i; int i;
@ -682,13 +696,14 @@ static void calculate_sac(const struct dive *dive, const struct divecomputer *dc
{ {
struct gasmix gasmix = gasmix_invalid; struct gasmix gasmix = gasmix_invalid;
const struct event *ev = NULL; const struct event *ev = NULL;
bool *gases, *gases_scratch;
gases = calloc(pi->nr_cylinders, sizeof(*gases)); // Use std::vector<char> instead of std::vector<bool> to avoid silly "optimization"
// by the standard library.
std::vector<char> gases(pi->nr_cylinders, 0);
/* This might be premature optimization, but let's allocate the gas array for /* This might be premature optimization, but let's allocate the gas array for
* the fill_sac function only once an not once per sample */ * the fill_sac function only once an not once per sample */
gases_scratch = malloc(pi->nr_cylinders * sizeof(*gases)); std::vector<char>gases_scratch(pi->nr_cylinders, 0);
for (int i = 0; i < pi->nr; i++) { for (int i = 0; i < pi->nr; i++) {
struct plot_data *entry = pi->entry + i; struct plot_data *entry = pi->entry + i;
@ -700,18 +715,17 @@ static void calculate_sac(const struct dive *dive, const struct divecomputer *dc
fill_sac(dive, pi, i, gases, gases_scratch); fill_sac(dive, pi, i, gases, gases_scratch);
} }
free(gases);
free(gases_scratch);
} }
static void populate_secondary_sensor_data(const struct divecomputer *dc, struct plot_info *pi) static void populate_secondary_sensor_data(const struct divecomputer *dc, struct plot_info *pi)
{ {
int *seen = calloc(pi->nr_cylinders, sizeof(*seen)); std::vector<int> seen(pi->nr_cylinders, 0);
for (int idx = 0; idx < pi->nr; ++idx) for (int idx = 0; idx < pi->nr; ++idx) {
for (int c = 0; c < pi->nr_cylinders; ++c) for (int c = 0; c < pi->nr_cylinders; ++c) {
if (get_plot_pressure_data(pi, idx, SENSOR_PR, c)) if (get_plot_pressure_data(pi, idx, SENSOR_PR, c))
++seen[c]; // Count instances so we can differentiate a real sensor from just start and end pressure ++seen[c]; // Count instances so we can differentiate a real sensor from just start and end pressure
}
}
int idx = 0; int idx = 0;
/* We should try to see if it has interesting pressure data here */ /* We should try to see if it has interesting pressure data here */
for (int i = 0; i < dc->samples && idx < pi->nr; i++) { for (int i = 0; i < dc->samples && idx < pi->nr; i++) {
@ -727,7 +741,6 @@ static void populate_secondary_sensor_data(const struct divecomputer *dc, struct
if (sample->sensor[s] != NO_SENSOR && seen[sample->sensor[s]] < 3 && sample->pressure[s].mbar) if (sample->sensor[s] != NO_SENSOR && seen[sample->sensor[s]] < 3 && sample->pressure[s].mbar)
set_plot_pressure_data(pi, idx, SENSOR_PR, sample->sensor[s], sample->pressure[s].mbar); set_plot_pressure_data(pi, idx, SENSOR_PR, sample->sensor[s], sample->pressure[s].mbar);
} }
free(seen);
} }
/* /*
@ -754,16 +767,11 @@ static void setup_gas_sensor_pressure(const struct dive *dive, const struct dive
/* FIXME: The planner uses a dummy one-past-end cylinder for surface air! */ /* FIXME: The planner uses a dummy one-past-end cylinder for surface air! */
int num_cyl = pi->nr_cylinders + 1; int num_cyl = pi->nr_cylinders + 1;
int *seen = malloc(num_cyl * sizeof(*seen)); std::vector<int> seen (num_cyl, 0);
int *first = malloc(num_cyl * sizeof(*first)); std::vector<int> first(num_cyl, 0);
int *last = malloc(num_cyl * sizeof(*last)); std::vector<int> last(num_cyl, INT_MAX);
const struct divecomputer *secondary; const struct divecomputer *secondary;
for (i = 0; i < num_cyl; i++) {
seen[i] = 0;
first[i] = 0;
last[i] = INT_MAX;
}
prev = explicit_first_cylinder(dive, dc); prev = explicit_first_cylinder(dive, dc);
seen[prev] = 1; seen[prev] = 1;
@ -841,10 +849,6 @@ static void setup_gas_sensor_pressure(const struct dive *dive, const struct dive
continue; continue;
populate_secondary_sensor_data(secondary, pi); populate_secondary_sensor_data(secondary, pi);
} while ((secondary = secondary->next) != NULL); } while ((secondary = secondary->next) != NULL);
free(seen);
free(first);
free(last);
} }
/* calculate DECO STOP / TTS / NDL */ /* calculate DECO STOP / TTS / NDL */
@ -859,7 +863,7 @@ static void calculate_ndl_tts(struct deco_state *ds, const struct dive *dive, st
const int time_stepsize = 60; const int time_stepsize = 60;
const int deco_stepsize = M_OR_FT(3, 10); const int deco_stepsize = M_OR_FT(3, 10);
/* at what depth is the current deco-step? */ /* at what depth is the current deco-step? */
int next_stop = ROUND_UP(deco_allowed_depth( int next_stop = round_up(deco_allowed_depth(
tissue_tolerance_calc(ds, dive, depth_to_bar(entry->depth, dive), in_planner), tissue_tolerance_calc(ds, dive, depth_to_bar(entry->depth, dive), in_planner),
surface_pressure, dive, 1), deco_stepsize); surface_pressure, dive, 1), deco_stepsize);
int ascent_depth = entry->depth; int ascent_depth = entry->depth;
@ -894,7 +898,7 @@ static void calculate_ndl_tts(struct deco_state *ds, const struct dive *dive, st
for (; ascent_depth > next_stop; ascent_depth -= ascent_s_per_step * ascent_velocity(ascent_depth, entry->running_sum / entry->sec, 0), entry->tts_calc += ascent_s_per_step) { for (; ascent_depth > next_stop; ascent_depth -= ascent_s_per_step * ascent_velocity(ascent_depth, entry->running_sum / entry->sec, 0), entry->tts_calc += ascent_s_per_step) {
add_segment(ds, depth_to_bar(ascent_depth, dive), add_segment(ds, depth_to_bar(ascent_depth, dive),
gasmix, ascent_s_per_step, entry->o2pressure.mbar, divemode, prefs.decosac, in_planner); gasmix, ascent_s_per_step, entry->o2pressure.mbar, divemode, prefs.decosac, in_planner);
next_stop = ROUND_UP(deco_allowed_depth(tissue_tolerance_calc(ds, dive, depth_to_bar(ascent_depth, dive), in_planner), next_stop = round_up(deco_allowed_depth(tissue_tolerance_calc(ds, dive, depth_to_bar(ascent_depth, dive), in_planner),
surface_pressure, dive, 1), deco_stepsize); surface_pressure, dive, 1), deco_stepsize);
} }
ascent_depth = next_stop; ascent_depth = next_stop;
@ -1016,7 +1020,7 @@ static void calculate_deco_information(struct deco_state *ds, const struct deco_
vpmb_start_gradient(ds); vpmb_start_gradient(ds);
/* For CVA calculations, deco time = dive time remaining is a good guess, /* For CVA calculations, deco time = dive time remaining is a good guess,
but we want to over-estimate deco_time for the first iteration so it but we want to over-estimate deco_time for the first iteration so it
converges correctly, so add 30min*/ converges correctly, so add 30min */
if (!in_planner) if (!in_planner)
ds->deco_time = pi->maxtime - t1 + 1800; ds->deco_time = pi->maxtime - t1 + 1800;
vpmb_next_gradient(ds, ds->deco_time, surface_pressure / 1000.0, in_planner); vpmb_next_gradient(ds, ds->deco_time, surface_pressure / 1000.0, in_planner);
@ -1058,7 +1062,7 @@ static void calculate_deco_information(struct deco_state *ds, const struct deco_
entry->depth < max_ceiling - 100 && entry->sec > 0) { entry->depth < max_ceiling - 100 && entry->sec > 0) {
struct dive *non_const_dive = (struct dive *)dive; // cast away const! struct dive *non_const_dive = (struct dive *)dive; // cast away const!
add_event(&non_const_dive->dc, entry->sec, SAMPLE_EVENT_CEILING, -1, max_ceiling / 1000, add_event(&non_const_dive->dc, entry->sec, SAMPLE_EVENT_CEILING, -1, max_ceiling / 1000,
translate("gettextFromC", "planned waypoint above ceiling")); qPrintable(gettextFromC::tr("planned waypoint above ceiling")));
pi->waypoint_above_ceiling = true; pi->waypoint_above_ceiling = true;
} }
@ -1101,7 +1105,7 @@ static void calculate_deco_information(struct deco_state *ds, const struct deco_
* at end of whole minute after clearing ceiling. The deepest ceiling when planning a dive * at end of whole minute after clearing ceiling. The deepest ceiling when planning a dive
* comes typically 10-60s after the end of the bottom time, so add 20s to the calculated * comes typically 10-60s after the end of the bottom time, so add 20s to the calculated
* deco time. */ * deco time. */
ds->deco_time = ROUND_UP(time_clear_ceiling - time_deep_ceiling + 20, 60) + 20; ds->deco_time = round_up(time_clear_ceiling - time_deep_ceiling + 20, 60) + 20;
vpmb_next_gradient(ds, ds->deco_time, surface_pressure / 1000.0, in_planner); vpmb_next_gradient(ds, ds->deco_time, surface_pressure / 1000.0, in_planner);
final_tts = 0; final_tts = 0;
last_ndl_tts_calc_time = 0; last_ndl_tts_calc_time = 0;
@ -1308,6 +1312,7 @@ static void debug_print_profiledata(struct plot_info *pi)
/* /*
* Initialize a plot_info structure to all-zeroes * Initialize a plot_info structure to all-zeroes
*/ */
extern "C"
void init_plot_info(struct plot_info *pi) void init_plot_info(struct plot_info *pi)
{ {
memset(pi, 0, sizeof(*pi)); memset(pi, 0, sizeof(*pi));
@ -1323,6 +1328,7 @@ void init_plot_info(struct plot_info *pi)
* The old data will be freed. Before the first call, the plot * The old data will be freed. Before the first call, the plot
* info must be initialized with init_plot_info(). * info must be initialized with init_plot_info().
*/ */
extern "C"
void create_plot_info_new(const struct dive *dive, const struct divecomputer *dc, struct plot_info *pi, const struct deco_state *planner_ds) void create_plot_info_new(const struct dive *dive, const struct divecomputer *dc, struct plot_info *pi, const struct deco_state *planner_ds)
{ {
int o2, he, o2max; int o2, he, o2max;
@ -1333,14 +1339,14 @@ void create_plot_info_new(const struct dive *dive, const struct divecomputer *dc
calculate_max_limits_new(dive, dc, pi, in_planner); calculate_max_limits_new(dive, dc, pi, in_planner);
get_dive_gas(dive, &o2, &he, &o2max); get_dive_gas(dive, &o2, &he, &o2max);
if (dc->divemode == FREEDIVE) { if (dc->divemode == FREEDIVE) {
pi->dive_type = FREEDIVING; pi->dive_type = plot_info::FREEDIVING;
} else if (he > 0) { } else if (he > 0) {
pi->dive_type = TRIMIX; pi->dive_type = plot_info::TRIMIX;
} else { } else {
if (o2) if (o2)
pi->dive_type = NITROX; pi->dive_type = plot_info::NITROX;
else else
pi->dive_type = AIR; pi->dive_type = plot_info::AIR;
} }
populate_plot_entries(dive, dc, pi); populate_plot_entries(dive, dc, pi);
@ -1364,7 +1370,7 @@ void create_plot_info_new(const struct dive *dive, const struct divecomputer *dc
analyze_plot_info(pi); analyze_plot_info(pi);
} }
static void plot_string(const struct dive *d, const struct plot_info *pi, int idx, struct membuffer *b) static std::vector<QString> plot_string(const struct dive *d, const struct plot_info *pi, int idx)
{ {
int pressurevalue, mod, ead, end, eadd; int pressurevalue, mod, ead, end, eadd;
const char *depth_unit, *pressure_unit, *temp_unit, *vertical_speed_unit; const char *depth_unit, *pressure_unit, *temp_unit, *vertical_speed_unit;
@ -1372,65 +1378,69 @@ static void plot_string(const struct dive *d, const struct plot_info *pi, int id
int decimals, cyl; int decimals, cyl;
const char *unit; const char *unit;
const struct plot_data *entry = pi->entry + idx; const struct plot_data *entry = pi->entry + idx;
std::vector<QString> res;
depthvalue = get_depth_units(entry->depth, NULL, &depth_unit); depthvalue = get_depth_units(entry->depth, NULL, &depth_unit);
put_format_loc(b, translate("gettextFromC", "@: %d:%02d\nD: %.1f%s\n"), FRACTION(entry->sec, 60), depthvalue, depth_unit); res.push_back(qasprintf_loc(translate("gettextFromC", "@: %d:%02d"), FRACTION(entry->sec, 60), depthvalue));
res.push_back(qasprintf_loc(translate("gettextFromC", "D: %.1f%s"), depth_unit));
for (cyl = 0; cyl < pi->nr_cylinders; cyl++) { for (cyl = 0; cyl < pi->nr_cylinders; cyl++) {
int mbar = get_plot_pressure(pi, idx, cyl); int mbar = get_plot_pressure(pi, idx, cyl);
if (!mbar) if (!mbar)
continue; continue;
struct gasmix mix = get_cylinder(d, cyl)->gasmix; struct gasmix mix = get_cylinder(d, cyl)->gasmix;
pressurevalue = get_pressure_units(mbar, &pressure_unit); pressurevalue = get_pressure_units(mbar, &pressure_unit);
put_format_loc(b, translate("gettextFromC", "P: %d%s (%s)\n"), pressurevalue, pressure_unit, gasname(mix)); res.push_back(qasprintf_loc(translate("gettextFromC", "P: %d%s (%s)"), pressurevalue, pressure_unit, gasname(mix)));
} }
if (entry->temperature) { if (entry->temperature) {
tempvalue = get_temp_units(entry->temperature, &temp_unit); tempvalue = get_temp_units(entry->temperature, &temp_unit);
put_format_loc(b, translate("gettextFromC", "T: %.1f%s\n"), tempvalue, temp_unit); res.push_back(qasprintf_loc(translate("gettextFromC", "T: %.1f%s"), tempvalue, temp_unit));
} }
speedvalue = get_vertical_speed_units(abs(entry->speed), NULL, &vertical_speed_unit); speedvalue = get_vertical_speed_units(abs(entry->speed), NULL, &vertical_speed_unit);
/* Ascending speeds are positive, descending are negative */ /* Ascending speeds are positive, descending are negative */
if (entry->speed > 0) if (entry->speed > 0)
speedvalue *= -1; speedvalue *= -1;
put_format_loc(b, translate("gettextFromC", "V: %.1f%s\n"), speedvalue, vertical_speed_unit); res.push_back(qasprintf_loc(translate("gettextFromC", "V: %.1f%s"), speedvalue, vertical_speed_unit));
sacvalue = get_volume_units(entry->sac, &decimals, &unit); sacvalue = get_volume_units(entry->sac, &decimals, &unit);
if (entry->sac && prefs.show_sac) if (entry->sac && prefs.show_sac)
put_format_loc(b, translate("gettextFromC", "SAC: %.*f%s/min\n"), decimals, sacvalue, unit); res.push_back(qasprintf_loc(translate("gettextFromC", "SAC: %.*f%s/min"), decimals, sacvalue, unit));
if (entry->cns) if (entry->cns)
put_format_loc(b, translate("gettextFromC", "CNS: %u%%\n"), entry->cns); res.push_back(qasprintf_loc(translate("gettextFromC", "CNS: %u%%"), entry->cns));
if (prefs.pp_graphs.po2 && entry->pressures.o2 > 0) { if (prefs.pp_graphs.po2 && entry->pressures.o2 > 0) {
put_format_loc(b, translate("gettextFromC", "pO₂: %.2fbar\n"), entry->pressures.o2); res.push_back(qasprintf_loc(translate("gettextFromC", "pO₂: %.2fbar"), entry->pressures.o2));
if (entry->scr_OC_pO2.mbar) if (entry->scr_OC_pO2.mbar)
put_format_loc(b, translate("gettextFromC", "SCR ΔpO₂: %.2fbar\n"), entry->scr_OC_pO2.mbar/1000.0 - entry->pressures.o2); res.push_back(qasprintf_loc(translate("gettextFromC", "SCR ΔpO₂: %.2fbar"), entry->scr_OC_pO2.mbar/1000.0 - entry->pressures.o2));
} }
if (prefs.pp_graphs.pn2 && entry->pressures.n2 > 0) if (prefs.pp_graphs.pn2 && entry->pressures.n2 > 0)
put_format_loc(b, translate("gettextFromC", "pN₂: %.2fbar\n"), entry->pressures.n2); res.push_back(qasprintf_loc(translate("gettextFromC", "pN₂: %.2fbar"), entry->pressures.n2));
if (prefs.pp_graphs.phe && entry->pressures.he > 0) if (prefs.pp_graphs.phe && entry->pressures.he > 0)
put_format_loc(b, translate("gettextFromC", "pHe: %.2fbar\n"), entry->pressures.he); res.push_back(qasprintf_loc(translate("gettextFromC", "pHe: %.2fbar"), entry->pressures.he));
if (prefs.mod && entry->mod > 0) { if (prefs.mod && entry->mod > 0) {
mod = lrint(get_depth_units(entry->mod, NULL, &depth_unit)); mod = lrint(get_depth_units(entry->mod, NULL, &depth_unit));
put_format_loc(b, translate("gettextFromC", "MOD: %d%s\n"), mod, depth_unit); res.push_back(qasprintf_loc(translate("gettextFromC", "MOD: %d%s"), mod, depth_unit));
} }
eadd = lrint(get_depth_units(entry->eadd, NULL, &depth_unit)); eadd = lrint(get_depth_units(entry->eadd, NULL, &depth_unit));
if (prefs.ead) { if (prefs.ead) {
switch (pi->dive_type) { switch (pi->dive_type) {
case NITROX: case plot_info::NITROX:
if (entry->ead > 0) { if (entry->ead > 0) {
ead = lrint(get_depth_units(entry->ead, NULL, &depth_unit)); ead = lrint(get_depth_units(entry->ead, NULL, &depth_unit));
put_format_loc(b, translate("gettextFromC", "EAD: %d%s\nEADD: %d%s / %.1fg/\n"), ead, depth_unit, eadd, depth_unit, entry->density); res.push_back(qasprintf_loc(translate("gettextFromC", "EAD: %d%s"), ead, depth_unit));
res.push_back(qasprintf_loc(translate("gettextFromC", "EADD: %d%s / %.1fg/"), eadd, depth_unit, entry->density));
break; break;
} }
case TRIMIX: case plot_info::TRIMIX:
if (entry->end > 0) { if (entry->end > 0) {
end = lrint(get_depth_units(entry->end, NULL, &depth_unit)); end = lrint(get_depth_units(entry->end, NULL, &depth_unit));
put_format_loc(b, translate("gettextFromC", "END: %d%s\nEADD: %d%s / %.1fg/\n"), end, depth_unit, eadd, depth_unit, entry->density); res.push_back(qasprintf_loc(translate("gettextFromC", "END: %d%s"), end, depth_unit));
res.push_back(qasprintf_loc(translate("gettextFromC", "EADD: %d%s / %.1fg/"), eadd, depth_unit, entry->density));
break; break;
} }
case AIR: case plot_info::AIR:
if (entry->density > 0) { if (entry->density > 0) {
put_format_loc(b, translate("gettextFromC", "Density: %.1fg/\n"), entry->density); res.push_back(qasprintf_loc(translate("gettextFromC", "Density: %.1fg/"), entry->density));
} }
case FREEDIVING: case plot_info::FREEDIVING:
/* nothing */ /* nothing */
break; break;
} }
@ -1440,151 +1450,143 @@ static void plot_string(const struct dive *d, const struct plot_info *pi, int id
if (entry->ndl > 0) { if (entry->ndl > 0) {
/* this is a safety stop as we still have ndl */ /* this is a safety stop as we still have ndl */
if (entry->stoptime) if (entry->stoptime)
put_format_loc(b, translate("gettextFromC", "Safety stop: %umin @ %.0f%s\n"), DIV_UP(entry->stoptime, 60), res.push_back(qasprintf_loc(translate("gettextFromC", "Safety stop: %umin @ %.0f%s"), div_up(entry->stoptime, 60),
depthvalue, depth_unit); depthvalue, depth_unit));
else else
put_format_loc(b, translate("gettextFromC", "Safety stop: unknown time @ %.0f%s\n"), res.push_back(qasprintf_loc(translate("gettextFromC", "Safety stop: unknown time @ %.0f%s"),
depthvalue, depth_unit); depthvalue, depth_unit));
} else { } else {
/* actual deco stop */ /* actual deco stop */
if (entry->stoptime) if (entry->stoptime)
put_format_loc(b, translate("gettextFromC", "Deco: %umin @ %.0f%s\n"), DIV_UP(entry->stoptime, 60), res.push_back(qasprintf_loc(translate("gettextFromC", "Deco: %umin @ %.0f%s"), div_up(entry->stoptime, 60),
depthvalue, depth_unit); depthvalue, depth_unit));
else else
put_format_loc(b, translate("gettextFromC", "Deco: unknown time @ %.0f%s\n"), res.push_back(qasprintf_loc(translate("gettextFromC", "Deco: unknown time @ %.0f%s"),
depthvalue, depth_unit); depthvalue, depth_unit));
} }
} else if (entry->in_deco) { } else if (entry->in_deco) {
put_string(b, translate("gettextFromC", "In deco\n")); res.push_back(translate("gettextFromC", "In deco"));
} else if (entry->ndl >= 0) { } else if (entry->ndl >= 0) {
put_format_loc(b, translate("gettextFromC", "NDL: %umin\n"), DIV_UP(entry->ndl, 60)); res.push_back(qasprintf_loc(translate("gettextFromC", "NDL: %umin"), div_up(entry->ndl, 60)));
} }
if (entry->tts) if (entry->tts)
put_format_loc(b, translate("gettextFromC", "TTS: %umin\n"), DIV_UP(entry->tts, 60)); res.push_back(qasprintf_loc(translate("gettextFromC", "TTS: %umin"), div_up(entry->tts, 60)));
if (entry->stopdepth_calc && entry->stoptime_calc) { if (entry->stopdepth_calc && entry->stoptime_calc) {
depthvalue = get_depth_units(entry->stopdepth_calc, NULL, &depth_unit); depthvalue = get_depth_units(entry->stopdepth_calc, NULL, &depth_unit);
put_format_loc(b, translate("gettextFromC", "Deco: %umin @ %.0f%s (calc)\n"), DIV_UP(entry->stoptime_calc, 60), res.push_back(qasprintf_loc(translate("gettextFromC", "Deco: %umin @ %.0f%s (calc)"), div_up(entry->stoptime_calc, 60),
depthvalue, depth_unit); depthvalue, depth_unit));
} else if (entry->in_deco_calc) { } else if (entry->in_deco_calc) {
/* This means that we have no NDL left, /* This means that we have no NDL left,
* and we have no deco stop, * and we have no deco stop,
* so if we just accend to the surface slowly * so if we just accend to the surface slowly
* (ascent_mm_per_step / ascent_s_per_step) * (ascent_mm_per_step / ascent_s_per_step)
* everything will be ok. */ * everything will be ok. */
put_string(b, translate("gettextFromC", "In deco (calc)\n")); res.push_back(translate("gettextFromC", "In deco (calc)"));
} else if (prefs.calcndltts && entry->ndl_calc != 0) { } else if (prefs.calcndltts && entry->ndl_calc != 0) {
if(entry->ndl_calc < MAX_PROFILE_DECO) if(entry->ndl_calc < MAX_PROFILE_DECO)
put_format_loc(b, translate("gettextFromC", "NDL: %umin (calc)\n"), DIV_UP(entry->ndl_calc, 60)); res.push_back(qasprintf_loc(translate("gettextFromC", "NDL: %umin (calc)"), div_up(entry->ndl_calc, 60)));
else else
put_string(b, translate("gettextFromC", "NDL: >2h (calc)\n")); res.push_back(translate("gettextFromC", "NDL: >2h (calc)"));
} }
if (entry->tts_calc) { if (entry->tts_calc) {
if (entry->tts_calc < MAX_PROFILE_DECO) if (entry->tts_calc < MAX_PROFILE_DECO)
put_format_loc(b, translate("gettextFromC", "TTS: %umin (calc)\n"), DIV_UP(entry->tts_calc, 60)); res.push_back(qasprintf_loc(translate("gettextFromC", "TTS: %umin (calc)"), div_up(entry->tts_calc, 60)));
else else
put_string(b, translate("gettextFromC", "TTS: >2h (calc)\n")); res.push_back(translate("gettextFromC", "TTS: >2h (calc)"));
} }
if (entry->rbt) if (entry->rbt)
put_format_loc(b, translate("gettextFromC", "RBT: %umin\n"), DIV_UP(entry->rbt, 60)); res.push_back(qasprintf_loc(translate("gettextFromC", "RBT: %umin"), div_up(entry->rbt, 60)));
if (prefs.decoinfo) { if (prefs.decoinfo) {
if (entry->current_gf > 0.0) if (entry->current_gf > 0.0)
put_format(b, translate("gettextFromC", "GF %d%%\n"), (int)(100.0 * entry->current_gf)); res.push_back(qasprintf_loc(translate("gettextFromC", "GF %d%%"), (int)(100.0 * entry->current_gf)));
if (entry->surface_gf > 0.0) if (entry->surface_gf > 0.0)
put_format(b, translate("gettextFromC", "Surface GF %.0f%%\n"), entry->surface_gf); res.push_back(qasprintf_loc(translate("gettextFromC", "Surface GF %.0f%%"), entry->surface_gf));
if (entry->ceiling) { if (entry->ceiling) {
depthvalue = get_depth_units(entry->ceiling, NULL, &depth_unit); depthvalue = get_depth_units(entry->ceiling, NULL, &depth_unit);
put_format_loc(b, translate("gettextFromC", "Calculated ceiling %.1f%s\n"), depthvalue, depth_unit); res.push_back(qasprintf_loc(translate("gettextFromC", "Calculated ceiling %.1f%s"), depthvalue, depth_unit));
if (prefs.calcalltissues) { if (prefs.calcalltissues) {
int k; int k;
for (k = 0; k < 16; k++) { for (k = 0; k < 16; k++) {
if (entry->ceilings[k]) { if (entry->ceilings[k]) {
depthvalue = get_depth_units(entry->ceilings[k], NULL, &depth_unit); depthvalue = get_depth_units(entry->ceilings[k], NULL, &depth_unit);
put_format_loc(b, translate("gettextFromC", "Tissue %.0fmin: %.1f%s\n"), buehlmann_N2_t_halflife[k], depthvalue, depth_unit); res.push_back(qasprintf_loc(translate("gettextFromC", "Tissue %.0fmin: %.1f%s"), buehlmann_N2_t_halflife[k], depthvalue, depth_unit));
} }
} }
} }
} }
} }
if (entry->icd_warning) if (entry->icd_warning)
put_format(b, "%s", translate("gettextFromC", "ICD in leading tissue\n")); res.push_back(translate("gettextFromC", "ICD in leading tissue"));
if (entry->heartbeat && prefs.hrgraph) if (entry->heartbeat && prefs.hrgraph)
put_format_loc(b, translate("gettextFromC", "heart rate: %d\n"), entry->heartbeat); res.push_back(qasprintf_loc(translate("gettextFromC", "heart rate: %d"), entry->heartbeat));
if (entry->bearing >= 0) if (entry->bearing >= 0)
put_format_loc(b, translate("gettextFromC", "bearing: %d\n"), entry->bearing); res.push_back(qasprintf_loc(translate("gettextFromC", "bearing: %d"), entry->bearing));
if (entry->running_sum) { if (entry->running_sum) {
depthvalue = get_depth_units(entry->running_sum / entry->sec, NULL, &depth_unit); depthvalue = get_depth_units(entry->running_sum / entry->sec, NULL, &depth_unit);
put_format_loc(b, translate("gettextFromC", "mean depth to here %.1f%s\n"), depthvalue, depth_unit); res.push_back(qasprintf_loc(translate("gettextFromC", "mean depth to here %.1f%s"), depthvalue, depth_unit));
} }
strip_mb(b); return res;
} }
int get_plot_details_new(const struct dive *d, const struct plot_info *pi, int time, struct membuffer *mb) std::pair<int, std::vector<QString>> get_plot_details_new(const struct dive *d, const struct plot_info *pi, int time)
{ {
int i;
/* The two first and the two last plot entries do not have useful data */ /* The two first and the two last plot entries do not have useful data */
if (pi->nr <= 4) if (pi->nr <= 4)
return 0; return { 0, {} };
for (i = 2; i < pi->nr - 3; i++) {
if (pi->entry[i].sec >= time) // binary search for sample index
break; auto it = std::lower_bound(pi->entry + 2, pi->entry + pi->nr - 3, time,
} [] (const plot_data &d, int time)
plot_string(d, pi, i, mb); { return d.sec < time; });
return i; int idx = it - pi->entry;
auto strings = plot_string(d, pi, idx);
return std::make_pair(idx, strings);
} }
/* Compare two plot_data entries and writes the results into a string */ static QString space = QStringLiteral(" ");
void compare_samples(const struct dive *d, const struct plot_info *pi, int idx1, int idx2, char *buf, int bufsize, bool sum)
{
struct plot_data *start, *stop, *data;
const char *depth_unit, *pressure_unit, *vertical_speed_unit;
char *buf2 = malloc(bufsize);
int avg_speed, max_asc_speed, max_desc_speed;
int delta_depth, avg_depth, max_depth, min_depth;
int pressurevalue;
int last_sec, delta_time;
/* Compare two plot_data entries and writes the results into a set of strings */
std::vector<QString> compare_samples(const struct dive *d, const struct plot_info *pi, int idx1, int idx2, bool sum)
{
const char *depth_unit, *pressure_unit, *vertical_speed_unit;
double depthvalue, speedvalue; double depthvalue, speedvalue;
if (bufsize > 0) std::vector<QString> res;
buf[0] = '\0'; if (idx1 < 0 || idx2 < 0)
if (idx1 < 0 || idx2 < 0) { return res;
free(buf2);
return;
}
if (pi->entry[idx1].sec > pi->entry[idx2].sec) { if (pi->entry[idx1].sec > pi->entry[idx2].sec) {
int tmp = idx2; int tmp = idx2;
idx2 = idx1; idx2 = idx1;
idx1 = tmp; idx1 = tmp;
} else if (pi->entry[idx1].sec == pi->entry[idx2].sec) { } else if (pi->entry[idx1].sec == pi->entry[idx2].sec) {
free(buf2); return res;
return;
} }
start = pi->entry + idx1; struct plot_data *start = pi->entry + idx1;
stop = pi->entry + idx2; struct plot_data *stop = pi->entry + idx2;
avg_speed = 0; int avg_speed = 0;
max_asc_speed = 0; int max_asc_speed = 0;
max_desc_speed = 0; int max_desc_speed = 0;
delta_depth = abs(start->depth - stop->depth); int delta_depth = abs(start->depth - stop->depth);
delta_time = abs(start->sec - stop->sec); int delta_time = abs(start->sec - stop->sec);
avg_depth = 0; int avg_depth = 0;
max_depth = 0; int max_depth = 0;
min_depth = INT_MAX; int min_depth = INT_MAX;
last_sec = start->sec; int last_sec = start->sec;
volume_t cylinder_volume = { .mliter = 0, }; volume_t cylinder_volume = { .mliter = 0, };
int *start_pressures = calloc((size_t)pi->nr_cylinders, sizeof(int)); std::vector<int> start_pressures(pi->nr_cylinders, 0);
int *last_pressures = calloc((size_t)pi->nr_cylinders, sizeof(int)); std::vector<int> last_pressures(pi->nr_cylinders, 0);
int *bar_used = calloc((size_t)pi->nr_cylinders, sizeof(int)); std::vector<int> bar_used(pi->nr_cylinders, 0);
int *volumes_used = calloc((size_t)pi->nr_cylinders, sizeof(int)); std::vector<int> volumes_used(pi->nr_cylinders, 0);
bool *cylinder_is_used = calloc((size_t)pi->nr_cylinders, sizeof(bool)); std::vector<char> cylinder_is_used(pi->nr_cylinders, false);
data = start; struct plot_data *data = start;
for (int i = idx1; i < idx2; ++i) { for (int i = idx1; i < idx2; ++i) {
data = pi->entry + i; data = pi->entry + i;
if (sum) if (sum)
@ -1603,7 +1605,7 @@ void compare_samples(const struct dive *d, const struct plot_info *pi, int idx1,
if (data->depth > max_depth) if (data->depth > max_depth)
max_depth = data->depth; max_depth = data->depth;
for (unsigned cylinder_index = 0; cylinder_index < pi->nr_cylinders; cylinder_index++) { for (int cylinder_index = 0; cylinder_index < pi->nr_cylinders; cylinder_index++) {
int next_pressure = get_plot_pressure(pi, i, cylinder_index); int next_pressure = get_plot_pressure(pi, i, cylinder_index);
if (next_pressure && !start_pressures[cylinder_index]) if (next_pressure && !start_pressures[cylinder_index])
start_pressures[cylinder_index] = next_pressure; start_pressures[cylinder_index] = next_pressure;
@ -1629,47 +1631,38 @@ void compare_samples(const struct dive *d, const struct plot_info *pi, int idx1,
last_sec = data->sec; last_sec = data->sec;
} }
free(start_pressures);
free(last_pressures);
avg_depth /= stop->sec - start->sec; avg_depth /= stop->sec - start->sec;
avg_speed /= stop->sec - start->sec; avg_speed /= stop->sec - start->sec;
snprintf_loc(buf, bufsize, translate("gettextFromC", "ΔT:%d:%02dmin"), delta_time / 60, delta_time % 60); QString l = qasprintf_loc(translate("gettextFromC", "ΔT:%d:%02dmin"), delta_time / 60, delta_time % 60);
memcpy(buf2, buf, bufsize);
depthvalue = get_depth_units(delta_depth, NULL, &depth_unit); depthvalue = get_depth_units(delta_depth, NULL, &depth_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s ΔD:%.1f%s"), buf2, depthvalue, depth_unit); l += space + qasprintf_loc(translate("gettextFromC", "ΔD:%.1f%s"), depthvalue, depth_unit);
memcpy(buf2, buf, bufsize);
depthvalue = get_depth_units(min_depth, NULL, &depth_unit); depthvalue = get_depth_units(min_depth, NULL, &depth_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s ↓D:%.1f%s"), buf2, depthvalue, depth_unit); l += space + qasprintf_loc(translate("gettextFromC", "↓D:%.1f%s"), depthvalue, depth_unit);
memcpy(buf2, buf, bufsize);
depthvalue = get_depth_units(max_depth, NULL, &depth_unit); depthvalue = get_depth_units(max_depth, NULL, &depth_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s ↑D:%.1f%s"), buf2, depthvalue, depth_unit); l += space + qasprintf_loc(translate("gettextFromC", "↑D:%.1f%s"), depthvalue, depth_unit);
memcpy(buf2, buf, bufsize);
depthvalue = get_depth_units(avg_depth, NULL, &depth_unit); depthvalue = get_depth_units(avg_depth, NULL, &depth_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s øD:%.1f%s\n"), buf2, depthvalue, depth_unit); l += space + qasprintf_loc(translate("gettextFromC", "øD:%.1f%s"), depthvalue, depth_unit);
memcpy(buf2, buf, bufsize); res.push_back(l);
speedvalue = get_vertical_speed_units(abs(max_desc_speed), NULL, &vertical_speed_unit); speedvalue = get_vertical_speed_units(abs(max_desc_speed), NULL, &vertical_speed_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s ↓V:%.2f%s"), buf2, speedvalue, vertical_speed_unit); l = qasprintf_loc(translate("gettextFromC", "↓V:%.2f%s"), speedvalue, vertical_speed_unit);
memcpy(buf2, buf, bufsize);
speedvalue = get_vertical_speed_units(abs(max_asc_speed), NULL, &vertical_speed_unit); speedvalue = get_vertical_speed_units(abs(max_asc_speed), NULL, &vertical_speed_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s ↑V:%.2f%s"), buf2, speedvalue, vertical_speed_unit); l += space + qasprintf_loc(translate("gettextFromC", "↑V:%.2f%s"), speedvalue, vertical_speed_unit);
memcpy(buf2, buf, bufsize);
speedvalue = get_vertical_speed_units(abs(avg_speed), NULL, &vertical_speed_unit); speedvalue = get_vertical_speed_units(abs(avg_speed), NULL, &vertical_speed_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s øV:%.2f%s"), buf2, speedvalue, vertical_speed_unit); l += space + qasprintf_loc(translate("gettextFromC", "øV:%.2f%s"), speedvalue, vertical_speed_unit);
int total_bar_used = 0; int total_bar_used = 0;
int total_volume_used = 0; int total_volume_used = 0;
bool cylindersizes_are_identical = true; bool cylindersizes_are_identical = true;
bool sac_is_determinable = true; bool sac_is_determinable = true;
for (unsigned cylinder_index = 0; cylinder_index < pi->nr_cylinders; cylinder_index++) for (int cylinder_index = 0; cylinder_index < pi->nr_cylinders; cylinder_index++) {
if (cylinder_is_used[cylinder_index]) { if (cylinder_is_used[cylinder_index]) {
total_bar_used += bar_used[cylinder_index]; total_bar_used += bar_used[cylinder_index];
total_volume_used += volumes_used[cylinder_index]; total_volume_used += volumes_used[cylinder_index];
@ -1685,20 +1678,16 @@ void compare_samples(const struct dive *d, const struct plot_info *pi, int idx1,
sac_is_determinable = false; sac_is_determinable = false;
} }
} }
free(bar_used); }
free(volumes_used);
free(cylinder_is_used);
// No point printing 'bar used' if we know it's meaningless because cylinders of different size were used // No point printing 'bar used' if we know it's meaningless because cylinders of different size were used
if (cylindersizes_are_identical && total_bar_used) { if (cylindersizes_are_identical && total_bar_used) {
pressurevalue = get_pressure_units(total_bar_used, &pressure_unit); int pressurevalue = get_pressure_units(total_bar_used, &pressure_unit);
memcpy(buf2, buf, bufsize); l += space + qasprintf_loc(translate("gettextFromC", "ΔP:%d%s"), pressurevalue, pressure_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s ΔP:%d%s"), buf2, pressurevalue, pressure_unit);
} }
// We can't calculate the SAC if the volume for some of the cylinders used is unknown // We can't calculate the SAC if the volume for some of the cylinders used is unknown
if (sac_is_determinable && total_volume_used) { if (sac_is_determinable && total_volume_used) {
double volume_value;
int volume_precision; int volume_precision;
const char *volume_unit; const char *volume_unit;
@ -1707,10 +1696,10 @@ void compare_samples(const struct dive *d, const struct plot_info *pi, int idx1,
/* milliliters per minute */ /* milliliters per minute */
int sac = lrint(total_volume_used / atm * 60 / delta_time); int sac = lrint(total_volume_used / atm * 60 / delta_time);
memcpy(buf2, buf, bufsize); double volume_value = get_volume_units(sac, &volume_precision, &volume_unit);
volume_value = get_volume_units(sac, &volume_precision, &volume_unit); l += space + qasprintf_loc(translate("gettextFromC", "SAC:%.*f%s/min"), volume_precision, volume_value, volume_unit);
snprintf_loc(buf, bufsize, translate("gettextFromC", "%s SAC:%.*f%s/min"), buf2, volume_precision, volume_value, volume_unit);
} }
res.push_back(l);
free(buf2); return res;
} }

View File

@ -97,11 +97,9 @@ struct plot_info {
#define AMB_PERCENTAGE 50.0 #define AMB_PERCENTAGE 50.0
extern void compare_samples(const struct dive *d, const struct plot_info *pi, int idx1, int idx2, char *buf, int bufsize, bool sum);
extern void init_plot_info(struct plot_info *pi); extern void init_plot_info(struct plot_info *pi);
/* when planner_dc is non-null, this is called in planner mode. */ /* when planner_dc is non-null, this is called in planner mode. */
extern void create_plot_info_new(const struct dive *dive, const struct divecomputer *dc, struct plot_info *pi, const struct deco_state *planner_ds); extern void create_plot_info_new(const struct dive *dive, const struct divecomputer *dc, struct plot_info *pi, const struct deco_state *planner_ds);
extern int get_plot_details_new(const struct dive *d, const struct plot_info *pi, int time, struct membuffer *);
extern void free_plot_info_data(struct plot_info *pi); extern void free_plot_info_data(struct plot_info *pi);
/* /*
@ -145,5 +143,12 @@ static inline int get_plot_pressure(const struct plot_info *pi, int idx, int cyl
#ifdef __cplusplus #ifdef __cplusplus
} }
// C++ only formatting functions
#include <QString>
// Returns index of sample and array of strings describing the dive details at given time
std::pair<int, std::vector<QString>> get_plot_details_new(const struct dive *d, const struct plot_info *pi, int time);
std::vector<QString> compare_samples(const struct dive *d, const struct plot_info *pi, int idx1, int idx2, bool sum);
#endif #endif
#endif // PROFILE_H #endif // PROFILE_H

View File

@ -22,7 +22,6 @@
#include "desktop-widgets/divelogexportdialog.h" #include "desktop-widgets/divelogexportdialog.h"
#include "desktop-widgets/diveshareexportdialog.h" #include "desktop-widgets/diveshareexportdialog.h"
#include "desktop-widgets/subsurfacewebservices.h" #include "desktop-widgets/subsurfacewebservices.h"
#include "profile-widget/profilewidget2.h"
// Retrieves the current unit settings defined in the Subsurface preferences. // Retrieves the current unit settings defined in the Subsurface preferences.
#define GET_UNIT(name, field, f, t) \ #define GET_UNIT(name, field, f, t) \

View File

@ -56,7 +56,6 @@
#include "commands/command.h" #include "commands/command.h"
#include "profilewidget.h" #include "profilewidget.h"
#include "profile-widget/profilewidget2.h"
#ifndef NO_PRINTING #ifndef NO_PRINTING
#include "desktop-widgets/printdialog.h" #include "desktop-widgets/printdialog.h"
@ -671,7 +670,7 @@ void MainWindow::on_actionReplanDive_triggered()
disableShortcuts(true); disableShortcuts(true);
plannerWidgets->prepareReplanDive(current_dive); plannerWidgets->prepareReplanDive(current_dive);
profile->setPlanState(plannerWidgets->getDive(), profile->dc); profile->plotDive(plannerWidgets->getDive(), profile->dc, true);
plannerWidgets->replanDive(profile->dc); plannerWidgets->replanDive(profile->dc);
} }
@ -685,7 +684,7 @@ void MainWindow::on_actionDivePlanner_triggered()
disableShortcuts(true); disableShortcuts(true);
plannerWidgets->preparePlanDive(current_dive); plannerWidgets->preparePlanDive(current_dive);
profile->setPlanState(plannerWidgets->getDive(), 0); profile->plotDive(plannerWidgets->getDive(), 0, true);
plannerWidgets->planDive(); plannerWidgets->planDive();
} }

View File

@ -8,7 +8,6 @@
#include "qt-models/cylindermodel.h" #include "qt-models/cylindermodel.h"
#include "qt-models/models.h" #include "qt-models/models.h"
#include "desktop-widgets/starwidget.h" #include "desktop-widgets/starwidget.h"
#include "profile-widget/profilewidget2.h"
#include "qt-models/tankinfomodel.h" #include "qt-models/tankinfomodel.h"
#include "qt-models/weightsysteminfomodel.h" #include "qt-models/weightsysteminfomodel.h"
#include "qt-models/weightmodel.h" #include "qt-models/weightmodel.h"

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "profilewidget.h" #include "profilewidget.h"
#include "profile-widget/profilewidget2.h" #include "profile-widget/profileview.h"
#include "commands/command.h" #include "commands/command.h"
#include "core/color.h" #include "core/color.h"
#include "core/selection.h" #include "core/selection.h"
@ -10,10 +10,14 @@
#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 <QStackedWidget>
#include <QLabel> #include <QLabel>
#include <QMimeData>
#include <QQmlEngine>
#include <QQuickWidget>
#include <QSignalBlocker>
#include <QStackedWidget>
#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 {
@ -52,6 +56,58 @@ 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"));
ProfileWidget::ProfileWidget() : d(nullptr), dc(0), originalDive(nullptr), placingCommand(false) ProfileWidget::ProfileWidget() : d(nullptr), dc(0), originalDive(nullptr), placingCommand(false)
{ {
ui.setupUi(this); ui.setupUi(this);
@ -72,7 +128,10 @@ ProfileWidget::ProfileWidget() : d(nullptr), dc(0), originalDive(nullptr), placi
emptyView.reset(new EmptyView); emptyView.reset(new EmptyView);
view.reset(new ProfileWidget2(DivePlannerPointsModel::instance(), 1.0, this)); viewWidget.reset(new ProfileViewWidget(*this));
viewWidget->setSource(urlProfileView);
viewWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
QToolBar *toolBar = new QToolBar(this); QToolBar *toolBar = new QToolBar(this);
for (QAction *a: toolbarActions) for (QAction *a: toolbarActions)
toolBar->addAction(a); toolBar->addAction(a);
@ -81,7 +140,7 @@ ProfileWidget::ProfileWidget() : d(nullptr), dc(0), originalDive(nullptr), placi
stack = new QStackedWidget(this); stack = new QStackedWidget(this);
stack->addWidget(emptyView.get()); stack->addWidget(emptyView.get());
stack->addWidget(view.get()); stack->addWidget(viewWidget.get());
QHBoxLayout *layout = new QHBoxLayout(this); QHBoxLayout *layout = new QHBoxLayout(this);
layout->setSpacing(0); layout->setSpacing(0);
@ -121,10 +180,6 @@ ProfileWidget::ProfileWidget() : d(nullptr), dc(0), originalDive(nullptr), placi
connect(ui.profPO2, &QAction::triggered, pp_gas, &qPrefPartialPressureGas::set_po2); connect(ui.profPO2, &QAction::triggered, pp_gas, &qPrefPartialPressureGas::set_po2);
connect(&diveListNotifier, &DiveListNotifier::divesChanged, this, &ProfileWidget::divesChanged); connect(&diveListNotifier, &DiveListNotifier::divesChanged, this, &ProfileWidget::divesChanged);
connect(&diveListNotifier, &DiveListNotifier::settingsChanged, view.get(), &ProfileWidget2::settingsChanged);
connect(view.get(), &ProfileWidget2::stopAdded, this, &ProfileWidget::stopAdded);
connect(view.get(), &ProfileWidget2::stopRemoved, this, &ProfileWidget::stopRemoved);
connect(view.get(), &ProfileWidget2::stopMoved, this, &ProfileWidget::stopMoved);
ui.profCalcAllTissues->setChecked(qPrefTechnicalDetails::calcalltissues()); ui.profCalcAllTissues->setChecked(qPrefTechnicalDetails::calcalltissues());
ui.profCalcCeiling->setChecked(qPrefTechnicalDetails::calcceiling()); ui.profCalcCeiling->setChecked(qPrefTechnicalDetails::calcceiling());
@ -151,6 +206,33 @@ ProfileWidget::~ProfileWidget()
{ {
} }
// hack around the Qt6 bug where the QML object gets destroyed and recreated
ProfileView *ProfileWidget::getView()
{
ProfileView *view = qobject_cast<ProfileView *>(viewWidget->rootObject());
if (!view)
qWarning("Oops. The root of the StatsView is not a StatsView.");
if (view) {
// try to prevent the JS garbage collection from freeing the object
// this appears to fail with Qt6 which is why we still look up the
// object from the rootObject
viewWidget->engine()->setObjectOwnership(view, QQmlEngine::CppOwnership);
view->setParent(this);
view->setVisible(isVisible()); // Synchronize visibility of widget and QtQuick-view.
if (!view->initialized) {
view->setPlannerModel(*DivePlannerPointsModel::instance());
//connect(&diveListNotifier, &DiveListNotifier::settingsChanged, view.get(), &ProfileWidget2::settingsChanged);
connect(view, &ProfileView::stopAdded, this, &ProfileWidget::stopAdded);
connect(view, &ProfileView::stopRemoved, this, &ProfileWidget::stopRemoved);
connect(view, &ProfileView::stopMoved, this, &ProfileWidget::stopMoved);
view->initialized = true;
}
}
return view;
}
void ProfileWidget::setEnabledToolbar(bool enabled) void ProfileWidget::setEnabledToolbar(bool enabled)
{ {
for (QAction *b: toolbarActions) for (QAction *b: toolbarActions)
@ -190,7 +272,7 @@ void ProfileWidget::plotCurrentDive()
plotDive(d, dc); plotDive(d, dc);
} }
void ProfileWidget::plotDive(dive *dIn, int dcIn) void ProfileWidget::plotDive(dive *dIn, int dcIn, bool planMode)
{ {
d = dIn; d = dIn;
@ -214,14 +296,16 @@ void ProfileWidget::plotDive(dive *dIn, int dcIn)
editDive(); editDive();
} }
auto view = getView();
setEnabledToolbar(d != nullptr); setEnabledToolbar(d != nullptr);
if (editedDive) { if (editedDive) {
view->plotDive(editedDive.get(), editedDc); view->plotDive(editedDive.get(), editedDc, ProfileView::RenderFlags::EditMode);
setDive(editedDive.get()); setDive(editedDive.get());
} else if (d) { } else if (d) {
view->setProfileState(d, dc); int flags = planMode ? ProfileView::RenderFlags::PlanMode
: ProfileView::RenderFlags::None;
view->resetZoom(); // when switching dive, reset the zoomLevel view->resetZoom(); // when switching dive, reset the zoomLevel
view->plotDive(d, dc); view->plotDive(d, dc, flags);
setDive(d); setDive(d);
} else { } else {
view->clear(); view->clear();
@ -263,8 +347,8 @@ void ProfileWidget::divesChanged(const QVector<dive *> &dives, DiveField field)
if (!d || !dives.contains(d) || !(field.duration || field.depth) || placingCommand) if (!d || !dives.contains(d) || !(field.duration || field.depth) || placingCommand)
return; return;
// If were editing the current dive and not currently // If we're editing the current dive and not currently
// placing command, we have to update the edited dive. // placing a command, we have to update the edited dive.
if (editedDive) { if (editedDive) {
copy_dive(d, editedDive.get()); copy_dive(d, editedDive.get());
// TODO: Holy moly that function sends too many signals. Fix it! // TODO: Holy moly that function sends too many signals. Fix it!
@ -274,13 +358,6 @@ void ProfileWidget::divesChanged(const QVector<dive *> &dives, DiveField field)
plotCurrentDive(); plotCurrentDive();
} }
void ProfileWidget::setPlanState(const struct dive *d, int dc)
{
exitEditMode();
view->setPlanState(d, dc);
setDive(d);
}
void ProfileWidget::unsetProfHR() void ProfileWidget::unsetProfHR()
{ {
ui.profHR->setChecked(false); ui.profHR->setChecked(false);
@ -299,9 +376,13 @@ void ProfileWidget::editDive()
editedDc = dc; editedDc = dc;
copy_dive(d, editedDive.get()); // Work on a copy of the dive copy_dive(d, editedDive.get()); // Work on a copy of the dive
originalDive = d; originalDive = d;
// We don't want the DivePlannerPointsModel send signals while reloading,
// because that would reload the just deleted dive. And we will be reloading
// the dive anyway. Control-flow here is truly horrible.
QSignalBlocker blocker(DivePlannerPointsModel::instance());
DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD);
DivePlannerPointsModel::instance()->loadFromDive(editedDive.get(), editedDc); DivePlannerPointsModel::instance()->loadFromDive(editedDive.get(), editedDc);
view->setEditState(editedDive.get(), editedDc);
} }
void ProfileWidget::exitEditMode() void ProfileWidget::exitEditMode()
@ -309,7 +390,7 @@ void ProfileWidget::exitEditMode()
if (!editedDive) if (!editedDive)
return; return;
DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING);
view->setProfileState(d, dc); // switch back to original dive before erasing the copy. //view->setProfileState(d, dc); // switch back to original dive before erasing the copy.
editedDive.reset(); editedDive.reset();
originalDive = nullptr; originalDive = nullptr;
} }
@ -359,3 +440,12 @@ void ProfileWidget::stopMoved(int count)
Setter s(placingCommand, true); Setter s(placingCommand, true);
Command::editProfile(editedDive.get(), editedDc, Command::EditProfileType::MOVE, count); Command::editProfile(editedDive.get(), editedDc, Command::EditProfileType::MOVE, count);
} }
void ProfileWidget::dropPicture(const QString &filename, QPoint pos)
{
auto view = getView();
if (!d || !view)
return;
offset_t offset { (int32_t)lrint(view->timeAt(pos)) };
Command::setPictureOffset(d, filename, offset);
}

View File

@ -11,8 +11,9 @@
#include <vector> #include <vector>
struct dive; struct dive;
class ProfileWidget2; class ProfileView;
class EmptyView; class EmptyView;
class QQuickWidget;
class QStackedWidget; class QStackedWidget;
extern "C" void free_dive(struct dive *); extern "C" void free_dive(struct dive *);
@ -22,13 +23,12 @@ class ProfileWidget : public QWidget {
public: public:
ProfileWidget(); ProfileWidget();
~ProfileWidget(); ~ProfileWidget();
std::unique_ptr<ProfileWidget2> view; void plotDive(struct dive *d, int dc, bool planMode = false); // Attempt to keep DC number id dc < 0
void plotDive(struct dive *d, int dc); // Attempt to keep DC number id dc < 0
void plotCurrentDive(); void plotCurrentDive();
void setPlanState(const struct dive *d, int dc);
void setEnabledToolbar(bool enabled); void setEnabledToolbar(bool enabled);
void nextDC(); void nextDC();
void prevDC(); void prevDC();
void dropPicture(const QString &filename, QPoint p);
dive *d; dive *d;
int dc; int dc;
private private
@ -40,6 +40,8 @@ slots:
void stopRemoved(int count); void stopRemoved(int count);
void stopMoved(int count); void stopMoved(int count);
private: private:
ProfileView *getView();
std::unique_ptr<QQuickWidget> viewWidget;
std::unique_ptr<EmptyView> emptyView; std::unique_ptr<EmptyView> emptyView;
std::vector<QAction *> toolbarActions; std::vector<QAction *> toolbarActions;
Ui::ProfileWidget ui; Ui::ProfileWidget ui;

View File

@ -0,0 +1,6 @@
// SPDX-License-Identifier: GPL-2.0
import org.subsurfacedivelog.mobile 1.0
ProfileView {
property real dpr: 1.0
}

View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/qml">
<file>profileview.qml</file>
</qresource>
</RCC>

View File

@ -20,7 +20,6 @@
#include "libdivecomputer/parser.h" #include "libdivecomputer/parser.h"
#include "desktop-widgets/divelistview.h" #include "desktop-widgets/divelistview.h"
#include "core/selection.h" #include "core/selection.h"
#include "profile-widget/profilewidget2.h"
#include "commands/command.h" #include "commands/command.h"
#include "core/metadata.h" #include "core/metadata.h"
#include "core/tag.h" #include "core/tag.h"

View File

@ -2,7 +2,6 @@
#include "TabDiveInformation.h" #include "TabDiveInformation.h"
#include "maintab.h" #include "maintab.h"
#include "ui_TabDiveInformation.h" #include "ui_TabDiveInformation.h"
#include "profile-widget/profilewidget2.h"
#include "../tagwidget.h" #include "../tagwidget.h"
#include "commands/command.h" #include "commands/command.h"
#include "core/subsurface-string.h" #include "core/subsurface-string.h"

View File

@ -231,13 +231,13 @@ Item {
Layout.columnSpan: 3 Layout.columnSpan: 3
clip: true clip: true
QMLProfile { ProfileView {
id: qmlProfile id: qmlProfile
visible: !noDive visible: !noDive
anchors.fill: parent anchors.fill: parent
clip: true clip: true
property real lastScale: 1.0 // final scale at the end of previous pinch
diveId: detailsView.myId diveId: detailsView.myId
property real dpr: 0.8 // TODO: make this dynamic
Rectangle { Rectangle {
color: "transparent" color: "transparent"
opacity: 0.6 opacity: 0.6
@ -253,36 +253,20 @@ Item {
// before realizing that this is actually a pinch/zoom. So let's reset this // before realizing that this is actually a pinch/zoom. So let's reset this
// just in case // just in case
qmlProfile.opacity = 1.0 qmlProfile.opacity = 1.0
if (manager.verboseEnabled) qmlProfile.pinchStart()
manager.appendTextToLog("pinch started w/ previousScale " + qmlProfile.lastScale)
} }
onPinchUpdated: { onPinchUpdated: {
if (pinch.scale * qmlProfile.lastScale < 1.0) qmlProfile.pinch(pinch.scale)
qmlProfile.lastScale = 1.0 / pinch.scale // this way we never shrink and the changes stay smooth
// the underlying widget deals with the scaling, no need to send an update request
qmlProfile.scale = pinch.scale * qmlProfile.lastScale
if (manager.verboseEnabled)
manager.appendTextToLog("pinch updated to scale " + qmlProfile.scale);
}
onPinchFinished: {
// remember the final scale value so we can continue from there next time the user pinches
qmlProfile.lastScale = pinch.scale * qmlProfile.lastScale
} }
MouseArea { MouseArea {
// we want to pan the profile if we are zoomed in, but we want to immediately // we want to pan the profile if we are zoomed in, but we want to immediately
// pass the mouse events through to the ListView if we are not. That way you // pass the mouse events through to the ListView if we are not. That way you
// can swipe through the dive list, even if you happen to swipe the profile // can swipe through the dive list, even if you happen to swipe the profile
property bool isZoomed: qmlProfile.scale - 1.0 > 0.02 property bool isZoomed: qmlProfile.zoomLevel > 1.02
// this indicates that we are actually dragging // this indicates that we are actually dragging
property bool dragging: false property bool dragging: false
// cursor/finger position as we start dragging
property real initialX
property real initialY
// the offset previously used to show the profile
property real oldXOffset
property real oldYOffset
// if the profile is not scaled in, don't start panning // if the profile is not scaled in, don't start panning
// but if the profile is scaled in, then start almost immediately // but if the profile is scaled in, then start almost immediately
@ -291,16 +275,6 @@ Item {
// pass events through to the parent and eventually into the ListView // pass events through to the parent and eventually into the ListView
propagateComposedEvents: true propagateComposedEvents: true
// for testing / debugging on a desktop
scrollGestureEnabled: true
onWheel: {
manager.appendTextToLog("wheel " + wheel.angleDelta)
if (wheel.angleDelta.y > 0)
qmlProfile.scale += 0.2
if (wheel.angleDelta.y < 0 && qmlProfile.scale > 1.1)
qmlProfile.scale -= 0.2
}
anchors.fill: parent anchors.fill: parent
drag.target: qmlProfile drag.target: qmlProfile
drag.axis: Drag.XAndYAxis drag.axis: Drag.XAndYAxis
@ -311,10 +285,7 @@ Item {
} }
onPressAndHold: { onPressAndHold: {
dragging = true; dragging = true;
oldXOffset = qmlProfile.xOffset qmlProfile.panStart(mouse.x, mouse.y)
oldYOffset = qmlProfile.yOffset
initialX = mouse.x
initialY = mouse.y
if (manager.verboseEnabled) if (manager.verboseEnabled)
manager.appendTextToLog("press and hold at mouse" + Math.round(10 * mouse.x) / 10 + " / " + Math.round(10 * mouse.y) / 10) manager.appendTextToLog("press and hold at mouse" + Math.round(10 * mouse.x) / 10 + " / " + Math.round(10 * mouse.y) / 10)
// give visual feedback to the user that they now can drag // give visual feedback to the user that they now can drag
@ -322,13 +293,9 @@ Item {
} }
onPositionChanged: { onPositionChanged: {
if (dragging) { if (dragging) {
var x = (mouse.x - initialX) / qmlProfile.scale
var y = (mouse.y - initialY) / qmlProfile.scale
if (manager.verboseEnabled) if (manager.verboseEnabled)
manager.appendTextToLog("drag mouse " + Math.round(10 * mouse.x) / 10 + " / " + Math.round(10 * mouse.y) / 10 + " delta " + Math.round(x) + " / " + Math.round(y)) manager.appendTextToLog("drag mouse " + Math.round(10 * mouse.x) / 10 + " / " + Math.round(10 * mouse.y) / 10 + " delta " + Math.round(x) + " / " + Math.round(y))
qmlProfile.xOffset = oldXOffset + x qmlProfile.pan(mouse.x, mouse.y)
qmlProfile.yOffset = oldYOffset + y
qmlProfile.update()
} else { } else {
mouse.accepted = false mouse.accepted = false
} }
@ -344,7 +311,6 @@ Item {
onClicked: { onClicked: {
// reset the position if not zoomed in // reset the position if not zoomed in
if (!isZoomed) { if (!isZoomed) {
qmlProfile.xOffset = qmlProfile.yOffset = oldXOffset = oldYOffset = 0
mouse.accepted = false mouse.accepted = false
} }
} }

View File

@ -6,42 +6,37 @@ set(SUBSURFACE_PROFILE_LIB_SRCS
divecartesianaxis.h divecartesianaxis.h
diveeventitem.cpp diveeventitem.cpp
diveeventitem.h diveeventitem.h
divelineitem.cpp
divelineitem.h
divepixmapcache.cpp divepixmapcache.cpp
divepixmapcache.h divepixmapcache.h
divepixmapitem.cpp
divepixmapitem.h
divepercentageitem.cpp divepercentageitem.cpp
divepercentageitem.h divepercentageitem.h
diveprofileitem.cpp diveprofileitem.cpp
diveprofileitem.h diveprofileitem.h
diverectitem.cpp
diverectitem.h
divetextitem.cpp divetextitem.cpp
divetextitem.h divetextitem.h
divetooltipitem.cpp handleitem.cpp
divetooltipitem.h handleitem.h
pictureitem.h
pictureitem.cpp
profilescene.cpp profilescene.cpp
profilescene.h profilescene.h
profiletranslations.h
profileview.cpp
profileview.h
ruleritem.cpp
ruleritem.h
tankitem.cpp tankitem.cpp
tankitem.h tankitem.h
tooltipitem.h
tooltipitem.cpp
) )
if (SUBSURFACE_TARGET_EXECUTABLE MATCHES "MobileExecutable") if (SUBSURFACE_TARGET_EXECUTABLE MATCHES "MobileExecutable")
set(SUBSURFACE_PROFILE_LIB_SRCS set(SUBSURFACE_PROFILE_LIB_SRCS
${SUBSURFACE_PROFILE_LIB_SRCS} ${SUBSURFACE_PROFILE_LIB_SRCS}
qmlprofile.cpp
qmlprofile.h
) )
else () else ()
set(SUBSURFACE_PROFILE_LIB_SRCS set(SUBSURFACE_PROFILE_LIB_SRCS
${SUBSURFACE_PROFILE_LIB_SRCS} ${SUBSURFACE_PROFILE_LIB_SRCS}
divehandler.cpp
divehandler.h
profilewidget2.cpp
profilewidget2.h
ruleritem.cpp
ruleritem.h
) )
endif () endif ()
source_group("Subsurface Profile" FILES ${SUBSURFACE_PROFILE_LIB_SRCS}) source_group("Subsurface Profile" FILES ${SUBSURFACE_PROFILE_LIB_SRCS})

View File

@ -5,7 +5,6 @@
#include "core/qthelper.h" #include "core/qthelper.h"
#include "core/subsurface-float.h" #include "core/subsurface-float.h"
#include "profile-widget/animationfunctions.h" #include "profile-widget/animationfunctions.h"
#include "profile-widget/divelineitem.h"
#include "profile-widget/profilescene.h" #include "profile-widget/profilescene.h"
static const double labelSpaceHorizontal = 2.0; // space between label and ticks static const double labelSpaceHorizontal = 2.0; // space between label and ticks
@ -277,7 +276,7 @@ DiveCartesianAxis::Label DiveCartesianAxis::createLabel(double value, double pos
if (lineVisibility) { if (lineVisibility) {
label.lineStart = linePos(posStart); label.lineStart = linePos(posStart);
label.lineEnd = linePos(pos); label.lineEnd = linePos(pos);
label.line = std::make_unique<DiveLineItem>(this); label.line = std::make_unique<QGraphicsLineItem>(this);
label.line->setPen(gridPen); label.line->setPen(gridPen);
label.line->setZValue(0); label.line->setZValue(0);
label.line->setLine(animSpeed <= 0 ? label.lineEnd : label.lineStart); label.line->setLine(animSpeed <= 0 ? label.lineEnd : label.lineStart);

View File

@ -10,7 +10,6 @@
class ProfileScene; class ProfileScene;
class DiveTextItem; class DiveTextItem;
class DiveLineItem;
class DiveCartesianAxis : public QGraphicsLineItem { class DiveCartesianAxis : public QGraphicsLineItem {
private: private:
@ -57,7 +56,7 @@ private:
QLineF lineStart; QLineF lineStart;
QLineF lineEnd; QLineF lineEnd;
std::unique_ptr<DiveTextItem> label; std::unique_ptr<DiveTextItem> label;
std::unique_ptr<DiveLineItem> line; std::unique_ptr<QGraphicsLineItem> line;
}; };
Position position; Position position;
bool inverted; // Top-to-bottom or right-to-left axis. bool inverted; // Top-to-bottom or right-to-left axis.

View File

@ -4,7 +4,6 @@
#include "profile-widget/divepixmapcache.h" #include "profile-widget/divepixmapcache.h"
#include "profile-widget/animationfunctions.h" #include "profile-widget/animationfunctions.h"
#include "core/event.h" #include "core/event.h"
#include "core/eventtype.h"
#include "core/format.h" #include "core/format.h"
#include "core/profile.h" #include "core/profile.h"
#include "core/gettextfromc.h" #include "core/gettextfromc.h"
@ -17,7 +16,9 @@ static int depthAtTime(const plot_info &pi, duration_t time);
DiveEventItem::DiveEventItem(const struct dive *d, struct event *ev, struct gasmix lastgasmix, DiveEventItem::DiveEventItem(const struct dive *d, struct event *ev, struct gasmix lastgasmix,
const plot_info &pi, DiveCartesianAxis *hAxis, DiveCartesianAxis *vAxis, const plot_info &pi, DiveCartesianAxis *hAxis, DiveCartesianAxis *vAxis,
int speed, const DivePixmaps &pixmaps, QGraphicsItem *parent) : DivePixmapItem(parent), int speed, const DivePixmaps &pixmaps, QGraphicsItem *parent) : QGraphicsPixmapItem(parent),
text(setupToolTipString(d, ev, lastgasmix)),
pixmap(setupPixmap(d, ev, lastgasmix, pixmaps)),
vAxis(vAxis), vAxis(vAxis),
hAxis(hAxis), hAxis(hAxis),
ev(ev), ev(ev),
@ -25,10 +26,11 @@ DiveEventItem::DiveEventItem(const struct dive *d, struct event *ev, struct gasm
depth(depthAtTime(pi, ev->time)) depth(depthAtTime(pi, ev->time))
{ {
setFlag(ItemIgnoresTransformations); setFlag(ItemIgnoresTransformations);
setPixmap(pixmap);
setupPixmap(lastgasmix, pixmaps);
setupToolTipString(lastgasmix);
recalculatePos(); recalculatePos();
if (ev->type == SAMPLE_EVENT_BOOKMARK)
setOffset(QPointF(0.0, -pixmap.height()));
} }
DiveEventItem::~DiveEventItem() DiveEventItem::~DiveEventItem()
@ -45,45 +47,31 @@ struct event *DiveEventItem::getEventMutable()
return ev; return ev;
} }
void DiveEventItem::setupPixmap(struct gasmix lastgasmix, const DivePixmaps &pixmaps) QPixmap DiveEventItem::setupPixmap(const struct dive *dive, const struct event *ev, struct gasmix lastgasmix, const DivePixmaps &pixmaps)
{ {
event_severity severity = get_event_severity(ev); event_severity severity = get_event_severity(ev);
if (empty_string(ev->name)) { if (empty_string(ev->name))
setPixmap(pixmaps.warning); return pixmaps.warning;
} else if (same_string_caseinsensitive(ev->name, "modechange")) {
if (ev->value == 0) if (same_string_caseinsensitive(ev->name, "modechange"))
setPixmap(pixmaps.bailout); return ev->value == 0 ? pixmaps.bailout : pixmaps.onCCRLoop;
else
setPixmap(pixmaps.onCCRLoop); if (ev->type == SAMPLE_EVENT_BOOKMARK)
} else if (ev->type == SAMPLE_EVENT_BOOKMARK) { return pixmaps.bookmark;
setPixmap(pixmaps.bookmark);
setOffset(QPointF(0.0, -pixmap().height())); if (event_is_gaschange(ev)) {
} else if (event_is_gaschange(ev)) {
struct gasmix mix = get_gasmix_from_event(dive, ev); struct gasmix mix = get_gasmix_from_event(dive, ev);
struct icd_data icd_data; struct icd_data icd_data;
bool icd = isobaric_counterdiffusion(lastgasmix, mix, &icd_data); bool icd = isobaric_counterdiffusion(lastgasmix, mix, &icd_data);
if (mix.he.permille) { if (mix.he.permille)
if (icd) return icd ? pixmaps.gaschangeTrimixICD : pixmaps.gaschangeTrimix;
setPixmap(pixmaps.gaschangeTrimixICD); if (gasmix_is_air(mix))
else return icd ? pixmaps.gaschangeAirICD : pixmaps.gaschangeAir;
setPixmap(pixmaps.gaschangeTrimix); if (mix.o2.permille == 1000)
} else if (gasmix_is_air(mix)) { return icd ? pixmaps.gaschangeOxygenICD : pixmaps.gaschangeOxygen;
if (icd) return icd ? pixmaps.gaschangeEANICD : pixmaps.gaschangeEAN;
setPixmap(pixmaps.gaschangeAirICD);
else
setPixmap(pixmaps.gaschangeAir);
} else if (mix.o2.permille == 1000) {
if (icd)
setPixmap(pixmaps.gaschangeOxygenICD);
else
setPixmap(pixmaps.gaschangeOxygen);
} else {
if (icd)
setPixmap(pixmaps.gaschangeEANICD);
else
setPixmap(pixmaps.gaschangeEAN);
} }
} else if ((((ev->flags & SAMPLE_FLAGS_SEVERITY_MASK) >> SAMPLE_FLAGS_SEVERITY_SHIFT) == 1) || if ((((ev->flags & SAMPLE_FLAGS_SEVERITY_MASK) >> SAMPLE_FLAGS_SEVERITY_SHIFT) == 1) ||
// those are useless internals of the dive computer // those are useless internals of the dive computer
same_string_caseinsensitive(ev->name, "heading") || same_string_caseinsensitive(ev->name, "heading") ||
(same_string_caseinsensitive(ev->name, "SP change") && ev->time.seconds == 0)) { (same_string_caseinsensitive(ev->name, "SP change") && ev->time.seconds == 0)) {
@ -94,35 +82,35 @@ void DiveEventItem::setupPixmap(struct gasmix lastgasmix, const DivePixmaps &pix
// so set an "almost invisible" pixmap (a narrow but somewhat tall, basically transparent pixmap) // so set an "almost invisible" pixmap (a narrow but somewhat tall, basically transparent pixmap)
// that allows tooltips to work when we don't want to show a specific // that allows tooltips to work when we don't want to show a specific
// pixmap for an event, but want to show the event value in the tooltip // pixmap for an event, but want to show the event value in the tooltip
setPixmap(pixmaps.transparent); return pixmaps.transparent;
} else if (severity == EVENT_SEVERITY_INFO) { }
setPixmap(pixmaps.info); if (severity == EVENT_SEVERITY_INFO)
} else if (severity == EVENT_SEVERITY_WARN) { return pixmaps.info;
setPixmap(pixmaps.warning); if (severity == EVENT_SEVERITY_WARN)
} else if (severity == EVENT_SEVERITY_ALARM) { return pixmaps.warning;
setPixmap(pixmaps.violation); if (severity == EVENT_SEVERITY_ALARM)
} else if (same_string_caseinsensitive(ev->name, "violation") || // generic libdivecomputer return pixmaps.violation;
if (same_string_caseinsensitive(ev->name, "violation") || // generic libdivecomputer
same_string_caseinsensitive(ev->name, "Safety stop violation") || // the rest are from the Uemis downloader same_string_caseinsensitive(ev->name, "Safety stop violation") || // the rest are from the Uemis downloader
same_string_caseinsensitive(ev->name, "pO₂ ascend alarm") || same_string_caseinsensitive(ev->name, "pO₂ ascend alarm") ||
same_string_caseinsensitive(ev->name, "RGT alert") || same_string_caseinsensitive(ev->name, "RGT alert") ||
same_string_caseinsensitive(ev->name, "Dive time alert") || same_string_caseinsensitive(ev->name, "Dive time alert") ||
same_string_caseinsensitive(ev->name, "Low battery alert") || same_string_caseinsensitive(ev->name, "Low battery alert") ||
same_string_caseinsensitive(ev->name, "Speed alarm")) { same_string_caseinsensitive(ev->name, "Speed alarm"))
setPixmap(pixmaps.violation); return pixmaps.violation;
} else if (same_string_caseinsensitive(ev->name, "non stop time") || // generic libdivecomputer if (same_string_caseinsensitive(ev->name, "non stop time") || // generic libdivecomputer
same_string_caseinsensitive(ev->name, "safety stop") || same_string_caseinsensitive(ev->name, "safety stop") ||
same_string_caseinsensitive(ev->name, "safety stop (voluntary)") || same_string_caseinsensitive(ev->name, "safety stop (voluntary)") ||
same_string_caseinsensitive(ev->name, "Tank change suggested") || // Uemis downloader same_string_caseinsensitive(ev->name, "Tank change suggested") || // Uemis downloader
same_string_caseinsensitive(ev->name, "Marker")) { same_string_caseinsensitive(ev->name, "Marker"))
setPixmap(pixmaps.info); return pixmaps.info;
} else {
// we should do some guessing based on the type / name of the event; // we should do some guessing based on the type / name of the event;
// for now they all get the warning icon // for now they all get the warning icon
setPixmap(pixmaps.warning); return pixmaps.warning;
}
} }
void DiveEventItem::setupToolTipString(struct gasmix lastgasmix) QString DiveEventItem::setupToolTipString(const struct dive *dive, const struct event *ev, struct gasmix lastgasmix)
{ {
// we display the event on screen - so translate // we display the event on screen - so translate
QString name = gettextFromC::tr(ev->name); QString name = gettextFromC::tr(ev->name);
@ -166,7 +154,7 @@ void DiveEventItem::setupToolTipString(struct gasmix lastgasmix)
name += ev->flags & SAMPLE_FLAGS_BEGIN ? tr(" begin", "Starts with space!") : name += ev->flags & SAMPLE_FLAGS_BEGIN ? tr(" begin", "Starts with space!") :
ev->flags & SAMPLE_FLAGS_END ? tr(" end", "Starts with space!") : ""; ev->flags & SAMPLE_FLAGS_END ? tr(" end", "Starts with space!") : "";
} }
setToolTip(QString("<img height=\"16\" src=\":status-warning-icon\">&nbsp; ") + name); return name;
} }
void DiveEventItem::eventVisibilityChanged(const QString&, bool) void DiveEventItem::eventVisibilityChanged(const QString&, bool)
@ -228,7 +216,6 @@ void DiveEventItem::recalculatePos()
hide(); hide();
return; return;
} }
setVisible(!ev->hidden && !is_event_type_hidden(ev));
double x = hAxis->posAtValue(ev->time.seconds); double x = hAxis->posAtValue(ev->time.seconds);
double y = vAxis->posAtValue(depth); double y = vAxis->posAtValue(depth);
setPos(x, y); setPos(x, y);

View File

@ -2,15 +2,16 @@
#ifndef DIVEEVENTITEM_H #ifndef DIVEEVENTITEM_H
#define DIVEEVENTITEM_H #define DIVEEVENTITEM_H
#include "divepixmapitem.h" #include <QCoreApplication> // for Q_DECLARE_TR_FUNCTIONS
#include <QGraphicsPixmapItem>
class DiveCartesianAxis; class DiveCartesianAxis;
class DivePixmaps; class DivePixmaps;
struct event; struct event;
struct plot_info; struct plot_info;
class DiveEventItem : public DivePixmapItem { class DiveEventItem : public QGraphicsPixmapItem {
Q_OBJECT Q_DECLARE_TR_FUNCTIONS(DiveEventItem)
public: public:
DiveEventItem(const struct dive *d, struct event *ev, struct gasmix lastgasmix, DiveEventItem(const struct dive *d, struct event *ev, struct gasmix lastgasmix,
const struct plot_info &pi, DiveCartesianAxis *hAxis, DiveCartesianAxis *vAxis, const struct plot_info &pi, DiveCartesianAxis *hAxis, DiveCartesianAxis *vAxis,
@ -24,10 +25,11 @@ public:
static bool isInteresting(const struct dive *d, const struct divecomputer *dc, static bool isInteresting(const struct dive *d, const struct divecomputer *dc,
const struct event *ev, const struct plot_info &pi, const struct event *ev, const struct plot_info &pi,
int firstSecond, int lastSecond); int firstSecond, int lastSecond);
const QString text;
const QPixmap pixmap;
private: private:
void setupToolTipString(struct gasmix lastgasmix); static QString setupToolTipString(const struct dive *d, const struct event *ev, struct gasmix lastgasmix);
void setupPixmap(struct gasmix lastgasmix, const DivePixmaps &pixmaps); static QPixmap setupPixmap(const struct dive *d, const struct event *ev, struct gasmix lastgasmix, const DivePixmaps &pixmaps);
void recalculatePos(); void recalculatePos();
DiveCartesianAxis *vAxis; DiveCartesianAxis *vAxis;
DiveCartesianAxis *hAxis; DiveCartesianAxis *hAxis;

View File

@ -1,95 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#include "divehandler.h"
#include "profilewidget2.h"
#include "profilescene.h"
#include "core/dive.h"
#include "core/gettextfromc.h"
#include "core/qthelper.h"
#include "qt-models/diveplannermodel.h"
#include <QMenu>
#include <QGraphicsSceneMouseEvent>
#include <QSettings>
DiveHandler::DiveHandler(const struct dive *d) : dive(d)
{
setRect(-5, -5, 10, 10);
setFlags(ItemIgnoresTransformations | ItemIsSelectable | ItemIsMovable | ItemSendsGeometryChanges);
setBrush(Qt::white);
setZValue(2);
t.start();
}
int DiveHandler::parentIndex()
{
ProfileWidget2 *view = qobject_cast<ProfileWidget2 *>(scene()->views().first());
return view->handleIndex(this);
}
void DiveHandler::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
QMenu m;
// Don't have a gas selection for the last point
emit released();
DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance();
QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS);
if (index.sibling(index.row() + 1, index.column()).isValid()) {
QStringList gases = get_dive_gas_list(dive);
for (int i = 0; i < gases.size(); i++) {
QAction *action = new QAction(&m);
action->setText(gases[i]);
action->setData(i);
connect(action, &QAction::triggered, this, &DiveHandler::changeGas);
m.addAction(action);
}
}
// don't allow removing the last point
if (plannerModel->rowCount() > 1) {
m.addSeparator();
m.addAction(gettextFromC::tr("Remove this point"), this, &DiveHandler::selfRemove);
m.exec(event->screenPos());
}
}
void DiveHandler::selfRemove()
{
#ifndef SUBSURFACE_MOBILE
setSelected(true);
ProfileWidget2 *view = qobject_cast<ProfileWidget2 *>(scene()->views().first());
view->keyDeleteAction();
#endif
}
void DiveHandler::changeGas()
{
QAction *action = qobject_cast<QAction *>(sender());
DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance();
QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS);
plannerModel->gasChange(index.sibling(index.row() + 1, index.column()), action->data().toInt());
}
void DiveHandler::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (t.elapsed() < 40)
return;
t.start();
ProfileWidget2 *view = qobject_cast<ProfileWidget2*>(scene()->views().first());
if(!view->profileScene->pointOnProfile(event->scenePos()))
return;
QGraphicsEllipseItem::mouseMoveEvent(event);
emit moved();
}
void DiveHandler::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem::mousePressEvent(event);
emit clicked();
}
void DiveHandler::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem::mouseReleaseEvent(event);
emit released();
}

View File

@ -1,35 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef DIVEHANDLER_HPP
#define DIVEHANDLER_HPP
#include <QGraphicsPathItem>
#include <QElapsedTimer>
struct dive;
class DiveHandler : public QObject, public QGraphicsEllipseItem {
Q_OBJECT
public:
DiveHandler(const struct dive *d);
protected:
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event);
void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
signals:
void moved();
void clicked();
void released();
private:
int parentIndex();
public
slots:
void selfRemove();
void changeGas();
private:
const struct dive *dive;
QElapsedTimer t;
};
#endif

View File

@ -1,6 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#include "divelineitem.h"
DiveLineItem::DiveLineItem(QGraphicsItem *parent) : QGraphicsLineItem(parent)
{
}

View File

@ -1,16 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef DIVELINEITEM_H
#define DIVELINEITEM_H
#include <QObject>
#include <QGraphicsLineItem>
class DiveLineItem : public QObject, public QGraphicsLineItem {
Q_OBJECT
Q_PROPERTY(QPointF pos READ pos WRITE setPos)
Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity)
public:
DiveLineItem(QGraphicsItem *parent = 0);
};
#endif // DIVELINEITEM_H

View File

@ -1,107 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#include "profile-widget/divepixmapitem.h"
#include "profile-widget/animationfunctions.h"
#include "core/pref.h"
#include "core/qthelper.h"
#include "core/settings/qPrefDisplay.h"
#include "core/subsurface-qt/divelistnotifier.h"
#include <QDesktopServices>
#include <QPen>
#include <QUrl>
#include <QGraphicsSceneMouseEvent>
DivePixmapItem::DivePixmapItem(QGraphicsItem *parent) : QGraphicsPixmapItem(parent)
{
}
CloseButtonItem::CloseButtonItem(QGraphicsItem *parent): DivePixmapItem(parent)
{
static QPixmap p = QPixmap(":list-remove-icon");
setPixmap(p);
setFlag(ItemIgnoresTransformations);
}
void CloseButtonItem::mousePressEvent(QGraphicsSceneMouseEvent *)
{
emit clicked();
}
DivePictureItem::DivePictureItem(QGraphicsItem *parent): DivePixmapItem(parent),
canvas(new QGraphicsRectItem(this)),
shadow(new QGraphicsRectItem(this)),
button(new CloseButtonItem(this)),
baseZValue(0.0)
{
setFlag(ItemIgnoresTransformations);
setAcceptHoverEvents(true);
setScale(0.2);
connect(&diveListNotifier, &DiveListNotifier::settingsChanged, this, &DivePictureItem::settingsChanged);
connect(button, &CloseButtonItem::clicked, [this] () { emit removePicture(fileUrl); });
canvas->setPen(Qt::NoPen);
canvas->setBrush(QColor(Qt::white));
canvas->setFlag(ItemStacksBehindParent);
canvas->setZValue(-1);
shadow->setPos(5,5);
shadow->setPen(Qt::NoPen);
shadow->setBrush(QColor(Qt::lightGray));
shadow->setFlag(ItemStacksBehindParent);
shadow->setZValue(-2);
button->setScale(0.2);
button->setZValue(7);
button->hide();
}
// The base z-value is used for correct paint-order of the thumbnails. On hoverEnter the z-value is raised
// so that the thumbnail is drawn on top of all other thumbnails and on hoverExit it is restored to the base value.
void DivePictureItem::setBaseZValue(double z)
{
baseZValue = z;
setZValue(z);
}
void DivePictureItem::settingsChanged()
{
setVisible(prefs.show_pictures_in_profile);
}
void DivePictureItem::setPixmap(const QPixmap &pix)
{
DivePixmapItem::setPixmap(pix);
QRectF r = boundingRect();
canvas->setRect(0 - 10, 0 -10, r.width() + 20, r.height() + 20);
shadow->setRect(canvas->rect());
button->setPos(boundingRect().width() - button->boundingRect().width() * 0.2,
boundingRect().height() - button->boundingRect().height() * 0.2);
}
void DivePictureItem::hoverEnterEvent(QGraphicsSceneHoverEvent*)
{
Animations::scaleTo(this, qPrefDisplay::animation_speed(), 1.0);
setZValue(baseZValue + 5.0);
button->setOpacity(0);
button->show();
Animations::show(button, qPrefDisplay::animation_speed());
}
void DivePictureItem::setFileUrl(const QString &s)
{
fileUrl = s;
}
void DivePictureItem::hoverLeaveEvent(QGraphicsSceneHoverEvent*)
{
Animations::scaleTo(this, qPrefDisplay::animation_speed(), 0.2);
setZValue(baseZValue);
Animations::hide(button, qPrefDisplay::animation_speed());
}
void DivePictureItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
QDesktopServices::openUrl(QUrl::fromLocalFile(localFilePath(fileUrl)));
}

View File

@ -1,51 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef DIVEPIXMAPITEM_H
#define DIVEPIXMAPITEM_H
#include <QObject>
#include <QGraphicsPixmapItem>
class DivePixmapItem : public QObject, public QGraphicsPixmapItem {
Q_OBJECT
Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity)
Q_PROPERTY(QPointF pos WRITE setPos READ pos)
Q_PROPERTY(qreal x WRITE setX READ x)
Q_PROPERTY(qreal y WRITE setY READ y)
public:
DivePixmapItem(QGraphicsItem *parent = 0);
};
class CloseButtonItem : public DivePixmapItem {
Q_OBJECT
public:
CloseButtonItem(QGraphicsItem *parent = 0);
signals:
void clicked();
private:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
};
class DivePictureItem : public DivePixmapItem {
Q_OBJECT
Q_PROPERTY(qreal scale WRITE setScale READ scale)
public:
DivePictureItem(QGraphicsItem *parent = 0);
void setPixmap(const QPixmap& pix);
void setBaseZValue(double z);
void setFileUrl(const QString& s);
signals:
void removePicture(const QString &fileUrl);
public slots:
void settingsChanged();
private:
void hoverEnterEvent(QGraphicsSceneHoverEvent *event);
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
QString fileUrl;
QGraphicsRectItem *canvas;
QGraphicsRectItem *shadow;
CloseButtonItem *button;
double baseZValue;
};
#endif // DIVEPIXMAPITEM_H

View File

@ -9,7 +9,8 @@
#include "core/settings/qPrefTechnicalDetails.h" #include "core/settings/qPrefTechnicalDetails.h"
#include "core/settings/qPrefLog.h" #include "core/settings/qPrefLog.h"
#include "libdivecomputer/parser.h" #include "libdivecomputer/parser.h"
#include "profile-widget/profilewidget2.h"
#include <QPainter>
AbstractProfilePolygonItem::AbstractProfilePolygonItem(const plot_info &pInfo, const DiveCartesianAxis &horizontal, AbstractProfilePolygonItem::AbstractProfilePolygonItem(const plot_info &pInfo, const DiveCartesianAxis &horizontal,
const DiveCartesianAxis &vertical, DataAccessor accessor, const DiveCartesianAxis &vertical, DataAccessor accessor,

View File

@ -5,8 +5,6 @@
#include <QGraphicsPolygonItem> #include <QGraphicsPolygonItem>
#include <memory> #include <memory>
#include "divelineitem.h"
/* This is the Profile Item, it should be used for quite a lot of things /* This is the Profile Item, it should be used for quite a lot of things
on the profile view. The usage should be pretty simple: on the profile view. The usage should be pretty simple:

View File

@ -1,6 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#include "diverectitem.h"
DiveRectItem::DiveRectItem(QObject *parent, QGraphicsItem *parentItem) : QObject(parent), QGraphicsRectItem(parentItem)
{
}

View File

@ -1,18 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef DIVERECTITEM_H
#define DIVERECTITEM_H
#include <QObject>
#include <QGraphicsRectItem>
class DiveRectItem : public QObject, public QGraphicsRectItem {
Q_OBJECT
Q_PROPERTY(QRectF rect WRITE setRect READ rect)
Q_PROPERTY(QPointF pos WRITE setPos READ pos)
Q_PROPERTY(qreal x WRITE setX READ x)
Q_PROPERTY(qreal y WRITE setY READ y)
public:
DiveRectItem(QObject *parent = 0, QGraphicsItem *parentItem = 0);
};
#endif // DIVERECTITEM_H

View File

@ -1,11 +1,12 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "divetextitem.h" #include "divetextitem.h"
#include "profilewidget2.h"
#include "core/color.h" #include "core/color.h"
#include "core/errorhelper.h" #include "core/errorhelper.h"
#include <QBrush> #include <cmath>
#include <QApplication> #include <QApplication>
#include <QBrush>
#include <QPainter>
static const double outlineSize = 3.0; static const double outlineSize = 3.0;
@ -89,6 +90,11 @@ double DiveTextItem::fontHeight(double dpr, double scale)
return (double)fm.height(); return (double)fm.height();
} }
double DiveTextItem::width() const
{
return boundingRect().width();
}
double DiveTextItem::height() const double DiveTextItem::height() const
{ {
return fontHeight(dpr, scale) + outlineSize * dpr; return fontHeight(dpr, scale) + outlineSize * dpr;

View File

@ -2,17 +2,12 @@
#ifndef DIVETEXTITEM_H #ifndef DIVETEXTITEM_H
#define DIVETEXTITEM_H #define DIVETEXTITEM_H
#include <QObject>
#include <QFont>
#include <QGraphicsPixmapItem> #include <QGraphicsPixmapItem>
class QBrush; class QBrush;
/* A Line Item that has animated-properties. */ /* A Line Item that has animated-properties. */
class DiveTextItem : public QObject, public QGraphicsPixmapItem { class DiveTextItem : public QGraphicsPixmapItem {
Q_OBJECT
Q_PROPERTY(QPointF pos READ pos WRITE setPos)
Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity)
public: public:
// Note: vertical centring is based on the actual rendered text, not on the font metrics. // Note: vertical centring is based on the actual rendered text, not on the font metrics.
// This is fine for placing text in the "tankbar", but it will look disastrous when // This is fine for placing text in the "tankbar", but it will look disastrous when
@ -22,6 +17,7 @@ public:
const QString &text(); const QString &text();
static double fontHeight(double dpr, double scale); static double fontHeight(double dpr, double scale);
static std::pair<double, double> getLabelSize(double dpr, double scale, const QString &label); static std::pair<double, double> getLabelSize(double dpr, double scale, const QString &label);
double width() const;
double height() const; double height() const;
private: private:

View File

@ -1,279 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#include "profile-widget/divetooltipitem.h"
#include "profile-widget/divecartesianaxis.h"
#include "core/membuffer.h"
#include "core/metrics.h"
#include "core/settings/qPrefDisplay.h"
#include "core/settings/qPrefTechnicalDetails.h"
#include <QPropertyAnimation>
#include <QGraphicsView>
#include "core/qthelper.h"
void ToolTipItem::addToolTip(const QString &toolTip, const QPixmap &pixmap)
{
const IconMetrics &iconMetrics = defaultIconMetrics();
QGraphicsPixmapItem *iconItem = 0;
double yValue = title->boundingRect().height() + iconMetrics.spacing;
for (ToolTip t: toolTips)
yValue += t.second->boundingRect().height();
if (entryToolTip.second)
yValue += entryToolTip.second->boundingRect().height();
iconItem = new QGraphicsPixmapItem(this);
if (!pixmap.isNull())
iconItem->setPixmap(pixmap);
const int sp2 = iconMetrics.spacing * 2;
iconItem->setPos(sp2, yValue);
QGraphicsTextItem *textItem = new QGraphicsTextItem(this);
textItem->setHtml(toolTip);
textItem->setPos(sp2 + iconMetrics.sz_small + sp2, yValue);
textItem->setDefaultTextColor(Qt::white);
textItem->setFlag(ItemIgnoresTransformations);
toolTips.push_back(qMakePair(iconItem, textItem));
}
void ToolTipItem::clear()
{
for (ToolTip t: toolTips) {
delete t.first;
delete t.second;
}
toolTips.clear();
}
void ToolTipItem::setRect(const QRectF &r)
{
if (r == rect())
return;
QGraphicsRectItem::setRect(r);
updateTitlePosition();
}
void ToolTipItem::collapse()
{
int dim = defaultIconMetrics().sz_small;
if (qPrefDisplay::animation_speed()) {
QPropertyAnimation *animation = new QPropertyAnimation(this, "rect");
animation->setDuration(100);
animation->setStartValue(nextRectangle);
animation->setEndValue(QRect(0, 0, dim, dim));
animation->start(QAbstractAnimation::DeleteWhenStopped);
} else {
setRect(nextRectangle);
}
clear();
status = COLLAPSED;
}
void ToolTipItem::expand()
{
if (!title)
return;
const IconMetrics &iconMetrics = defaultIconMetrics();
double width = 0, height = title->boundingRect().height() + iconMetrics.spacing;
for (const ToolTip &t: toolTips) {
QRectF sRect = t.second->boundingRect();
if (sRect.width() > width)
width = sRect.width();
height += sRect.height();
}
if (entryToolTip.first) {
QRectF sRect = entryToolTip.second->boundingRect();
if (sRect.width() > width)
width = sRect.width();
height += sRect.height();
}
const int sp2 = iconMetrics.spacing * 2;
// pixmap left padding, icon, pixmap right padding, right padding
width += sp2 + iconMetrics.sz_small + sp2 + sp2 * 2;
// bottom padding
height += sp2;
// clip the tooltip width
if (width < title->boundingRect().width() + sp2)
width = title->boundingRect().width() + sp2;
// clip the height
if (entryToolTip.first) {
const int minH = lrint(entryToolTip.first->y() + entryToolTip.first->pixmap().height() + sp2);
if (height < minH)
height = minH;
} else if (height < iconMetrics.sz_small) {
height = iconMetrics.sz_small;
}
nextRectangle.setWidth(width);
nextRectangle.setHeight(height);
if (nextRectangle != rect()) {
if (qPrefDisplay::animation_speed()) {
QPropertyAnimation *animation = new QPropertyAnimation(this, "rect", this);
animation->setDuration(prefs.animation_speed);
animation->setStartValue(rect());
animation->setEndValue(nextRectangle);
animation->start(QAbstractAnimation::DeleteWhenStopped);
} else {
setRect(nextRectangle);
}
}
status = EXPANDED;
}
ToolTipItem::ToolTipItem(QGraphicsItem *parent) : RoundRectItem(8.0, parent),
title(new QGraphicsSimpleTextItem(tr("Information"), this)),
status(COLLAPSED),
tissues(16,60),
painter(&tissues),
timeAxis(0),
lastTime(-1)
{
clearPlotInfo();
entryToolTip.first = NULL;
entryToolTip.second = NULL;
setFlags(ItemIgnoresTransformations | ItemIsMovable | ItemClipsChildrenToShape);
QColor c = QColor(Qt::black);
c.setAlpha(155);
setBrush(c);
setZValue(99);
addToolTip(QString(), QPixmap(16,60));
entryToolTip = toolTips.first();
toolTips.clear();
title->setFlag(ItemIgnoresTransformations);
title->setPen(QPen(Qt::white, 1));
title->setBrush(Qt::white);
setPen(QPen(Qt::white, 2));
connect(qPrefTechnicalDetails::instance(), &qPrefTechnicalDetails::infoboxChanged, this, &ToolTipItem::settingsChanged);
refreshTime.start();
}
ToolTipItem::~ToolTipItem()
{
clear();
}
void ToolTipItem::updateTitlePosition()
{
const IconMetrics &iconMetrics = defaultIconMetrics();
if (rect().width() < title->boundingRect().width() + iconMetrics.spacing * 4) {
QRectF newRect = rect();
newRect.setWidth(title->boundingRect().width() + iconMetrics.spacing * 4);
newRect.setHeight((newRect.height() && isExpanded()) ? newRect.height() : iconMetrics.sz_small);
setRect(newRect);
}
title->setPos(rect().width() / 2 - title->boundingRect().width() / 2 - 1, 0);
}
bool ToolTipItem::isExpanded() const
{
return status == EXPANDED;
}
void ToolTipItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
persistPos();
QGraphicsRectItem::mouseReleaseEvent(event);
for (QGraphicsItem *item: oldSelection)
item->setSelected(true);
}
void ToolTipItem::persistPos() const
{
qPrefDisplay::set_tooltip_position(pos());
}
void ToolTipItem::readPos()
{
QPointF value = qPrefDisplay::tooltip_position();
if (!scene()->sceneRect().contains(value))
value = QPointF(0, 0);
setPos(value);
}
void ToolTipItem::setPlotInfo(const plot_info &plot)
{
pInfo = plot;
}
void ToolTipItem::clearPlotInfo()
{
memset(&pInfo, 0, sizeof(pInfo));
}
void ToolTipItem::setTimeAxis(DiveCartesianAxis *axis)
{
timeAxis = axis;
}
void ToolTipItem::refresh(const dive *d, const QPointF &pos, bool inPlanner)
{
struct membufferpp mb;
if(refreshTime.elapsed() < 40)
return;
refreshTime.start();
int time = lrint(timeAxis->valueAt(pos));
lastTime = time;
clear();
int idx = get_plot_details_new(d, &pInfo, time, &mb);
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) {
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);
entryToolTip.second->setPlainText(QString::fromUtf8(mb.buffer, mb.len));
}
entryToolTip.first->setPixmap(tissues);
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());
}
expand();
}
void ToolTipItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
oldSelection = scene()->selectedItems();
scene()->clearSelection();
QGraphicsItem::mousePressEvent(event);
}
void ToolTipItem::settingsChanged(bool value)
{
setVisible(value);
}

View File

@ -1,73 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef DIVETOOLTIPITEM_H
#define DIVETOOLTIPITEM_H
#include <QVector>
#include <QPair>
#include <QRectF>
#include <QIcon>
#include <QElapsedTimer>
#include <QPainter>
#include "backend-shared/roundrectitem.h"
#include "core/profile.h"
struct dive;
class DiveCartesianAxis;
class QGraphicsLineItem;
class QGraphicsSimpleTextItem;
class QGraphicsPixmapItem;
/* To use a tooltip, simply ->setToolTip on the QGraphicsItem that you want
* or, if it's a "global" tooltip, set it on the mouseMoveEvent of the ProfileGraphicsView.
*/
class ToolTipItem : public QObject, public RoundRectItem {
Q_OBJECT
Q_PROPERTY(QRectF rect READ rect WRITE setRect)
public:
enum Status {
COLLAPSED,
EXPANDED
};
explicit ToolTipItem(QGraphicsItem *parent = 0);
~ToolTipItem();
void refresh(const dive *d, const QPointF &pos, bool inPlanner);
void readPos();
void mousePressEvent(QGraphicsSceneMouseEvent *event);
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
void setTimeAxis(DiveCartesianAxis *axis);
void setPlotInfo(const plot_info &plot);
void clearPlotInfo();
void settingsChanged(bool value);
public
slots:
void setRect(const QRectF &rect);
private:
typedef QPair<QGraphicsPixmapItem *, QGraphicsTextItem *> ToolTip;
QVector<ToolTip> toolTips;
ToolTip entryToolTip;
QGraphicsSimpleTextItem *title;
Status status;
QPixmap tissues;
QPainter painter;
QRectF rectangle;
QRectF nextRectangle;
DiveCartesianAxis *timeAxis;
plot_info pInfo;
int lastTime;
QElapsedTimer refreshTime;
QList<QGraphicsItem*> oldSelection;
void addToolTip(const QString &toolTip, const QPixmap &pixmap);
void collapse();
void expand();
void clear();
bool isExpanded() const;
void persistPos() const;
void updateTitlePosition();
};
#endif // DIVETOOLTIPITEM_H

View File

@ -0,0 +1,153 @@
// SPDX-License-Identifier: GPL-2.0
#include "handleitem.h"
#include "profileview.h"
#include "zvalues.h"
#include <QApplication>
static QColor handleBorderColor(Qt::black);
static QColor handleColor(Qt::white);
static QColor gasColor(Qt::black);
static constexpr double handleRadius = 5.0;
class HandleItemHandle : public ChartDiskItem {
ProfileView &profileView;
int idx;
public:
HandleItemHandle(ChartView &view, double dpr, int idx, ProfileView &profileView) :
ChartDiskItem(view,
ProfileZValue::Handles,
QPen(handleBorderColor, dpr),
QBrush(handleColor),
true),
profileView(profileView),
idx(idx)
{
}
void setIdx(int idxIn)
{
idx = idxIn;
}
void drag(QPointF pos) override
{
profileView.handleDragged(idx, pos);
}
void startDrag(QPointF) override
{
profileView.handleSelected(idx);
}
void stopDrag(QPointF) override
{
profileView.handleReleased(idx);
}
};
HandleItem::HandleItem(ProfileView &view, double dpr, int idx) :
handle(view.createChartItem<HandleItemHandle>(dpr, idx, view)),
dpr(dpr),
view(view)
{
handle->resize(handleRadius * dpr);
}
HandleItem::~HandleItem()
{
}
void HandleItem::del()
{
handle.del();
if (text)
text.del();
}
void HandleItem::setIdx(int idx)
{
handle->setIdx(idx);
}
void HandleItem::setPos(QPointF pos)
{
handle->setPos(pos);
}
QPointF HandleItem::getPos() const
{
return handle->getPos();
}
void HandleItem::setTextPos(QPointF pos)
{
if (text)
text->setPos(pos);
}
void HandleItem::setVisible(bool handleVisible, bool textVisible)
{
handle->setVisible(handleVisible);
if (text)
text->setVisible(textVisible);
}
// duplicate code in tooltipitem.cpp
static QFont makeFont(double dpr)
{
QFont font(qApp->font());
if (dpr != 1.0) {
int pixelSize = font.pixelSize();
if (pixelSize > 0) {
pixelSize = lrint(static_cast<double>(pixelSize) * dpr);
font.setPixelSize(pixelSize);
} else {
font.setPointSizeF(font.pointSizeF() * dpr);
}
}
return font;
}
void HandleItem::setText(const QString &s)
{
if (text && std::exchange(oldText, s) == s)
return;
if (text)
text.del();
QFont f = makeFont(dpr);
text = view.createChartItem<ChartTextItem>(ProfileZValue::Handles, f, s);
text->setColor(gasColor);
}
/*
void HandleItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
QMenu m;
// Don't have a gas selection for the last point
emit released();
DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance();
QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS);
if (index.sibling(index.row() + 1, index.column()).isValid()) {
QStringList gases = get_dive_gas_list(dive);
for (int i = 0; i < gases.size(); i++) {
QAction *action = new QAction(&m);
action->setText(gases[i]);
action->setData(i);
connect(action, &QAction::triggered, this, &HandleItem::changeGas);
m.addAction(action);
}
}
// don't allow removing the last point
if (plannerModel->rowCount() > 1) {
m.addSeparator();
m.addAction(gettextFromC::tr("Remove this point"), this, &HandleItem::selfRemove);
m.exec(event->screenPos());
}
}
void HandleItem::changeGas()
{
QAction *action = qobject_cast<QAction *>(sender());
DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance();
QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS);
plannerModel->gasChange(index.sibling(index.row() + 1, index.column()), action->data().toInt());
}
*/

View File

@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef DIVEHANDLER_HPP
#define DIVEHANDLER_HPP
#include "qt-quick/chartitem.h"
class HandleItemHandle;
class HandleItemText;
class ProfileView;
class HandleItem {
ChartItemPtr<HandleItemHandle> handle;
ChartItemPtr<ChartTextItem> text;
QString oldText;
public:
HandleItem(ProfileView &view, double dpr, int idx);
~HandleItem();
void setVisible(bool handle, bool text);
void setPos(QPointF point);
QPointF getPos() const;
void setTextPos(QPointF point);
void setText(const QString &text);
void setIdx(int idx); // we may have to rearrange the handles when editing the dive
void del(); // Deletes objects - must not be used any longer
private:
double dpr;
ProfileView &view;
protected:
//void contextMenuEvent(QGraphicsSceneContextMenuEvent *event);
//void mousePressEvent(QGraphicsSceneMouseEvent *event);
//void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
public
slots:
//void selfRemove();
//void changeGas();
};
#endif

View 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();
}

View 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

View File

@ -9,6 +9,7 @@
#include "tankitem.h" #include "tankitem.h"
#include "core/device.h" #include "core/device.h"
#include "core/event.h" #include "core/event.h"
#include "core/eventtype.h"
#include "core/pref.h" #include "core/pref.h"
#include "core/profile.h" #include "core/profile.h"
#include "core/qthelper.h" // for decoMode() #include "core/qthelper.h" // for decoMode()
@ -16,37 +17,9 @@
#include "core/subsurface-string.h" #include "core/subsurface-string.h"
#include "core/settings/qPrefDisplay.h" #include "core/settings/qPrefDisplay.h"
#include "qt-models/diveplannermodel.h" #include "qt-models/diveplannermodel.h"
#include <QAbstractAnimation>
static const double diveComputerTextBorder = 1.0; static const double diveComputerTextBorder = 1.0;
// Class for animations (if any). Might want to do our own.
class ProfileAnimation : public QAbstractAnimation {
ProfileScene &scene;
// For historical reasons, speed is actually the duration
// (i.e. the reciprocal of speed). Ouch, that hurts.
int speed;
int duration() const override
{
return speed;
}
void updateCurrentTime(int time) override
{
// Note: we explicitly pass 1.0 at the end, so that
// the callee can do a simple float comparison for "end".
scene.anim(time == speed ? 1.0
: static_cast<double>(time) / speed);
}
public:
ProfileAnimation(ProfileScene &scene, int animSpeed) :
scene(scene),
speed(animSpeed)
{
start();
}
};
template<typename T, class... Args> template<typename T, class... Args>
T *ProfileScene::createItem(const DiveCartesianAxis &vAxis, DataAccessor accessor, int z, Args&&... args) T *ProfileScene::createItem(const DiveCartesianAxis &vAxis, DataAccessor accessor, int z, Args&&... args)
{ {
@ -120,6 +93,7 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) :
[](const plot_data &item) { return 0.0; }, // unused [](const plot_data &item) { return 0.0; }, // unused
1, dpr)), 1, dpr)),
diveComputerText(new DiveTextItem(dpr, 1.0, Qt::AlignRight | Qt::AlignTop, nullptr)), diveComputerText(new DiveTextItem(dpr, 1.0, Qt::AlignRight | Qt::AlignTop, nullptr)),
eventsHiddenText(new DiveTextItem(dpr, 1.0, Qt::AlignRight | Qt::AlignTop, nullptr)),
reportedCeiling(createItem<DiveReportedCeiling>(*profileYAxis, reportedCeiling(createItem<DiveReportedCeiling>(*profileYAxis,
[](const plot_data &item) { return (double)item.ceiling; }, [](const plot_data &item) { return (double)item.ceiling; },
1, dpr)), 1, dpr)),
@ -170,6 +144,7 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) :
// Add items to scene // Add items to scene
addItem(diveComputerText); addItem(diveComputerText);
addItem(eventsHiddenText);
addItem(tankItem); addItem(tankItem);
addItem(decoModelParameters); addItem(decoModelParameters);
addItem(profileYAxis); addItem(profileYAxis);
@ -183,6 +158,8 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) :
for (AbstractProfilePolygonItem *item: profileItems) for (AbstractProfilePolygonItem *item: profileItems)
addItem(item); addItem(item);
eventsHiddenText->set(tr("Hidden events!"), getColor(TIME_TEXT, isGrayscale));
} }
ProfileScene::~ProfileScene() ProfileScene::~ProfileScene()
@ -190,15 +167,16 @@ ProfileScene::~ProfileScene()
free_plot_info_data(&plotInfo); free_plot_info_data(&plotInfo);
} }
const plot_info &ProfileScene::getPlotInfo() const
{
return plotInfo;
}
void ProfileScene::clear() void ProfileScene::clear()
{ {
for (AbstractProfilePolygonItem *item: profileItems) for (AbstractProfilePolygonItem *item: profileItems)
item->clear(); item->clear();
// the events will have connected slots which can fire after
// the dive and its data have been deleted - so explictly delete
// the DiveEventItems
qDeleteAll(eventItems);
eventItems.clear(); eventItems.clear();
free_plot_info_data(&plotInfo); free_plot_info_data(&plotInfo);
empty = true; empty = true;
@ -314,8 +292,10 @@ void ProfileScene::updateAxes(bool diveHasHeartBeat, bool simplified)
profileYAxis->setGridIsMultipleOfThree( qPrefDisplay::three_m_based_grid() ); profileYAxis->setGridIsMultipleOfThree( qPrefDisplay::three_m_based_grid() );
// Place the fixed dive computer text at the bottom // Place the fixed dive computer text at the bottom
double bottomBorder = sceneRect().height() - diveComputerText->height() - 2.0 * dpr * diveComputerTextBorder; double bottomTextHeight = std::max(diveComputerText->height(), eventsHiddenText->height());
diveComputerText->setPos(0.0, bottomBorder + dpr * diveComputerTextBorder); double bottomBorder = sceneRect().height() - bottomTextHeight - 2.0 * dpr * diveComputerTextBorder;
diveComputerText->setPos(0.0, round(bottomBorder + dpr * diveComputerTextBorder));
eventsHiddenText->setPos(width - dpr * diveComputerTextBorder - eventsHiddenText->width(), bottomBorder + dpr * diveComputerTextBorder);
double topBorder = 0.0; double topBorder = 0.0;
@ -394,6 +374,17 @@ bool ProfileScene::pointOnProfile(const QPointF &point) const
return timeAxis->pointInRange(point.x()) && profileYAxis->pointInRange(point.y()); return timeAxis->pointInRange(point.x()) && profileYAxis->pointInRange(point.y());
} }
bool ProfileScene::pointOnDiveComputerText(const QPointF &point) const
{
return diveComputerText->boundingRect().contains(point - diveComputerText->pos());
}
bool ProfileScene::pointOnEventsHiddenText(const QPointF &point) const
{
return eventsHiddenText->isVisible() &&
eventsHiddenText->boundingRect().contains(point - eventsHiddenText->pos());
}
static double max_gas(const plot_info &pi, double gas_pressures::*gas) static double max_gas(const plot_info &pi, double gas_pressures::*gas)
{ {
double ret = -1; double ret = -1;
@ -404,8 +395,9 @@ 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, DivePlannerPointsModel *plannerModel, void ProfileScene::plotDive(const struct dive *dIn, int dcIn, int animSpeed, bool simplified,
bool inPlanner, bool instant, bool keepPlotInfo, bool calcMax, double zoom, double zoomedPosition) DivePlannerPointsModel *plannerModel,
bool inPlanner, bool keepPlotInfo, bool calcMax, double zoom, double zoomedPosition)
{ {
d = dIn; d = dIn;
dc = dcIn; dc = dcIn;
@ -439,8 +431,6 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
keepPlotInfo = false; keepPlotInfo = false;
empty = false; empty = false;
int animSpeed = instant || printMode ? 0 : qPrefDisplay::animation_speed();
// A non-null planner_ds signals to create_plot_info_new that the dive is currently planned. // A non-null planner_ds signals to create_plot_info_new that the dive is currently planned.
struct deco_state *planner_ds = inPlanner && plannerModel ? &plannerModel->final_deco_state : nullptr; struct deco_state *planner_ds = inPlanner && plannerModel ? &plannerModel->final_deco_state : nullptr;
@ -457,14 +447,13 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
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);
// When we found that we don't have enough place to draw, the state was set to empty.
if (empty)
return;
int newMaxtime = get_maxtime(&plotInfo); int newMaxtime = get_maxtime(&plotInfo);
if (calcMax || newMaxtime > maxtime) if (calcMax || newMaxtime > maxtime)
maxtime = newMaxtime; maxtime = newMaxtime;
@ -546,15 +535,15 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
if (prefs.percentagegraph) if (prefs.percentagegraph)
percentageItem->replot(d, currentdc, plotInfo); percentageItem->replot(d, currentdc, plotInfo);
// The event items are a bit special since we don't know how many events are going to
// exist on a dive, so I cant create cache items for that. that's why they are here
// while all other items are up there on the constructor.
qDeleteAll(eventItems);
eventItems.clear(); eventItems.clear();
struct event *event = currentdc->events;
struct gasmix lastgasmix = get_gasmix_at_time(d, currentdc, duration_t{1}); struct gasmix lastgasmix = get_gasmix_at_time(d, currentdc, duration_t{1});
while (event) { bool has_hidden_events = false;
for (struct event *event = currentdc->events; event; event = event->next) {
if (event->hidden || is_event_type_hidden(event)) {
has_hidden_events = true;
continue;
}
// if print mode is selected only draw headings, SP change, gas events or bookmark event // if print mode is selected only draw headings, SP change, gas events or bookmark event
if (printMode) { if (printMode) {
if (empty_string(event->name) || if (empty_string(event->name) ||
@ -562,21 +551,20 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
(same_string(event->name, "SP change") && event->time.seconds == 0) || (same_string(event->name, "SP change") && event->time.seconds == 0) ||
event_is_gaschange(event) || event_is_gaschange(event) ||
event->type == SAMPLE_EVENT_BOOKMARK)) { event->type == SAMPLE_EVENT_BOOKMARK)) {
event = event->next;
continue; continue;
} }
} }
if (DiveEventItem::isInteresting(d, currentdc, event, plotInfo, firstSecond, lastSecond)) { if (DiveEventItem::isInteresting(d, currentdc, event, plotInfo, firstSecond, lastSecond)) {
auto item = new DiveEventItem(d, event, lastgasmix, plotInfo, auto item = std::make_unique<DiveEventItem>(d, event, lastgasmix, plotInfo,
timeAxis, profileYAxis, animSpeed, *pixmaps); timeAxis, profileYAxis, animSpeed, *pixmaps);
item->setZValue(2); item->setZValue(2);
addItem(item); addItem(item.get());
eventItems.push_back(item); eventItems.push_back(std::move(item));
} }
if (event_is_gaschange(event)) if (event_is_gaschange(event))
lastgasmix = get_gasmix_from_event(d, event); lastgasmix = get_gasmix_from_event(d, event);
event = event->next;
} }
eventsHiddenText->setVisible(has_hidden_events);
QString dcText = get_dc_nickname(currentdc); QString dcText = get_dc_nickname(currentdc);
if (dcText == "planned dive") if (dcText == "planned dive")
@ -589,12 +577,6 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
if (nr > 1) if (nr > 1)
dcText += tr(" (#%1 of %2)").arg(dc + 1).arg(nr); dcText += tr(" (#%1 of %2)").arg(dc + 1).arg(nr);
diveComputerText->set(dcText, getColor(TIME_TEXT, isGrayscale)); diveComputerText->set(dcText, getColor(TIME_TEXT, isGrayscale));
// Reset animation.
if (animSpeed <= 0)
animation.reset();
else
animation = std::make_unique<ProfileAnimation>(*this, animSpeed);
} }
void ProfileScene::anim(double fraction) void ProfileScene::anim(double fraction)
@ -609,7 +591,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, plannerModel, inPlanner, true, 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));
@ -649,3 +631,64 @@ double ProfileScene::calcZoomPosition(double zoom, double originalPos, double de
double newPos = newRelStart / factor; double newPos = newRelStart / factor;
return std::clamp(newPos, 0.0, 1.0); return std::clamp(newPos, 0.0, 1.0);
} }
int ProfileScene::timeAt(QPointF pos) const
{
return lrint(timeAxis->valueAt(pos));
}
int ProfileScene::depthAt(QPointF pos) const
{
return lrint(profileYAxis->valueAt(pos));
}
std::pair<double, double> ProfileScene::minMaxTime() const
{
return { timeAxis->minimum(), timeAxis->maximum() };
}
std::pair<double, double> ProfileScene::minMaxX() const
{
return timeAxis->screenMinMax();
}
std::pair<double, double> ProfileScene::minMaxY() const
{
return profileYAxis->screenMinMax();
}
double ProfileScene::yToScreen(double y) const
{
auto [min, max] = profileYAxis->screenMinMax();
return min + (max - min) * y;
}
double ProfileScene::posAtTime(double time) const
{
return timeAxis->posAtValue(time);
}
double ProfileScene::posAtDepth(double depth) const
{
return profileYAxis->posAtValue(depth);
}
std::vector<std::pair<QString, QPixmap>> ProfileScene::eventsAt(QPointF pos) const
{
std::vector<std::pair<QString, QPixmap>> res;
for (const auto &item: eventItems) {
if (!item->contains(item->mapFromScene(pos)))
continue;
res.emplace_back(item->text, item->pixmap);
}
return res;
}
DiveEventItem *ProfileScene::eventAtPosition(QPointF pos) const
{
for (const auto &item: eventItems) {
if (item->contains(item->mapFromScene(pos)))
return item.get();
}
return nullptr;
}

View File

@ -27,7 +27,6 @@ class DiveReportedCeiling;
class DiveTemperatureItem; class DiveTemperatureItem;
class DiveTextItem; class DiveTextItem;
class PartialPressureGasItem; class PartialPressureGasItem;
class ProfileAnimation;
class TankItem; class TankItem;
class ProfileScene : public QGraphicsScene { class ProfileScene : public QGraphicsScene {
@ -38,20 +37,35 @@ public:
void resize(QSizeF size); void resize(QSizeF size);
void clear(); void clear();
bool pointOnProfile(const QPointF &point) const; bool pointOnProfile(const QPointF &point) const;
bool pointOnDiveComputerText(const QPointF &point) const;
bool pointOnEventsHiddenText(const QPointF &point) const;
void anim(double fraction); // Called by the animation with 0.0-1.0 (start to stop). void anim(double fraction); // Called by the animation with 0.0-1.0 (start to stop).
// 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, DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false, void plotDive(const struct dive *d, int dc, int animSpeed, bool simplified,
bool instant = false, bool keepPlotInfo = false, bool calcMax = true, double zoom = 1.0, double zoomedPosition = 0.0); DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false,
bool keepPlotInfo = false, bool calcMax = true, double zoom = 1.0, double zoomedPosition = 0.0);
void draw(QPainter *painter, const QRect &pos, void draw(QPainter *painter, const QRect &pos,
const struct dive *d, int dc, const struct dive *d, int dc,
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;
int timeAt(QPointF pos) const; // time in seconds
int depthAt(QPointF pos) const; // depth in mm
std::pair<double, double> minMaxTime() const; // time in seconds
std::pair<double, double> minMaxX() const; // minimum and maximum x positions of canvas
std::pair<double, double> minMaxY() const; // minimum and maximum y positions of canvas
double posAtTime(double time) const; // time in seconds
double posAtDepth(double depth) const;
double yToScreen(double y) const; // For pictures: depth given in fration of displayed range.
std::vector<std::pair<QString, QPixmap>> eventsAt(QPointF pos) const;
DiveEventItem *eventAtPosition(QPointF pos) const; // null if no event icon at position.
const struct dive *d; const struct dive *d;
int dc; int dc;
QRectF profileRegion; // Region inside the axes, where the crosshair is painted in plan and edit mode.
private: private:
using DataAccessor = double (*)(const plot_data &data); using DataAccessor = double (*)(const plot_data &data);
template<typename T, class... Args> T *createItem(const DiveCartesianAxis &vAxis, DataAccessor accessor, int z, Args&&... args); template<typename T, class... Args> T *createItem(const DiveCartesianAxis &vAxis, DataAccessor accessor, int z, Args&&... args);
@ -61,7 +75,6 @@ private:
void updateVisibility(bool diveHasHeartBeat, bool simplified); // Update visibility of non-interactive chart features according to preferences void updateVisibility(bool diveHasHeartBeat, bool simplified); // Update visibility of non-interactive chart features according to preferences
void updateAxes(bool diveHasHeartBeat, bool simplified); // Update axes according to preferences void updateAxes(bool diveHasHeartBeat, bool simplified); // Update axes according to preferences
friend class ProfileWidget2; // For now, give the ProfileWidget full access to the objects on the scene
double dpr; // Device Pixel Ratio. A DPR of one corresponds to a "standard" PC screen. double dpr; // Device Pixel Ratio. A DPR of one corresponds to a "standard" PC screen.
bool printMode; bool printMode;
bool isGrayscale; bool isGrayscale;
@ -70,7 +83,6 @@ private:
int maxdepth; int maxdepth;
struct plot_info plotInfo; struct plot_info plotInfo;
QRectF profileRegion; // Region inside the axes, where the crosshair is painted in plan and edit mode.
DiveCartesianAxis *profileYAxis; DiveCartesianAxis *profileYAxis;
DiveCartesianAxis *gasYAxis; DiveCartesianAxis *gasYAxis;
DiveCartesianAxis *temperatureAxis; DiveCartesianAxis *temperatureAxis;
@ -83,8 +95,9 @@ private:
DiveTemperatureItem *temperatureItem; DiveTemperatureItem *temperatureItem;
DiveMeanDepthItem *meanDepthItem; DiveMeanDepthItem *meanDepthItem;
DiveGasPressureItem *gasPressureItem; DiveGasPressureItem *gasPressureItem;
QList<DiveEventItem *> eventItems; std::vector<std::unique_ptr<DiveEventItem>> eventItems;
DiveTextItem *diveComputerText; DiveTextItem *diveComputerText;
DiveTextItem *eventsHiddenText;
DiveReportedCeiling *reportedCeiling; DiveReportedCeiling *reportedCeiling;
PartialPressureGasItem *pn2GasItem; PartialPressureGasItem *pn2GasItem;
PartialPressureGasItem *pheGasItem; PartialPressureGasItem *pheGasItem;
@ -101,7 +114,6 @@ private:
DivePercentageItem *percentageItem; DivePercentageItem *percentageItem;
TankItem *tankItem; TankItem *tankItem;
std::shared_ptr<const DivePixmaps> pixmaps; std::shared_ptr<const DivePixmaps> pixmaps;
std::unique_ptr<ProfileAnimation> animation;
std::vector<DiveCartesianAxis *> animatedAxes; std::vector<DiveCartesianAxis *> animatedAxes;
}; };

View File

@ -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 <QObject>
class ProfileTranslations : public QObject
{
Q_OBJECT
};
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,189 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef PROFILE_VIEW_H
#define PROFILE_VIEW_H
#include "qt-quick/chartview.h"
#include "core/pictureobj.h"
#include "core/units.h"
#include <memory>
class ChartGraphicsSceneItem;
class ChartLineItem;
class ChartRectItem;
class DivePlannerPointsModel;
class HandleItem;
class PictureItem;
class ProfileAnimation;
class ProfileScene;
class ToolTipItem;
class RulerItem;
class QModelIndex;
class ProfileView : public ChartView {
Q_OBJECT
// Communication with the mobile interface is via properties. I hate it.
Q_PROPERTY(int diveId READ getDiveId WRITE setDiveId)
Q_PROPERTY(int numDC READ numDC NOTIFY numDCChanged)
Q_PROPERTY(double zoomLevel MEMBER zoomLevel NOTIFY zoomLevelChanged)
public:
ProfileView();
ProfileView(QQuickItem *parent);
~ProfileView();
// Flag set when constructing the object. Because QtQuick may decide to destroy the old one. :(
bool initialized;
void setPlannerModel(DivePlannerPointsModel &model); // enables planning and edit mdoe
struct RenderFlags {
static constexpr int None = 0;
static constexpr int Instant = 1 << 0;
static constexpr int DontRecalculatePlotInfo = 1 << 1;
static constexpr int EditMode = 1 << 2;
static constexpr int PlanMode = 1 << 3;
static constexpr int Simplified = 1 << 4; // For mobile's overview page
static constexpr int DontCalculateMax = 1 << 5;
};
void plotDive(const struct dive *d, int dc, int flags = RenderFlags::None);
int timeAt(QPointF pos) const;
void clear();
void resetZoom();
void anim(double fraction);
void rulerDragged(); // Called by the RulterItem when a handle was dragged.
void handleSelected(int idx); // Called by the HandleItem when it is clicked.
void handleDragged(int idx, QPointF pos); // Called by the HandleItem when it is dragged.
void handleReleased(int idx); // Called by the HandleItem when it is released.
// For mobile
Q_INVOKABLE void pinchStart();
Q_INVOKABLE void pinch(double factor);
Q_INVOKABLE void nextDC();
Q_INVOKABLE void prevDC();
Q_INVOKABLE void panStart(double x, double y);
Q_INVOKABLE void pan(double x, double y);
signals:
void numDCChanged();
void zoomLevelChanged();
void stopAdded(); // only emitted in edit mode
void stopRemoved(int count); // only emitted in edit mode
void stopMoved(int count); // only emitted in edit mode
private:
enum Mode {
Normal,
Edit,
Plan
};
const struct dive *d;
int dc;
DivePlannerPointsModel *plannerModel;
Mode mode;
bool simplified;
double dpr;
double zoomLevel, zoomLevelPinchStart;
double zoomedPosition; // Position when zoomed: 0.0 = beginning, 1.0 = end.
bool panning; // Currently panning.
double panningOriginalMousePosition;
double panningOriginalProfilePosition;
bool empty; // No dive shown.
QColor background;
std::unique_ptr<ProfileScene> profileScene;
ChartItemPtr<ChartGraphicsSceneItem> profileItem;
std::unique_ptr<ProfileAnimation> animation;
ChartItemPtr<ChartLineItem> mouseFollowerHorizontal, mouseFollowerVertical;
dive *mutable_dive() const; // for functions manipulating the dive
void plotAreaChanged(const QSizeF &size) override;
void resetPointers() override;
void replot();
int rerenderFlags() const;
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;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent *e) override;
void contextMenu(const QPointF pos, const QPoint globalPos);
void addSetpointChange(int seconds);
ChartItemPtr<ToolTipItem> tooltip;
void updateTooltip(QPointF pos, bool plannerMode, int animSpeed);
std::unique_ptr<ProfileAnimation> tooltip_animation;
std::unique_ptr<RulerItem> ruler;
void updateRuler(int animSpeed);
std::unique_ptr<ProfileAnimation> ruler_animation;
void updateMouseFollowers(QPointF pos);
QPointF previousHoverMovePosition;
std::vector<std::unique_ptr<HandleItem>> handles;
int selectedHandleIdx;
void clearHandles();
void resetHandles();
void placeHandles();
void reindexHandles();
void moveHandle(int time, int depth);
void deleteHandle();
void pointsInserted(const QModelIndex &, int from, int to);
void pointsRemoved(const QModelIndex &, int start, int end);
void pointsMoved(const QModelIndex &, int start, int end, const QModelIndex &destination, int row);
// DC related
void renameCurrentDC();
// Event related
void editEventName(struct event *event);
void unhideEvents();
// 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<PictureObj> pics);
void pictureOffsetChanged(dive *d, QString filename, offset_t offset);
void updateDurationLine(PictureEntry &e);
void updateThumbnail(QString filename, QImage thumbnail, duration_t duration);
void updateThumbnailPaintOrder();
void calculatePictureYPositions();
void updateThumbnailXPos(PictureEntry &e);
void plotPictures(const struct dive *d, bool synchronous);
void updateThumbnails();
void clearPictures();
void removePictureThumbnail(PictureEntry &entry);
void unhighlightPicture();
void shrinkPictureItem(PictureEntry &e, int animSpeed);
void growPictureItem(PictureEntry &e, int animSpeed);
void moveThumbnailBefore(PictureEntry &e, std::vector<PictureEntry>::iterator &before);
PictureEntry *getPictureUnderMouse(QPointF pos);
// For mobile
int getDiveId() const;
void setDiveId(int id);
int numDC() const;
void rotateDC(int dir);
};
#endif

File diff suppressed because it is too large Load Diff

View File

@ -1,205 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef PROFILEWIDGET2_H
#define PROFILEWIDGET2_H
#include <QGraphicsView>
#include <vector>
#include <memory>
// /* The idea of this widget is to display and edit the profile.
// * It has:
// * 1 - ToolTip / Legend item, displays every information of the current mouse position on it, plus the legends of the maps.
// * 2 - ToolBox, displays the QActions that are used to do special stuff on the profile ( like activating the plugins. )
// * 3 - Cartesian Axis for depth ( y )
// * 4 - Cartesian Axis for Gases ( y )
// * 5 - Cartesian Axis for Time ( x )
// *
// * It needs to be dynamic, things should *flow* on it, not just appear / disappear.
// */
#include "profile-widget/divelineitem.h"
#include "core/pictureobj.h"
#include "core/units.h"
#include "core/subsurface-qt/divelistnotifier.h"
class ProfileScene;
class RulerItem2;
struct dive;
class ToolTipItem;
class DiveEventItem;
class DivePlannerPointsModel;
class DiveHandler;
class QGraphicsSimpleTextItem;
class QModelIndex;
class DivePictureItem;
class ProfileWidget2 : public QGraphicsView {
Q_OBJECT
public:
enum State {
PROFILE,
EDIT,
PLAN,
INIT
};
struct RenderFlags {
static constexpr int None = 0;
static constexpr int Instant = 1 << 0;
static constexpr int DontRecalculatePlotInfo = 1 << 1;
};
// Pass null as plannerModel if no support for planning required
ProfileWidget2(DivePlannerPointsModel *plannerModel, double dpr, QWidget *parent = 0);
~ProfileWidget2();
void resetZoom();
void plotDive(const struct dive *d, int dc, int flags = RenderFlags::None);
void setProfileState(const struct dive *d, int dc);
void setPlanState(const struct dive *d, int dc);
void setEditState(const struct dive *d, int dc);
bool isPlanner() const;
void clear();
#ifndef SUBSURFACE_MOBILE
bool eventFilter(QObject *, QEvent *) override;
#endif
std::unique_ptr<ProfileScene> profileScene;
State currentState;
signals:
void stopAdded(); // only emitted in edit mode
void stopRemoved(int count); // only emitted in edit mode
void stopMoved(int count); // only emitted in edit mode
public
slots: // Necessary to call from QAction's signals.
void settingsChanged();
void actionRequestedReplot(bool triggered);
void divesChanged(const QVector<dive *> &dives, DiveField field);
#ifndef SUBSURFACE_MOBILE
void plotPictures();
void picturesRemoved(dive *d, QVector<QString> filenames);
void picturesAdded(dive *d, QVector<PictureObj> pics);
void pointsReset();
void pointInserted(const QModelIndex &parent, int start, int end);
void pointsRemoved(const QModelIndex &, int start, int end);
void pointsMoved(const QModelIndex &, int start, int end, const QModelIndex &destination, int row);
void updateThumbnail(QString filename, QImage thumbnail, duration_t duration);
void profileChanged(dive *d);
void pictureOffsetChanged(dive *d, QString filename, offset_t offset);
void removePicture(const QString &fileUrl);
/* this is called for every move on the handlers. maybe we can speed up this a bit? */
void divePlannerHandlerMoved();
void divePlannerHandlerClicked();
void divePlannerHandlerReleased();
#endif
private:
void setProfileState(); // keep currently displayed dive
void resizeEvent(QResizeEvent *event) override;
#ifndef SUBSURFACE_MOBILE
void wheelEvent(QWheelEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent *e) override;
#endif
void dropEvent(QDropEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void replot();
void setZoom(int level);
void changeGas(int tank, int seconds);
void setupSceneAndFlags();
void addItemsToScene();
void setupItemOnScene();
void disconnectTemporaryConnections();
struct plot_data *getEntryFromPos(QPointF pos);
void clearPictures();
void plotPicturesInternal(const struct dive *d, bool synchronous);
void updateThumbnails();
void addDivemodeSwitch(int seconds, int divemode);
void addBookmark(int seconds);
void splitDive(int seconds);
void addSetpointChange(int seconds);
void removeEvent(DiveEventItem *item);
void hideEvent(DiveEventItem *item);
void hideEventType(DiveEventItem *item);
void editName(DiveEventItem *item);
void unhideEvents();
void unhideEventTypes();
void makeFirstDC();
void deleteCurrentDC();
void splitCurrentDC();
void renameCurrentDC();
DivePlannerPointsModel *plannerModel; // If null, no planning supported.
int zoomLevel;
double zoomedPosition; // Position, when zoomed: 0.0 = beginning, 1.0 = end.
#ifndef SUBSURFACE_MOBILE
ToolTipItem *toolTipItem;
#endif
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;
RulerItem2 *rulerItem;
#endif
std::vector<std::unique_ptr<QGraphicsSimpleTextItem>> gases;
#ifndef SUBSURFACE_MOBILE
// The list of pictures in this plot. The pictures are sorted by offset in seconds.
// For the same offset, sort by filename.
// Pictures that are outside of the dive time are not shown.
struct PictureEntry {
offset_t offset;
duration_t duration;
QString 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 QString &filenameIn, ProfileWidget2 *profile, bool synchronous);
bool operator< (const PictureEntry &e) const;
};
void updateThumbnailXPos(PictureEntry &e);
std::vector<PictureEntry> pictures;
void calculatePictureYPositions();
void updateDurationLine(PictureEntry &e);
void updateThumbnailPaintOrder();
void keyDeleteAction();
void keyUpAction();
void keyDownAction();
void keyLeftAction();
void keyRightAction();
#endif
std::vector<std::unique_ptr<DiveHandler>> handles;
int handleIndex(const DiveHandler *h) const;
#ifndef SUBSURFACE_MOBILE
void connectPlannerModel();
void repositionDiveHandlers();
int fixHandlerIndex(DiveHandler *activeHandler);
DiveHandler *createHandle();
QGraphicsSimpleTextItem *createGas();
#endif
friend class DiveHandler;
bool shouldCalculateMax; // Calculate maximum time and depth (default). False when dragging handles.
std::vector<int> selectedDiveHandleIndices() const;
// We store a const pointer to the shown dive. However, the undo commands want
// (understandably) a non-const pointer. Since the profile has a context-menu
// with actions, it needs such a non-const pointer. This function turns the
// currently shown dive into such a pointer. Ugly, yes.
struct dive *mutable_dive() const;
};
#endif // PROFILEWIDGET2_H

View File

@ -1,162 +0,0 @@
// SPDX-License-Identifier: GPL-2.
#include "qmlprofile.h"
#include "profilescene.h"
#include "mobile-widgets/qmlmanager.h"
#include "core/errorhelper.h"
#include "core/subsurface-float.h"
#include "core/metrics.h"
#include "core/subsurface-string.h"
#include <QTransform>
#include <QScreen>
#include <QElapsedTimer>
QMLProfile::QMLProfile(QQuickItem *parent) :
QQuickPaintedItem(parent),
m_diveId(0),
m_dc(0),
m_devicePixelRatio(1.0),
m_margin(0),
m_xOffset(0.0),
m_yOffset(0.0)
{
createProfileView();
setAntialiasing(true);
setFlags(QQuickItem::ItemClipsChildrenToShape | QQuickItem::ItemHasContents );
connect(QMLManager::instance(), &QMLManager::sendScreenChanged, this, &QMLProfile::screenChanged);
connect(this, &QMLProfile::scaleChanged, this, &QMLProfile::triggerUpdate);
connect(&diveListNotifier, &DiveListNotifier::divesChanged, this, &QMLProfile::divesChanged);
setDevicePixelRatio(QMLManager::instance()->lastDevicePixelRatio());
}
QMLProfile::~QMLProfile()
{
}
void QMLProfile::createProfileView()
{
m_profileWidget.reset(new ProfileScene(m_devicePixelRatio * 0.8, false, false));
}
// we need this so we can connect update() to the scaleChanged() signal - which the connect above cannot do
// directly as it chokes on the default parameter for update().
// If the scale changes we may need to change our offsets to ensure that we still only show a subset of
// the profile and not empty space around it, which the paint() method below will take care of, which will
// eventually get called after we call update()
void QMLProfile::triggerUpdate()
{
update();
}
void QMLProfile::paint(QPainter *painter)
{
QElapsedTimer timer;
if (verbose)
timer.start();
// let's look at the intended size of the content and scale our scene accordingly
// for some odd reason the painter transformation is set up to scale by the dpr - which results
// in applying that dpr scaling twice. So we hard-code it here to be the identity matrix
QRect painterRect = painter->viewport();
painter->resetTransform();
if (m_diveId < 0)
return;
struct dive *d = get_dive_by_uniq_id(m_diveId);
if (!d)
return;
m_profileWidget->draw(painter, painterRect, d, m_dc, nullptr, false);
}
void QMLProfile::setMargin(int margin)
{
m_margin = margin;
}
int QMLProfile::diveId() const
{
return m_diveId;
}
void QMLProfile::setDiveId(int diveId)
{
m_diveId = diveId;
emit numDCChanged();
}
qreal QMLProfile::devicePixelRatio() const
{
return m_devicePixelRatio;
}
void QMLProfile::setDevicePixelRatio(qreal dpr)
{
if (dpr != m_devicePixelRatio) {
m_devicePixelRatio = dpr;
// Recreate the view to redraw the text items with the new scale.
createProfileView();
emit devicePixelRatioChanged();
}
}
// don't update the profile here, have the user update x and y and then manually trigger an update
void QMLProfile::setXOffset(qreal value)
{
if (nearly_equal(value, m_xOffset))
return;
m_xOffset = value;
emit xOffsetChanged();
}
// don't update the profile here, have the user update x and y and then manually trigger an update
void QMLProfile::setYOffset(qreal value)
{
if (nearly_equal(value, m_yOffset))
return;
m_yOffset = value;
emit yOffsetChanged();
}
void QMLProfile::screenChanged(QScreen *screen)
{
setDevicePixelRatio(screen->devicePixelRatio());
}
void QMLProfile::divesChanged(const QVector<dive *> &dives, DiveField)
{
for (struct dive *d: dives) {
if (d->id == m_diveId) {
qDebug() << "dive #" << d->number << "changed, trigger profile update";
triggerUpdate();
return;
}
}
}
void QMLProfile::nextDC()
{
rotateDC(1);
}
void QMLProfile::prevDC()
{
rotateDC(-1);
}
void QMLProfile::rotateDC(int dir)
{
struct dive *d = get_dive_by_uniq_id(m_diveId);
if (!d)
return;
int numDC = number_of_computers(d);
if (numDC == 1)
return;
m_dc = (m_dc + dir) % numDC;
if (m_dc < 0)
m_dc += numDC;
triggerUpdate();
}
int QMLProfile::numDC() const
{
struct dive *d = get_dive_by_uniq_id(m_diveId);
return d ? number_of_computers(d) : 0;
}

View File

@ -1,62 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef QMLPROFILE_H
#define QMLPROFILE_H
#include "core/subsurface-qt/divelistnotifier.h"
#include <QQuickPaintedItem>
#include <memory>
class ProfileScene;
class QMLProfile : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(int diveId MEMBER m_diveId WRITE setDiveId)
Q_PROPERTY(int numDC READ numDC NOTIFY numDCChanged)
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio WRITE setDevicePixelRatio NOTIFY devicePixelRatioChanged)
Q_PROPERTY(qreal xOffset MEMBER m_xOffset WRITE setXOffset NOTIFY xOffsetChanged)
Q_PROPERTY(qreal yOffset MEMBER m_yOffset WRITE setYOffset NOTIFY yOffsetChanged)
public:
explicit QMLProfile(QQuickItem *parent = 0);
~QMLProfile();
void paint(QPainter *painter);
int diveId() const;
void setDiveId(int diveId);
qreal devicePixelRatio() const;
void setDevicePixelRatio(qreal dpr);
void setXOffset(qreal value);
void setYOffset(qreal value);
Q_INVOKABLE void nextDC();
Q_INVOKABLE void prevDC();
public slots:
void setMargin(int margin);
void screenChanged(QScreen *screen);
void triggerUpdate();
private:
int m_diveId;
int m_dc;
qreal m_devicePixelRatio;
int m_margin;
qreal m_xOffset, m_yOffset;
std::unique_ptr<ProfileScene> m_profileWidget;
void createProfileView();
void rotateDC(int dir);
int numDC() const;
private slots:
void divesChanged(const QVector<dive *> &dives, DiveField);
signals:
void rightAlignedChanged();
void devicePixelRatioChanged();
void xOffsetChanged();
void yOffsetChanged();
void numDCChanged();
};
#endif // QMLPROFILE_H

View File

@ -1,177 +1,191 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "profile-widget/ruleritem.h" #include "ruleritem.h"
#include "profile-widget/profilewidget2.h" #include "profilescene.h"
#include "profileview.h"
#include "zvalues.h"
#include "core/settings/qPrefTechnicalDetails.h" #include "core/settings/qPrefTechnicalDetails.h"
#include <qgraphicssceneevent.h>
#include "core/profile.h" #include "core/profile.h"
RulerNodeItem2::RulerNodeItem2() : #include <QApplication>
pInfo(NULL),
idx(-1),
ruler(NULL),
timeAxis(NULL),
depthAxis(NULL)
{
setRect(-8, -8, 16, 16);
setBrush(QColor(0xff, 0, 0, 127));
setPen(QColor(Qt::red));
setFlag(ItemIsMovable);
setFlag(ItemSendsGeometryChanges);
setFlag(ItemIgnoresTransformations);
}
void RulerNodeItem2::setPlotInfo(const plot_info &info) static QColor handleBorderColor(Qt::red);
{ static QColor handleColor(0xff, 0, 0, 0x80);
pInfo = &info; static constexpr double handleRadius = 8.0;
idx = 0;
}
void RulerNodeItem2::setRuler(RulerItem2 *r) static QColor lineColor(Qt::black);
{ static constexpr double lineWidth = 1.0;
ruler = r;
}
void RulerNodeItem2::recalculate() static constexpr int tooltipBorder = 1;
{ static constexpr double tooltipBorderRadius = 2.0; // Radius of rounded corners
if (!pInfo || pInfo->nr <= 0) static QColor tooltipBorderColor(Qt::black);
return; static QColor tooltipColor(0xff, 0xff, 0xff, 190);
static QColor tooltipFontColor(Qt::black);
const struct plot_data &last = pInfo->entry[pInfo->nr - 1]; class RulerItemHandle : public ChartDiskItem
if (x() < 0) { {
setPos(0, y()); public:
} else if (x() > timeAxis->posAtValue(last.sec)) { ProfileView &profileView;
setPos(timeAxis->posAtValue(last.sec), depthAxis->posAtValue(last.depth)); double xpos;
// The first argument is passed twice, to avoid an downcast. Yes, that's silly.
RulerItemHandle(ChartView &view, ProfileView &profileView, double dpr) :
ChartDiskItem(view, ProfileZValue::RulerItem,
QPen(handleBorderColor, dpr),
QBrush(handleColor),
true),
profileView(profileView),
xpos(0.0)
{
}
// The call chain here is weird: this calls into the ProfileScene, which then calls
// back into the RulerItem. The reason is that the ProfileScene knows the current
// dive etc. This seems more robust than storing the current dive in the subobject.
void drag(QPointF pos) override
{
xpos = pos.x();
profileView.rulerDragged();
}
};
// duplicate code in tooltipitem.cpp
static QFont makeFont(double dpr)
{
QFont font(qApp->font());
if (dpr != 1.0) {
int pixelSize = font.pixelSize();
if (pixelSize > 0) {
pixelSize = lrint(static_cast<double>(pixelSize) * dpr);
font.setPixelSize(pixelSize);
} else { } else {
idx = 0; font.setPointSizeF(font.pointSizeF() * dpr);
while (idx < pInfo->nr && timeAxis->posAtValue(pInfo->entry[idx].sec) < x())
++idx;
const struct plot_data &data = pInfo->entry[idx];
setPos(timeAxis->posAtValue(data.sec), depthAxis->posAtValue(data.depth));
} }
}
return font;
} }
void RulerNodeItem2::mouseMoveEvent(QGraphicsSceneMouseEvent *event) static ChartItemPtr<RulerItemHandle> makeHandle(ProfileView &view, double dpr)
{ {
qreal x = event->scenePos().x(); auto res = view.createChartItem<RulerItemHandle>(view, dpr);
if (x < 0.0) res->resize(handleRadius * dpr);
x = 0.0; return res;
setPos(x, event->scenePos().y());
recalculate();
ruler->recalculate();
} }
RulerItem2::RulerItem2() : pInfo(NULL), RulerItem::RulerItem(ProfileView &view, double dpr) :
source(new RulerNodeItem2()), line(view.createChartItem<ChartLineItem>(ProfileZValue::RulerItem,
dest(new RulerNodeItem2()), lineColor, lineWidth * dpr)),
timeAxis(NULL), handle1(makeHandle(view, dpr)),
depthAxis(NULL), handle2(makeHandle(view, dpr)),
textItemBack(new QGraphicsRectItem(this)), tooltip(view.createChartItem<AnimatedChartRectItem>(ProfileZValue::RulerItem,
textItem(new QGraphicsSimpleTextItem(this)) QPen(tooltipBorderColor, lrint(tooltipBorder * dpr)),
QBrush(tooltipColor), tooltipBorderRadius * dpr,
false)),
font(makeFont(dpr)),
fm(font),
fontHeight(fm.height())
{ {
source->setRuler(this);
dest->setRuler(this);
textItem->setFlag(QGraphicsItem::ItemIgnoresTransformations);
textItemBack->setBrush(QColor(0xff, 0xff, 0xff, 190));
textItemBack->setPen(QColor(Qt::white));
textItemBack->setFlag(QGraphicsItem::ItemIgnoresTransformations);
setPen(QPen(QColor(Qt::black), 0.0));
#ifndef SUBSURFACE_MOBILE
connect(qPrefTechnicalDetails::instance(), &qPrefTechnicalDetails::rulergraphChanged, this, &RulerItem2::settingsChanged);
#endif
} }
void RulerItem2::settingsChanged(bool value) RulerItem::~RulerItem()
{ {
ProfileWidget2 *profWidget = NULL;
if (scene() && scene()->views().count())
profWidget = qobject_cast<ProfileWidget2 *>(scene()->views().first());
setVisible( (profWidget && profWidget->currentState == ProfileWidget2::PROFILE) ? value : false);
} }
void RulerItem2::recalculate() void RulerItem::setVisible(bool visible)
{ {
char buffer[500]; line->setVisible(visible);
QPointF tmp; handle1->setVisible(visible);
QFont font; handle2->setVisible(visible);
QFontMetrics fm(font); tooltip->setVisible(visible);
}
if (timeAxis == NULL || depthAxis == NULL || !pInfo || pInfo->nr == 0) // Binary search to find index at time stamp
static int get_idx_at_time(const plot_info &info, int time)
{
auto entry = std::lower_bound(info.entry, info.entry + info.nr, time,
[](const plot_data &d, int time)
{ return d.sec < time; });
return entry < info.entry + info.nr ? entry - info.entry
: info.nr - 1;
}
void RulerItem::update(const dive *d, double dpr, const ProfileScene &scene, const plot_info &info, int animspeed)
{
if (info.nr == 0)
return; // Nothing to display
auto [minX, maxX] = scene.minMaxX();
auto [minY, maxY] = scene.minMaxY();
double x1 = std::clamp(handle1->xpos, minX, maxX);
double x2 = std::clamp(handle2->xpos, minX, maxX);
int time1 = lrint(scene.timeAt(QPointF(x1, 0.0)));
int time2 = lrint(scene.timeAt(QPointF(x2, 0.0)));
int idx1 = get_idx_at_time(info, time1);
int idx2 = get_idx_at_time(info, time2);
double y1 = scene.posAtDepth(info.entry[idx1].depth);
double y2 = scene.posAtDepth(info.entry[idx2].depth);
QPointF pos1(x1, y1);
QPointF pos2(x2, y2);
line->setLine(pos1, pos2);
handle1->setPos(pos1);
handle2->setPos(pos2);
if (idx1 == idx2) {
tooltip->setVisible(false);
return; return;
prepareGeometryChange();
startPoint = mapFromItem(source, 0, 0);
endPoint = mapFromItem(dest, 0, 0);
if (startPoint.x() > endPoint.x()) {
tmp = endPoint;
endPoint = startPoint;
startPoint = tmp;
} }
QLineF line(startPoint, endPoint);
setLine(line);
compare_samples(dive, pInfo, source->idx, dest->idx, buffer, 500, 1);
text = QString(buffer);
// draw text auto strings = compare_samples(d, &info, idx1, idx2, true);
QGraphicsView *view = scene()->views().first(); if (strings.empty()) {
QPoint begin = view->mapFromScene(mapToScene(startPoint)); tooltip->setVisible(false);
textItem->setText(text); return;
qreal tgtX = startPoint.x();
const qreal diff = begin.x() + textItem->boundingRect().width();
// clamp so that the text doesn't go out of the screen to the right
if (diff > view->width()) {
begin.setX(lrint(begin.x() - (diff - view->width())));
tgtX = mapFromScene(view->mapToScene(begin)).x();
} }
// always show the text bellow the lowest of the start and end points tooltip->setVisible(true);
qreal tgtY = (startPoint.y() >= endPoint.y()) ? startPoint.y() : endPoint.y();
// this isn't exactly optimal, since we want to scale the 1.0, 4.0 distances as well
textItem->setPos(tgtX - 1.0, tgtY + 4.0);
// setup the text background double width = 0;
textItemBack->setVisible(startPoint.x() != endPoint.x()); std::vector<int> string_widths;
textItemBack->setPos(textItem->x(), textItem->y()); string_widths.reserve(strings.size());
textItemBack->setRect(0, 0, textItem->boundingRect().width(), textItem->boundingRect().height()); for (const QString &s: strings) {
int w = fm.size(Qt::TextSingleLine, s).width();
width = std::max(width, static_cast<double>(w));
string_widths.push_back(w);
}
width += 6.0 * tooltipBorder * dpr;
double height = static_cast<double>(strings.size()) * fontHeight +
4.0 * tooltipBorder * dpr;
QPixmap pixmap(lrint(width), lrint(height));
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setFont(font);
painter.setPen(QPen(tooltipFontColor)); // QPainter uses QPen to set text color!
double x = 4.0 * tooltipBorder * dpr;
double y = 2.0 * tooltipBorder * dpr;
for (size_t i = 0; i < strings.size(); ++i) {
QRectF rect(x, y, string_widths[i], fontHeight);
painter.drawText(rect, strings[i]);
y += fontHeight;
}
tooltip->setPixmap(pixmap, animspeed);
if (pos2.x() < pos1.x())
std::swap(pos1, pos2);
double xpos = width < maxX - minX ?
std::clamp(pos1.x() + handleRadius * dpr, 0.0, maxX - width) : 0.0;
double ypos = height < maxY - minY ?
std::clamp(pos1.y() + handleRadius * dpr, 0.0, maxY - height) : 0.0;
tooltip->setPos(QPointF(xpos, ypos));
} }
RulerNodeItem2 *RulerItem2::sourceNode() const void RulerItem::anim(double progress)
{ {
return source; tooltip->anim(progress);
}
RulerNodeItem2 *RulerItem2::destNode() const
{
return dest;
}
void RulerItem2::setPlotInfo(const struct dive *d, const plot_info &info)
{
dive = d;
pInfo = &info;
dest->setPlotInfo(info);
source->setPlotInfo(info);
dest->recalculate();
source->recalculate();
recalculate();
}
void RulerItem2::setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth)
{
timeAxis = time;
depthAxis = depth;
dest->depthAxis = depth;
dest->timeAxis = time;
source->depthAxis = depth;
source->timeAxis = time;
recalculate();
}
void RulerItem2::setVisible(bool visible)
{
QGraphicsLineItem::setVisible(visible);
source->setVisible(visible);
dest->setVisible(visible);
} }

View File

@ -2,59 +2,32 @@
#ifndef RULERITEM_H #ifndef RULERITEM_H
#define RULERITEM_H #define RULERITEM_H
#include <QObject> #include "qt-quick/chartitem.h"
#include <QGraphicsEllipseItem>
#include <QGraphicsObject>
#include "profile-widget/divecartesianaxis.h"
struct plot_data; #include <QFont>
#include <QFontMetrics>
class ProfileView;
class ProfileScene;
class RulerItemHandle;
struct plot_info; struct plot_info;
class RulerItem2; struct dive;
class RulerNodeItem2 : public QObject, public QGraphicsEllipseItem { class RulerItem {
Q_OBJECT ChartItemPtr<ChartLineItem> line;
friend class RulerItem2; ChartItemPtr<RulerItemHandle> handle1, handle2;
ChartItemPtr<AnimatedChartRectItem> tooltip;
QFont font;
QFontMetrics fm;
double fontHeight;
QPixmap title;
public: public:
explicit RulerNodeItem2(); RulerItem(ProfileView &view, double dpr);
void setRuler(RulerItem2 *r); ~RulerItem();
void setPlotInfo(const struct plot_info &info);
void recalculate();
private:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
const struct plot_info *pInfo;
int idx;
RulerItem2 *ruler;
DiveCartesianAxis *timeAxis;
DiveCartesianAxis *depthAxis;
};
class RulerItem2 : public QObject, public QGraphicsLineItem {
Q_OBJECT
public:
explicit RulerItem2();
void recalculate();
void setPlotInfo(const struct dive *d, const struct plot_info &pInfo);
RulerNodeItem2 *sourceNode() const;
RulerNodeItem2 *destNode() const;
void setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth);
void setVisible(bool visible); void setVisible(bool visible);
void update(const dive *d, double dpr, const ProfileScene &scene, const plot_info &info, int animspeed);
public void anim(double progress);
slots:
void settingsChanged(bool toggled);
private:
const struct dive *dive;
const struct plot_info *pInfo;
QPointF startPoint, endPoint;
RulerNodeItem2 *source, *dest;
QString text;
DiveCartesianAxis *timeAxis;
DiveCartesianAxis *depthAxis;
QGraphicsRectItem *textItemBack;
QGraphicsSimpleTextItem *textItem;
}; };
#endif #endif

View File

@ -2,7 +2,6 @@
#ifndef TANKITEM_H #ifndef TANKITEM_H
#define TANKITEM_H #define TANKITEM_H
#include "profile-widget/divelineitem.h"
#include "core/gas.h" #include "core/gas.h"
#include <QGraphicsRectItem> #include <QGraphicsRectItem>
#include <QBrush> #include <QBrush>

View File

@ -0,0 +1,168 @@
// 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 "core/settings/qPrefDisplay.h"
#include <cmath>
#include <QApplication>
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<double>(pixelSize) * dpr);
font.setPixelSize(pixelSize);
} else {
font.setPointSizeF(font.pointSizeF() * dpr);
}
}
return font;
}
ToolTipItem::ToolTipItem(ChartView &view, double dpr) :
AnimatedChartRectItem(view, ProfileZValue::ToolTipItem,
QPen(tooltipBorderColor, lrint(tooltipBorder * dpr)),
QBrush(tooltipColor), tooltipBorderRadius * dpr,
true),
font(makeFont(dpr)),
fm(font),
fontHeight(fm.height())
{
title = stringToPixmap(ProfileTranslations::tr("Information"));
QPointF pos = qPrefDisplay::tooltip_position();
setPos(pos);
}
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;
}
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,
const std::vector<std::pair<QString, QPixmap>> &events, bool inPlanner, int animSpeed)
{
auto [idx, strings] = get_plot_details_new(d, &pInfo, time);
QPixmap tissues = drawTissues(pInfo, dpr, idx, inPlanner);
std::vector<QSizeF> event_sizes;
std::vector<int> string_widths;
string_widths.reserve(strings.size());
width = title.size().width();
for (const QString &s: strings) {
int w = fm.size(Qt::TextSingleLine, s).width();
width = std::max(width, static_cast<double>(w));
string_widths.push_back(w);
}
height = (static_cast<double>(strings.size()) + 1.0) * fontHeight;
for (auto &[s, pixmap]: events) {
double text_width = fm.size(Qt::TextSingleLine, s).width();
double total_width = pixmap.width() + text_width;
double h = std::max(static_cast<double>(pixmap.height()), fontHeight);
width = std::max(width, total_width);
height += h;
event_sizes.emplace_back(text_width, h);
}
width += tissues.width();
width += 6.0 * tooltipBorder * dpr;
height = std::max(height, static_cast<double>(tissues.height()));
height += 4.0 * tooltipBorder * dpr + title.height();
QPixmap pixmap(lrint(width), lrint(height));
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setFont(font);
painter.setPen(QPen(tooltipFontColor)); // QPainter uses QPen to set text color!
double x = 4.0 * tooltipBorder * dpr + tissues.width();
double y = 2.0 * tooltipBorder * dpr;
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 * dpr), lrint(y), tissues, 0, 0, tissues.width(), tissues.height());
y += round(fontHeight);
for (size_t i = 0; i < strings.size(); ++i) {
QRectF rect(x, y, string_widths[i], fontHeight);
painter.drawText(rect, strings[i]);
y += fontHeight;
}
for (size_t i = 0; i < events.size(); ++i) {
QSizeF size = event_sizes[i];
auto &[s, pixmap] = events[i];
painter.drawPixmap(lrint(x), lrint(y + (size.height() - pixmap.height())/2.0), pixmap,
0, 0, pixmap.width(), pixmap.height());
QRectF rect(x + pixmap.width(), round(y + (size.height() - fontHeight) / 2.0), size.width(), fontHeight);
painter.drawText(rect, s);
y += size.height();
}
AnimatedChartRectItem::setPixmap(pixmap, animSpeed);
}
void ToolTipItem::stopDrag(QPointF pos)
{
qPrefDisplay::set_tooltip_position(pos);
}

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef PROFILE_TOOLTIPITEM_H
#define PROFILE_TOOLTIPITEM_H
#include "qt-quick/chartitem.h"
#include <QFont>
#include <QFontMetrics>
#include <QPixmap>
struct dive;
struct plot_info;
class ToolTipItem : public AnimatedChartRectItem {
public:
ToolTipItem(ChartView &view, double dpr);
void update(const dive *d, double dpr, int time, const plot_info &pInfo,
const std::vector<std::pair<QString, QPixmap>> &events, bool inPlanner, int animSpeed);
private:
QFont font;
QFontMetrics fm;
double fontHeight;
QPixmap title;
double width, height;
QPixmap stringToPixmap(const QString &s) const;
void stopDrag(QPointF pos) override;
};
#endif // PROFILE_TOOLTIPITEM

24
profile-widget/zvalues.h Normal file
View File

@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-2.0
// Defines the z-values of features in the profile view.
// Objects with higher z-values are painted on top of objects
// with smaller z-values. For the same z-value objects are
// drawn in order of addition to the scene.
#ifndef PROFILE_ZVALUES_H
#define PROFILE_ZVALUES_H
// Encapsulating an enum in a struct is stupid, but allows us
// to not poison the namespace and yet autoconvert to int
// (in constrast to enum class). enum is so broken!
struct ProfileZValue {
enum ZValues {
Profile = 0,
Pictures,
RulerItem,
Handles,
MouseFollower,
ToolTipItem,
Count
};
};
#endif

19
qt-quick/CMakeLists.txt Normal file
View File

@ -0,0 +1,19 @@
# code for qt-quick based charts
include_directories(.
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_BINARY_DIR}
)
set(SUBSURFACE_QTQUICK_SRCS
chartitem.cpp
chartitem.h
chartitemhelper.h
chartitem_ptr.h
chartview.cpp
chartview.h
)
source_group("Subsurface qtquick sourcecode" FILES ${SUBSURFACE_QTQUICK_SRCS})
add_library(subsurface_qtquick STATIC ${SUBSURFACE_QTQUICK_SRCS})
target_link_libraries(subsurface_qtquick ${QT_LIBRARIES})

370
qt-quick/chartitem.cpp Normal file
View File

@ -0,0 +1,370 @@
// SPDX-License-Identifier: GPL-2.0
#include "chartitem.h"
#include "chartitemhelper.h"
#include "chartitem_private.h"
#include <cmath>
#include <QGraphicsScene>
#include <QQuickWindow>
#include <QSGFlatColorMaterial>
#include <QSGImageNode>
ChartItem::ChartItem(ChartView &v, size_t z, bool dragable) :
dirty(false), prev(nullptr), next(nullptr),
zValue(z), dragable(dragable), view(v)
{
// Register before the derived constructors run, so that the
// derived classes can mark the item as dirty in the constructor.
v.registerChartItem(*this);
}
ChartItem::~ChartItem()
{
}
QSizeF ChartItem::sceneSize() const
{
return view.size();
}
void ChartItem::markDirty()
{
view.registerDirtyChartItem(*this);
}
QRectF ChartItem::getRect() const
{
return rect;
}
void ChartItem::startDrag(QPointF pos)
{
}
void ChartItem::drag(QPointF)
{
}
void ChartItem::stopDrag(QPointF pos)
{
}
static int round_up(double f)
{
return static_cast<int>(ceil(f));
}
ChartPixmapItem::ChartPixmapItem(ChartView &v, size_t z, bool dragable) : HideableChartItem(v, z, dragable),
scale(1.0), positionDirty(false), textureDirty(false)
{
}
ChartPixmapItem::~ChartPixmapItem()
{
painter.reset(); // Make sure to destroy painter before image that is painted on
}
void ChartPixmapItem::setTextureDirty()
{
textureDirty = true;
markDirty();
}
void ChartPixmapItem::setPositionDirty()
{
positionDirty = true;
markDirty();
}
void ChartPixmapItem::render()
{
doRearrange();
if (!node) {
createNode(view.w()->createImageNode());
addNodeToView();
}
updateVisible();
if (!img) {
resize(QSizeF(1,1));
img->fill(Qt::transparent);
}
if (textureDirty) {
texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel));
node->node->setTexture(texture.get());
textureDirty = false;
}
if (positionDirty) {
node->node->setRect(rect);
positionDirty = false;
}
}
// Scale size and round to integer (because non-integers give strange artifacts for me).
static QSizeF scaleSize(const QSizeF &s, double scale)
{
return { round(s.width() * scale),
round(s.height() * scale) };
}
void ChartPixmapItem::resize(QSizeF size)
{
QSize s_int(round_up(size.width()), round_up(size.height()));
if (img && s_int == img->size())
return;
painter.reset();
img.reset(new QImage(s_int, QImage::Format_ARGB32));
if (!img->isNull()) {
painter.reset(new QPainter(img.get()));
painter->setRenderHint(QPainter::Antialiasing);
}
rect.setSize(scaleSize(size, scale));
setPositionDirty(); // position includes the size.
setTextureDirty();
}
void ChartPixmapItem::drag(QPointF pos)
{
setPos(pos);
}
void ChartPixmapItem::setPos(QPointF pos)
{
rect.moveTopLeft(pos);
setPositionDirty();
}
void ChartPixmapItem::setScale(double scaleIn)
{
scale = scaleIn;
if (img) {
rect.setSize(scaleSize(img->size(), scale));
setPositionDirty(); // position includes the size.
}
}
void ChartGraphicsSceneItem::draw(QSizeF s, QColor background, QGraphicsScene &scene)
{
resize(s); // Noop if size doesn't change
if (!painter)
return; // Happens if we resize to (0,0)
img->fill(background);
scene.render(painter.get(), QRect(QPoint(), img->size()), scene.sceneRect(), Qt::IgnoreAspectRatio);
setTextureDirty();
}
ChartRectItem::ChartRectItem(ChartView &v, size_t z, const QPen &pen,
const QBrush &brush, double radius, bool dragable) :
ChartPixmapItem(v, z, dragable),
pen(pen), brush(brush), radius(radius)
{
}
ChartRectItem::~ChartRectItem()
{
}
void ChartRectItem::resize(QSizeF size)
{
ChartPixmapItem::resize(size);
if (!painter)
return;
img->fill(Qt::transparent);
painter->setPen(pen);
painter->setBrush(brush);
QSize imgSize = img->size();
int width = pen.width();
QRect rect(width / 2, width / 2, imgSize.width() - width, imgSize.height() - width);
painter->drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize);
}
AnimatedChartRectItem::~AnimatedChartRectItem()
{
}
void AnimatedChartRectItem::setPixmap(const QPixmap &p, int animSpeed)
{
if (animSpeed <= 0) {
resize(p.size());
if (painter)
painter->drawPixmap(0, 0, p, 0, 0, p.width(), p.height());
setTextureDirty();
return;
}
pixmap = p;
originalSize = img ? img->size() : QSize(1,1);
}
static int mid(int from, int to, double fraction)
{
return fraction == 1.0 ? to
: lrint(from + (to - from) * fraction);
}
void AnimatedChartRectItem::anim(double fraction)
{
QSize s(mid(originalSize.width(), pixmap.width(), fraction),
mid(originalSize.height(), pixmap.height(), fraction));
resize(s);
if (painter)
painter->drawPixmap(0, 0, pixmap, 0, 0, s.width(), s.height());
setTextureDirty();
}
ChartDiskItem::ChartDiskItem(ChartView &v, size_t z, const QPen &pen, const QBrush &brush, bool dragable) :
ChartPixmapItem(v, z, dragable),
pen(pen), brush(brush)
{
}
ChartDiskItem::~ChartDiskItem()
{
}
void ChartDiskItem::resize(double radius)
{
ChartPixmapItem::resize(QSizeF(2.0 * radius, 2.0 * radius));
if (!painter)
return;
img->fill(Qt::transparent);
painter->setPen(pen);
painter->setBrush(brush);
QSize imgSize = img->size();
int width = pen.width();
QRect rect(width / 2, width / 2, imgSize.width() - width, imgSize.height() - width);
painter->drawEllipse(rect);
}
// Moves the _center_ of the disk to given position.
void ChartDiskItem::setPos(QPointF pos)
{
rect.moveCenter(pos);
setPositionDirty();
}
QPointF ChartDiskItem::getPos() const
{
return rect.center();
}
ChartTextItem::ChartTextItem(ChartView &v, size_t z, const QFont &f, const std::vector<QString> &text, bool center) :
ChartPixmapItem(v, z), f(f), center(center)
{
QFontMetrics fm(f);
double totalWidth = 1.0;
fontHeight = static_cast<double>(fm.height());
double totalHeight = std::max(1.0, static_cast<double>(text.size()) * fontHeight);
items.reserve(text.size());
for (const QString &s: text) {
double w = fm.size(Qt::TextSingleLine, s).width();
items.push_back({ s, w });
if (w > totalWidth)
totalWidth = w;
}
resize(QSizeF(totalWidth, totalHeight));
}
ChartTextItem::ChartTextItem(ChartView &v, size_t z, const QFont &f, const QString &text) :
ChartTextItem(v, z, f, std::vector<QString>({ text }), true)
{
}
void ChartTextItem::setColor(const QColor &c)
{
setColor(c, Qt::transparent);
}
void ChartTextItem::setColor(const QColor &c, const QColor &background)
{
img->fill(background);
double y = 0.0;
painter->setPen(QPen(c));
painter->setFont(f);
double totalWidth = getRect().width();
for (const auto &[s, w]: items) {
double x = center ? round((totalWidth - w) / 2.0) : 0.0;
QRectF rect(x, y, w, fontHeight);
painter->drawText(rect, s);
y += fontHeight;
}
setTextureDirty();
}
ChartLineItemBase::~ChartLineItemBase()
{
}
void ChartLineItemBase::setLine(QPointF from, QPointF to)
{
rect = QRectF(from, to);
positionDirty = true;
markDirty();
}
void ChartLineItem::render()
{
doRearrange();
if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
geometry->setDrawingMode(QSGGeometry::DrawLines);
material.reset(new QSGFlatColorMaterial);
createNode();
node->setGeometry(geometry.get());
node->setMaterial(material.get());
addNodeToView();
positionDirty = materialDirty = true;
}
updateVisible();
if (positionDirty) {
// Attention: width is a geometry property and therefore handled by position dirty!
geometry->setLineWidth(static_cast<float>(width));
auto vertices = geometry->vertexDataAsPoint2D();
setPoint(vertices[0], rect.topLeft());
setPoint(vertices[1], rect.bottomRight());
node->markDirty(QSGNode::DirtyGeometry);
}
if (materialDirty) {
material->setColor(color);
node->markDirty(QSGNode::DirtyMaterial);
}
positionDirty = materialDirty = false;
}
void ChartRectLineItem::render()
{
doRearrange();
if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 5));
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
material.reset(new QSGFlatColorMaterial);
createNode();
node->setGeometry(geometry.get());
node->setMaterial(material.get());
addNodeToView();
positionDirty = materialDirty = true;
}
updateVisible();
if (positionDirty) {
// Attention: width is a geometry property and therefore handled by position dirty!
geometry->setLineWidth(static_cast<float>(width));
auto vertices = geometry->vertexDataAsPoint2D();
setPoint(vertices[0], rect.topLeft());
setPoint(vertices[1], rect.bottomLeft());
setPoint(vertices[2], rect.bottomRight());
setPoint(vertices[3], rect.topRight());
setPoint(vertices[4], rect.topLeft());
node->markDirty(QSGNode::DirtyGeometry);
}
if (materialDirty) {
material->setColor(color);
node->markDirty(QSGNode::DirtyMaterial);
}
positionDirty = materialDirty = false;
}

250
qt-quick/chartitem.h Normal file
View File

@ -0,0 +1,250 @@
// SPDX-License-Identifier: GPL-2.0
// Wrappers around QSGImageNode that allow painting onto an image
// and then turning that into a texture to be displayed in a QQuickItem.
#ifndef CHART_ITEM_H
#define CHART_ITEM_H
#include "chartitem_ptr.h"
#include "chartitemhelper.h"
#include <memory>
#include <QPainter>
class ChartView;
class QGraphicsScene;
class QSGGeometry;
class QSGGeometryNode;
class QSGFlatColorMaterial;
class QSGImageNode;
class QSGRectangleNode;
class QSGTexture;
class QSGTextureMaterial;
class ChartItem {
public:
// Only call on render thread!
virtual void render() = 0;
bool dirty; // If true, call render() when rebuilding the scene
ChartItem *prev, *next; // Double linked list of items
const size_t zValue;
const bool dragable; // Item can be dragged with the mouse. Must be set in constructor.
virtual ~ChartItem(); // Attention: must only be called by render thread.
QRectF getRect() const;
virtual void startDrag(QPointF pos); // Called when user clicks on a draggable item
virtual void drag(QPointF pos); // Called when dragging the item
virtual void stopDrag(QPointF pos); // Called when dragging the item finished
protected:
template <typename T> friend class ChartItemPtr;
ChartItem(ChartView &v, size_t z, bool dragable = false);
QSizeF sceneSize() const;
ChartView &view;
void markDirty();
QRectF rect;
};
template <typename Node>
class HideableChartItem : public ChartItem {
protected:
HideableChartItem(ChartView &v, size_t z, bool dragable = false);
std::unique_ptr<Node> node;
bool visible;
bool visibleChanged;
enum class MoveMode {
none, before, after
} moveMode; // Node will be moved before or after other node.
QSGNode *moveNode; // Node to be moved before/after, or nullptr if move to beginning/end.
template<class... Args>
void createNode(Args&&... args); // Call to create node with visibility flag.
void updateVisible(); // Must be called by child class to update visibility flag!
void addNodeToView(); // Must be called by child class after creating and initializing the QSG node.
void doRearrange(); // Call at beginning of render(), so that the node can be rearranged, if necessary.
public:
template <typename Node2>
friend class HideableChartItem;
template <typename Node2>
void moveBefore(HideableChartItem<Node2> &item);
void moveBack();
template <typename Node2>
void moveAfter(HideableChartItem<Node2> &item);
void moveFront();
void setVisible(bool visible);
bool isVisible() const;
};
// A shortcut for ChartItems based on a hideable proxy item
template <typename Node>
using HideableChartProxyItem = HideableChartItem<HideableQSGNode<QSGProxyNode<Node>>>;
// A chart item that blits a precalculated pixmap onto the scene.
// Can be scaled with setScale().
class ChartPixmapItem : public HideableChartProxyItem<QSGImageNode> {
public:
ChartPixmapItem(ChartView &v, size_t z, bool dragable = false);
~ChartPixmapItem();
virtual void setPos(QPointF pos);
void drag(QPointF pos) override; // calls setPos() by default
void setScale(double scale);
void render() override;
protected:
void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*.
std::unique_ptr<QPainter> painter;
std::unique_ptr<QImage> img;
void setTextureDirty();
void setPositionDirty();
double scale;
private:
bool positionDirty; // true if the position changed since last render
bool textureDirty; // true if the pixmap changed since last render
std::unique_ptr<QSGTexture> texture;
};
// Renders a QGraphicsScene
class ChartGraphicsSceneItem : public ChartPixmapItem
{
public:
using ChartPixmapItem::ChartPixmapItem;
void draw(QSizeF s, QColor background, QGraphicsScene &scene);
};
// Draw a rectangular background after resize. Children are responsible for calling update().
class ChartRectItem : public ChartPixmapItem {
public:
ChartRectItem(ChartView &v, size_t z, const QPen &pen, const QBrush &brush, double radius, bool dragable = false);
~ChartRectItem();
void resize(QSizeF size);
private:
QPen pen;
QBrush brush;
double radius;
};
// A ChartRectItem that is animated on size change.
// Same as ChartPixmapItem, but resize is called with a "speed" parameter
// (which is actually a time parameter for historical reasons).
class AnimatedChartRectItem : public ChartRectItem {
public:
using ChartRectItem::ChartRectItem;
~AnimatedChartRectItem();
void setPixmap(const QPixmap &pixmap, int animSpeed);
void anim(double progress);
private:
QPixmap pixmap;
QSize originalSize;
};
// A solid disk, potentially with border.
class ChartDiskItem : public ChartPixmapItem {
public:
ChartDiskItem(ChartView &v, size_t z, const QPen &pen, const QBrush &brush, bool dragable = false);
~ChartDiskItem();
void resize(double radius);
void setPos(QPointF pos) override;
QPointF getPos() const;
private:
QPen pen;
QBrush brush;
};
// Attention: text is only drawn after calling setColor()!
class ChartTextItem : public ChartPixmapItem {
public:
ChartTextItem(ChartView &v, size_t z, const QFont &f, const std::vector<QString> &text, bool center);
ChartTextItem(ChartView &v, size_t z, const QFont &f, const QString &text);
void setColor(const QColor &color); // Draw on transparent background
void setColor(const QColor &color, const QColor &background); // Fill rectangle with given background color
private:
const QFont &f;
double fontHeight;
bool center;
struct Item {
QString s;
double width;
};
std::vector<Item> items;
};
// Common data for line and rect items.
// Both use the "QRect rect" item of the base class.
// Lines are drawn from top-left to bottom-right.
class ChartLineItemBase : public HideableChartItem<HideableQSGNode<QSGGeometryNode>> {
public:
ChartLineItemBase(ChartView &v, size_t z, QColor color, double width);
~ChartLineItemBase();
void setLine(QPointF from, QPointF to);
protected:
QColor color;
double width;
bool positionDirty;
bool materialDirty;
std::unique_ptr<QSGFlatColorMaterial> material;
std::unique_ptr<QSGGeometry> geometry;
};
class ChartLineItem : public ChartLineItemBase {
public:
using ChartLineItemBase::ChartLineItemBase;
void render() override;
};
// A simple rectangle without fill. Specified by any two opposing vertices.
class ChartRectLineItem : public ChartLineItemBase {
public:
using ChartLineItemBase::ChartLineItemBase;
void render() override;
};
// Implementation detail of templates - move to serparate header file
template <typename Node>
template <typename Node2>
void HideableChartItem<Node>::moveBefore(HideableChartItem<Node2> &item)
{
moveMode = MoveMode::before;
moveNode = item.node.get();
markDirty();
}
template <typename Node>
void HideableChartItem<Node>::moveBack()
{
moveMode = MoveMode::before;
moveNode = nullptr;
markDirty();
}
template <typename Node>
template <typename Node2>
void HideableChartItem<Node>::moveAfter(HideableChartItem<Node2> &item)
{
moveMode = MoveMode::after;
moveNode = item.node.get();
markDirty();
}
template <typename Node>
void HideableChartItem<Node>::moveFront()
{
moveMode = MoveMode::after;
moveNode = nullptr;
markDirty();
}
template <typename Node>
void HideableChartItem<Node>::setVisible(bool visibleIn)
{
if (visible == visibleIn)
return;
visible = visibleIn;
visibleChanged = true;
markDirty();
}
template <typename Node>
bool HideableChartItem<Node>::isVisible() const
{
return visible;
}
#endif

View 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

60
qt-quick/chartitem_ptr.h Normal file
View File

@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-2.0
// A stupid pointer class that initializes to null and can be copy
// assigned. This is for historical reasons: unique_ptrs to ChartItems
// were replaced by plain pointers. Instead of nulling the plain pointers
// in the constructors, use this. Ultimately, we might think about making
// this thing smarter, once removal of individual ChartItems is implemented.
#ifndef CHARITEM_PTR_H
#define CHARITEM_PTR_H
template <typename T>
class ChartItemPtr {
friend class ChartView; // Only the chart view can create these pointers
T *ptr;
ChartItemPtr(T *ptr) : ptr(ptr)
{
}
public:
ChartItemPtr() : ptr(nullptr)
{
}
ChartItemPtr(const ChartItemPtr &p) : ptr(p.ptr)
{
}
ChartItemPtr(ChartItemPtr &&p) : ptr(p.ptr)
{
}
void reset()
{
ptr = nullptr;
}
ChartItemPtr &operator=(const ChartItemPtr &p)
{
ptr = p.ptr;
return *this;
}
operator bool() const
{
return !!ptr;
}
bool operator!() const
{
return !ptr;
}
T &operator*() const
{
return *ptr;
}
T *operator->() const
{
return ptr;
}
void del()
{
if (!ptr)
return;
ptr->view.deleteChartItem(*this);
}
};
#endif

100
qt-quick/chartitemhelper.h Normal file
View File

@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-2.0
// QSGNode template jugglery to overcome API flaws.
#ifndef CHARTITEM_HELPER_H
#define CHARTITEM_HELPER_H
#include <memory>
#include <QSGNode>
// In general, we want chart items to be hideable. For example to show/hide
// labels on demand. Very sadly, the QSG API is absolutely terrible with
// respect to temporarily disabling. Instead of simply having a flag,
// a QSGNode is queried using the "isSubtreeBlocked()" virtual function(!).
//
// Not only is this a slow operation performed on every single node, it
// also is often not possible to override this function: For improved
// performance, the documentation recommends to create QSG nodes via
// QQuickWindow. This provides nodes optimized for the actual hardware.
// However, this obviously means that these nodes cannot be derived from!
//
// In that case, there are two possibilities: Add a proxy node with an
// overridden "isSubtreeBlocked()" function or remove the node from the
// scene. The former was chosen here, because it is less complex.
//
// The following slightly cryptic templates are used to unify the two
// cases: The QSGNode is generated by our own code or the QSGNode is
// obtained from QQuickWindow.
//
// The "HideableQSGNode<Node>" template augments the QSGNode "Node"
// by a "setVisible()" function and overrides "isSubtreeBlocked()"
//
// The "QSGProxyNode<Node>" template is a QSGNode with a single
// child of type "Node".
//
// Thus, if the node can be created, use:
// HideableQSGNode<NodeTypeThatCanBeCreated> node
// and if the node can only be obtained from QQuickWindow, use:
// HideableQSGNode<QSGProxyNode<NodeThatCantBeCreated>> node
// The latter should obviously be typedef-ed.
//
// Yes, that's all horrible, but if nothing else it teaches us about
// composition.
template <typename Node>
class HideableQSGNode : public Node {
bool hidden;
bool isSubtreeBlocked() const override final;
public:
template<class... Args>
HideableQSGNode(bool visible, Args&&... args);
void setVisible(bool visible);
};
template <typename Node>
class QSGProxyNode : public QSGNode {
public:
std::unique_ptr<Node> node;
QSGProxyNode(Node *node);
};
// Implementation detail of templates - move to serparate header file
template <typename Node>
QSGProxyNode<Node>::QSGProxyNode(Node *node) : node(node)
{
appendChildNode(node);
}
template <typename Node>
bool HideableQSGNode<Node>::isSubtreeBlocked() const
{
return hidden;
}
template <typename Node>
template<class... Args>
HideableQSGNode<Node>::HideableQSGNode(bool visible, Args&&... args) :
Node(std::forward<Args>(args)...),
hidden(!visible)
{
}
template <typename Node>
void HideableQSGNode<Node>::setVisible(bool visible)
{
hidden = !visible;
Node::markDirty(QSGNode::DirtySubtreeBlocked);
}
// Helper function to set points
inline void setPoint(QSGGeometry::Point2D &v, const QPointF &p)
{
v.set(static_cast<float>(p.x()), static_cast<float>(p.y()));
}
inline void setPoint(QSGGeometry::TexturedPoint2D &v, const QPointF &p, const QPointF &t)
{
v.set(static_cast<float>(p.x()), static_cast<float>(p.y()),
static_cast<float>(t.x()), static_cast<float>(t.y()));
}
#endif

353
qt-quick/chartview.cpp Normal file
View File

@ -0,0 +1,353 @@
// SPDX-License-Identifier: GPL-2.0
#include "chartview.h"
#include "chartitem.h"
#include <QQuickWindow>
#include <QSGRectangleNode>
ChartView::ChartView(QQuickItem *parent, size_t maxZ) : QQuickItem(parent),
maxZ(maxZ),
backgroundDirty(true),
rootNode(nullptr)
{
setFlag(ItemHasContents, true);
}
// Define a hideable dummy QSG node that is used as a parent node to make
// all objects of a z-level visible / invisible.
using ZNode = HideableQSGNode<QSGNode>;
class RootNode : public QSGNode
{
public:
RootNode(ChartView *view, QColor backgroundColor, size_t maxZ);
~RootNode();
ChartView *view;
std::unique_ptr<QSGRectangleNode> backgroundNode; // solid background
// We entertain one node per Z-level.
std::vector<std::unique_ptr<ZNode>> zNodes;
};
RootNode::RootNode(ChartView *view, QColor backgroundColor, size_t maxZ) : view(view)
{
zNodes.resize(maxZ);
// Add a background rectangle with a solid color. This could
// also be done on the widget level, but would have to be done
// separately for desktop and mobile, so do it here.
backgroundNode.reset(view->w()->createRectangleNode());
appendChildNode(backgroundNode.get());
for (auto &zNode: zNodes) {
zNode.reset(new ZNode(true));
appendChildNode(zNode.get());
}
}
RootNode::~RootNode()
{
if (view)
view->emergencyShutdown();
}
ChartView::~ChartView()
{
// Sometimes the rootNode is destructed before the view,
// sometimes the other way around. QtQuick is a mess!
if (rootNode)
rootNode->view = nullptr;
}
void ChartView::freeDeletedChartItems()
{
ChartItem *nextitem;
for (ChartItem *item = deletedItems.first; item; item = nextitem) {
nextitem = item->next;
delete item;
}
deletedItems.clear();
}
QSGNode *ChartView::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
// The QtQuick drawing interface is utterly bizzare with a distinct 1980ies-style memory management.
// This is just a copy of what is found in Qt's documentation.
RootNode *n = static_cast<RootNode *>(oldNode);
if (!n)
n = rootNode = new RootNode(this, backgroundColor, maxZ);
// Delete all chart items that are marked for deletion.
freeDeletedChartItems();
if (backgroundDirty) {
rootNode->backgroundNode->setRect(plotRect);
backgroundDirty = false;
}
for (ChartItem *item = dirtyItems.first; item; item = item->next) {
item->render();
item->dirty = false;
}
dirtyItems.splice(cleanItems);
return n;
}
// When reparenting the QQuickWidget, QtQuick decides to delete our rootNode
// and with it all the QSG nodes, even though we have *not* given the
// permission to do so! If the widget is reused, we try to delete the
// stale items, whose nodes have already been deleted by QtQuick, leading
// to a double-free(). Instead of searching for the cause of this behavior,
// let's just hook into the rootNode's destructor and delete the objects
// in a controlled manner, so that QtQuick has no more access to them.
void ChartView::emergencyShutdown()
{
// Mark clean and dirty chart items for deletion...
cleanItems.splice(deletedItems);
dirtyItems.splice(deletedItems);
dragableItems.clear();
draggedItem.reset();
// ...and delete them.
freeDeletedChartItems();
// Now delete all the pointers we might have to chart features,
// axes, etc. Note that all pointers to chart items are non
// owning, so this only resets stale references, but does not
// lead to any additional deletion of chart items.
resetPointers();
// The rootNode is being deleted -> remove the reference to that
rootNode = nullptr;
}
void ChartView::clearItems()
{
cleanItems.splice(deletedItems);
dirtyItems.splice(deletedItems);
}
static ZNode &getZNode(RootNode &rootNode, size_t z)
{
size_t idx = std::clamp(z, (size_t)0, rootNode.zNodes.size());
return *rootNode.zNodes[idx];
}
void ChartView::addQSGNode(QSGNode *node, size_t z, bool moveAfter, QSGNode *node2)
{
auto &parent = getZNode(*rootNode, z);
if (node2) {
if (moveAfter)
parent.insertChildNodeAfter(node, node2);
else
parent.insertChildNodeBefore(node, node2);
} else {
if (moveAfter)
parent.prependChildNode(node);
else
parent.appendChildNode(node);
}
}
void ChartView::moveNodeBefore(QSGNode *node, size_t z, QSGNode *before)
{
auto &parent = getZNode(*rootNode, z);
parent.removeChildNode(node);
parent.insertChildNodeBefore(node, before);
}
void ChartView::moveNodeBack(QSGNode *node, size_t z)
{
auto &parent = getZNode(*rootNode, z);
parent.removeChildNode(node);
parent.appendChildNode(node);
}
void ChartView::moveNodeAfter(QSGNode *node, size_t z, QSGNode *before)
{
auto &parent = getZNode(*rootNode, z);
parent.removeChildNode(node);
parent.insertChildNodeAfter(node, before);
}
void ChartView::moveNodeFront(QSGNode *node, size_t z)
{
auto &parent = getZNode(*rootNode, z);
parent.removeChildNode(node);
parent.prependChildNode(node);
}
void ChartView::registerChartItem(ChartItem &item)
{
cleanItems.append(item);
if (item.dragable)
dragableItems.push_back(&item);
}
void ChartView::registerDirtyChartItem(ChartItem &item)
{
if (item.dirty)
return;
cleanItems.remove(item);
dirtyItems.append(item);
item.dirty = true;
}
void ChartView::deleteChartItemInternal(ChartItem &item)
{
if (item.dirty)
dirtyItems.remove(item);
else
cleanItems.remove(item);
deletedItems.append(item);
if (item.dragable) {
// This becomes inefficient if there are many dragable items!
auto it = std::find(dragableItems.begin(), dragableItems.end(), &item);
if (it == dragableItems.end())
fprintf(stderr, "warning: dragable item not found\n");
else
dragableItems.erase(it);
}
}
ChartView::ChartItemList::ChartItemList() : first(nullptr), last(nullptr)
{
}
void ChartView::ChartItemList::clear()
{
first = last = nullptr;
}
void ChartView::ChartItemList::remove(ChartItem &item)
{
if (item.next)
item.next->prev = item.prev;
else
last = item.prev;
if (item.prev)
item.prev->next = item.next;
else
first = item.next;
item.prev = item.next = nullptr;
}
void ChartView::ChartItemList::append(ChartItem &item)
{
if (!first) {
item.prev = nullptr;
first = &item;
} else {
item.prev = last;
last->next = &item;
}
item.next = nullptr;
last = &item;
}
void ChartView::ChartItemList::splice(ChartItemList &l2)
{
if (!first) // if list is empty -> nothing to do.
return;
if (!l2.first) {
l2 = *this;
} else {
l2.last->next = first;
first->prev = l2.last;
l2.last = last;
}
clear();
}
QQuickWindow *ChartView::w() const
{
return window();
}
void ChartView::setBackgroundColor(QColor color)
{
backgroundColor = color;
if (rootNode)
rootNode->backgroundNode->setColor(color);
}
QSizeF ChartView::size() const
{
return boundingRect().size();
}
QRectF ChartView::plotArea() const
{
return plotRect;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void ChartView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
#else
void ChartView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
#endif
{
plotRect = QRectF(QPointF(0.0, 0.0), newGeometry.size());
backgroundDirty = true;
plotAreaChanged(plotRect.size());
// Do we need to call the base-class' version of geometryChanged? Probably for QML?
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QQuickItem::geometryChange(newGeometry, oldGeometry);
#else
QQuickItem::geometryChanged(newGeometry, oldGeometry);
#endif
}
void ChartView::setLayerVisibility(size_t z, bool visible)
{
if (rootNode && z < rootNode->zNodes.size())
rootNode->zNodes[z]->setVisible(visible);
}
void ChartView::mousePressEvent(QMouseEvent *event)
{
// Only left-button for drag events.
if (event->button() != Qt::LeftButton)
return event->ignore();
QPointF pos = event->localPos();
for (auto item: dragableItems) {
QRectF rect = item->getRect();
if (rect.contains(pos)) {
dragStartMouse = pos;
dragStartItem = rect.topLeft();
draggedItem = item;
draggedItem->startDrag(pos);
grabMouse();
setKeepMouseGrab(true); // don't allow Qt to steal the grab
event->accept();
return;
}
}
event->ignore();
}
void ChartView::mouseReleaseEvent(QMouseEvent *event)
{
if (draggedItem) {
QPointF pos = event->localPos();
draggedItem->stopDrag(pos);
draggedItem.reset();
ungrabMouse();
event->accept();
}
}
void ChartView::mouseMoveEvent(QMouseEvent *event)
{
if (draggedItem) {
QSizeF sceneSize = size();
if (sceneSize.width() <= 1.0 || sceneSize.height() <= 1.0)
return;
draggedItem->drag(event->pos() - dragStartMouse + dragStartItem);
update();
}
}

114
qt-quick/chartview.h Normal file
View File

@ -0,0 +1,114 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef CHART_VIEW_H
#define CHART_VIEW_H
#include "chartitem_ptr.h"
#include <QQuickItem>
class ChartItem;
class QSGTexture;
class RootNode; // Internal implementation detail
class ChartView : public QQuickItem {
Q_OBJECT
public:
ChartView(QQuickItem *parent, size_t maxZ);
~ChartView();
QQuickWindow *w() const; // Make window available to items
QSizeF size() const;
QRectF plotArea() const;
void setBackgroundColor(QColor color); // Chart must be replot for color to become effective.
void addQSGNode(QSGNode *node, size_t z, bool moveAfter, QSGNode *node2);
// Must only be called in render thread!
// If node2 is nullptr move to begin or end of list.
void moveNodeBefore(QSGNode *node, size_t z, QSGNode *before);
void moveNodeBack(QSGNode *node, size_t z);
void moveNodeAfter(QSGNode *node, size_t z, QSGNode *after);
void moveNodeFront(QSGNode *node, size_t z);
void registerChartItem(ChartItem &item);
void registerDirtyChartItem(ChartItem &item);
void emergencyShutdown(); // Called when QQuick decides to delete our root node.
// Create a chart item and add it to the scene.
// The item must not be deleted by the caller, but can be
// scheduled for deletion using deleteChartItem() below.
// Most items can be made invisible, which is preferred over deletion.
// All items on the scene will be deleted once the chart is reset.
template <typename T, class... Args>
ChartItemPtr<T> createChartItem(Args&&... args);
template <typename T>
void deleteChartItem(ChartItemPtr<T> &item);
protected:
void setLayerVisibility(size_t z, bool visible);
void clearItems();
// This is called when Qt decided to reset our rootNode, which invalidates all items on the chart.
// The base class must invalidate all pointers and references.
virtual void resetPointers() = 0;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
// QtQuick related things
size_t maxZ;
bool backgroundDirty;
QRectF plotRect;
QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;
QColor backgroundColor;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
#else
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override;
#endif
virtual void plotAreaChanged(const QSizeF &size) = 0;
RootNode *rootNode;
// There are three double linked lists of chart items:
// clean items, dirty items and items to be deleted.
// Note that only the render thread must delete chart items,
// and therefore these lists are the only owning pointers
// to chart items. All other pointers are non-owning and
// can therefore become stale.
struct ChartItemList {
ChartItemList();
ChartItem *first, *last;
void append(ChartItem &item);
void remove(ChartItem &item);
void clear();
void splice(ChartItemList &list);
};
ChartItemList cleanItems, dirtyItems, deletedItems;
void deleteChartItemInternal(ChartItem &item);
void freeDeletedChartItems();
// Keep a list of dragable items. For now, there are no many,
// so keep it unsorted. In the future we might want to sort by
// coordinates.
std::vector<ChartItem *> dragableItems;
ChartItemPtr<ChartItem> draggedItem;
QPointF dragStartMouse, dragStartItem;
};
// This implementation detail must be known to users of the class.
// Perhaps move it into a chartview_impl.h file.
template <typename T, class... Args>
ChartItemPtr<T> ChartView::createChartItem(Args&&... args)
{
return ChartItemPtr(new T(*this, std::forward<Args>(args)...));
}
template <typename T>
void ChartView::deleteChartItem(ChartItemPtr<T> &item)
{
deleteChartItemInternal(*item);
item.reset();
}
#endif

View File

@ -266,7 +266,7 @@ std::vector<BarSeries::SubItem> BarSeries::makeSubItems(std::vector<SubItemDesc>
for (auto &[v, dives, label]: items) { for (auto &[v, dives, label]: items) {
if (v > 0.0) { if (v > 0.0) {
bool selected = allDivesSelected(dives); bool selected = allDivesSelected(dives);
res.push_back({ view.createChartItem<ChartBarItem>(ChartZValue::Series, barBorderWidth), res.push_back({ view.createChartItem<ChartBarItem>(ChartZValue::Series, theme, barBorderWidth),
std::move(dives), std::move(dives),
{}, from, from + v, bin_nr, selected }); {}, from, from + v, bin_nr, selected });
if (!label.empty()) if (!label.empty())
@ -412,7 +412,7 @@ bool BarSeries::hover(QPointF pos)
Item &item = items[highlighted.bar]; Item &item = items[highlighted.bar];
item.highlight(index.subitem, true, binCount(), theme); item.highlight(index.subitem, true, binCount(), theme);
if (!information) if (!information)
information = view.createChartItem<InformationBox>(); information = view.createChartItem<InformationBox>(theme);
information->setText(makeInfo(item, highlighted.subitem), pos); information->setText(makeInfo(item, highlighted.subitem), pos);
information->setVisible(true); information->setVisible(true);
} else { } else {

View File

@ -5,9 +5,9 @@
#ifndef BAR_SERIES_H #ifndef BAR_SERIES_H
#define BAR_SERIES_H #define BAR_SERIES_H
#include "statshelper.h"
#include "statsseries.h" #include "statsseries.h"
#include "statsvariables.h" #include "statsvariables.h"
#include "qt-quick/chartitem_ptr.h"
#include <memory> #include <memory>
#include <vector> #include <vector>

View File

@ -32,7 +32,7 @@ BoxSeries::Item::Item(StatsView &view, BoxSeries *series, double lowerBound, dou
binName(binName), binName(binName),
selected(allDivesSelected(q.dives)) selected(allDivesSelected(q.dives))
{ {
item = view.createChartItem<ChartBoxItem>(ChartZValue::Series, boxBorderWidth); item = view.createChartItem<ChartBoxItem>(ChartZValue::Series, theme, boxBorderWidth);
highlight(false, theme); highlight(false, theme);
updatePosition(series); updatePosition(series);
} }
@ -130,7 +130,7 @@ bool BoxSeries::hover(QPointF pos)
Item &item = *items[highlighted]; Item &item = *items[highlighted];
item.highlight(true, theme); item.highlight(true, theme);
if (!information) if (!information)
information = view.createChartItem<InformationBox>(); information = view.createChartItem<InformationBox>(theme);
information->setText(formatInformation(item), pos); information->setText(formatInformation(item), pos);
information->setVisible(true); information->setVisible(true);
} else { } else {

View File

@ -3,6 +3,8 @@
#include "statscolors.h" #include "statscolors.h"
#include "statsview.h" #include "statsview.h"
#include "core/globals.h" #include "core/globals.h"
#include "qt-quick/chartitemhelper.h"
#include "qt-quick/chartitem_private.h"
#include <cmath> #include <cmath>
#include <QQuickWindow> #include <QQuickWindow>
@ -14,104 +16,11 @@
static int selectionOverlayPixelSize = 2; static int selectionOverlayPixelSize = 2;
static int round_up(double f)
{
return static_cast<int>(ceil(f));
}
ChartItem::ChartItem(StatsView &v, ChartZValue z) :
dirty(false), prev(nullptr), next(nullptr),
zValue(z), view(v)
{
// Register before the derived constructors run, so that the
// derived classes can mark the item as dirty in the constructor.
v.registerChartItem(*this);
}
ChartItem::~ChartItem()
{
}
QSizeF ChartItem::sceneSize() const
{
return view.size();
}
void ChartItem::markDirty()
{
view.registerDirtyChartItem(*this);
}
ChartPixmapItem::ChartPixmapItem(StatsView &v, ChartZValue z) : HideableChartItem(v, z),
positionDirty(false), textureDirty(false)
{
}
ChartPixmapItem::~ChartPixmapItem()
{
painter.reset(); // Make sure to destroy painter before image that is painted on
}
void ChartPixmapItem::setTextureDirty()
{
textureDirty = true;
markDirty();
}
void ChartPixmapItem::setPositionDirty()
{
positionDirty = true;
markDirty();
}
void ChartPixmapItem::render(const StatsTheme &)
{
if (!node) {
createNode(view.w()->createImageNode());
view.addQSGNode(node.get(), zValue);
}
updateVisible();
if (!img) {
resize(QSizeF(1,1));
img->fill(Qt::transparent);
}
if (textureDirty) {
texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel));
node->node->setTexture(texture.get());
textureDirty = false;
}
if (positionDirty) {
node->node->setRect(rect);
positionDirty = false;
}
}
void ChartPixmapItem::resize(QSizeF size)
{
painter.reset();
img.reset(new QImage(round_up(size.width()), round_up(size.height()), QImage::Format_ARGB32));
painter.reset(new QPainter(img.get()));
painter->setRenderHint(QPainter::Antialiasing);
rect.setSize(size);
setTextureDirty();
}
void ChartPixmapItem::setPos(QPointF pos)
{
rect.moveTopLeft(pos);
setPositionDirty();
}
QRectF ChartPixmapItem::getRect() const
{
return rect;
}
static const int scatterItemDiameter = 10; static const int scatterItemDiameter = 10;
static const int scatterItemBorder = 1; static const int scatterItemBorder = 1;
ChartScatterItem::ChartScatterItem(StatsView &v, ChartZValue z, bool selected) : HideableChartItem(v, z), ChartScatterItem::ChartScatterItem(ChartView &v, size_t z, const StatsTheme &theme, bool selected) : HideableChartItem(v, z),
theme(theme),
positionDirty(false), textureDirty(false), positionDirty(false), textureDirty(false),
highlight(selected ? Highlight::Selected : Highlight::Unselected) highlight(selected ? Highlight::Selected : Highlight::Unselected)
{ {
@ -122,7 +31,7 @@ ChartScatterItem::~ChartScatterItem()
{ {
} }
static QSGTexture *createScatterTexture(StatsView &view, const QColor &color, const QColor &borderColor) static QSGTexture *createScatterTexture(ChartView &view, const QColor &color, const QColor &borderColor)
{ {
QImage img(scatterItemDiameter, scatterItemDiameter, QImage::Format_ARGB32); QImage img(scatterItemDiameter, scatterItemDiameter, QImage::Format_ARGB32);
img.fill(Qt::transparent); img.fill(Qt::transparent);
@ -138,7 +47,7 @@ static QSGTexture *createScatterTexture(StatsView &view, const QColor &color, co
return view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel); return view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel);
} }
QSGTexture *ChartScatterItem::getTexture(const StatsTheme &theme) const QSGTexture *ChartScatterItem::getTexture() const
{ {
switch (highlight) { switch (highlight) {
default: default:
@ -151,8 +60,9 @@ QSGTexture *ChartScatterItem::getTexture(const StatsTheme &theme) const
} }
} }
void ChartScatterItem::render(const StatsTheme &theme) 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));
@ -160,12 +70,12 @@ void ChartScatterItem::render(const StatsTheme &theme)
} }
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();
if (textureDirty) { if (textureDirty) {
node->node->setTexture(getTexture(theme)); node->node->setTexture(getTexture());
textureDirty = false; textureDirty = false;
} }
if (positionDirty) { if (positionDirty) {
@ -213,73 +123,8 @@ QRectF ChartScatterItem::getRect() const
return rect; return rect;
} }
ChartRectItem::ChartRectItem(StatsView &v, ChartZValue z, ChartPieItem::ChartPieItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth) : ChartPixmapItem(v, z),
const QPen &pen, const QBrush &brush, double radius) : ChartPixmapItem(v, z), theme(theme),
pen(pen), brush(brush), radius(radius)
{
}
ChartRectItem::~ChartRectItem()
{
}
void ChartRectItem::resize(QSizeF size)
{
ChartPixmapItem::resize(size);
img->fill(Qt::transparent);
painter->setPen(pen);
painter->setBrush(brush);
QSize imgSize = img->size();
int width = pen.width();
QRect rect(width / 2, width / 2, imgSize.width() - width, imgSize.height() - width);
painter->drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize);
}
ChartTextItem::ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const std::vector<QString> &text, bool center) :
ChartPixmapItem(v, z), f(f), center(center)
{
QFontMetrics fm(f);
double totalWidth = 1.0;
fontHeight = static_cast<double>(fm.height());
double totalHeight = std::max(1.0, static_cast<double>(text.size()) * fontHeight);
items.reserve(text.size());
for (const QString &s: text) {
double w = fm.size(Qt::TextSingleLine, s).width();
items.push_back({ s, w });
if (w > totalWidth)
totalWidth = w;
}
resize(QSizeF(totalWidth, totalHeight));
}
ChartTextItem::ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const QString &text) :
ChartTextItem(v, z, f, std::vector<QString>({ text }), true)
{
}
void ChartTextItem::setColor(const QColor &c)
{
setColor(c, Qt::transparent);
}
void ChartTextItem::setColor(const QColor &c, const QColor &background)
{
img->fill(background);
double y = 0.0;
painter->setPen(QPen(c));
painter->setFont(f);
double totalWidth = getRect().width();
for (const auto &[s, w]: items) {
double x = center ? round((totalWidth - w) / 2.0) : 0.0;
QRectF rect(x, y, w, fontHeight);
painter->drawText(rect, s);
y += fontHeight;
}
setTextureDirty();
}
ChartPieItem::ChartPieItem(StatsView &v, ChartZValue z, double borderWidth) : ChartPixmapItem(v, z),
borderWidth(borderWidth) borderWidth(borderWidth)
{ {
} }
@ -300,7 +145,7 @@ static QBrush makeBrush(QColor fill, bool selected, const StatsTheme &theme)
return QBrush(img); return QBrush(img);
} }
void ChartPieItem::drawSegment(double from, double to, QColor fill, QColor border, bool selected, const StatsTheme &theme) void ChartPieItem::drawSegment(double from, double to, QColor fill, QColor border, bool selected)
{ {
painter->setPen(QPen(border, borderWidth)); painter->setPen(QPen(border, borderWidth));
painter->setBrush(makeBrush(fill, selected, theme)); painter->setBrush(makeBrush(fill, selected, theme));
@ -320,101 +165,13 @@ void ChartPieItem::resize(QSizeF size)
img->fill(Qt::transparent); img->fill(Qt::transparent);
} }
ChartLineItemBase::ChartLineItemBase(StatsView &v, ChartZValue z, QColor color, double width) : HideableChartItem(v, z), ChartLineItemBase::ChartLineItemBase(ChartView &v, size_t z, QColor color, double width) : HideableChartItem(v, z),
color(color), width(width), positionDirty(false), materialDirty(false) color(color), width(width), positionDirty(false), materialDirty(false)
{ {
} }
ChartLineItemBase::~ChartLineItemBase() ChartBarItem::ChartBarItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth) : HideableChartItem(v, z),
{ theme(theme),
}
void ChartLineItemBase::setLine(QPointF fromIn, QPointF toIn)
{
from = fromIn;
to = toIn;
positionDirty = true;
markDirty();
}
// Helper function to set points
void setPoint(QSGGeometry::Point2D &v, const QPointF &p)
{
v.set(static_cast<float>(p.x()), static_cast<float>(p.y()));
}
void setPoint(QSGGeometry::TexturedPoint2D &v, const QPointF &p, const QPointF &t)
{
v.set(static_cast<float>(p.x()), static_cast<float>(p.y()),
static_cast<float>(t.x()), static_cast<float>(t.y()));
}
void ChartLineItem::render(const StatsTheme &)
{
if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
geometry->setDrawingMode(QSGGeometry::DrawLines);
material.reset(new QSGFlatColorMaterial);
createNode();
node->setGeometry(geometry.get());
node->setMaterial(material.get());
view.addQSGNode(node.get(), zValue);
positionDirty = materialDirty = true;
}
updateVisible();
if (positionDirty) {
// Attention: width is a geometry property and therefore handled by position dirty!
geometry->setLineWidth(static_cast<float>(width));
auto vertices = geometry->vertexDataAsPoint2D();
setPoint(vertices[0], from);
setPoint(vertices[1], to);
node->markDirty(QSGNode::DirtyGeometry);
}
if (materialDirty) {
material->setColor(color);
node->markDirty(QSGNode::DirtyMaterial);
}
positionDirty = materialDirty = false;
}
void ChartRectLineItem::render(const StatsTheme &)
{
if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 5));
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
material.reset(new QSGFlatColorMaterial);
createNode();
node->setGeometry(geometry.get());
node->setMaterial(material.get());
view.addQSGNode(node.get(), zValue);
positionDirty = materialDirty = true;
}
updateVisible();
if (positionDirty) {
// Attention: width is a geometry property and therefore handled by position dirty!
geometry->setLineWidth(static_cast<float>(width));
auto vertices = geometry->vertexDataAsPoint2D();
setPoint(vertices[0], from);
setPoint(vertices[1], QPointF(from.x(), to.y()));
setPoint(vertices[2], to);
setPoint(vertices[3], QPointF(to.x(), from.y()));
setPoint(vertices[4], from);
node->markDirty(QSGNode::DirtyGeometry);
}
if (materialDirty) {
material->setColor(color);
node->markDirty(QSGNode::DirtyMaterial);
}
positionDirty = materialDirty = false;
}
ChartBarItem::ChartBarItem(StatsView &v, ChartZValue z, double borderWidth) : HideableChartItem(v, z),
borderWidth(borderWidth), selected(false), borderWidth(borderWidth), selected(false),
positionDirty(false), colorDirty(false), selectedDirty(false) positionDirty(false), colorDirty(false), selectedDirty(false)
{ {
@ -424,7 +181,7 @@ ChartBarItem::~ChartBarItem()
{ {
} }
QSGTexture *ChartBarItem::getSelectedTexture(const StatsTheme &theme) const QSGTexture *ChartBarItem::getSelectedTexture() const
{ {
if (!theme.selectedTexture) { if (!theme.selectedTexture) {
QImage img(2, 2, QImage::Format_ARGB32); QImage img(2, 2, QImage::Format_ARGB32);
@ -436,8 +193,9 @@ QSGTexture *ChartBarItem::getSelectedTexture(const StatsTheme &theme) const
return theme.selectedTexture; return theme.selectedTexture;
} }
void ChartBarItem::render(const StatsTheme &theme) void ChartBarItem::render()
{ {
doRearrange();
if (!node) { if (!node) {
createNode(view.w()->createRectangleNode()); createNode(view.w()->createRectangleNode());
@ -450,7 +208,7 @@ void ChartBarItem::render(const StatsTheme &theme)
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();
@ -481,7 +239,7 @@ void ChartBarItem::render(const StatsTheme &theme)
selectionGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4)); selectionGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4));
selectionGeometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); selectionGeometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
selectionMaterial.reset(new QSGTextureMaterial); selectionMaterial.reset(new QSGTextureMaterial);
selectionMaterial->setTexture(getSelectedTexture(theme)); selectionMaterial->setTexture(getSelectedTexture());
selectionMaterial->setHorizontalWrapMode(QSGTexture::Repeat); selectionMaterial->setHorizontalWrapMode(QSGTexture::Repeat);
selectionMaterial->setVerticalWrapMode(QSGTexture::Repeat); selectionMaterial->setVerticalWrapMode(QSGTexture::Repeat);
selectionNode.reset(new QSGGeometryNode); selectionNode.reset(new QSGGeometryNode);
@ -545,8 +303,8 @@ QRectF ChartBarItem::getRect() const
return rect; return rect;
} }
ChartBoxItem::ChartBoxItem(StatsView &v, ChartZValue z, double borderWidth) : ChartBoxItem::ChartBoxItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth) :
ChartBarItem(v, z, borderWidth) ChartBarItem(v, z, theme, borderWidth)
{ {
} }
@ -554,12 +312,13 @@ ChartBoxItem::~ChartBoxItem()
{ {
} }
void ChartBoxItem::render(const StatsTheme &theme) 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;
ChartBarItem::render(theme); // This will create the base node, so no need to check for that. ChartBarItem::render(); // This will create the base node, so no need to check for that.
if (!whiskersNode) { if (!whiskersNode) {
whiskersGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 10)); whiskersGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 10));
whiskersGeometry->setDrawingMode(QSGGeometry::DrawLines); whiskersGeometry->setDrawingMode(QSGGeometry::DrawLines);

View File

@ -1,160 +1,36 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
// Wrappers around QSGImageNode that allow painting onto an image // Chart item specific to the statistics module
// and then turning that into a texture to be displayed in a QQuickItem. #ifndef STATS_CHART_ITEM_H
#ifndef CHART_ITEM_H #define STATS_CHART_ITEM_H
#define CHART_ITEM_H
#include "statshelper.h" #include "qt-quick/chartitem.h"
#include <memory>
#include <QPainter>
class QSGGeometry;
class QSGGeometryNode;
class QSGFlatColorMaterial;
class QSGImageNode;
class QSGRectangleNode;
class QSGTexture;
class QSGTextureMaterial;
class StatsTheme; class StatsTheme;
class StatsView; class ChartView;
enum class ChartZValue : int;
class ChartItem {
public:
// Only call on render thread!
virtual void render(const StatsTheme &theme) = 0;
bool dirty; // If true, call render() when rebuilding the scene
ChartItem *prev, *next; // Double linked list of items
const ChartZValue zValue;
virtual ~ChartItem(); // Attention: must only be called by render thread.
protected:
ChartItem(StatsView &v, ChartZValue z);
QSizeF sceneSize() const;
StatsView &view;
void markDirty();
};
template <typename Node>
class HideableChartItem : public ChartItem {
protected:
HideableChartItem(StatsView &v, ChartZValue z);
std::unique_ptr<Node> node;
bool visible;
bool visibleChanged;
template<class... Args>
void createNode(Args&&... args); // Call to create node with visibility flag.
void updateVisible(); // Must be called by child class to update visibility flag!
public:
void setVisible(bool visible);
};
// A shortcut for ChartItems based on a hideable proxy item
template <typename Node>
using HideableChartProxyItem = HideableChartItem<HideableQSGNode<QSGProxyNode<Node>>>;
// A chart item that blits a precalculated pixmap onto the scene.
class ChartPixmapItem : public HideableChartProxyItem<QSGImageNode> {
public:
ChartPixmapItem(StatsView &v, ChartZValue z);
~ChartPixmapItem();
void setPos(QPointF pos);
void render(const StatsTheme &theme) override;
QRectF getRect() const;
protected:
void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*.
std::unique_ptr<QPainter> painter;
std::unique_ptr<QImage> img;
void setTextureDirty();
void setPositionDirty();
QRectF rect;
private:
bool positionDirty; // true if the position changed since last render
bool textureDirty; // true if the pixmap changed since last render
std::unique_ptr<QSGTexture> texture;
};
// Draw a rectangular background after resize. Children are responsible for calling update().
class ChartRectItem : public ChartPixmapItem {
public:
ChartRectItem(StatsView &v, ChartZValue z, const QPen &pen, const QBrush &brush, double radius);
~ChartRectItem();
void resize(QSizeF size);
private:
QPen pen;
QBrush brush;
double radius;
};
// Attention: text is only drawn after calling setColor()!
class ChartTextItem : public ChartPixmapItem {
public:
ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const std::vector<QString> &text, bool center);
ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const QString &text);
void setColor(const QColor &color); // Draw on transparent background
void setColor(const QColor &color, const QColor &background); // Fill rectangle with given background color
private:
const QFont &f;
double fontHeight;
bool center;
struct Item {
QString s;
double width;
};
std::vector<Item> items;
};
// A pie chart item: draws disk segments onto a pixmap. // A pie chart item: draws disk segments onto a pixmap.
class ChartPieItem : public ChartPixmapItem { class ChartPieItem : public ChartPixmapItem {
public: public:
ChartPieItem(StatsView &v, ChartZValue z, double borderWidth); ChartPieItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth);
void drawSegment(double from, double to, QColor fill, QColor border, bool selected, const StatsTheme &theme); // from and to are relative (0-1 is full disk). void drawSegment(double from, double to, QColor fill, QColor border, bool selected); // from and to are relative (0-1 is full disk).
void resize(QSizeF size); // As in base class, but clears the canvas void resize(QSizeF size); // As in base class, but clears the canvas
private: private:
const StatsTheme &theme;
double borderWidth; double borderWidth;
}; };
// Common data for line and rect items. Both are represented by two points.
class ChartLineItemBase : public HideableChartItem<HideableQSGNode<QSGGeometryNode>> {
public:
ChartLineItemBase(StatsView &v, ChartZValue z, QColor color, double width);
~ChartLineItemBase();
void setLine(QPointF from, QPointF to);
protected:
QPointF from, to;
QColor color;
double width;
bool positionDirty;
bool materialDirty;
std::unique_ptr<QSGFlatColorMaterial> material;
std::unique_ptr<QSGGeometry> geometry;
};
class ChartLineItem : public ChartLineItemBase {
public:
using ChartLineItemBase::ChartLineItemBase;
void render(const StatsTheme &theme) override;
};
// A simple rectangle without fill. Specified by any two opposing vertices.
class ChartRectLineItem : public ChartLineItemBase {
public:
using ChartLineItemBase::ChartLineItemBase;
void render(const StatsTheme &theme) override;
};
// A bar in a bar chart: a rectangle bordered by lines. // A bar in a bar chart: a rectangle bordered by lines.
class ChartBarItem : public HideableChartProxyItem<QSGRectangleNode> { class ChartBarItem : public HideableChartProxyItem<QSGRectangleNode> {
public: public:
ChartBarItem(StatsView &v, ChartZValue z, double borderWidth); ChartBarItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth);
~ChartBarItem(); ~ChartBarItem();
void setColor(QColor color, QColor borderColor); void setColor(QColor color, QColor borderColor);
void setRect(const QRectF &rect); void setRect(const QRectF &rect);
void setSelected(bool selected); void setSelected(bool selected);
QRectF getRect() const; QRectF getRect() const;
void render(const StatsTheme &theme) override; void render() override;
protected: protected:
const StatsTheme &theme;
QColor color, borderColor; QColor color, borderColor;
double borderWidth; double borderWidth;
QRectF rect; QRectF rect;
@ -170,17 +46,17 @@ private:
std::unique_ptr<QSGGeometryNode> selectionNode; std::unique_ptr<QSGGeometryNode> selectionNode;
std::unique_ptr<QSGTextureMaterial> selectionMaterial; std::unique_ptr<QSGTextureMaterial> selectionMaterial;
std::unique_ptr<QSGGeometry> selectionGeometry; std::unique_ptr<QSGGeometry> selectionGeometry;
QSGTexture *getSelectedTexture(const StatsTheme &theme) const; QSGTexture *getSelectedTexture() const;
}; };
// A box-and-whiskers item. This is a bit lazy: derive from the bar item and add whiskers. // A box-and-whiskers item. This is a bit lazy: derive from the bar item and add whiskers.
class ChartBoxItem : public ChartBarItem { class ChartBoxItem : public ChartBarItem {
public: public:
ChartBoxItem(StatsView &v, ChartZValue z, double borderWidth); ChartBoxItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth);
~ChartBoxItem(); ~ChartBoxItem();
void setBox(const QRectF &rect, double min, double max, double median); // The rect describes Q1, Q3. void setBox(const QRectF &rect, double min, double max, double median); // The rect describes Q1, Q3.
QRectF getRect() const; // Note: this extends the center rectangle to include the whiskers. QRectF getRect() const; // Note: this extends the center rectangle to include the whiskers.
void render(const StatsTheme &theme) override; void render() override;
private: private:
double min, max, median; double min, max, median;
std::unique_ptr<QSGGeometryNode> whiskersNode; std::unique_ptr<QSGGeometryNode> whiskersNode;
@ -194,7 +70,7 @@ private:
// scatter item here, but so it is for now. // scatter item here, but so it is for now.
class ChartScatterItem : public HideableChartProxyItem<QSGImageNode> { class ChartScatterItem : public HideableChartProxyItem<QSGImageNode> {
public: public:
ChartScatterItem(StatsView &v, ChartZValue z, bool selected); ChartScatterItem(ChartView &v, size_t z, const StatsTheme &theme, bool selected);
~ChartScatterItem(); ~ChartScatterItem();
// Currently, there is no highlighted and selected status. // Currently, there is no highlighted and selected status.
@ -205,49 +81,17 @@ public:
}; };
void setPos(QPointF pos); // Specifies the *center* of the item. void setPos(QPointF pos); // Specifies the *center* of the item.
void setHighlight(Highlight highlight); // In the future, support different kinds of scatter items. void setHighlight(Highlight highlight); // In the future, support different kinds of scatter items.
void render(const StatsTheme &theme) override; void render() override;
QRectF getRect() const; QRectF getRect() const;
bool contains(QPointF point) const; bool contains(QPointF point) const;
bool inRect(const QRectF &rect) const; bool inRect(const QRectF &rect) const;
private: private:
QSGTexture *getTexture(const StatsTheme &theme) const; const StatsTheme &theme;
QSGTexture *getTexture() const;
QRectF rect; QRectF rect;
QSizeF textureSize; QSizeF textureSize;
bool positionDirty, textureDirty; bool positionDirty, textureDirty;
Highlight highlight; Highlight highlight;
}; };
// Implementation detail of templates - move to serparate header file
template <typename Node>
void HideableChartItem<Node>::setVisible(bool visibleIn)
{
if (visible == visibleIn)
return;
visible = visibleIn;
visibleChanged = true;
markDirty();
}
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>
HideableChartItem<Node>::HideableChartItem(StatsView &v, ChartZValue z) : ChartItem(v, z),
visible(true), visibleChanged(false)
{
}
template <typename Node>
void HideableChartItem<Node>::updateVisible()
{
if (visibleChanged)
node->setVisible(visible);
visibleChanged = false;
}
#endif #endif

View File

@ -5,7 +5,7 @@
static const double histogramMarkerWidth = 2.0; static const double histogramMarkerWidth = 2.0;
HistogramMarker::HistogramMarker(StatsView &view, double val, bool horizontal, HistogramMarker::HistogramMarker(ChartView &view, double val, bool horizontal,
QColor color, StatsAxis *xAxis, StatsAxis *yAxis) : QColor color, StatsAxis *xAxis, StatsAxis *yAxis) :
ChartLineItem(view, ChartZValue::ChartFeatures, color, histogramMarkerWidth), ChartLineItem(view, ChartZValue::ChartFeatures, color, histogramMarkerWidth),
xAxis(xAxis), yAxis(yAxis), xAxis(xAxis), yAxis(yAxis),

View File

@ -5,12 +5,11 @@
#include "chartitem.h" #include "chartitem.h"
class StatsAxis; class StatsAxis;
class StatsView;
// A line marking median or mean in histograms // A line marking median or mean in histograms
class HistogramMarker : public ChartLineItem { class HistogramMarker : public ChartLineItem {
public: public:
HistogramMarker(StatsView &view, double val, bool horizontal, QColor color, StatsAxis *xAxis, StatsAxis *yAxis); HistogramMarker(ChartView &view, double val, bool horizontal, QColor color, StatsAxis *xAxis, StatsAxis *yAxis);
void updatePosition(); void updatePosition();
private: private:
StatsAxis *xAxis, *yAxis; StatsAxis *xAxis, *yAxis;

View File

@ -1,6 +1,5 @@
#include "informationbox.h" #include "informationbox.h"
#include "statscolors.h" #include "statscolors.h"
#include "statsview.h"
#include "zvalues.h" #include "zvalues.h"
#include <QFontMetrics> #include <QFontMetrics>
@ -9,11 +8,11 @@ static const int informationBorder = 2;
static const double informationBorderRadius = 4.0; // Radius of rounded corners static const double informationBorderRadius = 4.0; // Radius of rounded corners
static const int distanceFromPointer = 10; // Distance to place box from mouse pointer or scatter item static const int distanceFromPointer = 10; // Distance to place box from mouse pointer or scatter item
InformationBox::InformationBox(StatsView &v) : InformationBox::InformationBox(ChartView &v, const StatsTheme &theme) :
ChartRectItem(v, ChartZValue::InformationBox, ChartRectItem(v, ChartZValue::InformationBox,
QPen(v.getCurrentTheme().informationBorderColor, informationBorder), QPen(theme.informationBorderColor, informationBorder),
QBrush(v.getCurrentTheme().informationColor), informationBorderRadius), QBrush(theme.informationColor), informationBorderRadius),
theme(v.getCurrentTheme()), theme(theme),
width(0.0), width(0.0),
height(0.0) height(0.0)
{ {

View File

@ -10,11 +10,12 @@
#include <memory> #include <memory>
struct dive; struct dive;
class StatsView; class ChartView;
class StatsTheme;
// Information window showing data of highlighted dive // Information window showing data of highlighted dive
struct InformationBox : ChartRectItem { struct InformationBox : ChartRectItem {
InformationBox(StatsView &); InformationBox(ChartView &, const StatsTheme &theme);
void setText(const std::vector<QString> &text, QPointF pos); void setText(const std::vector<QString> &text, QPointF pos);
void setPos(QPointF pos); void setPos(QPointF pos);
int recommendedMaxLines() const; int recommendedMaxLines() const;

View File

@ -14,12 +14,13 @@ static const double legendBoxBorderRadius = 4.0; // radius of rounded corners
static const double legendBoxScale = 0.8; // 1.0: text-height of the used font static const double legendBoxScale = 0.8; // 1.0: text-height of the used font
static const double legendInternalBorderSize = 2.0; static const double legendInternalBorderSize = 2.0;
Legend::Legend(StatsView &view, const std::vector<QString> &names) : Legend::Legend(ChartView &view, const StatsTheme &theme, const std::vector<QString> &names) :
ChartRectItem(view, ChartZValue::Legend, ChartRectItem(view, ChartZValue::Legend,
QPen(view.getCurrentTheme().legendBorderColor, legendBorderSize), QPen(theme.legendBorderColor, legendBorderSize),
QBrush(view.getCurrentTheme().legendColor), legendBoxBorderRadius), QBrush(theme.legendColor), legendBoxBorderRadius,
true),
displayedItems(0), width(0.0), height(0.0), displayedItems(0), width(0.0), height(0.0),
theme(view.getCurrentTheme()), theme(theme),
posInitialized(false) posInitialized(false)
{ {
entries.reserve(names.size()); entries.reserve(names.size());

View File

@ -3,7 +3,7 @@
#ifndef STATS_LEGEND_H #ifndef STATS_LEGEND_H
#define STATS_LEGEND_H #define STATS_LEGEND_H
#include "chartitem.h" #include "qt-quick/chartitem.h"
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -13,7 +13,7 @@ class StatsTheme;
class Legend : public ChartRectItem { class Legend : public ChartRectItem {
public: public:
Legend(StatsView &view, const std::vector<QString> &names); Legend(ChartView &view, const StatsTheme &theme, const std::vector<QString> &names);
void resize(); // called when the chart size changes. void resize(); // called when the chart size changes.
void setPos(QPointF pos); // Attention: not virtual - always call on this class. void setPos(QPointF pos); // Attention: not virtual - always call on this class.
private: private:

View File

@ -78,13 +78,13 @@ void PieSeries::Item::highlight(ChartPieItem &item, int bin_nr, bool highlight,
QColor border = highlight ? theme.highlightedBorderColor : theme.borderColor; QColor border = highlight ? theme.highlightedBorderColor : theme.borderColor;
if (innerLabel) if (innerLabel)
innerLabel->setColor(highlight ? theme.darkLabelColor : theme.labelColor(bin_nr, numBins), fill); innerLabel->setColor(highlight ? theme.darkLabelColor : theme.labelColor(bin_nr, numBins), fill);
item.drawSegment(angleFrom, angleTo, fill, border, selected, theme); item.drawSegment(angleFrom, angleTo, fill, border, selected);
} }
PieSeries::PieSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, const QString &categoryName, PieSeries::PieSeries(StatsView &view, StatsAxis *xAxis, StatsAxis *yAxis, const QString &categoryName,
std::vector<std::pair<QString, std::vector<dive *>>> data, ChartSortMode sortMode) : std::vector<std::pair<QString, std::vector<dive *>>> data, ChartSortMode sortMode) :
StatsSeries(view, xAxis, yAxis), StatsSeries(view, xAxis, yAxis),
item(view.createChartItem<ChartPieItem>(ChartZValue::Series, pieBorderWidth)), item(view.createChartItem<ChartPieItem>(ChartZValue::Series, theme, pieBorderWidth)),
categoryName(categoryName), categoryName(categoryName),
radius(0), radius(0),
highlighted(-1), highlighted(-1),
@ -257,7 +257,7 @@ bool PieSeries::hover(QPointF pos)
if (highlighted >= 0 && highlighted < (int)items.size()) { if (highlighted >= 0 && highlighted < (int)items.size()) {
items[highlighted].highlight(*item, highlighted, true, (int)items.size(), theme); items[highlighted].highlight(*item, highlighted, true, (int)items.size(), theme);
if (!information) if (!information)
information = view.createChartItem<InformationBox>(); information = view.createChartItem<InformationBox>(theme);
information->setText(makeInfo(highlighted), pos); information->setText(makeInfo(highlighted), pos);
information->setVisible(true); information->setVisible(true);
} else { } else {

View File

@ -3,8 +3,8 @@
#ifndef PIE_SERIES_H #ifndef PIE_SERIES_H
#define PIE_SERIES_H #define PIE_SERIES_H
#include "statshelper.h"
#include "statsseries.h" #include "statsseries.h"
#include "qt-quick/chartitem_ptr.h"
#include <memory> #include <memory>
#include <vector> #include <vector>

View File

@ -7,8 +7,8 @@
static const double quartileMarkerSize = 15.0; static const double quartileMarkerSize = 15.0;
QuartileMarker::QuartileMarker(StatsView &view, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis) : QuartileMarker::QuartileMarker(ChartView &view, const StatsTheme &theme, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis) :
ChartLineItem(view, ChartZValue::ChartFeatures, view.getCurrentTheme().quartileMarkerColor, 2.0), ChartLineItem(view, ChartZValue::ChartFeatures, theme.quartileMarkerColor, 2.0),
xAxis(xAxis), yAxis(yAxis), xAxis(xAxis), yAxis(yAxis),
pos(pos), pos(pos),
value(value) value(value)

View File

@ -5,11 +5,10 @@
#include "chartitem.h" #include "chartitem.h"
class StatsAxis; class StatsAxis;
class StatsView;
class QuartileMarker : public ChartLineItem { class QuartileMarker : public ChartLineItem {
public: public:
QuartileMarker(StatsView &view, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis); QuartileMarker(ChartView &view, const StatsTheme &theme, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis);
~QuartileMarker(); ~QuartileMarker();
void updatePosition(); void updatePosition();
private: private:

View File

@ -9,10 +9,10 @@
static const double regressionLineWidth = 2.0; static const double regressionLineWidth = 2.0;
RegressionItem::RegressionItem(StatsView &view, regression_data reg, RegressionItem::RegressionItem(ChartView &view, const StatsTheme &theme, regression_data reg,
StatsAxis *xAxis, StatsAxis *yAxis) : StatsAxis *xAxis, StatsAxis *yAxis) :
ChartPixmapItem(view, ChartZValue::ChartFeatures), ChartPixmapItem(view, ChartZValue::ChartFeatures),
theme(view.getCurrentTheme()), theme(theme),
xAxis(xAxis), yAxis(yAxis), reg(reg), xAxis(xAxis), yAxis(yAxis), reg(reg),
regression(true), confidence(true) regression(true), confidence(true)
{ {

View File

@ -6,7 +6,6 @@
class StatsAxis; class StatsAxis;
class StatsTheme; class StatsTheme;
class StatsView;
struct regression_data { struct regression_data {
double a,b; double a,b;
@ -16,7 +15,7 @@ struct regression_data {
class RegressionItem : public ChartPixmapItem { class RegressionItem : public ChartPixmapItem {
public: public:
RegressionItem(StatsView &view, regression_data data, StatsAxis *xAxis, StatsAxis *yAxis); RegressionItem(ChartView &view, const StatsTheme &theme, regression_data data, StatsAxis *xAxis, StatsAxis *yAxis);
~RegressionItem(); ~RegressionItem();
void updatePosition(); void updatePosition();
void setFeatures(bool regression, bool confidence); void setFeatures(bool regression, bool confidence);

View File

@ -3,7 +3,6 @@
#include "chartitem.h" #include "chartitem.h"
#include "informationbox.h" #include "informationbox.h"
#include "statscolors.h" #include "statscolors.h"
#include "statshelper.h"
#include "statstranslations.h" #include "statstranslations.h"
#include "statsvariables.h" #include "statsvariables.h"
#include "statsview.h" #include "statsview.h"
@ -24,8 +23,8 @@ ScatterSeries::~ScatterSeries()
{ {
} }
ScatterSeries::Item::Item(StatsView &view, ScatterSeries *series, dive *d, double pos, double value) : ScatterSeries::Item::Item(StatsView &view, ScatterSeries *series, const StatsTheme &theme, dive *d, double pos, double value) :
item(view.createChartItem<ChartScatterItem>(ChartZValue::Series, d->selected)), item(view.createChartItem<ChartScatterItem>(ChartZValue::Series, theme, d->selected)),
d(d), d(d),
selected(d->selected), selected(d->selected),
pos(pos), pos(pos),
@ -50,7 +49,7 @@ void ScatterSeries::Item::highlight(bool highlight)
void ScatterSeries::append(dive *d, double pos, double value) void ScatterSeries::append(dive *d, double pos, double value)
{ {
items.emplace_back(view, this, d, pos, value); items.emplace_back(view, this, theme, d, pos, value);
} }
void ScatterSeries::updatePositions() void ScatterSeries::updatePositions()
@ -183,7 +182,7 @@ bool ScatterSeries::hover(QPointF pos)
return false; return false;
} else { } else {
if (!information) if (!information)
information = view.createChartItem<InformationBox>(); information = view.createChartItem<InformationBox>(theme);
std::vector<QString> text; std::vector<QString> text;
text.reserve(highlighted.size() * 5); text.reserve(highlighted.size() * 5);

View File

@ -6,6 +6,7 @@
#include "statshelper.h" #include "statshelper.h"
#include "statsseries.h" #include "statsseries.h"
#include "qt-quick/chartitem_ptr.h"
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -41,7 +42,7 @@ private:
dive *d; dive *d;
bool selected; bool selected;
double pos, value; double pos, value;
Item(StatsView &view, ScatterSeries *series, dive *d, double pos, double value); Item(StatsView &view, ScatterSeries *series, const StatsTheme &theme, dive *d, double pos, double value);
void updatePosition(ScatterSeries *series); void updatePosition(ScatterSeries *series);
void highlight(bool highlight); void highlight(bool highlight);
}; };

View File

@ -1,7 +1,6 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
#include "statsaxis.h" #include "statsaxis.h"
#include "statscolors.h" #include "statscolors.h"
#include "statshelper.h"
#include "statstranslations.h" #include "statstranslations.h"
#include "statsvariables.h" #include "statsvariables.h"
#include "statsview.h" #include "statsview.h"
@ -25,9 +24,9 @@ static const double axisLabelSpaceVertical = 2.0; // Space between axis or ticks
static const double axisTitleSpaceHorizontal = 2.0; // Space between labels and title static const double axisTitleSpaceHorizontal = 2.0; // Space between labels and title
static const double axisTitleSpaceVertical = 2.0; // Space between labels and title static const double axisTitleSpaceVertical = 2.0; // Space between labels and title
StatsAxis::StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks) : StatsAxis::StatsAxis(ChartView &view, const StatsTheme &theme, const QString &title, bool horizontal, bool labelsBetweenTicks) :
ChartPixmapItem(view, ChartZValue::Axes), ChartPixmapItem(view, ChartZValue::Axes),
theme(view.getCurrentTheme()), theme(theme),
line(view.createChartItem<ChartLineItem>(ChartZValue::Axes, theme.axisColor, axisWidth)), line(view.createChartItem<ChartLineItem>(ChartZValue::Axes, theme.axisColor, axisWidth)),
title(title), horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks), title(title), horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks),
size(1.0), zeroOnScreen(0.0), min(0.0), max(1.0), labelWidth(0.0) size(1.0), zeroOnScreen(0.0), min(0.0), max(1.0), labelWidth(0.0)
@ -256,8 +255,9 @@ void StatsAxis::setPos(QPointF pos)
} }
} }
ValueAxis::ValueAxis(StatsView &view, const QString &title, double min, double max, int decimals, bool horizontal) : ValueAxis::ValueAxis(ChartView &view, const StatsTheme &theme,
StatsAxis(view, title, horizontal, false), const QString &title, double min, double max, int decimals, bool horizontal) :
StatsAxis(view, theme, title, horizontal, false),
min(min), max(max), decimals(decimals) min(min), max(max), decimals(decimals)
{ {
// Avoid degenerate cases // Avoid degenerate cases
@ -317,8 +317,8 @@ void ValueAxis::updateLabels()
} }
} }
CountAxis::CountAxis(StatsView &view, const QString &title, int count, bool horizontal) : CountAxis::CountAxis(ChartView &view, const StatsTheme &theme, const QString &title, int count, bool horizontal) :
ValueAxis(view, title, 0.0, (double)count, 0, horizontal), ValueAxis(view, theme, title, 0.0, (double)count, 0, horizontal),
count(count) count(count)
{ {
} }
@ -376,8 +376,9 @@ void CountAxis::updateLabels()
} }
} }
CategoryAxis::CategoryAxis(StatsView &view, const QString &title, const std::vector<QString> &labels, bool horizontal) : CategoryAxis::CategoryAxis(ChartView &view, const StatsTheme &theme,
StatsAxis(view, title, horizontal, true), const QString &title, const std::vector<QString> &labels, bool horizontal) :
StatsAxis(view, theme, title, horizontal, true),
labelsText(labels) labelsText(labels)
{ {
if (!labels.empty()) if (!labels.empty())
@ -437,8 +438,9 @@ void CategoryAxis::updateLabels()
} }
} }
HistogramAxis::HistogramAxis(StatsView &view, const QString &title, std::vector<HistogramAxisEntry> bins, bool horizontal) : HistogramAxis::HistogramAxis(ChartView &view, const StatsTheme &theme,
StatsAxis(view, title, horizontal, false), const QString &title, std::vector<HistogramAxisEntry> bins, bool horizontal) :
StatsAxis(view, theme, title, horizontal, false),
bin_values(std::move(bins)) bin_values(std::move(bins))
{ {
if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category
@ -643,7 +645,7 @@ static std::vector<HistogramAxisEntry> timeRangeToBins(double from, double to)
return res; return res;
} }
DateAxis::DateAxis(StatsView &view, const QString &title, double from, double to, bool horizontal) : DateAxis::DateAxis(ChartView &view, const StatsTheme &theme, const QString &title, double from, double to, bool horizontal) :
HistogramAxis(view, title, timeRangeToBins(from, to), horizontal) HistogramAxis(view, theme, title, timeRangeToBins(from, to), horizontal)
{ {
} }

View File

@ -3,12 +3,10 @@
#define STATS_AXIS_H #define STATS_AXIS_H
#include "chartitem.h" #include "chartitem.h"
#include "statshelper.h"
#include <memory> #include <memory>
#include <vector> #include <vector>
class StatsView;
class ChartLineItem; class ChartLineItem;
class QFontMetrics; class QFontMetrics;
@ -34,7 +32,7 @@ public:
std::vector<double> ticksPositions() const; // Positions in screen coordinates std::vector<double> ticksPositions() const; // Positions in screen coordinates
protected: protected:
StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks); StatsAxis(ChartView &view, const StatsTheme &theme, const QString &title, bool horizontal, bool labelsBetweenTicks);
const StatsTheme &theme; // Initialized once in constructor. const StatsTheme &theme; // Initialized once in constructor.
ChartItemPtr<ChartLineItem> line; ChartItemPtr<ChartLineItem> line;
@ -73,7 +71,8 @@ private:
class ValueAxis : public StatsAxis { class ValueAxis : public StatsAxis {
public: public:
ValueAxis(StatsView &view, const QString &title, double min, double max, int decimals, bool horizontal); ValueAxis(ChartView &view, const StatsTheme &theme, const QString &title,
double min, double max, int decimals, bool horizontal);
private: private:
double min, max; double min, max;
int decimals; int decimals;
@ -83,7 +82,7 @@ private:
class CountAxis : public ValueAxis { class CountAxis : public ValueAxis {
public: public:
CountAxis(StatsView &view, const QString &title, int count, bool horizontal); CountAxis(ChartView &view, const StatsTheme &theme, const QString &title, int count, bool horizontal);
private: private:
int count; int count;
void updateLabels() override; void updateLabels() override;
@ -92,7 +91,8 @@ private:
class CategoryAxis : public StatsAxis { class CategoryAxis : public StatsAxis {
public: public:
CategoryAxis(StatsView &view, const QString &title, const std::vector<QString> &labels, bool horizontal); CategoryAxis(ChartView &view, const StatsTheme &theme, const QString &title,
const std::vector<QString> &labels, bool horizontal);
private: private:
std::vector<QString> labelsText; std::vector<QString> labelsText;
void updateLabels(); void updateLabels();
@ -107,7 +107,8 @@ struct HistogramAxisEntry {
class HistogramAxis : public StatsAxis { class HistogramAxis : public StatsAxis {
public: public:
HistogramAxis(StatsView &view, const QString &title, std::vector<HistogramAxisEntry> bin_values, bool horizontal); HistogramAxis(ChartView &view, const StatsTheme &theme, const QString &title,
std::vector<HistogramAxisEntry> bin_values, bool horizontal);
private: private:
void updateLabels() override; void updateLabels() override;
std::pair<QString, QString> getFirstLastLabel() const override; std::pair<QString, QString> getFirstLastLabel() const override;
@ -117,7 +118,7 @@ private:
class DateAxis : public HistogramAxis { class DateAxis : public HistogramAxis {
public: public:
DateAxis(StatsView &view, const QString &title, double from, double to, bool horizontal); DateAxis(ChartView &view, const StatsTheme &theme, const QString &title, double from, double to, bool horizontal);
}; };
#endif #endif

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
// The background grid of a chart // The background grid of a chart
#include "statshelper.h" #include "qt-quick/chartitem_ptr.h"
#include <memory> #include <memory>
#include <vector> #include <vector>

View File

@ -1,14 +1,12 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0
// Helper functions to render the stats. Includes // Helper functions to render the stats.
// QSGNode template jugglery to overcome API flaws.
#ifndef STATSHELPER_H #ifndef STATSHELPER_H
#define STATSHELPER_H #define STATSHELPER_H
#include <memory>
#include <vector> #include <vector>
#include <QPointF> #include <QPointF>
#include <QSGNode>
#include "core/dive.h" struct dive;
// Round positions to integer values to avoid ugly artifacts // Round positions to integer values to avoid ugly artifacts
QPointF roundPos(const QPointF &p); QPointF roundPos(const QPointF &p);
@ -16,128 +14,4 @@ QPointF roundPos(const QPointF &p);
// Are all dives in this vector selected? // Are all dives in this vector selected?
bool allDivesSelected(const std::vector<dive *> &dives); bool allDivesSelected(const std::vector<dive *> &dives);
// A stupid pointer class that initializes to null and can be copy
// assigned. This is for historical reasons: unique_ptrs to ChartItems
// were replaced by plain pointers. Instead of nulling the plain pointers
// in the constructors, use this. Ultimately, we might think about making
// this thing smarter, once removal of individual ChartItems is implemented.
template <typename T>
class ChartItemPtr {
friend class StatsView; // Only the stats view can create these pointers
T *ptr;
ChartItemPtr(T *ptr) : ptr(ptr)
{
}
public:
ChartItemPtr() : ptr(nullptr)
{
}
ChartItemPtr(const ChartItemPtr &p) : ptr(p.ptr)
{
}
void reset()
{
ptr = nullptr;
}
ChartItemPtr &operator=(const ChartItemPtr &p)
{
ptr = p.ptr;
return *this;
}
operator bool() const
{
return !!ptr;
}
bool operator!() const
{
return !ptr;
}
T &operator*() const
{
return *ptr;
}
T *operator->() const
{
return ptr;
}
};
// In general, we want chart items to be hideable. For example to show/hide
// labels on demand. Very sadly, the QSG API is absolutely terrible with
// respect to temporarily disabling. Instead of simply having a flag,
// a QSGNode is queried using the "isSubtreeBlocked()" virtual function(!).
//
// Not only is this a slow operation performed on every single node, it
// also is often not possible to override this function: For improved
// performance, the documentation recommends to create QSG nodes via
// QQuickWindow. This provides nodes optimized for the actual hardware.
// However, this obviously means that these nodes cannot be derived from!
//
// In that case, there are two possibilities: Add a proxy node with an
// overridden "isSubtreeBlocked()" function or remove the node from the
// scene. The former was chosen here, because it is less complex.
//
// The following slightly cryptic templates are used to unify the two
// cases: The QSGNode is generated by our own code or the QSGNode is
// obtained from QQuickWindow.
//
// The "HideableQSGNode<Node>" template augments the QSGNode "Node"
// by a "setVisible()" function and overrides "isSubtreeBlocked()"
//
// The "QSGProxyNode<Node>" template is a QSGNode with a single
// child of type "Node".
//
// Thus, if the node can be created, use:
// HideableQSGNode<NodeTypeThatCanBeCreated> node
// and if the node can only be obtained from QQuickWindow, use:
// HideableQSGNode<QSGProxyNode<NodeThatCantBeCreated>> node
// The latter should obviously be typedef-ed.
//
// Yes, that's all horrible, but if nothing else it teaches us about
// composition.
template <typename Node>
class HideableQSGNode : public Node {
bool hidden;
bool isSubtreeBlocked() const override final;
public:
template<class... Args>
HideableQSGNode(bool visible, Args&&... args);
void setVisible(bool visible);
};
template <typename Node>
class QSGProxyNode : public QSGNode {
public:
std::unique_ptr<Node> node;
QSGProxyNode(Node *node);
};
// Implementation detail of templates - move to serparate header file
template <typename Node>
QSGProxyNode<Node>::QSGProxyNode(Node *node) : node(node)
{
appendChildNode(node);
}
template <typename Node>
bool HideableQSGNode<Node>::isSubtreeBlocked() const
{
return hidden;
}
template <typename Node>
template<class... Args>
HideableQSGNode<Node>::HideableQSGNode(bool visible, Args&&... args) :
Node(std::forward<Args>(args)...),
hidden(!visible)
{
}
template <typename Node>
void HideableQSGNode<Node>::setVisible(bool visible)
{
hidden = !visible;
Node::markDirty(QSGNode::DirtySubtreeBlocked);
}
#endif #endif

View File

@ -21,29 +21,21 @@
#include "core/selection.h" #include "core/selection.h"
#include "core/trip.h" #include "core/trip.h"
#include <array> // for std::array
#include <cmath> #include <cmath>
#include <QQuickItem>
#include <QQuickWindow>
#include <QSGImageNode>
#include <QSGRectangleNode>
#include <QSGTexture>
// Constants that control the graph layouts // Constants that control the graph layouts
static const double sceneBorder = 5.0; // Border between scene edges and statitistics view static const double sceneBorder = 5.0; // Border between scene edges and statitistics view
static const double titleBorder = 2.0; // Border between title and chart static const double titleBorder = 2.0; // Border between title and chart
static const double selectionLassoWidth = 2.0; // Border between title and chart static const double selectionLassoWidth = 2.0; // Border between title and chart
StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent), StatsView::StatsView(QQuickItem *parent) : ChartView(parent, ChartZValue::Count),
backgroundDirty(true),
currentTheme(&getStatsTheme(false)), currentTheme(&getStatsTheme(false)),
highlightedSeries(nullptr), highlightedSeries(nullptr),
xAxis(nullptr), xAxis(nullptr),
yAxis(nullptr), yAxis(nullptr),
draggedItem(nullptr), restrictDives(false)
restrictDives(false),
rootNode(nullptr)
{ {
setBackgroundColor(currentTheme->backgroundColor);
setFlag(ItemHasContents, true); setFlag(ItemHasContents, true);
connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible); connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible);
@ -67,22 +59,12 @@ StatsView::~StatsView()
void StatsView::mousePressEvent(QMouseEvent *event) void StatsView::mousePressEvent(QMouseEvent *event)
{ {
QPointF pos = event->localPos(); // Handle dragging of items
ChartView::mousePressEvent(event);
// Currently, we only support dragging of the legend. If other objects if (event->isAccepted())
// should be made draggable, this needs to be generalized.
if (legend) {
QRectF rect = legend->getRect();
if (legend->getRect().contains(pos)) {
dragStartMouse = pos;
dragStartItem = rect.topLeft();
draggedItem = &*legend;
grabMouse();
setKeepMouseGrab(true); // don't allow Qt to steal the grab
return; return;
}
}
QPointF pos = event->localPos();
SelectionModifier modifier; SelectionModifier modifier;
modifier.shift = (event->modifiers() & Qt::ShiftModifier) != 0; modifier.shift = (event->modifiers() & Qt::ShiftModifier) != 0;
modifier.ctrl = (event->modifiers() & Qt::ControlModifier) != 0; modifier.ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
@ -97,7 +79,7 @@ void StatsView::mousePressEvent(QMouseEvent *event)
{ return s->supportsLassoSelection(); })) { { return s->supportsLassoSelection(); })) {
if (selectionRect) if (selectionRect)
deleteChartItem(selectionRect); // Ooops. Already a selection in place. deleteChartItem(selectionRect); // Ooops. Already a selection in place.
dragStartMouse = pos; selectionStartMouse = pos;
selectionRect = createChartItem<ChartRectLineItem>(ChartZValue::Selection, currentTheme->selectionLassoColor, selectionLassoWidth); selectionRect = createChartItem<ChartRectLineItem>(ChartZValue::Selection, currentTheme->selectionLassoColor, selectionLassoWidth);
selectionModifier = modifier; selectionModifier = modifier;
oldSelection = modifier.ctrl ? getDiveSelection() : std::vector<dive *>(); oldSelection = modifier.ctrl ? getDiveSelection() : std::vector<dive *>();
@ -107,12 +89,9 @@ void StatsView::mousePressEvent(QMouseEvent *event)
} }
} }
void StatsView::mouseReleaseEvent(QMouseEvent *) void StatsView::mouseReleaseEvent(QMouseEvent *event)
{ {
if (draggedItem) { ChartView::mouseReleaseEvent(event);
draggedItem = nullptr;
ungrabMouse();
}
if (selectionRect) { if (selectionRect) {
deleteChartItem(selectionRect); deleteChartItem(selectionRect);
@ -121,187 +100,10 @@ void StatsView::mouseReleaseEvent(QMouseEvent *)
} }
} }
// Define a hideable dummy QSG node that is used as a parent node to make
// all objects of a z-level visible / invisible.
using ZNode = HideableQSGNode<QSGNode>;
class RootNode : public QSGNode
{
public:
RootNode(StatsView &view);
~RootNode();
StatsView &view;
std::unique_ptr<QSGRectangleNode> backgroundNode; // solid background
// We entertain one node per Z-level.
std::array<std::unique_ptr<ZNode>, (size_t)ChartZValue::Count> zNodes;
};
RootNode::RootNode(StatsView &view) : view(view)
{
// Add a background rectangle with a solid color. This could
// also be done on the widget level, but would have to be done
// separately for desktop and mobile, so do it here.
backgroundNode.reset(view.w()->createRectangleNode());
backgroundNode->setColor(view.getCurrentTheme().backgroundColor);
appendChildNode(backgroundNode.get());
for (auto &zNode: zNodes) {
zNode.reset(new ZNode(true));
appendChildNode(zNode.get());
}
}
RootNode::~RootNode()
{
view.emergencyShutdown();
}
void StatsView::freeDeletedChartItems()
{
ChartItem *nextitem;
for (ChartItem *item = deletedItems.first; item; item = nextitem) {
nextitem = item->next;
delete item;
}
deletedItems.clear();
}
QSGNode *StatsView::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
// The QtQuick drawing interface is utterly bizzare with a distinct 1980ies-style memory management.
// This is just a copy of what is found in Qt's documentation.
RootNode *n = static_cast<RootNode *>(oldNode);
if (!n)
n = rootNode = new RootNode(*this);
// Delete all chart items that are marked for deletion.
freeDeletedChartItems();
if (backgroundDirty) {
rootNode->backgroundNode->setRect(plotRect);
backgroundDirty = false;
}
for (ChartItem *item = dirtyItems.first; item; item = item->next) {
item->render(*currentTheme);
item->dirty = false;
}
dirtyItems.splice(cleanItems);
return n;
}
// When reparenting the QQuickWidget, QtQuick decides to delete our rootNode
// and with it all the QSG nodes, even though we have *not* given the
// permission to do so! If the widget is reused, we try to delete the
// stale items, whose nodes have already been deleted by QtQuick, leading
// to a double-free(). Instead of searching for the cause of this behavior,
// let's just hook into the rootNodes destructor and delete the objects
// in a controlled manner, so that QtQuick has no more access to them.
void StatsView::emergencyShutdown()
{
// Mark clean and dirty chart items for deletion...
cleanItems.splice(deletedItems);
dirtyItems.splice(deletedItems);
// ...and delete them.
freeDeletedChartItems();
// Now delete all the pointers we might have to chart features,
// axes, etc. Note that all pointers to chart items are non
// owning, so this only resets stale references, but does not
// lead to any additional deletion of chart items.
reset();
// The rootNode is being deleted -> remove the reference to that
rootNode = nullptr;
}
void StatsView::addQSGNode(QSGNode *node, ChartZValue z)
{
int idx = std::clamp((int)z, 0, (int)ChartZValue::Count - 1);
rootNode->zNodes[idx]->appendChildNode(node);
}
void StatsView::registerChartItem(ChartItem &item)
{
cleanItems.append(item);
}
void StatsView::registerDirtyChartItem(ChartItem &item)
{
if (item.dirty)
return;
cleanItems.remove(item);
dirtyItems.append(item);
item.dirty = true;
}
void StatsView::deleteChartItemInternal(ChartItem &item)
{
if (item.dirty)
dirtyItems.remove(item);
else
cleanItems.remove(item);
deletedItems.append(item);
}
StatsView::ChartItemList::ChartItemList() : first(nullptr), last(nullptr)
{
}
void StatsView::ChartItemList::clear()
{
first = last = nullptr;
}
void StatsView::ChartItemList::remove(ChartItem &item)
{
if (item.next)
item.next->prev = item.prev;
else
last = item.prev;
if (item.prev)
item.prev->next = item.next;
else
first = item.next;
item.prev = item.next = nullptr;
}
void StatsView::ChartItemList::append(ChartItem &item)
{
if (!first) {
first = &item;
} else {
item.prev = last;
last->next = &item;
}
last = &item;
}
void StatsView::ChartItemList::splice(ChartItemList &l2)
{
if (!first) // if list is empty -> nothing to do.
return;
if (!l2.first) {
l2 = *this;
} else {
l2.last->next = first;
first->prev = l2.last;
l2.last = last;
}
clear();
}
QQuickWindow *StatsView::w() const
{
return window();
}
void StatsView::setTheme(bool dark) void StatsView::setTheme(bool dark)
{ {
currentTheme = &getStatsTheme(dark); currentTheme = &getStatsTheme(dark);
rootNode->backgroundNode->setColor(currentTheme->backgroundColor); setBackgroundColor(currentTheme->backgroundColor);
} }
const StatsTheme &StatsView::getCurrentTheme() const const StatsTheme &StatsView::getCurrentTheme() const
@ -309,34 +111,6 @@ const StatsTheme &StatsView::getCurrentTheme() const
return *currentTheme; return *currentTheme;
} }
QSizeF StatsView::size() const
{
return boundingRect().size();
}
QRectF StatsView::plotArea() const
{
return plotRect;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void StatsView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
#else
void StatsView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
#endif
{
plotRect = QRectF(QPointF(0.0, 0.0), newGeometry.size());
backgroundDirty = true;
plotAreaChanged(plotRect.size());
// Do we need to call the base-class' version of geometryChanged? Probably for QML?
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QQuickItem::geometryChange(newGeometry, oldGeometry);
#else
QQuickItem::geometryChanged(newGeometry, oldGeometry);
#endif
}
void StatsView::plotAreaChanged(const QSizeF &s) void StatsView::plotAreaChanged(const QSizeF &s)
{ {
double left = sceneBorder; double left = sceneBorder;
@ -404,17 +178,11 @@ void StatsView::divesSelected(const QVector<dive *> &dives)
void StatsView::mouseMoveEvent(QMouseEvent *event) void StatsView::mouseMoveEvent(QMouseEvent *event)
{ {
if (draggedItem) { ChartView::mouseMoveEvent(event);
QSizeF sceneSize = size();
if (sceneSize.width() <= 1.0 || sceneSize.height() <= 1.0)
return;
draggedItem->setPos(event->pos() - dragStartMouse + dragStartItem);
update();
}
if (selectionRect) { if (selectionRect) {
QPointF p1 = event->pos(); QPointF p1 = event->pos();
QPointF p2 = dragStartMouse; QPointF p2 = selectionStartMouse;
selectionRect->setLine(p1, p2); selectionRect->setLine(p1, p2);
QRectF rect(std::min(p1.x(), p2.x()), std::min(p1.y(), p2.y()), QRectF rect(std::min(p1.x(), p2.x()), std::min(p1.y(), p2.y()),
fabs(p2.x() - p1.x()), fabs(p2.y() - p1.y())); fabs(p2.x() - p1.x()), fabs(p2.y() - p1.y()));
@ -483,7 +251,7 @@ void StatsView::updateTitlePos()
template <typename T, class... Args> template <typename T, class... Args>
T *StatsView::createAxis(const QString &title, Args&&... args) T *StatsView::createAxis(const QString &title, Args&&... args)
{ {
return &*createChartItem<T>(title, std::forward<Args>(args)...); return &*createChartItem<T>(*currentTheme, title, std::forward<Args>(args)...);
} }
void StatsView::setAxes(StatsAxis *x, StatsAxis *y) void StatsView::setAxes(StatsAxis *x, StatsAxis *y)
@ -495,10 +263,15 @@ void StatsView::setAxes(StatsAxis *x, StatsAxis *y)
} }
void StatsView::reset() void StatsView::reset()
{
resetPointers();
clearItems();
}
void StatsView::resetPointers()
{ {
highlightedSeries = nullptr; highlightedSeries = nullptr;
xAxis = yAxis = nullptr; xAxis = yAxis = nullptr;
draggedItem = nullptr;
title.reset(); title.reset();
legend.reset(); legend.reset();
regressionItem.reset(); regressionItem.reset();
@ -506,10 +279,6 @@ void StatsView::reset()
medianMarker.reset(); medianMarker.reset();
selectionRect.reset(); selectionRect.reset();
// Mark clean and dirty chart items for deletion
cleanItems.splice(deletedItems);
dirtyItems.splice(deletedItems);
series.clear(); series.clear();
quartileMarkers.clear(); quartileMarkers.clear();
grid.reset(); grid.reset();
@ -539,7 +308,7 @@ void StatsView::plot(const StatsState &stateIn)
state = stateIn; state = stateIn;
plotChart(); plotChart();
updateFeatures(); // Show / hide chart features, such as legend, etc. updateFeatures(); // Show / hide chart features, such as legend, etc.
plotAreaChanged(plotRect.size()); plotAreaChanged(plotArea().size());
update(); update();
} }
@ -610,8 +379,7 @@ void StatsView::updateFeatures()
legend->setVisible(state.legend); legend->setVisible(state.legend);
// For labels, we are brutal: simply show/hide the whole z-level with the labels // For labels, we are brutal: simply show/hide the whole z-level with the labels
if (rootNode) setLayerVisibility(ChartZValue::SeriesLabels, state.labels);
rootNode->zNodes[(int)ChartZValue::SeriesLabels]->setVisible(state.labels);
if (meanMarker) if (meanMarker)
meanMarker->setVisible(state.mean); meanMarker->setVisible(state.mean);
@ -764,7 +532,7 @@ void StatsView::plotBarChart(const std::vector<dive *> &dives,
setAxes(catAxis, valAxis); setAxes(catAxis, valAxis);
// Paint legend first, because the bin-names will be moved away from. // Paint legend first, because the bin-names will be moved away from.
legend = createChartItem<Legend>(data.vbinNames); legend = createChartItem<Legend>(*currentTheme, data.vbinNames);
std::vector<BarSeries::MultiItem> items; std::vector<BarSeries::MultiItem> items;
items.reserve(data.hbins.size()); items.reserve(data.hbins.size());
@ -989,7 +757,7 @@ void StatsView::plotPieChart(const std::vector<dive *> &dives, ChartSortMode sor
PieSeries *series = createSeries<PieSeries>(categoryVariable->name(), std::move(data), sortMode); PieSeries *series = createSeries<PieSeries>(categoryVariable->name(), std::move(data), sortMode);
legend = createChartItem<Legend>(series->binNames()); legend = createChartItem<Legend>(*currentTheme, series->binNames());
} }
void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives, void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives,
@ -1059,11 +827,11 @@ void StatsView::plotDiscreteScatter(const std::vector<dive *> &dives,
StatsQuartiles quartiles = StatsVariable::quartiles(array); StatsQuartiles quartiles = StatsVariable::quartiles(array);
if (quartiles.isValid()) { if (quartiles.isValid()) {
quartileMarkers.push_back(createChartItem<QuartileMarker>( quartileMarkers.push_back(createChartItem<QuartileMarker>(
x, quartiles.q1, catAxis, valAxis)); *currentTheme, x, quartiles.q1, catAxis, valAxis));
quartileMarkers.push_back(createChartItem<QuartileMarker>( quartileMarkers.push_back(createChartItem<QuartileMarker>(
x, quartiles.q2, catAxis, valAxis)); *currentTheme, x, quartiles.q2, catAxis, valAxis));
quartileMarkers.push_back(createChartItem<QuartileMarker>( quartileMarkers.push_back(createChartItem<QuartileMarker>(
x, quartiles.q3, catAxis, valAxis)); *currentTheme, x, quartiles.q3, catAxis, valAxis));
} }
x += 1.0; x += 1.0;
} }
@ -1214,7 +982,7 @@ void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives,
*categoryBinner, categoryBins, !isHorizontal); *categoryBinner, categoryBins, !isHorizontal);
BarPlotData data(categoryBins, *valueBinner); BarPlotData data(categoryBins, *valueBinner);
legend = createChartItem<Legend>(data.vbinNames); legend = createChartItem<Legend>(*currentTheme, data.vbinNames);
CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal); CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal);
@ -1365,5 +1133,5 @@ void StatsView::plotScatter(const std::vector<dive *> &dives, const StatsVariabl
// y = ax + b // y = ax + b
struct regression_data reg = linear_regression(points); struct regression_data reg = linear_regression(points);
if (!std::isnan(reg.a)) if (!std::isnan(reg.a))
regressionItem = createChartItem<RegressionItem>(reg, xAxis, yAxis); regressionItem = createChartItem<RegressionItem>(*currentTheme, reg, xAxis, yAxis);
} }

View File

@ -3,12 +3,12 @@
#define STATS_VIEW_H #define STATS_VIEW_H
#include "statsstate.h" #include "statsstate.h"
#include "statshelper.h"
#include "statsselection.h" #include "statsselection.h"
#include "qt-quick/chartview.h"
#include <memory> #include <memory>
#include <QImage> #include <QImage>
#include <QPainter> #include <QPainter>
#include <QQuickItem>
struct dive; struct dive;
struct StatsBinner; struct StatsBinner;
@ -18,7 +18,6 @@ struct StatsVariable;
class StatsSeries; class StatsSeries;
class CategoryAxis; class CategoryAxis;
class ChartItem;
class ChartRectLineItem; class ChartRectLineItem;
class ChartTextItem; class ChartTextItem;
class CountAxis; class CountAxis;
@ -30,15 +29,12 @@ class StatsAxis;
class StatsGrid; class StatsGrid;
class StatsTheme; class StatsTheme;
class Legend; class Legend;
class QSGTexture;
class RootNode; // Internal implementation detail
enum class ChartSubType : int; enum class ChartSubType : int;
enum class ChartZValue : int;
enum class StatsOperation : int; enum class StatsOperation : int;
enum class ChartSortMode : int; enum class ChartSortMode : int;
class StatsView : public QQuickItem { class StatsView : public ChartView {
Q_OBJECT Q_OBJECT
public: public:
StatsView(); StatsView();
@ -50,42 +46,15 @@ public:
void restrictToSelection(); void restrictToSelection();
void unrestrict(); void unrestrict();
int restrictionCount() const; // <0: no restriction int restrictionCount() const; // <0: no restriction
QQuickWindow *w() const; // Make window available to items
QSizeF size() const;
QRectF plotArea() const;
void setTheme(bool dark); // Chart must be replot for theme to become effective. void setTheme(bool dark); // Chart must be replot for theme to become effective.
const StatsTheme &getCurrentTheme() const; const StatsTheme &getCurrentTheme() const;
void addQSGNode(QSGNode *node, ChartZValue z); // Must only be called in render thread!
void registerChartItem(ChartItem &item);
void registerDirtyChartItem(ChartItem &item);
void emergencyShutdown(); // Called when QQuick decides to delete our root node.
// Create a chart item and add it to the scene.
// The item must not be deleted by the caller, but can be
// scheduled for deletion using deleteChartItem() below.
// Most items can be made invisible, which is preferred over deletion.
// All items on the scene will be deleted once the chart is reset.
template <typename T, class... Args>
ChartItemPtr<T> createChartItem(Args&&... args);
template <typename T>
void deleteChartItem(ChartItemPtr<T> &item);
private slots:
void replotIfVisible(); void replotIfVisible();
void divesSelected(const QVector<dive *> &dives); void divesSelected(const QVector<dive *> &dives);
private: private:
// QtQuick related things void plotAreaChanged(const QSizeF &size) override;
bool backgroundDirty;
QRectF plotRect;
QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
#else
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override;
#endif
void plotAreaChanged(const QSizeF &size);
void reset(); // clears all series and axes void reset(); // clears all series and axes
void resetPointers() override;
void setAxes(StatsAxis *x, StatsAxis *y); void setAxes(StatsAxis *x, StatsAxis *y);
void plotBarChart(const std::vector<dive *> &dives, void plotBarChart(const std::vector<dive *> &dives,
ChartSubType subType, ChartSortMode sortMode, ChartSubType subType, ChartSortMode sortMode,
@ -151,10 +120,9 @@ private:
StatsAxis *xAxis, *yAxis; StatsAxis *xAxis, *yAxis;
ChartItemPtr<ChartTextItem> title; ChartItemPtr<ChartTextItem> title;
ChartItemPtr<Legend> legend; ChartItemPtr<Legend> legend;
Legend *draggedItem;
ChartItemPtr<RegressionItem> regressionItem; ChartItemPtr<RegressionItem> regressionItem;
ChartItemPtr<ChartRectLineItem> selectionRect; ChartItemPtr<ChartRectLineItem> selectionRect;
QPointF dragStartMouse, dragStartItem; QPointF selectionStartMouse;
SelectionModifier selectionModifier; SelectionModifier selectionModifier;
std::vector<dive *> oldSelection; std::vector<dive *> oldSelection;
bool restrictDives; bool restrictDives;
@ -165,40 +133,6 @@ private:
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override;
RootNode *rootNode;
// There are three double linked lists of chart items:
// clean items, dirty items and items to be deleted.
// Note that only the render thread must delete chart items,
// and therefore these lists are the only owning pointers
// to chart items. All other pointers are non-owning and
// can therefore become stale.
struct ChartItemList {
ChartItemList();
ChartItem *first, *last;
void append(ChartItem &item);
void remove(ChartItem &item);
void clear();
void splice(ChartItemList &list);
};
ChartItemList cleanItems, dirtyItems, deletedItems;
void deleteChartItemInternal(ChartItem &item);
void freeDeletedChartItems();
}; };
// This implementation detail must be known to users of the class.
// Perhaps move it into a statsview_impl.h file.
template <typename T, class... Args>
ChartItemPtr<T> StatsView::createChartItem(Args&&... args)
{
return ChartItemPtr(new T(*this, std::forward<Args>(args)...));
}
template <typename T>
void StatsView::deleteChartItem(ChartItemPtr<T> &item)
{
deleteChartItemInternal(*item);
item.reset();
}
#endif #endif

View File

@ -4,8 +4,13 @@
// with smaller z-values. For the same z-value objects are // with smaller z-values. For the same z-value objects are
// drawn in order of addition to the scene. // drawn in order of addition to the scene.
#ifndef ZVALUES_H #ifndef ZVALUES_H
#define ZVALUES_H
enum class ChartZValue { // Encapsulating an enum in a struct is stupid, but allows us
// to not poison the namespace and yet autoconvert to int
// (in constrast to enum class). enum is so broken!
struct ChartZValue {
enum ZValues {
Grid = 0, Grid = 0,
Series, Series,
Axes, Axes,
@ -15,6 +20,7 @@ enum class ChartZValue {
InformationBox, InformationBox,
Legend, Legend,
Count Count
};
}; };
#endif #endif

View File

@ -7,6 +7,7 @@
#include "qt-models/maplocationmodel.h" #include "qt-models/maplocationmodel.h"
#endif #endif
#include "profile-widget/profileview.h"
#include "stats/statsview.h" #include "stats/statsview.h"
#include "core/devicedetails.h" #include "core/devicedetails.h"
#include "core/globals.h" #include "core/globals.h"
@ -26,7 +27,6 @@
#include "qt-models/divesummarymodel.h" #include "qt-models/divesummarymodel.h"
#include "qt-models/messagehandlermodel.h" #include "qt-models/messagehandlermodel.h"
#include "qt-models/mobilelistmodel.h" #include "qt-models/mobilelistmodel.h"
#include "profile-widget/qmlprofile.h"
#include "core/downloadfromdcthread.h" #include "core/downloadfromdcthread.h"
#include "core/subsurfacestartup.h" // for testqml #include "core/subsurfacestartup.h" // for testqml
#include "core/metrics.h" #include "core/metrics.h"
@ -219,7 +219,6 @@ static void register_qml_types(QQmlEngine *engine)
#ifdef SUBSURFACE_MOBILE #ifdef SUBSURFACE_MOBILE
register_qml_type<QMLManager>("QMLManager"); register_qml_type<QMLManager>("QMLManager");
register_qml_type<StatsManager>("StatsManager"); register_qml_type<StatsManager>("StatsManager");
register_qml_type<QMLProfile>("QMLProfile");
register_qml_type<DiveImportedModel>("DCImportModel"); register_qml_type<DiveImportedModel>("DCImportModel");
register_qml_type<DiveSummaryModel>("DiveSummaryModel"); register_qml_type<DiveSummaryModel>("DiveSummaryModel");
register_qml_type<ChartListModel>("ChartListModel"); register_qml_type<ChartListModel>("ChartListModel");
@ -229,5 +228,6 @@ static void register_qml_types(QQmlEngine *engine)
register_qml_type<MapWidgetHelper>("MapWidgetHelper"); register_qml_type<MapWidgetHelper>("MapWidgetHelper");
register_qml_type<MapLocationModel>("MapLocationModel"); register_qml_type<MapLocationModel>("MapLocationModel");
#endif #endif
register_qml_type<ProfileView>("ProfileView");
register_qml_type<StatsView>("StatsView"); register_qml_type<StatsView>("StatsView");
} }

View File

@ -31,6 +31,8 @@ void TestProfile::init()
QCoreApplication::setOrganizationName("Subsurface"); QCoreApplication::setOrganizationName("Subsurface");
QCoreApplication::setOrganizationDomain("subsurface.hohndel.org"); QCoreApplication::setOrganizationDomain("subsurface.hohndel.org");
QCoreApplication::setApplicationName("Subsurface"); QCoreApplication::setApplicationName("Subsurface");
setlocale(LC_ALL, "C");
} }
void TestProfile::testProfileExport() void TestProfile::testProfileExport()
{ {