diff --git a/gui/include/Visualization/VisualizationSelectionZoneItem.h b/gui/include/Visualization/VisualizationSelectionZoneItem.h new file mode 100644 index 0000000..25220c8 --- /dev/null +++ b/gui/include/Visualization/VisualizationSelectionZoneItem.h @@ -0,0 +1,39 @@ +#ifndef SCIQLOP_VISUALIZATIONSELECTIONZONEITEM_H +#define SCIQLOP_VISUALIZATIONSELECTIONZONEITEM_H + +#include +#include +#include + +class VisualizationSelectionZoneItem : public QCPItemRect { +public: + VisualizationSelectionZoneItem(QCustomPlot *plot); + virtual ~VisualizationSelectionZoneItem(); + + void setName(const QString &name); + QString name() const; + + SqpRange range() const; + void setRange(double tstart, double tend); + void setStart(double tstart); + void setEnd(double tend); + + void setColor(const QColor &color); + + void setEditionEnabled(bool value); + bool isEditionEnabled() const; + + Qt::CursorShape curshorShapeForPosition(const QPoint &position) const; + void setHovered(bool value); + +protected: + void mousePressEvent(QMouseEvent *event, const QVariant &details) override; + void mouseMoveEvent(QMouseEvent *event, const QPointF &startPos) override; + void mouseReleaseEvent(QMouseEvent *event, const QPointF &startPos) override; + +private: + class VisualizationSelectionZoneItemPrivate; + spimpl::unique_impl_ptr impl; +}; + +#endif // SCIQLOP_VISUALIZATIONSELECTIONZONEITEM_H diff --git a/gui/meson.build b/gui/meson.build index 8d3e873..c350f40 100644 --- a/gui/meson.build +++ b/gui/meson.build @@ -82,7 +82,8 @@ gui_sources = [ 'src/Visualization/VisualizationCursorItem.cpp', 'src/Visualization/ColorScaleEditor.cpp', 'src/Visualization/SqpColorScale.cpp', - 'src/Visualization/QCPColorMapIterator.cpp' + 'src/Visualization/QCPColorMapIterator.cpp', + 'src/Visualization/VisualizationSelectionZoneItem.cpp' ] gui_inc = include_directories(['include']) diff --git a/gui/src/Visualization/VisualizationGraphRenderingDelegate.cpp b/gui/src/Visualization/VisualizationGraphRenderingDelegate.cpp index bb70482..1cb0d7e 100644 --- a/gui/src/Visualization/VisualizationGraphRenderingDelegate.cpp +++ b/gui/src/Visualization/VisualizationGraphRenderingDelegate.cpp @@ -48,6 +48,7 @@ void initPointTracerStyle(QCPItemTracer &tracer) noexcept tracer.setSize(3); tracer.setPen(QPen(Qt::black)); tracer.setBrush(Qt::black); + tracer.setSelectable(false); } QPixmap pixmap(const QString &iconPath) noexcept @@ -94,6 +95,7 @@ void initTitleTextStyle(QCPItemText &text) noexcept text.setPositionAlignment(Qt::AlignTop | Qt::AlignLeft); text.position->setType(QCPItemPosition::ptAxisRectRatio); text.position->setCoords(0.5, 0); + text.setSelectable(false); } /** diff --git a/gui/src/Visualization/VisualizationGraphWidget.cpp b/gui/src/Visualization/VisualizationGraphWidget.cpp index bc9cf8d..c9145ea 100644 --- a/gui/src/Visualization/VisualizationGraphWidget.cpp +++ b/gui/src/Visualization/VisualizationGraphWidget.cpp @@ -4,6 +4,7 @@ #include "Visualization/VisualizationDefs.h" #include "Visualization/VisualizationGraphHelper.h" #include "Visualization/VisualizationGraphRenderingDelegate.h" +#include "Visualization/VisualizationSelectionZoneItem.h" #include "Visualization/VisualizationZoneWidget.h" #include "ui_VisualizationGraphWidget.h" @@ -71,18 +72,14 @@ struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { /// Delegate used to attach rendering features to the plot std::unique_ptr m_RenderingDelegate; - QCPItemRect *m_DrawingRect = nullptr; + QCPItemRect *m_DrawingZoomRect = nullptr; + std::unique_ptr m_HorizontalCursor = nullptr; std::unique_ptr m_VerticalCursor = nullptr; - void configureDrawingRect() - { - if (m_DrawingRect) { - QPen p; - p.setWidth(2); - m_DrawingRect->setPen(p); - } - } + VisualizationSelectionZoneItem *m_DrawingZone = nullptr; + VisualizationSelectionZoneItem *m_HoveredZone = nullptr; + QVector m_SelectionZones; void startDrawingRect(const QPoint &pos, QCustomPlot &plot) { @@ -90,22 +87,59 @@ struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { auto axisPos = posToAxisPos(pos, plot); - m_DrawingRect = new QCPItemRect{&plot}; - configureDrawingRect(); + m_DrawingZoomRect = new QCPItemRect{&plot}; + QPen p; + p.setWidth(2); + m_DrawingZoomRect->setPen(p); - m_DrawingRect->topLeft->setCoords(axisPos); - m_DrawingRect->bottomRight->setCoords(axisPos); + m_DrawingZoomRect->topLeft->setCoords(axisPos); + m_DrawingZoomRect->bottomRight->setCoords(axisPos); } void removeDrawingRect(QCustomPlot &plot) { - if (m_DrawingRect) { - plot.removeItem(m_DrawingRect); // the item is deleted by QCustomPlot - m_DrawingRect = nullptr; + if (m_DrawingZoomRect) { + plot.removeItem(m_DrawingZoomRect); // the item is deleted by QCustomPlot + m_DrawingZoomRect = nullptr; plot.replot(QCustomPlot::rpQueuedReplot); } } + void startDrawingZone(const QPoint &pos, QCustomPlot &plot) + { + endDrawingZone(plot); + + auto axisPos = posToAxisPos(pos, plot); + + m_DrawingZone = new VisualizationSelectionZoneItem{&plot}; + m_DrawingZone->setRange(axisPos.x(), axisPos.x()); + m_DrawingZone->setEditionEnabled(false); + } + + void endDrawingZone(QCustomPlot &plot) + { + if (m_DrawingZone) { + auto drawingZoneRange = m_DrawingZone->range(); + if (qAbs(drawingZoneRange.m_TEnd - drawingZoneRange.m_TStart) > 0) { + m_DrawingZone->setEditionEnabled(true); + m_SelectionZones.append(m_DrawingZone); + } + else { + plot.removeItem(m_DrawingZone); // the item is deleted by QCustomPlot + } + + plot.replot(QCustomPlot::rpQueuedReplot); + m_DrawingZone = nullptr; + } + } + + void setSelectionZonesEditionEnabled(bool value) + { + for (auto s : m_SelectionZones) { + s->setEditionEnabled(value); + } + } + QPointF posToAxisPos(const QPoint &pos, QCustomPlot &plot) const { auto axisX = plot.axisRect()->axis(QCPAxis::atBottom); @@ -117,7 +151,6 @@ struct VisualizationGraphWidget::VisualizationGraphWidgetPrivate { { 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()); } }; @@ -133,7 +166,7 @@ VisualizationGraphWidget::VisualizationGraphWidget(const QString &name, QWidget setAttribute(Qt::WA_DeleteOnClose); // Set qcpplot properties : - // - Drag (on x-axis) and zoom are enabled + // - zoom is enabled // - Mouse wheel on qcpplot is intercepted to determine the zoom orientation ui->widget->setInteractions(QCP::iRangeZoom | QCP::iSelectItems); @@ -428,6 +461,11 @@ void VisualizationGraphWidget::leaveEvent(QEvent *event) else { qCWarning(LOG_VisualizationGraphWidget()) << "leaveEvent: No parent zone widget"; } + + if (impl->m_HoveredZone) { + impl->m_HoveredZone->setHovered(false); + impl->m_HoveredZone = nullptr; + } } QCustomPlot &VisualizationGraphWidget::plot() noexcept @@ -508,10 +546,15 @@ void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept auto axisPos = impl->posToAxisPos(event->pos(), plot()); - if (impl->m_DrawingRect) { - impl->m_DrawingRect->bottomRight->setCoords(axisPos); + // Zoom box and zone drawing + if (impl->m_DrawingZoomRect) { + impl->m_DrawingZoomRect->bottomRight->setCoords(axisPos); + } + else if (impl->m_DrawingZone) { + impl->m_DrawingZone->setEnd(axisPos.x()); } + // Cursor if (auto parentZone = parentZoneWidget()) { if (impl->pointIsInAxisRect(axisPos, plot())) { parentZone->notifyMouseMoveInGraph(event->pos(), axisPos, this); @@ -524,6 +567,44 @@ void VisualizationGraphWidget::onMouseMove(QMouseEvent *event) noexcept qCWarning(LOG_VisualizationGraphWidget()) << "onMouseMove: No parent zone widget"; } + // Search for the selection zone under the mouse + VisualizationSelectionZoneItem *selectionZoneItemUnderCursor = nullptr; + auto minDistanceToZone = -1; + for (auto zone : impl->m_SelectionZones) { + auto distanceToZone = zone->selectTest(event->pos(), true); + if ((minDistanceToZone < 0 || distanceToZone <= minDistanceToZone) && distanceToZone >= 0 + && distanceToZone < plot().selectionTolerance()) { + selectionZoneItemUnderCursor = zone; + } + } + + if (selectionZoneItemUnderCursor && !impl->m_DrawingZone + && sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::SelectionZones) { + + // Sets the appropriate cursor shape + auto cursorShape = selectionZoneItemUnderCursor->curshorShapeForPosition(event->pos()); + setCursor(cursorShape); + + // Manages the hovered zone + if (selectionZoneItemUnderCursor != impl->m_HoveredZone) { + if (impl->m_HoveredZone) { + impl->m_HoveredZone->setHovered(false); + } + selectionZoneItemUnderCursor->setHovered(true); + impl->m_HoveredZone = selectionZoneItemUnderCursor; + plot().replot(QCustomPlot::rpQueuedReplot); + } + } + else { + // There is no zone under the mouse or the interaction mode is not "selection zones" + if (impl->m_HoveredZone) { + impl->m_HoveredZone->setHovered(false); + impl->m_HoveredZone = nullptr; + } + + setCursor(Qt::ArrowCursor); + } + VisualizationDragWidget::mouseMoveEvent(event); } @@ -561,24 +642,41 @@ void VisualizationGraphWidget::onMouseWheel(QWheelEvent *event) noexcept void VisualizationGraphWidget::onMousePress(QMouseEvent *event) noexcept { if (sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::ZoomBox) { + // Starts a zoom box impl->startDrawingRect(event->pos(), plot()); } + else if (sqpApp->plotsInteractionMode() == SqpApplication::PlotsInteractionMode::SelectionZones + && impl->m_DrawingZone == nullptr) { + // Starts a new selection zone + auto itemAtPos = plot().itemAt(event->pos(), true); + if (!itemAtPos) { + impl->startDrawingZone(event->pos(), plot()); + } + } + + // Allows mouse panning only in default mode + plot().setInteraction(QCP::iRangeDrag, sqpApp->plotsInteractionMode() + == SqpApplication::PlotsInteractionMode::None); + + // Allows zone edition only in selection zone mode + impl->setSelectionZonesEditionEnabled(sqpApp->plotsInteractionMode() + == SqpApplication::PlotsInteractionMode::SelectionZones); VisualizationDragWidget::mousePressEvent(event); } void VisualizationGraphWidget::onMouseRelease(QMouseEvent *event) noexcept { - if (impl->m_DrawingRect) { + if (impl->m_DrawingZoomRect) { auto axisX = plot().axisRect()->axis(QCPAxis::atBottom); auto axisY = plot().axisRect()->axis(QCPAxis::atLeft); - auto newAxisXRange = QCPRange{impl->m_DrawingRect->topLeft->coords().x(), - impl->m_DrawingRect->bottomRight->coords().x()}; + auto newAxisXRange = QCPRange{impl->m_DrawingZoomRect->topLeft->coords().x(), + impl->m_DrawingZoomRect->bottomRight->coords().x()}; - auto newAxisYRange = QCPRange{impl->m_DrawingRect->topLeft->coords().y(), - impl->m_DrawingRect->bottomRight->coords().y()}; + auto newAxisYRange = QCPRange{impl->m_DrawingZoomRect->topLeft->coords().y(), + impl->m_DrawingZoomRect->bottomRight->coords().y()}; impl->removeDrawingRect(plot()); @@ -591,6 +689,8 @@ void VisualizationGraphWidget::onMouseRelease(QMouseEvent *event) noexcept } } + impl->endDrawingZone(plot()); + impl->m_IsCalibration = false; } diff --git a/gui/src/Visualization/VisualizationSelectionZoneItem.cpp b/gui/src/Visualization/VisualizationSelectionZoneItem.cpp new file mode 100644 index 0000000..a5665aa --- /dev/null +++ b/gui/src/Visualization/VisualizationSelectionZoneItem.cpp @@ -0,0 +1,271 @@ +#include "Visualization/VisualizationSelectionZoneItem.h" + +struct VisualizationSelectionZoneItem::VisualizationSelectionZoneItemPrivate { + + QCustomPlot *m_Plot; + double m_T1 = 0; + double m_T2 = 0; + QColor m_Color; + + bool m_IsEditionEnabled = true; + double m_MovedOrinalT1 = 0; + double m_MovedOrinalT2 = 0; + + QCPItemStraightLine *m_LeftLine; + QCPItemStraightLine *m_RightLine; + QCPItemText *m_NameLabelItem = nullptr; + + enum class EditionMode { NoEdition, ResizeLeft, ResizeRight, Move }; + EditionMode m_CurrentEditionMode; + + VisualizationSelectionZoneItemPrivate(QCustomPlot *plot) + : m_Plot(plot), m_Color(Qt::blue), m_CurrentEditionMode(EditionMode::NoEdition) + { + } + + void updatePosition(VisualizationSelectionZoneItem *item) + { + item->topLeft->setCoords(m_T1, 0); + item->bottomRight->setCoords(m_T2, 1); + } + + EditionMode getEditionMode(const QPoint &pos, const VisualizationSelectionZoneItem *zoneItem) + { + auto distanceLeft = m_LeftLine->selectTest(pos, false); + auto distanceRight = m_RightLine->selectTest(pos, false); + auto distance = zoneItem->selectTest(pos, true); + + if (distanceRight <= distance) { + return VisualizationSelectionZoneItemPrivate::EditionMode::ResizeRight; + } + else if (distanceLeft <= distance) { + return VisualizationSelectionZoneItemPrivate::EditionMode::ResizeLeft; + } + + return VisualizationSelectionZoneItemPrivate::EditionMode::Move; + } +}; + +VisualizationSelectionZoneItem::VisualizationSelectionZoneItem(QCustomPlot *plot) + : QCPItemRect(plot), + impl{spimpl::make_unique_impl(plot)} +{ + topLeft->setTypeX(QCPItemPosition::ptPlotCoords); + topLeft->setTypeY(QCPItemPosition::ptAxisRectRatio); + bottomRight->setTypeX(QCPItemPosition::ptPlotCoords); + bottomRight->setTypeY(QCPItemPosition::ptAxisRectRatio); + + impl->m_RightLine = new QCPItemStraightLine(plot); + impl->m_RightLine->point1->setParentAnchor(topRight); + impl->m_RightLine->point2->setParentAnchor(bottomRight); + impl->m_RightLine->point1->setTypeX(QCPItemPosition::ptAbsolute); + impl->m_RightLine->point1->setTypeY(QCPItemPosition::ptAbsolute); + impl->m_RightLine->point2->setTypeX(QCPItemPosition::ptAbsolute); + impl->m_RightLine->point2->setTypeY(QCPItemPosition::ptAbsolute); + + impl->m_LeftLine = new QCPItemStraightLine(plot); + impl->m_LeftLine->point1->setParentAnchor(topLeft); + impl->m_LeftLine->point2->setParentAnchor(bottomLeft); + impl->m_LeftLine->point1->setTypeX(QCPItemPosition::ptAbsolute); + impl->m_LeftLine->point1->setTypeY(QCPItemPosition::ptAbsolute); + impl->m_LeftLine->point2->setTypeX(QCPItemPosition::ptAbsolute); + impl->m_LeftLine->point2->setTypeY(QCPItemPosition::ptAbsolute); + + impl->m_RightLine->setSelectable(false); + impl->m_LeftLine->setSelectable(false); + + // connect(this, &VisualizationSelectionZoneItem::selectionChanged, impl->m_RightLine, + // &QCPItemStraightLine::setSelected); + // connect(this, &VisualizationSelectionZoneItem::selectionChanged, impl->m_LeftLine, + // &QCPItemStraightLine::setSelected); + + setColor(QColor("#E79D41")); +} + +VisualizationSelectionZoneItem::~VisualizationSelectionZoneItem() +{ + impl->m_Plot->removeItem(impl->m_RightLine); + impl->m_Plot->removeItem(impl->m_LeftLine); +} + +void VisualizationSelectionZoneItem::setName(const QString &name) +{ + if (name.isEmpty() && impl->m_NameLabelItem) { + impl->m_Plot->removeItem(impl->m_NameLabelItem); + impl->m_NameLabelItem = nullptr; + } + else if (!impl->m_NameLabelItem) { + impl->m_NameLabelItem = new QCPItemText(impl->m_Plot); + impl->m_NameLabelItem->setText(name); + impl->m_NameLabelItem->setPositionAlignment(Qt::AlignHCenter | Qt::AlignTop); + impl->m_NameLabelItem->setColor(impl->m_Color); + impl->m_NameLabelItem->position->setParentAnchor(top); + } +} + +QString VisualizationSelectionZoneItem::name() const +{ + if (!impl->m_NameLabelItem) { + return QString(); + } + + return impl->m_NameLabelItem->text(); +} + +SqpRange VisualizationSelectionZoneItem::range() const +{ + SqpRange range; + range.m_TStart = impl->m_T1 <= impl->m_T2 ? impl->m_T1 : impl->m_T2; + range.m_TEnd = impl->m_T1 > impl->m_T2 ? impl->m_T1 : impl->m_T2; + return range; +} + +void VisualizationSelectionZoneItem::setRange(double tstart, double tend) +{ + impl->m_T1 = tstart; + impl->m_T2 = tend; + impl->updatePosition(this); +} + +void VisualizationSelectionZoneItem::setStart(double tstart) +{ + impl->m_T1 = tstart; + impl->updatePosition(this); +} + +void VisualizationSelectionZoneItem::setEnd(double tend) +{ + impl->m_T2 = tend; + impl->updatePosition(this); +} + +void VisualizationSelectionZoneItem::setColor(const QColor &color) +{ + impl->m_Color = color; + + auto brushColor = color; + brushColor.setAlpha(40); + setBrush(QBrush(brushColor)); + setPen(QPen(Qt::NoPen)); + + auto selectedBrushColor = brushColor; + selectedBrushColor.setAlpha(65); + setSelectedBrush(QBrush(selectedBrushColor)); + setSelectedPen(QPen(Qt::NoPen)); + + auto linePen = QPen(color); + linePen.setStyle(Qt::SolidLine); + linePen.setWidth(2); + + auto selectedLinePen = linePen; + selectedLinePen.setColor(color.darker(30)); + + impl->m_LeftLine->setPen(linePen); + impl->m_RightLine->setPen(linePen); + + impl->m_LeftLine->setSelectedPen(selectedLinePen); + impl->m_RightLine->setSelectedPen(selectedLinePen); +} + +void VisualizationSelectionZoneItem::setEditionEnabled(bool value) +{ + impl->m_IsEditionEnabled = value; + setSelectable(value); + if (!value) { + setSelected(false); + } +} + +bool VisualizationSelectionZoneItem::isEditionEnabled() const +{ + return impl->m_IsEditionEnabled; +} + +Qt::CursorShape +VisualizationSelectionZoneItem::curshorShapeForPosition(const QPoint &position) const +{ + auto mode = impl->m_CurrentEditionMode + == VisualizationSelectionZoneItemPrivate::EditionMode::NoEdition + ? impl->getEditionMode(position, this) + : impl->m_CurrentEditionMode; + switch (mode) { + case VisualizationSelectionZoneItemPrivate::EditionMode::Move: + return Qt::SizeAllCursor; + case VisualizationSelectionZoneItemPrivate::EditionMode::ResizeLeft: + case VisualizationSelectionZoneItemPrivate::EditionMode::ResizeRight: // fallthrough + return Qt::SizeHorCursor; + default: + return Qt::ArrowCursor; + } +} + +void VisualizationSelectionZoneItem::setHovered(bool value) +{ + if (value) { + auto linePen = impl->m_LeftLine->pen(); + linePen.setStyle(Qt::DotLine); + linePen.setWidth(3); + + auto selectedLinePen = impl->m_LeftLine->selectedPen(); + ; + selectedLinePen.setStyle(Qt::DotLine); + selectedLinePen.setWidth(3); + + impl->m_LeftLine->setPen(linePen); + impl->m_RightLine->setPen(linePen); + + impl->m_LeftLine->setSelectedPen(selectedLinePen); + impl->m_RightLine->setSelectedPen(selectedLinePen); + } + else { + setColor(impl->m_Color); + } +} + +void VisualizationSelectionZoneItem::mousePressEvent(QMouseEvent *event, const QVariant &details) +{ + if (isEditionEnabled()) { + impl->m_CurrentEditionMode = impl->getEditionMode(event->pos(), this); + + impl->m_MovedOrinalT1 = impl->m_T1; + impl->m_MovedOrinalT2 = impl->m_T2; + } + else { + event->ignore(); + } +} + +void VisualizationSelectionZoneItem::mouseMoveEvent(QMouseEvent *event, const QPointF &startPos) +{ + if (isEditionEnabled()) { + auto axis = impl->m_Plot->axisRect()->axis(QCPAxis::atBottom); + auto diff = axis->pixelToCoord(event->pos().x()) - axis->pixelToCoord(startPos.x()); + + switch (impl->m_CurrentEditionMode) { + case VisualizationSelectionZoneItemPrivate::EditionMode::Move: + setRange(impl->m_MovedOrinalT1 + diff, impl->m_MovedOrinalT2 + diff); + break; + case VisualizationSelectionZoneItemPrivate::EditionMode::ResizeLeft: + setStart(impl->m_MovedOrinalT1 + diff); + break; + case VisualizationSelectionZoneItemPrivate::EditionMode::ResizeRight: + setEnd(impl->m_MovedOrinalT2 + diff); + break; + // default: + // unknown edition mode + } + } + else { + event->ignore(); + } +} + +void VisualizationSelectionZoneItem::mouseReleaseEvent(QMouseEvent *event, const QPointF &startPos) +{ + if (isEditionEnabled()) { + impl->m_CurrentEditionMode = VisualizationSelectionZoneItemPrivate::EditionMode::NoEdition; + } + else { + event->ignore(); + } +}