File indexing completed on 2024-05-12 03:48:23

0001 /*
0002     File                 : WorksheetElement.cpp
0003     Project              : LabPlot
0004     Description          : Base class for all Worksheet children.
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2009 Tilman Benkert <thzs@gmx.net>
0007     SPDX-FileCopyrightText: 2012-2023 Alexander Semke <alexander.semke@web.de>
0008     SPDX-FileCopyrightText: 2024 Stefan Gerlach <stefan.gerlach@uni.kn>
0009 
0010     SPDX-License-Identifier: GPL-2.0-or-later
0011 */
0012 
0013 #include "backend/worksheet/WorksheetElement.h"
0014 #include "backend/core/AspectPrivate.h"
0015 #include "backend/core/Project.h"
0016 #include "backend/lib/XmlStreamReader.h"
0017 #include "backend/lib/commandtemplates.h"
0018 #include "backend/worksheet/Worksheet.h"
0019 #include "backend/worksheet/WorksheetElementPrivate.h"
0020 #include "plots/AbstractPlot.h"
0021 #include "plots/PlotArea.h"
0022 #include "plots/cartesian/CartesianCoordinateSystem.h"
0023 #include "plots/cartesian/Plot.h"
0024 
0025 #include <KLocalizedString>
0026 #include <QGraphicsItem>
0027 #include <QGraphicsScene>
0028 #include <QGraphicsSceneContextMenuEvent>
0029 #include <QKeyEvent>
0030 #include <QMenu>
0031 #include <QPen>
0032 
0033 /**
0034  * \class WorksheetElement
0035  * \brief Base class for all Worksheet children.
0036  *
0037  */
0038 // WorksheetElement::WorksheetElement(const QString& name, AspectType type)
0039 //: AbstractAspect(name, type), d_ptr(new WorksheetElementPrivate(this)) {
0040 //  init();
0041 //}
0042 
0043 WorksheetElement::WorksheetElement(const QString& name, WorksheetElementPrivate* dd, AspectType type)
0044     : AbstractAspect(name, type)
0045     , d_ptr(dd) {
0046     init();
0047 }
0048 
0049 void WorksheetElement::init() {
0050     Q_D(WorksheetElement);
0051     d->setData(0, static_cast<quint64>(type()));
0052 }
0053 
0054 WorksheetElement::~WorksheetElement() {
0055     delete m_moveBehindMenu;
0056     delete m_moveInFrontOfMenu;
0057     delete m_drawingOrderMenu;
0058 }
0059 
0060 void WorksheetElement::finalizeAdd() {
0061     DEBUG(Q_FUNC_INFO)
0062     if (!m_plot) {
0063         // determine the plot parent which is not neccessarily the parent aspect like for
0064         // * child CustomPoint in InfoeElement
0065         // * child XYCurves in QQPlot
0066         // * etc.
0067         m_plot = dynamic_cast<CartesianPlot*>(parent(AspectType::CartesianPlot));
0068     }
0069 
0070     if (m_plot) {
0071         cSystem = dynamic_cast<const CartesianCoordinateSystem*>(m_plot->coordinateSystem(m_cSystemIndex));
0072         Q_EMIT plotRangeListChanged();
0073     } else
0074         DEBUG(Q_FUNC_INFO << ", WARNING: no plot available.")
0075 }
0076 
0077 /**
0078  * \fn QGraphicsItem *WorksheetElement::graphicsItem() const
0079  * \brief Return the graphics item representing this element.
0080  *
0081  */
0082 QGraphicsItem* WorksheetElement::graphicsItem() const {
0083     return d_ptr;
0084 }
0085 
0086 /*!
0087  * \brief WorksheetElement::setParentGraphicsItem
0088  * Sets the parent graphicsitem, needed for binding to coord
0089  * \param item parent graphicsitem
0090  */
0091 void WorksheetElement::setParentGraphicsItem(QGraphicsItem* item) {
0092     Q_D(WorksheetElement);
0093     d->setParentItem(item);
0094 }
0095 
0096 /**
0097  * \fn void WorksheetElement::setVisible(bool on)
0098  * \brief Show/hide the element.
0099  *
0100  */
0101 
0102 /**
0103  * \fn bool WorksheetElement::isVisible() const
0104  * \brief Return whether the element is (at least) partially visible.
0105  *
0106  */
0107 
0108 /**
0109  * \brief Return whether the element is fully visible (i.e., including all child elements).
0110  *
0111  * The standard implementation returns isVisible().
0112  */
0113 bool WorksheetElement::isFullyVisible() const {
0114     return isVisible();
0115 }
0116 
0117 void WorksheetElement::setSuppressRetransform(bool value) {
0118     Q_D(WorksheetElement);
0119     d->suppressRetransform = value;
0120 }
0121 
0122 /**
0123  * \fn void WorksheetElement::setPrinting(bool on)
0124  * \brief Switches the printing mode on/off
0125  *
0126  */
0127 void WorksheetElement::setPrinting(bool printing) {
0128     m_printing = printing;
0129 }
0130 
0131 bool WorksheetElement::isPrinting() const {
0132     return m_printing;
0133 }
0134 
0135 void WorksheetElement::setZValue(qreal value) {
0136     graphicsItem()->setZValue(value);
0137 }
0138 
0139 void WorksheetElement::changeVisibility() {
0140     Q_D(const WorksheetElement);
0141     this->setVisible(!d->isVisible());
0142 }
0143 
0144 void WorksheetElement::changeLocking() {
0145     this->setLock(!isLocked());
0146 }
0147 
0148 STD_SETTER_CMD_IMPL_S(WorksheetElement, SetLock, bool, lock)
0149 void WorksheetElement::setLock(bool lock) {
0150     Q_D(WorksheetElement);
0151     if (lock != d->lock) {
0152         if (!lock && isHovered())
0153             setHover(false);
0154         exec(new WorksheetElementSetLockCmd(d, lock, lock ? ki18n("%1: lock") : ki18n("%1: unlock")));
0155     }
0156 }
0157 
0158 STD_SWAP_METHOD_SETTER_CMD_IMPL_F(WorksheetElement, SetVisible, bool, swapVisible, update)
0159 void WorksheetElement::setVisible(bool on) {
0160     Q_D(WorksheetElement);
0161     exec(new WorksheetElementSetVisibleCmd(d, on, on ? ki18n("%1: set visible") : ki18n("%1: set invisible")));
0162 }
0163 
0164 bool WorksheetElementPrivate::swapVisible(bool on) {
0165     bool oldValue = isVisible();
0166 
0167     // When making a graphics item invisible, it gets deselected in the scene.
0168     // In this case we don't want to deselect the item in the project explorer.
0169     // We need to supress the deselection in the view.
0170     auto* worksheet = static_cast<Worksheet*>(q->parent(AspectType::Worksheet));
0171     if (worksheet) {
0172         worksheet->suppressSelectionChangedEvent(true);
0173         setVisible(on);
0174         worksheet->suppressSelectionChangedEvent(false);
0175     } else
0176         setVisible(on);
0177 
0178     Q_EMIT q->changed();
0179     Q_EMIT q->visibleChanged(on);
0180     return oldValue;
0181 }
0182 
0183 bool WorksheetElement::isVisible() const {
0184     Q_D(const WorksheetElement);
0185     return d->isVisible();
0186 }
0187 
0188 /**
0189     This does exactly what Qt internally does to creates a shape from a painter path.
0190 */
0191 QPainterPath WorksheetElement::shapeFromPath(const QPainterPath& path, const QPen& pen) {
0192     if (path == QPainterPath())
0193         return path;
0194 
0195     //  PERFTRACE("WorksheetElement::shapeFromPath()");
0196 
0197     // TODO: We unfortunately need this hack as QPainterPathStroker will set a width of 1.0
0198     // if we pass a value of 0.0 to QPainterPathStroker::setWidth()
0199     const qreal penWidthZero = qreal(1.e-8);
0200 
0201     QPainterPathStroker ps;
0202     ps.setCapStyle(pen.capStyle());
0203     if (pen.widthF() <= 0.0)
0204         ps.setWidth(penWidthZero);
0205     else
0206         ps.setWidth(pen.widthF());
0207     ps.setJoinStyle(pen.joinStyle());
0208     ps.setMiterLimit(pen.miterLimit());
0209 
0210     QPainterPath p = ps.createStroke(path);
0211     p.addPath(path);
0212 
0213     return p;
0214 }
0215 
0216 QAction* WorksheetElement::visibilityAction() {
0217     if (!m_visibilityAction) {
0218         m_visibilityAction = new QAction(QIcon::fromTheme(QStringLiteral("view-visible")), i18n("Visible"), this);
0219         m_visibilityAction->setCheckable(true);
0220         connect(m_visibilityAction, &QAction::triggered, this, &WorksheetElement::changeVisibility);
0221     }
0222     return m_visibilityAction;
0223 }
0224 
0225 QAction* WorksheetElement::lockingAction() {
0226     if (!m_lockingAction) {
0227         m_lockingAction = new QAction(QIcon::fromTheme(QStringLiteral("hidemouse")), i18n("Lock"), this);
0228         m_lockingAction->setCheckable(true);
0229         connect(m_lockingAction, &QAction::triggered, this, &WorksheetElement::changeLocking);
0230     }
0231     return m_lockingAction;
0232 }
0233 
0234 QMenu* WorksheetElement::createContextMenu() {
0235     if (!m_drawingOrderMenu) {
0236         m_drawingOrderMenu = new QMenu(i18n("Drawing &order"));
0237         m_drawingOrderMenu->setIcon(QIcon::fromTheme(QStringLiteral("layer-bottom")));
0238 
0239         m_moveBehindMenu = new QMenu(i18n("Move &behind"));
0240         m_moveBehindMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-arrow-down")));
0241         m_drawingOrderMenu->addMenu(m_moveBehindMenu);
0242 
0243         m_moveInFrontOfMenu = new QMenu(i18n("Move in &front of"));
0244         m_moveInFrontOfMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-arrow-up")));
0245         m_drawingOrderMenu->addMenu(m_moveInFrontOfMenu);
0246 
0247         connect(m_drawingOrderMenu, &QMenu::aboutToShow, this, &WorksheetElement::prepareDrawingOrderMenu);
0248         connect(m_moveBehindMenu, &QMenu::triggered, this, &WorksheetElement::execMoveBehind);
0249         connect(m_moveInFrontOfMenu, &QMenu::triggered, this, &WorksheetElement::execMoveInFrontOf);
0250     }
0251 
0252     QMenu* menu = AbstractAspect::createContextMenu();
0253     QAction* firstAction = menu->actions().at(1); // skip the first action because of the "title-action"
0254 
0255     auto* visibilityAction = this->visibilityAction();
0256     visibilityAction->setChecked(isVisible());
0257     menu->insertAction(firstAction, visibilityAction);
0258     menu->insertSeparator(firstAction);
0259 
0260     // don't add the lock action for elements which cannot be freely moved on the worksheet (like axis and curves/plots)
0261     if (!dynamic_cast<Axis*>(this) && !dynamic_cast<Plot*>(this)) {
0262         auto* lockingAction = this->lockingAction();
0263         lockingAction->setChecked(isLocked());
0264         menu->insertAction(firstAction, lockingAction);
0265         menu->insertSeparator(firstAction);
0266     }
0267 
0268     // add the sub-menu for the drawing order
0269     // don't add the drawing order menu for axes and legends, they're always drawn on top of each other elements
0270     if (type() == AspectType::Axis || type() == AspectType::CartesianPlotLegend)
0271         return menu;
0272 
0273     // for plots in a worksheet with an active layout the Z-factor is not relevant but we still
0274     // want to use the "Drawing order" menu to be able to change the position/order of the plot in the layout.
0275     // Since the order of the child in the list of children is opposite to the Z-factor, we change
0276     // the names of the menus to adapt to the reversed logic.
0277     if (dynamic_cast<AbstractPlot*>(this)) {
0278         const auto* w = dynamic_cast<const Worksheet*>(this->parentAspect());
0279         if (!w)
0280             return menu;
0281 
0282         if (w->layout() != Worksheet::Layout::NoLayout) {
0283             m_moveBehindMenu->setTitle(i18n("Move in &front of"));
0284             m_moveBehindMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-arrow-up")));
0285             m_moveInFrontOfMenu->setTitle(i18n("Move &behind"));
0286             m_moveInFrontOfMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-arrow-down")));
0287         } else {
0288             m_moveBehindMenu->setTitle(i18n("Move &behind"));
0289             m_moveBehindMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-arrow-down")));
0290             m_moveInFrontOfMenu->setTitle(i18n("Move in &front of"));
0291             m_moveInFrontOfMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-arrow-up")));
0292         }
0293     }
0294 
0295     // don't add the drawing order menu if the parent element has no other children
0296     int children = 0;
0297     for (auto* child : parentAspect()->children<WorksheetElement>()) {
0298         if (child->type() != AspectType::Axis && child->type() != AspectType::CartesianPlotLegend)
0299             children++;
0300     }
0301 
0302     if (children > 1) {
0303         menu->addSeparator();
0304         menu->addMenu(m_drawingOrderMenu);
0305     }
0306 
0307     return menu;
0308 }
0309 
0310 void WorksheetElement::prepareDrawingOrderMenu() {
0311     const auto* parent = parentAspect();
0312     const int index = parent->indexOfChild<WorksheetElement>(this);
0313     const auto& children = parent->children<WorksheetElement>();
0314 
0315     //"move behind" sub-menu
0316     m_moveBehindMenu->clear();
0317     for (int i = 0; i < index; ++i) {
0318         const auto* elem = children.at(i);
0319         // axes and legends are always drawn on top of other elements, don't add them to the menu
0320         if (elem->type() != AspectType::Axis && elem->type() != AspectType::CartesianPlotLegend) {
0321             auto* action = m_moveBehindMenu->addAction(elem->icon(), elem->name());
0322             action->setData(i);
0323         }
0324     }
0325 
0326     //"move in front of" sub-menu
0327     m_moveInFrontOfMenu->clear();
0328     for (int i = index + 1; i < children.size(); ++i) {
0329         const auto* elem = children.at(i);
0330         // axes and legends are always drawn on top of other elements, don't add them to the menu
0331         if (elem->type() != AspectType::Axis && elem->type() != AspectType::CartesianPlotLegend) {
0332             auto* action = m_moveInFrontOfMenu->addAction(elem->icon(), elem->name());
0333             action->setData(i);
0334         }
0335     }
0336 
0337     // hide the sub-menus if they don't have any entries
0338     m_moveInFrontOfMenu->menuAction()->setVisible(!m_moveInFrontOfMenu->isEmpty());
0339     m_moveBehindMenu->menuAction()->setVisible(!m_moveBehindMenu->isEmpty());
0340 }
0341 
0342 void WorksheetElement::execMoveInFrontOf(QAction* action) {
0343     Q_EMIT moveBegin();
0344     auto* parent = parentAspect();
0345     int index = action->data().toInt();
0346     auto* sibling1 = parent->child<WorksheetElement>(index);
0347     auto* sibling2 = parent->child<WorksheetElement>(index + 1);
0348     beginMacro(i18n("%1: move behind %2.", name(), sibling1->name()));
0349     setMoved(true);
0350     remove();
0351     parent->insertChildBefore(this, sibling2);
0352     setMoved(false);
0353     endMacro();
0354     Q_EMIT moveEnd();
0355 }
0356 
0357 void WorksheetElement::execMoveBehind(QAction* action) {
0358     Q_EMIT moveBegin();
0359     auto* parent = parentAspect();
0360     int index = action->data().toInt();
0361     auto* sibling = parent->child<WorksheetElement>(index);
0362     beginMacro(i18n("%1: move in front of %2.", name(), sibling->name()));
0363     setMoved(true);
0364     remove();
0365     parent->insertChildBefore(this, sibling);
0366     setMoved(false);
0367     endMacro();
0368     Q_EMIT moveEnd();
0369 }
0370 
0371 QPointF WorksheetElement::align(QPointF pos, QRectF rect, HorizontalAlignment horAlign, VerticalAlignment vertAlign, bool positive) const {
0372     // positive is right
0373     double xAlign = 0.;
0374     switch (horAlign) {
0375     case HorizontalAlignment::Left:
0376         xAlign = rect.width() / 2.;
0377         break;
0378     case HorizontalAlignment::Right:
0379         xAlign = -rect.width() / 2.;
0380         break;
0381     case HorizontalAlignment::Center:
0382         break;
0383     }
0384 
0385     // positive is to top
0386     double yAlign = 0.;
0387     switch (vertAlign) {
0388     case VerticalAlignment::Bottom:
0389         yAlign = -rect.height() / 2.;
0390         break;
0391     case VerticalAlignment::Top:
0392         yAlign = rect.height() / 2.;
0393         break;
0394     case VerticalAlignment::Center:
0395         break;
0396     }
0397 
0398     // For yAlign it must be two times plus.
0399     if (positive)
0400         return {pos.x() + xAlign, pos.y() + yAlign};
0401     else
0402         return {pos.x() - xAlign, pos.y() + yAlign};
0403 }
0404 
0405 QRectF WorksheetElement::parentRect() const {
0406     QRectF rect;
0407     auto* parent = parentAspect();
0408     if (parent && parent->type() == AspectType::CartesianPlot && plot()) {
0409         if (type() != AspectType::Axis)
0410             rect = plot()->graphicsItem()->mapRectFromScene(plot()->rect());
0411         else
0412             rect = plot()->dataRect(); // axes are positioned relative to the data rect and not to the whole plot rect
0413     } else {
0414         const auto* parent = graphicsItem()->parentItem();
0415         if (parent) {
0416             rect = parent->boundingRect();
0417         } else {
0418             if (graphicsItem()->scene())
0419                 rect = graphicsItem()->scene()->sceneRect();
0420         }
0421     }
0422 
0423     return rect;
0424 }
0425 
0426 /*!
0427  * \brief parentPosToRelativePos
0428  * Converts the absolute position of the element in parent coordinates into the distance between the
0429  * alignment point of the parent and the element
0430  * \param parentPos Element position in parent coordinates
0431  * \param parentRect Parent data rect
0432  * \param rect element's rect
0433  * \param position contains the alignement of the element to the parent
0434  * \return distance between the parent position to the element
0435  */
0436 QPointF WorksheetElement::parentPosToRelativePos(QPointF parentPos, PositionWrapper position) const {
0437     // increasing relative pos hor --> right
0438     // increasing relative pos vert --> top
0439     // increasing parent pos hor --> right
0440     // increasing parent pos vert --> bottom
0441 
0442     QRectF parentRect = this->parentRect();
0443     QPointF relPos;
0444 
0445     double percentage = 0.;
0446     switch (position.horizontalPosition) {
0447     case HorizontalPosition::Left:
0448         break;
0449     case HorizontalPosition::Center:
0450         percentage = 0.5;
0451         break;
0452     case HorizontalPosition::Right:
0453         percentage = 1.0;
0454         break;
0455     case HorizontalPosition::Relative:
0456         percentage = position.point.x();
0457     }
0458 
0459     relPos.setX(parentPos.x() - (parentRect.x() + parentRect.width() * percentage));
0460 
0461     switch (position.verticalPosition) {
0462     case VerticalPosition::Top:
0463         percentage = 0.;
0464         break;
0465     case VerticalPosition::Center:
0466         percentage = 0.5;
0467         break;
0468     case VerticalPosition::Bottom:
0469         percentage = 1.0;
0470         break;
0471     case VerticalPosition::Relative:
0472         percentage = position.point.y();
0473     }
0474 
0475     relPos.setY(parentRect.y() + parentRect.height() * percentage - parentPos.y());
0476 
0477     return relPos;
0478 }
0479 
0480 /*!
0481  * \brief relativePosToParentPos
0482  * \param parentRect
0483  * \param rect element's rect
0484  * \param position contains the alignment of the element to the parent
0485  * \return parent position
0486  */
0487 QPointF WorksheetElement::relativePosToParentPos(PositionWrapper position) const {
0488     // increasing relative pos hor --> right
0489     // increasing relative pos vert --> top
0490     // increasing parent pos hor --> right
0491     // increasing parent pos vert --> bottom
0492 
0493     QRectF parentRect = this->parentRect();
0494     QPointF parentPos;
0495 
0496     double percentage = 0.;
0497     switch (position.horizontalPosition) {
0498     case HorizontalPosition::Left:
0499     case HorizontalPosition::Relative:
0500         break;
0501     case HorizontalPosition::Center:
0502         percentage = 0.5;
0503         break;
0504     case HorizontalPosition::Right:
0505         percentage = 1.0;
0506         break;
0507     }
0508 
0509     if (position.horizontalPosition == HorizontalPosition::Relative)
0510         parentPos.setX(parentRect.x() + parentRect.width() * position.point.x());
0511     else
0512         parentPos.setX(parentRect.x() + parentRect.width() * percentage + position.point.x());
0513 
0514     switch (position.verticalPosition) {
0515     case VerticalPosition::Top:
0516         percentage = 0.;
0517         break;
0518     case VerticalPosition::Center:
0519         percentage = 0.5;
0520         break;
0521     case VerticalPosition::Bottom:
0522         percentage = 1.0;
0523         break;
0524     case VerticalPosition::Relative:
0525         break;
0526     }
0527 
0528     if (position.verticalPosition == VerticalPosition::Relative)
0529         parentPos.setY(parentRect.y() + parentRect.height() * position.point.y());
0530     else
0531         parentPos.setY(parentRect.y() + parentRect.height() * percentage - position.point.y());
0532 
0533     return parentPos;
0534 }
0535 
0536 void WorksheetElement::save(QXmlStreamWriter* writer) const {
0537     Q_D(const WorksheetElement);
0538     writer->writeAttribute(QStringLiteral("x"), QString::number(d->position.point.x()));
0539     writer->writeAttribute(QStringLiteral("y"), QString::number(d->position.point.y()));
0540     writer->writeAttribute(QStringLiteral("horizontalPosition"), QString::number(static_cast<int>(d->position.horizontalPosition)));
0541     writer->writeAttribute(QStringLiteral("verticalPosition"), QString::number(static_cast<int>(d->position.verticalPosition)));
0542     writer->writeAttribute(QStringLiteral("horizontalAlignment"), QString::number(static_cast<int>(d->horizontalAlignment)));
0543     writer->writeAttribute(QStringLiteral("verticalAlignment"), QString::number(static_cast<int>(d->verticalAlignment)));
0544     writer->writeAttribute(QStringLiteral("rotationAngle"), QString::number(d->rotation()));
0545     writer->writeAttribute(QStringLiteral("plotRangeIndex"), QString::number(m_cSystemIndex));
0546     writer->writeAttribute(QStringLiteral("visible"), QString::number(d->isVisible()));
0547     writer->writeAttribute(QStringLiteral("coordinateBinding"), QString::number(d->coordinateBindingEnabled));
0548     writer->writeAttribute(QStringLiteral("logicalPosX"), QString::number(d->positionLogical.x()));
0549     writer->writeAttribute(QStringLiteral("logicalPosY"), QString::number(d->positionLogical.y()));
0550     writer->writeAttribute(QStringLiteral("locked"), QString::number(d->lock));
0551 }
0552 
0553 bool WorksheetElement::load(XmlStreamReader* reader, bool preview) {
0554     if (preview)
0555         return true;
0556 
0557     Q_D(WorksheetElement);
0558     auto attribs = reader->attributes();
0559 
0560     auto str = attribs.value(QStringLiteral("x")).toString();
0561     if (str.isEmpty())
0562         reader->raiseMissingAttributeWarning(QStringLiteral("x"));
0563     else
0564         d->position.point.setX(str.toDouble());
0565 
0566     str = attribs.value(QStringLiteral("y")).toString();
0567     if (str.isEmpty())
0568         reader->raiseMissingAttributeWarning(QStringLiteral("y"));
0569     else
0570         d->position.point.setY(str.toDouble());
0571 
0572     READ_INT_VALUE("horizontalPosition", position.horizontalPosition, HorizontalPosition);
0573     READ_INT_VALUE("verticalPosition", position.verticalPosition, VerticalPosition);
0574     if (Project::xmlVersion() < 1) {
0575         // Before 2.9.0 the position.point is only used when horizontalPosition or
0576         // vertical position was set to Custom, otherwise the label was attached to the
0577         // "position" and it was not possible to arrange relative to this anchor point
0578         // From 2.9.0, the horizontalPosition and verticalPosition indicate the anchor
0579         // point and position.point indicates the distance to them
0580         if (d->position.horizontalPosition != HorizontalPosition::Relative) {
0581             d->position.point.setX(0);
0582             if (d->position.horizontalPosition == HorizontalPosition::Left)
0583                 d->horizontalAlignment = HorizontalAlignment::Left;
0584             else if (d->position.horizontalPosition == HorizontalPosition::Right)
0585                 d->horizontalAlignment = HorizontalAlignment::Right;
0586         } else // TODO
0587             d->position.horizontalPosition = HorizontalPosition::Center;
0588 
0589         if (d->position.verticalPosition != VerticalPosition::Relative) {
0590             d->position.point.setY(0);
0591             if (d->position.verticalPosition == VerticalPosition::Top)
0592                 d->verticalAlignment = VerticalAlignment::Top;
0593             else if (d->position.verticalPosition == VerticalPosition::Bottom)
0594                 d->verticalAlignment = VerticalAlignment::Bottom;
0595         } else // TODO
0596             d->position.verticalPosition = VerticalPosition::Center;
0597 
0598         // in the old format the order was reversed, multiply by -1 here
0599         d->position.point.setY(-d->position.point.y());
0600     } else {
0601         READ_INT_VALUE("horizontalAlignment", horizontalAlignment, HorizontalAlignment);
0602         READ_INT_VALUE("verticalAlignment", verticalAlignment, VerticalAlignment);
0603     }
0604     if (project()->xmlVersion() >= 8) {
0605         QGRAPHICSITEM_READ_DOUBLE_VALUE("rotationAngle", Rotation);
0606     } else {
0607         str = attribs.value(QStringLiteral("rotationAngle")).toString();
0608         if (str.isEmpty())
0609             reader->raiseMissingAttributeWarning(QStringLiteral("rotationAngle"));
0610         else
0611             d->setRotation(-1 * str.toDouble());
0612     }
0613     READ_INT_VALUE_DIRECT("plotRangeIndex", m_cSystemIndex, int);
0614 
0615     str = attribs.value(QStringLiteral("visible")).toString();
0616     if (str.isEmpty())
0617         reader->raiseMissingAttributeWarning(QStringLiteral("visible"));
0618     else
0619         d->setVisible(str.toInt());
0620 
0621     READ_INT_VALUE("coordinateBinding", coordinateBindingEnabled, bool);
0622 
0623     str = attribs.value(QStringLiteral("logicalPosX")).toString();
0624     if (str.isEmpty())
0625         reader->raiseMissingAttributeWarning(QStringLiteral("logicalPosX"));
0626     else
0627         d->positionLogical.setX(str.toDouble());
0628 
0629     str = attribs.value(QStringLiteral("logicalPosY")).toString();
0630     if (str.isEmpty())
0631         reader->raiseMissingAttributeWarning(QStringLiteral("logicalPosY"));
0632     else
0633         d->positionLogical.setY(str.toDouble());
0634 
0635     str = attribs.value(QStringLiteral("locked")).toString();
0636     if (str.isEmpty())
0637         reader->raiseMissingAttributeWarning(QStringLiteral("locked"));
0638     else
0639         d->lock = static_cast<bool>(str.toInt());
0640 
0641     return true;
0642 }
0643 
0644 void WorksheetElement::loadThemeConfig(const KConfig&) {
0645 }
0646 
0647 void WorksheetElement::saveThemeConfig(const KConfig&) {
0648 }
0649 
0650 // coordinate system
0651 
0652 class SetCoordinateSystemIndexCmd : public QUndoCommand {
0653 public:
0654     SetCoordinateSystemIndexCmd(WorksheetElement* element, int index, QUndoCommand* parent = nullptr)
0655         : QUndoCommand(parent)
0656         , m_element(element)
0657         , m_index(index) {
0658     }
0659 
0660     virtual void redo() override {
0661         const auto oldIndex = m_element->m_cSystemIndex;
0662         m_element->m_cSystemIndex = m_index;
0663         if (m_element->plot())
0664             m_element->cSystem = dynamic_cast<const CartesianCoordinateSystem*>(m_element->plot()->coordinateSystem(m_index));
0665         else
0666             DEBUG(Q_FUNC_INFO << ", WARNING: No plot found. Failed setting csystem index.")
0667 
0668         m_index = oldIndex;
0669         m_element->retransform();
0670         Q_EMIT m_element->coordinateSystemIndexChanged(m_element->m_cSystemIndex);
0671     }
0672 
0673     virtual void undo() override {
0674         redo();
0675     }
0676 
0677 private:
0678     WorksheetElement* m_element;
0679     int m_index;
0680 };
0681 
0682 void WorksheetElement::setCoordinateSystemIndex(int index, QUndoCommand* parent) {
0683     if (index != m_cSystemIndex) {
0684         auto* command = new SetCoordinateSystemIndexCmd(this, index, parent);
0685         if (!parent)
0686             exec(command);
0687     } else if (!cSystem) {
0688         // during load the index will be set,
0689         // but the element might not have yet a plot assigned
0690         if (plot())
0691             cSystem = dynamic_cast<const CartesianCoordinateSystem*>(plot()->coordinateSystem(index));
0692         retransform();
0693     }
0694 }
0695 
0696 int WorksheetElement::coordinateSystemCount() const {
0697     if (m_plot)
0698         return m_plot->coordinateSystemCount();
0699     DEBUG(Q_FUNC_INFO << ", WARNING: no plot set!")
0700 
0701     return 0;
0702 }
0703 
0704 QString WorksheetElement::coordinateSystemInfo(const int index) const {
0705     if (m_plot)
0706         return m_plot->coordinateSystem(index)->info();
0707 
0708     return {};
0709 }
0710 
0711 bool WorksheetElement::isHovered() const {
0712     Q_D(const WorksheetElement);
0713     return d->isHovered();
0714 }
0715 
0716 void WorksheetElement::setHover(bool on) {
0717     Q_D(WorksheetElement);
0718     d->setHover(on);
0719 }
0720 
0721 /* ============================ getter methods ================= */
0722 BASIC_SHARED_D_READER_IMPL(WorksheetElement, WorksheetElement::PositionWrapper, position, position)
0723 BASIC_SHARED_D_READER_IMPL(WorksheetElement, WorksheetElement::HorizontalAlignment, horizontalAlignment, horizontalAlignment)
0724 BASIC_SHARED_D_READER_IMPL(WorksheetElement, WorksheetElement::VerticalAlignment, verticalAlignment, verticalAlignment)
0725 BASIC_SHARED_D_READER_IMPL(WorksheetElement, QPointF, positionLogical, positionLogical)
0726 BASIC_SHARED_D_READER_IMPL(WorksheetElement,
0727                            qreal,
0728                            rotationAngle,
0729                            rotation() * -1) // the rotation is in qgraphicsitem different to the convention used in labplot
0730 BASIC_SHARED_D_READER_IMPL(WorksheetElement, bool, coordinateBindingEnabled, coordinateBindingEnabled)
0731 BASIC_SHARED_D_READER_IMPL(WorksheetElement, qreal, scale, scale())
0732 BASIC_SHARED_D_READER_IMPL(WorksheetElement, bool, isLocked, lock)
0733 
0734 /* ============================ setter methods and undo commands ================= */
0735 STD_SETTER_CMD_IMPL_F_S_SC(WorksheetElement, SetPosition, WorksheetElement::PositionWrapper, position, updatePosition, objectPositionChanged)
0736 void WorksheetElement::setPosition(const PositionWrapper& pos) {
0737     Q_D(WorksheetElement);
0738     if (pos.point != d->position.point || pos.horizontalPosition != d->position.horizontalPosition || pos.verticalPosition != d->position.verticalPosition
0739         || pos.positionLimit != d->position.positionLimit)
0740         exec(new WorksheetElementSetPositionCmd(d, pos, ki18n("%1: set position")));
0741 }
0742 
0743 STD_SETTER_CMD_IMPL_F_S_SC(WorksheetElement,
0744                            SetHorizontalAlignment,
0745                            WorksheetElement::HorizontalAlignment,
0746                            horizontalAlignment,
0747                            updatePosition,
0748                            objectPositionChanged)
0749 void WorksheetElement::setHorizontalAlignment(const WorksheetElement::HorizontalAlignment hAlign) {
0750     Q_D(WorksheetElement);
0751     if (hAlign != d->horizontalAlignment)
0752         exec(new WorksheetElementSetHorizontalAlignmentCmd(d, hAlign, ki18n("%1: set horizontal alignment")));
0753 }
0754 
0755 STD_SETTER_CMD_IMPL_F_S_SC(WorksheetElement,
0756                            SetVerticalAlignment,
0757                            WorksheetElement::VerticalAlignment,
0758                            verticalAlignment,
0759                            updatePosition,
0760                            objectPositionChanged)
0761 void WorksheetElement::setVerticalAlignment(const WorksheetElement::VerticalAlignment vAlign) {
0762     Q_D(WorksheetElement);
0763     if (vAlign != d->verticalAlignment)
0764         exec(new WorksheetElementSetVerticalAlignmentCmd(d, vAlign, ki18n("%1: set vertical alignment")));
0765 }
0766 
0767 STD_SETTER_CMD_IMPL_S(WorksheetElement, SetCoordinateBindingEnabled, bool, coordinateBindingEnabled) // do I need a final method?
0768 bool WorksheetElement::setCoordinateBindingEnabled(bool on) {
0769     Q_D(WorksheetElement);
0770     if (on && !cSystem)
0771         return false;
0772     if (on != d->coordinateBindingEnabled) {
0773         // Must not be in the Undo Command,
0774         // because if done once, logical and
0775         // scene pos are synched and therefore
0776         // when changing it does not have any visual effect
0777         d->updatePosition();
0778         exec(new WorksheetElementSetCoordinateBindingEnabledCmd(d, on, on ? ki18n("%1: use logical coordinates") : ki18n("%1: set invisible")));
0779         return true;
0780     }
0781     return true;
0782 }
0783 
0784 STD_SETTER_CMD_IMPL_F_S_SC(WorksheetElement, SetPositionLogical, QPointF, positionLogical, updatePosition, objectPositionChanged)
0785 void WorksheetElement::setPositionLogical(QPointF pos) {
0786     Q_D(WorksheetElement);
0787     if (pos != d->positionLogical)
0788         exec(new WorksheetElementSetPositionLogicalCmd(d, pos, ki18n("%1: set logical position")));
0789 }
0790 
0791 /*!
0792  * \brief WorksheetElement::setPosition
0793  * sets the position without undo/redo-stuff
0794  * \param point point in scene coordinates
0795  */
0796 void WorksheetElement::setPosition(QPointF point) {
0797     Q_D(WorksheetElement);
0798     if (point != d->position.point) {
0799         d->position.point = point;
0800         retransform();
0801     }
0802 }
0803 
0804 /*!
0805  * position is set to invalid if the parent item is not drawn on the scene
0806  * (e.g. axis is not drawn because it's outside plot ranges -> don't draw axis' title label)
0807  */
0808 void WorksheetElement::setPositionInvalid(bool invalid) {
0809     Q_D(WorksheetElement);
0810     if (invalid != d->positionInvalid)
0811         d->positionInvalid = invalid;
0812 }
0813 
0814 GRAPHICSITEM_SETTER_CMD_IMPL_F_S(WorksheetElement, SetRotationAngle, qreal, rotation, setRotation, recalcShapeAndBoundingRect)
0815 void WorksheetElement::setRotationAngle(qreal angle) {
0816     const qreal angle_graphicsItem = -angle;
0817     Q_D(WorksheetElement);
0818     if (angle_graphicsItem != d->rotation())
0819         exec(new WorksheetElementSetRotationAngleCmd(d, angle_graphicsItem, ki18n("%1: set rotation angle")));
0820 }
0821 
0822 // ##############################################################################
0823 // ####################### Private implementation ###############################
0824 // ##############################################################################
0825 WorksheetElementPrivate::WorksheetElementPrivate(WorksheetElement* owner)
0826     : q(owner) {
0827 }
0828 
0829 QString WorksheetElementPrivate::name() const {
0830     return q->name();
0831 }
0832 
0833 QRectF WorksheetElementPrivate::boundingRect() const {
0834     return m_boundingRectangle;
0835 }
0836 
0837 QPainterPath WorksheetElementPrivate::shape() const {
0838     return m_shape;
0839 }
0840 
0841 void WorksheetElementPrivate::paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) {
0842 }
0843 
0844 void WorksheetElementPrivate::updatePosition() {
0845     QPointF p;
0846     if (coordinateBindingEnabled && q->cSystem) {
0847         // the position in logical coordinates was changed, calculate the position in scene coordinates
0848         // insidePlot will get false if the point lies outside of the datarect
0849         p = q->cSystem->mapLogicalToScene(positionLogical, insidePlot, AbstractCoordinateSystem::MappingFlag::SuppressPageClippingVisible);
0850         position.point = q->parentPosToRelativePos(mapPlotAreaToParent(p), position);
0851         Q_EMIT q->positionChanged(position);
0852     } else {
0853         insidePlot = true; // not important if within the datarect or not
0854         p = q->relativePosToParentPos(position);
0855 
0856         // the position in scene coordinates was changed, calculate the position in logical coordinates
0857         if (q->cSystem && q->cSystem->isValid()) {
0858             positionLogical = q->cSystem->mapSceneToLogical(mapParentToPlotArea(p), AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);
0859             Q_EMIT q->positionLogicalChanged(positionLogical);
0860         }
0861     }
0862 
0863     p = q->align(p, boundingRect(), horizontalAlignment, verticalAlignment, true);
0864 
0865     suppressItemChangeEvent = true;
0866     setPos(p);
0867     suppressItemChangeEvent = false;
0868 
0869     Q_EMIT q->changed();
0870 }
0871 
0872 bool WorksheetElementPrivate::sceneEvent(QEvent* event) {
0873     // don't allow to move the element with the mouse or with the cursor keys if it's locked, react on all other events
0874     if (lock && (event->type() == QEvent::GraphicsSceneMouseMove || event->type() == QEvent::KeyPress)) {
0875         event->ignore();
0876         return true;
0877     }
0878     return QGraphicsItem::sceneEvent(event);
0879 }
0880 
0881 void WorksheetElementPrivate::keyPressEvent(QKeyEvent* event) {
0882     const bool keyVertical = event->key() == Qt::Key_Up || event->key() == Qt::Key_Down;
0883     const bool keyHorizontal = event->key() == Qt::Key_Left || event->key() == Qt::Key_Right;
0884     if ((keyHorizontal && position.positionLimit != WorksheetElement::PositionLimit::Y)
0885         || (keyVertical && position.positionLimit != WorksheetElement::PositionLimit::X)) {
0886         const int delta = 5; // always in scene coordinates
0887 
0888         WorksheetElement::PositionWrapper tempPosition = position;
0889         if (coordinateBindingEnabled && q->cSystem) {
0890             if (!q->cSystem->isValid())
0891                 return;
0892             // the position in logical coordinates was changed, calculate the position in scene coordinates
0893             bool visible;
0894             QPointF p = q->cSystem->mapLogicalToScene(positionLogical, visible, AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);
0895             if (event->key() == Qt::Key_Left) {
0896                 p.setX(p.x() - delta);
0897             } else if (event->key() == Qt::Key_Right) {
0898                 p.setX(p.x() + delta);
0899             } else if (event->key() == Qt::Key_Up) {
0900                 p.setY(p.y() - delta); // y-axis is reversed, change the sign here
0901             } else if (event->key() == Qt::Key_Down) {
0902                 p.setY(p.y() + delta);
0903             }
0904             auto pLogic = q->cSystem->mapSceneToLogical(p, AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);
0905             q->setPositionLogical(pLogic); // So it is undoable
0906         } else {
0907             QPointF point = q->parentPosToRelativePos(pos(), position);
0908             point = q->align(point, m_boundingRectangle, horizontalAlignment, verticalAlignment, false);
0909 
0910             if (event->key() == Qt::Key_Left) {
0911                 point.setX(point.x() - delta);
0912             } else if (event->key() == Qt::Key_Right) {
0913                 point.setX(point.x() + delta);
0914             } else if (event->key() == Qt::Key_Up) {
0915                 point.setY(point.y() + delta);
0916             } else if (event->key() == Qt::Key_Down) {
0917                 point.setY(point.y() - delta);
0918             }
0919             tempPosition.point = point;
0920             q->setPosition(tempPosition); // So it is undoable
0921         }
0922         event->accept();
0923     } else
0924         QGraphicsItem::keyPressEvent(event);
0925 }
0926 
0927 void WorksheetElementPrivate::mouseMoveEvent(QGraphicsSceneMouseEvent* event) {
0928     if (!m_moveStarted && event->button() == Qt::LeftButton)
0929         m_moveStarted = true;
0930 
0931     QGraphicsItem::mouseMoveEvent(event);
0932 }
0933 
0934 void WorksheetElementPrivate::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) {
0935     if (!m_moveStarted) {
0936         QGraphicsItem::mouseReleaseEvent(event);
0937         return;
0938     }
0939 
0940     // convert position of the item in parent coordinates to label's position
0941     QPointF point = q->parentPosToRelativePos(pos(), position);
0942     point = q->align(point, boundingRect(), horizontalAlignment, verticalAlignment, false);
0943     if (point != position.point) {
0944         // position was changed -> set the position related member variables
0945         suppressRetransform = true;
0946         WorksheetElement::PositionWrapper tempPosition = position;
0947         tempPosition.point = point;
0948         q->setPosition(tempPosition);
0949         updatePosition(); // to update the logical position if available
0950         suppressRetransform = false;
0951     }
0952 
0953     m_moveStarted = false;
0954     QGraphicsItem::mouseReleaseEvent(event);
0955 }
0956 
0957 QVariant WorksheetElementPrivate::itemChange(GraphicsItemChange change, const QVariant& value) {
0958     if (suppressItemChangeEvent)
0959         return value;
0960 
0961     if (change == QGraphicsItem::ItemPositionChange) {
0962         auto currPos = pos();
0963         auto newPos = value.toPointF();
0964         switch (position.positionLimit) {
0965         case WorksheetElement::PositionLimit::X:
0966             newPos.setY(currPos.y());
0967             break;
0968         case WorksheetElement::PositionLimit::Y:
0969             newPos.setX(currPos.x());
0970             break;
0971         case WorksheetElement::PositionLimit::None:
0972         default:
0973             break;
0974         }
0975 
0976         // don't use setPosition here, because then all small changes are on the undo stack
0977         // setPosition is used then in mouseReleaseEvent
0978         if (coordinateBindingEnabled) {
0979             if (!q->cSystem->isValid())
0980                 return QGraphicsItem::itemChange(change, value);
0981             QPointF pos = q->align(newPos, m_boundingRectangle, horizontalAlignment, verticalAlignment, false);
0982 
0983             positionLogical = q->cSystem->mapSceneToLogical(mapParentToPlotArea(pos), AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);
0984             Q_EMIT q->positionLogicalChanged(positionLogical);
0985             Q_EMIT q->objectPositionChanged();
0986         } else {
0987             // convert item's center point in parent's coordinates
0988             WorksheetElement::PositionWrapper tempPosition = position;
0989             tempPosition.point = q->parentPosToRelativePos(newPos, position);
0990             tempPosition.point = q->align(tempPosition.point, boundingRect(), horizontalAlignment, verticalAlignment, false);
0991 
0992             // Q_EMIT the signals in order to notify the UI.
0993             Q_EMIT q->positionChanged(tempPosition);
0994             Q_EMIT q->objectPositionChanged();
0995         }
0996         return QGraphicsItem::itemChange(change, newPos);
0997     }
0998 
0999     return QGraphicsItem::itemChange(change, value);
1000 }
1001 
1002 /*!
1003  * \brief TextLabelPrivate::mapParentToPlotArea
1004  * Mapping a point from parent coordinates to plotArea coordinates
1005  * Needed because in some cases the parent is not the PlotArea, but a child of it (Marker/InfoElement)
1006  * IMPORTANT: function is also used in Custompoint, so when changing anything, change it also there
1007  * \param point point in parent coordinates
1008  * \return point in PlotArea coordinates
1009  */
1010 QPointF WorksheetElementPrivate::mapParentToPlotArea(QPointF point) const {
1011     auto* parent = q->parent(AspectType::CartesianPlot);
1012     if (parent) {
1013         auto* plot = static_cast<CartesianPlot*>(parent);
1014         // mapping from parent to item coordinates and them to plot area
1015         return mapToItem(plot->plotArea()->graphicsItem(), mapFromParent(point));
1016     }
1017 
1018     return point; // don't map if no parent set. Then it's during load
1019 }
1020 
1021 /*!
1022  * \brief TextLabelPrivate::mapPlotAreaToParent
1023  * Mapping a point from the PlotArea (CartesianPlot::plotArea) coordinates to the parent
1024  * coordinates of this item
1025  * Needed because in some cases the parent is not the PlotArea, but a child of it (Marker/InfoElement)
1026  * IMPORTANT: function is also used in Custompoint, so when changing anything, change it also there
1027  * \param point point in plotArea coordinates
1028  * \return point in parent coordinates
1029  */
1030 QPointF WorksheetElementPrivate::mapPlotAreaToParent(QPointF point) const {
1031     auto* parent = q->parent(AspectType::CartesianPlot);
1032     if (parent) {
1033         auto* plot = static_cast<CartesianPlot*>(parent);
1034         // first mapping to item coordinates and from there back to parent
1035         // WorksheetinfoElement: parentItem()->parentItem() == plot->graphicsItem()
1036         // plot->graphicsItem().pos() == plot->plotArea()->graphicsItem().pos()
1037         return mapToParent(mapFromItem(plot->plotArea()->graphicsItem(), point));
1038     }
1039 
1040     return point; // don't map if no parent set. Then it's during load
1041 }
1042 
1043 void WorksheetElementPrivate::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
1044     if (!isSelected())
1045         setHover(true);
1046 }
1047 
1048 void WorksheetElementPrivate::hoverLeaveEvent(QGraphicsSceneHoverEvent*) {
1049     setHover(false);
1050 }
1051 
1052 void WorksheetElementPrivate::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) {
1053     // don't show any context menu if the element is hidden which is the case
1054     // for example for axis and plot title labels. For such objects the context menu
1055     // of their parents, i.e. of axis and plot, is used.
1056     if (!q->hidden()) {
1057         auto* menu = q->createContextMenu();
1058         if (menu)
1059             menu->exec(event->screenPos());
1060     }
1061 }
1062 
1063 bool WorksheetElementPrivate::isHovered() const {
1064     return m_hovered;
1065 }
1066 
1067 void WorksheetElementPrivate::setHover(bool on) {
1068     if (on == m_hovered)
1069         return; // don't update if state not changed
1070 
1071     m_hovered = on;
1072     Q_EMIT q->hoveredChanged(on);
1073     update();
1074 }