Merge pull request #1355 from ispyisail/feat/upright-symbol-texts
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m54s

Add an option to keep texts of turned symbols horizontal
This commit is contained in:
ispyisail
2026-10-07 21:39:49 +13:00
committed by GitHub
14 changed files with 444 additions and 103 deletions
+5 -1
View File
@@ -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('<project version="0.100.0" title=%s>\n</project>\n'
# <symbol_texts>: 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('<project version="0.100.0" title=%s>\n'
' <symbol_texts upright="true"/>\n</project>\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)
+7
View File
@@ -4299,6 +4299,13 @@ class Integration(unittest.TestCase):
self.assertEqual(r["project"]["title"], 'A "b" & <c>')
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"])
+27 -18
View File
@@ -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<QString> 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());
};
+52 -42
View File
@@ -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<QUuid, int> key(cacheKey(location),
(horizontal ? 1 : 0) | (vertical ? 2 : 0));
const QPair<QUuid, int> 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<QUuid, int> key(uuid_, (mirror.m11() < 0 ? 1 : 0)
| (mirror.m22() < 0 ? 2 : 0));
const QPair<QUuid, int> 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);
+10 -8
View File
@@ -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<QUuid, QPicture> m_pictures_H;
QHash<QUuid, QPicture> m_low_pictures_H;
/// Drawings for mirrored elements, by element and mirror
/// (horizontal, vertical, both), see getMirroredPictures()
QHash<QPair<QUuid, int>, QPicture> m_mirrored_pictures_H;
QHash<QPair<QUuid, int>, 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<QPair<QUuid, int>, QPicture> m_readable_pictures_H;
QHash<QPair<QUuid, int>, QPicture> m_readable_low_pictures_H;
/// What build() undoes on each text, read by parseText()
QTransform m_build_texts_undo;
QHash<QUuid, QPixmap> m_pixmap_H;
QHash<QUuid, primitives> m_primitives_H;
static ElementPictureFactory* m_factory;
+75 -20
View File
@@ -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;
}
+8 -2
View File
@@ -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 <Terminal *> 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<QPointF> m_plc_table_positions; // Positions of plc_table parts in the element definition
+14 -12
View File
@@ -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<Element *>(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<Element *>(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);
};
+70
View File
@@ -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<Element *>(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 <symbol_texts> 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 <wiring_rules> 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
<symbol_texts> 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 <wiring_rules> child of
+7
View File
@@ -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;
@@ -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);
}
@@ -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_;
+12
View File
@@ -788,6 +788,18 @@ if(QET_HAS_SCRIPTING)
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"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=\"$<TARGET_FILE:qelectrotech>\""
"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(
+138
View File
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
// A project can keep the texts drawn in its turned symbols horizontal
// (<symbol_texts upright="true"/>, 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<double> 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<double> 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("<project"))) + 1;
QVERIFY(root_end > 0);
xml.insert(root_end, QStringLiteral("\n<symbol_texts upright=\"true\"/>"));
QFile on_file(on);
QVERIFY(on_file.open(QIODevice::WriteOnly));
on_file.write(xml.toUtf8());
on_file.close();
const QList<double> turned = textRotations(exportDxf(off, QStringLiteral("dxf-off")),
QStringLiteral("M"));
const QList<double> 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"