File indexing completed on 2025-01-26 03:34:14

0001 /*
0002     File                 : ReferenceLine.cpp
0003     Project              : LabPlot
0004     Description          : Custom user-defined point on the plot
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2020-2022 Alexander Semke <alexander.semke@web.de>
0007     SPDX-FileCopyrightText: 2021 Stefan Gerlach <stefan.gerlach@uni.kn>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "ReferenceLine.h"
0013 #include "ReferenceLinePrivate.h"
0014 #include "backend/lib/XmlStreamReader.h"
0015 #include "backend/lib/commandtemplates.h"
0016 #include "backend/worksheet/Line.h"
0017 #include "backend/worksheet/Worksheet.h"
0018 #include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h"
0019 #include "backend/worksheet/plots/cartesian/CartesianPlot.h"
0020 #include "kdefrontend/GuiTools.h"
0021 
0022 #include <QActionGroup>
0023 #include <QGraphicsSceneMouseEvent>
0024 #include <QMenu>
0025 #include <QPainter>
0026 
0027 #include <KConfig>
0028 #include <KConfigGroup>
0029 #include <KLocalizedString>
0030 
0031 using Dimension = CartesianCoordinateSystem::Dimension;
0032 
0033 /**
0034  * \class ReferenceLine
0035  * \brief A customizable point.
0036  *
0037  * The position can be either specified by mouse events or by providing the
0038  * x- and y- coordinates in parent's coordinate system
0039  */
0040 
0041 ReferenceLine::ReferenceLine(CartesianPlot* plot, const QString& name)
0042     : WorksheetElement(name, new ReferenceLinePrivate(this), AspectType::ReferenceLine) {
0043     m_plot = plot;
0044     init();
0045 }
0046 
0047 // no need to delete the d-pointer here - it inherits from QGraphicsItem
0048 // and is deleted during the cleanup in QGraphicsScene
0049 ReferenceLine::~ReferenceLine() = default;
0050 
0051 void ReferenceLine::init() {
0052     Q_D(ReferenceLine);
0053 
0054     KConfig config;
0055     KConfigGroup group = config.group(QStringLiteral("ReferenceLine"));
0056 
0057     d->orientation = (Orientation)group.readEntry(QStringLiteral("Orientation"), static_cast<int>(Orientation::Vertical));
0058     switch (d->orientation) {
0059     case WorksheetElement::Orientation::Horizontal:
0060         d->position.positionLimit = WorksheetElement::PositionLimit::Y;
0061         break;
0062     case WorksheetElement::Orientation::Vertical:
0063         d->position.positionLimit = WorksheetElement::PositionLimit::X;
0064         break;
0065     case WorksheetElement::Orientation::Both:
0066         d->position.positionLimit = WorksheetElement::PositionLimit::None;
0067         break;
0068     }
0069 
0070     if (plot()) {
0071         d->coordinateBindingEnabled = true;
0072         // default position
0073         auto cs = plot()->coordinateSystem(plot()->defaultCoordinateSystemIndex());
0074         const auto x = m_plot->range(Dimension::X, cs->index(Dimension::X)).center();
0075         const auto y = m_plot->range(Dimension::Y, cs->index(Dimension::Y)).center();
0076         DEBUG(Q_FUNC_INFO << ", x/y pos = " << x << " / " << y)
0077         d->positionLogical = QPointF(x, y);
0078     } else
0079         d->position.point = QPointF(0, 0);
0080     d->updatePosition(); // To update also scene coordinates
0081 
0082     // line
0083     d->line = new Line(QString());
0084     d->line->setHidden(true);
0085     addChild(d->line);
0086     d->line->init(group);
0087     connect(d->line, &Line::updatePixmapRequested, [=] {
0088         d->update();
0089     });
0090     connect(d->line, &Line::updateRequested, [=] {
0091         d->recalcShapeAndBoundingRect();
0092     });
0093 }
0094 
0095 /*!
0096     Returns an icon to be used in the project explorer.
0097 */
0098 QIcon ReferenceLine::icon() const {
0099     return QIcon::fromTheme(QStringLiteral("draw-line"));
0100 }
0101 
0102 void ReferenceLine::initActions() {
0103     // Orientation
0104     auto* orientationActionGroup = new QActionGroup(this);
0105     orientationActionGroup->setExclusive(true);
0106     connect(orientationActionGroup, &QActionGroup::triggered, this, &ReferenceLine::orientationChangedSlot);
0107 
0108     orientationHorizontalAction = new QAction(QIcon::fromTheme(QStringLiteral("labplot-axis-horizontal")), i18n("Horizontal"), orientationActionGroup);
0109     orientationHorizontalAction->setCheckable(true);
0110 
0111     orientationVerticalAction = new QAction(QIcon::fromTheme(QStringLiteral("labplot-axis-vertical")), i18n("Vertical"), orientationActionGroup);
0112     orientationVerticalAction->setCheckable(true);
0113 
0114     // Line
0115     lineStyleActionGroup = new QActionGroup(this);
0116     lineStyleActionGroup->setExclusive(true);
0117     connect(lineStyleActionGroup, &QActionGroup::triggered, this, &ReferenceLine::lineStyleChanged);
0118 
0119     lineColorActionGroup = new QActionGroup(this);
0120     lineColorActionGroup->setExclusive(true);
0121     connect(lineColorActionGroup, &QActionGroup::triggered, this, &ReferenceLine::lineColorChanged);
0122 }
0123 
0124 void ReferenceLine::initMenus() {
0125     this->initActions();
0126 
0127     // Orientation
0128     orientationMenu = new QMenu(i18n("Orientation"));
0129     orientationMenu->setIcon(QIcon::fromTheme(QStringLiteral("labplot-axis-horizontal")));
0130     orientationMenu->addAction(orientationHorizontalAction);
0131     orientationMenu->addAction(orientationVerticalAction);
0132 
0133     // Line
0134     lineMenu = new QMenu(i18n("Line"));
0135     lineMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-line")));
0136     lineStyleMenu = new QMenu(i18n("Style"), lineMenu);
0137     lineStyleMenu->setIcon(QIcon::fromTheme(QStringLiteral("object-stroke-style")));
0138     lineMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-line")));
0139     lineMenu->addMenu(lineStyleMenu);
0140 
0141     lineColorMenu = new QMenu(i18n("Color"), lineMenu);
0142     lineColorMenu->setIcon(QIcon::fromTheme(QStringLiteral("fill-color")));
0143     GuiTools::fillColorMenu(lineColorMenu, lineColorActionGroup);
0144     lineMenu->addMenu(lineColorMenu);
0145 }
0146 
0147 QMenu* ReferenceLine::createContextMenu() {
0148     if (!orientationMenu)
0149         initMenus();
0150 
0151     QMenu* menu = WorksheetElement::createContextMenu();
0152     QAction* visibilityAction = this->visibilityAction();
0153 
0154     Q_D(const ReferenceLine);
0155 
0156     // Orientation
0157     if (d->orientation == Orientation::Horizontal)
0158         orientationHorizontalAction->setChecked(true);
0159     else
0160         orientationVerticalAction->setChecked(true);
0161     menu->insertMenu(visibilityAction, orientationMenu);
0162 
0163     // Line styles
0164     const auto& pen = d->line->pen();
0165     GuiTools::updatePenStyles(lineStyleMenu, lineStyleActionGroup, pen.color());
0166     GuiTools::selectPenStyleAction(lineStyleActionGroup, pen.style());
0167     GuiTools::selectColorAction(lineColorActionGroup, pen.color());
0168 
0169     menu->insertMenu(visibilityAction, lineMenu);
0170     menu->insertSeparator(visibilityAction);
0171 
0172     return menu;
0173 }
0174 
0175 void ReferenceLine::retransform() {
0176     Q_D(ReferenceLine);
0177     d->retransform();
0178 }
0179 
0180 void ReferenceLine::handleResize(double /*horizontalRatio*/, double /*verticalRatio*/, bool /*pageResize*/) {
0181 }
0182 
0183 /* ============================ getter methods ================= */
0184 BASIC_SHARED_D_READER_IMPL(ReferenceLine, ReferenceLine::Orientation, orientation, orientation)
0185 
0186 Line* ReferenceLine::line() const {
0187     Q_D(const ReferenceLine);
0188     return d->line;
0189 }
0190 
0191 /* ============================ setter methods and undo commands ================= */
0192 STD_SETTER_CMD_IMPL_F_S(ReferenceLine, SetOrientation, ReferenceLine::Orientation, orientation, updateOrientation)
0193 void ReferenceLine::setOrientation(Orientation orientation) {
0194     Q_D(ReferenceLine);
0195     if (orientation != d->orientation)
0196         exec(new ReferenceLineSetOrientationCmd(d, orientation, ki18n("%1: set orientation")));
0197 }
0198 
0199 // ##############################################################################
0200 // ######  SLOTs for changes triggered via QActions in the context menu  ########
0201 // ##############################################################################
0202 void ReferenceLine::orientationChangedSlot(QAction* action) {
0203     if (action == orientationHorizontalAction)
0204         this->setOrientation(Orientation::Horizontal);
0205     else
0206         this->setOrientation(Orientation::Vertical);
0207 }
0208 
0209 void ReferenceLine::lineStyleChanged(QAction* action) {
0210     Q_D(const ReferenceLine);
0211     d->line->setStyle(GuiTools::penStyleFromAction(lineStyleActionGroup, action));
0212 }
0213 
0214 void ReferenceLine::lineColorChanged(QAction* action) {
0215     Q_D(const ReferenceLine);
0216     d->line->setColor(GuiTools::colorFromAction(lineColorActionGroup, action));
0217 }
0218 
0219 // ##############################################################################
0220 // ####################### Private implementation ###############################
0221 // ##############################################################################
0222 ReferenceLinePrivate::ReferenceLinePrivate(ReferenceLine* owner)
0223     : WorksheetElementPrivate(owner)
0224     , q(owner) {
0225     setFlag(QGraphicsItem::ItemSendsGeometryChanges);
0226     setFlag(QGraphicsItem::ItemIsMovable);
0227     setFlag(QGraphicsItem::ItemIsSelectable);
0228     setFlag(QGraphicsItem::ItemIsFocusable);
0229     setAcceptHoverEvents(true);
0230 }
0231 
0232 /*!
0233     calculates the position and the bounding box of the item/point. Called on geometry or properties changes.
0234  */
0235 void ReferenceLinePrivate::retransform() {
0236     if (suppressRetransform || !q->cSystem || q->isLoading())
0237         return;
0238 
0239     auto cs = q->plot()->coordinateSystem(q->coordinateSystemIndex());
0240     const auto xRange{q->m_plot->range(Dimension::X, cs->index(Dimension::X))};
0241     const auto yRange{q->m_plot->range(Dimension::Y, cs->index(Dimension::Y))};
0242 
0243     // calculate the position in the scene coordinates
0244     if (orientation == ReferenceLine::Orientation::Vertical)
0245         positionLogical = QPointF(positionLogical.x(), yRange.center());
0246     else
0247         positionLogical = QPointF(xRange.center(), positionLogical.y());
0248     updatePosition(); // To update position.point
0249 
0250     // position.point contains already the scene position, but here it will be determined,
0251     // if the point lies outside of the datarect or not
0252     QVector<QPointF> listScene = q->cSystem->mapLogicalToScene(Points() << positionLogical);
0253     QDEBUG(Q_FUNC_INFO << ", scene list = " << listScene)
0254 
0255     if (!listScene.isEmpty()) {
0256         insidePlot = true;
0257 
0258         // determine the length of the line to be drawn
0259         QVector<QPointF> pointsLogical;
0260         if (orientation == ReferenceLine::Orientation::Vertical)
0261             pointsLogical << QPointF(positionLogical.x(), yRange.start()) << QPointF(positionLogical.x(), yRange.end());
0262         else
0263             pointsLogical << QPointF(xRange.start(), positionLogical.y()) << QPointF(xRange.end(), positionLogical.y());
0264 
0265         QVector<QPointF> pointsScene = q->cSystem->mapLogicalToScene(pointsLogical);
0266 
0267         if (pointsScene.size() > 1) {
0268             if (orientation == ReferenceLine::Orientation::Vertical)
0269                 length = pointsScene.at(0).y() - pointsScene.at(1).y();
0270             else
0271                 length = pointsScene.at(0).x() - pointsScene.at(1).x();
0272         }
0273     } else
0274         insidePlot = false;
0275     QDEBUG(Q_FUNC_INFO << ", scene list after = " << listScene)
0276 
0277     recalcShapeAndBoundingRect();
0278 }
0279 
0280 void ReferenceLinePrivate::updateOrientation() {
0281     switch (orientation) {
0282     case WorksheetElement::Orientation::Horizontal:
0283         position.positionLimit = WorksheetElement::PositionLimit::Y;
0284         break;
0285     case WorksheetElement::Orientation::Vertical:
0286         position.positionLimit = WorksheetElement::PositionLimit::X;
0287         break;
0288     case WorksheetElement::Orientation::Both:
0289         position.positionLimit = WorksheetElement::PositionLimit::None;
0290         break;
0291     }
0292     retransform();
0293 }
0294 
0295 /*!
0296   recalculates the outer bounds and the shape of the item.
0297 */
0298 void ReferenceLinePrivate::recalcShapeAndBoundingRect() {
0299     prepareGeometryChange();
0300 
0301     m_shape = QPainterPath();
0302     if (insidePlot) {
0303         QPainterPath path;
0304         if (orientation == ReferenceLine::Orientation::Horizontal) {
0305             path.moveTo(-length / 2, 0);
0306             path.lineTo(length / 2, 0);
0307         } else {
0308             path.moveTo(0, length / 2);
0309             path.lineTo(0, -length / 2);
0310         }
0311         m_shape.addPath(WorksheetElement::shapeFromPath(path, line->pen()));
0312         m_boundingRectangle = m_shape.boundingRect();
0313     }
0314 }
0315 
0316 void ReferenceLinePrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) {
0317     if (!insidePlot)
0318         return;
0319 
0320     painter->setOpacity(line->opacity());
0321     painter->setPen(line->pen());
0322     if (orientation == ReferenceLine::Orientation::Horizontal)
0323         painter->drawLine(-length / 2, 0, length / 2, 0);
0324     else
0325         painter->drawLine(0, length / 2, 0, -length / 2);
0326 
0327     if (m_hovered && !isSelected() && !q->isPrinting()) {
0328         painter->setPen(QPen(QApplication::palette().color(QPalette::Shadow), 2, Qt::SolidLine));
0329         painter->drawPath(m_shape);
0330     }
0331 
0332     if (isSelected() && !q->isPrinting()) {
0333         painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), 2, Qt::SolidLine));
0334         painter->drawPath(m_shape);
0335     }
0336 }
0337 
0338 // ##############################################################################
0339 // ##################  Serialization/Deserialization  ###########################
0340 // ##############################################################################
0341 //! Save as XML
0342 void ReferenceLine::save(QXmlStreamWriter* writer) const {
0343     Q_D(const ReferenceLine);
0344 
0345     writer->writeStartElement(QStringLiteral("referenceLine"));
0346     writeBasicAttributes(writer);
0347     writeCommentElement(writer);
0348 
0349     writer->writeStartElement(QStringLiteral("geometry"));
0350     WorksheetElement::save(writer);
0351     writer->writeAttribute(QStringLiteral("orientation"), QString::number(static_cast<int>(d->orientation)));
0352     writer->writeEndElement();
0353 
0354     d->line->save(writer);
0355 
0356     writer->writeEndElement(); // close "ReferenceLine" section
0357 }
0358 
0359 //! Load from XML
0360 bool ReferenceLine::load(XmlStreamReader* reader, bool preview) {
0361     Q_D(ReferenceLine);
0362 
0363     if (!readBasicAttributes(reader))
0364         return false;
0365 
0366     QXmlStreamAttributes attribs;
0367     QString str;
0368 
0369     while (!reader->atEnd()) {
0370         reader->readNext();
0371         if (reader->isEndElement() && reader->name() == QLatin1String("referenceLine"))
0372             break;
0373 
0374         if (!reader->isStartElement())
0375             continue;
0376 
0377         if (!preview && reader->name() == QLatin1String("comment")) {
0378             if (!readCommentElement(reader))
0379                 return false;
0380         } else if (!preview && reader->name() == QLatin1String("general")) {
0381             // old logic for the position for xml version < 6
0382             Q_D(ReferenceLine);
0383             attribs = reader->attributes();
0384             auto str = attribs.value(QStringLiteral("position")).toString();
0385             if (str.isEmpty())
0386                 reader->raiseMissingAttributeWarning(QStringLiteral("position"));
0387             else {
0388                 d->positionLogical.setX(str.toDouble());
0389                 d->positionLogical.setY(str.toDouble());
0390             }
0391             d->coordinateBindingEnabled = true;
0392 
0393             READ_INT_VALUE("orientation", orientation, Orientation);
0394             READ_INT_VALUE_DIRECT("plotRangeIndex", m_cSystemIndex, int);
0395 
0396             str = attribs.value(QStringLiteral("visible")).toString();
0397             if (str.isEmpty())
0398                 reader->raiseMissingAttributeWarning(QStringLiteral("visible"));
0399             else
0400                 d->setVisible(str.toInt());
0401         } else if (!preview && reader->name() == QLatin1String("geometry")) {
0402             attribs = reader->attributes();
0403             // new logic for the position for xmlVersion >= 6
0404             READ_INT_VALUE("orientation", orientation, Orientation);
0405             WorksheetElement::load(reader, preview);
0406         } else if (!preview && reader->name() == QLatin1String("line")) {
0407             d->line->load(reader, preview);
0408         } else { // unknown element
0409             reader->raiseUnknownElementWarning();
0410             if (!reader->skipToEndElement())
0411                 return false;
0412         }
0413     }
0414     return true;
0415 }
0416 
0417 // ##############################################################################
0418 // #########################  Theme management ##################################
0419 // ##############################################################################
0420 void ReferenceLine::loadThemeConfig(const KConfig& config) {
0421     Q_D(ReferenceLine);
0422 
0423     // for the properties of the line read the properties of the axis line
0424     const KConfigGroup& group = config.group(QStringLiteral("Axis"));
0425     d->line->loadThemeConfig(group);
0426 }