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 d61041d58..f0351966b 100644
--- a/sources/dxfexport.cpp
+++ b/sources/dxfexport.cpp
@@ -50,19 +50,26 @@ namespace {
rectangles, circles, polygons and arcs, and its terminals when
@a draw_terminals, as if it were placed at @a elem_pos_x,
@a elem_pos_y and turned @a rotation_angle degrees, with @a elmt's
- mirrors when @a mirrored. Used for every symbol drawn in full, and
- once per block for a block's content.
+ mirrors and its texts kept readable (Element::symbolTextsTransform())
+ when @a as_placed. Used for every symbol drawn in full, and once per
+ block for a block's content.
*/
void drawSymbol(const QString &file_path, Element *elmt,
qreal elem_pos_x, qreal elem_pos_y,
double rotation_angle, bool draw_terminals,
- bool mirrored = false)
+ bool as_placed = false)
{
using namespace DxfExport;
//The mirrors of an element are about its own axes, before it is
//rotated, see Element::setMirror()
- const bool mirror_h = mirrored && elmt -> hasHorizontalMirror();
- const bool mirror_v = mirrored && elmt -> hasVerticalMirror();
+ const bool mirror_h = as_placed && elmt -> hasHorizontalMirror();
+ const bool mirror_v = as_placed && elmt -> hasVerticalMirror();
+ const QTransform texts_transform = as_placed
+ ? elmt -> symbolTextsTransform()
+ : QTransform();
+ const qreal text_turn = as_placed && elmt -> hasUprightSymbolTexts()
+ ? 0
+ : rotation_angle;
const qreal mirror_x = mirror_h ? -1 : 1;
const qreal mirror_y = mirror_v ? -1 : 1;
ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location());
@@ -75,11 +82,13 @@ namespace {
fontSize = text->font().pixelSize();
QPointF text_pos = text->pos();
- if (mirror_h || mirror_v)
+ //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.
+ 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());
@@ -88,19 +97,18 @@ namespace {
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 = DxfExport::rotation_transformed(x, y, elem_pos_x, elem_pos_y, -rotation_angle);
+ QPointF transformed_point = DxfExport::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');
@@ -384,7 +392,8 @@ void DxfExport::write(Diagram *diagram, int width, int height,
QSet used;
for (Element *elmt : std::as_const(list_elements)) {
const QString key = elmt -> location().toString();
- if (elmt -> isMirrored() || block_names.contains(key)
+ if (!elmt -> symbolTextsTransform().isIdentity()
+ || block_names.contains(key)
|| drawsNothing(elmt, properties.draw_terminals))
continue;
QString file_name = elmt -> location().fileName();
@@ -402,11 +411,11 @@ void DxfExport::write(Diagram *diagram, int width, int height,
block_models << elmt;
}
}
- //A mirrored symbol is drawn in full: an INSERT with a negative
- //scale would mirror its texts too, which QElectroTech keeps
- //readable
+ //A mirrored symbol, or a turned one whose texts stay horizontal,
+ //is drawn in full: an INSERT would mirror or turn its texts too,
+ //which QElectroTech keeps readable
const auto blockOf = [&block_names](Element *elmt) {
- return elmt -> isMirrored()
+ return !elmt -> symbolTextsTransform().isIdentity()
? QString()
: block_names.value(elmt -> location().toString());
};
diff --git a/sources/factory/elementpicturefactory.cpp b/sources/factory/elementpicturefactory.cpp
index d060f46d9..3c6ef11b5 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);
//The text items belong to the cache; nothing else holds them.
@@ -255,16 +267,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();
@@ -324,7 +336,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
@@ -378,21 +390,20 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
//End of the drawing
painter.end();
low_painter.end();
- m_build_mirror = QTransform();
+ m_build_texts_undo = QTransform();
qDeleteAll(low_primitives.m_texts);
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;
}
@@ -714,17 +725,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 e02e9e26e..683b745f0 100644
--- a/tests/qttest/CMakeLists.txt
+++ b/tests/qttest/CMakeLists.txt
@@ -788,6 +788,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"