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

0001 /*
0002     File                 : ReferenceRange.cpp
0003     Project              : LabPlot
0004     Description          : Reference range on the plot
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2022-2023 Alexander Semke <alexander.semke@web.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "ReferenceRange.h"
0012 #include "ReferenceRangePrivate.h"
0013 #include "backend/lib/XmlStreamReader.h"
0014 #include "backend/lib/commandtemplates.h"
0015 #include "backend/worksheet/Background.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 /**
0032  * \class ReferenceRange
0033  * \brief A customizable point.
0034  *
0035  * The position can be either specified by mouse events or by providing the
0036  * x- and y- coordinates in parent's coordinate system
0037  */
0038 
0039 ReferenceRange::ReferenceRange(CartesianPlot* plot, const QString& name)
0040     : WorksheetElement(name, new ReferenceRangePrivate(this), AspectType::ReferenceRange) {
0041     m_plot = plot;
0042     init();
0043 }
0044 
0045 // no need to delete the d-pointer here - it inherits from QGraphicsItem
0046 // and is deleted during the cleanup in QGraphicsScene
0047 ReferenceRange::~ReferenceRange() = default;
0048 
0049 void ReferenceRange::init() {
0050     Q_D(ReferenceRange);
0051 
0052     KConfig config;
0053     KConfigGroup group = config.group(QStringLiteral("ReferenceRange"));
0054 
0055     d->orientation = (Orientation)group.readEntry(QStringLiteral("Orientation"), static_cast<int>(Orientation::Vertical));
0056     switch (d->orientation) {
0057     case WorksheetElement::Orientation::Horizontal:
0058         d->position.positionLimit = WorksheetElement::PositionLimit::Y;
0059         break;
0060     case WorksheetElement::Orientation::Vertical:
0061         d->position.positionLimit = WorksheetElement::PositionLimit::X;
0062         break;
0063     case WorksheetElement::Orientation::Both:
0064         d->position.positionLimit = WorksheetElement::PositionLimit::None;
0065         break;
0066     }
0067 
0068     if (plot()) {
0069         m_cSystemIndex = plot()->defaultCoordinateSystemIndex();
0070         cSystem = plot()->coordinateSystem(m_cSystemIndex);
0071         d->coordinateBindingEnabled = true;
0072         // default position - 10% of the plot width/height positioned around the center
0073         auto cs = plot()->coordinateSystem(coordinateSystemIndex());
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         const auto w = m_plot->range(Dimension::X, cs->index(Dimension::X)).length() * 0.1;
0077         const auto h = m_plot->range(Dimension::Y, cs->index(Dimension::Y)).length() * 0.1;
0078         d->positionLogical = QPointF(x, y);
0079         d->positionLogicalStart = QPointF(x - w / 2, y - h / 2);
0080         d->positionLogicalEnd = QPointF(x + w / 2, y + h / 2);
0081     } else
0082         d->position.point = QPointF(0, 0); // center of parent
0083     d->updatePosition(); // to update also scene coordinates
0084 
0085     // background
0086     d->background = new Background(QString());
0087     d->background->setEnabledAvailable(true);
0088     addChild(d->background);
0089     d->background->setHidden(true);
0090     d->background->init(group);
0091     connect(d->background, &Background::updateRequested, [=] {
0092         d->update();
0093     });
0094 
0095     // border
0096     d->line = new Line(QString());
0097     d->line->setHidden(true);
0098     addChild(d->line);
0099     d->line->init(group);
0100     connect(d->line, &Line::updatePixmapRequested, [=] {
0101         d->update();
0102     });
0103     connect(d->line, &Line::updateRequested, [=] {
0104         d->recalcShapeAndBoundingRect();
0105     });
0106 
0107     connect(this, &WorksheetElement::objectPositionChanged, this, &ReferenceRange::updateStartEndPositions);
0108     retransform();
0109 }
0110 
0111 /*!
0112     Returns an icon to be used in the project explorer.
0113 */
0114 QIcon ReferenceRange::icon() const {
0115     return QIcon::fromTheme(QStringLiteral("draw-rectangle"));
0116 }
0117 
0118 void ReferenceRange::initActions() {
0119     // Orientation
0120     auto* orientationActionGroup = new QActionGroup(this);
0121     orientationActionGroup->setExclusive(true);
0122     connect(orientationActionGroup, &QActionGroup::triggered, this, &ReferenceRange::orientationChangedSlot);
0123 
0124     orientationHorizontalAction = new QAction(QIcon::fromTheme(QStringLiteral("labplot-axis-horizontal")), i18n("Horizontal"), orientationActionGroup);
0125     orientationHorizontalAction->setCheckable(true);
0126 
0127     orientationVerticalAction = new QAction(QIcon::fromTheme(QStringLiteral("labplot-axis-vertical")), i18n("Vertical"), orientationActionGroup);
0128     orientationVerticalAction->setCheckable(true);
0129 
0130     // Line
0131     lineStyleActionGroup = new QActionGroup(this);
0132     lineStyleActionGroup->setExclusive(true);
0133     connect(lineStyleActionGroup, &QActionGroup::triggered, this, &ReferenceRange::lineStyleChanged);
0134 
0135     lineColorActionGroup = new QActionGroup(this);
0136     lineColorActionGroup->setExclusive(true);
0137     connect(lineColorActionGroup, &QActionGroup::triggered, this, &ReferenceRange::lineColorChanged);
0138 }
0139 
0140 void ReferenceRange::initMenus() {
0141     this->initActions();
0142 
0143     // Orientation
0144     orientationMenu = new QMenu(i18n("Orientation"));
0145     orientationMenu->setIcon(QIcon::fromTheme(QStringLiteral("labplot-axis-horizontal")));
0146     orientationMenu->addAction(orientationHorizontalAction);
0147     orientationMenu->addAction(orientationVerticalAction);
0148 
0149     // Line
0150     lineMenu = new QMenu(i18n("Border Line"));
0151     lineMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-line")));
0152     lineStyleMenu = new QMenu(i18n("Style"), lineMenu);
0153     lineStyleMenu->setIcon(QIcon::fromTheme(QStringLiteral("object-stroke-style")));
0154     lineMenu->setIcon(QIcon::fromTheme(QStringLiteral("draw-line")));
0155     lineMenu->addMenu(lineStyleMenu);
0156 
0157     lineColorMenu = new QMenu(i18n("Color"), lineMenu);
0158     lineColorMenu->setIcon(QIcon::fromTheme(QStringLiteral("fill-color")));
0159     GuiTools::fillColorMenu(lineColorMenu, lineColorActionGroup);
0160     lineMenu->addMenu(lineColorMenu);
0161 }
0162 
0163 QMenu* ReferenceRange::createContextMenu() {
0164     if (!orientationMenu)
0165         initMenus();
0166 
0167     QMenu* menu = WorksheetElement::createContextMenu();
0168     QAction* visibilityAction = this->visibilityAction();
0169 
0170     Q_D(const ReferenceRange);
0171 
0172     // Orientation
0173     if (d->orientation == Orientation::Horizontal)
0174         orientationHorizontalAction->setChecked(true);
0175     else
0176         orientationVerticalAction->setChecked(true);
0177     menu->insertMenu(visibilityAction, orientationMenu);
0178 
0179     // Border line styles
0180     const auto& pen = d->line->pen();
0181     GuiTools::updatePenStyles(lineStyleMenu, lineStyleActionGroup, pen.color());
0182     GuiTools::selectPenStyleAction(lineStyleActionGroup, pen.style());
0183     GuiTools::selectColorAction(lineColorActionGroup, pen.color());
0184 
0185     menu->insertMenu(visibilityAction, lineMenu);
0186     menu->insertSeparator(visibilityAction);
0187 
0188     return menu;
0189 }
0190 
0191 void ReferenceRange::retransform() {
0192     Q_D(ReferenceRange);
0193     d->retransform();
0194 }
0195 
0196 void ReferenceRange::handleResize(double /*horizontalRatio*/, double /*verticalRatio*/, bool /*pageResize*/) {
0197 }
0198 
0199 /* ============================ getter methods ================= */
0200 BASIC_SHARED_D_READER_IMPL(ReferenceRange, ReferenceRange::Orientation, orientation, orientation)
0201 BASIC_SHARED_D_READER_IMPL(ReferenceRange, QPointF, positionLogicalStart, positionLogicalStart)
0202 BASIC_SHARED_D_READER_IMPL(ReferenceRange, QPointF, positionLogicalEnd, positionLogicalEnd)
0203 
0204 Line* ReferenceRange::line() const {
0205     Q_D(const ReferenceRange);
0206     return d->line;
0207 }
0208 
0209 Background* ReferenceRange::background() const {
0210     Q_D(const ReferenceRange);
0211     return d->background;
0212 }
0213 
0214 /* ============================ setter methods and undo commands ================= */
0215 STD_SETTER_CMD_IMPL_F_S(ReferenceRange, SetOrientation, ReferenceRange::Orientation, orientation, updateOrientation)
0216 void ReferenceRange::setOrientation(Orientation orientation) {
0217     Q_D(ReferenceRange);
0218     if (orientation != d->orientation)
0219         exec(new ReferenceRangeSetOrientationCmd(d, orientation, ki18n("%1: set orientation")));
0220 }
0221 
0222 STD_SETTER_CMD_IMPL_F_S(ReferenceRange, SetPositionLogicalStart, QPointF, positionLogicalStart, retransform)
0223 void ReferenceRange::setPositionLogicalStart(QPointF pos) {
0224     Q_D(ReferenceRange);
0225     if (pos != d->positionLogicalStart)
0226         exec(new ReferenceRangeSetPositionLogicalStartCmd(d, pos, ki18n("%1: set start logical position")));
0227 }
0228 
0229 STD_SETTER_CMD_IMPL_F_S(ReferenceRange, SetPositionLogicalEnd, QPointF, positionLogicalEnd, retransform)
0230 void ReferenceRange::setPositionLogicalEnd(QPointF pos) {
0231     Q_D(ReferenceRange);
0232     if (pos != d->positionLogicalEnd)
0233         exec(new ReferenceRangeSetPositionLogicalEndCmd(d, pos, ki18n("%1: set end logical position")));
0234 }
0235 
0236 // ##############################################################################
0237 // ######  SLOTs for changes triggered via QActions in the context menu  ########
0238 // ##############################################################################
0239 void ReferenceRange::orientationChangedSlot(QAction* action) {
0240     if (action == orientationHorizontalAction)
0241         this->setOrientation(Orientation::Horizontal);
0242     else
0243         this->setOrientation(Orientation::Vertical);
0244 }
0245 
0246 void ReferenceRange::lineStyleChanged(QAction* action) {
0247     Q_D(const ReferenceRange);
0248     d->line->setStyle(GuiTools::penStyleFromAction(lineStyleActionGroup, action));
0249 }
0250 
0251 void ReferenceRange::lineColorChanged(QAction* action) {
0252     Q_D(const ReferenceRange);
0253     d->line->setColor(GuiTools::colorFromAction(lineColorActionGroup, action));
0254 }
0255 
0256 // ##############################################################################
0257 // ####################### Private implementation ###############################
0258 // ##############################################################################
0259 ReferenceRangePrivate::ReferenceRangePrivate(ReferenceRange* owner)
0260     : WorksheetElementPrivate(owner)
0261     , q(owner) {
0262     setFlag(QGraphicsItem::ItemSendsGeometryChanges);
0263     setFlag(QGraphicsItem::ItemIsMovable);
0264     setFlag(QGraphicsItem::ItemIsSelectable);
0265     setFlag(QGraphicsItem::ItemIsFocusable);
0266     setAcceptHoverEvents(true);
0267 }
0268 
0269 QPointF ReferenceRangePrivate::recalculateRect() {
0270     auto cs = q->plot()->coordinateSystem(q->coordinateSystemIndex());
0271     if (!cs->isValid())
0272         return QPointF();
0273 
0274     // calculate rect in logical coordinates
0275     QPointF p1, p2;
0276     switch (orientation) {
0277     case ReferenceRange::Orientation::Vertical: {
0278         const auto yRange{q->m_plot->range(Dimension::Y, cs->index(Dimension::Y))};
0279         p1 = QPointF(positionLogicalStart.x(), yRange.start());
0280         p2 = QPointF(positionLogicalEnd.x(), yRange.end());
0281         break;
0282     }
0283     case ReferenceRange::Orientation::Horizontal: {
0284         const auto xRange{q->m_plot->range(Dimension::X, cs->index(Dimension::X))};
0285         p1 = QPointF(xRange.start(), positionLogicalStart.y());
0286         p2 = QPointF(xRange.end(), positionLogicalEnd.y());
0287         break;
0288     }
0289     case ReferenceRange::Orientation::Both: {
0290         p1 = QPointF(positionLogicalStart.x(), positionLogicalStart.y());
0291         p2 = QPointF(positionLogicalEnd.x(), positionLogicalEnd.y());
0292         break;
0293     }
0294     }
0295     const auto pointsSceneUnclipped = cs->mapLogicalToScene({p1, p2}, AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);
0296     // New position without considering clipping!
0297     const auto newPosScene =
0298         QPointF((pointsSceneUnclipped.at(0).x() + pointsSceneUnclipped.at(1).x()) / 2, (pointsSceneUnclipped.at(0).y() + pointsSceneUnclipped.at(1).y()) / 2);
0299 
0300     auto point0Clipped = pointsSceneUnclipped.at(0);
0301     auto point1Clipped = pointsSceneUnclipped.at(1);
0302 
0303     const auto diffXUnclipped = qAbs(point0Clipped.x() - point1Clipped.x());
0304     const auto diffYUnclipped = qAbs(point0Clipped.y() - point1Clipped.y());
0305 
0306     // Clipping
0307     const QRectF& dataRect = static_cast<const CartesianPlot*>(q->plot())->dataRect();
0308     if (point0Clipped.x() < point1Clipped.x()) {
0309         if (point0Clipped.x() < dataRect.left()) {
0310             m_leftClipped = true;
0311             point0Clipped.setX(dataRect.left());
0312         } else
0313             m_leftClipped = false;
0314 
0315         if (point1Clipped.x() > dataRect.right()) {
0316             m_rightClipped = true;
0317             point1Clipped.setX(dataRect.right());
0318         } else
0319             m_rightClipped = false;
0320         const auto diffX = point1Clipped.x() - point0Clipped.x();
0321         rect.setX(-diffXUnclipped / 2 + point0Clipped.x()
0322                   - pointsSceneUnclipped.at(0).x()); // -diffXUnclipped/2 is the value it would be shifted if no clipping happens
0323         if (diffX >= 0)
0324             rect.setWidth(diffX);
0325         else
0326             rect.setWidth(0);
0327     } else {
0328         if (point1Clipped.x() < dataRect.left()) {
0329             m_leftClipped = true;
0330             point1Clipped.setX(dataRect.left());
0331         } else
0332             m_leftClipped = false;
0333 
0334         if (point0Clipped.x() > dataRect.right()) {
0335             m_rightClipped = true;
0336             point0Clipped.setX(dataRect.right());
0337         } else
0338             m_rightClipped = false;
0339         const auto diffX = point0Clipped.x() - point1Clipped.x();
0340         rect.setX(-diffXUnclipped / 2 + point1Clipped.x() - pointsSceneUnclipped.at(1).x());
0341         if (diffX >= 0)
0342             rect.setWidth(diffX);
0343         else
0344             rect.setWidth(0);
0345     }
0346 
0347     if (point0Clipped.y() < point1Clipped.y()) {
0348         if (point0Clipped.y() < dataRect.top()) {
0349             m_topClipped = true;
0350             point0Clipped.setY(dataRect.top());
0351         } else
0352             m_topClipped = false;
0353 
0354         if (point1Clipped.y() > dataRect.bottom()) {
0355             m_bottomClipped = true;
0356             point1Clipped.setY(dataRect.bottom());
0357         } else
0358             m_bottomClipped = false;
0359         const auto diff = point1Clipped.y() - point0Clipped.y();
0360         rect.setY(-diffYUnclipped / 2 + point0Clipped.y() - pointsSceneUnclipped.at(0).y());
0361         if (diff >= 0)
0362             rect.setHeight(diff);
0363         else
0364             rect.setHeight(0);
0365     } else {
0366         if (point1Clipped.y() < dataRect.top()) {
0367             m_topClipped = true;
0368             point1Clipped.setY(dataRect.top());
0369         } else
0370             m_topClipped = false;
0371 
0372         if (point0Clipped.y() > dataRect.bottom()) {
0373             m_bottomClipped = true;
0374             point0Clipped.setY(dataRect.bottom());
0375         } else
0376             m_bottomClipped = false;
0377         const auto diff = point0Clipped.y() - point1Clipped.y();
0378         rect.setY(-diffYUnclipped / 2 + point1Clipped.y() - pointsSceneUnclipped.at(1).y());
0379         if (diff >= 0)
0380             rect.setHeight(diff);
0381         else
0382             rect.setHeight(0);
0383     }
0384 
0385     recalcShapeAndBoundingRect();
0386     return newPosScene;
0387 }
0388 
0389 /*!
0390     calculates the position and the bounding box of the item/point. Called on geometry or properties changes.
0391  */
0392 void ReferenceRangePrivate::retransform() {
0393     if (suppressRetransform || !q->cSystem || q->isLoading())
0394         return;
0395 
0396     const QPointF newPosScene = recalculateRect();
0397 
0398     auto cs = q->plot()->coordinateSystem(q->coordinateSystemIndex());
0399 
0400     positionLogical = cs->mapSceneToLogical(newPosScene, CartesianCoordinateSystem::MappingFlag::SuppressPageClipping);
0401     updatePosition();
0402 }
0403 
0404 void ReferenceRangePrivate::updateOrientation() {
0405     switch (orientation) {
0406     case WorksheetElement::Orientation::Horizontal:
0407         position.positionLimit = WorksheetElement::PositionLimit::Y;
0408         break;
0409     case WorksheetElement::Orientation::Vertical:
0410         position.positionLimit = WorksheetElement::PositionLimit::X;
0411         break;
0412     case WorksheetElement::Orientation::Both:
0413         position.positionLimit = WorksheetElement::PositionLimit::None;
0414         break;
0415     }
0416     retransform();
0417 }
0418 
0419 /*!
0420  * called when the user moves the graphics item with the mouse and the scene position of the item is changed.
0421  * Here we update the logical coordinates for the start and end points based on the new valud for the logical
0422  * position \c newPosition of the item's center and notify the dock widget.
0423  */
0424 // TODO: make this undo/redo-able
0425 void ReferenceRange::updateStartEndPositions() {
0426     Q_D(ReferenceRange);
0427     if (d->orientation == WorksheetElement::Orientation::Horizontal) {
0428         const double width = (d->positionLogicalEnd.y() - d->positionLogicalStart.y()) / 2;
0429         d->positionLogicalStart.setY(d->positionLogical.y() - width);
0430         d->positionLogicalEnd.setY(d->positionLogical.y() + width);
0431     } else {
0432         const double width = (d->positionLogicalEnd.x() - d->positionLogicalStart.x()) / 2;
0433         d->positionLogicalStart.setX(d->positionLogical.x() - width);
0434         d->positionLogicalEnd.setX(d->positionLogical.x() + width);
0435     }
0436 
0437     // Update boundingrect
0438     d->recalculateRect();
0439 
0440     Q_EMIT positionLogicalStartChanged(d->positionLogicalStart);
0441     Q_EMIT positionLogicalEndChanged(d->positionLogicalEnd);
0442 }
0443 
0444 /*!
0445   recalculates the outer bounds and the shape of the item.
0446 */
0447 void ReferenceRangePrivate::recalcShapeAndBoundingRect() {
0448     prepareGeometryChange();
0449 
0450     m_shape = QPainterPath();
0451     if (m_visible) {
0452         QPainterPath path;
0453 
0454         if (!m_topClipped && !m_rightClipped && !m_bottomClipped && !m_leftClipped) {
0455             path.addRect(rect);
0456         } else {
0457             if (!m_topClipped) {
0458                 path.moveTo(rect.topLeft());
0459                 path.lineTo(rect.topRight());
0460             }
0461             if (!m_rightClipped) {
0462                 if (m_topClipped)
0463                     path.moveTo(rect.topRight());
0464                 path.lineTo(rect.bottomRight());
0465             }
0466             if (!m_bottomClipped) {
0467                 if (m_rightClipped)
0468                     path.moveTo(rect.bottomRight());
0469                 path.lineTo(rect.bottomLeft());
0470             }
0471             if (!m_leftClipped) {
0472                 if (m_bottomClipped)
0473                     path.moveTo(rect.bottomLeft());
0474                 path.lineTo(rect.topLeft());
0475             }
0476         }
0477 
0478         m_shape.addPath(WorksheetElement::shapeFromPath(path, line->pen()));
0479         m_boundingRectangle = m_shape.boundingRect();
0480     }
0481 }
0482 
0483 void ReferenceRangePrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) {
0484     if (!m_visible)
0485         return;
0486 
0487     if (rect.width() == 0 || rect.height() == 0)
0488         return;
0489 
0490     // draw filling
0491     if (background->enabled()) {
0492         painter->setOpacity(background->opacity());
0493         painter->setPen(Qt::NoPen);
0494         drawFilling(painter);
0495     }
0496 
0497     // draw the background
0498     painter->drawRect(rect);
0499 
0500     // draw the border
0501     if (line->style() != Qt::NoPen) {
0502         painter->setPen(line->pen());
0503         painter->setBrush(Qt::NoBrush);
0504         painter->setOpacity(line->opacity());
0505     }
0506 
0507     painter->drawPath(m_shape);
0508 
0509     if (m_hovered && !isSelected() && !q->isPrinting()) {
0510         painter->setPen(QPen(QApplication::palette().color(QPalette::Shadow), 2, Qt::SolidLine));
0511         painter->drawPath(m_shape);
0512     }
0513 
0514     if (isSelected() && !q->isPrinting()) {
0515         painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), 2, Qt::SolidLine));
0516         painter->drawPath(m_shape);
0517     }
0518 }
0519 
0520 void ReferenceRangePrivate::drawFilling(QPainter* painter) const {
0521     if (background->type() == Background::Type::Color) {
0522         switch (background->colorStyle()) {
0523         case Background::ColorStyle::SingleColor: {
0524             painter->setBrush(QBrush(background->firstColor()));
0525             break;
0526         }
0527         case Background::ColorStyle::HorizontalLinearGradient: {
0528             QLinearGradient linearGrad(rect.topLeft(), rect.topRight());
0529             linearGrad.setColorAt(0, background->firstColor());
0530             linearGrad.setColorAt(1, background->secondColor());
0531             painter->setBrush(QBrush(linearGrad));
0532             break;
0533         }
0534         case Background::ColorStyle::VerticalLinearGradient: {
0535             QLinearGradient linearGrad(rect.topLeft(), rect.bottomLeft());
0536             linearGrad.setColorAt(0, background->firstColor());
0537             linearGrad.setColorAt(1, background->secondColor());
0538             painter->setBrush(QBrush(linearGrad));
0539             break;
0540         }
0541         case Background::ColorStyle::TopLeftDiagonalLinearGradient: {
0542             QLinearGradient linearGrad(rect.topLeft(), rect.bottomRight());
0543             linearGrad.setColorAt(0, background->firstColor());
0544             linearGrad.setColorAt(1, background->secondColor());
0545             painter->setBrush(QBrush(linearGrad));
0546             break;
0547         }
0548         case Background::ColorStyle::BottomLeftDiagonalLinearGradient: {
0549             QLinearGradient linearGrad(rect.bottomLeft(), rect.topRight());
0550             linearGrad.setColorAt(0, background->firstColor());
0551             linearGrad.setColorAt(1, background->secondColor());
0552             painter->setBrush(QBrush(linearGrad));
0553             break;
0554         }
0555         case Background::ColorStyle::RadialGradient: {
0556             QRadialGradient radialGrad(rect.center(), rect.width() / 2);
0557             radialGrad.setColorAt(0, background->firstColor());
0558             radialGrad.setColorAt(1, background->secondColor());
0559             painter->setBrush(QBrush(radialGrad));
0560             break;
0561         }
0562         }
0563     } else if (background->type() == Background::Type::Image) {
0564         if (!background->fileName().trimmed().isEmpty()) {
0565             QPixmap pix(background->fileName());
0566             switch (background->imageStyle()) {
0567             case Background::ImageStyle::ScaledCropped:
0568                 pix = pix.scaled(rect.size().toSize(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
0569                 painter->setBrush(QBrush(pix));
0570                 painter->setBrushOrigin(pix.size().width() / 2, pix.size().height() / 2);
0571                 break;
0572             case Background::ImageStyle::Scaled:
0573                 pix = pix.scaled(rect.size().toSize(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0574                 painter->setBrush(QBrush(pix));
0575                 painter->setBrushOrigin(pix.size().width() / 2, pix.size().height() / 2);
0576                 break;
0577             case Background::ImageStyle::ScaledAspectRatio:
0578                 pix = pix.scaled(rect.size().toSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
0579                 painter->setBrush(QBrush(pix));
0580                 painter->setBrushOrigin(pix.size().width() / 2, pix.size().height() / 2);
0581                 break;
0582             case Background::ImageStyle::Centered:
0583                 painter->drawPixmap(QPointF(rect.center().x() - pix.size().width() / 2, rect.center().y() - pix.size().height() / 2), pix);
0584                 break;
0585             case Background::ImageStyle::Tiled:
0586                 painter->setBrush(QBrush(pix));
0587                 break;
0588             case Background::ImageStyle::CenterTiled:
0589                 painter->setBrush(QBrush(pix));
0590                 painter->setBrushOrigin(pix.size().width() / 2, pix.size().height() / 2);
0591             }
0592         }
0593     } else if (background->type() == Background::Type::Pattern) {
0594         painter->setBrush(QBrush(background->firstColor(), background->brushStyle()));
0595     }
0596 }
0597 
0598 // ##############################################################################
0599 // ##################  Serialization/Deserialization  ###########################
0600 // ##############################################################################
0601 //! Save as XML
0602 void ReferenceRange::save(QXmlStreamWriter* writer) const {
0603     Q_D(const ReferenceRange);
0604 
0605     writer->writeStartElement(QStringLiteral("referenceRange"));
0606     writeBasicAttributes(writer);
0607     writeCommentElement(writer);
0608 
0609     // position and orientation
0610     writer->writeStartElement(QStringLiteral("geometry"));
0611     WorksheetElement::save(writer);
0612     writer->writeAttribute(QStringLiteral("logicalPosStartX"), QString::number(d->positionLogicalStart.x()));
0613     writer->writeAttribute(QStringLiteral("logicalPosStartY"), QString::number(d->positionLogicalStart.y()));
0614     writer->writeAttribute(QStringLiteral("logicalPosEndX"), QString::number(d->positionLogicalEnd.x()));
0615     writer->writeAttribute(QStringLiteral("logicalPosEndY"), QString::number(d->positionLogicalEnd.y()));
0616     writer->writeAttribute(QStringLiteral("orientation"), QString::number(static_cast<int>(d->orientation)));
0617     writer->writeEndElement();
0618 
0619     d->background->save(writer);
0620     d->line->save(writer);
0621 
0622     writer->writeEndElement(); // close "ReferenceRange" section
0623 }
0624 
0625 //! Load from XML
0626 bool ReferenceRange::load(XmlStreamReader* reader, bool preview) {
0627     Q_D(ReferenceRange);
0628 
0629     if (!readBasicAttributes(reader))
0630         return false;
0631 
0632     QXmlStreamAttributes attribs;
0633     QString str;
0634 
0635     while (!reader->atEnd()) {
0636         reader->readNext();
0637         if (reader->isEndElement() && reader->name() == QStringLiteral("referenceRange"))
0638             break;
0639 
0640         if (!reader->isStartElement())
0641             continue;
0642 
0643         if (!preview && reader->name() == QStringLiteral("comment")) {
0644             if (!readCommentElement(reader))
0645                 return false;
0646         } else if (!preview && reader->name() == QStringLiteral("geometry")) {
0647             attribs = reader->attributes();
0648             READ_INT_VALUE("orientation", orientation, Orientation);
0649             WorksheetElement::load(reader, preview);
0650 
0651             str = attribs.value(QStringLiteral("logicalPosStartX")).toString();
0652             if (str.isEmpty())
0653                 reader->raiseMissingAttributeWarning(QStringLiteral("logicalPosStartX"));
0654             else
0655                 d->positionLogicalStart.setX(str.toDouble());
0656 
0657             str = attribs.value(QStringLiteral("logicalPosStartY")).toString();
0658             if (str.isEmpty())
0659                 reader->raiseMissingAttributeWarning(QStringLiteral("logicalPosStartY"));
0660             else
0661                 d->positionLogicalStart.setY(str.toDouble());
0662 
0663             str = attribs.value(QStringLiteral("logicalPosEndX")).toString();
0664             if (str.isEmpty())
0665                 reader->raiseMissingAttributeWarning(QStringLiteral("logicalPosEndX"));
0666             else
0667                 d->positionLogicalEnd.setX(str.toDouble());
0668 
0669             str = attribs.value(QStringLiteral("logicalPosEndY")).toString();
0670             if (str.isEmpty())
0671                 reader->raiseMissingAttributeWarning(QStringLiteral("logicalPosEndY"));
0672             else
0673                 d->positionLogicalEnd.setY(str.toDouble());
0674         } else if (!preview && reader->name() == QStringLiteral("background"))
0675             d->background->load(reader, preview);
0676         else if (!preview && reader->name() == QStringLiteral("line"))
0677             d->line->load(reader, preview);
0678         else { // unknown element
0679             reader->raiseUnknownElementWarning();
0680             if (!reader->skipToEndElement())
0681                 return false;
0682         }
0683     }
0684     return true;
0685 }
0686 
0687 // ##############################################################################
0688 // #########################  Theme management ##################################
0689 // ##############################################################################
0690 void ReferenceRange::loadThemeConfig(const KConfig& config) {
0691     // determine the index of the current range in the list of all range children
0692     // and apply the theme color for this index
0693     const auto* plot = dynamic_cast<const CartesianPlot*>(parentAspect());
0694     if (!plot)
0695         return;
0696 
0697     int index = 0;
0698     const auto& children = plot->children<WorksheetElement>();
0699     for (auto* child : children) {
0700         if (child == this)
0701             break;
0702 
0703         if (child->inherits(AspectType::ReferenceRange))
0704             ++index;
0705     }
0706 
0707     const auto& themeColor = plot->themeColorPalette(index);
0708 
0709     KConfigGroup group;
0710     if (config.hasGroup(QStringLiteral("Theme")))
0711         group = config.group(QStringLiteral("Axis")); // when loading from the theme config, use the same properties as for Axis
0712     else
0713         group = config.group(QStringLiteral("ReferenceRange"));
0714 
0715     Q_D(ReferenceRange);
0716     d->line->loadThemeConfig(group);
0717     d->background->loadThemeConfig(group, themeColor);
0718 }