Merge remote-tracking branch 'upstream/master' into wiring-manager

Conflicts resolved in 24 files. Notable decisions:

- ico/icon-themes.qrc: upstream's icon folder restructure kept; the 36
  cable icon entries re-added under the new qet/qet-dark path scheme
  (XML valid, every referenced file exists).
- Toolbar system: upstream's DiagramToolbarSettings now owns the
  toolbar filling. The cable tool is registered as
  diagrameditor.add_cable (generic group loop) and listed on the
  "diagram" toolbar right after the auto-break conductor action,
  separated by a divider, exactly like before the merge.
- Paste: the cable lines of a copy are handed to
  PasteNumberingImport::push() as extra commands, so paste + cable
  lines + the numbering question stay ONE undo step on both paste
  paths (DiagramView::paste and DiagramEventAddPaste::commit).
- PDF links: upstream's deterministic link sorting kept together with
  the cable cross-reference injector (injectLinkTo -> links list).
- Element renumbering: upstream's ElementAutoNumSchemeCommand::renumber
  replaces the old formula-based body of
  QETProject::renumberElementsBySchemeTitle; the cable numbering rule
  API (cableAutoNum, axis priority, rule name) is kept in full.
- Auto-numbering pages: upstream's scheme management kept, the Câbles
  tab added as index 4 (changeToCableTab), both entry points
  changeToCable() and changeToElement() kept side by side.
- SelectAutonumW: dirty tracking and the contextEdited signal moved
  into upstream's insertPartRow() row API, so single-rule mode, the
  rule-removable button and "confirm without change leaves the project
  alone" keep working.
- xrefpropertieswidget: upstream's offset clamping kept; the cable
  branch (Texte field, Police button, hidden display options) kept;
  the stack-overlapping checkbox is hidden for cables.

Verified after the merge: build green (515/515, no errors), ctest
85/86 (only the pre-existing headless tst_menubarkeyboard failure),
offscreen smoke exit 124.
This commit is contained in:
Kellermorph
2026-10-09 14:09:08 +02:00
1767 changed files with 194544 additions and 72846 deletions
+115 -9
View File
@@ -94,6 +94,49 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
}
}
/**
@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 texts_transform : the mirrors, then the turn, of the element
@param picture
@param low_picture
*/
void ElementPictureFactory::getReadablePictures(const ElementsLocation &location,
const QTransform &texts_transform,
QPicture &picture,
QPicture &low_picture)
{
if(!location.exist() || texts_transform.isIdentity()) {
return;
}
const QPair<QUuid, int> key(cacheKey(location), readableKey(texts_transform));
if(!m_readable_pictures_H.contains(key)
&& !build(location, nullptr, nullptr, texts_transform)) {
return;
}
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());
}
/**
@brief ElementPictureFactory::dropCache
Forget the cached drawing of the element at @p location, so the next
@@ -113,8 +156,16 @@ void ElementPictureFactory::dropCache(const ElementsLocation &location)
const QUuid uuid = cacheKey(location);
m_pictures_H.remove(uuid);
m_low_pictures_H.remove(uuid);
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);
//The text items belong to the cache; nothing else holds them.
qDeleteAll(m_primitives_H.take(uuid).m_texts);
}
/**
@@ -135,14 +186,29 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
{
auto doc = location.pugiXml();
//size
int w = doc.document_element().attribute("width").as_int();
int h = doc.document_element().attribute("height").as_int();
//Bounded first, so the rounding below cannot overflow on a
//crafted file.
int w = qBound(0, doc.document_element().attribute("width").as_int(), 1000000);
int h = qBound(0, doc.document_element().attribute("height").as_int(), 1000000);
while (w % 10) ++ w;
while (h % 10) ++ h;
//hotspot
int hsx = qMin(doc.document_element().attribute("hotspot_x").as_int(), w);
int hsy = qMin(doc.document_element().attribute("hotspot_y").as_int(), h);
//The size comes straight from the file. A symbol whose parts
//span hundreds of thousands of pixels would ask for a pixmap of
//hundreds of gigabytes; draw it scaled down to fit instead. The
//largest symbols in the shipped collection are about 3200 px,
//so this only changes files nobody would draw on purpose.
const int max_side = 4096;
qreal scale = 1.0;
if (w > max_side || h > max_side) {
scale = qreal(max_side) / qMax(w, h);
w = qMax(1, qRound(w * scale));
h = qMax(1, qRound(h * scale));
}
QPixmap pix(w, h);
//Element definitions almost always draw with a hardcoded black
//stroke color, on the assumption of the white diagram sheet they
@@ -157,6 +223,7 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
QPainter painter(&pix);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.scale(scale, scale);
painter.translate(hsx, hsy);
painter.drawPicture(0, 0, m_pictures_H.value(uuid));
@@ -200,11 +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_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)
QPicture *low_picture,
const QTransform &texts_transform)
{
QDomElement dom = location.xml();
@@ -233,6 +305,9 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
QPainter painter;
QPicture pic;
primitives primitives_;
//The low-zoom pass fills a second set of primitives that is only
//drawn, never kept: its text items are deleted at the end.
primitives low_primitives;
if (picture) {
painter.begin(picture);
}
@@ -261,6 +336,8 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
tmp.setCosmetic(true);
low_painter.setPen(tmp);
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
QDomElement plc_master_data;
@@ -304,8 +381,7 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
// complex font/text operations.
} else {
parseElement(qde, painter, primitives_);
primitives fake_prim;
parseElement(qde, low_painter, fake_prim);
parseElement(qde, low_painter, low_primitives);
}
}
}
@@ -314,8 +390,23 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
//End of the drawing
painter.end();
low_painter.end();
m_build_texts_undo = QTransform();
qDeleteAll(low_primitives.m_texts);
const auto uuid_ = cacheKey(location);
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_, readableKey(texts_transform));
if (!picture) {
m_readable_pictures_H.insert(key, pic);
}
if (!low_picture) {
m_readable_low_pictures_H.insert(key, low_pic);
}
return true;
}
if (!picture) {
m_pictures_H.insert(uuid_, pic);
m_primitives_H.insert(uuid_, primitives_);
@@ -596,8 +687,6 @@ void ElementPictureFactory::parseText(const QDomElement &dom, QPainter &painter,
text_document.setPlainText(dom.attribute("text"));
painter.setTransform(QTransform(), false);
painter.translate(dom.attribute("x").toDouble(), dom.attribute("y").toDouble());
painter.rotate(dom.attribute("rotation", "0").toDouble());
/*
Moves the QPainter's coordinate system to render in the right place;
@@ -631,7 +720,24 @@ void ElementPictureFactory::parseText(const QDomElement &dom, QPainter &painter,
}
}
painter.translate(qpainter_offset);
QTransform text_transform;
text_transform.translate(dom.attribute("x").toDouble(), dom.attribute("y").toDouble());
text_transform.rotate(dom.attribute("rotation", "0").toDouble());
text_transform.translate(qpainter_offset.x(), qpainter_offset.y());
if (!m_build_texts_undo.isIdentity())
{
//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_texts_undo
* QTransform::fromTranslate(centre.x(), centre.y()));
}
painter.setTransform(text_transform, true);
// force the palette used to render the QTextDocument
QAbstractTextDocumentLayout::PaintContext ctx;
+14 -1
View File
@@ -21,6 +21,8 @@
#include <QMutex>
#include <QSharedPointer>
#include <QHash>
#include <QPair>
#include <QTransform>
class ElementsLocation;
class QPicture;
@@ -83,6 +85,8 @@ class ElementPictureFactory
}
void getPictures(const ElementsLocation &location, QPicture &picture, QPicture &low_picture);
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);
void dropCache(const ElementsLocation &location);
@@ -94,7 +98,9 @@ class ElementPictureFactory
~ElementPictureFactory();
static QUuid cacheKey(const ElementsLocation &location);
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr);
static int readableKey(const QTransform &texts_transform);
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr,
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;
@@ -108,6 +114,13 @@ class ElementPictureFactory
QHash<QUuid, QPicture> m_pictures_H;
QHash<QUuid, QPicture> m_low_pictures_H;
/// 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;
+25 -17
View File
@@ -42,6 +42,7 @@
#include "../ui/shapegraphicsitempropertieswidget.h"
#include <QGraphicsItem>
#include <QSet>
#include <QSettings>
/**
@@ -97,22 +98,30 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
PropertiesEditorWidget *editor,
QWidget *parent)
{
//Selecting a conductor's text label edits its parent conductor (#500),
//mirroring how double-clicking the label opens the conductor dialog.
//A rubber band over wires selects their labels too: each label
//counts as its conductor, once.
QList<QGraphicsItem *> mapped;
QSet<QGraphicsItem *> seen;
for (QGraphicsItem *qgi : std::as_const(items)) {
if (auto *cti = qgraphicsitem_cast<ConductorTextItem *>(qgi)) {
if (Conductor *parent_cond = cti->parentConductor()) {
qgi = parent_cond;
}
}
if (!seen.contains(qgi)) {
seen.insert(qgi);
mapped << qgi;
}
}
items = mapped;
const int count_ = items.size();
if (count_ == 0) {
return nullptr;
}
QGraphicsItem *item = items.first();
//Selecting a conductor's text label edits its parent conductor (#500),
//mirroring how double-clicking the label opens the conductor dialog.
if (count_ == 1) {
if (auto *cti = qgraphicsitem_cast<ConductorTextItem *>(item)) {
if (Conductor *parent_cond = cti->parentConductor()) {
items = {parent_cond};
item = parent_cond;
}
}
}
const int type_ = item->type();
//The editor widget can only edit one item
@@ -139,18 +148,17 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
false).toBool()) {
return nullptr;
}
//Prototype (#500): single-conductor editing in the dock.
if (count_ > 1) {
return nullptr;
QList<Conductor *> conductors;
for (QGraphicsItem *qgi : std::as_const(items)) {
conductors << static_cast<Conductor *>(qgi);
}
auto conductor = static_cast<Conductor*>(item);
if (class_name == ConductorPropertiesEditorWidget::staticMetaObject.className())
{
static_cast<ConductorPropertiesEditorWidget*>(editor)->setConductor(conductor);
static_cast<ConductorPropertiesEditorWidget*>(editor)->setConductors(conductors);
return editor;
}
return new ConductorPropertiesEditorWidget(conductor, parent);
return new ConductorPropertiesEditorWidget(conductors, parent);
}
case Element::Type: //1000
{