diff --git a/gui/include/Common/VisualizationDef.h b/gui/include/Common/VisualizationDef.h index 34f87f4..37194bd 100644 --- a/gui/include/Common/VisualizationDef.h +++ b/gui/include/Common/VisualizationDef.h @@ -4,4 +4,5 @@ /// Minimum height for graph added in zones (in pixels) extern const int GRAPH_MINIMUM_HEIGHT; + #endif // SCIQLOP_VISUALIZATIONDEF_H diff --git a/gui/include/Visualization/VisualizationCursorItem.h b/gui/include/Visualization/VisualizationCursorItem.h new file mode 100644 index 0000000..d34d555 --- /dev/null +++ b/gui/include/Visualization/VisualizationCursorItem.h @@ -0,0 +1,26 @@ +#ifndef VISUALIZATIONCURSORITEM_H +#define VISUALIZATIONCURSORITEM_H + +#include +#include + +class QCustomPlot; + +class VisualizationCursorItem { +public: + VisualizationCursorItem(QCustomPlot *plot); + + void setVisible(bool value); + bool isVisible() const; + + void setPosition(double value); + void setAbsolutePosition(double value); + void setOrientation(Qt::Orientation orientation); + void setLabelText(const QString &text); + +private: + class VisualizationCursorItemPrivate; + spimpl::unique_impl_ptr impl; +}; + +#endif // VISUALIZATIONCURSORITEM_H diff --git a/gui/include/Visualization/VisualizationGraphWidget.h b/gui/include/Visualization/VisualizationGraphWidget.h index c97c9ca..19a4cc3 100644 --- a/gui/include/Visualization/VisualizationGraphWidget.h +++ b/gui/include/Visualization/VisualizationGraphWidget.h @@ -62,6 +62,18 @@ public: bool isDragAllowed() const override; void highlightForMerge(bool highlighted) override; + // Cursors + /// Adds or moves the vertical cursor at the specified value on the x-axis + void addVerticalCursor(double time); + /// Adds or moves the vertical cursor at the specified value on the x-axis + void addVerticalCursorAtViewportPosition(double position); + void removeVerticalCursor(); + /// Adds or moves the vertical cursor at the specified value on the y-axis + void addHorizontalCursor(double value); + /// Adds or moves the vertical cursor at the specified value on the y-axis + void addHorizontalCursorAtViewportPosition(double position); + void removeHorizontalCursor(); + signals: void synchronize(const SqpRange &range, const SqpRange &oldRange); void requestDataLoading(QVector > variable, const SqpRange &range, diff --git a/gui/include/Visualization/VisualizationZoneWidget.h b/gui/include/Visualization/VisualizationZoneWidget.h index f364645..211c0e5 100644 --- a/gui/include/Visualization/VisualizationZoneWidget.h +++ b/gui/include/Visualization/VisualizationZoneWidget.h @@ -70,6 +70,10 @@ public: QMimeData *mimeData() const override; bool isDragAllowed() const override; + void notifyMouseMoveInGraph(const QPointF &graphPosition, const QPointF &plotPosition, + VisualizationGraphWidget *graphWidget); + void notifyMouseLeaveGraph(VisualizationGraphWidget *graphWidget); + protected: void closeEvent(QCloseEvent *event) override; diff --git a/gui/meson.build b/gui/meson.build index a3bf57f..aba980d 100644 --- a/gui/meson.build +++ b/gui/meson.build @@ -79,7 +79,8 @@ gui_sources = [ 'src/Visualization/VisualizationDragWidget.cpp', 'src/Visualization/AxisRenderingUtils.cpp', 'src/Visualization/PlottablesRenderingUtils.cpp', - 'src/Visualization/MacScrollBarStyle.cpp' + 'src/Visualization/MacScrollBarStyle.cpp', + 'src/Visualization/VisualizationCursorItem.cpp' ] gui_inc = include_directories(['include']) diff --git a/gui/src/Visualization/VisualizationCursorItem.cpp b/gui/src/Visualization/VisualizationCursorItem.cpp new file mode 100644 index 0000000..ed74329 --- /dev/null +++ b/gui/src/Visualization/VisualizationCursorItem.cpp @@ -0,0 +1,163 @@ +#include +#include +#include + +/// Width of the cursor in pixel +const auto CURSOR_WIDTH = 3; + +/// Color of the cursor in the graph +const auto CURSOR_COLOR = QColor{68, 114, 196}; + +/// Line style of the cursor in the graph +auto CURSOR_PEN_STYLE = Qt::DotLine; + +struct VisualizationCursorItem::VisualizationCursorItemPrivate { + + QCustomPlot *m_Plot = nullptr; + + QCPItemStraightLine *m_LineItem = nullptr; + QCPItemText *m_LabelItem = nullptr; + + Qt::Orientation m_Orientation; + double m_Position = 0.0; + bool m_IsAbsolutePosition = false; + QString m_LabelText; + + explicit VisualizationCursorItemPrivate(QCustomPlot *plot) + : m_Plot(plot), m_Orientation(Qt::Vertical) + { + } + + void updateOrientation() + { + if (m_LineItem) { + switch (m_Orientation) { + case Qt::Vertical: + m_LineItem->point1->setTypeX(m_IsAbsolutePosition + ? QCPItemPosition::ptAbsolute + : QCPItemPosition::ptPlotCoords); + m_LineItem->point1->setTypeY(QCPItemPosition::ptAxisRectRatio); + m_LineItem->point2->setTypeX(m_IsAbsolutePosition + ? QCPItemPosition::ptAbsolute + : QCPItemPosition::ptPlotCoords); + m_LineItem->point2->setTypeY(QCPItemPosition::ptAxisRectRatio); + m_LabelItem->setPositionAlignment(Qt::AlignLeft | Qt::AlignTop); + break; + case Qt::Horizontal: + m_LineItem->point1->setTypeX(QCPItemPosition::ptAxisRectRatio); + m_LineItem->point1->setTypeY(m_IsAbsolutePosition + ? QCPItemPosition::ptAbsolute + : QCPItemPosition::ptPlotCoords); + m_LineItem->point2->setTypeX(QCPItemPosition::ptAxisRectRatio); + m_LineItem->point2->setTypeY(m_IsAbsolutePosition + ? QCPItemPosition::ptAbsolute + : QCPItemPosition::ptPlotCoords); + m_LabelItem->setPositionAlignment(Qt::AlignRight | Qt::AlignBottom); + } + } + } + + void updateCursorPosition() + { + if (m_LineItem) { + switch (m_Orientation) { + case Qt::Vertical: + m_LineItem->point1->setCoords(m_Position, 0); + m_LineItem->point2->setCoords(m_Position, 1); + m_LabelItem->position->setCoords(5, 5); + break; + case Qt::Horizontal: + m_LineItem->point1->setCoords(1, m_Position); + m_LineItem->point2->setCoords(0, m_Position); + m_LabelItem->position->setCoords(-5, -5); + } + } + } + + void updateLabelText() + { + if (m_LabelItem) { + m_LabelItem->setText(m_LabelText); + } + } +}; + +VisualizationCursorItem::VisualizationCursorItem(QCustomPlot *plot) + : impl{spimpl::make_unique_impl(plot)} +{ +} + +void VisualizationCursorItem::setVisible(bool value) +{ + if (value != isVisible()) { + + if (value) { + Q_ASSERT(!impl->m_LineItem && !impl->m_Plot); + + impl->m_LineItem = new QCPItemStraightLine{impl->m_Plot}; + auto pen = QPen{CURSOR_PEN_STYLE}; + pen.setColor(CURSOR_COLOR); + pen.setWidth(CURSOR_WIDTH); + impl->m_LineItem->setPen(pen); + impl->m_LineItem->setSelectable(false); + + impl->m_LabelItem = new QCPItemText{impl->m_Plot}; + impl->m_LabelItem->setColor(CURSOR_COLOR); + impl->m_LabelItem->setSelectable(false); + impl->m_LabelItem->position->setParentAnchor(impl->m_LineItem->point1); + impl->m_LabelItem->position->setTypeX(QCPItemPosition::ptAbsolute); + impl->m_LabelItem->position->setTypeY(QCPItemPosition::ptAbsolute); + + auto font = impl->m_LabelItem->font(); + font.setPointSize(10); + font.setBold(true); + impl->m_LabelItem->setFont(font); + + impl->updateOrientation(); + impl->updateLabelText(); + impl->updateCursorPosition(); + } + else { + Q_ASSERT(impl->m_LineItem && impl->m_Plot); + + // Note: the items are destroyed by QCustomPlot in removeItem + impl->m_Plot->removeItem(impl->m_LineItem); + impl->m_LineItem = nullptr; + impl->m_Plot->removeItem(impl->m_LabelItem); + impl->m_LabelItem = nullptr; + } + } +} + +bool VisualizationCursorItem::isVisible() const +{ + return impl->m_LineItem != nullptr; +} + +void VisualizationCursorItem::setPosition(double value) +{ + impl->m_Position = value; + impl->m_IsAbsolutePosition = false; + impl->updateLabelText(); + impl->updateCursorPosition(); +} + +void VisualizationCursorItem::setAbsolutePosition(double value) +{ + setPosition(value); + impl->m_IsAbsolutePosition = true; +} + +void VisualizationCursorItem::setOrientation(Qt::Orientation orientation) +{ + impl->m_Orientation = orientation; + impl->updateLabelText(); + impl->updateOrientation(); + impl->updateCursorPosition(); +} + +void VisualizationCursorItem::setLabelText(const QString &text) +{ + impl->m_LabelText = text; + impl->updateLabelText(); +} diff --git a/gui/src/Visualization/VisualizationDragDropContainer.cpp b/gui/src/Visualization/VisualizationDragDropContainer.cpp index df5b3f0..caa8ef6 100644 --- a/gui/src/Visualization/VisualizationDragDropContainer.cpp +++ b/gui/src/Visualization/VisualizationDragDropContainer.cpp @@ -21,7 +21,7 @@ struct VisualizationDragDropContainer::VisualizationDragDropContainerPrivate { QVBoxLayout *m_Layout; QHash m_AcceptedMimeTypes; QString m_PlaceHolderText; - DragDropHelper::PlaceHolderType m_PlaceHolderType = DragDropHelper::PlaceHolderType::Graph; + DragDropHelper::PlaceHolderType m_PlaceHolderType; VisualizationDragDropContainer::AcceptMimeDataFunction m_AcceptMimeDataFun = [](auto mimeData) { return true; }; @@ -29,6 +29,7 @@ struct VisualizationDragDropContainer::VisualizationDragDropContainerPrivate { int m_MinContainerHeight = 0; explicit VisualizationDragDropContainerPrivate(QWidget *widget) + : m_PlaceHolderType(DragDropHelper::PlaceHolderType::Graph) { m_Layout = new QVBoxLayout(widget); m_Layout->setContentsMargins(0, 0, 0, 0); diff --git a/gui/src/Visualization/VisualizationGraphWidget.cpp b/gui/src/Visualization/VisualizationGraphWidget.cpp index 962f32a..8671838 100644 --- a/gui/src/Visualization/VisualizationGraphWidget.cpp +++ b/gui/src/Visualization/VisualizationGraphWidget.cpp @@ -1,5 +1,6 @@ #include "Visualization/VisualizationGraphWidget.h" #include "Visualization/IVisualizationWidgetVisitor.h" +#include "Visualization/VisualizationCursorItem.h" #include "Visualization/VisualizationDefs.h" #include "Visualization/VisualizationGraphHelper.h" #include "Visualization/VisualizationGraphRenderingDelegate.h" @@ -37,6 +38,9 @@ const auto VERTICAL_PAN_MODIFIER = Qt::AltModifier; /// Minimum size for the zoom box, in percentage of the axis range const auto ZOOM_BOX_MIN_SIZE = 0.8; +/// Format of the dates appearing in the label of a cursor +const auto CURSOR_LABELS_DATETIME_FORMAT = QStringLiteral("yyyy/MM/dd\nhh:mm:ss:zzz"); + } // namespace struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { @@ -58,6 +62,8 @@ struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { std::unique_ptr m_RenderingDelegate; QCPItemRect *m_DrawingRect = nullptr; + std::unique_ptr m_HorizontalCursor = nullptr; + std::unique_ptr m_VerticalCursor = nullptr; void configureDrawingRect() { @@ -96,6 +102,14 @@ struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { auto axisY = plot.axisRect()->axis(QCPAxis::atLeft); return QPointF{axisX->pixelToCoord(pos.x()), axisY->pixelToCoord(pos.y())}; } + + bool pointIsInAxisRect(const QPointF &axisPoint, QCustomPlot &plot) const + { + auto axisX = plot.axisRect()->axis(QCPAxis::atBottom); + auto axisY = plot.axisRect()->axis(QCPAxis::atLeft); + + return axisX->range().contains(axisPoint.x()) && axisY->range().contains(axisPoint.y()); + } }; VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget *parent) @@ -116,6 +130,12 @@ VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget // The delegate must be initialized after the ui as it uses the plot impl->m_RenderingDelegate = std::make_unique(*this); + // Init the cursors + impl->m_HorizontalCursor = std::make_unique(&plot()); + impl->m_HorizontalCursor->setOrientation(Qt::Horizontal); + impl->m_VerticalCursor = std::make_unique(&plot()); + impl->m_VerticalCursor->setOrientation(Qt::Vertical); + connect(ui->widget, &QCustomPlot::mousePress, this, &VisualizationGraphWidget::onMousePress); connect(ui->widget, &QCustomPlot::mouseRelease, this, &VisualizationGraphWidget::onMouseRelease); @@ -308,6 +328,55 @@ void VisualizationGraphWidget::highlightForMerge(bool highlighted) plot().update(); } +void VisualizationGraphWidget::addVerticalCursor(double time) +{ + impl->m_VerticalCursor->setPosition(time); + impl->m_VerticalCursor->setVisible(true); + + auto text + = DateUtils::dateTime(time).toString(CURSOR_LABELS_DATETIME_FORMAT).replace(' ', '\n'); + impl->m_VerticalCursor->setLabelText(text); +} + +void VisualizationGraphWidget::addVerticalCursorAtViewportPosition(double position) +{ + impl->m_VerticalCursor->setAbsolutePosition(position); + impl->m_VerticalCursor->setVisible(true); + + auto axis = plot().axisRect()->axis(QCPAxis::atBottom); + auto text + = DateUtils::dateTime(axis->pixelToCoord(position)).toString(CURSOR_LABELS_DATETIME_FORMAT); + impl->m_VerticalCursor->setLabelText(text); +} + +void VisualizationGraphWidget::removeVerticalCursor() +{ + impl->m_VerticalCursor->setVisible(false); + plot().replot(QCustomPlot::rpQueuedReplot); +} + +void VisualizationGraphWidget::addHorizontalCursor(double value) +{ + impl->m_HorizontalCursor->setPosition(value); + impl->m_HorizontalCursor->setVisible(true); + impl->m_HorizontalCursor->setLabelText(QString::number(value)); +} + +void VisualizationGraphWidget::addHorizontalCursorAtViewportPosition(double position) +{ + impl->m_HorizontalCursor->setAbsolutePosition(position); + impl->m_HorizontalCursor->setVisible(true); + + auto axis = plot().axisRect()->axis(QCPAxis::atLeft); + impl->m_HorizontalCursor->setLabelText(QString::number(axis->pixelToCoord(position))); +} + +void VisualizationGraphWidget::removeHorizontalCursor() +{ + impl->m_HorizontalCursor->setVisible(false); + plot().replot(QCustomPlot::rpQueuedReplot); +} + void VisualizationGraphWidget::closeEvent(QCloseEvent *event) { Q_UNUSED(event); @@ -328,6 +397,13 @@ void VisualizationGraphWidget::leaveEvent(QEvent *event) { Q_UNUSED(event); impl->m_RenderingDelegate->showGraphOverlay(false); + + if (auto parentZone = parentZoneWidget()) { + parentZone->notifyMouseLeaveGraph(this); + } + else { + qCWarning(LOG_VisualizationGraphWidget()) << "leaveEvent: No parent zone widget"; + } } QCustomPlot &VisualizationGraphWidget::plot() noexcept @@ -380,6 +456,20 @@ void VisualizationGraphWidget::onRangeChanged(const QCPRange &t1, const QCPRange emit synchronize(graphRange, oldGraphRange); } } + + auto pos = mapFromGlobal(QCursor::pos()); + auto axisPos = impl->posToAxisPos(pos, plot()); + if (auto parentZone = parentZoneWidget()) { + if (impl->pointIsInAxisRect(axisPos, plot())) { + parentZone->notifyMouseMoveInGraph(pos, axisPos, this); + } + else { + parentZone->notifyMouseLeaveGraph(this); + } + } + else { + qCWarning(LOG_VisualizationGraphWidget()) << "onMouseMove: No parent zone widget"; + } } void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept @@ -387,11 +477,24 @@ void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept // Handles plot rendering when mouse is moving impl->m_RenderingDelegate->onMouseMove(event); + auto axisPos = impl->posToAxisPos(event->pos(), plot()); + if (impl->m_DrawingRect) { - auto axisPos = impl->posToAxisPos(event->pos(), plot()); impl->m_DrawingRect->bottomRight->setCoords(axisPos); } + if (auto parentZone = parentZoneWidget()) { + if (impl->pointIsInAxisRect(axisPos, plot())) { + parentZone->notifyMouseMoveInGraph(event->pos(), axisPos, this); + } + else { + parentZone->notifyMouseLeaveGraph(this); + } + } + else { + qCWarning(LOG_VisualizationGraphWidget()) << "onMouseMove: No parent zone widget"; + } + VisualizationDragWidget::mouseMoveEvent(event); } diff --git a/gui/src/Visualization/VisualizationZoneWidget.cpp b/gui/src/Visualization/VisualizationZoneWidget.cpp index 3a52978..ed90b7c 100644 --- a/gui/src/Visualization/VisualizationZoneWidget.cpp +++ b/gui/src/Visualization/VisualizationZoneWidget.cpp @@ -53,7 +53,7 @@ void processGraphs(QLayout &layout, Fun fun) for (auto i = 0; i < layout.count(); ++i) { if (auto item = layout.itemAt(i)) { if (auto visualizationGraphWidget - = dynamic_cast(item->widget())) { + = qobject_cast(item->widget())) { fun(*visualizationGraphWidget); } } @@ -347,6 +347,59 @@ bool VisualizationZoneWidget::isDragAllowed() const return true; } +void VisualizationZoneWidget::notifyMouseMoveInGraph(const QPointF &graphPosition, + const QPointF &plotPosition, + VisualizationGraphWidget *graphWidget) +{ + processGraphs(*ui->dragDropContainer->layout(), [&graphPosition, &plotPosition, &graphWidget]( + VisualizationGraphWidget &processedGraph) { + + switch (sqpApp->plotsCursorMode()) { + case SqpApplication::PlotsCursorMode::Vertical: + processedGraph.removeHorizontalCursor(); + processedGraph.addVerticalCursorAtViewportPosition(graphPosition.x()); + break; + case SqpApplication::PlotsCursorMode::Temporal: + processedGraph.addVerticalCursor(plotPosition.x()); + processedGraph.removeHorizontalCursor(); + break; + case SqpApplication::PlotsCursorMode::Horizontal: + processedGraph.removeVerticalCursor(); + if (&processedGraph == graphWidget) { + processedGraph.addHorizontalCursorAtViewportPosition(graphPosition.y()); + } + else { + processedGraph.removeHorizontalCursor(); + } + break; + case SqpApplication::PlotsCursorMode::Cross: + if (&processedGraph == graphWidget) { + processedGraph.addVerticalCursorAtViewportPosition(graphPosition.x()); + processedGraph.addHorizontalCursorAtViewportPosition(graphPosition.y()); + } + else { + processedGraph.removeHorizontalCursor(); + processedGraph.removeVerticalCursor(); + } + break; + case SqpApplication::PlotsCursorMode::NoCursor: + processedGraph.removeHorizontalCursor(); + processedGraph.removeVerticalCursor(); + break; + } + + + }); +} + +void VisualizationZoneWidget::notifyMouseLeaveGraph(VisualizationGraphWidget *graphWidget) +{ + processGraphs(*ui->dragDropContainer->layout(), [](VisualizationGraphWidget &processedGraph) { + processedGraph.removeHorizontalCursor(); + processedGraph.removeVerticalCursor(); + }); +} + void VisualizationZoneWidget::closeEvent(QCloseEvent *event) { // Closes graphs in the zone