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"