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

0001 /*
0002     File                 : TextLabel.cpp
0003     Project              : LabPlot
0004     Description          : Text label supporting reach text and latex formatting
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2009 Tilman Benkert <thzs@gmx.net>
0007     SPDX-FileCopyrightText: 2012-2023 Alexander Semke <alexander.semke@web.de>
0008     SPDX-FileCopyrightText: 2019-2022 Stefan Gerlach <stefan.gerlach@uni.kn>
0009 
0010     SPDX-License-Identifier: GPL-2.0-or-later
0011 */
0012 
0013 #include "TextLabel.h"
0014 #include "TextLabelPrivate.h"
0015 #include "Worksheet.h"
0016 #include "backend/core/Project.h"
0017 #include "backend/core/Settings.h"
0018 #include "backend/lib/XmlStreamReader.h"
0019 #include "backend/lib/commandtemplates.h"
0020 #include "backend/lib/macros.h"
0021 #include "backend/worksheet/plots/PlotArea.h"
0022 #include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h"
0023 #include "backend/worksheet/plots/cartesian/CartesianPlot.h"
0024 #include "kdefrontend/GuiTools.h"
0025 
0026 #include <QApplication>
0027 #include <QBuffer>
0028 #include <QGraphicsScene>
0029 #include <QGraphicsSceneMouseEvent>
0030 #include <QKeyEvent>
0031 #include <QMenu>
0032 #include <QPainter>
0033 #include <QTextCharFormat>
0034 #include <QTextCursor>
0035 #include <QTextDocument>
0036 #include <QtConcurrent/QtConcurrentRun>
0037 
0038 #include <KConfig>
0039 #include <KConfigGroup>
0040 #include <KLocalizedString>
0041 #include <QIcon>
0042 
0043 #ifdef HAVE_DISCOUNT
0044 extern "C" {
0045 #include <mkdio.h>
0046 }
0047 #endif
0048 
0049 class ScaledTextItem : public QGraphicsTextItem {
0050 public:
0051     explicit ScaledTextItem(QGraphicsItem* parent = nullptr)
0052         : QGraphicsTextItem(parent) {
0053     }
0054 
0055     QRectF scaledBoundingRect() const {
0056         auto rect = QGraphicsTextItem::boundingRect();
0057         rect.setHeight(rect.height() * scale());
0058         rect.setWidth(rect.width() * scale());
0059         // rect.moveTopLeft(QPointF(-rect.width() * scale() / 2, -rect.height() * scale() / 2));
0060         return rect;
0061     }
0062 
0063 protected:
0064     void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override {
0065 #if DEBUG_TEXTLABEL_BOUNDING_RECT
0066         painter->setPen(QColor(Qt::GlobalColor::green));
0067         painter->drawRect(boundingRect());
0068 
0069         painter->setPen(QColor(Qt::GlobalColor::black));
0070         painter->drawRect(QRectF(-5, -5, 10, 10));
0071 #endif
0072         QGraphicsTextItem::paint(painter, option, widget);
0073     }
0074 
0075 private:
0076     QRectF boundingRect() const override {
0077         return QGraphicsTextItem::boundingRect();
0078     }
0079 
0080 private:
0081 };
0082 
0083 /**
0084  * \class TextLabel
0085  * \brief A label supporting rendering of html- and tex-formatted texts.
0086  *
0087  * The label is aligned relative to the specified position.
0088  * The position can be either specified by providing the x- and y- coordinates
0089  * in parent's coordinate system, or by specifying one of the predefined position
0090  * flags (\c HorizontalPosition, \c VerticalPosition).
0091  */
0092 
0093 TextLabel::TextLabel(const QString& name, Type type)
0094     : WorksheetElement(name, new TextLabelPrivate(this), AspectType::TextLabel)
0095     , m_type(type) {
0096     init();
0097 }
0098 
0099 TextLabel::TextLabel(const QString& name, TextLabelPrivate* dd, Type type)
0100     : WorksheetElement(name, dd, AspectType::TextLabel)
0101     , m_type(type) {
0102     init();
0103 }
0104 
0105 TextLabel::TextLabel(const QString& name, CartesianPlot* plot, Type type)
0106     : WorksheetElement(name, new TextLabelPrivate(this), AspectType::TextLabel)
0107     , m_type(type) {
0108     m_plot = plot;
0109     cSystem = dynamic_cast<const CartesianCoordinateSystem*>(m_plot->coordinateSystem(m_cSystemIndex));
0110     init();
0111 }
0112 
0113 void TextLabel::init() {
0114     QString groupName;
0115     switch (m_type) {
0116     case Type::General:
0117         groupName = QStringLiteral("TextLabel");
0118         break;
0119     case Type::PlotTitle:
0120         groupName = QStringLiteral("PlotTitle");
0121         break;
0122     case Type::AxisTitle:
0123         groupName = QStringLiteral("AxisTitle");
0124         break;
0125     case Type::PlotLegendTitle:
0126         groupName = QStringLiteral("PlotLegendTitle");
0127         break;
0128     case Type::InfoElementLabel:
0129         groupName = QStringLiteral("InfoElementLabel");
0130     }
0131 
0132     const KConfig config;
0133     DEBUG(" config has group \"" << STDSTRING(groupName) << "\": " << config.hasGroup(groupName));
0134     // group is always valid if you call config.group(..;
0135     KConfigGroup group;
0136     if (config.hasGroup(groupName))
0137         group = config.group(groupName);
0138 
0139     Q_D(TextLabel);
0140     d->position.point = QPointF(0, 0);
0141     if (m_type == Type::PlotTitle || m_type == Type::PlotLegendTitle) {
0142         d->position.verticalPosition = WorksheetElement::VerticalPosition::Top;
0143         d->position.horizontalPosition = WorksheetElement::HorizontalPosition::Center;
0144         d->verticalAlignment = WorksheetElement::VerticalAlignment::Top;
0145     } else if (m_type == Type::AxisTitle) {
0146         d->position.horizontalPosition = WorksheetElement::HorizontalPosition::Center;
0147         d->position.verticalPosition = WorksheetElement::VerticalPosition::Center;
0148     }
0149 
0150     KConfigGroup conf = Settings::group(QStringLiteral("Settings_Worksheet"));
0151     const auto& engine = conf.readEntry(QStringLiteral("LaTeXEngine"), "");
0152     if (engine == QLatin1String("lualatex"))
0153         d->teXFont.setFamily(QStringLiteral("Latin Modern Roman"));
0154 
0155     // read settings from config if group exists
0156     if (group.isValid()) {
0157         // properties common to all types
0158         d->textWrapper.mode = static_cast<TextLabel::Mode>(group.readEntry(QStringLiteral("Mode"), static_cast<int>(d->textWrapper.mode)));
0159         d->teXFont.setFamily(group.readEntry(QStringLiteral("TeXFontFamily"), d->teXFont.family()));
0160         d->teXFont.setPointSize(group.readEntry(QStringLiteral("TeXFontSize"), d->teXFont.pointSize()));
0161         d->fontColor = group.readEntry(QStringLiteral("FontColor"), d->fontColor);
0162         d->backgroundColor = group.readEntry(QStringLiteral("BackgroundColor"), d->backgroundColor);
0163         d->setRotation(group.readEntry(QStringLiteral("Rotation"), d->rotation()));
0164 
0165         // border
0166         d->borderShape = (TextLabel::BorderShape)group.readEntry(QStringLiteral("BorderShape"), (int)d->borderShape);
0167         d->borderPen = QPen(group.readEntry(QStringLiteral("BorderColor"), d->borderPen.color()),
0168                             group.readEntry(QStringLiteral("BorderWidth"), d->borderPen.width()),
0169                             (Qt::PenStyle)group.readEntry(QStringLiteral("BorderStyle"), (int)(d->borderPen.style())));
0170         d->borderOpacity = group.readEntry(QStringLiteral("BorderOpacity"), d->borderOpacity);
0171 
0172         // position and alignment relevant properties
0173         d->position.point.setX(group.readEntry(QStringLiteral("PositionXValue"), 0.));
0174         d->position.point.setY(group.readEntry(QStringLiteral("PositionYValue"), 0.));
0175         d->position.horizontalPosition = (HorizontalPosition)group.readEntry(QStringLiteral("PositionX"), (int)d->position.horizontalPosition);
0176         d->position.verticalPosition = (VerticalPosition)group.readEntry(QStringLiteral("PositionY"), (int)d->position.verticalPosition);
0177         d->horizontalAlignment =
0178             (WorksheetElement::HorizontalAlignment)group.readEntry(QStringLiteral("HorizontalAlignment"), static_cast<int>(d->horizontalAlignment));
0179         d->verticalAlignment =
0180             (WorksheetElement::VerticalAlignment)group.readEntry(QStringLiteral("VerticalAlignment"), static_cast<int>(d->verticalAlignment));
0181         if (cSystem && cSystem->isValid())
0182             d->positionLogical = cSystem->mapSceneToLogical(d->position.point, AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);
0183     }
0184     d->updatePosition();
0185 
0186     DEBUG(Q_FUNC_INFO << ", default/run time image resolution: " << d->teXImageResolution << '/' << QApplication::primaryScreen()->physicalDotsPerInchX());
0187     connect(&d->teXImageFutureWatcher, &QFutureWatcher<QByteArray>::finished, this, &TextLabel::updateTeXImage);
0188 }
0189 
0190 // no need to delete the d-pointer here - it inherits from QGraphicsItem
0191 // and is deleted during the cleanup in QGraphicsScene
0192 TextLabel::~TextLabel() = default;
0193 
0194 void TextLabel::setZoomFactor(double factor) {
0195     Q_D(TextLabel);
0196     d->setZoomFactor(factor);
0197 }
0198 
0199 void TextLabel::retransform() {
0200     Q_D(TextLabel);
0201     d->retransform();
0202 }
0203 
0204 void TextLabel::handleResize(double horizontalRatio, double verticalRatio, bool /*pageResize*/) {
0205     DEBUG(Q_FUNC_INFO);
0206 
0207     double ratio = 0;
0208     if (horizontalRatio > 1.0 || verticalRatio > 1.0)
0209         ratio = std::max(horizontalRatio, verticalRatio);
0210     else
0211         ratio = std::min(horizontalRatio, verticalRatio);
0212 
0213     Q_D(TextLabel);
0214     d->teXFont.setPointSizeF(d->teXFont.pointSizeF() * ratio);
0215     d->updateText();
0216 
0217     // TODO: doesn't seem to work
0218     QTextDocument doc;
0219     doc.setHtml(d->textWrapper.text);
0220     QTextCursor cursor(&doc);
0221     cursor.select(QTextCursor::Document);
0222     QTextCharFormat fmt = cursor.charFormat();
0223     QFont font = fmt.font();
0224     font.setPointSizeF(font.pointSizeF() * ratio);
0225     fmt.setFont(font);
0226     cursor.setCharFormat(fmt);
0227 }
0228 
0229 /*!
0230     Returns an icon to be used in the project explorer.
0231 */
0232 QIcon TextLabel::icon() const {
0233     switch (text().mode) {
0234     case Mode::Markdown:
0235         return QIcon::fromTheme(QStringLiteral("text-x-markdown"));
0236         break;
0237     case Mode::LaTeX:
0238         return QIcon::fromTheme(QStringLiteral("text-x-tex"));
0239         break;
0240     case Mode::Text:
0241     default:
0242         return QIcon::fromTheme(QStringLiteral("draw-text"));
0243         // return QIcon::fromTheme(QLatin1String("text-x-plain"));
0244     }
0245 }
0246 
0247 /* ============================ getter methods ================= */
0248 BASIC_SHARED_D_READER_IMPL(TextLabel, TextLabel::TextWrapper, text, textWrapper)
0249 BASIC_SHARED_D_READER_IMPL(TextLabel, QColor, fontColor, fontColor)
0250 BASIC_SHARED_D_READER_IMPL(TextLabel, QColor, backgroundColor, backgroundColor)
0251 BASIC_SHARED_D_READER_IMPL(TextLabel, QFont, teXFont, teXFont)
0252 BASIC_SHARED_D_READER_IMPL(TextLabel, TextLabel::BorderShape, borderShape, borderShape)
0253 BASIC_SHARED_D_READER_IMPL(TextLabel, QPen, borderPen, borderPen)
0254 BASIC_SHARED_D_READER_IMPL(TextLabel, qreal, borderOpacity, borderOpacity)
0255 
0256 /* ============================ setter methods and undo commands ================= */
0257 STD_SETTER_CMD_IMPL_F_S(TextLabel, SetTeXBackgroundColor, QColor, backgroundColor, updateText)
0258 void TextLabel::setBackgroundColor(const QColor color) {
0259     QDEBUG(Q_FUNC_INFO << ", color = " << color)
0260     Q_D(TextLabel);
0261     if (color != d->backgroundColor)
0262         exec(new TextLabelSetTeXBackgroundColorCmd(d, color, ki18n("%1: set background color")));
0263 }
0264 
0265 STD_SETTER_CMD_IMPL_F_S(TextLabel, SetText, TextLabel::TextWrapper, textWrapper, updateText)
0266 void TextLabel::setText(const TextWrapper& textWrapper) {
0267     Q_D(TextLabel);
0268     // DEBUG("********************\n" << Q_FUNC_INFO << ", old/new mode = " << (int)d->textWrapper.mode << " " << (int)textWrapper.mode)
0269     // DEBUG("\nOLD TEXT = " << STDSTRING(d->textWrapper.text) << '\n')
0270     // DEBUG("NEW TEXT = " << STDSTRING(textWrapper.text) << '\n')
0271     // QDEBUG("COLORS: color =" << d->fontColor << ", background color =" << d->backgroundColor)
0272 
0273     if (d->textWrapper != textWrapper) {
0274         bool oldEmpty = d->textWrapper.text.isEmpty();
0275         if (textWrapper.mode == TextLabel::Mode::Text && !textWrapper.text.isEmpty()) {
0276             QTextEdit pte(d->textWrapper.text); // te with previous text
0277             // restore formatting when text changes or switching back to text mode
0278             if (d->textWrapper.mode != TextLabel::Mode::Text || oldEmpty || pte.toPlainText().isEmpty()) {
0279                 // DEBUG("RESTORE FORMATTING")
0280                 QTextEdit te(d->textWrapper.text);
0281                 te.selectAll();
0282                 te.setText(textWrapper.text);
0283                 te.selectAll();
0284                 te.setTextColor(d->fontColor);
0285                 te.setTextBackgroundColor(d->backgroundColor);
0286 
0287                 TextWrapper tw = textWrapper;
0288                 tw.text = te.toHtml();
0289                 // DEBUG("\nTW TEXT = " << STDSTRING(tw.text) << std::endl)
0290                 exec(new TextLabelSetTextCmd(d, tw, ki18n("%1: set label text")));
0291             } else { // the existing text is being modified
0292                 QUndoCommand* parent = nullptr;
0293                 TextLabelSetTextCmd* command = nullptr;
0294                 QTextEdit te;
0295                 te.setHtml(textWrapper.text);
0296                 te.selectAll();
0297                 if (textWrapper.text.indexOf(QStringLiteral("background-color:#")) != -1) {
0298                     const auto& bgColor = te.textBackgroundColor();
0299                     if (bgColor != d->backgroundColor) {
0300                         parent = new QUndoCommand(ki18n("%1: set label text").subs(name()).toString());
0301                         new TextLabelSetTeXBackgroundColorCmd(d, bgColor, ki18n("%1: set background color"), parent);
0302                     }
0303                     command = new TextLabelSetTextCmd(d, textWrapper, ki18n("%1: set label text"), parent);
0304                 } else {
0305                     // no color available yet, plain text is being provided -> set the color from member variable
0306                     te.setTextBackgroundColor(d->backgroundColor);
0307                     TextWrapper tw = textWrapper;
0308                     tw.text = te.toHtml();
0309                     command = new TextLabelSetTextCmd(d, tw, ki18n("%1: set label text"), parent);
0310                 }
0311 
0312                 if (!parent)
0313                     exec(command);
0314                 else
0315                     exec(parent);
0316             }
0317         } else
0318             exec(new TextLabelSetTextCmd(d, textWrapper, ki18n("%1: set label text")));
0319 
0320         // If previously the text was empty, the bounding rect is zero
0321         // therefore the alignment did not work properly.
0322         // If text is added, the bounding rectangle is updated
0323         // and then the position must be changed to consider alignment
0324         if (oldEmpty)
0325             d->updatePosition();
0326     }
0327 
0328     // DEBUG(Q_FUNC_INFO << " DONE\n***********************")
0329 }
0330 
0331 STD_SETTER_CMD_IMPL_F_S(TextLabel, SetPlaceholderText, TextLabel::TextWrapper, textWrapper, updateText)
0332 void TextLabel::setPlaceholderText(const TextWrapper& textWrapper) {
0333     Q_D(TextLabel);
0334     if ((textWrapper.textPlaceholder != d->textWrapper.textPlaceholder) || (textWrapper.mode != d->textWrapper.mode))
0335         exec(new TextLabelSetPlaceholderTextCmd(d, textWrapper, ki18n("%1: set label placeholdertext")));
0336 }
0337 
0338 STD_SETTER_CMD_IMPL_F_S(TextLabel, SetTeXFont, QFont, teXFont, updateText)
0339 void TextLabel::setTeXFont(const QFont& font) {
0340     Q_D(TextLabel);
0341     if (font != d->teXFont)
0342         exec(new TextLabelSetTeXFontCmd(d, font, ki18n("%1: set TeX main font")));
0343 }
0344 
0345 STD_SETTER_CMD_IMPL_F_S(TextLabel, SetTeXFontColor, QColor, fontColor, updateText)
0346 void TextLabel::setFontColor(const QColor color) {
0347     Q_D(TextLabel);
0348     if (color != d->fontColor)
0349         exec(new TextLabelSetTeXFontColorCmd(d, color, ki18n("%1: set font color")));
0350 }
0351 
0352 // Border
0353 STD_SETTER_CMD_IMPL_F_S(TextLabel, SetBorderShape, TextLabel::BorderShape, borderShape, updateBorder)
0354 void TextLabel::setBorderShape(TextLabel::BorderShape shape) {
0355     Q_D(TextLabel);
0356     if (shape != d->borderShape)
0357         exec(new TextLabelSetBorderShapeCmd(d, shape, ki18n("%1: set border shape")));
0358 }
0359 
0360 STD_SETTER_CMD_IMPL_F_S(TextLabel, SetBorderPen, QPen, borderPen, update)
0361 void TextLabel::setBorderPen(const QPen& pen) {
0362     Q_D(TextLabel);
0363     if (pen != d->borderPen)
0364         exec(new TextLabelSetBorderPenCmd(d, pen, ki18n("%1: set border")));
0365 }
0366 
0367 STD_SETTER_CMD_IMPL_F_S(TextLabel, SetBorderOpacity, qreal, borderOpacity, update)
0368 void TextLabel::setBorderOpacity(qreal opacity) {
0369     Q_D(TextLabel);
0370     if (opacity != d->borderOpacity)
0371         exec(new TextLabelSetBorderOpacityCmd(d, opacity, ki18n("%1: set border opacity")));
0372 }
0373 
0374 // misc
0375 
0376 QRectF TextLabel::size() {
0377     Q_D(TextLabel);
0378     return d->size();
0379 }
0380 
0381 QPointF TextLabel::findNearestGluePoint(QPointF scenePoint) {
0382     Q_D(TextLabel);
0383     return d->findNearestGluePoint(scenePoint);
0384 }
0385 
0386 TextLabel::GluePoint TextLabel::gluePointAt(int index) {
0387     Q_D(TextLabel);
0388     return d->gluePointAt(index);
0389 }
0390 
0391 int TextLabel::gluePointCount() {
0392     Q_D(const TextLabel);
0393     return d->m_gluePoints.length();
0394 }
0395 
0396 void TextLabel::updateTeXImage() {
0397     Q_D(TextLabel);
0398     d->updateTeXImage();
0399 }
0400 
0401 // ##############################################################################
0402 // ####################### Private implementation ###############################
0403 // ##############################################################################
0404 TextLabelPrivate::TextLabelPrivate(TextLabel* owner)
0405     : WorksheetElementPrivate(owner)
0406     , q(owner) {
0407     setFlag(QGraphicsItem::ItemIsSelectable);
0408     setFlag(QGraphicsItem::ItemIsMovable);
0409     setFlag(QGraphicsItem::ItemSendsGeometryChanges);
0410     setFlag(QGraphicsItem::ItemIsFocusable);
0411     setAcceptHoverEvents(true);
0412 
0413     // scaling:
0414     // we need to scale from the font size specified in points to scene units.
0415     // furthermore, we create the tex-image in a higher resolution than usual desktop resolution
0416     //  -> take this into account
0417     // m_textItem is only used for the normal text not for latex. So the scale is only needed for the
0418     // normal text, for latex the generated image will be shown directly
0419     m_textItem = new ScaledTextItem(this);
0420     m_textItem->setScale(Worksheet::convertToSceneUnits(1, Worksheet::Unit::Point));
0421     m_textItem->setTextInteractionFlags(Qt::NoTextInteraction);
0422 }
0423 
0424 /*!
0425  * \brief TextLabelPrivate::size
0426  * \return Size and position of the TextLabel in scene coords
0427  */
0428 QRectF TextLabelPrivate::size() {
0429     double w, h;
0430     if (textWrapper.mode == TextLabel::Mode::LaTeX) {
0431         // image size is in pixel, convert to scene units
0432         w = teXImage.width() * teXImageScaleFactor;
0433         h = teXImage.height() * teXImageScaleFactor;
0434     } else {
0435         // size is in points, convert to scene units
0436         //  TODO: the shift and scaling is just a workaround to avoid the big bounding box
0437         //  s.a. TextLabelPrivate::updateBoundingRect()
0438 
0439         // double xShift = 23., yScale = 0.8;
0440         // better scaling for multiline Markdown
0441         // if (textWrapper.mode == TextLabel::Mode::Markdown && textWrapper.text.contains(QLatin1Char('\n')))
0442         //  yScale = 0.95;
0443         // see updateBoundingRect()
0444         w = m_textItem->scaledBoundingRect().width();
0445         h = m_textItem->scaledBoundingRect().height();
0446     }
0447     qreal x = position.point.x();
0448     qreal y = position.point.y();
0449     return {x, y, w, h};
0450 }
0451 
0452 /*!
0453  * \brief TextLabelPrivate::findNearestGluePoint
0454  * Finds the glue point, which is nearest to @param point in scene coords.
0455  * \param point reference position
0456  * \return Nearest point to @param point
0457  */
0458 QPointF TextLabelPrivate::findNearestGluePoint(QPointF scenePoint) {
0459     if (m_gluePoints.isEmpty())
0460         return boundingRect().center();
0461 
0462     if (m_gluePoints.length() == 1)
0463         return mapParentToPlotArea(mapToParent(m_gluePoints.at(0).point));
0464 
0465     QPointF point = mapParentToPlotArea(mapToParent(m_gluePoints.at(0).point));
0466     QPointF nearestPoint = point;
0467     double distance2 = pow(point.x() - scenePoint.x(), 2) + pow(point.y() - scenePoint.y(), 2);
0468     // assumption, more than one point available
0469     for (int i = 1; i < m_gluePoints.length(); i++) {
0470         point = mapParentToPlotArea(mapToParent(m_gluePoints.at(i).point));
0471         double distance2_temp = pow(point.x() - scenePoint.x(), 2) + pow(point.y() - scenePoint.y(), 2);
0472         if (distance2_temp < distance2) {
0473             nearestPoint = point;
0474             distance2 = distance2_temp;
0475         }
0476     }
0477 
0478     return nearestPoint;
0479 }
0480 
0481 /*!
0482  * \brief TextLabelPrivate::gluePointAt
0483  * Returns the gluePoint at a specific index.
0484  * \param index
0485  * \return Returns gluepoint at index \param index
0486  */
0487 TextLabel::GluePoint TextLabelPrivate::gluePointAt(int index) {
0488     QPointF pos;
0489     QString name;
0490 
0491     if (m_gluePoints.isEmpty() || index > m_gluePoints.length()) {
0492         pos = boundingRect().center();
0493         name = QLatin1String("center");
0494     } else if (index < 0) {
0495         pos = m_gluePoints.at(0).point;
0496         name = m_gluePoints.at(0).name;
0497     } else {
0498         pos = m_gluePoints.at(index).point;
0499         name = m_gluePoints.at(index).name;
0500     }
0501 
0502     return {mapParentToPlotArea(mapToParent(pos)), name};
0503 }
0504 
0505 /*!
0506     calculates the position and the bounding box of the label. Called on geometry or text changes.
0507  */
0508 void TextLabelPrivate::retransform() {
0509     const bool suppress = suppressRetransform || q->isLoading();
0510     trackRetransformCalled(suppress);
0511     if (suppress)
0512         return;
0513 
0514     updatePosition(); // needed, because CartesianPlot calls retransform if some operations are done
0515     updateBorder();
0516 }
0517 
0518 void TextLabelPrivate::setZoomFactor(double factor) {
0519     zoomFactor = factor;
0520 
0521     if (textWrapper.mode == TextLabel::Mode::LaTeX) {
0522         teXImage = GuiTools::imageFromPDFData(teXPdfData, zoomFactor);
0523         retransform();
0524     }
0525 }
0526 
0527 /*!
0528     calculates the position of the label, when the position relative to the parent was specified (left, right, etc.)
0529 */
0530 void TextLabelPrivate::updatePosition() {
0531     if (q->isLoading())
0532         return;
0533 
0534     if (q->m_type == TextLabel::Type::AxisTitle) {
0535         // In an axis element, the label is part of the bounding rect of the axis
0536         // so it is not possible to align with the bounding rect
0537         QPointF p = position.point;
0538         suppressItemChangeEvent = true;
0539         setPos(p);
0540         suppressItemChangeEvent = false;
0541 
0542         Q_EMIT q->positionChanged(position);
0543 
0544         // the position in scene coordinates was changed, calculate the position in logical coordinates
0545         if (q->cSystem) {
0546             if (!coordinateBindingEnabled) {
0547                 QPointF pos = q->align(p, m_boundingRectangle, horizontalAlignment, verticalAlignment, false);
0548                 positionLogical = q->cSystem->mapSceneToLogical(pos, AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);
0549             }
0550             Q_EMIT q->positionLogicalChanged(positionLogical);
0551         }
0552     } else
0553         WorksheetElementPrivate::updatePosition();
0554 }
0555 
0556 /*!
0557     updates the static text.
0558  */
0559 void TextLabelPrivate::updateText() {
0560     // DEBUG(Q_FUNC_INFO)
0561     if (suppressRetransform)
0562         return;
0563 
0564     switch (textWrapper.mode) {
0565     case TextLabel::Mode::Text: {
0566         // DEBUG(Q_FUNC_INFO << ", TEXT = " << STDSTRING(textWrapper.text))
0567 
0568         // use colors when text not empty and not already defined in text
0569         if (!textWrapper.text.isEmpty() && !textWrapper.text.contains(QLatin1String(" color:"))) {
0570             QTextEdit te(textWrapper.text);
0571             te.selectAll();
0572             te.setTextColor(fontColor);
0573 
0574             // don't set background color (not used if not inside text)
0575             textWrapper.text = te.toHtml();
0576         }
0577 
0578         m_textItem->show();
0579         m_textItem->setHtml(textWrapper.text);
0580 
0581         // the size of the label was most probably changed,
0582         // recalculate the bounding box of the label
0583         updateBoundingRect();
0584         break;
0585     }
0586     case TextLabel::Mode::LaTeX: {
0587         m_textItem->hide();
0588         TeXRenderer::Formatting format;
0589         format.fontColor = fontColor;
0590         format.backgroundColor = backgroundColor;
0591         format.fontSize = teXFont.pointSize();
0592         format.fontFamily = teXFont.family();
0593         format.dpi = teXImageResolution;
0594         QFuture<QByteArray> future = QtConcurrent::run(TeXRenderer::renderImageLaTeX, textWrapper.text, &teXRenderResult, format);
0595         teXImageFutureWatcher.setFuture(future);
0596 
0597         // don't need to call retransform() here since it is done in updateTeXImage
0598         // when the asynchronous rendering of the image is finished.
0599         break;
0600     }
0601     case TextLabel::Mode::Markdown: {
0602 #ifdef HAVE_DISCOUNT
0603         auto mdCharArray = textWrapper.text.toUtf8();
0604 #ifdef HAVE_DISCOUNT3
0605         MMIOT* mdHandle = mkd_string(mdCharArray.data(), mdCharArray.size() + 1, nullptr);
0606 
0607         mkd_flag_t* v3flags = mkd_flags();
0608         mkd_set_flag_num(v3flags, MKD_LATEX);
0609         mkd_set_flag_num(v3flags, MKD_FENCEDCODE);
0610         mkd_set_flag_num(v3flags, MKD_GITHUBTAGS);
0611 
0612         if (!mkd_compile(mdHandle, v3flags)) {
0613 #else
0614         MMIOT* mdHandle = mkd_string(mdCharArray.data(), mdCharArray.size() + 1, 0);
0615 
0616         unsigned int flags = MKD_LATEX | MKD_FENCEDCODE | MKD_GITHUBTAGS;
0617         if (!mkd_compile(mdHandle, flags)) {
0618 #endif
0619             DEBUG(Q_FUNC_INFO << ", Failed to compile the markdown document");
0620             mkd_cleanup(mdHandle);
0621             return;
0622         }
0623         char* htmlDocument;
0624         int htmlSize = mkd_document(mdHandle, &htmlDocument);
0625         QString html = QString::fromUtf8(htmlDocument, htmlSize);
0626 
0627         mkd_cleanup(mdHandle);
0628 
0629         // use QTextEdit to add other global properties (font size and colors)
0630         QTextEdit te;
0631         te.setHtml(html);
0632         te.selectAll();
0633         te.setTextColor(fontColor);
0634         te.setFontPointSize(teXFont.pointSize());
0635         te.setTextBackgroundColor(backgroundColor);
0636 
0637         m_textItem->setHtml(te.toHtml());
0638         m_textItem->show();
0639 
0640         updateBoundingRect();
0641 #endif
0642     }
0643     }
0644 }
0645 
0646 void TextLabelPrivate::updateBoundingRect() {
0647     // DEBUG(Q_FUNC_INFO)
0648     // determine the size of the label in scene units.
0649     double w, h;
0650     if (textWrapper.mode == TextLabel::Mode::LaTeX) {
0651         // image size is in pixel, convert to scene units.
0652         // the image is scaled so we have a good image quality when the worksheet was zoomed,
0653         // for the bounding rect we need to scale back since it's scaled again in paint() when drawing the rect
0654         w = teXImage.width() * teXImageScaleFactor / zoomFactor;
0655         h = teXImage.height() * teXImageScaleFactor / zoomFactor;
0656     } else {
0657         // size is in points, convert to scene units
0658         // QDEBUG(" BOUNDING RECT = " << m_textItem->scaledBoundingRect())
0659         //  TODO: the shift and scaling is just a workaround to avoid the big bounding box
0660         //  s.a. TextLabelPrivate::size()
0661 
0662         // double xShift = 23., yScale = 0.8;
0663         //  better scaling for multiline Markdown
0664         // if (textWrapper.mode == TextLabel::Mode::Markdown && textWrapper.text.contains(QLatin1Char('\n')))
0665         //  yScale = 0.95;
0666         w = m_textItem->scaledBoundingRect().width(); // - xShift;
0667         h = m_textItem->scaledBoundingRect().height(); // * yScale;
0668         m_textItem->setPos(-w / 2, -h / 2);
0669     }
0670 
0671     // DEBUG(Q_FUNC_INFO << ", scale factor = " << scaleFactor << ", w/h = " << w << " / " << h)
0672     m_boundingRectangle.setX(-w / 2);
0673     m_boundingRectangle.setY(-h / 2);
0674     m_boundingRectangle.setWidth(w);
0675     m_boundingRectangle.setHeight(h);
0676 
0677     updatePosition();
0678     updateBorder();
0679 }
0680 
0681 void TextLabelPrivate::updateTeXImage() {
0682     if (zoomFactor == -1.0) {
0683         // the view was not zoomed after the label was added so the zoom factor is not set yet.
0684         // determine the current zoom factor in the view and use it
0685         auto* worksheet = static_cast<const Worksheet*>(q->parent(AspectType::Worksheet));
0686         if (!worksheet)
0687             return;
0688         zoomFactor = worksheet->zoomFactor();
0689     }
0690     teXPdfData = teXImageFutureWatcher.result();
0691     teXImage = GuiTools::imageFromPDFData(teXPdfData, zoomFactor);
0692     updateBoundingRect();
0693     DEBUG(Q_FUNC_INFO << ", TeX renderer successful = " << teXRenderResult.successful);
0694     Q_EMIT q->teXImageUpdated(teXRenderResult);
0695 }
0696 
0697 void TextLabelPrivate::updateBorder() {
0698     using GluePoint = TextLabel::GluePoint;
0699 
0700     borderShapePath = QPainterPath();
0701     const auto& br = m_boundingRectangle;
0702     switch (borderShape) {
0703     case (TextLabel::BorderShape::NoBorder): {
0704         m_gluePoints.clear();
0705         m_gluePoints.append(GluePoint(QPointF(br.x() + br.width() / 2, br.y()), QStringLiteral("top")));
0706         m_gluePoints.append(GluePoint(QPointF(br.x() + br.width(), br.y() + br.height() / 2), QStringLiteral("right")));
0707         m_gluePoints.append(GluePoint(QPointF(br.x() + br.width() / 2, br.y() + br.height()), QStringLiteral("bottom")));
0708         m_gluePoints.append(GluePoint(QPointF(br.x(), br.y() + br.height() / 2), QStringLiteral("left")));
0709         break;
0710     }
0711     case (TextLabel::BorderShape::Rect): {
0712         borderShapePath.addRect(br);
0713 
0714         m_gluePoints.clear();
0715         m_gluePoints.append(GluePoint(QPointF(br.x() + br.width() / 2, br.y()), QStringLiteral("top")));
0716         m_gluePoints.append(GluePoint(QPointF(br.x() + br.width(), br.y() + br.height() / 2), QStringLiteral("right")));
0717         m_gluePoints.append(GluePoint(QPointF(br.x() + br.width() / 2, br.y() + br.height()), QStringLiteral("bottom")));
0718         m_gluePoints.append(GluePoint(QPointF(br.x(), br.y() + br.height() / 2), QStringLiteral("left")));
0719         break;
0720     }
0721     case (TextLabel::BorderShape::Ellipse): {
0722         const double xs = br.x();
0723         const double ys = br.y();
0724         const double w = br.width();
0725         const double h = br.height();
0726         const QRectF ellipseRect(xs - 0.1 * w, ys - 0.1 * h, 1.2 * w, 1.2 * h);
0727         borderShapePath.addEllipse(ellipseRect);
0728 
0729         m_gluePoints.clear();
0730         m_gluePoints.append(GluePoint(QPointF(ellipseRect.x() + ellipseRect.width() / 2, ellipseRect.y()), QStringLiteral("top")));
0731         m_gluePoints.append(GluePoint(QPointF(ellipseRect.x() + ellipseRect.width(), ellipseRect.y() + ellipseRect.height() / 2), QStringLiteral("right")));
0732         m_gluePoints.append(GluePoint(QPointF(ellipseRect.x() + ellipseRect.width() / 2, ellipseRect.y() + ellipseRect.height()), QStringLiteral("bottom")));
0733         m_gluePoints.append(GluePoint(QPointF(ellipseRect.x(), ellipseRect.y() + ellipseRect.height() / 2), QStringLiteral("left")));
0734         break;
0735     }
0736     case (TextLabel::BorderShape::RoundSideRect): {
0737         const double xs = br.x();
0738         const double ys = br.y();
0739         const double w = br.width();
0740         const double h = br.height();
0741         borderShapePath.moveTo(xs, ys);
0742         borderShapePath.lineTo(xs + w, ys);
0743         borderShapePath.quadTo(xs + w + h / 2, ys + h / 2, xs + w, ys + h);
0744         borderShapePath.lineTo(xs, ys + h);
0745         borderShapePath.quadTo(xs - h / 2, ys + h / 2, xs, ys);
0746 
0747         m_gluePoints.clear();
0748         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys), QStringLiteral("top")));
0749         m_gluePoints.append(GluePoint(QPointF(xs + w + h / 2, ys + h / 2), QStringLiteral("right")));
0750         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h), QStringLiteral("bottom")));
0751         m_gluePoints.append(GluePoint(QPointF(xs - h / 2, ys + h / 2), QStringLiteral("left")));
0752         break;
0753     }
0754     case (TextLabel::BorderShape::RoundCornerRect): {
0755         const double xs = br.x();
0756         const double ys = br.y();
0757         const double w = br.width();
0758         const double h = br.height();
0759         borderShapePath.moveTo(xs + h * 0.2, ys);
0760         borderShapePath.lineTo(xs + w - h * 0.2, ys);
0761         borderShapePath.quadTo(xs + w, ys, xs + w, ys + h * 0.2);
0762         borderShapePath.lineTo(xs + w, ys + h - 0.2 * h);
0763         borderShapePath.quadTo(xs + w, ys + h, xs + w - 0.2 * h, ys + h);
0764         borderShapePath.lineTo(xs + 0.2 * h, ys + h);
0765         borderShapePath.quadTo(xs, ys + h, xs, ys + h - 0.2 * h);
0766         borderShapePath.lineTo(xs, ys + 0.2 * h);
0767         borderShapePath.quadTo(xs, ys, xs + 0.2 * h, ys);
0768 
0769         m_gluePoints.clear();
0770         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys), QStringLiteral("top")));
0771         m_gluePoints.append(GluePoint(QPointF(xs + w, ys + h / 2), QStringLiteral("right")));
0772         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h), QStringLiteral("bottom")));
0773         m_gluePoints.append(GluePoint(QPointF(xs, ys + h / 2), QStringLiteral("left")));
0774         break;
0775     }
0776     case (TextLabel::BorderShape::InwardsRoundCornerRect): {
0777         const double xs = br.x();
0778         const double ys = br.y();
0779         const double w = br.width();
0780         const double h = br.height();
0781         borderShapePath.moveTo(xs, ys - 0.3 * h);
0782         borderShapePath.lineTo(xs + w, ys - 0.3 * h);
0783         borderShapePath.quadTo(xs + w, ys, xs + w + 0.3 * h, ys);
0784         borderShapePath.lineTo(xs + w + 0.3 * h, ys + h);
0785         borderShapePath.quadTo(xs + w, ys + h, xs + w, ys + h + 0.3 * h);
0786         borderShapePath.lineTo(xs, ys + h + 0.3 * h);
0787         borderShapePath.quadTo(xs, ys + h, xs - 0.3 * h, ys + h);
0788         borderShapePath.lineTo(xs - 0.3 * h, ys);
0789         borderShapePath.quadTo(xs, ys, xs, ys - 0.3 * h);
0790 
0791         m_gluePoints.clear();
0792         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys - 0.3 * h), QStringLiteral("top")));
0793         m_gluePoints.append(GluePoint(QPointF(xs + w + 0.3 * h, ys + h / 2), QStringLiteral("right")));
0794         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h + 0.3 * h), QStringLiteral("bottom")));
0795         m_gluePoints.append(GluePoint(QPointF(xs - 0.3 * h, ys + h / 2), QStringLiteral("left")));
0796         break;
0797     }
0798     case (TextLabel::BorderShape::DentedBorderRect): {
0799         const double xs = br.x();
0800         const double ys = br.y();
0801         const double w = br.width();
0802         const double h = br.height();
0803         borderShapePath.moveTo(xs - 0.2 * h, ys - 0.2 * h);
0804         borderShapePath.quadTo(xs + w / 2, ys, xs + w + 0.2 * h, ys - 0.2 * h);
0805         borderShapePath.quadTo(xs + w, ys + h / 2, xs + w + 0.2 * h, ys + h + 0.2 * h);
0806         borderShapePath.quadTo(xs + w / 2, ys + h, xs - 0.2 * h, ys + h + 0.2 * h);
0807         borderShapePath.quadTo(xs, ys + h / 2, xs - 0.2 * h, ys - 0.2 * h);
0808 
0809         m_gluePoints.clear();
0810         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys - 0.2 * h), QStringLiteral("top")));
0811         m_gluePoints.append(GluePoint(QPointF(xs + w + 0.2 * h, ys + h / 2), QStringLiteral("right")));
0812         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h + 0.2 * h), QStringLiteral("bottom")));
0813         m_gluePoints.append(GluePoint(QPointF(xs - 0.2 * h, ys + h / 2), QStringLiteral("left")));
0814         break;
0815     }
0816     case (TextLabel::BorderShape::Cuboid): {
0817         const double xs = br.x();
0818         const double ys = br.y();
0819         const double w = br.width();
0820         const double h = br.height();
0821         borderShapePath.moveTo(xs, ys);
0822         borderShapePath.lineTo(xs + w, ys);
0823         borderShapePath.lineTo(xs + w, ys + h);
0824         borderShapePath.lineTo(xs, ys + h);
0825         borderShapePath.lineTo(xs, ys);
0826         borderShapePath.lineTo(xs + 0.3 * h, ys - 0.2 * h);
0827         borderShapePath.lineTo(xs + w + 0.3 * h, ys - 0.2 * h);
0828         borderShapePath.lineTo(xs + w, ys);
0829         borderShapePath.moveTo(xs + w, ys + h);
0830         borderShapePath.lineTo(xs + w + 0.3 * h, ys + h - 0.2 * h);
0831         borderShapePath.lineTo(xs + w + 0.3 * h, ys - 0.2 * h);
0832 
0833         m_gluePoints.clear();
0834         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys - 0.1 * h), QStringLiteral("top")));
0835         m_gluePoints.append(GluePoint(QPointF(xs + w + 0.15 * h, ys + h / 2), QStringLiteral("right")));
0836         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h), QStringLiteral("bottom")));
0837         m_gluePoints.append(GluePoint(QPointF(xs, ys + h / 2), QStringLiteral("left")));
0838         break;
0839     }
0840     case (TextLabel::BorderShape::UpPointingRectangle): {
0841         const double xs = br.x();
0842         const double ys = br.y();
0843         const double w = br.width();
0844         const double h = br.height();
0845         borderShapePath.moveTo(xs + h * 0.2, ys);
0846         borderShapePath.lineTo(xs + w / 2 - 0.2 * h, ys);
0847         borderShapePath.lineTo(xs + w / 2, ys - 0.2 * h);
0848         borderShapePath.lineTo(xs + w / 2 + 0.2 * h, ys);
0849         borderShapePath.lineTo(xs + w - h * 0.2, ys);
0850         borderShapePath.quadTo(xs + w, ys, xs + w, ys + h * 0.2);
0851         borderShapePath.lineTo(xs + w, ys + h - 0.2 * h);
0852         borderShapePath.quadTo(xs + w, ys + h, xs + w - 0.2 * h, ys + h);
0853         borderShapePath.lineTo(xs + 0.2 * h, ys + h);
0854         borderShapePath.quadTo(xs, ys + h, xs, ys + h - 0.2 * h);
0855         borderShapePath.lineTo(xs, ys + 0.2 * h);
0856         borderShapePath.quadTo(xs, ys, xs + 0.2 * h, ys);
0857 
0858         m_gluePoints.clear();
0859         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys - h * 0.2), QStringLiteral("top")));
0860         m_gluePoints.append(GluePoint(QPointF(xs + w, ys + h / 2), QStringLiteral("right")));
0861         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h), QStringLiteral("bottom")));
0862         m_gluePoints.append(GluePoint(QPointF(xs, ys + h / 2), QStringLiteral("left")));
0863         break;
0864     }
0865     case (TextLabel::BorderShape::DownPointingRectangle): {
0866         const double xs = br.x();
0867         const double ys = br.y();
0868         const double w = br.width();
0869         const double h = br.height();
0870         borderShapePath.moveTo(xs + h * 0.2, ys);
0871         borderShapePath.lineTo(xs + w - h * 0.2, ys);
0872         borderShapePath.quadTo(xs + w, ys, xs + w, ys + h * 0.2);
0873         borderShapePath.lineTo(xs + w, ys + h - 0.2 * h);
0874         borderShapePath.quadTo(xs + w, ys + h, xs + w - 0.2 * h, ys + h);
0875         borderShapePath.lineTo(xs + w / 2 + 0.2 * h, ys + h);
0876         borderShapePath.lineTo(xs + w / 2, ys + h + 0.2 * h);
0877         borderShapePath.lineTo(xs + w / 2 - 0.2 * h, ys + h);
0878         borderShapePath.lineTo(xs + 0.2 * h, ys + h);
0879         borderShapePath.quadTo(xs, ys + h, xs, ys + h - 0.2 * h);
0880         borderShapePath.lineTo(xs, ys + 0.2 * h);
0881         borderShapePath.quadTo(xs, ys, xs + 0.2 * h, ys);
0882 
0883         m_gluePoints.clear();
0884         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys), QStringLiteral("top")));
0885         m_gluePoints.append(GluePoint(QPointF(xs + w, ys + h / 2), QStringLiteral("right")));
0886         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h + 0.2 * h), QStringLiteral("bottom")));
0887         m_gluePoints.append(GluePoint(QPointF(xs, ys + h / 2), QStringLiteral("left")));
0888         break;
0889     }
0890     case (TextLabel::BorderShape::LeftPointingRectangle): {
0891         const double xs = br.x();
0892         const double ys = br.y();
0893         const double w = br.width();
0894         const double h = br.height();
0895         borderShapePath.moveTo(xs + h * 0.2, ys);
0896         borderShapePath.lineTo(xs + w - h * 0.2, ys);
0897         borderShapePath.quadTo(xs + w, ys, xs + w, ys + h * 0.2);
0898         borderShapePath.lineTo(xs + w, ys + h - 0.2 * h);
0899         borderShapePath.quadTo(xs + w, ys + h, xs + w - 0.2 * h, ys + h);
0900         borderShapePath.lineTo(xs + 0.2 * h, ys + h);
0901         borderShapePath.quadTo(xs, ys + h, xs, ys + h - 0.2 * h);
0902         borderShapePath.lineTo(xs, ys + h / 2 + 0.2 * h);
0903         borderShapePath.lineTo(xs - 0.2 * h, ys + h / 2);
0904         borderShapePath.lineTo(xs, ys + h / 2 - 0.2 * h);
0905         borderShapePath.lineTo(xs, ys + 0.2 * h);
0906         borderShapePath.quadTo(xs, ys, xs + 0.2 * h, ys);
0907 
0908         m_gluePoints.clear();
0909         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys), QStringLiteral("top")));
0910         m_gluePoints.append(GluePoint(QPointF(xs + w, ys + h / 2), QStringLiteral("right")));
0911         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h), QStringLiteral("bottom")));
0912         m_gluePoints.append(GluePoint(QPointF(xs - 0.2 * h, ys + h / 2), QStringLiteral("left")));
0913         break;
0914     }
0915     case (TextLabel::BorderShape::RightPointingRectangle): {
0916         const double xs = br.x();
0917         const double ys = br.y();
0918         const double w = br.width();
0919         const double h = br.height();
0920         borderShapePath.moveTo(xs + h * 0.2, ys);
0921         borderShapePath.lineTo(xs + w - h * 0.2, ys);
0922         borderShapePath.quadTo(xs + w, ys, xs + w, ys + h * 0.2);
0923         borderShapePath.lineTo(xs + w, ys + h / 2 - 0.2 * h);
0924         borderShapePath.lineTo(xs + w + 0.2 * h, ys + h / 2);
0925         borderShapePath.lineTo(xs + w, ys + h / 2 + 0.2 * h);
0926         borderShapePath.lineTo(xs + w, ys + h - 0.2 * h);
0927         borderShapePath.quadTo(xs + w, ys + h, xs + w - 0.2 * h, ys + h);
0928         borderShapePath.lineTo(xs + 0.2 * h, ys + h);
0929         borderShapePath.quadTo(xs, ys + h, xs, ys + h - 0.2 * h);
0930         borderShapePath.lineTo(xs, ys + 0.2 * h);
0931         borderShapePath.quadTo(xs, ys, xs + 0.2 * h, ys);
0932 
0933         m_gluePoints.clear();
0934         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys), QStringLiteral("top")));
0935         m_gluePoints.append(GluePoint(QPointF(xs + w + 0.2 * h, ys + h / 2), QStringLiteral("right")));
0936         m_gluePoints.append(GluePoint(QPointF(xs + w / 2, ys + h), QStringLiteral("bottom")));
0937         m_gluePoints.append(GluePoint(QPointF(xs, ys + h / 2), QStringLiteral("left")));
0938         break;
0939     }
0940     }
0941 
0942     recalcShapeAndBoundingRect();
0943 }
0944 
0945 /*!
0946     Returns the shape of this item as a QPainterPath in local coordinates.
0947 */
0948 QPainterPath TextLabelPrivate::shape() const {
0949     return labelShape;
0950 }
0951 
0952 /*!
0953   recalculates the outer bounds and the shape of the label.
0954 */
0955 void TextLabelPrivate::recalcShapeAndBoundingRect() {
0956     prepareGeometryChange();
0957 
0958     labelShape = QPainterPath();
0959     if (borderShape != TextLabel::BorderShape::NoBorder) {
0960         labelShape.addPath(WorksheetElement::shapeFromPath(borderShapePath, borderPen));
0961         m_boundingRectangle = labelShape.boundingRect();
0962     } else
0963         labelShape.addRect(m_boundingRectangle);
0964 
0965     Q_EMIT q->changed();
0966 }
0967 
0968 void TextLabelPrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) {
0969     if (positionInvalid || textWrapper.text.isEmpty())
0970         return;
0971 
0972     // draw the text
0973     painter->save();
0974     switch (textWrapper.mode) {
0975     case TextLabel::Mode::LaTeX: {
0976         painter->setRenderHint(QPainter::SmoothPixmapTransform);
0977         if (m_boundingRectangle.width() != 0.0 && m_boundingRectangle.height() != 0.0)
0978             painter->drawImage(m_boundingRectangle, teXImage);
0979         break;
0980     }
0981     case TextLabel::Mode::Text:
0982     case TextLabel::Mode::Markdown: {
0983         // nothing to do here, the painting is done in the ScaledTextItem child
0984         break;
0985     }
0986     }
0987     painter->restore();
0988 
0989     // Fill the complete path with the background color, for text mode only since for latex and markdown
0990     // a rectangular image is drawn which is not cropped to the boundaries of the selected shape (eclipse, etc.)
0991     if (textWrapper.mode == TextLabel::Mode::Text)
0992         painter->fillPath(labelShape, QBrush(backgroundColor));
0993 
0994     // draw the border
0995     if (borderShape != TextLabel::BorderShape::NoBorder) {
0996         painter->save();
0997         painter->setPen(borderPen);
0998         painter->setOpacity(borderOpacity);
0999 
1000         painter->drawPath(borderShapePath);
1001         painter->restore();
1002     }
1003 
1004     // TODO: move the handling of m_hovered and the logic below
1005     // to draw the selectiong/hover box to WorksheetElementPrivate
1006     // so there is no need to duplicate this code in Plot, Label, Image, etc.
1007     const bool selected = isSelected();
1008     const bool hovered = (m_hovered && !selected);
1009     if ((hovered || selected) && !q->isPrinting()) {
1010         static double penWidth = 2.;
1011         if (hovered)
1012             painter->setPen(QPen(QApplication::palette().color(QPalette::Shadow), penWidth));
1013         else
1014             painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), penWidth));
1015 
1016         painter->drawPath(labelShape);
1017     }
1018 
1019 #if DEBUG_TEXTLABEL_BOUNDING_RECT
1020     painter->setPen(QColor(Qt::GlobalColor::red));
1021     painter->drawRect(boundingRect());
1022 
1023     painter->setPen(QColor(Qt::GlobalColor::darkGreen));
1024     painter->drawRect(m_boundingRectangle.marginsAdded(QMarginsF(3, 3, 3, 3)));
1025 
1026     painter->setPen(QColor(Qt::GlobalColor::blue));
1027     painter->drawEllipse(QRectF(-5, -5, 10, 10));
1028 #endif
1029 
1030 #if DEBUG_TEXTLABEL_GLUEPOINTS
1031     // just for debugging
1032     painter->setPen(QColor(Qt::GlobalColor::red));
1033     QRectF gluePointRect(0, 0, 10, 10);
1034     for (int i = 0; i < m_gluePoints.length(); i++) {
1035         gluePointRect.moveTo(m_gluePoints[i].point.x() - gluePointRect.width() / 2, m_gluePoints[i].point.y() - gluePointRect.height() / 2);
1036         painter->fillRect(gluePointRect, QColor(Qt::GlobalColor::red));
1037     }
1038 #endif
1039 }
1040 
1041 /*
1042 void TextLabelPrivate::mouseMoveEvent(QGraphicsSceneMouseEvent* event) {
1043     if(coordinateBinding)
1044         positionLogical = q->cSystem->mapSceneToLogical(mapParentToPlotArea(pos()), AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);
1045     return QGraphicsItem::mouseMoveEvent(event);
1046 }*/
1047 
1048 // ##############################################################################
1049 // ##################  Serialization/Deserialization  ###########################
1050 // ##############################################################################
1051 //! Save as XML
1052 void TextLabel::save(QXmlStreamWriter* writer) const {
1053     Q_D(const TextLabel);
1054 
1055     writer->writeStartElement(QStringLiteral("textLabel"));
1056     writeBasicAttributes(writer);
1057     writeCommentElement(writer);
1058 
1059     writer->writeStartElement(QStringLiteral("geometry"));
1060     WorksheetElement::save(writer);
1061     writer->writeEndElement();
1062 
1063     writer->writeStartElement(QStringLiteral("text"));
1064     writer->writeCharacters(d->textWrapper.text);
1065     writer->writeEndElement();
1066 
1067     if (!d->textWrapper.textPlaceholder.isEmpty()) {
1068         writer->writeStartElement(QStringLiteral("textPlaceholder"));
1069         writer->writeCharacters(d->textWrapper.textPlaceholder);
1070         writer->writeEndElement();
1071     }
1072 
1073     writer->writeStartElement(QStringLiteral("format"));
1074     writer->writeAttribute(QStringLiteral("placeholder"), QString::number(d->textWrapper.allowPlaceholder));
1075     writer->writeAttribute(QStringLiteral("mode"), QString::number(static_cast<int>(d->textWrapper.mode)));
1076     WRITE_QFONT(d->teXFont);
1077     WRITE_QCOLOR2(d->fontColor, "fontColor");
1078     WRITE_QCOLOR2(d->backgroundColor, "backgroundColor");
1079     writer->writeEndElement();
1080 
1081     // border
1082     writer->writeStartElement(QStringLiteral("border"));
1083     writer->writeAttribute(QStringLiteral("borderShape"), QString::number(static_cast<int>(d->borderShape)));
1084     WRITE_QPEN(d->borderPen);
1085     writer->writeAttribute(QStringLiteral("borderOpacity"), QString::number(d->borderOpacity));
1086     writer->writeEndElement();
1087 
1088     if (d->textWrapper.mode == TextLabel::Mode::LaTeX) {
1089         writer->writeStartElement(QStringLiteral("teXPdfData"));
1090         writer->writeCharacters(QLatin1String(d->teXPdfData.toBase64()));
1091         writer->writeEndElement();
1092     }
1093 
1094     writer->writeEndElement(); // close "textLabel" section
1095 }
1096 
1097 //! Load from XML
1098 bool TextLabel::load(XmlStreamReader* reader, bool preview) {
1099     if (!readBasicAttributes(reader))
1100         return false;
1101 
1102     Q_D(TextLabel);
1103     QXmlStreamAttributes attribs;
1104     QString str;
1105 
1106     while (!reader->atEnd()) {
1107         reader->readNext();
1108         if (reader->isEndElement() && reader->name() == QLatin1String("textLabel"))
1109             break;
1110 
1111         if (!reader->isStartElement())
1112             continue;
1113 
1114         if (!preview && reader->name() == QLatin1String("comment")) {
1115             if (!readCommentElement(reader))
1116                 return false;
1117         } else if (!preview && reader->name() == QLatin1String("geometry")) {
1118             WorksheetElement::load(reader, preview);
1119         } else if (!preview && reader->name() == QLatin1String("text"))
1120             d->textWrapper.text = reader->readElementText();
1121         else if (!preview && reader->name() == QLatin1String("textPlaceholder"))
1122             d->textWrapper.textPlaceholder = reader->readElementText();
1123         else if (!preview && reader->name() == QLatin1String("format")) {
1124             attribs = reader->attributes();
1125 
1126             if (Project::xmlVersion() < 4) {
1127                 str = attribs.value(QStringLiteral("teXUsed")).toString();
1128                 d->textWrapper.mode = static_cast<TextLabel::Mode>(str.toInt());
1129             } else
1130                 READ_INT_VALUE("mode", textWrapper.mode, TextLabel::Mode);
1131 
1132             str = attribs.value(QStringLiteral("placeholder")).toString();
1133             if (!str.isEmpty())
1134                 d->textWrapper.allowPlaceholder = str.toInt();
1135 
1136             READ_QFONT(d->teXFont);
1137             READ_QCOLOR2(d->fontColor, "fontColor");
1138             READ_QCOLOR2(d->backgroundColor, "backgroundColor");
1139         } else if (!preview && reader->name() == QLatin1String("border")) {
1140             attribs = reader->attributes();
1141             READ_INT_VALUE("borderShape", borderShape, BorderShape);
1142             READ_QPEN(d->borderPen);
1143             READ_DOUBLE_VALUE("borderOpacity", borderOpacity);
1144         } else if (!preview && reader->name() == QLatin1String("teXPdfData")) {
1145             reader->readNext();
1146             QString content = reader->text().toString().trimmed();
1147             d->teXPdfData = QByteArray::fromBase64(content.toLatin1());
1148             d->teXImage = GuiTools::imageFromPDFData(d->teXPdfData);
1149         } else { // unknown element
1150             reader->raiseUnknownElementWarning();
1151             if (!reader->skipToEndElement())
1152                 return false;
1153         }
1154     }
1155 
1156     if (preview)
1157         return true;
1158 
1159     // starting with XML version 10, the background color used in the html text is extracted
1160     // to fill the complete shape of the label and is also saved. For older projects we need
1161     // to extract it from hmtl here if available.
1162     if (d->textWrapper.mode == TextLabel::Mode::Text && Project::xmlVersion() < 10) {
1163         if (d->textWrapper.text.indexOf(QStringLiteral("background-color:#")) != -1) {
1164             QTextEdit te;
1165             te.setHtml(d->textWrapper.text);
1166             te.selectAll();
1167             d->backgroundColor = te.textBackgroundColor();
1168         } else
1169             d->backgroundColor.setAlpha(0);
1170     }
1171 
1172     // in case we use latex and the image was stored (older versions of LabPlot didn't save the image)and loaded,
1173     // we just need to call updateBoundingRect() to calculate the new rect.
1174     // otherwise, we set the static text and call updateBoundingRect() in updateText()
1175     if (!(d->textWrapper.mode == TextLabel::Mode::LaTeX && !d->teXPdfData.isEmpty()))
1176         d->updateText();
1177     else {
1178         d->m_textItem->hide();
1179         d->zoomFactor = 1.0; // on load the view is not zoomed yet
1180         d->updateBoundingRect();
1181     }
1182 
1183     return true;
1184 }
1185 
1186 // ##############################################################################
1187 // #########################  Theme management ##################################
1188 // ##############################################################################
1189 void TextLabel::loadThemeConfig(const KConfig& config) {
1190     DEBUG(Q_FUNC_INFO << ", label = " << STDSTRING(name()))
1191     Q_D(TextLabel);
1192 
1193     KConfigGroup group = config.group(QStringLiteral("Label"));
1194     // TODO: dark mode support?
1195     d->fontColor = group.readEntry(QStringLiteral("FontColor"), QColor(Qt::black)); // used when it's latex text
1196     d->backgroundColor = group.readEntry(QStringLiteral("BackgroundColor"), QColor(Qt::transparent)); // used when it's latex text
1197     if (d->textWrapper.mode == TextLabel::Mode::Text && !d->textWrapper.text.isEmpty()) {
1198         // To set the color in a html text, a QTextEdit must be used, QTextDocument is not enough
1199         QTextEdit te;
1200         te.setHtml(d->textWrapper.text);
1201         te.selectAll();
1202         te.setTextColor(d->fontColor);
1203         te.setTextBackgroundColor(d->backgroundColor); // for plain text no background color supported, due to bug https://bugreports.qt.io/browse/QTBUG-25420
1204 
1205         TextWrapper wrapper(te.toHtml(), TextLabel::Mode::Text, true);
1206         te.setHtml(d->textWrapper.textPlaceholder);
1207         te.selectAll();
1208         te.setTextColor(d->fontColor);
1209         te.setTextBackgroundColor(d->backgroundColor); // for plain text no background color supported, due to bug https://bugreports.qt.io/browse/QTBUG-25420
1210         wrapper.textPlaceholder = te.toHtml();
1211         wrapper.allowPlaceholder = d->textWrapper.allowPlaceholder;
1212 
1213         // update the text. also in the Widget to which is connected
1214 
1215         setText(wrapper);
1216     } else if (d->textWrapper.mode == TextLabel::Mode::LaTeX) {
1217         // call updateText() to re-render the LaTeX-image with the new text colors
1218         d->updateText();
1219     }
1220 
1221     // otherwise when changing theme while the textlabel dock is visible, the
1222     // color comboboxes do not change the color
1223     backgroundColorChanged(d->backgroundColor);
1224     fontColorChanged(d->fontColor);
1225 
1226     group = config.group(QStringLiteral("CartesianPlot"));
1227     QPen pen = this->borderPen();
1228     pen.setColor(group.readEntry(QStringLiteral("BorderColor"), pen.color()));
1229     pen.setStyle((Qt::PenStyle)(group.readEntry(QStringLiteral("BorderStyle"), (int)pen.style())));
1230     pen.setWidthF(group.readEntry(QStringLiteral("BorderWidth"), pen.widthF()));
1231     this->setBorderPen(pen);
1232     this->setBorderOpacity(group.readEntry(QStringLiteral("BorderOpacity"), this->borderOpacity()));
1233 }
1234 
1235 void TextLabel::saveThemeConfig(const KConfig& config) {
1236     KConfigGroup group = config.group(QStringLiteral("Label"));
1237     // TODO
1238     //  group.writeEntry("TeXFontColor", (QColor) this->fontColor());
1239 }