diff options
author | Tomaz Canabrava <tomaz.canabrava@intel.com> | 2015-09-03 14:20:19 -0300 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2015-10-30 10:36:49 -0700 |
commit | e49d6213ad129284a45d53c3fcdc03249e84efe2 (patch) | |
tree | 2946a666ab38af3375e7bb2b8c5dd887d4a7f9a1 /desktop-widgets/profile | |
parent | 588abd019fb2ed3f607682f2b6c7fe86a7a5bb90 (diff) | |
download | subsurface-e49d6213ad129284a45d53c3fcdc03249e84efe2.tar.gz |
Move qt-ui to desktop-widgets
Since we have now destkop and mobile versions, 'qt-ui' was a very
poor name choice for a folder that contains only destkop-enabled
widgets.
Also, move the graphicsview-common.h/cpp to subsurface-core because
it doesn't depend on qgraphicsview, it merely implements all the
colors that we use throughout Subsurface, and we will use colors on both
desktop and mobile versions
Same thing applies for metrics.h/cpp
Signed-off-by: Tomaz Canabrava <tomaz.canabrava@intel.com>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
Diffstat (limited to 'desktop-widgets/profile')
24 files changed, 5260 insertions, 0 deletions
diff --git a/desktop-widgets/profile/animationfunctions.cpp b/desktop-widgets/profile/animationfunctions.cpp new file mode 100644 index 000000000..a19d50c9d --- /dev/null +++ b/desktop-widgets/profile/animationfunctions.cpp @@ -0,0 +1,75 @@ +#include "animationfunctions.h" +#include "pref.h" +#include <QPropertyAnimation> + +namespace Animations { + + void hide(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + animation->setStartValue(1); + animation->setEndValue(0); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 0); + } + } + + void show(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + animation->setStartValue(0); + animation->setEndValue(1); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 1); + } + } + + void animDelete(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + obj->connect(animation, SIGNAL(finished()), SLOT(deleteLater())); + animation->setStartValue(1); + animation->setEndValue(0); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 0); + } + } + + void moveTo(QObject *obj, qreal x, qreal y) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "pos"); + animation->setDuration(prefs.animation_speed); + animation->setStartValue(obj->property("pos").toPointF()); + animation->setEndValue(QPointF(x, y)); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("pos", QPointF(x, y)); + } + } + + void scaleTo(QObject *obj, qreal scale) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "scale"); + animation->setDuration(prefs.animation_speed); + animation->setStartValue(obj->property("scale").toReal()); + animation->setEndValue(QVariant::fromValue(scale)); + animation->setEasingCurve(QEasingCurve::InCubic); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("scale", QVariant::fromValue(scale)); + } + } + + void moveTo(QObject *obj, const QPointF &pos) + { + moveTo(obj, pos.x(), pos.y()); + } +} diff --git a/desktop-widgets/profile/animationfunctions.h b/desktop-widgets/profile/animationfunctions.h new file mode 100644 index 000000000..3cfcff563 --- /dev/null +++ b/desktop-widgets/profile/animationfunctions.h @@ -0,0 +1,18 @@ +#ifndef ANIMATIONFUNCTIONS_H +#define ANIMATIONFUNCTIONS_H + +#include <QtGlobal> +#include <QPointF> + +class QObject; + +namespace Animations { + void hide(QObject *obj); + void show(QObject *obj); + void moveTo(QObject *obj, qreal x, qreal y); + void moveTo(QObject *obj, const QPointF &pos); + void animDelete(QObject *obj); + void scaleTo(QObject *obj, qreal scale); +} + +#endif // ANIMATIONFUNCTIONS_H diff --git a/desktop-widgets/profile/divecartesianaxis.cpp b/desktop-widgets/profile/divecartesianaxis.cpp new file mode 100644 index 000000000..bf5a5380c --- /dev/null +++ b/desktop-widgets/profile/divecartesianaxis.cpp @@ -0,0 +1,459 @@ +#include "divecartesianaxis.h" +#include "divetextitem.h" +#include "helpers.h" +#include "preferences.h" +#include "diveplotdatamodel.h" +#include "animationfunctions.h" +#include "mainwindow.h" +#include "divelineitem.h" +#include "profilewidget2.h" + +QPen DiveCartesianAxis::gridPen() +{ + QPen pen; + pen.setColor(getColor(TIME_GRID)); + /* cosmetic width() == 0 for lines in printMode + * having setCosmetic(true) and width() > 0 does not work when + * printing on OSX and Linux */ + pen.setWidth(DiveCartesianAxis::printMode ? 0 : 2); + pen.setCosmetic(true); + return pen; +} + +double DiveCartesianAxis::tickInterval() const +{ + return interval; +} + +double DiveCartesianAxis::tickSize() const +{ + return tick_size; +} + +void DiveCartesianAxis::setFontLabelScale(qreal scale) +{ + labelScale = scale; + changed = true; +} + +void DiveCartesianAxis::setPrintMode(bool mode) +{ + printMode = mode; + // update the QPen of all lines depending on printMode + QPen newPen = gridPen(); + QColor oldColor = pen().brush().color(); + newPen.setBrush(oldColor); + setPen(newPen); + Q_FOREACH (DiveLineItem *item, lines) + item->setPen(pen()); +} + +void DiveCartesianAxis::setMaximum(double maximum) +{ + if (IS_FP_SAME(max, maximum)) + return; + max = maximum; + changed = true; + emit maxChanged(); +} + +void DiveCartesianAxis::setMinimum(double minimum) +{ + if (IS_FP_SAME(min, minimum)) + return; + min = minimum; + changed = true; +} + +void DiveCartesianAxis::setTextColor(const QColor &color) +{ + textColor = color; +} + +DiveCartesianAxis::DiveCartesianAxis() : QObject(), + QGraphicsLineItem(), + printMode(false), + unitSystem(0), + orientation(LeftToRight), + min(0), + max(0), + interval(1), + tick_size(0), + textVisibility(true), + lineVisibility(true), + labelScale(1.0), + line_size(1), + changed(true) +{ + setPen(gridPen()); +} + +DiveCartesianAxis::~DiveCartesianAxis() +{ +} + +void DiveCartesianAxis::setLineSize(qreal lineSize) +{ + line_size = lineSize; + changed = true; +} + +void DiveCartesianAxis::setOrientation(Orientation o) +{ + orientation = o; + changed = true; +} + +QColor DiveCartesianAxis::colorForValue(double value) +{ + return QColor(Qt::black); +} + +void DiveCartesianAxis::setTextVisible(bool arg1) +{ + if (textVisibility == arg1) { + return; + } + textVisibility = arg1; + Q_FOREACH (DiveTextItem *item, labels) { + item->setVisible(textVisibility); + } +} + +void DiveCartesianAxis::setLinesVisible(bool arg1) +{ + if (lineVisibility == arg1) { + return; + } + lineVisibility = arg1; + Q_FOREACH (DiveLineItem *item, lines) { + item->setVisible(lineVisibility); + } +} + +template <typename T> +void emptyList(QList<T *> &list, double steps) +{ + if (!list.isEmpty() && list.size() > steps) { + while (list.size() > steps) { + T *removedItem = list.takeLast(); + Animations::animDelete(removedItem); + } + } +} + +void DiveCartesianAxis::updateTicks(color_indice_t color) +{ + if (!scene() || (!changed && !MainWindow::instance()->graphics()->getPrintMode())) + return; + QLineF m = line(); + // unused so far: + // QGraphicsView *view = scene()->views().first(); + double steps = (max - min) / interval; + double currValueText = min; + double currValueLine = min; + + if (steps < 1) + return; + + emptyList(labels, steps); + emptyList(lines, steps); + + // Move the remaining Ticks / Text to it's corerct position + // Regartind the possibly new values for the Axis + qreal begin, stepSize; + if (orientation == TopToBottom) { + begin = m.y1(); + stepSize = (m.y2() - m.y1()); + } else if (orientation == BottomToTop) { + begin = m.y2(); + stepSize = (m.y2() - m.y1()); + } else if (orientation == LeftToRight) { + begin = m.x1(); + stepSize = (m.x2() - m.x1()); + } else /* if (orientation == RightToLeft) */ { + begin = m.x2(); + stepSize = (m.x2() - m.x1()); + } + stepSize = stepSize / steps; + + for (int i = 0, count = labels.size(); i < count; i++, currValueText += interval) { + qreal childPos = (orientation == TopToBottom || orientation == LeftToRight) ? + begin + i * stepSize : + begin - i * stepSize; + + labels[i]->setText(textForValue(currValueText)); + if (orientation == LeftToRight || orientation == RightToLeft) { + Animations::moveTo(labels[i],childPos, m.y1() + tick_size); + } else { + Animations::moveTo(labels[i],m.x1() - tick_size, childPos); + } + } + + for (int i = 0, count = lines.size(); i < count; i++, currValueLine += interval) { + qreal childPos = (orientation == TopToBottom || orientation == LeftToRight) ? + begin + i * stepSize : + begin - i * stepSize; + + if (orientation == LeftToRight || orientation == RightToLeft) { + Animations::moveTo(lines[i],childPos, m.y1()); + } else { + Animations::moveTo(lines[i],m.x1(), childPos); + } + } + + // Add's the rest of the needed Ticks / Text. + for (int i = labels.size(); i < steps; i++, currValueText += interval) { + qreal childPos; + if (orientation == TopToBottom || orientation == LeftToRight) { + childPos = begin + i * stepSize; + } else { + childPos = begin - i * stepSize; + } + DiveTextItem *label = new DiveTextItem(this); + label->setText(textForValue(currValueText)); + label->setBrush(colorForValue(currValueText)); + label->setScale(fontLabelScale()); + label->setZValue(1); + labels.push_back(label); + if (orientation == RightToLeft || orientation == LeftToRight) { + label->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); + label->setPos(scene()->sceneRect().width() + 10, m.y1() + tick_size); // position it outside of the scene); + Animations::moveTo(label,childPos, m.y1() + tick_size); + } else { + label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); + label->setPos(m.x1() - tick_size, scene()->sceneRect().height() + 10); + Animations::moveTo(label,m.x1() - tick_size, childPos); + } + } + + // Add's the rest of the needed Ticks / Text. + for (int i = lines.size(); i < steps; i++, currValueText += interval) { + qreal childPos; + if (orientation == TopToBottom || orientation == LeftToRight) { + childPos = begin + i * stepSize; + } else { + childPos = begin - i * stepSize; + } + DiveLineItem *line = new DiveLineItem(this); + QPen pen = gridPen(); + pen.setBrush(getColor(color)); + line->setPen(pen); + line->setZValue(0); + lines.push_back(line); + if (orientation == RightToLeft || orientation == LeftToRight) { + line->setLine(0, -line_size, 0, 0); + line->setPos(scene()->sceneRect().width() + 10, m.y1()); // position it outside of the scene); + Animations::moveTo(line,childPos, m.y1()); + } else { + QPointF p1 = mapFromScene(3, 0); + QPointF p2 = mapFromScene(line_size, 0); + line->setLine(p1.x(), 0, p2.x(), 0); + line->setPos(m.x1(), scene()->sceneRect().height() + 10); + Animations::moveTo(line,m.x1(), childPos); + } + } + + Q_FOREACH (DiveTextItem *item, labels) + item->setVisible(textVisibility); + Q_FOREACH (DiveLineItem *item, lines) + item->setVisible(lineVisibility); + changed = false; +} + +void DiveCartesianAxis::setLine(const QLineF &line) +{ + QGraphicsLineItem::setLine(line); + changed = true; +} + +void DiveCartesianAxis::animateChangeLine(const QLineF &newLine) +{ + setLine(newLine); + updateTicks(); + sizeChanged(); +} + +QString DiveCartesianAxis::textForValue(double value) +{ + return QString::number(value); +} + +void DiveCartesianAxis::setTickSize(qreal size) +{ + tick_size = size; +} + +void DiveCartesianAxis::setTickInterval(double i) +{ + interval = i; +} + +qreal DiveCartesianAxis::valueAt(const QPointF &p) const +{ + QLineF m = line(); + QPointF relativePosition = p; + relativePosition -= pos(); // normalize p based on the axis' offset on screen + + double retValue = (orientation == LeftToRight || orientation == RightToLeft) ? + max * (relativePosition.x() - m.x1()) / (m.x2() - m.x1()) : + max * (relativePosition.y() - m.y1()) / (m.y2() - m.y1()); + return retValue; +} + +qreal DiveCartesianAxis::posAtValue(qreal value) +{ + QLineF m = line(); + QPointF p = pos(); + + double size = max - min; + // unused for now: + // double distanceFromOrigin = value - min; + double percent = IS_FP_SAME(min, max) ? 0.0 : (value - min) / size; + + + double realSize = orientation == LeftToRight || orientation == RightToLeft ? + m.x2() - m.x1() : + m.y2() - m.y1(); + + // Inverted axis, just invert the percentage. + if (orientation == RightToLeft || orientation == BottomToTop) + percent = 1 - percent; + + double retValue = realSize * percent; + double adjusted = + orientation == LeftToRight ? retValue + m.x1() + p.x() : + orientation == RightToLeft ? retValue + m.x1() + p.x() : + orientation == TopToBottom ? retValue + m.y1() + p.y() : + /* entation == BottomToTop */ retValue + m.y1() + p.y(); + return adjusted; +} + +qreal DiveCartesianAxis::percentAt(const QPointF &p) +{ + qreal value = valueAt(p); + double size = max - min; + double percent = value / size; + return percent; +} + +double DiveCartesianAxis::maximum() const +{ + return max; +} + +double DiveCartesianAxis::minimum() const +{ + return min; +} + +double DiveCartesianAxis::fontLabelScale() const +{ + return labelScale; +} + +void DiveCartesianAxis::setColor(const QColor &color) +{ + QPen defaultPen = gridPen(); + defaultPen.setColor(color); + defaultPen.setJoinStyle(Qt::RoundJoin); + defaultPen.setCapStyle(Qt::RoundCap); + setPen(defaultPen); +} + +QString DepthAxis::textForValue(double value) +{ + if (value == 0) + return QString(); + return get_depth_string(value, false, false); +} + +QColor DepthAxis::colorForValue(double value) +{ + Q_UNUSED(value); + return QColor(Qt::red); +} + +DepthAxis::DepthAxis() +{ + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + changed = true; + settingsChanged(); +} + +void DepthAxis::settingsChanged() +{ + static int unitSystem = prefs.units.length; + if ( unitSystem == prefs.units.length ) + return; + changed = true; + updateTicks(); + unitSystem = prefs.units.length; +} + +QColor TimeAxis::colorForValue(double value) +{ + Q_UNUSED(value); + return QColor(Qt::blue); +} + +QString TimeAxis::textForValue(double value) +{ + int nr = value / 60; + if (maximum() < 600) + return QString("%1:%2").arg(nr).arg((int)value % 60, 2, 10, QChar('0')); + return QString::number(nr); +} + +void TimeAxis::updateTicks() +{ + DiveCartesianAxis::updateTicks(); + if (maximum() > 600) { + for (int i = 0; i < labels.count(); i++) { + labels[i]->setVisible(i % 2); + } + } +} + +QString TemperatureAxis::textForValue(double value) +{ + return QString::number(mkelvin_to_C((int)value)); +} + +PartialGasPressureAxis::PartialGasPressureAxis() : + DiveCartesianAxis(), + model(NULL) +{ + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void PartialGasPressureAxis::setModel(DivePlotDataModel *m) +{ + model = m; + connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(settingsChanged())); + settingsChanged(); +} + +void PartialGasPressureAxis::settingsChanged() +{ + bool showPhe = prefs.pp_graphs.phe; + bool showPn2 = prefs.pp_graphs.pn2; + bool showPo2 = prefs.pp_graphs.po2; + setVisible(showPhe || showPn2 || showPo2); + if (!model->rowCount()) + return; + + double max = showPhe ? model->pheMax() : -1; + if (showPn2 && model->pn2Max() > max) + max = model->pn2Max(); + if (showPo2 && model->po2Max() > max) + max = model->po2Max(); + + qreal pp = floor(max * 10.0) / 10.0 + 0.2; + if (IS_FP_SAME(maximum(), pp)) + return; + + setMaximum(pp); + setTickInterval(pp > 4 ? 0.5 : 0.25); + updateTicks(); +} diff --git a/desktop-widgets/profile/divecartesianaxis.h b/desktop-widgets/profile/divecartesianaxis.h new file mode 100644 index 000000000..cc7d0bcf7 --- /dev/null +++ b/desktop-widgets/profile/divecartesianaxis.h @@ -0,0 +1,122 @@ +#ifndef DIVECARTESIANAXIS_H +#define DIVECARTESIANAXIS_H + +#include <QObject> +#include <QGraphicsLineItem> +#include "subsurface-core/color.h" + +class QPropertyAnimation; +class DiveTextItem; +class DiveLineItem; +class DivePlotDataModel; + +class DiveCartesianAxis : public QObject, public QGraphicsLineItem { + Q_OBJECT + Q_PROPERTY(QLineF line WRITE setLine READ line) + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +private: + bool printMode; + QPen gridPen(); +public: + enum Orientation { + TopToBottom, + BottomToTop, + LeftToRight, + RightToLeft + }; + DiveCartesianAxis(); + virtual ~DiveCartesianAxis(); + void setPrintMode(bool mode); + void setMinimum(double minimum); + void setMaximum(double maximum); + void setTickInterval(double interval); + void setOrientation(Orientation orientation); + void setTickSize(qreal size); + void setFontLabelScale(qreal scale); + double minimum() const; + double maximum() const; + double tickInterval() const; + double tickSize() const; + double fontLabelScale() const; + qreal valueAt(const QPointF &p) const; + qreal percentAt(const QPointF &p); + qreal posAtValue(qreal value); + void setColor(const QColor &color); + void setTextColor(const QColor &color); + void animateChangeLine(const QLineF &newLine); + void setTextVisible(bool arg1); + void setLinesVisible(bool arg1); + void setLineSize(qreal lineSize); + void setLine(const QLineF& line); + int unitSystem; +public +slots: + virtual void updateTicks(color_indice_t color = TIME_GRID); + +signals: + void sizeChanged(); + void maxChanged(); + +protected: + virtual QString textForValue(double value); + virtual QColor colorForValue(double value); + Orientation orientation; + QList<DiveTextItem *> labels; + QList<DiveLineItem *> lines; + double min; + double max; + double interval; + double tick_size; + QColor textColor; + bool textVisibility; + bool lineVisibility; + double labelScale; + qreal line_size; + bool changed; +}; + +class DepthAxis : public DiveCartesianAxis { + Q_OBJECT +public: + DepthAxis(); + +protected: + QString textForValue(double value); + QColor colorForValue(double value); +private +slots: + void settingsChanged(); +}; + +class TimeAxis : public DiveCartesianAxis { + Q_OBJECT +public: + virtual void updateTicks(); + +protected: + QString textForValue(double value); + QColor colorForValue(double value); +}; + +class TemperatureAxis : public DiveCartesianAxis { + Q_OBJECT +protected: + QString textForValue(double value); +}; + +class PartialGasPressureAxis : public DiveCartesianAxis { + Q_OBJECT +public: + PartialGasPressureAxis(); + void setModel(DivePlotDataModel *model); +public +slots: + void settingsChanged(); + +private: + DivePlotDataModel *model; +}; + +#endif // DIVECARTESIANAXIS_H diff --git a/desktop-widgets/profile/diveeventitem.cpp b/desktop-widgets/profile/diveeventitem.cpp new file mode 100644 index 000000000..0bbc84267 --- /dev/null +++ b/desktop-widgets/profile/diveeventitem.cpp @@ -0,0 +1,172 @@ +#include "diveeventitem.h" +#include "diveplotdatamodel.h" +#include "divecartesianaxis.h" +#include "animationfunctions.h" +#include "libdivecomputer.h" +#include "profile.h" +#include "gettextfromc.h" +#include "metrics.h" + +extern struct ev_select *ev_namelist; +extern int evn_used; + +DiveEventItem::DiveEventItem(QObject *parent) : DivePixmapItem(parent), + vAxis(NULL), + hAxis(NULL), + dataModel(NULL), + internalEvent(NULL) +{ + setFlag(ItemIgnoresTransformations); +} + + +void DiveEventItem::setHorizontalAxis(DiveCartesianAxis *axis) +{ + hAxis = axis; + recalculatePos(true); +} + +void DiveEventItem::setModel(DivePlotDataModel *model) +{ + dataModel = model; + recalculatePos(true); +} + +void DiveEventItem::setVerticalAxis(DiveCartesianAxis *axis) +{ + vAxis = axis; + recalculatePos(true); + connect(vAxis, SIGNAL(sizeChanged()), this, SLOT(recalculatePos())); +} + +struct event *DiveEventItem::getEvent() +{ + return internalEvent; +} + +void DiveEventItem::setEvent(struct event *ev) +{ + if (!ev) + return; + internalEvent = ev; + setupPixmap(); + setupToolTipString(); + recalculatePos(true); +} + +void DiveEventItem::setupPixmap() +{ + const IconMetrics& metrics = defaultIconMetrics(); + int sz_bigger = metrics.sz_med + metrics.sz_small; // ex 40px + int sz_pix = sz_bigger/2; // ex 20px + +#define EVENT_PIXMAP(PIX) QPixmap(QString(PIX)).scaled(sz_pix, sz_pix, Qt::KeepAspectRatio, Qt::SmoothTransformation) +#define EVENT_PIXMAP_BIGGER(PIX) QPixmap(QString(PIX)).scaled(sz_bigger, sz_bigger, Qt::KeepAspectRatio, Qt::SmoothTransformation) + if (same_string(internalEvent->name, "")) { + setPixmap(EVENT_PIXMAP(":warning")); + } else if (internalEvent->type == SAMPLE_EVENT_BOOKMARK) { + setPixmap(EVENT_PIXMAP(":flag")); + } else if (strcmp(internalEvent->name, "heading") == 0 || + (same_string(internalEvent->name, "SP change") && internalEvent->time.seconds == 0)) { + // 2 cases: + // a) some dive computers have heading in every sample + // b) at t=0 we might have an "SP change" to indicate dive type + // in both cases we want to get the right data into the tooltip but don't want the visual clutter + // 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 + // pixmap for an event, but want to show the event value in the tooltip + QPixmap transparentPixmap(4, 20); + transparentPixmap.fill(QColor::fromRgbF(1.0, 1.0, 1.0, 0.01)); + setPixmap(transparentPixmap); + } else if (event_is_gaschange(internalEvent)) { + if (internalEvent->gas.mix.he.permille) + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeTrimix")); + else if (gasmix_is_air(&internalEvent->gas.mix)) + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeAir")); + else + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeNitrox")); + } else { + setPixmap(EVENT_PIXMAP(":warning")); + } +#undef EVENT_PIXMAP +} + +void DiveEventItem::setupToolTipString() +{ + // we display the event on screen - so translate + QString name = gettextFromC::instance()->tr(internalEvent->name); + int value = internalEvent->value; + int type = internalEvent->type; + if (value) { + if (event_is_gaschange(internalEvent)) { + name += ": "; + name += gasname(&internalEvent->gas.mix); + + /* Do we have an explicit cylinder index? Show it. */ + if (internalEvent->gas.index >= 0) + name += QString(" (cyl %1)").arg(internalEvent->gas.index+1); + } else if (type == SAMPLE_EVENT_PO2 && name == "SP change") { + name += QString(":%1").arg((double)value / 1000); + } else { + name += QString(":%1").arg(value); + } + } else if (type == SAMPLE_EVENT_PO2 && name == "SP change") { + // this is a bad idea - we are abusing an existing event type that is supposed to + // warn of high or low pO₂ and are turning it into a set point change event + name += "\n" + tr("Manual switch to OC"); + } else { + name += internalEvent->flags == SAMPLE_FLAGS_BEGIN ? tr(" begin", "Starts with space!") : + internalEvent->flags == SAMPLE_FLAGS_END ? tr(" end", "Starts with space!") : ""; + } + // qDebug() << name; + setToolTip(name); +} + +void DiveEventItem::eventVisibilityChanged(const QString &eventName, bool visible) +{ +} + +bool DiveEventItem::shouldBeHidden() +{ + struct event *event = internalEvent; + + /* + * Some gas change events are special. Some dive computers just tell us the initial gas this way. + * Don't bother showing those + */ + struct sample *first_sample = &get_dive_dc(&displayed_dive, dc_number)->sample[0]; + if (!strcmp(event->name, "gaschange") && + (event->time.seconds == 0 || + (first_sample && event->time.seconds == first_sample->time.seconds))) + return true; + + for (int i = 0; i < evn_used; i++) { + if (!strcmp(event->name, ev_namelist[i].ev_name) && ev_namelist[i].plot_ev == false) + return true; + } + return false; +} + +void DiveEventItem::recalculatePos(bool instant) +{ + if (!vAxis || !hAxis || !internalEvent || !dataModel) + return; + + QModelIndexList result = dataModel->match(dataModel->index(0, DivePlotDataModel::TIME), Qt::DisplayRole, internalEvent->time.seconds); + if (result.isEmpty()) { + Q_ASSERT("can't find a spot in the dataModel"); + hide(); + return; + } + if (!isVisible() && !shouldBeHidden()) + show(); + int depth = dataModel->data(dataModel->index(result.first().row(), DivePlotDataModel::DEPTH)).toInt(); + qreal x = hAxis->posAtValue(internalEvent->time.seconds); + qreal y = vAxis->posAtValue(depth); + if (!instant) + Animations::moveTo(this, x, y); + else + setPos(x, y); + if (isVisible() && shouldBeHidden()) + hide(); +} diff --git a/desktop-widgets/profile/diveeventitem.h b/desktop-widgets/profile/diveeventitem.h new file mode 100644 index 000000000..f358fee6d --- /dev/null +++ b/desktop-widgets/profile/diveeventitem.h @@ -0,0 +1,34 @@ +#ifndef DIVEEVENTITEM_H +#define DIVEEVENTITEM_H + +#include "divepixmapitem.h" + +class DiveCartesianAxis; +class DivePlotDataModel; +struct event; + +class DiveEventItem : public DivePixmapItem { + Q_OBJECT +public: + DiveEventItem(QObject *parent = 0); + void setEvent(struct event *ev); + struct event *getEvent(); + void eventVisibilityChanged(const QString &eventName, bool visible); + void setVerticalAxis(DiveCartesianAxis *axis); + void setHorizontalAxis(DiveCartesianAxis *axis); + void setModel(DivePlotDataModel *model); + bool shouldBeHidden(); +public +slots: + void recalculatePos(bool instant = false); + +private: + void setupToolTipString(); + void setupPixmap(); + DiveCartesianAxis *vAxis; + DiveCartesianAxis *hAxis; + DivePlotDataModel *dataModel; + struct event *internalEvent; +}; + +#endif // DIVEEVENTITEM_H diff --git a/desktop-widgets/profile/divelineitem.cpp b/desktop-widgets/profile/divelineitem.cpp new file mode 100644 index 000000000..f9e288a44 --- /dev/null +++ b/desktop-widgets/profile/divelineitem.cpp @@ -0,0 +1,5 @@ +#include "divelineitem.h" + +DiveLineItem::DiveLineItem(QGraphicsItem *parent) : QGraphicsLineItem(parent) +{ +} diff --git a/desktop-widgets/profile/divelineitem.h b/desktop-widgets/profile/divelineitem.h new file mode 100644 index 000000000..ec88e9da5 --- /dev/null +++ b/desktop-widgets/profile/divelineitem.h @@ -0,0 +1,15 @@ +#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 diff --git a/desktop-widgets/profile/divepixmapitem.cpp b/desktop-widgets/profile/divepixmapitem.cpp new file mode 100644 index 000000000..581f6f9b4 --- /dev/null +++ b/desktop-widgets/profile/divepixmapitem.cpp @@ -0,0 +1,130 @@ +#include "divepixmapitem.h" +#include "animationfunctions.h" +#include "divepicturemodel.h" +#include <preferences.h> + +#include <QDesktopServices> +#include <QGraphicsView> +#include <QUrl> + +DivePixmapItem::DivePixmapItem(QObject *parent) : QObject(parent), QGraphicsPixmapItem() +{ +} + +DiveButtonItem::DiveButtonItem(QObject *parent): DivePixmapItem(parent) +{ +} + +void DiveButtonItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mousePressEvent(event); + emit clicked(); +} + +// If we have many many pictures on screen, maybe a shared-pixmap would be better to +// paint on screen, but for now, this. +CloseButtonItem::CloseButtonItem(QObject *parent): DiveButtonItem(parent) +{ + static QPixmap p = QPixmap(":trash"); + setPixmap(p); + setFlag(ItemIgnoresTransformations); +} + +void CloseButtonItem::hide() +{ + DiveButtonItem::hide(); +} + +void CloseButtonItem::show() +{ + DiveButtonItem::show(); +} + +DivePictureItem::DivePictureItem(QObject *parent): DivePixmapItem(parent), + canvas(new QGraphicsRectItem(this)), + shadow(new QGraphicsRectItem(this)) +{ + setFlag(ItemIgnoresTransformations); + setAcceptHoverEvents(true); + setScale(0.2); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + setVisible(prefs.show_pictures_in_profile); + + 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); +} + +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()); +} + +CloseButtonItem *button = NULL; +void DivePictureItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) +{ + Animations::scaleTo(this, 1.0); + setZValue(5); + + if(!button) { + button = new CloseButtonItem(); + button->setScale(0.2); + button->setZValue(7); + scene()->addItem(button); + } + button->setParentItem(this); + button->setPos(boundingRect().width() - button->boundingRect().width() * 0.2, + boundingRect().height() - button->boundingRect().height() * 0.2); + button->setOpacity(0); + button->show(); + Animations::show(button); + button->disconnect(); + connect(button, SIGNAL(clicked()), this, SLOT(removePicture())); +} + +void DivePictureItem::setFileUrl(const QString &s) +{ + fileUrl = s; +} + +void DivePictureItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) +{ + Animations::scaleTo(this, 0.2); + setZValue(0); + if(button){ + button->setParentItem(NULL); + Animations::hide(button); + } +} + +DivePictureItem::~DivePictureItem(){ + if(button){ + button->setParentItem(NULL); + Animations::hide(button); + } +} + +void DivePictureItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(fileUrl)); +} + +void DivePictureItem::removePicture() +{ + DivePictureModel::instance()->removePicture(fileUrl); +} diff --git a/desktop-widgets/profile/divepixmapitem.h b/desktop-widgets/profile/divepixmapitem.h new file mode 100644 index 000000000..02c1523f7 --- /dev/null +++ b/desktop-widgets/profile/divepixmapitem.h @@ -0,0 +1,57 @@ +#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(QObject *parent = 0); +}; + +class DivePictureItem : public DivePixmapItem { + Q_OBJECT + Q_PROPERTY(qreal scale WRITE setScale READ scale) +public: + DivePictureItem(QObject *parent = 0); + virtual ~DivePictureItem(); + void setPixmap(const QPixmap& pix); +public slots: + void settingsChanged(); + void removePicture(); + void setFileUrl(const QString& s); +protected: + void hoverEnterEvent(QGraphicsSceneHoverEvent *event); + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); + void mousePressEvent(QGraphicsSceneMouseEvent *event); +private: + QString fileUrl; + QGraphicsRectItem *canvas; + QGraphicsRectItem *shadow; +}; + +class DiveButtonItem : public DivePixmapItem { + Q_OBJECT +public: + DiveButtonItem(QObject *parent = 0); +protected: + virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); +signals: + void clicked(); +}; + +class CloseButtonItem : public DiveButtonItem { + Q_OBJECT +public: + CloseButtonItem(QObject *parent = 0); +public slots: + void hide(); + void show(); +}; + +#endif // DIVEPIXMAPITEM_H diff --git a/desktop-widgets/profile/diveprofileitem.cpp b/desktop-widgets/profile/diveprofileitem.cpp new file mode 100644 index 000000000..2c814678a --- /dev/null +++ b/desktop-widgets/profile/diveprofileitem.cpp @@ -0,0 +1,979 @@ +#include "diveprofileitem.h" +#include "diveplotdatamodel.h" +#include "divecartesianaxis.h" +#include "divetextitem.h" +#include "animationfunctions.h" +#include "dive.h" +#include "profile.h" +#include "preferences.h" +#include "diveplannermodel.h" +#include "helpers.h" +#include "libdivecomputer/parser.h" +#include "mainwindow.h" +#include "maintab.h" +#include "profile/profilewidget2.h" +#include "diveplanner.h" + +#include <QSettings> + +AbstractProfilePolygonItem::AbstractProfilePolygonItem() : QObject(), QGraphicsPolygonItem(), hAxis(NULL), vAxis(NULL), dataModel(NULL), hDataColumn(-1), vDataColumn(-1) +{ + setCacheMode(DeviceCoordinateCache); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void AbstractProfilePolygonItem::settingsChanged() +{ +} + +void AbstractProfilePolygonItem::setHorizontalAxis(DiveCartesianAxis *horizontal) +{ + hAxis = horizontal; + connect(hAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setHorizontalDataColumn(int column) +{ + hDataColumn = column; + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setModel(DivePlotDataModel *model) +{ + dataModel = model; + connect(dataModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(modelDataChanged(QModelIndex, QModelIndex))); + connect(dataModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), this, SLOT(modelDataRemoved(QModelIndex, int, int))); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::modelDataRemoved(const QModelIndex &parent, int from, int to) +{ + setPolygon(QPolygonF()); + qDeleteAll(texts); + texts.clear(); +} + +void AbstractProfilePolygonItem::setVerticalAxis(DiveCartesianAxis *vertical) +{ + vAxis = vertical; + connect(vAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + connect(vAxis, SIGNAL(maxChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setVerticalDataColumn(int column) +{ + vDataColumn = column; + modelDataChanged(); +} + +bool AbstractProfilePolygonItem::shouldCalculateStuff(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!hAxis || !vAxis) + return false; + if (!dataModel || dataModel->rowCount() == 0) + return false; + if (hDataColumn == -1 || vDataColumn == -1) + return false; + if (topLeft.isValid() && bottomRight.isValid()) { + if ((topLeft.column() >= vDataColumn || topLeft.column() >= hDataColumn) && + (bottomRight.column() <= vDataColumn || topLeft.column() <= hDataColumn)) { + return true; + } + } + return true; +} + +void AbstractProfilePolygonItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + + // Calculate the polygon. This is the polygon that will be painted on screen + // on the ::paint method. Here we calculate the correct position of the points + // regarting our cartesian plane ( made by the hAxis and vAxis ), the QPolygonF + // is an array of QPointF's, so we basically get the point from the model, convert + // to our coordinates, store. no painting is done here. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + qreal horizontalValue = dataModel->index(i, hDataColumn).data().toReal(); + qreal verticalValue = dataModel->index(i, vDataColumn).data().toReal(); + QPointF point(hAxis->posAtValue(horizontalValue), vAxis->posAtValue(verticalValue)); + poly.append(point); + } + setPolygon(poly); + + qDeleteAll(texts); + texts.clear(); +} + +DiveProfileItem::DiveProfileItem() : show_reported_ceiling(0), reported_ceiling_in_red(0) +{ +} + +void DiveProfileItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(widget); + if (polygon().isEmpty()) + return; + + painter->save(); + // This paints the Polygon + Background. I'm setting the pen to QPen() so we don't get a black line here, + // after all we need to plot the correct velocities colors later. + setPen(Qt::NoPen); + QGraphicsPolygonItem::paint(painter, option, widget); + + // Here we actually paint the boundaries of the Polygon using the colors that the model provides. + // Those are the speed colors of the dives. + QPen pen; + pen.setCosmetic(true); + pen.setWidth(2); + QPolygonF poly = polygon(); + // This paints the colors of the velocities. + for (int i = 1, count = dataModel->rowCount(); i < count; i++) { + QModelIndex colorIndex = dataModel->index(i, DivePlotDataModel::COLOR); + pen.setBrush(QBrush(colorIndex.data(Qt::BackgroundRole).value<QColor>())); + painter->setPen(pen); + painter->drawLine(poly[i - 1], poly[i]); + } + painter->restore(); +} + +int DiveProfileItem::maxCeiling(int row) +{ + int max = -1; + plot_data *entry = dataModel->data().entry + row; + for (int tissue = 0; tissue < 16; tissue++) { + if (max < entry->ceilings[tissue]) + max = entry->ceilings[tissue]; + } + return max; +} + +void DiveProfileItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + bool eventAdded = false; + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + AbstractProfilePolygonItem::modelDataChanged(topLeft, bottomRight); + if (polygon().isEmpty()) + return; + + show_reported_ceiling = prefs.dcceiling; + reported_ceiling_in_red = prefs.redceiling; + profileColor = getColor(DEPTH_BOTTOM); + + int currState = qobject_cast<ProfileWidget2 *>(scene()->views().first())->currentState; + if (currState == ProfileWidget2::PLAN) { + plot_data *entry = dataModel->data().entry; + for (int i = 0; i < dataModel->rowCount(); i++, entry++) { + int max = maxCeiling(i); + // Don't scream if we violate the ceiling by a few cm + if (entry->depth < max - 100 && entry->sec > 0) { + profileColor = QColor(Qt::red); + if (!eventAdded) { + add_event(&displayed_dive.dc, entry->sec, SAMPLE_EVENT_CEILING, -1, max / 1000, "planned waypoint above ceiling"); + eventAdded = true; + } + } + } + } + + /* Show any ceiling we may have encountered */ + if (prefs.dcceiling && !prefs.redceiling) { + QPolygonF p = polygon(); + plot_data *entry = dataModel->data().entry + dataModel->rowCount() - 1; + for (int i = dataModel->rowCount() - 1; i >= 0; i--, entry--) { + if (!entry->in_deco) { + /* not in deco implies this is a safety stop, no ceiling */ + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(0))); + } else { + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(qMin(entry->stopdepth, entry->depth)))); + } + } + setPolygon(p); + } + + // This is the blueish gradient that the Depth Profile should have. + // It's a simple QLinearGradient with 2 stops, starting from top to bottom. + QLinearGradient pat(0, polygon().boundingRect().top(), 0, polygon().boundingRect().bottom()); + pat.setColorAt(1, profileColor); + pat.setColorAt(0, getColor(DEPTH_TOP)); + setBrush(QBrush(pat)); + + int last = -1; + for (int i = 0, count = dataModel->rowCount(); i < count; i++) { + + struct plot_data *entry = dataModel->data().entry + i; + if (entry->depth < 2000) + continue; + + if ((entry == entry->max[2]) && entry->depth / 100 != last) { + plot_depth_sample(entry, Qt::AlignHCenter | Qt::AlignBottom, getColor(SAMPLE_DEEP)); + last = entry->depth / 100; + } + + if ((entry == entry->min[2]) && entry->depth / 100 != last) { + plot_depth_sample(entry, Qt::AlignHCenter | Qt::AlignTop, getColor(SAMPLE_SHALLOW)); + last = entry->depth / 100; + } + + if (entry->depth != last) + last = -1; + } +} + +void DiveProfileItem::settingsChanged() +{ + //TODO: Only modelDataChanged() here if we need to rebuild the graph ( for instance, + // if the prefs.dcceiling are enabled, but prefs.redceiling is disabled + // and only if it changed something. let's not waste cpu cycles repoloting something we don't need to. + modelDataChanged(); +} + +void DiveProfileItem::plot_depth_sample(struct plot_data *entry, QFlags<Qt::AlignmentFlag> flags, const QColor &color) +{ + int decimals; + double d = get_depth_units(entry->depth, &decimals, NULL); + DiveTextItem *item = new DiveTextItem(this); + item->setPos(hAxis->posAtValue(entry->sec), vAxis->posAtValue(entry->depth)); + item->setText(QString("%1").arg(d, 0, 'f', 1)); + item->setAlignment(flags); + item->setBrush(color); + texts.append(item); +} + +DiveHeartrateItem::DiveHeartrateItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::HR_PLOT))); + pen.setCosmetic(true); + pen.setWidth(1); + setPen(pen); + settingsChanged(); +} + +void DiveHeartrateItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int last = -300, last_printed_hr = 0, sec = 0; + struct { + int sec; + int hr; + } hist[3] = {}; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + qDeleteAll(texts); + texts.clear(); + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int hr = dataModel->index(i, vDataColumn).data().toInt(); + if (!hr) + continue; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(hr)); + poly.append(point); + if (hr == hist[2].hr) + // same as last one, no point in looking at printing + continue; + hist[0] = hist[1]; + hist[1] = hist[2]; + hist[2].sec = sec; + hist[2].hr = hr; + // don't print a HR + // if it's not a local min / max + // if it's been less than 5min and less than a 20 beats change OR + // if it's been less than 2min OR if the change from the + // last print is less than 10 beats + // to test min / max requires three points, so we now look at the + // previous one + sec = hist[1].sec; + hr = hist[1].hr; + if ((hist[0].hr < hr && hr < hist[2].hr) || + (hist[0].hr > hr && hr > hist[2].hr) || + ((sec < last + 300) && (abs(hr - last_printed_hr) < 20)) || + (sec < last + 120) || + (abs(hr - last_printed_hr) < 10)) + continue; + last = sec; + createTextItem(sec, hr); + last_printed_hr = hr; + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveHeartrateItem::createTextItem(int sec, int hr) +{ + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignBottom); + text->setBrush(getColor(HR_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec), vAxis->posAtValue(hr))); + text->setScale(0.7); // need to call this BEFORE setText() + text->setText(QString("%1").arg(hr)); + texts.append(text); +} + +void DiveHeartrateItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveHeartrateItem::settingsChanged() +{ + setVisible(prefs.hrgraph); +} + +DivePercentageItem::DivePercentageItem(int i) +{ + QPen pen; + QColor color; + color.setHsl(100 + 10 * i, 200, 100); + pen.setBrush(QBrush(color)); + pen.setCosmetic(true); + pen.setWidth(1); + setPen(pen); + settingsChanged(); +} + +void DivePercentageItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int hr = dataModel->index(i, vDataColumn).data().toInt(); + if (!hr) + continue; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(hr)); + poly.append(point); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DivePercentageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DivePercentageItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveAmbPressureItem::DiveAmbPressureItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::AMB_PRESSURE_LINE))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveAmbPressureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int hr = dataModel->index(i, vDataColumn).data().toInt(); + if (!hr) + continue; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(hr)); + poly.append(point); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveAmbPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveAmbPressureItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveGFLineItem::DiveGFLineItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::GF_LINE))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveGFLineItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int hr = dataModel->index(i, vDataColumn).data().toInt(); + if (!hr) + continue; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(hr)); + poly.append(point); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveGFLineItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveGFLineItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveTemperatureItem::DiveTemperatureItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::TEMP_PLOT))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); +} + +void DiveTemperatureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int last = -300, last_printed_temp = 0, sec = 0, last_valid_temp = 0; + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + qDeleteAll(texts); + texts.clear(); + // Ignore empty values. things do not look good with '0' as temperature in kelvin... + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int mkelvin = dataModel->index(i, vDataColumn).data().toInt(); + if (!mkelvin) + continue; + last_valid_temp = mkelvin; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(mkelvin)); + poly.append(point); + + /* don't print a temperature + * if it's been less than 5min and less than a 2K change OR + * if it's been less than 2min OR if the change from the + * last print is less than .4K (and therefore less than 1F) */ + if (((sec < last + 300) && (abs(mkelvin - last_printed_temp) < 2000)) || + (sec < last + 120) || + (abs(mkelvin - last_printed_temp) < 400)) + continue; + last = sec; + if (mkelvin > 200000) + createTextItem(sec, mkelvin); + last_printed_temp = mkelvin; + } + setPolygon(poly); + + /* it would be nice to print the end temperature, if it's + * different or if the last temperature print has been more + * than a quarter of the dive back */ + if (last_valid_temp > 200000 && + ((abs(last_valid_temp - last_printed_temp) > 500) || ((double)last / (double)sec < 0.75))) { + createTextItem(sec, last_valid_temp); + } + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveTemperatureItem::createTextItem(int sec, int mkelvin) +{ + double deg; + const char *unit; + deg = get_temp_units(mkelvin, &unit); + + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignBottom); + text->setBrush(getColor(TEMP_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec), vAxis->posAtValue(mkelvin))); + text->setScale(0.8); // need to call this BEFORE setText() + text->setText(QString("%1%2").arg(deg, 0, 'f', 1).arg(unit)); + texts.append(text); +} + +void DiveTemperatureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +DiveMeanDepthItem::DiveMeanDepthItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::HR_AXIS))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveMeanDepthItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + double meandepthvalue = 0.0; + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + QPolygonF poly; + plot_data *entry = dataModel->data().entry; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++, entry++) { + // Ignore empty values + if (entry->running_sum == 0 || entry->sec == 0) + continue; + + meandepthvalue = entry->running_sum / entry->sec; + QPointF point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(meandepthvalue)); + poly.append(point); + } + lastRunningSum = meandepthvalue; + setPolygon(poly); + createTextItem(); +} + + +void DiveMeanDepthItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveMeanDepthItem::settingsChanged() +{ + setVisible(prefs.show_average_depth); +} + +void DiveMeanDepthItem::createTextItem() { + plot_data *entry = dataModel->data().entry; + int sec = entry[dataModel->rowCount()-1].sec; + qDeleteAll(texts); + texts.clear(); + int decimals; + const char *unitText; + double d = get_depth_units(lastRunningSum, &decimals, &unitText); + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignTop); + text->setBrush(getColor(TEMP_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec) + 1, vAxis->posAtValue(lastRunningSum))); + text->setScale(0.8); // need to call this BEFORE setText() + text->setText(QString("%1%2").arg(d, 0, 'f', 1).arg(unitText)); + texts.append(text); +} + +void DiveGasPressureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + int last_index = -1; + int o2mbar; + QPolygonF boundingPoly, o2Poly; // This is the "Whole Item", but a pressure can be divided in N Polygons. + polygons.clear(); + if (displayed_dive.dc.divemode == CCR) + polygons.append(o2Poly); + + for (int i = 0, count = dataModel->rowCount(); i < count; i++) { + o2mbar = 0; + plot_data *entry = dataModel->data().entry + i; + int mbar = GET_PRESSURE(entry); + if (displayed_dive.dc.divemode == CCR) + o2mbar = GET_O2CYLINDER_PRESSURE(entry); + + if (entry->cylinderindex != last_index) { + polygons.append(QPolygonF()); // this is the polygon that will be actually drawn on screen. + last_index = entry->cylinderindex; + } + if (!mbar) { + continue; + } + if (o2mbar) { + QPointF o2point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(o2mbar)); + boundingPoly.push_back(o2point); + polygons.first().push_back(o2point); + } + + QPointF point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(mbar)); + boundingPoly.push_back(point); // The BoundingRect + polygons.last().push_back(point); // The polygon thta will be plotted. + } + setPolygon(boundingPoly); + qDeleteAll(texts); + texts.clear(); + int mbar, cyl; + int seen_cyl[MAX_CYLINDERS] = { false, }; + int last_pressure[MAX_CYLINDERS] = { 0, }; + int last_time[MAX_CYLINDERS] = { 0, }; + struct plot_data *entry; + + cyl = -1; + o2mbar = 0; + + double print_y_offset[8][2] = { { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 } ,{ 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 } }; + // CCR dives: These are offset values used to print the gas lables and pressures on a CCR dive profile at + // appropriate Y-coordinates: One doublet of values for each of 8 cylinders. + // Order of offsets within a doublet: gas lable offset; gas pressure offset. + // The array is initialised with default values that apply to non-CCR dives. + + bool offsets_initialised = false; + int o2cyl = -1, dilcyl = -1; + QFlags<Qt::AlignmentFlag> alignVar= Qt::AlignTop, align_dil = Qt::AlignBottom, align_o2 = Qt::AlignTop; + double axisRange = (vAxis->maximum() - vAxis->minimum())/1000; // Convert axis pressure range to bar + double axisLog = log10(log10(axisRange)); + for (int i = 0, count = dataModel->rowCount(); i < count; i++) { + entry = dataModel->data().entry + i; + mbar = GET_PRESSURE(entry); + if (displayed_dive.dc.divemode == CCR && displayed_dive.oxygen_cylinder_index >= 0) + o2mbar = GET_O2CYLINDER_PRESSURE(entry); + + if (o2mbar) { // If there is an o2mbar value then this is a CCR dive. Then do: + // The first time an o2 value is detected, see if the oxygen cyl pressure graph starts above or below the dil graph + if (!offsets_initialised) { // Initialise the parameters for placing the text correctly near the graph line: + o2cyl = displayed_dive.oxygen_cylinder_index; + dilcyl = displayed_dive.diluent_cylinder_index; + if ((o2mbar > mbar)) { // If above, write o2 start cyl pressure above graph and diluent pressure below graph: + print_y_offset[o2cyl][0] = -7 * axisLog; // y offset for oxygen gas lable (above); pressure offsets=-0.5, already initialised + print_y_offset[dilcyl][0] = 5 * axisLog; // y offset for diluent gas lable (below) + } else { // ... else write o2 start cyl pressure below graph: + print_y_offset[o2cyl][0] = 5 * axisLog; // o2 lable & pressure below graph; pressure offsets=-0.5, already initialised + print_y_offset[dilcyl][0] = -7.8 * axisLog; // and diluent lable above graph. + align_dil = Qt::AlignTop; + align_o2 = Qt::AlignBottom; + } + offsets_initialised = true; + } + + if (!seen_cyl[displayed_dive.oxygen_cylinder_index]) { //For o2, on the left of profile, write lable and pressure + plotPressureValue(o2mbar, entry->sec, align_o2, print_y_offset[o2cyl][1]); + plotGasValue(o2mbar, entry->sec, displayed_dive.cylinder[displayed_dive.oxygen_cylinder_index].gasmix, align_o2, print_y_offset[o2cyl][0]); + seen_cyl[displayed_dive.oxygen_cylinder_index] = true; + } + last_pressure[displayed_dive.oxygen_cylinder_index] = o2mbar; + last_time[displayed_dive.oxygen_cylinder_index] = entry->sec; + alignVar = align_dil; + } + + if (!mbar) + continue; + + if (cyl != entry->cylinderindex) { // Pressure value near the left hand edge of the profile - other cylinders: + cyl = entry->cylinderindex; // For each other cylinder, write the gas lable and pressure + if (!seen_cyl[cyl]) { + plotPressureValue(mbar, entry->sec, alignVar, print_y_offset[cyl][1]); + plotGasValue(mbar, entry->sec, displayed_dive.cylinder[cyl].gasmix, align_dil, print_y_offset[cyl][0]); + seen_cyl[cyl] = true; + } + } + last_pressure[cyl] = mbar; + last_time[cyl] = entry->sec; + } + + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { // For each cylinder, on right hand side of profile, write cylinder pressure + alignVar = ((o2cyl >= 0) && (cyl == displayed_dive.oxygen_cylinder_index)) ? align_o2 : align_dil; + if (last_time[cyl]) { + plotPressureValue(last_pressure[cyl], last_time[cyl], (alignVar | Qt::AlignLeft), print_y_offset[cyl][1]); + } + } +} + +void DiveGasPressureItem::plotPressureValue(int mbar, int sec, QFlags<Qt::AlignmentFlag> align, double pressure_offset) +{ + const char *unit; + int pressure = get_pressure_units(mbar, &unit); + DiveTextItem *text = new DiveTextItem(this); + text->setPos(hAxis->posAtValue(sec), vAxis->posAtValue(mbar) + pressure_offset ); + text->setText(QString("%1 %2").arg(pressure).arg(unit)); + text->setAlignment(align); + text->setBrush(getColor(PRESSURE_TEXT)); + texts.push_back(text); +} + +void DiveGasPressureItem::plotGasValue(int mbar, int sec, struct gasmix gasmix, QFlags<Qt::AlignmentFlag> align, double gasname_offset) +{ + QString gas = get_gas_string(gasmix); + DiveTextItem *text = new DiveTextItem(this); + text->setPos(hAxis->posAtValue(sec), vAxis->posAtValue(mbar) + gasname_offset ); + text->setText(gas); + text->setAlignment(align); + text->setBrush(getColor(PRESSURE_TEXT)); + texts.push_back(text); +} + +void DiveGasPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + QPen pen; + pen.setCosmetic(true); + pen.setWidth(2); + painter->save(); + struct plot_data *entry; + Q_FOREACH (const QPolygonF &poly, polygons) { + entry = dataModel->data().entry; + for (int i = 1, count = poly.count(); i < count; i++, entry++) { + if (entry->sac) + pen.setBrush(getSacColor(entry->sac, displayed_dive.sac)); + else + pen.setBrush(MED_GRAY_HIGH_TRANS); + painter->setPen(pen); + painter->drawLine(poly[i - 1], poly[i]); + } + } + painter->restore(); +} + +DiveCalculatedCeiling::DiveCalculatedCeiling() : is3mIncrement(false) +{ + settingsChanged(); +} + +void DiveCalculatedCeiling::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (MainWindow::instance()->information()) + connect(MainWindow::instance()->information(), SIGNAL(dateTimeChanged()), this, SLOT(recalc()), Qt::UniqueConnection); + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + AbstractProfilePolygonItem::modelDataChanged(topLeft, bottomRight); + // Add 2 points to close the polygon. + QPolygonF poly = polygon(); + if (poly.isEmpty()) + return; + QPointF p1 = poly.first(); + QPointF p2 = poly.last(); + + poly.prepend(QPointF(p1.x(), vAxis->posAtValue(0))); + poly.append(QPointF(p2.x(), vAxis->posAtValue(0))); + setPolygon(poly); + + QLinearGradient pat(0, polygon().boundingRect().top(), 0, polygon().boundingRect().bottom()); + pat.setColorAt(0, getColor(CALC_CEILING_SHALLOW)); + pat.setColorAt(1, getColor(CALC_CEILING_DEEP)); + setPen(QPen(QBrush(Qt::NoBrush), 0)); + setBrush(pat); +} + +void DiveCalculatedCeiling::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + QGraphicsPolygonItem::paint(painter, option, widget); +} + +DiveCalculatedTissue::DiveCalculatedTissue() +{ + settingsChanged(); +} + +void DiveCalculatedTissue::settingsChanged() +{ + setVisible(prefs.calcalltissues && prefs.calcceiling); +} + +void DiveReportedCeiling::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + QPolygonF p; + p.append(QPointF(hAxis->posAtValue(0), vAxis->posAtValue(0))); + plot_data *entry = dataModel->data().entry; + for (int i = 0, count = dataModel->rowCount(); i < count; i++, entry++) { + if (entry->in_deco && entry->stopdepth) { + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(qMin(entry->stopdepth, entry->depth)))); + } else { + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(0))); + } + } + setPolygon(p); + QLinearGradient pat(0, p.boundingRect().top(), 0, p.boundingRect().bottom()); + // does the user want the ceiling in "surface color" or in red? + if (prefs.redceiling) { + pat.setColorAt(0, getColor(CEILING_SHALLOW)); + pat.setColorAt(1, getColor(CEILING_DEEP)); + } else { + pat.setColorAt(0, getColor(BACKGROUND_TRANS)); + pat.setColorAt(1, getColor(BACKGROUND_TRANS)); + } + setPen(QPen(QBrush(Qt::NoBrush), 0)); + setBrush(pat); +} + +void DiveCalculatedCeiling::recalc() +{ + dataModel->calculateDecompression(); +} + +void DiveCalculatedCeiling::settingsChanged() +{ + if (dataModel && is3mIncrement != prefs.calcceiling3m) { + // recalculate that part. + recalc(); + } + is3mIncrement = prefs.calcceiling3m; + setVisible(prefs.calcceiling); +} + +void DiveReportedCeiling::settingsChanged() +{ + setVisible(prefs.dcceiling); +} + +void DiveReportedCeiling::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + QGraphicsPolygonItem::paint(painter, option, widget); +} + +void PartialPressureGasItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + //AbstractProfilePolygonItem::modelDataChanged(); + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + plot_data *entry = dataModel->data().entry; + QPolygonF poly; + QPolygonF alertpoly; + alertPolygons.clear(); + QSettings s; + s.beginGroup("TecDetails"); + double threshold = 0.0; + if (thresholdPtr) + threshold = *thresholdPtr; + bool inAlertFragment = false; + for (int i = 0; i < dataModel->rowCount(); i++, entry++) { + double value = dataModel->index(i, vDataColumn).data().toDouble(); + int time = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(time), vAxis->posAtValue(value)); + poly.push_back(point); + if (value >= threshold) { + if (inAlertFragment) { + alertPolygons.back().push_back(point); + } else { + alertpoly.clear(); + alertpoly.push_back(point); + alertPolygons.append(alertpoly); + inAlertFragment = true; + } + } else { + inAlertFragment = false; + } + } + setPolygon(poly); + /* + createPPLegend(trUtf8("pN" UTF8_SUBSCRIPT_2),getColor(PN2), legendPos); + */ +} + +void PartialPressureGasItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + const qreal pWidth = 0.0; + painter->save(); + painter->setPen(QPen(normalColor, pWidth)); + painter->drawPolyline(polygon()); + + QPolygonF poly; + painter->setPen(QPen(alertColor, pWidth)); + Q_FOREACH (const QPolygonF &poly, alertPolygons) + painter->drawPolyline(poly); + painter->restore(); +} + +void PartialPressureGasItem::setThreshouldSettingsKey(double *prefPointer) +{ + thresholdPtr = prefPointer; +} + +PartialPressureGasItem::PartialPressureGasItem() : + thresholdPtr(NULL) +{ +} + +void PartialPressureGasItem::settingsChanged() +{ + QSettings s; + s.beginGroup("TecDetails"); + setVisible(s.value(visibilityKey).toBool()); +} + +void PartialPressureGasItem::setVisibilitySettingsKey(const QString &key) +{ + visibilityKey = key; +} + +void PartialPressureGasItem::setColors(const QColor &normal, const QColor &alert) +{ + normalColor = normal; + alertColor = alert; +} diff --git a/desktop-widgets/profile/diveprofileitem.h b/desktop-widgets/profile/diveprofileitem.h new file mode 100644 index 000000000..0bba7f7a3 --- /dev/null +++ b/desktop-widgets/profile/diveprofileitem.h @@ -0,0 +1,225 @@ +#ifndef DIVEPROFILEITEM_H +#define DIVEPROFILEITEM_H + +#include <QObject> +#include <QGraphicsPolygonItem> +#include <QModelIndex> + +#include "divelineitem.h" + +/* 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: + + DiveProfileItem *profile = new DiveProfileItem(); + profile->setVerticalAxis( profileYAxis ); + profile->setHorizontalAxis( timeAxis ); + profile->setModel( DiveDataModel ); + profile->setHorizontalDataColumn( DiveDataModel::TIME ); + profile->setVerticalDataColumn( DiveDataModel::DEPTH ); + scene()->addItem(profile); + + This is a generically item and should be used as a base for others, I think... +*/ + +class DivePlotDataModel; +class DiveTextItem; +class DiveCartesianAxis; +class QAbstractTableModel; +struct plot_data; + +class AbstractProfilePolygonItem : public QObject, public QGraphicsPolygonItem { + Q_OBJECT + 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: + AbstractProfilePolygonItem(); + void setVerticalAxis(DiveCartesianAxis *vertical); + void setHorizontalAxis(DiveCartesianAxis *horizontal); + void setModel(DivePlotDataModel *model); + void setHorizontalDataColumn(int column); + void setVerticalDataColumn(int column); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0) = 0; + virtual void clear() + { + } +public +slots: + virtual void settingsChanged(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void modelDataRemoved(const QModelIndex &parent, int from, int to); + +protected: + /* when the model emits a 'datachanged' signal, this method below should be used to check if the + * modified data affects this particular item ( for example, when setting the '3m increment' + * the data for Ceiling and tissues will be changed, and only those. so, the topLeft will be the CEILING + * column and the bottomRight will have the TISSUE_16 column. this method takes the vDataColumn and hDataColumn + * into consideration when returning 'true' for "yes, continue the calculation', and 'false' for + * 'do not recalculate, we already have the right data. + */ + bool shouldCalculateStuff(const QModelIndex &topLeft, const QModelIndex &bottomRight); + + DiveCartesianAxis *hAxis; + DiveCartesianAxis *vAxis; + DivePlotDataModel *dataModel; + int hDataColumn; + int vDataColumn; + QList<DiveTextItem *> texts; +}; + +class DiveProfileItem : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + DiveProfileItem(); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void settingsChanged(); + void plot_depth_sample(struct plot_data *entry, QFlags<Qt::AlignmentFlag> flags, const QColor &color); + int maxCeiling(int row); + +private: + unsigned int show_reported_ceiling; + unsigned int reported_ceiling_in_red; + QColor profileColor; +}; + +class DiveMeanDepthItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveMeanDepthItem(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); + +private: + void createTextItem(); + double lastRunningSum; + QString visibilityKey; +}; + +class DiveTemperatureItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveTemperatureItem(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + +private: + void createTextItem(int seconds, int mkelvin); +}; + +class DiveHeartrateItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveHeartrateItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + void createTextItem(int seconds, int hr); + QString visibilityKey; +}; + +class DivePercentageItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DivePercentageItem(int i); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveAmbPressureItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveAmbPressureItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveGFLineItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveGFLineItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveGasPressureItem : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + +private: + void plotPressureValue(int mbar, int sec, QFlags<Qt::AlignmentFlag> align, double offset); + void plotGasValue(int mbar, int sec, struct gasmix gasmix, QFlags<Qt::AlignmentFlag> align, double offset); + QVector<QPolygonF> polygons; +}; + +class DiveCalculatedCeiling : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + DiveCalculatedCeiling(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); + +public +slots: + void recalc(); + +private: + bool is3mIncrement; +}; + +class DiveReportedCeiling : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); +}; + +class DiveCalculatedTissue : public DiveCalculatedCeiling { + Q_OBJECT +public: + DiveCalculatedTissue(); + virtual void settingsChanged(); +}; + +class PartialPressureGasItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + PartialPressureGasItem(); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void settingsChanged(); + void setThreshouldSettingsKey(double *prefPointer); + void setVisibilitySettingsKey(const QString &setVisibilitySettingsKey); + void setColors(const QColor &normalColor, const QColor &alertColor); + +private: + QVector<QPolygonF> alertPolygons; + double *thresholdPtr; + QString visibilityKey; + QColor normalColor; + QColor alertColor; +}; +#endif // DIVEPROFILEITEM_H diff --git a/desktop-widgets/profile/diverectitem.cpp b/desktop-widgets/profile/diverectitem.cpp new file mode 100644 index 000000000..8cb60c3f5 --- /dev/null +++ b/desktop-widgets/profile/diverectitem.cpp @@ -0,0 +1,5 @@ +#include "diverectitem.h" + +DiveRectItem::DiveRectItem(QObject *parent, QGraphicsItem *parentItem) : QObject(parent), QGraphicsRectItem(parentItem) +{ +} diff --git a/desktop-widgets/profile/diverectitem.h b/desktop-widgets/profile/diverectitem.h new file mode 100644 index 000000000..e616cf591 --- /dev/null +++ b/desktop-widgets/profile/diverectitem.h @@ -0,0 +1,17 @@ +#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 diff --git a/desktop-widgets/profile/divetextitem.cpp b/desktop-widgets/profile/divetextitem.cpp new file mode 100644 index 000000000..3bf00d68f --- /dev/null +++ b/desktop-widgets/profile/divetextitem.cpp @@ -0,0 +1,113 @@ +#include "divetextitem.h" +#include "mainwindow.h" +#include "profilewidget2.h" +#include "subsurface-core/color.h" + +#include <QBrush> + +DiveTextItem::DiveTextItem(QGraphicsItem *parent) : QGraphicsItemGroup(parent), + internalAlignFlags(Qt::AlignHCenter | Qt::AlignVCenter), + textBackgroundItem(new QGraphicsPathItem(this)), + textItem(new QGraphicsPathItem(this)), + printScale(1.0), + scale(1.0), + connected(false) +{ + setFlag(ItemIgnoresTransformations); + textBackgroundItem->setBrush(QBrush(getColor(TEXT_BACKGROUND))); + textBackgroundItem->setPen(Qt::NoPen); + textItem->setPen(Qt::NoPen); +} + +void DiveTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + updateText(); + QGraphicsItemGroup::paint(painter, option, widget); +} + +void DiveTextItem::fontPrintScaleUpdate(double scale) +{ + printScale = scale; +} + +void DiveTextItem::setAlignment(int alignFlags) +{ + if (alignFlags != internalAlignFlags) { + internalAlignFlags = alignFlags; + } +} + +void DiveTextItem::setBrush(const QBrush &b) +{ + textItem->setBrush(b); +} + +void DiveTextItem::setScale(double newscale) +{ + if (scale != newscale) { + scale = newscale; + } +} + +void DiveTextItem::setText(const QString &t) +{ + if (internalText != t) { + if (!connected) { + if (scene()) { + // by now we should be on a scene. grab the profile widget from it and setup our printScale + // and connect to the signal that makes sure we keep track if that changes + ProfileWidget2 *profile = qobject_cast<ProfileWidget2 *>(scene()->views().first()); + connect(profile, SIGNAL(fontPrintScaleChanged(double)), this, SLOT(fontPrintScaleUpdate(double)), Qt::UniqueConnection); + fontPrintScaleUpdate(profile->getFontPrintScale()); + connected = true; + } else { + qDebug() << "called before scene was set up" << t; + } + } + internalText = t; + updateText(); + } +} + +const QString &DiveTextItem::text() +{ + return internalText; +} + +void DiveTextItem::updateText() +{ + double size; + if (internalText.isEmpty()) { + return; + } + + QFont fnt(qApp->font()); + if ((size = fnt.pixelSize()) > 0) { + // set in pixels - so the scale factor may not make a difference if it's too close to 1 + size *= scale * printScale; + fnt.setPixelSize(size); + } else { + size = fnt.pointSizeF(); + size *= scale * printScale; + fnt.setPointSizeF(size); + } + QFontMetrics fm(fnt); + + QPainterPath textPath; + qreal xPos = 0, yPos = 0; + + QRectF rect = fm.boundingRect(internalText); + yPos = (internalAlignFlags & Qt::AlignTop) ? 0 : + (internalAlignFlags & Qt::AlignBottom) ? +rect.height() : + /*(internalAlignFlags & Qt::AlignVCenter ? */ +rect.height() / 4; + + xPos = (internalAlignFlags & Qt::AlignLeft) ? -rect.width() : + (internalAlignFlags & Qt::AlignHCenter) ? -rect.width() / 2 : + /* (internalAlignFlags & Qt::AlignRight) */ 0; + + textPath.addText(xPos, yPos, fnt, internalText); + QPainterPathStroker stroker; + stroker.setWidth(3); + textBackgroundItem->setPath(stroker.createStroke(textPath)); + textItem->setPath(textPath); +} diff --git a/desktop-widgets/profile/divetextitem.h b/desktop-widgets/profile/divetextitem.h new file mode 100644 index 000000000..be0adf292 --- /dev/null +++ b/desktop-widgets/profile/divetextitem.h @@ -0,0 +1,38 @@ +#ifndef DIVETEXTITEM_H +#define DIVETEXTITEM_H + +#include <QObject> +#include <QGraphicsItemGroup> + +class QBrush; + +/* A Line Item that has animated-properties. */ +class DiveTextItem : public QObject, public QGraphicsItemGroup { + Q_OBJECT + Q_PROPERTY(QPointF pos READ pos WRITE setPos) + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) +public: + DiveTextItem(QGraphicsItem *parent = 0); + void setText(const QString &text); + void setAlignment(int alignFlags); + void setScale(double newscale); + void setBrush(const QBrush &brush); + const QString &text(); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + +private +slots: + void fontPrintScaleUpdate(double scale); + +private: + void updateText(); + int internalAlignFlags; + QGraphicsPathItem *textBackgroundItem; + QGraphicsPathItem *textItem; + QString internalText; + double printScale; + double scale; + bool connected; +}; + +#endif // DIVETEXTITEM_H diff --git a/desktop-widgets/profile/divetooltipitem.cpp b/desktop-widgets/profile/divetooltipitem.cpp new file mode 100644 index 000000000..d4818422b --- /dev/null +++ b/desktop-widgets/profile/divetooltipitem.cpp @@ -0,0 +1,285 @@ +#include "divetooltipitem.h" +#include "divecartesianaxis.h" +#include "dive.h" +#include "profile.h" +#include "membuffer.h" +#include "metrics.h" +#include <QPropertyAnimation> +#include <QSettings> +#include <QGraphicsView> +#include <QStyleOptionGraphicsItem> + +#define PORT_IN_PROGRESS 1 +#ifdef PORT_IN_PROGRESS +#include "display.h" +#endif + +void ToolTipItem::addToolTip(const QString &toolTip, const QIcon &icon, const QPixmap& pixmap) +{ + const IconMetrics& iconMetrics = defaultIconMetrics(); + + QGraphicsPixmapItem *iconItem = 0; + double yValue = title->boundingRect().height() + iconMetrics.spacing; + Q_FOREACH (ToolTip t, toolTips) { + yValue += t.second->boundingRect().height(); + } + if (entryToolTip.second) { + yValue += entryToolTip.second->boundingRect().height(); + } + iconItem = new QGraphicsPixmapItem(this); + if (!icon.isNull()) { + iconItem->setPixmap(icon.pixmap(iconMetrics.sz_small, iconMetrics.sz_small)); + } else if (!pixmap.isNull()) { + iconItem->setPixmap(pixmap); + } + iconItem->setPos(iconMetrics.spacing, yValue); + + QGraphicsSimpleTextItem *textItem = new QGraphicsSimpleTextItem(toolTip, this); + textItem->setPos(iconMetrics.spacing + iconMetrics.sz_small + iconMetrics.spacing, yValue); + textItem->setBrush(QBrush(Qt::white)); + textItem->setFlag(ItemIgnoresTransformations); + toolTips.push_back(qMakePair(iconItem, textItem)); +} + +void ToolTipItem::clear() +{ + Q_FOREACH (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 (prefs.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; + Q_FOREACH (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(); + } + + /* Left padding, Icon Size, space, right padding */ + width += iconMetrics.spacing + iconMetrics.sz_small + iconMetrics.spacing + iconMetrics.spacing; + + if (width < title->boundingRect().width() + iconMetrics.spacing * 2) + width = title->boundingRect().width() + iconMetrics.spacing * 2; + + if (height < iconMetrics.sz_small) + height = iconMetrics.sz_small; + + nextRectangle.setWidth(width); + nextRectangle.setHeight(height); + + if (nextRectangle != rect()) { + if (prefs.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) : QGraphicsRectItem(parent), + title(new QGraphicsSimpleTextItem(tr("Information"), this)), + status(COLLAPSED), + timeAxis(0), + lastTime(-1) +{ + memset(&pInfo, 0, sizeof(pInfo)); + entryToolTip.first = NULL; + entryToolTip.second = NULL; + setFlags(ItemIgnoresTransformations | ItemIsMovable | ItemClipsChildrenToShape); + + QColor c = QColor(Qt::black); + c.setAlpha(155); + setBrush(c); + + setZValue(99); + + addToolTip(QString(), QIcon(), 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)); + 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); + Q_FOREACH (QGraphicsItem *item, oldSelection) { + item->setSelected(true); + } +} + +void ToolTipItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(widget); + painter->save(); + painter->setClipRect(option->rect); + painter->setPen(pen()); + painter->setBrush(brush()); + painter->drawRoundedRect(rect(), 10, 10, Qt::AbsoluteSize); + painter->restore(); +} + +void ToolTipItem::persistPos() +{ + QSettings s; + s.beginGroup("ProfileMap"); + s.setValue("tooltip_position", pos()); + s.endGroup(); +} + +void ToolTipItem::readPos() +{ + QSettings s; + s.beginGroup("ProfileMap"); + QPointF value = s.value("tooltip_position").toPoint(); + if (!scene()->sceneRect().contains(value)) { + value = QPointF(0, 0); + } + setPos(value); +} + +void ToolTipItem::setPlotInfo(const plot_info &plot) +{ + pInfo = plot; +} + +void ToolTipItem::setTimeAxis(DiveCartesianAxis *axis) +{ + timeAxis = axis; +} + +void ToolTipItem::refresh(const QPointF &pos) +{ + struct plot_data *entry; + static QPixmap tissues(16,60); + static QPainter painter(&tissues); + static struct membuffer mb = { 0 }; + + if(refreshTime.elapsed() < 40) + return; + refreshTime.start(); + + int time = timeAxis->valueAt(pos); + if (time == lastTime) + return; + + lastTime = time; + clear(); + + mb.len = 0; + entry = get_plot_details_new(&pInfo, time, &mb); + if (entry) { + 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); + painter.setPen(QColor(0, 0, 0, 255)); + painter.drawLine(0, 60 - entry->gfline / 2, 16, 60 - entry->gfline / 2); + painter.drawLine(0, 60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure / 2, + 16, 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.first->setPixmap(tissues); + entryToolTip.second->setText(QString::fromUtf8(mb.buffer, mb.len)); + } + + Q_FOREACH (QGraphicsItem *item, scene()->items(pos, Qt::IntersectsItemBoundingRect + ,Qt::DescendingOrder, scene()->views().first()->transform())) { + if (!item->toolTip().isEmpty()) + addToolTip(item->toolTip()); + } + expand(); +} + +void ToolTipItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + oldSelection = scene()->selectedItems(); + scene()->clearSelection(); + QGraphicsItem::mousePressEvent(event); +} diff --git a/desktop-widgets/profile/divetooltipitem.h b/desktop-widgets/profile/divetooltipitem.h new file mode 100644 index 000000000..4fa7ec2d7 --- /dev/null +++ b/desktop-widgets/profile/divetooltipitem.h @@ -0,0 +1,67 @@ +#ifndef DIVETOOLTIPITEM_H +#define DIVETOOLTIPITEM_H + +#include <QGraphicsRectItem> +#include <QVector> +#include <QPair> +#include <QRectF> +#include <QIcon> +#include <QTime> +#include "display.h" + +class DiveCartesianAxis; +class QGraphicsLineItem; +class QGraphicsSimpleTextItem; +class QGraphicsPixmapItem; +struct graphics_context; + +/* 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 QGraphicsRectItem { + Q_OBJECT + void updateTitlePosition(); + Q_PROPERTY(QRectF rect READ rect WRITE setRect) + +public: + enum Status { + COLLAPSED, + EXPANDED + }; + + explicit ToolTipItem(QGraphicsItem *parent = 0); + virtual ~ToolTipItem(); + + void collapse(); + void expand(); + void clear(); + void addToolTip(const QString &toolTip, const QIcon &icon = QIcon(), const QPixmap &pixmap = QPixmap()); + void refresh(const QPointF &pos); + bool isExpanded() const; + void persistPos(); + void readPos(); + void mousePressEvent(QGraphicsSceneMouseEvent *event); + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); + void setTimeAxis(DiveCartesianAxis *axis); + void setPlotInfo(const plot_info &plot); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); +public +slots: + void setRect(const QRectF &rect); + +private: + typedef QPair<QGraphicsPixmapItem *, QGraphicsSimpleTextItem *> ToolTip; + QVector<ToolTip> toolTips; + ToolTip entryToolTip; + QGraphicsSimpleTextItem *title; + Status status; + QRectF rectangle; + QRectF nextRectangle; + DiveCartesianAxis *timeAxis; + plot_info pInfo; + int lastTime; + QTime refreshTime; + QList<QGraphicsItem*> oldSelection; +}; + +#endif // DIVETOOLTIPITEM_H diff --git a/desktop-widgets/profile/profilewidget2.cpp b/desktop-widgets/profile/profilewidget2.cpp new file mode 100644 index 000000000..3ccd1bb6d --- /dev/null +++ b/desktop-widgets/profile/profilewidget2.cpp @@ -0,0 +1,1836 @@ +#include "profilewidget2.h" +#include "diveplotdatamodel.h" +#include "helpers.h" +#include "profile.h" +#include "diveeventitem.h" +#include "divetextitem.h" +#include "divetooltipitem.h" +#include "planner.h" +#include "device.h" +#include "ruleritem.h" +#include "tankitem.h" +#include "pref.h" +#include "divepicturewidget.h" +#include "diveplannermodel.h" +#include "models.h" +#include "divepicturemodel.h" +#include "maintab.h" +#include "diveplanner.h" + +#include <libdivecomputer/parser.h> +#include <QScrollBar> +#include <QtCore/qmath.h> +#include <QMessageBox> +#include <QInputDialog> +#include <QDebug> +#include <QWheelEvent> + +#ifndef QT_NO_DEBUG +#include <QTableView> +#endif +#include "mainwindow.h" +#include <preferences.h> + +/* This is the global 'Item position' variable. + * it should tell you where to position things up + * on the canvas. + * + * please, please, please, use this instead of + * hard coding the item on the scene with a random + * value. + */ +static struct _ItemPos { + struct _Pos { + QPointF on; + QPointF off; + }; + struct _Axis { + _Pos pos; + QLineF shrinked; + QLineF expanded; + QLineF intermediate; + }; + _Pos background; + _Pos dcLabel; + _Pos tankBar; + _Axis depth; + _Axis partialPressure; + _Axis partialPressureTissue; + _Axis partialPressureWithTankBar; + _Axis percentage; + _Axis percentageWithTankBar; + _Axis time; + _Axis cylinder; + _Axis temperature; + _Axis temperatureAll; + _Axis heartBeat; + _Axis heartBeatWithTankBar; +} itemPos; + +ProfileWidget2::ProfileWidget2(QWidget *parent) : QGraphicsView(parent), + currentState(INVALID), + dataModel(new DivePlotDataModel(this)), + zoomLevel(0), + zoomFactor(1.15), + background(new DivePixmapItem()), + backgroundFile(":poster"), + toolTipItem(new ToolTipItem()), + isPlotZoomed(prefs.zoomed_plot),// no! bad use of prefs. 'PreferencesDialog::loadSettings' NOT CALLED yet. + profileYAxis(new DepthAxis()), + gasYAxis(new PartialGasPressureAxis()), + temperatureAxis(new TemperatureAxis()), + timeAxis(new TimeAxis()), + diveProfileItem(new DiveProfileItem()), + temperatureItem(new DiveTemperatureItem()), + meanDepthItem(new DiveMeanDepthItem()), + cylinderPressureAxis(new DiveCartesianAxis()), + gasPressureItem(new DiveGasPressureItem()), + diveComputerText(new DiveTextItem()), + diveCeiling(new DiveCalculatedCeiling()), + gradientFactor(new DiveTextItem()), + reportedCeiling(new DiveReportedCeiling()), + pn2GasItem(new PartialPressureGasItem()), + pheGasItem(new PartialPressureGasItem()), + po2GasItem(new PartialPressureGasItem()), + o2SetpointGasItem(new PartialPressureGasItem()), + ccrsensor1GasItem(new PartialPressureGasItem()), + ccrsensor2GasItem(new PartialPressureGasItem()), + ccrsensor3GasItem(new PartialPressureGasItem()), + heartBeatAxis(new DiveCartesianAxis()), + heartBeatItem(new DiveHeartrateItem()), + percentageAxis(new DiveCartesianAxis()), + ambPressureItem(new DiveAmbPressureItem()), + gflineItem(new DiveGFLineItem()), + mouseFollowerVertical(new DiveLineItem()), + mouseFollowerHorizontal(new DiveLineItem()), + rulerItem(new RulerItem2()), + tankItem(new TankItem()), + isGrayscale(false), + printMode(false), + shouldCalculateMaxTime(true), + shouldCalculateMaxDepth(true), + fontPrintScale(1.0) +{ + // would like to be able to ASSERT here that PreferencesDialog::loadSettings has been called. + isPlotZoomed = prefs.zoomed_plot; // now it seems that 'prefs' has loaded our preferences + + memset(&plotInfo, 0, sizeof(plotInfo)); + + setupSceneAndFlags(); + setupItemSizes(); + setupItemOnScene(); + addItemsToScene(); + scene()->installEventFilter(this); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + QAction *action = NULL; +#define ADD_ACTION(SHORTCUT, Slot) \ + action = new QAction(this); \ + action->setShortcut(SHORTCUT); \ + action->setShortcutContext(Qt::WindowShortcut); \ + addAction(action); \ + connect(action, SIGNAL(triggered(bool)), this, SLOT(Slot)); \ + actionsForKeys[SHORTCUT] = action; + + ADD_ACTION(Qt::Key_Escape, keyEscAction()); + ADD_ACTION(Qt::Key_Delete, keyDeleteAction()); + ADD_ACTION(Qt::Key_Up, keyUpAction()); + ADD_ACTION(Qt::Key_Down, keyDownAction()); + ADD_ACTION(Qt::Key_Left, keyLeftAction()); + ADD_ACTION(Qt::Key_Right, keyRightAction()); +#undef ADD_ACTION + +#if !defined(QT_NO_DEBUG) && defined(SHOW_PLOT_INFO_TABLE) + QTableView *diveDepthTableView = new QTableView(); + diveDepthTableView->setModel(dataModel); + diveDepthTableView->show(); +#endif +} + + +ProfileWidget2::~ProfileWidget2() +{ + delete background; + delete toolTipItem; + delete profileYAxis; + delete gasYAxis; + delete temperatureAxis; + delete timeAxis; + delete diveProfileItem; + delete temperatureItem; + delete meanDepthItem; + delete cylinderPressureAxis; + delete gasPressureItem; + delete diveComputerText; + delete diveCeiling; + delete reportedCeiling; + delete pn2GasItem; + delete pheGasItem; + delete po2GasItem; + delete o2SetpointGasItem; + delete ccrsensor1GasItem; + delete ccrsensor2GasItem; + delete ccrsensor3GasItem; + delete heartBeatAxis; + delete heartBeatItem; + delete percentageAxis; + delete ambPressureItem; + delete gflineItem; + delete mouseFollowerVertical; + delete mouseFollowerHorizontal; + delete rulerItem; + delete tankItem; +} + +#define SUBSURFACE_OBJ_DATA 1 +#define SUBSURFACE_OBJ_DC_TEXT 0x42 + +void ProfileWidget2::addItemsToScene() +{ + scene()->addItem(background); + scene()->addItem(toolTipItem); + scene()->addItem(profileYAxis); + scene()->addItem(gasYAxis); + scene()->addItem(temperatureAxis); + scene()->addItem(timeAxis); + scene()->addItem(diveProfileItem); + scene()->addItem(cylinderPressureAxis); + scene()->addItem(temperatureItem); + scene()->addItem(meanDepthItem); + scene()->addItem(gasPressureItem); + // I cannot seem to figure out if an object that I find with itemAt() on the scene + // is the object I am looking for - my guess is there's a simple way in Qt to do that + // but nothing I tried worked. + // so instead this adds a special magic key/value pair to the object to mark it + diveComputerText->setData(SUBSURFACE_OBJ_DATA, SUBSURFACE_OBJ_DC_TEXT); + scene()->addItem(diveComputerText); + scene()->addItem(diveCeiling); + scene()->addItem(gradientFactor); + scene()->addItem(reportedCeiling); + scene()->addItem(pn2GasItem); + scene()->addItem(pheGasItem); + scene()->addItem(po2GasItem); + scene()->addItem(o2SetpointGasItem); + scene()->addItem(ccrsensor1GasItem); + scene()->addItem(ccrsensor2GasItem); + scene()->addItem(ccrsensor3GasItem); + scene()->addItem(percentageAxis); + scene()->addItem(heartBeatAxis); + scene()->addItem(heartBeatItem); + scene()->addItem(rulerItem); + scene()->addItem(rulerItem->sourceNode()); + scene()->addItem(rulerItem->destNode()); + scene()->addItem(tankItem); + scene()->addItem(mouseFollowerHorizontal); + scene()->addItem(mouseFollowerVertical); + QPen pen(QColor(Qt::red).lighter()); + pen.setWidth(0); + mouseFollowerHorizontal->setPen(pen); + mouseFollowerVertical->setPen(pen); + Q_FOREACH (DiveCalculatedTissue *tissue, allTissues) { + scene()->addItem(tissue); + } + Q_FOREACH (DivePercentageItem *percentage, allPercentages) { + scene()->addItem(percentage); + } + scene()->addItem(ambPressureItem); + scene()->addItem(gflineItem); +} + +void ProfileWidget2::setupItemOnScene() +{ + background->setZValue(9999); + toolTipItem->setZValue(9998); + toolTipItem->setTimeAxis(timeAxis); + rulerItem->setZValue(9997); + tankItem->setZValue(100); + + profileYAxis->setOrientation(DiveCartesianAxis::TopToBottom); + profileYAxis->setMinimum(0); + profileYAxis->setTickInterval(M_OR_FT(10, 30)); + profileYAxis->setTickSize(0.5); + profileYAxis->setLineSize(96); + + timeAxis->setLineSize(92); + timeAxis->setTickSize(-0.5); + + gasYAxis->setOrientation(DiveCartesianAxis::BottomToTop); + gasYAxis->setTickInterval(1); + gasYAxis->setTickSize(1); + gasYAxis->setMinimum(0); + gasYAxis->setModel(dataModel); + gasYAxis->setFontLabelScale(0.7); + gasYAxis->setLineSize(96); + + heartBeatAxis->setOrientation(DiveCartesianAxis::BottomToTop); + heartBeatAxis->setTickSize(0.2); + heartBeatAxis->setTickInterval(10); + heartBeatAxis->setFontLabelScale(0.7); + heartBeatAxis->setLineSize(96); + + percentageAxis->setOrientation(DiveCartesianAxis::BottomToTop); + percentageAxis->setTickSize(0.2); + percentageAxis->setTickInterval(10); + percentageAxis->setFontLabelScale(0.7); + percentageAxis->setLineSize(96); + + temperatureAxis->setOrientation(DiveCartesianAxis::BottomToTop); + temperatureAxis->setTickSize(2); + temperatureAxis->setTickInterval(300); + + cylinderPressureAxis->setOrientation(DiveCartesianAxis::BottomToTop); + cylinderPressureAxis->setTickSize(2); + cylinderPressureAxis->setTickInterval(30000); + + + diveComputerText->setAlignment(Qt::AlignRight | Qt::AlignTop); + diveComputerText->setBrush(getColor(TIME_TEXT, isGrayscale)); + + rulerItem->setAxis(timeAxis, profileYAxis); + tankItem->setHorizontalAxis(timeAxis); + + // show the gradient factor at the top in the center + gradientFactor->setY(0); + gradientFactor->setX(50); + gradientFactor->setBrush(getColor(PRESSURE_TEXT)); + gradientFactor->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + + setupItem(reportedCeiling, timeAxis, profileYAxis, dataModel, DivePlotDataModel::CEILING, DivePlotDataModel::TIME, 1); + setupItem(diveCeiling, timeAxis, profileYAxis, dataModel, DivePlotDataModel::CEILING, DivePlotDataModel::TIME, 1); + for (int i = 0; i < 16; i++) { + DiveCalculatedTissue *tissueItem = new DiveCalculatedTissue(); + setupItem(tissueItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::TISSUE_1 + i, DivePlotDataModel::TIME, 1 + i); + allTissues.append(tissueItem); + DivePercentageItem *percentageItem = new DivePercentageItem(i); + setupItem(percentageItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::PERCENTAGE_1 + i, DivePlotDataModel::TIME, 1 + i); + allPercentages.append(percentageItem); + } + setupItem(gasPressureItem, timeAxis, cylinderPressureAxis, dataModel, DivePlotDataModel::TEMPERATURE, DivePlotDataModel::TIME, 1); + setupItem(temperatureItem, timeAxis, temperatureAxis, dataModel, DivePlotDataModel::TEMPERATURE, DivePlotDataModel::TIME, 1); + setupItem(heartBeatItem, timeAxis, heartBeatAxis, dataModel, DivePlotDataModel::HEARTBEAT, DivePlotDataModel::TIME, 1); + setupItem(ambPressureItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::AMBPRESSURE, DivePlotDataModel::TIME, 1); + setupItem(gflineItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::GFLINE, DivePlotDataModel::TIME, 1); + setupItem(diveProfileItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::DEPTH, DivePlotDataModel::TIME, 0); + setupItem(meanDepthItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::INSTANT_MEANDEPTH, DivePlotDataModel::TIME, 1); + + +#define CREATE_PP_GAS(ITEM, VERTICAL_COLUMN, COLOR, COLOR_ALERT, THRESHOULD_SETTINGS, VISIBILITY_SETTINGS) \ + setupItem(ITEM, timeAxis, gasYAxis, dataModel, DivePlotDataModel::VERTICAL_COLUMN, DivePlotDataModel::TIME, 0); \ + ITEM->setThreshouldSettingsKey(THRESHOULD_SETTINGS); \ + ITEM->setVisibilitySettingsKey(VISIBILITY_SETTINGS); \ + ITEM->setColors(getColor(COLOR, isGrayscale), getColor(COLOR_ALERT, isGrayscale)); \ + ITEM->settingsChanged(); \ + ITEM->setZValue(99); + + CREATE_PP_GAS(pn2GasItem, PN2, PN2, PN2_ALERT, &prefs.pp_graphs.pn2_threshold, "pn2graph"); + CREATE_PP_GAS(pheGasItem, PHE, PHE, PHE_ALERT, &prefs.pp_graphs.phe_threshold, "phegraph"); + CREATE_PP_GAS(po2GasItem, PO2, PO2, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "po2graph"); + CREATE_PP_GAS(o2SetpointGasItem, O2SETPOINT, PO2_ALERT, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "po2graph"); + CREATE_PP_GAS(ccrsensor1GasItem, CCRSENSOR1, CCRSENSOR1, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); + CREATE_PP_GAS(ccrsensor2GasItem, CCRSENSOR2, CCRSENSOR2, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); + CREATE_PP_GAS(ccrsensor3GasItem, CCRSENSOR3, CCRSENSOR3, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); +#undef CREATE_PP_GAS + + temperatureAxis->setTextVisible(false); + temperatureAxis->setLinesVisible(false); + cylinderPressureAxis->setTextVisible(false); + cylinderPressureAxis->setLinesVisible(false); + timeAxis->setLinesVisible(true); + profileYAxis->setLinesVisible(true); + gasYAxis->setZValue(timeAxis->zValue() + 1); + heartBeatAxis->setTextVisible(true); + heartBeatAxis->setLinesVisible(true); + percentageAxis->setTextVisible(true); + percentageAxis->setLinesVisible(true); + + replotEnabled = true; +} + +void ProfileWidget2::replot(struct dive *d) +{ + if (!replotEnabled) + return; + dataModel->clear(); + plotDive(d, true); +} + +void ProfileWidget2::setupItemSizes() +{ + // Scene is *always* (double) 100 / 100. + // Background Config + /* Much probably a better math is needed here. + * good thing is that we only need to change the + * Axis and everything else is auto-adjusted.* + */ + + itemPos.background.on.setX(0); + itemPos.background.on.setY(0); + itemPos.background.off.setX(0); + itemPos.background.off.setY(110); + + //Depth Axis Config + itemPos.depth.pos.on.setX(3); + itemPos.depth.pos.on.setY(3); + itemPos.depth.pos.off.setX(-2); + itemPos.depth.pos.off.setY(3); + itemPos.depth.expanded.setP1(QPointF(0, 0)); + itemPos.depth.expanded.setP2(QPointF(0, 85)); + itemPos.depth.shrinked.setP1(QPointF(0, 0)); + itemPos.depth.shrinked.setP2(QPointF(0, 55)); + itemPos.depth.intermediate.setP1(QPointF(0, 0)); + itemPos.depth.intermediate.setP2(QPointF(0, 65)); + + // Time Axis Config + itemPos.time.pos.on.setX(3); + itemPos.time.pos.on.setY(95); + itemPos.time.pos.off.setX(3); + itemPos.time.pos.off.setY(110); + itemPos.time.expanded.setP1(QPointF(0, 0)); + itemPos.time.expanded.setP2(QPointF(94, 0)); + + // Partial Gas Axis Config + itemPos.partialPressure.pos.on.setX(97); + itemPos.partialPressure.pos.on.setY(75); + itemPos.partialPressure.pos.off.setX(110); + itemPos.partialPressure.pos.off.setY(63); + itemPos.partialPressure.expanded.setP1(QPointF(0, 0)); + itemPos.partialPressure.expanded.setP2(QPointF(0, 19)); + itemPos.partialPressureWithTankBar = itemPos.partialPressure; + itemPos.partialPressureWithTankBar.expanded.setP2(QPointF(0, 17)); + itemPos.partialPressureTissue = itemPos.partialPressure; + itemPos.partialPressureTissue.pos.on.setX(97); + itemPos.partialPressureTissue.pos.on.setY(65); + itemPos.partialPressureTissue.expanded.setP2(QPointF(0, 16)); + + // cylinder axis config + itemPos.cylinder.pos.on.setX(3); + itemPos.cylinder.pos.on.setY(20); + itemPos.cylinder.pos.off.setX(-10); + itemPos.cylinder.pos.off.setY(20); + itemPos.cylinder.expanded.setP1(QPointF(0, 15)); + itemPos.cylinder.expanded.setP2(QPointF(0, 50)); + itemPos.cylinder.shrinked.setP1(QPointF(0, 0)); + itemPos.cylinder.shrinked.setP2(QPointF(0, 20)); + itemPos.cylinder.intermediate.setP1(QPointF(0, 0)); + itemPos.cylinder.intermediate.setP2(QPointF(0, 20)); + + // Temperature axis config + itemPos.temperature.pos.on.setX(3); + itemPos.temperature.pos.on.setY(60); + itemPos.temperatureAll.pos.on.setY(51); + itemPos.temperature.pos.off.setX(-10); + itemPos.temperature.pos.off.setY(40); + itemPos.temperature.expanded.setP1(QPointF(0, 20)); + itemPos.temperature.expanded.setP2(QPointF(0, 33)); + itemPos.temperature.shrinked.setP1(QPointF(0, 2)); + itemPos.temperature.shrinked.setP2(QPointF(0, 12)); + itemPos.temperature.intermediate.setP1(QPointF(0, 2)); + itemPos.temperature.intermediate.setP2(QPointF(0, 12)); + + // Heartbeat axis config + itemPos.heartBeat.pos.on.setX(3); + itemPos.heartBeat.pos.on.setY(82); + itemPos.heartBeat.expanded.setP1(QPointF(0, 0)); + itemPos.heartBeat.expanded.setP2(QPointF(0, 10)); + itemPos.heartBeatWithTankBar = itemPos.heartBeat; + itemPos.heartBeatWithTankBar.expanded.setP2(QPointF(0, 7)); + + // Percentage axis config + itemPos.percentage.pos.on.setX(3); + itemPos.percentage.pos.on.setY(80); + itemPos.percentage.expanded.setP1(QPointF(0, 0)); + itemPos.percentage.expanded.setP2(QPointF(0, 15)); + itemPos.percentageWithTankBar = itemPos.percentage; + itemPos.percentageWithTankBar.expanded.setP2(QPointF(0, 12)); + + itemPos.dcLabel.on.setX(3); + itemPos.dcLabel.on.setY(100); + itemPos.dcLabel.off.setX(-10); + itemPos.dcLabel.off.setY(100); + + itemPos.tankBar.on.setX(0); + itemPos.tankBar.on.setY(91.5); +} + +void ProfileWidget2::setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *hAxis, + DiveCartesianAxis *vAxis, DivePlotDataModel *model, + int vData, int hData, int zValue) +{ + item->setHorizontalAxis(hAxis); + item->setVerticalAxis(vAxis); + item->setModel(model); + item->setVerticalDataColumn(vData); + item->setHorizontalDataColumn(hData); + item->setZValue(zValue); +} + +void ProfileWidget2::setupSceneAndFlags() +{ + setScene(new QGraphicsScene(this)); + scene()->setSceneRect(0, 0, 100, 100); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scene()->setItemIndexMethod(QGraphicsScene::NoIndex); + setOptimizationFlags(QGraphicsView::DontSavePainterState); + setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); + setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); + setMouseTracking(true); + background->setFlag(QGraphicsItem::ItemIgnoresTransformations); +} + +void ProfileWidget2::resetZoom() +{ + if (!zoomLevel) + return; + const qreal defScale = 1.0 / qPow(zoomFactor, (qreal)zoomLevel); + scale(defScale, defScale); + zoomLevel = 0; +} + +// Currently just one dive, but the plan is to enable All of the selected dives. +void ProfileWidget2::plotDive(struct dive *d, bool force) +{ + static bool firstCall = true; + QTime measureDuration; // let's measure how long this takes us (maybe we'll turn of TTL calculation later + measureDuration.start(); + + if (currentState != ADD && currentState != PLAN) { + if (!d) { + if (selected_dive == -1) + return; + d = current_dive; // display the current dive + } + + // No need to do this again if we are already showing the same dive + // computer of the same dive, so we check the unique id of the dive + // and the selected dive computer number against the ones we are + // showing (can't compare the dive pointers as those might change). + if (d->id == displayed_dive.id && dc_number == dataModel->dcShown() && !force) + return; + + // this copies the dive and makes copies of all the relevant additional data + copy_dive(d, &displayed_dive); + gradientFactor->setText(QString("GF %1/%2").arg(prefs.gflow).arg(prefs.gfhigh)); + } else { + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + plannerModel->createTemporaryPlan(); + struct diveplan &diveplan = plannerModel->getDiveplan(); + if (!diveplan.dp) { + plannerModel->deleteTemporaryPlan(); + return; + } + gradientFactor->setText(QString("GF %1/%2").arg(diveplan.gflow).arg(diveplan.gfhigh)); + } + + // special handling for the first time we display things + int animSpeedBackup = 0; + if (firstCall && MainWindow::instance()->filesFromCommandLine()) { + animSpeedBackup = prefs.animation_speed; + prefs.animation_speed = 0; + firstCall = false; + } + + // restore default zoom level + resetZoom(); + + // reset some item visibility on printMode changes + toolTipItem->setVisible(!printMode); + rulerItem->setVisible(prefs.rulergraph && !printMode && currentState != PLAN && currentState != ADD); + + if (currentState == EMPTY) + setProfileState(); + + // next get the dive computer structure - if there are no samples + // let's create a fake profile that's somewhat reasonable for the + // data that we have + struct divecomputer *currentdc = select_dc(&displayed_dive); + Q_ASSERT(currentdc); + if (!currentdc || !currentdc->samples) { + currentdc = fake_dc(currentdc); + } + + bool setpointflag = (currentdc->divemode == CCR) && prefs.pp_graphs.po2 && current_dive; + bool sensorflag = setpointflag && prefs.show_ccr_sensors; + o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint); + ccrsensor1GasItem->setVisible(sensorflag); + ccrsensor2GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 1)); + ccrsensor3GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 2)); + + /* This struct holds all the data that's about to be plotted. + * I'm not sure this is the best approach ( but since we are + * interpolating some points of the Dive, maybe it is... ) + * The Calculation of the points should be done per graph, + * so I'll *not* calculate everything if something is not being + * shown. + */ + plotInfo = calculate_max_limits_new(&displayed_dive, currentdc); + create_plot_info_new(&displayed_dive, currentdc, &plotInfo, !shouldCalculateMaxDepth); + if (shouldCalculateMaxTime) + maxtime = get_maxtime(&plotInfo); + + /* Only update the max depth if it's bigger than the current ones + * when we are dragging the handler to plan / add dive. + * otherwhise, update normally. + */ + int newMaxDepth = get_maxdepth(&plotInfo); + if (!shouldCalculateMaxDepth) { + if (maxdepth < newMaxDepth) { + maxdepth = newMaxDepth; + } + } else { + maxdepth = newMaxDepth; + } + + dataModel->setDive(&displayed_dive, plotInfo); + toolTipItem->setPlotInfo(plotInfo); + + // It seems that I'll have a lot of boilerplate setting the model / axis for + // each item, I'll mostly like to fix this in the future, but I'll keep at this for now. + profileYAxis->setMaximum(maxdepth); + profileYAxis->updateTicks(); + + temperatureAxis->setMinimum(plotInfo.mintemp); + temperatureAxis->setMaximum(plotInfo.maxtemp - plotInfo.mintemp > 2000 ? plotInfo.maxtemp : plotInfo.mintemp + 2000); + + if (plotInfo.maxhr) { + heartBeatAxis->setMinimum(plotInfo.minhr); + heartBeatAxis->setMaximum(plotInfo.maxhr); + heartBeatAxis->updateTicks(HR_AXIS); // this shows the ticks + } + heartBeatAxis->setVisible(prefs.hrgraph && plotInfo.maxhr); + + percentageAxis->setMinimum(0); + percentageAxis->setMaximum(100); + percentageAxis->setVisible(false); + percentageAxis->updateTicks(HR_AXIS); + + timeAxis->setMaximum(maxtime); + int i, incr; + static int increments[8] = { 10, 20, 30, 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60 }; + /* Time markers: at most every 10 seconds, but no more than 12 markers. + * We start out with 10 seconds and increment up to 30 minutes, + * depending on the dive time. + * This allows for 6h dives - enough (I hope) for even the craziest + * divers - but just in case, for those 8h depth-record-breaking dives, + * we double the interval if this still doesn't get us to 12 or fewer + * time markers */ + i = 0; + while (i < 7 && maxtime / increments[i] > 12) + i++; + incr = increments[i]; + while (maxtime / incr > 12) + incr *= 2; + timeAxis->setTickInterval(incr); + timeAxis->updateTicks(); + cylinderPressureAxis->setMinimum(plotInfo.minpressure); + cylinderPressureAxis->setMaximum(plotInfo.maxpressure); + + rulerItem->setPlotInfo(plotInfo); + tankItem->setData(dataModel, &plotInfo, &displayed_dive); + + dataModel->emitDataChanged(); + // 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(); + struct event *event = currentdc->events; + while (event) { + // if print mode is selected only draw headings, SP change, gas events or bookmark event + if (printMode) { + if (same_string(event->name, "") || + !(strcmp(event->name, "heading") == 0 || + (same_string(event->name, "SP change") && event->time.seconds == 0) || + event_is_gaschange(event) || + event->type == SAMPLE_EVENT_BOOKMARK)) { + event = event->next; + continue; + } + } + DiveEventItem *item = new DiveEventItem(); + item->setHorizontalAxis(timeAxis); + item->setVerticalAxis(profileYAxis); + item->setModel(dataModel); + item->setEvent(event); + item->setZValue(2); + scene()->addItem(item); + eventItems.push_back(item); + event = event->next; + } + // Only set visible the events that should be visible + Q_FOREACH (DiveEventItem *event, eventItems) { + event->setVisible(!event->shouldBeHidden()); + } + QString dcText = get_dc_nickname(currentdc->model, currentdc->deviceid); + int nr; + if ((nr = number_of_computers(&displayed_dive)) > 1) + dcText += tr(" (#%1 of %2)").arg(dc_number + 1).arg(nr); + if (dcText.isEmpty()) + dcText = tr("Unknown dive computer"); + diveComputerText->setText(dcText); + if (MainWindow::instance()->filesFromCommandLine() && animSpeedBackup != 0) { + prefs.animation_speed = animSpeedBackup; + } + + if (currentState == ADD || currentState == PLAN) { // TODO: figure a way to move this from here. + repositionDiveHandlers(); + DivePlannerPointsModel *model = DivePlannerPointsModel::instance(); + model->deleteTemporaryPlan(); + } + plotPictures(); + + // OK, how long did this take us? Anything above the second is way too long, + // so if we are calculation TTS / NDL then let's force that off. + if (measureDuration.elapsed() > 1000 && prefs.calcndltts) { + MainWindow::instance()->turnOffNdlTts(); + MainWindow::instance()->getNotificationWidget()->showNotification(tr("Show NDL / TTS was disabled because of excessive processing time"), KMessageWidget::Error); + } + MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + +} + +void ProfileWidget2::recalcCeiling() +{ + diveCeiling->recalc(); +} + +void ProfileWidget2::settingsChanged() +{ + // if we are showing calculated ceilings then we have to replot() + // because the GF could have changed; otherwise we try to avoid replot() + bool needReplot = prefs.calcceiling; + if ((prefs.percentagegraph||prefs.hrgraph) && PP_GRAPHS_ENABLED) { + profileYAxis->animateChangeLine(itemPos.depth.shrinked); + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.shrinked); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked); + + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + }else { + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->animateChangeLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + gasYAxis->setPos(itemPos.partialPressureTissue.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded); + + } else if (PP_GRAPHS_ENABLED || prefs.hrgraph || prefs.percentagegraph) { + profileYAxis->animateChangeLine(itemPos.depth.intermediate); + temperatureAxis->setPos(itemPos.temperature.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.intermediate); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate); + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + gasYAxis->setPos(itemPos.partialPressureWithTankBar.pos.on); + gasYAxis->setLine(itemPos.partialPressureWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + } else { + gasYAxis->setPos(itemPos.partialPressure.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressure.expanded); + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->setLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + } else { + profileYAxis->animateChangeLine(itemPos.depth.expanded); + if (prefs.tankbar) { + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + } else { + temperatureAxis->setPos(itemPos.temperature.pos.on); + } + temperatureAxis->animateChangeLine(itemPos.temperature.expanded); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded); + } + + tankItem->setVisible(prefs.tankbar); + if (prefs.zoomed_plot != isPlotZoomed) { + isPlotZoomed = prefs.zoomed_plot; + needReplot = true; + } + if (needReplot) + replot(); +} + +void ProfileWidget2::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + fitInView(sceneRect(), Qt::IgnoreAspectRatio); + fixBackgroundPos(); +} + +void ProfileWidget2::mousePressEvent(QMouseEvent *event) +{ + if (zoomLevel) + return; + QGraphicsView::mousePressEvent(event); + if (currentState == PLAN) + shouldCalculateMaxTime = false; +} + +void ProfileWidget2::divePlannerHandlerClicked() +{ + if (zoomLevel) + return; + shouldCalculateMaxDepth = false; + replot(); +} + +void ProfileWidget2::divePlannerHandlerReleased() +{ + if (zoomLevel) + return; + shouldCalculateMaxDepth = true; + replot(); +} + +void ProfileWidget2::mouseReleaseEvent(QMouseEvent *event) +{ + if (zoomLevel) + return; + QGraphicsView::mouseReleaseEvent(event); + if (currentState == PLAN) { + shouldCalculateMaxTime = true; + replot(); + } +} + +void ProfileWidget2::fixBackgroundPos() +{ + static QPixmap toBeScaled(backgroundFile); + if (currentState != EMPTY) + return; + QPixmap p = toBeScaled.scaledToHeight(viewport()->height() - 40, Qt::SmoothTransformation); + int x = viewport()->width() / 2 - p.width() / 2; + int y = viewport()->height() / 2 - p.height() / 2; + background->setPixmap(p); + background->setX(mapToScene(x, 0).x()); + background->setY(mapToScene(y, 20).y()); +} + +void ProfileWidget2::wheelEvent(QWheelEvent *event) +{ + if (currentState == EMPTY) + return; + QPoint toolTipPos = mapFromScene(toolTipItem->pos()); + if (event->buttons() == Qt::LeftButton) + return; + if (event->delta() > 0 && zoomLevel < 20) { + scale(zoomFactor, zoomFactor); + zoomLevel++; + } else if (event->delta() < 0 && zoomLevel > 0) { + // Zooming out + scale(1.0 / zoomFactor, 1.0 / zoomFactor); + zoomLevel--; + } + scrollViewTo(event->pos()); + toolTipItem->setPos(mapToScene(toolTipPos)); +} + +void ProfileWidget2::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (currentState == PLAN || currentState == ADD) { + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + QPointF mappedPos = mapToScene(event->pos()); + if (isPointOutOfBoundaries(mappedPos)) + return; + + int minutes = rint(timeAxis->valueAt(mappedPos) / 60); + int milimeters = rint(profileYAxis->valueAt(mappedPos) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); + plannerModel->addStop(milimeters, minutes * 60, 0, 0, true); + } +} + +bool ProfileWidget2::isPointOutOfBoundaries(const QPointF &point) const +{ + double xpos = timeAxis->valueAt(point); + double ypos = profileYAxis->valueAt(point); + return (xpos > timeAxis->maximum() || + xpos < timeAxis->minimum() || + ypos > profileYAxis->maximum() || + ypos < profileYAxis->minimum()); +} + +void ProfileWidget2::scrollViewTo(const QPoint &pos) +{ + /* since we cannot use translate() directly on the scene we hack on + * the scroll bars (hidden) functionality */ + if (!zoomLevel || currentState == EMPTY) + return; + QScrollBar *vs = verticalScrollBar(); + QScrollBar *hs = horizontalScrollBar(); + const qreal yRat = (qreal)pos.y() / viewport()->height(); + const qreal xRat = (qreal)pos.x() / viewport()->width(); + vs->setValue(yRat * vs->maximum()); + hs->setValue(xRat * hs->maximum()); +} + +void ProfileWidget2::mouseMoveEvent(QMouseEvent *event) +{ + QPointF pos = mapToScene(event->pos()); + toolTipItem->refresh(pos); + if (zoomLevel == 0) { + QGraphicsView::mouseMoveEvent(event); + } else { + QPoint toolTipPos = mapFromScene(toolTipItem->pos()); + scrollViewTo(event->pos()); + toolTipItem->setPos(mapToScene(toolTipPos)); + } + + qreal vValue = profileYAxis->valueAt(pos); + qreal hValue = timeAxis->valueAt(pos); + if (profileYAxis->maximum() >= vValue && profileYAxis->minimum() <= vValue) { + mouseFollowerHorizontal->setPos(timeAxis->pos().x(), pos.y()); + } + if (timeAxis->maximum() >= hValue && timeAxis->minimum() <= hValue) { + mouseFollowerVertical->setPos(pos.x(), profileYAxis->line().y1()); + } +} + +bool ProfileWidget2::eventFilter(QObject *object, QEvent *event) +{ + QGraphicsScene *s = qobject_cast<QGraphicsScene *>(object); + if (s && event->type() == QEvent::GraphicsSceneHelp) { + event->ignore(); + return true; + } + return QGraphicsView::eventFilter(object, event); +} + +void ProfileWidget2::setEmptyState() +{ + // Then starting Empty State, move the background up. + if (currentState == EMPTY) + return; + + disconnectTemporaryConnections(); + setBackgroundBrush(getColor(::BACKGROUND, isGrayscale)); + dataModel->clear(); + currentState = EMPTY; + MainWindow::instance()->setEnabledToolbar(false); + + fixBackgroundPos(); + background->setVisible(true); + + profileYAxis->setVisible(false); + gasYAxis->setVisible(false); + timeAxis->setVisible(false); + temperatureAxis->setVisible(false); + cylinderPressureAxis->setVisible(false); + toolTipItem->setVisible(false); + diveComputerText->setVisible(false); + diveCeiling->setVisible(false); + gradientFactor->setVisible(false); + reportedCeiling->setVisible(false); + rulerItem->setVisible(false); + tankItem->setVisible(false); + pn2GasItem->setVisible(false); + po2GasItem->setVisible(false); + o2SetpointGasItem->setVisible(false); + ccrsensor1GasItem->setVisible(false); + ccrsensor2GasItem->setVisible(false); + ccrsensor3GasItem->setVisible(false); + pheGasItem->setVisible(false); + ambPressureItem->setVisible(false); + gflineItem->setVisible(false); + mouseFollowerHorizontal->setVisible(false); + mouseFollowerVertical->setVisible(false); + +#define HIDE_ALL(TYPE, CONTAINER) \ + Q_FOREACH (TYPE *item, CONTAINER) item->setVisible(false); + HIDE_ALL(DiveCalculatedTissue, allTissues); + HIDE_ALL(DivePercentageItem, allPercentages); + HIDE_ALL(DiveEventItem, eventItems); + HIDE_ALL(DiveHandler, handles); + HIDE_ALL(QGraphicsSimpleTextItem, gases); +#undef HIDE_ALL +} + +void ProfileWidget2::setProfileState() +{ + // Then starting Empty State, move the background up. + if (currentState == PROFILE) + return; + + disconnectTemporaryConnections(); + connect(DivePictureModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(plotPictures())); + connect(DivePictureModel::instance(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(plotPictures())); + connect(DivePictureModel::instance(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(plotPictures())); + /* show the same stuff that the profile shows. */ + + //TODO: Move the DC handling to another method. + MainWindow::instance()->enableShortcuts(); + + currentState = PROFILE; + MainWindow::instance()->setEnabledToolbar(true); + toolTipItem->readPos(); + setBackgroundBrush(getColor(::BACKGROUND, isGrayscale)); + + background->setVisible(false); + toolTipItem->setVisible(true); + profileYAxis->setVisible(true); + gasYAxis->setVisible(true); + timeAxis->setVisible(true); + temperatureAxis->setVisible(true); + cylinderPressureAxis->setVisible(true); + + profileYAxis->setPos(itemPos.depth.pos.on); + if ((prefs.percentagegraph||prefs.hrgraph) && PP_GRAPHS_ENABLED) { + profileYAxis->animateChangeLine(itemPos.depth.shrinked); + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.shrinked); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked); + + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + }else { + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->animateChangeLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + gasYAxis->setPos(itemPos.partialPressureTissue.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded); + + } else if (PP_GRAPHS_ENABLED || prefs.hrgraph || prefs.percentagegraph) { + profileYAxis->animateChangeLine(itemPos.depth.intermediate); + temperatureAxis->setPos(itemPos.temperature.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.intermediate); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate); + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + gasYAxis->setPos(itemPos.partialPressureWithTankBar.pos.on); + gasYAxis->setLine(itemPos.partialPressureWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + } else { + gasYAxis->setPos(itemPos.partialPressure.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressure.expanded); + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->setLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + } else { + profileYAxis->animateChangeLine(itemPos.depth.expanded); + if (prefs.tankbar) { + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + } else { + temperatureAxis->setPos(itemPos.temperature.pos.on); + } + temperatureAxis->animateChangeLine(itemPos.temperature.expanded); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded); + } + pn2GasItem->setVisible(prefs.pp_graphs.pn2); + po2GasItem->setVisible(prefs.pp_graphs.po2); + pheGasItem->setVisible(prefs.pp_graphs.phe); + + bool setpointflag = current_dive && (current_dc->divemode == CCR) && prefs.pp_graphs.po2; + bool sensorflag = setpointflag && prefs.show_ccr_sensors; + o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint); + ccrsensor1GasItem->setVisible(sensorflag); + ccrsensor2GasItem->setVisible(sensorflag && (current_dc->no_o2sensors > 1)); + ccrsensor3GasItem->setVisible(sensorflag && (current_dc->no_o2sensors > 2)); + + timeAxis->setPos(itemPos.time.pos.on); + timeAxis->setLine(itemPos.time.expanded); + + cylinderPressureAxis->setPos(itemPos.cylinder.pos.on); + heartBeatItem->setVisible(prefs.hrgraph); + meanDepthItem->setVisible(prefs.show_average_depth); + + diveComputerText->setVisible(true); + diveComputerText->setPos(itemPos.dcLabel.on); + + diveCeiling->setVisible(prefs.calcceiling); + gradientFactor->setVisible(prefs.calcceiling); + reportedCeiling->setVisible(prefs.dcceiling); + + if (prefs.calcalltissues) { + Q_FOREACH (DiveCalculatedTissue *tissue, allTissues) { + tissue->setVisible(true); + } + } + + if (prefs.percentagegraph) { + Q_FOREACH (DivePercentageItem *percentage, allPercentages) { + percentage->setVisible(true); + } + + ambPressureItem->setVisible(true); + gflineItem->setVisible(true); + } + + rulerItem->setVisible(prefs.rulergraph); + tankItem->setVisible(prefs.tankbar); + tankItem->setPos(itemPos.tankBar.on); + +#define HIDE_ALL(TYPE, CONTAINER) \ + Q_FOREACH (TYPE *item, CONTAINER) item->setVisible(false); + HIDE_ALL(DiveHandler, handles); + HIDE_ALL(QGraphicsSimpleTextItem, gases); +#undef HIDE_ALL + mouseFollowerHorizontal->setVisible(false); + mouseFollowerVertical->setVisible(false); +} + +void ProfileWidget2::clearHandlers() +{ + if (handles.count()) { + foreach (DiveHandler *handle, handles) { + scene()->removeItem(handle); + delete handle; + } + handles.clear(); + } +} + +void ProfileWidget2::setToolTipVisibile(bool visible) +{ + toolTipItem->setVisible(visible); +} + +void ProfileWidget2::setAddState() +{ + if (currentState == ADD) + return; + + clearHandlers(); + setProfileState(); + mouseFollowerHorizontal->setVisible(true); + mouseFollowerVertical->setVisible(true); + mouseFollowerHorizontal->setLine(timeAxis->line()); + mouseFollowerVertical->setLine(QLineF(0, profileYAxis->pos().y(), 0, timeAxis->pos().y())); + disconnectTemporaryConnections(); + //TODO: Move this method to another place, shouldn't be on mainwindow. + MainWindow::instance()->disableShortcuts(false); + actionsForKeys[Qt::Key_Left]->setShortcut(Qt::Key_Left); + actionsForKeys[Qt::Key_Right]->setShortcut(Qt::Key_Right); + actionsForKeys[Qt::Key_Up]->setShortcut(Qt::Key_Up); + actionsForKeys[Qt::Key_Down]->setShortcut(Qt::Key_Down); + actionsForKeys[Qt::Key_Escape]->setShortcut(Qt::Key_Escape); + actionsForKeys[Qt::Key_Delete]->setShortcut(Qt::Key_Delete); + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + connect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + connect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + connect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + connect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + /* show the same stuff that the profile shows. */ + currentState = ADD; /* enable the add state. */ + diveCeiling->setVisible(true); + gradientFactor->setVisible(true); + setBackgroundBrush(QColor("#A7DCFF")); +} + +void ProfileWidget2::setPlanState() +{ + if (currentState == PLAN) + return; + + setProfileState(); + mouseFollowerHorizontal->setVisible(true); + mouseFollowerVertical->setVisible(true); + mouseFollowerHorizontal->setLine(timeAxis->line()); + mouseFollowerVertical->setLine(QLineF(0, profileYAxis->pos().y(), 0, timeAxis->pos().y())); + disconnectTemporaryConnections(); + //TODO: Move this method to another place, shouldn't be on mainwindow. + MainWindow::instance()->disableShortcuts(); + actionsForKeys[Qt::Key_Left]->setShortcut(Qt::Key_Left); + actionsForKeys[Qt::Key_Right]->setShortcut(Qt::Key_Right); + actionsForKeys[Qt::Key_Up]->setShortcut(Qt::Key_Up); + actionsForKeys[Qt::Key_Down]->setShortcut(Qt::Key_Down); + actionsForKeys[Qt::Key_Escape]->setShortcut(Qt::Key_Escape); + actionsForKeys[Qt::Key_Delete]->setShortcut(Qt::Key_Delete); + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + connect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + connect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + connect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + connect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + /* show the same stuff that the profile shows. */ + currentState = PLAN; /* enable the add state. */ + diveCeiling->setVisible(true); + gradientFactor->setVisible(true); + setBackgroundBrush(QColor("#D7E3EF")); +} + +extern struct ev_select *ev_namelist; +extern int evn_allocated; +extern int evn_used; + +bool ProfileWidget2::isPlanner() +{ + return currentState == PLAN; +} + +bool ProfileWidget2::isAddOrPlanner() +{ + return currentState == PLAN || currentState == ADD; +} + +struct plot_data *ProfileWidget2::getEntryFromPos(QPointF pos) +{ + // find the time stamp corresponding to the mouse position + int seconds = timeAxis->valueAt(pos); + struct plot_data *entry = NULL; + + for (int i = 0; i < plotInfo.nr; i++) { + entry = plotInfo.entry + i; + if (entry->sec >= seconds) + break; + } + return entry; +} + +void ProfileWidget2::setReplot(bool state) +{ + replotEnabled = state; +} + +void ProfileWidget2::contextMenuEvent(QContextMenuEvent *event) +{ + if (currentState == ADD || currentState == PLAN) { + QGraphicsView::contextMenuEvent(event); + return; + } + QMenu m; + bool isDCName = false; + if (selected_dive == -1) + return; + // figure out if we are ontop of the dive computer name in the profile + QGraphicsItem *sceneItem = itemAt(mapFromGlobal(event->globalPos())); + if (sceneItem) { + QGraphicsItem *parentItem = sceneItem; + while (parentItem) { + if (parentItem->data(SUBSURFACE_OBJ_DATA) == SUBSURFACE_OBJ_DC_TEXT) { + isDCName = true; + break; + } + parentItem = parentItem->parentItem(); + } + if (isDCName) { + if (dc_number == 0 && count_divecomputers() == 1) + // nothing to do, can't delete or reorder + return; + // create menu to show when right clicking on dive computer name + if (dc_number > 0) + m.addAction(tr("Make first divecomputer"), this, SLOT(makeFirstDC())); + if (count_divecomputers() > 1) + m.addAction(tr("Delete this divecomputer"), this, SLOT(deleteCurrentDC())); + m.exec(event->globalPos()); + // don't show the regular profile context menu + return; + } + } + // create the profile context menu + QPointF scenePos = mapToScene(event->pos()); + struct plot_data *entry = getEntryFromPos(scenePos); + GasSelectionModel *model = GasSelectionModel::instance(); + model->repopulate(); + int rowCount = model->rowCount(); + if (rowCount > 1) { + // if we have more than one gas, offer to switch to another one + QMenu *gasChange = m.addMenu(tr("Add gas change")); + for (int i = 0; i < rowCount; i++) { + QAction *action = new QAction(&m); + action->setText(model->data(model->index(i, 0), Qt::DisplayRole).toString() + QString(tr(" (Tank %1)")).arg(i + 1)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(changeGas())); + action->setData(event->globalPos()); + if (i == entry->cylinderindex) + action->setDisabled(true); + gasChange->addAction(action); + } + } + QAction *setpointAction = m.addAction(tr("Add set-point change"), this, SLOT(addSetpointChange())); + setpointAction->setData(event->globalPos()); + QAction *action = m.addAction(tr("Add bookmark"), this, SLOT(addBookmark())); + action->setData(event->globalPos()); + + if (same_string(current_dc->model, "manually added dive")) + QAction *editProfileAction = m.addAction(tr("Edit the profile"), MainWindow::instance(), SLOT(editCurrentDive())); + + if (DiveEventItem *item = dynamic_cast<DiveEventItem *>(sceneItem)) { + action = new QAction(&m); + action->setText(tr("Remove event")); + action->setData(QVariant::fromValue<void *>(item)); // so we know what to remove. + connect(action, SIGNAL(triggered(bool)), this, SLOT(removeEvent())); + m.addAction(action); + action = new QAction(&m); + action->setText(tr("Hide similar events")); + action->setData(QVariant::fromValue<void *>(item)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(hideEvents())); + m.addAction(action); + struct event *dcEvent = item->getEvent(); + if (dcEvent->type == SAMPLE_EVENT_BOOKMARK) { + action = new QAction(&m); + action->setText(tr("Edit name")); + action->setData(QVariant::fromValue<void *>(item)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(editName())); + m.addAction(action); + } +#if 0 // FIXME::: FINISH OR DISABLE + // this shows how to figure out if we should ask the user if they want adjust interpolated pressures + // at either side of a gas change + if (dcEvent->type == SAMPLE_EVENT_GASCHANGE || dcEvent->type == SAMPLE_EVENT_GASCHANGE2) { + qDebug() << "figure out if there are interpolated pressures"; + struct plot_data *gasChangeEntry = entry; + struct plot_data *newGasEntry; + while (gasChangeEntry > plotInfo.entry) { + --gasChangeEntry; + if (gasChangeEntry->sec <= dcEvent->time.seconds) + break; + } + qDebug() << "at gas change at" << gasChangeEntry->sec << ": sensor pressure" << gasChangeEntry->pressure[0] << "interpolated" << gasChangeEntry->pressure[1]; + // now gasChangeEntry points at the gas change, that entry has the final pressure of + // the old tank, the next entry has the starting pressure of the next tank + if (gasChangeEntry + 1 <= plotInfo.entry + plotInfo.nr) { + newGasEntry = gasChangeEntry + 1; + qDebug() << "after gas change at " << newGasEntry->sec << ": sensor pressure" << newGasEntry->pressure[0] << "interpolated" << newGasEntry->pressure[1]; + if (SENSOR_PRESSURE(gasChangeEntry) == 0 || displayed_dive.cylinder[gasChangeEntry->cylinderindex].sample_start.mbar == 0) { + // if we have no sensorpressure or if we have no pressure from samples we can assume that + // we only have interpolated pressure (the pressure in the entry may be stored in the sensor + // pressure field if this is the first or last entry for this tank... see details in gaspressures.c + pressure_t pressure; + pressure.mbar = INTERPOLATED_PRESSURE(gasChangeEntry) ? : SENSOR_PRESSURE(gasChangeEntry); + QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of tank %1 (currently interpolated as %2)") + .arg(gasChangeEntry->cylinderindex + 1).arg(get_pressure_string(pressure))); + } + if (SENSOR_PRESSURE(newGasEntry) == 0 || displayed_dive.cylinder[newGasEntry->cylinderindex].sample_start.mbar == 0) { + // we only have interpolated press -- see commend above + pressure_t pressure; + pressure.mbar = INTERPOLATED_PRESSURE(newGasEntry) ? : SENSOR_PRESSURE(newGasEntry); + QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of tank %1 (currently interpolated as %2)") + .arg(newGasEntry->cylinderindex + 1).arg(get_pressure_string(pressure))); + } + } + } +#endif + } + bool some_hidden = false; + for (int i = 0; i < evn_used; i++) { + if (ev_namelist[i].plot_ev == false) { + some_hidden = true; + break; + } + } + if (some_hidden) { + action = m.addAction(tr("Unhide all events"), this, SLOT(unhideEvents())); + action->setData(event->globalPos()); + } + m.exec(event->globalPos()); +} + +void ProfileWidget2::deleteCurrentDC() +{ + delete_current_divecomputer(); + mark_divelist_changed(true); + // we need to force it since it's likely the same dive and same dc_number - but that's a different dive computer now + MainWindow::instance()->graphics()->plotDive(0, true); + MainWindow::instance()->refreshDisplay(); +} + +void ProfileWidget2::makeFirstDC() +{ + make_first_dc(); + mark_divelist_changed(true); + // this is now the first DC, so we need to redraw the profile and refresh the dive list + // (and no, it's not just enough to rewrite the text - the first DC is special so values in the + // dive list may change). + // As a side benefit, this returns focus to the dive list. + dc_number = 0; + MainWindow::instance()->refreshDisplay(); +} + +void ProfileWidget2::hideEvents() +{ + QAction *action = qobject_cast<QAction *>(sender()); + DiveEventItem *item = static_cast<DiveEventItem *>(action->data().value<void *>()); + struct event *event = item->getEvent(); + + if (QMessageBox::question(MainWindow::instance(), + TITLE_OR_TEXT(tr("Hide events"), tr("Hide all %1 events?").arg(event->name)), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + if (!same_string(event->name, "")) { + for (int i = 0; i < evn_used; i++) { + if (same_string(event->name, ev_namelist[i].ev_name)) { + ev_namelist[i].plot_ev = false; + break; + } + } + Q_FOREACH (DiveEventItem *evItem, eventItems) { + if (same_string(evItem->getEvent()->name, event->name)) + evItem->hide(); + } + } else { + item->hide(); + } + } +} + +void ProfileWidget2::unhideEvents() +{ + for (int i = 0; i < evn_used; i++) { + ev_namelist[i].plot_ev = true; + } + Q_FOREACH (DiveEventItem *item, eventItems) + item->show(); +} + +void ProfileWidget2::removeEvent() +{ + QAction *action = qobject_cast<QAction *>(sender()); + DiveEventItem *item = static_cast<DiveEventItem *>(action->data().value<void *>()); + struct event *event = item->getEvent(); + + if (QMessageBox::question(MainWindow::instance(), TITLE_OR_TEXT( + tr("Remove the selected event?"), + tr("%1 @ %2:%3").arg(event->name).arg(event->time.seconds / 60).arg(event->time.seconds % 60, 2, 10, QChar('0'))), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + remove_event(event); + mark_divelist_changed(true); + replot(); + } +} + +void ProfileWidget2::addBookmark() +{ + QAction *action = qobject_cast<QAction *>(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + add_event(current_dc, timeAxis->valueAt(scenePos), SAMPLE_EVENT_BOOKMARK, 0, 0, "bookmark"); + mark_divelist_changed(true); + replot(); +} + +void ProfileWidget2::addSetpointChange() +{ + QAction *action = qobject_cast<QAction *>(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + SetpointDialog::instance()->setpointData(current_dc, timeAxis->valueAt(scenePos)); + SetpointDialog::instance()->show(); +} + +void ProfileWidget2::changeGas() +{ + QAction *action = qobject_cast<QAction *>(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + QString gas = action->text(); + gas.remove(QRegExp(" \\(.*\\)")); + + // backup the things on the dataModel, since we will clear that out. + struct gasmix gasmix; + qreal sec_val = timeAxis->valueAt(scenePos); + + // no gas changes before the dive starts + unsigned int seconds = (sec_val < 0.0) ? 0 : (unsigned int)sec_val; + + // if there is a gas change at this time stamp, remove it before adding the new one + struct event *gasChangeEvent = current_dc->events; + while ((gasChangeEvent = get_next_event(gasChangeEvent, "gaschange")) != NULL) { + if (gasChangeEvent->time.seconds == seconds) { + remove_event(gasChangeEvent); + gasChangeEvent = current_dc->events; + } else { + gasChangeEvent = gasChangeEvent->next; + } + } + validate_gas(gas.toUtf8().constData(), &gasmix); + QRegExp rx("\\(\\D*(\\d+)"); + int tank; + if (rx.indexIn(action->text()) > -1) { + tank = rx.cap(1).toInt() - 1; // we display the tank 1 based + } else { + qDebug() << "failed to parse tank number"; + tank = get_gasidx(&displayed_dive, &gasmix); + } + // add this both to the displayed dive and the current dive + add_gas_switch_event(current_dive, current_dc, seconds, tank); + add_gas_switch_event(&displayed_dive, get_dive_dc(&displayed_dive, dc_number), seconds, tank); + // this means we potentially have a new tank that is being used and needs to be shown + fixup_dive(&displayed_dive); + + // FIXME - this no longer gets written to the dive list - so we need to enableEdition() here + + MainWindow::instance()->information()->updateDiveInfo(); + mark_divelist_changed(true); + replot(); +} + +bool ProfileWidget2::getPrintMode() +{ + return printMode; +} + +void ProfileWidget2::setPrintMode(bool mode, bool grayscale) +{ + printMode = mode; + resetZoom(); + + // set printMode for axes + profileYAxis->setPrintMode(mode); + gasYAxis->setPrintMode(mode); + temperatureAxis->setPrintMode(mode); + timeAxis->setPrintMode(mode); + cylinderPressureAxis->setPrintMode(mode); + heartBeatAxis->setPrintMode(mode); + percentageAxis->setPrintMode(mode); + + isGrayscale = mode ? grayscale : false; + mouseFollowerHorizontal->setVisible(!mode); + mouseFollowerVertical->setVisible(!mode); +} + +void ProfileWidget2::setFontPrintScale(double scale) +{ + fontPrintScale = scale; + emit fontPrintScaleChanged(scale); +} + +double ProfileWidget2::getFontPrintScale() +{ + if (printMode) + return fontPrintScale; + else + return 1.0; +} + +void ProfileWidget2::editName() +{ + QAction *action = qobject_cast<QAction *>(sender()); + DiveEventItem *item = static_cast<DiveEventItem *>(action->data().value<void *>()); + struct event *event = item->getEvent(); + bool ok; + QString newName = QInputDialog::getText(MainWindow::instance(), tr("Edit name of bookmark"), + tr("Custom name:"), QLineEdit::Normal, + event->name, &ok); + if (ok && !newName.isEmpty()) { + if (newName.length() > 22) { //longer names will display as garbage. + QMessageBox lengthWarning; + lengthWarning.setText(tr("Name is too long!")); + lengthWarning.exec(); + return; + } + // order is important! first update the current dive (by matching the unchanged event), + // then update the displayed dive (as event is part of the events on displayed dive + // and will be freed as part of changing the name! + update_event_name(current_dive, event, newName.toUtf8().data()); + update_event_name(&displayed_dive, event, newName.toUtf8().data()); + mark_divelist_changed(true); + replot(); + } +} + +void ProfileWidget2::disconnectTemporaryConnections() +{ + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + disconnect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + disconnect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + + disconnect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + disconnect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + + Q_FOREACH (QAction *action, actionsForKeys.values()) { + action->setShortcut(QKeySequence()); + action->setShortcutContext(Qt::WidgetShortcut); + } +} + +void ProfileWidget2::pointInserted(const QModelIndex &parent, int start, int end) +{ + DiveHandler *item = new DiveHandler(); + scene()->addItem(item); + handles << item; + + connect(item, SIGNAL(moved()), this, SLOT(recreatePlannedDive())); + connect(item, SIGNAL(clicked()), this, SLOT(divePlannerHandlerClicked())); + connect(item, SIGNAL(released()), this, SLOT(divePlannerHandlerReleased())); + QGraphicsSimpleTextItem *gasChooseBtn = new QGraphicsSimpleTextItem(); + scene()->addItem(gasChooseBtn); + gasChooseBtn->setZValue(10); + gasChooseBtn->setFlag(QGraphicsItem::ItemIgnoresTransformations); + gases << gasChooseBtn; + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + if (plannerModel->recalcQ()) + replot(); +} + +void ProfileWidget2::pointsRemoved(const QModelIndex &, int start, int end) +{ // start and end are inclusive. + int num = (end - start) + 1; + for (int i = num; i != 0; i--) { + delete handles.back(); + handles.pop_back(); + delete gases.back(); + gases.pop_back(); + } + scene()->clearSelection(); + replot(); +} + +void ProfileWidget2::repositionDiveHandlers() +{ + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + // Re-position the user generated dive handlers + struct gasmix mix, lastmix; + for (int i = 0; i < plannerModel->rowCount(); i++) { + struct divedatapoint datapoint = plannerModel->at(i); + if (datapoint.time == 0) // those are the magic entries for tanks + continue; + DiveHandler *h = handles.at(i); + h->setVisible(datapoint.entered); + h->setPos(timeAxis->posAtValue(datapoint.time), profileYAxis->posAtValue(datapoint.depth)); + QPointF p1; + if (i == 0) { + if (prefs.drop_stone_mode) + // place the text on the straight line from the drop to stone position + p1 = QPointF(timeAxis->posAtValue(datapoint.depth / prefs.descrate), + profileYAxis->posAtValue(datapoint.depth)); + else + // place the text on the straight line from the origin to the first position + p1 = QPointF(timeAxis->posAtValue(0), profileYAxis->posAtValue(0)); + } else { + // place the text on the line from the last position + p1 = handles[i - 1]->pos(); + } + QPointF p2 = handles[i]->pos(); + QLineF line(p1, p2); + QPointF pos = line.pointAt(0.5); + gases[i]->setPos(pos); + gases[i]->setText(get_divepoint_gas_string(datapoint)); + gases[i]->setVisible(datapoint.entered && + (i == 0 || gases[i]->text() != gases[i-1]->text())); + } +} + +int ProfileWidget2::fixHandlerIndex(DiveHandler *activeHandler) +{ + int index = handles.indexOf(activeHandler); + if (index > 0 && index < handles.count() - 1) { + DiveHandler *before = handles[index - 1]; + if (before->pos().x() > activeHandler->pos().x()) { + handles.swap(index, index - 1); + return index - 1; + } + DiveHandler *after = handles[index + 1]; + if (after->pos().x() < activeHandler->pos().x()) { + handles.swap(index, index + 1); + return index + 1; + } + } + return index; +} + +void ProfileWidget2::recreatePlannedDive() +{ + DiveHandler *activeHandler = qobject_cast<DiveHandler *>(sender()); + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + int index = fixHandlerIndex(activeHandler); + int mintime = 0, maxtime = (timeAxis->maximum() + 10) * 60; + if (index > 0) + mintime = plannerModel->at(index - 1).time; + if (index < plannerModel->size() - 1) + maxtime = plannerModel->at(index + 1).time; + + int minutes = rint(timeAxis->valueAt(activeHandler->pos()) / 60); + if (minutes * 60 <= mintime || minutes * 60 >= maxtime) + return; + + divedatapoint data = plannerModel->at(index); + data.depth = rint(profileYAxis->valueAt(activeHandler->pos()) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); + data.time = rint(timeAxis->valueAt(activeHandler->pos())); + + plannerModel->editStop(index, data); +} + +void ProfileWidget2::keyDownAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + if (dp.depth >= profileYAxis->maximum()) + continue; + + dp.depth += M_OR_FT(1, 5); + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyUpAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + + if (dp.depth <= 0) + continue; + + dp.depth -= M_OR_FT(1, 5); + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyLeftAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + + if (dp.time / 60 <= 0) + continue; + + // don't overlap positions. + // maybe this is a good place for a 'goto'? + double xpos = timeAxis->posAtValue((dp.time - 60) / 60); + bool nextStep = false; + Q_FOREACH (DiveHandler *h, handles) { + if (IS_FP_SAME(h->pos().x(), xpos)) { + nextStep = true; + break; + } + } + if (nextStep) + continue; + + dp.time -= 60; + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyRightAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + if (dp.time / 60.0 >= timeAxis->maximum()) + continue; + + // don't overlap positions. + // maybe this is a good place for a 'goto'? + double xpos = timeAxis->posAtValue((dp.time + 60) / 60); + bool nextStep = false; + Q_FOREACH (DiveHandler *h, handles) { + if (IS_FP_SAME(h->pos().x(), xpos)) { + nextStep = true; + break; + } + } + if (nextStep) + continue; + + dp.time += 60; + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyDeleteAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + int selCount = scene()->selectedItems().count(); + if (selCount) { + QVector<int> selectedIndexes; + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + selectedIndexes.push_back(handles.indexOf(handler)); + handler->hide(); + } + } + plannerModel->removeSelectedPoints(selectedIndexes); + } +} + +void ProfileWidget2::keyEscAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + if (scene()->selectedItems().count()) { + scene()->clearSelection(); + return; + } + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + if (plannerModel->isPlanner()) + plannerModel->cancelPlan(); +} + +void ProfileWidget2::plotPictures() +{ + Q_FOREACH (DivePictureItem *item, pictures) { + item->hide(); + item->deleteLater(); + } + pictures.clear(); + + if (printMode) + return; + + double x, y, lastX = -1.0, lastY = -1.0; + DivePictureModel *m = DivePictureModel::instance(); + for (int i = 0; i < m->rowCount(); i++) { + int offsetSeconds = m->index(i, 1).data(Qt::UserRole).value<int>(); + // it's a correct picture, but doesn't have a timestamp: only show on the widget near the + // information area. + if (!offsetSeconds) + continue; + DivePictureItem *item = new DivePictureItem(); + item->setPixmap(m->index(i, 0).data(Qt::DecorationRole).value<QPixmap>()); + item->setFileUrl(m->index(i, 1).data().toString()); + // let's put the picture at the correct time, but at a fixed "depth" on the profile + // not sure this is ideal, but it seems to look right. + x = timeAxis->posAtValue(offsetSeconds); + if (i == 0) + y = 10; + else if (fabs(x - lastX) < 4) + y = lastY + 3; + else + y = 10; + lastX = x; + lastY = y; + item->setPos(x, y); + scene()->addItem(item); + pictures.push_back(item); + } +} diff --git a/desktop-widgets/profile/profilewidget2.h b/desktop-widgets/profile/profilewidget2.h new file mode 100644 index 000000000..f11ec5be1 --- /dev/null +++ b/desktop-widgets/profile/profilewidget2.h @@ -0,0 +1,211 @@ +#ifndef PROFILEWIDGET2_H +#define PROFILEWIDGET2_H + +#include <QGraphicsView> + +// /* 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 "divelineitem.h" +#include "diveprofileitem.h" +#include "display.h" + +class RulerItem2; +struct dive; +struct plot_info; +class ToolTipItem; +class DiveMeanDepth; +class DiveReportedCeiling; +class DiveTextItem; +class TemperatureAxis; +class DiveEventItem; +class DivePlotDataModel; +class DivePixmapItem; +class DiveRectItem; +class DepthAxis; +class DiveCartesianAxis; +class DiveProfileItem; +class TimeAxis; +class DiveTemperatureItem; +class DiveHeartrateItem; +class PercentageItem; +class DiveGasPressureItem; +class DiveCalculatedCeiling; +class DiveCalculatedTissue; +class PartialPressureGasItem; +class PartialGasPressureAxis; +class AbstractProfilePolygonItem; +class TankItem; +class DiveHandler; +class QGraphicsSimpleTextItem; +class QModelIndex; +class DivePictureItem; + +class ProfileWidget2 : public QGraphicsView { + Q_OBJECT +public: + enum State { + EMPTY, + PROFILE, + EDIT, + ADD, + PLAN, + INVALID + }; + enum Items { + BACKGROUND, + PROFILE_Y_AXIS, + GAS_Y_AXIS, + TIME_AXIS, + DEPTH_CONTROLLER, + TIME_CONTROLLER, + COLUMNS + }; + + ProfileWidget2(QWidget *parent = 0); + void resetZoom(); + void plotDive(struct dive *d = 0, bool force = false); + virtual bool eventFilter(QObject *, QEvent *); + void setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *hAxis, DiveCartesianAxis *vAxis, DivePlotDataModel *model, int vData, int hData, int zValue); + void setPrintMode(bool mode, bool grayscale = false); + bool getPrintMode(); + bool isPointOutOfBoundaries(const QPointF &point) const; + bool isPlanner(); + bool isAddOrPlanner(); + double getFontPrintScale(); + void setFontPrintScale(double scale); + void clearHandlers(); + void recalcCeiling(); + void setToolTipVisibile(bool visible); + State currentState; + +signals: + void fontPrintScaleChanged(double scale); + +public +slots: // Necessary to call from QAction's signals. + void settingsChanged(); + void setEmptyState(); + void setProfileState(); + void setPlanState(); + void setAddState(); + void changeGas(); + void addSetpointChange(); + void addBookmark(); + void hideEvents(); + void unhideEvents(); + void removeEvent(); + void editName(); + void makeFirstDC(); + void deleteCurrentDC(); + void pointInserted(const QModelIndex &parent, int start, int end); + void pointsRemoved(const QModelIndex &, int start, int end); + void plotPictures(); + void setReplot(bool state); + void replot(dive *d = 0); + + /* this is called for every move on the handlers. maybe we can speed up this a bit? */ + void recreatePlannedDive(); + + /* key press handlers */ + void keyEscAction(); + void keyDeleteAction(); + void keyUpAction(); + void keyDownAction(); + void keyLeftAction(); + void keyRightAction(); + + void divePlannerHandlerClicked(); + void divePlannerHandlerReleased(); + +protected: + virtual ~ProfileWidget2(); + virtual void resizeEvent(QResizeEvent *event); + virtual void wheelEvent(QWheelEvent *event); + virtual void mouseMoveEvent(QMouseEvent *event); + virtual void contextMenuEvent(QContextMenuEvent *event); + virtual void mouseDoubleClickEvent(QMouseEvent *event); + virtual void mousePressEvent(QMouseEvent *event); + virtual void mouseReleaseEvent(QMouseEvent *event); + +private: /*methods*/ + void fixBackgroundPos(); + void scrollViewTo(const QPoint &pos); + void setupSceneAndFlags(); + void setupItemSizes(); + void addItemsToScene(); + void setupItemOnScene(); + void disconnectTemporaryConnections(); + struct plot_data *getEntryFromPos(QPointF pos); + +private: + DivePlotDataModel *dataModel; + int zoomLevel; + qreal zoomFactor; + DivePixmapItem *background; + QString backgroundFile; + ToolTipItem *toolTipItem; + bool isPlotZoomed; + bool replotEnabled; + // All those here should probably be merged into one structure, + // So it's esyer to replicate for more dives later. + // In the meantime, keep it here. + struct plot_info plotInfo; + DepthAxis *profileYAxis; + PartialGasPressureAxis *gasYAxis; + TemperatureAxis *temperatureAxis; + TimeAxis *timeAxis; + DiveProfileItem *diveProfileItem; + DiveTemperatureItem *temperatureItem; + DiveMeanDepthItem *meanDepthItem; + DiveCartesianAxis *cylinderPressureAxis; + DiveGasPressureItem *gasPressureItem; + QList<DiveEventItem *> eventItems; + DiveTextItem *diveComputerText; + DiveCalculatedCeiling *diveCeiling; + DiveTextItem *gradientFactor; + QList<DiveCalculatedTissue *> allTissues; + DiveReportedCeiling *reportedCeiling; + PartialPressureGasItem *pn2GasItem; + PartialPressureGasItem *pheGasItem; + PartialPressureGasItem *po2GasItem; + PartialPressureGasItem *o2SetpointGasItem; + PartialPressureGasItem *ccrsensor1GasItem; + PartialPressureGasItem *ccrsensor2GasItem; + PartialPressureGasItem *ccrsensor3GasItem; + DiveCartesianAxis *heartBeatAxis; + DiveHeartrateItem *heartBeatItem; + DiveCartesianAxis *percentageAxis; + QList<DivePercentageItem *> allPercentages; + DiveAmbPressureItem *ambPressureItem; + DiveGFLineItem *gflineItem; + DiveLineItem *mouseFollowerVertical; + DiveLineItem *mouseFollowerHorizontal; + RulerItem2 *rulerItem; + TankItem *tankItem; + bool isGrayscale; + bool printMode; + + //specifics for ADD and PLAN + QList<DiveHandler *> handles; + QList<QGraphicsSimpleTextItem *> gases; + QList<DivePictureItem *> pictures; + void repositionDiveHandlers(); + int fixHandlerIndex(DiveHandler *activeHandler); + friend class DiveHandler; + QHash<Qt::Key, QAction *> actionsForKeys; + bool shouldCalculateMaxTime; + bool shouldCalculateMaxDepth; + int maxtime; + int maxdepth; + double fontPrintScale; +}; + +#endif // PROFILEWIDGET2_H diff --git a/desktop-widgets/profile/ruleritem.cpp b/desktop-widgets/profile/ruleritem.cpp new file mode 100644 index 000000000..830985552 --- /dev/null +++ b/desktop-widgets/profile/ruleritem.cpp @@ -0,0 +1,179 @@ +#include "ruleritem.h" +#include "preferences.h" +#include "mainwindow.h" +#include "profilewidget2.h" +#include "display.h" + +#include <qgraphicssceneevent.h> + +#include "profile.h" + +RulerNodeItem2::RulerNodeItem2() : + entry(NULL), + ruler(NULL), + timeAxis(NULL), + depthAxis(NULL) +{ + memset(&pInfo, 0, sizeof(pInfo)); + setRect(-8, -8, 16, 16); + setBrush(QColor(0xff, 0, 0, 127)); + setPen(QColor(Qt::red)); + setFlag(ItemIsMovable); + setFlag(ItemSendsGeometryChanges); + setFlag(ItemIgnoresTransformations); +} + +void RulerNodeItem2::setPlotInfo(plot_info &info) +{ + pInfo = info; + entry = pInfo.entry; +} + +void RulerNodeItem2::setRuler(RulerItem2 *r) +{ + ruler = r; +} + +void RulerNodeItem2::recalculate() +{ + struct plot_data *data = pInfo.entry + (pInfo.nr - 1); + uint16_t count = 0; + if (x() < 0) { + setPos(0, y()); + } else if (x() > timeAxis->posAtValue(data->sec)) { + setPos(timeAxis->posAtValue(data->sec), depthAxis->posAtValue(data->depth)); + } else { + data = pInfo.entry; + count = 0; + while (timeAxis->posAtValue(data->sec) < x() && count < pInfo.nr) { + data = pInfo.entry + count; + count++; + } + setPos(timeAxis->posAtValue(data->sec), depthAxis->posAtValue(data->depth)); + entry = data; + } +} + +void RulerNodeItem2::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + qreal x = event->scenePos().x(); + if (x < 0.0) + x = 0.0; + setPos(x, event->scenePos().y()); + recalculate(); + ruler->recalculate(); +} + +RulerItem2::RulerItem2() : source(new RulerNodeItem2()), + dest(new RulerNodeItem2()), + timeAxis(NULL), + depthAxis(NULL), + textItemBack(new QGraphicsRectItem(this)), + textItem(new QGraphicsSimpleTextItem(this)) +{ + memset(&pInfo, 0, sizeof(pInfo)); + 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)); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void RulerItem2::settingsChanged() +{ + ProfileWidget2 *profWidget = NULL; + if (scene() && scene()->views().count()) + profWidget = qobject_cast<ProfileWidget2 *>(scene()->views().first()); + + if (profWidget && profWidget->currentState == ProfileWidget2::PROFILE) + setVisible(prefs.rulergraph); + else + setVisible(false); +} + +void RulerItem2::recalculate() +{ + char buffer[500]; + QPointF tmp; + QFont font; + QFontMetrics fm(font); + + if (timeAxis == NULL || depthAxis == NULL || pInfo.nr == 0) + 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(source->entry, dest->entry, buffer, 500, 1); + text = QString(buffer); + + // draw text + QGraphicsView *view = scene()->views().first(); + QPoint begin = view->mapFromScene(mapToScene(startPoint)); + textItem->setText(text); + 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(begin.x() - (diff - view->width())); + tgtX = mapFromScene(view->mapToScene(begin)).x(); + } + // always show the text bellow the lowest of the start and end points + 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 + textItemBack->setVisible(startPoint.x() != endPoint.x()); + textItemBack->setPos(textItem->x(), textItem->y()); + textItemBack->setRect(0, 0, textItem->boundingRect().width(), textItem->boundingRect().height()); +} + +RulerNodeItem2 *RulerItem2::sourceNode() const +{ + return source; +} + +RulerNodeItem2 *RulerItem2::destNode() const +{ + return dest; +} + +void RulerItem2::setPlotInfo(plot_info info) +{ + 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); +} diff --git a/desktop-widgets/profile/ruleritem.h b/desktop-widgets/profile/ruleritem.h new file mode 100644 index 000000000..4fad0451c --- /dev/null +++ b/desktop-widgets/profile/ruleritem.h @@ -0,0 +1,59 @@ +#ifndef RULERITEM_H +#define RULERITEM_H + +#include <QObject> +#include <QGraphicsEllipseItem> +#include <QGraphicsObject> +#include "divecartesianaxis.h" +#include "display.h" + +struct plot_data; +class RulerItem2; + +class RulerNodeItem2 : public QObject, public QGraphicsEllipseItem { + Q_OBJECT + friend class RulerItem2; + +public: + explicit RulerNodeItem2(); + void setRuler(RulerItem2 *r); + void setPlotInfo(struct plot_info &info); + void recalculate(); + +protected: + virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event); +private: + struct plot_info pInfo; + struct plot_data *entry; + RulerItem2 *ruler; + DiveCartesianAxis *timeAxis; + DiveCartesianAxis *depthAxis; +}; + +class RulerItem2 : public QObject, public QGraphicsLineItem { + Q_OBJECT +public: + explicit RulerItem2(); + void recalculate(); + + void setPlotInfo(struct plot_info pInfo); + RulerNodeItem2 *sourceNode() const; + RulerNodeItem2 *destNode() const; + void setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth); + void setVisible(bool visible); + +public +slots: + void settingsChanged(); + +private: + struct plot_info pInfo; + QPointF startPoint, endPoint; + RulerNodeItem2 *source, *dest; + QString text; + DiveCartesianAxis *timeAxis; + DiveCartesianAxis *depthAxis; + QGraphicsRectItem *textItemBack; + QGraphicsSimpleTextItem *textItem; +}; +#endif diff --git a/desktop-widgets/profile/tankitem.cpp b/desktop-widgets/profile/tankitem.cpp new file mode 100644 index 000000000..c0e75a371 --- /dev/null +++ b/desktop-widgets/profile/tankitem.cpp @@ -0,0 +1,120 @@ +#include "tankitem.h" +#include "diveplotdatamodel.h" +#include "divetextitem.h" +#include "profile.h" +#include <QPen> + +TankItem::TankItem(QObject *parent) : + QGraphicsRectItem(), + dataModel(0), + pInfoEntry(0), + pInfoNr(0) +{ + height = 3; + QColor red(PERSIANRED1); + QColor blue(AIR_BLUE); + QColor yellow(NITROX_YELLOW); + QColor green(NITROX_GREEN); + QLinearGradient nitroxGradient(QPointF(0, 0), QPointF(0, height)); + nitroxGradient.setColorAt(0.0, green); + nitroxGradient.setColorAt(0.49, green); + nitroxGradient.setColorAt(0.5, yellow); + nitroxGradient.setColorAt(1.0, yellow); + nitrox = nitroxGradient; + oxygen = green; + QLinearGradient trimixGradient(QPointF(0, 0), QPointF(0, height)); + trimixGradient.setColorAt(0.0, green); + trimixGradient.setColorAt(0.49, green); + trimixGradient.setColorAt(0.5, red); + trimixGradient.setColorAt(1.0, red); + trimix = trimixGradient; + air = blue; + memset(&diveCylinderStore, 0, sizeof(diveCylinderStore)); +} + +TankItem::~TankItem() +{ + // Should this be clear_dive(diveCylinderStore)? + for (int i = 0; i < MAX_CYLINDERS; i++) + free((void *)diveCylinderStore.cylinder[i].type.description); +} + +void TankItem::setData(DivePlotDataModel *model, struct plot_info *plotInfo, struct dive *d) +{ + free(pInfoEntry); + // the plotInfo and dive structures passed in could become invalid before we stop using them, + // so copy the data that we need + int size = plotInfo->nr * sizeof(plotInfo->entry[0]); + pInfoEntry = (struct plot_data *)malloc(size); + pInfoNr = plotInfo->nr; + memcpy(pInfoEntry, plotInfo->entry, size); + copy_cylinders(d, &diveCylinderStore, false); + dataModel = model; + connect(dataModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(modelDataChanged(QModelIndex, QModelIndex)), Qt::UniqueConnection); + modelDataChanged(); +} + +void TankItem::createBar(qreal x, qreal w, struct gasmix *gas) +{ + // pick the right gradient, size, position and text + QGraphicsRectItem *rect = new QGraphicsRectItem(x, 0, w, height, this); + if (gasmix_is_air(gas)) + rect->setBrush(air); + else if (gas->he.permille) + rect->setBrush(trimix); + else if (gas->o2.permille == 1000) + rect->setBrush(oxygen); + else + rect->setBrush(nitrox); + rect->setPen(QPen(QBrush(), 0.0)); // get rid of the thick line around the rectangle + rects.push_back(rect); + DiveTextItem *label = new DiveTextItem(rect); + label->setText(gasname(gas)); + label->setBrush(Qt::black); + label->setPos(x + 1, 0); + label->setAlignment(Qt::AlignBottom | Qt::AlignRight); + label->setZValue(101); +} + +void TankItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + + if (!dataModel || !pInfoEntry || !pInfoNr) + return; + + // remove the old rectangles + foreach (QGraphicsRectItem *r, rects) { + delete(r); + } + rects.clear(); + + // walk the list and figure out which tanks go where + struct plot_data *entry = pInfoEntry; + int cylIdx = entry->cylinderindex; + int i = -1; + int startTime = 0; + struct gasmix *gas = &diveCylinderStore.cylinder[cylIdx].gasmix; + qreal width, left; + while (++i < pInfoNr) { + entry = &pInfoEntry[i]; + if (entry->cylinderindex == cylIdx) + continue; + width = hAxis->posAtValue(entry->sec) - hAxis->posAtValue(startTime); + left = hAxis->posAtValue(startTime); + createBar(left, width, gas); + cylIdx = entry->cylinderindex; + gas = &diveCylinderStore.cylinder[cylIdx].gasmix; + startTime = entry->sec; + } + width = hAxis->posAtValue(entry->sec) - hAxis->posAtValue(startTime); + left = hAxis->posAtValue(startTime); + createBar(left, width, gas); +} + +void TankItem::setHorizontalAxis(DiveCartesianAxis *horizontal) +{ + hAxis = horizontal; + connect(hAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} diff --git a/desktop-widgets/profile/tankitem.h b/desktop-widgets/profile/tankitem.h new file mode 100644 index 000000000..fd685fc82 --- /dev/null +++ b/desktop-widgets/profile/tankitem.h @@ -0,0 +1,39 @@ +#ifndef TANKITEM_H +#define TANKITEM_H + +#include <QGraphicsItem> +#include <QModelIndex> +#include <QBrush> +#include "divelineitem.h" +#include "divecartesianaxis.h" +#include "dive.h" + +class TankItem : public QObject, public QGraphicsRectItem +{ + Q_OBJECT + +public: + explicit TankItem(QObject *parent = 0); + ~TankItem(); + void setHorizontalAxis(DiveCartesianAxis *horizontal); + void setData(DivePlotDataModel *model, struct plot_info *plotInfo, struct dive *d); + +signals: + +public slots: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + +private: + void createBar(qreal x, qreal w, struct gasmix *gas); + DivePlotDataModel *dataModel; + DiveCartesianAxis *hAxis; + int hDataColumn; + struct dive diveCylinderStore; + struct plot_data *pInfoEntry; + int pInfoNr; + qreal height; + QBrush air, nitrox, oxygen, trimix; + QList<QGraphicsRectItem *> rects; +}; + +#endif // TANKITEM_H |