diff --git a/sources/qetgraphicsitem/diagramimageitem.cpp b/sources/qetgraphicsitem/diagramimageitem.cpp index 25f2fc164..8e3b58c71 100644 --- a/sources/qetgraphicsitem/diagramimageitem.cpp +++ b/sources/qetgraphicsitem/diagramimageitem.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -69,7 +70,7 @@ DiagramImageItem::DiagramImageItem(const QPixmap &pixmap, QetGraphicsItem *paren // those are a single uniform scale() float, which is exactly why an // image could never break its own aspect ratio before this class // gained proper independent scaleX/scaleY. - m_transform.pivot = boundingRect().center(); + m_transform.pivot = imageRect().center(); setTransform(m_transform.toMatrix()); setFlags(QGraphicsItem::ItemIsSelectable|QGraphicsItem::ItemIsMovable|QGraphicsItem::ItemSendsGeometryChanges); setAcceptHoverEvents(true); @@ -110,6 +111,45 @@ void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * painter -> drawRect(boundingRect()); painter -> restore(); } + + if (!m_label.isEmpty()) + { + // Undo the picture's own scale so the caption keeps the size of + // every other text on the folio whether the picture is shown at + // 5 % or 500 %. Rotation and skew are applied after scale in + // ShapeTransform::toMatrix(), so the caption still turns with it. + const QRectF r = labelRect(); + painter -> save(); + painter -> translate(r.topLeft()); + painter -> scale(1.0 / m_label_scale.x(), 1.0 / m_label_scale.y()); + painter -> setFont(m_label_font); + painter -> setPen(Qt::black); + painter -> drawText(QRectF(QPointF(0, 0), m_label_size), Qt::AlignCenter, m_label); + painter -> restore(); + } +} + +/** + @brief DiagramImageItem::setLabel + Set the caption drawn under the picture. An empty string removes it. + @param label +*/ +void DiagramImageItem::setLabel(const QString &label) +{ + if (label == m_label) + return; + + prepareGeometryChange(); + m_label = label; + // Font looked up here, not in paint(): diagramTextsFont() reads + // QSettings, far too slow to do on every repaint. + if (!m_label.isEmpty()) + m_label_font = QETApp::diagramTextsFont(); + m_label_size = m_label.isEmpty() + ? QSizeF() + : QFontMetricsF(m_label_font).size(0, m_label); + updateLabelScale(); + emit labelChanged(); } /** @@ -278,7 +318,7 @@ void DiagramImageItem::setPivotRaw(const QPointF &newPivot) void DiagramImageItem::resetPivotToBoundingRectCenter() { m_pivotIsCustom = false; - setPivot(boundingRect().center()); + setPivot(imageRect().center()); } namespace { @@ -1263,6 +1303,11 @@ QVariant DiagramImageItem::itemChange(GraphicsItemChange change, const QVariant } refreshInteractionHints(); } + else if (change == ItemTransformChange && !m_label.isEmpty()) + { + prepareGeometryChange(); + updateLabelScale(); + } else if (change == ItemPositionHasChanged || change == ItemTransformHasChanged) { if (!m_deferHandleReposition) @@ -1308,6 +1353,18 @@ QPixmap DiagramImageItem::computeDisplayPixmap(const QPixmap &base, const QRect @return a QRectF represent the bounding rectangle */ QRectF DiagramImageItem::boundingRect() const +{ + if (m_label.isEmpty()) + return imageRect(); + return imageRect().united(labelRect()); +} + +/** + @brief DiagramImageItem::imageRect + @return the picture's own rectangle, without its label. Handles, the + pivot and every other piece of geometry work on this one. +*/ +QRectF DiagramImageItem::imageRect() const { if (!pixmap_.isNull()) { return (QRectF(pixmap_.rect())); @@ -1317,6 +1374,34 @@ QRectF DiagramImageItem::boundingRect() const } } +/** + @brief DiagramImageItem::labelRect + @return the label's rectangle in item coordinates, centred just under + the picture. Divided by the picture's scale, because paint() draws + the label unscaled. +*/ +QRectF DiagramImageItem::labelRect() const +{ + const QRectF image = imageRect(); + const qreal w = m_label_size.width() / m_label_scale.x(); + const qreal h = m_label_size.height() / m_label_scale.y(); + const qreal gap = 2.0 / m_label_scale.y(); + return QRectF(image.center().x() - w / 2.0, image.bottom() + gap, w, h); +} + +/** + @brief DiagramImageItem::updateLabelScale + The label rect depends on the picture's scale, so boundingRect() + changes with it. m_transform is already updated by the time the item + transform changes, so the scale is cached here and only refreshed + after prepareGeometryChange() has recorded the old rect. +*/ +void DiagramImageItem::updateLabelScale() +{ + const auto safe = [](qreal v) { return qFuzzyIsNull(v) ? 1.0 : qAbs(v); }; + m_label_scale = QPointF(safe(m_transform.scaleX), safe(m_transform.scaleY)); +} + /** @brief DiagramImageItem::name @return the generic name of this item (picture) @@ -1426,7 +1511,7 @@ bool DiagramImageItem::fromXml(const QDomElement &e) m_transform.rotation = e.attribute("rotation").toDouble(); m_transform.scaleX = e.attribute("size").toDouble(); m_transform.scaleY = m_transform.scaleX; - m_transform.pivot = boundingRect().center(); + m_transform.pivot = imageRect().center(); m_pivotIsCustom = false; const QDomElement transformElement = e.firstChildElement("transform"); @@ -1447,6 +1532,7 @@ bool DiagramImageItem::fromXml(const QDomElement &e) QGraphicsObject::setPos(e.attribute("x").toDouble(), e.attribute("y").toDouble()); setZValue(e.attribute("z", QString::number(this->zValue())).toDouble()); is_movable_ = (e.attribute("is_movable").toInt()); + setLabel(e.attribute("label")); return (true); } @@ -1477,6 +1563,10 @@ QDomElement DiagramImageItem::toXml(QDomDocument &document) const result.setAttribute("rotation", QString::number(QET::correctAngle(m_transform.rotation))); result.setAttribute("size", QString::number(m_transform.scaleX)); result.setAttribute("is_movable", bool(is_movable_)); + // An attribute, not a child element: fromXml() of every earlier + // version requires the base64 text to be the first child. + if (!m_label.isEmpty()) + result.setAttribute("label", m_label); //write the pixmap in the xml element after he was been transformed to base64 QByteArray array; diff --git a/sources/qetgraphicsitem/diagramimageitem.h b/sources/qetgraphicsitem/diagramimageitem.h index afdbaf7b5..ac8d7c9db 100644 --- a/sources/qetgraphicsitem/diagramimageitem.h +++ b/sources/qetgraphicsitem/diagramimageitem.h @@ -23,6 +23,7 @@ #include "../ui/imagetransparentcolordialog.h" #include +#include #include #include #include @@ -46,6 +47,7 @@ class DiagramImageItem : public QetGraphicsItem { Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged) Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged) Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged) + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) // A second, deliberately non-compensating property on the SAME // underlying value -- setPivot() (above) intentionally adjusts // pos() to keep the image visually in place, which is exactly @@ -123,10 +125,13 @@ class DiagramImageItem : public QetGraphicsItem { QPointF pivot() const { return m_transform.pivot; } void setPivot(const QPointF &pivot); void setPivotRaw(const QPointF &pivot); + QString label() const { return m_label; } + void setLabel(const QString &label); signals: void pixmapChanged(); void transformChanged(); + void labelChanged(); protected: void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override; @@ -175,6 +180,9 @@ class DiagramImageItem : public QetGraphicsItem { static QString hintForHandleRole(HandleRole role); void showStatusHint(const QString &text) const; void clearStatusHint() const; + QRectF imageRect() const; + QRectF labelRect() const; + void updateLabelScale(); protected: QPixmap pixmap_; @@ -212,6 +220,13 @@ class DiagramImageItem : public QetGraphicsItem { QPointF m_original_pos; // scene position at the start of a resize/rotate/pivot drag, for Escape-to-cancel ShapeTransform m_original_transform; bool m_deferHandleReposition = false; // see setPivot()'s comment + // Optional caption drawn centred under the picture (issue #349). + // Empty by default, and then neither saved nor painted, so a picture + // without one costs exactly what it did before. + QString m_label; + QFont m_label_font; + QSizeF m_label_size; // in scene units, measured once in setLabel() + QPointF m_label_scale{1.0, 1.0}; // scale the label rect was last computed for -- see updateLabelScale() bool m_resizeCenterAnchored = false; // decided once, at press time -- see handlerMousePressEvent()'s comment for why, mirroring the identical fix already made for shape creation }; #endif diff --git a/sources/ui/imagepropertieswidget.cpp b/sources/ui/imagepropertieswidget.cpp index b2cccca2b..3b0931437 100644 --- a/sources/ui/imagepropertieswidget.cpp +++ b/sources/ui/imagepropertieswidget.cpp @@ -64,16 +64,21 @@ void ImagePropertiesWidget::setImageItem(DiagramImageItem *image) this->setEnabled(true); if (m_image == image) return; if (m_image) + { disconnect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi); + disconnect(m_image, &DiagramImageItem::labelChanged, this, &ImagePropertiesWidget::updateUi); + } m_image = image; connect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi); + connect(m_image, &DiagramImageItem::labelChanged, this, &ImagePropertiesWidget::updateUi); m_movable = image->isMovable(); m_scaleX = m_image->scaleFactorX(); m_scaleY = m_image->scaleFactorY(); m_rotation = m_image->rotationAngle(); m_skewX = m_image->skewX(); m_skewY = m_image->skewY(); + m_label = m_image->label(); updateUi(); } @@ -101,6 +106,7 @@ void ImagePropertiesWidget::apply() m_rotation = m_image->rotationAngle(); m_skewX = m_image->skewX(); m_skewY = m_image->skewY(); + m_label = m_image->label(); } /** @@ -116,6 +122,7 @@ void ImagePropertiesWidget::reset() m_image->setRotationAngle(m_rotation); m_image->setSkewX(m_skewX); m_image->setSkewY(m_skewY); + m_image->setLabel(m_label); m_image->setMovable(m_movable); updateUi(); } @@ -138,6 +145,7 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit) connect (ui->m_angle_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply); connect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply); connect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply); + connect (ui->m_label_le, &QLineEdit::editingFinished, this, &ImagePropertiesWidget::apply); } else { @@ -146,6 +154,7 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit) disconnect (ui->m_angle_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply); disconnect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply); disconnect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply); + disconnect (ui->m_label_le, &QLineEdit::editingFinished, this, &ImagePropertiesWidget::apply); } return true; @@ -191,6 +200,19 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const chain("skewX", m_skewX, newSkewX, tr("Modifier l'inclinaison d'une image")); chain("skewY", m_skewY, newSkewY, tr("Modifier l'inclinaison d'une image")); + // Not through chain(): a string cannot be animated. + const QString newLabel = ui->m_label_le->text(); + if (newLabel != m_label) + { + if (undo) + new QPropertyUndoCommand(m_image, "label", m_label, newLabel, undo); + else + { + undo = new QPropertyUndoCommand(m_image, "label", m_label, newLabel); + undo->setText(tr("Modifier le libellé d'une image")); + } + } + return undo; } @@ -221,6 +243,8 @@ void ImagePropertiesWidget::updateUi() ui->m_skew_x_sb->setValue(m_image->skewX()); ui->m_skew_y_sb->setValue(m_image->skewY()); ui->m_lock_pos_cb->setChecked(!m_image->isMovable()); + if (ui->m_label_le->text() != m_image->label()) + ui->m_label_le->setText(m_image->label()); } /** diff --git a/sources/ui/imagepropertieswidget.h b/sources/ui/imagepropertieswidget.h index 438b5e3b8..c692d7892 100644 --- a/sources/ui/imagepropertieswidget.h +++ b/sources/ui/imagepropertieswidget.h @@ -70,6 +70,7 @@ class ImagePropertiesWidget : public PropertiesEditorWidget qreal m_rotation; qreal m_skewX; qreal m_skewY; + QString m_label; // Guards the width/height spinboxes' mutual updates when // "Conserver les proportions" is checked, so setting one // programmatically in response to the other doesn't re-trigger diff --git a/sources/ui/imagepropertieswidget.ui b/sources/ui/imagepropertieswidget.ui index 36ff7ddf3..4c9954cd6 100644 --- a/sources/ui/imagepropertieswidget.ui +++ b/sources/ui/imagepropertieswidget.ui @@ -152,14 +152,28 @@ - + + + + Libellé + + + + + + + Texte affiché sous l'image. Il suit l'image quand elle est déplacée, copiée ou tournée. + + + + Verrouiller la position - + Qt::Vertical