From 592cbdbf0bbc826bb43950a0b9e779116e3262ac Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 7 Oct 2026 12:43:04 +1300 Subject: [PATCH] Keep the texts of turned symbols horizontal, per project (#1335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A project can now keep the texts drawn in its symbols, and the names of their terminals, horizontal when a symbol is turned: Project properties > General, "Garder horizontaux les textes des symboles pivotés". The box of each text turns with the symbol; the text does not, and reads as it does in the symbol itself. It is a project setting, saved as and only when on. A new project starts with it on; a project saved without it (every existing one) reads with it off, so it looks and saves exactly as before, and looks the same on every computer. The MCP server's new projects start with it on too. It builds on the mirror of #1354, which already redraws the texts of a mirrored symbol readable: what the symbol does to its texts is now its mirrors and, with the setting on, its turn (Element::symbolTextsTransform()). ElementPictureFactory caches one drawing per such transform, terminal names undo it the same way, and the DXF export places the texts alike. The fields of a symbol (label, comment...) already keep their angle with "Garder la rotation visuelle" and are left as they are. Known limit: two texts stacked in a symbol end up side by side when it is turned, and can overlap when kept horizontal; the setting can be turned off for such a project. Test: tst_uprightsymboltexts turns the symbols of a folio and checks the angle of each motor's "M" in the DXF, with the setting off and on. Co-Authored-By: Claude Opus 5.5 --- misc/qet-mcp/qet_mcp.py | 6 +- misc/qet-mcp/test_qet_mcp.py | 7 + sources/dxfexport.cpp | 19 +-- sources/factory/elementpicturefactory.cpp | 94 +++++++------ sources/factory/elementpicturefactory.h | 18 +-- sources/qetgraphicsitem/element.cpp | 95 ++++++++++--- sources/qetgraphicsitem/element.h | 10 +- sources/qetgraphicsitem/terminal.cpp | 26 ++-- sources/qetproject.cpp | 70 ++++++++++ sources/qetproject.h | 7 + sources/ui/configpage/projectconfigpages.cpp | 18 +++ sources/ui/configpage/projectconfigpages.h | 1 + tests/qttest/CMakeLists.txt | 12 ++ tests/qttest/tst_uprightsymboltexts.cpp | 138 +++++++++++++++++++ 14 files changed, 428 insertions(+), 93 deletions(-) create mode 100644 tests/qttest/tst_uprightsymboltexts.cpp diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index f614ba98a..eae28f5f1 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -3567,7 +3567,11 @@ def tool_project_new(binary: str, output: str, title: str = "Untitled", out.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix="qet-mcp-new-") as tmp: skeleton = Path(tmp) / "skeleton.qet" - skeleton.write_text('\n\n' + # : a project made in QElectroTech starts with the texts + # of its turned symbols kept horizontal; one read from a file without + # it does not, so say it here as QElectroTech would have + skeleton.write_text('\n' + ' \n\n' % quoteattr(title), encoding="utf-8") result = _run_qet(binary, [str(skeleton)], timeout=timeout, elements_dir=elements_dir, script="\n".join(script), tail=200_000) diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 4582e19da..8e876c2c3 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -4299,6 +4299,13 @@ class Integration(unittest.TestCase): self.assertEqual(r["project"]["title"], 'A "b" & ') self.assertEqual([f["title"] for f in r["project"]["folios"]], ["Power", "Control"]) + def test_new_project_keeps_symbol_texts_horizontal(self): + """Like a project made in QElectroTech, and kept by its save.""" + r = m.tool_project_new(BINARY, self.sb.p("u.qet"), title="U", folios=1) + self.assertTrue(r["ok"]) + root = ET.parse(r["output"]).getroot() + self.assertEqual(root.find("symbol_texts").get("upright"), "true") + def test_new_project_with_zero_folios(self): r = m.tool_project_new(BINARY, self.sb.p("z.qet"), title="Z", folios=0) self.assertTrue(r["ok"]) diff --git a/sources/dxfexport.cpp b/sources/dxfexport.cpp index cf96f02e9..86ec87780 100644 --- a/sources/dxfexport.cpp +++ b/sources/dxfexport.cpp @@ -197,11 +197,15 @@ void DxfExport::write(Diagram *diagram, int width, int height, fontSize = text->font().pixelSize(); QPointF text_pos = text->pos(); - if (elmt -> isMirrored()) + //A text the element mirrors, or turns in a project that keeps + //symbol texts horizontal, keeps reading as in the symbol: + //only its box moves, as ElementPictureFactory draws it on the + //folio. It is then drawn as in a symbol neither mirrored nor + //turned, moved to where the element puts the box's centre. + const QTransform texts_transform = elmt -> symbolTextsTransform(); + const qreal text_turn = elmt -> hasUprightSymbolTexts() ? 0 : rotation_angle; + if (!texts_transform.isIdentity()) { - //The text keeps reading normally: only its box is - //mirrored, about its own centre, as ElementPictureFactory - //draws it on the folio QTransform box_transform; box_transform.translate(text->pos().x(), text->pos().y()); box_transform.rotate(text->rotation()); @@ -210,19 +214,18 @@ void DxfExport::write(Diagram *diagram, int width, int height, text->boundingRect().width(), text->boundingRect().height()); const QPointF centre = box_transform.mapRect(box).center(); - text_pos += QPointF(centre.x() * (mirror_x - 1), - centre.y() * (mirror_y - 1)); + text_pos += texts_transform.map(centre) - centre; } qreal x = elem_pos_x + text_pos.x(); qreal y = elem_pos_y + text_pos.y(); - qreal angle = text -> rotation() + rotation_angle; + qreal angle = text -> rotation() + text_turn; qreal angler = angle * M_PI/180; int xdir = -sin(angler); int ydir = -cos(angler); - QPointF transformed_point = rotation_transformed(x, y, elem_pos_x, elem_pos_y, -rotation_angle); + QPointF transformed_point = rotation_transformed(x, y, elem_pos_x, elem_pos_y, -text_turn); x = transformed_point.x() - ydir * fontSize * 0.5; y = transformed_point.y() - xdir * fontSize * 0.5; QStringList lines = text->text().split('\n'); diff --git a/sources/factory/elementpicturefactory.cpp b/sources/factory/elementpicturefactory.cpp index ba2892aa2..f0a44ae36 100644 --- a/sources/factory/elementpicturefactory.cpp +++ b/sources/factory/elementpicturefactory.cpp @@ -95,37 +95,46 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu } /** - @brief ElementPictureFactory::getMirroredPictures - Same as getPictures(), for an element mirrored on its folio - (Element::setMirror()). The drawing is the same; only the texts differ: - each one is drawn so that, once the element's mirror is applied, its box - lands mirrored but its letters still read normally. + @brief ElementPictureFactory::getReadablePictures + Same as getPictures(), for an element that mirrors or turns its symbol + on a folio (Element::symbolTextsTransform()). The drawing is the same; + only the texts differ: each one is drawn so that, once the element has + applied @p texts_transform, its box lands where the element puts it but + its letters read as they do in the symbol itself. @param location - @param horizontal : the element's left and right swap - @param vertical : the element's top and bottom swap + @param texts_transform : the mirrors, then the turn, of the element @param picture @param low_picture */ -void ElementPictureFactory::getMirroredPictures(const ElementsLocation &location, - bool horizontal, - bool vertical, +void ElementPictureFactory::getReadablePictures(const ElementsLocation &location, + const QTransform &texts_transform, QPicture &picture, QPicture &low_picture) { - if(!location.exist() || !(horizontal || vertical)) { + if(!location.exist() || texts_transform.isIdentity()) { return; } - const QPair key(cacheKey(location), - (horizontal ? 1 : 0) | (vertical ? 2 : 0)); + const QPair key(cacheKey(location), readableKey(texts_transform)); - if(!m_mirrored_pictures_H.contains(key) - && !build(location, nullptr, nullptr, - QTransform::fromScale(horizontal ? -1 : 1, vertical ? -1 : 1))) { + if(!m_readable_pictures_H.contains(key) + && !build(location, nullptr, nullptr, texts_transform)) { return; } - picture = m_mirrored_pictures_H.value(key); - low_picture = m_mirrored_low_pictures_H.value(key); + picture = m_readable_pictures_H.value(key); + low_picture = m_readable_low_pictures_H.value(key); +} + +/** + @brief ElementPictureFactory::readableKey + @param texts_transform : mirrors and quarter turns only + @return a number for @p texts_transform, the same for the same one +*/ +int ElementPictureFactory::readableKey(const QTransform &texts_transform) +{ + auto digit = [](qreal value) {return qRound(value) + 1;}; // -1, 0 or 1 + return ((digit(texts_transform.m11()) * 3 + digit(texts_transform.m12())) * 3 + + digit(texts_transform.m21())) * 3 + digit(texts_transform.m22()); } /** @@ -147,9 +156,12 @@ void ElementPictureFactory::dropCache(const ElementsLocation &location) const QUuid uuid = cacheKey(location); m_pictures_H.remove(uuid); m_low_pictures_H.remove(uuid); - for (int mirror = 1 ; mirror <= 3 ; ++mirror) { - m_mirrored_pictures_H.remove(qMakePair(uuid, mirror)); - m_mirrored_low_pictures_H.remove(qMakePair(uuid, mirror)); + const auto keys = m_readable_pictures_H.keys(); + for (const auto &key : keys) { + if (key.first == uuid) { + m_readable_pictures_H.remove(key); + m_readable_low_pictures_H.remove(key); + } } m_pixmap_H.remove(uuid); m_primitives_H.remove(uuid); @@ -254,16 +266,16 @@ ElementPictureFactory::~ElementPictureFactory() this function draw on it and don't store it. if null, this function create a QPicture for normal and low zoom, draw on it and store it in m_pictures_H and m_low_pictures_H - (m_mirrored_pictures_H and m_mirrored_low_pictures_H if @p mirror - is not the identity) - @param mirror : the mirror of an element to draw the drawing for, see - getMirroredPictures() + (m_readable_pictures_H and m_readable_low_pictures_H if + @p texts_transform is not the identity) + @param texts_transform : what an element does to the texts of its + symbol, which the drawing undoes, see getReadablePictures() @return */ bool ElementPictureFactory::build(const ElementsLocation &location, QPicture *picture, QPicture *low_picture, - const QTransform &mirror) + const QTransform &texts_transform) { QDomElement dom = location.xml(); @@ -320,7 +332,7 @@ bool ElementPictureFactory::build(const ElementsLocation &location, tmp.setCosmetic(true); low_painter.setPen(tmp); - m_build_mirror = mirror; + m_build_texts_undo = texts_transform.inverted(); //scroll of the Children of the Definition: Parts of the Drawing // Extract PLC master data for rendering plc_table parts @@ -375,20 +387,19 @@ bool ElementPictureFactory::build(const ElementsLocation &location, //End of the drawing painter.end(); low_painter.end(); - m_build_mirror = QTransform(); + m_build_texts_undo = QTransform(); const auto uuid_ = cacheKey(location); - if (!mirror.isIdentity()) { - //The primitives are those of the drawing without mirror, - //already kept by the build of that one + if (!texts_transform.isIdentity()) { + //The primitives are those of the plain drawing, already kept + //by the build of that one qDeleteAll(primitives_.m_texts); - const QPair key(uuid_, (mirror.m11() < 0 ? 1 : 0) - | (mirror.m22() < 0 ? 2 : 0)); + const QPair key(uuid_, readableKey(texts_transform)); if (!picture) { - m_mirrored_pictures_H.insert(key, pic); + m_readable_pictures_H.insert(key, pic); } if (!low_picture) { - m_mirrored_low_pictures_H.insert(key, low_pic); + m_readable_low_pictures_H.insert(key, low_pic); } return true; } @@ -710,17 +721,16 @@ void ElementPictureFactory::parseText(const QDomElement &dom, QPainter &painter, text_transform.rotate(dom.attribute("rotation", "0").toDouble()); text_transform.translate(qpainter_offset.x(), qpainter_offset.y()); - if (!m_build_mirror.isIdentity()) + if (!m_build_texts_undo.isIdentity()) { - //The element is mirrored about its own axes when it is drawn. - //Mirror the text the same way first, about the centre of its - //box: the two mirrors cancel on the letters, which read - //normally, and the box still ends up where the element's - //mirror puts it. + //The element mirrors or turns its symbol when it is drawn. + //Undo that on the text first, about the centre of its box: the + //letters read as in the symbol itself, and the box still ends + //up where the element puts it. const QRectF box(QPointF(0, 0), text_document.size()); const QPointF centre = text_transform.mapRect(box).center(); painter.setTransform(QTransform::fromTranslate(-centre.x(), -centre.y()) - * m_build_mirror + * m_build_texts_undo * QTransform::fromTranslate(centre.x(), centre.y())); } painter.setTransform(text_transform, true); diff --git a/sources/factory/elementpicturefactory.h b/sources/factory/elementpicturefactory.h index 298442c3e..b2b16c26b 100644 --- a/sources/factory/elementpicturefactory.h +++ b/sources/factory/elementpicturefactory.h @@ -85,7 +85,7 @@ class ElementPictureFactory } void getPictures(const ElementsLocation &location, QPicture &picture, QPicture &low_picture); - void getMirroredPictures(const ElementsLocation &location, bool horizontal, bool vertical, + void getReadablePictures(const ElementsLocation &location, const QTransform &texts_transform, QPicture &picture, QPicture &low_picture); QPixmap pixmap(const ElementsLocation &location); ElementPictureFactory::primitives getPrimitives(const ElementsLocation &location); @@ -98,8 +98,9 @@ class ElementPictureFactory ~ElementPictureFactory(); static QUuid cacheKey(const ElementsLocation &location); + static int readableKey(const QTransform &texts_transform); bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr, - const QTransform &mirror=QTransform()); + const QTransform &texts_transform=QTransform()); void parseElement(const QDomElement &dom, QPainter &painter, primitives &prim) const; void parseLine (const QDomElement &dom, QPainter &painter, primitives &prim) const; void parseRect (const QDomElement &dom, QPainter &painter, primitives &prim) const; @@ -113,12 +114,13 @@ class ElementPictureFactory QHash m_pictures_H; QHash m_low_pictures_H; - /// Drawings for mirrored elements, by element and mirror - /// (horizontal, vertical, both), see getMirroredPictures() - QHash, QPicture> m_mirrored_pictures_H; - QHash, QPicture> m_mirrored_low_pictures_H; - /// The mirror build() draws for, read by parseText() - QTransform m_build_mirror; + /// Drawings whose texts read normally once an element has + /// mirrored or turned them, by element and readableKey(), see + /// getReadablePictures() + QHash, QPicture> m_readable_pictures_H; + QHash, QPicture> m_readable_low_pictures_H; + /// What build() undoes on each text, read by parseText() + QTransform m_build_texts_undo; QHash m_pixmap_H; QHash m_primitives_H; static ElementPictureFactory* m_factory; diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index 755898ee3..139ccb1ae 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -167,6 +167,8 @@ Element::Element( //The mirror is about the element's own axis, so it follows the //rotation, see applyMirrorTransform() connect(this, &Element::rotationChanged, this, &Element::applyMirrorTransform); + //Texts kept horizontal in a turned symbol depend on the turn + connect(this, &Element::rotationChanged, this, [this]() {updateSymbolPictures();}); } /** @@ -299,16 +301,7 @@ void Element::setMirror(bool horizontal, bool vertical) m_horizontal_mirror = horizontal; m_vertical_mirror = vertical; - m_mirrored_picture = QPicture(); - m_mirrored_low_zoom_picture = QPicture(); - if (isMirrored()) - ElementPictureFactory::instance()->getMirroredPictures( - m_location, - horizontal, - vertical, - m_mirrored_picture, - m_mirrored_low_zoom_picture); - + updateSymbolPictures(); applyMirrorTransform(); keepChildrenReadable(); update(); @@ -325,6 +318,69 @@ QTransform Element::mirrorTransform() const m_vertical_mirror ? -1 : 1); } +/** + @brief Element::hasUprightSymbolTexts + @return true if the project of this element keeps the texts of its + turned symbols horizontal (QETProject::uprightSymbolTexts()) +*/ +bool Element::hasUprightSymbolTexts() const +{ + const Diagram *d = diagram(); + return d && d->project() && d->project()->uprightSymbolTexts(); +} + +/** + @brief Element::symbolTextsTransform + @return what this element does to the texts drawn in its symbol, which + the drawing undoes to keep them readable (updateSymbolPictures()): + its mirrors, and its turn when the project keeps them horizontal. + The identity when the symbol is drawn as it is. +*/ +QTransform Element::symbolTextsTransform() const +{ + //QTransform's product applies its left operand first + QTransform transform = mirrorTransform(); + if (hasUprightSymbolTexts()) + transform *= QTransform().rotate(orientation() * 90); + return transform; +} + +/** + @brief Element::updateSymbolPictures + Take the drawing of the symbol whose texts read normally once this + element has mirrored and turned it (symbolTextsTransform()), or drop it + when the plain drawing does. + @param force : take it again even if the transform did not change, as + when the symbol's definition was reloaded +*/ +void Element::updateSymbolPictures(bool force) +{ + const QTransform transform = symbolTextsTransform(); + if (transform.isIdentity()) + { + if (!m_readable_transform.isIdentity()) + { + m_readable_transform = QTransform(); + m_readable_picture = QPicture(); + m_readable_low_zoom_picture = QPicture(); + update(); + } + return; + } + if (transform == m_readable_transform && !force) + return; + + m_readable_transform = transform; + m_readable_picture = QPicture(); + m_readable_low_zoom_picture = QPicture(); + ElementPictureFactory::instance()->getReadablePictures( + m_location, + transform, + m_readable_picture, + m_readable_low_zoom_picture); + update(); +} + /** @brief Element::applyMirrorTransform QGraphicsItem applies transform() after the rotation, so the mirrors of @@ -427,9 +483,10 @@ void Element::paint( QBrush brush; painter->setPen(pen); painter->setBrush(brush); - const QPicture &picture = isMirrored() ? m_mirrored_picture : m_picture; - const QPicture &low_zoom_picture = isMirrored() ? m_mirrored_low_zoom_picture - : m_low_zoom_picture; + const bool readable = !m_readable_transform.isIdentity(); + const QPicture &picture = readable ? m_readable_picture : m_picture; + const QPicture &low_zoom_picture = readable ? m_readable_low_zoom_picture + : m_low_zoom_picture; if (options && options->levelOfDetailFromTransform(painter->worldTransform()) < 0.5) { if (!low_zoom_picture.isNull()) @@ -1965,6 +2022,10 @@ QVariant Element::itemChange(GraphicsItemChange change, const QVariant &value) deti->refreshResizeHandlesVisibility(); } } + //Whether the texts of a turned symbol stay horizontal is the + //project's setting, known once the element is on one of its folios + else if (change == QGraphicsItem::ItemSceneHasChanged) + updateSymbolPictures(); return QetGraphicsItem::itemChange(change, value); } @@ -2186,13 +2247,7 @@ Element::ReloadPictureResult Element::reloadPicture() m_picture = picture; m_low_zoom_picture = low_zoom_picture; - if (isMirrored()) - ElementPictureFactory::instance()->getMirroredPictures( - m_location, - m_horizontal_mirror, - m_vertical_mirror, - m_mirrored_picture, - m_mirrored_low_zoom_picture); + updateSymbolPictures(true); update(); return ReloadPictureResult::Reloaded; } diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index e5b3d2295..6c2a4b352 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -195,6 +195,9 @@ class Element : public QetGraphicsItem void setHorizontalMirror(bool mirror); void setVerticalMirror(bool mirror); QTransform mirrorTransform() const; + bool hasUprightSymbolTexts() const; + QTransform symbolTextsTransform() const; + void updateSymbolPictures(bool force = false); void keepReadable(QGraphicsItem *child) const; //METHODS related to texts @@ -307,8 +310,11 @@ class Element : public QetGraphicsItem QList m_terminals; QPicture m_picture; QPicture m_low_zoom_picture; - QPicture m_mirrored_picture; - QPicture m_mirrored_low_zoom_picture; + /// The drawing with its texts kept readable, when the symbol's + /// mirror or turn would not leave them so (m_readable_transform) + QPicture m_readable_picture; + QPicture m_readable_low_zoom_picture; + QTransform m_readable_transform; ElementData m_data; QList m_plc_table_positions; // Positions of plc_table parts in the element definition diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index 3a8564ab3..67a19e400 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -306,20 +306,22 @@ void Terminal::paint( QFontMetrics fm(d->m_label_font); QSizeF text_size = fm.size(Qt::TextSingleLine, display_name); - // On a mirrored element, mirror the name a second time about - // the centre of its box, in the frame it is drawn in (turned - // by label_rotation): its box stays where the element's mirror - // puts it, but it reads normally (Element::keepReadable()) - const Element *mirrored = qgraphicsitem_cast(parentItem()); - if (mirrored && !mirrored->isMirrored()) mirrored = nullptr; - auto keep_readable = [painter, mirrored](const QRectF &rect, qreal label_rotation) { - if (!mirrored) return; - const QTransform mirror = QTransform().rotate(label_rotation) - * mirrored->mirrorTransform() - * QTransform().rotate(-label_rotation); + // When the element mirrors its symbol, or turns it in a project + // that keeps symbol texts horizontal, undo that on the name + // about the centre of its box, in the frame it is drawn in + // (turned by label_rotation): its box stays where the element + // puts it, but it reads as in the symbol (Element::keepReadable()) + const Element *element = qgraphicsitem_cast(parentItem()); + const QTransform texts_transform = element ? element->symbolTextsTransform() + : QTransform(); + auto keep_readable = [painter, &texts_transform](const QRectF &rect, qreal label_rotation) { + if (texts_transform.isIdentity()) return; + const QTransform undo = QTransform().rotate(label_rotation) + * texts_transform.inverted() + * QTransform().rotate(-label_rotation); const QPointF c = rect.center(); painter->setTransform(QTransform::fromTranslate(-c.x(), -c.y()) - * mirror + * undo * QTransform::fromTranslate(c.x(), c.y()), true); }; diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 74fb18481..d4de0ce64 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -74,6 +74,11 @@ m_project_properties_handler{this} m_elements_collection = new XmlElementCollection(this); init(); + //A new project keeps the texts of its turned symbols horizontal; a + //project saved without the setting is read with it off, so it looks + //as it always did (readSymbolTextsXml()) + m_upright_symbol_texts = true; + QSettings settings; //Read auto break conductor default from global settings @@ -1380,6 +1385,37 @@ void QETProject::setWireHops(WireHops::Mode mode) } } +/** + @brief QETProject::uprightSymbolTexts + @return true if the texts drawn in a symbol stay horizontal, and read + normally, when the symbol is turned or mirrored on a folio, instead of + turning with it. A mirrored symbol always keeps its texts readable. +*/ +bool QETProject::uprightSymbolTexts() const { + return m_upright_symbol_texts; +} + +/** + @brief QETProject::setUprightSymbolTexts + Set whether the texts drawn in a turned symbol stay horizontal, and + redraw every symbol of the project. + @param upright +*/ +void QETProject::setUprightSymbolTexts(bool upright) +{ + if (upright == m_upright_symbol_texts) { + return; + } + m_upright_symbol_texts = upright; + for (Diagram *diagram : diagrams()) { + for (QGraphicsItem *item : diagram->items()) { + if (Element *element = qgraphicsitem_cast(item)) { + element->updateSymbolPictures(); + } + } + } +} + /** @brief QETProject::wiringRules @return how many wires a terminal of this project may take @@ -1521,6 +1557,7 @@ QDomDocument QETProject::toXml() // local, non-transmitted usage tracking (time spent on this project) writeUsageXml(project_root); writeWireHopsXml(project_root); + writeSymbolTextsXml(project_root); writeWiringRulesXml(project_root); // Properties for news diagrams @@ -2032,6 +2069,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project) //Load the local, non-transmitted usage tracking readUsageXml(xml_project); readWireHopsXml(xml_project); + readSymbolTextsXml(xml_project); readWiringRulesXml(xml_project); //Load the default properties for the new diagrams @@ -2229,6 +2267,21 @@ void QETProject::readWireHopsXml(QDomDocument &xml_project) : WireHops::fromString(crossings.attribute(QStringLiteral("hop"))); } +/** + @brief QETProject::readSymbolTextsXml + Read the element of the project, if any. A project + without it was saved before the setting existed, or with it off, and + keeps the texts of its turned symbols turned. + @param xml_project : the xml description of the project +*/ +void QETProject::readSymbolTextsXml(QDomDocument &xml_project) +{ + const QDomElement texts = xml_project.documentElement() + .firstChildElement(QStringLiteral("symbol_texts")); + m_upright_symbol_texts = !texts.isNull() + && texts.attribute(QStringLiteral("upright")) == QLatin1String("true"); +} + /** @brief QETProject::readWiringRulesXml Read the element of the project, if any. @@ -2420,6 +2473,23 @@ void QETProject::writeWireHopsXml(QDomElement &xml_element) xml_element.appendChild(crossings); } +/** + @brief QETProject::writeSymbolTextsXml + Export whether the texts of turned symbols stay horizontal, as a + child of \a xml_element. Written only when on, so a + project with it off saves exactly as before. +*/ +void QETProject::writeSymbolTextsXml(QDomElement &xml_element) +{ + if (!m_upright_symbol_texts) { + return; + } + QDomElement texts = xml_element.ownerDocument() + .createElement(QStringLiteral("symbol_texts")); + texts.setAttribute(QStringLiteral("upright"), QStringLiteral("true")); + xml_element.appendChild(texts); +} + /** @brief QETProject::writeWiringRulesXml Export the project's wiring rules as a child of diff --git a/sources/qetproject.h b/sources/qetproject.h index 220ae0e49..212d08169 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -229,6 +229,8 @@ class QETProject : public QObject void setAutoConductor (bool ac); WireHops::Mode wireHops() const; void setWireHops(WireHops::Mode mode); + bool uprightSymbolTexts() const; + void setUprightSymbolTexts(bool upright); WiringRules::Settings wiringRules() const; WiringRules::Settings projectWiringRules() const; void setWiringRules(const WiringRules::Settings &rules); @@ -329,12 +331,14 @@ class QETProject : public QObject void readTerminalStripXml(const QDomDocument &xml_project); void readUsageXml(QDomDocument &xml_project); void readWireHopsXml(QDomDocument &xml_project); + void readSymbolTextsXml(QDomDocument &xml_project); void readWiringRulesXml(QDomDocument &xml_project); void writeProjectPropertiesXml(QDomElement &); void writeDefaultPropertiesXml(QDomElement &); void writeUsageXml(QDomElement &); void writeWireHopsXml(QDomElement &); + void writeSymbolTextsXml(QDomElement &); void writeWiringRulesXml(QDomElement &); void addDiagram(Diagram *diagram, int pos = -1); void detachDiagram(Diagram *diagram); @@ -400,6 +404,9 @@ class QETProject : public QObject void linkElementsToElementAutoNums(); bool m_auto_conductor = true; WireHops::Mode m_wire_hops = WireHops::Mode::None; + /// Texts drawn in a turned symbol stay horizontal (on for a new + /// project, off for one saved without it), see uprightSymbolTexts() + bool m_upright_symbol_texts = false; WiringRules::Settings m_wiring_rules; bool m_auto_break_conductor = false; XmlElementCollection *m_elements_collection = nullptr; diff --git a/sources/ui/configpage/projectconfigpages.cpp b/sources/ui/configpage/projectconfigpages.cpp index d2873cff8..3bdafa80f 100644 --- a/sources/ui/configpage/projectconfigpages.cpp +++ b/sources/ui/configpage/projectconfigpages.cpp @@ -185,6 +185,11 @@ void ProjectMainConfigPage::applyProjectConf() modified_project = true; } + if (m_project -> uprightSymbolTexts() != upright_symbol_texts_cb_ -> isChecked()) { + m_project -> setUprightSymbolTexts(upright_symbol_texts_cb_ -> isChecked()); + modified_project = true; + } + //Following the application's rules stores nothing in the project WiringRules::Settings wiring_rules; if (!use_application_rules_cb_ -> isChecked()) { @@ -252,6 +257,14 @@ void ProjectMainConfigPage::initWidgets() "Seul le dessin change : aucun élément n'est ajouté et aucun conducteur n'est coupé.", "tooltip")); + //Texts of turned symbols kept horizontal + upright_symbol_texts_cb_ = new QCheckBox(tr("Garder horizontaux les textes des symboles pivotés", + "checkbox label")); + upright_symbol_texts_cb_ -> setToolTip(tr("Les textes dessinés dans un symbole et les noms de ses bornes restent " + "lisibles quand le symbole est pivoté : leur cadre tourne avec le symbole, " + "pas le texte. Décochez pour qu'ils tournent avec le symbole, comme avant.", + "tooltip")); + //How many wires a terminal may take (discussion #1158) wiring_rules_gb_ = new QGroupBox(tr("Conducteurs par borne", "group box title")); use_application_rules_cb_ = new QCheckBox(tr("Utiliser les réglages de l'application", "checkbox label")); @@ -318,6 +331,9 @@ void ProjectMainConfigPage::initLayout() main_layout0 -> addLayout(wire_hops_layout0); main_layout0 -> addSpacing(10); + main_layout0 -> addWidget(upright_symbol_texts_cb_); + main_layout0 -> addSpacing(10); + QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_); wiring_rules_layout -> addWidget(use_application_rules_cb_); QHBoxLayout *max_wires_layout = new QHBoxLayout(); @@ -351,6 +367,7 @@ void ProjectMainConfigPage::readValuesFromProject() const int wire_hops_index = wire_hops_cb_ -> findData(WireHops::toString(m_project -> wireHops())); wire_hops_cb_ -> setCurrentIndex(qMax(0, wire_hops_index)); + upright_symbol_texts_cb_ -> setChecked(m_project -> uprightSymbolTexts()); //The rules that apply: the project's own or the application's const WiringRules::Settings wiring_rules = m_project -> wiringRules(); @@ -406,6 +423,7 @@ void ProjectMainConfigPage::adjustReadOnly() usage_enabled_cb_ -> setDisabled(is_read_only); usage_reset_pb_ -> setDisabled(is_read_only); wire_hops_cb_ -> setDisabled(is_read_only); + upright_symbol_texts_cb_ -> setDisabled(is_read_only); wiring_rules_gb_ -> setDisabled(is_read_only); } diff --git a/sources/ui/configpage/projectconfigpages.h b/sources/ui/configpage/projectconfigpages.h index 98b040d9e..435df2c62 100644 --- a/sources/ui/configpage/projectconfigpages.h +++ b/sources/ui/configpage/projectconfigpages.h @@ -139,6 +139,7 @@ class ProjectMainConfigPage : public ProjectConfigPage { QPushButton *usage_reset_pb_; QLabel *wire_hops_label_; QComboBox *wire_hops_cb_; + QCheckBox *upright_symbol_texts_cb_; QGroupBox *wiring_rules_gb_; QCheckBox *use_application_rules_cb_; QLabel *max_wires_label_; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 6be7351e9..78a782061 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -676,6 +676,18 @@ if(QET_HAS_SCRIPTING) "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + # A project can keep the texts of its turned symbols horizontal: the + # DXF of a turned folio of tremie_vibrante.qet with the setting off and on. + add_executable( + tst_uprightsymboltexts + tst_uprightsymboltexts.cpp) + add_test(NAME tst_uprightsymboltexts COMMAND tst_uprightsymboltexts) + add_dependencies(tst_uprightsymboltexts qelectrotech) + target_link_libraries(tst_uprightsymboltexts PRIVATE Qt::Test) + target_compile_definitions(tst_uprightsymboltexts PRIVATE + "QET_TEST_BINARY_PATH=\"$\"" + "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + # Undoing a crop of a picture restores its crop rectangle too, so a # project saved after the undo does not record the undone crop. add_executable( diff --git a/tests/qttest/tst_uprightsymboltexts.cpp b/tests/qttest/tst_uprightsymboltexts.cpp new file mode 100644 index 000000000..68e853849 --- /dev/null +++ b/tests/qttest/tst_uprightsymboltexts.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include +#include + +// A project can keep the texts drawn in its turned symbols horizontal +// (, Project properties). Through the real +// binary on tremie_vibrante.qet: the symbols of the first folio are turned +// by 90 degrees, then the folio is exported to DXF with the setting off and +// on. The "M" of each motor turns with its symbol when it is off, and stays +// horizontal when it is on. +class tst_uprightsymboltexts : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + QProcessEnvironment environment() + { + const QString home = m_dir.filePath(QStringLiteral("home")); + QDir().mkpath(home); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); + env.insert(QStringLiteral("HOME"), home); + env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config")); + env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data")); + env.insert(QStringLiteral("TMPDIR"), m_dir.path()); + return env; + } + + bool runQet(const QStringList &arguments) + { + QProcess proc; + proc.setProcessEnvironment(environment()); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments); + return proc.waitForFinished(120000) && proc.exitCode() == 0; + } + + // The rotation (DXF group code 50) of every TEXT entity of @p dxf whose + // text (group code 1) is @p text + static QList textRotations(const QString &dxf, const QString &text) + { + QFile file(dxf); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return {}; + const QStringList lines = QString::fromUtf8(file.readAll()) + .split(QLatin1Char('\n')); + QList rotations; + bool in_text = false, match = false; + double rotation = 0; + for (int i = 0; i + 1 < lines.size(); i += 2) { + const QString code = lines.at(i).trimmed(); + const QString value = lines.at(i + 1).trimmed(); + if (code == QLatin1String("0")) { + if (in_text && match) rotations << rotation; + in_text = value == QLatin1String("TEXT"); + match = false; + rotation = 0; + } else if (in_text && code == QLatin1String("1")) { + match = value == text; + } else if (in_text && code == QLatin1String("50")) { + rotation = value.toDouble(); + } + } + return rotations; + } + + // The DXF of the first folio of @p project, exported into @p name + QString exportDxf(const QString &project, const QString &name) + { + const QString out = m_dir.filePath(name); + QDir().mkpath(out); + if (!runQet({QStringLiteral("--export-dxf"), project, out})) return {}; + const QStringList files = QDir(out).entryList({QStringLiteral("01*.dxf")}); + return files.isEmpty() ? QString() : QDir(out).filePath(files.first()); + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + } + + void motorTextStaysHorizontal() + { + //The symbols of the first folio, turned by 90 degrees + const QString off = m_dir.filePath(QStringLiteral("off.qet")); + const QString script = m_dir.filePath(QStringLiteral("turn.js")); + QFile js(script); + QVERIFY(js.open(QIODevice::WriteOnly)); + js.write(QStringLiteral( + "var els = qet.elementUuids(0);\n" + "for (var e = 0; e < els.length; e++) qet.rotateElement(0, els[e], 90);\n" + "qet.save(%1);\n").arg(QLatin1Char('"') + off + QLatin1Char('"')).toUtf8()); + js.close(); + QVERIFY(runQet({QStringLiteral("--run"), script, + QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet")})); + + //An existing project is read with the setting off, and saved so + QFile off_file(off); + QVERIFY(off_file.open(QIODevice::ReadOnly)); + QString xml = QString::fromUtf8(off_file.readAll()); + off_file.close(); + QVERIFY(!xml.contains(QStringLiteral("symbol_texts"))); + + //The same project with the setting on + const QString on = m_dir.filePath(QStringLiteral("on.qet")); + const int root_end = xml.indexOf(QLatin1Char('>'), xml.indexOf(QStringLiteral(" 0); + xml.insert(root_end, QStringLiteral("\n")); + QFile on_file(on); + QVERIFY(on_file.open(QIODevice::WriteOnly)); + on_file.write(xml.toUtf8()); + on_file.close(); + + const QList turned = textRotations(exportDxf(off, QStringLiteral("dxf-off")), + QStringLiteral("M")); + const QList upright = textRotations(exportDxf(on, QStringLiteral("dxf-on")), + QStringLiteral("M")); + //The two motors of the folio + QCOMPARE(turned.size(), 2); + QCOMPARE(upright.size(), 2); + for (double rotation : turned) + QCOMPARE(std::fmod(rotation, 360.0), 270.0); + for (double rotation : upright) + QCOMPARE(std::fmod(rotation, 360.0), 0.0); + } +}; + +QTEST_APPLESS_MAIN(tst_uprightsymboltexts) + +#include "tst_uprightsymboltexts.moc"