diff --git a/sources/editor/graphicspart/partdynamictextfield.cpp b/sources/editor/graphicspart/partdynamictextfield.cpp index 9c26a70d6..027a07ac4 100644 --- a/sources/editor/graphicspart/partdynamictextfield.cpp +++ b/sources/editor/graphicspart/partdynamictextfield.cpp @@ -19,6 +19,7 @@ #include "../../QPropertyUndoCommand/qpropertyundocommand.h" #include "../../qetapp.h" +#include "../../textanchor.h" #include "../elementscene.h" #include "../../utils/qetutils.h" #include @@ -474,6 +475,41 @@ Qt::Alignment PartDynamicTextField::alignment() const return m_alignment; } +/** + @brief PartDynamicTextField::anchorPos + @return the anchor point of this text, in parent coordinates: + the point of the text chosen by the alignment (top-left, right edge, + centre...). For a top-left aligned text this is pos(). + Same as DiagramTextItem::anchorPos(). +*/ +QPointF PartDynamicTextField::anchorPos() const +{ + return TextAnchor::pos(this, m_alignment); +} + +/** + @brief PartDynamicTextField::setAnchorPos + Move this text so that its anchor point (see anchorPos()) is at anchor. + @param anchor : in parent coordinates +*/ +void PartDynamicTextField::setAnchorPos(const QPointF &anchor) +{ + setPos(TextAnchor::itemPosFor(this, m_alignment, anchor)); +} + +/** + @brief PartDynamicTextField::setAlignmentAtAnchor + Change the alignment and move the text so that its new anchor point + is where the previous one was. + @param alignment +*/ +void PartDynamicTextField::setAlignmentAtAnchor(Qt::Alignment alignment) +{ + const QPointF anchor = anchorPos(); + setAlignment(alignment); + setAnchorPos(anchor); +} + void PartDynamicTextField::setFont(const QFont &font) { if (font == this -> font()) { return; diff --git a/sources/editor/graphicspart/partdynamictextfield.h b/sources/editor/graphicspart/partdynamictextfield.h index fa0c49fcf..afaff41b7 100644 --- a/sources/editor/graphicspart/partdynamictextfield.h +++ b/sources/editor/graphicspart/partdynamictextfield.h @@ -42,6 +42,8 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart Q_PROPERTY(bool frame READ frame WRITE setFrame NOTIFY frameChanged) Q_PROPERTY(qreal textWidth READ textWidth WRITE setTextWidth NOTIFY textWidthChanged) Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged) + Q_PROPERTY(QPointF anchorPos READ anchorPos WRITE setAnchorPos) + Q_PROPERTY(Qt::Alignment alignmentAtAnchor READ alignment WRITE setAlignmentAtAnchor NOTIFY alignmentChanged) Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged) Q_PROPERTY(bool keepVisualRotation READ keepVisualRotation WRITE setKeepVisualRotation NOTIFY keepVisualRotationChanged) Q_PROPERTY(bool rotationPointCenter READ rotationPointCenter WRITE setRotationPointCenter NOTIFY rotationPointCenterChanged) @@ -101,6 +103,9 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart void setPlainText(const QString &text); void setAlignment(Qt::Alignment alignment); Qt::Alignment alignment() const; + QPointF anchorPos() const; + void setAnchorPos(const QPointF &anchor); + void setAlignmentAtAnchor(Qt::Alignment alignment); void setFont(const QFont &font); void setKeepVisualRotation(const bool &keep); bool keepVisualRotation() const; diff --git a/sources/editor/ui/dynamictextfieldeditor.cpp b/sources/editor/ui/dynamictextfieldeditor.cpp index 3f444a8e3..47d87a91c 100644 --- a/sources/editor/ui/dynamictextfieldeditor.cpp +++ b/sources/editor/ui/dynamictextfieldeditor.cpp @@ -134,8 +134,8 @@ QList DynamicTextFieldEditor::currentParts() const void DynamicTextFieldEditor::updateForm() { if(m_text_field) { - ui -> m_x_sb -> setValue(m_text_field.data() -> x()); - ui -> m_y_sb -> setValue(m_text_field.data() ->y ()); + ui -> m_x_sb -> setValue(m_text_field.data() -> anchorPos().x()); + ui -> m_y_sb -> setValue(m_text_field.data() -> anchorPos().y()); ui -> m_rotation_sb -> setValue(QET::correctAngle(m_text_field.data() -> rotation())); ui -> m_frame_cb -> setChecked(m_text_field.data() -> frame()); ui -> m_user_text_le -> setText(m_text_field.data() -> text()); @@ -211,6 +211,9 @@ void DynamicTextFieldEditor::setUpConnections() m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::compositeTextChanged,this, [this](){this -> updateForm();}); m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::keepVisualRotationChanged, this, [this](){this -> updateForm();}); m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::rotationPointCenterChanged, this, [this](){this -> updateForm();}); + m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::alignmentChanged, this, [this](){this -> updateForm();}); + m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::xChanged, this, [this](){this -> updateForm();}); + m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::yChanged, this, [this](){this -> updateForm();}); // Refresh info combo when element data changes (e.g. type switched to PLC-Slave) m_connection_list << connect(elementEditor()->elementScene(), &ElementScene::elementInfoChanged, @@ -290,7 +293,8 @@ void DynamicTextFieldEditor::on_m_x_sb_editingFinished() { double value = ui -> m_x_sb -> value(); for (int i = 0; i < m_parts.length(); i++) { - QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "x", m_parts[i] -> x(), value); + const QPointF anchor = m_parts[i] -> anchorPos(); + QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "anchorPos", anchor, QPointF(value, anchor.y())); undo -> setText(tr("Déplacer un champ texte")); undo -> enableAnimation(true); undoStack().push(undo); @@ -301,7 +305,8 @@ void DynamicTextFieldEditor::on_m_y_sb_editingFinished() { double value = ui -> m_y_sb -> value(); for (int i = 0; i < m_parts.length(); i++) { - QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "y", m_parts[i] -> y(), value); + const QPointF anchor = m_parts[i] -> anchorPos(); + QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "anchorPos", anchor, QPointF(anchor.x(), value)); undo -> setText(tr("Déplacer un champ texte")); undo -> enableAnimation(true); undoStack().push(undo); @@ -459,7 +464,7 @@ void DynamicTextFieldEditor::on_m_alignment_pb_clicked() if(atd.alignment() != m_parts[i] -> alignment()) { QPropertyUndoCommand *undo =\ new QPropertyUndoCommand( - m_parts[i], "alignment", QVariant(m_parts[i] -> alignment()), QVariant(atd.alignment())); + m_parts[i], "alignmentAtAnchor", QVariant(m_parts[i] -> alignment()), QVariant(atd.alignment())); undo -> setText(tr("Modifier l'alignement d'un champ texte")); undoStack().push(undo); } diff --git a/sources/qetgraphicsitem/diagramtextitem.cpp b/sources/qetgraphicsitem/diagramtextitem.cpp index 6e1a7aa02..c94695926 100644 --- a/sources/qetgraphicsitem/diagramtextitem.cpp +++ b/sources/qetgraphicsitem/diagramtextitem.cpp @@ -21,6 +21,7 @@ #include "../diagramcommands.h" #include "../qetapp.h" #include "../richtext/richtexteditor_p.h" +#include "../textanchor.h" /** @brief DiagramTextItem::DiagramTextItem @@ -193,6 +194,41 @@ Qt::Alignment DiagramTextItem::alignment() const return m_alignment; } +/** + @brief DiagramTextItem::anchorPos + @return the anchor point of this text, in parent coordinates: + the point of the text chosen by the alignment (top-left, right edge, + centre...). For a top-left aligned text this is pos(). + The anchor stays in place when the text changes, see finishAlignment(). +*/ +QPointF DiagramTextItem::anchorPos() const +{ + return TextAnchor::pos(this, m_alignment); +} + +/** + @brief DiagramTextItem::setAnchorPos + Move this text so that its anchor point (see anchorPos()) is at anchor. + @param anchor : in parent coordinates +*/ +void DiagramTextItem::setAnchorPos(const QPointF &anchor) +{ + setPos(TextAnchor::itemPosFor(this, m_alignment, anchor)); +} + +/** + @brief DiagramTextItem::setAlignmentAtAnchor + Change the alignment and move the text so that its new anchor point + is where the previous one was. + @param alignment +*/ +void DiagramTextItem::setAlignmentAtAnchor(const Qt::Alignment &alignment) +{ + const QPointF anchor = anchorPos(); + setAlignment(alignment); + setAnchorPos(anchor); +} + /** @brief DiagramTextItem::frameRect @return the rect used to draw a frame around this text diff --git a/sources/qetgraphicsitem/diagramtextitem.h b/sources/qetgraphicsitem/diagramtextitem.h index 26bd7cdde..e0c807cca 100644 --- a/sources/qetgraphicsitem/diagramtextitem.h +++ b/sources/qetgraphicsitem/diagramtextitem.h @@ -36,6 +36,8 @@ class DiagramTextItem : public QGraphicsTextItem Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged) + Q_PROPERTY(QPointF anchorPos READ anchorPos WRITE setAnchorPos) + Q_PROPERTY(Qt::Alignment alignmentAtAnchor READ alignment WRITE setAlignmentAtAnchor NOTIFY alignmentChanged) Q_PROPERTY(QString plainText READ toPlainText WRITE setPlainText) Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged) @@ -77,6 +79,10 @@ class DiagramTextItem : public QGraphicsTextItem void setAlignment(const Qt::Alignment &alignment); Qt::Alignment alignment() const; bool m_block_alignment = false; + + QPointF anchorPos() const; + void setAnchorPos(const QPointF &anchor); + void setAlignmentAtAnchor(const Qt::Alignment &alignment); QRectF frameRect() const; diff --git a/sources/textanchor.h b/sources/textanchor.h new file mode 100644 index 000000000..0c6983ab0 --- /dev/null +++ b/sources/textanchor.h @@ -0,0 +1,82 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef TEXTANCHOR_H +#define TEXTANCHOR_H + +#include +#include +#include + +/** + The anchor point of a text: the point of the text chosen by its + alignment (top-left, right edge, centre...). It is the point that + stays in place when the text changes, and the point shown as + "Position X/Y" in the text properties. + + The saved position of a text is still its top-left corner (pos()), + so existing projects and symbols are read and written unchanged. +*/ +namespace TextAnchor +{ + /** + @return the point of rect chosen by alignment, in the + coordinates of rect. Left and top are x = 0 and y = 0, as + finishAlignment() of the text items assumes. + */ + inline QPointF localPoint(const QRectF &rect, Qt::Alignment alignment) + { + qreal x = 0, y = 0; + + if (alignment & Qt::AlignRight) + x = rect.right(); + else if (alignment & Qt::AlignHCenter) + x = rect.center().x(); + + if (alignment & Qt::AlignBottom) + y = rect.bottom(); + else if (alignment & Qt::AlignVCenter) + y = rect.center().y(); + + return QPointF(x, y); + } + + /** + @return the anchor point of item for alignment, in parent + coordinates. For a top-left alignment this is item->pos(). + The rotation of the item is taken into account. + */ + inline QPointF pos(const QGraphicsItem *item, Qt::Alignment alignment) + { + return item->pos() + + item->mapToParent(localPoint(item->boundingRect(), alignment)) + - item->mapToParent(QPointF(0, 0)); + } + + /** + @return the position (top-left corner) to give to item so that + its anchor point for alignment is at anchor. + */ + inline QPointF itemPosFor(const QGraphicsItem *item, + Qt::Alignment alignment, + const QPointF &anchor) + { + return item->pos() + anchor - pos(item, alignment); + } +} + +#endif // TEXTANCHOR_H diff --git a/sources/ui/dynamicelementtextmodel.cpp b/sources/ui/dynamicelementtextmodel.cpp index 3939d50b3..a3e825218 100644 --- a/sources/ui/dynamicelementtextmodel.cpp +++ b/sources/ui/dynamicelementtextmodel.cpp @@ -303,7 +303,7 @@ QList DynamicElementTextModel::itemsForText( x_pos->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QStandardItem *x_pos_a = new QStandardItem; - x_pos_a->setData(deti->pos().x(), Qt::EditRole); + x_pos_a->setData(deti->anchorPos().x(), Qt::EditRole); x_pos_a->setData(DynamicElementTextModel::pos, Qt::UserRole+1); x_pos_a->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled @@ -318,7 +318,7 @@ QList DynamicElementTextModel::itemsForText( y_pos->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QStandardItem *y_pos_a = new QStandardItem; - y_pos_a->setData(deti->pos().y(), Qt::EditRole); + y_pos_a->setData(deti->anchorPos().y(), Qt::EditRole); y_pos_a->setData(DynamicElementTextModel::pos, Qt::UserRole+1); y_pos_a->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled @@ -600,9 +600,9 @@ QUndoCommand *DynamicElementTextModel::undoForEditedText( { QPointF p(text_qsi->child(x_txt_row,1)->data(Qt::EditRole).toDouble(), text_qsi->child(y_txt_row,1)->data(Qt::EditRole).toDouble()); - if(p != deti->pos()) + if(p != deti->anchorPos()) { - QPropertyUndoCommand *quc = new QPropertyUndoCommand(deti, "pos", QVariant(deti->pos()), QVariant(p), undo); + QPropertyUndoCommand *quc = new QPropertyUndoCommand(deti, "anchorPos", QVariant(deti->anchorPos()), QVariant(p), undo); quc->setAnimated(true, false); quc->setText(tr("Déplacer un texte d'élément")); } @@ -636,7 +636,7 @@ QUndoCommand *DynamicElementTextModel::undoForEditedText( Qt::Alignment alignment = text_qsi->child(align_txt_row, 1)->data(Qt::UserRole+2).value(); if (alignment != deti->alignment()) { - QPropertyUndoCommand *quc = new QPropertyUndoCommand(deti, "alignment", QVariant(deti->alignment()), QVariant(alignment), undo); + QPropertyUndoCommand *quc = new QPropertyUndoCommand(deti, "alignmentAtAnchor", QVariant(deti->alignment()), QVariant(alignment), undo); quc->setText(tr("Modifier l'alignement d'un texte d'élément")); } } @@ -1508,9 +1508,9 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti, case pos: { if(qsi->child(x_txt_row,1)) - qsi->child(x_txt_row,1)->setData(deti->pos().x(), Qt::EditRole); + qsi->child(x_txt_row,1)->setData(deti->anchorPos().x(), Qt::EditRole); if(qsi->child(y_txt_row,1)) - qsi->child(y_txt_row,1)->setData(deti->pos().y(), Qt::EditRole); + qsi->child(y_txt_row,1)->setData(deti->anchorPos().y(), Qt::EditRole); break; } case frame: @@ -1531,6 +1531,14 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti, } default:break; } + + //Rotating or resizing the text moves its anchor point (Position X/Y) + if ((type == rotation || type == textWidth) + && qsi->child(x_txt_row,1) && qsi->child(y_txt_row,1)) + { + qsi->child(x_txt_row,1)->setData(deti->anchorPos().x(), Qt::EditRole); + qsi->child(y_txt_row,1)->setData(deti->anchorPos().y(), Qt::EditRole); + } m_block_dataChanged = false; } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 9476eb944..1371aa6d6 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -131,6 +131,15 @@ add_executable(tst_itemgroups tst_itemgroups.cpp ${QET_DIR}/sources/itemgroups.c add_test(NAME tst_itemgroups COMMAND tst_itemgroups) target_include_directories(tst_itemgroups PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_itemgroups PRIVATE Qt::Test Qt::Widgets Qt::Xml) + +# textanchor.h is header-only: the point of a text chosen by its alignment, +# shown as Position X/Y in the text properties (#1155). +add_executable(tst_textanchor tst_textanchor.cpp) +add_test(NAME tst_textanchor COMMAND tst_textanchor) +set_tests_properties(tst_textanchor PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +target_include_directories(tst_textanchor PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_textanchor PRIVATE Qt::Test Qt::Widgets) + # legacyelementtypes.h is header-only: the rewrite of element type names in # nomenclature queries saved before June 2022. add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp) diff --git a/tests/qttest/tst_textanchor.cpp b/tests/qttest/tst_textanchor.cpp new file mode 100644 index 000000000..9c47a8f15 --- /dev/null +++ b/tests/qttest/tst_textanchor.cpp @@ -0,0 +1,135 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include +#include + +#include "textanchor.h" + +static bool samePoint(const QPointF &a, const QPointF &b) +{ + return qAbs(a.x() - b.x()) < 1e-9 && qAbs(a.y() - b.y()) < 1e-9; +} + +class tst_textanchor : public QObject +{ + Q_OBJECT + +private slots: + // The texts of issue #1155: Position X/Y is the point chosen by the + // alignment, so a right-aligned label keeps its right edge at X. + void pointOfTheText_data() + { + QTest::addColumn("alignment"); + QTest::addColumn("fx"); // fraction of the width + QTest::addColumn("fy"); // fraction of the height + + QTest::newRow("top left") << int(Qt::AlignTop | Qt::AlignLeft) << 0.0 << 0.0; + QTest::newRow("top right") << int(Qt::AlignTop | Qt::AlignRight) << 1.0 << 0.0; + QTest::newRow("centre") << int(Qt::AlignVCenter | Qt::AlignHCenter) << 0.5 << 0.5; + QTest::newRow("middle left") << int(Qt::AlignVCenter | Qt::AlignLeft) << 0.0 << 0.5; + QTest::newRow("middle right") << int(Qt::AlignVCenter | Qt::AlignRight) << 1.0 << 0.5; + QTest::newRow("bottom centre") << int(Qt::AlignBottom | Qt::AlignHCenter) << 0.5 << 1.0; + } + + void pointOfTheText() + { + QFETCH(int, alignment); + QFETCH(qreal, fx); + QFETCH(qreal, fy); + + QGraphicsTextItem text(QStringLiteral("XT2:12")); + text.setPos(-52, -3); + const QRectF r = text.boundingRect(); + + const QPointF expected(-52 + fx * r.width(), -3 + fy * r.height()); + QVERIFY(samePoint(TextAnchor::pos(&text, Qt::Alignment(alignment)), expected)); + } + + // Top-left alignment keeps today's numbers: the anchor is pos(), + // whatever the rotation or the rotation point. + void topLeftIsPos() + { + QGraphicsTextItem text(QStringLiteral("-K1")); + text.setPos(12.5, -40); + for (qreal angle : {0.0, 90.0, 180.0, 270.0, 30.0}) { + text.setRotation(angle); + for (bool centre : {false, true}) { + text.setTransformOriginPoint(centre ? text.boundingRect().center() : QPointF()); + QVERIFY(samePoint(TextAnchor::pos(&text, Qt::AlignTop | Qt::AlignLeft), text.pos())); + } + } + } + + // A rotated text: the anchor is the point of the text where it is + // drawn, e.g. the right edge of a text turned by 90° is below pos(). + void rotatedTextAnchorIsDrawnPoint() + { + QGraphicsTextItem text(QStringLiteral("XT2:12")); + text.setPos(100, 100); + text.setRotation(90); + const QRectF r = text.boundingRect(); + + const QPointF anchor = TextAnchor::pos(&text, Qt::AlignTop | Qt::AlignRight); + QVERIFY(samePoint(anchor, text.mapToParent(QPointF(r.right(), 0)))); + QVERIFY(samePoint(anchor, QPointF(100, 100 + r.right()))); + } + + // Typing an anchor puts that point of the text there, for every + // alignment, rotation and rotation point. + void itemPosForRoundTrips() + { + const QList horizontal {Qt::AlignLeft, Qt::AlignHCenter, Qt::AlignRight}; + const QList vertical {Qt::AlignTop, Qt::AlignVCenter, Qt::AlignBottom}; + + QGraphicsTextItem text(QStringLiteral("Right.")); + text.setPos(-81, -13); + const QPointF anchor(-52, -3); + + for (Qt::Alignment h : horizontal) + for (Qt::Alignment v : vertical) + for (qreal angle : {0.0, 90.0, 270.0, 45.0}) + for (bool centre : {false, true}) { + text.setRotation(angle); + text.setTransformOriginPoint(centre ? text.boundingRect().center() : QPointF()); + text.setPos(TextAnchor::itemPosFor(&text, h | v, anchor)); + QVERIFY2(samePoint(TextAnchor::pos(&text, h | v), anchor), + qPrintable(QStringLiteral("h %1 v %2 angle %3 centre %4") + .arg(int(h)).arg(int(v)).arg(angle).arg(centre))); + } + } + + // A longer text with the same anchor keeps its right edge in place: + // the gap between a label and its symbol does not change (#1155). + void rightEdgeStaysWhenTextGrows() + { + QGraphicsTextItem text(QStringLiteral("XT2:2")); + const QPointF anchor(-10, 0); + const Qt::Alignment right = Qt::AlignTop | Qt::AlignRight; + text.setPos(TextAnchor::itemPosFor(&text, right, anchor)); + const qreal short_left = text.pos().x(); + + text.setPlainText(QStringLiteral("XT2:12")); + text.setPos(TextAnchor::itemPosFor(&text, right, anchor)); + + QVERIFY(text.pos().x() < short_left); + QCOMPARE(text.pos().x() + text.boundingRect().right(), anchor.x()); + } +}; + +QTEST_MAIN(tst_textanchor) +#include "tst_textanchor.moc"