Merge pull request #1068 from ispyisail/feat/349-image-label

Add a label to pictures (#349)
This commit is contained in:
Laurent Trinques
2026-09-27 08:04:44 +02:00
committed by GitHub
5 changed files with 149 additions and 5 deletions
+93 -3
View File
@@ -35,6 +35,7 @@
#include <QBuffer>
#include <QFileDialog>
#include <QFileInfo>
#include <QFontMetricsF>
#include <QGraphicsSceneContextMenuEvent>
#include <QImageWriter>
#include <QMenu>
@@ -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)
@@ -1474,7 +1559,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");
@@ -1495,6 +1580,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);
}
@@ -1525,6 +1611,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
const QByteArray &array = encodedPng(pixmap_, m_png_cache, m_png_cache_key);
@@ -23,6 +23,7 @@
#include "../ui/imagetransparentcolordialog.h"
#include <QColor>
#include <QFont>
#include <QList>
#include <QStyleOptionGraphicsItem>
#include <QVector>
@@ -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;
@@ -176,6 +181,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_;
@@ -223,6 +231,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
+24
View File
@@ -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());
}
/**
+1
View File
@@ -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
+16 -2
View File
@@ -152,14 +152,28 @@
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<item row="6" column="0">
<widget class="QLabel" name="label_label">
<property name="text">
<string>Libellé</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QLineEdit" name="m_label_le">
<property name="toolTip">
<string>Texte affiché sous l'image. Il suit l'image quand elle est déplacée, copiée ou tournée.</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="m_lock_pos_cb">
<property name="text">
<string>Verrouiller la position</string>
</property>
</widget>
</item>
<item row="7" column="0">
<item row="8" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>