// ************************************************************************************************ // // BornAgain: simulate and fit reflection and scattering // //! @file GUI/View/Scene/MaskGraphicsScene.cpp //! @brief Implements class MaskGraphicsScene. //! //! @homepage http://www.bornagainproject.org //! @license GNU General Public License v3 or higher (see COPYING) //! @copyright Forschungszentrum Jülich GmbH 2018 //! @authors Scientific Computing Group at MLZ (see CITATION, AUTHORS) // // ************************************************************************************************ #include "GUI/View/Scene/MaskGraphicsScene.h" #include "Base/Util/Assert.h" #include "GUI/Model/Data/Data2DItem.h" #include "GUI/Model/Mask/MasksSet.h" #include "GUI/Model/Mask/PointItem.h" #include "GUI/Model/Project/ProjectDocument.h" #include "GUI/View/Overlay/EllipseOverlay.h" #include "GUI/View/Overlay/FullframeOverlay.h" #include "GUI/View/Overlay/LineOverlays.h" #include "GUI/View/Overlay/PolygonOverlay.h" #include "GUI/View/Overlay/ROIOverlay.h" #include "GUI/View/Overlay/RectangleOverlay.h" #include "GUI/View/Overlay/SizeHandle.h" #include "GUI/View/Overlay/VertexOverlay.h" #include "GUI/View/Overlay/Viewport.h" #include "GUI/View/Plotter/ColorMap.h" #include "GUI/View/Scene/MaskGraphicsProxy.h" #include <QGraphicsSceneContextMenuEvent> namespace { const qreal min_distance_to_create_rect = 10; //! Return true if area beneath the mouse contains views of given type. template <class T> bool areaContains(QVector<QGraphicsItem*> items) { for (QGraphicsItem* item : items) if (dynamic_cast<T*>(item)) return true; return false; } IOverlay* createOverlay(OverlayItem* item, ColorMap* plot) { if (auto* mask = dynamic_cast<RectangleItem*>(item)) return new RectangleOverlay(mask, plot); if (auto* mask = dynamic_cast<PolygonItem*>(item)) return new PolygonOverlay(mask, plot); if (auto* mask = dynamic_cast<PointItem*>(item)) return new VertexOverlay(mask, plot); if (auto* mask = dynamic_cast<VerticalLineItem*>(item)) return new VerticalLineOverlay(mask, plot); if (auto* mask = dynamic_cast<HorizontalLineItem*>(item)) return new HorizontalLineOverlay(mask, plot); if (auto* mask = dynamic_cast<EllipseItem*>(item)) return new EllipseOverlay(mask, plot); if (auto* mask = dynamic_cast<FullframeItem*>(item)) return new FullframeOverlay(mask, plot); if (auto* mask = dynamic_cast<RegionOfInterestItem*>(item)) return new ROIOverlay(mask, plot); ASSERT_NEVER; } } // namespace MaskGraphicsScene::MaskGraphicsScene() { m_plot = std::make_unique<ColorMap>(); // otherwise segfault when switching back (#989) m_proxy = new MaskGraphicsProxy; m_proxy->setWidget(m_plot.get()); addItem(m_proxy); // takes ownership } MaskGraphicsScene::~MaskGraphicsScene() = default; void MaskGraphicsScene::associateItems(Data2DItem* data_item) { ASSERT(data_item); m_data_item = data_item; m_plot->itemToMap(data_item); m_masks = data_item->masksRW(); m_prjns = data_item->prjnsRW(); ASSERT(m_masks); ASSERT(m_prjns); connect(m_masks, &MasksSet::setChanged, this, &MaskGraphicsScene::updateMost); connect(m_prjns, &MasksSet::setChanged, this, &MaskGraphicsScene::updateMost); updateMost(); } void MaskGraphicsScene::updateSize(const QSize& newSize) { if (!m_proxy) return; m_proxy->resize(newSize); setSceneRect(0, 0, newSize.width(), newSize.height()); m_proxy->setPos(0, 0); } // ************************************************************************************************ // public slots // ************************************************************************************************ void MaskGraphicsScene::onActivityChanged(Canvas2DMode::Flag mode) { if (!m_proxy) return; if (m_drawing_in_progress && m_mode == Canvas2DMode::POLYGON && mode >= Canvas2DMode::PAN_ZOOM) cancelCurrentDrawing(); m_mode = mode; const bool zooming = m_mode == Canvas2DMode::PAN_ZOOM; m_proxy->setZooming(zooming); for (auto const& [item, overlay] : m_mask2overlay) { overlay->setAcceptedMouseButtons(zooming ? Qt::NoButton : Qt::LeftButton); overlay->setCursor(zooming ? Qt::ArrowCursor : Qt::SizeAllCursor); } m_plot->setCursor(zooming ? Qt::PointingHandCursor : Qt::ArrowCursor); } void MaskGraphicsScene::cancelCurrentDrawing() { if (!m_drawing_in_progress) return; ASSERT(m_active_mask); m_masks->delete_current(); setDrawingInProgress(false); } // ************************************************************************************************ // private slots // ************************************************************************************************ // ************************************************************************************************ // private (override QGraphicsScene) // ************************************************************************************************ void MaskGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() & Qt::LeftButton) m_mouse_is_pressed = true; if (event->buttons() & Qt::RightButton) { if (m_drawing_in_progress) cancelCurrentDrawing(); else makeViewAtMousePosSelected(event); return; } if (!isValidMouseClick(event)) return QGraphicsScene::mousePressEvent(event); if (isValidForPolygonDrawing(event)) processPolygonItem(event); else if (isValidForLineDrawing(event)) processLineItem(event); else if (isValidForMaskAllDrawing(event)) processFullframeItem(event); else if (isValidForRectangleShapeDrawing(event)) setDrawingInProgress(true); else QGraphicsScene::mousePressEvent(event); } void MaskGraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { if (m_drawing_in_progress && Canvas2DMode::basedOnRectangle(m_mode)) { processRectangleOrEllipseItem(event); return; } QGraphicsScene::mouseMoveEvent(event); if ((m_drawing_in_progress && m_mode == Canvas2DMode::POLYGON) || Canvas2DMode::isLineMode(m_mode)) { m_mouse_position = event->scenePos(); QGraphicsScene::invalidate(); // Cached content in layers is invalidated and redrawn. } } //! Finalizes item drawing or pass events to other items. void MaskGraphicsScene::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { m_mouse_is_pressed = false; if (!m_drawing_in_progress) QGraphicsScene::mouseReleaseEvent(event); else if (Canvas2DMode::basedOnRectangle(m_mode)) { if (m_active_mask) { // drawing ended up with item drawn, let's make it selected if (IOverlay* overlay = m_mask2overlay[m_active_mask]) overlay->setSelected(true); } else { // drawing ended without item to be draw (too short mouse move) // making item beneath of mouse release position to be selected makeViewAtMousePosSelected(event); } setDrawingInProgress(false); } } //! Draws dashed line to the current mouse position if we are constructing a polygon //! or if we are going to place a horizontal or vertical line. void MaskGraphicsScene::drawForeground(QPainter* painter, const QRectF&) { if (!m_plot) return; if (m_mouse_position == QPointF()) return; painter->setPen(QPen((Canvas2DMode::isPrjn(m_mode) ? QColorConstants::Svg::darkcyan : QColorConstants::Svg::darkolivegreen), 2, Qt::DashLine)); if (const PolygonOverlay* polygon = currentPolygon()) { painter->drawLine(QLineF(polygon->lastAddedPoint(), m_mouse_position)); } else if (Canvas2DMode::isLineMode(m_mode)) { const QRectF& plot_scene_rectangle = GUI::Util::viewportRectangle(m_plot.get()); if (!plot_scene_rectangle.contains(m_mouse_position)) return; if (Canvas2DMode::isVerticalLine(m_mode)) { QPointF p1(m_mouse_position.x(), plot_scene_rectangle.bottom()); QPointF p2(m_mouse_position.x(), plot_scene_rectangle.top()); painter->drawLine(QLineF(p1, p2)); } if (Canvas2DMode::isHorizontalLine(m_mode)) { QPointF p1(plot_scene_rectangle.left(), m_mouse_position.y()); QPointF p2(plot_scene_rectangle.right(), m_mouse_position.y()); painter->drawLine(QLineF(p1, p2)); } } } //! Creates item context menu if there is IMaskView beneath the mouse right click. void MaskGraphicsScene::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { if (m_drawing_in_progress) return; if (dynamic_cast<IOverlay*>(itemAt(event->scenePos(), QTransform()))) emit itemContextMenuRequest(event->screenPos()); } // ************************************************************************************************ // private modifying functions // ************************************************************************************************ void MaskGraphicsScene::updateMost() { disconnect(m_masks); disconnect(m_prjns); m_mask2overlay.clear(); // removes all items from the map for (QGraphicsItem* t : items()) if (t != m_proxy) removeItem(t); updateOverlays(); } //! Runs through the model and creates corresponding views. void MaskGraphicsScene::updateOverlays() { ASSERT(m_masks); for (MaskItem* mask_item : *m_masks) { IOverlay* overlay = registerOverlay(mask_item); ASSERT(overlay); // Add views for the points of the PolygonItem if (auto* polygon_item = dynamic_cast<PolygonItem*>(mask_item)) { auto* polygon_overlay = dynamic_cast<PolygonOverlay*>(overlay); ASSERT(polygon_overlay); for (PointItem* point_item : polygon_item->points()) { IOverlay* point_overlay = registerOverlay(point_item); point_overlay->show(); polygon_overlay->addOverlay(point_overlay); } } } // update Z-values of all IMaskView to reflect stacking order int z = 0; for (const MaskItem* maskItem : *m_masks) { if (IOverlay* overlay = m_mask2overlay[maskItem]) overlay->setZValue(z); ++z; } ASSERT(m_prjns); for (MaskItem* t : *m_prjns) { IOverlay* overlay = registerOverlay(t); ASSERT(overlay); } } //! Creates a view for given item. IOverlay* MaskGraphicsScene::registerOverlay(OverlayItem* item) { ASSERT(m_plot); ASSERT(item); IOverlay* overlay = m_mask2overlay[item]; if (!overlay) { overlay = ::createOverlay(item, m_plot.get()); m_mask2overlay[item] = overlay; addItem(overlay); // takes ownership } disconnect(item); connect(item, &OverlayItem::maskGeometryChanged, [this] { if (m_mouse_is_pressed) gDoc->setModified(); // manual mask movement }); if (auto* line_item = dynamic_cast<LineItem*>(item)) { connect(line_item, &OverlayItem::maskGeometryChanged, [this, line_item] { emit lineItemMoved(line_item); // -> update projections plot }); connect(line_item, &OverlayItem::maskToBeDestroyed, [this, line_item] { emit lineItemDeleted(line_item); }); } connect(item, &OverlayItem::maskGeometryChanged, overlay, &IOverlay::onGeometryChange); if (auto* mask_item = dynamic_cast<MaskItem*>(item)) { if (auto* mask_overlay = dynamic_cast<IMaskOverlay*>(overlay)) connect(mask_item, &OverlayItem::maskVisibilityChanged, mask_overlay, &IMaskOverlay::onVisibilityChange); else ASSERT_NEVER; } overlay->update_view(); return overlay; } //! Removes single view from scene. void MaskGraphicsScene::removeOverlay(OverlayItem* item) { if (auto it = m_mask2overlay.find(item); it != m_mask2overlay.end()) { // at first, delete views for the points of the PolygonItem if (const auto* polygon_item = dynamic_cast<const PolygonItem*>(item)) for (PointItem* point_item : polygon_item->points()) removeOverlay(point_item); IOverlay* overlay = it->second; overlay->setSelected(false); m_mask2overlay.erase(it); removeItem(overlay); delete overlay; } } void MaskGraphicsScene::setDrawingInProgress(bool value) { m_drawing_in_progress = value; if (value) gDoc->setModified(); // manual mask creation else m_active_mask = nullptr; } void MaskGraphicsScene::makeViewAtMousePosSelected(QGraphicsSceneMouseEvent* event) { if (QGraphicsItem* graphicsItem = itemAt(event->scenePos(), QTransform())) graphicsItem->setSelected(true); } //! Processes RectangleItem and EllipseItem drawing. //! Called upon mouse move event (with left button down). //! If mouse has moved sufficiently far, a new item is created. //! On further calls, size and position of the rectangle are updated. void MaskGraphicsScene::processRectangleOrEllipseItem(QGraphicsSceneMouseEvent* event) { ASSERT(m_plot); const QPointF click_pos = event->buttonDownScenePos(Qt::LeftButton); const QPointF mouse_pos = event->scenePos(); //... Create new item? if (!m_active_mask) { if (QLineF(mouse_pos, click_pos).length() < min_distance_to_create_rect) return; // selected area is too small => don't create object yet if (m_mode == Canvas2DMode::RECTANGLE) m_active_mask = new RectangleItem; else if (m_mode == Canvas2DMode::ELLIPSE) m_active_mask = new EllipseItem; else if (m_mode == Canvas2DMode::ROI) m_active_mask = new RegionOfInterestItem; else ASSERT_NEVER; m_masks->add_item(m_active_mask); if (m_mode != Canvas2DMode::ROI) m_active_mask->setMaskValue(m_mask_value); } //... Update item geometry const double dxl = std::min(click_pos.x(), mouse_pos.x()); const double dxh = std::max(click_pos.x(), mouse_pos.x()); const double dyl = std::min(click_pos.y(), mouse_pos.y()); const double dyh = std::max(click_pos.y(), mouse_pos.y()); const double xl = m_plot->xAxis->pixelToCoord(dxl); const double xh = m_plot->xAxis->pixelToCoord(dxh); const double yl = m_plot->yAxis->pixelToCoord(dyl); const double yh = m_plot->yAxis->pixelToCoord(dyh); if (auto* rectItem = dynamic_cast<RectangleItem*>(m_active_mask)) { // RectangleItem or RegionOfInterestItem rectItem->setXLow(xl); rectItem->setXHig(xh); rectItem->setYLow(yh); rectItem->setYHig(yl); } else if (auto* ellItem = dynamic_cast<EllipseItem*>(m_active_mask)) { ellItem->setXCenter((xh + xl) / 2); ellItem->setYCenter((yh + yl) / 2); ellItem->setXRadius((xh - xl) / 2); ellItem->setYRadius((yl - yh) / 2); } emit m_active_mask->maskGeometryChanged(); // produce views for the created shape updateOverlays(); } void MaskGraphicsScene::processPolygonItem(QGraphicsSceneMouseEvent* event) { ASSERT(m_plot); ASSERT(m_mode == Canvas2DMode::POLYGON); if (!m_active_mask) { setDrawingInProgress(true); MaskItem* new_poly = new PolygonItem; m_masks->add_item(new_poly); new_poly->setMaskValue(m_mask_value); m_active_mask = new_poly; } ASSERT(dynamic_cast<PolygonItem*>(m_active_mask)); if (PolygonOverlay* polygon = currentPolygon()) { if (polygon->closePolygonIfNecessary()) { setDrawingInProgress(false); m_mouse_position = {}; return; } } const QPointF click_pos = event->buttonDownScenePos(Qt::LeftButton); const double x = m_plot->xAxis->pixelToCoord(click_pos.x()); const double y = m_plot->yAxis->pixelToCoord(click_pos.y()); dynamic_cast<PolygonItem*>(m_active_mask)->addPoint(x, y); updateOverlays(); } void MaskGraphicsScene::processLineItem(QGraphicsSceneMouseEvent* event) { ASSERT(m_plot); setDrawingInProgress(true); QPointF pos = event->buttonDownScenePos(Qt::LeftButton); if (Canvas2DMode::isVerticalLine(m_mode)) m_active_mask = new VerticalLineItem(m_plot->xAxis->pixelToCoord(pos.x())); else if (Canvas2DMode::isHorizontalLine(m_mode)) m_active_mask = new HorizontalLineItem(m_plot->yAxis->pixelToCoord(pos.y())); else ASSERT_NEVER; if (Canvas2DMode::isPrjn(m_mode)) m_prjns->add_item(m_active_mask); else m_masks->add_item(m_active_mask); emit m_active_mask->maskGeometryChanged(); m_active_mask->setMaskValue(m_mask_value); emit lineItemProcessed(); setDrawingInProgress(false); } // TODO: check FullframeItem void MaskGraphicsScene::processFullframeItem(QGraphicsSceneMouseEvent* event) { Q_UNUSED(event); setDrawingInProgress(true); MaskItem* item = new FullframeItem; m_masks->add_item(item); m_active_mask = item; setDrawingInProgress(false); } // ************************************************************************************************ // private const functions // ************************************************************************************************ //! Returns true if left mouse bottom click was inside ColorMap viewport rectangle. bool MaskGraphicsScene::isValidMouseClick(QGraphicsSceneMouseEvent* event) const { return (event->buttons() & Qt::LeftButton) && m_plot && GUI::Util::viewportRectangle(m_plot.get()).contains(event->scenePos()); } //! Returns true if mouse click is valid for rectangular/elliptic/ROI shapes. bool MaskGraphicsScene::isValidForRectangleShapeDrawing(QGraphicsSceneMouseEvent* event) const { if (m_drawing_in_progress) return false; if (!Canvas2DMode::basedOnRectangle(m_mode)) return false; if (areaContains<SizeHandle>(items(event->scenePos()))) return false; if (m_mode == Canvas2DMode::ROI) // only one ROI is allowed for (const auto& [item, overlay] : m_mask2overlay) if (dynamic_cast<const RegionOfInterestItem*>(item)) return false; return true; } //! Returns true if mouse click is in context suitable for polygon drawing. bool MaskGraphicsScene::isValidForPolygonDrawing(QGraphicsSceneMouseEvent* event) const { if (m_mode != Canvas2DMode::POLYGON) return false; if (!m_drawing_in_progress && areaContains<VertexOverlay>(items(event->scenePos()))) return false; return true; } //! Returns true if mouse click is in context suitable for line drawing. bool MaskGraphicsScene::isValidForLineDrawing(QGraphicsSceneMouseEvent* event) const { if (m_drawing_in_progress) return false; if (!Canvas2DMode::isLineMode(m_mode)) return false; if (QGraphicsItem* graphicsItem = itemAt(event->scenePos(), QTransform())) if (dynamic_cast<LineOverlay*>(graphicsItem)) return false; return true; } //! Returns true if FullframeItem can be drawn. Only one item of such type is allowed. bool MaskGraphicsScene::isValidForMaskAllDrawing(QGraphicsSceneMouseEvent*) const { if (m_drawing_in_progress) return false; if (m_mode != Canvas2DMode::MASKALL) return false; for (const auto& [item, overlay] : m_mask2overlay) if (dynamic_cast<const FullframeItem*>(item)) return false; return true; } //! Returns polygon which is currently under the drawing. PolygonOverlay* MaskGraphicsScene::currentPolygon() { if (m_drawing_in_progress && m_mode == Canvas2DMode::POLYGON && m_active_mask) return dynamic_cast<PolygonOverlay*>(m_mask2overlay[m_active_mask]); return nullptr; }